@@ -48,8 +48,54 @@ import { Script, constants } from "node:vm";
4848 * immutable and shared by every project on the machine, while the harness doing the loading is
4949 * whichever one is running now, and two applications embedding the harness need not agree on
5050 * where their `effect` lives.
51+ *
52+ * Each package maps to the `Symbol.for` key of its {@link SharedRegistry}.
53+ */
54+ const sharedPackages : ReadonlyMap < string , string > = new Map ( [
55+ [ "effect" , "codework.v1.effect" ] ,
56+ [ "@codeworksh/plugin" , "codework.v1.plugin" ] ,
57+ ] ) ;
58+
59+ /** One module of a shared package, as a bundled host publishes it. */
60+ export interface SharedModule {
61+ /** Its export names; an ES module's exports must be known before it runs. */
62+ readonly names : ReadonlyArray < string > ;
63+ readonly load : ( ) => Promise < Record < string , unknown > > ;
64+ }
65+
66+ /**
67+ * A shared package's modules by specifier (`effect/Schema`), published on
68+ * `globalThis[Symbol.for(key)]` by a host that is a single bundled file, such as the standalone
69+ * binary. Such a host has no `effect` on disk to resolve to, so plugins get virtual modules that
70+ * re-export its own instances. Modules load on first import: `effect` alone has hundreds.
5171 */
52- const sharedPackages : ReadonlySet < string > = new Set ( [ "effect" , "@codeworksh/plugin" ] ) ;
72+ export type SharedRegistry = Readonly < Record < string , SharedModule > > ;
73+
74+ const sharedScheme = "codeworkshared:" ;
75+
76+ function registryOf ( key : string ) : SharedRegistry | undefined {
77+ return ( globalThis as Record < symbol , SharedRegistry | undefined > ) [ Symbol . for ( key ) ] ;
78+ }
79+
80+ /** Publish a bundled host's registries, by package name, before any plugin loads. */
81+ export function share ( packages : Readonly < Record < string , SharedRegistry > > ) : void {
82+ for ( const [ name , registry ] of Object . entries ( packages ) ) {
83+ const key = sharedPackages . get ( name ) ;
84+ if ( key === undefined ) throw new Error ( `${ name } is not a package shared with plugins` ) ;
85+ ( globalThis as Record < symbol , SharedRegistry > ) [ Symbol . for ( key ) ] = registry ;
86+ }
87+ }
88+
89+ /** A virtual module re-exporting the host's instance of `specifier`. */
90+ function sharedSource ( key : string , specifier : string , names : ReadonlyArray < string > ) : string {
91+ const registry = `globalThis[Symbol.for(${ JSON . stringify ( key ) } )]` ;
92+ return [
93+ `const m = await ${ registry } [${ JSON . stringify ( specifier ) } ].load();` ,
94+ ...names . map ( ( name , index ) => `const $${ index } = m[${ JSON . stringify ( name ) } ];` ) ,
95+ // String export names, since some are reserved words (`Effect.void`).
96+ `export { ${ names . map ( ( name , index ) => `$${ index } as ${ JSON . stringify ( name ) } ` ) . join ( ", " ) } };` ,
97+ ] . join ( "\n" ) ;
98+ }
5399
54100/** The package a bare specifier names: `effect/Schema` -> `effect`, `@a/b/c` -> `@a/b`. */
55101function packageOf ( specifier : string ) : string {
@@ -59,18 +105,34 @@ function packageOf(specifier: string): string {
59105}
60106
61107/**
62- * Resolve the shared packages as though this module had imported them, for as long as `run`
63- * takes. Anything else resolves exactly as it would have.
108+ * Resolve the shared packages to the host's own instances, for as long as `run` takes: from its
109+ * registry when it published one, otherwise as though this module had imported them. Anything
110+ * else resolves exactly as it would have.
64111 */
65- function withSharedModules < A > ( run : ( ) => Promise < A > ) : Promise < A > {
112+ async function withSharedModules < A > ( run : ( ) => Promise < A > ) : Promise < A > {
66113 const hook = registerHooks ( {
67114 resolve ( specifier , context , nextResolve ) {
68- return sharedPackages . has ( packageOf ( specifier ) )
69- ? nextResolve ( specifier , { ...context , parentURL : import . meta. url } )
70- : nextResolve ( specifier , context ) ;
115+ const key = sharedPackages . get ( packageOf ( specifier ) ) ;
116+ if ( key === undefined ) return nextResolve ( specifier , context ) ;
117+ const registry = registryOf ( key ) ;
118+ if ( registry === undefined ) return nextResolve ( specifier , { ...context , parentURL : import . meta. url } ) ;
119+ // Falling back to the plugin's own copy would bring back the two-instance failure above.
120+ if ( ! Object . hasOwn ( registry , specifier ) ) throw new Error ( `${ specifier } is not shared with plugins` ) ;
121+ return { url : `${ sharedScheme } ${ specifier } ` , shortCircuit : true } ;
122+ } ,
123+ load ( url , context , nextLoad ) {
124+ if ( ! url . startsWith ( sharedScheme ) ) return nextLoad ( url , context ) ;
125+ const specifier = url . slice ( sharedScheme . length ) ;
126+ const key = sharedPackages . get ( packageOf ( specifier ) ) ! ;
127+ const shared = registryOf ( key ) ! [ specifier ] ! ;
128+ return { format : "module" , source : sharedSource ( key , specifier , shared . names ) , shortCircuit : true } ;
71129 } ,
72130 } ) ;
73- return run ( ) . finally ( ( ) => hook . deregister ( ) ) ;
131+ try {
132+ return await run ( ) ;
133+ } finally {
134+ hook . deregister ( ) ;
135+ }
74136}
75137
76138/** Use Node's main loader even when the caller runs inside a bundler or test VM. */
0 commit comments