扩展宿主调用链扫描
自动发现 H5 HostBridge 真实能力调用链 补齐 runtime 回读与 wrapper 消费者扫描兜底 区分壳源码和 H5 业务调用链的禁替身词表 将视觉小说生产预览数据从 mock 命名收口为本地预览命名 更新原生壳验收文档和共享决策记录
This commit is contained in:
+233
-11
@@ -20,15 +20,74 @@ const productionShellScanRoots = [
|
||||
'packages/shared/src/contracts/hostBridge.ts',
|
||||
'src/services/host-bridge',
|
||||
];
|
||||
const h5HostBridgeCallChainScanFiles = [
|
||||
'src/services/clipboard.ts',
|
||||
'src/services/appTitle.ts',
|
||||
'src/services/runtimeAudioFeedback.ts',
|
||||
const h5HostBridgeFacadeModule = 'src/services/host-bridge/hostBridge';
|
||||
const h5HostBridgeScannedFacadeImports = new Set([
|
||||
'canUseHostShareGrid',
|
||||
'captureHostImageFile',
|
||||
'exportHostAudioFile',
|
||||
'exportHostImageFile',
|
||||
'exportHostTextFile',
|
||||
'getHostAppearanceColorScheme',
|
||||
'getHostNetworkStatus',
|
||||
'getNativeAppHostRuntime',
|
||||
'importHostAudioFile',
|
||||
'importHostImageFile',
|
||||
'importHostTextFile',
|
||||
'navigateHostNativePage',
|
||||
'openHostExternalUrl',
|
||||
'openHostShare',
|
||||
'openHostShareGrid',
|
||||
'readHostClipboardText',
|
||||
'refreshNativeAppHostRuntime',
|
||||
'reloadHostWebView',
|
||||
'requestHostHapticsImpact',
|
||||
'requestHostLogin',
|
||||
'requestHostPayment',
|
||||
'setHostAppBadgeCount',
|
||||
'setHostAppTitle',
|
||||
'setHostShareTarget',
|
||||
'showHostLocalNotification',
|
||||
'subscribeHostAppLifecycle',
|
||||
'subscribeHostImageDrop',
|
||||
'subscribeHostNavigationCanGoBack',
|
||||
'subscribeHostNetworkStatusChange',
|
||||
'subscribeHostRuntimeChange',
|
||||
'writeHostClipboardText',
|
||||
]);
|
||||
const h5HostBridgeCallChainWrapperFiles = [
|
||||
'src/hooks/useHostLifecycleActive.ts',
|
||||
'src/hooks/useHostNetworkOnline.ts',
|
||||
'src/components/platform-entry/platformProfileHostClipboard.ts',
|
||||
];
|
||||
const h5HostBridgeRequiredCallChainFiles = [
|
||||
'src/App.tsx',
|
||||
'src/components/auth/AuthGate.tsx',
|
||||
'src/components/common/CreativeAudioInputPanel.tsx',
|
||||
'src/components/common/CreativeImageInputPanel.tsx',
|
||||
'src/components/common/PublishShareModal.tsx',
|
||||
'src/components/common/publishShareCardImage.ts',
|
||||
'src/components/common/CreativeAudioInputPanel.tsx',
|
||||
'src/components/creation-agent/CreationAgentWorkspace.tsx',
|
||||
'src/components/match3d-runtime/Match3DRuntimeShell.tsx',
|
||||
'src/components/platform-entry/PlatformEntryFlowShellImpl.tsx',
|
||||
'src/components/platform-entry/PlatformFeedbackView.tsx',
|
||||
'src/components/platform-entry/PlatformProfilePrimitives.tsx',
|
||||
'src/components/platform-entry/PlatformProfileReferralModal.tsx',
|
||||
'src/components/platform-entry/PlatformProfileRewardCodeRedeemModal.tsx',
|
||||
'src/components/platform-entry/usePlatformProfileCenterController.ts',
|
||||
'src/components/puzzle-runtime/PuzzleRuntimeShell.tsx',
|
||||
'src/components/rpg-creation-result/RpgCreationAssetDebugPanel.tsx',
|
||||
'src/components/rpg-entry/RpgEntryHomeView.tsx',
|
||||
'src/components/square-hole-result/SquareHoleResultView.tsx',
|
||||
'src/hooks/useBackgroundMusic.ts',
|
||||
'src/hooks/useHostLifecycleActive.ts',
|
||||
'src/hooks/useHostNetworkOnline.ts',
|
||||
'src/main.tsx',
|
||||
'src/services/appTitle.ts',
|
||||
'src/services/clipboard.ts',
|
||||
'src/services/runtimeAudioFeedback.ts',
|
||||
'src/services/wechatMiniProgramShareTarget.ts',
|
||||
'src/services/wechatMiniProgramSubscribe.ts',
|
||||
'src/services/wechatMiniProgramShareGrid.ts',
|
||||
];
|
||||
const expectedWechatHostBridgeFiles = [
|
||||
'dispatch.js',
|
||||
@@ -126,6 +185,7 @@ const productionShellExtensions = new Set([
|
||||
'.wxml',
|
||||
'.wxss',
|
||||
]);
|
||||
const h5ProductionSourceExtensions = new Set(['.js', '.jsx', '.ts', '.tsx']);
|
||||
const productionShellExcludedSegments = new Set([
|
||||
'.expo',
|
||||
'.expo-export-smoke',
|
||||
@@ -148,6 +208,15 @@ const productionShellDevScaffoldTerms = [
|
||||
'模' + '拟',
|
||||
'伪' + '造',
|
||||
];
|
||||
const h5HostBridgeCallChainDevScaffoldTerms = [
|
||||
'mo' + 'ck',
|
||||
'fa' + 'ke',
|
||||
'st' + 'ub',
|
||||
'TO' + 'DO',
|
||||
'FIX' + 'ME',
|
||||
'模' + '拟',
|
||||
'伪' + '造',
|
||||
];
|
||||
|
||||
const h5HostBridgeTests = [
|
||||
'packages/shared/src/contracts/hostBridge.test.ts',
|
||||
@@ -234,6 +303,19 @@ function shouldScanProductionShellFile(filePath) {
|
||||
return productionShellExtensions.has(path.extname(filePath));
|
||||
}
|
||||
|
||||
function shouldScanH5ProductionSourceFile(filePath) {
|
||||
const normalizedPath = filePath.split(path.sep).join('/');
|
||||
if (
|
||||
normalizedPath.includes('.test.') ||
|
||||
normalizedPath.includes('/test-utils/') ||
|
||||
normalizedPath.includes('/services/host-bridge/')
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return h5ProductionSourceExtensions.has(path.extname(filePath));
|
||||
}
|
||||
|
||||
function collectProductionShellFiles(entryPath) {
|
||||
const normalizedPath = entryPath.split(path.sep).join('/');
|
||||
if (productionShellExcludedPaths.has(normalizedPath)) {
|
||||
@@ -259,16 +341,145 @@ function collectProductionShellFiles(entryPath) {
|
||||
return shouldScanProductionShellFile(entryPath) ? [entryPath] : [];
|
||||
}
|
||||
|
||||
function assertNoProductionShellDevScaffoldTerms() {
|
||||
const files = [
|
||||
...productionShellScanRoots,
|
||||
...h5HostBridgeCallChainScanFiles,
|
||||
].flatMap(collectProductionShellFiles);
|
||||
function collectFiles(entryPath, shouldIncludeFile) {
|
||||
if (!fs.existsSync(entryPath)) {
|
||||
throw new Error(`scan path does not exist: ${entryPath}`);
|
||||
}
|
||||
|
||||
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) => collectFiles(path.join(entryPath, entry.name), shouldIncludeFile));
|
||||
}
|
||||
|
||||
return shouldIncludeFile(entryPath) ? [entryPath] : [];
|
||||
}
|
||||
|
||||
function normalizeModulePath(modulePath) {
|
||||
return modulePath.split(path.sep).join('/').replace(/\.(jsx?|tsx?)$/, '');
|
||||
}
|
||||
|
||||
function importedModulePath(fromFile, specifier) {
|
||||
if (specifier === '@') {
|
||||
return '.';
|
||||
}
|
||||
if (specifier.startsWith('@/')) {
|
||||
return normalizeModulePath(specifier.slice(2));
|
||||
}
|
||||
if (!specifier.startsWith('.')) {
|
||||
return specifier;
|
||||
}
|
||||
|
||||
return normalizeModulePath(path.normalize(path.join(path.dirname(fromFile), specifier)));
|
||||
}
|
||||
|
||||
function extractImportSpecifiers(source) {
|
||||
const specifiers = [];
|
||||
for (const match of source.matchAll(/import\s+(?:type\s+)?[\s\S]*?\s+from\s+['"]([^'"]+)['"]/g)) {
|
||||
specifiers.push(match[1]);
|
||||
}
|
||||
for (const match of source.matchAll(/export\s+(?:type\s+)?\{[\s\S]*?\}\s+from\s+['"]([^'"]+)['"]/g)) {
|
||||
specifiers.push(match[1]);
|
||||
}
|
||||
for (const match of source.matchAll(/import\s*['"]([^'"]+)['"]/g)) {
|
||||
specifiers.push(match[1]);
|
||||
}
|
||||
|
||||
return specifiers;
|
||||
}
|
||||
|
||||
function extractNamedImportsFromModule(source, fromFile, targetModule) {
|
||||
const importedNames = new Set();
|
||||
const importPattern =
|
||||
/import\s+(?:type\s+)?(?:\{([\s\S]*?)\}|[A-Za-z0-9_$]+\s*,\s*\{([\s\S]*?)\})\s+from\s+['"]([^'"]+)['"]/g;
|
||||
const exportPattern =
|
||||
/export\s+(?:type\s+)?\{([\s\S]*?)\}\s+from\s+['"]([^'"]+)['"]/g;
|
||||
const collectNamedImports = (importBody) => {
|
||||
for (const rawImport of importBody.split(',')) {
|
||||
const cleanedImport = rawImport
|
||||
.replace(/\btype\s+/g, '')
|
||||
.trim();
|
||||
if (!cleanedImport) {
|
||||
continue;
|
||||
}
|
||||
importedNames.add(cleanedImport.split(/\s+as\s+/)[0]?.trim() ?? '');
|
||||
}
|
||||
};
|
||||
|
||||
for (const match of source.matchAll(importPattern)) {
|
||||
if (importedModulePath(fromFile, match[3]) !== targetModule) {
|
||||
continue;
|
||||
}
|
||||
|
||||
collectNamedImports(match[1] ?? match[2] ?? '');
|
||||
}
|
||||
|
||||
for (const match of source.matchAll(exportPattern)) {
|
||||
if (importedModulePath(fromFile, match[2]) !== targetModule) {
|
||||
continue;
|
||||
}
|
||||
|
||||
collectNamedImports(match[1] ?? '');
|
||||
}
|
||||
|
||||
return importedNames;
|
||||
}
|
||||
|
||||
function collectH5HostBridgeCallChainFiles() {
|
||||
const sourceFiles = collectFiles('src', shouldScanH5ProductionSourceFile);
|
||||
const wrapperModules = new Set(
|
||||
h5HostBridgeCallChainWrapperFiles.map(normalizeModulePath),
|
||||
);
|
||||
const scannedFiles = new Set();
|
||||
|
||||
for (const file of sourceFiles) {
|
||||
const source = fs.readFileSync(file, 'utf8');
|
||||
const imports = extractImportSpecifiers(source).map((specifier) =>
|
||||
importedModulePath(file, specifier),
|
||||
);
|
||||
const facadeImports = extractNamedImportsFromModule(
|
||||
source,
|
||||
file,
|
||||
h5HostBridgeFacadeModule,
|
||||
);
|
||||
const importsScannedFacadeCapability = [...facadeImports].some((importName) =>
|
||||
h5HostBridgeScannedFacadeImports.has(importName),
|
||||
);
|
||||
if (
|
||||
importsScannedFacadeCapability ||
|
||||
imports.some((specifier) => wrapperModules.has(specifier))
|
||||
) {
|
||||
scannedFiles.add(file);
|
||||
}
|
||||
}
|
||||
|
||||
for (const wrapperFile of h5HostBridgeCallChainWrapperFiles) {
|
||||
scannedFiles.add(wrapperFile);
|
||||
}
|
||||
|
||||
const missingRequiredFiles = h5HostBridgeRequiredCallChainFiles.filter(
|
||||
(file) => !scannedFiles.has(file),
|
||||
);
|
||||
if (missingRequiredFiles.length > 0) {
|
||||
throw new Error(
|
||||
`H5 HostBridge call chain scan is missing required files: ${missingRequiredFiles.join(', ')}`,
|
||||
);
|
||||
}
|
||||
|
||||
return [...scannedFiles].sort();
|
||||
}
|
||||
|
||||
function assertNoDevScaffoldTermsInFiles(files, terms) {
|
||||
for (const file of files) {
|
||||
const source = fs.readFileSync(file, 'utf8');
|
||||
const lowerSource = source.toLowerCase();
|
||||
for (const term of productionShellDevScaffoldTerms) {
|
||||
for (const term of terms) {
|
||||
const matchIndex = lowerSource.indexOf(term.toLowerCase());
|
||||
if (matchIndex === -1) {
|
||||
continue;
|
||||
@@ -283,6 +494,17 @@ function assertNoProductionShellDevScaffoldTerms() {
|
||||
}
|
||||
}
|
||||
|
||||
function assertNoProductionShellDevScaffoldTerms() {
|
||||
assertNoDevScaffoldTermsInFiles(
|
||||
productionShellScanRoots.flatMap(collectProductionShellFiles),
|
||||
productionShellDevScaffoldTerms,
|
||||
);
|
||||
assertNoDevScaffoldTermsInFiles(
|
||||
collectH5HostBridgeCallChainFiles().flatMap(collectProductionShellFiles),
|
||||
h5HostBridgeCallChainDevScaffoldTerms,
|
||||
);
|
||||
}
|
||||
|
||||
function assertSameList(actual, expected, label) {
|
||||
if (
|
||||
actual.length !== expected.length ||
|
||||
|
||||
Reference in New Issue
Block a user