browser-module-runtime 0.0.8 → 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 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
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
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "browser-module-runtime",
3
- "version": "0.0.8",
3
+ "version": "0.0.9",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
package/src/html/index.js CHANGED
@@ -124,10 +124,13 @@ export function createHTMLRuntime(runtime,options={}){
124
124
  const doc=rootDocument(root);
125
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
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){
@@ -187,9 +190,11 @@ export function createHTMLRuntime(runtime,options={}){
187
190
  alive=false;observer?.disconnect();registry.remove(controller);
188
191
  await pending.catch(()=>{});
189
192
  }
193
+ Object.assign(controller,{registerScript,appendScript,appendAndRegister,process:scheduleScan,setRoot,moveTo,disconnect});
194
+ Object.defineProperty(controller,'appendTarget',{get(){return appendTarget;}});
190
195
  registry.add(controller);
191
196
  enableObservation();
192
- return {scope:resolvedScope,runtime,defineElement,registerScript,appendScript,appendAndRegister,process:scheduleScan,setRoot,moveTo,disconnect,get root(){return currentRoot;},get appendTarget(){return appendTarget;}};
197
+ return controller;
193
198
  }
194
199
 
195
200
  /** Create a scoped HTML controller and process existing source declarations once.
@@ -197,12 +202,37 @@ export function createHTMLRuntime(runtime,options={}){
197
202
  * Returns the controller so callers can use appendAndRegister(), setRoot() or disconnect().
198
203
  */
199
204
  export async function processHTML(runtime,options={}){
200
- const controller=createHTMLRuntime(runtime,options);
205
+ const {controller,created}=getOrCreateHTMLRuntime(runtime,options);
201
206
  try{
202
207
  await controller.process();
203
208
  return controller;
204
209
  }catch(error){
205
- await controller.disconnect();
210
+ if(created)await controller.disconnect();
206
211
  throw error;
207
212
  }
208
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;