release: WRNexusJS 0.4.0

This commit is contained in:
2026-07-27 12:42:18 +05:30
parent 8b728a3e5d
commit 30e5721e84
250 changed files with 10065 additions and 3923 deletions
+142 -64
View File
@@ -1,60 +1,27 @@
import type { PageAst, WrnDiagnostic } from "@wrnexus/syntax";
import { normalizeClientRuntime, normalizePackageAsset, validateStyleIds } from "./manifest.ts";
import type {
ClientRuntimeDefinition,
PackageAssetDefinition,
PackageStyleDefinition,
PackageRouteDefinition,
PackageMigrationDefinition,
PluginDevToolbarPanel,
PluginContributions,
PluginContext,
PluginInput,
PluginOrder,
PluginRunner,
TransformContext,
WrnexusPlugin,
} from "./types.ts";
export type PluginOrder = "pre" | "normal" | "post";
export * from "./types.ts";
export * from "./manifest.ts";
export { discoverPlugins, type DiscoverPluginOptions } from "./discovery.ts";
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));
}
}
export { definePlugin, flattenPlugins } from "./base.ts";
import { flattenPlugins } from "./base.ts";
function rank(plugin: WrnexusPlugin): number {
return plugin.enforce === "pre" ? 0 : plugin.enforce === "post" ? 2 : 1;
@@ -62,8 +29,7 @@ function rank(plugin: WrnexusPlugin): number {
/** Resolve plugin order deterministically and reject duplicates/cycles. */
export function resolvePlugins(input: PluginInput): WrnexusPlugin[] {
const plugins: WrnexusPlugin[] = [];
flatten(input, plugins);
const plugins = flattenPlugins(input);
const byName = new Map<string, WrnexusPlugin>();
for (const plugin of plugins) {
if (byName.has(plugin.name)) throw new Error(`WRN-PLUGIN-DUPLICATE: ${plugin.name}`);
@@ -114,26 +80,114 @@ export function resolvePlugins(input: PluginInput): WrnexusPlugin[] {
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>;
async function resolveContribution<T>(
value: T[] | ((context: PluginContext) => T[] | Promise<T[]>) | undefined,
context: PluginContext,
): Promise<T[]> {
if (!value) return [];
return typeof value === "function"
? await (value as (context: PluginContext) => T[] | Promise<T[]>)(context)
: value;
}
function styleRank(style: PackageStyleDefinition): number {
return style.order === "pre" ? 0 : style.order === "post" ? 2 : 1;
}
function sortStyles(styles: readonly PackageStyleDefinition[]): PackageStyleDefinition[] {
return styles
.map((style, index) => ({ style, index }))
.sort(
(left, right) => styleRank(left.style) - styleRank(right.style) || left.index - right.index,
)
.map(({ style }) => style);
}
function assertUnique<T>(kind: string, entries: readonly T[], key: (entry: T) => string): void {
const seen = new Set<string>();
for (const entry of entries) {
const value = key(entry);
if (seen.has(value)) throw new Error(`WRN-PLUGIN-${kind}-DUPLICATE: ${value}`);
seen.add(value);
}
}
export function createPluginRunner(input: PluginInput, context: PluginContext): PluginRunner {
const plugins = resolvePlugins(input);
let contributionCache: PluginContributions | null = null;
const transformContext = (file: string): TransformContext => ({ ...context, file });
const contributions = async (): Promise<PluginContributions> => {
if (contributionCache) return contributionCache;
const componentDirs: string[] = [];
const clientRuntimes: ClientRuntimeDefinition[] = [];
const assets: PackageAssetDefinition[] = [];
const styles: PackageStyleDefinition[] = [];
const routes: PackageRouteDefinition[] = [];
const middleware: string[] = [];
const migrations: PackageMigrationDefinition[] = [];
for (const plugin of plugins) {
componentDirs.push(...(await resolveContribution(plugin.componentDirs, context)));
clientRuntimes.push(...(await resolveContribution(plugin.clientRuntimes, context)));
assets.push(...(await resolveContribution(plugin.assets, context)));
styles.push(...(await resolveContribution(plugin.styleSources, context)));
routes.push(...(await resolveContribution(plugin.routeEntries, context)));
middleware.push(...(await resolveContribution(plugin.middleware, context)));
migrations.push(...(await resolveContribution(plugin.migrations, context)));
}
const normalizedRuntimes = clientRuntimes.map(normalizeClientRuntime);
const normalizedAssets = assets.map(normalizePackageAsset);
validateStyleIds(styles);
assertUnique("RUNTIME", normalizedRuntimes, (entry) => entry.id);
assertUnique("RUNTIME-PATH", normalizedRuntimes, (entry) => entry.publicPath!);
assertUnique("ASSET", normalizedAssets, (entry) => entry.id);
assertUnique("ASSET-PATH", normalizedAssets, (entry) => entry.publicPath!);
assertUnique(
"PUBLIC-PATH",
[...normalizedRuntimes, ...normalizedAssets],
(entry) => entry.publicPath!,
);
for (const route of routes) {
if (!route.path.startsWith("/") || route.path.includes("..")) {
throw new Error(`WRN-PLUGIN-ROUTE-PATH: unsafe ${route.kind} route '${route.path}'.`);
}
if (!route.entry) throw new Error(`WRN-PLUGIN-ROUTE-ENTRY: '${route.path}' has no entry.`);
}
assertUnique("ROUTE", routes, (entry) => `${entry.kind}:${entry.path}`);
for (const migration of migrations) {
if (!migration.id.trim())
throw new Error("WRN-PLUGIN-MIGRATION-ID: migration id is required.");
if (!migration.entry && migration.source === undefined) {
throw new Error(`WRN-PLUGIN-MIGRATION-SOURCE: ${migration.id} needs entry or source.`);
}
}
assertUnique("MIGRATION", migrations, (entry) => `${entry.database ?? "default"}:${entry.id}`);
contributionCache = {
componentDirs: [...new Set(componentDirs)],
clientRuntimes: normalizedRuntimes,
assets: normalizedAssets,
styles: sortStyles(styles),
routes,
middleware: [...new Set(middleware)],
migrations,
};
context.metadata.set("@wrnexus/plugin:contributions", contributionCache);
return contributionCache;
};
return {
plugins,
async configure(config) {
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();
},
async transformAst(ast, file) {
let current = ast;
@@ -153,6 +207,18 @@ export function createPluginRunner(input: PluginInput, context: PluginContext):
all.push(...((await plugin.diagnostics?.(ast, transformContext(file))) ?? []));
return all;
},
contributions,
async devToolbarPanels() {
const panels: PluginDevToolbarPanel[] = [];
for (const plugin of plugins)
panels.push(...((await plugin.devToolbarPanels?.(context)) ?? []));
return panels;
},
async transformRoutes(routes) {
let current = routes;
for (const plugin of plugins) current = (await plugin.routes?.(current, context)) ?? current;
return current;
},
async hook(name, value) {
for (const plugin of plugins) {
if (name === "buildStart") await plugin.buildStart?.(context);
@@ -162,3 +228,15 @@ export function createPluginRunner(input: PluginInput, context: PluginContext):
},
};
}
// Preserve direct type imports used by older applications.
export type {
PageAst,
WrnDiagnostic,
PluginOrder,
PluginContext,
TransformContext,
WrnexusPlugin,
PluginInput,
PluginRunner,
};