@ubean/build 0.6.9 → 0.7.0

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.
@@ -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-RzEXvOPR.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-ClIh5VKh.js";
@@ -16,13 +16,6 @@ import openapiTS, { astToString } from "openapi-typescript";
16
16
  * `src/env.ts` 读了 `import.meta.env.VITE_API_PROXY_KEYS`,而 `src/typings/env.d.ts`
17
17
  * 里没有对应声明 —— 手写清单没有东西能提醒它过期。
18
18
  */
19
- const VITE_BUILTIN_ENV_KEYS = /* @__PURE__ */ new Set([
20
- "MODE",
21
- "DEV",
22
- "PROD",
23
- "BASE_URL",
24
- "SSR"
25
- ]);
26
19
  /**
27
20
  * 产物文件名(单一来源:`generateEnvTypes()` 的默认值与 `CODEGEN_NON_CONTRACT_FILES`
28
21
  * 都用它,免得改了一处漏另一处)。
@@ -63,23 +56,28 @@ function renderModuleTypes(moduleId, fields, extraLines = []) {
63
56
  return `declare module '${moduleId}' {\n${body.join("\n")}\n}\n`;
64
57
  }
65
58
  /**
66
- * 从 `import.meta.env` 快照生成 `ImportMetaEnv`。
59
+ * 从 `import.meta.env` 快照生成 `ImportMetaEnv`,只声明 `VITE_` 前缀的 key。
67
60
  *
68
61
  * 值同时用于**类型**:字符串字面量能收窄成具体字符串 —— 但只在值是字面量时。这里选择
69
62
  * 宽化到 `string` / `number` / `boolean`,因为 `.env` 里的值一律是字符串,写成裸字面量
70
63
  * 会让 `VITE_API_ENV` 被推成 `"local"` 这种过窄类型,赋值时报错。
64
+ *
65
+ * **Vite 内建(`MODE` / `DEV` / `PROD` / `BASE_URL` / `SSR`)刻意不声明。** 它们已经由
66
+ * `vite/client`(`dist/vite/types/importMeta.d.ts`)声明,且那边**没有** `readonly` 修饰符 ——
67
+ * 重复声明会报 `TS2687: All declarations of 'MODE' must have identical modifiers`(实测
68
+ * soybean-admin-v3:`--skipLibCheck false` 下 8 条)。默认的 `skipLibCheck: true` 会把它吞掉,
69
+ * 于是这个冲突只对关掉 skipLibCheck 的项目可见。不声明就不冲突,而且那 5 个 key 是 Vite 常量,
70
+ * 没有漂移可言。
71
+ *
72
+ * 没有 `VITE_*` key 时返回 `undefined`,整个 `interface ImportMetaEnv` 块都不产出 —— 只留一个
73
+ * `readonly [key: string]: …` 兜底索引签名会反向削弱 `vite/client` 的声明(`strictImportMetaEnv`
74
+ * 关掉时它自带 `Record<string, any>`;我们那份索引签名把 `import.meta.env.X` 从 `any` 收窄成
75
+ * `string | boolean | undefined`,看着更严,实则是把 Vite 的类型面往下拉)。
71
76
  */
72
77
  function renderImportMetaEnv(importMetaEnv) {
73
- const keys = Object.keys(importMetaEnv).filter((key) => key.startsWith("VITE_") || VITE_BUILTIN_ENV_KEYS.has(key)).sort();
74
- if (keys.length === 0) return `interface ImportMetaEnv {\n readonly [key: string]: string | boolean | undefined;\n}\n`;
75
- return `interface ImportMetaEnv {\n${keys.map((key) => {
76
- const value = importMetaEnv[key];
77
- if (VITE_BUILTIN_ENV_KEYS.has(key)) {
78
- if (key === "MODE" || key === "BASE_URL") return ` readonly ${key}: string;`;
79
- return ` readonly ${key}: boolean;`;
80
- }
81
- return ` readonly ${key}: ${tsTypeOfLiteral(value)};`;
82
- }).join("\n")}\n}\n`;
78
+ const keys = Object.keys(importMetaEnv).filter((key) => key.startsWith("VITE_")).sort();
79
+ if (keys.length === 0) return void 0;
80
+ return `interface ImportMetaEnv {\n${keys.map((key) => ` readonly ${key}: ${tsTypeOfLiteral(importMetaEnv[key])};`).join("\n")}\n}\n`;
83
81
  }
