102 lines
4.0 KiB
JavaScript
102 lines
4.0 KiB
JavaScript
import { readdir, readFile } from 'node:fs/promises';
|
|
import path from 'node:path';
|
|
import process from 'node:process';
|
|
|
|
const root = process.cwd();
|
|
const sourceRoots = ['app', 'packages'];
|
|
const inventoryPath = path.join(root, 'docs', 'ref-compatibility-shims.md');
|
|
const beginPattern = /REF-COMPAT:BEGIN ([a-z0-9]+(?:-[a-z0-9]+)*)/g;
|
|
const endPattern = /REF-COMPAT:END ([a-z0-9]+(?:-[a-z0-9]+)*)/g;
|
|
const inventoryPattern = /<!-- REF-COMPAT-ID: ([a-z0-9]+(?:-[a-z0-9]+)*) -->/g;
|
|
const sourceExtensions = new Set(['.ts', '.tsx', '.vue', '.js', '.mjs', '.cjs']);
|
|
const ignoredDirectories = new Set(['node_modules', 'dist', 'coverage', '.turbo']);
|
|
|
|
const collectFiles = async (directory) => {
|
|
const entries = await readdir(directory, { withFileTypes: true });
|
|
const files = [];
|
|
for (const entry of entries) {
|
|
const absolute = path.join(directory, entry.name);
|
|
if (entry.isDirectory()) {
|
|
if (ignoredDirectories.has(entry.name)) {
|
|
continue;
|
|
}
|
|
files.push(...(await collectFiles(absolute)));
|
|
} else if (sourceExtensions.has(path.extname(entry.name))) {
|
|
files.push(absolute);
|
|
}
|
|
}
|
|
return files;
|
|
};
|
|
|
|
const fail = (message) => {
|
|
process.stderr.write(`ref compatibility marker check failed: ${message}\n`);
|
|
process.exitCode = 1;
|
|
};
|
|
|
|
const regionsById = new Map();
|
|
for (const sourceRoot of sourceRoots) {
|
|
for (const file of await collectFiles(path.join(root, sourceRoot))) {
|
|
const relative = path.relative(root, file);
|
|
const lines = (await readFile(file, 'utf8')).split(/\r?\n/);
|
|
let openRegion = null;
|
|
for (let index = 0; index < lines.length; index += 1) {
|
|
const lineNumber = index + 1;
|
|
const begins = [...lines[index].matchAll(beginPattern)].map((match) => match[1]);
|
|
const ends = [...lines[index].matchAll(endPattern)].map((match) => match[1]);
|
|
if (begins.length + ends.length > 1) {
|
|
fail(`${relative}:${lineNumber} contains more than one marker`);
|
|
continue;
|
|
}
|
|
if (begins.length === 1) {
|
|
if (openRegion) {
|
|
fail(`${relative}:${lineNumber} nests ${begins[0]} inside ${openRegion.id}`);
|
|
} else {
|
|
openRegion = { id: begins[0], line: lineNumber };
|
|
}
|
|
}
|
|
if (ends.length === 1) {
|
|
if (!openRegion) {
|
|
fail(`${relative}:${lineNumber} closes ${ends[0]} without a BEGIN marker`);
|
|
} else if (openRegion.id !== ends[0]) {
|
|
fail(`${relative}:${lineNumber} closes ${ends[0]} but ${openRegion.id} is open`);
|
|
} else {
|
|
const regions = regionsById.get(openRegion.id) ?? [];
|
|
regions.push(`${relative}:${openRegion.line}-${lineNumber}`);
|
|
regionsById.set(openRegion.id, regions);
|
|
openRegion = null;
|
|
}
|
|
}
|
|
}
|
|
if (openRegion) {
|
|
fail(`${relative}:${openRegion.line} leaves ${openRegion.id} open`);
|
|
}
|
|
}
|
|
}
|
|
|
|
const inventory = await readFile(inventoryPath, 'utf8');
|
|
const inventoryIds = [...inventory.matchAll(inventoryPattern)].map((match) => match[1]);
|
|
const uniqueInventoryIds = new Set(inventoryIds);
|
|
if (uniqueInventoryIds.size !== inventoryIds.length) {
|
|
fail('the inventory contains a duplicate REF-COMPAT-ID');
|
|
}
|
|
|
|
for (const id of regionsById.keys()) {
|
|
if (!uniqueInventoryIds.has(id)) {
|
|
fail(`${id} is marked in source but missing from docs/ref-compatibility-shims.md`);
|
|
}
|
|
}
|
|
for (const id of uniqueInventoryIds) {
|
|
if (!regionsById.has(id)) {
|
|
fail(`${id} is documented but has no source region`);
|
|
}
|
|
}
|
|
|
|
if (process.exitCode) {
|
|
process.exit(process.exitCode);
|
|
}
|
|
|
|
const regionCount = [...regionsById.values()].reduce((sum, regions) => sum + regions.length, 0);
|
|
process.stdout.write(
|
|
`ref compatibility markers: ${regionsById.size} ids, ${regionCount} source regions, inventory synchronized\n`
|
|
);
|