browser-module-runtime 0.0.7 → 0.0.9
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 +17 -4
- package/html.d.ts +5 -13
- package/index.d.ts +7 -0
- package/package.json +1 -1
- package/src/html/index.js +42 -14
- package/src/index.js +48 -1
package/README.md
CHANGED
|
@@ -5,9 +5,22 @@ A framework-neutral browser module runtime with a native ESM linker, independent
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
7
7
|
```sh
|
|
8
|
-
npm install browser-module-runtime@0.0.
|
|
8
|
+
npm install browser-module-runtime@0.0.9
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
+
## Runtime-bound HTML integration (0.0.9)
|
|
12
|
+
|
|
13
|
+
```js
|
|
14
|
+
import {createRuntime} from 'browser-module-runtime';
|
|
15
|
+
const runtime=createRuntime({plugins:[/* solid(), svelte(), octane() or vue() */]});
|
|
16
|
+
await runtime.html.process({scope:'demo'}); // process inert HTML declarations once
|
|
17
|
+
// Alternatively, without initial discovery:
|
|
18
|
+
const controller=await runtime.html.create({scope:'demo'});
|
|
19
|
+
// await controller.process(); // optional explicit later scan
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
The stable `runtime.html` facade loads the browser HTML adapter lazily, reuses scoped controllers for the same runtime/root/scope, and disconnects facade-owned controllers when `runtime.dispose()` runs. No browser DOM access, scanning, or observer starts merely from `createRuntime()`. Advanced standalones remain available: `import {createHTMLRuntime,processHTML} from 'browser-module-runtime/html'`. Controllers remain autonomous for `<runtime-render>` and `<runtime-element>`, while inert source `<script>` declarations need `process()` or opt-in `observe:true`.
|
|
23
|
+
|
|
11
24
|
## Define, import and update modules
|
|
12
25
|
|
|
13
26
|
```js
|
|
@@ -59,7 +72,7 @@ await handle.dispose();
|
|
|
59
72
|
### Public exports
|
|
60
73
|
|
|
61
74
|
- `browser-module-runtime`: runtime, build pipeline, cache and resolver utilities
|
|
62
|
-
- `browser-module-runtime/html`: `
|
|
75
|
+
- `browser-module-runtime/html`: `createHTMLRuntime` and generic `<runtime-render>` integration
|
|
63
76
|
- `browser-module-runtime/packages`: package policy utilities
|
|
64
77
|
- `browser-module-runtime/cache`: compile-cache implementations
|
|
65
78
|
|
|
@@ -68,8 +81,8 @@ await handle.dispose();
|
|
|
68
81
|
### Scoped HTML (development branch)
|
|
69
82
|
|
|
70
83
|
```js
|
|
71
|
-
import {
|
|
72
|
-
|
|
84
|
+
import {createHTMLRuntime} from 'browser-module-runtime/html';
|
|
85
|
+
createHTMLRuntime(runtime,{scope:'app',root:document}); // No initial scan or observer
|
|
73
86
|
```
|
|
74
87
|
|
|
75
88
|
```html
|
package/html.d.ts
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import type {Runtime} from './index.js';
|
|
2
2
|
import type {WebComponentOptions} from './web-components.js';
|
|
3
3
|
export interface HTMLController {
|
|
4
|
-
/** Scope name; `name` is a backwards compatible alias. */
|
|
5
|
-
readonly name:string;
|
|
6
4
|
readonly scope:string;
|
|
7
5
|
readonly root:Document|Element;
|
|
8
6
|
readonly appendTarget:Document|Element;
|
|
@@ -16,18 +14,12 @@ export interface HTMLController {
|
|
|
16
14
|
appendAndRegister(script:HTMLScriptElement,target?:Document|Element):Promise<HTMLScriptElement>;
|
|
17
15
|
/** Preferred explicit, repeatable processing of existing source declarations. */
|
|
18
16
|
process():Promise<void>;
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
/** @deprecated Use process(). */
|
|
22
|
-
scan():Promise<void>;
|
|
23
|
-
setRoot(root:Document|Element,options?:{processExisting?:boolean;registerExisting?:boolean}):Promise<Document|Element>;
|
|
24
|
-
moveTo(root:Document|Element,options?:{processExisting?:boolean;registerExisting?:boolean}):Promise<Document|Element>;
|
|
17
|
+
setRoot(root:Document|Element,options?:{processExisting?:boolean}):Promise<Document|Element>;
|
|
18
|
+
moveTo(root:Document|Element,options?:{processExisting?:boolean}):Promise<Document|Element>;
|
|
25
19
|
disconnect():Promise<void>;
|
|
26
20
|
}
|
|
27
|
-
export interface
|
|
21
|
+
export interface HTMLRuntimeOptions {
|
|
28
22
|
scope?:string;
|
|
29
|
-
/** Backward compatible alias for scope. */
|
|
30
|
-
name?:string;
|
|
31
23
|
root?:Document|Element;
|
|
32
24
|
/** Whether an element without an explicit scope can be owned by this controller. */
|
|
33
25
|
acceptUnscoped?:boolean;
|
|
@@ -35,9 +27,9 @@ export interface RegisterHTMLOptions {
|
|
|
35
27
|
observe?:boolean;
|
|
36
28
|
}
|
|
37
29
|
/** Does not scan automatically. <runtime-render> itself is an autonomous custom element. */
|
|
38
|
-
export declare function
|
|
30
|
+
export declare function createHTMLRuntime(runtime:Runtime,options?:HTMLRuntimeOptions):HTMLController;
|
|
39
31
|
|
|
40
32
|
/** High-level one-shot HTML discovery. Registers the controller and processes existing inert scripts.
|
|
41
33
|
* Returns the controller for subsequent root, observer and lifecycle operations.
|
|
42
34
|
*/
|
|
43
|
-
export declare function processHTML(runtime:Runtime,options?:
|
|
35
|
+
export declare function processHTML(runtime:Runtime,options?:HTMLRuntimeOptions):Promise<HTMLController>;
|
package/index.d.ts
CHANGED
|
@@ -66,6 +66,13 @@ export interface RuntimeOptions {
|
|
|
66
66
|
fallbackResolve?(specifier:string,importer?:string):string | undefined;
|
|
67
67
|
}
|
|
68
68
|
export interface Runtime {
|
|
69
|
+
/** Lazy, runtime-bound HTML integration. Requires a DOM only when called. */
|
|
70
|
+
html: {
|
|
71
|
+
/** Create or reuse controller without processing existing declarations. */
|
|
72
|
+
create(options?:import('./html.js').HTMLRuntimeOptions):Promise<import('./html.js').HTMLController>;
|
|
73
|
+
/** Create or reuse controller and process existing inert script declarations. */
|
|
74
|
+
process(options?:import('./html.js').HTMLRuntimeOptions):Promise<import('./html.js').HTMLController>;
|
|
75
|
+
};
|
|
69
76
|
ready: Promise<void>;
|
|
70
77
|
define(id:string,source:string,config?:{type?:string;format?:string;imports?:Record<string,unknown>}):string;
|
|
71
78
|
defineMany(definitions:ModuleSource[]):string[];
|
package/package.json
CHANGED
package/src/html/index.js
CHANGED
|
@@ -118,16 +118,19 @@ function defineAutonomousElements(doc){
|
|
|
118
118
|
}
|
|
119
119
|
let sequence=0;
|
|
120
120
|
/** Register a scoped HTML controller. Never scans automatically. */
|
|
121
|
-
export function
|
|
122
|
-
const {
|
|
123
|
-
if(!root)throw new TypeError('
|
|
121
|
+
export function createHTMLRuntime(runtime,options={}){
|
|
122
|
+
const {scope="default",root=globalThis.document,observe=false,acceptUnscoped=true}=options;
|
|
123
|
+
if(!root)throw new TypeError('createHTMLRuntime requires a DOM root');
|
|
124
124
|
const doc=rootDocument(root);
|
|
125
|
-
if(!doc)throw new TypeError('
|
|
125
|
+
if(!doc)throw new TypeError('createHTMLRuntime root must belong to a document');
|
|
126
126
|
const registry=getScopeRegistry(doc);
|
|
127
|
+
if([...registry.controllers].some(c=>c.alive&&c.runtime===runtime&&c.scope===scope&&c.root===root)){
|
|
128
|
+
throw new Error(`HTML controller already exists for scope ${JSON.stringify(scope)} and the same runtime/root; use runtime.html.create() to reuse it`);
|
|
129
|
+
}
|
|
127
130
|
defineAutonomousElements(doc);
|
|
128
|
-
const resolvedScope=scope
|
|
131
|
+
const resolvedScope=scope;
|
|
129
132
|
let alive=true,observer=null,currentRoot=root,appendTarget=root,pending=Promise.resolve();
|
|
130
|
-
const controller={id:++sequence,scope:resolvedScope,get root(){return currentRoot;},acceptUnscoped,runtime,get alive(){return alive;},defineElement};
|
|
133
|
+
const controller={id:++sequence,scope:resolvedScope,get root(){return currentRoot;},acceptUnscoped,observe,runtime,get alive(){return alive;},defineElement};
|
|
131
134
|
function defineElement(tag,config){return registerWebComponent(runtime,{...config,tag,document:doc});}
|
|
132
135
|
function owned(el){const match=registry.resolve(el);return match.controller===controller;}
|
|
133
136
|
async function registerScript(el){
|
|
@@ -174,13 +177,11 @@ export function registerHTML(runtime,options={}){
|
|
|
174
177
|
await registerScript(script);
|
|
175
178
|
return script;
|
|
176
179
|
}
|
|
177
|
-
async function setRoot(nextRoot,{processExisting
|
|
178
|
-
// processExisting is preferred; registerExisting remains a legacy alias.
|
|
179
|
-
const shouldProcess=processExisting??registerExisting;
|
|
180
|
+
async function setRoot(nextRoot,{processExisting=false}={}){
|
|
180
181
|
if(!alive)throw new Error('HTML controller is disconnected');
|
|
181
182
|
if(rootDocument(nextRoot)!==doc)throw new Error('HTML root must remain in the same document');
|
|
182
183
|
if(observer)observer.disconnect();currentRoot=nextRoot;registry.notify();enableObservation();
|
|
183
|
-
if(
|
|
184
|
+
if(processExisting)await scheduleScan();
|
|
184
185
|
return nextRoot;
|
|
185
186
|
}
|
|
186
187
|
async function moveTo(nextRoot,opts){const moved=await setRoot(nextRoot,opts);appendTarget=nextRoot;return moved;}
|
|
@@ -189,22 +190,49 @@ export function registerHTML(runtime,options={}){
|
|
|
189
190
|
alive=false;observer?.disconnect();registry.remove(controller);
|
|
190
191
|
await pending.catch(()=>{});
|
|
191
192
|
}
|
|
193
|
+
Object.assign(controller,{registerScript,appendScript,appendAndRegister,process:scheduleScan,setRoot,moveTo,disconnect});
|
|
194
|
+
Object.defineProperty(controller,'appendTarget',{get(){return appendTarget;}});
|
|
192
195
|
registry.add(controller);
|
|
193
196
|
enableObservation();
|
|
194
|
-
return
|
|
197
|
+
return controller;
|
|
195
198
|
}
|
|
196
199
|
|
|
197
200
|
/** Create a scoped HTML controller and process existing source declarations once.
|
|
198
|
-
* Unlike
|
|
201
|
+
* Unlike createHTMLRuntime(), this convenience function explicitly performs discovery.
|
|
199
202
|
* Returns the controller so callers can use appendAndRegister(), setRoot() or disconnect().
|
|
200
203
|
*/
|
|
201
204
|
export async function processHTML(runtime,options={}){
|
|
202
|
-
const controller=
|
|
205
|
+
const {controller,created}=getOrCreateHTMLRuntime(runtime,options);
|
|
203
206
|
try{
|
|
204
207
|
await controller.process();
|
|
205
208
|
return controller;
|
|
206
209
|
}catch(error){
|
|
207
|
-
await controller.disconnect();
|
|
210
|
+
if(created)await controller.disconnect();
|
|
208
211
|
throw error;
|
|
209
212
|
}
|
|
210
213
|
}
|
|
214
|
+
|
|
215
|
+
/** Locate an existing controller in the authoritative document scope registry.
|
|
216
|
+
* Unlike the low-level creator, this never silently adds a second owner for
|
|
217
|
+
* the same runtime, scope and root. Used by the lazy runtime.html facade.
|
|
218
|
+
*/
|
|
219
|
+
export function getOrCreateHTMLRuntime(runtime,options={}){
|
|
220
|
+
const root=options.root??globalThis.document;
|
|
221
|
+
if(!root||![1,9].includes(root.nodeType))throw new TypeError('runtime.html requires a Document or Element root (browser DOM)');
|
|
222
|
+
const doc=rootDocument(root);
|
|
223
|
+
if(!doc)throw new TypeError('runtime.html root must belong to a document');
|
|
224
|
+
const scope=options.scope??'default';
|
|
225
|
+
if(typeof scope!=='string'||!scope.trim())throw new TypeError('HTML scope must be a non-empty string');
|
|
226
|
+
const controllers=[...getScopeRegistry(doc).controllers].filter(c=>c.alive&&c.runtime===runtime&&c.scope===scope&&c.root===root);
|
|
227
|
+
if(controllers.length>1)throw new Error(`Duplicate HTML controllers for scope ${JSON.stringify(scope)} and the same runtime/root`);
|
|
228
|
+
const existing=controllers[0];
|
|
229
|
+
if(existing){
|
|
230
|
+
for(const key of ['observe','acceptUnscoped']){
|
|
231
|
+
if(Object.prototype.hasOwnProperty.call(options,key)&&options[key]!==existing[key]){
|
|
232
|
+
throw new Error(`HTML controller configuration conflict for scope ${JSON.stringify(scope)}: ${key} cannot change; disconnect the old controller first`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return {controller:existing,created:false};
|
|
236
|
+
}
|
|
237
|
+
return {controller:createHTMLRuntime(runtime,{...options,scope,root}),created:true};
|
|
238
|
+
}
|
package/src/index.js
CHANGED
|
@@ -23,9 +23,56 @@ export function createRuntime(options={}) {
|
|
|
23
23
|
const styles=createStyleManager();
|
|
24
24
|
const mounts=new Set();
|
|
25
25
|
let disposed=false;
|
|
26
|
+
// A stable, DOM-free facade. Loading the optional HTML adapter and creating
|
|
27
|
+
// custom elements are deferred until the first explicit HTML call.
|
|
28
|
+
const htmlOwners=new Set();
|
|
29
|
+
const htmlProcesses=new WeakMap();
|
|
30
|
+
const obtainHTML=async(options={})=>{
|
|
31
|
+
if(disposed)throw new Error('Runtime is disposed');
|
|
32
|
+
const {getOrCreateHTMLRuntime}=await import('./html/index.js');
|
|
33
|
+
if(disposed)throw new Error('Runtime is disposed');
|
|
34
|
+
const result=getOrCreateHTMLRuntime(runtime,options);
|
|
35
|
+
if(result.created)htmlOwners.add(result.controller);
|
|
36
|
+
return result;
|
|
37
|
+
};
|
|
38
|
+
const html={
|
|
39
|
+
async create(options={}){
|
|
40
|
+
return (await obtainHTML(options)).controller;
|
|
41
|
+
},
|
|
42
|
+
async process(options={}){
|
|
43
|
+
const {controller,created}=await obtainHTML(options);
|
|
44
|
+
// The authoritative registry handles identity; this queue only orders
|
|
45
|
+
// explicit processing passes for the same controller.
|
|
46
|
+
const previous=htmlProcesses.get(controller)||Promise.resolve();
|
|
47
|
+
const job=previous.catch(()=>{}).then(async()=>{
|
|
48
|
+
if(disposed||!controller.alive)throw new Error('HTML controller or runtime is disposed');
|
|
49
|
+
await runtime.ready;
|
|
50
|
+
if(disposed||!controller.alive)throw new Error('HTML controller or runtime is disposed');
|
|
51
|
+
await controller.process();
|
|
52
|
+
return controller;
|
|
53
|
+
});
|
|
54
|
+
htmlProcesses.set(controller,job);
|
|
55
|
+
try{return await job;}
|
|
56
|
+
catch(error){
|
|
57
|
+
// Only the call which CREATED the controller may undo it after an
|
|
58
|
+
// initial scan failure. Existing owners (especially external) survive.
|
|
59
|
+
if(created&&htmlOwners.has(controller)){
|
|
60
|
+
htmlOwners.delete(controller);
|
|
61
|
+
await controller.disconnect();
|
|
62
|
+
}
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
const disposeOwnedHTML=async()=>{
|
|
68
|
+
const controllers=[...htmlOwners];
|
|
69
|
+
htmlOwners.clear();
|
|
70
|
+
await Promise.allSettled(controllers.map(c=>c.disconnect()));
|
|
71
|
+
};
|
|
26
72
|
const runtime={
|
|
27
73
|
...core,
|
|
28
74
|
ready:null,
|
|
75
|
+
html,
|
|
29
76
|
defineModule(id,namespace){rejectProtected(id);return core.defineModule(id,namespace);},
|
|
30
77
|
defineUrl(id,url){rejectProtected(id);return core.defineUrl(id,url);},
|
|
31
78
|
define(id,source,config={}) {
|
|
@@ -99,7 +146,7 @@ export function createRuntime(options={}) {
|
|
|
99
146
|
mounts.add(out);
|
|
100
147
|
return out;
|
|
101
148
|
},
|
|
102
|
-
async dispose(){if(disposed)return;disposed=true;await Promise.allSettled([...mounts].map(m=>m.dispose()));styles.dispose();core.dispose();for(const plugin of [...plugins].reverse())await plugin.dispose?.();}
|
|
149
|
+
async dispose(){if(disposed)return;disposed=true;await disposeOwnedHTML();await Promise.allSettled([...mounts].map(m=>m.dispose()));styles.dispose();core.dispose();for(const plugin of [...plugins].reverse())await plugin.dispose?.();}
|
|
103
150
|
};
|
|
104
151
|
runtime.ready=Promise.resolve().then(async()=>{for(const plugin of plugins)await plugin.configure?.({runtime,pipeline});});
|
|
105
152
|
return runtime;
|