@bakery-framework/plugin-vue 2.0.0-alpha.12 → 2.0.0-alpha.13

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": "@bakery-framework/plugin-vue",
3
- "version": "2.0.0-alpha.12",
3
+ "version": "2.0.0-alpha.13",
4
4
  "description": "Bakery vue plugin.",
5
5
  "keywords": [
6
6
  "bakery",
@@ -43,7 +43,7 @@
43
43
  "vue": "^3.5.38"
44
44
  },
45
45
  "dependencies": {
46
- "@bakery-framework/core": "^2.0.0-alpha.12"
46
+ "@bakery-framework/core": "^2.0.0-alpha.13"
47
47
  },
48
48
  "engines": {
49
49
  "bun": ">=1.4.0"
package/src/compile.ts CHANGED
@@ -14,17 +14,49 @@ import type {
14
14
  VuePluginOptions,
15
15
  } from './types'
16
16
 
17
- let compiler: typeof import('@vue/compiler-sfc')
17
+ /**
18
+ * The **promise**, not the module.
19
+ *
20
+ * `compiler` was assigned after the await, so two requests arriving before the
21
+ * first load finished each started their own `await import()`. Bun's module
22
+ * cache made that harmless rather than correct; memoising the promise is what
23
+ * actually makes it one load, and it is what lets `preloadCompiler()` below
24
+ * overlap with the rest of boot while a request that beats it simply awaits
25
+ * the same thing.
26
+ */
27
+ let compilerLoad: Promise<typeof import('@vue/compiler-sfc')> | null = null
18
28
 
19
29
  async function loadCompiler() {
20
- if (!compiler) {
21
- try {
22
- compiler = await import('@vue/compiler-sfc')
23
- } catch {
30
+ if (!compilerLoad) {
31
+ compilerLoad = import('@vue/compiler-sfc').catch(() => {
32
+ // Cleared so a later attempt can retry - a package installed while the
33
+ // dev server is running should not need a restart to be found.
34
+ compilerLoad = null
24
35
  throw new Error('compiler-sfc not available. Run `bun add vue`.')
25
- }
36
+ })
26
37
  }
27
- return compiler
38
+ return await compilerLoad
39
+ }
40
+
41
+ /**
42
+ * Start loading the compiler now rather than on the first `.vue` request.
43
+ *
44
+ * `@vue/compiler-sfc` is a large module: measured from this repo's example
45
+ * app, **168-173 ms warm and 1,848 ms on a cold filesystem**, and every server
46
+ * process paid it inside the first Vue page it served. Called from the
47
+ * plugin's `onStart`, where it overlaps with the rest of boot.
48
+ *
49
+ * Deliberately not awaited by the caller and deliberately not fatal. An app
50
+ * can register this plugin before it has written a single `.vue` file, and
51
+ * refusing to boot over a package it does not need yet would be worse than
52
+ * the clear error the first request already gets. The rejection is swallowed
53
+ * here and `loadCompiler` raises it properly when something actually needs
54
+ * the compiler.
55
+ */
56
+ export function preloadCompiler(): void {
57
+ void loadCompiler().catch(() => {
58
+ // See above: absence is reported at the point of use, not at boot.
59
+ })
28
60
  }
29
61
 
30
62
  let vuePluginOptions: VuePluginOptions = {}
@@ -182,8 +214,8 @@ export async function compileTemplateBlock(
182
214
  export async function compileStyleBlock(
183
215
  options: CompileStyleOptions,
184
216
  ): Promise<SFCStyleCompileResults> {
185
- await loadCompiler()
186
- return compiler.compileStyle({
217
+ const sfc = await loadCompiler()
218
+ return sfc.compileStyle({
187
219
  source: options.style.content,
188
220
  filename: 'style.css',
189
221
  id: options.id,
package/src/handler.ts CHANGED
@@ -173,18 +173,60 @@ function containsRouteFile(dir: string, exts: string[]): boolean {
173
173
  * intercepted — that is the spelling for linking a raw file out of a
174
174
  * catch-all's subtree, and what `docs/plugins/vue.md` prescribes.
175
175
  *
176
- * Computed per page request, so files added or removed in dev are seen on the
177
- * next load without cache ceremony.
176
+ * Computed per page request in development, so a file added or removed is
177
+ * seen on the next load without cache ceremony. **Memoised in production**,
178
+ * where it cannot change: there is no watcher, and the `SIGHUP` handler in
179
+ * `core/init.ts` is a deliberate no-op, so the only way the page tree changes
180
+ * is a restart.
181
+ *
182
+ * Worth the branch because the walk recurses into every sibling directory to
183
+ * ask whether it holds a route file, and it runs on **every** catch-all page
184
+ * request. Measured against directory trees of an app's shape, three rounds
185
+ * each with a CPU-bound control flat at 33 ms:
186
+ *
187
+ * 4 directories, depth 2, 10 claims 0.48 ms
188
+ * 12 directories, depth 3, 24 claims 1.24 ms
189
+ * 30 directories, depth 4, 50 claims 4.44 ms
190
+ *
191
+ * An `LRUCache` rather than a plain map, per convention 6. The key is a
192
+ * resolved disk path that routing produced, not anything a client sends, so
193
+ * it is bounded by the app's own catch-all count either way - but the bound
194
+ * is stated rather than argued.
195
+ */
196
+ const claimedCache = new LRUCache<
197
+ string,
198
+ { claimed: string[]; claimedSingle: boolean }
199
+ >(100)
200
+
201
+ /**
202
+ * Test seam (convention 9).
203
+ *
204
+ * **A test process reports `PROD === '1'`**, so this memo is *on* by default
205
+ * under `bun test` — a test that writes into a page directory between two
206
+ * calls is running against the production behaviour whether it meant to or
207
+ * not. Clear it between such calls, or ask for development explicitly with
208
+ * `withEnvFlag('PROD', false, …)`.
178
209
  */
210
+ export function __resetClaimedCache(): void {
211
+ claimedCache.clear()
212
+ }
213
+
179
214
  export function claimedBeside(catchAllFile: string): {
180
215
  claimed: string[]
181
216
  claimedSingle: boolean
182
217
  } {
218
+ const resolved = fs.resolve(catchAllFile)
219
+ const memoised = import.meta.env.PROD === '1'
220
+ if (memoised) {
221
+ const hit = claimedCache.get(resolved)
222
+ if (hit) return hit
223
+ }
224
+
183
225
  const claimed = new Set<string>()
184
226
  let claimedSingle = false
185
227
 
186
- const dir = fs.resolve(catchAllFile).replace(/\/[^/]*$/, '')
187
- const self = fs.resolve(catchAllFile).slice(dir.length + 1)
228
+ const dir = resolved.replace(/\/[^/]*$/, '')
229
+ const self = resolved.slice(dir.length + 1)
188
230
  // The handler's own table (`['vue']`), read at call time so the two cannot
189
231
  // drift apart.
190
232
  const exts = VueHandler.config.ext.map(ext => `.${ext}`)
@@ -195,8 +237,11 @@ export function claimedBeside(catchAllFile: string): {
195
237
  } catch {
196
238
  // Unreadable directory: no visible siblings means nothing extra claimed,
197
239
  // and a hard load still routes correctly — the stamp is an optimisation
198
- // of honesty, not the source of it.
199
- return { claimed: [], claimedSingle: false }
240
+ // of honesty, not the source of it. Remembered like any other answer, so
241
+ // an unreadable directory does not re-throw on every request.
242
+ const empty = { claimed: [], claimedSingle: false }
243
+ if (memoised) claimedCache.set(resolved, empty)
244
+ return empty
200
245
  }
201
246
 
202
247
  for (const entry of entries) {
@@ -223,7 +268,9 @@ export function claimedBeside(catchAllFile: string): {
223
268
  if (stem && stem !== name) claimed.add(stem)
224
269
  }
225
270
 
226
- return { claimed: [...claimed], claimedSingle }
271
+ const result = { claimed: [...claimed], claimedSingle }
272
+ if (memoised) claimedCache.set(resolved, result)
273
+ return result
227
274
  }
228
275
 
229
276
  export class VueHandler extends DynamicHandler {
package/src/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { definePlugin } from '@bakery-framework/core/plugins'
2
+ import { preloadCompiler } from './compile'
2
3
  import { setVuePluginOptions } from './compile'
3
4
  import type { VuePluginOptions } from './types'
4
5
  import { rewriteVueImports } from './utils'
@@ -48,6 +49,19 @@ export default function vuePlugin(options?: VuePluginOptions) {
48
49
  const { setupVue } = await import('./setup')
49
50
  await setupVue()
50
51
  },
52
+
53
+ /**
54
+ * Start loading `@vue/compiler-sfc` while the rest of boot runs.
55
+ *
56
+ * It is 168-173 ms warm and 1,848 ms on a cold filesystem, and without
57
+ * this every server process paid it inside the first Vue page it served.
58
+ * Not awaited: the load overlaps with everything else here, and a request
59
+ * that arrives before it finishes awaits the same promise rather than
60
+ * starting a second one.
61
+ */
62
+ onStart() {
63
+ preloadCompiler()
64
+ },
51
65
  onCompile(content, path) {
52
66
  if (
53
67
  !path.endsWith('.ts') &&