#!/usr/bin/env node import console from "node:console"; import { existsSync, readFileSync, readdirSync, statSync } from "node:fs"; import { dirname, join, relative } from "node:path"; import process from "node:process"; import { spawnSync } from "node:child_process"; import { fileURLToPath } from "node:url"; import { createRequire } from "node:module"; const root = join(dirname(fileURLToPath(import.meta.url)), ".."); const require = createRequire(import.meta.url); const failures = []; const warnings = []; const passes = []; const fail = (message) => failures.push(message); const pass = (message) => passes.push(message); const readJson = (path) => JSON.parse(readFileSync(path, "utf8")); function walk(dir, predicate = () => true) { if (!existsSync(dir)) return []; const out = []; for (const name of readdirSync(dir)) { if (["node_modules", ".git", "dist", ".wrnexus"].includes(name)) continue; const path = join(dir, name); const stat = statSync(path); if (stat.isDirectory()) out.push(...walk(path, predicate)); else if (predicate(path)) out.push(path); } return out; } const packageDirs = readdirSync(join(root, "packages")) .filter((name) => existsSync(join(root, "packages", name, "package.json"))) .sort(); if (packageDirs.length !== 33) fail(`Expected 33 framework packages, found ${packageDirs.length}`); else pass("33 framework packages are present"); for (const name of packageDirs) { const manifest = readJson(join(root, "packages", name, "package.json")); if (manifest.version !== "0.6.0") fail(`packages/${name} is ${manifest.version ?? "unversioned"}`); } if (!failures.some((item) => item.startsWith("packages/"))) pass("All framework packages are version 0.6.0"); for (const [label, path] of [ ["root", "package.json"], ["VS Code extension", "editors/vscode/package.json"], ["managed CAPTCHA service", "services/managed-captcha/package.json"], ]) { const version = readJson(join(root, path)).version; if (version !== "0.6.0") fail(`${label} version is ${version}`); else pass(`${label} version is 0.6.0`); } for (const path of [ "packages/store/src/index.ts", "packages/typecheck/src/index.ts", "packages/syntax/src/v060.ts", "packages/compiler/src/client-codegen.ts", "packages/compiler/src/server-codegen.ts", "packages/compiler/src/type-codegen.ts", "packages/compiler/src/store-codegen.ts", "packages/csr/src/outputs.ts", "packages/csr/src/server-client.ts", "packages/ssr/src/store-context.ts", "packages/ssr/src/rpc.ts", "UPGRADE-0.6.0.md", "ROLLBACK-0.6.0.md", "RELEASE_NOTES-0.6.0.md", ]) { if (!existsSync(join(root, path))) fail(`Missing required v0.6 file: ${path}`); } const syntaxManifest = readJson(join(root, "packages/syntax/package.json")); const syntaxIndexSource = readFileSync(join(root, "packages/syntax/src/index.ts"), "utf8"); const compilerCodegenSource = readFileSync(join(root, "packages/compiler/src/codegen.ts"), "utf8"); const publicV060ImportPattern = /(?:\bfrom\s*|\brequire\s*\()\s*["']@wrnexus\/syntax\/v060["']/; const publicV060Imports = walk(root, (path) => /\.(?:ts|tsx|js|mjs|cjs)$/.test(path)).filter( (path) => publicV060ImportPattern.test(readFileSync(path, "utf8")), ); if (syntaxManifest.exports?.["./v060"]) fail("@wrnexus/syntax still exposes the version-specific ./v060 subpath"); if (!syntaxIndexSource.includes("stripRuntimeFunctionModifiers")) fail("@wrnexus/syntax root does not re-export v0.6 helpers"); if (!compilerCodegenSource.includes('from "@wrnexus/syntax"')) fail("compiler does not import syntax helpers from @wrnexus/syntax"); for (const path of publicV060Imports) fail(`${relative(root, path)} imports the forbidden @wrnexus/syntax/v060 subpath`); if ( !failures.some( (item) => item.includes("syntax/v060") || item.includes("root does not re-export") || item.includes("compiler does not import syntax helpers"), ) ) { pass("v0.6 syntax helpers are exposed through @wrnexus/syntax only"); } const parserSource = readFileSync(join(root, "packages/syntax/src/v060.ts"), "utf8"); for (const marker of [ "parseOutputs", "parseStructuredImports", "parseStateDeclarations", "parseRuntimeFunctions", "parseStoreLifecycle", ]) { if (!parserSource.includes(marker)) fail(`v0.6 parser helper missing: ${marker}`); } if ( ![ "parseOutputs", "parseStructuredImports", "parseStateDeclarations", "parseRuntimeFunctions", "parseStoreLifecycle", ].some((marker) => !parserSource.includes(marker)) ) { pass("v0.6 parser helpers are present"); } const uiFiles = walk(join(root, "packages/ui/components"), (path) => path.endsWith(".wrn")); const forbidden = [ ["$emit", /\$emit\s*\(/], ["legacy @event", /^\s*@event\b/m], ["$event", /\$event\b/], ["event.detail", /\bevent\.detail\b/], ["unclassified function", /^\s*(?:async\s+)?function\s+[A-Za-z_$]/m], ]; for (const file of uiFiles) { const source = readFileSync(file, "utf8"); for (const [label, pattern] of forbidden) { if (pattern.test(source)) fail(`${relative(root, file)} contains ${label}`); } } if (!failures.some((item) => item.startsWith("packages/ui/components/"))) { pass(`${uiFiles.length} UI components use classified functions and typed output declarations`); } function outputBlocks(source) { const blocks = []; for (const match of source.matchAll(/\boutputs\s*\{/g)) { const brace = source.indexOf("{", match.index); let depth = 0; let quote = ""; for (let index = brace; index < source.length; index++) { const char = source[index]; if (quote) { if (char === "\\") index++; else if (char === quote) quote = ""; continue; } if (char === '"' || char === "'" || char === "`") quote = char; else if (char === "{") depth++; else if (char === "}" && --depth === 0) { blocks.push(source.slice(brace + 1, index)); break; } } } return blocks; } for (const file of uiFiles) { const source = readFileSync(file, "utf8"); if (outputBlocks(source).some((block) => /\bunknown\b/.test(block))) { fail(`${relative(root, file)} contains an untyped output payload contract`); } } if (!failures.some((item) => item.includes("untyped output payload contract"))) { pass("All UI output payload contracts are concrete and contain no unknown types"); } for (const [path, markers] of [ ["packages/compiler/src/client-codegen.ts", ["RESERVED_BINDINGS", "!parameterNames.has"]], [ "packages/compiler/src/store-codegen.ts", [ "const server = context.server", "persist.migrate", "persist.validate", "__wrnexusApplyStoreHotUpdate", ], ], ["packages/compiler/src/server-codegen.ts", ["remotelyReferencedServerFunctions"]], [ "packages/dev-server/src/pipeline.ts", ["setCompileImportOptions", 'mode: "compatible"', "WRN-IMPORT-IMPLICIT"], ], ["packages/dev-server/src/runtime.ts", ["store-update", "__wrnexusApplyStoreHotUpdate"]], ]) { const source = readFileSync(join(root, path), "utf8"); for (const marker of markers) if (!source.includes(marker)) fail(`${path} is missing focused fix marker: ${marker}`); } if (!failures.some((item) => item.includes("focused fix marker"))) { pass("Focused fixes 1-7 are wired into compiler, store, import, RPC, and HMR sources"); } const storeTypesSource = readFileSync(join(root, "packages/store/src/types.ts"), "utf8"); const storeRuntimeSource = readFileSync(join(root, "packages/store/src/index.ts"), "utf8"); const storeCodegenSource = readFileSync( join(root, "packages/compiler/src/store-codegen.ts"), "utf8", ); for (const [label, condition] of [ [ "separate client/server state generics", /StoreCombinedState<[\s\S]*?CS extends object[\s\S]*?SS extends object/.test( storeTypesSource, ) && /createClientState\?: \(\) => CS/.test(storeTypesSource) && /createServerState\?: \(\) => SS/.test(storeTypesSource), ], [ "callable action default", /StoreFunction = \(\.\.\.args: any\[\]\) => any/.test(storeTypesSource) && /Record/.test(storeTypesSource), ], [ "non-colliding initialization promise", /readonly whenReady: Promise/.test(storeTypesSource) && !/readonly ready: Promise/.test(storeTypesSource) && /await instance\.whenReady/.test(storeRuntimeSource), ], [ "browser store whenReady code generation", /whenReady: Promise\.resolve\(\)/.test(storeCodegenSource) && /core\.whenReady = init\(\)/.test(storeCodegenSource), ], ]) { if (!condition) fail(`Store type regression: missing ${label}`); } if (!failures.some((item) => item.startsWith("Store type regression:"))) { pass( "Store client/server state, callable actions, and ready-state collision regressions are guarded", ); } const reactiveRuntimeSource = readFileSync( join(root, "packages/csr/src/reactive-runtime.ts"), "utf8", ); const clientCodegenSource = readFileSync( join(root, "packages/compiler/src/client-codegen.ts"), "utf8", ); const syntaxParserSource = readFileSync(join(root, "packages/syntax/src/parser.ts"), "utf8"); const compilerMainSource = readFileSync(join(root, "packages/compiler/src/codegen.ts"), "utf8"); for (const [label, condition] of [ [ "synchronous hydration without a browser module", reactiveRuntimeSource.includes('if (!moduleUrl || moduleUrl === "__WRNEXUS_CLIENT_MODULE__")'), ], [ "runtime binding collision guard", clientCodegenSource.includes("RUNTIME_BINDINGS") && clientCodegenSource.includes("!RUNTIME_BINDINGS.has(prop.name)"), ], [ "unknown lifecycle hook validation", syntaxParserSource.includes("Unknown lifecycle hook") && syntaxParserSource.includes("Unknown store lifecycle hook"), ], [ "synchronous pages without imported stores", compilerMainSource.includes('storeBindings.length > 0 ? "async " : ""'), ], [ "mutable store dispose lifecycle", storeRuntimeSource.includes('runLifecycle("$dispose", definition.lifecycle?.dispose)'), ], ]) { if (!condition) fail(`R7 regression: missing ${label}`); } const rangeSliderReference = readJson( join(root, "packages/ui/component-reference.json"), ).components.find((component) => component.name === "RangeSlider"); if ( !rangeSliderReference || !["input", "change", "focus", "blur"].every((name) => rangeSliderReference.events.includes(name)) ) { fail("R7 regression: RangeSlider output reference is incomplete"); } if (!failures.some((item) => item.startsWith("R7 regression:"))) { pass( "R7 parser, hydration, codegen, store lifecycle, and component-reference regressions are guarded", ); } const uiReference = readJson(join(root, "packages/ui/component-reference.json")); const uiCatalog = readJson(join(root, "packages/ui/component-catalog.json")); const declaredUiNames = uiFiles .map((file) => { const declaration = /^\s*component\s+([A-Za-z][A-Za-z0-9_]*)\b/m.exec( readFileSync(file, "utf8"), ); return declaration?.[1] ?? null; }) .filter(Boolean) .sort(); const referenceNames = uiReference.components.map((component) => component.name).sort(); const catalogNames = uiCatalog.components.map((component) => component.name).sort(); if (JSON.stringify(referenceNames) !== JSON.stringify(declaredUiNames)) { fail("R8 regression: component reference does not match bundled UI declarations"); } if (JSON.stringify(catalogNames) !== JSON.stringify(declaredUiNames)) { fail("R8 regression: component catalog does not match bundled UI declarations"); } try { const { compileWireFile } = require(join(root, "editors/vscode/src/compiler.cjs")); for (const file of uiFiles) { const output = compileWireFile(readFileSync(file, "utf8"), file); if (!output.includes("${__wireSpreadAttrs(__attrs)}")) { fail(`R8 regression: ${relative(root, file)} does not forward native attributes`); } } } catch (error) { fail(`R8 regression: bundled UI compilation failed: ${error.message}`); } if (!failures.some((item) => item.startsWith("R8 regression:"))) { pass( "R8 UI compilation, readonly-prop diagnostics, native attributes, catalog, and reference are guarded", ); } const toggleCountSource = readFileSync( join(root, "packages/ui/components/ToggleCount.wrn"), "utf8", ); for (const [label, condition] of [ [ "peer client-function bindings", clientCodegenSource.includes("context.functions") && clientCodegenSource.includes("const scopedContext = { ...context, functions }") && clientCodegenSource.includes("handler(scopedContext, ...args)"), ], [ "drift-free ToggleCount animation scheduling", toggleCountSource.includes("duration * frame / totalFrames") && toggleCountSource.includes("scheduleValueFrame(") && toggleCountSource.includes("data-animation-token") && !toggleCountSource.includes("setTimeout(\n animateValueFrame"), ], ]) { if (!condition) fail(`R9 regression: missing ${label}`); } if (!failures.some((item) => item.startsWith("R9 regression:"))) { pass("R9 peer-function binding and drift-free ToggleCount animation are guarded"); } try { const { parse, generateTargets } = require(join(root, "editors/vscode/src/compiler.cjs")); const targets = generateTargets( parse(`component PeerCalls { state { value: number = 0 } functions { client function increment(): void { value += 1 } client function run(): void { increment() } } view { } }`), ); const executable = new Function( `${targets.browser.replace(/^export\s+/gm, "")}\nreturn { bindClientScope };`, )(); const state = { value: 0 }; const functions = executable.bindClientScope({ state, props: {}, output: {}, server: {}, refs: {}, }); functions.run(); if (state.value !== 1) { fail(`R10 regression: peer client function state was overwritten (${state.value})`); } else { pass("R10 peer client functions preserve shared scoped state"); } } catch (error) { fail(`R10 regression: peer client function execution failed: ${error.message}`); } const compilerTargetTestSource = readFileSync( join(root, "packages/compiler/test/v060-targets.test.ts"), "utf8", ); if (!compilerTargetTestSource.includes('targets.browser.replace(/^export\\s+/gm, "")')) { fail( "R11 regression: peer-function executable test does not strip export keywords with a whitespace regex", ); } else { pass("R11 peer-function executable regression test uses the correct export-stripping regex"); } const releaseSource = readFileSync(join(root, "scripts/release.ts"), "utf8"); const referenceGeneratorSource = readFileSync( join(root, "scripts/generate-ui-component-reference.mjs"), "utf8", ); for (const [label, condition] of [ [ "canonical generated-reference Git comparison", releaseSource.includes('["diff", "--quiet", "--", ...generatedFiles]') && releaseSource.includes('["diff", "--name-status", "--", ...generatedFiles]'), ], [ "component catalog included in the release reference gate", releaseSource.includes('"packages/ui/component-catalog.json"'), ], [ "platform-independent component reference ordering", referenceGeneratorSource.includes("function compareText(left, right)") && referenceGeneratorSource.includes("compareText(left.name, right.name)") && !referenceGeneratorSource.includes("localeCompare"), ], ]) { if (!condition) fail(`R12 regression: missing ${label}`); } if (!failures.some((item) => item.startsWith("R12 regression:"))) { pass("R12 release reference gate is canonical, complete, and platform independent"); } const showcaseFiles = walk(join(root, "examples/component-showcase/app"), (path) => path.endsWith(".wrn"), ); for (const file of showcaseFiles) { const source = readFileSync(file, "utf8"); if (/\b(?:open|defaultOpen)\s*=\s*(?:"true"|'\{true\}'|"\{true\}")/.test(source)) { fail(`${relative(root, file)} forces an overlay open`); } } if (!failures.some((item) => item.includes("forces an overlay open"))) { pass( `${showcaseFiles.length} generated showcase pages contain no forced-open overlay regression`, ); } for (const file of walk(root, (path) => path.endsWith(".json"))) { try { readJson(file); } catch (error) { fail(`Invalid JSON ${relative(root, file)}: ${error.message}`); } } if (!failures.some((item) => item.startsWith("Invalid JSON"))) pass("All JSON files parse"); for (const file of walk(root, (path) => /\.(?:mjs|cjs|js)$/.test(path))) { const check = spawnSync(process.execPath, ["--check", file], { encoding: "utf8" }); if (check.status !== 0) fail(`JavaScript syntax failed: ${relative(root, file)}\n${check.stderr.trim()}`); } if (!failures.some((item) => item.startsWith("JavaScript syntax failed"))) pass("JavaScript/MJS syntax checks pass"); const bun = spawnSync(process.platform === "win32" ? "bun.exe" : "bun", ["--version"], { encoding: "utf8", }); if (bun.status !== 0) warnings.push("Bun is unavailable; run `bun install` and `bun run check` on the target machine."); else pass(`Bun ${bun.stdout.trim()} is available`); console.log("WRNexusJS v0.6 validation"); for (const message of passes) console.log(`PASS ${message}`); for (const message of warnings) console.warn(`WARN ${message}`); for (const message of failures) console.error(`FAIL ${message}`); console.log(`\n${passes.length} passed, ${warnings.length} warnings, ${failures.length} failed`); process.exitCode = failures.length ? 1 : 0;