diff --git a/src/index.ts b/src/index.ts index b0dc34a..e99a8ab 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,7 +3,7 @@ import type { Class } from "./decorators"; import { type InterfaceConnection, internal } from "./internal"; import { Logging } from "./logging"; import { - AsyncProxy, + type AsyncProxy, type EventProxy, GetResponsibleModule, IsInterfaceProxy, @@ -17,6 +17,7 @@ export { EventProxy, GetInterfaceProxyIdentity, GetResponsibleModule, + InterfaceFunction, IsInterfaceProxy, RegisteringProxy, RunWithResponsibleModule, @@ -75,24 +76,6 @@ export function GetMetadata< type Func = (...args: A) => R; -/** - * Creates an interface function proxy. - * - * Returns a function that routes calls through an AsyncProxy, allowing for module-aware - * asynchronous function calls that can be implemented by other modules. - * - * @returns A function that proxies calls to the implementation when available - */ -export function InterfaceFunction< - T extends Func = Func, - R = Awaited>, ->(identity?: string): (...args: Parameters) => Promise { - const proxy = new AsyncProxy(identity); - const func = (...args: Parameters) => proxy.call(...args); - func.proxy = proxy; - return func; -} - type RID = T extends (id: infer P, ...args: any[]) => void ? P : never; type InterfaceImplType = T extends RegisteringProxy @@ -328,3 +311,6 @@ export function GetInterfaceInstance( (connection) => connection.id === connectionID, ); } + +export * from "./modules"; +export * from "./runtime"; diff --git a/src/modules.ts b/src/modules.ts index a708a05..33c3af1 100644 --- a/src/modules.ts +++ b/src/modules.ts @@ -1,4 +1,3 @@ -import { EventProxy, InterfaceFunction } from "."; import { getModuleContext, internal, @@ -8,6 +7,7 @@ import { type RuntimeCleanup, runWithModuleContext, } from "./internal"; +import { EventProxy, InterfaceFunction } from "./proxies"; /** Runs work with module ownership and an optional provider route across awaits. */ export function RunWithModuleContext( diff --git a/src/proxies.ts b/src/proxies.ts index 212aa6c..1db7b2f 100644 --- a/src/proxies.ts +++ b/src/proxies.ts @@ -339,6 +339,17 @@ export class AsyncProxy>> { } } +/** Creates an interface function backed by an asynchronous proxy. */ +export function InterfaceFunction< + T extends Func = Func, + R = Awaited>, +>(identity?: string): (...args: Parameters) => Promise { + const proxy = new AsyncProxy(identity); + const func = (...args: Parameters) => proxy.call(...args); + func.proxy = proxy; + return func; +} + /** Proxy for provider-aware register and unregister handlers. */ export class RegisteringProxy { public readonly [PROXY_BRAND]: ProxyBrand; diff --git a/src/runtime.ts b/src/runtime.ts index 0de93aa..50da403 100644 --- a/src/runtime.ts +++ b/src/runtime.ts @@ -1,4 +1,4 @@ -import { InterfaceFunction } from "."; +import { InterfaceFunction } from "./proxies"; /** * Information about the runtime environment of the running Antelope project. diff --git a/src/tests/root-declarations.test.ts b/src/tests/root-declarations.test.ts new file mode 100644 index 0000000..5ef9aa7 --- /dev/null +++ b/src/tests/root-declarations.test.ts @@ -0,0 +1,64 @@ +import { execFileSync } from "node:child_process"; +import { resolve } from "node:path"; +import { expect } from "chai"; +import * as declarations from ".."; +import * as modules from "../modules"; +import * as runtime from "../runtime"; + +const PACKAGE_ROOT = resolve(__dirname, "..", ".."); +const FRESH_PROCESS_CONTRACT = ` +const assert = require("node:assert/strict"); +const path = require("node:path"); +const root = process.env.INTERFACE_CORE_ROOT; +const entries = { + root: path.join(root, "dist"), + modules: path.join(root, "dist", "modules.js"), + runtime: path.join(root, "dist", "runtime.js"), +}; +const loaded = Object.fromEntries( + process.env.INTERFACE_CORE_ORDER.split(",").map((entry) => [entry, require(entries[entry])]), +); +const core = loaded.root; +const modules = loaded.modules; +const runtime = loaded.runtime; +assert.equal(core.Events, modules.Events); +assert.equal(core.ListModules, modules.ListModules); +assert.equal(core.GetModuleInfo, modules.GetModuleInfo); +assert.equal(core.GetRuntimeInfo, runtime.GetRuntimeInfo); +assert.equal(core.RegisterDevServer, runtime.RegisterDevServer); +assert.equal(core.IsInterfaceProxy(core.ListModules.proxy), true); +assert.equal(core.IsInterfaceProxy(core.GetRuntimeInfo.proxy), true); +assert.equal(core.GetInterfaceProxyIdentity(core.ListModules.proxy), "async:modules.ListModules"); +assert.equal(core.GetInterfaceProxyIdentity(core.GetRuntimeInfo.proxy), "async:runtime.GetRuntimeInfo"); +`; + +function runFreshProcess(order: string): void { + execFileSync(process.execPath, ["-e", FRESH_PROCESS_CONTRACT], { + env: { + ...process.env, + INTERFACE_CORE_ORDER: order, + INTERFACE_CORE_ROOT: PACKAGE_ROOT, + }, + }); +} + +describe("root interface declarations", () => { + it("exports the canonical runtime proxies", () => { + expect(declarations.GetRuntimeInfo).to.equal(runtime.GetRuntimeInfo); + expect(declarations.RegisterDevServer).to.equal(runtime.RegisterDevServer); + }); + + it("exports the canonical module proxies", () => { + expect(declarations.Events).to.equal(modules.Events); + expect(declarations.ListModules).to.equal(modules.ListModules); + expect(declarations.GetModuleInfo).to.equal(modules.GetModuleInfo); + }); + + it("loads complete canonical declarations when the root loads first", () => { + runFreshProcess("root,runtime,modules"); + }); + + it("loads complete canonical declarations when subpaths load first", () => { + runFreshProcess("modules,runtime,root"); + }); +}); diff --git a/test/package-consumer.mjs b/test/package-consumer.mjs index f7668dc..00710bd 100644 --- a/test/package-consumer.mjs +++ b/test/package-consumer.mjs @@ -23,12 +23,23 @@ function createConsumer(tarball) { }, }), ); - writeFileSync(join(temporary, "contract.cjs"), consumerSource); + writeFileSync( + join(temporary, "contract-root-first.cjs"), + createConsumerSource(rootFirstImports), + ); + writeFileSync( + join(temporary, "contract-subpaths-first.cjs"), + createConsumerSource(subpathsFirstImports), + ); writeFileSync(join(temporary, "contract.ts"), typeConsumerSource); } const typeConsumerSource = ` -import type { InterfaceConnection } from "@antelopejs/interface-core"; +import { + GetRuntimeInfo, + ListModules, + type InterfaceConnection, +} from "@antelopejs/interface-core"; import type { ModuleExecutionContext } from "@antelopejs/interface-core/modules"; const connection: InterfaceConnection = { @@ -42,13 +53,36 @@ const context: ModuleExecutionContext = { }; void connection; void context; +void GetRuntimeInfo; +void ListModules; `; -const consumerSource = ` -const assert = require("node:assert/strict"); +const rootFirstImports = ` const core = require("@antelopejs/interface-core"); -const { internal } = require("@antelopejs/interface-core/internal"); const modules = require("@antelopejs/interface-core/modules"); +const runtime = require("@antelopejs/interface-core/runtime"); +`; + +const subpathsFirstImports = ` +const modules = require("@antelopejs/interface-core/modules"); +const runtime = require("@antelopejs/interface-core/runtime"); +const core = require("@antelopejs/interface-core"); +`; + +function createConsumerSource(imports) { + return ` +const assert = require("node:assert/strict"); +${imports} +const { internal } = require("@antelopejs/interface-core/internal"); + +assert.equal(core.GetRuntimeInfo, runtime.GetRuntimeInfo); +assert.equal(core.RegisterDevServer, runtime.RegisterDevServer); +assert.equal(core.ListModules, modules.ListModules); +assert.equal(core.Events, modules.Events); +assert.equal(core.IsInterfaceProxy(core.GetRuntimeInfo.proxy), true); +assert.equal(core.IsInterfaceProxy(core.ListModules.proxy), true); +assert.equal(core.GetInterfaceProxyIdentity(core.GetRuntimeInfo.proxy), "async:runtime.GetRuntimeInfo"); +assert.equal(core.GetInterfaceProxyIdentity(core.ListModules.proxy), "async:modules.ListModules"); const proxy = core.InterfaceFunction("package-consumer.context"); const identity = core.GetInterfaceProxyIdentity(proxy.proxy); @@ -100,6 +134,7 @@ modules.RunWithModuleContext(providerContext, () => { process.exitCode = 1; }); `; +} try { run("corepack", ["pnpm", "run", "build"], repository); @@ -135,7 +170,8 @@ try { ], temporary, ); - run(process.execPath, ["contract.cjs"], temporary); + run(process.execPath, ["contract-root-first.cjs"], temporary); + run(process.execPath, ["contract-subpaths-first.cjs"], temporary); } finally { rmSync(temporary, { force: true, recursive: true }); }