@hyperspan/plugin-vue 1.0.0 → 1.0.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperspan/plugin-vue",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Hyperspan Plugin for Vue.js Client Islands",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -0,0 +1,4 @@
1
+ /** Bun-safe entry: same runtime API as `@vue/compiler-sfc`, typings from the package root. */
2
+ declare module '@vue/compiler-sfc/dist/compiler-sfc.esm-browser.js' {
3
+ export * from '@vue/compiler-sfc';
4
+ }
package/src/index.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import './types.d';
1
2
  import { JS_IMPORT_MAP, JS_ISLAND_PUBLIC_PATH } from '@hyperspan/framework/client/js';
2
3
  import { assetHash } from '@hyperspan/framework/utils';
3
4
  import { IS_PROD } from '@hyperspan/framework/server';
@@ -5,11 +6,39 @@ import { join, resolve } from 'node:path';
5
6
  import type { Hyperspan as HS } from '@hyperspan/framework';
6
7
  import { html } from '@hyperspan/html';
7
8
  import debug from 'debug';
8
- import { parse, compileScript, compileTemplate, rewriteDefault } from '@vue/compiler-sfc';
9
- import './types.d';
9
+ // Use the ESM browser build: the Node CJS build inlines consolidate and static `require()`
10
+ // calls for optional engines (velocityjs, pug, etc.). Bun resolves those at bundle time and
11
+ // errors unless every optional package is installed. The browser bundle has the same SFC API
12
+ // without those preprocessors — fine for compile-from-source string workflows.
13
+ import { parse, compileScript, compileTemplate, rewriteDefault } from '@vue/compiler-sfc/dist/compiler-sfc.esm-browser.js';
10
14
 
11
15
  const log = debug('hyperspan:plugin-vue');
12
16
 
17
+ /** Dev: stable `[name].js` via Bun default. Prod: hashed filenames for caching. */
18
+ const ISLAND_JS_NAMING = IS_PROD ? '[dir]/[name]-[hash].[ext]' : undefined;
19
+
20
+ function islandBundleBaseName(outputPath: string): string {
21
+ return String(outputPath.split('/').reverse()[0]!.replace(/\.js$/i, ''));
22
+ }
23
+
24
+ /** Prefer the real entry artifact — `outputs[0]` is often a shared chunk, not the .vue entry. */
25
+ function pickEntryPointJsOutput(
26
+ outputs: ReadonlyArray<{ path: string; kind?: string }>,
27
+ entrySourcePath: string
28
+ ): { path: string } {
29
+ const js = outputs.filter((o) => o.path.endsWith('.js'));
30
+ const entry = js.find((o) => o.kind === 'entry-point');
31
+ if (entry) return entry;
32
+ const sourceBase = entrySourcePath.split('/').pop()!.replace(/\.(vue|ts)$/i, '');
33
+ const byName = js.find((o) => {
34
+ const b = islandBundleBaseName(o.path);
35
+ return b === sourceBase || b.startsWith(`${sourceBase}-`);
36
+ });
37
+ if (byName) return byName;
38
+ if (js[0]) return js[0];
39
+ throw new Error('[Hyperspan] Vue island client build produced no JS output');
40
+ }
41
+
13
42
  /**
14
43
  * Build the island wrapper HTML: a div for SSR content + a module script tag for client hydration.
15
44
  * Exported so it can be imported by generated island module code and used directly in tests.
@@ -40,7 +69,9 @@ export async function renderVueSSR(Component: any, props: any = {}): Promise<str
40
69
  return renderToString(app);
41
70
  }
42
71
 
43
- const VUE_ISLAND_CACHE = new Map<string, string>();
72
+ type VueIslandCacheEntry = { contents: string; esmName: string };
73
+
74
+ const VUE_ISLAND_CACHE = new Map<string, VueIslandCacheEntry>();
44
75
 
45
76
  /**
46
77
  * Compile a Vue SFC to JavaScript using @vue/compiler-sfc.
@@ -79,6 +110,7 @@ async function compileVueSFC(
79
110
  id,
80
111
  ssr,
81
112
  scoped: descriptor.styles.some((s) => s.scoped),
113
+ ssrCssVars: [],
82
114
  // Pass binding metadata so the template compiler knows which vars are
83
115
  // <script setup> bindings and generates $setup.x refs instead of _ctx.x
84
116
  compilerOptions: bindingMetadata ? { bindingMetadata } : undefined,
@@ -103,16 +135,6 @@ async function compileVueSFC(
103
135
  */
