**背景**:这些改动原属本工作区另外几个会话(T01/T02 等),**那些会话已被用户删除** ⇒
工作树里的成果处于"无主"状态,一次错误 checkout / 覆盖即**永久丢失** ⇒ 代入库保全。
口径遵循本项目**先例**(`39a1f2e` / `b617cdb`:**别人的活,代入库并在提交信息里注明**)。
**内容**:档案 101「能力管理」改名 + 页内 tab 分页|档案 102 语言切换搬入「用户设置」|
`07-实例UI分区登记表.md`|`scripts/find-ui*.mjs`(UI 元素定位工具)|`poc/portal-entry/`(0.5.3)|
`src/web/locale-pref.ts` + `home-files.ts`(语言偏好持久化)|`test/locale-pref.test.mjs`|
`package.json`|`BRIEF.md` / `INDEX.md` / `docs-manifest.json` / `03-路线图与待办.md` / 档案 100 增量。
**已做最小健全性检查**(⚠️ **未跑完整构建 / 单测** —— 那是原会话的验收职责,本次只求"不丢"):
- JSON 合法:`package.json` / `poc/business-plugins/package.json` / `docs-manifest.json` ✓
- 4 个 TS 文件 `{}`/`()` 配平 ✓;新增文件均非空 ✓
- 规模:13 文件改动 +340/−210,新增 12 条
**未 push**(按 §4 提交边界:用户说"提交",未说"推送")。
89 lines
5.7 KiB
JavaScript
89 lines
5.7 KiB
JavaScript
#!/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/<name>-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()', /hostCtx\.locale\.getLocale\(\)/.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)
|