Files
jason.woltje 9353675d1c webui: a WYSIWYG Markdown editor in the Console docs view, built from reviewable source (row 154, Q168, #1648)
Jason's Q168: a visual editor for the Console's project documents,
held to row 148's rendering rules.

Library. ProseMirror with prosemirror-markdown and markdown-it, chosen
in the phase 1 library report (LIBRARY.md, REPORT.md in the packet).
packages/webui/third_party/editor/ holds 19 upstream packages as source,
byte for byte, each with its license, plus NOTICE and PROVENANCE.json.

Build. packages/webui/editor/ holds the model, view, entry and
build.mjs. esbuild 0.28.2 is an exact devDependency of packages/webui
with its own lock; editor/toolchain.mjs checks the version and the
binary's sha256 before any build. MOSAIC_REQUIRE_EDITOR_BUILD=1 turns a
missing or different esbuild into a failure, and the rebuild test then
checks the committed src/public/editor.js and editor.css match a fresh
build. The built bundle is committed under Mos's conditions
(mos-0755, mos-0765).

Console. The docs view gets Visual and Source modes. A save still
writes Markdown through the existing project write path, with the same
stale-base and read-only handling. A mode switch reuses the draft and
reads nothing from the project. Links and images keep row 148's
target rules, and no raw HTML runs. serve.mjs serves /editor.js and
/editor.css; index.html loads them.

Owner Researcher, brief docs/plans/2026-10-16_q168-wysiwyg-editor.md.
Round 5 is a rebase onto d88613bac (row 155); only docs/TOOLS.md,
webui/README.md, public/bus.js and serve.mjs differ from the round
before. Reviewers on candidate manifest 3a4bc21c...3b05: Dewey
(security) approve 29027, Filbert approve 29026. Mos's decision 123 go
for public.diff 902f0244 (handwritten part ea59079e): mos-0824. Sage gate
on 72c3fa598 with MOSAIC_REQUIRE_EDITOR_BUILD=1: GATE_PASSED 25/25
(node 2397, selftests 422), 22:07:29Z to 22:13:50Z; the rebuild test
ran and passed.

Nothing live.
2026-10-18 17:14:39 -05:00

61 lines
4.1 KiB
JavaScript

// usage: node editor/toolchain.mjs
// Row 154 (#1648, Q168): is the pinned esbuild installed, and is it the pinned binary?
// The editor bundle is only worth re-checking with the esbuild it was built with. Install it in packages/webui with
// `npm ci --ignore-scripts --no-audit --no-fund` (package-lock.json is the record). This file never imports esbuild, so
// it runs, and says why, when esbuild is not installed.
// absent esbuild is not installed here, or it is another version (the root install has one that comes in through
// pi-coding-agent and changes when pi changes). A routine run skips the rebuild and prints the reason.
// With MOSAIC_REQUIRE_EDITOR_BUILD=1 it is a failure instead; the landing gate sets that.
// refused esbuild is the pinned version but not the pinned binary (sha256 differs, no sha recorded for this
// platform, or ESBUILD_BINARY_PATH points somewhere else). Always a failure.
// Exit: 0 usable, or absent without the switch (prints "skip: <reason>"); 3 absent with the switch; 4 refused.
import { createHash } from 'node:crypto';
import { readFileSync } from 'node:fs';
import { createRequire } from 'node:module';
import { dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
export const ESBUILD = '0.28.2';
// sha256 of node_modules/@esbuild/<platform>-<arch>/bin/esbuild in 0.28.2. Add a platform only after checking it
// against the registry tarball; a platform with no entry here is refused, not trusted.
export const BINARY_SHA256 = {
'linux-x64': 'e1698a3d5c6c0798fee4fd3b5cc816651f460c63d390a7a26ea4beb0b1884100',
};
const sha256 = buf => createHash('sha256').update(buf).digest('hex');
export function toolchain(from = import.meta.url) {
const here = createRequire(from);
let pkgPath;
try { pkgPath = here.resolve('esbuild/package.json'); } catch { return { ok: false, kind: 'absent', reason: `esbuild is not installed; the editor rebuild needs exactly ${ESBUILD} (npm ci --ignore-scripts in packages/webui)` }; }
const { version } = JSON.parse(readFileSync(pkgPath, 'utf8'));
if (version !== ESBUILD) return { ok: false, kind: 'absent', reason: `esbuild ${version} is installed at ${dirname(pkgPath)}; the editor rebuild needs exactly ${ESBUILD} (npm ci --ignore-scripts in packages/webui)` };
if (process.env.ESBUILD_BINARY_PATH) return { ok: false, kind: 'refused', reason: 'ESBUILD_BINARY_PATH is set, so esbuild would run a binary that was not checked' };
const platform = `${process.platform}-${process.arch}`, expected = BINARY_SHA256[platform];
if (!expected) return { ok: false, kind: 'refused', reason: `no binary sha256 is recorded for ${platform}` };
let binary;
try { binary = createRequire(pkgPath).resolve(`@esbuild/${platform}/bin/esbuild`); } catch { return { ok: false, kind: 'refused', reason: `esbuild ${version} is installed without its @esbuild/${platform} binary` }; }
const actual = sha256(readFileSync(binary));
if (actual !== expected) return { ok: false, kind: 'refused', reason: `the esbuild binary ${binary} is sha256 ${actual}; the pinned binary for ${platform} is ${expected}` };
return { ok: true, version, binary, sha256: actual };
}
// MOSAIC_REQUIRE_EDITOR_BUILD: "1" requires the rebuild; unset, "" and "0" do not. Anything else throws, so a typo
// such as "true" cannot quietly turn the requirement off.
export function required(env = process.env) {
const v = env.MOSAIC_REQUIRE_EDITOR_BUILD;
if (v === undefined || v === '' || v === '0') return false;
if (v === '1') return true;
throw new Error(`MOSAIC_REQUIRE_EDITOR_BUILD is "${v}"; it is 1 (require the editor rebuild) or unset`);
}
// run: rebuild and compare. skip: say why and go on. fail: stop.
export const verdict = (t, req) => (t.ok ? 'run' : t.kind === 'absent' && !req ? 'skip' : 'fail');
if (process.argv[1] === fileURLToPath(import.meta.url)) {
const t = toolchain(), v = verdict(t, required());
if (v === 'run') console.log(`esbuild ${t.version} ${t.binary} sha256 ${t.sha256}`);
else if (v === 'skip') console.log(`skip: ${t.reason}`);
else { console.error(`${t.kind === 'absent' ? 'missing' : 'refused'}: ${t.reason}`); process.exit(t.kind === 'absent' ? 3 : 4); }
}