/** * targets.mjs — 部署目標的唯一讀取點(deploy.mjs 與 verify-live.mjs 共用)。 * * 存在的理由:宣告值(deploy.targets.json)只准被解讀一次。 * 「部署時印在終端機的值」「寫進產物的值」「事後驗線上的值」若各自去讀、各自算, * 三者就會漂移——2026-08-08 那場事故的形狀正是「印的是 A、推的是 B」。 * 這支把「一個目標展開成期望的產物長相」定死成一個函式,三邊共用同一個答案。 */ import { readFileSync } from 'node:fs'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; export const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..'); export function loadTargets() { const raw = JSON.parse(readFileSync(join(ROOT, 'deploy.targets.json'), 'utf8')); const profiles = raw._profiles; if (!profiles) throw new Error('deploy.targets.json 缺 _profiles(profile → views/home 對照)'); const names = Object.keys(raw).filter((k) => !k.startsWith('_')); return { raw, profiles, names }; } export function resolveTarget(name) { const { raw, profiles, names } = loadTargets(); const t = raw[name]; if (!t) { const err = new Error(`未知的部署目標:"${name}"。可用:${names.join(' / ')}`); err.usage = true; throw err; } const p = profiles[t.profile]; if (!p) { throw new Error( `目標 ${name} 的 profile="${t.profile}" 在 _profiles 裡沒有定義(可用:${Object.keys(profiles).join(' / ')})。` + '\n宣告了一個沒人知道怎麼落地的 profile ⇒ 拒絕部署,不要猜。', ); } if (!t.apiBase) throw new Error(`目標 ${name} 沒有 apiBase——空值會讓前端安靜地連不上,拒絕部署。`); if (!t.accountId) throw new Error(`目標 ${name} 沒有 accountId——不指定帳號可能部到別人的站上,拒絕部署。`); if (!Array.isArray(t.verifyUrls) || t.verifyUrls.length === 0) { throw new Error(`目標 ${name} 沒有 verifyUrls——沒有對外網址就無法驗「站上跑的=宣告的」,拒絕部署。`); } return { name, ...t, views: p.views, home: p.home }; } /** 這個目標「應該長成什麼樣」——產物閘與線上閘都比對這一份。 */ export function expected(t) { return { configJs: configJsFor(t), apiBase: t.apiBase, viewsLine: ` var VIEWS = ${JSON.stringify(t.views)};`, homeLine: ` var HOME = ${JSON.stringify(t.home)};`, }; } export function configJsFor(t) { return ( '// 由 console-ui/scripts/deploy.mjs 於部署時依 deploy.targets.json 產生——請勿手改,也不進 git。\n' + `// 目標:${t.name}(${t.description})\n` + `window.ARCRUN_CONFIG = { apiBase: ${JSON.stringify(t.apiBase)} };\n` ); } /** 從 config.js 的文字裡取出 apiBase(線上/產物共用同一個解析法)。 */ export function parseApiBase(text) { const m = text.match(/apiBase\s*:\s*"([^"]*)"/); return m ? m[1] : null; }