Files
WRNexusJS/packages/plugin/src/discovery.ts
T
Clintchiz 586a6db8ff
Quality / quality (ubuntu-latest) (push) Failing after 21s
Quality / quality (windows-latest) (push) Canceled after 0s
release: WRNexusJS 0.8.0
2026-08-02 23:18:51 +05:30

374 lines
14 KiB
TypeScript

import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
import { dirname, join, resolve } from "node:path";
import { createRequire } from "node:module";
import { pathToFileURL } from "node:url";
import type {
PackagePluginManifest,
PluginInput,
WrnexusPackageManifest,
WrnexusPlugin,
ClientRuntimeDefinition,
PackageAssetDefinition,
PackageStyleDefinition,
PackageRouteDefinition,
PackageMigrationDefinition,
PluginPermission,
} from "./types.ts";
import { definePlugin, flattenPlugins } from "./base.ts";
import { testPluginCompatibility } from "./compatibility.ts";
interface PackageJson {
name?: string;
version?: string;
dependencies?: Record<string, string>;
optionalDependencies?: Record<string, string>;
devDependencies?: Record<string, string>;
peerDependencies?: Record<string, string>;
workspaces?: string[] | { packages?: string[] };
wrnexus?: WrnexusPackageManifest;
}
export interface DiscoverPluginOptions {
includeDevDependencies?: boolean;
/** Fail package discovery immediately instead of degrading to a warning. */
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 {
try {
return JSON.parse(readFileSync(path, "utf8")) as PackageJson;
} catch {
return null;
}
}
function nearestWorkspaceRoot(start: string): string | null {
let current = resolve(start);
while (true) {
const packageJson = readJson(join(current, "package.json"));
if (packageJson?.workspaces) return current;
const parent = dirname(current);
if (parent === current) return null;
current = parent;
}
}
function workspacePatterns(pkg: PackageJson): string[] {
if (Array.isArray(pkg.workspaces)) return pkg.workspaces;
return pkg.workspaces?.packages ?? [];
}
function expandSimpleWorkspacePattern(root: string, pattern: string): string[] {
const normalized = pattern.replace(/\\/g, "/").replace(/\/$/, "");
if (!normalized.includes("*")) return [join(root, normalized)];
const star = normalized.indexOf("*");
const before = normalized.slice(0, star).replace(/\/$/, "");
const after = normalized.slice(star + 1).replace(/^\//, "");
const base = join(root, before);
if (!existsSync(base)) return [];
return readdirSync(base)
.map((name) => join(base, name, after))
.filter((path) => existsSync(path) && statSync(path).isDirectory());
}
function workspacePackageRoots(root: string): Map<string, string> {
const result = new Map<string, string>();
const rootPackage = readJson(join(root, "package.json"));
if (!rootPackage) return result;
for (const pattern of workspacePatterns(rootPackage)) {
for (const candidate of expandSimpleWorkspacePattern(root, pattern)) {
const pkg = readJson(join(candidate, "package.json"));
if (pkg?.name) result.set(pkg.name, candidate);
}
}
return result;
}
function resolvePackageRoot(
appRoot: string,
name: string,
workspaces: Map<string, string>,
): string | null {
const workspace = workspaces.get(name);
if (workspace) return workspace;
const require = createRequire(join(appRoot, "package.json"));
try {
return dirname(require.resolve(`${name}/package.json`));
} catch {
try {
const entry = require.resolve(name);
let current = dirname(entry);
while (true) {
const pkg = readJson(join(current, "package.json"));
if (pkg?.name === name) return current;
const parent = dirname(current);
if (parent === current) return null;
current = parent;
}
} catch {
return null;
}
}
}
function resolveRuntime(
packageRoot: string,
value: ClientRuntimeDefinition,
): ClientRuntimeDefinition {
return {
...value,
entry: value.entry ? resolve(packageRoot, value.entry) : undefined,
};
}
function resolveAsset(packageRoot: string, value: PackageAssetDefinition): PackageAssetDefinition {
return {
...value,
entry: value.entry ? resolve(packageRoot, value.entry) : undefined,
};
}
function resolveStyle(packageRoot: string, value: PackageStyleDefinition): PackageStyleDefinition {
return {
...value,
entry: value.entry ? resolve(packageRoot, value.entry) : undefined,
source: value.source ? resolve(packageRoot, value.source) : undefined,
};
}
function resolveRoute(packageRoot: string, value: PackageRouteDefinition): PackageRouteDefinition {
return { ...value, entry: resolve(packageRoot, value.entry) };
}
function resolveMigration(
packageRoot: string,
value: PackageMigrationDefinition,
): PackageMigrationDefinition {
return { ...value, entry: value.entry ? resolve(packageRoot, value.entry) : undefined };
}
function manifestContributionPlugin(
packageRoot: string,
packageName: string,
packageVersion: string | undefined,
manifest: WrnexusPackageManifest,
): WrnexusPlugin | null {
const components = (manifest.components ?? []).map((value) => resolve(packageRoot, value));
const runtimes = (manifest.clientRuntimes ?? []).map((value) =>
resolveRuntime(packageRoot, value),
);
const assets = (manifest.assets ?? []).map((value) => resolveAsset(packageRoot, value));
const styles = (manifest.styles ?? []).map((value) => resolveStyle(packageRoot, value));
const routes = (manifest.routes ?? []).map((value) => resolveRoute(packageRoot, value));
const middleware = (manifest.middleware ?? []).map((value) => resolve(packageRoot, value));
const migrations = (manifest.migrations ?? []).map((value) =>
resolveMigration(packageRoot, value),
);
if (
!components.length &&
!runtimes.length &&
!assets.length &&
!styles.length &&
!routes.length &&
!middleware.length &&
!migrations.length
)
return null;
return definePlugin({
name: `${packageName}/manifest`,
version: manifest.version ?? packageVersion,
componentDirs: components,
clientRuntimes: runtimes,
assets,
styleSources: styles,
routeEntries: routes,
middleware,
migrations,
});
}
function pluginManifest(value: string | PackagePluginManifest): PackagePluginManifest {
return typeof value === "string" ? { plugin: value } : value;
}
async function loadPlugin(
packageRoot: string,
manifest: PackagePluginManifest,
): Promise<WrnexusPlugin> {
const modulePath = resolve(packageRoot, manifest.plugin);
const mod = (await import(pathToFileURL(modulePath).href)) as Record<string, unknown>;
const exportName = manifest.export;
let candidate = exportName ? mod[exportName] : (mod.default ?? mod.plugin);
if (candidate === undefined) {
throw new Error(`WRN-PLUGIN-DISCOVERY-EXPORT: ${modulePath} has no plugin export.`);
}
const shouldInvoke =
manifest.factory === true || (manifest.factory !== false && typeof candidate === "function");
if (shouldInvoke) candidate = await (candidate as () => unknown)();
return definePlugin(candidate as WrnexusPlugin);
}
function dependencyNames(pkg: PackageJson, options: DiscoverPluginOptions): string[] {
const all = {
...(pkg.dependencies ?? {}),
...(pkg.optionalDependencies ?? {}),
...(options.includeDevDependencies === false ? {} : (pkg.devDependencies ?? {})),
...(options.includePeerDependencies ? (pkg.peerDependencies ?? {}) : {}),
};
return Object.keys(all).sort();
}
/**
* Discover package plugins declared through package.json `wrnexus.plugin`.
* Explicit plugins win; automatic duplicates are skipped instead of failing.
*/
export async function discoverPlugins(
appRoot: string,
explicit: PluginInput,
options: DiscoverPluginOptions = {},
): Promise<PluginInput> {
const appPackage = readJson(join(appRoot, "package.json"));
if (!appPackage) return explicit;
const explicitPlugins = flattenPlugins(explicit);
const explicitNames = new Set(explicitPlugins.map((plugin) => plugin.name));
const workspaceRoot = nearestWorkspaceRoot(appRoot);
const workspaces = workspaceRoot
? workspacePackageRoots(workspaceRoot)
: new Map<string, string>();
const automatic: WrnexusPlugin[] = [];
for (const name of dependencyNames(appPackage, options)) {
const packageRoot = resolvePackageRoot(appRoot, name, workspaces);
if (!packageRoot) continue;
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,
packageJson?.version,
packageManifest,
);
if (manifestPlugin && !explicitNames.has(manifestPlugin.name)) automatic.push(manifestPlugin);
const declared = packageManifest.plugin;
if (!declared) continue;
const manifest = pluginManifest(declared);
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);
}
} catch (error) {
const message = `Failed to auto-load ${name}: ${error instanceof Error ? error.message : String(error)}`;
if (options.strict) throw new Error(`WRN-PLUGIN-DISCOVERY: ${message}`, { cause: error });
options.warn?.(message);
}
}
return [automatic, explicitPlugins];
}