Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,5 @@ temp/
/npm/pineforge_codegen/
/npm/tables.json
/npm/release.json
/npm/transpile.worker.mjs
/npm/glue.py
10 changes: 10 additions & 0 deletions npm/index.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Entry for @pineforge/codegen-pyodide. Resolves the packaged payload paths and
// metadata so consumers (the app's build + Node oracle/grammar tooling) read
// everything from this one dependency — no git submodule.
import { readFileSync } from "node:fs";
import { createRequire } from "node:module";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
Expand All @@ -18,3 +19,12 @@ export const archivePath = join(HERE, `pineforge_codegen-${release.codegen}.tar.
// so `import pineforge_codegen` resolves (oracle tests, grammar gen).
export const sourceRoot = HERE;
export const codegenSourceDir = join(HERE, "pineforge_codegen");

// Absolute path to the shipped ESM module worker — consumers copy it into their
// served /pyodide/ dir and load it via `new Worker(url, { type: "module" })`.
export const workerPath = join(HERE, "transpile.worker.mjs");

// Canonical Pyodide glue source (single source: gate/glue.py) — runPython'd to
// define transpile_json. The worker embeds this verbatim; exported here too so
// Node consumers (parity tests) run the exact same glue.
export const glue = readFileSync(join(HERE, "glue.py"), "utf8");
4 changes: 3 additions & 1 deletion npm/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@
"pineforge_codegen-*.tar.gz",
"pineforge_codegen/**/*",
"tables.json",
"release.json"
"release.json",
"transpile.worker.mjs",
"glue.py"
],
"engines": {
"node": ">=20"
Expand Down
8 changes: 7 additions & 1 deletion scripts/build-npm-package.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const release = JSON.parse(readFileSync(RELEASE, "utf8"));
if (release.codegen !== VERSION) fail(`release.json codegen ${release.codegen} != VERSION ${VERSION}`);

// 1. Clean payload (keep tracked manifest + index).
for (const p of ["pineforge_codegen", "tables.json", "release.json"]) {
for (const p of ["pineforge_codegen", "tables.json", "release.json", "glue.py", "transpile.worker.mjs"]) {
rmSync(join(NPM, p), { recursive: true, force: true });
}
const versionedArchive = `pineforge_codegen-${VERSION}.tar.gz`;
Expand All @@ -55,6 +55,12 @@ const tablesJson = execFileSync("python3", [join(ROOT, "scripts", "dump-tables.p
});
writeFileSync(join(NPM, "tables.json"), tablesJson);

// --- canonical glue + generated module worker (single source: gate/glue.py) ---
const glueSrc = readFileSync(join(ROOT, "gate", "glue.py"), "utf8");
writeFileSync(join(NPM, "glue.py"), glueSrc);
const workerTemplate = readFileSync(join(ROOT, "scripts", "worker-template.mjs"), "utf8");
writeFileSync(join(NPM, "transpile.worker.mjs"), workerTemplate.replace("__GLUE__", glueSrc));

// 5. Sync npm/package.json version from VERSION.
const pkgPath = join(NPM, "package.json");
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
Expand Down
49 changes: 49 additions & 0 deletions scripts/worker-template.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// GENERATED by scripts/build-npm-package.mjs from gate/glue.py — DO NOT EDIT.
// Canonical client-side Pyodide transpile worker, shipped by
// @pineforge/codegen-pyodide. Consumers serve it from /pyodide/ and load it via:
// new Worker("/pyodide/transpile.worker.mjs", { type: "module" })
// A real ESM module worker (string-URL load bypasses the bundler), so Pyodide
// 314's module-worker requirement is satisfied. Mirrors the old in-app
// transpile.worker.ts; protocol is identical (see app protocol.ts).
import { loadPyodide } from "/pyodide/pyodide.mjs";

const GLUE = `__GLUE__`;

const post = (m) => self.postMessage(m);
let transpileJson = null;

async function init() {
try {
const pyodide = await loadPyodide({ indexURL: "/pyodide/" });
const manifestRes = await fetch("/pyodide/manifest.json", { cache: "no-cache" });
if (!manifestRes.ok) throw new Error(`fetch /pyodide/manifest.json: ${manifestRes.status}`);
const manifest = await manifestRes.json();
const archiveRes = await fetch(`/pyodide/${manifest.archive}`);
if (!archiveRes.ok) throw new Error(`fetch /pyodide/${manifest.archive}: ${archiveRes.status}`);
const buf = await archiveRes.arrayBuffer();
pyodide.unpackArchive(buf, "gztar", { extractDir: "/codegen" });
pyodide.runPython(GLUE);
const fn = pyodide.globals.get("transpile_json");
transpileJson = (source) => fn(source);
post({ type: "ready", codegenVersion: manifest.codegenVersion });
} catch (err) {
post({ type: "init-error", error: err instanceof Error ? err.message : String(err) });
}
}

const initPromise = init();

self.onmessage = async (ev) => {
const { id, source } = ev.data;
await initPromise;
if (!transpileJson) {
post({ type: "crash", id, error: "pyodide failed to initialize" });
return;
}
try {
const result = JSON.parse(transpileJson(source));
post({ type: "result", id, result });
} catch (err) {
post({ type: "crash", id, error: err instanceof Error ? err.message : String(err) });
}
};
Loading