@bakery-framework/plugin-vue 2.0.0-alpha.9 → 2.0.0-rc.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/README.md CHANGED
@@ -7,7 +7,7 @@ Vue single-file components for
7
7
  bun add @bakery-framework/plugin-vue vue
8
8
  ```
9
9
 
10
- `vue` and `@vue/compiler-sfc` are peer dependencies — installing `vue` brings
10
+ `vue` and `@vue/compiler-sfc` are peer dependencies: installing `vue` brings
11
11
  the compiler with it.
12
12
 
13
13
  ## Usage
@@ -28,11 +28,11 @@ Options are typed as `VuePluginOptions`.
28
28
 
29
29
  ## License
30
30
 
31
- MIT with the Commons Clause v1.0 — see [LICENSE](./LICENSE).
31
+ MIT with the Commons Clause v1.0. See [LICENSE](./LICENSE).
32
32
 
33
- **Not an OSI-approved licence.** The Commons Clause removes the right to *sell*
34
- the software — meaning to charge for a product or service whose value derives
33
+ **Not an OSI-approved license.** The Commons Clause removes the right to *sell*
34
+ the software: meaning to charge for a product or service whose value derives
35
35
  substantially from it, hosting and support included. Everything else the MIT
36
- licence grants is unchanged: use it, modify it, ship it inside your own product.
37
- If your organisation only permits OSI-approved dependencies, this will not pass
36
+ license grants is unchanged: use it, modify it, ship it inside your own product.
37
+ If your organization only permits OSI-approved dependencies, this will not pass
38
38
  that check.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bakery-framework/plugin-vue",
3
- "version": "2.0.0-alpha.9",
3
+ "version": "2.0.0-rc.2",
4
4
  "description": "Bakery vue plugin.",
5
5
  "keywords": [
6
6
  "bakery",
@@ -43,9 +43,9 @@
43
43
  "vue": "^3.5.38"
44
44
  },
45
45
  "dependencies": {
46
- "@bakery-framework/core": "^2.0.0-alpha.9"
46
+ "@bakery-framework/core": "^2.0.0-rc.2"
47
47
  },
48
48
  "engines": {
49
- "bun": ">=1.3.14"
49
+ "bun": ">=1.4.0"
50
50
  }
51
51
  }
package/src/actions.ts CHANGED
@@ -49,8 +49,8 @@ export function validateActionRequest(req: Request, url: URL): Response | null {
49
49
  /**
50
50
  * Gate the component a `__vue_action` request will actually execute.
51
51
  *
52
- * `__vue_file` replaces the whole execution target — id, path and server
53
- * script — so every check that ran against the *route's* descriptor stopped
52
+ * `__vue_file` replaces the whole execution target (id, path and server
53
+ * script), so every check that ran against the *route's* descriptor stopped
54
54
  * describing the code that runs. The route was gated; the target was not, and
55
55
  * `POST /any-public-page?__vue_action=x&__vue_file=admin/Panel.vue` ran
56
56
  * `admin/Panel.vue`'s top-level server code with nothing to justify it.
@@ -65,7 +65,7 @@ export function validateActionTarget(
65
65
  ): Response | null {
66
66
  // A `page-only` component is a page: reachable at its own route and nowhere
67
67
  // else. Naming one in `__vue_file` from a *different* route is the bypass in
68
- // its clearest form. `module-only` is deliberately not gated the same way —
68
+ // its clearest form. `module-only` is deliberately not gated the same way:
69
69
  // a module exists to be embedded in someone else's page, so its actions are
70
70
  // invoked from that page's route by design, and the generated stub does
71
71
  // exactly that.
package/src/chunks.ts CHANGED
@@ -21,7 +21,7 @@ const VUE_ENTRIES = {
21
21
  } as const
22
22
 
23
23
  /**
24
- * The one canonical URL for the served Vue build — the import-map alias
24
+ * The one canonical URL for the served Vue build: the import-map alias
25
25
  * (`setup.ts`) and the request check below both read it, so they cannot drift.
26
26
  *
27
27
  * The variant is part of the filename, not just of the entry choice, because
@@ -66,7 +66,12 @@ export async function serveVueChunk(
66
66
  entrypoints: [sourcePath],
67
67
  target: 'browser',
68
68
  format: 'esm',
69
- minify: import.meta.env.PROD,
69
+ // `Boolean(...)`, not the flag directly: the mode flags are `'1'`/`''`
70
+ // strings (`core/init.ts`), and `Bun.build` **rejects** a non-boolean
71
+ // `minify` rather than coercing it. Core's `compiler.ts` already wrapped
72
+ // its three; this one did not, and the build would have thrown on every
73
+ // production SFC chunk.
74
+ minify: Boolean(import.meta.env.PROD),
70
75
  define: BUNDLE_DEFINES,
71
76
  })
72
77
 
package/src/client.ts CHANGED
@@ -1,17 +1,17 @@
1
1
  /**
2
- * `defineLayout()` — browser-side navigation for catch-all pages.
2
+ * `defineLayout()`: browser-side navigation for catch-all pages.
3
3
  *
4
4
  * A catch-all page (`[...slug].vue`, `[...slug!].vue`) owns every URL under
5
5
  * its directory: whatever the path, the server serves the same file. That
6
- * invariant is what makes client-side navigation *safe* here — swapping
6
+ * invariant is what makes client-side navigation *safe* here: swapping
7
7
  * content on a URL change can never disagree with what a hard reload would
8
- * serve — and it is why this API is only available on catch-all pages: on any
8
+ * serve, and it is why this API is only available on catch-all pages: on any
9
9
  * other page, two URLs mean two different files, and intercepting the
10
10
  * navigation would show the wrong one. The server stamps the route's shape
11
11
  * into `globalThis.__vue_route`; `defineLayout()` throws without it.
12
12
  *
13
13
  * The page becomes its subtree's layout: it reads `segments` and renders
14
- * whichever of its own components the path means — no `<slot />`, no extra
14
+ * whichever of its own components the path means, no `<slot />`, no extra
15
15
  * file. Clicks on same-origin links under the base are intercepted and become
16
16
  * a `pushState` plus a reactive update; links that leave the base navigate
17
17
  * normally; back/forward is handled the same way, falling back to a real
@@ -23,7 +23,7 @@
23
23
  */
24
24
  import { type Ref, ref } from 'vue'
25
25
 
26
- /** What the server stamps on the page — see `handleHtml` in `handler.ts`. */
26
+ /** What the server stamps on the page. See `handleHtml` in `handler.ts`. */
27
27
  type StampedRoute = {
28
28
  catchAll: boolean
29
29
  /** URL prefix owned by the page: '' for a root catch-all, else '/admin'. */
@@ -31,9 +31,11 @@ type StampedRoute = {
31
31
  /** The catch-all's param name (`slug` in `[...slug!]`). */
32
32
  param: string | null
33
33
  /**
34
- * First path segments the catch-all's *siblings* claim — `faculty` when
35
- * `faculty/[id].vue` sits beside the catch-all. Those URLs belong to more
36
- * specific routes, so they get real navigations, not soft ones.
34
+ * First path segments the catch-all's sibling *routes* claim: `faculty`
35
+ * when `faculty/[id].vue` sits beside the catch-all. Those URLs belong to
36
+ * more specific routes, so they get real navigations, not soft ones. Route
37
+ * names only, and only on catch-all pages: the stamp is readable by every
38
+ * visitor, so it must never be a directory listing.
37
39
  */
38
40
  claimed?: string[]
39
41
  /** True when a `[param]` sibling claims every single-segment path. */
@@ -41,16 +43,16 @@ type StampedRoute = {
41
43
  }
42
44
 
43
45
  export type LayoutNavigation = {
44
- /** Path segments under the base — `[]` on the bare directory. */
46
+ /** Path segments under the base: `[]` on the bare directory. */
45
47
  readonly segments: Ref<string[]>
46
48
  /** The URL prefix this page owns. */
47
49
  readonly base: string
48
50
  /** Navigate within the subtree; segments or a path, `/`-prefixed or not. */
49
51
  navigate(to: string | string[]): void
50
52
  /**
51
- * Listen for navigations. Return `false` from a listener to cancel one —
53
+ * Listen for navigations. Return `false` from a listener to cancel one:
52
54
  * cancellation applies to clicks and `navigate()`; back/forward cannot be
53
- * cancelled, only observed, because the history entry has already moved.
55
+ * canceled, only observed, because the history entry has already moved.
54
56
  */
55
57
  on(listener: LayoutListener): () => void
56
58
  }
@@ -67,7 +69,7 @@ export function isUnderBase(base: string, path: string): boolean {
67
69
  return path === base || path.startsWith(`${base}/`)
68
70
  }
69
71
 
70
- /** The segments of `path` below `base` — `[]` for the base itself. */
72
+ /** The segments of `path` below `base`: `[]` for the base itself. */
71
73
  export function segmentsUnder(base: string, path: string): string[] {
72
74
  const rest = base === '' ? path : path.slice(base.length)
73
75
  return rest.split('/').filter(Boolean)
@@ -85,7 +87,7 @@ function pathFor(base: string, to: string | string[]): string {
85
87
  export function defineLayout(): LayoutNavigation {
86
88
  const route = (globalThis as any).__vue_route as StampedRoute | undefined
87
89
 
88
- // The guard is the contract, not a formality — see the module comment.
90
+ // The guard is the contract, not a formality. See the module comment.
89
91
  if (!route?.catchAll) {
90
92
  throw new Error(
91
93
  'defineLayout() is only available on catch-all pages ' +
@@ -100,7 +102,7 @@ export function defineLayout(): LayoutNavigation {
100
102
  const listeners = new Set<LayoutListener>()
101
103
 
102
104
  // The catch-all owns only what nothing else claims. A sibling route under
103
- // the base — `faculty/[id].vue` beside `[...slug].vue` — wins those URLs on
105
+ // the base: `faculty/[id].vue` beside `[...slug].vue`: wins those URLs on
104
106
  // the server, so a soft-nav there would render this page where a hard load
105
107
  // renders that one.
106
108
  function claimedElsewhere(next: string[]): boolean {
@@ -126,14 +128,14 @@ export function defineLayout(): LayoutNavigation {
126
128
  function go(to: string | string[], cause: 'click' | 'navigate'): void {
127
129
  const path = pathFor(base, to)
128
130
  if (!isUnderBase(base, path)) {
129
- // Leaving the subtree is a real navigation — the next URL belongs to a
131
+ // Leaving the subtree is a real navigation: the next URL belongs to a
130
132
  // different file, and pretending otherwise would render a lie.
131
133
  if (typeof location !== 'undefined') location.href = path
132
134
  return
133
135
  }
134
136
  const next = segmentsUnder(base, path)
135
137
  if (claimedElsewhere(next)) {
136
- // Under the base, but a more specific route's territory — real
138
+ // Under the base, but a more specific route's territory: real
137
139
  // navigation, same reasoning as leaving the base.
138
140
  if (typeof location !== 'undefined') location.href = path
139
141
  return
@@ -186,7 +188,7 @@ export function defineLayout(): LayoutNavigation {
186
188
  location.reload()
187
189
  return
188
190
  }
189
- fire(next, 'history') // observable, not cancellable — see `on`
191
+ fire(next, 'history') // observable, not cancellable. See `on`
190
192
  segments.value = next
191
193
  })
192
194
  }
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; memoizing 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 = {}
@@ -137,7 +169,7 @@ export async function compileTemplateBlock(
137
169
  // Simple expression: `{{ value }}`.
138
170
  //
139
171
  // Comments inside an interpolation are not handled specially,
140
- // deliberately: Vue itself cannot parse `{{ total // pesos }}` —
172
+ // deliberately: Vue itself cannot parse `{{ total // pesos }}`,
141
173
  // measured against bare `compileTemplate`, which reports the same
142
174
  // SyntaxError and emits the raw broken expression. Stripping the
143
175
  // comment here just traded that for an unprefixed binding, because
@@ -166,13 +198,30 @@ export async function compileTemplateBlock(
166
198
 
167
199
  // Reported to the caller, not merely logged: a template Vue cannot compile
168
200
  // emits the raw unparseable expression into the render function, so the
169
- // module *cannot run* — `{{ total // pesos }}` is the measured example, and
201
+ // module *cannot run*, `{{ total // pesos }}` is the measured example, and
170
202
  // Vue-alone behaves identically. Serving it anyway was a browser-side
171
203
  // SyntaxError with a healthy-looking 200.
172
204
  const errors = (result.errors ?? []).map(e =>
173
205
  e instanceof Error ? e.message : String(e),
174
206
  )
175
207
 
208
+ // `export function render` becomes a local `function render`, which
209
+ // `assembleComponent` then hangs off `__sfc__.render`.
210
+ //
211
+ // **This must stay above `compileText`, and the anchor is why.** The
212
+ // template compiler emits multi-line source with the export at a line
213
+ // start, so `^` matches. `compileText` minifies to a single line where the
214
+ // export lands after the `import{…}from"vue";` prologue, and `^` cannot
215
+ // reach it there. Measured over four render shapes: correct on all four
216
+ // before minification, wrong on the one real shape after. Swapping these
217
+ // two lines therefore breaks silently: the bundle still builds, still
218
+ // serves 200, and ships a module with a stray `export` in the middle of it.
219
+ //
220
+ // A regex is the wrong tool for this and no spelling fixes it. An
221
+ // anchor-free `\bexport\s+(?=function\b)` handles the minified shape and
222
+ // then eats the words out of a string literal that contains them. The
223
+ // trade was measured rather than argued; the ordering is the cheaper
224
+ // guarantee, so it is the one written down.
176
225
  let code = result.code
177
226
  code = code.replace(/^export\s+/m, '')
178
227
  code = await compileText(code)
@@ -182,8 +231,8 @@ export async function compileTemplateBlock(
182
231
  export async function compileStyleBlock(
183
232
  options: CompileStyleOptions,
184
233
  ): Promise<SFCStyleCompileResults> {
185
- await loadCompiler()
186
- return compiler.compileStyle({
234
+ const sfc = await loadCompiler()
235
+ return sfc.compileStyle({
187
236
  source: options.style.content,
188
237
  filename: 'style.css',
189
238
  id: options.id,
@@ -201,8 +250,20 @@ export function assembleComponent(options: AssembleComponentOptions): string {
201
250
  )
202
251
 
203
252
  if (renderCode) {
204
- const renderFn = renderCode.replace(/^export\s+/m, '')
205
- output += `\n${renderFn}\n${COMPONENT_VAR}.render = render;`
253
+ // No export strip here, deliberately. `compileTemplateBlock` has already
254
+ // removed it, above minification where the removal works, and repeating
255
+ // it on this input is a no-op that *reads* as a backstop.
256
+ //
257
+ // The line that used to sit here was `/^export\s+/m` over
258
+ // already-minified code. Measured against four render shapes it fired on
259
+ // three and missed the fourth, and the fourth is the only one this
260
+ // function is ever handed: minified, with the export sitting after the
261
+ // import prologue where `^` cannot see it. A guard that cannot fire on
262
+ // its own input is worse than none, because the next reader counts on it.
263
+ //
264
+ // Three lines above is what the right spelling looks like when the input
265
+ // may be minified: `\b` and no anchor.
266
+ output += `\n${renderCode}\n${COMPONENT_VAR}.render = render;`
206
267
  }
207
268
 
208
269
  if (scopeId) {
@@ -215,7 +276,7 @@ export function assembleComponent(options: AssembleComponentOptions): string {
215
276
  if (layoutRoute) {
216
277
  // The page renders into the layout's default <slot />. The import is a
217
278
  // plain `.vue` specifier so `rewriteVueImports` gives it the same
218
- // `?__vue_script=module` treatment as any component import — the layout
279
+ // `?__vue_script=module` treatment as any component import: the layout
219
280
  // is just a component that happens to be discovered by convention.
220
281
  output +=
221
282
  `\nimport { createApp, h as __h } from 'vue';` +
@@ -228,7 +289,7 @@ export function assembleComponent(options: AssembleComponentOptions): string {
228
289
  }
229
290
 
230
291
  // Full build only. `app.config.compilerOptions` is read exclusively by the
231
- // in-browser template compiler, which the runtime build does not carry —
292
+ // in-browser template compiler, which the runtime build does not carry:
232
293
  // there, the assignment does nothing except make Vue log a warning about
233
294
  // itself on every page, even for apps that configured nothing.
234
295
  if (vueBuildVariant() === 'full') {
@@ -246,7 +307,7 @@ export function assembleComponent(options: AssembleComponentOptions): string {
246
307
  /**
247
308
  * Mirror `resolveIsCustomElement` in the browser, so runtime-compiled templates
248
309
  * agree with what the SFC compiler did. A predicate that closes over server-side
249
- * state cannot be serialized — it falls back to the built-in tag.
310
+ * state cannot be serialized: it falls back to the built-in tag.
250
311
  */
251
312
  function buildRuntimeCustomElementCheck(): string {
252
313
  const ce = vuePluginOptions.customElements
package/src/handler.ts CHANGED
@@ -57,7 +57,7 @@ const RX_SERVER_DATA_TOKEN = new RegExp(`\\b${VUE_SERVER_DATA_TOKEN}\\b`, 'g')
57
57
  /**
58
58
  * Compiled module code with the server-data token still in place. Components
59
59
  * with a `<script server>` block get per-request data, so the code cannot be
60
- * cached on disk with the data baked in — but the expensive compile can be.
60
+ * cached on disk with the data baked in, but the expensive compile can be.
61
61
  */
62
62
  const tokenizedModuleCache = new LRUCache<
63
63
  string,
@@ -84,10 +84,10 @@ function buildActionStub(fn: string, relPath: string) {
84
84
 
85
85
  /**
86
86
  * Route path of the nearest `layout.vue`, walking from the page **file** up
87
- * to the serve root — the file, not the URL, so a catch-all page
87
+ * to the serve root, the file, not the URL, so a catch-all page
88
88
  * (`admin/[...slug!].vue`) is wrapped by `admin/layout.vue` no matter how
89
89
  * deep the request path goes. Null when nothing is found, when the page
90
- * opted out with `<meta no-layout />`, or when the page *is* a layout —
90
+ * opted out with `<meta no-layout />`, or when the page *is* a layout:
91
91
  * layouts do not nest in v1, deliberately: nesting needs an ordering story
92
92
  * (which slot, whose styles win) that should be designed, not implied.
93
93
  */
@@ -113,42 +113,135 @@ function findLayoutRoute(filePath: string, meta: VueMeta): string | null {
113
113
  }
114
114
 
115
115
  /**
116
- * What the catch-all's siblings claim, for the `defineLayout()` stamp.
116
+ * Does `dir` hold a file this handler routes, at any depth? A sibling
117
+ * directory claims its first segment only when it does: the claim exists for
118
+ * `faculty/[id].vue`-shaped subtrees, and a directory of assets or helpers
119
+ * routes nowhere more specific than the catch-all, so stamping its name would
120
+ * disclose it for no navigational gain. Files at each level are checked
121
+ * before any subdirectory is entered, so the common shallow layout answers
122
+ * without recursing. `Dirent.isDirectory()` is false for symlinks, which is
123
+ * what keeps the walk from cycling.
124
+ */
125
+ function containsRouteFile(dir: string, exts: string[]): boolean {
126
+ let entries: import('node:fs').Dirent[]
127
+ try {
128
+ entries = readdirSync(dir, { withFileTypes: true })
129
+ } catch {
130
+ // Unreadable: treated as holding no routes, same reasoning as the catch
131
+ // in claimedBeside below.
132
+ return false
133
+ }
134
+
135
+ const dirs: string[] = []
136
+ for (const entry of entries) {
137
+ if (entry.name.startsWith('.')) continue
138
+ if (entry.isDirectory()) dirs.push(`${dir}/${entry.name}`)
139
+ else if (exts.some(ext => entry.name.endsWith(ext))) return true
140
+ }
141
+ return dirs.some(sub => containsRouteFile(sub, exts))
142
+ }
143
+
144
+ /**
145
+ * What the catch-all's sibling *routes* claim, for the `defineLayout()` stamp.
117
146
  *
118
147
  * A catch-all owns only *what nothing else claims*: with
119
148
  * `admin/[...slug].vue` beside `admin/faculty/[id].vue`, the URL
120
- * `/admin/faculty/7` is under the base but belongs to `[id].vue` — so the
149
+ * `/admin/faculty/7` is under the base but belongs to `[id].vue`, so the
121
150
  * client-side router must yield it to a real navigation, or a soft-nav shows
122
151
  * the catch-all's rendering where a hard reload shows a different page.
123
152
  *
124
153
  * First-level granularity is exactly the server's precedence boundary: every
125
- * more-specific route — an exact sibling file, a child index, a deeper
126
- * catch-all — lives inside some sibling entry, so excluding the entry
154
+ * more-specific route (an exact sibling file, a child index, a deeper
155
+ * catch-all) lives inside some sibling entry, so excluding the entry
127
156
  * excludes the whole claim. A `[param]` sibling claims *every* single-segment
128
157
  * path, which is what `claimedSingle` carries. `layout.vue` claims nothing (it
129
158
  * is not routable), and the catch-all file itself is the page being served.
130
159
  *
131
- * Computed per page request, so files added or removed in dev are seen on the
132
- * next load without cache ceremony.
160
+ * Only entries the handler's own extension table routes are claims. The stamp
161
+ * is serialized into the HTML of every served page, so each name in it is
162
+ * published to any visitor, and this function used to list *every* sibling
163
+ * stem, which put non-route file names (`sample.bin`, `script.ts`,
164
+ * `index.tsx`) from the source directory into production responses: a
165
+ * directory listing of `src/`, observed in a smoke test of the published
166
+ * alpha. A file sibling therefore counts only with a routed extension, and a
167
+ * directory sibling only when a route file exists somewhere under it.
168
+ *
169
+ * The boundary that filter accepts: a *non-route* file under the base is
170
+ * served by core's real-file-beats-catch-all rule (`findDynamicRoute`), and
171
+ * the stamp no longer names it, so a plain anchor to one soft-navigates into
172
+ * the catch-all's view. An anchor carrying `target` or `download` is never
173
+ * intercepted: that is the spelling for linking a raw file out of a
174
+ * catch-all's subtree, and what `docs/plugins/vue.md` prescribes.
175
+ *
176
+ * Computed per page request in development, so a file added or removed is
177
+ * seen on the next load without cache ceremony. **Memoized 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 behavior whether it meant to or
207
+ * not. Clear it between such calls, or ask for development explicitly with
208
+ * `withEnvFlag('PROD', false, …)`.
133
209
  */
210
+ export function __resetClaimedCache(): void {
211
+ claimedCache.clear()
212
+ }
213
+
134
214
  export function claimedBeside(catchAllFile: string): {
135
215
  claimed: string[]
136
216
  claimedSingle: boolean
137
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
+
138
225
  const claimed = new Set<string>()
139
226
  let claimedSingle = false
140
227
 
141
- const dir = fs.resolve(catchAllFile).replace(/\/[^/]*$/, '')
142
- const self = fs.resolve(catchAllFile).slice(dir.length + 1)
228
+ const dir = resolved.replace(/\/[^/]*$/, '')
229
+ const self = resolved.slice(dir.length + 1)
230
+ // The handler's own table (`['vue']`), read at call time so the two cannot
231
+ // drift apart.
232
+ const exts = VueHandler.config.ext.map(ext => `.${ext}`)
143
233
 
144
234
  let entries: import('node:fs').Dirent[]
145
235
  try {
146
236
  entries = readdirSync(dir, { withFileTypes: true })
147
237
  } catch {
148
238
  // Unreadable directory: no visible siblings means nothing extra claimed,
149
- // and a hard load still routes correctly — the stamp is an optimisation
150
- // of honesty, not the source of it.
151
- return { claimed: [], claimedSingle: false }
239
+ // and a hard load still routes correctly. The stamp is an optimization
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
152
245
  }
153
246
 
154
247
  for (const entry of entries) {
@@ -157,19 +250,27 @@ export function claimedBeside(catchAllFile: string): {
157
250
  if (name.startsWith('.')) continue
158
251
 
159
252
  if (RX_CATCHALL.test(name) || RX_OPT_CATCHALL.test(name)) continue
253
+
254
+ const isRoute = entry.isDirectory()
255
+ ? containsRouteFile(`${dir}/${name}`, exts)
256
+ : exts.some(ext => name.endsWith(ext))
257
+ if (!isRoute) continue
258
+
160
259
  if (RX_DYNAMIC.test(name)) {
161
260
  claimedSingle = true
162
261
  continue
163
262
  }
164
263
 
165
264
  claimed.add(name)
166
- // `reports.vue` also claims `/base/reports` — the extensionless spelling
265
+ // `reports.vue` also claims `/base/reports`: the extensionless spelling
167
266
  // is the one links actually use.
168
267
  const stem = name.replace(/\.[^.]+$/, '')
169
268
  if (stem && stem !== name) claimed.add(stem)
170
269
  }
171
270
 
172
- return { claimed: [...claimed], claimedSingle }
271
+ const result = { claimed: [...claimed], claimedSingle }
272
+ if (memoised) claimedCache.set(resolved, result)
273
+ return result
173
274
  }
174
275
 
175
276
  export class VueHandler extends DynamicHandler {
@@ -254,11 +355,11 @@ export class VueHandler extends DynamicHandler {
254
355
  // render blank: the injected block above has no `export default`, so
255
356
  // `assembleComponent` had nothing to rewrite into `const __sfc__ =`
256
357
  // and the module died with `ReferenceError: __sfc__ is not defined`.
257
- // The documented workaround was a setup block — so inject one. The
358
+ // The documented workaround was a setup block, so inject one. The
258
359
  // comment inside is load-bearing: the SFC parser *discards* a block
259
360
  // whose content is only whitespace, which is also why the workaround
260
361
  // had to be a non-empty block. It also makes the server exports
261
- // template-visible — compileScript only records plain-script bindings
362
+ // template-visible: compileScript only records plain-script bindings
262
363
  // when a setup block exists.
263
364
  if (!/<script\s[^>]*\bsetup\b|<script\s+setup/i.test(cleanContent)) {
264
365
  cleanContent += `\n<script setup${langAttr}>\n// injected: carries the server-data bindings above\n</script>\n`
@@ -320,7 +421,7 @@ export class VueHandler extends DynamicHandler {
320
421
  if (compiled.errors.length) {
321
422
  // Thrown, not logged-and-served: a template Vue could not compile has
322
423
  // the raw unparseable expression in its render function, so serving it
323
- // is a browser-side SyntaxError behind a 200 and an empty page — the
424
+ // is a browser-side SyntaxError behind a 200 and an empty page, the
324
425
  // report that surfaced this described exactly that. The throw lands in
325
426
  // the error registry as a 500 that names the file and the error.
326
427
  throw new Error(
@@ -371,7 +472,7 @@ export class VueHandler extends DynamicHandler {
371
472
  // Root scripts carry the build variant in their name for the same reason
372
473
  // the chunk does (`vueChunkPath`): the cache is keyed on the *source's*
373
474
  // mtime, and flipping `build` in server.config.ts touches no source file
374
- // — measured serving a root compiled under 'runtime' after the flip to
475
+ //, measured serving a root compiled under 'runtime' after the flip to
375
476
  // 'full', missing the isCustomElement bridge the full build exists for.
376
477
  // Only roots: the variant changes nothing in a subcomponent's output.
377
478
  // The layout joins the name for the same reason the variant does: the
@@ -460,7 +561,13 @@ export class VueHandler extends DynamicHandler {
460
561
  routePath: string,
461
562
  serverParams: any,
462
563
  parsed: ParsedCacheEntry,
463
- route?: { catchAll: boolean; base: string; param: string | null },
564
+ route?: {
565
+ catchAll: boolean
566
+ base: string
567
+ param: string | null
568
+ claimed?: string[]
569
+ claimedSingle?: boolean
570
+ },
464
571
  ) {
465
572
  const { hasCss, serverScript } = parsed
466
573
  const hasServerData =
@@ -479,7 +586,7 @@ export class VueHandler extends DynamicHandler {
479
586
 
480
587
  // The route's shape, for `defineLayout()` (`client.ts`): the guard that
481
588
  // restricts it to catch-all pages reads `catchAll` from here, so the
482
- // stamp is the enforcement, not a convenience. Stamped on every page —
589
+ // stamp is the enforcement, not a convenience. Stamped on every page:
483
590
  // a non-catch-all page carries `catchAll: false`, which is what makes
484
591
  // the client-side error message possible instead of a bare undefined.
485
592
  const routeDecl = route
@@ -491,7 +598,7 @@ export class VueHandler extends DynamicHandler {
491
598
  () => serverDecl + routeDecl,
492
599
  )
493
600
 
494
- // Static markup, injected verbatim — see parseSkeleton for why it is
601
+ // Static markup, injected verbatim. See parseSkeleton for why it is
495
602
  // never rendered. mount() replaces the container children, so it
496
603
  // disappears the moment the real component is up.
497
604
  if (parsed.skeleton) {
@@ -556,8 +663,8 @@ function asDirectResponse(value: any) {
556
663
  * Serve `value` if the server script already produced a complete response,
557
664
  * otherwise `null` so the caller treats it as page data.
558
665
  *
559
- * Both paths through `sharedHandler` — the `__vue_action` call and the ordinary
560
- * page render — need exactly this, and each carried its own verbatim copy. A
666
+ * Both paths through `sharedHandler` (the `__vue_action` call and the ordinary
667
+ * page render) need exactly this, and each carried its own verbatim copy. A
561
668
  * `BunFile` goes through `ETag.sendFile` so a conditional request can 304; the
562
669
  * `new Response` is the fallback for the un-cacheable case.
563
670
  */
@@ -570,7 +677,7 @@ async function serveIfDirect(value: any, req: Request) {
570
677
  return (await ETag.sendFile(direct, req)) || new Response(direct as any)
571
678
  }
572
679
 
573
- // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: request handler — one branch per SFC render path
680
+ // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: request handler, one branch per SFC render path
574
681
  async function sharedHandler(
575
682
  this: typeof DynamicHandler | typeof DynamicErrorHandler,
576
683
  path: string,
@@ -613,7 +720,7 @@ async function sharedHandler(
613
720
  }
614
721
 
615
722
  // A layout is scaffolding, not a destination: /admin/layout must not render
616
- // as a page. Script and css requests pass — they are how the root script of
723
+ // as a page. Script and css requests pass: they are how the root script of
617
724
  // every page under it imports the thing.
618
725
  if (routePath.endsWith('/layout.vue') && !isScript && !isCss) {
619
726
  return response.error('Not Found', 404)
@@ -660,7 +767,7 @@ async function sharedHandler(
660
767
  }
661
768
 
662
769
  // The gates above ran against the route. This one runs against whatever
663
- // `__vue_file` chose, which is the thing that is about to execute — and it
770
+ // `__vue_file` chose, which is the thing that is about to execute, and it
664
771
  // runs before the body is read, so a rejected request costs a parse it
665
772
  // never needed.
666
773
  const denied = validateActionTarget(
@@ -694,7 +801,7 @@ async function sharedHandler(
694
801
  return response.json.success('OK', actionResult)
695
802
  }
696
803
 
697
- // Root scripts and stylesheets carry no per-request data — running the server
804
+ // Root scripts and stylesheets carry no per-request data: running the server
698
805
  // block for them would re-execute every top-level query on each sub-request.
699
806
  const needsServerData = !isCss && vueScriptParam !== 'root'
700
807
 
@@ -724,6 +831,7 @@ async function sharedHandler(
724
831
  // `base` is the URL prefix the page owns: the file's directory. For
725
832
  // `wiki/[...page!].vue` that is `/wiki`; for a root-level catch-all it is
726
833
  // the empty string, which `defineLayout` treats as "everything".
834
+ const catchAll = Boolean(info.catchAll)
727
835
  return VueHandler.handleHtml(
728
836
  id,
729
837
  finalParams,
@@ -731,10 +839,13 @@ async function sharedHandler(
731
839
  serverParams,
732
840
  parsed,
733
841
  {
734
- catchAll: Boolean(info.catchAll),
842
+ catchAll,
735
843
  base: routePath.slice(0, routePath.lastIndexOf('/')),
736
844
  param: info.params.length ? info.params[info.params.length - 1] : null,
737
- ...claimedBeside(diskFile.name ?? ''),
845
+ // Catch-all pages only: `defineLayout()` refuses every other page, so
846
+ // on those the claims would be sibling names published in the HTML with
847
+ // no reader. Skipping the stamp also skips the directory scan.
848
+ ...(catchAll ? claimedBeside(diskFile.name ?? '') : null),
738
849
  },
739
850
  )
740
851
  }
package/src/index.ts CHANGED
@@ -1,16 +1,28 @@
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'
5
6
 
6
7
  /**
7
8
  * `vuePlugin` is this package's only export, and its parameter type lived in a
8
- * `.d.ts` reachable through no working specifier — so a consumer could pass
9
+ * `.d.ts` reachable through no working specifier, so a consumer could pass
9
10
  * options but never name their type. Re-exported here, where the function that
10
11
  * takes them is.
11
12
  */
12
13
  export type { CustomElementsOption, VuePluginOptions } from './types'
13
14
 
15
+ /**
16
+ * The request body inside a `<script server>` block, and anything it calls.
17
+ *
18
+ * Pairs with `getRequest()` from `@bakery-framework/core`. Both replace the
19
+ * `req` and `body` globals this plugin declared in `vue.d.ts`, which only
20
+ * reached a file when that `.d.ts` happened to land in the tsconfig project
21
+ * an editor resolved for it. The globals still work; they are no longer the
22
+ * only way, and they are no longer load-bearing.
23
+ */
24
+ export { getBody } from './server-context'
25
+
14
26
  export default function vuePlugin(options?: VuePluginOptions) {
15
27
  if (options) setVuePluginOptions(options)
16
28
  return definePlugin({
@@ -19,7 +31,7 @@ export default function vuePlugin(options?: VuePluginOptions) {
19
31
  // Declarative, read by the tsconfig generator at dev boot rather than by
20
32
  // the server. `.vue` needs its own project for two reasons core cannot
21
33
  // cover: plain `tsc` cannot parse an SFC at all (so this is for `vue-tsc`),
22
- // and `vue.d.ts` declares `req` and `body` for SFC scope — globals core
34
+ // and `vue.d.ts` declares `req` and `body` for SFC scope. Globals core
23
35
  // deliberately does not provide. Before this, those declarations shipped in
24
36
  // the package and were reachable by no app.
25
37
  tsconfig: {
@@ -27,7 +39,7 @@ export default function vuePlugin(options?: VuePluginOptions) {
27
39
  name: 'vue',
28
40
  // An SFC's `<script>` is rendered on the server, so it reaches the ORM
29
41
  // and the app's schema registration the way an API route does. Without
30
- // this the tables fall back to `any` in every `.vue` file — silently,
42
+ // this the tables fall back to `any` in every `.vue` file: silently,
31
43
  // because the untyped mode is a supported state and does not error.
32
44
  server: true,
33
45
  extends: '@bakery-framework/core/tsconfig.vue.json',
@@ -38,7 +50,7 @@ export default function vuePlugin(options?: VuePluginOptions) {
38
50
  // An SFC's `<script>` is browser code, so an `importMap` alias inside one
39
51
  // is resolved by the browser's own import map and has to typecheck. The
40
52
  // flag is off by default precisely because the *server* project must not
41
- // have it — an alias only the browser can satisfy would typecheck there
53
+ // have it: an alias only the browser can satisfy would typecheck there
42
54
  // and fail at runtime. Vue is the case the flag exists to allow.
43
55
  importMapPaths: true,
44
56
  },
@@ -48,6 +60,19 @@ export default function vuePlugin(options?: VuePluginOptions) {
48
60
  const { setupVue } = await import('./setup')
49
61
  await setupVue()
50
62
  },
63
+
64
+ /**
65
+ * Start loading `@vue/compiler-sfc` while the rest of boot runs.
66
+ *
67
+ * It is 168-173 ms warm and 1,848 ms on a cold filesystem, and without
68
+ * this every server process paid it inside the first Vue page it served.
69
+ * Not awaited: the load overlaps with everything else here, and a request
70
+ * that arrives before it finishes awaits the same promise rather than
71
+ * starting a second one.
72
+ */
73
+ onStart() {
74
+ preloadCompiler()
75
+ },
51
76
  onCompile(content, path) {
52
77
  if (
53
78
  !path.endsWith('.ts') &&
@@ -0,0 +1,37 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks'
2
+
3
+ /**
4
+ * The parsed body of the request a `<script server>` block is running for.
5
+ *
6
+ * Separate from core's `getRequest()` deliberately: the request is core's and
7
+ * every surface has one, while `body` is this plugin's own computation
8
+ * (`VueHandler.params(req, finalParams)`, which merges route params with the
9
+ * parsed payload). A `.tsx` page receives its body as a function argument and
10
+ * has no use for this.
11
+ *
12
+ * Why it exists at all is the same reason as `getRequest()`. The block used
13
+ * to reach `body` as a global declared in `vue.d.ts`, which only works if
14
+ * that `.d.ts` lands in whatever tsconfig project an editor resolves for the
15
+ * SFC, and for an SFC it routinely does not. An import cannot fail that way,
16
+ * it carries a real type instead of `any`, and it reaches a helper the block
17
+ * calls rather than stopping at the wrapper's parameter list.
18
+ */
19
+ const bodyStore = new AsyncLocalStorage<{ body: unknown }>()
20
+
21
+ /** Run `fn` with `body` visible to `getBody()`. Called by the plugin only. */
22
+ export function runWithServerBody<T>(body: unknown, fn: () => T): T {
23
+ return bodyStore.run({ body }, fn)
24
+ }
25
+
26
+ /**
27
+ * The request body, inside a `<script server>` block or anything it calls.
28
+ *
29
+ * Unlike {@link import('@bakery-framework/core').getRequest} this does **not**
30
+ * throw when absent: a GET with no payload and no route params is an ordinary
31
+ * request, and `undefined` is the honest answer rather than an error
32
+ * condition. The type parameter is the caller's assertion about a shape the
33
+ * framework cannot know, exactly as it was when this was an `any` global.
34
+ */
35
+ export function getBody<T = unknown>(): T | undefined {
36
+ return bodyStore.getStore()?.body as T | undefined
37
+ }
package/src/setup.ts CHANGED
@@ -20,7 +20,7 @@ export function setupVue() {
20
20
  initVueVersion()
21
21
  Bakery.handlers.fetch.set(VueHandler, 58)
22
22
  Bakery.handlers.error.set(VueErrorHandler, 18)
23
- // `vueChunkPath` is the single writer of this URL — it carries the build
23
+ // `vueChunkPath` is the single writer of this URL: it carries the build
24
24
  // variant (`<version>.runtime.js` / `<version>.full.js`), and the serving
25
25
  // check in `chunks.ts` reads the same function.
26
26
  Bakery.config.importMap.vue = vueChunkPath()
package/src/types.d.ts CHANGED
@@ -79,8 +79,8 @@ export interface ParsedCacheEntry {
79
79
  meta: VueMeta
80
80
  /**
81
81
  * Inner markup of a `<template skeleton>` block, or null. Static by
82
- * construction — extracted before compilation, never rendered on the
83
- * server — so nothing request- or user-derived can reach it.
82
+ * construction (extracted before compilation, never rendered on the
83
+ * server), so nothing request- or user-derived can reach it.
84
84
  */
85
85
  skeleton: string | null
86
86
  /**
@@ -113,7 +113,7 @@ export interface VuePluginOptions {
113
113
  * normally needs: SFC templates are compiled to render functions on the
114
114
  * server, and `customElements` is applied there too, so the browser never
115
115
  * compiles a template. Opt into `'full'` only for components that hand Vue a
116
- * raw `template:` string at runtime — those are compiled in the browser and
116
+ * raw `template:` string at runtime: those are compiled in the browser and
117
117
  * fail on the runtime build with Vue's "runtime compilation is not
118
118
  * supported" error.
119
119
  */
package/src/utils.ts CHANGED
@@ -7,6 +7,7 @@ import { LRUCache } from '@bakery-framework/core/cache/lru'
7
7
  import { Bakery } from '@bakery-framework/core/core/bakery'
8
8
  import { Logger } from '@bakery-framework/core/logger'
9
9
  import { fs, is, Try } from '@bakery-framework/core/utils'
10
+ import { runWithServerBody } from './server-context'
10
11
  import type { ParsedCacheEntry, ServerResponseOptions, VueMeta } from './types'
11
12
 
12
13
  const logger = new Logger('vue')
@@ -32,7 +33,7 @@ export const RX_IMPORT_VUE_FILE =
32
33
  export const RX_EXPORT_BRACE = /\bexport\s*\{([\s\S]*?)\}/g
33
34
  export const RX_EXPORT_HANGING = /\bexport\s+(const|let|var)\s+(\w+)\s*=\s*/g
34
35
  export const RX_EXPORT_FUNCTION = /\bexport\s+(?:async\s+)?function\s+(\w+)/g
35
- /** `export const fn = async (a) => {}` / `= function () {}` — callable, not data. */
36
+ /** `export const fn = async (a) => {}` / `= function () {}`: callable, not data. */
36
37
  export const RX_EXPORT_CALLABLE_CONST =
37
38
  /\bexport\s+(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?(?:function\b|(?:\([^)]*\)|\w+)\s*=>)/g
38
39
  export const RX_TOP_LEVEL_IMPORT =
@@ -61,10 +62,10 @@ const CLOSING_TAG = '</script'
61
62
  /**
62
63
  * Find the `</script>` that actually closes a server block, skipping ones that
63
64
  * appear inside string literals, template literals, or comments. A plain
64
- * non-greedy regex stops at the first match — so server code containing
65
+ * non-greedy regex stops at the first match, so server code containing
65
66
  * `"</script>"` would be cut short and its remainder left in the client bundle.
66
67
  */
67
- // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: character scanner — locates a block end past nested quotes
68
+ // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: character scanner, locates a block end past nested quotes
68
69
  function findServerScriptEnd(raw: string, start: number): number {
69
70
  let index = start
70
71
 
@@ -191,9 +192,9 @@ export function rewriteRelativeImports(
191
192
  /**
192
193
  * Find where the expression starting at `start` ends. Tracks bracket depth and
193
194
  * skips strings/comments, so a multi-line object, function, or arrow body
194
- * survives intact — stopping at the first newline breaks all three.
195
+ * survives intact: stopping at the first newline breaks all three.
195
196
  */
196
- // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: character scanner — brace and quote state machine
197
+ // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: character scanner, brace and quote state machine
197
198
  function findExpressionEnd(code: string, start: number): number {
198
199
  let depth = 0
199
200
  let index = start
@@ -378,12 +379,12 @@ export function compileServerBlock(
378
379
  const ${p}actions = new Set(${actionAllowList})
379
380
 
380
381
  // NOTE: the top-level statements below run before middleware. Middleware can
381
- // stop the response but cannot stop top-level code from executing — put
382
+ // stop the response but cannot stop top-level code from executing. Put
382
383
  // auth-gated work inside an exported function, not at the top level.
383
384
  export default async function ${p}server(req: any, body: any, actionName?: string, actionArgs?: any[]) {
384
385
  const ${p}result: any = {}
385
386
 
386
- // The allow-list is static — it is this script's own exports — so it can
387
+ // The allow-list is static (it is this script's own exports), so it can
387
388
  // be answered before a line of the component runs. It used to be checked
388
389
  // after the body, which meant naming an action that does not exist still
389
390
  // executed every top-level statement in the file; with \`__vue_file\`
@@ -524,7 +525,11 @@ export async function getServerResponse(options: ServerResponseOptions) {
524
525
  })
525
526
  const execPromise = Promise.resolve(
526
527
  is.function(exported)
527
- ? exported(req, body, actionName, actionArgs)
528
+ ? // The parameters stay, so every existing block keeps working; the
529
+ // store is what lets `getBody()` reach a helper the block calls.
530
+ runWithServerBody(body, () =>
531
+ exported(req, body, actionName, actionArgs),
532
+ )
528
533
  : exported,
529
534
  )
530
535
  const params = await Promise.race([execPromise, timeoutPromise])
@@ -573,13 +578,13 @@ const RX_META_SKIPPABLE = /^\s+|^<!--[\s\S]*?-->/
573
578
  /**
574
579
  * Extract a `<template skeleton>` block: its inner markup goes into the HTML
575
580
  * shell's `#app` so the user sees something before the bundle hydrates, and
576
- * the block is removed from the SFC — the compiler allows only one template.
581
+ * the block is removed from the SFC, the compiler allows only one template.
577
582
  *
578
583
  * **Static by design, and the design is a security decision.** The markup is
579
584
  * injected verbatim: never compiled, never rendered on the server, so
580
585
  * interpolations do not evaluate and nothing request- or session-derived can
581
586
  * end up in it. A server-rendered skeleton cached across requests would serve
582
- * one user's data to another. Scoped styles do not reach it either — the
587
+ * one user's data to another. Scoped styles do not reach it either: the
583
588
  * scope attributes are stamped by the compiler this block never meets.
584
589
  *
585
590
  * One block per file; nested `<template>` elements inside it are not