Files
gongxue-base/scripts/taro-route-contract-test.js

131 lines
5.2 KiB
JavaScript

import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
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 parseAppRoutes() {
const text = readText(appConfigPath);
const match = text.match(/const\s+allPageRoutes\s*=\s*\[([\s\S]*?)\]/m);
assert.ok(match, 'apps/taro/src/app.config.ts must define const allPageRoutes = [...]');
return [...match[1].matchAll(/['"`]([^'"`]+)['"`]/g)]
.map(item => item[1].trim())
.filter(route => route.startsWith('pages/'));
}
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();
}
const appRoutes = parseAppRoutes();
const appRouteSet = new Set(appRoutes);
const actualRoutes = walkIndexPages(pagesRoot).map(routeFromIndexFile);
const actualRouteSet = new Set(actualRoutes);
const portalLandingRouteMap = parsePortalLandingRoutes();
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)`);