release: WRNexusJS 0.3.0
This commit is contained in:
+191
-21
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user