import console from "node:console"; import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs"; import { createRequire } from "node:module"; import { dirname, join, relative, resolve } from "node:path"; import process from "node:process"; import { fileURLToPath } from "node:url"; const root = resolve(dirname(fileURLToPath(import.meta.url)), ".."); const require = createRequire(import.meta.url); const componentPackages = new Set([ "auth", "captcha", "i18n", "image", "realtime", "ui", "uploader", "validation", ]); const requestedHelperFiles = { auth: "src/helpers.ts", captcha: "src/helpers.ts", db: "src/helpers.ts", encryption: "src/http.ts", i18n: "src/index.ts", image: "src/index.ts", jwt: "src/helpers.ts", realtime: "src/messages.ts", uploader: "src/helpers.ts", validation: "src/helpers.ts", }; function writeIfChanged(path, source) { if (existsSync(path) && readFileSync(path, "utf8") === source) return false; writeFileSync(path, source, "utf8"); return true; } function walkFiles(directory, pattern) { const files = []; if (!existsSync(directory)) return files; const visit = (current) => { for (const entry of readdirSync(current, { withFileTypes: true })) { const path = join(current, entry.name); if (entry.isDirectory()) visit(path); else if (entry.isFile() && pattern.test(entry.name)) files.push(path); } }; visit(directory); return files.sort(); } function helperIsPublic(entrySource, helperFile, main) { if (!helperFile || helperFile === main) return true; const relativeHelper = `./${relative(dirname(main), helperFile).replace(/\\/g, "/").replace(/\.ts$/, ".ts")}`; const withoutExtension = relativeHelper.replace(/\.ts$/, ""); return entrySource.includes(relativeHelper) || entrySource.includes(withoutExtension); } const compiler = require(join(root, "editors", "vscode", "src", "compiler.cjs")); const uiCatalog = JSON.parse( readFileSync(join(root, "packages", "ui", "component-catalog.json"), "utf8"), ); const uiComponentNames = new Set(uiCatalog.components.map((entry) => entry.name)); const packageDirectories = readdirSync(join(root, "packages"), { withFileTypes: true }) .filter( (entry) => entry.isDirectory() && existsSync(join(root, "packages", entry.name, "package.json")), ) .map((entry) => entry.name) .sort(); const packages = []; const failures = []; let componentCount = 0; let helperPackageCount = 0; for (const directory of packageDirectories) { const packageRoot = join(root, "packages", directory); const manifest = JSON.parse(readFileSync(join(packageRoot, "package.json"), "utf8")); const main = String(manifest.main ?? manifest.exports?.["."] ?? "src/index.ts").replace( /^\.\//, "", ); const entry = join(packageRoot, main); const entrySource = existsSync(entry) ? readFileSync(entry, "utf8") : ""; const componentsDir = join(packageRoot, "components"); const components = existsSync(componentsDir) ? readdirSync(componentsDir) .filter((name) => name.endsWith(".wrn")) .sort() : []; const tests = walkFiles(join(packageRoot, "test"), /\.(?:test|spec)\.[cm]?[jt]s$/).map((path) => relative(join(packageRoot, "test"), path).replace(/\\/g, "/"), ); const sourceFiles = walkFiles(join(packageRoot, "src"), /\.[cm]?ts$/); const allSource = sourceFiles.map((path) => readFileSync(path, "utf8")).join("\n"); const exported = [ ...allSource.matchAll( /\bexport\s+(?:declare\s+)?(?:async\s+)?(?:function|class|const|let|var|interface|type|enum)\s+([A-Za-z_$][\w$]*)/g, ), ].map((match) => match[1]); const helperFile = requestedHelperFiles[directory]; const hasHelperKit = helperFile ? existsSync(join(packageRoot, helperFile)) : exported.length > 0 || /export\s+\{/.test(allSource) || /export\s+\*/.test(allSource) || directory === "cli"; const publicHelper = helperIsPublic(entrySource, helperFile, main); if (hasHelperKit) helperPackageCount++; componentCount += components.length; if (manifest.version !== "0.8.0") failures.push(`${manifest.name}: version is ${manifest.version}`); if (!existsSync(entry)) failures.push(`${manifest.name}: missing public entry ${main}`); if (!existsSync(join(packageRoot, "README.md"))) failures.push(`${manifest.name}: missing README.md`); if (!tests.length) failures.push(`${manifest.name}: missing package test`); if (!hasHelperKit) failures.push(`${manifest.name}: no public helper/API surface found`); if (!publicHelper) failures.push(`${manifest.name}: ${helperFile} is not exported by ${main}`); const usedUiComponents = new Set(); const componentFailures = []; for (const component of components) { const file = join(componentsDir, component); const source = readFileSync(file, "utf8"); for (const match of source.matchAll(/<([A-Z][A-Za-z0-9]*)\b/g)) { if (uiComponentNames.has(match[1])) usedUiComponents.add(match[1]); } try { compiler.compileWireFile(source); } catch (error) { componentFailures.push( `${component}: ${error instanceof Error ? error.message : String(error)}`, ); } } if (componentPackages.has(directory)) { if (!components.length) failures.push(`${manifest.name}: component package has no .wrn components`); if (directory !== "ui" && manifest.dependencies?.["@wrnexus/ui"] !== "workspace:*") { failures.push(`${manifest.name}: component package must depend on @wrnexus/ui`); } if (directory !== "ui" && usedUiComponents.size === 0) { failures.push(`${manifest.name}: package blocks do not compose any @wrnexus/ui component`); } if (directory !== "ui" && !manifest.exports?.["./plugin"]) { failures.push(`${manifest.name}: component package must export ./plugin`); } } for (const issue of componentFailures) failures.push(`${manifest.name}: ${issue}`); packages.push({ name: manifest.name, directory, version: manifest.version, kind: componentPackages.has(directory) ? "component-kit" : "helper-kit", entry: relative(root, entry).replace(/\\/g, "/"), helperFile: helperFile ?? null, helperIsPublic: publicHelper, publicExportCount: exported.length, components, uiComponents: [...usedUiComponents].sort(), tests, readme: existsSync(join(packageRoot, "README.md")), usesUi: directory === "ui" || manifest.dependencies?.["@wrnexus/ui"] === "workspace:*", }); } const authSource = readdirSync(join(root, "packages", "auth", "components")) .filter((name) => name.endsWith(".wrn")) .map((name) => readFileSync(join(root, "packages", "auth", "components", name), "utf8")) .join("\n"); if (/<(?:button|select|textarea)\b/.test(authSource)) { failures.push("@wrnexus/auth: visible native controls must use @wrnexus/ui components"); } for (const match of authSource.matchAll(//g)) { if (!/\btype=(?:"hidden"|'hidden')/i.test(match[0])) { failures.push( "@wrnexus/auth: visible native input must use @wrnexus/ui Input/PinInput components", ); break; } } const report = { version: "0.8.0", summary: { packageCount: packages.length, helperPackageCount, componentPackageCount: packages.filter((entry) => entry.kind === "component-kit").length, componentCount, failures: failures.length, }, packages, failures, }; const markdown = [ "# WRNexusJS 0.8.0 Package Kits", "", "Every framework package has a public API/helper surface, package tests, and documentation. Packages that directly render developer-facing UI additionally provide package-owned `.wrn` blocks composed from `@wrnexus/ui`.", "", `- Packages: ${report.summary.packageCount}`, `- Helper/API kits: ${report.summary.helperPackageCount}`, `- Component kits: ${report.summary.componentPackageCount}`, `- Package-owned components: ${report.summary.componentCount}`, "", "| Package | Kit | Helper/API entry | Components | UI composition | Tests |", "|---|---|---|---:|---|---:|", ...packages.map( (entry) => `| \`${entry.name}\` | ${entry.kind} | ${entry.helperFile ? `\`${entry.helperFile}\`` : `\`${entry.entry}\``} | ${entry.components.length} | ${entry.uiComponents.length ? entry.uiComponents.map((name) => `\`${name}\``).join(", ") : "—"} | ${entry.tests.length} |`, ), "", "## Component-kit rule", "", "Infrastructure-only packages remain helper/API kits and do not pull browser UI into server code. UI-facing packages own complete blocks and register them through their package plugin.", "", "## Security boundary for encrypted HTTP bodies", "", "`@wrnexus/encryption` encrypted HTTP envelopes add application-layer confidentiality, replay checks, and method/path/request binding. They do not replace HTTPS and cannot hide data from a browser user who receives the decryption key. Use them for service-to-service, native/mobile, controlled agents, or field-level protection with server-managed keys.", "", failures.length ? "## Failures\n\n" + failures.map((failure) => `- ${failure}`).join("\n") + "\n" : "## Audit result\n\nAll package-kit requirements pass.\n", ].join("\n"); if (process.argv.includes("--write")) { const reportDir = join(root, ".wrnexus", "reports"); mkdirSync(reportDir, { recursive: true }); writeIfChanged( join(reportDir, "PACKAGE-KITS-0.8.0.json"), `${JSON.stringify(report, null, 2)}\n`, ); writeIfChanged(join(reportDir, "PACKAGE-KITS-0.8.0.md"), `${markdown}\n`); } console.log(JSON.stringify(report.summary, null, 2)); for (const failure of failures) console.error(`FAIL ${failure}`); if (failures.length) process.exitCode = 1;