@hyperspan/plugin-vue 1.0.1 → 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 +80 -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.
|
|
@@ -104,16 +135,6 @@ async function compileVueSFC(
|
|
|
104
135
|
*/
|
|
105
136
|
async function copyVueToPublicFolder(config: HS.Config) {
|
|
106
137
|
const outdir = join('./', config.publicDir, JS_ISLAND_PUBLIC_PATH);
|
|
107
|
-
const devOutputFile = join(outdir, 'vue-client.js');
|
|
108
|
-
|
|
109
|
-
// In dev mode, skip the build if the file already exists to avoid
|
|
110
|
-
// conflicts with bun --watch (building vue triggers watch restarts)
|
|
111
|
-
if (!IS_PROD && (await Bun.file(devOutputFile).exists())) {
|
|
112
|
-
const builtFilePath = `${JS_ISLAND_PUBLIC_PATH}/vue-client.js`;
|
|
113
|
-
JS_IMPORT_MAP.set('vue', builtFilePath);
|
|
114
|
-
JS_IMPORT_MAP.set('vue/dist/vue.esm-bundler.js', builtFilePath);
|
|
115
|
-
return;
|
|
116
|
-
}
|
|
117
138
|
|
|
118
139
|
const currentNodeEnv = process.env.NODE_ENV || 'production';
|
|
119
140
|
const sourceFile = resolve(__dirname, './vue-client.ts');
|
|
@@ -123,7 +144,7 @@ async function copyVueToPublicFolder(config: HS.Config) {
|
|
|
123
144
|
const result = await Bun.build({
|
|
124
145
|
entrypoints: [sourceFile],
|
|
125
146
|
outdir,
|
|
126
|
-
naming:
|
|
147
|
+
naming: ISLAND_JS_NAMING,
|
|
127
148
|
minify: true,
|
|
128
149
|
format: 'esm',
|
|
129
150
|
target: 'browser',
|
|
@@ -135,7 +156,8 @@ async function copyVueToPublicFolder(config: HS.Config) {
|
|
|
135
156
|
});
|
|
136
157
|
process.env.NODE_ENV = currentNodeEnv;
|
|
137
158
|
|
|
138
|
-
const
|
|
159
|
+
const vueClientEntry = pickEntryPointJsOutput(result.outputs, sourceFile);
|
|
160
|
+
const builtFileName = islandBundleBaseName(vueClientEntry.path);
|
|
139
161
|
const builtFilePath = `${JS_ISLAND_PUBLIC_PATH}/${builtFileName}.js`;
|
|
140
162
|
|
|
141
163
|
JS_IMPORT_MAP.set('vue', builtFilePath);
|
|
@@ -163,11 +185,18 @@ export function vuePlugin(): HS.Plugin {
|
|
|
163
185
|
log('vue file loaded', args.path);
|
|
164
186
|
const jsId = assetHash(args.path);
|
|
165
187
|
|
|
188
|
+
if (!JS_IMPORT_MAP.has('vue')) {
|
|
189
|
+
await copyVueToPublicFolder(config);
|
|
190
|
+
}
|
|
191
|
+
|
|
166
192
|
// Cache: Avoid re-processing the same file
|
|
167
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`);
|
|
168
197
|
log('vue file cached', args.path);
|
|
169
198
|
return {
|
|
170
|
-
contents:
|
|
199
|
+
contents: hit.contents,
|
|
171
200
|
loader: 'js',
|
|
172
201
|
};
|
|
173
202
|
}
|
|
@@ -180,55 +209,41 @@ export function vuePlugin(): HS.Plugin {
|
|
|
180
209
|
const ssrCode = await compileVueSFC(source, args.path, jsId, true);
|
|
181
210
|
|
|
182
211
|
const outdir = join('./', config.publicDir, JS_ISLAND_PUBLIC_PATH);
|
|
183
|
-
const baseName = args.path.split('/').pop()!.replace('.vue', '');
|
|
184
|
-
|
|
185
|
-
let esmName: string;
|
|
186
|
-
if (!IS_PROD) {
|
|
187
|
-
// In dev mode, write compiled JS directly — avoids nested Bun.build() inside
|
|
188
|
-
// Bun.plugin() onLoad which causes EISDIR conflicts under bun --watch.
|
|
189
|
-
// The browser import map resolves 'vue' to vue-client.js.
|
|
190
|
-
const clientCode = await compileVueSFC(source, args.path, jsId, false);
|
|
191
|
-
await Bun.write(join(outdir, `${baseName}.js`), clientCode);
|
|
192
|
-
esmName = baseName;
|
|
193
|
-
} else {
|
|
194
|
-
// Production: full bundle with tree-shaking and minification
|
|
195
|
-
const vueClientPlugin = {
|
|
196
|
-
name: 'vue-client-compiler',
|
|
197
|
-
setup(clientBuild: any) {
|
|
198
|
-
clientBuild.onLoad(
|
|
199
|
-
{ filter: /\.vue$/ },
|
|
200
|
-
async ({ path }: { path: string }) => {
|
|
201
|
-
const src = await Bun.file(path).text();
|
|
202
|
-
const clientId = assetHash(path);
|
|
203
|
-
const compiledCode = await compileVueSFC(src, path, clientId, false);
|
|
204
|
-
return { contents: compiledCode, loader: 'js' };
|
|
205
|
-
}
|
|
206
|
-
);
|
|
207
|
-
},
|
|
208
|
-
};
|
|
209
212
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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);
|
|
232
247
|
|
|
233
248
|
JS_IMPORT_MAP.set(esmName, `${JS_ISLAND_PUBLIC_PATH}/${esmName}.js`);
|
|
234
249
|
log('added to import map', esmName, `${JS_ISLAND_PUBLIC_PATH}/${esmName}.js`);
|
|
@@ -280,7 +295,7 @@ ${componentName}.__HS_ISLAND = {
|
|
|
280
295
|
};
|
|
281
296
|
`;
|
|
282
297
|
|
|
283
|
-
VUE_ISLAND_CACHE.set(jsId, moduleCode);
|
|
298
|
+
VUE_ISLAND_CACHE.set(jsId, { contents: moduleCode, esmName });
|
|
284
299
|
|
|
285
300
|
return {
|
|
286
301
|
contents: moduleCode,
|