d71fde8b9a
拆分移动壳能力清单与分发模块 拆分桌面壳能力清单与分发模块 补充微信壳协议与分发薄索引 同步三端桥接层门禁和文档
376 lines
9.5 KiB
JavaScript
376 lines
9.5 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
import {spawnSync} from 'node:child_process';
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
|
|
const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
|
|
const productionShellRoots = [
|
|
'apps/mobile-shell',
|
|
'apps/desktop-shell',
|
|
'miniprogram',
|
|
];
|
|
const expectedWechatHostBridgeFiles = [
|
|
'dispatch.js',
|
|
'payment.js',
|
|
'payment.test.js',
|
|
'protocol.js',
|
|
'protocol.test.js',
|
|
'shareGrid.js',
|
|
'shareGrid.test.js',
|
|
'subscribeMessage.js',
|
|
'subscribeMessage.test.js',
|
|
'webView.js',
|
|
'webView.test.js',
|
|
];
|
|
const expectedWechatShellFiles = [
|
|
'payment.js',
|
|
'payment.test.js',
|
|
'shareGrid.js',
|
|
'shareGrid.test.js',
|
|
'subscribeMessage.js',
|
|
'subscribeMessage.test.js',
|
|
'webView.js',
|
|
'webView.test.js',
|
|
];
|
|
const expectedMobileHostBridgeFiles = [
|
|
'bridge.test.ts',
|
|
'bridge.ts',
|
|
'capabilities.ts',
|
|
'dispatch.ts',
|
|
'files.ts',
|
|
'protocol.ts',
|
|
'share.ts',
|
|
];
|
|
const expectedMobileShellFiles = [
|
|
'ShellApp.tsx',
|
|
'deepLink.test.ts',
|
|
'deepLink.ts',
|
|
'lifecycle.test.ts',
|
|
'lifecycle.ts',
|
|
'navigation.test.ts',
|
|
'navigation.ts',
|
|
'network.test.ts',
|
|
'network.ts',
|
|
'runtime.ts',
|
|
'safeArea.test.ts',
|
|
'safeArea.ts',
|
|
'url.test.ts',
|
|
'url.ts',
|
|
'webViewPolicy.test.ts',
|
|
'webViewPolicy.ts',
|
|
];
|
|
const expectedDesktopHostBridgeRustFiles = [
|
|
'capabilities.rs',
|
|
'dispatch.rs',
|
|
'files.rs',
|
|
'mod.rs',
|
|
'protocol.rs',
|
|
'share.rs',
|
|
];
|
|
const expectedDesktopShellRustFiles = [
|
|
'deep_link.rs',
|
|
'events.rs',
|
|
'file_drop.rs',
|
|
'lifecycle.rs',
|
|
'mod.rs',
|
|
'navigation.rs',
|
|
'network.rs',
|
|
'runtime.rs',
|
|
'tray.rs',
|
|
'url.rs',
|
|
'webview.rs',
|
|
];
|
|
const productionShellExtensions = new Set([
|
|
'.json',
|
|
'.mjs',
|
|
'.rs',
|
|
'.toml',
|
|
'.ts',
|
|
'.tsx',
|
|
'.wxml',
|
|
'.wxss',
|
|
]);
|
|
const productionShellExcludedSegments = new Set([
|
|
'.expo',
|
|
'.expo-export-smoke',
|
|
'node_modules',
|
|
'target',
|
|
'test-utils',
|
|
]);
|
|
const productionShellExcludedPaths = new Set([
|
|
'apps/desktop-shell/src-tauri/gen',
|
|
'apps/desktop-shell/src-tauri/permissions/autogenerated',
|
|
]);
|
|
const productionShellDevScaffoldTerms = [
|
|
'mo' + 'ck',
|
|
'fa' + 'ke',
|
|
'place' + 'holder',
|
|
'st' + 'ub',
|
|
'TO' + 'DO',
|
|
'FIX' + 'ME',
|
|
'占' + '位',
|
|
'模' + '拟',
|
|
'伪' + '造',
|
|
];
|
|
|
|
const h5HostBridgeTests = [
|
|
'packages/shared/src/contracts/hostBridge.test.ts',
|
|
'src/services/host-bridge/hostBridge.test.ts',
|
|
'src/services/host-bridge/nativeAppHostBridge.test.ts',
|
|
'src/App.test.tsx',
|
|
'src/components/common/PublishShareModal.test.tsx',
|
|
'src/services/runtimeAudioFeedback.test.ts',
|
|
'src/services/clipboard.test.ts',
|
|
'src/services/appTitle.test.ts',
|
|
];
|
|
|
|
const steps = [
|
|
{
|
|
label: 'h5-host-bridge-tests',
|
|
command: npmCommand,
|
|
args: ['run', 'test', '--', ...h5HostBridgeTests],
|
|
},
|
|
{
|
|
label: 'mobile-shell-typecheck',
|
|
command: npmCommand,
|
|
args: ['run', 'mobile-shell:typecheck'],
|
|
},
|
|
{
|
|
label: 'mobile-shell-test',
|
|
command: npmCommand,
|
|
args: ['run', 'mobile-shell:test'],
|
|
},
|
|
{
|
|
label: 'mobile-shell-expo-config-smoke',
|
|
command: npmCommand,
|
|
args: ['run', 'mobile-shell:config'],
|
|
},
|
|
{
|
|
label: 'mobile-shell-expo-export-smoke',
|
|
command: npmCommand,
|
|
args: ['run', 'mobile-shell:export'],
|
|
},
|
|
{
|
|
label: 'desktop-shell-typecheck',
|
|
command: npmCommand,
|
|
args: ['run', 'desktop-shell:typecheck'],
|
|
},
|
|
{
|
|
label: 'desktop-shell-test',
|
|
command: npmCommand,
|
|
args: ['run', 'desktop-shell:test'],
|
|
},
|
|
{
|
|
label: 'desktop-shell-release-build-smoke',
|
|
command: npmCommand,
|
|
args: ['run', 'desktop-shell:build', '--', '--no-bundle'],
|
|
},
|
|
];
|
|
|
|
function shouldScanProductionShellFile(filePath) {
|
|
const normalizedPath = filePath.split(path.sep).join('/');
|
|
if (
|
|
normalizedPath.includes('.test.') ||
|
|
normalizedPath.endsWith('/scripts/check-config.mjs')
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
return productionShellExtensions.has(path.extname(filePath));
|
|
}
|
|
|
|
function collectProductionShellFiles(entryPath) {
|
|
const normalizedPath = entryPath.split(path.sep).join('/');
|
|
if (productionShellExcludedPaths.has(normalizedPath)) {
|
|
return [];
|
|
}
|
|
|
|
const stats = fs.statSync(entryPath);
|
|
if (stats.isDirectory()) {
|
|
const name = path.basename(entryPath);
|
|
if (productionShellExcludedSegments.has(name)) {
|
|
return [];
|
|
}
|
|
|
|
return fs
|
|
.readdirSync(entryPath, { withFileTypes: true })
|
|
.flatMap((entry) => collectProductionShellFiles(path.join(entryPath, entry.name)));
|
|
}
|
|
|
|
return shouldScanProductionShellFile(entryPath) ? [entryPath] : [];
|
|
}
|
|
|
|
function assertNoProductionShellDevScaffoldTerms() {
|
|
const files = productionShellRoots.flatMap(collectProductionShellFiles);
|
|
|
|
for (const file of files) {
|
|
const source = fs.readFileSync(file, 'utf8');
|
|
const lowerSource = source.toLowerCase();
|
|
for (const term of productionShellDevScaffoldTerms) {
|
|
const matchIndex = lowerSource.indexOf(term.toLowerCase());
|
|
if (matchIndex === -1) {
|
|
continue;
|
|
}
|
|
|
|
const line =
|
|
source.slice(0, matchIndex).split('\n').length;
|
|
throw new Error(
|
|
`production native shell source must not include ${term}: ${file}:${line}`,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
function assertSameList(actual, expected, label) {
|
|
if (
|
|
actual.length !== expected.length ||
|
|
actual.some((value, index) => value !== expected[index])
|
|
) {
|
|
throw new Error(
|
|
`${label} drifted: expected ${expected.join(', ')} but got ${actual.join(', ')}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
function assertHostBridgeLayerLayout() {
|
|
const wechatBridgeFiles = fs
|
|
.readdirSync('miniprogram/host-bridge', { withFileTypes: true })
|
|
.filter((entry) => entry.isFile())
|
|
.map((entry) => entry.name)
|
|
.sort();
|
|
assertSameList(
|
|
wechatBridgeFiles,
|
|
expectedWechatHostBridgeFiles,
|
|
'wechat host bridge files',
|
|
);
|
|
|
|
const wechatShellLayerFiles = fs
|
|
.readdirSync('miniprogram/shell', { withFileTypes: true })
|
|
.filter((entry) => entry.isFile())
|
|
.map((entry) => entry.name)
|
|
.sort();
|
|
assertSameList(
|
|
wechatShellLayerFiles,
|
|
expectedWechatShellFiles,
|
|
'wechat shell files',
|
|
);
|
|
|
|
const wechatPagePaths = [
|
|
'miniprogram/pages/web-view/index.js',
|
|
'miniprogram/pages/wechat-pay/index.js',
|
|
'miniprogram/pages/share-grid/index.js',
|
|
'miniprogram/pages/subscribe-message/index.js',
|
|
];
|
|
for (const pagePath of wechatPagePaths) {
|
|
const source = fs.readFileSync(pagePath, 'utf8');
|
|
if (!source.includes("require('../../shell/")) {
|
|
throw new Error(`${pagePath} must import from miniprogram/shell`);
|
|
}
|
|
if (
|
|
source.includes("require('../../host-bridge/") ||
|
|
source.includes("require('./index.shared')")
|
|
) {
|
|
throw new Error(`${pagePath} must not import bridge logic directly`);
|
|
}
|
|
}
|
|
|
|
const mobileHostBridgeFiles = fs
|
|
.readdirSync('apps/mobile-shell/src/host-bridge', { withFileTypes: true })
|
|
.filter((entry) => entry.isFile())
|
|
.map((entry) => entry.name)
|
|
.sort();
|
|
assertSameList(
|
|
mobileHostBridgeFiles,
|
|
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,
|
|
expectedMobileShellFiles,
|
|
'mobile shell bridge files',
|
|
);
|
|
|
|
const mobileAppSource = fs.readFileSync('apps/mobile-shell/App.tsx', 'utf8');
|
|
if (!mobileAppSource.includes("import ShellApp from './src/shell/ShellApp';")) {
|
|
throw new Error('mobile shell App.tsx must import from apps/mobile-shell/src/shell');
|
|
}
|
|
if (mobileAppSource.includes('./src/host-bridge/')) {
|
|
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,
|
|
['main.rs'],
|
|
'desktop shell entrypoint Rust files',
|
|
);
|
|
|
|
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,
|
|
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,
|
|
expectedDesktopShellRustFiles,
|
|
'desktop shell Rust bridge files',
|
|
);
|
|
}
|
|
|
|
for (const step of steps) {
|
|
console.log(`[check:native-shells] ${step.label}`);
|
|
const result = spawnSync(step.command, step.args, {
|
|
cwd: process.cwd(),
|
|
stdio: 'inherit',
|
|
});
|
|
|
|
if (result.error) {
|
|
console.error(
|
|
`[check:native-shells] failed to start ${step.label}: ${result.error.message}`,
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
if (result.signal) {
|
|
console.error(
|
|
`[check:native-shells] ${step.label} was terminated by signal ${result.signal}`,
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
if ((result.status ?? 0) !== 0) {
|
|
process.exit(result.status ?? 1);
|
|
}
|
|
}
|
|
|
|
console.log('[check:native-shells] host-bridge-layer-layout');
|
|
assertHostBridgeLayerLayout();
|
|
|
|
console.log('[check:native-shells] production-shell-dev-scaffold-scan');
|
|
assertNoProductionShellDevScaffoldTerms();
|
|
|
|
console.log('[check:native-shells] OK');
|