import assert from 'node:assert/strict'; import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import { pathToFileURL } from 'node:url'; const repoRoot = process.cwd(); const taroRoot = path.join(repoRoot, 'apps', 'taro'); const configPath = path.join(taroRoot, 'config', 'index.ts'); const appConfigPath = path.join(taroRoot, 'src', 'app.config.ts'); const projectConfigPath = path.join(taroRoot, 'project.config.json'); const rootPackage = JSON.parse(fs.readFileSync(path.join(repoRoot, 'package.json'), 'utf8')); const taroPackage = JSON.parse(fs.readFileSync(path.join(taroRoot, 'package.json'), 'utf8')); const bootstrapSource = fs.readFileSync(path.join(taroRoot, 'src', 'pages', 'bootstrap', 'index.tsx'), 'utf8'); const h5InteractionSmokeSource = fs.readFileSync(path.join(repoRoot, 'scripts', 'taro-h5-interaction-smoke.js'), 'utf8'); const h5RuntimePatchCheck = 'node ../../scripts/taro-components-h5-runtime-patch.js --check'; const weappBuildModule = await import(pathToFileURL(path.join(repoRoot, 'scripts', 'build-weapp-student.js')).href); const weappGuardModule = await import(pathToFileURL(path.join(repoRoot, 'scripts', 'taro-weapp-release-guardrails.js')).href); const matrix = [ { id: 'h5.student', taroEnv: 'h5', portal: 'student', releaseMode: 'production', outputRoot: 'dist/h5-student', appScript: 'build:h5:student', rootScript: 'build:taro:h5:student' }, { id: 'h5.tenant', taroEnv: 'h5', portal: 'tenant-admin', releaseMode: 'production', outputRoot: 'dist/h5-tenant-admin', appScript: 'build:h5:tenant', rootScript: 'build:taro:h5:tenant' }, { id: 'h5.platform', taroEnv: 'h5', portal: 'platform-admin', releaseMode: 'production', outputRoot: 'dist/h5-platform-admin', appScript: 'build:h5:platform', rootScript: 'build:taro:h5:platform' }, { id: 'weapp.student', taroEnv: 'weapp', portal: 'student', releaseMode: 'preview', outputRoot: 'dist/weapp-student', appScript: 'build:weapp:student', rootScript: 'build:taro:weapp:student' }, ]; async function importForBuild(filePath, build) { process.env.TARO_ENV = build.taroEnv; process.env.TARO_APP_PORTAL = build.portal; process.env.TARO_APP_RELEASE_MODE = build.releaseMode || 'preview'; if (build.apiBaseUrl === undefined) delete process.env.TARO_APP_API_BASE_URL; else process.env.TARO_APP_API_BASE_URL = build.apiBaseUrl; if (build.tenantCode === undefined) delete process.env.TARO_APP_TENANT_CODE; else process.env.TARO_APP_TENANT_CODE = build.tenantCode; if (build.weappTenantMode === undefined) delete process.env.TARO_APP_WEAPP_TENANT_MODE; else process.env.TARO_APP_WEAPP_TENANT_MODE = build.weappTenantMode; globalThis.defineAppConfig = value => value; const moduleUrl = pathToFileURL(filePath); moduleUrl.searchParams.set('matrix', build.id); return (await import(moduleUrl.href)).default; } const originalPortal = process.env.TARO_APP_PORTAL; const originalTaroEnv = process.env.TARO_ENV; const originalReleaseMode = process.env.TARO_APP_RELEASE_MODE; const originalApiBaseUrl = process.env.TARO_APP_API_BASE_URL; const originalTenantCode = process.env.TARO_APP_TENANT_CODE; const originalWeappTenantMode = process.env.TARO_APP_WEAPP_TENANT_MODE; const observedOutputRoots = []; for (const build of matrix) { const config = await importForBuild(configPath, build); const appConfig = await importForBuild(appConfigPath, build); assert.equal(config.outputRoot, build.outputRoot, `${build.id} must write to ${build.outputRoot}`); if (build.taroEnv === 'h5') { assert.notEqual(config.h5?.useDeprecatedAdapterComponent, true, `${build.id} must keep the reviewed modern Taro component adapter`); assert.equal(config.h5?.devServer?.host, '127.0.0.1', `${build.id} development server must bind to loopback`); assert.deepEqual( config.h5?.devServer?.allowedHosts, ['localhost', '127.0.0.1'], `${build.id} development server must reject untrusted Host headers`, ); } const publicBuildConfig = JSON.parse(config.defineConstants?.__TARO_PUBLIC_BUILD_CONFIG__ || '{}'); assert.equal(publicBuildConfig.portal, build.portal, `${build.id} must compile its portal into the public build config`); assert.equal(publicBuildConfig.target, build.taroEnv, `${build.id} must compile its target into the public build config`); assert.equal(publicBuildConfig.releaseMode, build.releaseMode, `${build.id} must compile its release mode into the public build config`); assert.equal(publicBuildConfig.weappTenantMode, build.taroEnv === 'weapp' ? 'launch' : '', `${build.id} must compile its WeApp tenant mode`); if (build.releaseMode === 'production') { assert.doesNotMatch(JSON.stringify(publicBuildConfig), /(?:127\.0\.0\.1|localhost)/i, `${build.id} must not compile a local API fallback`); } observedOutputRoots.push(config.outputRoot); const appScript = taroPackage.scripts?.[build.appScript] || ''; const rootScript = rootPackage.scripts?.[build.rootScript] || ''; if (build.taroEnv === 'weapp') { assert.ok(appScript.includes('build-weapp-student.js'), `${build.appScript} must use the guarded WeApp build wrapper`); } else { assert.ok(appScript.startsWith(`${h5RuntimePatchCheck} && `), `${build.appScript} must verify the reviewed Taro H5 runtime patches`); assert.ok(appScript.includes(`TARO_ENV=${build.taroEnv}`), `${build.appScript} must pin TARO_ENV=${build.taroEnv}`); assert.ok(appScript.includes(`TARO_APP_PORTAL=${build.portal}`), `${build.appScript} must pin TARO_APP_PORTAL=${build.portal}`); assert.ok(appScript.includes('TARO_APP_RELEASE_MODE=production'), `${build.appScript} must fail closed on missing production runtime config`); assert.ok(appScript.includes(`--type ${build.taroEnv}`), `${build.appScript} must build the ${build.taroEnv} target`); } assert.ok(rootScript.includes('@tiku-saas/taro') && rootScript.includes(build.appScript), `${build.rootScript} must delegate to the Taro workspace script`); if (build.taroEnv === 'h5') { assert.equal(appConfig.subPackages, undefined, `${build.id} must not emit mini-program subpackages`); } else { assert.deepEqual(appConfig.pages, ['pages/bootstrap/index'], 'Student WeApp main package must stay minimal'); assert.equal(appConfig.subPackages?.[0]?.root, 'pages/student', 'Student WeApp must keep all business pages in the student subpackage'); assert.equal(appConfig.lazyCodeLoading, 'requiredComponents', 'Student WeApp must enable required-component lazy loading'); } } assert.equal(new Set(observedOutputRoots).size, matrix.length, 'Every platform/portal build must have an isolated output directory'); assert.ok(!taroPackage.scripts?.['build:weapp:tenant'], 'Tenant admin is H5-only and must not expose a WeApp build'); assert.ok(!taroPackage.scripts?.['build:weapp:platform'], 'Platform admin is H5-only and must not expose a WeApp build'); assert.ok(rootPackage.scripts?.['build:taro:h5:preview'], 'Root package must expose the three-portal H5 preview build'); assert.equal( taroPackage.scripts?.postinstall, 'node ../../scripts/taro-components-h5-runtime-patch.js --apply', 'Taro workspace installation must apply the reviewed H5 runtime patches', ); assert.ok(taroPackage.scripts?.['dev:h5']?.startsWith(`${h5RuntimePatchCheck} && `), 'H5 development must verify the runtime patches'); for (const item of [ ['student', 'student'], ['tenant', 'tenant-admin'], ['platform', 'platform-admin'], ]) { const [scriptSuffix, portal] = item; const appScriptName = `build:h5:${scriptSuffix}:preview`; const rootScriptName = `build:taro:h5:${scriptSuffix}:preview`; const appScript = taroPackage.scripts?.[appScriptName] || ''; const rootScript = rootPackage.scripts?.[rootScriptName] || ''; assert.ok(appScript.startsWith(`${h5RuntimePatchCheck} && `), `${appScriptName} must verify the reviewed Taro H5 runtime patches`); assert.ok(appScript.includes(`TARO_APP_PORTAL=${portal}`), `${appScriptName} must pin TARO_APP_PORTAL=${portal}`); assert.ok(appScript.includes('TARO_APP_RELEASE_MODE=preview'), `${appScriptName} must compile preview mode`); assert.ok(rootScript.includes(appScriptName), `${rootScriptName} must delegate to ${appScriptName}`); } assert.match(h5InteractionSmokeSource, /assertServeOnlyPreviewBuild/); assert.match(h5InteractionSmokeSource, /not a preview build/); assert.match(h5InteractionSmokeSource, /assertRuntimeHealthy/); assert.match(h5InteractionSmokeSource, /Runtime\.exceptionThrown/); assert.match(h5InteractionSmokeSource, /runCrossPortalRuntimeProbe/); assert.match(h5InteractionSmokeSource, /runTaroInputWatcherRaceProbe/); assert.match(h5InteractionSmokeSource, /runTaroButtonLoadingRaceProbe/); for (const portal of ['tenant-admin', 'platform-admin']) { await assert.rejects( () => importForBuild(appConfigPath, { id: `unsupported.weapp.${portal}`, taroEnv: 'weapp', portal }), /H5-only/, `${portal} WeApp configuration must fail fast instead of producing an unsupported admin mini-program`, ); } const projectConfig = JSON.parse(fs.readFileSync(projectConfigPath, 'utf8')); assert.equal(projectConfig.miniprogramRoot, 'dist/weapp-student/', 'WeChat developer tools must open the isolated student WeApp output'); assert.ok( bootstrapSource.includes('redirectToLogin') && bootstrapSource.includes('if (!authorized)'), 'Student WeApp bootstrap page must route unauthenticated users into the login page inside the student subpackage', ); assert.ok(taroPackage.scripts?.['build:weapp:student:production']?.includes('--production'), 'Production WeApp build must use strict public config validation'); assert.ok(rootPackage.scripts?.['build:taro:weapp:student:production'], 'Root package must expose the production WeApp build'); const productionWeappConfig = await importForBuild(configPath, { id: 'weapp.student.production', taroEnv: 'weapp', portal: 'student', releaseMode: 'production', apiBaseUrl: 'https://api.gongxue100.com', tenantCode: 'campus-north', weappTenantMode: 'fixed', }); const productionPublicConfig = JSON.parse(productionWeappConfig.defineConstants?.__TARO_PUBLIC_BUILD_CONFIG__ || '{}'); assert.deepEqual( productionPublicConfig, { portal: 'student', target: 'weapp', releaseMode: 'production', weappTenantMode: 'fixed', apiBaseUrl: 'https://api.gongxue100.com', supabaseUrl: '', supabasePublishableKey: '', tenantCode: 'campus-north', }, 'Production WeApp public config must be fully resolved at compile time', ); assert.doesNotMatch(JSON.stringify(productionPublicConfig), /(?:127\.0\.0\.1|localhost)/i, 'Production WeApp compile constants must not contain local fallbacks'); const launchWeappConfig = await importForBuild(configPath, { id: 'weapp.student.launch', taroEnv: 'weapp', portal: 'student', releaseMode: 'production', apiBaseUrl: 'https://api.gongxue100.com', tenantCode: 'inherited-tenant', weappTenantMode: 'launch', }); const launchPublicConfig = JSON.parse(launchWeappConfig.defineConstants?.__TARO_PUBLIC_BUILD_CONFIG__ || '{}'); assert.equal(launchPublicConfig.weappTenantMode, 'launch'); assert.equal(launchPublicConfig.tenantCode, '', 'Launch mode must clear an inherited compile-time tenant code'); assert.deepEqual( weappBuildModule.resolveWeappBuildConfig({ TARO_APP_API_BASE_URL: 'https://api.gongxue100.com/', WECHAT_MINIAPP_APP_ID: 'wx6f3a9c2d4e8b1a70', TARO_APP_WEAPP_TENANT_MODE: 'fixed', TARO_APP_TENANT_CODE: 'campus-north', }, { production: true }), { production: true, tenantMode: 'fixed', tenantCode: 'campus-north', apiBaseUrl: 'https://api.gongxue100.com', appId: 'wx6f3a9c2d4e8b1a70', }, ); assert.equal(weappBuildModule.resolveWeappTenantMode({}, false), 'launch'); assert.equal(weappBuildModule.resolveWeappTenantMode({ TARO_APP_TENANT_CODE: 'campus-north' }, false), 'fixed'); assert.throws( () => weappBuildModule.resolveWeappBuildConfig({ TARO_APP_API_BASE_URL: 'https://api.gongxue100.com', WECHAT_MINIAPP_APP_ID: 'wx6f3a9c2d4e8b1a70', TARO_APP_WEAPP_TENANT_MODE: 'launch', TARO_APP_TENANT_CODE: 'inherited-tenant', }, { production: true }), /must be empty.*launch/, ); for (const apiBaseUrl of [ 'http://api.gongxue100.com', 'https://localhost:8787', 'https://127.0.0.1:8787', 'https://[::1]:8787', 'https://api.example', 'https://api.example.test', 'https://api.internal.local', ]) { assert.throws(() => weappBuildModule.validateProductionApiBaseUrl(apiBaseUrl), /TARO_APP_API_BASE_URL/); } for (const appId of ['touristappid', 'wx0000000000000000', 'wx0123456789abcdef']) { assert.throws(() => weappBuildModule.validateProductionWechatAppId(appId), /WECHAT_MINIAPP_APP_ID/); } for (const tenantCode of ['tenant-production', 'replace-with-tenant-code', 'example', 'test', 'demo', 'smoke', 'placeholder', 'changeme']) { assert.throws(() => weappBuildModule.validateProductionTenantCode(tenantCode), /placeholder tenant code/); } const guardFixture = fs.mkdtempSync(path.join(os.tmpdir(), 'taro-weapp-guard-')); try { fs.mkdirSync(path.join(guardFixture, 'pages', 'student'), { recursive: true }); fs.writeFileSync(path.join(guardFixture, 'project.config.json'), JSON.stringify({ appid: 'wx6f3a9c2d4e8b1a70', setting: { urlCheck: true }, })); fs.writeFileSync(path.join(guardFixture, 'app.json'), JSON.stringify({ pages: ['pages/bootstrap/index'], subPackages: [{ root: 'pages/student', pages: ['home/index'] }], })); fs.writeFileSync(path.join(guardFixture, 'common.js'), `const config=${JSON.stringify(productionPublicConfig)};`); fs.writeFileSync(path.join(guardFixture, 'pages', 'student', 'home.js'), 'module.exports = {};'); const releaseChecks = weappGuardModule.inspectWeappRelease({ distRoot: guardFixture, requireProduction: true }); assert.equal(releaseChecks.some(check => check.status === 'fail'), false, JSON.stringify(releaseChecks)); fs.writeFileSync(path.join(guardFixture, 'common.js'), `const config=${JSON.stringify({ ...productionPublicConfig, weappTenantMode: 'launch', tenantCode: 'compiled-fallback', })};`); const unsafeLaunchChecks = weappGuardModule.inspectWeappRelease({ distRoot: guardFixture, requireProduction: true }); assert.ok(unsafeLaunchChecks.some(check => check.id === 'weapp.public_config.tenant_code' && check.status === 'fail')); } finally { fs.rmSync(guardFixture, { recursive: true, force: true }); } for (const invalid of [ { id: 'invalid.portal', taroEnv: 'h5', portal: 'studnet' }, { id: 'invalid.target', taroEnv: '../h5', portal: 'student' }, ]) { await assert.rejects(() => importForBuild(configPath, invalid), /Unsupported Taro/, `${invalid.id} must fail fast`); } for (const [key, value] of [ ['TARO_APP_PORTAL', originalPortal], ['TARO_ENV', originalTaroEnv], ['TARO_APP_RELEASE_MODE', originalReleaseMode], ['TARO_APP_API_BASE_URL', originalApiBaseUrl], ['TARO_APP_TENANT_CODE', originalTenantCode], ['TARO_APP_WEAPP_TENANT_MODE', originalWeappTenantMode], ]) { if (value === undefined) delete process.env[key]; else process.env[key] = value; } console.log(`[PASS] Taro build matrix contract (${matrix.map(item => `${item.id}=${item.outputRoot}`).join(', ')})`);