diff --git a/scripts/deploy-config.js b/scripts/deploy-config.js index 05de3a0..7532eba 100644 --- a/scripts/deploy-config.js +++ b/scripts/deploy-config.js @@ -11,55 +11,71 @@ export function injectWorkerVersion(configText, version) { return updated; } -export function extractWorkerName(configText) { +/** + * 提取 worker 名称。 + * - envName=null(默认):返回顶层 `name`(生产环境名) + * - envName="X":在 `[env.X]` 块内查找 `name`,找不到则回落到顶层 + */ +export function extractWorkerName(configText, envName = null) { + if (envName) { + const lines = configText.split('\n'); + const envHeader = `[env.${envName}]`; + let inEnvBlock = false; + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed === envHeader) { + inEnvBlock = true; + continue; + } + if (!inEnvBlock) continue; + if (/^\[/.test(trimmed)) break; // 进入下一个 section,env 块结束 + const nameMatch = trimmed.match(/^name\s*=\s*"([^"]+)"/); + if (nameMatch) return nameMatch[1]; + } + // 未在 env 块内找到 name,回落到顶层 + } const match = configText.match(/^name\s*=\s*"([^"]+)"/m); return match ? match[1] : null; } -export function injectKvNamespaceId(configText, id) { - // Match the first [[kv_namespaces]] block that contains binding = "SECRETS_KV" - // and is NOT inside an [env.*] section. +/** + * 把 KV namespace id 注入到指定块内(首个 binding = "SECRETS_KV" 的 [[kv_namespaces]] 数组项)。 + * - envName=null(默认):目标为顶层 `[[kv_namespaces]]` + * - envName="X":目标为 `[[env.X.kv_namespaces]]` + * + * 已存在 id 时覆盖;不存在时插入到 binding 行后。其他块(含其它 env 的 KV 块)不会被改动。 + */ +export function injectKvNamespaceId(configText, id, envName = null) { const lines = configText.split('\n'); - let inEnvSection = false; - let kvBlockStart = -1; + const targetHeader = envName + ? `[[env.${envName}.kv_namespaces]]` + : '[[kv_namespaces]]'; + + let inTargetBlock = false; let bindingLine = -1; let existingIdLine = -1; for (let i = 0; i < lines.length; i++) { const trimmed = lines[i].trim(); - // Track whether we're inside an [env.*] section - if (/^\[env\./.test(trimmed)) { - inEnvSection = true; - continue; - } - if (/^\[(?!env\.)/.test(trimmed) || /^\[\[(?!env\.)/.test(trimmed)) { - inEnvSection = false; - } - - if (inEnvSection) continue; - - if (trimmed === '[[kv_namespaces]]') { - kvBlockStart = i; + // 任何 section header 都视为当前块结束 + if (/^\[/.test(trimmed)) { + if (inTargetBlock && bindingLine >= 0) { + break; // 已经在目标块里找到 binding,停止扫描 + } + inTargetBlock = trimmed === targetHeader; bindingLine = -1; existingIdLine = -1; continue; } - if (kvBlockStart >= 0) { - // End of block: next section header or empty line after content - if (/^\[/.test(trimmed) || /^\[\[/.test(trimmed)) { - if (bindingLine >= 0) break; - kvBlockStart = -1; - continue; - } + if (!inTargetBlock) continue; - if (/^binding\s*=\s*"SECRETS_KV"/.test(trimmed)) { - bindingLine = i; - } - if (/^id\s*=\s*"/.test(trimmed)) { - existingIdLine = i; - } + if (/^binding\s*=\s*"SECRETS_KV"/.test(trimmed)) { + bindingLine = i; + } + if (/^id\s*=\s*"/.test(trimmed)) { + existingIdLine = i; } } diff --git a/scripts/deploy.js b/scripts/deploy.js index fd6d358..0a7ac1b 100644 --- a/scripts/deploy.js +++ b/scripts/deploy.js @@ -31,7 +31,8 @@ const versionStrategy = args.includes('--git') ? '--git' : ''; const envIndex = args.indexOf('--env'); -const envArg = envIndex !== -1 && args[envIndex + 1] ? `--env ${args[envIndex + 1]}` : ''; +const envName = envIndex !== -1 && args[envIndex + 1] ? args[envIndex + 1] : null; +const envArg = envName ? `--env ${envName}` : ''; console.log(''); console.log('🚀 ========================================'); @@ -56,9 +57,10 @@ try { // Step 2.5: 自动检测并绑定已有 KV namespace,防止重复创建 console.log('🔍 Step 2.5: 检测已有 KV namespace...'); - const existingKv = findExistingKvId(extractWorkerName(modifiedConfig)); + const workerName = extractWorkerName(modifiedConfig, envName); + const existingKv = findExistingKvId(workerName, envName); if (existingKv) { - modifiedConfig = injectKvNamespaceId(modifiedConfig, existingKv.id); + modifiedConfig = injectKvNamespaceId(modifiedConfig, existingKv.id, envName); console.log(` ✅ 复用已有 KV: ${existingKv.title} (${existingKv.id})`); } else { console.log(' ℹ️ 未检测到已有 KV,将由 Wrangler 自动创建'); @@ -112,27 +114,64 @@ function generateVersion(versionStrategyArg) { return execSync(versionCmd, { encoding: 'utf-8' }).trim().split('\n')[0]; } -function findExistingKvId(workerName) { +function findExistingKvId(workerName, envName = null) { + if (!workerName) return null; + + let namespaces; try { const output = execSync('npx wrangler kv namespace list', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'], }); - const namespaces = JSON.parse(output); - if (!namespaces.length) return null; - - // 按优先级匹配,覆盖 wrangler auto-provision、Dashboard 创建、老版本等命名格式 - const match = - namespaces.find(ns => ns.title === `${workerName}-SECRETS_KV`) || - namespaces.find(ns => ns.title === `${workerName}-secrets-kv`) || - namespaces.find(ns => ns.title === 'SECRETS_KV') || - namespaces.find(ns => ns.title === workerName) || - namespaces.find(ns => ns.title.includes('SECRETS_KV')) || - namespaces.find(ns => ns.title.includes('secrets-kv')) || - (namespaces.length === 1 ? namespaces[0] : null); - - return match ? { id: match.id, title: match.title } : null; + namespaces = JSON.parse(output); } catch { return null; } + if (!namespaces.length) return null; + + // 短名映射:开发环境常缩写为 dev / prod + const ENV_ALIASES = { development: 'dev', production: 'prod' }; + const envAlias = envName ? (ENV_ALIASES[envName] || envName) : null; + + // 推断 base 名(去除可能的 env 后缀):worker "2fa-dev" + envAlias "dev" → base "2fa" + const stripSuffix = (name, suffix) => + suffix && name.endsWith(`-${suffix}`) ? name.slice(0, -(suffix.length + 1)) : name; + const baseName = envAlias ? stripSuffix(workerName, envAlias) : workerName; + + // 候选 title 列表,越靠前越优先 + const candidates = []; + if (envName) { + candidates.push( + `${workerName}-secrets-kv`, // 2fa-dev-secrets-kv + `${workerName}-SECRETS_KV`, + `${baseName}-secrets-kv-${envAlias}`, // 2fa-secrets-kv-dev ← 当前命名 + `${baseName}-secrets-kv-${envName}`, // 2fa-secrets-kv-development + `${envAlias}-${baseName}-SECRETS_KV`, + `${envName}-${baseName}-SECRETS_KV`, + `${envName}-SECRETS_KV`, // development-SECRETS_KV(旧命名) + ); + } else { + candidates.push( + `${workerName}-secrets-kv`, // 2fa-secrets-kv ← 当前命名 + `${workerName}-SECRETS_KV`, + 'SECRETS_KV', + workerName, + ); + } + + for (const title of candidates) { + const match = namespaces.find(ns => ns.title === title); + if (match) return { id: match.id, title: match.title }; + } + + // env 部署只走精确匹配,避免误把生产 KV 命中给 dev + if (envName) return null; + + // 顶层部署的 fuzzy 兜底(保持原有兼容性) + const fuzzy = + namespaces.find(ns => ns.title.includes('SECRETS_KV')) || + namespaces.find(ns => ns.title.includes('secrets-kv')) || + (namespaces.length === 1 ? namespaces[0] : null); + + return fuzzy ? { id: fuzzy.id, title: fuzzy.title } : null; } diff --git a/scripts/merge-wrangler-config.js b/scripts/merge-wrangler-config.js index b80636a..0f54155 100644 --- a/scripts/merge-wrangler-config.js +++ b/scripts/merge-wrangler-config.js @@ -31,6 +31,9 @@ merged = mergeVarsSection(merged, local, '[vars]', ['SW_VERSION']); // Preserve environment-specific names and vars. merged = preserveSectionLineAssignment(merged, local, '[env.development]', 'name'); +// 保留本地维护者在 dev 块内显式声明的 routes(典型用法:routes = [] 防止继承顶层自定义域名, +// 避免 deploy:dev 抢占生产域名)。注意:当前实现只支持单行赋值(含 routes = [])。 +merged = preserveSectionLineAssignment(merged, local, '[env.development]', 'routes'); merged = mergeVarsSection(merged, local, '[env.development.vars]', ['SW_VERSION']); // Merge KV bindings, preserving existing IDs while adopting upstream structure. diff --git a/tests/scripts/deploy-config.test.js b/tests/scripts/deploy-config.test.js index 077ee84..c07c9f6 100644 --- a/tests/scripts/deploy-config.test.js +++ b/tests/scripts/deploy-config.test.js @@ -36,6 +36,36 @@ describe('extractWorkerName', () => { it('returns null when name is missing', () => { expect(extractWorkerName('main = "src/worker.js"')).toBeNull(); }); + + it('extracts env-specific name when envName is provided', () => { + const config = `name = "2fa" + +[env.development] +name = "2fa-dev" + +[env.development.vars] +SW_VERSION = "v1" +`; + expect(extractWorkerName(config, 'development')).toBe('2fa-dev'); + }); + + it('falls back to top-level name when env block has no name', () => { + const config = `name = "2fa" + +[env.staging] + +[env.staging.vars] +SW_VERSION = "v1" +`; + expect(extractWorkerName(config, 'staging')).toBe('2fa'); + }); + + it('returns top-level name when env block does not exist', () => { + const config = `name = "2fa" +main = "src/worker.js" +`; + expect(extractWorkerName(config, 'production')).toBe('2fa'); + }); }); describe('injectKvNamespaceId', () => { @@ -81,4 +111,36 @@ binding = "SECRETS_KV" const noKvConfig = 'name = "2fa"\n[vars]\nSW_VERSION = "v1"\n'; expect(injectKvNamespaceId(noKvConfig, 'abc123')).toBe(noKvConfig); }); + + it('injects id into env.development block when envName is provided', () => { + const result = injectKvNamespaceId(baseConfig, 'dev-id', 'development'); + const devBlock = result.split('[[env.development.kv_namespaces]]')[1]; + expect(devBlock).toContain('id = "dev-id"'); + }); + + it('does not modify top-level kv_namespaces when envName is provided', () => { + const result = injectKvNamespaceId(baseConfig, 'dev-id', 'development'); + const topBlock = result.split('[[kv_namespaces]]')[1].split('[')[0]; + expect(topBlock).not.toContain('id = "dev-id"'); + }); + + it('replaces existing env id when envName is provided', () => { + const configWithDevId = baseConfig.replace( + '[[env.development.kv_namespaces]]\nbinding = "SECRETS_KV"', + '[[env.development.kv_namespaces]]\nbinding = "SECRETS_KV"\nid = "old-dev-id"' + ); + const result = injectKvNamespaceId(configWithDevId, 'new-dev-id', 'development'); + expect(result).toContain('id = "new-dev-id"'); + expect(result).not.toContain('old-dev-id'); + }); + + it('does not match a different env block when envName is provided', () => { + const configWithStaging = baseConfig + ` +[[env.staging.kv_namespaces]] +binding = "SECRETS_KV" +`; + const result = injectKvNamespaceId(configWithStaging, 'dev-id', 'development'); + const stagingBlock = result.split('[[env.staging.kv_namespaces]]')[1]; + expect(stagingBlock).not.toContain('id = "dev-id"'); + }); });