mirror of
https://github.com/zoriya/v10.git
synced 2026-08-05 13:48:14 +00:00
171 lines
5.0 KiB
JavaScript
171 lines
5.0 KiB
JavaScript
/**
|
|
* Validates that all server entry points are safe to import in a non-browser
|
|
* environment.
|
|
*
|
|
* Auto-discovers packages from `packages/`, reads their `exports` field to find
|
|
* entries with a `browser`/`default` condition split, resolves the `default`
|
|
* (server) path, and dynamically imports each one. Any module that throws
|
|
* during evaluation (e.g. due to missing browser globals) is reported.
|
|
*
|
|
* Requires packages to be built first (`pnpm build:packages`).
|
|
*
|
|
* Usage: node .github/scripts/ssr-check.js
|
|
*/
|
|
|
|
import {
|
|
existsSync,
|
|
readFileSync,
|
|
readdirSync,
|
|
} from 'node:fs';
|
|
import { resolve, join } from 'node:path';
|
|
import { pathToFileURL, fileURLToPath } from 'node:url';
|
|
|
|
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
|
const ROOT = resolve(__dirname, '../..');
|
|
const PACKAGES_DIR = join(ROOT, 'packages');
|
|
|
|
const SKIP_PACKAGES = new Set([
|
|
'react-native',
|
|
'skins',
|
|
'icons',
|
|
]);
|
|
|
|
/**
|
|
* Resolve the server path from an export value.
|
|
*
|
|
* Only returns a path for entries with a `browser`/`default` split (these are
|
|
* the ones that have separate server builds). The top-level `default` is the
|
|
* server entry point.
|
|
*/
|
|
function resolveServerPath(exportValue) {
|
|
if (typeof exportValue !== 'object' || exportValue === null) return null;
|
|
if (!('browser' in exportValue)) return null;
|
|
return typeof exportValue.default === 'string' ? exportValue.default : null;
|
|
}
|
|
|
|
/**
|
|
* Resolve a wildcard export key to actual server files on disk.
|
|
*/
|
|
function resolveWildcard(pkgDir, exportKey, exportValue) {
|
|
const serverPath = resolveServerPath(exportValue);
|
|
if (!serverPath) return [];
|
|
|
|
const fullPattern = resolve(pkgDir, serverPath);
|
|
const starIdx = fullPattern.indexOf('*');
|
|
if (starIdx === -1) return [];
|
|
|
|
const lastSlash = fullPattern.lastIndexOf('/', starIdx);
|
|
const scanDir = fullPattern.slice(0, lastSlash);
|
|
const prefix = fullPattern.slice(lastSlash + 1, starIdx);
|
|
const suffix = fullPattern.slice(starIdx + 1);
|
|
|
|
if (!existsSync(scanDir)) return [];
|
|
|
|
const isDirectoryPattern = suffix.startsWith('/');
|
|
|
|
return readdirSync(scanDir, { withFileTypes: true })
|
|
.filter((d) => {
|
|
if (!d.name.startsWith(prefix)) return false;
|
|
return isDirectoryPattern ? d.isDirectory() : d.isFile();
|
|
})
|
|
.filter((d) => {
|
|
if (isDirectoryPattern) return true;
|
|
return d.name.endsWith(suffix);
|
|
})
|
|
.map((d) => {
|
|
const stem = isDirectoryPattern
|
|
? d.name.slice(prefix.length)
|
|
: d.name.slice(prefix.length, d.name.length - suffix.length);
|
|
const fullPath = fullPattern.replace('*', stem);
|
|
return { stem, fullPath };
|
|
})
|
|
.filter(({ fullPath }) => {
|
|
if (!existsSync(fullPath)) return false;
|
|
return readFileSync(fullPath, 'utf8').trim().length > 0;
|
|
})
|
|
.sort((a, b) => a.stem.localeCompare(b.stem))
|
|
.map(({ stem, fullPath }) => ({
|
|
label: `${exportKey.replace('*', stem)}`,
|
|
path: fullPath,
|
|
}));
|
|
}
|
|
|
|
/** Discover all server entry points across packages. */
|
|
function discoverServerEntries() {
|
|
const entries = [];
|
|
|
|
for (const dirName of readdirSync(PACKAGES_DIR).sort()) {
|
|
if (SKIP_PACKAGES.has(dirName)) continue;
|
|
|
|
const pkgJsonPath = join(PACKAGES_DIR, dirName, 'package.json');
|
|
if (!existsSync(pkgJsonPath)) continue;
|
|
|
|
const pkgJson = JSON.parse(readFileSync(pkgJsonPath, 'utf8'));
|
|
if (!pkgJson.exports) continue;
|
|
|
|
const pkgName = pkgJson.name;
|
|
const pkgDir = join(PACKAGES_DIR, dirName);
|
|
|
|
for (const [key, value] of Object.entries(pkgJson.exports)) {
|
|
if (key.endsWith('.css')) continue;
|
|
|
|
if (key.includes('*')) {
|
|
for (const r of resolveWildcard(pkgDir, key, value)) {
|
|
entries.push({
|
|
name: `${pkgName}/${r.label.slice(2)}`,
|
|
path: r.path,
|
|
});
|
|
}
|
|
continue;
|
|
}
|
|
|
|
const serverPath = resolveServerPath(value);
|
|
if (!serverPath) continue;
|
|
|
|
const absolutePath = resolve(pkgDir, serverPath);
|
|
if (!existsSync(absolutePath)) continue;
|
|
if (readFileSync(absolutePath, 'utf8').trim().length === 0) continue;
|
|
|
|
const label = key === '.' ? pkgName : `${pkgName}/${key.slice(2)}`;
|
|
entries.push({ name: label, path: absolutePath });
|
|
}
|
|
}
|
|
|
|
return entries;
|
|
}
|
|
|
|
const entries = discoverServerEntries();
|
|
|
|
if (entries.length === 0) {
|
|
console.error('No server entry points found. Are packages built?');
|
|
process.exit(1);
|
|
}
|
|
|
|
console.log(`Checking ${entries.length} server entry points...\n`);
|
|
|
|
const failures = [];
|
|
|
|
for (const entry of entries) {
|
|
const url = pathToFileURL(entry.path).href;
|
|
try {
|
|
await import(url);
|
|
console.log(` ✓ ${entry.name}`);
|
|
} catch (error) {
|
|
console.log(` ✗ ${entry.name}`);
|
|
failures.push({ name: entry.name, error });
|
|
}
|
|
}
|
|
|
|
console.log();
|
|
|
|
if (failures.length > 0) {
|
|
console.error(`${failures.length} entry point(s) failed:\n`);
|
|
for (const { name, error } of failures) {
|
|
console.error(` ${name}:`);
|
|
console.error(` ${error.message}\n`);
|
|
}
|
|
process.exit(1);
|
|
}
|
|
|
|
console.log(`All ${entries.length} server entry points imported successfully.`);
|