84
82
  /**
85
83
  * `renderEnvServerModule()` 生成的源码除了字段还会 `export function getSecret(key)`
@@ -99,16 +97,12 @@ const ENV_SERVER_EXTRA_EXPORTS = [" /** 读取未在 `env.schema` 里声明的
99
97
  */
100
98
  async function generateEnvTypes(options) {
101
99
  const { outDir, fileName = ENV_TYPES_FILE_NAME, modules, importMetaEnv = {} } = options;
100
+ const importMetaEnvBlock = renderImportMetaEnv(importMetaEnv);
102
101
  const contents = `// Auto-generated by ubean from \`env.schema\` in ubean.config.ts — do not edit manually.
103
102
  // Provides typed access to \`ubean:env/client\` / \`ubean:env/server\` and declares ImportMetaEnv.
104
103
  /* eslint-disable */
105
104
  /// <reference types="vite/client" />
106
-
107
- ${renderImportMetaEnv(importMetaEnv)}
108
- interface ImportMeta {
109
- readonly env: ImportMetaEnv;
110
- }
111
-
105
+ ${importMetaEnvBlock ? `\n${importMetaEnvBlock}interface ImportMeta {\n readonly env: ImportMetaEnv;\n}\n` : ""}
112
106
  ${renderModuleTypes("ubean:env/client", modules.client)}${renderModuleTypes("ubean:env/server", modules.server, ENV_SERVER_EXTRA_EXPORTS)}`;
113
107
  await mkdir(outDir, { recursive: true });
114
108
  const outPath = join(outDir, fileName);
@@ -499,7 +493,7 @@ async function generateAutoImports(_scanResult, options) {
499
493
  virtualComponents.push(...scanned.virtualComponents);
500
494
  }
501
495
  }
502
- const componentsDts = generateComponentsDts(components, componentsDtsPath);
496
+ const componentsDts = generateComponentsDts(components, componentsDtsPath, await readComponentsDtsEntries(componentsDtsPath));
503
497
  await writeFile(componentsDtsPath, componentsDts, "utf-8");
504
498
  const virtualComponentsDtsPath = join(outDir, "virtual-components.d.ts");
505
499
  await writeFile(virtualComponentsDtsPath, generateVirtualComponentsDts(virtualComponents, virtualComponentsDtsPath), "utf-8");
@@ -567,6 +561,39 @@ function generateVirtualComponentsDts(virtual, dtsPath) {
567
561
  return `${lines.join("\n")}\n`;
568
562
  }
569
563
  /**
564
+ * 读回 `components.d.ts` 里已有的条目(名字 → 声明)。
565
+ *
566
+ * 这个文件有**两个写入者**:本函数(每次重扫整体重写)与 unplugin-vue-components(dev 下
567
+ * 按需重写,只扫它编译过的 SFC)。两边都只列自己知道的东西,所以必须**合并**而不是覆盖。
568
+ *
569
+ * 后果(实测 soybean-admin-v3):`<SConfigProvider>` 只在 `src/app.vue` 里用,而 app.vue 只在
570
+ * 渲染某个页面时才被加载 —— 于是 `SConfigProvider` / `SButton` 先被 unplugin 写进 d.ts,
571
+ * 随后任意文件改动触发重扫,本函数把它们整个覆盖掉(我们只扫 `src/components` 与内置 4 个,
572
+ * 既不认识模块注册的 resolver 也不认识 `RouterLink`/`RouterView`)。用户看到的就是
573
+ * 「组件自动导入时有时无」。
574
+ *
575
+ * 为什么不去遍历模块注册的 resolver 拿候选名:`@vean/ui` 主入口不导出可枚举的组件清单
576
+ * (161 个导出里只有一部分是组件),resolver 本身只能按名字查表。没有候选列表就只能合并。
577
+ *
578
+ * 不解析 AST:这是两个写入器共有的扁平接口,行格式稳定(` Name: typeof import('…')['…']`)。
579
+ */
580
+ async function readComponentsDtsEntries(dtsPath) {
581
+ const entries = /* @__PURE__ */ new Map();
582
+ let raw;
583
+ try {
584
+ raw = await readFile(dtsPath, "utf-8");
585
+ } catch {
586
+ return entries;
587
+ }
588
+ for (const line of raw.split("\n")) {
589
+ const match = /^ {4}(?:"((?:[^"\\]|\\.)*)"|([A-Za-z_$][\w$]*)): (typeof import\('.*'\)\[[^\]]+\])$/.exec(line);
590
+ if (!match) continue;
591
+ const name = match[1] !== void 0 ? JSON.parse(`"${match[1]}"`) : match[2];
592
+ entries.set(name, match[3]);
593
+ }
594
+ return entries;
595
+ }
596
+ /**
570
597
  * Generate the `.ubean/components.d.ts` in a format compatible with
571
598
  * unplugin-vue-components (both writers own the same file).
572
599
  *
@@ -576,10 +603,15 @@ function generateVirtualComponentsDts(virtual, dtsPath) {
576
603
  * `import`/`const` declaration lines. Entries must therefore be
577
604
  * self-contained inline `typeof import('...')` declarations so they stay
578
605
  * valid after unplugin merges them.
606
+ *
607
+ * 反向也必须成立:`previous`(上一份文件里的条目)**优先保留**。unplugin 在 dev 下按需
608
+ * 收集(只扫它编译过的 SFC),我们只扫 `src/components` + 内置 4 个 —— 覆盖式重写会把
609
+ * 它写进去的第三方组件(`SButton`/`SConfigProvider`/`RouterLink`…)全抹掉。
579
610
  */
