forked from wangziqi/gongxue-base
181 lines
7.9 KiB
JavaScript
181 lines
7.9 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import { pathToFileURL } from 'node:url';
|
|
|
|
const repoRoot = process.cwd();
|
|
const taroSrc = path.join(repoRoot, 'apps', 'taro', 'src');
|
|
const pagesRoot = path.join(taroSrc, 'pages');
|
|
const appConfigPath = path.join(taroSrc, 'app.config.ts');
|
|
const bootstrapPath = path.join(pagesRoot, 'bootstrap', 'index.tsx');
|
|
const routeGuardPath = path.join(taroSrc, 'services', 'routeGuard.ts');
|
|
const h5StaticSmokePath = path.join(repoRoot, 'scripts', 'taro-h5-static-smoke.js');
|
|
const frontendHandoffPath = path.join(repoRoot, 'docs', 'refactor', 'frontend-handoff-index.md');
|
|
|
|
function readText(filePath) {
|
|
return fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
|
|
}
|
|
|
|
function normalizeSlashes(value) {
|
|
return value.replace(/\\/g, '/');
|
|
}
|
|
|
|
function walkIndexPages(dir) {
|
|
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
const files = [];
|
|
for (const entry of entries) {
|
|
const entryPath = path.join(dir, entry.name);
|
|
if (entry.isDirectory()) {
|
|
files.push(...walkIndexPages(entryPath));
|
|
continue;
|
|
}
|
|
if (entry.name === 'index.tsx') files.push(entryPath);
|
|
}
|
|
return files;
|
|
}
|
|
|
|
function parsePortalLandingRoutes() {
|
|
const text = readText(appConfigPath);
|
|
const match = text.match(/const\s+portalLandingRoutes\s*:\s*Record<string,\s*string>\s*=\s*\{([\s\S]*?)\}/m);
|
|
assert.ok(match, 'apps/taro/src/app.config.ts must define portalLandingRoutes');
|
|
return Object.fromEntries(
|
|
[...match[1].matchAll(/['"`]?([\w-]+)['"`]?\s*:\s*['"`]([^'"`]+)['"`]/g)]
|
|
.map(item => [item[1], item[2]]),
|
|
);
|
|
}
|
|
|
|
function routeFromIndexFile(filePath) {
|
|
return normalizeSlashes(path.relative(taroSrc, filePath)).replace(/\/index\.tsx$/, '/index');
|
|
}
|
|
|
|
function parseLiteralPagePaths(text) {
|
|
return [...text.matchAll(/['"`](\/pages\/[^'"`?#]+)['"`]/g)]
|
|
.map(item => item[1].replace(/^\/+/, ''))
|
|
.filter(route => route.endsWith('/index'));
|
|
}
|
|
|
|
function uniqueSorted(items) {
|
|
return [...new Set(items)].sort();
|
|
}
|
|
|
|
async function loadAppConfig(portal, taroEnv) {
|
|
process.env.TARO_APP_PORTAL = portal;
|
|
process.env.TARO_ENV = taroEnv;
|
|
globalThis.defineAppConfig = value => value;
|
|
const moduleUrl = pathToFileURL(appConfigPath);
|
|
moduleUrl.searchParams.set('portal', portal);
|
|
moduleUrl.searchParams.set('env', taroEnv);
|
|
return (await import(moduleUrl.href)).default;
|
|
}
|
|
|
|
function expandedRoutes(config) {
|
|
const mainRoutes = config.pages || [];
|
|
const packageRoutes = (config.subPackages || config.subpackages || []).flatMap(item => (
|
|
(item.pages || []).map(route => `${String(item.root || '').replace(/\/$/, '')}/${String(route).replace(/^\//, '')}`)
|
|
));
|
|
return [...mainRoutes, ...packageRoutes];
|
|
}
|
|
|
|
const actualRoutes = walkIndexPages(pagesRoot).map(routeFromIndexFile);
|
|
const actualRouteSet = new Set(actualRoutes);
|
|
const portalLandingRouteMap = parsePortalLandingRoutes();
|
|
const originalPortal = process.env.TARO_APP_PORTAL;
|
|
const originalTaroEnv = process.env.TARO_ENV;
|
|
const portalConfigs = {
|
|
student: await loadAppConfig('student', 'h5'),
|
|
'tenant-admin': await loadAppConfig('tenant-admin', 'h5'),
|
|
'platform-admin': await loadAppConfig('platform-admin', 'h5'),
|
|
};
|
|
const studentWeappConfig = await loadAppConfig('student', 'weapp');
|
|
if (originalPortal === undefined) delete process.env.TARO_APP_PORTAL;
|
|
else process.env.TARO_APP_PORTAL = originalPortal;
|
|
if (originalTaroEnv === undefined) delete process.env.TARO_ENV;
|
|
else process.env.TARO_ENV = originalTaroEnv;
|
|
|
|
const sharedH5Routes = ['pages/bootstrap/index', 'pages/student/login/index'];
|
|
const expectedRoutesByPortal = {
|
|
student: actualRoutes.filter(route => route === 'pages/bootstrap/index' || route.startsWith('pages/student/')),
|
|
'tenant-admin': actualRoutes.filter(route => sharedH5Routes.includes(route) || route.startsWith('pages/tenant-admin/')),
|
|
'platform-admin': actualRoutes.filter(route => sharedH5Routes.includes(route) || route.startsWith('pages/platform-admin/')),
|
|
};
|
|
|
|
for (const [portal, config] of Object.entries(portalConfigs)) {
|
|
const routes = config.pages || [];
|
|
assert.equal(routes.length, new Set(routes).size, `${portal} H5 config must not contain duplicate page routes`);
|
|
assert.equal(routes[0], portalLandingRouteMap[portal], `${portal} H5 landing route must be the first page`);
|
|
assert.deepEqual(
|
|
uniqueSorted(routes),
|
|
uniqueSorted(expectedRoutesByPortal[portal]),
|
|
`${portal} H5 build must package only its own portal pages plus shared bootstrap/login pages`,
|
|
);
|
|
}
|
|
|
|
assert.deepEqual(studentWeappConfig.pages, ['pages/bootstrap/index'], 'Student WeApp main package must contain only the bootstrap page');
|
|
assert.equal(studentWeappConfig.subPackages?.length, 1, 'Student WeApp must use one stable student subpackage');
|
|
assert.equal(studentWeappConfig.subPackages?.[0]?.root, 'pages/student', 'Student WeApp subpackage root must preserve existing student routes');
|
|
assert.deepEqual(
|
|
uniqueSorted(expandedRoutes(studentWeappConfig)),
|
|
uniqueSorted(expectedRoutesByPortal.student),
|
|
'Student WeApp main package and subpackage must cover every student route without admin pages',
|
|
);
|
|
|
|
const appRoutes = uniqueSorted(Object.values(portalConfigs).flatMap(config => config.pages || []));
|
|
const appRouteSet = new Set(appRoutes);
|
|
|
|
assert.equal(appRoutes.length, appRouteSet.size, 'app.config.ts must not contain duplicate page routes');
|
|
|
|
const missingFiles = appRoutes.filter(route => !actualRouteSet.has(route));
|
|
const unregisteredPages = actualRoutes.filter(route => !appRouteSet.has(route));
|
|
assert.deepEqual(missingFiles, [], 'Every app.config.ts route must have a matching pages/**/index.tsx file');
|
|
assert.deepEqual(unregisteredPages, [], 'Every pages/**/index.tsx file must be registered in app.config.ts');
|
|
|
|
const expectedPortalLandingRoutes = [
|
|
'pages/platform-admin/workbench/index',
|
|
'pages/student/home/index',
|
|
'pages/tenant-admin/workbench/index',
|
|
];
|
|
assert.deepEqual(
|
|
uniqueSorted(Object.values(portalLandingRouteMap)),
|
|
expectedPortalLandingRoutes,
|
|
'Portal-specific H5 builds must put each portal landing route first',
|
|
);
|
|
for (const route of Object.values(portalLandingRouteMap)) {
|
|
assert.ok(appRouteSet.has(route), `Portal landing route is not registered in app.config.ts: ${route}`);
|
|
}
|
|
|
|
const bootstrapPageRoutes = parseLiteralPagePaths(readText(bootstrapPath));
|
|
const routeGuardPageRoutes = parseLiteralPagePaths(readText(routeGuardPath));
|
|
const publicRoutes = new Set([
|
|
'pages/bootstrap/index',
|
|
'pages/student/login/index',
|
|
'pages/student/region/index',
|
|
]);
|
|
const routeGuardLandingRoutes = routeGuardPageRoutes.filter(route => !publicRoutes.has(route));
|
|
assert.deepEqual(
|
|
uniqueSorted(routeGuardLandingRoutes),
|
|
expectedPortalLandingRoutes,
|
|
'Route guard landing routes must stay explicit for the three H5 portals',
|
|
);
|
|
for (const route of [...bootstrapPageRoutes, ...routeGuardPageRoutes]) {
|
|
assert.ok(appRouteSet.has(route), `Referenced route is not registered in app.config.ts: ${route}`);
|
|
}
|
|
|
|
const staticSmokeLandingRoutes = parseLiteralPagePaths(readText(h5StaticSmokePath));
|
|
assert.deepEqual(
|
|
uniqueSorted(staticSmokeLandingRoutes),
|
|
uniqueSorted(routeGuardLandingRoutes),
|
|
'H5 static smoke landing routes must match route guard landing routes',
|
|
);
|
|
for (const route of staticSmokeLandingRoutes) {
|
|
assert.ok(appRouteSet.has(route), `H5 static smoke landing route is not registered in app.config.ts: ${route}`);
|
|
}
|
|
|
|
const handoffRoutes = [...readText(frontendHandoffPath).matchAll(/apps\/taro\/src\/(pages\/[^`|\s]+\/index\.tsx)/g)]
|
|
.map(item => item[1].replace(/\/index\.tsx$/, '/index'));
|
|
for (const route of uniqueSorted(handoffRoutes)) {
|
|
assert.ok(appRouteSet.has(route), `Frontend handoff doc references a page that is not registered in app.config.ts: ${route}`);
|
|
assert.ok(actualRouteSet.has(route), `Frontend handoff doc references a page file that is missing: ${route}`);
|
|
}
|
|
|
|
console.log(`[PASS] Taro route contract (${appRoutes.length} registered pages; H5 portals cropped; WeApp student subpackage verified)`);
|