release: WRNexusJS 0.3.0

This commit is contained in:
2026-07-22 17:29:08 +05:30
parent 13dfa31d19
commit 07d8fb59d6
145 changed files with 9664 additions and 3881 deletions
+191 -21
View File
@@ -12,12 +12,20 @@
* the running CLI's own version (the default — pair with `bunx @wrnexus/cli@latest
* update` to jump to the newest release with no network guesswork).
*
* Migrations are CONSERVATIVE: they only add/refresh framework-owned things and
* never clobber your own code or edited CLAUDE.md. Add new ones to `MIGRATIONS`
* Migrations are CONSERVATIVE: source rewrites are syntax-aware, idempotent, and
* protected by a complete pre-update backup. User-owned files are never replaced wholesale. Add new ones to `MIGRATIONS`
* as the framework evolves — that is how "new things" reach existing apps.
*/
import { cpSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import {
cpSync,
existsSync,
mkdirSync,
readFileSync,
readdirSync,
statSync,
writeFileSync,
} from "node:fs";
import { spawnSync } from "node:child_process";
import { dirname, join, resolve } from "node:path";
import { pathToFileURL } from "node:url";
@@ -86,9 +94,95 @@ const LEGACY_VSCODE_EXTENSIONS =
2,
) + "\n";
function walkProjectFiles(dir: string, extension: string): string[] {
if (!existsSync(dir)) return [];
const out: string[] = [];
for (const entry of readdirSync(dir)) {
if (["node_modules", "dist", ".git", ".wrnexus"].includes(entry)) continue;
const path = join(dir, entry);
const stat = statSync(path);
if (stat.isDirectory()) out.push(...walkProjectFiles(path, extension));
else if (stat.isFile() && path.endsWith(extension)) out.push(path);
}
return out;
}
function formatInlineProps(source: string): string {
return source.replace(
/(^[ \t]*)props\s*\{([^{}\n]*)\}/gm,
(whole, indent: string, body: string) => {
const declaration =
/([A-Za-z_$][\w$]*)(\s*:\s*[^=]+?)?\s*=\s*("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\[\]|\{\}|true|false|null|undefined|-?\d+(?:\.\d+)?)/gy;
const values: string[] = [];
let offset = 0;
while (offset < body.length) {
while (/\s/.test(body[offset] ?? "")) offset++;
if (offset >= body.length) break;
declaration.lastIndex = offset;
const match = declaration.exec(body);
if (!match || match.index !== offset) return whole;
values.push(`${match[1]}${match[2] ?? ""} = ${match[3]}`);
offset = declaration.lastIndex;
}
if (values.length < 2) return whole;
return `${indent}props {\n${values.map((value) => `${indent} ${value}`).join("\n")}\n${indent}}`;
},
);
}
function quoteLegacyDynamicAttributes(source: string): string {
let output = "";
let index = 0;
while (index < source.length) {
const start = source.indexOf("<", index);
if (start < 0) return output + source.slice(index);
output += source.slice(index, start);
if (source.startsWith("<!--", start)) {
const end = source.indexOf("-->", start + 4);
if (end < 0) return output + source.slice(start);
output += source.slice(start, end + 3);
index = end + 3;
continue;
}
let end = start + 1;
let quote = "";
let braceDepth = 0;
for (; end < source.length; end++) {
const char = source[end]!;
if (quote) {
if (char === "\\") end++;
else if (char === quote) quote = "";
continue;
}
if (char === '"' || char === "'") quote = char;
else if (char === "{") braceDepth++;
else if (char === "}") braceDepth = Math.max(0, braceDepth - 1);
else if (char === ">" && braceDepth === 0) break;
}
if (end >= source.length) return output + source.slice(start);
let tag = source.slice(start, end + 1);
if (!/^<\/?[A-Za-z]/.test(tag) || /^<\//.test(tag)) {
output += tag;
index = end + 1;
continue;
}
tag = tag.replace(
/(\s[@:#A-Za-z_$][\w$:.-]*)\s*=\s*\{([^{}']+)\}/g,
(_all, name: string, expr: string) => `${name}='{${expr.trim()}}'`,
);
output += tag;
index = end + 1;
}
return output;
}
export function migrateWrnSource(source: string): string {
return formatInlineProps(quoteLegacyDynamicAttributes(source));
}
/**
* Versioned, idempotent upgrade steps. Each MUST be safe to re-run and MUST NOT
* overwrite user code. Append new entries with the version that ships them.
* Versioned, idempotent upgrade steps. Each MUST be safe to re-run. Source
* migrations must preserve semantics and are protected by update backups.
*/
const MIGRATIONS: Migration[] = [
{
@@ -359,7 +453,7 @@ const MIGRATIONS: Migration[] = [
},
{
version: "0.2.32",
id: "fix-runtime-bug",
id: "fix-runtime-bug-0-2-32",
description:
"Fixes safe encoding and browser parsing of multiline component behavior metadata.",
apply() {
@@ -368,7 +462,7 @@ const MIGRATIONS: Migration[] = [
},
{
version: "0.2.33",
id: "fix-runtime-bug",
id: "fix-runtime-bug-0-2-33",
description:
"Fixes safe encoding and browser parsing of multiline component behavior metadata.",
apply() {
@@ -431,7 +525,7 @@ const MIGRATIONS: Migration[] = [
},
{
version: "0.2.40",
id: "conditions-bug",
id: "conditions-bug-0-2-40",
description:
"Adds server-rendered component each/if blocks and fixes dynamic component rendering.",
apply() {
@@ -440,7 +534,7 @@ const MIGRATIONS: Migration[] = [
},
{
version: "0.2.41",
id: "conditions-bug",
id: "conditions-bug-0-2-41",
description:
"Adds server-rendered component each/if blocks and fixes dynamic component rendering.",
apply() {
@@ -449,7 +543,7 @@ const MIGRATIONS: Migration[] = [
},
{
version: "0.2.42",
id: "conditions-bug",
id: "conditions-bug-0-2-42",
description:
"Adds server-rendered component each/if blocks and fixes dynamic component rendering.",
apply() {
@@ -458,7 +552,7 @@ const MIGRATIONS: Migration[] = [
},
{
version: "0.2.43",
id: "conditions-bug",
id: "conditions-bug-0-2-43",
description:
"Adds server-rendered component each/if blocks and fixes dynamic component rendering.",
apply() {
@@ -467,7 +561,7 @@ const MIGRATIONS: Migration[] = [
},
{
version: "0.2.44",
id: "conditions-bug",
id: "conditions-bug-0-2-44",
description:
"Adds server-rendered component each/if blocks and fixes dynamic component rendering.",
apply() {
@@ -476,7 +570,7 @@ const MIGRATIONS: Migration[] = [
},
{
version: "0.2.45",
id: "conditions-bug",
id: "conditions-bug-0-2-45",
description:
"Adds server-rendered component each/if blocks and fixes dynamic component rendering.",
apply() {
@@ -485,7 +579,7 @@ const MIGRATIONS: Migration[] = [
},
{
version: "0.2.46",
id: "conditions-bug",
id: "conditions-bug-0-2-46",
description:
"Adds server-rendered component each/if blocks and fixes dynamic component rendering.",
apply() {
@@ -494,7 +588,7 @@ const MIGRATIONS: Migration[] = [
},
{
version: "0.2.47",
id: "conditions-bug",
id: "conditions-bug-0-2-47",
description:
"Adds server-rendered component each/if blocks and fixes dynamic component rendering.",
apply() {
@@ -503,7 +597,7 @@ const MIGRATIONS: Migration[] = [
},
{
version: "0.2.48",
id: "components-update",
id: "components-update-0-2-48",
description: "Adds reactive hydration support for server-rendered each loop locals.",
apply() {
// No generated application files require automatic migration.
@@ -511,7 +605,7 @@ const MIGRATIONS: Migration[] = [
},
{
version: "0.2.49",
id: "components-update",
id: "components-update-0-2-49",
description: "Adds reactive hydration support for server-rendered each loop locals.",
apply() {
// No generated application files require automatic migration.
@@ -519,7 +613,7 @@ const MIGRATIONS: Migration[] = [
},
{
version: "0.2.50",
id: "components-update",
id: "components-update-0-2-50",
description: "Adds reactive hydration support for server-rendered each loop locals.",
apply() {
// No generated application files require automatic migration.
@@ -769,7 +863,7 @@ const MIGRATIONS: Migration[] = [
},
{
version: "0.2.78",
id: "ui-components-fix",
id: "ui-components-fix-2",
description: "Dev toolbar for optimization and developer.",
apply() {
// Language and shared UI behavior improve automatically after updating.
@@ -777,12 +871,87 @@ const MIGRATIONS: Migration[] = [
},
{
version: "0.2.79",
id: "ui-components-fix",
id: "ui-components-fix-3",
description: "Dev toolbar for optimization and developer.",
apply() {
// Language and shared UI behavior improve automatically after updating.
},
},
{
version: "0.3.0",
id: "language-foundation-and-compatible-platform-upgrade",
description:
"Adds the shared syntax/AST package, stable diagnostics, plugins, partial hydration, advanced routing, build analysis, tracing, typed data APIs, and safe WRN source normalization.",
apply(ctx) {
const runnable = existsSync(join(ctx.appRoot, "app", "pages"));
const packageFile = join(ctx.appRoot, "package.json");
const pkg = JSON.parse(readFileSync(packageFile, "utf8")) as Record<string, any>;
const scripts = (pkg.scripts ??= {});
const dependencies = (pkg.dependencies ??= {});
const addedScripts: string[] = [];
const addedDependencies: string[] = [];
if (runnable) {
for (const [name, command] of Object.entries({
doctor: "wrnexus doctor .",
"config:explain": "wrnexus config . --explain",
analyze: "wrnexus analyze .",
"update:preview": "wrnexus update . --dry-run",
})) {
if (!scripts[name]) {
scripts[name] = command;
addedScripts.push(name);
}
}
for (const name of ["@wrnexus/syntax", "@wrnexus/plugin"]) {
if (!dependencies[name]) {
dependencies[name] = `^${ctx.to}`;
addedDependencies.push(name);
}
}
}
if (addedScripts.length) ctx.log(`+ package scripts: ${addedScripts.join(", ")}`);
if (addedDependencies.length) ctx.log(`+ dependencies: ${addedDependencies.join(", ")}`);
if (!ctx.dryRun && (addedScripts.length || addedDependencies.length)) {
writeFileSync(packageFile, JSON.stringify(pkg, null, 2) + "\n", "utf8");
}
const changedFiles: string[] = [];
for (const file of walkProjectFiles(join(ctx.appRoot, "app"), ".wrn")) {
const before = readFileSync(file, "utf8");
const after = migrateWrnSource(before);
if (after === before) continue;
changedFiles.push(file.slice(ctx.appRoot.length + 1).replace(/\\/g, "/"));
ctx.log(`~ normalized ${changedFiles[changedFiles.length - 1]}`);
if (!ctx.dryRun) writeFileSync(file, after, "utf8");
}
const reportFile = join(ctx.appRoot, ".wrnexus", "migrations", "0.3.0.json");
ctx.log(`+ .wrnexus/migrations/0.3.0.json (${changedFiles.length} source files normalized)`);
if (!ctx.dryRun) {
mkdirSync(dirname(reportFile), { recursive: true });
writeFileSync(
reportFile,
JSON.stringify(
{
version: "0.3.0",
from: ctx.from,
appliedAt: new Date().toISOString(),
changedFiles,
compatibility: {
legacyDataFor: true,
legacyEachBlocks: true,
legacyComponentMounts: true,
quotedDynamicAttributes: true,
},
},
null,
2,
) + "\n",
"utf8",
);
}
},
},
];
/** Release tooling uses this to require an explicit migration entry per version. */
@@ -862,12 +1031,13 @@ function backupProjectFiles(appRoot: string, from: string, target: string): stri
".vscode/extensions.json",
"CLAUDE.md",
"public/llms.txt",
"app",
]) {
const source = join(appRoot, name);
if (existsSync(source)) {
const destination = join(backup, name);
mkdirSync(dirname(destination), { recursive: true });
cpSync(source, destination);
cpSync(source, destination, { recursive: statSync(source).isDirectory() });
}
}
return backup;