@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 +1 -1
- package/src/compiler-sfc-esm-browser.d.ts +4 -0
- package/src/index.ts +81 -65
- package/tsconfig.json +1 -0
package/package.json
CHANGED
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
|
-
|
|
9
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
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:
|
|
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
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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,
|