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 ontod88613bac(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 on72c3fa598with 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.
61 lines
4.1 KiB
JavaScript
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); }
|
|
}
|