收紧宿主壳结构门禁

根级原生壳门禁按完整路径反查三端桥接层文档

移动端和桌面端壳目录拒绝未登记子目录或生产入口

桌面端单端配置检查改为精确 Rust 模块清单

更新宿主壳架构文档和共享记忆中的验收口径
This commit is contained in:
2026-06-19 07:54:52 +08:00
parent de2798d415
commit e27834930f
6 changed files with 75 additions and 58 deletions
+6 -6
View File
@@ -1466,13 +1466,13 @@ const sharedTauriCommand = extractTsStringConst(
);
const allowedTauriCommands = [sharedTauriCommand];
const requiredRustHostModules = [
'app.rs',
'host_bridge/capabilities.rs',
'host_bridge/dispatch.rs',
'host_bridge/files.rs',
'host_bridge/mod.rs',
'host_bridge/protocol.rs',
'host_bridge/share.rs',
'app.rs',
'main.rs',
'shell/deep_link.rs',
'shell/events.rs',
@@ -1666,11 +1666,11 @@ assertSameList(
['host_bridge_request'],
'shared Tauri HostBridge command',
);
for (const moduleName of requiredRustHostModules) {
if (!collectRustSourceRelativePaths(rustHostSourceFiles).includes(moduleName)) {
throw new Error(`desktop shell Rust bridge module missing ${moduleName}`);
}
}
assertSameList(
collectRustSourceRelativePaths(rustHostSourceFiles),
requiredRustHostModules,
'desktop shell Rust bridge modules',
);
assertSameList(
extractNativeAppTauriInvokeCommands(nativeAppHostBridgeSource),
['HOST_BRIDGE_TAURI_COMMAND'],
@@ -2524,7 +2524,7 @@
## 2026-06-19 三端宿主桥接层结构文档反查
- 背景:三端桥接层已经拆出移动 `scanner.ts`、桌面 `menu.rs``window_state.rs` 等职责文件,但如果只更新代码和目录门禁,`宿主壳能力统一协议``ExpoReactNative与Tauri宿主壳方案` 可能继续保留旧清单,后续开发者按文档扩展时仍会把能力放回错误 owner。
- 决策:`scripts/check-native-shells.mjs` 的三端桥接层目录清单同时作为文档反查来源。根级门禁会确认两份前端架构文档都显式列出微信桥接层、微信 shell、微信页面包装层、移动桥接层、移动 shell、桌面入口、桌面桥接层和桌面 shell 的当前生产文件新增、删除或改名这些职责文件时,必须同时更新脚本清单、两份架构文档和相关实现,不允许只改一端。
- 决策:`scripts/check-native-shells.mjs` 的三端桥接层目录清单同时作为文档反查来源。根级门禁会确认两份前端架构文档都按完整相对路径列出微信桥接层、微信 shell、微信页面包装层、移动桥接层、移动 shell、桌面入口、桌面桥接层和桌面 shell 的当前生产文件,并拒绝这些目录出现未登记子目录或生产入口;`apps/desktop-shell/scripts/check-config.mjs` 也必须用精确 Rust 模块清单拦截桌面壳单端结构漂移。新增、删除或改名这些职责文件时,必须同时更新脚本清单、两份架构文档、单端门禁和相关实现,不允许只改一端。
- 影响范围:`scripts/check-native-shells.mjs``docs/【前端架构】宿主壳能力统一协议-2026-06-17.md``docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md`、三端宿主壳源码布局。
- 验证方式:`npm run check:native-shells``npm run check:encoding``git diff --check`
@@ -216,7 +216,7 @@ npm run build
npm run check:native-shells
```
该命令会覆盖 H5 HostBridge 关键测试、微信 / Expo / Tauri 三端桥接层文件结构门禁、Expo 壳 typecheck / test / config smoke / Metro export smoke、Tauri 壳 typecheck / cargo test、桌面壳 release `--no-bundle` 构建烟测,以及可分发壳与 H5 HostBridge 真实调用链的临时替身词扫描,确认 Expo managed config、移动端 iOS / Android production bundle、打包 H5 资产、Tauri release 入口和 H5 HostBridge 真实调用链没有漂移;扫描范围包含微信小程序壳生产 `.js`、共享 HostBridge 契约、H5 native transport,并自动覆盖已接入真实宿主能力 facade 的 H5 生产调用链文件。壳源码和配置继续严格禁止 mock / fake / placeholder / stub / TODO / FIXME / 占位 / 模拟 / 伪造;H5 业务调用链允许正常表单 `placeholder` 属性和业务占位图文案,但仍禁止 mock / fake / stub / TODO / FIXME / 模拟 / 伪造等替身痕迹。
该命令会覆盖 H5 HostBridge 关键测试、微信 / Expo / Tauri 三端桥接层文件结构门禁、完整相对路径文档反查、Expo 壳 typecheck / test / config smoke / Metro export smoke、Tauri 壳 typecheck / cargo test、桌面壳 release `--no-bundle` 构建烟测,以及可分发壳与 H5 HostBridge 真实调用链的临时替身词扫描,确认 Expo managed config、移动端 iOS / Android production bundle、打包 H5 资产、Tauri release 入口和 H5 HostBridge 真实调用链没有漂移;扫描范围包含微信小程序壳生产 `.js`、共享 HostBridge 契约、H5 native transport,并自动覆盖已接入真实宿主能力 facade 的 H5 生产调用链文件。壳源码和配置继续严格禁止 mock / fake / placeholder / stub / TODO / FIXME / 占位 / 模拟 / 伪造;H5 业务调用链允许正常表单 `placeholder` 属性和业务占位图文案,但仍禁止 mock / fake / stub / TODO / FIXME / 模拟 / 伪造等替身痕迹。
该命令会反查微信小程序 `WECHAT_HOST_CAPABILITIES` 与共享 `HOST_BRIDGE_WECHAT_MINI_PROGRAM_CAPABILITIES` 一致;小程序生产代码继续保留 CommonJS 运行时镜像,不直接 import TypeScript shared 包。
该命令同时会运行微信小程序 `miniprogram/host-bridge/``miniprogram/shell/``pages/web-view` 样式和 `scripts/miniprogram-web-view-auth.test.ts` 的壳层测试,保证微信桥接层拆分后的支付、订阅消息、九宫切图、分享目标和 WebView 登录 / 分享入口行为与 Expo、Tauri 壳一起验收。
该命令还会反查微信小程序 `app.json.pages``host-bridge/protocol.js` 页面 URL、H5 小程序页面常量、H5 订阅授权页面常量、WebView 分享入口、分享目标消息类型、`WEB_VIEW_SOURCE_QUERY`、微信请求头运行时标记、H5 runtime parser、H5 路由保留字段和 H5 / API base URL 格式,避免页面路由、来源标记、宿主上下文 query 或域名配置在微信壳、H5 HostBridge 与运行时配置之间分叉。生产 / 开发 H5 与 API 域名都必须显式配置为纯 HTTPS domain,运行时开发域名回退生产域名只作为异常兜底。
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+63 -50
View File
@@ -188,15 +188,15 @@ const expectedDesktopShellRustFiles = [
const documentedShellLayerGroups = [
{
label: 'wechat host bridge files',
files: expectedWechatHostBridgeFiles.filter(
(fileName) => !fileName.includes('.test.'),
),
files: expectedWechatHostBridgeFiles
.filter((fileName) => !fileName.includes('.test.'))
.map((fileName) => `miniprogram/host-bridge/${fileName}`),
},
{
label: 'wechat shell files',
files: expectedWechatShellFiles.filter(
(fileName) => !fileName.includes('.test.'),
),
files: expectedWechatShellFiles
.filter((fileName) => !fileName.includes('.test.'))
.map((fileName) => `miniprogram/shell/${fileName}`),
},
{
label: 'wechat page wrapper files',
@@ -204,32 +204,39 @@ const documentedShellLayerGroups = [
([route, files]) =>
files
.filter((fileName) => !fileName.includes('.test.'))
.map((fileName) => `${route}/${fileName}`),
.map((fileName) => `miniprogram/pages/${route}/${fileName}`),
),
},
{
label: 'mobile host bridge files',
files: expectedMobileHostBridgeFiles.filter(
(fileName) => !fileName.includes('.test.'),
),
files: expectedMobileHostBridgeFiles
.filter((fileName) => !fileName.includes('.test.'))
.map((fileName) => `apps/mobile-shell/src/host-bridge/${fileName}`),
},
{
label: 'mobile shell files',
files: expectedMobileShellFiles.filter(
(fileName) => !fileName.includes('.test.'),
),
files: expectedMobileShellFiles
.filter((fileName) => !fileName.includes('.test.'))
.map((fileName) => `apps/mobile-shell/src/shell/${fileName}`),
},
{
label: 'desktop entrypoint files',
files: ['app.rs', 'main.rs'],
files: [
'apps/desktop-shell/src-tauri/src/app.rs',
'apps/desktop-shell/src-tauri/src/main.rs',
],
},
{
label: 'desktop host bridge files',
files: expectedDesktopHostBridgeRustFiles,
files: expectedDesktopHostBridgeRustFiles.map(
(fileName) => `apps/desktop-shell/src-tauri/src/host_bridge/${fileName}`,
),
},
{
label: 'desktop shell files',
files: expectedDesktopShellRustFiles,
files: expectedDesktopShellRustFiles.map(
(fileName) => `apps/desktop-shell/src-tauri/src/shell/${fileName}`,
),
},
];
const capabilityListMarkers = {
@@ -601,6 +608,25 @@ function assertSameList(actual, expected, label) {
}
}
function readDirectoryEntryList(directory, label) {
const entries = fs
.readdirSync(directory, { withFileTypes: true })
.map((entry) => {
if (entry.isFile()) {
return `file:${entry.name}`;
}
if (entry.isDirectory()) {
return `dir:${entry.name}`;
}
throw new Error(`${label} must not contain special entries: ${entry.name}`);
})
.sort();
if (entries.length === 0) {
throw new Error(`${label} must not be empty`);
}
return entries;
}
function readDirectoryFileList(directory, label) {
const files = [];
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
@@ -1155,26 +1181,19 @@ function assertHostBridgeLayerLayout() {
}
}
const mobileHostBridgeFiles = fs
.readdirSync('apps/mobile-shell/src/host-bridge', { withFileTypes: true })
.filter((entry) => entry.isFile())
.map((entry) => entry.name)
.sort();
assertSameList(
mobileHostBridgeFiles,
readDirectoryFileList(
'apps/mobile-shell/src/host-bridge',
'mobile host bridge files',
),
expectedMobileHostBridgeFiles,
'mobile host bridge files',
);
const mobileShellLayerFiles = fs
.readdirSync('apps/mobile-shell/src/shell', { withFileTypes: true })
.filter((entry) => entry.isFile())
.map((entry) => entry.name)
.sort();
assertSameList(
mobileShellLayerFiles,
readDirectoryFileList('apps/mobile-shell/src/shell', 'mobile shell files'),
expectedMobileShellFiles,
'mobile shell bridge files',
'mobile shell files',
);
const mobileAppSource = fs.readFileSync('apps/mobile-shell/App.tsx', 'utf8');
@@ -1185,15 +1204,13 @@ function assertHostBridgeLayerLayout() {
throw new Error('mobile shell App.tsx must not import HostBridge directly');
}
const desktopEntrypointRustFiles = fs
.readdirSync('apps/desktop-shell/src-tauri/src', { withFileTypes: true })
.filter((entry) => entry.isFile() && entry.name.endsWith('.rs'))
.map((entry) => entry.name)
.sort();
assertSameList(
desktopEntrypointRustFiles,
['app.rs', 'main.rs'],
'desktop shell entrypoint Rust files',
readDirectoryEntryList(
'apps/desktop-shell/src-tauri/src',
'desktop shell Rust root entries',
),
['dir:host_bridge', 'dir:shell', 'file:app.rs', 'file:main.rs'],
'desktop shell Rust root entries',
);
const desktopMainSource = fs.readFileSync(
@@ -1220,26 +1237,22 @@ function assertHostBridgeLayerLayout() {
throw new Error('desktop shell app.rs must own Tauri app setup');
}
const desktopHostBridgeRustFiles = fs
.readdirSync('apps/desktop-shell/src-tauri/src/host_bridge', { withFileTypes: true })
.filter((entry) => entry.isFile() && entry.name.endsWith('.rs'))
.map((entry) => entry.name)
.sort();
assertSameList(
desktopHostBridgeRustFiles,
readDirectoryFileList(
'apps/desktop-shell/src-tauri/src/host_bridge',
'desktop host bridge Rust files',
),
expectedDesktopHostBridgeRustFiles,
'desktop host bridge Rust files',
);
const desktopShellRustFiles = fs
.readdirSync('apps/desktop-shell/src-tauri/src/shell', { withFileTypes: true })
.filter((entry) => entry.isFile() && entry.name.endsWith('.rs'))
.map((entry) => entry.name)
.sort();
assertSameList(
desktopShellRustFiles,
readDirectoryFileList(
'apps/desktop-shell/src-tauri/src/shell',
'desktop shell Rust files',
),
expectedDesktopShellRustFiles,
'desktop shell Rust bridge files',
'desktop shell Rust files',
);
}