580
- function generateComponentsDts(components, dtsPath) {
611
+ function generateComponentsDts(components, dtsPath, previous = /* @__PURE__ */ new Map()) {
581
612
  const dtsDir = fileDirname(toPosixPath(normalize(dtsPath)));
582
613
  const entries = /* @__PURE__ */ new Map();
614
+ for (const [name, declaration] of previous) entries.set(name, declaration);
583
615
  for (const name of [
584
616
  "Link",
585
617
  "Head",
@@ -38,6 +38,14 @@ export interface PrerendererOptions {
38
38
  statusCode: number;
39
39
  }>;
40
40
  }
41
+ /**
42
+ * `definePage({ redirect })` 的页面是否在预渲染时应跳过。
43
+ *
44
+ * 静态产物里没有重定向的执行点:SSG 直接渲染路径会渲出源页面的 HTML,
45
+ * fullstack 管道下 fetcher 拿到的是空体的 302(写盘就是空文件)。两者都是错的,
46
+ * 所以两边共用这一个判据(调用方负责出 warn,本模块只负责跳过)。
47
+ */
48
+ export declare function isRedirectPage(page: ScannedPageRoute): boolean;
41
49
  /**
42
50
  * 从 routeRules 中提取标记了 `prerender: true` 或 `ppr: true` 的路由模式
43
51
  * (P9-03 + P9-04)。
package/dist/prerender.js CHANGED
@@ -18,6 +18,16 @@ const DATA_PAYLOAD_REGEX = new RegExp(`<script id="${DATA_PAYLOAD_ID}" type="app
18
18
  function isDynamicPath(path) {
19
19
  return /\[.*?\]/.test(path) || path.includes(":");
20
20
  }
21
+ /**
22
+ * `definePage({ redirect })` 的页面是否在预渲染时应跳过。
23
+ *
24
+ * 静态产物里没有重定向的执行点:SSG 直接渲染路径会渲出源页面的 HTML,
25
+ * fullstack 管道下 fetcher 拿到的是空体的 302(写盘就是空文件)。两者都是错的,
26
+ * 所以两边共用这一个判据(调用方负责出 warn,本模块只负责跳过)。
27
+ */
28
+ function isRedirectPage(page) {
29
+ return page.routable !== false && Boolean(page.redirect);
30
+ }
21
31
  function normalizePagePath(filePath) {
22
32
  let path = filePath.replace(/\.(vue|ts|js|tsx|jsx|md|mdx)$/, "");
23
33
  if (path === "index" || path.endsWith("/index")) path = path.slice(0, -5);
@@ -63,9 +73,14 @@ function collectPrerenderRoutes(pages, options = {}) {
63
73
  ...extractPrerenderRoutesFromRules(options.routeRules),
64
74
  ...options.contentRoutes ?? []
65
75
  ];
76
+ const redirectPaths = /* @__PURE__ */ new Set();
66
77
  const allPageRoutes = [];
67
78
  for (const page of pages) {
68
79
  if (page.routable === false) continue;
80
+ if (isRedirectPage(page)) {
81
+ redirectPaths.add(normalizePagePath(page.path));
82
+ continue;
83
+ }
69
84
  const path = normalizePagePath(page.path);
70
85
  if (!isDynamicPath(path)) allPageRoutes.push(path);
71
86
  }
@@ -74,6 +89,7 @@ function collectPrerenderRoutes(pages, options = {}) {
74
89
  else for (const pattern of include) if (pattern.includes("*")) {
75
90
  for (const r of allPageRoutes) if (matchGlob(r, pattern)) matched.add(r);
76
91
  } else matched.add(pattern.startsWith("/") ? pattern : `/${pattern}`);
92
+ for (const path of redirectPaths) matched.delete(path);
77
93
  const skipped = [];
78
94
  for (const r of matched) if (exclude.some((p) => matchGlob(r, p))) skipped.push(r);
79
95
  for (const r of skipped) matched.delete(r);
@@ -232,6 +248,8 @@ async function prerender(options) {
232
248
  contentRoutes: options.contentRoutes
233
249
  });
234
250
  const queue = [...options.expandRoutes ? options.expandRoutes(initialRoutes) : initialRoutes];
251
+ const redirectPages = options.pages.filter(isRedirectPage).map((p) => normalizePagePath(p.path));
252
+ if (redirectPages.length > 0) _logger.warn(`Skipping prerender for ${redirectPages.length} redirect page(s) (${redirectPages.join(", ")}) — static output has no redirect execution point`);
235
253
  if (options.notFoundRoute) queue.unshift(STATIC_NOT_FOUND_ROUTE);
236
254
  const visited = /* @__PURE__ */ new Set();
237
255
  const results = [];
@@ -325,4 +343,4 @@ function generatePrerenderManifest(result, baseUrl = "/") {
325
343
  };
326
344
  }
327
345
  //#endregion
328
- export { collectPrerenderRoutes, extractDataPayload, extractLinks, extractPrerenderRoutesFromRules, generatePrerenderManifest, prerender, resolvePrerenderStaticDir, routeToDataFilePath, routeToFilePath, writePrerenderedFile };
346
+ export { collectPrerenderRoutes, extractDataPayload, extractLinks, extractPrerenderRoutesFromRules, generatePrerenderManifest, isRedirectPage, prerender, resolvePrerenderStaticDir, routeToDataFilePath, routeToFilePath, writePrerenderedFile };
@@ -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-k9_n0K9x.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-C5MyVhMg.js";
@@ -1,7 +1,7 @@
1
1
  import { a as ssrSingletonProdOptimizeExclude, g as createVirtualRegistry, o as ssrSingletonProdSsr, x as __exportAll } from "./ssr-singleton-CUHZLgiO.js";
2
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";
3
+ import { a as generateOpenApiTypes, i as generateTypes } from "./codegen-ClIh5VKh.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-ClGXUBJz.js";
5
5
  import { createStaticSsgRenderer } from "./static-render.js";
6
6
  import { prerender, resolvePrerenderStaticDir } from "./prerender.js";
7
7
  import { resolvePageKind } from "@ubean/vue";
@@ -96,11 +96,23 @@ const _pageToRoute = p => {
96
96
  // only contains definePage metadata, not a Vue component).
97
97
  const _targetPage = p.pageKind === 'reuse' && p.reuseTarget ? _pages.find(tp => tp.name === p.reuseTarget) : undefined;
98
98
  const _loaderKey = _targetPage?.relativePath || p.relativePath;
99
+ const _routePath = p.route.replace(/\\*\\*:(\\w[\\w-]*)/g, ':$1(.*)*');
99
100
  return {
100
- path: _localeVueParam
101
- ? toVueRouterLocalePath(p.route.replace(/\\*\\*:(\\w[\\w-]*)/g, ':$1(.*)*'), _localeVueParam)
102
- : p.route.replace(/\\*\\*:(\\w[\\w-]*)/g, ':$1(.*)*'),
101
+ path: _localeVueParam ? toVueRouterLocalePath(_routePath, _localeVueParam) : _routePath,
103
102
  name: p.name,
103
+ // \`definePage({ redirect })\`: 不渲染组件,访问时跳转(与服务端 302 同语义)。
104
+ // 字符串重定向不插值参数,带语言前缀时用函数形式拼 —— 与客户端 / dev 同形。
105
+ // 注意这里是**生成期模板**:\`p.redirect\` 必须原样落到产物里(运行期才按页取值)。
106
+ ...(p.redirect
107
+ ? {
108
+ redirect: _localeVueParam
109
+ ? to =>
110
+ to.params.locale
111
+ ? '/' + to.params.locale + (p.redirect === '/' ? '' : p.redirect)
112
+ : p.redirect
113
+ : p.redirect
114
+ }
115
+ : {}),
104
116
  component: async () => {
105
117
  const loader = pageLoaders[_loaderKey];
106
118
  if (!loader) throw new Error('Page loader not found: ' + _loaderKey);
@@ -139,7 +151,7 @@ for (const _group of _routeGroups.values()) {
139
151
  // 注意组件加载器在 _record 上(_primary 是扫描得到的页面条目,没有 component 字段 ——
140
152
  // 写成 _primary.component 会得到 undefined,vue-router 报 Invalid route component)。
141
153
  const _components = {};
142
- if (_group.default) _components.default = _record.component;
154
+ if (_group.default && !_group.default.redirect) _components.default = _record.component;
143
155
  for (const _slotPage of _group.slots) _components[_slotPage.slot] = _pageToRoute(_slotPage).component;
144
156
  delete _record.component;
145
157
  _record.components = _components;
@@ -940,6 +952,23 @@ function createBuildEnvironments(input) {
940
952
  return {
941
953
  client: {
942
954
  consumer: "client",
955
+ /**
956
+ * 客户端环境的顶层 `input` 用**虚拟模块 id**,而不是 `build.rollupOptions.input` 里的绝对路径。
957
+ *
958
+ * 原因:Vite 的依赖扫描(`computeEntries`)优先读顶层 `input`,并走
959
+ * `pluginContainer.resolveId()` —— 虚拟模块能被插件解析;而 `rollupOptions.input` 是
960
+ * 磁盘路径,在没有 `src/entry.client.ts` 的项目里指向 `.ubean/virtual/client-entry.mjs`,
961
+ * 那是**构建产物**:dev 从不写它。
962
+ *
963
+ * 后果(实测 soybean-admin-v3,v0.6.9):扫描抛
964
+ * `failed to resolve rolldownOptions.input value: "…/.ubean/virtual/client-entry.mjs"`,
965
+ * 依赖预构建整段跳过 —— dev server 能起、页面能渲染,但依赖不预构建(首屏请求瀑布 + 慢),
966
+ * 且报错直接吐给用户。
967
+ *
968
+ * 用虚拟 id 还顺带让扫描真的爬进客户端入口(拿到 `vue` / `vue-router` / UI 库等依赖)。
969
+ * 构建期取值不变:解析后仍是同一个虚拟模块。
970
+ */
971
+ input: "virtual:ubean-client-entry",
943
972
  optimizeDeps: { exclude: clientOptimizeDepsExclude() },
944
973
  build: {
945
974
  outDir: outDirs.public,
@@ -1473,7 +1502,8 @@ function serializePagesForEntry(pages) {
1473
1502
  reuseTarget: p.reuseTarget,
1474
1503
  pageMeta: p.pageMeta,
1475
1504
  matchers: p.matchers,
1476
- slot: p.slot
1505
+ slot: p.slot,
1506
+ redirect: p.redirect
1477
1507
  })));
1478
1508
  }
1479
1509
  async function generateVirtualModulesToDisk(cwd, config, scanResult, virtualDir, preset, registry, contentSnapshot) {
@@ -1523,18 +1553,21 @@ async function generateVirtualModulesToDisk(cwd, config, scanResult, virtualDir,
1523
1553
  const content = mod ? await mod.load() : "";
1524
1554
  await writeFile(join(virtualDir, filename), header + content, "utf-8");
1525
1555
  }
1526
- await writeModule("ubean:routes", "routes.mjs");
1527
- await writeModule("ubean:app-config", "app-config.mjs");
1528
- await writeModule("ubean:locales", "locales.mjs");
1529
- if (hasPages) {
1530
- await writeModule("ubean:pages", "pages.ts");
1531
- await writeModule("ubean:meta", "meta.mjs");
1532
- await writeModule("virtual:ubean-pages", "vue-pages.ts", "");
1533
- await writeModule("virtual:ubean-app", "vue-app.ts", "");
1534
- await writeModule("virtual:ubean-client-entry", "client-entry.mjs", "");
1535
- }
1536
- if (hasServer) await writeModule("virtual:ubean-server", "server-entry.ts", "");
1537
- await writeFile(join(virtualDir, "islands-registry.ts"), "export const islands = {};\n", "utf-8");
1556
+ /**
1557
+ * 只写**有磁盘消费者**的虚拟模块。
1558
+ *
1559
+ * 这里曾经把 12 个虚拟模块全写了一遍,其中 10 个没有任何读取方 —— 构建 / dev 都从注册表取
1560
+ * 虚拟 id(`ubeanVite` 的 `resolveId`/`load`、dev 的 `loadModule`),磁盘副本纯装饰。代价不是
1561
+ * 空间而是**类型检查**:scaffold 模板的 `include` 是 `.ubean` 全目录,这些文件会被 `vue-tsc` 扫进去,
1562
+ * 而它们只带 `// @ts-nocheck` 头部(`server-entry.ts` 连头部都是空串)—— 实测 soybean-admin-v3
1563
+ * 的 30 个类型错误**全部**来自生成产物,`src/` 一个都没有。
1564
+ *
1565
+ * 剩下的三个各自有真实读取方:
1566
+ * - `client-entry.mjs`:`createBuildEnvironments()` 的 `clientInput` 回退路径(无 `src/entry.client.ts` 时);
1567
+ * - `server-entry.mjs`:`ubean` 环境的 `build.rollupOptions.input`,真正的构建入口;
1568
+ * - `index.html`:`writeClientIndexHtml()` 在 `prepareBuild` 里单独写,见 `build-app.ts`。
1569
+ */
1570
+ if (hasPages) await writeModule("virtual:ubean-client-entry", "client-entry.mjs", "");
1538
1571
  const pagesGlob = JSON.stringify(`${viteSrcPrefix}/pages/**/*.{${pagesExts}}`);
1539
1572
  const routesGlob = JSON.stringify(`${viteSrcPrefix}/routes/**/*.{ts,js,mjs}`);
1540
1573
  const middlewareGlob = JSON.stringify(`${viteSrcPrefix}/middleware/**/*.{ts,js,mjs}`);
package/dist/vite.d.ts CHANGED
@@ -392,6 +392,9 @@ export declare function ubeanPreviewPlugin(options?: UbeanPreviewPluginOptions):
392
392
  //#region src/dev/dev-ssr-routes.d.ts
393
393
  /** 扫描器方言 `**:slug`(catch-all)→ vue-router 的 `:slug(.*)*`。 */
394
394
  export declare function toVueRouterPagePath(route: string): string;
395
+ interface DevSsrRedirectLocation {
396
+ params: Record<string, string | string[]>;
397
+ }
395
398
  /** vue-router 路由记录的运行期形状(只声明 dev 侧真正用到的字段)。 */
396
399
  interface DevSsrRoute {
397
400
  path: string;
@@ -399,6 +402,13 @@ interface DevSsrRoute {
399
402
  /** 普通路由用 `component`;并行路由(`@slot/`)用 `components` 命名视图,二者互斥。 */
400
403
  component?: () => Promise<unknown>;
401
404
  components?: Record<string, () => Promise<unknown>>;
405
+ /**
406
+ * `definePage({ redirect })` 的目标 —— 本路由不渲染组件,访问时跳转。
407
+ *
408
+ * 与客户端 `virtual:ubean-pages` 同形:字符串是静态路径,带语言前缀时用
409
+ * 函数形式按 `to.params.locale` 补前缀(字符串重定向不插值参数)。
410
+ */
411
+ redirect?: string | ((to: DevSsrRedirectLocation) => string);
402
412
  meta: {
403
413
  /** 单层布局名、多层嵌套数组(P9-17)或 `false`(禁用)。 */
404
414
  layout: string | string[] | false | null;
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-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";
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-ClGXUBJz.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-C5MyVhMg.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,5 +1,5 @@
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-RzEXvOPR.js";
2
+ import { D as resolveComponentsConfig, E as resolveAutoImportsConfig, O as toArray, S as getAutoImportPresets, f as COMPONENT_HALF_GLOBS } from "./codegen-ClIh5VKh.js";
3
3
  import { createRequire } from "node:module";
4
4
  import { generatePagesModuleSource } from "@ubean/vue/vite";
5
5
  import { resolvePageKind } from "@ubean/vue";
@@ -40,6 +40,19 @@ function toVueRouterPagePath(route) {
40
40
  return route.replace(/\*\*:(\w[\w-]*)/g, ":$1(.*)*");
41
41
  }
42
42
  /**
43
+ * `definePage({ redirect })` 的 vue-router 目标。
44
+ *
45
+ * 字符串重定向是**静态路径**,vue-router 不插值参数,所以带语言前缀时必须用
46
+ * 函数形式拼一次(客户端 `generatePagesModuleSource` 生成的是同一个形状)。
47
+ * `to.params.locale` 的运行期值就是 URL 段本身,不需要再查 locale 表;默认语言在
48
+ * `prefix_except_default` / `prefix_and_default` 下没有 param,自然不带前缀。
49
+ */
50
+ function redirectTarget(target, localeVueParam) {
51
+ if (!localeVueParam) return target;
52
+ const suffix = target === "/" ? "" : target;
53
+ return (to) => to.params.locale ? `/${to.params.locale}${suffix}` : target;
54
+ }
55
+ /**
43
56
  * 构建 SSR 路由表。
44
57
  *
45
58
  * 顺序与形状刻意对齐客户端:页面按扫描顺序、reuse 路由复用目标页面的组件、
@@ -61,7 +74,7 @@ function buildDevSsrRoutes(options) {
61
74
  const routeFor = (page) => ({
62
75
  path: withLocale(toVueRouterPagePath(page.route)),
63
76
  name: page.name,
64
- component: componentFor(page),
77
+ ...page.redirect ? { redirect: redirectTarget(page.redirect, localeVueParam) } : { component: componentFor(page) },
65
78
  meta: {
66
79
  layout: page.layout === false ? false : page.layout || defaultLayout,
67
80
  pageName: page.name,
@@ -83,7 +96,7 @@ function buildDevSsrRoutes(options) {
83
96
  const route = routeFor(primary);
84
97
  if (group.slots.length > 0) {
85
98
  const components = {};
86
- if (group.default) components.default = componentFor(group.default);
99
+ if (group.default && !group.default.redirect) components.default = componentFor(group.default);
87
100
  for (const slotPage of group.slots) components[slotPage.slot] = componentFor(slotPage);
88
101
  delete route.component;
89
102
  route.components = components;
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-lK-WDLZZ.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-ClGXUBJz.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ubean/build",
3
- "version": "0.6.9",
3
+ "version": "0.7.0",
4
4
  "description": "Vite plugins, production build, SSG prerender, type codegen, and Server Actions plugin for ubean",
5
5
  "files": [
6
6
  "dist"
@@ -49,19 +49,19 @@
49
49
  },
50
50
  "dependencies": {
51
51
  "@intlify/unplugin-vue-i18n": "11.2.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",
52
+ "@ubean/app": "0.7.0",
53
+ "@ubean/client": "0.7.0",
54
+ "@ubean/config": "0.7.0",
55
+ "@ubean/i18n": "0.7.0",
56
+ "@ubean/islands": "0.7.0",
57
+ "@ubean/markdown": "0.7.0",
58
+ "@ubean/pages": "0.7.0",
59
+ "@ubean/preset": "0.7.0",
60
+ "@ubean/routes": "0.7.0",
61
+ "@ubean/scan": "0.7.0",
62
+ "@ubean/server": "0.7.0",
63
+ "@ubean/shared": "0.7.0",
64
+ "@ubean/vue": "0.7.0",
65
65
  "@vitejs/plugin-vue": "^6.0.10",
66
66
  "@vitejs/plugin-vue-jsx": "^5.1.6",
67
67
  "openapi-typescript": "^7.13.0",