diff --git a/package.json b/package.json index 2fd237e..4da142f 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "i18n": "node scripts/i18n-validate.cjs", "i18n:validate": "node scripts/i18n-validate.cjs", "deploy": "wrangler deploy", - "deploy:kv": "wrangler deploy -c wrangler.kv.toml", + "deploy:kv": "node scripts/ensure-kv.cjs && wrangler deploy -c wrangler.kv.toml", "deploy:demo": "npm run build:demo && wrangler pages deploy dist --project-name nw-demo" }, "keywords": [ diff --git a/scripts/ensure-kv.cjs b/scripts/ensure-kv.cjs new file mode 100644 index 0000000..af0bdb9 --- /dev/null +++ b/scripts/ensure-kv.cjs @@ -0,0 +1,64 @@ +#!/usr/bin/env node +/** + * Make `deploy:kv` idempotent across repeated builds. + * + * KV namespaces are referenced in wrangler config by account-scoped `id`, not + * by name. The template ships without an id so fresh accounts can provision one + * on first deploy. In non-interactive builds, wrangler may try to create the + * same namespace again on later builds and fail with code 10014. + */ +const { execSync } = require('node:child_process'); +const fs = require('node:fs'); +const path = require('node:path'); + +const CONFIG = path.resolve(__dirname, '..', 'wrangler.kv.toml'); +const BINDING = 'ATTACHMENTS_KV'; + +const wrangler = (args) => + execSync(`npx wrangler ${args}`, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'inherit'] }); + +function bindingBlockHasId(toml) { + const blocks = toml.match(/\[\[kv_namespaces\]\][^[]*/g) || []; + const block = blocks.find((entry) => new RegExp(`binding\\s*=\\s*"${BINDING}"`).test(entry)); + return block ? /^\s*id\s*=/m.test(block) : false; +} + +function expectedTitle(toml) { + const name = (toml.match(/^\s*name\s*=\s*"([^"]+)"/m) || [])[1] || 'worker'; + return `${name}-${BINDING.toLowerCase().replace(/_/g, '-')}`; +} + +function resolveId(title) { + const list = JSON.parse(wrangler('kv namespace list')); + const hit = + list.find((namespace) => namespace.title === title) || + list.find((namespace) => typeof namespace.title === 'string' && namespace.title.endsWith('attachments-kv')); + if (hit) { + console.log(`[ensure-kv] reusing existing namespace "${hit.title}" (${hit.id})`); + return hit.id; + } + + const out = wrangler(`kv namespace create "${title}"`); + const id = (out.match(/id\s*=\s*"([0-9a-fA-F]{32})"/) || [])[1]; + if (!id) throw new Error(`[ensure-kv] could not parse new namespace id from:\n${out}`); + console.log(`[ensure-kv] created namespace "${title}" (${id})`); + return id; +} + +function main() { + let toml = fs.readFileSync(CONFIG, 'utf8'); + if (bindingBlockHasId(toml)) { + console.log(`[ensure-kv] ${BINDING} already pinned in wrangler.kv.toml; nothing to do`); + return; + } + + const id = resolveId(expectedTitle(toml)); + toml = toml.replace( + new RegExp(`(\\[\\[kv_namespaces\\]\\]\\s*\\n\\s*binding\\s*=\\s*"${BINDING}")`), + `$1\nid = "${id}"` + ); + fs.writeFileSync(CONFIG, toml); + console.log('[ensure-kv] pinned id into wrangler.kv.toml for this build'); +} + +main();