Files
pad/web/scripts/build-materializer.mjs
T
xarmian fa2755e183 feat(materialize): headless op-log materializer bundle, goja runner and worker (TASK-2198 U2) (#1678)
* feat(materialize): headless op-log materializer bundle, goja runner and worker (TASK-2198 U2)

Builds the editor's own JS (Yjs -> y-tiptap -> Tiptap schema -> tiptap-markdown
-> the tab's flush pipeline) into one IIFE, embeds it, and runs it in goja. Not
wired into the server yet (U3 supervisor, U4 write path).

- web/src/lib/collab/materializer/: bundle entry (extension list mirrors
  Editor.svelte's collab branch via the U1 shared modules), op-log replay
  mirroring wsProvider's MESSAGE_SYNC arm, linkedom DOM shim and polyfills
  from the spike, and a vendored patch of prosemirror-markdown
  MarkdownSerializerState.atBlank (quadratic under goja; equivalent).
- web/scripts/build-materializer.mjs (esbuild) -> web/build-materializer/,
  run by `npm run build`; embedded as pad.MaterializerJS.
- internal/materialize: Runner (Go-native atob/btoa/TextEncoder/TextDecoder/
  getRandomValues, schema-version check, ctx interrupt with a clean VM after),
  and RunWorker's length-prefixed JSON protocol behind the hidden
  `pad __materialize-worker` command.
- Parity: vitest drives two real Editor.svelte mounts and compares
  materialize() with the tab's flush on 50 seeded cases; the same cases are
  committed as testdata/corpus.json and replayed in goja (TestCorpusParity).
- CI Go jobs, release tests, Docker and Nix produce the bundle; linkedom and
  esbuild are new devDependencies.

Claude-Session: https://claude.ai/code/session_01H63zaWhkb5pAafx2WGjrVC

* test(materialize): pin the atBlank patch's equivalence under V8 too (TASK-2198 U2, codex r1)

Codex r1 claimed upstream's /(^|\n)$/ is true for a trailing CR in JS. It is
not (checked in V8: false for "\r" and "a\u2028", as the patch returns),
but the only equivalence test ran under goja and the patch header cited a
V8-side test that did not exist. This adds it, with the line-terminator edge
cases, and it fails when the patch checks CR instead of LF.

Claude-Session: https://claude.ai/code/session_01H63zaWhkb5pAafx2WGjrVC

* build(nix): vendorHash for goja (TASK-2198 U2)

Taken from the Nix job's recompute on c7fa1931 (specified sha256-iEqv…,
got sha256-P1bQ…), so main does not depend on the heal job after merge.

Claude-Session: https://claude.ai/code/session_01H63zaWhkb5pAafx2WGjrVC
2026-09-30 02:50:10 -04:00

65 lines
2.3 KiB
JavaScript

// Builds the headless materializer bundle (TASK-2198):
// src/lib/collab/materializer/entry.ts -> build-materializer/materializer.js
//
// ONE self-contained IIFE that assigns globalThis.Materializer. The Go binary
// embeds it (embed.go, `//go:embed web/build-materializer/materializer.js`)
// and runs it in goja (internal/materialize), so a fresh checkout needs this
// file before `go build` works, exactly like web/build. `npm run build` runs
// it after `vite build`.
//
// Usage: node scripts/build-materializer.mjs [--outfile <path>] [--define KEY=VALUE]...
// --outfile write somewhere else (tests build deliberately broken variants
// this way, never over the real output)
// --define extra esbuild defines, same syntax as esbuild's
import { build } from 'esbuild';
import path from 'node:path';
import fs from 'node:fs';
import { fileURLToPath } from 'node:url';
const web = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
let outfile = path.join(web, 'build-materializer', 'materializer.js');
const extraDefines = {};
const argv = process.argv.slice(2);
for (let i = 0; i < argv.length; i++) {
const a = argv[i];
if (a === '--outfile') outfile = path.resolve(argv[++i]);
else if (a === '--define') {
const kv = argv[++i];
const eq = kv.indexOf('=');
extraDefines[kv.slice(0, eq)] = kv.slice(eq + 1);
} else {
console.error(`build-materializer: unknown argument ${a}`);
process.exit(2);
}
}
await build({
entryPoints: [path.join(web, 'src/lib/collab/materializer/entry.ts')],
bundle: true,
format: 'iife',
// Neutral, not browser: nothing here may assume a window. The DOM the
// editor needs comes from domShim.ts (linkedom).
platform: 'neutral',
mainFields: ['module', 'main'],
conditions: ['import', 'default'],
// goja implements ES2022 minus a few corners; esbuild lowers anything newer.
target: 'es2022',
minify: true,
legalComments: 'none',
outfile,
alias: { $lib: path.join(web, 'src/lib') },
loader: { '.json': 'json', '.svg': 'text' },
define: {
'process.env.NODE_ENV': '"production"',
'import.meta.env.DEV': 'false',
'import.meta.env.PROD': 'true',
'import.meta.env.SSR': 'false',
...extraDefines,
},
logLevel: 'warning',
});
const bytes = fs.statSync(outfile).size;
console.log(`build-materializer: wrote ${path.relative(process.cwd(), outfile)} (${bytes} bytes)`);