browser-runtime-vue 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.
package/README.md CHANGED
@@ -1,3 +1,25 @@
1
- # Temporary Holding Version
1
+ # browser-runtime-vue
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Vue 3 SFC compilation and rendering adapter for `browser-module-runtime`.
4
+
5
+ ```js
6
+ import {createRuntime} from 'browser-module-runtime';
7
+ import {vue} from 'browser-runtime-vue';
8
+
9
+ const runtime=createRuntime({plugins:[vue({version:'3.5.18'})]});
10
+ runtime.define('/Counter.vue',`<script setup>
11
+ import {ref} from 'vue';
12
+ const count=ref(0);
13
+ </script>
14
+ <template><button @click="count++">Count: {{count}}</button></template>
15
+ <style scoped>button { color: royalblue }</style>`,{type:'vue'});
16
+ const mount=await runtime.mount({module:'/Counter.vue',target:'#app'});
17
+ await mount.updateProps({initial:5});
18
+ await mount.dispose();
19
+ ```
20
+
21
+ The adapter loads `vue/compiler-sfc` lazily through the owning runtime's package resolver. Both compiler and renderer follow the same version pin. It supports normal JS `<script>`, `<script setup>`, `<template>`, ordinary and scoped CSS, and native reactive `updateProps()`. Stylesheet assets use the generic runtime's reference-counted style manager.
22
+
23
+ **Declarative HTML:** `<script type="vue" module="/Counter.vue">` accepts `<$script setup> ... </$script>` to avoid premature HTML parser termination; `<runtime-render>` and `custom-element="my-counter"` use the existing generic controller. External `.vue` and `runtime.define()` source use regular `<script>`.
24
+
25
+ Current limitations: TypeScript/preprocessor SFC blocks, CSS Modules, external `src` blocks, and Shadow DOM integration are not included in this first adapter. Vue CSS `v-bind()` and associated compiler options have not been browser-verified. CDN browser tests require network access and the package version to be published.
package/index.d.ts ADDED
@@ -0,0 +1 @@
1
+ export * from './src/index.js';
package/package.json CHANGED
@@ -1,6 +1,13 @@
1
1
  {
2
2
  "name": "browser-runtime-vue",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.0.1",
4
+ "type": "module",
5
+ "description": "Vue 3 SFC compiler and reactive renderer adapter for browser-module-runtime.",
6
+ "exports": {".": {"types": "./index.d.ts", "import": "./src/index.js"}},
7
+ "files": ["src", "index.d.ts", "README.md"],
8
+ "peerDependencies": {"browser-module-runtime": "0.0.3", "vue": "^3.5.0"},
9
+ "peerDependenciesMeta": {"vue": {"optional": true}},
10
+ "engines": {"node": ">=20"},
11
+ "publishConfig": {"access": "public"},
12
+ "keywords": ["vue", "sfc", "browser", "runtime", "compiler", "adapter"]
13
+ }
package/src/index.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ import type {RuntimePlugin} from 'browser-module-runtime';
2
+ export interface VueAdapterOptions {
3
+ compiler?: {parse(source:string,options?:Record<string,unknown>):any;compileScript(descriptor:any,options?:Record<string,unknown>):any;compileTemplate(options?:Record<string,unknown>):any;compileStyle(options?:Record<string,unknown>):any};
4
+ renderer?: {createApp(component:unknown):{mount(target:Element):unknown;unmount():unknown};h(component:unknown,props:Record<string,unknown>):unknown;reactive<T extends object>(value:T):T};
5
+ version?:string;
6
+ provider?:string;
7
+ scriptOptions?:Record<string,unknown>;
8
+ templateOptions?:Record<string,unknown>;
9
+ styleOptions?:Record<string,unknown>;
10
+ }
11
+ export declare function vue(options?:VueAdapterOptions):RuntimePlugin;
12
+ export declare function normalizeInlineVue(source:string):string;
13
+ export declare function compileVueSfc(compiler:NonNullable<VueAdapterOptions['compiler']>,source:string,filename:string,options?:Pick<VueAdapterOptions,'scriptOptions'|'templateOptions'|'styleOptions'>):{code:string;assets:Array<{type:'css';id:string;content:string}>;diagnostics:unknown[]};
package/src/index.js ADDED
@@ -0,0 +1,103 @@
1
+ import {normalizeInlineVue} from './inline-source.js';
2
+ export {normalizeInlineVue} from './inline-source.js';
3
+
4
+ /** Vue 3 Single-File Component compiler and renderer adapter.
5
+ * Uses Vue's own compiler-sfc, pinned to the same release as the renderer.
6
+ */
7
+ export function vue({compiler,renderer,version='3.5.18',provider='esm.sh',scriptOptions={},templateOptions={},styleOptions={}}={}) {
8
+ let sfcCompiler=compiler;
9
+ let vueRuntime=renderer;
10
+ let ownerRuntime;
11
+ const packageEntries=/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(String(version))
12
+ ? {vue:{provider,version:String(version)}} : {};
13
+ return {
14
+ name:'vue',version:String(version),packageEntries,
15
+ html:{normalizeInlineSource:normalizeInlineVue},
16
+ configure({runtime}){ownerRuntime=runtime;},
17
+ compilers:[{
18
+ types:['vue'],extensions:['.vue'],
19
+ async compile(source,{id}){
20
+ sfcCompiler ||= await (ownerRuntime ? ownerRuntime.import('vue/compiler-sfc') : import('vue/compiler-sfc'));
21
+ return compileVueSfc(sfcCompiler,source,id,{scriptOptions,templateOptions,styleOptions});
22
+ }
23
+ }],
24
+ renderers:[{
25
+ name:'vue',types:['vue'],
26
+ async mount({component,target,props}){
27
+ vueRuntime ||= await (ownerRuntime ? ownerRuntime.import('vue') : import('vue'));
28
+ if(typeof vueRuntime.createApp!=='function'||typeof vueRuntime.h!=='function'||typeof vueRuntime.reactive!=='function')
29
+ throw new TypeError('Vue renderer requires createApp(), h(), and reactive().');
30
+ // Parent wrapper keeps the component mounted when props change.
31
+ // Spreading state inside render tracks every property and allows additions/removals.
32
+ const state=vueRuntime.reactive({...props});
33
+ const wrapper={name:'BrowserRuntimeVueHost',render(){return vueRuntime.h(component,{...state});}};
34
+ const app=vueRuntime.createApp(wrapper);
35
+ app.mount(target);
36
+ let disposed=false;
37
+ return {
38
+ updateProps(next){
39
+ if(disposed)throw new Error('Vue mount has been disposed');
40
+ if(!next||typeof next!=='object'||Array.isArray(next))throw new TypeError('Vue props must be a plain object');
41
+ for(const key of Object.keys(state))if(!Object.hasOwn(next,key))delete state[key];
42
+ Object.assign(state,next);
43
+ },
44
+ dispose(){if(!disposed){disposed=true;app.unmount();}}
45
+ };
46
+ }
47
+ }]
48
+ };
49
+ }
50
+
51
+ /** Pure transform. Public for deterministic tests and non-DOM build tooling. */
52
+ export function compileVueSfc(compiler,source,filename,{scriptOptions={},templateOptions={},styleOptions={}}={}) {
53
+ if(!compiler||['parse','compileScript','compileTemplate','compileStyle'].some(key=>typeof compiler[key]!=='function'))
54
+ throw new TypeError('Vue compiler must expose parse(), compileScript(), compileTemplate() and compileStyle().');
55
+ const parsed=compiler.parse(source,{filename});
56
+ if(parsed.errors?.length)throw new Error(`Vue SFC parse failed: ${formatErrors(parsed.errors)}`);
57
+ const descriptor=parsed.descriptor;
58
+ if(!descriptor)throw new TypeError('Vue compiler parse() must return an SFC descriptor');
59
+ if(descriptor.template?.src||descriptor.script?.src||descriptor.scriptSetup?.src||descriptor.styles?.some(s=>s.src))
60
+ throw new Error(`External SFC block src attributes are not supported in ${filename}; use an inline block.`);
61
+ if(descriptor.styles?.some(s=>s.module))throw new Error(`Vue CSS Modules are not yet supported in ${filename}`);
62
+ if([descriptor.script,descriptor.scriptSetup].some(block=>block?.lang && !['js','javascript'].includes(block.lang)))
63
+ throw new Error(`Vue SFC script languages other than JavaScript require an additional transform: ${filename}`);
64
+ if(descriptor.template?.lang)throw new Error(`Vue SFC template preprocessors are not supported: ${filename}`);
65
+ if(descriptor.styles?.some(s=>s.lang && s.lang!=='css'))throw new Error(`Vue SFC style preprocessors are not supported: ${filename}`);
66
+ const hasScoped=Boolean(descriptor.styles?.some(s=>s.scoped));
67
+ const scope='data-v-'+shortHash(filename+'\0'+source);
68
+ const variable='__bmr_vue_sfc__';
69
+ const hasScript=Boolean(descriptor.script||descriptor.scriptSetup);
70
+ const inlineTemplate=Boolean(descriptor.scriptSetup&&descriptor.template);
71
+ const compiledScript=hasScript ? compiler.compileScript(descriptor,{
72
+ ...scriptOptions,id:scope,inlineTemplate,
73
+ genDefaultAs:variable,
74
+ ...(inlineTemplate?{templateOptions:{...templateOptions}}:{})
75
+ }) : null;
76
+ let code=compiledScript?.content||`const ${variable} = {};`;
77
+ if(!inlineTemplate && descriptor.template){
78
+ const compiledTemplate=compiler.compileTemplate({
79
+ filename,id:scope,source:descriptor.template.content,
80
+ scoped:hasScoped, ...templateOptions,
81
+ compilerOptions:{...(templateOptions.compilerOptions||{}),bindingMetadata:compiledScript?.bindings}
82
+ });
83
+ if(compiledTemplate.errors?.length)throw new Error(`Vue SFC template failed: ${formatErrors(compiledTemplate.errors)}`);
84
+ // compileTemplate emits `export function render` + imports from the Vue runtime.
85
+ code+='\n'+compiledTemplate.code+'\n'+variable+'.render = render;';
86
+ }
87
+ if(hasScoped)code+='\n'+variable+'.__scopeId = '+JSON.stringify(scope)+';';
88
+ code+='\nexport default '+variable+';';
89
+ const assets=[];
90
+ for(const [index,style] of (descriptor.styles||[]).entries()){
91
+ const result=compiler.compileStyle({filename,id:scope,source:style.content,scoped:Boolean(style.scoped),...styleOptions});
92
+ if(result.errors?.length)throw new Error(`Vue SFC style #${index} failed: ${formatErrors(result.errors)}`);
93
+ if(result.code)assets.push({type:'css',id:`${filename}:vue-css:${scope}:${index}`,content:result.code});
94
+ }
95
+ return {code,assets,diagnostics:[]};
96
+ }
97
+
98
+ function shortHash(value){
99
+ let hash=2166136261;
100
+ for(let i=0;i<value.length;i++){hash^=value.charCodeAt(i);hash=Math.imul(hash,16777619);}
101
+ return (hash>>>0).toString(36);
102
+ }
103
+ function formatErrors(errors){return errors.map(err=>typeof err==='string'?err:err.message||String(err)).join('; ');}
@@ -0,0 +1,76 @@
1
+ /** Normalize HTML-safe Vue <$script> delimiters without altering normal Vue source.
2
+ * Replacement is same-width (the '$' becomes a space), preserving source positions.
3
+ * Only call for inline HTML declarations, never for external files or runtime.define().
4
+ */
5
+ function skipString(source, position) {
6
+ const quote = source[position];
7
+ let i = position + 1;
8
+ for (; i < source.length; i++) {
9
+ if (source[i] === '\\') { i++; continue; }
10
+ if (source[i] === quote) return i + 1;
11
+ }
12
+ return i;
13
+ }
14
+ function skipJsToken(source, i) {
15
+ if (source[i] === '"' || source[i] === "'" || source[i] === '`') return skipString(source, i);
16
+ if (source.startsWith('//', i)) { const end = source.indexOf('\n', i + 2); return end < 0 ? source.length : end; }
17
+ if (source.startsWith('/*', i)) { const end = source.indexOf('*/', i + 2); return end < 0 ? source.length : end + 2; }
18
+ return i;
19
+ }
20
+ function tagEnd(source, start) {
21
+ let i = start;
22
+ while (i < source.length) {
23
+ const skipped = skipJsToken(source, i);
24
+ if (skipped !== i) { i = skipped; continue; }
25
+ if (source[i] === '>') return i + 1;
26
+ i++;
27
+ }
28
+ throw new SyntaxError('Unclosed <$script> opening tag in inline Vue source');
29
+ }
30
+ export function normalizeInlineVue(source) {
31
+ if (typeof source !== 'string') throw new TypeError('Inline Vue source must be a string');
32
+ let result = '';
33
+ let cursor = 0;
34
+ let i = 0;
35
+ while (i < source.length) {
36
+ if (source.startsWith('<!--', i)) { const end = source.indexOf('-->', i + 4); i = end < 0 ? source.length : end + 3; continue; }
37
+ // An HTML tag's attribute string must not be mistaken for a script delimiter.
38
+ if (source.startsWith('<style', i) && /[\s>]/.test(source[i + 6] || '>')) {
39
+ const end = source.indexOf('</style>', tagEnd(source, i));
40
+ i = end < 0 ? source.length : end + '</style>'.length; continue;
41
+ }
42
+ if (source.startsWith('<$script', i) && /[\s>]/.test(source[i + 8] || '>')) {
43
+ const openingEnd = tagEnd(source, i + 8);
44
+ let closing = openingEnd;
45
+ while (closing < source.length) {
46
+ const skipped = skipJsToken(source, closing);
47
+ if (skipped !== closing) { closing = skipped; continue; }
48
+ if (source.startsWith('</$script', closing) && /[\s>]/.test(source[closing + 9] || '>')) break;
49
+ closing++;
50
+ }
51
+ if (closing >= source.length) throw new SyntaxError('Missing </$script> closing tag in inline Vue source');
52
+ const closingEnd = tagEnd(source, closing + 9);
53
+ result += source.slice(cursor, i) + '<script ' + source.slice(i + 8, closing) + '</script ' + source.slice(closing + 9, closingEnd);
54
+ i = closingEnd;
55
+ cursor = i;
56
+ continue;
57
+ }
58
+ if (source[i] === '<') {
59
+ const maybeTag = /^<\/?[A-Za-z][\w:-]*/.exec(source.slice(i));
60
+ if (maybeTag) { i = tagEnd(source, i + maybeTag[0].length); continue; }
61
+ }
62
+ if (source[i] === '{') {
63
+ let depth = 1; let j = i + 1;
64
+ while (j < source.length && depth > 0) {
65
+ const skipped = skipJsToken(source, j);
66
+ if (skipped !== j) { j = skipped; continue; }
67
+ if (source[j] === '{') depth++;
68
+ if (source[j] === '}') depth--;
69
+ j++;
70
+ }
71
+ i = j; continue;
72
+ }
73
+ i++;
74
+ }
75
+ return result + source.slice(cursor);
76
+ }