原生壳补齐能力清单门禁

在原生壳方案文档中补齐移动端和桌面端真实能力完整清单

让统一原生壳检查从源码能力清单反查方案文档

防止 HostBridge 能力实现、入口 URL 和文档再次漂移
This commit is contained in:
2026-06-18 22:32:21 +08:00
parent 0ad56a878f
commit e9c6f8d6df
2 changed files with 105 additions and 0 deletions
File diff suppressed because one or more lines are too long
+99
View File
@@ -5,6 +5,8 @@ import fs from 'node:fs';
import path from 'node:path';
const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';
const nativeShellPlanPath =
'docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md';
const productionShellRoots = [
'apps/mobile-shell',
@@ -89,6 +91,11 @@ const expectedDesktopShellRustFiles = [
'webview.rs',
'window_state.rs',
];
const capabilityListMarkers = {
desktop: '桌面壳当前真实能力完整清单为',
mobile: '移动壳当前通用真实能力完整清单为',
mobileIosExtra: '移动壳 iOS 额外真实能力为',
};
const productionShellExtensions = new Set([
'.json',
'.mjs',
@@ -241,6 +248,95 @@ function assertSameList(actual, expected, label) {
}
}
function extractTsStringArray(source, exportName, seen = new Set()) {
if (seen.has(exportName)) {
throw new Error(`cyclic string array export ${exportName}`);
}
const match = source.match(
new RegExp(`export const ${exportName}[^=]*= \\[([\\s\\S]*?)\\](?: as const)?;`),
);
if (!match) {
throw new Error(`unable to read ${exportName}`);
}
const nextSeen = new Set(seen);
nextSeen.add(exportName);
const entries = [];
for (const entry of match[1].matchAll(/\.\.\s*([A-Z0-9_]+)|'([^']+)'/g)) {
if (entry[1]) {
entries.push(...extractTsStringArray(source, entry[1], nextSeen));
} else {
entries.push(entry[2]);
}
}
return entries;
}
function extractRustCapabilities(source) {
const match = source.match(/fn capabilities\(\)[^{]*\{[\s\S]*?vec!\[([\s\S]*?)\]\s*\}/);
if (!match) {
throw new Error('unable to read desktop shell capabilities');
}
return [...match[1].matchAll(/"([^"]+)"/g)].map((entry) => entry[1]);
}
function extractDocumentCapabilityList(source, marker) {
const markerIndex = source.indexOf(marker);
if (markerIndex === -1) {
throw new Error(`native shell plan missing ${marker}`);
}
const sentenceEnd = source.indexOf('。', markerIndex);
const sentence = source.slice(
markerIndex,
sentenceEnd === -1 ? undefined : sentenceEnd,
);
return [...sentence.matchAll(/`([^`]+)`/g)].map((entry) => entry[1]);
}
function assertNativeShellCapabilityPlan() {
const planSource = fs.readFileSync(nativeShellPlanPath, 'utf8');
const mobileCapabilitySource = fs.readFileSync(
'apps/mobile-shell/src/host-bridge/capabilities.ts',
'utf8',
);
const desktopCapabilitySource = fs.readFileSync(
'apps/desktop-shell/src-tauri/src/host_bridge/capabilities.rs',
'utf8',
);
const mobileCapabilities = extractTsStringArray(
mobileCapabilitySource,
'MOBILE_HOST_CAPABILITIES',
);
const iosMobileCapabilities = extractTsStringArray(
mobileCapabilitySource,
'IOS_MOBILE_HOST_CAPABILITIES',
);
const iosExtraCapabilities = iosMobileCapabilities.filter(
(capability) => !mobileCapabilities.includes(capability),
);
const desktopCapabilities = extractRustCapabilities(desktopCapabilitySource);
assertSameList(
extractDocumentCapabilityList(planSource, capabilityListMarkers.mobile),
mobileCapabilities,
'mobile shell documented common capabilities',
);
assertSameList(
extractDocumentCapabilityList(planSource, capabilityListMarkers.mobileIosExtra),
iosExtraCapabilities,
'mobile shell documented iOS extra capabilities',
);
assertSameList(
extractDocumentCapabilityList(planSource, capabilityListMarkers.desktop),
desktopCapabilities,
'desktop shell documented capabilities',
);
}
function assertHostBridgeLayerLayout() {
const wechatBridgeFiles = fs
.readdirSync('miniprogram/host-bridge', { withFileTypes: true })
@@ -376,6 +472,9 @@ for (const step of steps) {
console.log('[check:native-shells] host-bridge-layer-layout');
assertHostBridgeLayerLayout();
console.log('[check:native-shells] native-shell-capability-plan');
assertNativeShellCapabilityPlan();
console.log('[check:native-shells] production-shell-dev-scaffold-scan');
assertNoProductionShellDevScaffoldTerms();