Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -220,8 +220,8 @@ jobs:
const mobileManifest = JSON.parse(fs.readFileSync('.lighthouseci-mobile/manifest.json', 'utf8'));
const desktopManifest = JSON.parse(fs.readFileSync('.lighthouseci-desktop/manifest.json', 'utf8'));

const mobile = mobileManifest[0];
const desktop = desktopManifest[0];
const snap = JSON.parse(fs.readFileSync('scripts/routes-snapshot.json', 'utf8'));
const base = 'http://localhost:4173';

// Thresholds must stay in sync with lhci-mobile.json / lhci-desktop.json.
// Performance is not gated in CI — GitHub-hosted runner scores vary too
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsx scripts/validate-content.ts && node scripts/check-sdk-size.mjs && tsc -b && vite build && node scripts/check-chunk-budget.mjs && node scripts/inline-css.js && node scripts/gen-blog-manifest.mjs && tsx scripts/og.ts && tsx scripts/sitemap.ts && node scripts/gen-rss.mjs",
"build": "tsx scripts/validate-content.ts && node scripts/check-sdk-size.mjs && tsc -b && vite build && node scripts/check-chunk-budget.mjs && node scripts/inline-css.js && node scripts/gen-blog-manifest.mjs && tsx scripts/og.ts && tsx scripts/sitemap.ts && node scripts/gen-rss.mjs && tsx scripts/gen-routes-snapshot.ts",
"check:chunk-budget": "node scripts/check-chunk-budget.mjs",
"generate:sdk-size": "node scripts/check-sdk-size.mjs --write",
"og:generate": "tsx scripts/og.ts",
Expand Down
78 changes: 78 additions & 0 deletions scripts/assert-route-coverage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/**
* Build-time assertion: every route registered in App.tsx must have an explicit
* quality-check policy in src/routes/inventory.ts.
*
* Run as part of `pnpm build` (via scripts/validate-content.ts, which imports
* this) so that adding a route to the router without a policy entry is a
* build error, not a silent omission.
*
* Strategy: parse the static path strings out of App.tsx with a regex rather
* than importing the module (which would require a full React environment) and
* compare them against the inventory.
*/

import { readFileSync } from 'fs';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
import { STATIC_ROUTES, DYNAMIC_ROUTE_FAMILIES, SPECIAL_ROUTES } from '../src/routes/inventory.js';

const __dirname = dirname(fileURLToPath(import.meta.url));
const rootDir = join(__dirname, '..');
const APP_PATH = join(rootDir, 'src', 'App.tsx');

