Files
dsh_shenxian/scripts/verify-portal-entry.mjs
T
admin 3efd68517f chore: 并入已删除会话的在途成果(防丢失;原会话已删,未做功能验收)
**背景**:这些改动原属本工作区另外几个会话(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 提交边界:用户说"提交",未说"推送")。
2026-09-15 21:17:12 +08:00

89 lines
5.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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)