124 lines
4.0 KiB
JavaScript
124 lines
4.0 KiB
JavaScript
#!/usr/bin/env node
|
|
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
import { join, relative, resolve, sep } from "node:path";
|
|
import console from "node:console";
|
|
import process from "node:process";
|
|
import { URL, fileURLToPath } from "node:url";
|
|
|
|
const root = resolve(fileURLToPath(new URL("..", import.meta.url)));
|
|
const packagesDir = join(root, "packages");
|
|
const BUILT_INS = new Set([
|
|
"Async",
|
|
"Loading",
|
|
"Success",
|
|
"Error",
|
|
"Static",
|
|
"Dynamic",
|
|
"Portal",
|
|
"Transition",
|
|
"Component",
|
|
"KeepAlive",
|
|
"Slot",
|
|
]);
|
|
|
|
const IGNORED_DIRECTORIES = new Set([
|
|
"node_modules",
|
|
"dist",
|
|
"coverage",
|
|
".git",
|
|
".wrnexus",
|
|
".publish",
|
|
]);
|
|
|
|
function walk(directory, extension, output = []) {
|
|
if (!existsSync(directory)) return output;
|
|
for (const name of readdirSync(directory)) {
|
|
if (IGNORED_DIRECTORIES.has(name)) continue;
|
|
const file = join(directory, name);
|
|
const stat = statSync(file);
|
|
if (stat.isDirectory()) walk(file, extension, output);
|
|
else if (file.endsWith(extension)) output.push(file);
|
|
}
|
|
return output;
|
|
}
|
|
|
|
const packageManifests = new Map();
|
|
for (const name of readdirSync(packagesDir)) {
|
|
const manifestPath = join(packagesDir, name, "package.json");
|
|
if (!existsSync(manifestPath)) continue;
|
|
const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
packageManifests.set(name, {
|
|
name: manifest.name,
|
|
dependencies: new Set([
|
|
...Object.keys(manifest.dependencies ?? {}),
|
|
...Object.keys(manifest.peerDependencies ?? {}),
|
|
...Object.keys(manifest.optionalDependencies ?? {}),
|
|
]),
|
|
});
|
|
}
|
|
|
|
const files = walk(packagesDir, ".wrn");
|
|
const declared = new Map();
|
|
for (const file of files) {
|
|
const source = readFileSync(file, "utf8");
|
|
const match = /\b(?:component|layout|page)\s+([A-Z][A-Za-z0-9_$]*)\b/.exec(source);
|
|
if (match) declared.set(match[1], file);
|
|
}
|
|
|
|
const failures = [];
|
|
for (const file of files) {
|
|
const source = readFileSync(file, "utf8");
|
|
const packageName = relative(packagesDir, file).split(sep)[0];
|
|
const manifest = packageManifests.get(packageName);
|
|
for (const match of source.matchAll(/\bfrom\s+["'](@wrnexus\/[^"']+)["']/g)) {
|
|
const dependency = match[1].split("/").slice(0, 2).join("/");
|
|
if (dependency === manifest?.name || manifest?.dependencies.has(dependency)) continue;
|
|
failures.push(
|
|
`${relative(root, file).replace(/\\/g, "/")}: ${dependency} must be declared as a package dependency`,
|
|
);
|
|
}
|
|
const imported = new Set();
|
|
for (const match of source.matchAll(
|
|
/^\s*import\s+([A-Z][A-Za-z0-9_$]*)\s+from\s+["'][^"']+["']/gm,
|
|
)) {
|
|
imported.add(match[1]);
|
|
}
|
|
for (const match of source.matchAll(/^\s*import\s*\{([^}]+)\}\s*from\s*["'][^"']+["']/gm)) {
|
|
for (const part of match[1].split(",")) {
|
|
const binding = part
|
|
.trim()
|
|
.replace(/^type\s+/, "")
|
|
.split(/\s+as\s+/)
|
|
.pop()
|
|
?.trim();
|
|
if (binding) imported.add(binding);
|
|
}
|
|
}
|
|
const own = /\b(?:component|layout|page)\s+([A-Z][A-Za-z0-9_$]*)\b/.exec(source)?.[1];
|
|
const used = new Set([...source.matchAll(/<([A-Z][A-Za-z0-9_$]*)\b/g)].map((match) => match[1]));
|
|
for (const name of used) {
|
|
if (name === own || BUILT_INS.has(name) || !declared.has(name) || imported.has(name)) continue;
|
|
failures.push(
|
|
`${relative(root, file).replace(/\\/g, "/")}: <${name}> requires an explicit import`,
|
|
);
|
|
}
|
|
const layoutSymbol = /\blayout\s*=\s*([A-Z][A-Za-z0-9_$]*)\b/.exec(source)?.[1];
|
|
if (
|
|
layoutSymbol &&
|
|
layoutSymbol !== own &&
|
|
declared.has(layoutSymbol) &&
|
|
!imported.has(layoutSymbol)
|
|
) {
|
|
failures.push(
|
|
`${relative(root, file).replace(/\\/g, "/")}: layout ${layoutSymbol} requires an explicit import`,
|
|
);
|
|
}
|
|
}
|
|
|
|
if (failures.length) {
|
|
console.error(`WRN-IMPORT-PACKAGE: ${failures.length} package component import error(s):`);
|
|
for (const failure of failures) console.error(`- ${failure}`);
|
|
process.exit(1);
|
|
}
|
|
console.log(`✓ Package component imports are explicit (${files.length} WRN files checked)`);
|