release: WRNexusJS 0.4.0
This commit is contained in:
+142
-64
@@ -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,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user