104
136
  async function copyVueToPublicFolder(config: HS.Config) {
105
137
  const outdir = join('./', config.publicDir, JS_ISLAND_PUBLIC_PATH);
106
- const devOutputFile = join(outdir, 'vue-client.js');
107
-
108
- // In dev mode, skip the build if the file already exists to avoid
109
- // conflicts with bun --watch (building vue triggers watch restarts)
110
- if (!IS_PROD && (await Bun.file(devOutputFile).exists())) {
111
- const builtFilePath = `${JS_ISLAND_PUBLIC_PATH}/vue-client.js`;
112
- JS_IMPORT_MAP.set('vue', builtFilePath);
113
- JS_IMPORT_MAP.set('vue/dist/vue.esm-bundler.js', builtFilePath);
114
- return;
115
- }
116
138
 
117
139
  const currentNodeEnv = process.env.NODE_ENV || 'production';
118
140
  const sourceFile = resolve(__dirname, './vue-client.ts');
@@ -122,7 +144,7 @@ async function copyVueToPublicFolder(config: HS.Config) {
122
144
  const result = await Bun.build({
123
145
  entrypoints: [sourceFile],
124
146
  outdir,
125
- naming: IS_PROD ? '[dir]/[name]-[hash].[ext]' : undefined,
147
+ naming: ISLAND_JS_NAMING,
126
148
  minify: true,
127
149
  format: 'esm',
128
150
  target: 'browser',
@@ -134,7 +156,8 @@ async function copyVueToPublicFolder(config: HS.Config) {
134
156
  });
135
157
  process.env.NODE_ENV = currentNodeEnv;
136
158
 
137
- const builtFileName = String(result.outputs[0].path.split('/').reverse()[0]).replace('.js', '');
159
+ const vueClientEntry = pickEntryPointJsOutput(result.outputs, sourceFile);
160
+ const builtFileName = islandBundleBaseName(vueClientEntry.path);
138
161
  const builtFilePath = `${JS_ISLAND_PUBLIC_PATH}/${builtFileName}.js`;
139
162
 
140
163
  JS_IMPORT_MAP.set('vue', builtFilePath);
@@ -162,11 +185,18 @@ export function vuePlugin(): HS.Plugin {
162
185
  log('vue file loaded', args.path);
163
186
  const jsId = assetHash(args.path);
164
187
 
188
+ if (!JS_IMPORT_MAP.has('vue')) {
189
+ await copyVueToPublicFolder(config);
190
+ }
191
+
165
192
  // Cache: Avoid re-processing the same file
166
193
  if (VUE_ISLAND_CACHE.has(jsId)) {
194
+ const hit = VUE_ISLAND_CACHE.get(jsId)!;
195
+ // Re-register: JS_IMPORT_MAP may have been reset (e.g. module reload) while this cache survives.
196
+ JS_IMPORT_MAP.set(hit.esmName, `${JS_ISLAND_PUBLIC_PATH}/${hit.esmName}.js`);
167
197
  log('vue file cached', args.path);
168
198
  return {
169
- contents: VUE_ISLAND_CACHE.get(jsId) || '',
199
+ contents: hit.contents,
170
200
  loader: 'js',
171
201
  };
172
202
  }
@@ -179,55 +209,41 @@ export function vuePlugin(): HS.Plugin {
179
209
  const ssrCode = await compileVueSFC(source, args.path, jsId, true);
180
210
 
181
211
  const outdir = join('./', config.publicDir, JS_ISLAND_PUBLIC_PATH);
182
- const baseName = args.path.split('/').pop()!.replace('.vue', '');
183
-
184
- let esmName: string;
185
- if (!IS_PROD) {
186
- // In dev mode, write compiled JS directly — avoids nested Bun.build() inside
187
- // Bun.plugin() onLoad which causes EISDIR conflicts under bun --watch.
188
- // The browser import map resolves 'vue' to vue-client.js.
189
- const clientCode = await compileVueSFC(source, args.path, jsId, false);
190
- await Bun.write(join(outdir, `${baseName}.js`), clientCode);
191
- esmName = baseName;
192
- } else {
193
- // Production: full bundle with tree-shaking and minification
194
- const vueClientPlugin = {
195
- name: 'vue-client-compiler',
196
- setup(clientBuild: any) {
197
- clientBuild.onLoad(
198
- { filter: /\.vue$/ },
199
- async ({ path }: { path: string }) => {
200
- const src = await Bun.file(path).text();
201
- const clientId = assetHash(path);
202
- const compiledCode = await compileVueSFC(src, path, clientId, false);
203
- return { contents: compiledCode, loader: 'js' };
204
- }
205
- );
206
- },
207
- };
208
212
 
209
- const clientResult = await Bun.build({
210
- entrypoints: [args.path],
211
- outdir,
212
- naming: '[dir]/[name]-[hash].[ext]',
213
- external: Array.from(JS_IMPORT_MAP.keys()),
214
- minify: true,
215
- format: 'esm',
216
- target: 'browser',
217
- plugins: [vueClientPlugin],
218
- env: 'APP_PUBLIC_*',
219
- define: {
220
- __VUE_OPTIONS_API__: 'true',
221
- __VUE_PROD_DEVTOOLS__: 'false',
222
- __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: 'false',
223
- },
224
- });
225
-
226
- esmName = String(clientResult.outputs[0].path.split('/').reverse()[0]).replace(
227
- '.js',
228
- ''
229
- );
230
- }
213
+ const vueClientPlugin = {
214
+ name: 'vue-client-compiler',
215
+ setup(clientBuild: any) {
216
+ clientBuild.onLoad(
217
+ { filter: /\.vue$/ },
218
+ async ({ path }: { path: string }) => {
219
+ const src = await Bun.file(path).text();
220
+ const clientId = assetHash(path);
221
+ const compiledCode = await compileVueSFC(src, path, clientId, false);
222
+ return { contents: compiledCode, loader: 'js' };
223
+ }
224
+ );
225
+ },
226
+ };
227
+
228
+ const clientResult = await Bun.build({
229
+ entrypoints: [args.path],
230
+ outdir,
231
+ naming: ISLAND_JS_NAMING,
232
+ external: Array.from(JS_IMPORT_MAP.keys()),
233
+ minify: true,
234
+ format: 'esm',
235
+ target: 'browser',
236
+ plugins: [vueClientPlugin],
237
+ env: 'APP_PUBLIC_*',
238
+ define: {
239
+ __VUE_OPTIONS_API__: 'true',
240
+ __VUE_PROD_DEVTOOLS__: 'false',
241
+ __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: 'false',
242
+ },
243
+ });
244
+
245
+ const entryOut = pickEntryPointJsOutput(clientResult.outputs, args.path);
246
+ const esmName = islandBundleBaseName(entryOut.path);
231
247
 
232
248
  JS_IMPORT_MAP.set(esmName, `${JS_ISLAND_PUBLIC_PATH}/${esmName}.js`);
233
249
  log('added to import map', esmName, `${JS_ISLAND_PUBLIC_PATH}/${esmName}.js`);
@@ -279,7 +295,7 @@ ${componentName}.__HS_ISLAND = {
279
295
  };
280
296
  `;
281
297
 
282
- VUE_ISLAND_CACHE.set(jsId, moduleCode);
298
+ VUE_ISLAND_CACHE.set(jsId, { contents: moduleCode, esmName });
283
299
 
284
300
  return {
285
301
  contents: moduleCode,
package/tsconfig.json CHANGED
@@ -1,5 +1,6 @@
1
1
  {
2
2
  "compileOnSave": true,
3
+ "include": ["src/**/*.ts", "src/**/*.d.ts"],
3
4
  "compilerOptions": {
4
5
  "rootDir": "src",
5
6
  "outDir": "dist",