release: WRNexusJS 0.4.0
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { readdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { basename, join } from "node:path";
|
||||
import { join } from "node:path";
|
||||
|
||||
const uiRoot = join(import.meta.dirname, "..", "packages", "ui");
|
||||
const componentsDir = join(uiRoot, "components");
|
||||
@@ -45,10 +45,13 @@ function words(name) {
|
||||
|
||||
const components = readdirSync(componentsDir)
|
||||
.filter((file) => file.endsWith(".wrn"))
|
||||
.sort((a, b) => a.localeCompare(b))
|
||||
.map((file) => {
|
||||
const name = basename(file, ".wrn");
|
||||
const source = readFileSync(join(componentsDir, file), "utf8");
|
||||
const declaration = /^\s*component\s+([A-Za-z][A-Za-z0-9_]*)\b/m.exec(source);
|
||||
if (!declaration) {
|
||||
throw new Error(`UI component source does not declare a component: ${file}`);
|
||||
}
|
||||
const name = declaration[1];
|
||||
const entry = metadata.get(name);
|
||||
const slots = [...source.matchAll(/<slot(?:\s+name="([^"]+)")?/g)].map(
|
||||
(match) => match[1] ?? "default",
|
||||
@@ -78,7 +81,8 @@ const components = readdirSync(componentsDir)
|
||||
events,
|
||||
source: `components/${file}`,
|
||||
};
|
||||
});
|
||||
})
|
||||
.sort((left, right) => left.name.localeCompare(right.name));
|
||||
|
||||
writeFileSync(
|
||||
join(uiRoot, "component-reference.json"),
|
||||
|
||||
+41
-15
@@ -1,8 +1,10 @@
|
||||
#!/usr/bin/env node
|
||||
/* global console */
|
||||
import { cp, mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { spawn } from "node:child_process";
|
||||
import process from "node:process";
|
||||
|
||||
const scriptDir = dirname(fileURLToPath(import.meta.url));
|
||||
const bundleRoot = resolve(scriptDir, "..");
|
||||
@@ -15,18 +17,35 @@ const withShowcase = flags.has("--with-showcase");
|
||||
const withManaged = flags.has("--with-managed-service");
|
||||
const runChecks = flags.has("--check");
|
||||
|
||||
async function exists(path) { try { await stat(path); return true; } catch { return false; } }
|
||||
async function exists(path) {
|
||||
try {
|
||||
await stat(path);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
async function copyDirectory(source, destination) {
|
||||
if ((await exists(destination)) && force) await rm(destination, { recursive: true, force: true });
|
||||
await mkdir(dirname(destination), { recursive: true });
|
||||
await cp(source, destination, { recursive: true, force: true });
|
||||
}
|
||||
async function readJson(path) { return JSON.parse(await readFile(path, "utf8")); }
|
||||
async function writeJson(path, value) { await writeFile(path, `${JSON.stringify(value, null, 2)}\n`); }
|
||||
async function readJson(path) {
|
||||
return JSON.parse(await readFile(path, "utf8"));
|
||||
}
|
||||
async function writeJson(path, value) {
|
||||
await writeFile(path, `${JSON.stringify(value, null, 2)}\n`);
|
||||
}
|
||||
async function run(command, commandArgs) {
|
||||
await new Promise((resolvePromise, reject) => {
|
||||
const child = spawn(command, commandArgs, { cwd: target, stdio: "inherit", shell: process.platform === "win32" });
|
||||
child.on("exit", (code) => code === 0 ? resolvePromise() : reject(new Error(`${command} exited with ${code}`)));
|
||||
const child = spawn(command, commandArgs, {
|
||||
cwd: target,
|
||||
stdio: "inherit",
|
||||
shell: process.platform === "win32",
|
||||
});
|
||||
child.on("exit", (code) =>
|
||||
code === 0 ? resolvePromise() : reject(new Error(`${command} exited with ${code}`)),
|
||||
);
|
||||
child.on("error", reject);
|
||||
});
|
||||
}
|
||||
@@ -36,16 +55,22 @@ if (!(await exists(join(target, "packages"))) || !(await exists(join(target, "ts
|
||||
}
|
||||
|
||||
await copyDirectory(join(bundleRoot, "packages/captcha"), join(target, "packages/captcha"));
|
||||
await mkdir(join(target, "packages/ui/components"), { recursive: true });
|
||||
const uiComponent = join(target, "packages/ui/components/Captcha.wrn");
|
||||
if (!(await exists(uiComponent)) || force) {
|
||||
await cp(join(bundleRoot, "packages/captcha/components/Captcha.wrn"), uiComponent, { force: true });
|
||||
} else {
|
||||
console.warn(`Skipped existing ${uiComponent}; use --force to replace it.`);
|
||||
}
|
||||
|
||||
if (withShowcase) await copyDirectory(join(bundleRoot, "examples/captcha-showcase"), join(target, "examples/captcha-showcase"));
|
||||
if (withManaged) await copyDirectory(join(bundleRoot, "services/managed-captcha"), join(target, "services/managed-captcha"));
|
||||
// WRNexusJS 0.4 discovers package-owned components and browser runtimes through
|
||||
// package.json `wrnexus` metadata. Do not copy Captcha.wrn or captcha.js into the
|
||||
// UI/public directories: doing so creates stale duplicates that bypass package
|
||||
// upgrades and content-hashed production assets.
|
||||
|
||||
if (withShowcase)
|
||||
await copyDirectory(
|
||||
join(bundleRoot, "examples/captcha-showcase"),
|
||||
join(target, "examples/captcha-showcase"),
|
||||
);
|
||||
if (withManaged)
|
||||
await copyDirectory(
|
||||
join(bundleRoot, "services/managed-captcha"),
|
||||
join(target, "services/managed-captcha"),
|
||||
);
|
||||
|
||||
const tsconfigPath = join(target, "tsconfig.json");
|
||||
const tsconfig = await readJson(tsconfigPath);
|
||||
@@ -70,7 +95,8 @@ if (withManaged) {
|
||||
|
||||
console.log("Installed @wrnexus/captcha.");
|
||||
console.log("Canonical package: packages/captcha");
|
||||
console.log("Discoverable component: packages/ui/components/Captcha.wrn");
|
||||
console.log("Component/runtime discovery: automatic through package.json wrnexus.plugin");
|
||||
console.log("No public captcha.js or UI component copy is required.");
|
||||
|
||||
if (runChecks) {
|
||||
await run("bun", ["install"]);
|
||||
|
||||
+1
-1
@@ -254,7 +254,7 @@ function publish(all: PackageInfo[], version: string) {
|
||||
requireCleanAndPushed(root, "Framework");
|
||||
requireCleanAndPushed(docsRoot, "Docs");
|
||||
|
||||
run(process.execPath, ["run", "scripts/generate-ui-component-reference.mjs"]);
|
||||
verifyUiComponentReference();
|
||||
|
||||
run(process.execPath, ["run", "format"]);
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
/* global console */
|
||||
import { spawnSync } from "node:child_process";
|
||||
import process from "node:process";
|
||||
|
||||
const root = process.cwd();
|
||||
const bun = process.platform === "win32" ? "bun.exe" : "bun";
|
||||
|
||||
function run(command, args) {
|
||||
console.log(`\n> ${command} ${args.join(" ")}`);
|
||||
const result = spawnSync(command, args, {
|
||||
cwd: root,
|
||||
stdio: "inherit",
|
||||
env: process.env,
|
||||
});
|
||||
if (result.error) throw result.error;
|
||||
if (result.status !== 0) process.exit(result.status ?? 1);
|
||||
}
|
||||
|
||||
run(process.execPath, ["scripts/verify-0.4.mjs"]);
|
||||
run(bun, ["run", "check"]);
|
||||
run(bun, ["test", "integration"]);
|
||||
run(bun, ["run", "--cwd", "examples/basic-app", "test"]);
|
||||
run(bun, ["run", "--cwd", "examples/basic-app", "build"]);
|
||||
run(bun, ["run", "--cwd", "examples/component-showcase", "check"]);
|
||||
run(bun, ["run", "--cwd", "examples/captcha-showcase", "check"]);
|
||||
run(bun, ["test", "services/managed-captcha"]);
|
||||
|
||||
console.log("\n✓ WRNexusJS 0.4.0 complete validation passed.\n");
|
||||
@@ -0,0 +1,218 @@
|
||||
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
||||
import { join, relative } from "node:path";
|
||||
import process from "node:process";
|
||||
|
||||
const root = process.cwd();
|
||||
const VERSION = "0.4.0";
|
||||
const errors = [];
|
||||
const notes = [];
|
||||
const read = (file) => readFileSync(join(root, file), "utf8");
|
||||
const readJson = (file) => JSON.parse(read(file));
|
||||
|
||||
function files(dir, filename = "package.json") {
|
||||
const output = [];
|
||||
if (!existsSync(dir)) return output;
|
||||
for (const entry of readdirSync(dir)) {
|
||||
if (["node_modules", ".git", ".publish", "dist", ".wrnexus"].includes(entry)) {
|
||||
continue;
|
||||
}
|
||||
const path = join(dir, entry);
|
||||
const stat = statSync(path);
|
||||
if (stat.isDirectory()) output.push(...files(path, filename));
|
||||
else if (entry === filename) output.push(path);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
const rootPackage = readJson("package.json");
|
||||
if (rootPackage.version !== VERSION) {
|
||||
errors.push(`root package version is ${rootPackage.version ?? "<missing>"}, expected ${VERSION}`);
|
||||
}
|
||||
|
||||
const packageFiles = files(join(root, "packages"));
|
||||
const packages = new Map();
|
||||
for (const file of packageFiles) {
|
||||
const manifest = JSON.parse(readFileSync(file, "utf8"));
|
||||
if (!manifest.name?.startsWith("@wrnexus/")) continue;
|
||||
packages.set(manifest.name, { file, manifest });
|
||||
if (manifest.version !== VERSION) {
|
||||
errors.push(`${relative(root, file)} has version ${manifest.version ?? "<missing>"}`);
|
||||
}
|
||||
}
|
||||
if (packages.size !== 30) errors.push(`expected 30 framework packages, found ${packages.size}`);
|
||||
|
||||
const serviceManifest = readJson("services/managed-captcha/package.json");
|
||||
if (serviceManifest.version !== VERSION) {
|
||||
errors.push(`managed CAPTCHA service has version ${serviceManifest.version ?? "<missing>"}`);
|
||||
}
|
||||
|
||||
for (const { file, manifest } of packages.values()) {
|
||||
for (const field of [
|
||||
"dependencies",
|
||||
"devDependencies",
|
||||
"peerDependencies",
|
||||
"optionalDependencies",
|
||||
]) {
|
||||
for (const [name, range] of Object.entries(manifest[field] ?? {})) {
|
||||
if (String(range).startsWith("workspace:") && !packages.has(name)) {
|
||||
errors.push(`${relative(root, file)} references missing workspace ${name}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const required of [
|
||||
"packages/plugin/src/types.ts",
|
||||
"packages/plugin/src/manifest.ts",
|
||||
"packages/plugin/src/discovery.ts",
|
||||
"packages/dev-server/src/plugin-assets.ts",
|
||||
"packages/dev-server/src/plugin-migrations.ts",
|
||||
"packages/cli/src/inspect.ts",
|
||||
"packages/cli/src/system.ts",
|
||||
"packages/captcha/src/plugin.ts",
|
||||
"packages/captcha/assets/client/captcha.js",
|
||||
"packages/captcha/components/Captcha.wrn",
|
||||
"integration/platform-upgrade.test.ts",
|
||||
"docs/ARCHITECTURE-0.4.md",
|
||||
"docs/UPGRADE-0.4.md",
|
||||
"docs/PACKAGE-RUNTIMES-0.4.md",
|
||||
"docs/TEST-CHECKLIST-0.4.md",
|
||||
"CHANGELOG-0.4.0.md",
|
||||
]) {
|
||||
if (!existsSync(join(root, required))) errors.push(`missing ${required}`);
|
||||
}
|
||||
|
||||
const updateSource = read("packages/cli/src/update.ts");
|
||||
const migrations = [...updateSource.matchAll(/version:\s*"([^"]+)"[\s\S]*?id:\s*"([^"]+)"/g)].map(
|
||||
(match) => ({ version: match[1], id: match[2] }),
|
||||
);
|
||||
const migrationIds = new Set();
|
||||
for (const migration of migrations) {
|
||||
if (migrationIds.has(migration.id)) errors.push(`duplicate migration id ${migration.id}`);
|
||||
migrationIds.add(migration.id);
|
||||
}
|
||||
if (!migrations.some((migration) => migration.version === VERSION)) {
|
||||
errors.push(`missing ${VERSION} update migration`);
|
||||
}
|
||||
|
||||
const captchaComponent = read("packages/captcha/components/Captcha.wrn");
|
||||
if (!captchaComponent.includes('data-wrnexus-runtime="captcha"')) {
|
||||
errors.push("Captcha.wrn does not declare the captcha client runtime marker");
|
||||
}
|
||||
if (/<script\b/i.test(captchaComponent)) {
|
||||
errors.push("Captcha.wrn still contains a manual script tag");
|
||||
}
|
||||
for (const obsolete of [
|
||||
"packages/ui/components/Captcha.wrn",
|
||||
"examples/captcha-showcase/app/components/Captcha.wrn",
|
||||
"examples/captcha-showcase/public/assets/wrnexus/captcha.js",
|
||||
]) {
|
||||
if (existsSync(join(root, obsolete)))
|
||||
errors.push(`obsolete copied CAPTCHA file remains: ${obsolete}`);
|
||||
}
|
||||
|
||||
const captchaManifest = readJson("packages/captcha/package.json");
|
||||
if (!captchaManifest.wrnexus?.plugin?.plugin) {
|
||||
errors.push("@wrnexus/captcha does not publish a wrnexus.plugin manifest");
|
||||
}
|
||||
|
||||
const pluginSource = read("packages/plugin/src/index.ts");
|
||||
for (const marker of ["clientRuntimes", "componentDirs", "migrations", "PUBLIC-PATH"]) {
|
||||
if (!pluginSource.includes(marker)) errors.push(`plugin runner is missing ${marker}`);
|
||||
}
|
||||
|
||||
const runtimeSource = read("packages/dev-server/src/runtime.ts");
|
||||
if (!runtimeSource.includes("runtimeScriptsForMarkup")) {
|
||||
errors.push("server runtime is not injecting package runtimes from rendered markers");
|
||||
}
|
||||
if (!runtimeSource.includes("JSON.stringify(scripts)")) {
|
||||
errors.push("page ETags do not include structured script metadata");
|
||||
}
|
||||
|
||||
const navSource = read("packages/csr/src/nav-runtime.ts");
|
||||
for (const marker of ["__wrnexusRuntimes", "mountPackageRuntimes", "unmountPackageRuntimes"]) {
|
||||
if (!navSource.includes(marker)) errors.push(`CSR navigation is missing ${marker}`);
|
||||
}
|
||||
|
||||
const uiReference = readJson("packages/ui/component-reference.json");
|
||||
if (uiReference.components?.some((component) => component.name === "Captcha")) {
|
||||
errors.push("Captcha is still duplicated in the core UI component reference");
|
||||
}
|
||||
|
||||
const tsconfig = readJson("tsconfig.json");
|
||||
for (const name of packages.keys()) {
|
||||
if (!tsconfig.compilerOptions?.paths?.[name]) errors.push(`tsconfig paths missing ${name}`);
|
||||
}
|
||||
|
||||
try {
|
||||
const lock = JSON.parse(read("bun.lock").replace(/,\s*([}\]])/g, "$1"));
|
||||
for (const { file, manifest } of packages.values()) {
|
||||
const workspace = relative(root, file)
|
||||
.replace(/\\/g, "/")
|
||||
.replace(/\/package\.json$/, "");
|
||||
if (lock.workspaces?.[workspace]?.version !== VERSION) {
|
||||
errors.push(`bun.lock has a stale version for ${workspace}`);
|
||||
}
|
||||
if (!lock.packages?.[manifest.name]) errors.push(`bun.lock missing ${manifest.name}`);
|
||||
}
|
||||
} catch (error) {
|
||||
errors.push(`bun.lock structure is invalid: ${error instanceof Error ? error.message : error}`);
|
||||
}
|
||||
|
||||
const editorManifest = readJson("editors/vscode/package.json");
|
||||
if (editorManifest.version !== VERSION) {
|
||||
errors.push(`VS Code extension has version ${editorManifest.version ?? "<missing>"}`);
|
||||
}
|
||||
const editorLock = readJson("editors/vscode/package-lock.json");
|
||||
if (editorLock.version !== VERSION || editorLock.packages?.[""]?.version !== VERSION) {
|
||||
errors.push("VS Code extension package-lock version is stale");
|
||||
}
|
||||
|
||||
for (const obsolete of [
|
||||
".publish",
|
||||
"AUDIO-PLAYBACK-FIX.md",
|
||||
"CAPTCHA-FIX8-README.md",
|
||||
"CAPTCHA-FIXED7-INSTRUCTIONS.md",
|
||||
"WRNexusJS-0.3.0-test-fixes.patch",
|
||||
]) {
|
||||
if (existsSync(join(root, obsolete)))
|
||||
errors.push(`obsolete release artifact remains: ${obsolete}`);
|
||||
}
|
||||
|
||||
for (const file of [
|
||||
"editors/vscode/package.json",
|
||||
"editors/vscode/package-lock.json",
|
||||
"editors/vscode/snippets/wrn.json",
|
||||
"editors/vscode/syntaxes/wrn.tmLanguage.json",
|
||||
]) {
|
||||
try {
|
||||
readJson(file);
|
||||
} catch (error) {
|
||||
errors.push(`${file} is invalid JSON: ${error instanceof Error ? error.message : error}`);
|
||||
}
|
||||
}
|
||||
|
||||
notes.push(`${packages.size} framework packages aligned to ${VERSION}`);
|
||||
notes.push(`${migrations.length} unique updater migrations checked`);
|
||||
notes.push("automatic component/runtime/asset/migration discovery checked");
|
||||
notes.push("CAPTCHA has no copied component or public JavaScript requirement");
|
||||
notes.push("Bun workspace lock and editor JSON checked");
|
||||
|
||||
if (errors.length) {
|
||||
process.stderr.write(
|
||||
[
|
||||
`WRNexusJS ${VERSION} verification failed:`,
|
||||
...errors.map((error) => ` - ${error}`),
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
process.exitCode = 1;
|
||||
} else {
|
||||
process.stdout.write(
|
||||
[
|
||||
`WRNexusJS ${VERSION} structural verification passed.`,
|
||||
...notes.map((note) => ` ✓ ${note}`),
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user