| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219 |
- import { readFileSync, writeFileSync } from 'node:fs';
- import { spawnSync } from 'node:child_process';
- import { fileURLToPath } from 'node:url';
- import { dirname, resolve } from 'node:path';
- const __dirname = dirname(fileURLToPath(import.meta.url));
- const workerConfigPath = resolve(__dirname, '../worker/wrangler.jsonc');
- const bindingName = 'NOTICE_STORE';
- function readConfig() {
- return readFileSync(workerConfigPath, 'utf8');
- }
- function getWorkerName(source) {
- return source.match(/"name"\s*:\s*"([^"]+)"/)?.[1] || '';
- }
- function getConfiguredNamespaceId(source) {
- const escapedBinding = bindingName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
- const pattern = new RegExp(`\\{[\\s\\S]*?"binding"\\s*:\\s*"${escapedBinding}"[\\s\\S]*?"id"\\s*:\\s*"([^"]*)"[\\s\\S]*?\\}`);
- const id = source.match(pattern)?.[1]?.trim() || '';
- return id && !id.includes('<') ? id : '';
- }
- function runWrangler(args) {
- const result = spawnSync('npx', ['wrangler', ...args], {
- encoding: 'utf8',
- shell: process.platform === 'win32',
- });
- const output = `${result.stdout || ''}\n${result.stderr || ''}`.trim();
- return {
- status: result.status ?? 1,
- output,
- };
- }
- function parseNamespaceId(output) {
- const patterns = [
- /id\s*=\s*"([^"]+)"/i,
- /"id"\s*:\s*"([^"]+)"/i,
- /id[:=]\s*([0-9a-f]{32})/i,
- ];
- for (const pattern of patterns) {
- const match = output.match(pattern);
- if (match?.[1]) {
- return match[1];
- }
- }
- return '';
- }
- function parseNamespaceList(output, workerName) {
- const preferredTitles = new Set([
- bindingName,
- workerName ? `${workerName}-${bindingName}` : '',
- ].filter(Boolean));
- let namespaces;
- try {
- namespaces = JSON.parse(output);
- } catch {
- const start = output.indexOf('[');
- const end = output.lastIndexOf(']');
- if (start === -1 || end === -1 || end <= start) {
- return parseTextNamespaceList(output, preferredTitles);
- }
- try {
- namespaces = JSON.parse(output.slice(start, end + 1));
- } catch {
- return parseTextNamespaceList(output, preferredTitles);
- }
- }
- const items = Array.isArray(namespaces) ? namespaces : [];
- const exact = items.find((item) => preferredTitles.has(String(item.title || '')));
- if (exact?.id) {
- return exact.id;
- }
- const suffix = items.find((item) => String(item.title || '').endsWith(`-${bindingName}`));
- if (suffix?.id) {
- return suffix.id;
- }
- const textId = parseTextNamespaceList(output, preferredTitles);
- if (textId) {
- return textId;
- }
- return '';
- }
- function parseTextNamespaceList(output, preferredTitles) {
- const lines = String(output || '').split(/\r?\n/);
- const hexIdPattern = /\b[0-9a-f]{32}\b/i;
- for (const title of preferredTitles) {
- for (const line of lines) {
- if (!line.includes(title)) {
- continue;
- }
- const id = line.match(hexIdPattern)?.[0] || '';
- if (id) {
- return id;
- }
- }
- }
- for (const line of lines) {
- const cells = line.split('│').map((cell) => cell.trim()).filter(Boolean);
- const id = cells.find((cell) => hexIdPattern.test(cell))?.match(hexIdPattern)?.[0] || '';
- if (!id) {
- continue;
- }
- const title = cells.find((cell) => preferredTitles.has(cell) || cell.endsWith(`-${bindingName}`));
- if (title) {
- return id;
- }
- }
- return '';
- }
- function updateWranglerConfig(namespaceId) {
- const source = readConfig();
- const escapedBinding = bindingName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
- const bindingObjectPattern = new RegExp(`(\\{[\\s\\S]*?"binding"\\s*:\\s*"${escapedBinding}"[\\s\\S]*?"id"\\s*:\\s*")[^"]*("[\\s\\S]*?\\})`);
- if (bindingObjectPattern.test(source)) {
- writeFileSync(workerConfigPath, source.replace(bindingObjectPattern, `$1${namespaceId}$2`), 'utf8');
- return;
- }
- const namespaceBlock = ` "kv_namespaces": [\n {\n "binding": "${bindingName}",\n "id": "${namespaceId}"\n }\n ]`;
- const existingKvPattern = /"kv_namespaces"\s*:\s*\[/;
- if (existingKvPattern.test(source)) {
- const updated = source.replace(existingKvPattern, `"kv_namespaces": [\n {\n "binding": "${bindingName}",\n "id": "${namespaceId}"\n },`);
- writeFileSync(workerConfigPath, updated, 'utf8');
- return;
- }
- const insertAt = source.lastIndexOf('\n}');
- if (insertAt === -1) {
- throw new Error('Unable to locate closing brace in wrangler.jsonc');
- }
- const updated = `${source.slice(0, insertAt)},\n${namespaceBlock}${source.slice(insertAt)}`;
- writeFileSync(workerConfigPath, updated, 'utf8');
- }
- function printCredentialHelp(output) {
- if (output) {
- console.error(output);
- }
- console.error([
- 'Unable to create or find Cloudflare KV namespace NOTICE_STORE.',
- 'For CI, set CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID in the environment that runs analytics/worker deployment.',
- 'For local setup, run `npx wrangler login` or set CLOUDFLARE_API_TOKEN before `npm run setup:notice-kv`.',
- ].join('\n'));
- }
- const source = readConfig();
- const configuredId = getConfiguredNamespaceId(source);
- if (configuredId) {
- console.log(`NOTICE_STORE KV namespace already configured: ${configuredId}`);
- process.exit(0);
- }
- const envNamespaceId = String(process.env.NOTICE_STORE_ID || '').trim();
- if (envNamespaceId) {
- updateWranglerConfig(envNamespaceId);
- console.log(`NOTICE_STORE KV namespace configured from NOTICE_STORE_ID: ${envNamespaceId}`);
- process.exit(0);
- }
- const workerName = getWorkerName(source);
- const listResult = runWrangler(['kv', 'namespace', 'list']);
- if (listResult.status === 0) {
- const existingId = parseNamespaceList(listResult.output, workerName);
- if (existingId) {
- updateWranglerConfig(existingId);
- console.log(`NOTICE_STORE KV namespace reused: ${existingId}`);
- process.exit(0);
- }
- }
- const createResult = runWrangler(['kv', 'namespace', 'create', bindingName]);
- if (createResult.status !== 0) {
- if (/already exists/i.test(createResult.output)) {
- const retryListResult = runWrangler(['kv', 'namespace', 'list']);
- if (retryListResult.status === 0) {
- const existingId = parseNamespaceList(retryListResult.output, workerName);
- if (existingId) {
- updateWranglerConfig(existingId);
- console.log(`NOTICE_STORE KV namespace reused after create conflict: ${existingId}`);
- process.exit(0);
- }
- }
- }
- printCredentialHelp(createResult.output || listResult.output);
- process.exit(createResult.status || 1);
- }
- const namespaceId = parseNamespaceId(createResult.output);
- if (!namespaceId) {
- console.error(createResult.output);
- throw new Error('Unable to parse KV namespace id from Wrangler output.');
- }
- updateWranglerConfig(namespaceId);
- console.log(`NOTICE_STORE KV namespace created and configured: ${namespaceId}`);
|