browser-module-runtime 0.0.0-stage → 0.0.1

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.
@@ -0,0 +1,40 @@
1
+ function isNodeLike() {
2
+ return typeof process !== "undefined" && !!process.versions?.node;
3
+ }
4
+
5
+ export function createDefaultModuleUrlBackend() {
6
+ if (!isNodeLike() && typeof Blob !== "undefined" && typeof URL?.createObjectURL === "function") {
7
+ return createBlobModuleUrlBackend();
8
+ }
9
+
10
+ return createDataModuleUrlBackend();
11
+ }
12
+
13
+ export function createBlobModuleUrlBackend() {
14
+ return {
15
+ kind: "blob",
16
+ create(code) {
17
+ return URL.createObjectURL(new Blob([code], { type: "text/javascript" }));
18
+ },
19
+ revoke(url) {
20
+ URL.revokeObjectURL(url);
21
+ },
22
+ };
23
+ }
24
+
25
+ export function createDataModuleUrlBackend() {
26
+ let sequence = 0;
27
+
28
+ return {
29
+ kind: "data",
30
+ create(code) {
31
+ // Native ESM caches modules by URL. Recreating the same data: URL after
32
+ // invalidation would otherwise return the previously evaluated module.
33
+ // Give every backend create() call a fresh module identity, matching the
34
+ // naturally unique identity of URL.createObjectURL() in the blob backend.
35
+ const identity = ++sequence;
36
+ return `data:text/javascript;charset=utf-8,${encodeURIComponent(code)}#browser-module-runtime-${identity}`;
37
+ },
38
+ revoke() {},
39
+ };
40
+ }
@@ -0,0 +1 @@
1
+ export * from '../../html.js';
@@ -0,0 +1,48 @@
1
+ /** HTML controller with opt-in observation; not coupled to any UI framework. */
2
+ const registries=new WeakMap();
3
+
4
+ export function registerHTML(runtime,{name='default',root=globalThis.document,observe=false}={}) {
5
+ if(!root)throw new TypeError('registerHTML requires a DOM root');
6
+ const doc=root.ownerDocument||root;
7
+ let registry=registries.get(doc);
8
+ if(!registry){registry=new Map();registries.set(doc,registry);}
9
+ if(registry.has(name))throw new Error(`Runtime HTML name already registered: ${name}`);
10
+ registry.set(name,runtime);
11
+ const active=new Map();
12
+ let observer;
13
+ let alive=true;
14
+ let pending=Promise.resolve();
15
+ function isOwned(node) {return node.getAttribute('data-runtime')===name || node.getAttribute('runtime')===name || (!node.hasAttribute('data-runtime')&&!node.hasAttribute('runtime')&&name==='default');}
16
+ async function scan(){
17
+ if(!alive)return;
18
+ for(const el of root.querySelectorAll('script[type][module]')){
19
+ if(!isOwned(el)||el.dataset.runtimeRegistered==='true')continue;
20
+ const type=el.getAttribute('type');
21
+ if(!runtime.getSupportedTypes().includes(type))continue;
22
+ const id=el.getAttribute('module');
23
+ if(el.hasAttribute('data-src'))await runtime.loadSource(id,{type,url:el.getAttribute('data-src')});
24
+ else runtime.define(id,el.textContent,{type});
25
+ el.dataset.runtimeRegistered='true';
26
+ }
27
+ const targets=root.querySelectorAll('runtime-render');
28
+ for(const el of targets){
29
+ if(!isOwned(el)||active.has(el))continue;
30
+ let props={};
31
+ try {props=JSON.parse(el.getAttribute('props')||'{}');}catch(e){throw new Error(`Invalid props JSON in runtime-render: ${e.message}`);}
32
+ if(!props||typeof props!=='object'||Array.isArray(props))throw new Error('runtime-render props must be a JSON object');
33
+ const module=el.getAttribute('module');if(!module)continue;
34
+ const token={cancelled:false,handle:null};active.set(el,token);
35
+ try {token.handle=await runtime.mount({module,target:el,props,export:el.getAttribute('component')||'default'});if(token.cancelled)await token.handle.dispose();}
36
+ catch(e){active.delete(el);el.dispatchEvent(new CustomEvent('runtime-error',{detail:e}));throw e;}
37
+ }
38
+ for(const [el,token] of active){if(!el.isConnected||!root.contains(el)){active.delete(el);token.cancelled=true;await token.handle?.dispose();}}
39
+ }
40
+ function scheduleScan(){pending=pending.catch(()=>{}).then(scan);return pending;}
41
+ if(observe){observer=new MutationObserver(()=>scheduleScan());observer.observe(root,{subtree:true,childList:true});}
42
+ // Explicitly register this custom element only once per browser window.
43
+ const win=doc.defaultView;
44
+ if(win?.customElements&&!win.customElements.get('runtime-render')){
45
+ win.customElements.define('runtime-render',class extends win.HTMLElement {});
46
+ }
47
+ return {name,runtime,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);}};
48
+ }
package/src/index.d.ts ADDED
@@ -0,0 +1 @@
1
+ export * from '../index.js';
package/src/index.js ADDED
@@ -0,0 +1,93 @@
1
+ import {createRuntime as createModuleEngine} from './engine/runtime.js';
2
+ import {createBuildPipeline} from './build/pipeline.js';
3
+ import {createStyleManager} from './resources/styles.js';
4
+ import {mergePluginPackageEntries} from './plugins/package-entries.js';
5
+
6
+ /** Framework-neutral, synchronous factory. Async plugin setup is available on `.ready`. */
7
+ export function createRuntime(options={}) {
8
+ const plugins=options.plugins||[];
9
+ const artifactById=new Map();
10
+ const metadata=new Map();
11
+ const pipeline=createBuildPipeline({plugins,defaultType:options.defaultType||'javascript',onArtifact:(id,artifact)=>artifactById.set(id,artifact)});
12
+ const effectivePackages=mergePluginPackageEntries(options.packages,plugins);
13
+ const core=createModuleEngine({...options,packages:effectivePackages,compiler:pipeline});
14
+ const styles=createStyleManager();
15
+ const mounts=new Set();
16
+ let disposed=false;
17
+ const runtime={
18
+ ...core,
19
+ ready:null,
20
+ define(id,source,config={}) {
21
+ const type=pipeline.detectType(id,config.type||config.format);
22
+ metadata.set(id,{type});
23
+ return core.define(id,source,{...config,format:type});
24
+ },
25
+ defineMany(definitions) {
26
+ return definitions.map(({id,source,...opts})=>runtime.define(id,source,opts));
27
+ },
28
+ update(id,source,config={}) {
29
+ const type=pipeline.detectType(id,config.type||config.format||metadata.get(id)?.type);
30
+ metadata.set(id,{type});
31
+ return core.update(id,source,{...config,format:type});
32
+ },
33
+ remove(id,opts){metadata.delete(id);artifactById.delete(id);return core.remove(id,opts);},
34
+ clear(opts){const ids=core.clear(opts);for(const id of ids){metadata.delete(id);artifactById.delete(id);}return ids;},
35
+ getModuleInfo(id){const info=core.getModuleInfo(id);return info?{...info,type:metadata.get(id)?.type}:info;},
36
+ getBuildArtifact(id){return artifactById.get(id);},
37
+ getSourceType(id){return metadata.get(id)?.type;},
38
+ getSupportedTypes(){return [...pipeline.formats];},
39
+ async loadSource(id,{type,url=id}={}) {
40
+ await runtime.ready;
41
+ let source;
42
+ for(const plugin of plugins)if(plugin.load){source=await plugin.load({id,url,type}, {runtime});if(source!=null)break;}
43
+ if(source==null) {
44
+ const response=await fetch(url);
45
+ if(!response.ok)throw new Error(`Failed loading ${url}: HTTP ${response.status}`);
46
+ source=await response.text();
47
+ }
48
+ runtime.define(id, typeof source==='string'?source:source.code,{type});
49
+ return id;
50
+ },
51
+ async import(id){await runtime.ready;return core.import(id);},
52
+ async compile(id,opts){await runtime.ready;return core.compile(id,opts);},
53
+ async mount({module:id,export:exportName='default',target,props={}}) {
54
+ await runtime.ready;
55
+ if(typeof target==='string')target=document.querySelector(target);
56
+ if(!target)throw new TypeError(`Mount target not found: ${String(target)}`);
57
+ const type=metadata.get(id)?.type;
58
+ if(!type)throw new Error(`Module ${id} has no registered source type; define it before mounting.`);
59
+ const owner=pipeline.owners.get(type);
60
+ const adapter=plugins.find(p=>p.name===owner?.plugin);
61
+ const renderer=adapter?.renderers?.find(r=>(r.types||[]).includes(type));
62
+ if(!renderer)throw new Error(`No renderer registered for ${type}`);
63
+ const built=await runtime.compile(id);
64
+ const ns=await runtime.import(id);
65
+ if(!(exportName in ns))throw new Error(`Module ${id} does not export ${exportName}`);
66
+ const releases=[];
67
+ let handle;
68
+ try {
69
+ for(const asset of built.assets||[]) {
70
+ if(asset.type==='css')releases.push(styles.acquire({id:asset.id||`${id}:css`,content:asset.content},target.getRootNode?.()||target.ownerDocument));
71
+ }
72
+ handle=await renderer.mount({component:ns[exportName],target,props,runtime,id});
73
+ }catch(error){releases.forEach(fn=>fn());throw error;}
74
+ let closed=false;
75
+ const out={ready:Promise.resolve(),capabilities:{updateProps:typeof handle?.updateProps==='function'},
76
+ async updateProps(next){if(closed)throw new Error('Mount is disposed');if(typeof handle?.updateProps!=='function')throw new Error(`Renderer ${renderer.name||adapter.name} does not support updateProps`);return handle.updateProps(next);},
77
+ async dispose(){if(closed)return;closed=true;mounts.delete(out);try{await handle?.dispose?.();}finally{releases.forEach(fn=>fn());}}
78
+ };
79
+ mounts.add(out);
80
+ return out;
81
+ },
82
+ 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?.();}
83
+ };
84
+ runtime.ready=Promise.resolve().then(async()=>{for(const plugin of plugins)await plugin.configure?.({runtime,pipeline});});
85
+ return runtime;
86
+ }
87
+
88
+ export {analyzeImports} from './build/analyze.js';
89
+ export {BuildPhaseError,createBuildPipeline} from './build/pipeline.js';
90
+ export {createStyleManager} from './resources/styles.js';
91
+ export {createPackagePolicy,PackageVersionResolutionError,parsePackageSpecifier} from './engine/package-resolution/index.js';
92
+ export {createMemoryCompileCache,createLocalStorageCompileCache,createIndexedDBCompileCache} from './engine/compile-cache.js';
93
+ export {createDefaultModuleUrlBackend,createBlobModuleUrlBackend,createDataModuleUrlBackend} from './engine/url-backend.js';
@@ -0,0 +1,30 @@
1
+ /** Merge framework-owned package entries into the ordinary per-runtime policy.
2
+ * This is link-time configuration, not a browser import-map mutation. */
3
+ import {createPackagePolicy,PackageVersionResolutionError} from '../engine/package-resolution/index.js';
4
+
5
+ export function mergePluginPackageEntries(packages,plugins=[]) {
6
+ const source=packages??{};
7
+ const versions={...(source.versions??{})};
8
+ const entries={...(source.entries??{})};
9
+ for(const plugin of plugins) {
10
+ for(const [name,descriptor] of Object.entries(plugin.packageEntries??{})) {
11
+ const pluginPolicy=createPackagePolicy({entries:{[name]:descriptor}});
12
+ const provided=pluginPolicy.inspect(name);
13
+ if(Object.hasOwn(versions,name)||Object.hasOwn(entries,name)) {
14
+ const userPolicy=createPackagePolicy({
15
+ provider:source.provider,
16
+ ...(Object.hasOwn(versions,name)?{versions:{[name]:versions[name]}}:{entries:{[name]:entries[name]}}),
17
+ });
18
+ const selected=userPolicy.inspect(name);
19
+ if(selected?.url!==provided.url) {
20
+ throw new PackageVersionResolutionError('PACKAGE_VERSION_CONFLICT',
21
+ `Plugin ${JSON.stringify(plugin.name)} requires ${JSON.stringify(name)} from ${JSON.stringify(provided.url)}, but the runtime specifies ${JSON.stringify(selected?.url)}. Align the versions/providers instead of loading two framework runtimes.`,
22
+ {packageName:name,plugin:plugin.name,expected:provided.url,actual:selected?.url});
23
+ }
24
+ continue;
25
+ }
26
+ entries[name]=descriptor;
27
+ }
28
+ }
29
+ return {...source,versions,entries};
30
+ }
@@ -0,0 +1,18 @@
1
+ /** Per-resource-target stylesheet refcounts. Styles survive until final release. */
2
+ export function createStyleManager() {
3
+ const sheets = new Map();
4
+ function acquire({id,content},target = document) {
5
+ if(!target?.createElement && !target?.ownerDocument)throw new TypeError('A DOM document or shadow root is required for styles.');
6
+ const doc = target.ownerDocument || target;
7
+ const root = target.head || target;
8
+ let byTarget=sheets.get(root);if(!byTarget){byTarget=new Map();sheets.set(root,byTarget);}
9
+ const key=String(id);let entry=byTarget.get(key);
10
+ if(entry && entry.content!==content)throw new Error(`CSS asset conflict: ${key}`);
11
+ if(!entry){const element=doc.createElement('style');element.setAttribute('data-runtime-style',key);element.textContent=content;root.append(element);entry={element,count:0,content};byTarget.set(key,entry);}
12
+ entry.count++;
13
+ let released=false;
14
+ return ()=>{if(released)return;released=true;if(--entry.count===0){entry.element.remove();byTarget.delete(key);if(!byTarget.size)sheets.delete(root);}};
15
+ }
16
+ function dispose(){for(const records of sheets.values())for(const entry of records.values())entry.element.remove();sheets.clear();}
17
+ return { acquire,dispose,get size(){return [...sheets.values()].reduce((n,group)=>n+group.size,0);} };
18
+ }