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
7 changes: 5 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,11 @@ A scenario is data plus (optionally) a deterministic responder — no UI code:

- **Live tab says the agent is offline** — start it: `pnpm --filter agent dev`.
- **`next dev` crashes with `localStorage.getItem is not a function`** —
Node ≥ 25 ships a stub Web Storage global; the dev script already sets
`--localstorage-file`. Don't remove it.
Node ≥ 25 ships a stub Web Storage global; the dev launcher
(`apps/*/scripts/next-dev.mjs`) adds `--localstorage-file` automatically
wherever the running Node supports it. Don't bypass the launcher or inline
the flag in NODE_OPTIONS: Node < 22.4 rejects it with exit 9 before Next
even starts (that regression is unit-guarded).
- **Blank page / `Cannot find module './NNN.js'` in dev** — a `next build`
ran while `next dev` was up and corrupted `.next`. Stop dev, `rm -rf
apps/web/.next`, restart.
Expand Down
4 changes: 2 additions & 2 deletions apps/composer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
"name": "composer",
"version": "0.1.0",
"private": true,
"description": "The catalog composer: connect a project, inspect and enrich its discovered contract, map it through a data profile, preview the emitted A2UI catalog (wireframe or native registries), and validate the governed artifacts. Authoring UI only every transform runs in published packages, locally via the agent.",
"description": "The catalog composer: connect a project, inspect and enrich its discovered contract, map it through a data profile, preview the emitted A2UI catalog (wireframe or native registries), and validate the governed artifacts. Authoring UI only \u2014 every transform runs in published packages, locally via the agent.",
"license": "Apache-2.0",
"scripts": {
"dev": "pnpm --filter @dspack-studio/shadcn-renderers build:css && node scripts/demo-assets.mjs && NODE_OPTIONS=--localstorage-file=/tmp/dspack-composer-dev-localstorage next dev --port 3001",
"dev": "pnpm --filter @dspack-studio/shadcn-renderers build:css && node scripts/demo-assets.mjs && node scripts/next-dev.mjs dev --port 3001",
"build": "pnpm --filter @dspack-studio/shadcn-renderers build:css && node scripts/demo-assets.mjs && next build",
"test": "node scripts/demo-assets.mjs && vitest run --passWithNoTests",
"typecheck": "node scripts/demo-assets.mjs && tsc -p tsconfig.json"
Expand Down
37 changes: 37 additions & 0 deletions apps/composer/scripts/next-dev.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
* Version-safe Next dev launcher.
*
* Node ≥ 25 ships a stub server-side `localStorage` global whose methods are
* undefined unless backed by `--localstorage-file` — dependencies' `typeof
* localStorage` guards then pass and crash Next dev SSR (docs/
* IMPLEMENTATION_LOG.md). But the flag only EXISTS from Node 22.4, and our
* engines floor is 22: putting it inline in NODE_OPTIONS made `pnpm --filter
* composer dev` die with exit 9 ("not allowed in NODE_OPTIONS") on Node
* 22.0–22.3 before Next ever started. So probe what THIS Node permits and
* pass the flag only where it exists — the protection stays wherever the
* hazard exists (they ship together), and the documented quick-start works
* on every supported Node. Twin: apps/web/scripts/next-dev.mjs.
*/
import { spawn } from "node:child_process";
import { createRequire } from "node:module";

const STORAGE_FILE = "/tmp/dspack-composer-dev-localstorage";

/** Pure env builder, unit-tested: add the flag only when this Node allows it. */
export function webstorageEnv(baseEnv, allowedFlags, file = STORAGE_FILE) {
if (!allowedFlags.has("--localstorage-file")) return { ...baseEnv };
const flag = `--localstorage-file=${file}`;
const prior = baseEnv.NODE_OPTIONS;
return { ...baseEnv, NODE_OPTIONS: prior ? `${prior} ${flag}` : flag };
}

const invokedDirectly = process.argv[1] && import.meta.url.endsWith(process.argv[1].split("/").at(-1));
if (invokedDirectly) {
const require = createRequire(import.meta.url);
const nextBin = require.resolve("next/dist/bin/next");
const child = spawn(process.execPath, [nextBin, ...process.argv.slice(2)], {
stdio: "inherit",
env: webstorageEnv(process.env, process.allowedNodeEnvironmentFlags),
});
child.on("exit", (code, signal) => process.exit(code ?? (signal ? 1 : 0)));
}
44 changes: 44 additions & 0 deletions apps/composer/scripts/next-dev.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { describe, expect, it } from "vitest";
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { webstorageEnv } from "./next-dev.mjs";

/**
* The quick-start regression guard: `pnpm --filter composer dev` must work on
* EVERY engines-supported Node. Node < 22.4 rejects --localstorage-file in
* NODE_OPTIONS with exit 9 (reproduced on a real v22.0.0 binary), so the flag
* may never sit inline in a package script — only the version-probing
* launcher may add it, and only where this Node allows it.
*/
describe("next-dev launcher (quick-start regression)", () => {
it("no package script hardcodes --localstorage-file in NODE_OPTIONS", () => {
for (const rel of ["../package.json", "../../web/package.json"]) {
const pkg = JSON.parse(readFileSync(fileURLToPath(new URL(rel, import.meta.url)), "utf8"));
for (const [name, script] of Object.entries(pkg.scripts ?? {})) {
expect(script, `${pkg.name} ${name}`).not.toMatch(/NODE_OPTIONS=.*--localstorage-file/);
}
}
});

it("old Node (flag not allowed): env passes through untouched — no exit-9 bomb", () => {
const env = webstorageEnv({ PATH: "/bin" }, new Set(["--max-old-space-size"]));
expect(env.NODE_OPTIONS).toBeUndefined();
expect(env.PATH).toBe("/bin");
});

it("new Node (flag allowed): the SSR protection is applied", () => {
const env = webstorageEnv({}, new Set(["--localstorage-file"]));
expect(env.NODE_OPTIONS).toBe("--localstorage-file=/tmp/dspack-composer-dev-localstorage");
});
Comment on lines +29 to +32

it("preserves a caller's existing NODE_OPTIONS", () => {
const env = webstorageEnv({ NODE_OPTIONS: "--max-old-space-size=4096" }, new Set(["--localstorage-file"]));
expect(env.NODE_OPTIONS).toBe("--max-old-space-size=4096 --localstorage-file=/tmp/dspack-composer-dev-localstorage");
});

it("the RUNNING Node agrees with its own probe (sanity)", () => {
const env = webstorageEnv({}, process.allowedNodeEnvironmentFlags);
const has = process.allowedNodeEnvironmentFlags.has("--localstorage-file");
expect(!!env.NODE_OPTIONS).toBe(has);
});
});
2 changes: 1 addition & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"description": "The studio: canvas, timeline, X-ray, wire view. Phase 1: the contract's worked example rendered through Astryx with the theme dial.",
"license": "Apache-2.0",
"scripts": {
"dev": "pnpm --filter @dspack-studio/shadcn-renderers build:css && node scripts/take-home-assets.mjs && NODE_OPTIONS=--localstorage-file=/tmp/dspack-studio-dev-localstorage next dev",
"dev": "pnpm --filter @dspack-studio/shadcn-renderers build:css && node scripts/take-home-assets.mjs && node scripts/next-dev.mjs dev",
"build": "pnpm --filter @dspack-studio/contracts build:catalogs && pnpm --filter @dspack-studio/shadcn-renderers build:css && node scripts/take-home-assets.mjs && next build",
"test": "vitest run",
"start": "next start",
Expand Down
37 changes: 37 additions & 0 deletions apps/web/scripts/next-dev.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
* Version-safe Next dev launcher.
*
* Node ≥ 25 ships a stub server-side `localStorage` global whose methods are
* undefined unless backed by `--localstorage-file` — dependencies' `typeof
* localStorage` guards then pass and crash Next dev SSR (docs/
* IMPLEMENTATION_LOG.md). But the flag only EXISTS from Node 22.4, and our
* engines floor is 22: putting it inline in NODE_OPTIONS made `pnpm dev`
* die with exit 9 ("not allowed in NODE_OPTIONS") on Node 22.0–22.3 before
* Next ever started. So probe what THIS Node permits and pass the flag only
* where it exists — the protection stays wherever the hazard exists (they
* ship together), and the quick-start works on every supported Node.
* Twin: apps/composer/scripts/next-dev.mjs.
*/
import { spawn } from "node:child_process";
import { createRequire } from "node:module";

const STORAGE_FILE = "/tmp/dspack-studio-dev-localstorage";

/** Pure env builder: add the flag only when this Node allows it. */
export function webstorageEnv(baseEnv, allowedFlags, file = STORAGE_FILE) {
if (!allowedFlags.has("--localstorage-file")) return { ...baseEnv };
const flag = `--localstorage-file=${file}`;
const prior = baseEnv.NODE_OPTIONS;
return { ...baseEnv, NODE_OPTIONS: prior ? `${prior} ${flag}` : flag };
}

const invokedDirectly = process.argv[1] && import.meta.url.endsWith(process.argv[1].split("/").at(-1));
if (invokedDirectly) {
const require = createRequire(import.meta.url);
const nextBin = require.resolve("next/dist/bin/next");
const child = spawn(process.execPath, [nextBin, ...process.argv.slice(2)], {
stdio: "inherit",
env: webstorageEnv(process.env, process.allowedNodeEnvironmentFlags),
});
child.on("exit", (code, signal) => process.exit(code ?? (signal ? 1 : 0)));
}
Loading