Files
gongxue-base/scripts/taro-build-matrix-contract-test.js
2026-07-12 19:26:57 +08:00

276 lines
15 KiB
JavaScript

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(', ')})`);