release: WRNexusJS 0.4.0
This commit is contained in:
@@ -0,0 +1,272 @@
|
||||
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,
|
||||
} from "./types.ts";
|
||||
import { definePlugin, flattenPlugins } from "./base.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;
|
||||
}
|
||||
|
||||
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;
|
||||
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);
|
||||
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];
|
||||
}
|
||||
Reference in New Issue
Block a user