browser-module-runtime 0.0.3 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -64,3 +64,16 @@ await handle.dispose();
64
64
  - `browser-module-runtime/cache`: compile-cache implementations
65
65
 
66
66
  `<runtime-render>` mounts framework plugins into its own element. This new generic API is distinct from the older `solid-tag-runtime` and `<solid-render>` APIs; use those packages for applications requiring their existing behavior.
67
+
68
+ ### Scoped HTML (development branch)
69
+
70
+ ```js
71
+ import {registerHTML} from 'browser-module-runtime/html';
72
+ registerHTML(runtime,{scope:'app',root:document}); // No initial scan or observer
73
+ ```
74
+
75
+ ```html
76
+ <runtime-render scope="app" module="/Counter.jsx" props='{"initial":5}'></runtime-render>
77
+ ```
78
+
79
+ The custom element waits for the runtime and module automatically. For already parsed inert script declarations use an explicit `await html.registerExisting()`; choose `observe:true` only when discovering uncontrolled future script insertions is necessary. Use `html.appendAndRegister(script)` for controlled injection. See `docs/SCOPED_HTML.md` in the workspace. These behaviors are **not** part of previously published npm `0.0.4` without a new release.
package/html.d.ts CHANGED
@@ -1,4 +1,36 @@
1
1
  import type {Runtime} from './index.js';
2
2
  import type {WebComponentOptions} from './web-components.js';
