import { describe, expect, test } from "bun:test"; import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { createPluginRunner, discoverPlugins, resolvePlugins } from "../src/index.ts"; import { parse } from "@wrnexus/syntax"; describe("plugin ordering", () => { test("orders pre, normal, and post plugins", () => { expect( resolvePlugins([ { name: "post", enforce: "post" }, { name: "normal" }, { name: "pre", enforce: "pre" }, ]).map((plugin) => plugin.name), ).toEqual(["pre", "normal", "post"]); }); test("respects explicit dependencies", () => { expect( resolvePlugins([{ name: "b", after: ["a"] }, { name: "a" }]).map((plugin) => plugin.name), ).toEqual(["a", "b"]); }); }); test("runs the complete lifecycle and exposes ecosystem contribution channels", async () => { const calls: string[] = []; const runner = createPluginRunner( { name: "ecosystem", setup: () => { calls.push("setup"); }, configure: () => { calls.push("configure"); }, configSchemas: [ { namespace: "feature", validate(value) { calls.push(`schema:${String(value)}`); }, }, ], directives: [ { name: "focus", transform: (value) => ({ name: "data-focus", value }), }, ], cliCommands: [ { name: "hello", run: () => { calls.push("cli"); }, }, ], virtualModules: [{ id: "virtual:feature", load: () => "export default true" }], deploymentAdapters: [{ name: "test-cloud", build: (value) => value }], documentation: ["docs/feature.md"], typeDefinitions: ["types/feature.d.ts"], render: (html) => `${html}`, hmrUpdate: (files) => { calls.push(`hmr:${files.join(",")}`); }, deploy: () => { calls.push("deploy"); }, shutdown: () => { calls.push("shutdown"); }, }, context("."), ); await runner.configure({ feature: true }); await runner.configResolved({ feature: true }); const contributions = await runner.contributions(); expect(contributions.directives[0]?.name).toBe("focus"); expect(contributions.cliCommands[0]?.name).toBe("hello"); expect(contributions.virtualModules[0]?.id).toBe("virtual:feature"); expect(contributions.deploymentAdapters[0]?.name).toBe("test-cloud"); expect(contributions.documentation).toEqual(["docs/feature.md"]); expect(contributions.typeDefinitions).toEqual(["types/feature.d.ts"]); const transformed = await runner.transformAst( parse('page Demo { view { } }'), "app/pages/demo.wrn", ); const input = transformed.view.find((node) => node.type === "element"); expect(input?.type === "element" ? input.attrs[0]?.name : undefined).toBe("data-focus"); expect(await runner.render("
")).toContain(""); await runner.hook("hmrUpdate", ["app/page.wrn"]); await runner.hook("deploy", {}); await runner.hook("shutdown"); expect(calls).toEqual([ "setup", "configure", "schema:true", "hmr:app/page.wrn", "deploy", "shutdown", ]); }); function context(root: string) { return { root, mode: "development" as const, command: "dev" as const, metadata: new Map(), warn() {}, }; } test("normalizes package runtimes, assets, routes, middleware, and migrations", async () => { const root = mkdtempSync(join(tmpdir(), "wrnexus-plugin-")); try { const runner = createPluginRunner( { name: "system", clientRuntimes: [{ id: "system", source: "window.system = true" }], assets: [{ id: "system-data", source: "{}", contentType: "application/json" }], routeEntries: [{ kind: "api", path: "/api/system", entry: join(root, "route.ts") }], middleware: [join(root, "middleware.ts")], migrations: [{ id: "system-schema", source: "-- +up\nSELECT 1;" }], }, context(root), ); const contributions = await runner.contributions(); expect(contributions.clientRuntimes[0]?.publicPath).toBe("/__wrnexus/assets/system.js"); expect(contributions.assets[0]?.publicPath).toBe("/__wrnexus/assets/system-data.json"); expect(contributions.routes[0]?.path).toBe("/api/system"); expect(contributions.middleware).toEqual([join(root, "middleware.ts")]); expect(contributions.migrations[0]?.id).toBe("system-schema"); } finally { rmSync(root, { recursive: true, force: true }); } }); test("auto-discovers package.json contributions from workspace dependencies", async () => { const root = mkdtempSync(join(tmpdir(), "wrnexus-discovery-")); const app = join(root, "apps", "web"); const pkg = join(root, "packages", "example-system"); try { mkdirSync(join(app, "app", "pages"), { recursive: true }); mkdirSync(join(pkg, "components"), { recursive: true }); mkdirSync(join(pkg, "assets"), { recursive: true }); writeFileSync( join(root, "package.json"), JSON.stringify({ private: true, workspaces: ["apps/*", "packages/*"] }), ); writeFileSync( join(app, "package.json"), JSON.stringify({ name: "web", dependencies: { "@wrnexus/example-system": "workspace:*" } }), ); writeFileSync( join(pkg, "components", "Example.wrn"), "component Example { view {
Example
} }", ); writeFileSync(join(pkg, "assets", "runtime.js"), "window.example = true;"); writeFileSync( join(pkg, "package.json"), JSON.stringify({ name: "@wrnexus/example-system", version: "0.4.0", wrnexus: { components: ["./components"], clientRuntimes: [{ id: "example-system", entry: "./assets/runtime.js" }], migrations: [{ id: "example-schema", source: "-- +up\nSELECT 1;" }], }, }), ); const discovered = await discoverPlugins(app, undefined); const runner = createPluginRunner(discovered, context(app)); const contributions = await runner.contributions(); expect(runner.plugins.map((plugin) => plugin.name)).toContain( "@wrnexus/example-system/manifest", ); expect(contributions.componentDirs).toEqual([join(pkg, "components")]); expect(contributions.clientRuntimes[0]?.entry).toBe(join(pkg, "assets", "runtime.js")); expect(contributions.migrations[0]?.id).toBe("example-schema"); } finally { rmSync(root, { recursive: true, force: true }); } }); test("rejects duplicate runtime paths and migration ids", async () => { const runner = createPluginRunner( [ { name: "one", clientRuntimes: [{ id: "one", source: "", publicPath: "/same.js" }], migrations: [{ id: "schema", source: "SELECT 1" }], }, { name: "two", clientRuntimes: [{ id: "two", source: "", publicPath: "/same.js" }], migrations: [{ id: "schema", source: "SELECT 2" }], }, ], context("."), ); await expect(runner.contributions()).rejects.toThrow("WRN-PLUGIN-RUNTIME-PATH-DUPLICATE"); }); test("rejects client-runtime and package-asset path collisions", async () => { const runner = createPluginRunner( [ { name: "runtime-owner", clientRuntimes: [{ id: "runtime", source: "", publicPath: "/shared.js" }], }, { name: "asset-owner", assets: [ { id: "asset", source: "window.asset = true;", contentType: "text/javascript", publicPath: "/shared.js", }, ], }, ], context("."), ); await expect(runner.contributions()).rejects.toThrow("WRN-PLUGIN-PUBLIC-PATH-DUPLICATE"); }); test("strict discovery surfaces invalid package plugin exports", async () => { const root = mkdtempSync(join(tmpdir(), "wrnexus-strict-discovery-")); const app = join(root, "apps", "web"); const pkg = join(root, "packages", "broken-system"); try { mkdirSync(join(app, "app", "pages"), { recursive: true }); mkdirSync(pkg, { recursive: true }); writeFileSync( join(root, "package.json"), JSON.stringify({ private: true, workspaces: ["apps/*", "packages/*"] }), ); writeFileSync( join(app, "package.json"), JSON.stringify({ name: "web", dependencies: { "@wrnexus/broken-system": "workspace:*" }, }), ); writeFileSync(join(pkg, "plugin.mjs"), "export const unrelated = true;\n"); writeFileSync( join(pkg, "package.json"), JSON.stringify({ name: "@wrnexus/broken-system", version: "0.4.0", wrnexus: { plugin: "./plugin.mjs" }, }), ); await expect(discoverPlugins(app, undefined, { strict: true })).rejects.toThrow( "WRN-PLUGIN-DISCOVERY", ); } finally { rmSync(root, { recursive: true, force: true }); } }); test("discovery enforces declared deployment runtimes and capabilities", async () => { const root = mkdtempSync(join(tmpdir(), "wrnexus-runtime-discovery-")); const app = join(root, "apps", "web"); const pkg = join(root, "packages", "filesystem-plugin"); try { mkdirSync(app, { recursive: true }); mkdirSync(pkg, { recursive: true }); writeFileSync( join(root, "package.json"), JSON.stringify({ private: true, workspaces: ["apps/*", "packages/*"] }), ); writeFileSync( join(app, "package.json"), JSON.stringify({ name: "web", dependencies: { "filesystem-plugin": "workspace:*" } }), ); writeFileSync( join(pkg, "package.json"), JSON.stringify({ name: "filesystem-plugin", version: "1.0.0", wrnexus: { runtimes: ["bun", "node"], requires: ["filesystem"] }, }), ); await expect( discoverPlugins(app, undefined, { runtime: "edge", capabilities: ["crypto"] }), ).rejects.toThrow("WRN-PLUGIN-RUNTIME"); await expect( discoverPlugins(app, undefined, { runtime: "bun", capabilities: ["filesystem"] }), ).resolves.toBeDefined(); } finally { rmSync(root, { recursive: true, force: true }); } }); test("discovery enforces declared and application-granted plugin permissions", async () => { const root = mkdtempSync(join(tmpdir(), "wrnexus-permission-discovery-")); const app = join(root, "apps", "web"); const pkg = join(root, "packages", "route-plugin"); try { mkdirSync(app, { recursive: true }); mkdirSync(pkg, { recursive: true }); writeFileSync( join(root, "package.json"), JSON.stringify({ private: true, workspaces: ["apps/*", "packages/*"] }), ); writeFileSync( join(app, "package.json"), JSON.stringify({ name: "web", dependencies: { "route-plugin": "workspace:*" } }), ); writeFileSync(join(pkg, "route.ts"), "export default {};"); writeFileSync( join(pkg, "package.json"), JSON.stringify({ name: "route-plugin", version: "1.0.0", wrnexus: { permissions: ["routes"], routes: [{ kind: "api", path: "/api/plugin", entry: "./route.ts" }], }, }), ); await expect( discoverPlugins(app, undefined, { enforcePermissions: true, grantedPermissions: {} }), ).rejects.toThrow("WRN-PLUGIN-PERMISSION-DENIED"); await expect( discoverPlugins(app, undefined, { enforcePermissions: true, grantedPermissions: { "route-plugin": ["routes"] }, }), ).resolves.toBeDefined(); writeFileSync( join(pkg, "package.json"), JSON.stringify({ name: "route-plugin", version: "1.0.0", wrnexus: { routes: [{ kind: "api", path: "/api/plugin", entry: "./route.ts" }] }, }), ); await expect( discoverPlugins(app, undefined, { enforcePermissions: true, grantedPermissions: { "route-plugin": ["routes"] }, }), ).rejects.toThrow("WRN-PLUGIN-PERMISSION-UNDECLARED"); } finally { rmSync(root, { recursive: true, force: true }); } }); test("orders package style contributions by pre, normal, and post", async () => { const runner = createPluginRunner( { name: "styles", styleSources: [ { id: "post", source: "post", order: "post" }, { id: "normal", source: "normal" }, { id: "pre", source: "pre", order: "pre" }, { id: "normal-two", source: "normal-two", order: "normal" }, ], }, context("."), ); expect((await runner.contributions()).styles.map((style) => style.id)).toEqual([ "pre", "normal", "normal-two", "post", ]); });