#!/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)`);