#!/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']; const productionShellExtensions = new Set([ '.json', '.mjs', '.rs', '.toml', '.ts', '.tsx', ]); const productionShellExcludedSegments = new Set([ '.expo-export-smoke', 'node_modules', 'target', ]); 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 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}`, ); } } } 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] production-shell-dev-scaffold-scan'); assertNoProductionShellDevScaffoldTerms(); console.log('[check:native-shells] OK');