@rexezuge/tooling 1.0.6 → 1.0.7
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.
- package/README.md +13 -13
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/scripts/init-secrets.js.map +1 -1
- package/dist/scripts/lib/cli-args.js.map +1 -1
- package/dist/scripts/migrations-lock.js +1 -2
- package/dist/scripts/migrations-lock.js.map +1 -1
- package/dist/scripts/spa-shell.d.ts.map +1 -1
- package/dist/scripts/spa-shell.js.map +1 -1
- package/dist/scripts/verify-migrations.d.ts.map +1 -1
- package/dist/scripts/verify-migrations.js +6 -4
- package/dist/scripts/verify-migrations.js.map +1 -1
- package/dist/scripts/verify-spa-shell.js.map +1 -1
- package/dist/scripts/wrangler-config/patches.d.ts.map +1 -1
- package/dist/scripts/wrangler-config/patches.js +2 -1
- package/dist/scripts/wrangler-config/patches.js.map +1 -1
- package/dist/scripts/wrangler-config/resources.d.ts.map +1 -1
- package/dist/scripts/wrangler-config/resources.js.map +1 -1
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js.map +1 -1
- package/package.json +2 -2
- package/src/index.ts +8 -1
- package/src/scripts/backup/backup-rules.test.ts +3 -3
- package/src/scripts/init-secrets.ts +1 -3
- package/src/scripts/lib/cli-args.ts +0 -1
- package/src/scripts/migrations-lock.ts +1 -2
- package/src/scripts/spa-shell.ts +1 -3
- package/src/scripts/validate-locales.test.ts +10 -2
- package/src/scripts/verify-migrations.test.ts +15 -3
- package/src/scripts/verify-migrations.ts +6 -4
- package/src/scripts/verify-spa-shell.ts +3 -1
- package/src/scripts/wrangler-config/patches.ts +4 -1
- package/src/scripts/wrangler-config/resources.ts +5 -4
- package/src/test/integration-migrations.test.ts +13 -3
- package/src/vite.ts +2 -1
package/README.md
CHANGED
|
@@ -189,7 +189,7 @@ beforeAll(async () => {
|
|
|
189
189
|
|
|
190
190
|
it('upgrades a pre-0004 database', async () => {
|
|
191
191
|
await migrations.applyMigrations(env.DB, { to: '0003_href_prefix_mode.sql' }); // seed the old shape
|
|
192
|
-
await migrations.applyMigrations(env.DB, { from: '0004_user_identity.sql' });
|
|
192
|
+
await migrations.applyMigrations(env.DB, { from: '0004_user_identity.sql' }); // then the one under test
|
|
193
193
|
});
|
|
194
194
|
```
|
|
195
195
|
|
|
@@ -211,17 +211,17 @@ run in), or run it straight out of the installed package:
|
|
|
211
211
|
pnpm exec tsx node_modules/@rexezuge/tooling/src/scripts/<name>.ts [args]
|
|
212
212
|
```
|
|
213
213
|
|
|
214
|
-
| Script
|
|
215
|
-
|
|
|
216
|
-
| `check-god-files.ts`
|
|
217
|
-
| `ensure-spa-shell-stub.ts`
|
|
218
|
-
| `verify-spa-shell.ts`
|
|
219
|
-
| `validate-locales.ts`
|
|
220
|
-
| `verify-migrations.ts`
|
|
221
|
-
| `migrations-lock.ts`
|
|
222
|
-
| `prepare-wrangler-config.ts` | materializes `wrangler.jsonc` and provisions the resources it names
|
|
223
|
-
| `init-secrets.ts`
|
|
224
|
-
| `backup/encrypt-backup.ts`
|
|
214
|
+
| Script | What it does | Usage |
|
|
215
|
+
| ---------------------------- | -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ |
|
|
216
|
+
| `check-god-files.ts` | 300-line warn / 400-line critical guard, warn-only by default, with an allowlist | `--root <dir>` `--allowlist <file>` `--soft 300` `--hard 400` `--fail-on-hard` |
|
|
217
|
+
| `ensure-spa-shell-stub.ts` | writes the empty `spa-shell.ts` stub when absent; never overwrites | `[repo-root]` |
|
|
218
|
+
| `verify-spa-shell.ts` | rejects a missing, stubbed, or half-refreshed SPA shell | `[repo-root]` |
|
|
219
|
+
| `validate-locales.ts` | two-way key parity, empty values, `{{placeholder}}` parity, base-locale bundle | `[locales-dir]` |
|
|
220
|
+
| `verify-migrations.ts` | migration names, numbering, digests against the lock; read-only | `[migrations-dir]` |
|
|
221
|
+
| `migrations-lock.ts` | `--write` for the above: add-only lock update | `[migrations-dir]` |
|
|
222
|
+
| `prepare-wrangler-config.ts` | materializes `wrangler.jsonc` and provisions the resources it names | env-driven |
|
|
223
|
+
| `init-secrets.ts` | creates the declared Secrets Store entries, generate-if-absent | env-driven |
|
|
224
|
+
| `backup/encrypt-backup.ts` | compresses and AES-GCM-encrypts an exported dump | `BACKUP_ENCRYPTION_KEY` |
|
|
225
225
|
|
|
226
226
|
The rules each script enforces live in a module beside it, so the gates are unit
|
|
227
227
|
tested here: `god-files` rules in `check-god-files.ts`, `spa-shell` rules in
|
|
@@ -247,7 +247,7 @@ set a squash rewrites or absorbs.
|
|
|
247
247
|
**`verify-spa-shell.ts`** is the only thing standing between a fresh clone and a
|
|
248
248
|
deploy that serves a blank page: both `apps/web/dist/` and the generated
|
|
249
249
|
`spa-shell.ts` are gitignored, and `postinstall` writes a deliberately empty stub.
|
|
250
|
-
Known limit, stated in the module: a
|
|
250
|
+
Known limit, stated in the module: a _stale but self-consistent_ pair passes.
|
|
251
251
|
|
|
252
252
|
### The backup helpers
|
|
253
253
|
|
package/dist/index.d.ts
CHANGED
|
@@ -27,5 +27,5 @@ export type { Fetcher, PagesFunction, PagesFunctionContext, PagesProxyEnv, Pages
|
|
|
27
27
|
export { DurableObject, RpcTarget, WorkflowEntrypoint } from './test/mocks/cloudflare-workers';
|
|
28
28
|
export type { DurableObjectState, ExecutionContext } from './test/mocks/cloudflare-workers';
|
|
29
29
|
export { createMigrationHelper, readMigrationFiles, splitSql } from './test/integration-migrations';
|
|
30
|
-
export type { D1DatabaseLike, D1PreparedStatementLike, MigrationFile, MigrationHelper, MigrationHelperOptions, MigrationRange } from './test/integration-migrations';
|
|
30
|
+
export type { D1DatabaseLike, D1PreparedStatementLike, MigrationFile, MigrationHelper, MigrationHelperOptions, MigrationRange, } from './test/integration-migrations';
|
|
31
31
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,oBAAoB,EAAE,MAAM,UAAU,CAAC;AAChD,YAAY,EAAE,qBAAqB,EAAE,MAAM,UAAU,CAAC;AAEtD,OAAO,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAC;AAClD,YAAY,EAAE,kBAAkB,EAAE,uBAAuB,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAEzF,OAAO,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AACrD,YAAY,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAC;AAE3D,OAAO,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,MAAM,QAAQ,CAAC;AAClE,YAAY,EAAE,UAAU,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,QAAQ,CAAC;AAE/E,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAC3D,YAAY,EAAE,OAAO,EAAE,aAAa,EAAE,oBAAoB,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAO9H,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,kBAAkB,EAAE,MAAM,iCAAiC,CAAC;AAC/F,YAAY,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAC;AAE5F,OAAO,EAAE,qBAAqB,EAAE,kBAAkB,EAAE,QAAQ,EAAE,MAAM,+BAA+B,CAAC;AACpG,YAAY,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,oBAAoB,EAAE,MAAM,UAAU,CAAC;AAChD,YAAY,EAAE,qBAAqB,EAAE,MAAM,UAAU,CAAC;AAEtD,OAAO,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAC;AAClD,YAAY,EAAE,kBAAkB,EAAE,uBAAuB,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAEzF,OAAO,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AACrD,YAAY,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAC;AAE3D,OAAO,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,MAAM,QAAQ,CAAC;AAClE,YAAY,EAAE,UAAU,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,QAAQ,CAAC;AAE/E,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAC3D,YAAY,EAAE,OAAO,EAAE,aAAa,EAAE,oBAAoB,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAO9H,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,kBAAkB,EAAE,MAAM,iCAAiC,CAAC;AAC/F,YAAY,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAC;AAE5F,OAAO,EAAE,qBAAqB,EAAE,kBAAkB,EAAE,QAAQ,EAAE,MAAM,+BAA+B,CAAC;AACpG,YAAY,EACV,cAAc,EACd,uBAAuB,EACvB,aAAa,EACb,eAAe,EACf,sBAAsB,EACtB,cAAc,GACf,MAAM,+BAA+B,CAAC"}
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,oBAAoB,EAAE,MAAM,UAAU,CAAC;AAGhD,OAAO,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAC;AAGlD,OAAO,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AAGrD,OAAO,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,MAAM,QAAQ,CAAC;AAGlE,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAG3D,6EAA6E;AAC7E,EAAE;AACF,kFAAkF;AAClF,mCAAmC;AAEnC,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,kBAAkB,EAAE,MAAM,iCAAiC,CAAC;AAG/F,OAAO,EAAE,qBAAqB,EAAE,kBAAkB,EAAE,QAAQ,EAAE,MAAM,+BAA+B,CAAC","sourcesContent":["/**\n * `@rexezuge/tooling` — the tooling layer of the kit.\n *\n * The config factories and the test helpers are re-exported here, so a consumer has\n * one import for everything its `*.config.mts` files and its mocks need. The\n * scripts under `scripts/` are **not** re-exported: they resolve paths relative to\n * the repository they run in, so they are run from disk with `tsx`, not imported.\n * See `README.md` for the copy-in recipe and the per-script usage.\n *\n * The zero-dependency rule: nothing here imports another `@rexezuge/*` package. The\n * one exception in the tree is `scripts/backup/encrypt-backup.ts`, which uses\n * `@rexezuge/d1`'s AES-GCM so the backup and the workers share one definition of\n * \"what an encryption key looks like\"; that dependency is declared on the package\n * and is only ever resolved in the consumer's repo, where the kit is installed\n * alongside `@rexezuge/d1`.\n */\n\nexport { defineRepoLintConfig } from './eslint';\nexport type { RepoLintConfigOptions } from './eslint';\n\nexport { defineUnitVitestConfig } from './vitest';\nexport type { CoverageThresholds, UnitVitestConfigOptions, VitestAlias } from './vitest';\n\nexport { defineWebVitestConfig } from './vitest-web';\nexport type { WebVitestConfigOptions } from './vitest-web';\n\nexport { defineWebViteConfig, spaShellEmbedPlugin } from './vite';\nexport type { ProxyEntry, SpaShellTarget, WebViteConfigOptions } from './vite';\n\nexport { createPagesProxy } from './functions/pages-proxy';\nexport type { Fetcher, PagesFunction, PagesFunctionContext, PagesProxyEnv, PagesProxyOptions } from './functions/pages-proxy';\n\n// --- Test helpers ---------------------------------------------------------\n//\n// Aliased in a Vitest config, not imported by application code, so the mocks stay\n// where the suites can reach them.\n\nexport { DurableObject, RpcTarget, WorkflowEntrypoint } from './test/mocks/cloudflare-workers';\nexport type { DurableObjectState, ExecutionContext } from './test/mocks/cloudflare-workers';\n\nexport { createMigrationHelper, readMigrationFiles, splitSql } from './test/integration-migrations';\nexport type {
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,oBAAoB,EAAE,MAAM,UAAU,CAAC;AAGhD,OAAO,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAC;AAGlD,OAAO,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AAGrD,OAAO,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,MAAM,QAAQ,CAAC;AAGlE,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAG3D,6EAA6E;AAC7E,EAAE;AACF,kFAAkF;AAClF,mCAAmC;AAEnC,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,kBAAkB,EAAE,MAAM,iCAAiC,CAAC;AAG/F,OAAO,EAAE,qBAAqB,EAAE,kBAAkB,EAAE,QAAQ,EAAE,MAAM,+BAA+B,CAAC","sourcesContent":["/**\n * `@rexezuge/tooling` — the tooling layer of the kit.\n *\n * The config factories and the test helpers are re-exported here, so a consumer has\n * one import for everything its `*.config.mts` files and its mocks need. The\n * scripts under `scripts/` are **not** re-exported: they resolve paths relative to\n * the repository they run in, so they are run from disk with `tsx`, not imported.\n * See `README.md` for the copy-in recipe and the per-script usage.\n *\n * The zero-dependency rule: nothing here imports another `@rexezuge/*` package. The\n * one exception in the tree is `scripts/backup/encrypt-backup.ts`, which uses\n * `@rexezuge/d1`'s AES-GCM so the backup and the workers share one definition of\n * \"what an encryption key looks like\"; that dependency is declared on the package\n * and is only ever resolved in the consumer's repo, where the kit is installed\n * alongside `@rexezuge/d1`.\n */\n\nexport { defineRepoLintConfig } from './eslint';\nexport type { RepoLintConfigOptions } from './eslint';\n\nexport { defineUnitVitestConfig } from './vitest';\nexport type { CoverageThresholds, UnitVitestConfigOptions, VitestAlias } from './vitest';\n\nexport { defineWebVitestConfig } from './vitest-web';\nexport type { WebVitestConfigOptions } from './vitest-web';\n\nexport { defineWebViteConfig, spaShellEmbedPlugin } from './vite';\nexport type { ProxyEntry, SpaShellTarget, WebViteConfigOptions } from './vite';\n\nexport { createPagesProxy } from './functions/pages-proxy';\nexport type { Fetcher, PagesFunction, PagesFunctionContext, PagesProxyEnv, PagesProxyOptions } from './functions/pages-proxy';\n\n// --- Test helpers ---------------------------------------------------------\n//\n// Aliased in a Vitest config, not imported by application code, so the mocks stay\n// where the suites can reach them.\n\nexport { DurableObject, RpcTarget, WorkflowEntrypoint } from './test/mocks/cloudflare-workers';\nexport type { DurableObjectState, ExecutionContext } from './test/mocks/cloudflare-workers';\n\nexport { createMigrationHelper, readMigrationFiles, splitSql } from './test/integration-migrations';\nexport type {\n D1DatabaseLike,\n D1PreparedStatementLike,\n MigrationFile,\n MigrationHelper,\n MigrationHelperOptions,\n MigrationRange,\n} from './test/integration-migrations';\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"init-secrets.js","sourceRoot":"","sources":["../../src/scripts/init-secrets.ts"],"names":[],"mappings":"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","sourcesContent":["#!/usr/bin/env tsx\n/**\n * Create the Secrets Store entries declared in `wrangler.jsonc`, generating a fresh\n * value for each one that does not exist yet.\n *\n * Provenance: Edge-Sonic's `scripts/deploy/init-secrets.ts` and Durable-DAV's\n * `scripts/init-secrets.ts`, converged. Both record the same two defects that hid\n * a broken deploy for a whole release, and both fixes are carried over.\n *\n * ### Why this script exists at all\n *\n * A `secrets_store_secrets` binding fails the deploy unless the named secret\n * already exists inside the named store. Nothing creates it: Cloudflare will not\n * generate a value, and the template cannot carry one. So this script is the only\n * thing standing between a fresh account and a deploy that cannot succeed.\n *\n * ### The two defects that hid a broken deploy\n *\n * **The generator list was stale.** Durable-DAV's copy carried an allowlist of\n * exact names from the repository it was inherited from, all deleted when the repo\n * was renamed, so the very first iteration threw `Unknown secret` and never created\n * anything. So the rule here is the secret's **name shape**, not a list of names:\n * `*-encryption-key` gets a generated AES-GCM key and `*-signing-secret` gets 32\n * random bytes. Adding a secret to the template is then a one-line change needing\n * no matching edit here, which is the property the exact-name list did not have.\n *\n * **The throw was swallowed.** The original ended in `main().catch(console.error)`.\n * `console.error` returns `undefined` and never sets an exit status, so the process\n * exited **0** and CI marked the step green — the failure only surfaced one step\n * later, as an unexplainable `wrangler deploy` failure. Hence: the whole plan is\n * validated **before** any Cloudflare call, and a rejection reaches the runner as\n * a non-zero exit with a workflow annotation.\n *\n * ### Create-if-absent is a correctness property, not laziness\n *\n * An existing secret is left untouched. A replication encryption key seals every\n * remote target's credential; regenerating it on a redeploy would strand them all\n * as undecryptable. Rotation has to be a deliberate act with a re-encrypt path,\n * not a side effect of deploying.\n *\n * Requires a materialized `wrangler.jsonc` — run\n * `scripts/prepare-wrangler-config.ts` first. Fails rather than skipping when the\n * config is missing, because a step reporting \"Secret initialization complete\"\n * having created nothing is worse than a named error.\n */\n\nimport { execFileSync } from 'node:child_process';\nimport { existsSync, readFileSync } from 'node:fs';\nimport { parse } from 'jsonc-parser';\nimport { fail } from './lib/github-actions';\nimport { isEmptySecretsStoreListing, parseWranglerNames } from './lib/wrangler-table';\nimport { CONFIG_PATH, type WranglerConfig } from './wrangler-config/types';\n\n/**\n * The secret names the AES-GCM key generator produces, matched by **shape**.\n *\n * Keyed on the suffix because the alternative is the exact-name map that went\n * stale. See the header: a template naming `*-encryption-key` is then a one-line\n * change with no matching edit here, and a new secret breaks the deploy loudly\n * instead of silently.\n */\nconst ENCRYPTION_KEY_SUFFIX = '-encryption-key';\n\n/** The secret names the random signing secret generator produces. */\nconst SIGNING_SECRET_SUFFIX = '-signing-secret';\n\n/** A 256-bit AES-GCM key, base64-encoded — the shape the worker's crypto layer accepts. */\nasync function generateAESGCMKey(): Promise<string> {\n const key = await crypto.subtle.generateKey({ name: 'AES-GCM', length: 256 }, true, ['encrypt', 'decrypt']);\n const exported = await crypto.subtle.exportKey('raw', key);\n // Chunked rather than spread: `String.fromCharCode(...bytes)` passes 32 arguments,\n // which is fine today and is a stack overflow the day the key is longer.\n let binary = '';\n for (const byte of new Uint8Array(exported)) {\n binary += String.fromCodePoint(byte);\n }\n return btoa(binary);\n}\n\n/** A 256-bit random signing secret for internal self-calls, hex-encoded. */\nfunction generateSigningSecret(): Promise<string> {\n let hex = '';\n for (const byte of crypto.getRandomValues(new Uint8Array(32))) {\n hex += byte.toString(16).padStart(2, '0');\n }\n return Promise.resolve(hex);\n}\n\n/**\n * The generator for a secret name, chosen by the name's shape.\n *\n * A name matching neither is still refused rather than given a guess: the generator\n * is not the only thing that has to be right, and an unusable value that looks\n * provisioned is worse than a failure.\n */\nfunction generatorFor(secretName: string): () => Promise<string> {\n if (secretName.endsWith(ENCRYPTION_KEY_SUFFIX)) return generateAESGCMKey;\n if (secretName.endsWith(SIGNING_SECRET_SUFFIX)) return generateSigningSecret;\n throw new Error(\n `Unknown secret: ${secretName}. Name it '*-encryption-key' for a generated 32-byte AES-GCM key, ` +\n `or '*-signing-secret' for 32 random bytes, and teach this script the shape if neither fits.`,\n );\n}\n\n/** Whether this script can produce a value for `secretName`. */\nexport function canProvisionSecret(secretName: string): boolean {\n try {\n generatorFor(secretName);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Runs a wrangler subcommand, optionally feeding `input` on stdin.\n *\n * Arguments are passed as an argv array rather than interpolated into a shell\n * string, so a value containing shell metacharacters cannot alter the command.\n */\nfunction wrangler(args: readonly string[], input?: string): string {\n try {\n // eslint-disable-next-line sonarjs/no-os-command-from-path\n return execFileSync('pnpm', ['exec', 'wrangler', ...args], {\n encoding: 'utf8',\n stdio: [input === undefined ? 'ignore' : 'pipe', 'pipe', 'pipe'],\n ...(input !== undefined && { input }),\n });\n } catch (error: unknown) {\n const maybeProcessError = error as { stdout?: string | Buffer; stderr?: string | Buffer; message?: string };\n const stdout: string = maybeProcessError.stdout ? maybeProcessError.stdout.toString() : '';\n const stderr: string = maybeProcessError.stderr ? maybeProcessError.stderr.toString() : '';\n throw new Error(`Command failed: pnpm exec wrangler ${args.join(' ')}\\n${stdout}${stderr || maybeProcessError.message || ''}`);\n }\n}\n\nfunction parseWranglerConfig(): WranglerConfig {\n if (!existsSync(CONFIG_PATH)) {\n throw new Error(\n `${CONFIG_PATH} not found. Run scripts/prepare-wrangler-config.ts first — it is what creates the file this script reads.`,\n );\n }\n // `parse` is error-tolerant and returns a partial object, so an empty result means\n // malformed JSON rather than \"no secrets declared\". Treating that as an empty list\n // would report success having created nothing.\n const config = parse(readFileSync(CONFIG_PATH, 'utf8')) as WranglerConfig;\n if (config.secrets_store_secrets === undefined && Object.keys(config).length === 0) {\n throw new Error(`${CONFIG_PATH} is empty or not valid JSONC. Run scripts/prepare-wrangler-config.ts to regenerate it.`);\n }\n return config;\n}\n\n/** One secret to create, as the plan names it. */\ninterface PlannedSecret {\n storeId: string;\n secretName: string;\n}\n\n/**\n * Turn the config's bindings into a validated work list.\n *\n * Every entry is checked *before* the first Cloudflare call, so an\n * unprovisionable secret fails the step in milliseconds instead of after some other\n * secret has already been created.\n */\nexport function planSecrets(config: WranglerConfig): PlannedSecret[] {\n const plan: PlannedSecret[] = [];\n for (const [index, secret] of (config.secrets_store_secrets ?? []).entries()) {\n const label = secret.binding ?? `index ${index}`;\n const storeId = secret.store_id;\n const secretName = secret.secret_name;\n if (storeId === undefined || storeId === '' || secretName === undefined || secretName === '') {\n throw new Error(\n `secrets_store_secrets[${index}] (\"${label}\") needs both store_id and secret_name; ` +\n 'store_id is normally filled in by scripts/prepare-wrangler-config.ts.',\n );\n }\n if (!canProvisionSecret(secretName)) {\n throw new Error(`No generator for secret \"${secretName}\" (binding \"${label}\").`);\n }\n plan.push({ storeId, secretName });\n }\n return plan;\n}\n\n/**\n * List the secret names present in a store.\n *\n * Returns `undefined` when the listing itself failed, which is deliberately\n * distinct from an empty set: a transient CLI failure must not be read as \"the\n * secret is missing\" and cause a duplicate insert over a live value. An empty store\n * *is* a confirmed absence, so it maps to an empty set rather than `undefined`.\n *\n * The name is taken exactly. `output.includes(secretName)` is not a membership test:\n * `my-replication-encryption-key-2` satisfies a search for\n * `my-replication-encryption-key`, so a store holding only the suffixed entry would\n * report the unsuffixed one as already present and it would never be created.\n */\nfunction listSecretNames(storeId: string): Set<string> | undefined {\n let output: string;\n try {\n output = wrangler(['secrets-store', 'secret', 'list', storeId, '--remote']);\n } catch (error: unknown) {\n if (isEmptySecretsStoreListing(error)) {\n console.log(`Secrets Store ${storeId} is empty`);\n return new Set();\n }\n console.warn(`Unable to list secrets in store ${storeId}:`, error instanceof Error ? error.message : 'unknown error');\n return undefined;\n }\n return parseWranglerNames(output);\n}\n\nfunction createSecret(storeId: string, secretName: string, secretValue: string): void {\n console.log(`Creating secret: ${secretName}`);\n // The value goes in on stdin, never as an argv entry: an argv is visible to every\n // process on the runner through `ps`, and the previous\n // `echo \"${value}\" | wrangler … ${name}` also broke on `\"`, `$`, backticks and\n // newlines.\n wrangler(['secrets-store', 'secret', 'create', storeId, '--name', secretName, '--scopes', 'workers', '--remote'], secretValue);\n}\n\nasync function main(): Promise<void> {\n console.log('Initializing Cloudflare secrets...');\n const config = parseWranglerConfig();\n const plan = planSecrets(config);\n\n if (plan.length === 0) {\n throw new Error('wrangler.jsonc declares no secrets_store_secrets entries, so there is nothing to initialize.');\n }\n\n const listedByStore = new Map<string, Set<string> | undefined>();\n\n for (const { storeId, secretName } of plan) {\n if (!listedByStore.has(storeId)) {\n listedByStore.set(storeId, listSecretNames(storeId));\n }\n const existing = listedByStore.get(storeId);\n if (existing === undefined) {\n // Refuse to guess: creating a secret we could not read back risks overwriting\n // a live value with a freshly generated one.\n throw new Error(\n `Could not list secrets in store ${storeId}; refusing to create ${secretName} without confirming it is absent.`,\n );\n }\n if (existing.has(secretName)) {\n console.log(`Secret ${secretName} already exists; leaving it untouched.`);\n continue;\n }\n createSecret(storeId, secretName, await generatorFor(secretName)());\n existing.add(secretName);\n }\n\n console.log('Secret initialization complete');\n}\n\n// Top-level await rather than `.catch(console.error)`, so a rejected `main` surfaces\n// as a stack trace with its cause intact and the process exits non-zero. Either way\n// the exit status has to be right: this runs in the deploy pipeline, and swallowing\n// the rejection reported a failed key bootstrap as a green step.\ntry {\n await main();\n} catch (error: unknown) {\n fail(error instanceof Error ? `Secret initialization failed: ${error.message}` : String(error));\n}\n"]}
|
|
1
|
+
{"version":3,"file":"init-secrets.js","sourceRoot":"","sources":["../../src/scripts/init-secrets.ts"],"names":[],"mappings":"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","sourcesContent":["#!/usr/bin/env tsx\n/**\n * Create the Secrets Store entries declared in `wrangler.jsonc`, generating a fresh\n * value for each one that does not exist yet.\n *\n * Provenance: Edge-Sonic's `scripts/deploy/init-secrets.ts` and Durable-DAV's\n * `scripts/init-secrets.ts`, converged. Both record the same two defects that hid\n * a broken deploy for a whole release, and both fixes are carried over.\n *\n * ### Why this script exists at all\n *\n * A `secrets_store_secrets` binding fails the deploy unless the named secret\n * already exists inside the named store. Nothing creates it: Cloudflare will not\n * generate a value, and the template cannot carry one. So this script is the only\n * thing standing between a fresh account and a deploy that cannot succeed.\n *\n * ### The two defects that hid a broken deploy\n *\n * **The generator list was stale.** Durable-DAV's copy carried an allowlist of\n * exact names from the repository it was inherited from, all deleted when the repo\n * was renamed, so the very first iteration threw `Unknown secret` and never created\n * anything. So the rule here is the secret's **name shape**, not a list of names:\n * `*-encryption-key` gets a generated AES-GCM key and `*-signing-secret` gets 32\n * random bytes. Adding a secret to the template is then a one-line change needing\n * no matching edit here, which is the property the exact-name list did not have.\n *\n * **The throw was swallowed.** The original ended in `main().catch(console.error)`.\n * `console.error` returns `undefined` and never sets an exit status, so the process\n * exited **0** and CI marked the step green — the failure only surfaced one step\n * later, as an unexplainable `wrangler deploy` failure. Hence: the whole plan is\n * validated **before** any Cloudflare call, and a rejection reaches the runner as\n * a non-zero exit with a workflow annotation.\n *\n * ### Create-if-absent is a correctness property, not laziness\n *\n * An existing secret is left untouched. A replication encryption key seals every\n * remote target's credential; regenerating it on a redeploy would strand them all\n * as undecryptable. Rotation has to be a deliberate act with a re-encrypt path,\n * not a side effect of deploying.\n *\n * Requires a materialized `wrangler.jsonc` — run\n * `scripts/prepare-wrangler-config.ts` first. Fails rather than skipping when the\n * config is missing, because a step reporting \"Secret initialization complete\"\n * having created nothing is worse than a named error.\n */\n\nimport { execFileSync } from 'node:child_process';\nimport { existsSync, readFileSync } from 'node:fs';\nimport { parse } from 'jsonc-parser';\nimport { fail } from './lib/github-actions';\nimport { isEmptySecretsStoreListing, parseWranglerNames } from './lib/wrangler-table';\nimport { CONFIG_PATH, type WranglerConfig } from './wrangler-config/types';\n\n/**\n * The secret names the AES-GCM key generator produces, matched by **shape**.\n *\n * Keyed on the suffix because the alternative is the exact-name map that went\n * stale. See the header: a template naming `*-encryption-key` is then a one-line\n * change with no matching edit here, and a new secret breaks the deploy loudly\n * instead of silently.\n */\nconst ENCRYPTION_KEY_SUFFIX = '-encryption-key';\n\n/** The secret names the random signing secret generator produces. */\nconst SIGNING_SECRET_SUFFIX = '-signing-secret';\n\n/** A 256-bit AES-GCM key, base64-encoded — the shape the worker's crypto layer accepts. */\nasync function generateAESGCMKey(): Promise<string> {\n const key = await crypto.subtle.generateKey({ name: 'AES-GCM', length: 256 }, true, ['encrypt', 'decrypt']);\n const exported = await crypto.subtle.exportKey('raw', key);\n // Chunked rather than spread: `String.fromCharCode(...bytes)` passes 32 arguments,\n // which is fine today and is a stack overflow the day the key is longer.\n let binary = '';\n for (const byte of new Uint8Array(exported)) {\n binary += String.fromCodePoint(byte);\n }\n return btoa(binary);\n}\n\n/** A 256-bit random signing secret for internal self-calls, hex-encoded. */\nfunction generateSigningSecret(): Promise<string> {\n let hex = '';\n for (const byte of crypto.getRandomValues(new Uint8Array(32))) {\n hex += byte.toString(16).padStart(2, '0');\n }\n return Promise.resolve(hex);\n}\n\n/**\n * The generator for a secret name, chosen by the name's shape.\n *\n * A name matching neither is still refused rather than given a guess: the generator\n * is not the only thing that has to be right, and an unusable value that looks\n * provisioned is worse than a failure.\n */\nfunction generatorFor(secretName: string): () => Promise<string> {\n if (secretName.endsWith(ENCRYPTION_KEY_SUFFIX)) return generateAESGCMKey;\n if (secretName.endsWith(SIGNING_SECRET_SUFFIX)) return generateSigningSecret;\n throw new Error(\n `Unknown secret: ${secretName}. Name it '*-encryption-key' for a generated 32-byte AES-GCM key, ` +\n `or '*-signing-secret' for 32 random bytes, and teach this script the shape if neither fits.`,\n );\n}\n\n/** Whether this script can produce a value for `secretName`. */\nexport function canProvisionSecret(secretName: string): boolean {\n try {\n generatorFor(secretName);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Runs a wrangler subcommand, optionally feeding `input` on stdin.\n *\n * Arguments are passed as an argv array rather than interpolated into a shell\n * string, so a value containing shell metacharacters cannot alter the command.\n */\nfunction wrangler(args: readonly string[], input?: string): string {\n try {\n // eslint-disable-next-line sonarjs/no-os-command-from-path\n return execFileSync('pnpm', ['exec', 'wrangler', ...args], {\n encoding: 'utf8',\n stdio: [input === undefined ? 'ignore' : 'pipe', 'pipe', 'pipe'],\n ...(input !== undefined && { input }),\n });\n } catch (error: unknown) {\n const maybeProcessError = error as { stdout?: string | Buffer; stderr?: string | Buffer; message?: string };\n const stdout: string = maybeProcessError.stdout ? maybeProcessError.stdout.toString() : '';\n const stderr: string = maybeProcessError.stderr ? maybeProcessError.stderr.toString() : '';\n throw new Error(`Command failed: pnpm exec wrangler ${args.join(' ')}\\n${stdout}${stderr || maybeProcessError.message || ''}`);\n }\n}\n\nfunction parseWranglerConfig(): WranglerConfig {\n if (!existsSync(CONFIG_PATH)) {\n throw new Error(\n `${CONFIG_PATH} not found. Run scripts/prepare-wrangler-config.ts first — it is what creates the file this script reads.`,\n );\n }\n // `parse` is error-tolerant and returns a partial object, so an empty result means\n // malformed JSON rather than \"no secrets declared\". Treating that as an empty list\n // would report success having created nothing.\n const config = parse(readFileSync(CONFIG_PATH, 'utf8')) as WranglerConfig;\n if (config.secrets_store_secrets === undefined && Object.keys(config).length === 0) {\n throw new Error(`${CONFIG_PATH} is empty or not valid JSONC. Run scripts/prepare-wrangler-config.ts to regenerate it.`);\n }\n return config;\n}\n\n/** One secret to create, as the plan names it. */\ninterface PlannedSecret {\n storeId: string;\n secretName: string;\n}\n\n/**\n * Turn the config's bindings into a validated work list.\n *\n * Every entry is checked *before* the first Cloudflare call, so an\n * unprovisionable secret fails the step in milliseconds instead of after some other\n * secret has already been created.\n */\nexport function planSecrets(config: WranglerConfig): PlannedSecret[] {\n const plan: PlannedSecret[] = [];\n for (const [index, secret] of (config.secrets_store_secrets ?? []).entries()) {\n const label = secret.binding ?? `index ${index}`;\n const storeId = secret.store_id;\n const secretName = secret.secret_name;\n if (storeId === undefined || storeId === '' || secretName === undefined || secretName === '') {\n throw new Error(\n `secrets_store_secrets[${index}] (\"${label}\") needs both store_id and secret_name; ` +\n 'store_id is normally filled in by scripts/prepare-wrangler-config.ts.',\n );\n }\n if (!canProvisionSecret(secretName)) {\n throw new Error(`No generator for secret \"${secretName}\" (binding \"${label}\").`);\n }\n plan.push({ storeId, secretName });\n }\n return plan;\n}\n\n/**\n * List the secret names present in a store.\n *\n * Returns `undefined` when the listing itself failed, which is deliberately\n * distinct from an empty set: a transient CLI failure must not be read as \"the\n * secret is missing\" and cause a duplicate insert over a live value. An empty store\n * *is* a confirmed absence, so it maps to an empty set rather than `undefined`.\n *\n * The name is taken exactly. `output.includes(secretName)` is not a membership test:\n * `my-replication-encryption-key-2` satisfies a search for\n * `my-replication-encryption-key`, so a store holding only the suffixed entry would\n * report the unsuffixed one as already present and it would never be created.\n */\nfunction listSecretNames(storeId: string): Set<string> | undefined {\n let output: string;\n try {\n output = wrangler(['secrets-store', 'secret', 'list', storeId, '--remote']);\n } catch (error: unknown) {\n if (isEmptySecretsStoreListing(error)) {\n console.log(`Secrets Store ${storeId} is empty`);\n return new Set();\n }\n console.warn(`Unable to list secrets in store ${storeId}:`, error instanceof Error ? error.message : 'unknown error');\n return undefined;\n }\n return parseWranglerNames(output);\n}\n\nfunction createSecret(storeId: string, secretName: string, secretValue: string): void {\n console.log(`Creating secret: ${secretName}`);\n // The value goes in on stdin, never as an argv entry: an argv is visible to every\n // process on the runner through `ps`, and the previous\n // `echo \"${value}\" | wrangler … ${name}` also broke on `\"`, `$`, backticks and\n // newlines.\n wrangler(['secrets-store', 'secret', 'create', storeId, '--name', secretName, '--scopes', 'workers', '--remote'], secretValue);\n}\n\nasync function main(): Promise<void> {\n console.log('Initializing Cloudflare secrets...');\n const config = parseWranglerConfig();\n const plan = planSecrets(config);\n\n if (plan.length === 0) {\n throw new Error('wrangler.jsonc declares no secrets_store_secrets entries, so there is nothing to initialize.');\n }\n\n const listedByStore = new Map<string, Set<string> | undefined>();\n\n for (const { storeId, secretName } of plan) {\n if (!listedByStore.has(storeId)) {\n listedByStore.set(storeId, listSecretNames(storeId));\n }\n const existing = listedByStore.get(storeId);\n if (existing === undefined) {\n // Refuse to guess: creating a secret we could not read back risks overwriting\n // a live value with a freshly generated one.\n throw new Error(`Could not list secrets in store ${storeId}; refusing to create ${secretName} without confirming it is absent.`);\n }\n if (existing.has(secretName)) {\n console.log(`Secret ${secretName} already exists; leaving it untouched.`);\n continue;\n }\n createSecret(storeId, secretName, await generatorFor(secretName)());\n existing.add(secretName);\n }\n\n console.log('Secret initialization complete');\n}\n\n// Top-level await rather than `.catch(console.error)`, so a rejected `main` surfaces\n// as a stack trace with its cause intact and the process exits non-zero. Either way\n// the exit status has to be right: this runs in the deploy pipeline, and swallowing\n// the rejection reported a failed key bootstrap as a green step.\ntry {\n await main();\n} catch (error: unknown) {\n fail(error instanceof Error ? `Secret initialization failed: ${error.message}` : String(error));\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli-args.js","sourceRoot":"","sources":["../../../src/scripts/lib/cli-args.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Minimal flag parsing for the operator scripts.\n *\n * Provenance: ported from Edge-Sonic's `scripts/lib/cli-args.ts`, which is the\n * only typed arg parser in the family — every other repo reads `process.argv`\n * with `includes('--flag')` inline.\n *\n * The shape is generic: a declared set of value flags and boolean flags, so an\n * unknown or valueless flag is rejected **by name** rather than silently ignored.\n * A mistyped `--write` must not degrade into \"ran with no flags\" — which is what\n * an inline `argv.includes()` does, and what turns a migration-lock invocation\n * that misspelled the flag into a read-only run that reports success.\n *\n * Usage:\n *\n * ```ts\n * const flags = parseFlags(process.argv.slice(2), {\n * boolean: ['write', 'dry-run'],\n * value: ['locales-dir', 'root'],\n * });\n * if (isSet(flags, 'write')) { ... }\n * const dir = valueOf(flags, 'locales-dir');\n * ```\n */\n\n/**\n * Parsed flags: value flags map to their string, boolean flags to `true`.\n */\nexport type ParsedFlags = Map<string, string | true>;\n\n/**\n * Declares which flags take a value and which are bare switches.\n */\nexport interface FlagSpec {\n /**\n * `--name` takes a value, e.g. `--to new@example.com`.\n */\n value?: readonly string[];\n /**\n * `--dry-run` takes no value.\n */\n boolean?: readonly string[];\n /**\n * Short form → long form, e.g. `{ h: 'help' }`.\n */\n alias?: Readonly<Record<string, string>>;\n}\n\n/**\n * Parses `--flag value` and `--boolean` pairs out of `argv`.\n *\n * @throws when a value flag is missing its value, when a flag is repeated, or\n * when an undeclared flag is seen. Each case names the offending flag.\n */\nexport function parseFlags(argv: readonly string[], spec: FlagSpec): ParsedFlags {\n const valueFlags = new Set(spec.value);\n const booleanFlags = new Set(spec.boolean);\n const aliases = spec.alias ?? {};\n const flags: ParsedFlags = new Map();\n\n for (let index = 0; index < argv.length; index += 1) {\n const arg = argv[index] as string;\n if (!arg.startsWith('-')) {\n throw new Error(`Unexpected argument: ${arg}`);\n }\n const raw = arg.replace(/^--?/, '');\n const name = aliases[raw] ?? raw;\n\n if (booleanFlags.has(name)) {\n if (flags.has(name)) {\n throw new Error(`--${name} was given more than once`);\n }\n flags.set(name, true);\n continue;\n }\n\n if (!valueFlags.has(name)) {\n throw new Error(`Unknown argument: ${arg}`);\n }\n if (flags.has(name)) {\n throw new Error(`--${name} was given more than once`);\n }\n\n const value = argv[index + 1];\n // A value that starts with `-` is treated as a missing value so that\n // `--to --dry-run` reports the real problem instead of capturing the flag.\n if (value === undefined || value.startsWith('-')) {\n throw new Error(`--${name} requires a value`);\n }\n flags.set(name, value);\n index += 1;\n }\n\n return flags;\n}\n\n/**\n * Reads a value flag, or `undefined` when it was not supplied.\n */\nexport function valueOf(flags: ParsedFlags, name: string): string | undefined {\n const value = flags.get(name);\n return typeof value === 'string' ? value : undefined;\n}\n\n/**\n * Reads a boolean flag, defaulting to `false`.\n */\nexport function isSet(flags: ParsedFlags, name: string): boolean {\n return flags.get(name) === true;\n}\n\n/**\n * Split `argv` into its positional arguments and the rest, so a caller can accept\n * `tsx script.ts <dir>` alongside `--root <dir>`.\n *\n * A token that follows a value flag *is* that flag's value, not a positional: the\n * naive `argv.filter((arg) => !arg.startsWith('-'))` takes `.` out of\n * `--root . --fail-on-hard` and then `parseFlags` reports `--root requires a value`.\n * The value flags are passed in rather than inferred, because only the caller knows\n * which of its declared flags take one.\n *\n * @throws when more than `expected` positionals are given, naming them: a second\n * directory is far more likely to be a mistyped flag than an intended argument.\n */\nexport function splitPositional(\n argv: readonly string[],\n valueFlags: readonly string[],\n expected = 1,\n): { positional: string[]; rest: string[] } {\n const takes = new Set(valueFlags);\n const positional: string[] = [];\n const rest: string[] = [];\n\n for (let index = 0; index < argv.length; index += 1) {\n const arg = argv[index] as string;\n if (arg.startsWith('-')) {\n rest.push(arg);\n continue;\n }\n const previous = argv[index - 1] as string | undefined;\n const isFlagValue = previous !== undefined && previous.startsWith('-') && takes.has(previous.replace(/^--?/, ''));\n if (isFlagValue) {\n rest.push(arg);\n continue;\n }\n positional.push(arg);\n }\n\n if (positional.length > expected) {\n throw new Error(`Expected at most ${expected} positional argument(s), got: ${positional.join(', ')}`);\n }\n return { positional, rest };\n}\n
|
|
1
|
+
{"version":3,"file":"cli-args.js","sourceRoot":"","sources":["../../../src/scripts/lib/cli-args.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Minimal flag parsing for the operator scripts.\n *\n * Provenance: ported from Edge-Sonic's `scripts/lib/cli-args.ts`, which is the\n * only typed arg parser in the family — every other repo reads `process.argv`\n * with `includes('--flag')` inline.\n *\n * The shape is generic: a declared set of value flags and boolean flags, so an\n * unknown or valueless flag is rejected **by name** rather than silently ignored.\n * A mistyped `--write` must not degrade into \"ran with no flags\" — which is what\n * an inline `argv.includes()` does, and what turns a migration-lock invocation\n * that misspelled the flag into a read-only run that reports success.\n *\n * Usage:\n *\n * ```ts\n * const flags = parseFlags(process.argv.slice(2), {\n * boolean: ['write', 'dry-run'],\n * value: ['locales-dir', 'root'],\n * });\n * if (isSet(flags, 'write')) { ... }\n * const dir = valueOf(flags, 'locales-dir');\n * ```\n */\n\n/**\n * Parsed flags: value flags map to their string, boolean flags to `true`.\n */\nexport type ParsedFlags = Map<string, string | true>;\n\n/**\n * Declares which flags take a value and which are bare switches.\n */\nexport interface FlagSpec {\n /**\n * `--name` takes a value, e.g. `--to new@example.com`.\n */\n value?: readonly string[];\n /**\n * `--dry-run` takes no value.\n */\n boolean?: readonly string[];\n /**\n * Short form → long form, e.g. `{ h: 'help' }`.\n */\n alias?: Readonly<Record<string, string>>;\n}\n\n/**\n * Parses `--flag value` and `--boolean` pairs out of `argv`.\n *\n * @throws when a value flag is missing its value, when a flag is repeated, or\n * when an undeclared flag is seen. Each case names the offending flag.\n */\nexport function parseFlags(argv: readonly string[], spec: FlagSpec): ParsedFlags {\n const valueFlags = new Set(spec.value);\n const booleanFlags = new Set(spec.boolean);\n const aliases = spec.alias ?? {};\n const flags: ParsedFlags = new Map();\n\n for (let index = 0; index < argv.length; index += 1) {\n const arg = argv[index] as string;\n if (!arg.startsWith('-')) {\n throw new Error(`Unexpected argument: ${arg}`);\n }\n const raw = arg.replace(/^--?/, '');\n const name = aliases[raw] ?? raw;\n\n if (booleanFlags.has(name)) {\n if (flags.has(name)) {\n throw new Error(`--${name} was given more than once`);\n }\n flags.set(name, true);\n continue;\n }\n\n if (!valueFlags.has(name)) {\n throw new Error(`Unknown argument: ${arg}`);\n }\n if (flags.has(name)) {\n throw new Error(`--${name} was given more than once`);\n }\n\n const value = argv[index + 1];\n // A value that starts with `-` is treated as a missing value so that\n // `--to --dry-run` reports the real problem instead of capturing the flag.\n if (value === undefined || value.startsWith('-')) {\n throw new Error(`--${name} requires a value`);\n }\n flags.set(name, value);\n index += 1;\n }\n\n return flags;\n}\n\n/**\n * Reads a value flag, or `undefined` when it was not supplied.\n */\nexport function valueOf(flags: ParsedFlags, name: string): string | undefined {\n const value = flags.get(name);\n return typeof value === 'string' ? value : undefined;\n}\n\n/**\n * Reads a boolean flag, defaulting to `false`.\n */\nexport function isSet(flags: ParsedFlags, name: string): boolean {\n return flags.get(name) === true;\n}\n\n/**\n * Split `argv` into its positional arguments and the rest, so a caller can accept\n * `tsx script.ts <dir>` alongside `--root <dir>`.\n *\n * A token that follows a value flag *is* that flag's value, not a positional: the\n * naive `argv.filter((arg) => !arg.startsWith('-'))` takes `.` out of\n * `--root . --fail-on-hard` and then `parseFlags` reports `--root requires a value`.\n * The value flags are passed in rather than inferred, because only the caller knows\n * which of its declared flags take one.\n *\n * @throws when more than `expected` positionals are given, naming them: a second\n * directory is far more likely to be a mistyped flag than an intended argument.\n */\nexport function splitPositional(\n argv: readonly string[],\n valueFlags: readonly string[],\n expected = 1,\n): { positional: string[]; rest: string[] } {\n const takes = new Set(valueFlags);\n const positional: string[] = [];\n const rest: string[] = [];\n\n for (let index = 0; index < argv.length; index += 1) {\n const arg = argv[index] as string;\n if (arg.startsWith('-')) {\n rest.push(arg);\n continue;\n }\n const previous = argv[index - 1] as string | undefined;\n const isFlagValue = previous !== undefined && previous.startsWith('-') && takes.has(previous.replace(/^--?/, ''));\n if (isFlagValue) {\n rest.push(arg);\n continue;\n }\n positional.push(arg);\n }\n\n if (positional.length > expected) {\n throw new Error(`Expected at most ${expected} positional argument(s), got: ${positional.join(', ')}`);\n }\n return { positional, rest };\n}\n"]}
|
|
@@ -36,8 +36,7 @@ const report = verifyMigrations(migrationsDir, { write: true });
|
|
|
36
36
|
for (const problem of report.problems) {
|
|
37
37
|
console.error(`FAIL: ${problem.problem}`);
|
|
38
38
|
}
|
|
39
|
-
console.log(`${report.lock === null ? 'left' : 'wrote'} ${path.join(migrationsDir, LOCK_NAME)} ` +
|
|
40
|
-
`(${report.files.length} migration(s) recorded)`);
|
|
39
|
+
console.log(`${report.lock === null ? 'left' : 'wrote'} ${path.join(migrationsDir, LOCK_NAME)} ` + `(${report.files.length} migration(s) recorded)`);
|
|
41
40
|
// The lock is re-checked by the caller: `verify-migrations.ts` re-reads from disk,
|
|
42
41
|
// so running it after this command reports the same verdict CI will. A `--write`
|
|
43
42
|
// that could not resolve everything still exits non-zero here, which is why an
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"migrations-lock.js","sourceRoot":"","sources":["../../src/scripts/migrations-lock.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAElE,MAAM,eAAe,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC;AACzF,MAAM,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,eAAe,EAAE,YAAY,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;AAE/H,MAAM,MAAM,GAAG,gBAAgB,CAAC,aAAa,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;AAEhE,KAAK,MAAM,OAAO,IAAI,MAAM,CAAC,QAAQ,EAAE,CAAC;IACtC,OAAO,CAAC,KAAK,CAAC,SAAS,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC;AAC5C,CAAC;AAED,OAAO,CAAC,GAAG,CACT,GAAG,MAAM,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,IAAI,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,SAAS,CAAC,GAAG
|
|
1
|
+
{"version":3,"file":"migrations-lock.js","sourceRoot":"","sources":["../../src/scripts/migrations-lock.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAElE,MAAM,eAAe,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC;AACzF,MAAM,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,eAAe,EAAE,YAAY,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;AAE/H,MAAM,MAAM,GAAG,gBAAgB,CAAC,aAAa,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;AAEhE,KAAK,MAAM,OAAO,IAAI,MAAM,CAAC,QAAQ,EAAE,CAAC;IACtC,OAAO,CAAC,KAAK,CAAC,SAAS,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC;AAC5C,CAAC;AAED,OAAO,CAAC,GAAG,CACT,GAAG,MAAM,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,IAAI,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,SAAS,CAAC,GAAG,GAAG,IAAI,MAAM,CAAC,KAAK,CAAC,MAAM,yBAAyB,CACxI,CAAC;AAEF,mFAAmF;AACnF,iFAAiF;AACjF,+EAA+E;AAC/E,2EAA2E;AAC3E,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC","sourcesContent":["#!/usr/bin/env tsx\n/**\n * Record newly-added migration files in `migrations/migrations.lock.json`.\n *\n * Provenance: Edge-Sonic's `scripts/migrations/verify-migrations.ts --write`,\n * which is the only repo that splits the two operations into two commands. The\n * other repos have one script with a flag; the split is kept here because the two\n * calls have different audiences and different blast radii — `verify-migrations`\n * runs in CI and changes nothing, while this one is an operator action that writes\n * a file into version control.\n *\n * The rules live once, in `verify-migrations.ts`, so the two entrypoints cannot\n * disagree. `--write` is add-only: it records unlocked files, refreshes the\n * baseline's own entry, and drops entries the baseline absorbed. It never touches\n * another existing entry, so an incremental migration that has drifted still\n * fails afterwards — re-baselining one means deleting its entry by hand, which\n * shows up in the diff.\n *\n * There is deliberately no `--force`, because a write that could bless a drift\n * without reading the drift is the operator blessing their own edit, and they are\n * by definition the one making it.\n *\n * Usage, from the repo root:\n *\n * ```bash\n * pnpm exec tsx scripts/migrations-lock.ts # ./migrations\n * pnpm exec tsx scripts/migrations-lock.ts path/to/migrations\n * ```\n */\nimport path from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { LOCK_NAME, verifyMigrations } from './verify-migrations';\n\nconst REPOSITORY_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');\nconst migrationsDir = process.argv[2] === undefined ? path.join(REPOSITORY_ROOT, 'migrations') : path.resolve(process.argv[2]);\n\nconst report = verifyMigrations(migrationsDir, { write: true });\n\nfor (const problem of report.problems) {\n console.error(`FAIL: ${problem.problem}`);\n}\n\nconsole.log(\n `${report.lock === null ? 'left' : 'wrote'} ${path.join(migrationsDir, LOCK_NAME)} ` + `(${report.files.length} migration(s) recorded)`,\n);\n\n// The lock is re-checked by the caller: `verify-migrations.ts` re-reads from disk,\n// so running it after this command reports the same verdict CI will. A `--write`\n// that could not resolve everything still exits non-zero here, which is why an\n// incremental `edited` finding does not vanish just because someone wrote.\nprocess.exit(report.problems.length === 0 ? 0 : 1);\n"]}
|
|
@@ -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,CAYtF;
|
|
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;AAoCD;;;;;;;;;;;;;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 +1 @@
|
|
|
1
|
-
{"version":3,"file":"spa-shell.js","sourceRoot":"","sources":["../../src/scripts/spa-shell.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC7E,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAenD,MAAM,aAAa,GAAG,qCAAqC,CAAC;AAC5D,MAAM,kBAAkB,GAAG,0BAA0B,CAAC;AACtD,MAAM,YAAY,GAChB,iIAAiI,CAAC;AAEpI,qFAAqF;AACrF,MAAM,cAAc,GAAG,GAAG,CAAC;AAE3B,SAAS,YAAY,CAAC,OAAsB;IAC1C,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IACnC,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,EAAE,OAAO,CAAC,SAAS,IAAI,aAAa,CAAC,CAAC;IAC7D,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,EAAE,OAAO,CAAC,aAAa,IAAI,kBAAkB,CAAC,CAAC;IAC1E,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,QAAQ,CAAC,EAAE,CAAC;AAC9E,CAAC;AAED;;;;;GAKG;AACH,SAAS,kBAAkB,CAAC,OAAsB;IAChD,MAAM,EAAE,KAAK,EAAE,GAAG,YAAY,CAAC,OAAO,CAAC,CAAC;IACxC,SAAS,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC/C,IAAI,CAAC;QACH,aAAa,CAAC,KAAK,EAAE,YAAY,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;IACvE,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAK,KAA+B,EAAE,IAAI,KAAK,QAAQ,EAAE,CAAC;YACxD,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;QACzC,CAAC;QACD,MAAM,KAAK,CAAC;IACd,CAAC;IACD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;AACxC,CAAC;AAED,2EAA2E;AAC3E,SAAS,gBAAgB,CAAC,KAAa,EAAE,QAAkB;IACzD,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,EAAE,CAAC;QACvB,QAAQ,CAAC,IAAI,CACX,GAAG,KAAK,+DAA+D,CACxE,CAAC;QACF,OAAO,IAAI,CAAC;IACd,CAAC;IACD,MAAM,MAAM,GAAG,YAAY,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;IAC3C,MAAM,MAAM,GAAG,kCAAkC,CAAC;IAClD,MAAM,EAAE,GAAG,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IAClC,IAAI,EAAE,KAAK,CAAC,CAAC,EAAE,CAAC;QACd,QAAQ,CAAC,IAAI,CAAC,GAAG,KAAK,2DAA2D,CAAC,CAAC;QACnF,OAAO,IAAI,CAAC;IACd,CAAC;IACD,MAAM,OAAO,GAAG,MAAM,CAAC,KAAK,CAAC,EAAE,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;IACtE,IAAI,OAAO,KAAK,IAAI,IAAI,OAAO,KAAK,IAAI,EAAE,CAAC;QACzC,QAAQ,CAAC,IAAI,CACX,GAAG,KAAK,wIAAwI,CACjJ,CAAC;QACF,OAAO,IAAI,CAAC;IACd,CAAC;IACD,IAAI,IAAa,CAAC;IAClB,IAAI,CAAC;QACH,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IAC7B,CAAC;IAAC,MAAM,CAAC;QACP,QAAQ,CAAC,IAAI,CAAC,GAAG,KAAK,4DAA4D,CAAC,CAAC;QACpF,OAAO,IAAI,CAAC;IACd,CAAC;IACD,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,MAAM,GAAG,cAAc,EAAE,CAAC;QAC7D,QAAQ,CAAC,IAAI,CAAC,GAAG,KAAK,6FAA6F,CAAC,CAAC;QACrH,OAAO,IAAI,CAAC;IACd,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,SAAS,cAAc,CAAC,OAAsB;IAC5C,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,GAAG,YAAY,CAAC,OAAO,CAAC,CAAC;IAC/D,MAAM,QAAQ,GAAa,EAAE,CAAC;IAC9B,MAAM,IAAI,GAAG,gBAAgB,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC;IAC/C,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;QAClB,OAAO,EAAE,QAAQ,EAAE,CAAC;IACtB,CAAC;IACD,MAAM,UAAU,GAAG,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,mCAAmC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC,CAAW,CAAC,CAAC;IAC9G,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC5B,QAAQ,CAAC,IAAI,CAAC,oFAAoF,CAAC,CAAC;IACtG,CAAC;IACD,KAAK,MAAM,KAAK,IAAI,IAAI,GAAG,CAAC,UAAU,CAAC,EAAE,CAAC;QACxC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC,EAAE,CAAC;YACzC,QAAQ,CAAC,IAAI,CAAC,yCAAyC,KAAK,qBAAqB,UAAU,mBAAmB,CAAC,CAAC;QAClH,CAAC;IACH,CAAC;IACD,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;QAC3B,QAAQ,CAAC,IAAI,CAAC,GAAG,SAAS,8BAA8B,CAAC,CAAC;IAC5D,CAAC;SAAM,IAAI,YAAY,CAAC,SAAS,EAAE,MAAM,CAAC,KAAK,IAAI,EAAE,CAAC;QACpD,QAAQ,CAAC,IAAI,CAAC,GAAG,KAAK,mBAAmB,SAAS,mBAAmB,CAAC,CAAC;IACzE,CAAC;IACD,OAAO,EAAE,QAAQ,EAAE,CAAC;AACtB,CAAC;AAED,MAAM,UAAU,GAAG,eAAe,CAAC;AAEnC,mEAAmE;AACnE,SAAS,WAAW,CAAC,OAA0B,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;IAClE,MAAM,IAAI,GAAG,IAAI,CAAC,CAAC,CAAC,IAAI,QAAQ,CAAC;IACjC,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;QAC1C,OAAO,CAAC,KAAK,CAAC,4BAA4B,UAAU,eAAe,CAAC,CAAC;QACrE,OAAO,CAAC,CAAC;IACX,CAAC;IACD,MAAM,OAAO,GAAkB,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,IAAI,OAAO,CAAC,GAAG,EAAE,EAAE,CAAC;IAClE,IAAI,IAAI,KAAK,QAAQ,EAAE,CAAC;QACtB,MAAM,MAAM,GAAG,kBAAkB,CAAC,OAAO,CAAC,CAAC;QAC3C,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;YACnB,OAAO,CAAC,GAAG,CAAC,0CAA0C,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC;QACvE,CAAC;aAAM,CAAC;YACN,OAAO,CAAC,GAAG,CAAC,0BAA0B,MAAM,CAAC,IAAI,kCAAkC,CAAC,CAAC;QACvF,CAAC;QACD,OAAO,CAAC,CAAC;IACX,CAAC;IACD,MAAM,EAAE,QAAQ,EAAE,GAAG,cAAc,CAAC,OAAO,CAAC,CAAC;IAC7C,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACxB,OAAO,CAAC,KAAK,CAAC,yBAAyB,CAAC,CAAC;QACzC,KAAK,MAAM,OAAO,IAAI,QAAQ;YAAE,OAAO,CAAC,KAAK,CAAC,OAAO,OAAO,EAAE,CAAC,CAAC;QAChE,OAAO,CAAC,CAAC;IACX,CAAC;IACD,OAAO,CAAC,GAAG,CAAC,yBAAyB,CAAC,CAAC;IACvC,OAAO,CAAC,CAAC;AACX,CAAC;AAED,OAAO,EAAE,kBAAkB,EAAE,WAAW,EAAE,cAAc,EAAE,CAAC;AAG3D,6CAA6C;AAC7C,EAAE;AACF,iFAAiF;AACjF,oFAAoF;AACpF,kFAAkF;AAClF,oFAAoF;AACpF,uCAAuC;AACvC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,SAAS,IAAI,MAAM,CAAC,IAAI,CAAC,GAAG,KAAK,aAAa,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IAC7F,OAAO,CAAC,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACnD,CAAC","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
|
+
{"version":3,"file":"spa-shell.js","sourceRoot":"","sources":["../../src/scripts/spa-shell.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC7E,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAenD,MAAM,aAAa,GAAG,qCAAqC,CAAC;AAC5D,MAAM,kBAAkB,GAAG,0BAA0B,CAAC;AACtD,MAAM,YAAY,GAChB,iIAAiI,CAAC;AAEpI,qFAAqF;AACrF,MAAM,cAAc,GAAG,GAAG,CAAC;AAE3B,SAAS,YAAY,CAAC,OAAsB;IAC1C,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IACnC,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,EAAE,OAAO,CAAC,SAAS,IAAI,aAAa,CAAC,CAAC;IAC7D,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,EAAE,OAAO,CAAC,aAAa,IAAI,kBAAkB,CAAC,CAAC;IAC1E,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,QAAQ,CAAC,EAAE,CAAC;AAC9E,CAAC;AAED;;;;;GAKG;AACH,SAAS,kBAAkB,CAAC,OAAsB;IAChD,MAAM,EAAE,KAAK,EAAE,GAAG,YAAY,CAAC,OAAO,CAAC,CAAC;IACxC,SAAS,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC/C,IAAI,CAAC;QACH,aAAa,CAAC,KAAK,EAAE,YAAY,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;IACvE,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAK,KAA+B,EAAE,IAAI,KAAK,QAAQ,EAAE,CAAC;YACxD,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;QACzC,CAAC;QACD,MAAM,KAAK,CAAC;IACd,CAAC;IACD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;AACxC,CAAC;AAED,2EAA2E;AAC3E,SAAS,gBAAgB,CAAC,KAAa,EAAE,QAAkB;IACzD,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,EAAE,CAAC;QACvB,QAAQ,CAAC,IAAI,CAAC,GAAG,KAAK,+DAA+D,CAAC,CAAC;QACvF,OAAO,IAAI,CAAC;IACd,CAAC;IACD,MAAM,MAAM,GAAG,YAAY,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;IAC3C,MAAM,MAAM,GAAG,kCAAkC,CAAC;IAClD,MAAM,EAAE,GAAG,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IAClC,IAAI,EAAE,KAAK,CAAC,CAAC,EAAE,CAAC;QACd,QAAQ,CAAC,IAAI,CAAC,GAAG,KAAK,2DAA2D,CAAC,CAAC;QACnF,OAAO,IAAI,CAAC;IACd,CAAC;IACD,MAAM,OAAO,GAAG,MAAM,CAAC,KAAK,CAAC,EAAE,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;IACtE,IAAI,OAAO,KAAK,IAAI,IAAI,OAAO,KAAK,IAAI,EAAE,CAAC;QACzC,QAAQ,CAAC,IAAI,CACX,GAAG,KAAK,wIAAwI,CACjJ,CAAC;QACF,OAAO,IAAI,CAAC;IACd,CAAC;IACD,IAAI,IAAa,CAAC;IAClB,IAAI,CAAC;QACH,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IAC7B,CAAC;IAAC,MAAM,CAAC;QACP,QAAQ,CAAC,IAAI,CAAC,GAAG,KAAK,4DAA4D,CAAC,CAAC;QACpF,OAAO,IAAI,CAAC;IACd,CAAC;IACD,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,MAAM,GAAG,cAAc,EAAE,CAAC;QAC7D,QAAQ,CAAC,IAAI,CAAC,GAAG,KAAK,6FAA6F,CAAC,CAAC;QACrH,OAAO,IAAI,CAAC;IACd,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,SAAS,cAAc,CAAC,OAAsB;IAC5C,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,GAAG,YAAY,CAAC,OAAO,CAAC,CAAC;IAC/D,MAAM,QAAQ,GAAa,EAAE,CAAC;IAC9B,MAAM,IAAI,GAAG,gBAAgB,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC;IAC/C,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;QAClB,OAAO,EAAE,QAAQ,EAAE,CAAC;IACtB,CAAC;IACD,MAAM,UAAU,GAAG,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,mCAAmC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC,CAAW,CAAC,CAAC;IAC9G,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC5B,QAAQ,CAAC,IAAI,CAAC,oFAAoF,CAAC,CAAC;IACtG,CAAC;IACD,KAAK,MAAM,KAAK,IAAI,IAAI,GAAG,CAAC,UAAU,CAAC,EAAE,CAAC;QACxC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC,EAAE,CAAC;YACzC,QAAQ,CAAC,IAAI,CAAC,yCAAyC,KAAK,qBAAqB,UAAU,mBAAmB,CAAC,CAAC;QAClH,CAAC;IACH,CAAC;IACD,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;QAC3B,QAAQ,CAAC,IAAI,CAAC,GAAG,SAAS,8BAA8B,CAAC,CAAC;IAC5D,CAAC;SAAM,IAAI,YAAY,CAAC,SAAS,EAAE,MAAM,CAAC,KAAK,IAAI,EAAE,CAAC;QACpD,QAAQ,CAAC,IAAI,CAAC,GAAG,KAAK,mBAAmB,SAAS,mBAAmB,CAAC,CAAC;IACzE,CAAC;IACD,OAAO,EAAE,QAAQ,EAAE,CAAC;AACtB,CAAC;AAED,MAAM,UAAU,GAAG,eAAe,CAAC;AAEnC,mEAAmE;AACnE,SAAS,WAAW,CAAC,OAA0B,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;IAClE,MAAM,IAAI,GAAG,IAAI,CAAC,CAAC,CAAC,IAAI,QAAQ,CAAC;IACjC,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;QAC1C,OAAO,CAAC,KAAK,CAAC,4BAA4B,UAAU,eAAe,CAAC,CAAC;QACrE,OAAO,CAAC,CAAC;IACX,CAAC;IACD,MAAM,OAAO,GAAkB,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,IAAI,OAAO,CAAC,GAAG,EAAE,EAAE,CAAC;IAClE,IAAI,IAAI,KAAK,QAAQ,EAAE,CAAC;QACtB,MAAM,MAAM,GAAG,kBAAkB,CAAC,OAAO,CAAC,CAAC;QAC3C,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;YACnB,OAAO,CAAC,GAAG,CAAC,0CAA0C,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC;QACvE,CAAC;aAAM,CAAC;YACN,OAAO,CAAC,GAAG,CAAC,0BAA0B,MAAM,CAAC,IAAI,kCAAkC,CAAC,CAAC;QACvF,CAAC;QACD,OAAO,CAAC,CAAC;IACX,CAAC;IACD,MAAM,EAAE,QAAQ,EAAE,GAAG,cAAc,CAAC,OAAO,CAAC,CAAC;IAC7C,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACxB,OAAO,CAAC,KAAK,CAAC,yBAAyB,CAAC,CAAC;QACzC,KAAK,MAAM,OAAO,IAAI,QAAQ;YAAE,OAAO,CAAC,KAAK,CAAC,OAAO,OAAO,EAAE,CAAC,CAAC;QAChE,OAAO,CAAC,CAAC;IACX,CAAC;IACD,OAAO,CAAC,GAAG,CAAC,yBAAyB,CAAC,CAAC;IACvC,OAAO,CAAC,CAAC;AACX,CAAC;AAED,OAAO,EAAE,kBAAkB,EAAE,WAAW,EAAE,cAAc,EAAE,CAAC;AAG3D,6CAA6C;AAC7C,EAAE;AACF,iFAAiF;AACjF,oFAAoF;AACpF,kFAAkF;AAClF,oFAAoF;AACpF,uCAAuC;AACvC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,SAAS,IAAI,MAAM,CAAC,IAAI,CAAC,GAAG,KAAK,aAAa,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IAC7F,OAAO,CAAC,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACnD,CAAC","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(`${shell} is missing — run the build (only apps/web has a build step).`);\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;
|
|
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;AAyDD,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"}
|
|
@@ -171,7 +171,11 @@ function inspectMigrations(migrationsDir) {
|
|
|
171
171
|
function buildUpdatedLock(migrationsDir, files, existing, baseline) {
|
|
172
172
|
const digestOf = (name) => sha256Hex(readFileSync(join(migrationsDir, name), 'utf8'));
|
|
173
173
|
if (existing === null) {
|
|
174
|
-
return {
|
|
174
|
+
return {
|
|
175
|
+
version: LOCK_VERSION,
|
|
176
|
+
...(baseline !== undefined && { baseline }),
|
|
177
|
+
migrations: files.map((name) => ({ name, sha256: digestOf(name) })),
|
|
178
|
+
};
|
|
175
179
|
}
|
|
176
180
|
const kept = new Map();
|
|
177
181
|
for (const file of files) {
|
|
@@ -200,9 +204,7 @@ function buildUpdatedLock(migrationsDir, files, existing, baseline) {
|
|
|
200
204
|
return {
|
|
201
205
|
version: LOCK_VERSION,
|
|
202
206
|
...(baseline !== undefined && { baseline }),
|
|
203
|
-
migrations: [...kept]
|
|
204
|
-
.sort(([left], [right]) => left.localeCompare(right))
|
|
205
|
-
.map(([name, sha256]) => ({ name, sha256 })),
|
|
207
|
+
migrations: [...kept].sort(([left], [right]) => left.localeCompare(right)).map(([name, sha256]) => ({ name, sha256 })),
|
|
206
208
|
};
|
|
207
209
|
}
|
|
208
210
|
/** Verify (or with `write: true`, record) the lock against the files. */
|
|
@@ -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 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"]}
|
|
1
|
+
{"version":3,"file":"verify-migrations.js","sourceRoot":"","sources":["../../src/scripts/verify-migrations.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2DG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC/E,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAmCjC;;;;;;GAMG;AACH,MAAM,SAAS,GAAG,sBAAsB,CAAC;AAEzC,qFAAqF;AACrF,MAAM,YAAY,GAAG,2BAA2B,CAAC;AAEjD,+EAA+E;AAC/E,MAAM,cAAc,GAAG,qBAAqB,CAAC;AAE7C,8EAA8E;AAC9E,MAAM,YAAY,GAAG,CAAC,CAAC;AAEvB,SAAS,SAAS,CAAC,OAAe;IAChC,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AACpE,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,UAAU,CAAC,KAAwB;IACjD,IAAI,OAA2B,CAAC;IAChC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,KAAK,SAAS,IAAI,IAAI,GAAG,OAAO,CAAC,EAAE,CAAC;YAC3E,OAAO,GAAG,IAAI,CAAC;QACjB,CAAC;IACH,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,oFAAoF;AACpF,SAAS,UAAU,CAAC,IAAY,EAAE,QAA4B;IAC5D,OAAO,QAAQ,KAAK,SAAS,IAAI,IAAI,IAAI,QAAQ,CAAC;AACpD,CAAC;AAED;;;;;;GAMG;AACH,SAAS,iBAAiB,CAAC,aAAqB;IAC9C,MAAM,QAAQ,GAAuB,EAAE,CAAC;IACxC,IAAI,CAAC,UAAU,CAAC,aAAa,CAAC,EAAE,CAAC;QAC/B,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,GAAG,aAAa,iBAAiB,EAAE,CAAC,CAAC;QAC9D,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;IAC7C,CAAC;IAED,+EAA+E;IAC/E,8EAA8E;IAC9E,oDAAoD;IACpD,MAAM,KAAK,GAAG,WAAW,CAAC,aAAa,CAAC;SACrC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;SACvC,IAAI,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,CAAC;IAEpD,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YAC7B,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,GAAG,IAAI,qCAAqC,EAAE,CAAC,CAAC;QAC3E,CAAC;IACH,CAAC;IAED,MAAM,QAAQ,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC;IACnC,MAAM,QAAQ,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IAEnE,6EAA6E;IAC7E,2EAA2E;IAC3E,yEAAyE;IACzE,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,QAAQ,CAAC,MAAM,EAAE,KAAK,IAAI,CAAC,EAAE,CAAC;QACxD,MAAM,QAAQ,GAAG,QAAQ,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC;QACrC,MAAM,OAAO,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC;QAChC,IAAI,QAAQ,KAAK,SAAS,IAAI,OAAO,KAAK,SAAS,IAAI,OAAO,IAAI,QAAQ,EAAE,CAAC;YAC3E,QAAQ,CAAC,IAAI,CAAC;gBACZ,OAAO,EAAE,GAAG,KAAK,CAAC,KAAK,CAAC,IAAI,GAAG,uBAAuB,KAAK,CAAC,KAAK,GAAG,CAAC,CAAC,IAAI,GAAG,kEAAkE;aAChJ,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED,gFAAgF;IAChF,yEAAyE;IACzE,uCAAuC;IACvC,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;QAC3B,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,QAAQ,CAAC,MAAM,EAAE,KAAK,IAAI,CAAC,EAAE,CAAC;YACxD,MAAM,QAAQ,GAAG,MAAM,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;YACpD,IAAI,QAAQ,CAAC,KAAK,CAAC,KAAK,QAAQ,EAAE,CAAC;gBACjC,QAAQ,CAAC,IAAI,CAAC;oBACZ,OAAO,EAAE,mDAAmD,QAAQ,iBAAiB,KAAK,CAAC,KAAK,CAAC,IAAI,GAAG,EAAE;iBAC3G,CAAC,CAAC;YACL,CAAC;QACH,CAAC;IACH,CAAC;IAED,MAAM,QAAQ,GAAG,IAAI,CAAC,aAAa,EAAE,SAAS,CAAC,CAAC;IAChD,IAAI,IAAI,GAA6B,IAAI,CAAC;IAC1C,IAAI,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QACzB,IAAI,CAAC;YACH,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAsB,CAAC;QACzE,CAAC;QAAC,MAAM,CAAC;YACP,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,GAAG,SAAS,oBAAoB,EAAE,CAAC,CAAC;QAC/D,CAAC;IACH,CAAC;IAED,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;AACnC,CAAC;AAED;;;;;;;GAOG;AACH,SAAS,gBAAgB,CACvB,aAAqB,EACrB,KAAwB,EACxB,QAAkC,EAClC,QAA4B;IAE5B,MAAM,QAAQ,GAAG,CAAC,IAAY,EAAU,EAAE,CAAC,SAAS,CAAC,YAAY,CAAC,IAAI,CAAC,aAAa,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;IAEtG,IAAI,QAAQ,KAAK,IAAI,EAAE,CAAC;QACtB,OAAO;YACL,OAAO,EAAE,YAAY;YACrB,GAAG,CAAC,QAAQ,KAAK,SAAS,IAAI,EAAE,QAAQ,EAAE,CAAC;YAC3C,UAAU,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;SACpE,CAAC;IACJ,CAAC;IAED,MAAM,IAAI,GAAG,IAAI,GAAG,EAAkB,CAAC;IACvC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,QAAQ,GAAG,QAAQ,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC;QAC1E,IAAI,QAAQ,KAAK,SAAS;YAAE,SAAS;QACrC,+DAA+D;QAC/D,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;IACvE,CAAC;IACD,+EAA+E;IAC/E,0EAA0E;IAC1E,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC,EAAE,CAAC;YACjF,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC;QACjC,CAAC;IACH,CAAC;IACD,+EAA+E;IAC/E,4EAA4E;IAC5E,gFAAgF;IAChF,uBAAuB;IACvB,KAAK,MAAM,KAAK,IAAI,QAAQ,CAAC,UAAU,EAAE,CAAC;QACxC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,IAAI,EAAE,QAAQ,CAAC,EAAE,CAAC;YACrE,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;QACrC,CAAC;IACH,CAAC;IAED,OAAO;QACL,OAAO,EAAE,YAAY;QACrB,GAAG,CAAC,QAAQ,KAAK,SAAS,IAAI,EAAE,QAAQ,EAAE,CAAC;QAC3C,UAAU,EAAE,CAAC,GAAG,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,KAAK,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC;KACvH,CAAC;AACJ,CAAC;AAED,yEAAyE;AACzE,SAAS,gBAAgB,CAAC,aAAqB,EAAE,UAA+B,EAAE;IAChF,MAAM,MAAM,GAAG,iBAAiB,CAAC,aAAa,CAAC,CAAC;IAChD,MAAM,QAAQ,GAAG,UAAU,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAE1C,2EAA2E;IAC3E,8CAA8C;IAC9C,IAAI,MAAM,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC/B,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,IAAI,OAAO,CAAC,KAAK,KAAK,IAAI,EAAE,CAAC;QAC3B,MAAM,OAAO,GAAG,gBAAgB,CAAC,aAAa,EAAE,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;QACrF,MAAM,IAAI,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC;QACrD,MAAM,IAAI,GAAG,IAAI,CAAC,aAAa,EAAE,SAAS,CAAC,CAAC;QAC5C,IAAI,MAAM,GAAkB,IAAI,CAAC;QACjC,IAAI,CAAC;YACH,MAAM,GAAG,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QACtC,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAK,KAA+B,EAAE,IAAI,KAAK,QAAQ,EAAE,CAAC;gBACxD,MAAM,KAAK,CAAC;YACd,CAAC;QACH,CAAC;QACD,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;YACpB,aAAa,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;QACpC,CAAC;QACD,MAAM,CAAC,IAAI,GAAG,OAAO,CAAC;QACtB,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC;IACzB,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;QAClB,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,GAAG,SAAS,mCAAmC,EAAE,CAAC,CAAC;QACnF,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;IACrF,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,KAAK,EAAE,CAAC;QAChC,MAAM,QAAQ,GAAG,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACpC,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;YAC3B,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,GAAG,IAAI,0CAA0C,EAAE,CAAC,CAAC;YACrF,SAAS;QACX,CAAC;QACD,6EAA6E;QAC7E,kCAAkC;QAClC,IAAI,UAAU,CAAC,IAAI,EAAE,QAAQ,CAAC;YAAE,SAAS;QACzC,MAAM,MAAM,GAAG,SAAS,CAAC,YAAY,CAAC,IAAI,CAAC,aAAa,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;QAC1E,IAAI,MAAM,KAAK,QAAQ,EAAE,CAAC;YACxB,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC;gBACnB,OAAO,EAAE,GAAG,IAAI,oCAAoC,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI;aAC3G,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;QACpC,IAAI,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC;YAAE,SAAS;QAChD,IAAI,UAAU,CAAC,KAAK,CAAC,IAAI,EAAE,QAAQ,CAAC;YAAE,SAAS;QAC/C,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,GAAG,KAAK,CAAC,IAAI,sCAAsC,EAAE,CAAC,CAAC;IACzF,CAAC;IAED,8EAA8E;IAC9E,6EAA6E;IAC7E,sEAAsE;IACtE,MAAM,MAAM,GAAG,CAAC,GAAG,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IAC5F,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QACzB,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,KAAK,EAAE,CAAC;YAChC,IAAI,IAAI,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;gBACzC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC;oBACnB,OAAO,EAAE,GAAG,IAAI,iBAAiB,MAAM,kHAAkH,MAAM,EAAE;iBAClK,CAAC,CAAC;YACL,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,4EAA4E;AAC5E,SAAS,mBAAmB,CAAC,OAA0B,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;IAC1E,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,IAAI,YAAY,CAAC;IACtE,MAAM,KAAK,GAAG,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC;IACvC,MAAM,MAAM,GAAG,gBAAgB,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;IAChD,IAAI,MAAM,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC/B,OAAO,CAAC,KAAK,CAAC,mCAAmC,GAAG,GAAG,CAAC,CAAC;QACzD,KAAK,MAAM,OAAO,IAAI,MAAM,CAAC,QAAQ;YAAE,OAAO,CAAC,KAAK,CAAC,OAAO,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC;QAC/E,OAAO,CAAC,CAAC;IACX,CAAC;IACD,IAAI,KAAK,EAAE,CAAC;QACV,OAAO,CAAC,GAAG,CAAC,2BAA2B,MAAM,CAAC,KAAK,CAAC,MAAM,UAAU,CAAC,CAAC;QACtE,OAAO,CAAC,CAAC;IACX,CAAC;IACD,OAAO,CAAC,GAAG,CAAC,gCAAgC,MAAM,CAAC,KAAK,CAAC,MAAM,wBAAwB,CAAC,CAAC;IACzF,OAAO,CAAC,CAAC;AACX,CAAC;AAED,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,SAAS,EAAE,YAAY,EAAE,CAAC;AAG7F,6CAA6C;AAC7C,EAAE;AACF,iFAAiF;AACjF,oFAAoF;AACpF,kFAAkF;AAClF,oFAAoF;AACpF,uCAAuC;AACvC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,SAAS,IAAI,MAAM,CAAC,IAAI,CAAC,GAAG,KAAK,aAAa,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IAC7F,OAAO,CAAC,IAAI,CAAC,mBAAmB,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAC3D,CAAC","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 {\n version: LOCK_VERSION,\n ...(baseline !== undefined && { baseline }),\n migrations: files.map((name) => ({ name, sha256: digestOf(name) })),\n };\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].sort(([left], [right]) => left.localeCompare(right)).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"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"verify-spa-shell.js","sourceRoot":"","sources":["../../src/scripts/verify-spa-shell.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAE7C;;;;;;;GAOG;AACH,MAAM,eAAe,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC;AAEzF,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,SAAS,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;AAE7F,MAAM,EAAE,QAAQ,EAAE,GAAG,cAAc,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC;AAE9C,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;IACxB,OAAO,CAAC,KAAK,CAAC,yBAAyB,CAAC,CAAC;IACzC,KAAK,MAAM,OAAO,IAAI,QAAQ;QAAE,OAAO,CAAC,KAAK,CAAC,OAAO,OAAO,EAAE,CAAC,CAAC;IAChE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC;AAED,OAAO,CAAC,GAAG,
|
|
1
|
+
{"version":3,"file":"verify-spa-shell.js","sourceRoot":"","sources":["../../src/scripts/verify-spa-shell.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAE7C;;;;;;;GAOG;AACH,MAAM,eAAe,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC;AAEzF,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,SAAS,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;AAE7F,MAAM,EAAE,QAAQ,EAAE,GAAG,cAAc,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC;AAE9C,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;IACxB,OAAO,CAAC,KAAK,CAAC,yBAAyB,CAAC,CAAC;IACzC,KAAK,MAAM,OAAO,IAAI,QAAQ;QAAE,OAAO,CAAC,KAAK,CAAC,OAAO,OAAO,EAAE,CAAC,CAAC;IAChE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC;AAED,OAAO,CAAC,GAAG,CACT,2BAA2B,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,qCAAqC,CAAC,YAAY,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,eAAe,CAAC,IAAI,CAClI,CAAC","sourcesContent":["#!/usr/bin/env tsx\n/**\n * Fail when the SPA shell the API worker serves is missing or self-inconsistent.\n *\n * Provenance: Durable-DAV's and Durable-DAV-Router's `scripts/verify-spa-shell.mjs`\n * and Edge-Sonic's typed `scripts/build/verify-spa-shell.ts`. The rules — which of\n * the ways a shell can be wrong to look for — live in `spa-shell.ts`, so they are\n * unit tested without a build on disk; this file is the entrypoint that turns the\n * verdict into an exit code.\n *\n * What it catches:\n *\n * - the `postinstall` stub, or no build at all, still in place;\n * - `spa-shell.ts` referencing `/assets/...` bundles that were not emitted (a\n * partially cleaned or half-copied `dist/`, which serves a shell whose JS 404s\n * and therefore renders no operator UI at all);\n * - `spa-shell.ts` and `apps/web/dist/index.html` disagreeing, i.e. only one of\n * the two halves of the build was refreshed.\n *\n * **The build is a precondition of this check, not a convenience.** Both paths are\n * gitignored and `postinstall` writes a deliberately empty stub, so verifying\n * without building cannot pass — and it fails with precisely the message above,\n * which is the check working rather than the check being wrong.\n *\n * Usage, from the repo root:\n *\n * ```bash\n * pnpm exec tsx scripts/verify-spa-shell.ts # defaults to ./ as root\n * pnpm exec tsx scripts/verify-spa-shell.ts --root /path/to/repo\n * ```\n */\nimport path from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { verifySpaShell } from './spa-shell';\n\n/**\n * The repository root, derived from this file rather than from `process.cwd()`.\n *\n * Two levels up, because this file sits in `scripts/`. Deriving it means the gate\n * reads the same tree whatever directory it is invoked from — which is the\n * difference between a gate and a gate that passes because nobody ran it from the\n * right place.\n */\nconst REPOSITORY_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');\n\nconst root = process.argv[2] === undefined ? REPOSITORY_ROOT : path.resolve(process.argv[2]);\n\nconst { problems } = verifySpaShell({ root });\n\nif (problems.length > 0) {\n console.error('SPA shell check failed:');\n for (const problem of problems) console.error(` - ${problem}`);\n process.exit(1);\n}\n\nconsole.log(\n `SPA shell check passed (${path.join(root, 'apps/api/src/generated/spa-shell.ts')} matches ${path.join(root, 'apps/web/dist')}).`,\n);\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"patches.d.ts","sourceRoot":"","sources":["../../../src/scripts/wrangler-config/patches.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,OAAO,EAA8B,KAAK,cAAc,EAAE,MAAM,SAAS,CAAC;AAE1E,qDAAqD;AACrD,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,cAAc,CAAC;CACxB;AAED,wBAAgB,UAAU,IAAI,UAAU,CAGvC;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,KAAK,EAAE,OAAO,GAAG,MAAM,CAGtG;AAED,wFAAwF;AACxF,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAEjD;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAExE;AAED,wEAAwE;AACxE,wBAAgB,cAAc,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,CAgBnE;AAkBD;;;GAGG;AACH,wBAAgB,iBAAiB,IAAI,IAAI,CASxC;AAED;;;;;;;GAOG;AACH,wBAAgB,0BAA0B,IAAI,IAAI,CAYjD;AAED,wBAAgB,kBAAkB,IAAI,IAAI,CAYzC;AAED,wBAAgB,cAAc,IAAI,IAAI,
|
|
1
|
+
{"version":3,"file":"patches.d.ts","sourceRoot":"","sources":["../../../src/scripts/wrangler-config/patches.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,OAAO,EAA8B,KAAK,cAAc,EAAE,MAAM,SAAS,CAAC;AAE1E,qDAAqD;AACrD,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,cAAc,CAAC;CACxB;AAED,wBAAgB,UAAU,IAAI,UAAU,CAGvC;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,KAAK,EAAE,OAAO,GAAG,MAAM,CAGtG;AAED,wFAAwF;AACxF,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAEjD;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAExE;AAED,wEAAwE;AACxE,wBAAgB,cAAc,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,CAgBnE;AAkBD;;;GAGG;AACH,wBAAgB,iBAAiB,IAAI,IAAI,CASxC;AAED;;;;;;;GAOG;AACH,wBAAgB,0BAA0B,IAAI,IAAI,CAYjD;AAED,wBAAgB,kBAAkB,IAAI,IAAI,CAYzC;AAED,wBAAgB,cAAc,IAAI,IAAI,CAsBrC"}
|
|
@@ -125,7 +125,8 @@ export function applyVarsPatch() {
|
|
|
125
125
|
}
|
|
126
126
|
const prepared = readConfig();
|
|
127
127
|
let content = prepared.content;
|
|
128
|
-
if (prepared.config.vars !== undefined &&
|
|
128
|
+
if (prepared.config.vars !== undefined &&
|
|
129
|
+
(prepared.config.vars === null || typeof prepared.config.vars !== 'object' || Array.isArray(prepared.config.vars))) {
|
|
129
130
|
throw new Error('wrangler.jsonc vars must be an object before applying WRANGLER_VARS_PATCH_JSON.');
|
|
130
131
|
}
|
|
131
132
|
if (prepared.config.vars === undefined || prepared.config.vars === null) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"patches.js","sourceRoot":"","sources":["../../../src/scripts/wrangler-config/patches.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Reading, writing and patching `wrangler.jsonc`.\n *\n * Provenance: ported from Edge-Sonic's `scripts/lib/wrangler-config/patches.ts`.\n *\n * The config is JSONC — `//` comments and trailing commas — so it is edited\n * through jsonc-parser's `modify`/`applyEdits` rather than parsed and re-serialized.\n * A round trip through `JSON.parse` would drop every comment and reformat the\n * file, which is a diff nobody can review and a reason to regenerate the config by\n * hand instead.\n */\n\nimport { copyFileSync, readFileSync, writeFileSync } from 'node:fs';\nimport { applyEdits, modify, parse } from 'jsonc-parser';\nimport { CONFIG_PATH, TEMPLATE_PATH, type WranglerConfig } from './types';\n\n/** The config's current text and its parsed form. */\nexport interface ConfigFile {\n content: string;\n config: WranglerConfig;\n}\n\nexport function readConfig(): ConfigFile {\n const content = readFileSync(CONFIG_PATH, 'utf8');\n return { content, config: parse(content) as WranglerConfig };\n}\n\n/**\n * Apply one edit to the config text, preserving comments and formatting.\n */\nexport function writeConfigValue(content: string, path: Array<string | number>, value: unknown): string {\n const edits = modify(content, path, value, { formattingOptions: { insertSpaces: true, tabSize: 2, eol: '\\n' } });\n return applyEdits(content, edits);\n}\n\n/** Write the config, guaranteeing a trailing newline so Prettier sees a fixed point. */\nexport function writeConfig(content: string): void {\n writeFileSync(CONFIG_PATH, content.endsWith('\\n') ? content : `${content}\\n`);\n}\n\n/**\n * The top-level keys a caller wants to merge into the config, from the environment.\n *\n * `WRANGLER_PATCH_JSON` is how a repository variable supplies per-environment\n * settings without a second file: a repo with one worker and three environments\n * patches the routes and the vars rather than keeping three templates in step.\n */\nexport function parseTopLevelPatch(): Record<string, unknown> | undefined {\n return parseJsonObject(process.env['WRANGLER_PATCH_JSON'], 'WRANGLER_PATCH_JSON', 'a JSON object');\n}\n\n/** The `vars` entries a caller wants to merge, from the environment. */\nexport function parseVarsPatch(): Record<string, string> | undefined {\n const parsed = parseJsonObject(process.env['WRANGLER_VARS_PATCH_JSON'], 'WRANGLER_VARS_PATCH_JSON', 'a JSON object of string values');\n if (parsed === undefined) {\n return undefined;\n }\n const patch: Record<string, string> = {};\n for (const [key, value] of Object.entries(parsed)) {\n if (!key.trim()) {\n throw new Error('WRANGLER_VARS_PATCH_JSON contains an empty variable name.');\n }\n if (typeof value !== 'string') {\n throw new TypeError(`WRANGLER_VARS_PATCH_JSON value for ${key} must be a string.`);\n }\n patch[key] = value;\n }\n return patch;\n}\n\nfunction parseJsonObject(raw: string | undefined, name: string, expected: string): Record<string, unknown> | undefined {\n if (raw === undefined || !raw.trim()) {\n return undefined;\n }\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw) as unknown;\n } catch {\n throw new Error(`${name} must be ${expected}.`);\n }\n if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {\n throw new Error(`${name} must be ${expected}.`);\n }\n return parsed as Record<string, unknown>;\n}\n\n/**\n * Materialize the config: the repository variable when it is set, otherwise the\n * committed template.\n */\nexport function prepareConfigFile(): void {\n const dumpedConfig = process.env['WRANGLER_JSONC'];\n if (dumpedConfig?.trim()) {\n writeConfig(dumpedConfig);\n console.log('Wrote wrangler.jsonc from WRANGLER_JSONC repository variable.');\n return;\n }\n copyFileSync(TEMPLATE_PATH, CONFIG_PATH);\n console.log('WRANGLER_JSONC is empty; copied apps/api/wrangler.template.jsonc to wrangler.jsonc.');\n}\n\n/**\n * Refuse a config older than the template requires.\n *\n * The template is the one file in the repo that says which wrangler behaviour the\n * worker was written against, and a config written by an older template can name\n * bindings the worker no longer reads. Failing here rather than at deploy time is\n * the whole reason the check exists.\n */\nexport function ensureMinimumConfigVersion(): void {\n const template = parse(readFileSync(TEMPLATE_PATH, 'utf8')) as { $minimumVersion?: unknown };\n if (typeof template.$minimumVersion !== 'number') {\n return;\n }\n const { config } = readConfig();\n const currentVersion = config.$version;\n if (typeof currentVersion !== 'number' || currentVersion < template.$minimumVersion) {\n throw new Error(\n `wrangler.jsonc version (${typeof currentVersion === 'number' ? currentVersion : 'missing'}) is below minimum template version (${template.$minimumVersion}). Regenerate it from apps/api/wrangler.template.jsonc.`,\n );\n }\n}\n\nexport function applyTopLevelPatch(): void {\n const patch = parseTopLevelPatch();\n const entries = Object.entries(patch ?? {});\n if (entries.length === 0) {\n return;\n }\n let { content } = readConfig();\n for (const [key, value] of entries) {\n content = writeConfigValue(content, [key], value);\n }\n writeConfig(content);\n console.log(`Applied ${entries.length} Wrangler top-level patch entr${entries.length === 1 ? 'y' : 'ies'}.`);\n}\n\nexport function applyVarsPatch(): void {\n const patch = parseVarsPatch();\n const entries = Object.entries(patch ?? {});\n if (entries.length === 0) {\n return;\n }\n const prepared = readConfig();\n let content = prepared.content;\n if (prepared.config.vars !== undefined && (prepared.config.vars === null || typeof prepared.config.vars !== 'object' || Array.isArray(prepared.config.vars))) {\n throw new Error('wrangler.jsonc vars must be an object before applying WRANGLER_VARS_PATCH_JSON.');\n }\n if (prepared.config.vars === undefined || prepared.config.vars === null) {\n content = writeConfigValue(content, ['vars'], {});\n }\n for (const [key, value] of entries) {\n content = writeConfigValue(content, ['vars', key], value);\n }\n writeConfig(content);\n console.log(`Applied ${entries.length} Wrangler vars patch entr${entries.length === 1 ? 'y' : 'ies'}.`);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"patches.js","sourceRoot":"","sources":["../../../src/scripts/wrangler-config/patches.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Reading, writing and patching `wrangler.jsonc`.\n *\n * Provenance: ported from Edge-Sonic's `scripts/lib/wrangler-config/patches.ts`.\n *\n * The config is JSONC — `//` comments and trailing commas — so it is edited\n * through jsonc-parser's `modify`/`applyEdits` rather than parsed and re-serialized.\n * A round trip through `JSON.parse` would drop every comment and reformat the\n * file, which is a diff nobody can review and a reason to regenerate the config by\n * hand instead.\n */\n\nimport { copyFileSync, readFileSync, writeFileSync } from 'node:fs';\nimport { applyEdits, modify, parse } from 'jsonc-parser';\nimport { CONFIG_PATH, TEMPLATE_PATH, type WranglerConfig } from './types';\n\n/** The config's current text and its parsed form. */\nexport interface ConfigFile {\n content: string;\n config: WranglerConfig;\n}\n\nexport function readConfig(): ConfigFile {\n const content = readFileSync(CONFIG_PATH, 'utf8');\n return { content, config: parse(content) as WranglerConfig };\n}\n\n/**\n * Apply one edit to the config text, preserving comments and formatting.\n */\nexport function writeConfigValue(content: string, path: Array<string | number>, value: unknown): string {\n const edits = modify(content, path, value, { formattingOptions: { insertSpaces: true, tabSize: 2, eol: '\\n' } });\n return applyEdits(content, edits);\n}\n\n/** Write the config, guaranteeing a trailing newline so Prettier sees a fixed point. */\nexport function writeConfig(content: string): void {\n writeFileSync(CONFIG_PATH, content.endsWith('\\n') ? content : `${content}\\n`);\n}\n\n/**\n * The top-level keys a caller wants to merge into the config, from the environment.\n *\n * `WRANGLER_PATCH_JSON` is how a repository variable supplies per-environment\n * settings without a second file: a repo with one worker and three environments\n * patches the routes and the vars rather than keeping three templates in step.\n */\nexport function parseTopLevelPatch(): Record<string, unknown> | undefined {\n return parseJsonObject(process.env['WRANGLER_PATCH_JSON'], 'WRANGLER_PATCH_JSON', 'a JSON object');\n}\n\n/** The `vars` entries a caller wants to merge, from the environment. */\nexport function parseVarsPatch(): Record<string, string> | undefined {\n const parsed = parseJsonObject(process.env['WRANGLER_VARS_PATCH_JSON'], 'WRANGLER_VARS_PATCH_JSON', 'a JSON object of string values');\n if (parsed === undefined) {\n return undefined;\n }\n const patch: Record<string, string> = {};\n for (const [key, value] of Object.entries(parsed)) {\n if (!key.trim()) {\n throw new Error('WRANGLER_VARS_PATCH_JSON contains an empty variable name.');\n }\n if (typeof value !== 'string') {\n throw new TypeError(`WRANGLER_VARS_PATCH_JSON value for ${key} must be a string.`);\n }\n patch[key] = value;\n }\n return patch;\n}\n\nfunction parseJsonObject(raw: string | undefined, name: string, expected: string): Record<string, unknown> | undefined {\n if (raw === undefined || !raw.trim()) {\n return undefined;\n }\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw) as unknown;\n } catch {\n throw new Error(`${name} must be ${expected}.`);\n }\n if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {\n throw new Error(`${name} must be ${expected}.`);\n }\n return parsed as Record<string, unknown>;\n}\n\n/**\n * Materialize the config: the repository variable when it is set, otherwise the\n * committed template.\n */\nexport function prepareConfigFile(): void {\n const dumpedConfig = process.env['WRANGLER_JSONC'];\n if (dumpedConfig?.trim()) {\n writeConfig(dumpedConfig);\n console.log('Wrote wrangler.jsonc from WRANGLER_JSONC repository variable.');\n return;\n }\n copyFileSync(TEMPLATE_PATH, CONFIG_PATH);\n console.log('WRANGLER_JSONC is empty; copied apps/api/wrangler.template.jsonc to wrangler.jsonc.');\n}\n\n/**\n * Refuse a config older than the template requires.\n *\n * The template is the one file in the repo that says which wrangler behaviour the\n * worker was written against, and a config written by an older template can name\n * bindings the worker no longer reads. Failing here rather than at deploy time is\n * the whole reason the check exists.\n */\nexport function ensureMinimumConfigVersion(): void {\n const template = parse(readFileSync(TEMPLATE_PATH, 'utf8')) as { $minimumVersion?: unknown };\n if (typeof template.$minimumVersion !== 'number') {\n return;\n }\n const { config } = readConfig();\n const currentVersion = config.$version;\n if (typeof currentVersion !== 'number' || currentVersion < template.$minimumVersion) {\n throw new Error(\n `wrangler.jsonc version (${typeof currentVersion === 'number' ? currentVersion : 'missing'}) is below minimum template version (${template.$minimumVersion}). Regenerate it from apps/api/wrangler.template.jsonc.`,\n );\n }\n}\n\nexport function applyTopLevelPatch(): void {\n const patch = parseTopLevelPatch();\n const entries = Object.entries(patch ?? {});\n if (entries.length === 0) {\n return;\n }\n let { content } = readConfig();\n for (const [key, value] of entries) {\n content = writeConfigValue(content, [key], value);\n }\n writeConfig(content);\n console.log(`Applied ${entries.length} Wrangler top-level patch entr${entries.length === 1 ? 'y' : 'ies'}.`);\n}\n\nexport function applyVarsPatch(): void {\n const patch = parseVarsPatch();\n const entries = Object.entries(patch ?? {});\n if (entries.length === 0) {\n return;\n }\n const prepared = readConfig();\n let content = prepared.content;\n if (\n prepared.config.vars !== undefined &&\n (prepared.config.vars === null || typeof prepared.config.vars !== 'object' || Array.isArray(prepared.config.vars))\n ) {\n throw new Error('wrangler.jsonc vars must be an object before applying WRANGLER_VARS_PATCH_JSON.');\n }\n if (prepared.config.vars === undefined || prepared.config.vars === null) {\n content = writeConfigValue(content, ['vars'], {});\n }\n for (const [key, value] of entries) {\n content = writeConfigValue(content, ['vars', key], value);\n }\n writeConfig(content);\n console.log(`Applied ${entries.length} Wrangler vars patch entr${entries.length === 1 ? 'y' : 'ies'}.`);\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resources.d.ts","sourceRoot":"","sources":["../../../src/scripts/wrangler-config/resources.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAKH,OAAO,EAQL,KAAK,UAAU,EACf,KAAK,WAAW,EAChB,KAAK,WAAW,EAChB,KAAK,cAAc,EACpB,MAAM,SAAS,CAAC;AAIjB,iEAAiE;AACjE,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,0EAA0E;AAC1E,wBAAgB,OAAO,CAAC,QAAQ,EAAE,UAAU,GAAG,MAAM,GAAG,SAAS,CAEhE;AAED,wBAAgB,eAAe,IAAI,UAAU,EAAE,CAE9C;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,YAAY,EAAE,MAAM,GAAG,WAAW,CAclE;AAED,wBAAgB,gBAAgB,IAAI,WAAW,EAAE,CAEhD;AAED,iEAAiE;AACjE,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAElF;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,GAAG,WAAW,
|
|
1
|
+
{"version":3,"file":"resources.d.ts","sourceRoot":"","sources":["../../../src/scripts/wrangler-config/resources.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAKH,OAAO,EAQL,KAAK,UAAU,EACf,KAAK,WAAW,EAChB,KAAK,WAAW,EAChB,KAAK,cAAc,EACpB,MAAM,SAAS,CAAC;AAIjB,iEAAiE;AACjE,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,0EAA0E;AAC1E,wBAAgB,OAAO,CAAC,QAAQ,EAAE,UAAU,GAAG,MAAM,GAAG,SAAS,CAEhE;AAED,wBAAgB,eAAe,IAAI,UAAU,EAAE,CAE9C;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,YAAY,EAAE,MAAM,GAAG,WAAW,CAclE;AAED,wBAAgB,gBAAgB,IAAI,WAAW,EAAE,CAEhD;AAED,iEAAiE;AACjE,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAElF;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,GAAG,WAAW,CAkBtF;AAED,yFAAyF;AACzF,wBAAgB,qBAAqB,IAAI,MAAM,EAAE,CAEhD;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,cAAc,GAAG,MAAM,CAYxF;AAED,wDAAwD;AACxD,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,GAAG,WAAW,EAAE,CAiBpE;AAED,wBAAgB,gBAAgB,IAAI,WAAW,EAAE,CAQhD;AAED,wBAAgB,iBAAiB,IAAI,MAAM,CAmB1C;AAED,gFAAgF;AAChF,wBAAgB,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAUtD;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,cAAc,GAAG,MAAM,EAAE,CAc7D;AAED,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI,CAQhF;AA+DD;;;;;;;;GAQG;AACH,wBAAgB,0BAA0B,IAAI,MAAM,EAAE,CAqCrD"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resources.js","sourceRoot":"","sources":["../../../src/scripts/wrangler-config/resources.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Creating the resources `wrangler.jsonc` names, and writing their real ids over\n * the placeholders.\n *\n * Provenance: ported from Edge-Sonic's `scripts/lib/wrangler-config/resources.ts`.\n *\n * The placeholder rule: a template carries an all-zero id meaning \"resolve me\",\n * and a config carrying one cannot be deployed. So provisioning walks the config,\n * creates what is missing, and patches the id in place. A placeholder with nothing\n * to create it *under* — a KV id with no binding, a D1 id with no name — is\n * refused rather than skipped, because a silently skipped placeholder is a deploy\n * that fails later with no link back to the reason.\n */\n\nimport { writeFileSync } from 'node:fs';\nimport { parse } from 'jsonc-parser';\nimport { parseWranglerTableRows } from '../lib/wrangler-table';\nimport {\n CONFIG_PATH,\n DEFAULT_HEX_ID,\n DEFAULT_KV_NAMESPACE_NAMES,\n DEFAULT_SECRET_STORE_NAME,\n DEFAULT_UUID,\n DEFAULT_WORKER_NAME,\n VECTORIZE_DIMENSIONS,\n type D1Database,\n type KVNamespace,\n type SecretStore,\n type WranglerConfig,\n} from './types';\nimport { parseJsonArray, runWrangler } from './cli';\nimport { readConfig, writeConfig, writeConfigValue } from './patches';\n\n/** What `ensure*` did, so a caller can act on the difference. */\nexport interface Provisioned {\n id: string;\n created: boolean;\n}\n\n/** The id a D1 database is reported under, whichever key the CLI used. */\nexport function getD1Id(database: D1Database): string | undefined {\n return database.uuid ?? database.database_id ?? database.id;\n}\n\nexport function listD1Databases(): D1Database[] {\n return parseJsonArray<D1Database>(runWrangler(['d1', 'list', '--json']), 'wrangler d1 list --json');\n}\n\n/**\n * The D1 database `binding` names, created when it does not exist.\n *\n * Whether it was created is reported, not inferred: a database created moments ago\n * holds no user data, so a backup workflow must refuse to upload its empty dump.\n * The caller cannot re-derive that from the config, because this very function\n * patches the placeholder away before the reader ever runs.\n */\nexport function ensureD1Database(databaseName: string): Provisioned {\n let database = listD1Databases().find((candidate) => candidate.name === databaseName);\n let created = false;\n if (database === undefined) {\n console.log(`Creating D1 database: ${databaseName}`);\n runWrangler(['d1', 'create', databaseName]);\n database = listD1Databases().find((candidate) => candidate.name === databaseName);\n created = true;\n }\n const databaseId = database === undefined ? undefined : getD1Id(database);\n if (databaseId === undefined || databaseId === '') {\n throw new Error(`Unable to discover D1 database ID for ${databaseName}.`);\n }\n return { id: databaseId, created };\n}\n\nexport function listKVNamespaces(): KVNamespace[] {\n return parseJsonArray<KVNamespace>(runWrangler(['kv', 'namespace', 'list']), 'wrangler kv namespace list');\n}\n\n/** The namespace name a binding's namespace is created under. */\nexport function getKVNamespaceName(config: WranglerConfig, binding: string): string {\n return DEFAULT_KV_NAMESPACE_NAMES[binding] ?? `${config.name ?? DEFAULT_WORKER_NAME}-${binding.toLowerCase()}`;\n}\n\nexport function ensureKVNamespace(config: WranglerConfig, binding: string): Provisioned {\n const namespaceName = getKVNamespaceName(config, binding);\n const candidateNames = new Set([namespaceName, `${config.name ?? DEFAULT_WORKER_NAME}-${binding}`, binding]);\n let namespace = listKVNamespaces().find((candidate) => {\n const candidateName = candidate.title ?? candidate.name;\n return candidate.id !== undefined && candidateName !== undefined && candidateNames.has(candidateName);\n });\n let created = false;\n if (namespace === undefined) {\n console.log(`Creating KV namespace: ${namespaceName}`);\n runWrangler(['kv', 'namespace', 'create', namespaceName]);\n namespace = listKVNamespaces().find(\n (candidate) => candidate.id !== undefined && (candidate.title ?? candidate.name) === namespaceName,\n );\n created = true;\n }\n if (namespace?.id === undefined) {\n throw new Error(`Unable to discover KV namespace ID for ${namespaceName}.`);\n }\n return { id: namespace.id, created };\n}\n\n/** The bindings a deployment always needs, whether or not a custom config lists them. */\nexport function getRequiredKvBindings(): string[] {\n return Object.keys(DEFAULT_KV_NAMESPACE_NAMES);\n}\n\n/**\n * Add the always-needed KV bindings a hand-written config left out.\n *\n * A config supplied through `WRANGLER_JSONC` is edited by hand and drifts; without\n * this the worker starts with an unbound `CACHE` and fails on the first read.\n */\nexport function ensureRequiredKvBindings(content: string, config: WranglerConfig): string {\n const existing = new Set((config.kv_namespaces ?? []).map((namespace) => namespace.binding).filter((binding) => binding !== undefined));\n const missing = getRequiredKvBindings().filter((binding) => !existing.has(binding));\n if (missing.length === 0) {\n return content;\n }\n const next = [...(config.kv_namespaces ?? [])];\n for (const binding of missing) {\n console.log(`Adding missing KV namespace binding: ${binding}`);\n next.push({ binding, id: DEFAULT_HEX_ID });\n }\n return writeConfigValue(content, ['kv_namespaces'], next);\n}\n\n/** The stores in a `secrets-store store list` table. */\nexport function parseSecretStoresTable(output: string): SecretStore[] {\n const stores: SecretStore[] = [];\n // Through the shared table parser rather than a second inline split: `wrangler`\n // renders through `cli-table3`, whose cells are separated by U+2502 and are *not*\n // space-aligned, so a parser written for aligned output silently matches nothing\n // — which reads as \"the store does not exist\" and makes provisioning create a\n // duplicate.\n for (const row of parseWranglerTableRows(output)) {\n const name = row[0];\n const id = row[1];\n // Store ids are 32 hex characters; a row without one is a header, a border, or\n // an unrelated column, and is skipped rather than stored with an undefined id.\n if (name !== undefined && id !== undefined && /^[a-f0-9]{32}$/i.test(id)) {\n stores.push({ name, id });\n }\n }\n return stores;\n}\n\nexport function listSecretStores(): SecretStore[] {\n const output = runWrangler(['secrets-store', 'store', 'list', '--remote']);\n try {\n return parseJsonArray<SecretStore>(output, 'wrangler secrets-store store list --remote');\n } catch {\n // The subcommand prints a table rather than JSON on some versions.\n return parseSecretStoresTable(output);\n }\n}\n\nexport function ensureSecretStore(): string {\n const stores = listSecretStores();\n const existing = stores.find((candidate) => candidate.name === DEFAULT_SECRET_STORE_NAME) ?? stores[0];\n if (existing !== undefined) {\n return existing.id;\n }\n console.log(`Creating Secrets Store: ${DEFAULT_SECRET_STORE_NAME}`);\n const output = runWrangler(['secrets-store', 'store', 'create', DEFAULT_SECRET_STORE_NAME, '--remote']);\n const createdStoreId = /ID:\\s*([a-f0-9]{32})/i.exec(output)?.[1];\n if (createdStoreId !== undefined) {\n return createdStoreId;\n }\n // The create output did not name the id, so read it back rather than guessing.\n const refreshed = listSecretStores();\n const store = refreshed.find((candidate) => candidate.name === DEFAULT_SECRET_STORE_NAME) ?? refreshed[0];\n if (store?.id === undefined) {\n throw new Error(`Unable to discover Secrets Store ID for ${DEFAULT_SECRET_STORE_NAME}.`);\n }\n return store.id;\n}\n\n/** Create a queue when it is absent, reporting whether it had to be created. */\nexport function ensureQueue(queueName: string): boolean {\n try {\n runWrangler(['queues', 'info', queueName]);\n console.log(`Queue ${queueName} already exists.`);\n return false;\n } catch {\n console.log(`Creating queue: ${queueName}`);\n runWrangler(['queues', 'create', queueName]);\n return true;\n }\n}\n\n/**\n * Queue names a deployment needs, in first-seen order.\n *\n * Producers and consumers name the same queue twice — one queue, two roles — and a\n * name appearing in both must be created once. Pure so it is testable without\n * wrangler.\n */\nexport function queueNamesIn(config: WranglerConfig): string[] {\n const names: string[] = [];\n const seen = new Set<string>();\n for (const producer of config.queues?.producers ?? []) {\n if (producer.queue === '' || seen.has(producer.queue)) continue;\n seen.add(producer.queue);\n names.push(producer.queue);\n }\n for (const consumer of config.queues?.consumers ?? []) {\n if (consumer.queue === '' || seen.has(consumer.queue)) continue;\n seen.add(consumer.queue);\n names.push(consumer.queue);\n }\n return names;\n}\n\nexport function ensureVectorizeIndex(indexName: string, dimensions: number): void {\n try {\n runWrangler(['vectorize', 'info', indexName]);\n console.log(`Vectorize index ${indexName} already exists.`);\n } catch {\n console.log(`Creating Vectorize index: ${indexName} with ${dimensions} dimensions`);\n runWrangler(['vectorize', 'create', indexName, `--dimensions=${dimensions}`, '--metric=cosine']);\n }\n}\n\n/** Replace each placeholder D1 `database_id` with a real one. */\nfunction provisionD1Databases(content: string, config: WranglerConfig, created: string[]): string {\n let patched = content;\n for (const [index, database] of config.d1_databases?.entries() ?? []) {\n if (database.database_id !== DEFAULT_UUID) continue;\n if (database.database_name === undefined || database.database_name === '') {\n throw new Error(`D1 database binding ${database.binding ?? index} has a placeholder database_id but no database_name.`);\n }\n const resolved = ensureD1Database(database.database_name);\n if (resolved.created) {\n created.push(`d1:${database.database_name}`);\n }\n console.log(`Using D1 database ${database.database_name}: ${resolved.id}`);\n patched = writeConfigValue(patched, ['d1_databases', index, 'database_id'], resolved.id);\n }\n return patched;\n}\n\n/** Replace each placeholder KV namespace id with a real one. */\nfunction provisionKvNamespaces(content: string, config: WranglerConfig, created: string[]): string {\n let patched = content;\n for (const [index, namespace] of config.kv_namespaces?.entries() ?? []) {\n if (namespace.id !== DEFAULT_HEX_ID) continue;\n if (namespace.binding === undefined || namespace.binding === '') {\n throw new Error(`KV namespace at index ${index} has a placeholder id but no binding.`);\n }\n const resolved = ensureKVNamespace(config, namespace.binding);\n const namespaceName = getKVNamespaceName(config, namespace.binding);\n if (resolved.created) {\n created.push(`kv:${namespaceName}`);\n }\n console.log(`Using KV namespace ${namespaceName}: ${resolved.id}`);\n patched = writeConfigValue(patched, ['kv_namespaces', index, 'id'], resolved.id);\n }\n return patched;\n}\n\n/**\n * Point every placeholder Secrets Store `store_id` at one store, creating it if\n * absent. `existed` is read **before** the create, because whether the store was\n * created is what the caller reports.\n */\nfunction provisionSecretStore(content: string, config: WranglerConfig, created: string[]): string {\n const placeholders = (config.secrets_store_secrets ?? [])\n .map((secret, index) => ({ secret, index }))\n .filter(({ secret }) => secret.store_id === DEFAULT_HEX_ID);\n if (placeholders.length === 0) {\n return content;\n }\n const existed = listSecretStores().some((store) => store.name === DEFAULT_SECRET_STORE_NAME);\n const storeId = ensureSecretStore();\n if (!existed) {\n created.push(`secrets-store:${DEFAULT_SECRET_STORE_NAME}`);\n }\n console.log(`Using Secrets Store: ${storeId}`);\n return placeholders.reduce((patched, { index }) => writeConfigValue(patched, ['secrets_store_secrets', index, 'store_id'], storeId), content);\n}\n\n/**\n * Create every resource the config names, and write the real ids over the\n * placeholders.\n *\n * Returns what it had to create, as `<kind>:<name>` — returned rather than logged,\n * because a caller acts on it: the backup workflow refuses to export a D1 database\n * that appears here, since one created moments ago holds no user data and an empty\n * dump uploaded on schedule is a false sense of safety.\n */\nexport function provisionWranglerResources(): string[] {\n const created: string[] = [];\n let { content, config } = readConfig();\n\n content = ensureRequiredKvBindings(content, config);\n config = parse(content) as WranglerConfig;\n\n // Each of the three below re-parses before it patches, because each writes into\n // the config text the previous one produced. They are separate functions because\n // they are separate resources with separate rules, and one loop holding all\n // three would read as a single resource list with the differences hidden in the\n // bodies.\n content = provisionD1Databases(content, config, created);\n config = parse(content) as WranglerConfig;\n\n content = provisionKvNamespaces(content, config, created);\n config = parse(content) as WranglerConfig;\n\n content = provisionSecretStore(content, config, created);\n\n writeConfig(content);\n\n config = parse(content) as WranglerConfig;\n for (const queueName of queueNamesIn(config)) {\n if (ensureQueue(queueName)) {\n created.push(`queue:${queueName}`);\n }\n }\n\n // Vectorize indexes are name-based, so nothing in the file is patched — but the\n // created names are still reported like every other resource, so a deploy log\n // shows what appeared.\n for (const binding of config.vectorize ?? []) {\n ensureVectorizeIndex(binding.index_name, VECTORIZE_DIMENSIONS);\n }\n\n return created;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"resources.js","sourceRoot":"","sources":["../../../src/scripts/wrangler-config/resources.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Creating the resources `wrangler.jsonc` names, and writing their real ids over\n * the placeholders.\n *\n * Provenance: ported from Edge-Sonic's `scripts/lib/wrangler-config/resources.ts`.\n *\n * The placeholder rule: a template carries an all-zero id meaning \"resolve me\",\n * and a config carrying one cannot be deployed. So provisioning walks the config,\n * creates what is missing, and patches the id in place. A placeholder with nothing\n * to create it *under* — a KV id with no binding, a D1 id with no name — is\n * refused rather than skipped, because a silently skipped placeholder is a deploy\n * that fails later with no link back to the reason.\n */\n\nimport { writeFileSync } from 'node:fs';\nimport { parse } from 'jsonc-parser';\nimport { parseWranglerTableRows } from '../lib/wrangler-table';\nimport {\n CONFIG_PATH,\n DEFAULT_HEX_ID,\n DEFAULT_KV_NAMESPACE_NAMES,\n DEFAULT_SECRET_STORE_NAME,\n DEFAULT_UUID,\n DEFAULT_WORKER_NAME,\n VECTORIZE_DIMENSIONS,\n type D1Database,\n type KVNamespace,\n type SecretStore,\n type WranglerConfig,\n} from './types';\nimport { parseJsonArray, runWrangler } from './cli';\nimport { readConfig, writeConfig, writeConfigValue } from './patches';\n\n/** What `ensure*` did, so a caller can act on the difference. */\nexport interface Provisioned {\n id: string;\n created: boolean;\n}\n\n/** The id a D1 database is reported under, whichever key the CLI used. */\nexport function getD1Id(database: D1Database): string | undefined {\n return database.uuid ?? database.database_id ?? database.id;\n}\n\nexport function listD1Databases(): D1Database[] {\n return parseJsonArray<D1Database>(runWrangler(['d1', 'list', '--json']), 'wrangler d1 list --json');\n}\n\n/**\n * The D1 database `binding` names, created when it does not exist.\n *\n * Whether it was created is reported, not inferred: a database created moments ago\n * holds no user data, so a backup workflow must refuse to upload its empty dump.\n * The caller cannot re-derive that from the config, because this very function\n * patches the placeholder away before the reader ever runs.\n */\nexport function ensureD1Database(databaseName: string): Provisioned {\n let database = listD1Databases().find((candidate) => candidate.name === databaseName);\n let created = false;\n if (database === undefined) {\n console.log(`Creating D1 database: ${databaseName}`);\n runWrangler(['d1', 'create', databaseName]);\n database = listD1Databases().find((candidate) => candidate.name === databaseName);\n created = true;\n }\n const databaseId = database === undefined ? undefined : getD1Id(database);\n if (databaseId === undefined || databaseId === '') {\n throw new Error(`Unable to discover D1 database ID for ${databaseName}.`);\n }\n return { id: databaseId, created };\n}\n\nexport function listKVNamespaces(): KVNamespace[] {\n return parseJsonArray<KVNamespace>(runWrangler(['kv', 'namespace', 'list']), 'wrangler kv namespace list');\n}\n\n/** The namespace name a binding's namespace is created under. */\nexport function getKVNamespaceName(config: WranglerConfig, binding: string): string {\n return DEFAULT_KV_NAMESPACE_NAMES[binding] ?? `${config.name ?? DEFAULT_WORKER_NAME}-${binding.toLowerCase()}`;\n}\n\nexport function ensureKVNamespace(config: WranglerConfig, binding: string): Provisioned {\n const namespaceName = getKVNamespaceName(config, binding);\n const candidateNames = new Set([namespaceName, `${config.name ?? DEFAULT_WORKER_NAME}-${binding}`, binding]);\n let namespace = listKVNamespaces().find((candidate) => {\n const candidateName = candidate.title ?? candidate.name;\n return candidate.id !== undefined && candidateName !== undefined && candidateNames.has(candidateName);\n });\n let created = false;\n if (namespace === undefined) {\n console.log(`Creating KV namespace: ${namespaceName}`);\n runWrangler(['kv', 'namespace', 'create', namespaceName]);\n namespace = listKVNamespaces().find((candidate) => candidate.id !== undefined && (candidate.title ?? candidate.name) === namespaceName);\n created = true;\n }\n if (namespace?.id === undefined) {\n throw new Error(`Unable to discover KV namespace ID for ${namespaceName}.`);\n }\n return { id: namespace.id, created };\n}\n\n/** The bindings a deployment always needs, whether or not a custom config lists them. */\nexport function getRequiredKvBindings(): string[] {\n return Object.keys(DEFAULT_KV_NAMESPACE_NAMES);\n}\n\n/**\n * Add the always-needed KV bindings a hand-written config left out.\n *\n * A config supplied through `WRANGLER_JSONC` is edited by hand and drifts; without\n * this the worker starts with an unbound `CACHE` and fails on the first read.\n */\nexport function ensureRequiredKvBindings(content: string, config: WranglerConfig): string {\n const existing = new Set((config.kv_namespaces ?? []).map((namespace) => namespace.binding).filter((binding) => binding !== undefined));\n const missing = getRequiredKvBindings().filter((binding) => !existing.has(binding));\n if (missing.length === 0) {\n return content;\n }\n const next = [...(config.kv_namespaces ?? [])];\n for (const binding of missing) {\n console.log(`Adding missing KV namespace binding: ${binding}`);\n next.push({ binding, id: DEFAULT_HEX_ID });\n }\n return writeConfigValue(content, ['kv_namespaces'], next);\n}\n\n/** The stores in a `secrets-store store list` table. */\nexport function parseSecretStoresTable(output: string): SecretStore[] {\n const stores: SecretStore[] = [];\n // Through the shared table parser rather than a second inline split: `wrangler`\n // renders through `cli-table3`, whose cells are separated by U+2502 and are *not*\n // space-aligned, so a parser written for aligned output silently matches nothing\n // — which reads as \"the store does not exist\" and makes provisioning create a\n // duplicate.\n for (const row of parseWranglerTableRows(output)) {\n const name = row[0];\n const id = row[1];\n // Store ids are 32 hex characters; a row without one is a header, a border, or\n // an unrelated column, and is skipped rather than stored with an undefined id.\n if (name !== undefined && id !== undefined && /^[a-f0-9]{32}$/i.test(id)) {\n stores.push({ name, id });\n }\n }\n return stores;\n}\n\nexport function listSecretStores(): SecretStore[] {\n const output = runWrangler(['secrets-store', 'store', 'list', '--remote']);\n try {\n return parseJsonArray<SecretStore>(output, 'wrangler secrets-store store list --remote');\n } catch {\n // The subcommand prints a table rather than JSON on some versions.\n return parseSecretStoresTable(output);\n }\n}\n\nexport function ensureSecretStore(): string {\n const stores = listSecretStores();\n const existing = stores.find((candidate) => candidate.name === DEFAULT_SECRET_STORE_NAME) ?? stores[0];\n if (existing !== undefined) {\n return existing.id;\n }\n console.log(`Creating Secrets Store: ${DEFAULT_SECRET_STORE_NAME}`);\n const output = runWrangler(['secrets-store', 'store', 'create', DEFAULT_SECRET_STORE_NAME, '--remote']);\n const createdStoreId = /ID:\\s*([a-f0-9]{32})/i.exec(output)?.[1];\n if (createdStoreId !== undefined) {\n return createdStoreId;\n }\n // The create output did not name the id, so read it back rather than guessing.\n const refreshed = listSecretStores();\n const store = refreshed.find((candidate) => candidate.name === DEFAULT_SECRET_STORE_NAME) ?? refreshed[0];\n if (store?.id === undefined) {\n throw new Error(`Unable to discover Secrets Store ID for ${DEFAULT_SECRET_STORE_NAME}.`);\n }\n return store.id;\n}\n\n/** Create a queue when it is absent, reporting whether it had to be created. */\nexport function ensureQueue(queueName: string): boolean {\n try {\n runWrangler(['queues', 'info', queueName]);\n console.log(`Queue ${queueName} already exists.`);\n return false;\n } catch {\n console.log(`Creating queue: ${queueName}`);\n runWrangler(['queues', 'create', queueName]);\n return true;\n }\n}\n\n/**\n * Queue names a deployment needs, in first-seen order.\n *\n * Producers and consumers name the same queue twice — one queue, two roles — and a\n * name appearing in both must be created once. Pure so it is testable without\n * wrangler.\n */\nexport function queueNamesIn(config: WranglerConfig): string[] {\n const names: string[] = [];\n const seen = new Set<string>();\n for (const producer of config.queues?.producers ?? []) {\n if (producer.queue === '' || seen.has(producer.queue)) continue;\n seen.add(producer.queue);\n names.push(producer.queue);\n }\n for (const consumer of config.queues?.consumers ?? []) {\n if (consumer.queue === '' || seen.has(consumer.queue)) continue;\n seen.add(consumer.queue);\n names.push(consumer.queue);\n }\n return names;\n}\n\nexport function ensureVectorizeIndex(indexName: string, dimensions: number): void {\n try {\n runWrangler(['vectorize', 'info', indexName]);\n console.log(`Vectorize index ${indexName} already exists.`);\n } catch {\n console.log(`Creating Vectorize index: ${indexName} with ${dimensions} dimensions`);\n runWrangler(['vectorize', 'create', indexName, `--dimensions=${dimensions}`, '--metric=cosine']);\n }\n}\n\n/** Replace each placeholder D1 `database_id` with a real one. */\nfunction provisionD1Databases(content: string, config: WranglerConfig, created: string[]): string {\n let patched = content;\n for (const [index, database] of config.d1_databases?.entries() ?? []) {\n if (database.database_id !== DEFAULT_UUID) continue;\n if (database.database_name === undefined || database.database_name === '') {\n throw new Error(`D1 database binding ${database.binding ?? index} has a placeholder database_id but no database_name.`);\n }\n const resolved = ensureD1Database(database.database_name);\n if (resolved.created) {\n created.push(`d1:${database.database_name}`);\n }\n console.log(`Using D1 database ${database.database_name}: ${resolved.id}`);\n patched = writeConfigValue(patched, ['d1_databases', index, 'database_id'], resolved.id);\n }\n return patched;\n}\n\n/** Replace each placeholder KV namespace id with a real one. */\nfunction provisionKvNamespaces(content: string, config: WranglerConfig, created: string[]): string {\n let patched = content;\n for (const [index, namespace] of config.kv_namespaces?.entries() ?? []) {\n if (namespace.id !== DEFAULT_HEX_ID) continue;\n if (namespace.binding === undefined || namespace.binding === '') {\n throw new Error(`KV namespace at index ${index} has a placeholder id but no binding.`);\n }\n const resolved = ensureKVNamespace(config, namespace.binding);\n const namespaceName = getKVNamespaceName(config, namespace.binding);\n if (resolved.created) {\n created.push(`kv:${namespaceName}`);\n }\n console.log(`Using KV namespace ${namespaceName}: ${resolved.id}`);\n patched = writeConfigValue(patched, ['kv_namespaces', index, 'id'], resolved.id);\n }\n return patched;\n}\n\n/**\n * Point every placeholder Secrets Store `store_id` at one store, creating it if\n * absent. `existed` is read **before** the create, because whether the store was\n * created is what the caller reports.\n */\nfunction provisionSecretStore(content: string, config: WranglerConfig, created: string[]): string {\n const placeholders = (config.secrets_store_secrets ?? [])\n .map((secret, index) => ({ secret, index }))\n .filter(({ secret }) => secret.store_id === DEFAULT_HEX_ID);\n if (placeholders.length === 0) {\n return content;\n }\n const existed = listSecretStores().some((store) => store.name === DEFAULT_SECRET_STORE_NAME);\n const storeId = ensureSecretStore();\n if (!existed) {\n created.push(`secrets-store:${DEFAULT_SECRET_STORE_NAME}`);\n }\n console.log(`Using Secrets Store: ${storeId}`);\n return placeholders.reduce(\n (patched, { index }) => writeConfigValue(patched, ['secrets_store_secrets', index, 'store_id'], storeId),\n content,\n );\n}\n\n/**\n * Create every resource the config names, and write the real ids over the\n * placeholders.\n *\n * Returns what it had to create, as `<kind>:<name>` — returned rather than logged,\n * because a caller acts on it: the backup workflow refuses to export a D1 database\n * that appears here, since one created moments ago holds no user data and an empty\n * dump uploaded on schedule is a false sense of safety.\n */\nexport function provisionWranglerResources(): string[] {\n const created: string[] = [];\n let { content, config } = readConfig();\n\n content = ensureRequiredKvBindings(content, config);\n config = parse(content) as WranglerConfig;\n\n // Each of the three below re-parses before it patches, because each writes into\n // the config text the previous one produced. They are separate functions because\n // they are separate resources with separate rules, and one loop holding all\n // three would read as a single resource list with the differences hidden in the\n // bodies.\n content = provisionD1Databases(content, config, created);\n config = parse(content) as WranglerConfig;\n\n content = provisionKvNamespaces(content, config, created);\n config = parse(content) as WranglerConfig;\n\n content = provisionSecretStore(content, config, created);\n\n writeConfig(content);\n\n config = parse(content) as WranglerConfig;\n for (const queueName of queueNamesIn(config)) {\n if (ensureQueue(queueName)) {\n created.push(`queue:${queueName}`);\n }\n }\n\n // Vectorize indexes are name-based, so nothing in the file is patched — but the\n // created names are still reported like every other resource, so a deploy log\n // shows what appeared.\n for (const binding of config.vectorize ?? []) {\n ensureVectorizeIndex(binding.index_name, VECTORIZE_DIMENSIONS);\n }\n\n return created;\n}\n"]}
|
package/dist/vite.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vite.d.ts","sourceRoot":"","sources":["../src/vite.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAKH,OAAO,EAAE,YAAY,EAAE,KAAK,MAAM,EAAE,MAAM,eAAe,CAAC;AAE1D;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,YAAY,CAAC,EAAE,OAAO,CAAC;IAChC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC,CAAA;KAAE,KAAK,MAAM,GAAG,SAAS,CAAC;CAC/G;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B;;;;;OAKG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB;;;;;;;;;OASG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,GAAG,CAAC;IAC7B;;;;;OAKG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,GAAG,CAAC;CACnC;AAED;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC;;;;OAIG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC;;OAEG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC,CAAC;IACtD;;OAEG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,cAAc,CAAC;IACnC;;OAEG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB;;;OAGG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACxB;AA0CD,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,cAAc,GAAG,MAAM,
|
|
1
|
+
{"version":3,"file":"vite.d.ts","sourceRoot":"","sources":["../src/vite.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAKH,OAAO,EAAE,YAAY,EAAE,KAAK,MAAM,EAAE,MAAM,eAAe,CAAC;AAE1D;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,YAAY,CAAC,EAAE,OAAO,CAAC;IAChC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC,CAAA;KAAE,KAAK,MAAM,GAAG,SAAS,CAAC;CAC/G;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B;;;;;OAKG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB;;;;;;;;;OASG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,GAAG,CAAC;IAC7B;;;;;OAKG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,GAAG,CAAC;CACnC;AAED;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC;;;;OAIG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC;;OAEG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC,CAAC;IACtD;;OAEG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,cAAc,CAAC;IACnC;;OAEG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB;;;OAGG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACxB;AA0CD,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,cAAc,GAAG,MAAM,CA0BlE;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,GAAE,oBAAyB,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAcvG"}
|
package/dist/vite.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vite.js","sourceRoot":"","sources":["../src/vite.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The SPA's Vite config.\n *\n * Provenance: converged from the `apps/web/vite.config.ts` of the eight repos\n * that build a Vite SPA. They are one file with three variables: the plugin list\n * (`@vitejs/plugin-react` + `@tailwindcss/vite`), the dev-server proxy target,\n * and the `spa-shell-embed` plugin that copies `dist/index.html` into\n * `apps/api/src/generated/spa-shell.ts`.\n *\n * Canonical decisions:\n *\n * 1. **The plugins are passed in, not imported.** The kit stays zero-dependency:\n * `react`, `@vitejs/plugin-react` and `@tailwindcss/vite` belong to the app\n * that uses them, and importing them here would make the tooling package\n * undownloadable for a consumer without a React SPA.\n * 2. **The `spa-shell-embed` hook stays per-repo, and is passed in.** It is the\n * one genuinely repo-specific piece: each repo names its own worker and its\n * own generated-module path. What the kit provides is the *contract* — see\n * `spaShellEmbedPlugin`, which is the converged body of all eight copies.\n * 3. **`server.proxy` is data, not code.** The paths differ per repo\n * (`/user` + `/rest` in Edge-Sonic, `/api` in CalDAV-Bridge, `/tracker` in\n * ChordDHT-Tracker) but the shape never does, so it is an option with a\n * default.\n * 4. **`build.emptyOutDir: true`** is the default, because a stale asset in\n * `dist/` is served as a live bundle — the same half-refreshed build\n * `scripts/verify-spa-shell.ts` rejects.\n *\n * The consumer's `apps/web/vite.config.ts` becomes a handful of lines — see the\n * README.\n */\n\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';\nimport { dirname, resolve } from 'node:path';\nimport { fileURLToPath, pathToFileURL } from 'node:url';\nimport { defineConfig, type Plugin } from 'vitest/config';\n\n/**\n * One dev-server proxy entry, as Vite's `server.proxy` accepts it.\n */\nexport interface ProxyEntry {\n readonly target: string;\n readonly changeOrigin?: boolean;\n readonly bypass?: (request: { headers: Record<string, string | string[] | undefined> }) => string | undefined;\n}\n\n/**\n * Where the SPA's built `index.html` is copied to, and under what module name.\n */\nexport interface SpaShellTarget {\n /**\n * Path of the generated module, relative to the repository root.\n *\n * `apps/api/src/generated/spa-shell.ts` in every repo that embeds a shell; the\n * parameter exists because the path is the one thing that genuinely differs.\n */\n readonly path: string;\n /**\n * The repository root the path is resolved against, as a file URL or a path.\n *\n * Defaults to the directory the build runs in (`process.cwd()`), which is the\n * repo root when the build is `pnpm --filter @scope/web build`. Passed in by the\n * Vite config, not derived from `import.meta.url`: this plugin object is created\n * inside the kit's own module, so a URL relative to it points into\n * `node_modules/@rexezuge/tooling` and writes the shell somewhere no compiler\n * will ever look.\n */\n readonly root?: string | URL;\n /**\n * The built `index.html`, as a path or file URL.\n *\n * Defaults to `dist/index.html` under the same root. Vite's `closeBundle` runs\n * after the bundle is written, so this is the file the build just produced.\n */\n readonly distIndex?: string | URL;\n}\n\n/**\n * What `defineWebViteConfig` accepts.\n */\nexport interface WebViteConfigOptions {\n /**\n * The SPA's plugins — `react()`, `tailwindcss()`, and anything else the app\n * needs. Applied before the spa-shell plugin so the shell is written after the\n * bundle exists.\n */\n readonly plugins?: readonly Plugin[];\n /**\n * Dev-server proxy entries. Keyed by path prefix.\n */\n readonly proxy?: Readonly<Record<string, ProxyEntry>>;\n /**\n * Where the built shell is embedded. Omit to skip the embedding entirely.\n */\n readonly spaShell?: SpaShellTarget;\n /**\n * The build output directory.\n */\n readonly outDir?: string;\n /**\n * The directory this config is resolved from, as a file URL. Defaults to the\n * caller's location.\n */\n readonly root?: string;\n}\n\n/**\n * The `spa-shell-embed` plugin, converged from all eight copies.\n *\n * It exists because the API worker serves `SPA_HTML` from a module in its own\n * source tree, and both that module and `dist/` are gitignored — so a fresh\n * clone gets an empty stub from `postinstall` and every `GET /` returns a blank\n * page until someone runs a build. This plugin is what makes the build write the\n * real artefact, and `scripts/verify-spa-shell.ts` is what refuses to deploy\n * without it.\n *\n * Written as a factory rather than inlined so the exit conditions are in one\n * place: the `closeBundle` hook runs on every build, including the ones where\n * `dist/index.html` was never produced, and a plugin that throws there fails\n * builds that legitimately emit nothing.\n */\n/**\n * Normalise a path-or-URL option to a URL, so `new URL(...)` can be used on it.\n *\n * Small, and here because two options accept either form: a path is what a consumer\n * naturally writes in a config, and a URL is what `import.meta.url` hands them.\n * Accepting both is one line and saves a `fileURLToPath` at every call site.\n */\nfunction toUrl(value: string | URL): URL {\n return typeof value === 'string' ? pathToFileURL(value) : value;\n}\n\n/**\n * The same, as a **directory** URL — with the trailing separator `new URL(rel, base)`\n * needs.\n *\n * `new URL('apps/api/x.ts', 'file:///repo')` resolves to `/apps/api/x.ts`, silently\n * dropping the base. The base has to end in a separator, and a path from\n * `process.cwd()` never does, so this is where the plugin would otherwise write the\n * shell into the filesystem root.\n */\nfunction toDirUrl(value: string | URL): URL {\n const url = toUrl(value);\n return url.href.endsWith('/') ? url : new URL(`${url.href}/`);\n}\n\nexport function spaShellEmbedPlugin(target: SpaShellTarget): Plugin {\n return {\n name: 'spa-shell-embed',\n closeBundle() {\n // Resolved from the directory the build runs in, never from this module's own\n // URL: the plugin object is created here but executes in the consumer's Vite\n // process, and `new URL(path, import.meta.url)` would resolve against\n // `node_modules/@rexezuge/tooling`. `process.cwd()` is the repo root for\n // `pnpm --filter @scope/web build`, which is how every consumer runs it.\n const root = toDirUrl(target.root ?? process.cwd());\n const outputPath = fileURLToPath(new URL(target.path, root));\n const indexPath = target.distIndex === undefined ? fileURLToPath(new URL('dist/index.html', root)) : fileURLToPath(toUrl(target.distIndex));\n\n if (!existsSync(indexPath)) {\n console.warn('spa-shell-embed: dist/index.html not found, skipping');\n return;\n }\n\n const html = readFileSync(indexPath, 'utf8');\n const output = `// Auto-generated by Vite build plugin - DO NOT EDIT.\\nexport const SPA_HTML: string = ${JSON.stringify(html)};\\n`;\n\n mkdirSync(dirname(outputPath), { recursive: true });\n writeFileSync(outputPath, output);\n },\n };\n}\n\n/**\n * Build the SPA's Vite config.\n *\n * ```ts\n * // apps/web/vite.config.ts\n * import react from '@vitejs/plugin-react';\n * import { defineWebViteConfig, spaShellEmbedPlugin } from '@rexezuge/tooling/vite';\n *\n * export default defineWebViteConfig({\n * plugins: [react()],\n * proxy: { '/user': { target: 'http://localhost:8787', changeOrigin: true } },\n * spaShell: { path: '../../apps/api/src/generated/spa-shell.ts' },\n * });\n * ```\n */\nexport function defineWebViteConfig(options: WebViteConfigOptions = {}): ReturnType<typeof defineConfig> {\n const plugins: Plugin[] = [...(options.plugins ?? [])];\n if (options.spaShell !== undefined) {\n plugins.push(spaShellEmbedPlugin(options.spaShell));\n }\n\n return defineConfig({\n ...(plugins.length > 0 && { plugins }),\n ...(options.proxy !== undefined && { server: { proxy: { ...options.proxy } } }),\n build: {\n outDir: options.outDir ?? 'dist',\n emptyOutDir: true,\n },\n });\n}\n"]}
|
|
1
|
+
{"version":3,"file":"vite.js","sourceRoot":"","sources":["../src/vite.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The SPA's Vite config.\n *\n * Provenance: converged from the `apps/web/vite.config.ts` of the eight repos\n * that build a Vite SPA. They are one file with three variables: the plugin list\n * (`@vitejs/plugin-react` + `@tailwindcss/vite`), the dev-server proxy target,\n * and the `spa-shell-embed` plugin that copies `dist/index.html` into\n * `apps/api/src/generated/spa-shell.ts`.\n *\n * Canonical decisions:\n *\n * 1. **The plugins are passed in, not imported.** The kit stays zero-dependency:\n * `react`, `@vitejs/plugin-react` and `@tailwindcss/vite` belong to the app\n * that uses them, and importing them here would make the tooling package\n * undownloadable for a consumer without a React SPA.\n * 2. **The `spa-shell-embed` hook stays per-repo, and is passed in.** It is the\n * one genuinely repo-specific piece: each repo names its own worker and its\n * own generated-module path. What the kit provides is the *contract* — see\n * `spaShellEmbedPlugin`, which is the converged body of all eight copies.\n * 3. **`server.proxy` is data, not code.** The paths differ per repo\n * (`/user` + `/rest` in Edge-Sonic, `/api` in CalDAV-Bridge, `/tracker` in\n * ChordDHT-Tracker) but the shape never does, so it is an option with a\n * default.\n * 4. **`build.emptyOutDir: true`** is the default, because a stale asset in\n * `dist/` is served as a live bundle — the same half-refreshed build\n * `scripts/verify-spa-shell.ts` rejects.\n *\n * The consumer's `apps/web/vite.config.ts` becomes a handful of lines — see the\n * README.\n */\n\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';\nimport { dirname, resolve } from 'node:path';\nimport { fileURLToPath, pathToFileURL } from 'node:url';\nimport { defineConfig, type Plugin } from 'vitest/config';\n\n/**\n * One dev-server proxy entry, as Vite's `server.proxy` accepts it.\n */\nexport interface ProxyEntry {\n readonly target: string;\n readonly changeOrigin?: boolean;\n readonly bypass?: (request: { headers: Record<string, string | string[] | undefined> }) => string | undefined;\n}\n\n/**\n * Where the SPA's built `index.html` is copied to, and under what module name.\n */\nexport interface SpaShellTarget {\n /**\n * Path of the generated module, relative to the repository root.\n *\n * `apps/api/src/generated/spa-shell.ts` in every repo that embeds a shell; the\n * parameter exists because the path is the one thing that genuinely differs.\n */\n readonly path: string;\n /**\n * The repository root the path is resolved against, as a file URL or a path.\n *\n * Defaults to the directory the build runs in (`process.cwd()`), which is the\n * repo root when the build is `pnpm --filter @scope/web build`. Passed in by the\n * Vite config, not derived from `import.meta.url`: this plugin object is created\n * inside the kit's own module, so a URL relative to it points into\n * `node_modules/@rexezuge/tooling` and writes the shell somewhere no compiler\n * will ever look.\n */\n readonly root?: string | URL;\n /**\n * The built `index.html`, as a path or file URL.\n *\n * Defaults to `dist/index.html` under the same root. Vite's `closeBundle` runs\n * after the bundle is written, so this is the file the build just produced.\n */\n readonly distIndex?: string | URL;\n}\n\n/**\n * What `defineWebViteConfig` accepts.\n */\nexport interface WebViteConfigOptions {\n /**\n * The SPA's plugins — `react()`, `tailwindcss()`, and anything else the app\n * needs. Applied before the spa-shell plugin so the shell is written after the\n * bundle exists.\n */\n readonly plugins?: readonly Plugin[];\n /**\n * Dev-server proxy entries. Keyed by path prefix.\n */\n readonly proxy?: Readonly<Record<string, ProxyEntry>>;\n /**\n * Where the built shell is embedded. Omit to skip the embedding entirely.\n */\n readonly spaShell?: SpaShellTarget;\n /**\n * The build output directory.\n */\n readonly outDir?: string;\n /**\n * The directory this config is resolved from, as a file URL. Defaults to the\n * caller's location.\n */\n readonly root?: string;\n}\n\n/**\n * The `spa-shell-embed` plugin, converged from all eight copies.\n *\n * It exists because the API worker serves `SPA_HTML` from a module in its own\n * source tree, and both that module and `dist/` are gitignored — so a fresh\n * clone gets an empty stub from `postinstall` and every `GET /` returns a blank\n * page until someone runs a build. This plugin is what makes the build write the\n * real artefact, and `scripts/verify-spa-shell.ts` is what refuses to deploy\n * without it.\n *\n * Written as a factory rather than inlined so the exit conditions are in one\n * place: the `closeBundle` hook runs on every build, including the ones where\n * `dist/index.html` was never produced, and a plugin that throws there fails\n * builds that legitimately emit nothing.\n */\n/**\n * Normalise a path-or-URL option to a URL, so `new URL(...)` can be used on it.\n *\n * Small, and here because two options accept either form: a path is what a consumer\n * naturally writes in a config, and a URL is what `import.meta.url` hands them.\n * Accepting both is one line and saves a `fileURLToPath` at every call site.\n */\nfunction toUrl(value: string | URL): URL {\n return typeof value === 'string' ? pathToFileURL(value) : value;\n}\n\n/**\n * The same, as a **directory** URL — with the trailing separator `new URL(rel, base)`\n * needs.\n *\n * `new URL('apps/api/x.ts', 'file:///repo')` resolves to `/apps/api/x.ts`, silently\n * dropping the base. The base has to end in a separator, and a path from\n * `process.cwd()` never does, so this is where the plugin would otherwise write the\n * shell into the filesystem root.\n */\nfunction toDirUrl(value: string | URL): URL {\n const url = toUrl(value);\n return url.href.endsWith('/') ? url : new URL(`${url.href}/`);\n}\n\nexport function spaShellEmbedPlugin(target: SpaShellTarget): Plugin {\n return {\n name: 'spa-shell-embed',\n closeBundle() {\n // Resolved from the directory the build runs in, never from this module's own\n // URL: the plugin object is created here but executes in the consumer's Vite\n // process, and `new URL(path, import.meta.url)` would resolve against\n // `node_modules/@rexezuge/tooling`. `process.cwd()` is the repo root for\n // `pnpm --filter @scope/web build`, which is how every consumer runs it.\n const root = toDirUrl(target.root ?? process.cwd());\n const outputPath = fileURLToPath(new URL(target.path, root));\n const indexPath =\n target.distIndex === undefined ? fileURLToPath(new URL('dist/index.html', root)) : fileURLToPath(toUrl(target.distIndex));\n\n if (!existsSync(indexPath)) {\n console.warn('spa-shell-embed: dist/index.html not found, skipping');\n return;\n }\n\n const html = readFileSync(indexPath, 'utf8');\n const output = `// Auto-generated by Vite build plugin - DO NOT EDIT.\\nexport const SPA_HTML: string = ${JSON.stringify(html)};\\n`;\n\n mkdirSync(dirname(outputPath), { recursive: true });\n writeFileSync(outputPath, output);\n },\n };\n}\n\n/**\n * Build the SPA's Vite config.\n *\n * ```ts\n * // apps/web/vite.config.ts\n * import react from '@vitejs/plugin-react';\n * import { defineWebViteConfig, spaShellEmbedPlugin } from '@rexezuge/tooling/vite';\n *\n * export default defineWebViteConfig({\n * plugins: [react()],\n * proxy: { '/user': { target: 'http://localhost:8787', changeOrigin: true } },\n * spaShell: { path: '../../apps/api/src/generated/spa-shell.ts' },\n * });\n * ```\n */\nexport function defineWebViteConfig(options: WebViteConfigOptions = {}): ReturnType<typeof defineConfig> {\n const plugins: Plugin[] = [...(options.plugins ?? [])];\n if (options.spaShell !== undefined) {\n plugins.push(spaShellEmbedPlugin(options.spaShell));\n }\n\n return defineConfig({\n ...(plugins.length > 0 && { plugins }),\n ...(options.proxy !== undefined && { server: { proxy: { ...options.proxy } } }),\n build: {\n outDir: options.outDir ?? 'dist',\n emptyOutDir: true,\n },\n });\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rexezuge/tooling",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.7",
|
|
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.
|
|
29
|
+
"@rexezuge/d1": "1.0.7"
|
|
30
30
|
},
|
|
31
31
|
"peerDependencies": {
|
|
32
32
|
"eslint": "^10.0.0",
|
package/src/index.ts
CHANGED
|
@@ -39,4 +39,11 @@ export { DurableObject, RpcTarget, WorkflowEntrypoint } from './test/mocks/cloud
|
|
|
39
39
|
export type { DurableObjectState, ExecutionContext } from './test/mocks/cloudflare-workers';
|
|
40
40
|
|
|
41
41
|
export { createMigrationHelper, readMigrationFiles, splitSql } from './test/integration-migrations';
|
|
42
|
-
export type {
|
|
42
|
+
export type {
|
|
43
|
+
D1DatabaseLike,
|
|
44
|
+
D1PreparedStatementLike,
|
|
45
|
+
MigrationFile,
|
|
46
|
+
MigrationHelper,
|
|
47
|
+
MigrationHelperOptions,
|
|
48
|
+
MigrationRange,
|
|
49
|
+
} from './test/integration-migrations';
|
|
@@ -47,9 +47,9 @@ describe('evaluateConfig', () => {
|
|
|
47
47
|
});
|
|
48
48
|
|
|
49
49
|
it('fails a destination with no Cloudflare credentials', () => {
|
|
50
|
-
expect(
|
|
51
|
-
|
|
52
|
-
);
|
|
50
|
+
expect(
|
|
51
|
+
evaluateConfig({ BACKUP_ENCRYPTION_KEY: 'k', WEBDAV_URL: 'https://dav.example.com', WEBDAV_USER: 'u', WEBDAV_PASSWORD: 'p' }).errors,
|
|
52
|
+
).toStrictEqual(['A backup destination is configured but CLOUDFLARE_API_TOKEN / CLOUDFLARE_ACCOUNT_ID are missing.']);
|
|
53
53
|
});
|
|
54
54
|
|
|
55
55
|
it('passes when both a destination and the key are present', () => {
|
|
@@ -239,9 +239,7 @@ async function main(): Promise<void> {
|
|
|
239
239
|
if (existing === undefined) {
|
|
240
240
|
// Refuse to guess: creating a secret we could not read back risks overwriting
|
|
241
241
|
// a live value with a freshly generated one.
|
|
242
|
-
throw new Error(
|
|
243
|
-
`Could not list secrets in store ${storeId}; refusing to create ${secretName} without confirming it is absent.`,
|
|
244
|
-
);
|
|
242
|
+
throw new Error(`Could not list secrets in store ${storeId}; refusing to create ${secretName} without confirming it is absent.`);
|
|
245
243
|
}
|
|
246
244
|
if (existing.has(secretName)) {
|
|
247
245
|
console.log(`Secret ${secretName} already exists; leaving it untouched.`);
|
|
@@ -41,8 +41,7 @@ for (const problem of report.problems) {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
console.log(
|
|
44
|
-
`${report.lock === null ? 'left' : 'wrote'} ${path.join(migrationsDir, LOCK_NAME)} ` +
|
|
45
|
-
`(${report.files.length} migration(s) recorded)`,
|
|
44
|
+
`${report.lock === null ? 'left' : 'wrote'} ${path.join(migrationsDir, LOCK_NAME)} ` + `(${report.files.length} migration(s) recorded)`,
|
|
46
45
|
);
|
|
47
46
|
|
|
48
47
|
// The lock is re-checked by the caller: `verify-migrations.ts` re-reads from disk,
|
package/src/scripts/spa-shell.ts
CHANGED
|
@@ -68,9 +68,7 @@ function ensureSpaShellStub(options: SpaShellPaths): { created: boolean; path: s
|
|
|
68
68
|
/** The `SPA_HTML` string literal, decoded from the generated TS module. */
|
|
69
69
|
function readEmbeddedHtml(shell: string, problems: string[]): string | null {
|
|
70
70
|
if (!existsSync(shell)) {
|
|
71
|
-
problems.push(
|
|
72
|
-
`${shell} is missing — run the build (only apps/web has a build step).`,
|
|
73
|
-
);
|
|
71
|
+
problems.push(`${shell} is missing — run the build (only apps/web has a build step).`);
|
|
74
72
|
return null;
|
|
75
73
|
}
|
|
76
74
|
const source = readFileSync(shell, 'utf8');
|
|
@@ -62,8 +62,16 @@ describe('validateLocales', () => {
|
|
|
62
62
|
// `@rexezuge/i18n` ships `const enStrings: BackendStrings = { … }` per file, so
|
|
63
63
|
// the same gate covers the backend catalogs and the SPA JSON bundles.
|
|
64
64
|
const root = mkdtempSync(join(tmpdir(), 'locales-'));
|
|
65
|
-
writeFileSync(
|
|
66
|
-
|
|
65
|
+
writeFileSync(
|
|
66
|
+
join(root, 'en.ts'),
|
|
67
|
+
"import type { BackendStrings } from '../BackendStrings';\n\nconst enStrings: BackendStrings = {\n common: {\n unauthorized: 'Authentication Required.',\n forbidden: 'Access Denied.',\n },\n};\n\nexport { enStrings };\n",
|
|
68
|
+
'utf8',
|
|
69
|
+
);
|
|
70
|
+
writeFileSync(
|
|
71
|
+
join(root, 'de.ts'),
|
|
72
|
+
"const deStrings = {\n common: {\n unauthorized: 'Authentifizierung Erforderlich.',\n forbidden: 'Zugriff Verweigert.',\n },\n};\n",
|
|
73
|
+
'utf8',
|
|
74
|
+
);
|
|
67
75
|
expect(validateLocales(root).problems).toStrictEqual([]);
|
|
68
76
|
});
|
|
69
77
|
|
|
@@ -40,10 +40,18 @@ describe('verifyMigrations', () => {
|
|
|
40
40
|
|
|
41
41
|
it('fails on a bad name or a gap in numbering', () => {
|
|
42
42
|
const bad = tempMigrations({ 'users.sql': ONE });
|
|
43
|
-
expect(
|
|
43
|
+
expect(
|
|
44
|
+
verifyMigrations(bad)
|
|
45
|
+
.problems.map((problem) => problem.problem)
|
|
46
|
+
.join('\n'),
|
|
47
|
+
).toContain('does not match');
|
|
44
48
|
|
|
45
49
|
const gap = tempMigrations({ '0001_init.sql': ONE, '0003_skipped.sql': TWO });
|
|
46
|
-
expect(
|
|
50
|
+
expect(
|
|
51
|
+
verifyMigrations(gap)
|
|
52
|
+
.problems.map((problem) => problem.problem)
|
|
53
|
+
.join('\n'),
|
|
54
|
+
).toContain('not contiguous');
|
|
47
55
|
});
|
|
48
56
|
|
|
49
57
|
it('fails when a locked migration no longer exists', async () => {
|
|
@@ -51,7 +59,11 @@ describe('verifyMigrations', () => {
|
|
|
51
59
|
verifyMigrations(root, { write: true });
|
|
52
60
|
const { rmSync } = await import('node:fs');
|
|
53
61
|
rmSync(join(root, '0002_more.sql'));
|
|
54
|
-
expect(
|
|
62
|
+
expect(
|
|
63
|
+
verifyMigrations(root)
|
|
64
|
+
.problems.map((problem) => problem.problem)
|
|
65
|
+
.join('\n'),
|
|
66
|
+
).toContain('no longer exists');
|
|
55
67
|
});
|
|
56
68
|
|
|
57
69
|
it('writes a lock with per-file sha256 entries', () => {
|
|
@@ -225,7 +225,11 @@ function buildUpdatedLock(
|
|
|
225
225
|
const digestOf = (name: string): string => sha256Hex(readFileSync(join(migrationsDir, name), 'utf8'));
|
|
226
226
|
|
|
227
227
|
if (existing === null) {
|
|
228
|
-
return {
|
|
228
|
+
return {
|
|
229
|
+
version: LOCK_VERSION,
|
|
230
|
+
...(baseline !== undefined && { baseline }),
|
|
231
|
+
migrations: files.map((name) => ({ name, sha256: digestOf(name) })),
|
|
232
|
+
};
|
|
229
233
|
}
|
|
230
234
|
|
|
231
235
|
const kept = new Map<string, string>();
|
|
@@ -255,9 +259,7 @@ function buildUpdatedLock(
|
|
|
255
259
|
return {
|
|
256
260
|
version: LOCK_VERSION,
|
|
257
261
|
...(baseline !== undefined && { baseline }),
|
|
258
|
-
migrations: [...kept]
|
|
259
|
-
.sort(([left], [right]) => left.localeCompare(right))
|
|
260
|
-
.map(([name, sha256]) => ({ name, sha256 })),
|
|
262
|
+
migrations: [...kept].sort(([left], [right]) => left.localeCompare(right)).map(([name, sha256]) => ({ name, sha256 })),
|
|
261
263
|
};
|
|
262
264
|
}
|
|
263
265
|
|
|
@@ -53,4 +53,6 @@ if (problems.length > 0) {
|
|
|
53
53
|
process.exit(1);
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
console.log(
|
|
56
|
+
console.log(
|
|
57
|
+
`SPA shell check passed (${path.join(root, 'apps/api/src/generated/spa-shell.ts')} matches ${path.join(root, 'apps/web/dist')}).`,
|
|
58
|
+
);
|
|
@@ -143,7 +143,10 @@ export function applyVarsPatch(): void {
|
|
|
143
143
|
}
|
|
144
144
|
const prepared = readConfig();
|
|
145
145
|
let content = prepared.content;
|
|
146
|
-
if (
|
|
146
|
+
if (
|
|
147
|
+
prepared.config.vars !== undefined &&
|
|
148
|
+
(prepared.config.vars === null || typeof prepared.config.vars !== 'object' || Array.isArray(prepared.config.vars))
|
|
149
|
+
) {
|
|
147
150
|
throw new Error('wrangler.jsonc vars must be an object before applying WRANGLER_VARS_PATCH_JSON.');
|
|
148
151
|
}
|
|
149
152
|
if (prepared.config.vars === undefined || prepared.config.vars === null) {
|
|
@@ -90,9 +90,7 @@ export function ensureKVNamespace(config: WranglerConfig, binding: string): Prov
|
|
|
90
90
|
if (namespace === undefined) {
|
|
91
91
|
console.log(`Creating KV namespace: ${namespaceName}`);
|
|
92
92
|
runWrangler(['kv', 'namespace', 'create', namespaceName]);
|
|
93
|
-
namespace = listKVNamespaces().find(
|
|
94
|
-
(candidate) => candidate.id !== undefined && (candidate.title ?? candidate.name) === namespaceName,
|
|
95
|
-
);
|
|
93
|
+
namespace = listKVNamespaces().find((candidate) => candidate.id !== undefined && (candidate.title ?? candidate.name) === namespaceName);
|
|
96
94
|
created = true;
|
|
97
95
|
}
|
|
98
96
|
if (namespace?.id === undefined) {
|
|
@@ -278,7 +276,10 @@ function provisionSecretStore(content: string, config: WranglerConfig, created:
|
|
|
278
276
|
created.push(`secrets-store:${DEFAULT_SECRET_STORE_NAME}`);
|
|
279
277
|
}
|
|
280
278
|
console.log(`Using Secrets Store: ${storeId}`);
|
|
281
|
-
return placeholders.reduce(
|
|
279
|
+
return placeholders.reduce(
|
|
280
|
+
(patched, { index }) => writeConfigValue(patched, ['secrets_store_secrets', index, 'store_id'], storeId),
|
|
281
|
+
content,
|
|
282
|
+
);
|
|
282
283
|
}
|
|
283
284
|
|
|
284
285
|
/**
|
|
@@ -2,7 +2,15 @@ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
|
2
2
|
import { tmpdir } from 'node:os';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { afterAll, describe, expect, it } from 'vitest';
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
createMigrationHelper,
|
|
7
|
+
executableStatements,
|
|
8
|
+
readMigrationFiles,
|
|
9
|
+
splitSql,
|
|
10
|
+
type D1DatabaseLike,
|
|
11
|
+
type D1PreparedStatementLike,
|
|
12
|
+
type MigrationFile,
|
|
13
|
+
} from './integration-migrations';
|
|
6
14
|
|
|
7
15
|
const created: string[] = [];
|
|
8
16
|
|
|
@@ -148,10 +156,12 @@ describe('createMigrationHelper', () => {
|
|
|
148
156
|
it('refuses an unknown range endpoint, naming what is available', async () => {
|
|
149
157
|
const dir = tempTree({ '0001_init.sql': 'SELECT 1;' });
|
|
150
158
|
const helper = createMigrationHelper({ migrationsDir: dir });
|
|
151
|
-
await expect(helper.applyMigrations(fakeD1(), { from: '0009_missing.sql' })).rejects.toThrow(
|
|
159
|
+
await expect(helper.applyMigrations(fakeD1(), { from: '0009_missing.sql' })).rejects.toThrow(
|
|
160
|
+
/Unknown migration file: 0009_missing\.sql/,
|
|
161
|
+
);
|
|
152
162
|
});
|
|
153
163
|
|
|
154
|
-
it(
|
|
164
|
+
it("reports one file's SQL without reading the others", () => {
|
|
155
165
|
const dir = tempTree({ '0001_init.sql': 'SELECT 1;', '0002_more.sql': 'SELECT 2;' });
|
|
156
166
|
const helper = createMigrationHelper({ migrationsDir: dir });
|
|
157
167
|
expect(helper.migrationSqlOf('0002_more.sql')).toBe('SELECT 2;');
|
package/src/vite.ts
CHANGED
|
@@ -154,7 +154,8 @@ export function spaShellEmbedPlugin(target: SpaShellTarget): Plugin {
|
|
|
154
154
|
// `pnpm --filter @scope/web build`, which is how every consumer runs it.
|
|
155
155
|
const root = toDirUrl(target.root ?? process.cwd());
|
|
156
156
|
const outputPath = fileURLToPath(new URL(target.path, root));
|
|
157
|
-
const indexPath =
|
|
157
|
+
const indexPath =
|
|
158
|
+
target.distIndex === undefined ? fileURLToPath(new URL('dist/index.html', root)) : fileURLToPath(toUrl(target.distIndex));
|
|
158
159
|
|
|
159
160
|
if (!existsSync(indexPath)) {
|
|
160
161
|
console.warn('spa-shell-embed: dist/index.html not found, skipping');
|