release: WRNexusJS 0.3.0
This commit is contained in:
+147
-14
@@ -1,26 +1,99 @@
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { join, resolve } from "node:path";
|
||||
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
||||
import { extname, join, resolve } from "node:path";
|
||||
import { diagnose } from "@wrnexus/syntax";
|
||||
import { buildRouter, findRouteConflicts } from "@wrnexus/router";
|
||||
import { loadAppConfig, validateAppConfig } from "@wrnexus/styles";
|
||||
|
||||
export interface DoctorCheck {
|
||||
name: string;
|
||||
ok: boolean;
|
||||
detail: string;
|
||||
level?: "error" | "warning";
|
||||
}
|
||||
|
||||
function parseVersion(value: string): [number, number, number] {
|
||||
const match = /^(\d+)\.(\d+)\.(\d+)/.exec(value.replace(/^[^\d]*/, ""));
|
||||
return match ? [Number(match[1]), Number(match[2]), Number(match[3])] : [0, 0, 0];
|
||||
}
|
||||
|
||||
function versionAtLeast(value: string, minimum: string): boolean {
|
||||
const left = parseVersion(value);
|
||||
const right = parseVersion(minimum);
|
||||
for (let i = 0; i < 3; i++) {
|
||||
if (left[i] !== right[i]) return left[i]! > right[i]!;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function walk(dir: string, extension: string): string[] {
|
||||
if (!existsSync(dir)) return [];
|
||||
const files: 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()) files.push(...walk(path, extension));
|
||||
else if (stat.isFile() && extname(path) === extension) files.push(path);
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
function frameworkRanges(pkg: Record<string, unknown>): Map<string, string[]> {
|
||||
const ranges = new Map<string, string[]>();
|
||||
for (const field of ["dependencies", "devDependencies", "peerDependencies"]) {
|
||||
const deps = pkg[field] as Record<string, string> | undefined;
|
||||
for (const [name, range] of Object.entries(deps ?? {})) {
|
||||
if (!name.startsWith("@wrnexus/")) continue;
|
||||
const values = ranges.get(range) ?? [];
|
||||
values.push(name);
|
||||
ranges.set(range, values);
|
||||
}
|
||||
}
|
||||
return ranges;
|
||||
}
|
||||
|
||||
export function inspectProject(appRoot: string): DoctorCheck[] {
|
||||
const root = resolve(appRoot);
|
||||
const checks: DoctorCheck[] = [];
|
||||
const pkgPath = join(root, "package.json");
|
||||
const bunVersion = typeof Bun !== "undefined" ? String(Bun.version) : "";
|
||||
checks.push({
|
||||
name: "Bun runtime",
|
||||
ok: typeof Bun !== "undefined",
|
||||
detail: typeof Bun !== "undefined" ? `v${Bun.version}` : "Bun is required",
|
||||
ok: !!bunVersion && versionAtLeast(bunVersion, "1.3.0"),
|
||||
detail: bunVersion ? `v${bunVersion} (minimum 1.3.0)` : "Bun 1.3.0 or newer is required",
|
||||
});
|
||||
checks.push({
|
||||
name: "package.json",
|
||||
ok: existsSync(pkgPath),
|
||||
detail: existsSync(pkgPath) ? pkgPath : "Run this command from a WrNexus project root",
|
||||
detail: existsSync(pkgPath) ? pkgPath : "Run this command from a WRNexus project root",
|
||||
});
|
||||
|
||||
if (existsSync(pkgPath)) {
|
||||
try {
|
||||
const pkg = JSON.parse(readFileSync(pkgPath, "utf8")) as Record<string, unknown>;
|
||||
const ranges = frameworkRanges(pkg);
|
||||
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("; ")}`,
|
||||
});
|
||||
const marker = (pkg.wrnexus as { version?: string } | undefined)?.version;
|
||||
checks.push({
|
||||
name: "update marker",
|
||||
ok: !marker || versionAtLeast(marker, "0.3.0"),
|
||||
detail: marker ? `project last migrated to ${marker}` : "missing; run `wrnexus update`",
|
||||
level: "warning",
|
||||
});
|
||||
} catch {
|
||||
checks.push({ name: "package JSON", ok: false, detail: "package.json is invalid JSON" });
|
||||
}
|
||||
}
|
||||
|
||||
const app = join(root, "app");
|
||||
checks.push({
|
||||
name: "app/pages",
|
||||
@@ -34,13 +107,51 @@ export function inspectProject(appRoot: string): DoctorCheck[] {
|
||||
name: "configuration",
|
||||
ok: !!config,
|
||||
detail: config ?? "No wrnexus.config file; framework defaults will be used",
|
||||
level: "warning",
|
||||
});
|
||||
|
||||
const wrnFiles = walk(app, ".wrn");
|
||||
let syntaxErrors = 0;
|
||||
let syntaxWarnings = 0;
|
||||
for (const file of wrnFiles) {
|
||||
const diagnostics = diagnose(readFileSync(file, "utf8"), { file, accessibility: true });
|
||||
syntaxErrors += diagnostics.filter((item) => item.severity === "error").length;
|
||||
syntaxWarnings += diagnostics.filter((item) => item.severity !== "error").length;
|
||||
}
|
||||
checks.push({
|
||||
name: "WRN language",
|
||||
ok: syntaxErrors === 0,
|
||||
detail: `${wrnFiles.length} files, ${syntaxErrors} errors, ${syntaxWarnings} warnings`,
|
||||
});
|
||||
|
||||
if (existsSync(join(app, "pages"))) {
|
||||
try {
|
||||
const router = buildRouter(app);
|
||||
const conflicts = [
|
||||
...findRouteConflicts(router.pages),
|
||||
...findRouteConflicts(router.api),
|
||||
...findRouteConflicts(router.realtime),
|
||||
];
|
||||
checks.push({
|
||||
name: "route manifest",
|
||||
ok: conflicts.length === 0,
|
||||
detail: conflicts.length
|
||||
? conflicts.map((conflict) => conflict.raw).join(", ")
|
||||
: `${router.pages.length} pages, ${router.api.length} API, ${router.realtime.length} realtime`,
|
||||
});
|
||||
} catch (error) {
|
||||
checks.push({
|
||||
name: "route manifest",
|
||||
ok: false,
|
||||
detail: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const mobilePkg = join(root, "mobile", "package.json");
|
||||
if (existsSync(mobilePkg)) {
|
||||
try {
|
||||
const mobile = JSON.parse(readFileSync(mobilePkg, "utf8")) as {
|
||||
wrnexus?: { mode?: string };
|
||||
};
|
||||
const mobile = JSON.parse(readFileSync(mobilePkg, "utf8")) as { wrnexus?: { mode?: string } };
|
||||
checks.push({
|
||||
name: "mobile project",
|
||||
ok: mobile.wrnexus?.mode === "webview" || mobile.wrnexus?.mode === "native",
|
||||
@@ -57,12 +168,34 @@ export function inspectProject(appRoot: string): DoctorCheck[] {
|
||||
return checks;
|
||||
}
|
||||
|
||||
export function runDoctor(appRoot: string): boolean {
|
||||
const checks = inspectProject(appRoot);
|
||||
console.log("WrNexus doctor\n");
|
||||
for (const check of checks)
|
||||
console.log(` ${check.ok ? "✓" : "✗"} ${check.name}: ${check.detail}`);
|
||||
export async function runDoctor(appRoot: string): Promise<boolean> {
|
||||
const root = resolve(appRoot);
|
||||
const checks = inspectProject(root);
|
||||
try {
|
||||
const config = await loadAppConfig(root);
|
||||
const issues = validateAppConfig(config);
|
||||
checks.push({
|
||||
name: "resolved configuration",
|
||||
ok: issues.every((issue) => issue.severity !== "error"),
|
||||
detail: issues.length
|
||||
? issues.map((issue) => `${issue.path}: ${issue.message}`).join("; ")
|
||||
: "valid",
|
||||
level: issues.some((issue) => issue.severity === "error") ? "error" : "warning",
|
||||
});
|
||||
} catch (error) {
|
||||
checks.push({
|
||||
name: "resolved configuration",
|
||||
ok: false,
|
||||
detail: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
}
|
||||
|
||||
console.log("WRNexus doctor\n");
|
||||
for (const check of checks) {
|
||||
const optional = check.level === "warning";
|
||||
console.log(` ${check.ok ? "✓" : optional ? "⚠" : "✗"} ${check.name}: ${check.detail}`);
|
||||
}
|
||||
console.log("\n Security dependencies: run `bun audit`");
|
||||
console.log(" Complete verification: run `bun run check`");
|
||||
return checks.every((check) => check.ok || check.name === "configuration");
|
||||
return checks.every((check) => check.ok || check.level === "warning");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user