From 4a362e9b823d2f14d82646c9f420179aa9dd34f1 Mon Sep 17 00:00:00 2001 From: Morrow Contributors Date: Fri, 7 Aug 2026 18:28:59 +0800 Subject: [PATCH] fix: parse wrangler.jsonc as JSONC in the production config projection The committed Wrangler configuration gained comments, but the production deploy path still read it with JSON.parse, so every deploy:production run failed before reaching Wrangler. Parse it with jsonc-parser and cover the committed file itself so the projection cannot drift from the source format again. Co-Authored-By: Claude Fable 5 --- apps/desk/package.json | 1 + apps/desk/scripts/production-config.mjs | 13 +++++++++- apps/desk/test/production-config.node.test.ts | 26 ++++++++++++++++++- package-lock.json | 8 ++++++ 4 files changed, 46 insertions(+), 2 deletions(-) diff --git a/apps/desk/package.json b/apps/desk/package.json index bcdee48..47bfddf 100644 --- a/apps/desk/package.json +++ b/apps/desk/package.json @@ -92,6 +92,7 @@ "@playwright/test": "^1.62.0", "@types/node": "^24.0.0", "esbuild": "^0.28.1", + "jsonc-parser": "3.3.1", "typescript": "^7.0.0", "vitest": "^4.1.0", "wrangler": "^4.112.0" diff --git a/apps/desk/scripts/production-config.mjs b/apps/desk/scripts/production-config.mjs index 87c5d92..e452d49 100644 --- a/apps/desk/scripts/production-config.mjs +++ b/apps/desk/scripts/production-config.mjs @@ -2,6 +2,8 @@ import { readFile, writeFile } from 'node:fs/promises' import { dirname, resolve } from 'node:path' import { fileURLToPath, pathToFileURL } from 'node:url' +import { parse as parseJsonc } from 'jsonc-parser' + const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..') const sourcePath = resolve(repositoryRoot, 'wrangler.jsonc') const outputPath = resolve(repositoryRoot, 'wrangler.production.generated.json') @@ -12,6 +14,15 @@ function required(env, name, pattern) { return value } +export function parseWranglerSource(text) { + const errors = [] + const source = parseJsonc(text, errors, { allowTrailingComma: true }) + if (errors.length > 0 || typeof source !== 'object' || source === null || Array.isArray(source)) { + throw new Error('wrangler.jsonc is not valid JSONC') + } + return source +} + export function createProductionConfig(source, env) { const databaseId = required(env, 'ABLE_D1_DATABASE_ID', /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i) const databaseName = required(env, 'ABLE_D1_DATABASE_NAME', /^[a-z0-9][a-z0-9_-]{1,62}$/) @@ -50,7 +61,7 @@ export function createProductionConfig(source, env) { } if (process.argv[1] && pathToFileURL(resolve(process.argv[1])).href === import.meta.url) { - const source = JSON.parse(await readFile(sourcePath, 'utf8')) + const source = parseWranglerSource(await readFile(sourcePath, 'utf8')) const production = createProductionConfig(source, process.env) await writeFile(outputPath, `${JSON.stringify(production, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 }) console.log('Prepared ephemeral production Wrangler configuration') diff --git a/apps/desk/test/production-config.node.test.ts b/apps/desk/test/production-config.node.test.ts index bbce10b..4280df9 100644 --- a/apps/desk/test/production-config.node.test.ts +++ b/apps/desk/test/production-config.node.test.ts @@ -1,6 +1,8 @@ +import { readFile } from 'node:fs/promises' + import { describe, expect, it } from 'vitest' -import { createProductionConfig } from '../scripts/production-config.mjs' +import { createProductionConfig, parseWranglerSource } from '../scripts/production-config.mjs' const source = { name: 'able', @@ -55,4 +57,26 @@ describe('production Wrangler configuration', () => { .toThrow('ABLE_WORKER_NAME is invalid') } }) + + it('projects the committed wrangler.jsonc, comments included', async () => { + // The committed configuration is JSONC, so the deploy path must accept + // comments rather than assume plain JSON. + const committed = parseWranglerSource(await readFile(new URL('../wrangler.jsonc', import.meta.url), 'utf8')) + const configured = createProductionConfig(committed, { ...variables, ABLE_WORKER_NAME: 'predecessor' }) + + expect(configured.name).toBe('predecessor') + expect(configured.d1_databases[0]).toMatchObject({ + binding: 'DB', + database_name: variables.ABLE_D1_DATABASE_NAME, + database_id: variables.ABLE_D1_DATABASE_ID, + }) + expect(configured.r2_buckets[0]).toMatchObject({ bucket_name: variables.ABLE_R2_BUCKET_NAME }) + expect(configured.queues.producers[0].queue).toBe(variables.ABLE_MEDIA_QUEUE_NAME) + expect(configured.queues.consumers[0].queue).toBe(variables.ABLE_MEDIA_QUEUE_NAME) + }) + + it('rejects source text that is not valid JSONC', () => { + expect(() => parseWranglerSource('{ "name": ')).toThrow('wrangler.jsonc is not valid JSONC') + expect(() => parseWranglerSource('[]')).toThrow('wrangler.jsonc is not valid JSONC') + }) }) diff --git a/package-lock.json b/package-lock.json index c2b7dac..2275951 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39,6 +39,7 @@ "@playwright/test": "^1.62.0", "@types/node": "^24.0.0", "esbuild": "^0.28.1", + "jsonc-parser": "3.3.1", "typescript": "^7.0.0", "vitest": "^4.1.0", "wrangler": "^4.112.0" @@ -3933,6 +3934,13 @@ "node": ">=6" } }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true, + "license": "MIT" + }, "node_modules/kleur": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",