@ubean/build 0.6.2 → 0.6.4

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 ADDED
@@ -0,0 +1,572 @@
1
+ # @ubean/build
2
+
3
+ **English | [中文](./README.zh-CN.md)**
4
+
5
+ Vite plugins, production build orchestration, SSG prerender, type codegen, and the Server Actions plugin for ubean.
6
+
7
+ ```ts
8
+ // vite.config.ts — the single registration point of the whole toolchain
9
+ import { defineConfig } from 'vite-plus';
10
+ import { ubeanPlugin } from 'ubean/vite';
11
+
12
+ export default defineConfig({
13
+ plugins: [ubeanPlugin()]
14
+ });
15
+ ```
16
+
17
+ ## Table of Contents
18
+
19
+ - [Package Positioning](#package-positioning)
20
+ - [Install](#install)
21
+ - [Quick Start](#quick-start)
22
+ - [Vite Plugin](#vite-plugin)
23
+ - [Vue Plugin](#vue-plugin)
24
+ - [Virtual Modules](#virtual-modules)
25
+ - [Macros](#macros)
26
+ - [Dev Server Bootstrap](#dev-server-bootstrap)
27
+ - [Preview](#preview)
28
+ - [Server Actions](#server-actions)
29
+ - [Production Build](#production-build)
30
+ - [Prerender](#prerender)
31
+ - [Static Render Path](#static-render-path)
32
+ - [Codegen](#codegen)
33
+ - [Type Safety](#type-safety)
34
+ - [Architecture Notes](#architecture-notes)
35
+
36
+ ## Package Positioning
37
+
38
+ `@ubean/build` is the **build-time package** of ubean: everything that runs inside Vite, plus the
39
+ production-build and SSG pipeline that Vite drives.
40
+
41
+ | Responsible for | NOT responsible for |
42
+ | ------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
43
+ | The core Vite plugin (`ubeanPlugin` in `./vite`): environments, dev request routing, build orchestration, preview takeover | The **composed** `ubeanPlugin()` — that is `ubean/vite` in the `ubean` aggregator package |
44
+ | The Vue-side plugin (`ubeanVite` in `./vue`): page/app/entry virtual modules, auto-imports, component resolvers, `.vue` compile | Vue **compiler** behaviour itself — `@vitejs/plugin-vue` |
45
+ | Virtual-module registry, `definePage` macro extraction, component/CSS resolver registry | Page scanning — `@ubean/vue` owns pages, `@ubean/scan` owns the aggregating scan |
46
+ | Production orchestrations (`buildWithEnvironments`), preset build config, server/worker/handler entry generators | Deployment platform definitions — `@ubean/preset` |
47
+ | Prerender / SSG: route collection, route→file mapping, payload extraction, static-entry direct render | Runtime caching, ISR, PPR semantics — `@ubean/routes` / `@ubean/server` |
48
+ | Type codegen (`generateTypes` + the unplugin presets) and the config → Vite translation | Config loading/resolution — `@ubean/config` |
49
+ | Server Actions **source transform** (client stub / server `filePath` injection) | The `defineAction` runtime and the `POST /__actions` handler — `@ubean/routes` |
50
+
51
+ **Runtime dependency whitelist (main entry `@ubean/build`):** macros, the virtual-module registry, the
52
+ component/CSS resolver registry, and the SSR singleton policy — no `vite`, no `@vitejs/plugin-vue`, no
53
+ Vue compiler. Every heavier capability sits behind a subpath; `./vite` and `./vue` are the only two
54
+ that import Vite, and `./production`, `./prerender`, `./static-render`, `./codegen`, `./actions` are
55
+ build-time only — never import them from runtime route code (see [Architecture Notes](#architecture-notes)).
56
+
57
+ `vite` is declared as an **optional peer dependency** (`peerDependenciesMeta.vite.optional`).
58
+
59
+ ## Install
60
+
61
+ ```bash
62
+ pnpm add -D @ubean/build vite
63
+ ```
64
+
65
+ Most projects never install this package directly — the `ubean` aggregator depends on it and
66
+ re-exports the plugin through `ubean/vite`, `ubean/build`, and `ubean/ssr`.
67
+
68
+ ## Quick Start
69
+
70
+ The supported entry point is the composed plugin from the aggregator:
71
+
72
+ ```ts
73
+ // vite.config.ts
74
+ import { defineConfig } from 'vite-plus';
75
+ import { ubeanPlugin } from 'ubean/vite';
76
+
77
+ export default defineConfig({
78
+ plugins: [ubeanPlugin()]
79
+ });
80
+ ```
81
+
82
+ Composition only — no `await` needed. Vite awaits promises found in the `plugins` array before any
83
+ hook runs, so `plugins: [ubeanPlugin()]` is correct even though the factory is async.
84
+
85
+ Framework-agnostic or fine-grained use imports the subpaths directly:
86
+
87
+ ```ts
88
+ import { ubeanServerActionsPlugin } from '@ubean/build/actions';
89
+ import { prerender } from '@ubean/build/prerender';
90
+ import { createStaticSsgRenderer } from '@ubean/build/static-render';
91
+ import { ubeanPlugin as ubeanCorePlugin } from '@ubean/build/vite'; // no Vue, no islands
92
+ import { ubeanVite } from '@ubean/build/vue'; // Vue pages + app entry + auto-imports
93
+ ```
94
+
95
+ ## Vite Plugin
96
+
97
+ Entry point: `@ubean/build/vite`.
98
+
99
+ `ubeanPlugin()` is the framework-agnostic core plugin (`name: 'ubean:core'`, `enforce: 'pre'`). Its
100
+ `config` hook registers the `client` and `ubean` environments plus `builder.buildApp` — so **bare
101
+ `vite dev | build | preview` is the full toolchain**, and `ubean dev | build | preview` are thin
102
+ aliases over it.
103
+
104
+ ```ts
105
+ import { defineConfig } from 'vite-plus';
106
+ import { ubeanPlugin } from '@ubean/build/vite';
107
+
108
+ export default defineConfig({
109
+ plugins: [ubeanPlugin()] // config is loaded from ubean.config.ts if not passed
110
+ });
111
+ ```
112
+
113
+ ```ts
114
+ // Explicit config (what `ubean dev` / `ubean build` do internally)
115
+ ubeanPlugin({ config: resolvedConfig, registry: createVirtualRegistry() });
116
+ ```
117
+
118
+ | Hook | What it does |
119
+ | --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
120
+ | `config` | Loads `ubean.config.ts` if needed, runs the project dependency check, registers `environments` + `builder.buildApp` |
121
+ | `buildStart` | Scans the project, primes the dev-scan baseline, applies the scan, runs codegen |
122
+ | `resolveId` / `load` | Serves the virtual modules (`ubean:routes`, `ubean:pages`, `ubean:meta`, `ubean:app-config`, `ubean:locales`) + `virtual:ubean-asset-manifest` |
123
+ | `transform` | Extracts / strips `definePage` macros |
124
+ | `configureServer` | Registers the dev-scan coordinator and generates OpenAPI types on `listening` |
125
+ | `renderChunk` / `resolveId` | Worker targets only: replaces `node:*` with throwing stubs, rewrites `import.meta.url` |
126
+ | `configurePreviewServer` | Attaches the preview middleware |
127
+
128
+ `UbeanPluginOptions`:
129
+
130
+ | Option | Type | Description |
131
+ | ---------- | ----------------------- | ---------------------------------------------------------------------------------------- |
132
+ | `config` | `UbeanResolvedConfig` | Pre-resolved config; otherwise read from the global cache or loaded in `buildStart` |
133
+ | `registry` | `VirtualModuleRegistry` | Virtual modules injected by the caller; the plugin creates its own instance when omitted |
134
+
135
+ **`experimental.viteBuilder` is gone.** The switch was deleted together with the two-`viteBuild`
136
+ legacy orchestration (ADR-0012). Writing it in `ubean.config.ts` is ignored as an unknown field —
137
+ there is no in-config escape hatch, only a version rollback.
138
+
139
+ **Worker targets.** When the active preset's `entryType` is `'worker'`, the plugin swaps `node:fs`
140
+ and friends for stubs that throw, and rewrites `import.meta.url` to `"file:///worker.mjs"` (workerd's
141
+ ESM leaves it `undefined`, which breaks bundler-generated `createRequire` interop shims at module
142
+ instantiation time). These only apply to the `ubean` environment.
143
+
144
+ ## Vue Plugin
145
+
146
+ Entry point: `@ubean/build/vue` (also the subpath's default export).
147
+
148
+ `ubeanVite()` provides the Vue-specific half: the page/app/client-entry/server-entry virtual modules,
149
+ `unplugin-auto-import`, `unplugin-vue-components`, the Vue i18n plugin, and markdown pages.
150
+
151
+ ```ts
152
+ import { ubeanVite } from '@ubean/build/vue';
153
+
154
+ ubeanVite({
155
+ config,
156
+ ssr: true, // optional
157
+ vue: true, // default: register @vitejs/plugin-vue
158
+ registry
159
+ });
160
+ ```
161
+
162
+ | Option | Type | Description |
163
+ | ---------- | ----------------------- | -------------------------------------------------------------------------------------------------------- |
164
+ | `config` | `UbeanResolvedConfig` | Required — the resolved ubean config |
165
+ | `ssr` | `boolean` | Optional SSR flag |
166
+ | `registry` | `VirtualModuleRegistry` | Same instance as `ubeanPlugin()`; without it `createVirtualRegistry()` usage is per-plugin |
167
+ | `vue` | `boolean` | Register `@vitejs/plugin-vue` (default **true**). Pass `false` when you bring your own Vue plugin config |
168
+
169
+ `ubeanVite` owns the `@vitejs/plugin-vue` registration because it is the only entry point a plain
170
+ `vite dev` goes through — if the CLI registered it instead, the same plugin would compile `.vue`
171
+ files twice. `VUE_PLUGIN_INCLUDE` is exported alongside it (`[/\.vue$/, /\.md$/]`).
172
+
173
+ Module factory helpers, for tooling that builds its own plugin array:
174
+
175
+ ```ts
176
+ import {
177
+ createVuePagesVirtualModule,
178
+ createVueAppEntryVirtualModule,
179
+ createClientEntryVirtualModule,
180
+ createServerEntryVirtualModule
181
+ } from '@ubean/build/vue';
182
+ ```
183
+
184
+ ## Virtual Modules
185
+
186
+ Entry point: `@ubean/build` (main).
187
+
188
+ Virtual modules are plain objects behind a registry, not globals. Framework paths build one with
189
+ `createVirtualRegistry()` and **inject** it into the plugins — a module-level singleton would leak
190
+ state across the environments of a single builder (ADR-0012 §6).
191
+
192
+ ```ts
193
+ import { createVirtualRegistry, defineVirtualModule, defineVirtualModulePrefix } from '@ubean/build';
194
+
195
+ const registry = createVirtualRegistry([
196
+ defineVirtualModule('virtual:ubean-app-config', () => 'export const config = { mode: "fullstack" };'),
197
+ defineVirtualModulePrefix('virtual:ubean-page:', id => `export default {}; // ${id}`)
198
+ ]);
199
+ ```
200
+
201
+ | Export | Description |
202
+ | ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------- |
203
+ | `VirtualModuleRegistry` | Registry class: `register` / `resolveId` / `load` / `getModules` / `invalidate` |
204
+ | `createVirtualRegistry(modules?)` | Build an independent instance (the recommended form) |
205
+ | `defineVirtualModule(id, loader)` | Exact-id module |
206
+ | `defineVirtualModulePrefix(prefix, loader)` | Prefix-matched module family |
207
+ | `useVirtualRegistry()` / `resetVirtualRegistry()` | Deprecated module-level singleton — kept for existing tests only |
208
+ | `VirtualModule`, `VirtualModuleResolver`, `VirtualModuleContext`, `VirtualModuleTransform` | Types |
209
+
210
+ Framework-provided modules:
211
+
212
+ | Factory | Virtual module |
213
+ | ---------------------------------------------------------------------------- | ---------------------------- |
214
+ | `createRoutingVirtualModule(routes, middlewares, cwd)` | API routes + middleware |
215
+ | `createPagesVirtualModule(pages, layouts, cwd)` | Page / layout records |
216
+ | `createMetaVirtualModule()` | Route metadata |
217
+ | `createAppVirtualModule(apiRoutes, middlewares, pages, srcDir)` | App config |
218
+ | `createLocalesVirtualModule(locales, defaultLocale, srcDir, i18nConfigJson)` | Locale data + `loadLocale()` |
219
+
220
+ ## Macros
221
+
222
+ Entry point: `@ubean/build` (main).
223
+
224
+ `definePage` is a compile-time macro. `transformMacros()` extracts and removes the call inside
225
+ `src/pages/`, `src/routes/`, and `src/middleware/`; `stripMacros()` is the plain stripper.
226
+
227
+ ```ts
228
+ import { transformMacros, stripMacros } from '@ubean/build';
229
+
230
+ transformMacros('definePage({ cache: true })', '/app/src/pages/index.vue'); // → ''
231
+ stripMacros('definePage({ name: "Home" }); const a = 1;'); // → '; const a = 1;'
232
+ ```
233
+
234
+ `MACRO_NAMES` contains **only** `definePage`. Adding `defineHandlerMeta` / `defineMiddleware` there
235
+ would strip runtime function calls and produce syntax errors — those are real runtime functions.
236
+
237
+ Vue SFCs keep an empty `<script setup>` block (as a comment) when stripping would leave it blank: a
238
+ page whose `definePage` was its only setup content, but which also has a plain `<script>` (the
239
+ documented shape for `loader` + `definePage`), would otherwise lose its default export and fail with
240
+ `MISSING_EXPORT "default"`.
241
+
242
+ ## Dev Server Bootstrap
243
+
244
+ Entry point: `@ubean/build/vite`.
245
+
246
+ These exports own dev request routing and the host app, so `vite dev` works without the CLI.
247
+
248
+ ```ts
249
+ import { bootstrapDevApp, ubeanDevRequestPlugin, ensureDevApp } from '@ubean/build/vite';
250
+ ```
251
+
252
+ | Export | Description |
253
+ | ------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- |
254
+ | `bootstrapDevApp(options)` | Create the Hono dev app from a scan, wire it to the Vite SSR graph, prepare `ready`, expose `rebuild` |
255
+ | `createDevApp(options)` | Scan (optional) + create the host app — no Vite server needed |
256
+ | `createDevAppReady(options)` | Idempotent first-request initialisation: locales, `defineServer`, `app.init()`, `onServerReady` |
257
+ | `ubeanDevRequestPlugin(options)` | The request-routing plugin: pre-classification + post fallback |
258
+ | `createUbeanRequestHandlers(server, options)` | The handlers used by the plugin |
259
+ | `getDevApp(server)` / `ensureDevApp(server)` | Read / lazily bootstrap the dev app associated with a Vite server |
260
+ | `enhanceDevApp(options)` | Attach the SSR-graph loaders and renderer to an existing Hono app |
261
+ | `isFrameworkHtmlPage(url)` | Framework-owned HTML pages (`/_devtools`, `/_scalar`, all `/__devframes/**`) |
262
+ | `isViteResourceRequest(url, headers)` | Whether the request belongs to Vite |
263
+ | `collectDevCssLinks(moduleGraph)` / `injectStylesheetLinks(html, hrefs)` | CSS link injection |
264
+ | `toWebRequest(req, host, protocol)` / `sendWebResponse(res, webRes)` | Node ↔ Web adapters |
265
+
266
+ Dev-scan coordination (one watcher, one scan, one reload order):
267
+
268
+ | Export | Description |
269
+ | --------------------------------------------------------------------------- | ------------------------------------------------------------------------------ |
270
+ | `DEV_SCAN_DIRS` | `routes`, `middleware`, `pages`, `layouts`, `plugins`, `app`, `api`, `locales` |
271
+ | `createDevScanCoordinator(options)` / `getDevScanCoordinator(server, init)` | Coordinator lifecycle |
272
+ | `onDevScan(server, handler)` | Subscribe to scan results |
273
+ | `peekDevScanCoordinator(server)` | Non-initialising read |
274
+
275
+ `DEVTOOLS_PASS_THROUGH_PREFIXES` (`['/__devframes', '/__devtools', '/_devtools']`) is the single
276
+ declaration of the reserved dev namespace — matched by **bare string prefix**, not by path segment,
277
+ because segment matching misses sibling paths.
278
+
279
+ Dev security headers are mode-aware:
280
+
281
+ ```ts
282
+ import { resolveDevSecurityHeaders } from '@ubean/build/vite';
283
+
284
+ // `ssg` mode defaults to OFF: static output ships no framework-injected headers.
285
+ resolveDevSecurityHeaders(config); // → boolean | Record<string, unknown>
286
+ ```
287
+
288
+ The worker-based dev environment (`UbeanDevEnvironment`, `createUbeanDevEnvironmentFactory`,
289
+ `createEnvRunnerTransport`, `bridgeEnvRunnerInvokes`, `UBEAN_DEV_ENV_NAME`) and its generated entry
290
+ (`DEV_WORKER_FILE`, `getDevWorkerEntrySource`, `writeDevWorkerEntry`, `invalidateDevWorkerModules`)
291
+ are exported from the same subpath. They are a runnable capability, **not** the wired-up dev path:
292
+ `createEnvironment` currently has no registration point, and SSR executes in the main process.
293
+
294
+ ## Preview
295
+
296
+ Entry point: `@ubean/build/vite`.
297
+
298
+ `ubeanPreviewPlugin()` / `attachPreviewMiddleware()` make `vite preview` serve **the artifact itself**:
299
+ fullstack / backend load `createFetchHandler()` from the built `dist/server/entry.mjs` in-process; spa /
300
+ ssg are served by a static resolver shared with the CLI's built-in server.
301
+
302
+ ```ts
303
+ import { ubeanPreviewPlugin, resolvePreviewFile, previewMimeType } from '@ubean/build/vite';
304
+
305
+ ubeanPreviewPlugin();
306
+ ```
307
+
308
+ | Export | Description |
309
+ | --------------------------------------------------------------------- | --------------------------------------- |
310
+ | `ubeanPreviewPlugin(options?)` | The preview plugin |
311
+ | `attachPreviewMiddleware(server, config, host?)` | Attach to an existing preview server |
312
+ | `createPreviewMiddleware(options)` | The connect middleware |
313
+ | `resolvePreviewFile(root, pathname, mode)` | Static resolution for `'spa'` / `'ssg'` |
314
+ | `sendPreviewFile(res, path)` / `sendPreviewStatus(res, status, text)` | Response helpers |
315
+ | `previewMimeType(filePath)` | MIME lookup |
316
+
317
+ Cloudflare artifacts preview through miniflare, which is an optional dependency:
318
+
319
+ ```ts
320
+ import { createCloudflarePreviewRunner } from '@ubean/build/vite';
321
+
322
+ const result = await createCloudflarePreviewRunner({ cwd, outDir });
323
+ // → { ok: false, reason: 'miniflare-missing', hint: MINIFLARE_INSTALL_HINT }
324
+ ```
325
+
326
+ `MINIFLARE_INSTALL_HINT` names the alternative: _Install it with `pnpm add -D miniflare`, or preview
327
+ with `wrangler dev` instead._
328
+
329
+ ## Server Actions
330
+
331
+ Entry point: `@ubean/build/actions`.
332
+
333
+ The plugin detects `defineAction()` call expressions and splits them per environment: **server**
334
+ injects `filePath` (and the inferred `name`) so the action ID is stable across dev and build;
335
+ **client** replaces the call with `createActionStub('<id>')`, an RPC stub that POSTs to `/__actions`.
336
+
337
+ It is already part of the composed `ubeanPlugin()` from `ubean/vite` — you only add it manually when
338
+ composing your own plugin array.
339
+
340
+ ```ts
341
+ import { ubeanServerActionsPlugin } from '@ubean/build/actions';
342
+
343
+ plugin: ubeanServerActionsPlugin({ root: process.cwd() });
344
+ ```
345
+
346
+ | Export | Description |
347
+ | ----------------------------------------------------------- | -------------------------------------------------------------------- |
348
+ | `findDefineActionCalls(code)` / `hasDefineActionCall(code)` | Detection |
349
+ | `transformActionsForServer(code, filePath, root)` | Inject `filePath` / `name` |
350
+ | `transformActionsForClient(code, filePath, root)` | Replace with `createActionStub(...)` |
351
+ | `ServerActionsPluginOptions` | `{ root?: string }` — defaults to `process.cwd()` at plugin creation |
352
+
353
+ Name inference order: the `name` property in the options object → the variable/property the result is
354
+ assigned to (`export const login = defineAction(...)` → `'login'`) → `'anonymous'`.
355
+
356
+ ## Production Build
357
+
358
+ Entry point: `@ubean/build/production`.
359
+
360
+ ```ts
361
+ import { buildWithEnvironments, getPresetBuildConfig, generateNodeServerEntry } from '@ubean/build/production';
362
+
363
+ const manifest = await buildWithEnvironments({
364
+ cwd,
365
+ config,
366
+ preset,
367
+ scanResult,
368
+ minify: true,
369
+ sourcemap: false,
370
+ contentSnapshot
371
+ });
372
+ ```
373
+
374
+ | Export | Description |
375
+ | ------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
376
+ | `buildWithEnvironments(options)` | One `createBuilder` for the whole build; returns a `BuildManifest` |
377
+ | `BuildOptions` | `cwd`, `config`, `preset`, `scanResult`, `minify?`, `sourcemap?`, `contentSnapshot?` |
378
+ | `BuildManifest` | `assets`, `entry`, `clientDir`, `serverDir`, `preset` |
379
+ | `getPresetBuildConfig(preset)` | `{ format, target, entryType, external }` — `entryType` is `'node'` / `'worker'` / `'fetch'` |
380
+ | `generateNodeServerEntry()` / `generateStandardHandlerEntry()` / `generateCloudflareWorkerEntry()` | Per-entry-type server bundles |
381
+ | `generateVirtualModulesToDisk(cwd, config, scanResult, virtualDir, preset, registry, contentSnapshot?)` | Build-time snapshot of the virtual modules |
382
+ | `serializePagesForEntry(pages)` | Serialize scanned pages into the server entry |
383
+ | `createIslandsSsrStubPlugin()` | Non-functional islands registry for SSR |
384
+
385
+ **`serializePagesForEntry()` is a whitelist, and the whitelist is load-bearing.** It previously
386
+ serialized only `relativePath` / `name` / `route` / `layout` / `reuseTarget` / `isReuse` / `pageMeta`,
387
+ so `matchers` and `slot` **disappeared from production builds** — dev used the live scanned objects
388
+ and behaved correctly, while a `[id=numeric]` route returned 404 in dev and 200 in production. Add a
389
+ field to `ScannedPage` and you must add it here too; `packages/builder/test/page-metadata.test.ts`
390
+ locks the list field by field.
391
+
392
+ `buildWithEnvironments()` declares "the build is caller-driven" (`UBEAN_BUILD_DRIVEN_BY_CLI=1`) for
393
+ its duration so the plugin's `config` hook does **not** also register its own `buildApp` — otherwise
394
+ the plugin's return value would overwrite the inline `builder` field and the build would silently
395
+ produce nothing.
396
+
397
+ ## Prerender
398
+
399
+ Entry point: `@ubean/build/prerender`.
400
+
401
+ `prerender()` drives a `fetcher` over a collected route list, writes HTML files, and can externalize
402
+ the page payload as `__data.json`.
403
+
404
+ ```ts
405
+ import { prerender, collectPrerenderRoutes, routeToFilePath } from '@ubean/build/prerender';
406
+
407
+ const result = await prerender({
408
+ cwd,
409
+ outputDir: 'dist',
410
+ pages,
411
+ prerender: { all: true },
412
+ fetcher: async url => ({ html: await render(url), statusCode: 200 }),
413
+ expandRoutes: routes => routes, // i18n expansion hook
414
+ notFoundRoute: true // static SSG: enqueue the 404 sentinel → 404.html
415
+ });
416
+ ```
417
+
418
+ | Export | Description |
419
+ | -------------------------------------------------------- | ------------------------------------------------------------------------------- |
420
+ | `prerender(options)` | Full run → `PrerenderResult` |
421
+ | `collectPrerenderRoutes(pages, options)` | `{ routes, skipped }`; merges `include` + route-rule discovery + content routes |
422
+ | `extractPrerenderRoutesFromRules(routeRules)` | Routes marked `prerender: true` or `ppr: true` |
423
+ | `extractLinks(html, baseUrl?)` | Crawl support |
424
+ | `routeToFilePath(route, outputDir)` | `/` → `index.html`, `404` sentinel → `404.html`, `/robots.txt` preserved |
425
+ | `routeToDataFilePath(route, outputDir)` | `__data.json` path per route |
426
+ | `extractDataPayload(html, route)` | Pull `__UBEAN_DATA__` out of HTML → `ExtractedPayload` |
427
+ | `writePrerenderedFile(filePath, html)` | Write helper |
428
+ | `resolvePrerenderStaticDir(cwd, buildOutputDir, config)` | Resolves `<outputDir>/public` — no longer a hardcoded `dist/public` |
429
+ | `generatePrerenderManifest(result, baseUrl?)` | Manifest for deployment tooling |
430
+
431
+ `PrerendererOptions.fetcher` is the isomorphism point: the fullstack path supplies `createSsrFetcher`
432
+ (imports the real server entry and drives the Hono pipeline per route), the SSG path supplies the
433
+ direct renderer from `./static-render`.
434
+
435
+ ## Static Render Path
436
+
437
+ Entry point: `@ubean/build/static-render`.
438
+
439
+ `mode: 'ssg'` renders through a **minimal static entry** instead of the request pipeline (ADR-0011).
440
+ `createStaticSsgRenderer()` loads the built static entry and returns the same fetcher contract, so
441
+ `prerender()` needs no changes.
442
+
443
+ ```ts
444
+ import { createStaticSsgRenderer, matchRoutePattern, expandRoutesForLocales } from '@ubean/build/static-render';
445
+
446
+ const renderer = await createStaticSsgRenderer(cwd, manifest, {
447
+ pages,
448
+ notFoundPage,
449
+ i18n: { enabled: true, strategy: 'prefix_except_default', defaultLocale: 'en', locales: ['en', 'zh'] }
450
+ });
451
+
452
+ const result = await prerender({
453
+ ...options,
454
+ expandRoutes: renderer.expandRoutes,
455
+ fetcher: renderer.fetcher
456
+ });
457
+ ```
458
+
459
+ | Export | Description |
460
+ | ---------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
461
+ | `createStaticSsgRenderer(cwd, manifest, init)` | Loads `dist/server/entry.mjs`; returns `StaticSsgRenderer` or `undefined` on failure |
462
+ | `StaticSsgRenderer` | `{ fetcher, expandRoutes, renderStaticPage }` |
463
+ | `StaticSsgRenderer.renderStaticPage` | The function generated into the built static entry (built artifact, not an import from this subpath) |
464
+ | `compilePageRoute(page)` | Compile a scanned page into a matchable route |
465
+ | `matchRoutePattern(pages, path)` | Match with specificity ordering; returns `StaticRouteMatch \| null` |
466
+ | `expandRoutesForLocales(routes, i18n)` | Per-strategy URL expansion |
467
+ | `STATIC_NOT_FOUND_ROUTE` | `'/__ubean_404__'` — the 404 sentinel route |
468
+
469
+ **What the direct path does not have** (ADR-0011): no Hono app, no API routes, no middleware, no
470
+ crons, no IPX, no preset wrapper files. Page `loader` is **not executed** — exporting one logs a
471
+ one-time warning, and data must come from content collections, module constants, or client hydration.
472
+ Server actions, ISR / PPR / streaming, and route-rule redirect/rewrite have no execution point in
473
+ static output. The fullstack `prerender` path is the opposite: it drives the real server entry per
474
+ route, so loaders and middleware run.
475
+
476
+ `matchRoutePattern` understands both dialects: the scanner's (`:param`, `**:slug`, `:id?`) and
477
+ vue-router syntax coming from `definePage({ path })` (`:param(regex)`, `:param*`, `:param+`).
478
+ Parameters are `decodeURIComponent`-ed to match `c.req.param()`.
479
+
480
+ ## Codegen
481
+
482
+ Entry point: `@ubean/build/codegen`.
483
+
484
+ ```ts
485
+ import { generateTypes, CODEGEN_CONTRACT_VERSION, CODEGEN_FILES } from '@ubean/build/codegen';
486
+
487
+ const result = await generateTypes(scanResult, {
488
+ cwd,
489
+ srcDir: 'src',
490
+ buildDir: '.ubean',
491
+ autoImports: config.autoImports,
492
+ components: config.components
493
+ });
494
+ ```
495
+
496
+ | Export | Description |
497
+ | ---------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
498
+ | `generateTypes(result, options)` | Writes all declarations, returns `CodegenResult` + `codegen.manifest.json` |
499
+ | `generateRouteTypes(result, options)` | `routes.d.ts` (`ubean:routes`: `ApiRouteMap`, `ApiRoutePath`, `ApiMethod`) |
500
+ | `generatePageTypes(result, options)` | `pages.d.ts` (`ubean:pages`: `RouteName`, `LayoutName`) |
501
+ | `generateI18nTypes(result, options)` / `messagesToTsType(value, indent?)` | `i18n.d.ts` (`vue-i18n` `DefineLocaleMessage`) |
502
+ | `generateOpenApiTypes(schema, options)` | `openapi.d.ts` from an OpenAPI schema |
503
+ | `generateOpenApiTypesFromServer(baseUrl, options)` | Same, fetching `/_openapi.json` from a base URL |
504
+ | `generateAutoImports(scanResult, options)` | Auto-import declarations from the scan |
505
+ | `CODEGEN_CONTRACT_VERSION` / `CODEGEN_FILES` / `CodegenManifest` / `CodegenFileContract` | The emitted-file contract (versioned for studio / IDE consumers) |
506
+
507
+ Auto-import preset resolution — shared by the runtime plugin and codegen:
508
+
509
+ ```ts
510
+ import { resolveAutoImportsConfig, resolveComponentsConfig, getAutoImportPresets } from '@ubean/build/codegen';
511
+
512
+ const resolved = resolveAutoImportsConfig(config.autoImports);
513
+ const presets = getAutoImportPresets(resolved);
514
+ ```
515
+
516
+ `ResolvedAutoImports` decomposes `boolean | AutoImportOptions` into per-library switches — `enabled`,
517
+ `ubean` (default `true`), `vue`, `vueRouter`, `vueI18n`, `honoOpenapi` (the rest default `false`) — plus
518
+ the raw `options` for pass-through. The preset constants are exported too: `VUE_PRESET`,
519
+ `VUE_MACROS_PRESET`, `UBEAN_CLIENT_PRESET`, `VUE_ROUTER_PRESET`, `VUE_I18N_PRESET`,
520
+ `UBEAN_SERVER_PRESET`, `HONO_OPENAPI_PRESET`, `BUILTIN_PRESETS`, alongside `toArray`,
521
+ `getBuiltinComposables`, `getUbeanAutoImportConfig`, `getUbeanComponentsConfig`, and
522
+ `generateImportsTransform`. The paired-component halves (`**/*.server.vue`, `**/*.client.vue`) are
523
+ detected during the component scan, so they are declared from the same codegen run.
524
+
525
+ **Where codegen runs is part of the contract.** Previously only the CLI ran it, so bare
526
+ `vite dev` / `vite build` produced no `.ubean/*.d.ts` at all and user type-checks failed. The plugin
527
+ now runs `generateTypes()` in `buildStart` and yields when `UBEAN_CODEGEN_BY_CLI=1` or
528
+ `UBEAN_BUILD_DRIVEN_BY_CLI=1` is set, so exactly one executor writes each artifact.
529
+
530
+ ## Type Safety
531
+
532
+ - Every subpath ships `.d.ts` files (`types` is declared per export condition in `package.json`).
533
+ - `CodegenResult` returns the path of every artifact it wrote, so callers never guess at filenames.
534
+ - `CodegenManifest` + `CODEGEN_CONTRACT_VERSION` are the machine-readable contract for external
535
+ consumers (studio, IDE plugins) — `codegen.manifest.json` records, per file, its virtual module,
536
+ whether it is required, and whether it was actually generated this run.
537
+ - `PresetBuildConfig` is `ReturnType<typeof getPresetBuildConfig>`, so the server-environment
538
+ external lists stay in sync with the preset implementation.
539
+ - `VirtualModuleRegistry` implements `VirtualModuleResolver`, the read-only view the plugins consume —
540
+ plugin code cannot mutate the registry it was handed.
541
+ - Config typing flows through `@ubean/config`'s `ResolvedConfig`; every option in this package takes
542
+ that type rather than redeclaring config shapes.
543
+
544
+ ## Architecture Notes
545
+
546
+ - **Lifecycle ownership (ADR-0012).** The `config` hook registers `client` + `ubean` environments and
547
+ `builder.buildApp`; `vite dev | build | preview` are therefore the complete toolchain. The legacy
548
+ two-`viteBuild` orchestration and the `experimental.viteBuilder` switch **were deleted** — writing
549
+ the field now is silently ignored. Rollback is a version rollback, deliberately: a switch that can
550
+ change semantics silently costs more than a version rollback.
551
+ - **Three phases, two drivers.** `prepareBuild` → `runEnvBuilds` are shared; `buildWithEnvironments()`
552
+ owns its builder, while the plugin path lets Vite create the builder and only calls the two phases.
553
+ Calling `buildWithEnvironments()` from inside `buildApp` would recurse.
554
+ - **No `ssr` environment.** Deliberately not aligned with Nitro: ubean is Vue-only and the server
555
+ entry already fuses the Hono app with the Vue renderer, so a third environment would buy a
556
+ cross-environment singleton proxy and nothing else (ADR-0012 §2).
557
+ - **Output layout is `dist/{public,server}` + `dist/manifest.json`.** `.output/` is deliberately not
558
+ adopted — the layout is promised by the docs, by preview takeover, and by `analyze`'s baseline
559
+ reading (ADR-0012 §5).
560
+ - **Prerender stays after the server bundle.** Both render paths consume the built artifact, so
561
+ prerender cannot move before packing (a second deliberate deviation from Nitro, ADR-0012 §4).
562
+ - **Asset tags are inlined at build time**, not read from `<public>/.vite/manifest.json` at runtime.
563
+ - **Never import this package from runtime route code.** It pulls the whole build toolchain into the
564
+ server bundle: wasted size on Node, and a hard build failure on workers because the toolchain's
565
+ optional dependencies (`velocityjs`, `atpl`, …) cannot be bundled. Static assets and caching go
566
+ through the platform layer (ADR-0013).
567
+ - **`env-runner` must not become a dependency of this package** — it stays in `@ubean/cli`. Adding it
568
+ here triggers pnpm peer-variant drift, which shows up as `Plugin` type identity mismatches. The dev
569
+ environment code uses a local structural interface (`EnvRunnerLike`) instead.
570
+ - **Cross-instance shared state goes on `globalThis`, not in a module-level `Map`.** The dev SSR graph
571
+ inlines `ubean` while externalising `@ubean/vue`, so two module instances coexist and read different
572
+ registries with no error. The component/CSS resolver registry follows this rule.