@rexezuge/tooling 1.0.4 → 1.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"spa-shell.d.ts","sourceRoot":"","sources":["../../src/scripts/spa-shell.ts"],"names":[],"mappings":"AAmBA,UAAU,aAAa;IACrB,iBAAiB;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,kDAAkD;IAClD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mDAAmD;IACnD,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,UAAU,YAAY;IACpB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAiBD;;;;;GAKG;AACH,iBAAS,kBAAkB,CAAC,OAAO,EAAE,aAAa,GAAG;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAQtF;AAsCD;;;;;;;;;;;;;GAaG;AACH,iBAAS,cAAc,CAAC,OAAO,EAAE,aAAa,GAAG,YAAY,CAsB5D;AAID,mEAAmE;AACnE,iBAAS,WAAW,CAAC,IAAI,GAAE,SAAS,MAAM,EAA0B,GAAG,MAAM,CAwB5E;AAED,OAAO,EAAE,kBAAkB,EAAE,WAAW,EAAE,cAAc,EAAE,CAAC;AAC3D,YAAY,EAAE,aAAa,EAAE,YAAY,EAAE,CAAC"}
1
+ {"version":3,"file":"spa-shell.d.ts","sourceRoot":"","sources":["../../src/scripts/spa-shell.ts"],"names":[],"mappings":"AAmBA,UAAU,aAAa;IACrB,iBAAiB;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,kDAAkD;IAClD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mDAAmD;IACnD,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,UAAU,YAAY;IACpB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAiBD;;;;;GAKG;AACH,iBAAS,kBAAkB,CAAC,OAAO,EAAE,aAAa,GAAG;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAYtF;AAsCD;;;;;;;;;;;;;GAaG;AACH,iBAAS,cAAc,CAAC,OAAO,EAAE,aAAa,GAAG,YAAY,CAsB5D;AAID,mEAAmE;AACnE,iBAAS,WAAW,CAAC,IAAI,GAAE,SAAS,MAAM,EAA0B,GAAG,MAAM,CAwB5E;AAED,OAAO,EAAE,kBAAkB,EAAE,WAAW,EAAE,cAAc,EAAE,CAAC;AAC3D,YAAY,EAAE,aAAa,EAAE,YAAY,EAAE,CAAC"}
@@ -35,11 +35,16 @@ function resolvePaths(options) {
35
35
  */
36
36
  function ensureSpaShellStub(options) {
37
37
  const { shell } = resolvePaths(options);
38
- if (existsSync(shell)) {
39
- return { created: false, path: shell };
40
- }
41
38
  mkdirSync(dirname(shell), { recursive: true });
42
- writeFileSync(shell, STUB_CONTENT, 'utf8');
39
+ try {
40
+ writeFileSync(shell, STUB_CONTENT, { encoding: 'utf8', flag: 'wx' });
41
+ }
42
+ catch (error) {
43
+ if (error?.code === 'EEXIST') {
44
+ return { created: false, path: shell };
45
+ }
46
+ throw error;
47
+ }
43
48
  return { created: true, path: shell };
44
49
  }
45
50
  /** The `SPA_HTML` string literal, decoded from the generated TS module. */
@@ -1 +1 @@
1
- {"version":3,"file":"spa-shell.js","sourceRoot":"","sources":["../../src/scripts/spa-shell.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The SPA shell pair: `ensure-spa-shell-stub` + `verify-spa-shell`.\n *\n * Provenance: the `scripts/ensure-spa-shell-stub.mjs` +\n * `scripts/verify-spa-shell.mjs` pair (Durable-DAV) and Edge-Sonic's typed\n * `scripts/build/{ensure-spa-shell-stub,spa-shell-checks,verify-spa-shell}.ts`.\n * The generate step itself is a Vite plugin (`spaShellEmbedPlugin` in\n * `vite.ts`) because it must run on every build; this module holds the\n * freshness contract around the generated file.\n *\n * Why the pair exists: `apps/api/src/generated/spa-shell.ts` and\n * `apps/web/dist/` are both gitignored build artifacts. A fresh clone gets the\n * empty postinstall stub and every browser navigation returns a blank page,\n * with nothing in the source tree saying so.\n */\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';\nimport { pathToFileURL } from 'node:url';\nimport { dirname, join, resolve } from 'node:path';\n\ninterface SpaShellPaths {\n /** Repo root. */\n root: string;\n /** Generated TS module path, relative to root. */\n shellPath?: string;\n /** Built SPA index.html path, relative to root. */\n distIndexPath?: string;\n}\n\ninterface VerifyResult {\n problems: string[];\n}\n\nconst DEFAULT_SHELL = 'apps/api/src/generated/spa-shell.ts';\nconst DEFAULT_DIST_INDEX = 'apps/web/dist/index.html';\nconst STUB_CONTENT =\n \"// Auto-generated stub - real content is produced by the build step via the Vite plugin.\\nexport const SPA_HTML: string = '';\\n\";\n\n/** A built shell is a full Vite index.html; any small threshold rejects the stub. */\nconst MIN_HTML_CHARS = 200;\n\nfunction resolvePaths(options: SpaShellPaths): { shell: string; distIndex: string; distAssets: string } {\n const root = resolve(options.root);\n const shell = join(root, options.shellPath ?? DEFAULT_SHELL);\n const distIndex = join(root, options.distIndexPath ?? DEFAULT_DIST_INDEX);\n return { shell, distIndex, distAssets: join(dirname(distIndex), 'assets') };\n}\n\n/**\n * Create the empty stub when (and only when) nothing is there yet, so a fresh\n * clone typechecks and `pnpm run build` regenerates the real thing. Never\n * overwrites an existing file — overwriting would erase a real build with a\n * stub on the next `pnpm install`.\n */\nfunction ensureSpaShellStub(options: SpaShellPaths): { created: boolean; path: string } {\n const { shell } = resolvePaths(options);\n if (existsSync(shell)) {\n return { created: false, path: shell };\n }\n mkdirSync(dirname(shell), { recursive: true });\n writeFileSync(shell, STUB_CONTENT, 'utf8');\n return { created: true, path: shell };\n}\n\n/** The `SPA_HTML` string literal, decoded from the generated TS module. */\nfunction readEmbeddedHtml(shell: string, problems: string[]): string | null {\n if (!existsSync(shell)) {\n problems.push(\n `${shell} is missing — run the build (only apps/web has a build step).`,\n );\n return null;\n }\n const source = readFileSync(shell, 'utf8');\n const marker = 'export const SPA_HTML: string = ';\n const at = source.indexOf(marker);\n if (at === -1) {\n problems.push(`${shell} does not export SPA_HTML — regenerate it with the build.`);\n return null;\n }\n const literal = source.slice(at + marker.length).replace(/;\\s*$/, '');\n if (literal === \"''\" || literal === '\"\"') {\n problems.push(\n `${shell} still holds the empty postinstall stub — run the build. Until then every browser navigation served by the API worker is a blank page.`,\n );\n return null;\n }\n let html: unknown;\n try {\n html = JSON.parse(literal);\n } catch {\n problems.push(`${shell} is not the generated shape — delete it and run the build.`);\n return null;\n }\n if (typeof html !== 'string' || html.length < MIN_HTML_CHARS) {\n problems.push(`${shell} holds no usable HTML — run the build. Until then every browser navigation is a blank page.`);\n return null;\n }\n return html;\n}\n\n/**\n * Fail when the shell the API worker serves is missing or self-inconsistent:\n *\n * - the postinstall stub (or no build at all) still in place;\n * - `/assets/...` bundles referenced but not emitted (a half-cleaned dist,\n * which serves a shell whose JS 404s);\n * - `spa-shell.ts` and `dist/index.html` disagreeing — only one half of the\n * build was refreshed.\n *\n * Known limit, stated rather than papered over: a *stale but self-consistent*\n * pair passes. That class is covered at the source level by each repo's SPA\n * parser integration test; the remaining duty is operational — run the build\n * after touching `apps/web`, before deploy.\n */\nfunction verifySpaShell(options: SpaShellPaths): VerifyResult {\n const { shell, distIndex, distAssets } = resolvePaths(options);\n const problems: string[] = [];\n const html = readEmbeddedHtml(shell, problems);\n if (html === null) {\n return { problems };\n }\n const referenced = [...html.matchAll(/(?:src|href)=\"\\/assets\\/([^\"]+)\"/g)].map((match) => match[1] as string);\n if (referenced.length === 0) {\n problems.push('The embedded shell references no /assets/ bundles — it is not a real build output.');\n }\n for (const asset of new Set(referenced)) {\n if (!existsSync(join(distAssets, asset))) {\n problems.push(`The embedded shell references /assets/${asset}, which is not in ${distAssets} — run the build.`);\n }\n }\n if (!existsSync(distIndex)) {\n problems.push(`${distIndex} is missing — run the build.`);\n } else if (readFileSync(distIndex, 'utf8') !== html) {\n problems.push(`${shell} does not match ${distIndex} — run the build.`);\n }\n return { problems };\n}\n\nconst CLI_SHAPES = 'verify|ensure';\n\n/** CLI entry: `spaShellCli(['verify'|'ensure', 'repo-root?'])`. */\nfunction spaShellCli(argv: readonly string[] = process.argv.slice(2)): number {\n const mode = argv[0] ?? 'verify';\n if (!CLI_SHAPES.split('|').includes(mode)) {\n console.error(`Usage: tsx spa-shell.ts <${CLI_SHAPES}> [repo-root]`);\n return 1;\n }\n const options: SpaShellPaths = { root: argv[1] ?? process.cwd() };\n if (mode === 'ensure') {\n const result = ensureSpaShellStub(options);\n if (result.created) {\n console.log(`ensure-spa-shell-stub: created stub at ${result.path}`);\n } else {\n console.log(`ensure-spa-shell-stub: ${result.path} already present, left untouched`);\n }\n return 0;\n }\n const { problems } = verifySpaShell(options);\n if (problems.length > 0) {\n console.error('SPA shell check failed:');\n for (const problem of problems) console.error(` - ${problem}`);\n return 1;\n }\n console.log('SPA shell check passed.');\n return 0;\n}\n\nexport { ensureSpaShellStub, spaShellCli, verifySpaShell };\nexport type { SpaShellPaths, VerifyResult };\n\n// Run only when this file is the entrypoint.\n//\n// The rules above are pure and the tests import them directly, so a module-level\n// call would shell out (and exit) during a test run. The URL comparison rather than\n// an unconditional call is the same guard Durable-DAV's init-secrets.ts uses, and\n// it is what keeps the rule modules importable — which is the whole reason they are\n// separate modules in the first place.\nif (process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href) {\n process.exit(spaShellCli(process.argv.slice(2)));\n}\n"]}
1
+ {"version":3,"file":"spa-shell.js","sourceRoot":"","sources":["../../src/scripts/spa-shell.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The SPA shell pair: `ensure-spa-shell-stub` + `verify-spa-shell`.\n *\n * Provenance: the `scripts/ensure-spa-shell-stub.mjs` +\n * `scripts/verify-spa-shell.mjs` pair (Durable-DAV) and Edge-Sonic's typed\n * `scripts/build/{ensure-spa-shell-stub,spa-shell-checks,verify-spa-shell}.ts`.\n * The generate step itself is a Vite plugin (`spaShellEmbedPlugin` in\n * `vite.ts`) because it must run on every build; this module holds the\n * freshness contract around the generated file.\n *\n * Why the pair exists: `apps/api/src/generated/spa-shell.ts` and\n * `apps/web/dist/` are both gitignored build artifacts. A fresh clone gets the\n * empty postinstall stub and every browser navigation returns a blank page,\n * with nothing in the source tree saying so.\n */\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';\nimport { pathToFileURL } from 'node:url';\nimport { dirname, join, resolve } from 'node:path';\n\ninterface SpaShellPaths {\n /** Repo root. */\n root: string;\n /** Generated TS module path, relative to root. */\n shellPath?: string;\n /** Built SPA index.html path, relative to root. */\n distIndexPath?: string;\n}\n\ninterface VerifyResult {\n problems: string[];\n}\n\nconst DEFAULT_SHELL = 'apps/api/src/generated/spa-shell.ts';\nconst DEFAULT_DIST_INDEX = 'apps/web/dist/index.html';\nconst STUB_CONTENT =\n \"// Auto-generated stub - real content is produced by the build step via the Vite plugin.\\nexport const SPA_HTML: string = '';\\n\";\n\n/** A built shell is a full Vite index.html; any small threshold rejects the stub. */\nconst MIN_HTML_CHARS = 200;\n\nfunction resolvePaths(options: SpaShellPaths): { shell: string; distIndex: string; distAssets: string } {\n const root = resolve(options.root);\n const shell = join(root, options.shellPath ?? DEFAULT_SHELL);\n const distIndex = join(root, options.distIndexPath ?? DEFAULT_DIST_INDEX);\n return { shell, distIndex, distAssets: join(dirname(distIndex), 'assets') };\n}\n\n/**\n * Create the empty stub when (and only when) nothing is there yet, so a fresh\n * clone typechecks and `pnpm run build` regenerates the real thing. Never\n * overwrites an existing file — overwriting would erase a real build with a\n * stub on the next `pnpm install`.\n */\nfunction ensureSpaShellStub(options: SpaShellPaths): { created: boolean; path: string } {\n const { shell } = resolvePaths(options);\n mkdirSync(dirname(shell), { recursive: true });\n try {\n writeFileSync(shell, STUB_CONTENT, { encoding: 'utf8', flag: 'wx' });\n } catch (error) {\n if ((error as NodeJS.ErrnoException)?.code === 'EEXIST') {\n return { created: false, path: shell };\n }\n throw error;\n }\n return { created: true, path: shell };\n}\n\n/** The `SPA_HTML` string literal, decoded from the generated TS module. */\nfunction readEmbeddedHtml(shell: string, problems: string[]): string | null {\n if (!existsSync(shell)) {\n problems.push(\n `${shell} is missing — run the build (only apps/web has a build step).`,\n );\n return null;\n }\n const source = readFileSync(shell, 'utf8');\n const marker = 'export const SPA_HTML: string = ';\n const at = source.indexOf(marker);\n if (at === -1) {\n problems.push(`${shell} does not export SPA_HTML — regenerate it with the build.`);\n return null;\n }\n const literal = source.slice(at + marker.length).replace(/;\\s*$/, '');\n if (literal === \"''\" || literal === '\"\"') {\n problems.push(\n `${shell} still holds the empty postinstall stub — run the build. Until then every browser navigation served by the API worker is a blank page.`,\n );\n return null;\n }\n let html: unknown;\n try {\n html = JSON.parse(literal);\n } catch {\n problems.push(`${shell} is not the generated shape — delete it and run the build.`);\n return null;\n }\n if (typeof html !== 'string' || html.length < MIN_HTML_CHARS) {\n problems.push(`${shell} holds no usable HTML — run the build. Until then every browser navigation is a blank page.`);\n return null;\n }\n return html;\n}\n\n/**\n * Fail when the shell the API worker serves is missing or self-inconsistent:\n *\n * - the postinstall stub (or no build at all) still in place;\n * - `/assets/...` bundles referenced but not emitted (a half-cleaned dist,\n * which serves a shell whose JS 404s);\n * - `spa-shell.ts` and `dist/index.html` disagreeing — only one half of the\n * build was refreshed.\n *\n * Known limit, stated rather than papered over: a *stale but self-consistent*\n * pair passes. That class is covered at the source level by each repo's SPA\n * parser integration test; the remaining duty is operational — run the build\n * after touching `apps/web`, before deploy.\n */\nfunction verifySpaShell(options: SpaShellPaths): VerifyResult {\n const { shell, distIndex, distAssets } = resolvePaths(options);\n const problems: string[] = [];\n const html = readEmbeddedHtml(shell, problems);\n if (html === null) {\n return { problems };\n }\n const referenced = [...html.matchAll(/(?:src|href)=\"\\/assets\\/([^\"]+)\"/g)].map((match) => match[1] as string);\n if (referenced.length === 0) {\n problems.push('The embedded shell references no /assets/ bundles — it is not a real build output.');\n }\n for (const asset of new Set(referenced)) {\n if (!existsSync(join(distAssets, asset))) {\n problems.push(`The embedded shell references /assets/${asset}, which is not in ${distAssets} — run the build.`);\n }\n }\n if (!existsSync(distIndex)) {\n problems.push(`${distIndex} is missing — run the build.`);\n } else if (readFileSync(distIndex, 'utf8') !== html) {\n problems.push(`${shell} does not match ${distIndex} — run the build.`);\n }\n return { problems };\n}\n\nconst CLI_SHAPES = 'verify|ensure';\n\n/** CLI entry: `spaShellCli(['verify'|'ensure', 'repo-root?'])`. */\nfunction spaShellCli(argv: readonly string[] = process.argv.slice(2)): number {\n const mode = argv[0] ?? 'verify';\n if (!CLI_SHAPES.split('|').includes(mode)) {\n console.error(`Usage: tsx spa-shell.ts <${CLI_SHAPES}> [repo-root]`);\n return 1;\n }\n const options: SpaShellPaths = { root: argv[1] ?? process.cwd() };\n if (mode === 'ensure') {\n const result = ensureSpaShellStub(options);\n if (result.created) {\n console.log(`ensure-spa-shell-stub: created stub at ${result.path}`);\n } else {\n console.log(`ensure-spa-shell-stub: ${result.path} already present, left untouched`);\n }\n return 0;\n }\n const { problems } = verifySpaShell(options);\n if (problems.length > 0) {\n console.error('SPA shell check failed:');\n for (const problem of problems) console.error(` - ${problem}`);\n return 1;\n }\n console.log('SPA shell check passed.');\n return 0;\n}\n\nexport { ensureSpaShellStub, spaShellCli, verifySpaShell };\nexport type { SpaShellPaths, VerifyResult };\n\n// Run only when this file is the entrypoint.\n//\n// The rules above are pure and the tests import them directly, so a module-level\n// call would shell out (and exit) during a test run. The URL comparison rather than\n// an unconditional call is the same guard Durable-DAV's init-secrets.ts uses, and\n// it is what keeps the rule modules importable — which is the whole reason they are\n// separate modules in the first place.\nif (process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href) {\n process.exit(spaShellCli(process.argv.slice(2)));\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"verify-migrations.d.ts","sourceRoot":"","sources":["../../src/scripts/verify-migrations.ts"],"names":[],"mappings":"AAiEA,4CAA4C;AAC5C,UAAU,cAAc;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,oDAAoD;AACpD,UAAU,iBAAiB;IACzB,OAAO,EAAE,CAAC,CAAC;IACX;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,cAAc,EAAE,CAAC;CAC9B;AAED,6CAA6C;AAC7C,UAAU,gBAAgB;IACxB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,8BAA8B;AAC9B,UAAU,eAAe;IACvB,2CAA2C;IAC3C,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,QAAQ,EAAE,gBAAgB,EAAE,CAAC;IAC7B,0CAA0C;IAC1C,IAAI,EAAE,iBAAiB,GAAG,IAAI,CAAC;CAChC;AAED;;;;;;GAMG;AACH,QAAA,MAAM,SAAS,yBAAyB,CAAC;AAQzC,8EAA8E;AAC9E,QAAA,MAAM,YAAY,IAAI,CAAC;AAMvB;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,GAAG,SAAS,CAQvE;AAOD;;;;;;GAMG;AACH,iBAAS,iBAAiB,CAAC,aAAa,EAAE,MAAM,GAAG,eAAe,CA6DjE;AAuDD,yEAAyE;AACzE,iBAAS,gBAAgB,CAAC,aAAa,EAAE,MAAM,EAAE,OAAO,GAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAA;CAAO,GAAG,eAAe,CAmEnG;AAED,4EAA4E;AAC5E,iBAAS,mBAAmB,CAAC,IAAI,GAAE,SAAS,MAAM,EAA0B,GAAG,MAAM,CAepF;AAED,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,SAAS,EAAE,YAAY,EAAE,CAAC;AAC7F,YAAY,EAAE,cAAc,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,eAAe,EAAE,CAAC"}
1
+ {"version":3,"file":"verify-migrations.d.ts","sourceRoot":"","sources":["../../src/scripts/verify-migrations.ts"],"names":[],"mappings":"AAiEA,4CAA4C;AAC5C,UAAU,cAAc;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,oDAAoD;AACpD,UAAU,iBAAiB;IACzB,OAAO,EAAE,CAAC,CAAC;IACX;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,cAAc,EAAE,CAAC;CAC9B;AAED,6CAA6C;AAC7C,UAAU,gBAAgB;IACxB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,8BAA8B;AAC9B,UAAU,eAAe;IACvB,2CAA2C;IAC3C,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,QAAQ,EAAE,gBAAgB,EAAE,CAAC;IAC7B,0CAA0C;IAC1C,IAAI,EAAE,iBAAiB,GAAG,IAAI,CAAC;CAChC;AAED;;;;;;GAMG;AACH,QAAA,MAAM,SAAS,yBAAyB,CAAC;AAQzC,8EAA8E;AAC9E,QAAA,MAAM,YAAY,IAAI,CAAC;AAMvB;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,GAAG,SAAS,CAQvE;AAOD;;;;;;GAMG;AACH,iBAAS,iBAAiB,CAAC,aAAa,EAAE,MAAM,GAAG,eAAe,CA6DjE;AAuDD,yEAAyE;AACzE,iBAAS,gBAAgB,CAAC,aAAa,EAAE,MAAM,EAAE,OAAO,GAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAA;CAAO,GAAG,eAAe,CA0EnG;AAED,4EAA4E;AAC5E,iBAAS,mBAAmB,CAAC,IAAI,GAAE,SAAS,MAAM,EAA0B,GAAG,MAAM,CAepF;AAED,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,SAAS,EAAE,YAAY,EAAE,CAAC;AAC7F,YAAY,EAAE,cAAc,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,eAAe,EAAE,CAAC"}
@@ -218,7 +218,15 @@ function verifyMigrations(migrationsDir, options = {}) {
218
218
  const updated = buildUpdatedLock(migrationsDir, report.files, report.lock, baseline);
219
219
  const next = `${JSON.stringify(updated, null, 2)}\n`;
220
220
  const path = join(migrationsDir, LOCK_NAME);
221
- const before = existsSync(path) ? readFileSync(path, 'utf8') : null;
221
+ let before = null;
222
+ try {
223
+ before = readFileSync(path, 'utf8');
224
+ }
225
+ catch (error) {
226
+ if (error?.code !== 'ENOENT') {
227
+ throw error;
228
+ }
229
+ }
222
230
  if (before !== next) {
223
231
  writeFileSync(path, next, 'utf8');
224
232
  }
@@ -1 +1 @@
1
- {"version":3,"file":"verify-migrations.js","sourceRoot":"","sources":["../../src/scripts/verify-migrations.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Verify the migration set is complete, ordered, and locked; write the lock.\n *\n * Provenance: Edge-Sonic's `scripts/migrations/{verify-migrations,lock-check}.ts`\n * (the rule module plus its entrypoint) and AWS's `verify-migrations.ts`,\n * converged. Every copy checks the same properties of a `migrations/` directory;\n * they differ in lock-file naming, in whether the lock is enforced on CI, and in\n * how much of D1's ordering semantics they encode.\n *\n * ### Why a checksum lock at all\n *\n * D1 records *which* migrations it has applied — the filename and a timestamp —\n * but not *what the file contained* when it applied them. So editing a migration\n * that has already run is not caught by anything: the deploy still succeeds, the\n * local database built from the baseline plus the edited file picks up the new\n * statements, and production keeps the schema it had. The two databases then\n * differ silently and permanently, and the next migration that assumes the newer\n * shape fails only in production. A committed digest per file turns that into a\n * CI failure.\n *\n * ### What the checks are, and where each comes from\n *\n * - **`NNNN_snake_case.sql`** (AWS) — the numbering *is* the ordering: D1 sorts by\n * filename, so a file that sorts *after* the ones already applied is the only\n * kind that can be introduced safely.\n * - **Contiguous prefixes** (AWS) — a hole means one database applied 0001 and\n * 0003 while another applied only 0001, and nothing downstream can tell which.\n * Checked **only when no squash baseline exists**: a squash legitimately\n * absorbs its predecessors, so numbering restarts wherever it starts, and a\n * contiguity rule that fires on a squash is a rule that blocks the squash.\n * - **One entry per file** (all) — a file with no recorded digest is one nothing\n * can say anything about.\n * - **Digest unchanged** (all) — the edited-after-locking failure.\n * - **No orphan entries** (Edge-Sonic) — a locked file that is gone has been\n * applied to a live database and is no longer reproducible from this repo.\n * - **Strictly increasing prefixes** (Edge-Sonic's `out-of-order`) — a file\n * sorting *before* the newest locked one can never reach a database that has\n * applied that newest one, so its statements would only run on a fresh build.\n * - **No duplicate prefixes** (Edge-Sonic) — two files numbered `0033_` are not a\n * style problem: D1 orders them by the rest of the filename, so which one lands\n * on a given database depends on a lexicographic tiebreak nobody intended.\n *\n * ### The squash baseline\n *\n * `0008_squash.sql` holds the combined schema of every migration before it, and\n * those files are deleted when it is created. A later squash repeats that. So a\n * squash legitimately (a) rewrites one file and (b) removes files the lock still\n * lists, and neither may be reported as drift. The baseline is the\n * highest-numbered `NNNN_squash.sql` **on disk**, never the value the lock claims\n * — deriving it means a stale or forged `baseline` cannot widen the exempt set.\n *\n * ### The one write path\n *\n * `--write` is add-only where it counts: it records unlocked files, refreshes the\n * baseline's own entry, and drops entries the baseline absorbed. Every other\n * entry is left exactly as it is, so an incremental `edited` finding **survives a\n * `--write`** — re-baselining one means deleting its entry by hand, which shows\n * up in the diff. There is deliberately no `--force`, because a write that could\n * bless a drift would be the operator blessing their own edit.\n */\nimport { createHash } from 'node:crypto';\nimport { pathToFileURL } from 'node:url';\nimport { existsSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';\nimport { join } from 'node:path';\n\n/** One migration as the lock records it. */\ninterface MigrationEntry {\n name: string;\n sha256: string;\n}\n\n/** The parsed `migrations/migrations.lock.json`. */\ninterface MigrationLockFile {\n version: 1;\n /**\n * The squashed-baseline filename, when the repo has squashed.\n *\n * Everything at or before it is exempt from the `edited` and `orphan` checks,\n * because that is exactly the set a squash rewrites or absorbs.\n */\n baseline?: string;\n migrations: MigrationEntry[];\n}\n\n/** One failure, phrased for the operator. */\ninterface MigrationProblem {\n problem: string;\n}\n\n/** The outcome of one run. */\ninterface MigrationReport {\n /** Migration filenames, in apply order. */\n files: string[];\n problems: MigrationProblem[];\n /** The lock as it reads after the run. */\n lock: MigrationLockFile | null;\n}\n\n/**\n * The lock's filename, beside the `.sql` files.\n *\n * Named `migrations.lock.json` rather than `migrations.json` so it cannot be\n * mistaken for a migration: wrangler applies `*.sql` only, and a lock named like\n * one would be a deploy-time mystery.\n */\nconst LOCK_NAME = 'migrations.lock.json';\n\n/** The filename shape every migration must have. `NNNN_` is fixed at four digits. */\nconst NAME_PATTERN = /^(\\d{4})_[a-z0-9_]+\\.sql$/;\n\n/** The squashed-baseline shape. The highest-numbered match is the baseline. */\nconst SQUASH_PATTERN = /^\\d{4}_squash\\.sql$/;\n\n/** The lock's format version. Bumped only on an incompatible shape change. */\nconst LOCK_VERSION = 1;\n\nfunction sha256Hex(content: string): string {\n return createHash('sha256').update(content, 'utf8').digest('hex');\n}\n\n/**\n * The highest-numbered `NNNN_squash.sql` on disk, or `undefined` when the repo\n * has not squashed — in which case every migration is incremental and therefore\n * every one of them is immutable.\n */\nexport function baselineOf(files: readonly string[]): string | undefined {\n let highest: string | undefined;\n for (const name of files) {\n if (SQUASH_PATTERN.test(name) && (highest === undefined || name > highest)) {\n highest = name;\n }\n }\n return highest;\n}\n\n/** True when `name` is at or before the baseline, i.e. the set a squash absorbs. */\nfunction isSubsumed(name: string, baseline: string | undefined): boolean {\n return baseline !== undefined && name <= baseline;\n}\n\n/**\n * Read the migration set and validate its shape without consulting the lock.\n *\n * The lock is read here too, so one walk answers both \"what is on disk\" and \"what\n * is recorded\"; a lock that is merely *absent* is reported by `verifyMigrations`\n * rather than here, because that is the bootstrap case rather than a defect.\n */\nfunction inspectMigrations(migrationsDir: string): MigrationReport {\n const problems: MigrationProblem[] = [];\n if (!existsSync(migrationsDir)) {\n problems.push({ problem: `${migrationsDir} does not exist` });\n return { files: [], problems, lock: null };\n }\n\n // Top level only, matching wrangler's default `${migrationsDir}/*.sql`, so the\n // lock describes exactly the set wrangler will run. `migrations.lock.json` is\n // not one of them, because it is not a `.sql` file.\n const files = readdirSync(migrationsDir)\n .filter((name) => name.endsWith('.sql'))\n .sort((left, right) => left.localeCompare(right));\n\n for (const name of files) {\n if (!NAME_PATTERN.test(name)) {\n problems.push({ problem: `${name} does not match NNNN_snake_case.sql` });\n }\n }\n\n const baseline = baselineOf(files);\n const prefixes = files.map((name) => NAME_PATTERN.exec(name)?.[1]);\n\n // Strictly increasing: the property D1 actually depends on. A duplicate or a\n // descending prefix means two files compete for the same slot in the apply\n // order, and which one wins is a lexicographic tiebreak nobody intended.\n for (let index = 1; index < prefixes.length; index += 1) {\n const previous = prefixes[index - 1];\n const current = prefixes[index];\n if (previous !== undefined && current !== undefined && current <= previous) {\n problems.push({\n problem: `${files[index] ?? '?'} sorts at or before ${files[index - 1] ?? '?'} — D1 applies in filename order, so one of the two can never run`,\n });\n }\n }\n\n // Contiguity is checked only with no squash on disk: a squash deletes the files\n // it absorbs, so numbering restarts wherever it starts and a hole is the\n // expected shape rather than a defect.\n if (baseline === undefined) {\n for (let index = 0; index < prefixes.length; index += 1) {\n const expected = String(index + 1).padStart(4, '0');\n if (prefixes[index] !== expected) {\n problems.push({\n problem: `migration numbering is not contiguous: expected ${expected}_*.sql, found ${files[index] ?? '?'}`,\n });\n }\n }\n }\n\n const lockPath = join(migrationsDir, LOCK_NAME);\n let lock: MigrationLockFile | null = null;\n if (existsSync(lockPath)) {\n try {\n lock = JSON.parse(readFileSync(lockPath, 'utf8')) as MigrationLockFile;\n } catch {\n problems.push({ problem: `${LOCK_NAME} is not valid JSON` });\n }\n }\n\n return { files, problems, lock };\n}\n\n/**\n * The lock as `--write` would leave it.\n *\n * `null` when the existing lock cannot be trusted as a base, which is exactly\n * when a `--write` must refuse rather than overwrite contents it failed to\n * understand. A lock that is merely *absent* is not malformed: that is the\n * bootstrap case, and it is built from disk like any other.\n */\nfunction buildUpdatedLock(\n migrationsDir: string,\n files: readonly string[],\n existing: MigrationLockFile | null,\n baseline: string | undefined,\n): MigrationLockFile | null {\n const digestOf = (name: string): string => sha256Hex(readFileSync(join(migrationsDir, name), 'utf8'));\n\n if (existing === null) {\n return { version: LOCK_VERSION, ...(baseline !== undefined && { baseline }), migrations: files.map((name) => ({ name, sha256: digestOf(name) })) };\n }\n\n const kept = new Map<string, string>();\n for (const file of files) {\n const recorded = existing.migrations.find((entry) => entry.name === file);\n if (recorded === undefined) continue;\n // The baseline is the one file a squash is allowed to rewrite.\n kept.set(file, file === baseline ? digestOf(file) : recorded.sha256);\n }\n // Unlocked files are added — that is the new-migration case. Every other entry\n // is carried across untouched, so an `edited` finding survives the write.\n for (const file of files) {\n if (!kept.has(file) && !existing.migrations.some((entry) => entry.name === file)) {\n kept.set(file, digestOf(file));\n }\n }\n // Entries the baseline absorbed are dropped: committing a squash looks exactly\n // like this, and neither it nor a rewritten baseline can hide a divergence,\n // because a database that applied those files already has them recorded in D1's\n // own `d1_migrations`.\n for (const entry of existing.migrations) {\n if (!files.includes(entry.name) && !isSubsumed(entry.name, baseline)) {\n kept.set(entry.name, entry.sha256);\n }\n }\n\n return {\n version: LOCK_VERSION,\n ...(baseline !== undefined && { baseline }),\n migrations: [...kept]\n .sort(([left], [right]) => left.localeCompare(right))\n .map(([name, sha256]) => ({ name, sha256 })),\n };\n}\n\n/** Verify (or with `write: true`, record) the lock against the files. */\nfunction verifyMigrations(migrationsDir: string, options: { write?: boolean } = {}): MigrationReport {\n const report = inspectMigrations(migrationsDir);\n const baseline = baselineOf(report.files);\n\n // A malformed lock is a finding in its own right, and a `--write` must not\n // overwrite contents it failed to understand.\n if (report.problems.length > 0) {\n return report;\n }\n\n if (options.write === true) {\n const updated = buildUpdatedLock(migrationsDir, report.files, report.lock, baseline);\n const next = `${JSON.stringify(updated, null, 2)}\\n`;\n const path = join(migrationsDir, LOCK_NAME);\n const before = existsSync(path) ? readFileSync(path, 'utf8') : null;\n if (before !== next) {\n writeFileSync(path, next, 'utf8');\n }\n report.lock = updated;\n return report;\n }\n\n const lock = report.lock;\n if (lock === null) {\n report.problems.push({ problem: `${LOCK_NAME} is missing — run migrations:lock` });\n return report;\n }\n\n const recorded = new Map(lock.migrations.map((entry) => [entry.name, entry.sha256]));\n for (const name of report.files) {\n const expected = recorded.get(name);\n if (expected === undefined) {\n report.problems.push({ problem: `${name} has no lock entry — run migrations:lock` });\n continue;\n }\n // At or before the baseline, a squash rewrites its own file, and that is the\n // whole reason a baseline exists.\n if (isSubsumed(name, baseline)) continue;\n const actual = sha256Hex(readFileSync(join(migrationsDir, name), 'utf8'));\n if (actual !== expected) {\n report.problems.push({\n problem: `${name} changed after locking (expected ${expected.slice(0, 12)}…, got ${actual.slice(0, 12)}…)`,\n });\n }\n }\n\n for (const entry of lock.migrations) {\n if (report.files.includes(entry.name)) continue;\n if (isSubsumed(entry.name, baseline)) continue;\n report.problems.push({ problem: `${entry.name} is in the lock but no longer exists` });\n }\n\n // A file sorting before the newest locked one can never reach a database that\n // has already applied that newest one, so its statements would only run on a\n // fresh build — the same silent split the checksum exists to prevent.\n const newest = [...recorded.keys()].sort((left, right) => left.localeCompare(right)).at(-1);\n if (newest !== undefined) {\n for (const name of report.files) {\n if (name < newest && !recorded.has(name)) {\n report.problems.push({\n problem: `${name} sorts before ${newest}, which is already locked — D1 applies in filename order, so it would never run on a database that has applied ${newest}`,\n });\n }\n }\n }\n\n return report;\n}\n\n/** CLI entry: `verifyMigrationsCli(['[migrations-dir]?', '--write'?'])`. */\nfunction verifyMigrationsCli(argv: readonly string[] = process.argv.slice(2)): number {\n const dir = argv.find((arg) => !arg.startsWith('--')) ?? 'migrations';\n const write = argv.includes('--write');\n const report = verifyMigrations(dir, { write });\n if (report.problems.length > 0) {\n console.error(`Migration validation failed for ${dir}:`);\n for (const problem of report.problems) console.error(` - ${problem.problem}`);\n return 1;\n }\n if (write) {\n console.log(`Migration lock written (${report.files.length} files).`);\n return 0;\n }\n console.log(`Migration validation passed (${report.files.length} files, lock in sync).`);\n return 0;\n}\n\nexport { inspectMigrations, verifyMigrations, verifyMigrationsCli, LOCK_NAME, LOCK_VERSION };\nexport type { MigrationEntry, MigrationLockFile, MigrationProblem, MigrationReport };\n\n// Run only when this file is the entrypoint.\n//\n// The rules above are pure and the tests import them directly, so a module-level\n// call would shell out (and exit) during a test run. The URL comparison rather than\n// an unconditional call is the same guard Durable-DAV's init-secrets.ts uses, and\n// it is what keeps the rule modules importable — which is the whole reason they are\n// separate modules in the first place.\nif (process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href) {\n process.exit(verifyMigrationsCli(process.argv.slice(2)));\n}\n"]}
1
+ {"version":3,"file":"verify-migrations.js","sourceRoot":"","sources":["../../src/scripts/verify-migrations.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Verify the migration set is complete, ordered, and locked; write the lock.\n *\n * Provenance: Edge-Sonic's `scripts/migrations/{verify-migrations,lock-check}.ts`\n * (the rule module plus its entrypoint) and AWS's `verify-migrations.ts`,\n * converged. Every copy checks the same properties of a `migrations/` directory;\n * they differ in lock-file naming, in whether the lock is enforced on CI, and in\n * how much of D1's ordering semantics they encode.\n *\n * ### Why a checksum lock at all\n *\n * D1 records *which* migrations it has applied — the filename and a timestamp —\n * but not *what the file contained* when it applied them. So editing a migration\n * that has already run is not caught by anything: the deploy still succeeds, the\n * local database built from the baseline plus the edited file picks up the new\n * statements, and production keeps the schema it had. The two databases then\n * differ silently and permanently, and the next migration that assumes the newer\n * shape fails only in production. A committed digest per file turns that into a\n * CI failure.\n *\n * ### What the checks are, and where each comes from\n *\n * - **`NNNN_snake_case.sql`** (AWS) — the numbering *is* the ordering: D1 sorts by\n * filename, so a file that sorts *after* the ones already applied is the only\n * kind that can be introduced safely.\n * - **Contiguous prefixes** (AWS) — a hole means one database applied 0001 and\n * 0003 while another applied only 0001, and nothing downstream can tell which.\n * Checked **only when no squash baseline exists**: a squash legitimately\n * absorbs its predecessors, so numbering restarts wherever it starts, and a\n * contiguity rule that fires on a squash is a rule that blocks the squash.\n * - **One entry per file** (all) — a file with no recorded digest is one nothing\n * can say anything about.\n * - **Digest unchanged** (all) — the edited-after-locking failure.\n * - **No orphan entries** (Edge-Sonic) — a locked file that is gone has been\n * applied to a live database and is no longer reproducible from this repo.\n * - **Strictly increasing prefixes** (Edge-Sonic's `out-of-order`) — a file\n * sorting *before* the newest locked one can never reach a database that has\n * applied that newest one, so its statements would only run on a fresh build.\n * - **No duplicate prefixes** (Edge-Sonic) — two files numbered `0033_` are not a\n * style problem: D1 orders them by the rest of the filename, so which one lands\n * on a given database depends on a lexicographic tiebreak nobody intended.\n *\n * ### The squash baseline\n *\n * `0008_squash.sql` holds the combined schema of every migration before it, and\n * those files are deleted when it is created. A later squash repeats that. So a\n * squash legitimately (a) rewrites one file and (b) removes files the lock still\n * lists, and neither may be reported as drift. The baseline is the\n * highest-numbered `NNNN_squash.sql` **on disk**, never the value the lock claims\n * — deriving it means a stale or forged `baseline` cannot widen the exempt set.\n *\n * ### The one write path\n *\n * `--write` is add-only where it counts: it records unlocked files, refreshes the\n * baseline's own entry, and drops entries the baseline absorbed. Every other\n * entry is left exactly as it is, so an incremental `edited` finding **survives a\n * `--write`** — re-baselining one means deleting its entry by hand, which shows\n * up in the diff. There is deliberately no `--force`, because a write that could\n * bless a drift would be the operator blessing their own edit.\n */\nimport { createHash } from 'node:crypto';\nimport { pathToFileURL } from 'node:url';\nimport { existsSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';\nimport { join } from 'node:path';\n\n/** One migration as the lock records it. */\ninterface MigrationEntry {\n name: string;\n sha256: string;\n}\n\n/** The parsed `migrations/migrations.lock.json`. */\ninterface MigrationLockFile {\n version: 1;\n /**\n * The squashed-baseline filename, when the repo has squashed.\n *\n * Everything at or before it is exempt from the `edited` and `orphan` checks,\n * because that is exactly the set a squash rewrites or absorbs.\n */\n baseline?: string;\n migrations: MigrationEntry[];\n}\n\n/** One failure, phrased for the operator. */\ninterface MigrationProblem {\n problem: string;\n}\n\n/** The outcome of one run. */\ninterface MigrationReport {\n /** Migration filenames, in apply order. */\n files: string[];\n problems: MigrationProblem[];\n /** The lock as it reads after the run. */\n lock: MigrationLockFile | null;\n}\n\n/**\n * The lock's filename, beside the `.sql` files.\n *\n * Named `migrations.lock.json` rather than `migrations.json` so it cannot be\n * mistaken for a migration: wrangler applies `*.sql` only, and a lock named like\n * one would be a deploy-time mystery.\n */\nconst LOCK_NAME = 'migrations.lock.json';\n\n/** The filename shape every migration must have. `NNNN_` is fixed at four digits. */\nconst NAME_PATTERN = /^(\\d{4})_[a-z0-9_]+\\.sql$/;\n\n/** The squashed-baseline shape. The highest-numbered match is the baseline. */\nconst SQUASH_PATTERN = /^\\d{4}_squash\\.sql$/;\n\n/** The lock's format version. Bumped only on an incompatible shape change. */\nconst LOCK_VERSION = 1;\n\nfunction sha256Hex(content: string): string {\n return createHash('sha256').update(content, 'utf8').digest('hex');\n}\n\n/**\n * The highest-numbered `NNNN_squash.sql` on disk, or `undefined` when the repo\n * has not squashed — in which case every migration is incremental and therefore\n * every one of them is immutable.\n */\nexport function baselineOf(files: readonly string[]): string | undefined {\n let highest: string | undefined;\n for (const name of files) {\n if (SQUASH_PATTERN.test(name) && (highest === undefined || name > highest)) {\n highest = name;\n }\n }\n return highest;\n}\n\n/** True when `name` is at or before the baseline, i.e. the set a squash absorbs. */\nfunction isSubsumed(name: string, baseline: string | undefined): boolean {\n return baseline !== undefined && name <= baseline;\n}\n\n/**\n * Read the migration set and validate its shape without consulting the lock.\n *\n * The lock is read here too, so one walk answers both \"what is on disk\" and \"what\n * is recorded\"; a lock that is merely *absent* is reported by `verifyMigrations`\n * rather than here, because that is the bootstrap case rather than a defect.\n */\nfunction inspectMigrations(migrationsDir: string): MigrationReport {\n const problems: MigrationProblem[] = [];\n if (!existsSync(migrationsDir)) {\n problems.push({ problem: `${migrationsDir} does not exist` });\n return { files: [], problems, lock: null };\n }\n\n // Top level only, matching wrangler's default `${migrationsDir}/*.sql`, so the\n // lock describes exactly the set wrangler will run. `migrations.lock.json` is\n // not one of them, because it is not a `.sql` file.\n const files = readdirSync(migrationsDir)\n .filter((name) => name.endsWith('.sql'))\n .sort((left, right) => left.localeCompare(right));\n\n for (const name of files) {\n if (!NAME_PATTERN.test(name)) {\n problems.push({ problem: `${name} does not match NNNN_snake_case.sql` });\n }\n }\n\n const baseline = baselineOf(files);\n const prefixes = files.map((name) => NAME_PATTERN.exec(name)?.[1]);\n\n // Strictly increasing: the property D1 actually depends on. A duplicate or a\n // descending prefix means two files compete for the same slot in the apply\n // order, and which one wins is a lexicographic tiebreak nobody intended.\n for (let index = 1; index < prefixes.length; index += 1) {\n const previous = prefixes[index - 1];\n const current = prefixes[index];\n if (previous !== undefined && current !== undefined && current <= previous) {\n problems.push({\n problem: `${files[index] ?? '?'} sorts at or before ${files[index - 1] ?? '?'} — D1 applies in filename order, so one of the two can never run`,\n });\n }\n }\n\n // Contiguity is checked only with no squash on disk: a squash deletes the files\n // it absorbs, so numbering restarts wherever it starts and a hole is the\n // expected shape rather than a defect.\n if (baseline === undefined) {\n for (let index = 0; index < prefixes.length; index += 1) {\n const expected = String(index + 1).padStart(4, '0');\n if (prefixes[index] !== expected) {\n problems.push({\n problem: `migration numbering is not contiguous: expected ${expected}_*.sql, found ${files[index] ?? '?'}`,\n });\n }\n }\n }\n\n const lockPath = join(migrationsDir, LOCK_NAME);\n let lock: MigrationLockFile | null = null;\n if (existsSync(lockPath)) {\n try {\n lock = JSON.parse(readFileSync(lockPath, 'utf8')) as MigrationLockFile;\n } catch {\n problems.push({ problem: `${LOCK_NAME} is not valid JSON` });\n }\n }\n\n return { files, problems, lock };\n}\n\n/**\n * The lock as `--write` would leave it.\n *\n * `null` when the existing lock cannot be trusted as a base, which is exactly\n * when a `--write` must refuse rather than overwrite contents it failed to\n * understand. A lock that is merely *absent* is not malformed: that is the\n * bootstrap case, and it is built from disk like any other.\n */\nfunction buildUpdatedLock(\n migrationsDir: string,\n files: readonly string[],\n existing: MigrationLockFile | null,\n baseline: string | undefined,\n): MigrationLockFile | null {\n const digestOf = (name: string): string => sha256Hex(readFileSync(join(migrationsDir, name), 'utf8'));\n\n if (existing === null) {\n return { version: LOCK_VERSION, ...(baseline !== undefined && { baseline }), migrations: files.map((name) => ({ name, sha256: digestOf(name) })) };\n }\n\n const kept = new Map<string, string>();\n for (const file of files) {\n const recorded = existing.migrations.find((entry) => entry.name === file);\n if (recorded === undefined) continue;\n // The baseline is the one file a squash is allowed to rewrite.\n kept.set(file, file === baseline ? digestOf(file) : recorded.sha256);\n }\n // Unlocked files are added — that is the new-migration case. Every other entry\n // is carried across untouched, so an `edited` finding survives the write.\n for (const file of files) {\n if (!kept.has(file) && !existing.migrations.some((entry) => entry.name === file)) {\n kept.set(file, digestOf(file));\n }\n }\n // Entries the baseline absorbed are dropped: committing a squash looks exactly\n // like this, and neither it nor a rewritten baseline can hide a divergence,\n // because a database that applied those files already has them recorded in D1's\n // own `d1_migrations`.\n for (const entry of existing.migrations) {\n if (!files.includes(entry.name) && !isSubsumed(entry.name, baseline)) {\n kept.set(entry.name, entry.sha256);\n }\n }\n\n return {\n version: LOCK_VERSION,\n ...(baseline !== undefined && { baseline }),\n migrations: [...kept]\n .sort(([left], [right]) => left.localeCompare(right))\n .map(([name, sha256]) => ({ name, sha256 })),\n };\n}\n\n/** Verify (or with `write: true`, record) the lock against the files. */\nfunction verifyMigrations(migrationsDir: string, options: { write?: boolean } = {}): MigrationReport {\n const report = inspectMigrations(migrationsDir);\n const baseline = baselineOf(report.files);\n\n // A malformed lock is a finding in its own right, and a `--write` must not\n // overwrite contents it failed to understand.\n if (report.problems.length > 0) {\n return report;\n }\n\n if (options.write === true) {\n const updated = buildUpdatedLock(migrationsDir, report.files, report.lock, baseline);\n const next = `${JSON.stringify(updated, null, 2)}\\n`;\n const path = join(migrationsDir, LOCK_NAME);\n let before: string | null = null;\n try {\n before = readFileSync(path, 'utf8');\n } catch (error) {\n if ((error as NodeJS.ErrnoException)?.code !== 'ENOENT') {\n throw error;\n }\n }\n if (before !== next) {\n writeFileSync(path, next, 'utf8');\n }\n report.lock = updated;\n return report;\n }\n\n const lock = report.lock;\n if (lock === null) {\n report.problems.push({ problem: `${LOCK_NAME} is missing — run migrations:lock` });\n return report;\n }\n\n const recorded = new Map(lock.migrations.map((entry) => [entry.name, entry.sha256]));\n for (const name of report.files) {\n const expected = recorded.get(name);\n if (expected === undefined) {\n report.problems.push({ problem: `${name} has no lock entry — run migrations:lock` });\n continue;\n }\n // At or before the baseline, a squash rewrites its own file, and that is the\n // whole reason a baseline exists.\n if (isSubsumed(name, baseline)) continue;\n const actual = sha256Hex(readFileSync(join(migrationsDir, name), 'utf8'));\n if (actual !== expected) {\n report.problems.push({\n problem: `${name} changed after locking (expected ${expected.slice(0, 12)}…, got ${actual.slice(0, 12)}…)`,\n });\n }\n }\n\n for (const entry of lock.migrations) {\n if (report.files.includes(entry.name)) continue;\n if (isSubsumed(entry.name, baseline)) continue;\n report.problems.push({ problem: `${entry.name} is in the lock but no longer exists` });\n }\n\n // A file sorting before the newest locked one can never reach a database that\n // has already applied that newest one, so its statements would only run on a\n // fresh build — the same silent split the checksum exists to prevent.\n const newest = [...recorded.keys()].sort((left, right) => left.localeCompare(right)).at(-1);\n if (newest !== undefined) {\n for (const name of report.files) {\n if (name < newest && !recorded.has(name)) {\n report.problems.push({\n problem: `${name} sorts before ${newest}, which is already locked — D1 applies in filename order, so it would never run on a database that has applied ${newest}`,\n });\n }\n }\n }\n\n return report;\n}\n\n/** CLI entry: `verifyMigrationsCli(['[migrations-dir]?', '--write'?'])`. */\nfunction verifyMigrationsCli(argv: readonly string[] = process.argv.slice(2)): number {\n const dir = argv.find((arg) => !arg.startsWith('--')) ?? 'migrations';\n const write = argv.includes('--write');\n const report = verifyMigrations(dir, { write });\n if (report.problems.length > 0) {\n console.error(`Migration validation failed for ${dir}:`);\n for (const problem of report.problems) console.error(` - ${problem.problem}`);\n return 1;\n }\n if (write) {\n console.log(`Migration lock written (${report.files.length} files).`);\n return 0;\n }\n console.log(`Migration validation passed (${report.files.length} files, lock in sync).`);\n return 0;\n}\n\nexport { inspectMigrations, verifyMigrations, verifyMigrationsCli, LOCK_NAME, LOCK_VERSION };\nexport type { MigrationEntry, MigrationLockFile, MigrationProblem, MigrationReport };\n\n// Run only when this file is the entrypoint.\n//\n// The rules above are pure and the tests import them directly, so a module-level\n// call would shell out (and exit) during a test run. The URL comparison rather than\n// an unconditional call is the same guard Durable-DAV's init-secrets.ts uses, and\n// it is what keeps the rule modules importable — which is the whole reason they are\n// separate modules in the first place.\nif (process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href) {\n process.exit(verifyMigrationsCli(process.argv.slice(2)));\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rexezuge/tooling",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -26,7 +26,7 @@
26
26
  "src"
27
27
  ],
28
28
  "dependencies": {
29
- "@rexezuge/d1": "1.0.4"
29
+ "@rexezuge/d1": "1.0.6"
30
30
  },
31
31
  "peerDependencies": {
32
32
  "eslint": "^10.0.0",
@@ -20,8 +20,22 @@ runs:
20
20
  - shell: bash
21
21
  run: |
22
22
  attempt=0
23
- max=${{ inputs.max_attempts }}
24
- delay=${{ inputs.delay }}
23
+ max="$RETRY_MAX_ATTEMPTS"
24
+ delay="$RETRY_DELAY"
25
+ case "$max" in ''|*[!0-9]*)
26
+ printf '::error title=Retry Step::max_attempts must be a positive integer\n'
27
+ exit 1
28
+ ;;
29
+ esac
30
+ if [ "$max" -lt 1 ]; then
31
+ printf '::error title=Retry Step::max_attempts must be a positive integer\n'
32
+ exit 1
33
+ fi
34
+ case "$delay" in ''|*[!0-9]*)
35
+ printf '::error title=Retry Step::delay must be a non-negative integer\n'
36
+ exit 1
37
+ ;;
38
+ esac
25
39
  while true; do
26
40
  attempt=$(( attempt + 1 ))
27
41
  printf '::notice title=Retry Step::Attempt %d of %d\n' "$attempt" "$max"
@@ -37,3 +51,5 @@ runs:
37
51
  done
38
52
  env:
39
53
  RETRY_COMMAND: ${{ inputs.command }}
54
+ RETRY_MAX_ATTEMPTS: ${{ inputs.max_attempts }}
55
+ RETRY_DELAY: ${{ inputs.delay }}
@@ -5,7 +5,7 @@ runs:
5
5
  using: composite
6
6
  steps:
7
7
  - name: Install pnpm
8
- uses: pnpm/action-setup@v4
8
+ uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4
9
9
 
10
10
  - name: Setup Node.js
11
11
  uses: actions/setup-node@v4
@@ -53,11 +53,15 @@ function resolvePaths(options: SpaShellPaths): { shell: string; distIndex: strin
53
53
  */
54
54
  function ensureSpaShellStub(options: SpaShellPaths): { created: boolean; path: string } {
55
55
  const { shell } = resolvePaths(options);
56
- if (existsSync(shell)) {
57
- return { created: false, path: shell };
58
- }
59
56
  mkdirSync(dirname(shell), { recursive: true });
60
- writeFileSync(shell, STUB_CONTENT, 'utf8');
57
+ try {
58
+ writeFileSync(shell, STUB_CONTENT, { encoding: 'utf8', flag: 'wx' });
59
+ } catch (error) {
60
+ if ((error as NodeJS.ErrnoException)?.code === 'EEXIST') {
61
+ return { created: false, path: shell };
62
+ }
63
+ throw error;
64
+ }
61
65
  return { created: true, path: shell };
62
66
  }
63
67
 
@@ -276,7 +276,14 @@ function verifyMigrations(migrationsDir: string, options: { write?: boolean } =
276
276
  const updated = buildUpdatedLock(migrationsDir, report.files, report.lock, baseline);
277
277
  const next = `${JSON.stringify(updated, null, 2)}\n`;
278
278
  const path = join(migrationsDir, LOCK_NAME);
279
- const before = existsSync(path) ? readFileSync(path, 'utf8') : null;
279
+ let before: string | null = null;
280
+ try {
281
+ before = readFileSync(path, 'utf8');
282
+ } catch (error) {
283
+ if ((error as NodeJS.ErrnoException)?.code !== 'ENOENT') {
284
+ throw error;
285
+ }
286
+ }
280
287
  if (before !== next) {
281
288
  writeFileSync(path, next, 'utf8');
282
289
  }