refactor: delete the compatibility config surface
This commit is contained in:
@@ -13,8 +13,6 @@ import type { AppConfig } from "@wrnexus/styles";
|
||||
* framework-level headers and optional CORS.
|
||||
*/
|
||||
const config: AppConfig = {
|
||||
frameworkBehaviour: 1,
|
||||
compatibilityDate: "2026-08-02",
|
||||
head: [
|
||||
// --- Use a CSS framework via CDN (uncomment one) ---
|
||||
// Bootstrap:
|
||||
|
||||
@@ -4,8 +4,6 @@ import { join } from "node:path";
|
||||
import type { AppConfig } from "@wrnexus/styles";
|
||||
|
||||
const config: AppConfig = {
|
||||
compatibilityDate: "2026-08-02",
|
||||
frameworkBehaviour: 1,
|
||||
// v0.8 defaults: explicit imports, strict template types, safe stores, and
|
||||
// automatic progressive navigation. Package plugins are discovered from the
|
||||
// installed packages above; add custom plugins to this array when needed.
|
||||
@@ -19,14 +17,7 @@ const config: AppConfig = {
|
||||
checkComponentProps: true,
|
||||
generateDeclarations: true,
|
||||
},
|
||||
functions: { legacyDefaultRuntime: "current" },
|
||||
stores: { strictMutations: true, persistence: true },
|
||||
compatibility: {
|
||||
legacyEmit: false,
|
||||
legacyEventProps: false,
|
||||
legacyComponentDiscovery: false,
|
||||
stringLayouts: false,
|
||||
},
|
||||
experimental: {},
|
||||
|
||||
performance: {
|
||||
|
||||
@@ -4,8 +4,6 @@ import { join } from "node:path";
|
||||
import type { AppConfig } from "@wrnexus/styles";
|
||||
|
||||
const config: AppConfig = {
|
||||
compatibilityDate: "2026-08-02",
|
||||
frameworkBehaviour: 1,
|
||||
// v0.8 defaults: explicit imports, strict template types, safe stores, and
|
||||
// automatic progressive navigation. Package plugins are discovered from the
|
||||
// installed packages above; add custom plugins to this array when needed.
|
||||
@@ -19,14 +17,7 @@ const config: AppConfig = {
|
||||
checkComponentProps: true,
|
||||
generateDeclarations: true,
|
||||
},
|
||||
functions: { legacyDefaultRuntime: "current" },
|
||||
stores: { strictMutations: true, persistence: true },
|
||||
compatibility: {
|
||||
legacyEmit: false,
|
||||
legacyEventProps: false,
|
||||
legacyComponentDiscovery: false,
|
||||
stringLayouts: false,
|
||||
},
|
||||
experimental: {},
|
||||
|
||||
performance: {
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { basename, join, resolve } from "node:path";
|
||||
import {
|
||||
CURRENT_COMPATIBILITY_DATE,
|
||||
CURRENT_FRAMEWORK_BEHAVIOUR,
|
||||
loadRawConfig,
|
||||
resolveCompatibility,
|
||||
} from "@wrnexus/styles";
|
||||
|
||||
const CONFIG_NAMES = ["wrnexus.config.ts", "wrnexus.config.js", "wrnexus.config.mjs"];
|
||||
|
||||
function configPath(root: string): string | undefined {
|
||||
return CONFIG_NAMES.map((name) => join(root, name)).find(existsSync);
|
||||
}
|
||||
|
||||
export async function compatibilityReport(appRoot: string) {
|
||||
return resolveCompatibility(await loadRawConfig(resolve(appRoot)));
|
||||
}
|
||||
|
||||
export function upgradeCompatibility(appRoot: string): {
|
||||
file: string;
|
||||
backup: string;
|
||||
changed: boolean;
|
||||
} {
|
||||
const root = resolve(appRoot);
|
||||
const file = configPath(root);
|
||||
if (!file) throw new Error("WRN-COMPATIBILITY-NO-CONFIG: wrnexus.config.ts was not found.");
|
||||
const source = readFileSync(file, "utf8");
|
||||
let updated = source;
|
||||
const replace = (name: string, value: string) => {
|
||||
const pattern = new RegExp(`(^\\s*${name}\\s*:\\s*)(?:["'][^"']*["']|\\d+)(\\s*,?)`, "m");
|
||||
if (pattern.test(updated)) updated = updated.replace(pattern, `$1${value}$2`);
|
||||
else {
|
||||
const object = /(?:const\s+config[^=]*=|defineConfig\s*\(|export\s+default)\s*\{/m;
|
||||
if (!object.test(updated))
|
||||
throw new Error("WRN-COMPATIBILITY-CONFIG-SHAPE: unable to locate the root config object.");
|
||||
updated = updated.replace(object, (match) => `${match}\n ${name}: ${value},`);
|
||||
}
|
||||
};
|
||||
replace("compatibilityDate", JSON.stringify(CURRENT_COMPATIBILITY_DATE));
|
||||
replace("frameworkBehaviour", String(CURRENT_FRAMEWORK_BEHAVIOUR));
|
||||
if (updated === source) return { file, backup: "", changed: false };
|
||||
const directory = join(root, ".wrnexus", "compatibility-backups");
|
||||
mkdirSync(directory, { recursive: true });
|
||||
const backup = join(directory, `${Date.now()}-${basename(file)}`);
|
||||
copyFileSync(file, backup);
|
||||
writeFileSync(file, updated, "utf8");
|
||||
return { file, backup, changed: true };
|
||||
}
|
||||
|
||||
export async function runCompatibilityCommand(
|
||||
appRoot: string,
|
||||
command = "check",
|
||||
args: string[] = [],
|
||||
): Promise<boolean> {
|
||||
if (command === "upgrade") {
|
||||
const result = upgradeCompatibility(appRoot);
|
||||
if (args.includes("--json")) console.log(JSON.stringify(result, null, 2));
|
||||
else
|
||||
console.log(
|
||||
result.changed
|
||||
? `✓ Compatibility policy upgraded\n backup: ${result.backup}`
|
||||
: "✓ Compatibility policy already current",
|
||||
);
|
||||
}
|
||||
const report = await compatibilityReport(appRoot);
|
||||
if (args.includes("--json")) console.log(JSON.stringify(report, null, 2));
|
||||
else {
|
||||
console.log(`Compatibility date: ${report.effectiveDate} (current ${report.currentDate})`);
|
||||
console.log(
|
||||
`Framework behaviour: ${report.effectiveBehaviour} (current ${report.currentBehaviour})`,
|
||||
);
|
||||
for (const message of report.messages) console.log(`- ${message}`);
|
||||
}
|
||||
return !report.needsUpgrade && !report.future;
|
||||
}
|
||||
@@ -260,8 +260,6 @@ trim_trailing_whitespace = true
|
||||
"wrnexus.config.ts": `import type { AppConfig } from "@wrnexus/styles";
|
||||
|
||||
const config: AppConfig = {
|
||||
compatibilityDate: "2026-08-02",
|
||||
frameworkBehaviour: 1,
|
||||
// v0.8 defaults: explicit imports, strict template types, safe stores, and
|
||||
// automatic progressive navigation. Package plugins are discovered from the
|
||||
// installed packages above; add custom plugins to this array when needed.
|
||||
@@ -275,14 +273,7 @@ const config: AppConfig = {
|
||||
checkComponentProps: true,
|
||||
generateDeclarations: true,
|
||||
},
|
||||
functions: { legacyDefaultRuntime: "current" },
|
||||
stores: { strictMutations: true, persistence: true },
|
||||
compatibility: {
|
||||
legacyEmit: false,
|
||||
legacyEventProps: false,
|
||||
legacyComponentDiscovery: false,
|
||||
stringLayouts: false,
|
||||
},
|
||||
experimental: {},
|
||||
|
||||
performance: {
|
||||
|
||||
@@ -72,8 +72,6 @@ Usage:
|
||||
Run unit | component | api | browser | visual | accessibility | performance
|
||||
wrnexus profiles [app-dir] List config profiles (dev/prod/uat/…) and their env files
|
||||
wrnexus doctor [app-dir] [--fix] Check project health; optionally apply safe repairs
|
||||
wrnexus compatibility <check|explain|upgrade> [app-dir]
|
||||
Inspect or explicitly upgrade behavior defaults
|
||||
wrnexus contracts <check|snapshot> [app-dir]
|
||||
Detect breaking boundary contract changes
|
||||
wrnexus security <audit|headers|test> [app-dir]
|
||||
@@ -292,16 +290,6 @@ async function main(): Promise<void> {
|
||||
if (!healthy) process.exitCode = 1;
|
||||
break;
|
||||
}
|
||||
case "compatibility": {
|
||||
const { runCompatibilityCommand } = await import("./compatibility-command.ts");
|
||||
const subcommand = rest.find((value) => !value.startsWith("--")) ?? "check";
|
||||
const appRoot = rest.filter((value) => !value.startsWith("--"))[1] ?? ".";
|
||||
if (!["check", "explain", "upgrade"].includes(subcommand))
|
||||
throw new Error(`WRN-COMPATIBILITY-COMMAND: unknown command '${subcommand}'.`);
|
||||
const current = await runCompatibilityCommand(appRoot, subcommand, rest);
|
||||
if (!current && subcommand !== "explain") process.exitCode = 1;
|
||||
break;
|
||||
}
|
||||
case "contracts": {
|
||||
const { runContractsCommand } = await import("./contracts-command.ts");
|
||||
const subcommand = rest.find((value) => !value.startsWith("--")) ?? "check";
|
||||
|
||||
@@ -385,8 +385,8 @@ function updateV060Config(ctx: MigrationCtx): void {
|
||||
const file = candidates.map((name) => join(ctx.appRoot, name)).find(existsSync);
|
||||
if (!file) return;
|
||||
const current = readFileSync(file, "utf8");
|
||||
if (/\bimports\s*:/.test(current) && /\bcompatibility\s*:/.test(current)) return;
|
||||
const insertion = `\n imports: { mode: "compatible", autoImport: true, aliases: { "@": "./app" } },\n types: { strict: false, noImplicitAny: false, strictNullChecks: true, checkTemplates: true, checkComponentProps: true, generateDeclarations: true, globalTypes: "./app/types/global.d.ts" },\n functions: { legacyDefaultRuntime: "current" },\n stores: { strictMutations: true, persistence: true },\n compatibility: { legacyEmit: true, legacyEventProps: true, legacyComponentDiscovery: true, stringLayouts: true },`;
|
||||
if (/\bimports\s*:/.test(current)) return;
|
||||
const insertion = `\n imports: { mode: "compatible", autoImport: true, aliases: { "@": "./app" } },\n types: { strict: false, noImplicitAny: false, strictNullChecks: true, checkTemplates: true, checkComponentProps: true, generateDeclarations: true, globalTypes: "./app/types/global.d.ts" },\n stores: { strictMutations: true, persistence: true },`;
|
||||
const index = current.lastIndexOf("}");
|
||||
if (index < 0) return;
|
||||
const next =
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { mkdtempSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { compatibilityReport, upgradeCompatibility } from "../src/compatibility-command.ts";
|
||||
|
||||
test("compatibility upgrade is backed up, current, and idempotent", async () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "wrnexus-compatibility-"));
|
||||
const file = join(root, "wrnexus.config.ts");
|
||||
writeFileSync(file, `export default { port: 3000 };\n`);
|
||||
const first = upgradeCompatibility(root);
|
||||
expect(first.changed).toBe(true);
|
||||
expect(readFileSync(first.backup, "utf8")).toContain("port: 3000");
|
||||
expect(readFileSync(file, "utf8")).toContain('compatibilityDate: "2026-08-02"');
|
||||
expect(upgradeCompatibility(root).changed).toBe(false);
|
||||
expect((await compatibilityReport(root)).needsUpgrade).toBe(false);
|
||||
});
|
||||
@@ -96,7 +96,6 @@ test("scaffoldApp includes the complete v0.8 configuration and starter structure
|
||||
"imports:",
|
||||
"types:",
|
||||
"stores:",
|
||||
"compatibility:",
|
||||
"performance:",
|
||||
"observability:",
|
||||
"tenancy:",
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
export const CURRENT_COMPATIBILITY_DATE = "2026-08-02";
|
||||
export const CURRENT_FRAMEWORK_BEHAVIOUR = 1;
|
||||
|
||||
export interface CompatibilityPolicy {
|
||||
compatibilityDate?: string;
|
||||
frameworkBehaviour?: number;
|
||||
}
|
||||
|
||||
export interface CompatibilityReport {
|
||||
configuredDate?: string;
|
||||
effectiveDate: string;
|
||||
currentDate: string;
|
||||
configuredBehaviour?: number;
|
||||
effectiveBehaviour: number;
|
||||
currentBehaviour: number;
|
||||
needsUpgrade: boolean;
|
||||
future: boolean;
|
||||
messages: string[];
|
||||
}
|
||||
|
||||
export function isCompatibilityDate(value: string): boolean {
|
||||
if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) return false;
|
||||
const date = new Date(`${value}T00:00:00.000Z`);
|
||||
return Number.isFinite(date.getTime()) && date.toISOString().slice(0, 10) === value;
|
||||
}
|
||||
|
||||
export function resolveCompatibility(policy: CompatibilityPolicy): CompatibilityReport {
|
||||
const configuredDate = policy.compatibilityDate;
|
||||
const configuredBehaviour = policy.frameworkBehaviour;
|
||||
const effectiveDate = configuredDate ?? "1970-01-01";
|
||||
const effectiveBehaviour = configuredBehaviour ?? 0;
|
||||
const future =
|
||||
(configuredDate !== undefined && configuredDate > CURRENT_COMPATIBILITY_DATE) ||
|
||||
(configuredBehaviour !== undefined && configuredBehaviour > CURRENT_FRAMEWORK_BEHAVIOUR);
|
||||
const needsUpgrade =
|
||||
!future &&
|
||||
(effectiveDate < CURRENT_COMPATIBILITY_DATE ||
|
||||
effectiveBehaviour < CURRENT_FRAMEWORK_BEHAVIOUR);
|
||||
const messages: string[] = [];
|
||||
if (!configuredDate) messages.push("compatibilityDate is not configured; legacy defaults apply.");
|
||||
if (!configuredBehaviour)
|
||||
messages.push("frameworkBehaviour is not configured; behaviour version 0 applies.");
|
||||
if (future)
|
||||
messages.push("Configuration targets framework behavior newer than this CLI supports.");
|
||||
else if (needsUpgrade)
|
||||
messages.push("A newer compatibility policy is available; review it before upgrading.");
|
||||
else messages.push("Compatibility policy matches the current framework behavior.");
|
||||
return {
|
||||
configuredDate,
|
||||
effectiveDate,
|
||||
currentDate: CURRENT_COMPATIBILITY_DATE,
|
||||
configuredBehaviour,
|
||||
effectiveBehaviour,
|
||||
currentBehaviour: CURRENT_FRAMEWORK_BEHAVIOUR,
|
||||
needsUpgrade,
|
||||
future,
|
||||
messages,
|
||||
};
|
||||
}
|
||||
@@ -17,11 +17,6 @@ import type { StorageConfig } from "@wrnexus/uploader";
|
||||
import type { BrowserCookiesConfig, ThemeConfig } from "./theme.ts";
|
||||
import type { FontConfig } from "./fonts.ts";
|
||||
import { fontCspSources } from "./fonts.ts";
|
||||
import {
|
||||
isCompatibilityDate,
|
||||
resolveCompatibility,
|
||||
type CompatibilityPolicy,
|
||||
} from "./compatibility.ts";
|
||||
|
||||
export type Mode = "development" | "production";
|
||||
|
||||
@@ -230,23 +225,12 @@ export interface TypesConfig {
|
||||
globalTypes?: string;
|
||||
}
|
||||
|
||||
export interface FunctionsConfig {
|
||||
legacyDefaultRuntime?: "current" | "client" | "server" | "shared";
|
||||
}
|
||||
|
||||
export interface StoresConfig {
|
||||
strictMutations?: boolean;
|
||||
persistence?: boolean;
|
||||
}
|
||||
|
||||
export interface CompatibilityConfig {
|
||||
legacyEmit?: boolean;
|
||||
legacyEventProps?: boolean;
|
||||
legacyComponentDiscovery?: boolean;
|
||||
stringLayouts?: boolean;
|
||||
}
|
||||
|
||||
export interface AppConfig extends CompatibilityPolicy {
|
||||
export interface AppConfig {
|
||||
/** Ordered reusable configuration layers; the application always has final precedence. */
|
||||
extends?: string | string[];
|
||||
/** Compiler/dev/build plugins, resolved in deterministic pre/normal/post order. */
|
||||
@@ -260,12 +244,8 @@ export interface AppConfig extends CompatibilityPolicy {
|
||||
imports?: ImportsConfig;
|
||||
/** TypeScript-backed .wrn type checking and declaration generation. */
|
||||
types?: TypesConfig;
|
||||
/** Legacy function runtime behavior for existing applications. */
|
||||
functions?: FunctionsConfig;
|
||||
/** Typed global/page store behavior. */
|
||||
stores?: StoresConfig;
|
||||
/** Temporary v0.5 syntax compatibility switches. */
|
||||
compatibility?: CompatibilityConfig;
|
||||
/** Opt-in APIs that are not yet covered by stable compatibility guarantees. */
|
||||
experimental?: ExperimentalConfig;
|
||||
/** Route and asset budgets plus build analyzer behavior. */
|
||||
@@ -597,32 +577,23 @@ export function defineConfig(config: AppConfig): AppConfig {
|
||||
return config;
|
||||
}
|
||||
|
||||
const REMOVED_CONFIG_KEYS = [
|
||||
"compatibilityDate",
|
||||
"frameworkBehaviour",
|
||||
"functions",
|
||||
"compatibility",
|
||||
] as const;
|
||||
|
||||
export function validateAppConfig(config: AppConfig): ConfigIssue[] {
|
||||
const issues: ConfigIssue[] = [];
|
||||
if (config.compatibilityDate !== undefined && !isCompatibilityDate(config.compatibilityDate)) {
|
||||
issues.push({
|
||||
path: "compatibilityDate",
|
||||
severity: "error",
|
||||
message: "must be a real ISO calendar date in YYYY-MM-DD format",
|
||||
});
|
||||
}
|
||||
if (
|
||||
config.frameworkBehaviour !== undefined &&
|
||||
(!Number.isInteger(config.frameworkBehaviour) || config.frameworkBehaviour < 1)
|
||||
) {
|
||||
issues.push({
|
||||
path: "frameworkBehaviour",
|
||||
severity: "error",
|
||||
message: "must be a positive integer",
|
||||
});
|
||||
}
|
||||
const compatibility = resolveCompatibility(config);
|
||||
if (compatibility.future) {
|
||||
issues.push({
|
||||
path: "compatibilityDate",
|
||||
severity: "error",
|
||||
message: "targets framework behavior newer than this version supports",
|
||||
});
|
||||
for (const key of REMOVED_CONFIG_KEYS) {
|
||||
if ((config as Record<string, unknown>)[key] !== undefined) {
|
||||
issues.push({
|
||||
path: key,
|
||||
severity: "error",
|
||||
message: "was removed; delete it from the configuration",
|
||||
});
|
||||
}
|
||||
}
|
||||
const sampleRate = config.observability?.sampleRate;
|
||||
if (
|
||||
|
||||
@@ -36,13 +36,6 @@ export {
|
||||
resolveConfigLayers,
|
||||
validateAppConfig,
|
||||
} from "./config.ts";
|
||||
export {
|
||||
CURRENT_COMPATIBILITY_DATE,
|
||||
CURRENT_FRAMEWORK_BEHAVIOUR,
|
||||
isCompatibilityDate,
|
||||
resolveCompatibility,
|
||||
} from "./compatibility.ts";
|
||||
export type { CompatibilityPolicy, CompatibilityReport } from "./compatibility.ts";
|
||||
export { findStyleEntry, bundleCss } from "./styles.ts";
|
||||
export type { FontConfig, GoogleFont, LocalFontFace, FontDisplay } from "./fonts.ts";
|
||||
export { renderFontHead, renderProductionFontHead, fontCspSources } from "./fonts.ts";
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
resolveProfile,
|
||||
loadEnv,
|
||||
renderStyles,
|
||||
resolveCompatibility,
|
||||
validateAppConfig,
|
||||
explainAppConfig,
|
||||
} from "../src/index.ts";
|
||||
@@ -16,18 +15,6 @@ afterEach(() => {
|
||||
delete process.env.WRNEXUS_PROFILE;
|
||||
});
|
||||
|
||||
test("compatibility dates pin behavior and reject invalid or future policies", () => {
|
||||
expect(
|
||||
resolveCompatibility({ compatibilityDate: "2026-08-02", frameworkBehaviour: 1 }),
|
||||
).toMatchObject({ needsUpgrade: false, future: false, effectiveBehaviour: 1 });
|
||||
expect(validateAppConfig({ compatibilityDate: "2026-02-31" })).toContainEqual(
|
||||
expect.objectContaining({ path: "compatibilityDate", severity: "error" }),
|
||||
);
|
||||
expect(validateAppConfig({ frameworkBehaviour: 2 })).toContainEqual(
|
||||
expect.objectContaining({ severity: "error" }),
|
||||
);
|
||||
});
|
||||
|
||||
test("observability config requires bounded sampling and a valid OTLP endpoint", () => {
|
||||
expect(validateAppConfig({ observability: { sampleRate: Number.NaN } })).toContainEqual(
|
||||
expect.objectContaining({ path: "observability.sampleRate", severity: "error" }),
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { validateAppConfig } from "../src/config.ts";
|
||||
|
||||
// A stale config must fail loudly. Silently ignoring a removed key leaves
|
||||
// someone believing a flag still applies.
|
||||
const REMOVED = [
|
||||
{ key: "compatibilityDate", config: { compatibilityDate: "2026-08-02" } },
|
||||
{ key: "frameworkBehaviour", config: { frameworkBehaviour: 1 } },
|
||||
{ key: "functions", config: { functions: { legacyDefaultRuntime: "current" } } },
|
||||
{ key: "compatibility", config: { compatibility: { legacyEmit: false } } },
|
||||
];
|
||||
|
||||
for (const { key, config } of REMOVED) {
|
||||
test(`a config still setting "${key}" is rejected with a message naming it`, () => {
|
||||
const issues = validateAppConfig(config as never);
|
||||
const match = issues.find((issue) => issue.path === key || issue.path.startsWith(`${key}.`));
|
||||
|
||||
expect(match).toBeDefined();
|
||||
expect(match!.severity).toBe("error");
|
||||
expect(match!.message.toLowerCase()).toContain("removed");
|
||||
});
|
||||
}
|
||||
|
||||
test("a config without those keys is accepted", () => {
|
||||
const issues = validateAppConfig({} as never);
|
||||
|
||||
expect(issues.filter((issue) => issue.severity === "error")).toEqual([]);
|
||||
});
|
||||
Reference in New Issue
Block a user