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 +13 -0
- package/html.d.ts +34 -2
- package/index.d.ts +2 -0
- package/package.json +1 -1
- package/src/html/index.js +178 -61
- package/src/html/scopes.js +51 -0
- package/src/index.js +16 -2
- package/src/plugins/package-entries.js +30 -0
- package/src/web-components/index.js +14 -2
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 {
|
|
4
|
-
|
|
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
package/src/html/index.js
CHANGED
|
@@ -1,74 +1,191 @@
|
|
|
1
|
-
/** HTML
|
|
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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
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
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
|
36
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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});}
|