@ubean/build 0.6.7 → 0.6.9

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
@@ -383,7 +383,7 @@ const manifest = await buildWithEnvironments({
383
383
  | `createIslandsSsrStubPlugin()` | Non-functional islands registry for SSR |
384
384
 
385
385
  **`serializePagesForEntry()` is a whitelist, and the whitelist is load-bearing.** It previously
386
- serialized only `relativePath` / `name` / `route` / `layout` / `reuseTarget` / `isReuse` / `pageMeta`,
386
+ serialized only `relativePath` / `name` / `route` / `layout` / `reuseTarget` / `pageKind` / `pageMeta`,
387
387
  so `matchers` and `slot` **disappeared from production builds** — dev used the live scanned objects
388
388
  and behaved correctly, while a `[id=numeric]` route returned 404 in dev and 200 in production. Add a
389
389
  field to `ScannedPage` and you must add it here too; `packages/builder/test/page-metadata.test.ts`
package/README.zh-CN.md CHANGED
@@ -373,7 +373,7 @@ const manifest = await buildWithEnvironments({
373
373
  | `createIslandsSsrStubPlugin()` | SSR 期的 islands 注册表空壳 |
374
374
 
375
375
  **`serializePagesForEntry()` 是一份白名单,而这份白名单承载语义。** 它此前只序列化
376
- `relativePath` / `name` / `route` / `layout` / `reuseTarget` / `isReuse` / `pageMeta`,于是 `matchers` 与 `slot`
376
+ `relativePath` / `name` / `route` / `layout` / `reuseTarget` / `pageKind` / `pageMeta`,于是 `matchers` 与 `slot`
377
377
  **在生产构建里消失** —— dev 用扫描得到的活对象因此一切正常,而带 `[id=numeric]` 的路由在 dev 返回 404、
378
378
  在生产返回 200。给 `ScannedPage` 加字段就必须同步加到这里;`packages/builder/test/page-metadata.test.ts`
379
379
  逐字段锁住这份名单。
@@ -1 +1 @@
1
- export { d as BUILTIN_PRESETS, t as CODEGEN_CONTRACT_VERSION, n as CODEGEN_FILES, r as CODEGEN_NON_CONTRACT_FILES, p as HONO_OPENAPI_PRESET, m as UBEAN_CLIENT_PRESET, h as UBEAN_SERVER_PRESET, g as VUE_I18N_PRESET, _ as VUE_MACROS_PRESET, v as VUE_PRESET, y as VUE_ROUTER_PRESET, b as generateAutoImports, l as generateI18nTypes, x as generateImportsTransform, a as generateOpenApiTypes, o as generateOpenApiTypesFromServer, s as generatePageTypes, c as generateRouteTypes, i as generateTypes, S as getAutoImportPresets, C as getBuiltinComposables, w as getUbeanAutoImportConfig, T as getUbeanComponentsConfig, u as messagesToTsType, E as resolveAutoImportsConfig, D as resolveComponentsConfig, O as toArray } from "../codegen-CUlE95gB.js";
1
+ export { d as BUILTIN_PRESETS, t as CODEGEN_CONTRACT_VERSION, n as CODEGEN_FILES, r as CODEGEN_NON_CONTRACT_FILES, p as HONO_OPENAPI_PRESET, m as UBEAN_CLIENT_PRESET, h as UBEAN_SERVER_PRESET, g as VUE_I18N_PRESET, _ as VUE_MACROS_PRESET, v as VUE_PRESET, y as VUE_ROUTER_PRESET, b as generateAutoImports, l as generateI18nTypes, x as generateImportsTransform, a as generateOpenApiTypes, o as generateOpenApiTypesFromServer, s as generatePageTypes, c as generateRouteTypes, i as generateTypes, S as getAutoImportPresets, C as getBuiltinComposables, w as getUbeanAutoImportConfig, T as getUbeanComponentsConfig, u as messagesToTsType, E as resolveAutoImportsConfig, D as resolveComponentsConfig, O as toArray } from "../codegen-RzEXvOPR.js";
@@ -1,4 +1,4 @@
1
- import { r as resolveEnvModules } from "./types-JZjwXWJd.js";
1
+ import { r as resolveEnvModules } from "./types-C2WT1hXQ.js";
2
2
  import { isAbsolute, join, normalize, relative } from "pathe";
3
3
  import { mkdir, readFile, writeFile } from "node:fs/promises";
4
4
  import { glob } from "tinyglobby";
@@ -33,11 +33,34 @@ function tsTypeOfLiteral(value) {
33
33
  if (typeof value === "boolean") return "boolean";
34
34
  return "string";
35
35
  }
36
- function renderModuleTypes(moduleId, fields) {
37
- if (fields.length === 0) return `declare module '${moduleId}' {\n // env.schema is empty\n}\n`;
38
- return `declare module '${moduleId}' {\n${fields.map((f) => {
39
- return ` export const ${f.key}: ${f.type}${f.optional ? " | undefined" : ""};`;
40
- }).join("\n")}\n}\n`;
36
+ /**
37
+ * 把 `envField.*({ description })` 渲染成 JSDoc 行。
38
+ *
39
+ * 转义行内注释终止符:说明文字里出现它就提前闭合注释,后面的声明会变成语法错误 ——
40
+ * 而报错位置指向生成的 `.d.ts`(用户不该去改的文件),完全看不出根因。
41
+ * 多行说明逐行加 ` * ` 前缀,否则第二行会跑到注释外面。
42
+ */
43
+ function renderJsDoc(description) {
44
+ const lines = description.replaceAll("*/", "*\\/").split(/\r?\n/).map((line) => line.trimEnd());
45
+ if (lines.length === 1) return [` /** ${lines[0]} */`];
46
+ return [
47
+ " /**",
48
+ ...lines.map((line) => ` * ${line}`.trimEnd()),
49
+ " */"
50
+ ];
51
+ }
52
+ function renderModuleTypes(moduleId, fields, extraLines = []) {
53
+ const lines = fields.flatMap((f) => {
54
+ const decl = ` export const ${f.key}: ${f.type}${f.optional ? " | undefined" : ""};`;
55
+ return f.description ? [...renderJsDoc(f.description), decl] : [decl];
56
+ });
57
+ const body = [
58
+ ...lines,
59
+ ...lines.length > 0 && extraLines.length > 0 ? [""] : [],
60
+ ...extraLines
61
+ ];
62
+ if (body.length === 0) return `declare module '${moduleId}' {\n // env.schema is empty\n}\n`;
63
+ return `declare module '${moduleId}' {\n${body.join("\n")}\n}\n`;
41
64
  }
42
65
  /**
43
66
  * 从 `import.meta.env` 快照生成 `ImportMetaEnv`。
@@ -59,6 +82,15 @@ function renderImportMetaEnv(importMetaEnv) {
59
82
  }).join("\n")}\n}\n`;
60
83
  }
61
84
  /**
85
+ * `renderEnvServerModule()` 生成的源码除了字段还会 `export function getSecret(key)`
86
+ * (`packages/builder/src/env/types.ts`),但早先这里只声明字段 —— 用户按
87
+ * `docs/contracts/env-v1.md` 的指引去 import 它,`tsc` 直接报 TS2305。
88
+ *
89
+ * 源码里还有一个 `__env`,它是生成代码的内部聚合对象,刻意**不**声明:声明了就等于
90
+ * 把它纳入公开 API,而它有更好的具名导出可用。
91
+ */
92
+ const ENV_SERVER_EXTRA_EXPORTS = [" /** 读取未在 `env.schema` 里声明的变量(动态 / per-request 场景)。 */", " export function getSecret(key: string): string | undefined;"];
93
+ /**
62
94
  * 生成 `.ubean/env.d.ts`。
63
95
  *
64
96
  * 顶层**不能**出现 `export {}`:`ImportMetaEnv` 的声明必须留在全局作用域,一旦文件变成
@@ -77,7 +109,7 @@ interface ImportMeta {
77
109
  readonly env: ImportMetaEnv;
78
110
  }
79
111
 
80
- ${renderModuleTypes("ubean:env/client", modules.client)}${renderModuleTypes("ubean:env/server", modules.server)}`;
112
+ ${renderModuleTypes("ubean:env/client", modules.client)}${renderModuleTypes("ubean:env/server", modules.server, ENV_SERVER_EXTRA_EXPORTS)}`;
81
113
  await mkdir(outDir, { recursive: true });
82
114
  const outPath = join(outDir, fileName);
83
115
  await writeFile(outPath, contents, "utf8");
@@ -20,6 +20,8 @@ interface EnvFieldSpec {
20
20
  /** TypeScript 字面量类型,直接写进 `.ubean/env.d.ts`。 */
21
21
  type: string;
22
22
  optional: boolean;
23
+ /** 字段说明(`envField.*({ description })`),写进 `.ubean/env.d.ts` 的 JSDoc。 */
24
+ description?: string;
23
25
  /**
24
26
  * 已解析的值:
25
27
  * - `context: 'client'` → 构建期内联用的字面量(来自 `import.meta.env` / `.env`,已含 default 回退)
@@ -1,4 +1,4 @@
1
- import { n as renderEnvServerModule, r as resolveEnvModules, t as renderEnvClientModule } from "../types-JZjwXWJd.js";
1
+ import { n as renderEnvServerModule, r as resolveEnvModules, t as renderEnvClientModule } from "../types-C2WT1hXQ.js";
2
2
  import { loadEnv } from "vite";
3
3
  //#region src/env/plugin.ts
4
4
  /**
package/dist/index.d.ts CHANGED
@@ -109,7 +109,12 @@ export declare function ssrSingletonProdOptimizeExclude(extra?: string[]): strin
109
109
  export declare function createRoutingVirtualModule(routes: CompiledRoute[], middlewares: CompiledMiddleware[], cwd: string): VirtualModule;
110
110
  export declare function createPagesVirtualModule(pages: CompiledPage[], layouts: CompiledLayout[], cwd: string): VirtualModule;
111
111
  export declare function createMetaVirtualModule(): VirtualModule;
112
- export declare function createAppVirtualModule(apiRoutes: ScannedApiRoute[], middlewares: ScannedMiddleware[], pages: ScannedPageRoute[], srcDir: string): VirtualModule;
112
+ export declare function createAppVirtualModule(apiRoutes: ScannedApiRoute[], middlewares: ScannedMiddleware[], pages: ScannedPageRoute[], srcDir: string,
113
+ /**
114
+ * 页面 glob 扩展名(逗号分隔,无点),来自 `pageGlobExtensions()`。
115
+ * 不传时用全开集合,仅兼容旧调用方式。
116
+ */
117
+ pageExtensions?: string): VirtualModule;
113
118
  export declare function createLocalesVirtualModule(_locales: ScannedLocale[], defaultLocale: string | undefined, srcDir?: string, i18nConfigJson?: string): VirtualModule;
114
119
  //#endregion
115
120
  export { type VirtualModule, type VirtualModuleContext, VirtualModuleRegistry, type VirtualModuleResolver, type VirtualModuleTransform, createVirtualRegistry, defineVirtualModule, defineVirtualModulePrefix, resetVirtualRegistry, useVirtualRegistry };
package/dist/index.js CHANGED
@@ -1,3 +1,3 @@
1
1
  import { _ as defineVirtualModule, a as ssrSingletonProdOptimizeExclude, b as useVirtualRegistry, c as getComponentResolvers, d as registerComponentResolver, f as registerCssImport, g as createVirtualRegistry, h as VirtualModuleRegistry, i as ssrSingletonDevPolicy, l as getCssImports, m as resetModuleRegistry, n as SSR_SINGLETON_OPTIMIZE_EXCLUDE, o as ssrSingletonProdSsr, p as registerHeadScript, r as SSR_SINGLETON_PACKAGES, t as SSR_SINGLETON_DEDUPE, u as getHeadScripts, v as defineVirtualModulePrefix, y as resetVirtualRegistry } from "./ssr-singleton-CUHZLgiO.js";
2
- import { a as createRoutingVirtualModule, i as createPagesVirtualModule, n as createLocalesVirtualModule, o as stripMacros, r as createMetaVirtualModule, s as transformMacros, t as createAppVirtualModule } from "./virtual-modules-B3aE-Fmn.js";
2
+ import { a as createRoutingVirtualModule, i as createPagesVirtualModule, n as createLocalesVirtualModule, o as stripMacros, r as createMetaVirtualModule, s as transformMacros, t as createAppVirtualModule } from "./virtual-modules-7mD1FgVQ.js";
3
3
  export { SSR_SINGLETON_DEDUPE, SSR_SINGLETON_OPTIMIZE_EXCLUDE, SSR_SINGLETON_PACKAGES, VirtualModuleRegistry, createAppVirtualModule, createLocalesVirtualModule, createMetaVirtualModule, createPagesVirtualModule, createRoutingVirtualModule, createVirtualRegistry, defineVirtualModule, defineVirtualModulePrefix, getComponentResolvers, getCssImports, getHeadScripts, registerComponentResolver, registerCssImport, registerHeadScript, resetModuleRegistry, resetVirtualRegistry, ssrSingletonDevPolicy, ssrSingletonProdOptimizeExclude, ssrSingletonProdSsr, stripMacros, transformMacros, useVirtualRegistry };
@@ -46,7 +46,7 @@ export interface BuildManifest {
46
46
  /**
47
47
  * 把扫描到的页面序列化进服务端入口(`const _pages = [...]`)。
48
48
  *
49
- * **白名单必须覆盖语义字段**:此前只序列化了 `relativePath/name/route/layout/reuseTarget/isReuse/
49
+ * **白名单必须覆盖语义字段**:此前只序列化了 `relativePath/name/route/layout/reuseTarget/pageKind/
50
50
  * pageMeta`,于是 `matchers` / `slot` **在生产构建里丢失** ——
51
51
  * dev 用扫描得到的活对象(`enhanceDevApp` / `buildDevSsrRoutes` 直接拿 `scanResult.pages`),所以本地
52
52
  * 一切正常,只有构建产物坏掉:带着 `[id=numeric]` 的路由在 dev 返回 404、在生产返回 200(实测)。
@@ -1 +1 @@
1
- export { T as buildWithEnvironments, v as createIslandsSsrStubPlugin, y as generateCloudflareWorkerEntry, b as generateNodeServerEntry, x as generateStandardHandlerEntry, S as generateVirtualModulesToDisk, C as getPresetBuildConfig, w as serializePagesForEntry } from "./vite--HujaLpA.js";
1
+ export { T as buildWithEnvironments, v as createIslandsSsrStubPlugin, y as generateCloudflareWorkerEntry, b as generateNodeServerEntry, x as generateStandardHandlerEntry, S as generateVirtualModulesToDisk, C as getPresetBuildConfig, w as serializePagesForEntry } from "./vite-k9_n0K9x.js";
@@ -10,6 +10,17 @@ import { isEnvFieldMeta } from "@ubean/shared";
10
10
  * 维度规则照抄 Astro `astro:env`:`client+public` / `server+public` / `server+secret` 三种合法,
11
11
  * `client+secret` 在**规范化阶段**就抛错(配置期失败,比构建期失败早)。
12
12
  */
13
+ /**
14
+ * 判断一个 schema 条目是不是**运行期**校验器(Standard Schema / `safeParse`)。
15
+ *
16
+ * 这类值不能进 `env.schema` —— 见 `resolveEnvModules()` 里的抛错说明。
17
+ */
18
+ function isRuntimeSchema(def) {
19
+ if (typeof def !== "object" || def === null) return false;
20
+ const standard = def["~standard"];
21
+ if (typeof standard === "object" && standard !== null && typeof standard.validate === "function") return true;
22
+ return typeof def.safeParse === "function";
23
+ }
13
24
  function typeOfField(def) {
14
25
  if (isEnvFieldMeta(def)) {
15
26
  if (def.type === "enum") return (def.values ?? []).map((v) => JSON.stringify(v)).join(" | ") || "string";
@@ -44,6 +55,8 @@ function resolveEnvModules(input) {
44
55
  const client = [];
45
56
  const server = [];
46
57
  for (const [key, def] of Object.entries(schema)) {
58
+ if (isRuntimeSchema(def)) throw new Error(`Invalid env field "${key}": a runtime schema (Standard Schema / safeParse) cannot be used in \`env.schema\` in ubean.config.ts.\nGenerated env modules are source files and only carry plain metadata, so the validator would be silently dropped.
59
+ Use \`envField.*({ context, access })\` here, or move the field into \`defineEnv({ schema })\` in server code if you need schema validation.`);
47
60
  const { context, access } = dimensionsOf(def);
48
61
  if (context === "client" && access === "secret") throw new Error(`Invalid env field "${key}": context 'client' cannot be combined with access 'secret'. Secrets never reach the client — use \`{ context: 'server', access: 'secret' }\`.`);
49
62
  const meta = isEnvFieldMeta(def) ? def : null;
@@ -56,6 +69,7 @@ function resolveEnvModules(input) {
56
69
  optional,
57
70
  sourceKey: key
58
71
  };
72
+ if (meta?.description) spec.description = meta.description;
59
73
  if (context === "client") {
60
74
  const raw = importMetaEnv[key] ?? dotEnv[key];
61
75
  const resolved = raw === void 0 || raw === "" ? meta?.default : raw;
@@ -1,5 +1,6 @@
1
1
  import { _ as defineVirtualModule } from "./ssr-singleton-CUHZLgiO.js";
2
2
  import { extractScriptBlocks, parseScriptAst, statementCall } from "@ubean/vue/vite";
3
+ import { resolvePageKind } from "@ubean/vue";
3
4
  import { isAbsolute, relative } from "pathe";
4
5
  //#region src/macros.ts
5
6
  /**
@@ -157,7 +158,7 @@ function createMetaVirtualModule() {
157
158
  return `export const UBEAN_VERSION = "0.0.1";\nexport default { version: UBEAN_VERSION };`;
158
159
  });
159
160
  }
160
- function createAppVirtualModule(apiRoutes, middlewares, pages, srcDir) {
161
+ function createAppVirtualModule(apiRoutes, middlewares, pages, srcDir, pageExtensions) {
161
162
  return defineVirtualModule("ubean:app-config", () => {
162
163
  const routesJson = apiRoutes.map((r) => JSON.stringify({
163
164
  relativePath: r.relativePath,
@@ -174,8 +175,7 @@ function createAppVirtualModule(apiRoutes, middlewares, pages, srcDir) {
174
175
  relativePath: p.relativePath,
175
176
  name: p.name,
176
177
  route: p.route,
177
- isReuse: p.isReuse,
178
- isMeta: p.isMeta,
178
+ pageKind: resolvePageKind(p),
179
179
  routable: p.routable,
180
180
  layout: p.layout
181
181
  }));
@@ -184,7 +184,7 @@ function createAppVirtualModule(apiRoutes, middlewares, pages, srcDir) {
184
184
  return `
185
185
  const routeModules = import.meta.glob([${JSON.stringify(`${prefix}/routes/**/*.{ts,js,mjs}`)}], { eager: false });
186
186
  const middlewareModules = import.meta.glob([${JSON.stringify(`${prefix}/middleware/**/*.{ts,js,mjs}`)}], { eager: false });
187
- const pageModules = import.meta.glob([${JSON.stringify(`${prefix}/pages/**/*.{vue,ts,tsx,js,jsx}`)}], { eager: false });
187
+ const pageModules = import.meta.glob([${JSON.stringify(`${prefix}/pages/**/*.{${pageExtensions ?? "vue,ts,tsx,js,jsx"}}`)}], { eager: false });
188
188
 
189
189
  const _srcPrefix = ${JSON.stringify(prefix)};
190
190
  function normalizeKey(p) {
@@ -1,9 +1,10 @@
1
1
  import { a as ssrSingletonProdOptimizeExclude, g as createVirtualRegistry, o as ssrSingletonProdSsr, x as __exportAll } from "./ssr-singleton-CUHZLgiO.js";
2
- import { a as createRoutingVirtualModule, i as createPagesVirtualModule, n as createLocalesVirtualModule, r as createMetaVirtualModule, s as transformMacros, t as createAppVirtualModule } from "./virtual-modules-B3aE-Fmn.js";
3
- import { a as generateOpenApiTypes, i as generateTypes } from "./codegen-CUlE95gB.js";
4
- import { _ as sendWebResponse, a as createServerEntryVirtualModule, d as ensureDevApp, i as createClientEntryVirtualModule, n as ubeanVite, o as createVueAppEntryVirtualModule, r as composeHeadScripts, s as createVuePagesVirtualModule, v as toWebRequest, x as getDevScanCoordinator } from "./vue-Z4wVSxSE.js";
2
+ import { a as createRoutingVirtualModule, i as createPagesVirtualModule, n as createLocalesVirtualModule, r as createMetaVirtualModule, s as transformMacros, t as createAppVirtualModule } from "./virtual-modules-7mD1FgVQ.js";
3
+ import { a as generateOpenApiTypes, i as generateTypes } from "./codegen-RzEXvOPR.js";
4
+ import { _ as sendWebResponse, a as createServerEntryVirtualModule, d as ensureDevApp, i as createClientEntryVirtualModule, n as ubeanVite, o as createVueAppEntryVirtualModule, r as composeHeadScripts, s as createVuePagesVirtualModule, v as toWebRequest, x as getDevScanCoordinator } from "./vue-lK-WDLZZ.js";
5
5
  import { createStaticSsgRenderer } from "./static-render.js";
6
6
  import { prerender, resolvePrerenderStaticDir } from "./prerender.js";
7
+ import { resolvePageKind } from "@ubean/vue";
7
8
  import { dirname, isAbsolute, join, relative, resolve } from "pathe";
8
9
  import * as nodeFs from "node:fs";
9
10
  import { existsSync, readFileSync, readdirSync, realpathSync, statSync } from "node:fs";
@@ -35,6 +36,35 @@ function localeVueParamFromI18n(config) {
35
36
  }) || void 0;
36
37
  }
37
38
  //#endregion
39
+ //#region src/page-glob.ts
40
+ /**
41
+ * 页面 glob 的扩展名集合。
42
+ *
43
+ * `import.meta.glob` 的范围必须与扫描器一致,否则会出现「扫描说不含 tsx、产物却
44
+ * 仍然把 `src/pages/*.tsx` 拉进构建」→ 构建期 `[PARSE_ERROR] Unexpected JSX expression`
45
+ * (关掉 JSX 时依然报错的真实原因)。
46
+ *
47
+ * 两类后缀与 `pages.extensions` 无关、始终参与:
48
+ * - `ts` / `js` —— `.reuse.ts` / `.meta.ts`(及其 `.js` 版本)是纯元数据文件
49
+ * (只含 `definePage`),扫描器用独立 pattern `**\/*.reuse.{ts,js}` /
50
+ * `**\/*.meta.{ts,js}` 收起,**不受 `extensions` 影响**。glob 缺了 ts/js,
51
+ * 元数据页能扫到却加载不到 → 路由表与实际可加载模块脱节。
52
+ * 顺带覆盖用户把 `ts` 显式写进 `extensions` 的情形。
53
+ * - `md` / `mdx` —— markdown 页面,由 `markdown` 开关决定加不加
54
+ */
55
+ function pageGlobExtensions(extensions, markdownExts = []) {
56
+ const base = extensions && extensions.length > 0 ? extensions : [
57
+ "vue",
58
+ "tsx",
59
+ "jsx"
60
+ ];
61
+ const all = new Set(base);
62
+ all.add("ts");
63
+ all.add("js");
64
+ for (const ext of markdownExts) all.add(ext);
65
+ return [...all].join(",");
66
+ }
67
+ //#endregion
38
68
  //#region src/ssg-entry.ts
39
69
  /**
40
70
  * 生成 renderer setup 代码块(`_rendererRoutes` + `_pageRenderer`)。
@@ -64,7 +94,7 @@ const _localeVueParam = ${JSON.stringify(localeVueParam)};
64
94
  const _pageToRoute = p => {
65
95
  // For reuse routes, load the target page's module (the .reuse.ts file
66
96
  // only contains definePage metadata, not a Vue component).
67
- const _targetPage = p.isReuse && p.reuseTarget ? _pages.find(tp => tp.name === p.reuseTarget) : undefined;
97
+ const _targetPage = p.pageKind === 'reuse' && p.reuseTarget ? _pages.find(tp => tp.name === p.reuseTarget) : undefined;
68
98
  const _loaderKey = _targetPage?.relativePath || p.relativePath;
69
99
  return {
70
100
  path: _localeVueParam
@@ -80,7 +110,10 @@ const _pageToRoute = p => {
80
110
  meta: {
81
111
  layout: p.layout === false ? false : p.layout || _defaultLayout,
82
112
  pageName: p.name,
83
- cache: p.cache === true ? true : undefined
113
+ cache: p.cache === true ? true : undefined,
114
+ /* 同客户端 buildRouteMeta:透传 definePage({ meta })。否则靠 meta(如 iframeUrl)
115
+ 取参数的页面在 SSR 首屏渲染空态,水合后才有值。 */
116
+ ...(p.pageMeta?.meta || undefined)
84
117
  }
85
118
  };
86
119
  };
@@ -264,7 +297,7 @@ export async function renderStaticPage(input) {
264
297
 
265
298
  // loader 检测 warn(模块加载有缓存,此处与渲染共用同一实例)
266
299
  const _targetPage =
267
- page.isReuse && page.reuseTarget ? _pages.find(tp => tp.name === page.reuseTarget) : undefined;
300
+ page.pageKind === 'reuse' && page.reuseTarget ? _pages.find(tp => tp.name === page.reuseTarget) : undefined;
268
301
  const _loaderKey = (_targetPage && _targetPage.relativePath) || page.relativePath;
269
302
  const _modLoader = pageLoaders[_loaderKey];
270
303
  if (_modLoader && !_loaderWarned.has(page.name)) {
@@ -1415,7 +1448,7 @@ function toVitePath(p) {
1415
1448
  /**
1416
1449
  * 把扫描到的页面序列化进服务端入口(`const _pages = [...]`)。
1417
1450
  *
1418
- * **白名单必须覆盖语义字段**:此前只序列化了 `relativePath/name/route/layout/reuseTarget/isReuse/
1451
+ * **白名单必须覆盖语义字段**:此前只序列化了 `relativePath/name/route/layout/reuseTarget/pageKind/
1419
1452
  * pageMeta`,于是 `matchers` / `slot` **在生产构建里丢失** ——
1420
1453
  * dev 用扫描得到的活对象(`enhanceDevApp` / `buildDevSsrRoutes` 直接拿 `scanResult.pages`),所以本地
1421
1454
  * 一切正常,只有构建产物坏掉:带着 `[id=numeric]` 的路由在 dev 返回 404、在生产返回 200(实测)。
@@ -1434,8 +1467,7 @@ function serializePagesForEntry(pages) {
1434
1467
  path: p.path,
1435
1468
  layout: p.layout,
1436
1469
  cache: p.cache,
1437
- isReuse: p.isReuse,
1438
- isMeta: p.isMeta,
1470
+ pageKind: resolvePageKind(p),
1439
1471
  routable: p.routable,
1440
1472
  isMarkdown: p.isMarkdown,
1441
1473
  reuseTarget: p.reuseTarget,
@@ -1453,6 +1485,8 @@ async function generateVirtualModulesToDisk(cwd, config, scanResult, virtualDir,
1453
1485
  const srcDirAbs = resolve(cwd, config.srcDir);
1454
1486
  const viteSrcDir = toVitePath(relative(cwd, srcDirAbs));
1455
1487
  const viteSrcPrefix = viteSrcDir ? `/${viteSrcDir}` : "";
1488
+ const mdPageExts = config.markdown?.enabled === false ? [] : config.markdown?.mdx === true ? ["md", "mdx"] : ["md"];
1489
+ const pagesExts = pageGlobExtensions(config.pages?.extensions, mdPageExts);
1456
1490
  registry.register(createRoutingVirtualModule(scanResult.apiRoutes.map((r) => ({
1457
1491
  method: r.method?.toUpperCase() || "ALL",
1458
1492
  path: r.route,
@@ -1481,7 +1515,7 @@ async function generateVirtualModulesToDisk(cwd, config, scanResult, virtualDir,
1481
1515
  registry.register(createVueAppEntryVirtualModule(scanResult.appEntry));
1482
1516
  registry.register(createClientEntryVirtualModule());
1483
1517
  }
1484
- registry.register(createAppVirtualModule(scanResult.apiRoutes, scanResult.middlewares, hasPages ? scanResult.pages : [], viteSrcPrefix || "/"));
1518
+ registry.register(createAppVirtualModule(scanResult.apiRoutes, scanResult.middlewares, hasPages ? scanResult.pages : [], viteSrcPrefix || "/", pagesExts));
1485
1519
  registry.register(createLocalesVirtualModule(scanResult.locales, scanResult.defaultLocale, viteSrcPrefix || "/", serializeI18nConfig(config.i18n)));
1486
1520
  if (hasServer) registry.register(createServerEntryVirtualModule(scanResult.serverEntry));
1487
1521
  async function writeModule(id, filename, header = "// Auto-generated by ubean\n") {
@@ -1501,7 +1535,7 @@ async function generateVirtualModulesToDisk(cwd, config, scanResult, virtualDir,
1501
1535
  }
1502
1536
  if (hasServer) await writeModule("virtual:ubean-server", "server-entry.ts", "");
1503
1537
  await writeFile(join(virtualDir, "islands-registry.ts"), "export const islands = {};\n", "utf-8");
1504
- const pagesGlob = JSON.stringify(`${viteSrcPrefix}/pages/**/*.{vue,ts,tsx,js,jsx,md,mdx}`);
1538
+ const pagesGlob = JSON.stringify(`${viteSrcPrefix}/pages/**/*.{${pagesExts}}`);
1505
1539
  const routesGlob = JSON.stringify(`${viteSrcPrefix}/routes/**/*.{ts,js,mjs}`);
1506
1540
  const middlewareGlob = JSON.stringify(`${viteSrcPrefix}/middleware/**/*.{ts,js,mjs}`);
1507
1541
  const layoutsGlob = JSON.stringify(`${viteSrcPrefix}/layouts/**/*.{vue,ts}`);
@@ -2598,7 +2632,8 @@ function ubeanPlugin(options) {
2598
2632
  cwd: ubeanConfig.rootDir,
2599
2633
  srcDir: ubeanConfig.srcDir,
2600
2634
  dirs: ubeanConfig.dir,
2601
- ignore: ubeanConfig.scanOptions?.ignore
2635
+ ignore: ubeanConfig.scanOptions?.ignore,
2636
+ pageExtensions: ubeanConfig.pages?.extensions
2602
2637
  });
2603
2638
  const { snapshot: contentSnapshot } = await loadContentForBuild(ubeanConfig.rootDir, ubeanConfig.content);
2604
2639
  await runEnvBuilds(builder, await prepareBuild(createBuildContext({
@@ -2682,6 +2717,7 @@ function ubeanPlugin(options) {
2682
2717
  srcDir: ubeanConfig.srcDir,
2683
2718
  dirs: ubeanConfig.dir,
2684
2719
  ignore: ubeanConfig.scanOptions?.ignore,
2720
+ pageExtensions: ubeanConfig.pages?.extensions,
2685
2721
  source,
2686
2722
  scan: () => scanProjectOnce(),
2687
2723
  reload: () => server.ws.send({ type: "full-reload" }),
@@ -2762,7 +2798,8 @@ function ubeanPlugin(options) {
2762
2798
  cwd: ubeanConfig.rootDir,
2763
2799
  srcDir: ubeanConfig.srcDir,
2764
2800
  dirs: ubeanConfig.dir,
2765
- ignore: ubeanConfig.scanOptions?.ignore
2801
+ ignore: ubeanConfig.scanOptions?.ignore,
2802
+ pageExtensions: ubeanConfig.pages?.extensions
2766
2803
  });
2767
2804
  }
2768
2805
  async function scanAndRegister() {
@@ -2840,7 +2877,7 @@ function ubeanPlugin(options) {
2840
2877
  isDefault: l.isDefault
2841
2878
  })), ubeanConfig.rootDir));
2842
2879
  virtualRegistry.register(createMetaVirtualModule());
2843
- virtualRegistry.register(createAppVirtualModule(result.apiRoutes, result.middlewares, result.pages, viteSrcPrefix || "/"));
2880
+ virtualRegistry.register(createAppVirtualModule(result.apiRoutes, result.middlewares, result.pages, viteSrcPrefix || "/", pageGlobExtensions(ubeanConfig.pages?.extensions, ubeanConfig.markdown?.enabled === false ? [] : ubeanConfig.markdown?.mdx === true ? ["md", "mdx"] : ["md"])));
2844
2881
  virtualRegistry.register(createLocalesVirtualModule(result.locales, result.defaultLocale, viteSrcPrefix || "/", serializeI18nConfig(ubeanConfig.i18n)));
2845
2882
  await maybeGenerateRouteFiles(ubeanConfig, result).catch((err) => {
2846
2883
  console.warn("[ubean:core] Route file generation failed:", err?.message || err);
package/dist/vite.d.ts CHANGED
@@ -404,6 +404,12 @@ interface DevSsrRoute {
404
404
  layout: string | string[] | false | null;
405
405
  pageName: string;
406
406
  cache?: true;
407
+ /**
408
+ * 其余键是 `definePage({ meta })` 的透传(`iframeUrl` / `title` / `i18nKey` …)。
409
+ * 不声明的话,用到这些 meta 的页面(例如按 `meta.iframeUrl` 取地址的内嵌页)
410
+ * 在 SSR 首屏渲染成空态,水合后客户端才有值 —— 首屏与水合结果不一致。
411
+ */
412
+ [key: string]: unknown;
407
413
  };
408
414
  }
409
415
  interface DevSsrRoutesOptions {
package/dist/vite.js CHANGED
@@ -1,4 +1,4 @@
1
1
  import { g as createVirtualRegistry } from "./ssr-singleton-CUHZLgiO.js";
2
- import { A as buildDevSsrRoutes, C as peekDevScanCoordinator, D as resolveDevSecurityHeaders, E as createDevAppReady, O as resolveLocaleVueParam, S as onDevScan, T as createDevApp, _ as sendWebResponse, b as createDevScanCoordinator, c as DEVTOOLS_PASS_THROUGH_PREFIXES, d as ensureDevApp, f as getDevApp, g as ubeanDevRequestPlugin, h as isViteResourceRequest, j as toVueRouterPagePath, k as enhanceDevApp, l as collectDevCssLinks, m as isFrameworkHtmlPage, p as injectStylesheetLinks, u as createUbeanRequestHandlers, v as toWebRequest, w as bootstrapDevApp, x as getDevScanCoordinator, y as DEV_SCAN_DIRS } from "./vue-Z4wVSxSE.js";
3
- import { D as createCloudflarePreviewRunner, E as MINIFLARE_INSTALL_HINT, O as defaultLoadMiniflare, _ as ubeanPreviewPlugin, a as createEnvRunnerTransport, c as getDevWorkerEntrySource, d as attachPreviewMiddleware, f as createPreviewMiddleware, g as sendPreviewStatus, h as sendPreviewFile, i as bridgeEnvRunnerInvokes, k as readCompatibilityDate, l as invalidateDevWorkerModules, m as resolvePreviewFile, n as UBEAN_DEV_ENV_NAME, o as createUbeanDevEnvironmentFactory, p as previewMimeType, r as UbeanDevEnvironment, s as DEV_WORKER_FILE, t as ubeanPlugin, u as writeDevWorkerEntry } from "./vite--HujaLpA.js";
2
+ import { A as buildDevSsrRoutes, C as peekDevScanCoordinator, D as resolveDevSecurityHeaders, E as createDevAppReady, O as resolveLocaleVueParam, S as onDevScan, T as createDevApp, _ as sendWebResponse, b as createDevScanCoordinator, c as DEVTOOLS_PASS_THROUGH_PREFIXES, d as ensureDevApp, f as getDevApp, g as ubeanDevRequestPlugin, h as isViteResourceRequest, j as toVueRouterPagePath, k as enhanceDevApp, l as collectDevCssLinks, m as isFrameworkHtmlPage, p as injectStylesheetLinks, u as createUbeanRequestHandlers, v as toWebRequest, w as bootstrapDevApp, x as getDevScanCoordinator, y as DEV_SCAN_DIRS } from "./vue-lK-WDLZZ.js";
3
+ import { D as createCloudflarePreviewRunner, E as MINIFLARE_INSTALL_HINT, O as defaultLoadMiniflare, _ as ubeanPreviewPlugin, a as createEnvRunnerTransport, c as getDevWorkerEntrySource, d as attachPreviewMiddleware, f as createPreviewMiddleware, g as sendPreviewStatus, h as sendPreviewFile, i as bridgeEnvRunnerInvokes, k as readCompatibilityDate, l as invalidateDevWorkerModules, m as resolvePreviewFile, n as UBEAN_DEV_ENV_NAME, o as createUbeanDevEnvironmentFactory, p as previewMimeType, r as UbeanDevEnvironment, s as DEV_WORKER_FILE, t as ubeanPlugin, u as writeDevWorkerEntry } from "./vite-k9_n0K9x.js";
4
4
  export { DEVTOOLS_PASS_THROUGH_PREFIXES, DEV_SCAN_DIRS, DEV_WORKER_FILE, MINIFLARE_INSTALL_HINT, UBEAN_DEV_ENV_NAME, UbeanDevEnvironment, attachPreviewMiddleware, bootstrapDevApp, bridgeEnvRunnerInvokes, buildDevSsrRoutes, collectDevCssLinks, createCloudflarePreviewRunner, createDevApp, createDevAppReady, createDevScanCoordinator, createEnvRunnerTransport, createPreviewMiddleware, createUbeanDevEnvironmentFactory, createUbeanRequestHandlers, createVirtualRegistry, defaultLoadMiniflare, enhanceDevApp, ensureDevApp, getDevApp, getDevScanCoordinator, getDevWorkerEntrySource, injectStylesheetLinks, invalidateDevWorkerModules, isFrameworkHtmlPage, isViteResourceRequest, onDevScan, peekDevScanCoordinator, previewMimeType, readCompatibilityDate, resolveDevSecurityHeaders, resolveLocaleVueParam, resolvePreviewFile, sendPreviewFile, sendPreviewStatus, sendWebResponse, toVueRouterPagePath, toWebRequest, ubeanDevRequestPlugin, ubeanPlugin, ubeanPreviewPlugin, writeDevWorkerEntry };
@@ -1,10 +1,11 @@
1
1
  import { _ as defineVirtualModule, c as getComponentResolvers, g as createVirtualRegistry, i as ssrSingletonDevPolicy, l as getCssImports, u as getHeadScripts } from "./ssr-singleton-CUHZLgiO.js";
2
- import { D as resolveComponentsConfig, E as resolveAutoImportsConfig, O as toArray, S as getAutoImportPresets, f as COMPONENT_HALF_GLOBS } from "./codegen-CUlE95gB.js";
2
+ import { D as resolveComponentsConfig, E as resolveAutoImportsConfig, O as toArray, S as getAutoImportPresets, f as COMPONENT_HALF_GLOBS } from "./codegen-RzEXvOPR.js";
3
3
  import { createRequire } from "node:module";
4
4
  import { generatePagesModuleSource } from "@ubean/vue/vite";
5
+ import { resolvePageKind } from "@ubean/vue";
5
6
  import { isAbsolute, join, normalize, relative, resolve } from "pathe";
6
7
  import { existsSync } from "node:fs";
7
- import { loadUbeanConfig, localePathsOf } from "@ubean/config";
8
+ import { loadUbeanConfig, localePathsOf, resolvePageCompilers } from "@ubean/config";
8
9
  import { scanProject } from "@ubean/scan";
9
10
  import { applyServerConfig, createUbeanApp } from "@ubean/app";
10
11
  import { getVueLocaleParam, toVueRouterLocalePath } from "@ubean/i18n";
@@ -54,7 +55,7 @@ function buildDevSsrRoutes(options) {
54
55
  const pageByName = /* @__PURE__ */ new Map();
55
56
  for (const page of routablePages) pageByName.set(page.name, page);
56
57
  const componentFor = (page) => {
57
- const fullPath = (page.isReuse && page.reuseTarget ? pageByName.get(page.reuseTarget) : void 0)?.fullPath || page.fullPath;
58
+ const fullPath = (resolvePageKind(page) === "reuse" && page.reuseTarget ? pageByName.get(page.reuseTarget) : void 0)?.fullPath || page.fullPath;
58
59
  return async () => await loadComponent(fullPath);
59
60
  };
60
61
  const routeFor = (page) => ({
@@ -64,7 +65,8 @@ function buildDevSsrRoutes(options) {
64
65
  meta: {
65
66
  layout: page.layout === false ? false : page.layout || defaultLayout,
66
67
  pageName: page.name,
67
- cache: page.cache === true ? true : void 0
68
+ cache: page.cache === true ? true : void 0,
69
+ ...page.pageMeta?.meta
68
70
  }
69
71
  });
70
72
  const grouped = /* @__PURE__ */ new Map();
@@ -120,7 +122,7 @@ function enhanceDevApp(options) {
120
122
  for (const page of app.options.pages || []) pageByName.set(page.name, page);
121
123
  const pageLoaders = {};
122
124
  for (const page of app.options.pages || []) {
123
- const fullPath = (page.isReuse && page.reuseTarget ? pageByName.get(page.reuseTarget) : void 0)?.fullPath || page.fullPath;
125
+ const fullPath = (resolvePageKind(page) === "reuse" && page.reuseTarget ? pageByName.get(page.reuseTarget) : void 0)?.fullPath || page.fullPath;
124
126
  pageLoaders[page.relativePath] = () => loadModule(fullPath);
125
127
  }
126
128
  app.options.pageLoaders = pageLoaders;
@@ -241,7 +243,8 @@ async function createDevApp(options) {
241
243
  cwd: rootDir,
242
244
  srcDir: config.srcDir,
243
245
  dirs: config.dir,
244
- ignore: config.scanOptions?.ignore
246
+ ignore: config.scanOptions?.ignore,
247
+ pageExtensions: config.pages?.extensions
245
248
  });
246
249
  const app = createUbeanApp({
247
250
  rootDir,
@@ -1547,6 +1550,23 @@ export function resolveServerConfig(mode) {
1547
1550
  //#endregion
1548
1551
  //#region src/vue-plugin.ts
1549
1552
  const VUE_PLUGIN_INCLUDE = [/\.vue$/, /\.md$/];
1553
+ /**
1554
+ * 懒加载 `@vitejs/plugin-vue-jsx`。
1555
+ *
1556
+ * 用 `createRequire` 而不是静态 import:只有真的写了 JSX 页面时才付 babel 链路的加载
1557
+ * 成本(`@babel/core` + `@babel/plugin-transform-typescript` + `@vue/babel-plugin-jsx`)。
1558
+ * 同时用 ESM interop 兼容默认导出与具名导出两种形状(不同版本各写过一种)。
1559
+ */
1560
+ function createVueJsxPlugin() {
1561
+ const require = createRequire(import.meta.url);
1562
+ let mod;
1563
+ try {
1564
+ mod = require("@vitejs/plugin-vue-jsx");
1565
+ } catch {
1566
+ throw new Error("[ubean] `pages.extensions` 含 `tsx`/`jsx`,需要 `@vitejs/plugin-vue-jsx`,但未解析到该包。\n pnpm add -D @vitejs/plugin-vue-jsx\n 或从 `ubean.config.ts` 去掉 JSX 扩展名(`pages: { extensions: ['vue'] }`)。");
1567
+ }
1568
+ return (mod?.default?.default ?? mod?.default ?? mod)();
1569
+ }
1550
1570
  const VIRTUAL_PAGES = "virtual:ubean-pages";
1551
1571
  const VIRTUAL_APP = "virtual:ubean-app";
1552
1572
  const VIRTUAL_CLIENT = "virtual:ubean-client-entry";
@@ -1620,7 +1640,8 @@ function ubeanVite(options) {
1620
1640
  cwd: ubeanConfig.rootDir,
1621
1641
  srcDir: ubeanConfig.srcDir,
1622
1642
  dirs: ubeanConfig.dir,
1623
- ignore: ubeanConfig.scanOptions?.ignore
1643
+ ignore: ubeanConfig.scanOptions?.ignore,
1644
+ pageExtensions: ubeanConfig.pages?.extensions
1624
1645
  });
1625
1646
  }
1626
1647
  async function scanAndRegister() {
@@ -1737,6 +1758,7 @@ function ubeanVite(options) {
1737
1758
  srcDir: ubeanConfig.srcDir,
1738
1759
  dirs: ubeanConfig.dir,
1739
1760
  ignore: ubeanConfig.scanOptions?.ignore,
1761
+ pageExtensions: ubeanConfig.pages?.extensions,
1740
1762
  source,
1741
1763
  scan: scanProjectOnce,
1742
1764
  reload: () => server.ws.send({ type: "full-reload" }),
@@ -1756,10 +1778,12 @@ function ubeanVite(options) {
1756
1778
  });
1757
1779
  }
1758
1780
  }];
1759
- if (options.vue !== false) plugins.unshift(vue({
1781
+ const pageCompilers = resolvePageCompilers(ubeanConfig.pages?.extensions ?? []);
1782
+ if (options.vue !== false && pageCompilers.vue) plugins.unshift(vue({
1760
1783
  include: VUE_PLUGIN_INCLUDE,
1761
1784
  template: { compilerOptions: { isCustomElement: (tag) => tag.startsWith("ubean-") } }
1762
1785
  }));
1786
+ if (pageCompilers.jsx) plugins.push(createVueJsxPlugin());
1763
1787
  if (ubeanConfig.i18n?.enabled !== false) plugins.push(VueI18nPlugin({
1764
1788
  include: [join(srcDir, "locales/**")],
1765
1789
  ssr: true,
package/dist/vue.js CHANGED
@@ -1 +1 @@
1
- export { t as VUE_PLUGIN_INCLUDE, r as composeHeadScripts, i as createClientEntryVirtualModule, a as createServerEntryVirtualModule, o as createVueAppEntryVirtualModule, s as createVuePagesVirtualModule, n as default, n as ubeanVite } from "./vue-Z4wVSxSE.js";
1
+ export { t as VUE_PLUGIN_INCLUDE, r as composeHeadScripts, i as createClientEntryVirtualModule, a as createServerEntryVirtualModule, o as createVueAppEntryVirtualModule, s as createVuePagesVirtualModule, n as default, n as ubeanVite } from "./vue-lK-WDLZZ.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ubean/build",
3
- "version": "0.6.7",
3
+ "version": "0.6.9",
4
4
  "description": "Vite plugins, production build, SSG prerender, type codegen, and Server Actions plugin for ubean",
5
5
  "files": [
6
6
  "dist"
@@ -49,20 +49,21 @@
49
49
  },
50
50
  "dependencies": {
51
51
  "@intlify/unplugin-vue-i18n": "11.2.7",
52
- "@ubean/app": "0.6.7",
53
- "@ubean/client": "0.6.7",
54
- "@ubean/config": "0.6.7",
55
- "@ubean/i18n": "0.6.7",
56
- "@ubean/islands": "0.6.7",
57
- "@ubean/markdown": "0.6.7",
58
- "@ubean/pages": "0.6.7",
59
- "@ubean/preset": "0.6.7",
60
- "@ubean/routes": "0.6.7",
61
- "@ubean/scan": "0.6.7",
62
- "@ubean/server": "0.6.7",
63
- "@ubean/shared": "0.6.7",
64
- "@ubean/vue": "0.6.7",
52
+ "@ubean/app": "0.6.9",
53
+ "@ubean/client": "0.6.9",
54
+ "@ubean/config": "0.6.9",
55
+ "@ubean/i18n": "0.6.9",
56
+ "@ubean/islands": "0.6.9",
57
+ "@ubean/markdown": "0.6.9",
58
+ "@ubean/pages": "0.6.9",
59
+ "@ubean/preset": "0.6.9",
60
+ "@ubean/routes": "0.6.9",
61
+ "@ubean/scan": "0.6.9",
62
+ "@ubean/server": "0.6.9",
63
+ "@ubean/shared": "0.6.9",
64
+ "@ubean/vue": "0.6.9",
65
65
  "@vitejs/plugin-vue": "^6.0.10",
66
+ "@vitejs/plugin-vue-jsx": "^5.1.6",
66
67
  "openapi-typescript": "^7.13.0",
67
68
  "pathe": "^2.0.3",
68
69
  "tinyglobby": "^0.2.17",