release: WRNexusJS 0.6.0
This commit is contained in:
@@ -0,0 +1,442 @@
|
||||
#!/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<string, StoreFunction>/.test(storeTypesSource),
|
||||
],
|
||||
[
|
||||
"non-colliding initialization promise",
|
||||
/readonly whenReady: Promise<void>/.test(storeTypesSource) &&
|
||||
!/readonly ready: Promise<void>/.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 { <button @click='run()'>{value}</button> }
|
||||
}`),
|
||||
);
|
||||
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 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;
|
||||
Reference in New Issue
Block a user