3
- export interface HTMLController {readonly name:string;readonly runtime:Runtime;defineElement(tag:string,options:Omit<WebComponentOptions,'tag'>):CustomElementConstructor;registerExisting():Promise<void>;scan():Promise<void>;disconnect():Promise<void>}
4
- export declare function registerHTML(runtime:Runtime,options?:{name?:string;root?:Document|Element;observe?:boolean}):HTMLController;
3
+ export interface HTMLController {
4
+ /** Scope name; `name` is a backwards compatible alias. */
5
+ readonly name:string;
6
+ readonly scope:string;
7
+ readonly root:Document|Element;
8
+ readonly appendTarget:Document|Element;
9
+ readonly runtime:Runtime;
10
+ defineElement(tag:string,options:Omit<WebComponentOptions,'tag'>):CustomElementConstructor;
11
+ /** Registers an existing inert declaration without scanning. */
12
+ registerScript(script:HTMLScriptElement):Promise<boolean>;
13
+ /** Append only (no auto-registration). */
14
+ appendScript(script:HTMLScriptElement,target?:Document|Element):Promise<HTMLScriptElement>;
15
+ /** Controlled append and register without a subtree scan. */
16
+ appendAndRegister(script:HTMLScriptElement,target?:Document|Element):Promise<HTMLScriptElement>;
17
+ /** Explicit one-time discovery of scripts under root. */
18
+ registerExisting():Promise<void>;
19
+ /** Alias for registerExisting. */
20
+ scan():Promise<void>;
21
+ setRoot(root:Document|Element,options?:{registerExisting?:boolean}):Promise<Document|Element>;
22
+ moveTo(root:Document|Element,options?:{registerExisting?:boolean}):Promise<Document|Element>;
23
+ disconnect():Promise<void>;
24
+ }
25
+ export interface RegisterHTMLOptions {
26
+ scope?:string;
27
+ /** Backward compatible alias for scope. */
28
+ name?:string;
29
+ root?:Document|Element;
30
+ /** Whether an element without an explicit scope can be owned by this controller. */
31
+ acceptUnscoped?:boolean;
32
+ /** Opt-in DOM observation of newly inserted inert source declarations. Default false. */
33
+ observe?:boolean;
34
+ }
35
+ /** Does not scan automatically. <runtime-render> itself is an autonomous custom element. */
36
+ export declare function registerHTML(runtime:Runtime,options?:RegisterHTMLOptions):HTMLController;
package/index.d.ts CHANGED
@@ -39,6 +39,8 @@ export interface RuntimePlugin {
39
39
  cacheKey?: string;
40
40
  /** Framework-owned exact package roots merged into the runtime package policy. */
41
41
  packageEntries?: NonNullable<PackageOptions['entries']>;
42
+ /** Optional coordinated package URL family; precedence/conflict validated at factory creation. */
43
+ packageFamilyRules?: Record<string,{url:string;version:string;provider:string;subpathUrl?:(subpath:string)=>string}>;
42
44
  compilers?: CompilerAdapter[];
43
45
  renderers?: RendererAdapter[];
44
46
  preprocess?: TransformHook[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "browser-module-runtime",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
package/src/html/index.js CHANGED
@@ -1,74 +1,191 @@
1
- /** HTML controller with opt-in observation; not coupled to any UI framework. */
1
+ /** Scoped HTML integration: autonomous rendering, explicit discovery, opt-in observation. */
2
2
  import {registerWebComponent} from '../web-components/index.js';
3
+ import {getScopeRegistry,requestedScope,containsRoot} from './scopes.js';
3
4
 
4
- const registries=new WeakMap();
5
-
6
- function parseSchema(value,context){
7
- if(!value)return undefined;
8
- try {const parsed=JSON.parse(value);if(!parsed||typeof parsed!=='object'||Array.isArray(parsed))throw new Error('expected an object');return parsed;}
9
- catch(error){throw new Error(`Invalid prop schema in ${context}: ${error.message}`);}
5
+ function parseObject(value,context){
6
+ if(value==null||value==='')return {};
7
+ let result;
8
+ try{result=JSON.parse(value);}catch(e){throw new Error(`Invalid JSON in ${context}: ${e.message}`);}
9
+ if(!result||typeof result!=='object'||Array.isArray(result))throw new Error(`${context} must be a JSON object`);
10
+ return result;
11
+ }
12
+ function emit(el,type,detail){const win=el.ownerDocument.defaultView;el.dispatchEvent(new win.CustomEvent(type,{detail,bubbles:true,composed:true}));}
13
+ function state(el,value){el.setAttribute('data-runtime-state',value);}
14
+ function rootDocument(root){return root?.nodeType===9?root:root?.ownerDocument;}
15
+ function targetsWithin(root,selector){
16
+ return [...(root.matches?.(selector)?[root]:[]),...(root.querySelectorAll?.(selector)||[])];
10
17
  }
18
+ function elementOptions(el){return {module:el.getAttribute('module'),export:el.getAttribute('component')||'default',props:parseObject(el.getAttribute('element-props')||el.getAttribute('props'),`${el.localName} prop schema`),shadow:el.getAttribute('shadow')||'none'};}
11
19
 
12
- export function registerHTML(runtime,{name='default',root=globalThis.document,observe=false}={}) {
20
+ function defineAutonomousElements(doc){
21
+ const win=doc.defaultView;
22
+ if(!win?.customElements)return;
23
+ if(!win.customElements.get('runtime-render')){
24
+ class RuntimeRender extends win.HTMLElement {
25
+ static get observedAttributes(){return ['scope','runtime','data-runtime','module','component','props'];}
26
+ constructor(){super();this._owner=null;this._handle=null;this._abort=null;this._generation=0;this._unsubscribe=null;this._pending=null;this._lastProps='';this._reported=null;}
27
+ connectedCallback(){
28
+ if(!this._unsubscribe)this._unsubscribe=getScopeRegistry(doc).subscribe(()=>this._reconcile());
29
+ this._reconcile();
30
+ }
31
+ disconnectedCallback(){
32
+ const generation=++this._generation;
33
+ if(this._pending&&!this._handle){this._key=null;this._abort?.abort();}
34
+ queueMicrotask(()=>{if(!this.isConnected&&generation===this._generation){this._unsubscribe?.();this._unsubscribe=null;this._key=null;this._pending=null;this._release().catch(e=>this._report(e));state(this,'disposed');}});
35
+ }
36
+ attributeChangedCallback(name,oldValue,newValue){if(oldValue!==newValue&&this.isConnected)this._reconcile();}
37
+ _report(error){state(this,'error');if(this._reported!==error){this._reported=error;emit(this,'runtime-error',{error,module:this.getAttribute('module'),scope:requestedScope(this)});}}
38
+ async _release(){
39
+ this._abort?.abort();this._abort=null;
40
+ const handle=this._handle;this._handle=null;
41
+ if(handle){await handle.dispose();emit(this,'runtime-disposed',{module:this.getAttribute('module')});}
42
+ }
43
+ _reconcile(){
44
+ if(!this.isConnected)return;
45
+ const match=getScopeRegistry(doc).resolve(this);
46
+ const id=this.getAttribute('module')||'';
47
+ const exportName=this.getAttribute('component')||'default';
48
+ const propsText=this.getAttribute('props')||'{}';
49
+ const key=match.controller&&`${match.controller.id}|${id}|${exportName}`;
50
+ // Reconcile only on meaningful changes, not unrelated registry notifications.
51
+ if(match.error&&this._lastErrorMessage===match.error.message)return;
52
+ this._lastErrorMessage=match.error?.message||null;
53
+ if(!match.error&&this._key===key&&this._lastProps===propsText&&(this._handle||this._pending||!match.controller))return;
54
+ this._key=key;this._lastProps=propsText;
55
+ const generation=++this._generation;
56
+ this._abort?.abort();
57
+ this._pending=null;
58
+ const previous=this._handle;this._handle=null;
59
+ const controller=match.controller;
60
+ this._owner=controller;
61
+ if(match.error){this._report(match.error);if(previous)previous.dispose().catch(e=>this._report(e));return;}
62
+ if(!controller||!id){state(this,'pending');if(previous)previous.dispose().catch(e=>this._report(e));return;}
63
+ let props;
64
+ try{props=parseObject(propsText,'runtime-render props');}catch(e){this._report(e);if(previous)previous.dispose().catch(err=>this._report(err));return;}
65
+ this._reported=null;
66
+ if(previous&&this._previousKey===key){
67
+ // Update props in place where supported, otherwise remount.
68
+ if(previous.capabilities?.updateProps){
69
+ state(this,'mounting');
70
+ const task=Promise.resolve().then(()=>previous.updateProps(props)).then(async()=>{
71
+ if(generation!==this._generation||!this.isConnected){await previous.dispose();return;}
72
+ this._handle=previous;state(this,'ready');
73
+ }).catch(e=>this._report(e));
74
+ this._pending=task;task.finally(()=>{if(this._pending===task)this._pending=null;});return;
75
+ }
76
+ }
77
+ const abort=new AbortController();this._abort=abort;
78
+ state(this,'pending');
79
+ const task=(async()=>{
80
+ if(previous)await previous.dispose();
81
+ await controller.runtime.ready;
82
+ await controller.runtime.waitForModule(id,{signal:abort.signal});
83
+ if(abort.signal.aborted||generation!==this._generation||!this.isConnected)return;
84
+ state(this,'mounting');
85
+ const handle=await controller.runtime.mount({module:id,export:exportName,target:this,props});
86
+ if(abort.signal.aborted||generation!==this._generation||!this.isConnected){await handle.dispose();return;}
87
+ this._handle=handle;this._previousKey=key;state(this,'ready');emit(this,'runtime-ready',{module:id,scope:controller.scope,handle});
88
+ })().catch(e=>{if(!abort.signal.aborted&&generation===this._generation)this._report(e);});
89
+ this._pending=task;task.finally(()=>{if(this._pending===task)this._pending=null;});
90
+ }
91
+ }
92
+ win.customElements.define('runtime-render',RuntimeRender);
93
+ }
94
+ if(!win.customElements.get('runtime-element')){
95
+ class RuntimeElementDefinition extends win.HTMLElement {
96
+ static get observedAttributes(){return ['scope','runtime','data-runtime','tag','module','component','props','element-props'];}
97
+ constructor(){super();this._unsubscribe=null;this._registered=false;}
98
+ connectedCallback(){
99
+ if(!this._unsubscribe)this._unsubscribe=getScopeRegistry(doc).subscribe(()=>this._register());
100
+ this._register();
101
+ }
102
+ disconnectedCallback(){queueMicrotask(()=>{if(!this.isConnected){this._unsubscribe?.();this._unsubscribe=null;}});}
103
+ attributeChangedCallback(){if(this.isConnected)this._register();}
104
+ _register(){
105
+ if(!this.isConnected||this._registered)return;
106
+ const match=getScopeRegistry(doc).resolve(this);
107
+ if(match.error){emit(this,'runtime-error',{error:match.error});return;}
108
+ if(!match.controller){state(this,'pending');return;}
109
+ if(!this.getAttribute('tag')||!this.getAttribute('module'))return;
110
+ try{match.controller.defineElement(this.getAttribute('tag'),elementOptions(this));this._registered=true;this.hidden=true;state(this,'ready');emit(this,'runtime-ready',{tag:this.getAttribute('tag')});}
111
+ catch(error){state(this,'error');emit(this,'runtime-error',{error});}
112
+ }
113
+ }
114
+ win.customElements.define('runtime-element',RuntimeElementDefinition);
115
+ }
116
+ }
117
+ let sequence=0;
118
+ /** Register a scoped HTML controller. Never scans automatically. */
119
+ export function registerHTML(runtime,options={}){
120
+ const {name,scope,root=globalThis.document,observe=false,acceptUnscoped=true}=options;
13
121
  if(!root)throw new TypeError('registerHTML requires a DOM root');
14
- const doc=root.ownerDocument||root;
15
- let registry=registries.get(doc);
16
- if(!registry){registry=new Map();registries.set(doc,registry);}
17
- if(registry.has(name))throw new Error(`Runtime HTML name already registered: ${name}`);
18
- registry.set(name,runtime);
19
- const active=new Map();
20
- let observer;
21
- let alive=true;
22
- let pending=Promise.resolve();
122
+ const doc=rootDocument(root);
123
+ if(!doc)throw new TypeError('registerHTML root must belong to a document');
124
+ const registry=getScopeRegistry(doc);
125
+ defineAutonomousElements(doc);
126
+ const resolvedScope=scope||name||'default';
127
+ let alive=true,observer=null,currentRoot=root,appendTarget=root,pending=Promise.resolve();
128
+ const controller={id:++sequence,scope:resolvedScope,get root(){return currentRoot;},acceptUnscoped,runtime,get alive(){return alive;},defineElement};
23
129
  function defineElement(tag,config){return registerWebComponent(runtime,{...config,tag,document:doc});}
24
- function elementOptions(el){
25
- return {
26
- module:el.getAttribute('module'),
27
- export:el.getAttribute('component')||'default',
28
- props:parseSchema(el.getAttribute('element-props')||el.getAttribute('props'),el.tagName.toLowerCase()),
29
- shadow:el.getAttribute('shadow')||'none'
30
- };
130
+ function owned(el){const match=registry.resolve(el);return match.controller===controller;}
131
+ async function registerScript(el){
132
+ if(!alive||!owned(el)||el.dataset.runtimeRegistered==='true')return false;
133
+ const type=el.getAttribute('type');
134
+ if(!runtime.getSupportedTypes().includes(type))return false;
135
+ const id=el.getAttribute('module');if(!id)return false;
136
+ if(el.hasAttribute('data-src'))await runtime.loadSource(id,{type,url:el.getAttribute('data-src')});
137
+ else runtime.define(id,runtime.normalizeInlineSource(el.textContent,{type,id,element:el}),{type});
138
+ if(el.hasAttribute('custom-element')){
139
+ defineElement(el.getAttribute('custom-element'),elementOptions(el));
140
+ }
141
+ el.dataset.runtimeRegistered='true';
142
+ return true;
31
143
  }
32
- function isOwned(node) {return node.getAttribute('data-runtime')===name || node.getAttribute('runtime')===name || (!node.hasAttribute('data-runtime')&&!node.hasAttribute('runtime')&&name==='default');}
33
144
  async function scan(){
34
145
  if(!alive)return;
35
- for(const el of root.querySelectorAll('script[type][module]')){
36
- if(!isOwned(el)||el.dataset.runtimeRegistered==='true')continue;
37
- const type=el.getAttribute('type');
38
- if(!runtime.getSupportedTypes().includes(type))continue;
39
- const id=el.getAttribute('module');
40
- if(el.hasAttribute('data-src'))await runtime.loadSource(id,{type,url:el.getAttribute('data-src')});
41
- else runtime.define(id,runtime.normalizeInlineSource(el.textContent,{type,id,element:el}),{type});
42
- if(el.hasAttribute('custom-element')){
43
- defineElement(el.getAttribute('custom-element'),elementOptions(el));
44
- }
45
- el.dataset.runtimeRegistered='true';
46
- }
47
- for(const el of root.querySelectorAll('runtime-element[tag][module]')){
48
- if(!isOwned(el)||el.dataset.runtimeRegistered==='true')continue;
49
- defineElement(el.getAttribute('tag'),elementOptions(el));
50
- el.dataset.runtimeRegistered='true';
51
- el.hidden=true;
52
- }
53
- const targets=root.querySelectorAll('runtime-render');
54
- for(const el of targets){
55
- if(!isOwned(el)||active.has(el))continue;
56
- let props={};
57
- try {props=JSON.parse(el.getAttribute('props')||'{}');}catch(e){throw new Error(`Invalid props JSON in runtime-render: ${e.message}`);}
58
- if(!props||typeof props!=='object'||Array.isArray(props))throw new Error('runtime-render props must be a JSON object');
59
- const module=el.getAttribute('module');if(!module)continue;
60
- const token={cancelled:false,handle:null};active.set(el,token);
61
- try {token.handle=await runtime.mount({module,target:el,props,export:el.getAttribute('component')||'default'});if(token.cancelled)await token.handle.dispose();}
62
- catch(e){active.delete(el);el.dispatchEvent(new CustomEvent('runtime-error',{detail:e}));throw e;}
63
- }
64
- for(const [el,token] of active){if(!el.isConnected||!root.contains(el)){active.delete(el);token.cancelled=true;await token.handle?.dispose();}}
146
+ for(const el of targetsWithin(currentRoot,'script[type][module]'))await registerScript(el);
147
+ // runtime-element and runtime-render are autonomous, not scan work.
65
148
  }
66
149
  function scheduleScan(){pending=pending.catch(()=>{}).then(scan);return pending;}
67
- if(observe){observer=new MutationObserver(()=>scheduleScan());observer.observe(root,{subtree:true,childList:true});}
68
- // Explicitly register this custom element only once per browser window.
69
- const win=doc.defaultView;
70
- if(win?.customElements&&!win.customElements.get('runtime-render')){
71
- win.customElements.define('runtime-render',class extends win.HTMLElement {});
150
+ function enableObservation(){
151
+ if(observer)observer.disconnect();observer=null;
152
+ if(!observe)return;
153
+ const Observer=doc.defaultView?.MutationObserver;
154
+ if(!Observer)return;
155
+ observer=new Observer(records=>{
156
+ for(const record of records)for(const node of record.addedNodes){
157
+ if(node.nodeType!==1)continue;
158
+ if(node.matches?.('script[type][module]')||node.querySelector?.('script[type][module]')){
159
+ scheduleScan();return;
160
+ }
161
+ }
162
+ });
163
+ observer.observe(currentRoot,{subtree:true,childList:true});
164
+ }
165
+ async function appendScript(script,target=appendTarget){
166
+ if(script.localName!=='script')throw new TypeError('appendScript expects a script element');
167
+ const parent=target.nodeType===9?(target.head||target.body||target.documentElement):target;
168
+ parent.append(script);return script;
169
+ }
170
+ async function appendAndRegister(script,target=appendTarget){
171
+ await appendScript(script,target);
172
+ await registerScript(script);
173
+ return script;
174
+ }
175
+ async function setRoot(nextRoot,{registerExisting=false}={}){
176
+ if(!alive)throw new Error('HTML controller is disconnected');
177
+ if(rootDocument(nextRoot)!==doc)throw new Error('HTML root must remain in the same document');
178
+ if(observer)observer.disconnect();currentRoot=nextRoot;registry.notify();enableObservation();
179
+ if(registerExisting)await scheduleScan();
180
+ return nextRoot;
181
+ }
182
+ async function moveTo(nextRoot,opts){const moved=await setRoot(nextRoot,opts);appendTarget=nextRoot;return moved;}
183
+ async function disconnect(){
184
+ if(!alive)return;
185
+ alive=false;observer?.disconnect();registry.remove(controller);
186
+ await pending.catch(()=>{});
72
187
  }
73
- return {name,runtime,defineElement,registerExisting:scheduleScan,scan:scheduleScan,async disconnect(){if(!alive)return;alive=false;observer?.disconnect();await pending.catch(()=>{});for(const token of active.values()){token.cancelled=true;await token.handle?.dispose();}active.clear();registry.delete(name);}};
188
+ registry.add(controller);
189
+ enableObservation();
190
+ return {name:resolvedScope,scope:resolvedScope,runtime,defineElement,registerScript,appendScript,appendAndRegister,registerExisting:scheduleScan,scan:scheduleScan,setRoot,moveTo,disconnect,get root(){return currentRoot;},get appendTarget(){return appendTarget;}};
74
191
  }
@@ -0,0 +1,51 @@
1
+ /** Framework-neutral, document-owned HTML controller routing. No DOM observer or polling. */
2
+ const documents=new WeakMap();
3
+ const scopeAttributeNames=['scope','runtime','data-runtime'];
4
+
5
+ export function getScopeRegistry(doc){
6
+ if(!doc)throw new TypeError('HTML scope registry requires a document');
7
+ let registry=documents.get(doc);
8
+ if(!registry){
9
+ const controllers=new Set(),listeners=new Set();
10
+ registry={
11
+ controllers,
12
+ subscribe(fn){listeners.add(fn);return ()=>listeners.delete(fn);},
13
+ notify(){for(const fn of [...listeners])fn();},
14
+ add(controller){controllers.add(controller);registry.notify();},
15
+ remove(controller){if(controllers.delete(controller))registry.notify();},
16
+ resolve(element){return resolveScope([...controllers].filter(c=>c.alive),element);}
17
+ };
18
+ documents.set(doc,registry);
19
+ }
20
+ return registry;
21
+ }
22
+ export function requestedScope(element){
23
+ for(const attr of scopeAttributeNames){
24
+ const value=element.getAttribute?.(attr)?.trim();
25
+ if(value)return value;
26
+ }
27
+ return null;
28
+ }
29
+ export function containsRoot(root,element){
30
+ if(root===element)return true;
31
+ return Boolean(root?.contains?.(element));
32
+ }
33
+ export function resolveScope(controllers,element){
34
+ const explicit=requestedScope(element);
35
+ const possible=controllers.filter(c=>explicit ? c.scope===explicit : c.acceptUnscoped!==false);
36
+ const matches=possible.filter(c=>containsRoot(c.root,element));
37
+ if(matches.length===0){
38
+ // A completely missing scope is a pending dependency; a scope that exists
39
+ // but does not contain the element is a configuration error, as in solid-render.
40
+ if(possible.length){
41
+ return {controller:null,pending:false,error:new Error(explicit
42
+ ? `HTML runtime scope ${JSON.stringify(explicit)} is registered but no matching controller root contains this element.`
43
+ : 'No eligible HTML runtime has a root containing this unscoped element.')};
44
+ }
45
+ return {controller:null,pending:true,reason:explicit?`Waiting for HTML scope ${JSON.stringify(explicit)}`:'Waiting for an eligible HTML runtime'};
46
+ }
47
+ const specific=matches.filter(candidate=>matches.every(other=>candidate===other||!containsRoot(candidate.root,other.root)));
48
+ if(specific.length===1)return {controller:specific[0],pending:false};
49
+ const names=matches.map(c=>c.scope).join(', ');
50
+ return {controller:null,pending:false,error:new Error(`Ambiguous HTML runtime ownership${explicit?` for scope ${JSON.stringify(explicit)}`:''}: ${names}. Use a distinct scope or non-overlapping roots.`)};
51
+ }
package/src/index.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import {createRuntime as createModuleEngine} from './engine/runtime.js';
2
2
  import {createBuildPipeline} from './build/pipeline.js';
3
3
  import {createStyleManager} from './resources/styles.js';
4
- import {mergePluginPackageEntries} from './plugins/package-entries.js';
4
+ import {mergePluginPackageEntries,mergePluginPackageFamilyRules} from './plugins/package-entries.js';
5
+ import {parsePackageSpecifier,PackageVersionResolutionError} from './engine/package-resolution/index.js';
5
6
 
6
7
  /** Framework-neutral, synchronous factory. Async plugin setup is available on `.ready`. */
7
8
  export function createRuntime(options={}) {
@@ -10,22 +11,35 @@ export function createRuntime(options={}) {
10
11
  const metadata=new Map();
11
12
  const pipeline=createBuildPipeline({plugins,defaultType:options.defaultType||'javascript',onArtifact:(id,artifact)=>artifactById.set(id,artifact)});
12
13
  const effectivePackages=mergePluginPackageEntries(options.packages,plugins);
13
- const core=createModuleEngine({...options,packages:effectivePackages,compiler:pipeline});
14
+ const familyRules=mergePluginPackageFamilyRules(effectivePackages,plugins,options._solidFamilyRules,options.modules);
15
+ const protectedRoots=new Set(plugins.flatMap(plugin=>Object.keys(plugin.packageFamilyRules??{})));
16
+ function rejectProtected(id){
17
+ const name=parsePackageSpecifier(String(id))?.packageName;
18
+ if(name && protectedRoots.has(name))throw new PackageVersionResolutionError('PACKAGE_VERSION_CONFLICT',
19
+ `${JSON.stringify(id)} belongs to a framework-managed package family; use host mode to override its modules.`,{packageName:name});
20
+ }
21
+ for(const id of Object.keys(options.urls??{}))rejectProtected(id);
22
+ const core=createModuleEngine({...options,packages:effectivePackages,_solidFamilyRules:familyRules,compiler:pipeline});
14
23
  const styles=createStyleManager();
15
24
  const mounts=new Set();
16
25
  let disposed=false;
17
26
  const runtime={
18
27
  ...core,
19
28
  ready:null,
29
+ defineModule(id,namespace){rejectProtected(id);return core.defineModule(id,namespace);},
30
+ defineUrl(id,url){rejectProtected(id);return core.defineUrl(id,url);},
20
31
  define(id,source,config={}) {
32
+ rejectProtected(id);
21
33
  const type=pipeline.detectType(id,config.type||config.format);
22
34
  metadata.set(id,{type});
23
35
  return core.define(id,source,{...config,format:type});
24
36
  },
25
37
  defineMany(definitions) {
38
+ definitions.forEach(({id})=>rejectProtected(id));
26
39
  return definitions.map(({id,source,...opts})=>runtime.define(id,source,opts));
27
40
  },
28
41
  update(id,source,config={}) {
42
+ rejectProtected(id);
29
43
  const type=pipeline.detectType(id,config.type||config.format||metadata.get(id)?.type);
30
44
  metadata.set(id,{type});
31
45
  return core.update(id,source,{...config,format:type});
@@ -28,3 +28,33 @@ export function mergePluginPackageEntries(packages,plugins=[]) {
28
28
  }
29
29
  return {...source,versions,entries};
30
30
  }
31
+
32
+
33
+ /** Runtime-local provider rules for package families with strict identity
34
+ * requirements (e.g. Solid 2). Plugins own their family membership; the
35
+ * generic module engine only sees a regular root -> URL/subpath resolver map.
36
+ */
37
+ export function mergePluginPackageFamilyRules(packages, plugins=[], explicitRules={}, modules={}) {
38
+ const rules={...(explicitRules??{})};
39
+ const pinned=createPackagePolicy(packages);
40
+ for(const plugin of plugins){
41
+ for(const [name,rule] of Object.entries(plugin.packageFamilyRules??{})){
42
+ const previous=rules[name];
43
+ if(previous && (previous.url!==rule.url || previous.version!==rule.version))
44
+ throw new PackageVersionResolutionError('PACKAGE_VERSION_CONFLICT',
45
+ `Plugin ${JSON.stringify(plugin.name)} requires ${JSON.stringify(name)} from ${rule.url}, but another provider requires ${previous.url}.`,
46
+ {packageName:name,plugin:plugin.name,expected:rule.url,actual:previous.url});
47
+ const selected=pinned.inspect(name);
48
+ if(selected && selected.url!==rule.url)
49
+ throw new PackageVersionResolutionError('PACKAGE_VERSION_CONFLICT',
50
+ `Plugin ${JSON.stringify(plugin.name)} owns ${JSON.stringify(name)} at ${rule.url}, but the runtime pins ${selected.url}. Remove the duplicate family pin.`,
51
+ {packageName:name,plugin:plugin.name,expected:rule.url,actual:selected.url});
52
+ if(Object.hasOwn(modules,name))
53
+ throw new PackageVersionResolutionError('PACKAGE_VERSION_CONFLICT',
54
+ `Plugin ${JSON.stringify(plugin.name)} owns ${JSON.stringify(name)}; a caller-owned module namespace cannot guarantee the same framework identity. Use host mode.`,
55
+ {packageName:name,plugin:plugin.name});
56
+ rules[name]=rule;
57
+ }
58
+ }
59
+ return rules;
60
+ }
@@ -57,6 +57,7 @@ export function registerWebComponent(runtime,options={}) {
57
57
  super();
58
58
  this._runtimeHandle=null;
59
59
  this._mounting=null;
60
+ this._waitController=null;
60
61
  this._generation=0;
61
62
  this._propertyValues=Object.create(null);
62
63
  this._updateTask=Promise.resolve();
@@ -82,6 +83,7 @@ export function registerWebComponent(runtime,options={}) {
82
83
  connectedCallback(){
83
84
  for(const [key,value] of this._preUpgrade.splice(0))this[key]=value;
84
85
  const version=++this._generation;
86
+ if(this._mounting)this._waitController?.abort();
85
87
  queueMicrotask(()=>{if(this.isConnected&&version===this._generation)this._start(version);});
86
88
  }
87
89
  disconnectedCallback(){
@@ -115,19 +117,29 @@ export function registerWebComponent(runtime,options={}) {
115
117
  if(this._runtimeHandle||this._mounting||!this.isConnected||version!==this._generation)return;
116
118
  try{
117
119
  const props=this._collectProps();
118
- const mountPromise=runtime.mount({module,export:exportName,target:this,props});
120
+ const waitController=new AbortController();
121
+ this._waitController=waitController;
122
+ const mountPromise=(async()=>{
123
+ await runtime.ready;
124
+ if(typeof runtime.waitForModule==='function')await runtime.waitForModule(module,{signal:waitController.signal});
125
+ if(waitController.signal.aborted||version!==this._generation||!this.isConnected)return null;
126
+ return runtime.mount({module,export:exportName,target:this,props});
127
+ })();
119
128
  this._mounting=mountPromise;
120
129
  const handle=await mountPromise;
130
+ if(!handle)return;
121
131
  if(!this.isConnected||version!==this._generation){await handle.dispose();return;}
122
132
  this._runtimeHandle=handle;
123
133
  dispatch(this,'runtime-ready',{module,tag,handle});
124
- }catch(e){this._report(e);}finally{
134
+ }catch(e){if(e?.name!=='AbortError')this._report(e);}finally{
135
+ if(this._waitController?.signal.aborted || version===this._generation)this._waitController=null;
125
136
  this._mounting=null;
126
137
  if(this.isConnected&&!this._runtimeHandle&&version!==this._generation)
127
138
  queueMicrotask(()=>this._start(this._generation));
128
139
  }
129
140
  }
130
141
  async _stop(){
142
+ this._waitController?.abort();this._waitController=null;
131
143
  // A pending mount observes generation changes and disposes itself.
132
144
  const handle=this._runtimeHandle;this._runtimeHandle=null;
133
145
  if(handle){await handle.dispose();dispatch(this,'runtime-disposed',{module,tag});}