feat: publish changed packages independently
Quality / quality (ubuntu-latest) (push) Failing after 21s
Quality / quality (windows-latest) (push) Canceled after 0s

This commit is contained in:
2026-08-11 15:32:36 +05:30
parent feff30a2b5
commit 8d044565e3
67 changed files with 10935 additions and 876 deletions
+6 -27
View File
@@ -79,24 +79,7 @@ export function repairProject(appRoot: string): DoctorRepair[] {
try {
const source = readFileSync(pkgPath, "utf8");
const pkg = JSON.parse(source) as Record<string, unknown>;
const ranges = frameworkRanges(pkg);
const preferred = [...ranges.keys()].sort((left, right) => {
const a = parseVersion(left);
const b = parseVersion(right);
return b[0] - a[0] || b[1] - a[1] || b[2] - a[2];
})[0];
let changed = false;
if (preferred && ranges.size > 1) {
for (const field of ["dependencies", "devDependencies", "peerDependencies"]) {
const dependencies = pkg[field] as Record<string, string> | undefined;
for (const name of Object.keys(dependencies ?? {})) {
if (name.startsWith("@wrnexus/") && dependencies![name] !== preferred) {
dependencies![name] = preferred;
changed = true;
}
}
}
}
const marker = (pkg.wrnexus as Record<string, unknown> | undefined) ?? {};
if (!marker.version || !versionAtLeast(String(marker.version), "0.8.0")) {
marker.version = "0.8.0";
@@ -108,9 +91,7 @@ export function repairProject(appRoot: string): DoctorRepair[] {
repairs.push({
name: "package.json",
changed: true,
detail: preferred
? `aligned framework packages to ${preferred}`
: "recorded 0.8.0 marker",
detail: "recorded 0.8.0 migration marker",
});
}
} catch {
@@ -154,15 +135,13 @@ export function inspectProject(appRoot: string): DoctorCheck[] {
try {
const pkg = JSON.parse(readFileSync(pkgPath, "utf8")) as Record<string, unknown>;
const ranges = frameworkRanges(pkg);
const invalid = [...ranges.keys()].filter((range) => !/[~^]?\d+\.\d+\.\d+/.test(range));
checks.push({
name: "framework package versions",
ok: ranges.size <= 1,
detail:
ranges.size <= 1
? ([...ranges.keys()][0] ?? "No @wrnexus packages declared")
: `version skew: ${[...ranges.entries()]
.map(([range, names]) => `${range} (${names.join(", ")})`)
.join("; ")}`,
ok: invalid.length === 0,
detail: invalid.length
? `invalid ranges: ${invalid.join(", ")}`
: `${[...ranges.values()].reduce((count, names) => count + names.length, 0)} independently versioned package(s)`,
});
const marker = (pkg.wrnexus as { version?: string } | undefined)?.version;
checks.push({
+81 -14
View File
@@ -2,13 +2,13 @@
* `wrnexus update [dir] [--version=x.y.z | --latest] [--dry-run]`
*
* Upgrade an app (or every app in a workspace) to a WrNexus release:
* 1. Bump every `@wrnexus/*` dependency to the target version.
* 1. Resolve and bump every installed `@wrnexus/*` dependency independently.
* 2. `bun install`.
* 3. Refresh framework-owned reference files (public/llms.txt) and apply any
* versioned, idempotent migrations that newer releases introduce.
* 4. Record the applied version in package.json (`"wrnexus": { version }`).
*
* Target version resolution: `--version=x.y.z` > `--latest` (queries npm) >
* Migration version resolution: `--version=x.y.z` > `--latest` (queries npm) >
* 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).
*
@@ -27,7 +27,8 @@ import {
statSync,
writeFileSync,
} from "node:fs";
import { spawnSync } from "node:child_process";
import { execFile, spawnSync } from "node:child_process";
import { promisify } from "node:util";
import { basename, dirname, join, relative, resolve } from "node:path";
import { pathToFileURL } from "node:url";
import { formatWrn, parse } from "@wrnexus/syntax";
@@ -56,6 +57,31 @@ function latestPublished(): string | null {
}
}
const execFileAsync = promisify(execFile);
/** Resolve every installed WRNexus dependency independently and in parallel. */
export async function latestPackageVersions(names: string[]): Promise<Map<string, string>> {
const npm = process.platform === "win32" ? "npm.cmd" : "npm";
const unique = [...new Set(names.filter((name) => name.startsWith("@wrnexus/")))];
const entries = await Promise.all(
unique.map(async (name) => {
try {
const { stdout } = await execFileAsync(npm, ["view", name, "version", "--json"], {
encoding: "utf8",
});
const parsed = JSON.parse(stdout.trim()) as string | string[];
const version = Array.isArray(parsed) ? parsed.at(-1) : parsed;
return typeof version === "string" && /^\d+\.\d+\.\d+/.test(version)
? ([name, version] as const)
: null;
} catch {
return null;
}
}),
);
return new Map(entries.filter((entry): entry is readonly [string, string] => entry !== null));
}
/** Numeric compare of `x.y.z` (pre-release/build tags ignored). */
function cmp(a: string, b: string): number {
const pa = a.split("-")[0]!.split(".").map(Number);
@@ -2131,6 +2157,16 @@ const MIGRATIONS: Migration[] = [
// interface contracts produced by earlier releases.
},
},
{
version: "0.8.9",
id: "0.8.9-independent-package-updates",
description:
"Resolves and updates each installed WRNexus package to its independently published npm version.",
apply() {
// Dependency resolution is handled by the update command before install;
// no application source rewrite is required.
},
},
];
/** Release tooling uses this to require an explicit migration entry per version. */
@@ -2138,15 +2174,22 @@ export function updateMigrationVersions(): string[] {
return [...new Set(MIGRATIONS.map((migration) => migration.version))];
}
/** Bump every `@wrnexus/*` range to `^target`. Returns the human-readable changes. */
function bumpDeps(pkg: Record<string, unknown>, target: string): string[] {
/** Bump each `@wrnexus/*` range to its independently resolved target. */
export function bumpDeps(
pkg: Record<string, unknown>,
target: string,
packageVersions?: ReadonlyMap<string, string>,
): string[] {
const changed: string[] = [];
const next = `^${target}`;
for (const field of ["dependencies", "devDependencies", "peerDependencies"]) {
const deps = pkg[field] as Record<string, string> | undefined;
if (!deps) continue;
for (const name of Object.keys(deps)) {
if (name.startsWith("@wrnexus/") && deps[name] !== next) {
if (!name.startsWith("@wrnexus/")) continue;
const resolved = packageVersions ? packageVersions.get(name) : target;
if (!resolved) continue;
const next = `^${resolved}`;
if (deps[name] !== next) {
changed.push(`${name} ${deps[name]}${next}`);
deps[name] = next;
}
@@ -2240,7 +2283,11 @@ export function updateApp(
appRoot: string,
target: string,
dryRun: boolean,
options: { explicitImports?: boolean; report?: MigrationReport } = {},
options: {
explicitImports?: boolean;
report?: MigrationReport;
packageVersions?: ReadonlyMap<string, string>;
} = {},
): UpdatedApp | null {
const pkgPath = join(appRoot, "package.json");
if (!existsSync(pkgPath)) {
@@ -2264,7 +2311,7 @@ export function updateApp(
if (!dryRun) log(`backup: ${backupProjectFiles(appRoot, from, target)}`);
const changes = bumpDeps(pkg, target);
const changes = bumpDeps(pkg, target, options.packageVersions);
changes.forEach((c) => log(c));
if (!changes.length) log("dependencies already current");
@@ -2288,7 +2335,7 @@ export function updateApp(
// changes back before writing the dependency bump so neither side is lost.
if (!dryRun) {
Object.assign(pkg, JSON.parse(readFileSync(pkgPath, "utf8")) as Record<string, unknown>);
bumpDeps(pkg, target);
bumpDeps(pkg, target, options.packageVersions);
}
if (!dryRun) writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
return { root: appRoot, packagePath: pkgPath, pkg };
@@ -2386,11 +2433,30 @@ export async function runUpdate(dir: string, args: string[]): Promise<void> {
return;
}
console.log(`\n ⚡ wrnexus update → ${target}${dryRun ? " (dry run)" : ""}\n`);
// Workspace → update the root manifest + every app; else just this app.
const apps = await workspaceApps(root);
const targets = apps ? [root, ...apps] : [root];
const dependencyNames = targets.flatMap((targetRoot) => {
const file = join(targetRoot, "package.json");
if (!existsSync(file)) return [];
const manifest = JSON.parse(readFileSync(file, "utf8")) as Record<string, any>;
return ["dependencies", "devDependencies", "peerDependencies"].flatMap((field) =>
Object.keys(manifest[field] ?? {}).filter((name) => name.startsWith("@wrnexus/")),
);
});
const packageVersions = versionArg
? new Map<string, string>()
: await latestPackageVersions(dependencyNames);
console.log(`\n ⚡ wrnexus update → migrations ${target}${dryRun ? " (dry run)" : ""}`);
if (!versionArg) {
console.log(
` Resolved ${packageVersions.size} installed WRNexus package version(s) from npm.\n`,
);
} else {
console.log("");
}
const reports = new Map<string, MigrationReport>();
const updated = targets
.map((targetRoot) => {
@@ -2403,7 +2469,7 @@ export async function runUpdate(dir: string, args: string[]): Promise<void> {
parseFailures: [],
};
reports.set(targetRoot, report);
return updateApp(targetRoot, target, dryRun, { explicitImports, report });
return updateApp(targetRoot, target, dryRun, { explicitImports, report, packageVersions });
})
.filter(Boolean) as UpdatedApp[];
@@ -2453,6 +2519,7 @@ export async function runUpdate(dir: string, args: string[]): Promise<void> {
writeFileSync(app.packagePath, JSON.stringify(app.pkg, null, 2) + "\n", "utf8");
}
console.log(`\n ✓ Updated and verified ${updated.length} package(s) at ${target}.`);
console.log(`\n ✓ Updated and verified ${updated.length} project manifest(s).`);
console.log(` Migration checkpoint: ${target}; dependencies use their own npm versions.`);
console.log(` Backups are under .wrnexus/update-backups/. Review and redeploy.\n`);
}