feat: publish changed packages independently
This commit is contained in:
+81
-14
@@ -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`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user