release: WRNexusJS 0.8.0
Quality / quality (ubuntu-latest) (push) Failing after 21s
Quality / quality (windows-latest) (push) Canceled after 0s

This commit is contained in:
2026-08-02 23:18:51 +05:30
parent 87507edf59
commit 586a6db8ff
625 changed files with 243608 additions and 11210 deletions
+78
View File
@@ -0,0 +1,78 @@
import type { WrnexusPackageManifest } from "./types.ts";
export interface PluginCompatibilityTarget {
runtime: "bun" | "node" | "edge" | "worker" | "service-worker" | "browser";
version?: string;
os?: "win32" | "linux" | "darwin" | string;
capabilities?: readonly string[];
}
export interface PluginCompatibilityResult {
target: PluginCompatibilityTarget;
ok: boolean;
issues: Array<{
code:
| "WRN-PLUGIN-MATRIX-RUNTIME"
| "WRN-PLUGIN-MATRIX-VERSION"
| "WRN-PLUGIN-MATRIX-OS"
| "WRN-PLUGIN-MATRIX-CAPABILITY";
message: string;
}>;
}
function parts(version: string): number[] {
return version
.replace(/^[^\d]*/, "")
.split(/[.-]/)
.slice(0, 3)
.map((value) => Number(value) || 0);
}
function atLeast(version: string, minimum: string): boolean {
const left = parts(version);
const right = parts(minimum);
for (let index = 0; index < 3; index += 1) {
if (left[index] !== right[index]) return (left[index] ?? 0) > (right[index] ?? 0);
}
return true;
}
export function testPluginCompatibility(
manifest: WrnexusPackageManifest,
targets: readonly PluginCompatibilityTarget[],
): PluginCompatibilityResult[] {
return targets.map((target) => {
const issues: PluginCompatibilityResult["issues"] = [];
if (manifest.runtimes?.length && !manifest.runtimes.includes(target.runtime)) {
issues.push({
code: "WRN-PLUGIN-MATRIX-RUNTIME",
message: `${target.runtime} is not declared.`,
});
}
const compatibility = manifest.compatibility;
if (
target.runtime === "bun" &&
target.version &&
compatibility?.bunMin &&
!atLeast(target.version, compatibility.bunMin)
) {
issues.push({
code: "WRN-PLUGIN-MATRIX-VERSION",
message: `Bun ${target.version} is below ${compatibility.bunMin}.`,
});
}
if (target.os && compatibility?.os?.length && !compatibility.os.includes(target.os)) {
issues.push({ code: "WRN-PLUGIN-MATRIX-OS", message: `${target.os} is not declared.` });
}
const missing = (manifest.requires ?? []).filter(
(capability) => !target.capabilities?.includes(capability),
);
if (missing.length) {
issues.push({
code: "WRN-PLUGIN-MATRIX-CAPABILITY",
message: `Missing: ${missing.join(", ")}.`,
});
}
return { target: { ...target }, ok: issues.length === 0, issues };
});
}
+101
View File
@@ -12,8 +12,10 @@ import type {
PackageStyleDefinition,
PackageRouteDefinition,
PackageMigrationDefinition,
PluginPermission,
} from "./types.ts";
import { definePlugin, flattenPlugins } from "./base.ts";
import { testPluginCompatibility } from "./compatibility.ts";
interface PackageJson {
name?: string;
@@ -32,6 +34,70 @@ export interface DiscoverPluginOptions {
strict?: boolean;
includePeerDependencies?: boolean;
warn?: (message: string) => void;
runtime?: string;
capabilities?: readonly string[];
runtimeVersion?: string;
os?: string;
/** When true, every requested capability must be declared and granted by the application. */
enforcePermissions?: boolean;
grantedPermissions?: Readonly<Record<string, readonly PluginPermission[]>>;
}
function manifestPermissions(manifest: WrnexusPackageManifest): PluginPermission[] {
const permissions: PluginPermission[] = [];
if (manifest.components?.length) permissions.push("components");
if (manifest.clientRuntimes?.length) permissions.push("client-runtime");
if (manifest.assets?.length) permissions.push("assets");
if (manifest.styles?.length) permissions.push("styles");
if (manifest.routes?.length) permissions.push("routes");
if (manifest.middleware?.length) permissions.push("middleware");
if (manifest.migrations?.length) permissions.push("migrations");
return permissions;
}
function modulePermissions(plugin: WrnexusPlugin): PluginPermission[] {
const permissions: PluginPermission[] = [];
if (plugin.componentDirs) permissions.push("components");
if (plugin.clientRuntimes) permissions.push("client-runtime");
if (plugin.assets) permissions.push("assets");
if (plugin.styleSources) permissions.push("styles");
if (plugin.routeEntries || plugin.routes) permissions.push("routes");
if (plugin.middleware) permissions.push("middleware");
if (plugin.migrations) permissions.push("migrations");
if (plugin.configure || plugin.configResolved) permissions.push("config");
if (plugin.transformAst || plugin.transformCode) permissions.push("transform");
if (plugin.diagnostics || plugin.devToolbarPanels) permissions.push("diagnostics");
if (plugin.configureServer || plugin.buildStart || plugin.buildEnd) permissions.push("server");
if (plugin.cliCommands) permissions.push("cli");
if (plugin.directives) permissions.push("directives");
if (plugin.virtualModules) permissions.push("virtual-modules");
if (plugin.deploymentAdapters || plugin.deploy) permissions.push("deployment");
if (plugin.documentation) permissions.push("documentation");
if (plugin.typeDefinitions) permissions.push("types");
return [...new Set(permissions)];
}
function enforcePermissions(
packageName: string,
manifest: WrnexusPackageManifest,
required: readonly PluginPermission[],
options: DiscoverPluginOptions,
): void {
if (!options.enforcePermissions) return;
const declared = new Set(manifest.permissions ?? []);
const undeclared = required.filter((permission) => !declared.has(permission));
if (undeclared.length) {
throw new Error(
`WRN-PLUGIN-PERMISSION-UNDECLARED: ${packageName} uses ${undeclared.join(", ")} without declaring them.`,
);
}
const grants = new Set(options.grantedPermissions?.[packageName] ?? []);
const denied = required.filter((permission) => !grants.has(permission));
if (denied.length) {
throw new Error(
`WRN-PLUGIN-PERMISSION-DENIED: ${packageName} is not granted ${denied.join(", ")}.`,
);
}
}
function readJson(path: string): PackageJson | null {
@@ -244,6 +310,40 @@ export async function discoverPlugins(
const packageJson = readJson(join(packageRoot, "package.json"));
const packageManifest = packageJson?.wrnexus;
if (!packageManifest) continue;
enforcePermissions(name, packageManifest, manifestPermissions(packageManifest), options);
if (
options.runtime &&
packageManifest.runtimes?.length &&
!packageManifest.runtimes.includes(options.runtime as never)
) {
throw new Error(`WRN-PLUGIN-RUNTIME: ${name} does not support ${options.runtime}.`);
}
const missingCapabilities = (packageManifest.requires ?? []).filter(
(capability) => !options.capabilities?.includes(capability),
);
if (options.runtime && missingCapabilities.length) {
throw new Error(
`WRN-PLUGIN-CAPABILITY: ${name} requires unavailable capabilities: ${missingCapabilities.join(", ")}.`,
);
}
if (options.runtime) {
const matrix = testPluginCompatibility(packageManifest, [
{
runtime: options.runtime as
"bun" | "node" | "edge" | "worker" | "service-worker" | "browser",
version:
options.runtimeVersion ??
(options.runtime === "bun" && typeof Bun !== "undefined" ? Bun.version : undefined),
os: options.os ?? process.platform,
capabilities: options.capabilities,
},
])[0]!;
const compatibilityIssue = matrix.issues.find((issue) =>
["WRN-PLUGIN-MATRIX-VERSION", "WRN-PLUGIN-MATRIX-OS"].includes(issue.code),
);
if (compatibilityIssue)
throw new Error(`${compatibilityIssue.code}: ${name}: ${compatibilityIssue.message}`);
}
const manifestPlugin = manifestContributionPlugin(
packageRoot,
packageJson?.name ?? name,
@@ -258,6 +358,7 @@ export async function discoverPlugins(
if (manifest.autoDiscover === false) continue;
try {
const plugin = await loadPlugin(packageRoot, manifest);
enforcePermissions(name, packageManifest, modulePermissions(plugin), options);
if (!explicitNames.has(plugin.name) && !automatic.some((item) => item.name === plugin.name)) {
automatic.push(plugin);
}
+88 -3
View File
@@ -1,4 +1,4 @@
import type { PageAst, WrnDiagnostic } from "@wrnexus/syntax";
import type { PageAst, ViewNode, WrnDiagnostic } from "@wrnexus/syntax";
import { normalizeClientRuntime, normalizePackageAsset, validateStyleIds } from "./manifest.ts";
import type {
ClientRuntimeDefinition,
@@ -11,7 +11,13 @@ import type {
PluginContext,
PluginInput,
PluginOrder,
PluginPermission,
PluginRunner,
PluginDirective,
PluginCliCommand,
PluginVirtualModule,
PluginDeploymentAdapter,
PluginConfigSchema,
TransformContext,
WrnexusPlugin,
} from "./types.ts";
@@ -19,6 +25,8 @@ import type {
export * from "./types.ts";
export * from "./manifest.ts";
export { discoverPlugins, type DiscoverPluginOptions } from "./discovery.ts";
export { testPluginCompatibility } from "./compatibility.ts";
export type { PluginCompatibilityResult, PluginCompatibilityTarget } from "./compatibility.ts";
export { definePlugin, flattenPlugins } from "./base.ts";
import { flattenPlugins } from "./base.ts";
@@ -112,9 +120,40 @@ function assertUnique<T>(kind: string, entries: readonly T[], key: (entry: T) =>
}
}
async function transformDirectives(
nodes: ViewNode[],
directives: readonly PluginDirective[],
context: TransformContext,
): Promise<void> {
const registry = new Map(directives.map((directive) => [directive.name, directive]));
for (const node of nodes) {
if (node.type === "element") {
for (const attribute of node.attrs) {
if (!attribute.name.startsWith("use:")) continue;
const name = attribute.name.slice(4);
const directive = registry.get(name);
if (!directive) continue;
const transformed = await directive.transform?.(attribute.value, context);
if (transformed) {
attribute.name = transformed.name;
attribute.value = transformed.value;
} else attribute.name = `data-wrn-directive-${name}`;
}
await transformDirectives(node.children, directives, context);
} else if (node.type === "each") {
await transformDirectives(node.body, directives, context);
await transformDirectives(node.empty, directives, context);
} else if (node.type === "if") {
for (const branch of node.branches)
await transformDirectives(branch.body, directives, context);
}
}
}
export function createPluginRunner(input: PluginInput, context: PluginContext): PluginRunner {
const plugins = resolvePlugins(input);
let contributionCache: PluginContributions | null = null;
let setupComplete = false;
const transformContext = (file: string): TransformContext => ({ ...context, file });
const contributions = async (): Promise<PluginContributions> => {
@@ -126,6 +165,13 @@ export function createPluginRunner(input: PluginInput, context: PluginContext):
const routes: PackageRouteDefinition[] = [];
const middleware: string[] = [];
const migrations: PackageMigrationDefinition[] = [];
const directives: PluginDirective[] = [];
const cliCommands: PluginCliCommand[] = [];
const virtualModules: PluginVirtualModule[] = [];
const deploymentAdapters: PluginDeploymentAdapter[] = [];
const configSchemas: PluginConfigSchema[] = [];
const documentation: string[] = [];
const typeDefinitions: string[] = [];
for (const plugin of plugins) {
componentDirs.push(...(await resolveContribution(plugin.componentDirs, context)));
@@ -135,6 +181,13 @@ export function createPluginRunner(input: PluginInput, context: PluginContext):
routes.push(...(await resolveContribution(plugin.routeEntries, context)));
middleware.push(...(await resolveContribution(plugin.middleware, context)));
migrations.push(...(await resolveContribution(plugin.migrations, context)));
directives.push(...(await resolveContribution(plugin.directives, context)));
cliCommands.push(...(await resolveContribution(plugin.cliCommands, context)));
virtualModules.push(...(await resolveContribution(plugin.virtualModules, context)));
deploymentAdapters.push(...(await resolveContribution(plugin.deploymentAdapters, context)));
configSchemas.push(...(await resolveContribution(plugin.configSchemas, context)));
documentation.push(...(await resolveContribution(plugin.documentation, context)));
typeDefinitions.push(...(await resolveContribution(plugin.typeDefinitions, context)));
}
const normalizedRuntimes = clientRuntimes.map(normalizeClientRuntime);
@@ -164,6 +217,11 @@ export function createPluginRunner(input: PluginInput, context: PluginContext):
}
}
assertUnique("MIGRATION", migrations, (entry) => `${entry.database ?? "default"}:${entry.id}`);
assertUnique("DIRECTIVE", directives, (entry) => entry.name);
assertUnique("CLI-COMMAND", cliCommands, (entry) => entry.name);
assertUnique("VIRTUAL-MODULE", virtualModules, (entry) => entry.id);
assertUnique("DEPLOYMENT-ADAPTER", deploymentAdapters, (entry) => entry.name);
assertUnique("CONFIG-SCHEMA", configSchemas, (entry) => entry.namespace);
contributionCache = {
componentDirs: [...new Set(componentDirs)],
@@ -173,6 +231,13 @@ export function createPluginRunner(input: PluginInput, context: PluginContext):
routes,
middleware: [...new Set(middleware)],
migrations,
directives,
cliCommands,
virtualModules,
deploymentAdapters,
configSchemas,
documentation: [...new Set(documentation)],
typeDefinitions: [...new Set(typeDefinitions)],
};
context.metadata.set("@wrnexus/plugin:contributions", contributionCache);
return contributionCache;
@@ -181,18 +246,29 @@ export function createPluginRunner(input: PluginInput, context: PluginContext):
return {
plugins,
async configure(config) {
if (!setupComplete) {
for (const plugin of plugins) await plugin.setup?.(context);
setupComplete = true;
}
for (const plugin of plugins) await plugin.configure?.(config, context);
contributionCache = null;
},
async configResolved(config) {
for (const plugin of plugins) await plugin.configResolved?.(config, context);
contributionCache = null;
await contributions();
const resolved = await contributions();
for (const schema of resolved.configSchemas)
await schema.validate(config[schema.namespace], context);
},
async transformAst(ast, file) {
let current = ast;
for (const plugin of plugins)
current = (await plugin.transformAst?.(current, transformContext(file))) ?? current;
await transformDirectives(
current.view,
(await contributions()).directives,
transformContext(file),
);
return current;
},
async transformCode(code, file) {
@@ -219,11 +295,19 @@ export function createPluginRunner(input: PluginInput, context: PluginContext):
for (const plugin of plugins) current = (await plugin.routes?.(current, context)) ?? current;
return current;
},
async render(html) {
let current = html;
for (const plugin of plugins) current = (await plugin.render?.(current, context)) ?? current;
return current;
},
async hook(name, value) {
for (const plugin of plugins) {
if (name === "buildStart") await plugin.buildStart?.(context);
else if (name === "buildEnd") await plugin.buildEnd?.(value, context);
else await plugin.configureServer?.(value, context);
else if (name === "configureServer") await plugin.configureServer?.(value, context);
else if (name === "deploy") await plugin.deploy?.(value, context);
else if (name === "shutdown") await plugin.shutdown?.(context);
else await plugin.hmrUpdate?.((value as readonly string[]) ?? [], context);
}
},
};
@@ -237,6 +321,7 @@ export type {
PluginContext,
TransformContext,
WrnexusPlugin,
PluginPermission,
PluginInput,
PluginRunner,
};
+93 -2
View File
@@ -1,10 +1,28 @@
import type { PageAst, WrnDiagnostic } from "@wrnexus/syntax";
export type PluginOrder = "pre" | "normal" | "post";
export type PluginCommand = "dev" | "build" | "test";
export type PluginCommand = "dev" | "build" | "test" | "cli" | "deploy";
export type ClientRuntimeType = "module" | "script";
export type ClientRuntimeLoad = "eager" | "defer" | "idle";
export type ClientRuntimeInject = "head" | "body-end";
export type PluginPermission =
| "components"
| "client-runtime"
| "assets"
| "styles"
| "routes"
| "middleware"
| "migrations"
| "config"
| "transform"
| "diagnostics"
| "server"
| "cli"
| "directives"
| "virtual-modules"
| "deployment"
| "documentation"
| "types";
export interface PluginContext {
root: string;
@@ -100,6 +118,36 @@ export interface PluginDevToolbarPanel {
data?: unknown;
}
export interface PluginCliCommand {
name: string;
description?: string;
run(args: string[], context: PluginContext): void | Promise<void>;
}
export interface PluginVirtualModule {
id: string;
load(context: PluginContext): string | Promise<string>;
}
export interface PluginDirective {
name: string;
transform?: (
value: string,
context: TransformContext,
) => { name: string; value: string } | void | Promise<{ name: string; value: string } | void>;
}
export interface PluginDeploymentAdapter {
name: string;
build(output: unknown, context: PluginContext): unknown | Promise<unknown>;
deploy?(output: unknown, context: PluginContext): unknown | Promise<unknown>;
}
export interface PluginConfigSchema {
namespace: string;
validate(value: unknown, context: PluginContext): void | Promise<void>;
}
export interface PluginContributions {
componentDirs: string[];
clientRuntimes: ClientRuntimeDefinition[];
@@ -108,6 +156,13 @@ export interface PluginContributions {
routes: PackageRouteDefinition[];
middleware: string[];
migrations: PackageMigrationDefinition[];
directives: PluginDirective[];
cliCommands: PluginCliCommand[];
virtualModules: PluginVirtualModule[];
deploymentAdapters: PluginDeploymentAdapter[];
configSchemas: PluginConfigSchema[];
documentation: string[];
typeDefinitions: string[];
}
export interface WrnexusPlugin {
@@ -145,7 +200,25 @@ export interface WrnexusPlugin {
| ((
context: PluginContext,
) => PackageMigrationDefinition[] | Promise<PackageMigrationDefinition[]>);
directives?:
| PluginDirective[]
| ((context: PluginContext) => PluginDirective[] | Promise<PluginDirective[]>);
cliCommands?:
| PluginCliCommand[]
| ((context: PluginContext) => PluginCliCommand[] | Promise<PluginCliCommand[]>);
virtualModules?:
| PluginVirtualModule[]
| ((context: PluginContext) => PluginVirtualModule[] | Promise<PluginVirtualModule[]>);
deploymentAdapters?:
| PluginDeploymentAdapter[]
| ((context: PluginContext) => PluginDeploymentAdapter[] | Promise<PluginDeploymentAdapter[]>);
configSchemas?:
| PluginConfigSchema[]
| ((context: PluginContext) => PluginConfigSchema[] | Promise<PluginConfigSchema[]>);
documentation?: string[] | ((context: PluginContext) => string[] | Promise<string[]>);
typeDefinitions?: string[] | ((context: PluginContext) => string[] | Promise<string[]>);
setup?(context: PluginContext): void | Promise<void>;
configure?(config: Record<string, unknown>, context: PluginContext): void | Promise<void>;
configResolved?(
config: Readonly<Record<string, unknown>>,
@@ -158,6 +231,10 @@ export interface WrnexusPlugin {
configureServer?(server: unknown, context: PluginContext): void | Promise<void>;
buildStart?(context: PluginContext): void | Promise<void>;
buildEnd?(result: unknown, context: PluginContext): void | Promise<void>;
render?(html: string, context: PluginContext): string | void | Promise<string | void>;
deploy?(result: unknown, context: PluginContext): void | Promise<void>;
shutdown?(context: PluginContext): void | Promise<void>;
hmrUpdate?(files: readonly string[], context: PluginContext): void | Promise<void>;
devToolbarPanels?(
context: PluginContext,
): PluginDevToolbarPanel[] | Promise<PluginDevToolbarPanel[]>;
@@ -179,6 +256,16 @@ export interface PackagePluginManifest {
export interface WrnexusPackageManifest {
name?: string;
version?: string;
runtimes?: Array<"bun" | "node" | "edge" | "worker" | "service-worker" | "browser">;
requires?: Array<
"filesystem" | "tcp" | "process" | "websocket" | "crypto" | "streams" | "background-tasks"
>;
/** Framework capabilities requested by this package; applications may enforce explicit grants. */
permissions?: PluginPermission[];
compatibility?: {
bunMin?: string;
os?: string[];
};
plugin?: string | PackagePluginManifest;
components?: string[];
clientRuntimes?: ClientRuntimeDefinition[];
@@ -199,5 +286,9 @@ export interface PluginRunner {
contributions(): Promise<PluginContributions>;
devToolbarPanels(): Promise<PluginDevToolbarPanel[]>;
transformRoutes(routes: unknown[]): Promise<unknown[]>;
hook(name: "buildStart" | "buildEnd" | "configureServer", value?: unknown): Promise<void>;
render(html: string): Promise<string>;
hook(
name: "buildStart" | "buildEnd" | "configureServer" | "deploy" | "shutdown" | "hmrUpdate",
value?: unknown,
): Promise<void>;
}