禁止壳生产代码保留临时替身
移动壳配置检查扫描生产源码脚手架词 桌面壳配置检查扫描生产源码脚手架词 保留测试文件使用 mock 的边界 更新原生壳方案和团队决策记录
This commit is contained in:
@@ -21,6 +21,26 @@ const sharedContractPath = new URL(
|
||||
const sharedContractSource = fs.readFileSync(sharedContractPath, 'utf8');
|
||||
const mainPath = new URL('../src-tauri/src/main.rs', import.meta.url);
|
||||
const main = fs.readFileSync(mainPath, 'utf8');
|
||||
const productionSourceRoots = [
|
||||
new URL('../package.json', import.meta.url),
|
||||
new URL('../src-tauri/Cargo.toml', import.meta.url),
|
||||
new URL('../src-tauri/build.rs', import.meta.url),
|
||||
new URL('../src-tauri/capabilities/main.json', import.meta.url),
|
||||
new URL('../src-tauri/src/', import.meta.url),
|
||||
new URL('../src-tauri/tauri.conf.json', import.meta.url),
|
||||
];
|
||||
const productionFileExtensions = new Set(['.json', '.mjs', '.rs', '.toml']);
|
||||
const devScaffoldTerms = [
|
||||
'mo' + 'ck',
|
||||
'fa' + 'ke',
|
||||
'place' + 'holder',
|
||||
'st' + 'ub',
|
||||
'TO' + 'DO',
|
||||
'FIX' + 'ME',
|
||||
'占' + '位',
|
||||
'模' + '拟',
|
||||
'伪' + '造',
|
||||
];
|
||||
|
||||
function extractCargoPackageString(source, key) {
|
||||
const match = source.match(new RegExp(`^${key}\\s*=\\s*"([^"]+)"`, 'm'));
|
||||
@@ -31,6 +51,58 @@ function extractCargoPackageString(source, key) {
|
||||
return match[1];
|
||||
}
|
||||
|
||||
function collectProductionSourceFiles(entry) {
|
||||
const stats = fs.statSync(entry);
|
||||
if (stats.isDirectory()) {
|
||||
const directory = entry.href.endsWith('/') ? entry : new URL(`${entry.href}/`);
|
||||
return fs
|
||||
.readdirSync(entry, { withFileTypes: true })
|
||||
.flatMap((child) =>
|
||||
collectProductionSourceFiles(
|
||||
new URL(`${child.name}${child.isDirectory() ? '/' : ''}`, directory),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const path = entry.pathname;
|
||||
const extension = path.match(/\.[^.]+$/)?.[0] ?? '';
|
||||
if (!productionFileExtensions.has(extension)) {
|
||||
return [];
|
||||
}
|
||||
if (path.includes('.test.') || path.endsWith('/scripts/check-config.mjs')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [entry];
|
||||
}
|
||||
|
||||
function assertNoDevScaffoldTerms(files) {
|
||||
for (const file of files) {
|
||||
const source = fs.readFileSync(file, 'utf8');
|
||||
const lineStarts = [0];
|
||||
for (let index = 0; index < source.length; index += 1) {
|
||||
if (source[index] === '\n') {
|
||||
lineStarts.push(index + 1);
|
||||
}
|
||||
}
|
||||
|
||||
for (const term of devScaffoldTerms) {
|
||||
const matchIndex = source.toLowerCase().indexOf(term.toLowerCase());
|
||||
if (matchIndex === -1) {
|
||||
continue;
|
||||
}
|
||||
const line = lineStarts.filter((start) => start <= matchIndex).length;
|
||||
throw new Error(
|
||||
`desktop shell production source must not include ${term}: ${file.pathname}:${line}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assertNoDevScaffoldTerms(
|
||||
productionSourceRoots.flatMap((root) => collectProductionSourceFiles(root)),
|
||||
);
|
||||
|
||||
function extractStringArrayExport(source, exportName) {
|
||||
const match = source.match(
|
||||
new RegExp(`export const ${exportName}[^=]*= \\[([\\s\\S]*?)\\](?: as const)?;`),
|
||||
|
||||
@@ -19,6 +19,24 @@ const packagePath = new URL('../package.json', import.meta.url);
|
||||
const packageConfig = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
|
||||
const iconPath = new URL('../assets/icon.png', import.meta.url);
|
||||
const icon = PNG.sync.read(fs.readFileSync(iconPath));
|
||||
const productionSourceRoots = [
|
||||
new URL('../App.tsx', import.meta.url),
|
||||
new URL('../app.json', import.meta.url),
|
||||
new URL('../package.json', import.meta.url),
|
||||
new URL('../src/', import.meta.url),
|
||||
];
|
||||
const productionFileExtensions = new Set(['.json', '.mjs', '.ts', '.tsx']);
|
||||
const devScaffoldTerms = [
|
||||
'mo' + 'ck',
|
||||
'fa' + 'ke',
|
||||
'place' + 'holder',
|
||||
'st' + 'ub',
|
||||
'TO' + 'DO',
|
||||
'FIX' + 'ME',
|
||||
'占' + '位',
|
||||
'模' + '拟',
|
||||
'伪' + '造',
|
||||
];
|
||||
|
||||
function extractStringArrayExport(source, exportName) {
|
||||
const match = source.match(
|
||||
@@ -41,6 +59,58 @@ function extractStringArrayExport(source, exportName) {
|
||||
return entries;
|
||||
}
|
||||
|
||||
function collectProductionSourceFiles(entry) {
|
||||
const stats = fs.statSync(entry);
|
||||
if (stats.isDirectory()) {
|
||||
const directory = entry.href.endsWith('/') ? entry : new URL(`${entry.href}/`);
|
||||
return fs
|
||||
.readdirSync(entry, { withFileTypes: true })
|
||||
.flatMap((child) =>
|
||||
collectProductionSourceFiles(
|
||||
new URL(`${child.name}${child.isDirectory() ? '/' : ''}`, directory),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const path = entry.pathname;
|
||||
const extension = path.match(/\.[^.]+$/)?.[0] ?? '';
|
||||
if (!productionFileExtensions.has(extension)) {
|
||||
return [];
|
||||
}
|
||||
if (path.includes('.test.') || path.endsWith('/scripts/check-config.mjs')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [entry];
|
||||
}
|
||||
|
||||
function assertNoDevScaffoldTerms(files) {
|
||||
for (const file of files) {
|
||||
const source = fs.readFileSync(file, 'utf8');
|
||||
const lineStarts = [0];
|
||||
for (let index = 0; index < source.length; index += 1) {
|
||||
if (source[index] === '\n') {
|
||||
lineStarts.push(index + 1);
|
||||
}
|
||||
}
|
||||
|
||||
for (const term of devScaffoldTerms) {
|
||||
const matchIndex = source.toLowerCase().indexOf(term.toLowerCase());
|
||||
if (matchIndex === -1) {
|
||||
continue;
|
||||
}
|
||||
const line = lineStarts.filter((start) => start <= matchIndex).length;
|
||||
throw new Error(
|
||||
`mobile shell production source must not include ${term}: ${file.pathname}:${line}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assertNoDevScaffoldTerms(
|
||||
productionSourceRoots.flatMap((root) => collectProductionSourceFiles(root)),
|
||||
);
|
||||
|
||||
const sharedCapabilities = extractStringArrayExport(
|
||||
sharedContractSource,
|
||||
'HOST_BRIDGE_CAPABILITIES',
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
- 2026-06-18 Tauri 系统托盘:桌面壳启用真实 OS 托盘并复用品牌图标,托盘菜单只执行显示主窗口、刷新主窗口和退出应用,左键点击托盘图标恢复并聚焦主窗口;该能力归桌面壳自身,不进入 HostBridge capability,不向 H5 暴露托盘、菜单、shell 或任意窗口控制 API。托盘注册成功时主窗口关闭按钮只隐藏到托盘,必须通过托盘“退出”结束应用;托盘注册失败不得阻断主窗口启动,也不得拦截关闭,避免窗口消失后无法恢复。`check:native-shells` 和 Tauri cargo test 覆盖托盘配置、菜单动作映射和关闭策略。
|
||||
- 2026-06-18 Tauri 单实例:桌面壳启用 `tauri-plugin-single-instance` 并要求该插件最先注册;重复启动 App 时第二实例退出,只唤醒、取消最小化并聚焦已有主窗口,不把第二实例 argv / cwd / 深链内容作为事件透传给 H5。桌面深链后续如需接入,必须先定义受控 URL 归一和宿主边界,不能借单实例回调直接开放任意启动参数。
|
||||
- 2026-06-18 桌面壳安装包身份:Tauri 桌面壳的产品名固定为 `Genarrative`,应用 identifier 固定为 `world.genarrative.desktop`,Tauri 配置、`apps/desktop-shell/package.json` 与 Cargo package 版本统一为 `0.1.0`;Release 主窗口只加载打包的 `index.html` 和根 `dist` H5 资产,dev URL 只指向本机 Vite 调试入口。桌面壳 CSP 保持 `script-src 'self'`,不得加入 `unsafe-eval`、`tauri:` 或 `file:`,也不得在没有真实端点、签名密钥和发布流程前配置 updater;检查脚本会拒绝包身份、版本、CSP 或 updater 约束漂移。
|
||||
- 2026-06-18 壳生产代码禁用临时替身:Expo 与 Tauri 壳的生产源码和配置不得出现 mock / fake / placeholder / stub / TODO / FIXME 以及对应中文脚手架词;测试文件仍可使用 mock。两端壳配置检查会扫描生产入口、配置和壳实现,防止把临时替身、占位文案或伪实现带进可分发壳。
|
||||
- 影响范围:`src/services/host-bridge/`、未来 `apps/mobile-shell/`、未来 `apps/desktop-shell/`、移动端支付 / 分享 / 深链 / 推送、桌面端系统能力、AI H5 sandbox 的 GameBridge 边界。
|
||||
- 验证方式:普通浏览器、小程序、Expo 壳、Tauri 壳都能返回正确 `getHostRuntime()`;未支持能力能回退 H5;固定玩法在各宿主中读取同一作品数据和运行态 snapshot;AI sandbox 无法直接调用 HostBridge;Tauri release 不允许任意远端页面调用桌面命令。
|
||||
- 关联文档:`docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md`、`docs/【前端架构】宿主壳能力统一协议-2026-06-17.md`。
|
||||
|
||||
@@ -307,6 +307,8 @@ GameBridge 禁止:
|
||||
|
||||
2026-06-18 追加:桌面壳安装包身份固定为 `world.genarrative.desktop`,产品名为 `Genarrative`,Tauri、Node package 与 Cargo package 版本统一为 `0.1.0`。Release 主窗口只能从打包进二进制的 `index.html` 进入根 `dist` H5 资产,dev URL 只能指向本机 Vite 调试入口;CSP 必须保持 `script-src 'self'`,不得加入 `unsafe-eval`、`tauri:` 或 `file:` 这类扩大桌面攻击面的来源。当前不配置自动更新器,直到存在真实更新端点、签名密钥和发布流程再接入;`apps/desktop-shell/scripts/check-config.mjs` 会校验这些包身份、版本、CSP 和 updater 禁用约束。
|
||||
|
||||
2026-06-18 追加:两端壳的生产源码和配置禁止出现 mock / fake / placeholder / stub / TODO / FIXME 以及对应中文脚手架词;测试文件仍可使用 `vi.mock` 或等价测试替身。`apps/mobile-shell/scripts/check-config.mjs` 与 `apps/desktop-shell/scripts/check-config.mjs` 会扫描各自生产入口、配置和壳实现,防止把临时替身或占位文案带进可分发壳。
|
||||
|
||||
### Phase 4:宿主能力扩展
|
||||
|
||||
- 移动端接入系统分享、推送、原生登录和渠道支付。
|
||||
|
||||
Reference in New Issue
Block a user