release: WRNexusJS 0.3.0

This commit is contained in:
2026-07-22 17:29:08 +05:30
parent 13dfa31d19
commit 07d8fb59d6
145 changed files with 9664 additions and 3881 deletions
+164
View File
@@ -0,0 +1,164 @@
import type { PageAst, WrnDiagnostic } from "@wrnexus/syntax";
export type PluginOrder = "pre" | "normal" | "post";
export interface PluginContext {
root: string;
mode: "development" | "production";
command: "dev" | "build" | "test";
profile?: string;
metadata: Map<string, unknown>;
warn(message: string): void;
}
export interface TransformContext extends PluginContext {
file: string;
}
export interface WrnexusPlugin {
name: string;
version?: string;
enforce?: PluginOrder;
/** Plugin names that must execute first. */
after?: string[];
/** Plugin names that must execute later. */
before?: string[];
configure?(config: Record<string, unknown>, context: PluginContext): void | Promise<void>;
configResolved?(
config: Readonly<Record<string, unknown>>,
context: PluginContext,
): void | Promise<void>;
transformAst?(ast: PageAst, context: TransformContext): PageAst | void | Promise<PageAst | void>;
transformCode?(code: string, context: TransformContext): string | void | Promise<string | void>;
diagnostics?(ast: PageAst, context: TransformContext): WrnDiagnostic[] | Promise<WrnDiagnostic[]>;
routes?(routes: unknown[], context: PluginContext): unknown[] | void | Promise<unknown[] | void>;
configureServer?(server: unknown, context: PluginContext): void | Promise<void>;
buildStart?(context: PluginContext): void | Promise<void>;
buildEnd?(result: unknown, context: PluginContext): void | Promise<void>;
devToolbarPanels?(context: PluginContext): unknown[] | Promise<unknown[]>;
}
export type PluginInput = WrnexusPlugin | false | null | undefined | PluginInput[];
export function definePlugin(plugin: WrnexusPlugin): WrnexusPlugin {
if (!plugin.name || !/^[a-z0-9@][a-z0-9@/._-]*$/i.test(plugin.name)) {
throw new Error("WRN-PLUGIN-NAME: plugins require a stable package-style name.");
}
return plugin;
}
function flatten(input: PluginInput, output: WrnexusPlugin[]): void {
if (!input) return;
if (Array.isArray(input)) {
for (const entry of input) flatten(entry, output);
} else {
output.push(definePlugin(input));
}
}
function rank(plugin: WrnexusPlugin): number {
return plugin.enforce === "pre" ? 0 : plugin.enforce === "post" ? 2 : 1;
}
/** Resolve plugin order deterministically and reject duplicates/cycles. */
export function resolvePlugins(input: PluginInput): WrnexusPlugin[] {
const plugins: WrnexusPlugin[] = [];
flatten(input, plugins);
const byName = new Map<string, WrnexusPlugin>();
for (const plugin of plugins) {
if (byName.has(plugin.name)) throw new Error(`WRN-PLUGIN-DUPLICATE: ${plugin.name}`);
byName.set(plugin.name, plugin);
}
const edges = new Map<string, Set<string>>();
for (const plugin of plugins) edges.set(plugin.name, new Set());
for (const plugin of plugins) {
for (const dependency of plugin.after ?? []) {
if (byName.has(dependency)) edges.get(dependency)!.add(plugin.name);
}
for (const dependent of plugin.before ?? []) {
if (byName.has(dependent)) edges.get(plugin.name)!.add(dependent);
}
}
for (const left of plugins) {
for (const right of plugins) {
if (rank(left) < rank(right)) edges.get(left.name)!.add(right.name);
}
}
const indegree = new Map(plugins.map((plugin) => [plugin.name, 0]));
for (const targets of edges.values()) {
for (const target of targets) indegree.set(target, (indegree.get(target) ?? 0) + 1);
}
const ready = plugins
.filter((plugin) => indegree.get(plugin.name) === 0)
.sort((a, b) => rank(a) - rank(b) || a.name.localeCompare(b.name));
const resolved: WrnexusPlugin[] = [];
while (ready.length) {
const plugin = ready.shift()!;
resolved.push(plugin);
for (const target of edges.get(plugin.name) ?? []) {
indegree.set(target, indegree.get(target)! - 1);
if (indegree.get(target) === 0) {
ready.push(byName.get(target)!);
ready.sort((a, b) => rank(a) - rank(b) || a.name.localeCompare(b.name));
}
}
}
if (resolved.length !== plugins.length) {
const cyclic = plugins
.filter((plugin) => !resolved.includes(plugin))
.map((plugin) => plugin.name);
throw new Error(`WRN-PLUGIN-CYCLE: ${cyclic.join(", ")}`);
}
return resolved;
}
export interface PluginRunner {
readonly plugins: readonly WrnexusPlugin[];
configure(config: Record<string, unknown>): Promise<void>;
configResolved(config: Readonly<Record<string, unknown>>): Promise<void>;
transformAst(ast: PageAst, file: string): Promise<PageAst>;
transformCode(code: string, file: string): Promise<string>;
diagnostics(ast: PageAst, file: string): Promise<WrnDiagnostic[]>;
hook(name: "buildStart" | "buildEnd" | "configureServer", value?: unknown): Promise<void>;
}
export function createPluginRunner(input: PluginInput, context: PluginContext): PluginRunner {
const plugins = resolvePlugins(input);
const transformContext = (file: string): TransformContext => ({ ...context, file });
return {
plugins,
async configure(config) {
for (const plugin of plugins) await plugin.configure?.(config, context);
},
async configResolved(config) {
for (const plugin of plugins) await plugin.configResolved?.(config, context);
},
async transformAst(ast, file) {
let current = ast;
for (const plugin of plugins)
current = (await plugin.transformAst?.(current, transformContext(file))) ?? current;
return current;
},
async transformCode(code, file) {
let current = code;
for (const plugin of plugins)
current = (await plugin.transformCode?.(current, transformContext(file))) ?? current;
return current;
},
async diagnostics(ast, file) {
const all: WrnDiagnostic[] = [];
for (const plugin of plugins)
all.push(...((await plugin.diagnostics?.(ast, transformContext(file))) ?? []));
return all;
},
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);
}
},
};
}