#!/usr/bin/env node /** * verify-portal-entry.mjs —— `@dsh-local/portal-entry` 客户端面的**结构防线**(档案 102) * * 为什么需要(三条都真踩过): * ① **v0.5.1 事故**:打出来的 tgz **漏了 `lib/index.js`(host 半边)** ⇒ 每个实例 * `ERR_MODULE_NOT_FOUND` 起不来。⇒ 本脚本把"两个半边文件都在"钉成断言。 * ② **v0.4.1 事故 / R3**:`exports.default = apply` 会让 loader 选中裸函数当插件体 ⇒ * `ctx.slots` 抛「cannot get property slots without inject」⇒ 分区**永不渲染**(静默)。 * ③ **2026-09-15 变更**:「界面语言」切换搬进本区(原在 business-plugins 的「偏好设置」分区, * 该分区已撤)⇒ 需要钉住"语言行确实在、且真的调官方 locale API"。 * * ⚠️ **label 存的是转义 unicode**(`"\u7528\u6237\u8bbe\u7f6e"` = 用户设置)—— 这是本 bundle 的 * 既有写法,本脚本按**转义形态**断言(按 UTF-8 字面量 grep 会永远搜不到,2026-09-15 我为此 * 绕了很久)。 * * 用法:node scripts/verify-portal-entry.mjs 退出码 0=全绿 / 1=有失败 */ import { readFileSync, existsSync } from 'node:fs' import { fileURLToPath } from 'node:url' import { dirname, join } from 'node:path' const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..') const P = 'poc/portal-entry' const read = (p) => readFileSync(join(ROOT, p), 'utf8') let failed = 0 const ok = (name, cond, extra = '') => { console.log((cond ? ' ✓ ' : ' ✗ ') + name + (extra ? ' ' + extra : '')) if (!cond) failed++ } // ── ① 源码必须在仓里(2026-09-15 之前不在 ⇒ 只能从 tgz 反推源码,排查成本极高)── ok('源码在仓内(poc/portal-entry/lib/client.js)', existsSync(join(ROOT, P, 'lib', 'client.js'))) ok('host 半边也在(lib/index.js —— v0.5.1 事故的防线)', existsSync(join(ROOT, P, 'lib', 'index.js'))) ok('package.json 在', existsSync(join(ROOT, P, 'package.json'))) ok('cordis.patch.yml 在(bundle patch 必需)', existsSync(join(ROOT, P, 'cordis.patch.yml'))) const src = read(P + '/lib/client.js') const pkg = JSON.parse(read(P + '/package.json')) // ── ② 版本与元数据 ── // ⚠️ 0.5.4 事故:目录里上一版的 `.tgz` 被一并打进产物(package/-0.5.3.tgz)—— 与 // business-plugins 当年 0.2.5 完全同一个坑。防线:`.npmignore` 必须在且排除 `*.tgz`。 ok('.npmignore 存在', existsSync(join(ROOT, P, '.npmignore'))) ok('.npmignore 排除 *.tgz(防"旧产物打进新包"复发)', existsSync(join(ROOT, P, '.npmignore')) && read(P + '/.npmignore').includes('*.tgz')) ok('version ≥ 0.5.3(语言行引入版)', /^0\.5\.[3-9]$/.test(pkg.version) || /^0\.[6-9]/.test(pkg.version), '-> ' + pkg.version) ok('client bundle 元数据在(dsh.client.platform = web)', pkg.dsh && pkg.dsh.client && pkg.dsh.client.platform === 'web') // ── ③ 分区:id user-settings / order 103 / label = 用户设置(转义形态)── ok('注册 settings.section', src.includes('settings.section')) ok('section id = user-settings', src.includes('id: "user-settings"')) ok('order = 103', /order:\s*103/.test(src)) ok('label = 用户设置(转义 unicode \\u7528\\u6237\\u8bbe\\u7f6e)', src.includes('\\u7528\\u6237\\u8bbe\\u7f6e')) // ── ④ 语言切换(2026-09-15 搬入)── ok('inject 里要了 locale 服务', /const inject = \["slots", "locale"\]/.test(src)) ok('读当前语言走官方 ctx.locale.getLocale()', /\.getLocale\(\)/.test(src)) // 2026-09-20:dsh 0.1.5-rc.1 的 `getLocale()` 返回**快照对象** {active,locales,revision} // (0.1.2-rc.1 时代返回 id 字符串)⇒ 必须取 `.active`,否则下拉永远显示第一项、切换看着"没反应"。 ok('getLocale() 返回快照对象时取 .active(0.1.5-rc.1)', /typeof v === "object"\) v = v\.active/.test(src)) ok('切语言走官方 ctx.locale.setLocale()', /hostCtx\.locale\.setLocale\(id\)/.test(src)) ok('注册了自己的双语词典(ctx.locale.register)', /locale\.register\(PE_NS, PE_DICT\)/.test(src)) ok('语言项 en / zh 都在', src.includes('{ id: "en"') && src.includes('{ id: "zh"')) ok('渲染了 select(界面语言)', src.includes('"aria-label": tPe("lang")')) ok('切换后强制重渲染(setTick)', /setTick\(function \(n\)/.test(src)) { // 语言行必须真在 rows 里(否则写了不渲染 = 用户看不到)。 // ⚠️ 断言方向:代码是 `rows.push(jsxRuntime.jsxs("div", { key: "lang", …` ⇒ // `rows.push` 在 `key: "lang"` **之前**(首跑按"之后"判,误红一次)。 const at = src.indexOf('key: "lang"') ok('语言行确实 push 进 rows', at > 0 && src.slice(Math.max(0, at - 400), at).includes('rows.push'), '-> offset ' + at) } // ── ④b 语言偏好持久化(v0.5.4):调平台把选择写进 settings.yaml ── // 官方只对 loopback 持久化 ⇒ 平台侧补一刀;断言"确实调了",防止将来被误删成"切了记不住"。 ok('切换时调平台持久化(POST /api/me/locale)', /\/api\/me\/locale/.test(src)) ok('持久化请求带 locale 字段', /JSON\.stringify\(\{ locale: id \}\)/.test(src)) ok('持久化用 credentials: "include"(跨子域带会话)', /credentials: "include"/.test(src)) ok('持久化失败不影响本次切换(catch 兜底)', /\.catch\(function \(e\) \{ console\.warn\("\[portal-entry\] 语言偏好持久化失败/.test(src)) ok('颜色硬编码(本区在 DARK 主题,v0.4.3 结论:不可用 var(--dsw-*))', src.includes('#f9fafb') && src.includes('#43464d') && !/var\(--dsw-alias-label-primary/.test(src)) // ── ⑤ R3:不得出现 exports.default 赋值(v0.4.1 事故的根因)── ok('未出现 exports.default 赋值(R3)', /(?:^|\n)\s*(?:module\.)?exports\.default\s*=/.test(src) === false) ok('导出 apply + inject', /exports\.apply = apply/.test(src) && /exports\.inject = inject/.test(src)) console.log(failed === 0 ? '\n全部通过(portal-entry:结构 + 语言行 + R3 防线)' : `\n${failed} 项失败`) process.exit(failed === 0 ? 0 : 1)