/**
* Extract all `path="..."` values from App.tsx.
* Locale-prefixed variants built from template literals (e.g. `/${locale}/stellar`) are
* omitted — they are covered by the `localized` flag on the base path entry.
*/
function extractAppPaths(source: string): Set<string> {
const paths = new Set<string>();

const literalPattern = /\bpath=["']([^"'*]+?)["']/g;
let m: RegExpExecArray | null;
while ((m = literalPattern.exec(source)) !== null) {
paths.add(m[1]);
}

if (/\bpath=["']\*["']/.test(source)) {
paths.add('*');
}

// Remove locale-prefixed duplicates (/es/..., /pt/...) covered by the `localized` flag.
for (const p of paths) {
if (/^\/(es|pt)(\/|$)/.test(p)) {
paths.delete(p);
}
}

return paths;
}

export function assertAllAppRoutesHavePolicy(): void {
const source = readFileSync(APP_PATH, 'utf8');
const appPaths = extractAppPaths(source);

const inventoryPatterns = new Set<string>([
...STATIC_ROUTES.map((r) => r.path),
...DYNAMIC_ROUTE_FAMILIES.map((f) => f.pattern),
...SPECIAL_ROUTES.map((r) => r.pattern),
]);

const missing: string[] = [];
for (const p of appPaths) {
if (!inventoryPatterns.has(p)) {
missing.push(p);
}
}

if (missing.length > 0) {
const list = missing.map((p) => ` - ${p}`).join('\n');
throw new Error(
`Route inventory is out of date.\n` +
`The following routes exist in App.tsx but have no policy in src/routes/inventory.ts:\n` +
`${list}\n\n` +
`Add each missing route to STATIC_ROUTES, DYNAMIC_ROUTE_FAMILIES, or SPECIAL_ROUTES ` +
`in src/routes/inventory.ts and set its quality-check policy.`,
);
}
}
44 changes: 44 additions & 0 deletions scripts/gen-routes-snapshot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/**
* Writes scripts/routes-snapshot.json from the typed inventory so that
* plain-JS scripts (gen-sitemap.mjs) and CI YAML can consume it without
* needing TypeScript.
*
* Called automatically as part of `pnpm build` (see package.json).
*/
import { writeFileSync, mkdirSync } from 'fs';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
import {
STATIC_ROUTES,
DYNAMIC_ROUTE_FAMILIES,
SPECIAL_ROUTES,
smokeRoutes,
a11yRoutes,
lighthouseRoutes,
allStaticPaths,
localizedBasePaths,
} from '../src/routes/inventory.js';

const __dirname = dirname(fileURLToPath(import.meta.url));
const rootDir = join(__dirname, '..');

const snapshot = {
/** Static paths for the sitemap generator and OG pipeline. */
staticPaths: allStaticPaths(),
/** Paths that have /es/* and /pt/* variants. */
localizedBasePaths: Array.from(localizedBasePaths()),
/** Paths for the smoke test "still serves built site" assertion. */
smokeRoutes: smokeRoutes(),
/** Paths for axe / Playwright a11y checks. */
a11yRoutes: a11yRoutes(),
/** Paths for Lighthouse CI. */
lighthouseRoutes: lighthouseRoutes(),
/** Full inventory for tooling that needs policy detail. */
staticRoutes: STATIC_ROUTES,
dynamicRouteFamilies: DYNAMIC_ROUTE_FAMILIES,
specialRoutes: SPECIAL_ROUTES,
};

const outPath = join(rootDir, 'scripts', 'routes-snapshot.json');
writeFileSync(outPath, JSON.stringify(snapshot, null, 2) + '\n', 'utf8');
console.log(`routes-snapshot.json written (${snapshot.staticPaths.length} static paths)`);
52 changes: 36 additions & 16 deletions scripts/gen-sitemap.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,26 +10,43 @@ const publicDir = join(rootDir, 'public');
const siteUrl = 'https://usewraith.xyz';

/**
* Known static application routes
* Load the static route list from the pre-generated snapshot so this plain-JS
* script doesn't need to import TypeScript. The snapshot is written by
* `scripts/gen-routes-snapshot.ts` which runs before this script in the build.
*/
const knownRoutes = [
'/',
'/faq',
'/privacy',
'/use-cases',
'/roadmap',
'/case-studies',
'/stellar',
'/careers',
'/press',
];
function loadStaticPaths() {
const snapshotPath = join(rootDir, 'scripts', 'routes-snapshot.json');
if (!existsSync(snapshotPath)) {
console.warn(
'routes-snapshot.json not found — run `tsx scripts/gen-routes-snapshot.ts` first. ' +
'Falling back to empty static path list.',
);
return [];
}
try {
const snapshot = JSON.parse(readFileSync(snapshotPath, 'utf8'));
return Array.isArray(snapshot.staticPaths) ? snapshot.staticPaths : [];
} catch (err) {
console.warn('Could not parse routes-snapshot.json:', err.message);
return [];
}
}

/**
* Pages that have locale-prefixed alternates.
* Must stay in sync with LOCALE_URL_PREFIX in src/utils/seo.ts and the
* locale-prefixed <Route> entries in App.tsx.
* Read from the snapshot so this file stays in sync automatically.
*/
const LOCALE_PREFIXED_PAGES = ['/', '/stellar', '/grants', '/blog', '/case-studies'];
function loadLocalizedBasePaths() {
const snapshotPath = join(rootDir, 'scripts', 'routes-snapshot.json');
if (!existsSync(snapshotPath)) return ['/', '/stellar', '/grants', '/blog', '/case-studies'];
try {
const snapshot = JSON.parse(readFileSync(snapshotPath, 'utf8'));
return Array.isArray(snapshot.localizedBasePaths) ? snapshot.localizedBasePaths : [];
} catch {
return [];
}
}

const SUPPORTED_LOCALE_PREFIXES = ['/es', '/pt'];

/**
Expand Down Expand Up @@ -125,13 +142,16 @@ function getBlogTagRoutes() {

function generateSitemap() {
try {
const knownRoutes = loadStaticPaths();
const localizedPages = loadLocalizedBasePaths();

const csRoutes = getCaseStudyRoutes();
const distRoutes = existsSync(distDir) ? getDistRoutes(distDir) : [];
const tagRoutes = getBlogTagRoutes();

// Build locale-prefixed alternates for pages that have translations.
const localeRoutes = SUPPORTED_LOCALE_PREFIXES.flatMap((prefix) =>
LOCALE_PREFIXED_PAGES.map((page) => (page === '/' ? prefix : `${prefix}${page}`)),
localizedPages.map((page) => (page === '/' ? prefix : `${prefix}${page}`)),
);

const allRoutes = Array.from(
Expand Down
Loading
Loading