@ubean/vue 0.2.1 → 0.3.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.
package/README.md CHANGED
@@ -365,7 +365,7 @@ On SSR (via `SSR_KEY`), KeepAlive/Transition/Suspense/ErrorBoundary are skipped.
365
365
 
366
366
  Internal links render via `RouterLink`; external links (`http*`, `//`, `#`) render as native `<a target="_blank" rel="noopener noreferrer">`.
367
367
 
368
- Props: `to` (string or location object), `href`, `replace`, `activeClass`, `exactActiveClass`, `noActiveClass`. Path localization is opt-in via `LOCALIZE_PATH_KEY` (the framework runtime provides it; lean SPAs render paths verbatim).
368
+ Props: `to` (string or location object), `href`, `replace`, `activeClass`, `exactActiveClass`, `noActiveClass`, optional `locale`. Path localization is opt-in via `LOCALIZE_PATH_KEY` (the framework runtime provides it; lean SPAs render paths verbatim).
369
369
 
370
370
  ### `<SlotView>`
371
371
 
package/README.zh-CN.md CHANGED
@@ -372,7 +372,7 @@ SSR 场景(经 `SSR_KEY`)跳过 KeepAlive/Transition/Suspense/ErrorBoundary
372
372
 
373
373
  内部链接经 `RouterLink` 渲染;外部链接(`http*`、`//`、`#`)渲染为原生 `<a target="_blank" rel="noopener noreferrer">`。
374
374
 
375
- Props:`to`(字符串或位置对象)、`href`、`replace`、`activeClass`、`exactActiveClass`、`noActiveClass`。路径本地化经 `LOCALIZE_PATH_KEY` opt-in(框架运行时提供;精简 SPA 原样渲染路径)。
375
+ Props:`to`(字符串或位置对象)、`href`、`replace`、`activeClass`、`exactActiveClass`、`noActiveClass`、可选 `locale`。路径本地化经 `LOCALIZE_PATH_KEY` opt-in(框架运行时提供;精简 SPA 原样渲染路径)。
376
376
 
377
377
  ### `<SlotView>`
378
378
 
@@ -1,5 +1,5 @@
1
- import { a as ScannedLayout, o as ScannedPage } from "../types-VHF1RJu2.js";
2
- //#region src/generator/index.d.ts
1
+ import { a as ScannedLayout, o as ScannedPage } from "./types-CeX4SJit.js";
2
+ //#region src/generator.d.ts
3
3
  /**
4
4
  * 实体路由文件生成器(页面路由实体文件模式的唯一所有者,原
5
5
  * `@ubean/scan/generator`,已下沉到页面路由所有者 `@ubean/vue`)。
@@ -73,6 +73,11 @@ interface GeneratorOptions {
73
73
  * Return `null` to skip. Existing keys are NOT overwritten.
74
74
  */
75
75
  getRouteMeta?: (page: ScannedPage) => Record<string, unknown> | null;
76
+ /**
77
+ * Compact locale vue-router param (plain data from `getVueLocaleParam`).
78
+ * Example: `':locale(zh)?'`. Empty/undefined = no locale prefix.
79
+ */
80
+ localeVueParam?: string;
76
81
  /**
77
82
  * Compute the import specifier for a page file. Receives the absolute
78
83
  * `fullPath` of the page; should return a module specifier usable from
@@ -102,6 +107,7 @@ declare class RouteFileGenerator {
102
107
  constructor(options: GeneratorOptions);
103
108
  generate(scan: GeneratorScanInput): Promise<GeneratorResult>;
104
109
  renderRoutesFile(scan: GeneratorScanInput): string;
110
+ private toLocalePath;
105
111
  private renderRouteRecord;
106
112
  private computeMeta;
107
113
  renderImportsFile(scan: GeneratorScanInput): string;
@@ -1,4 +1,4 @@
1
- //#region src/generator/index.ts
1
+ //#region src/generator.ts
2
2
  const DEFAULT_HEADER_COMMENT = "// @generated by @ubean/vue/generator — do not edit manually.\n// This file is regenerated on every dev start / build.";
3
3
  async function generateRouteFiles(scan, options) {
4
4
  return new RouteFileGenerator(options).generate(scan);
@@ -51,10 +51,16 @@ ${scan.pages.map((p) => this.renderRouteRecord(p)).join(",\n")}
51
51
  ];
52
52
  `;
53
53
  }
54
+ toLocalePath(pagePath) {
55
+ const param = this.opts.localeVueParam;
56
+ if (!param) return pagePath;
57
+ if (pagePath === "/") return `/${param}`;
58
+ return `/${param}${pagePath}`;
59
+ }
54
60
  renderRouteRecord(page) {
55
61
  const parts = [];
56
62
  parts.push(` name: ${JSON.stringify(page.name)}`);
57
- parts.push(` path: ${JSON.stringify(page.route)}`);
63
+ parts.push(` path: ${JSON.stringify(this.toLocalePath(page.route))}`);
58
64
  const componentKey = page.isReuse && page.reuseTarget ? page.reuseTarget : page.name;
59
65
  parts.push(` component: ${JSON.stringify(componentKey)}`);
60
66
  if (page.layout !== void 0) {
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { a as ScannedLayout, i as ScanPagesResult, n as PageMeta, o as ScannedPage, r as ScanPagesOptions, t as PageHead } from "./types-VHF1RJu2.js";
1
+ import { a as ScannedLayout, i as ScanPagesResult, n as PageMeta, o as ScannedPage, r as ScanPagesOptions, t as PageHead } from "./types-CeX4SJit.js";
2
2
  import { Component, ComputedRef, InjectionKey, Plugin, PropType, Ref, VNode } from "vue";
3
3
  import { NavigationGuard, RouteMeta, Router } from "vue-router";
4
4
  //#region src/view-transitions.d.ts
@@ -27,7 +27,7 @@ declare const ERROR_KEY: InjectionKey<Component | null>;
27
27
  * (`@ubean/client`) provides the reactive `localizePath`; when absent,
28
28
  * `Link` renders paths verbatim — this package never imports i18n itself.
29
29
  */
30
- declare const LOCALIZE_PATH_KEY: InjectionKey<(path: string) => string>;
30
+ declare const LOCALIZE_PATH_KEY: InjectionKey<(path: string, locale?: string) => string>;
31
31
  /**
32
32
  * Layout chain context — provided by `LayoutChainRenderer` so that `<PageView />`
33
33
  * inside a layout knows whether to render the next nested layout or the actual
@@ -204,6 +204,11 @@ declare const Link: import("vue").DefineComponent<import("vue").ExtractPropTypes
204
204
  type: BooleanConstructor;
205
205
  default: boolean;
206
206
  };
207
+ /** Target locale for path localization; omit to use the current locale. */
208
+ locale: {
209
+ type: StringConstructor;
210
+ default: undefined;
211
+ };
207
212
  }>, () => VNode<import("vue").RendererNode, import("vue").RendererElement, {
208
213
  [key: string]: any;
209
214
  }>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
@@ -235,6 +240,11 @@ declare const Link: import("vue").DefineComponent<import("vue").ExtractPropTypes
235
240
  type: BooleanConstructor;
236
241
  default: boolean;
237
242
  };
243
+ /** Target locale for path localization; omit to use the current locale. */
244
+ locale: {
245
+ type: StringConstructor;
246
+ default: undefined;
247
+ };
238
248
  }>> & Readonly<{}>, {
239
249
  to: string | Record<string, unknown>;
240
250
  href: string;
@@ -243,6 +253,7 @@ declare const Link: import("vue").DefineComponent<import("vue").ExtractPropTypes
243
253
  activeClass: string;
244
254
  exactActiveClass: string;
245
255
  noActiveClass: boolean;
256
+ locale: string;
246
257
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
247
258
  /**
248
259
  * `PAGE_KEY` 注入的页面数据形状(框架工厂 / SSR 链路提供;精简 SPA
@@ -560,6 +571,11 @@ interface DefineClientPageOptions {
560
571
  * `setupPageHeadGuard()` 应用,SSR 链路经服务端扫描结果消费。
561
572
  */
562
573
  head?: PageHead;
574
+ /**
575
+ * Per-page select SSR (`false` | `'data-only'` | `true` | `'streaming'`).
576
+ * Consumed by the server router; also written to `route.meta.ssr`.
577
+ */
578
+ ssr?: boolean | 'streaming' | 'data-only';
563
579
  /** 任意扩展 meta,浅合并进 `route.meta`(类型上扩展 vue-router RouteMeta)。 */
564
580
  meta?: RouteMeta;
565
581
  }
@@ -725,7 +741,7 @@ interface PageHeadClient {
725
741
  }
726
742
  /**
727
743
  * 将静态 `PageHead` push 进 head 实例(falsy 字段自动跳过)。
728
- * 与 SSR 侧 `pushPageHead`(@ubean/ssr)语义一致,保证双端同构。
744
+ * 与 SSR 侧 `pushPageHead`(@ubean/client/ssr)语义一致,保证双端同构。
729
745
  */
730
746
  declare function pushPageHead(head: PageHeadClient, pageHead: PageHead): void;
731
747
  /**
@@ -776,6 +792,8 @@ declare module 'vue-router' {
776
792
  head?: PageHead;
777
793
  /** `[param=matcher]` mapping consumed by `createMatcherGuard()`. */
778
794
  matchers?: Record<string, string>;
795
+ /** Select SSR mode from `definePage({ ssr })`. */
796
+ ssr?: boolean | 'streaming' | 'data-only';
779
797
  }
780
798
  }
781
799
  //#endregion
package/dist/index.js CHANGED
@@ -753,6 +753,22 @@ const SlotView = defineComponent({
753
753
  };
754
754
  }
755
755
  });
756
+ function isExternalHref(path) {
757
+ return path.startsWith("http") || path.startsWith("//") || path.startsWith("#");
758
+ }
759
+ function resolveLinkTo(to, localize, locale) {
760
+ if (typeof to !== "string") {
761
+ if (!localize) return to;
762
+ const path = to.path;
763
+ if (typeof path !== "string" || isExternalHref(path)) return to;
764
+ return {
765
+ ...to,
766
+ path: localize(path, locale)
767
+ };
768
+ }
769
+ if (isExternalHref(to) || !localize) return to;
770
+ return localize(to, locale);
771
+ }
756
772
  /**
757
773
  * Link — internal links render via RouterLink; external links (`http*`,
758
774
  * `//`, `#`) render as native `<a target="_blank" rel="noopener noreferrer">`.
@@ -790,19 +806,17 @@ const Link = defineComponent({
790
806
  noActiveClass: {
791
807
  type: Boolean,
792
808
  default: false
809
+ },
810
+ /** Target locale for path localization; omit to use the current locale. */
811
+ locale: {
812
+ type: String,
813
+ default: void 0
793
814
  }
794
815
  },
795
816
  setup(props, { slots, attrs }) {
796
817
  const localize = inject(LOCALIZE_PATH_KEY, null);
797
- const resolvedTo = computed(() => {
798
- const path = props.to ?? props.href ?? "";
799
- if (typeof path === "string" && (path.startsWith("http") || path.startsWith("//") || path.startsWith("#"))) return path;
800
- return localize ? localize(path) : path;
801
- });
802
- const isExternal = computed(() => {
803
- const path = String(resolvedTo.value);
804
- return path.startsWith("http") || path.startsWith("//") || path.startsWith("#");
805
- });
818
+ const resolvedTo = computed(() => resolveLinkTo(props.to ?? props.href ?? "", localize, props.locale));
819
+ const isExternal = computed(() => typeof resolvedTo.value === "string" && isExternalHref(resolvedTo.value));
806
820
  return () => {
807
821
  if (isExternal.value) return h("a", {
808
822
  ...attrs,
@@ -1024,7 +1038,7 @@ function createMatcherGuard(options = {}) {
1024
1038
  //#region src/head.ts
1025
1039
  /**
1026
1040
  * 将静态 `PageHead` push 进 head 实例(falsy 字段自动跳过)。
1027
- * 与 SSR 侧 `pushPageHead`(@ubean/ssr)语义一致,保证双端同构。
1041
+ * 与 SSR 侧 `pushPageHead`(@ubean/client/ssr)语义一致,保证双端同构。
1028
1042
  */
1029
1043
  function pushPageHead(head, pageHead) {
1030
1044
  const input = {};
@@ -52,6 +52,11 @@ interface PageMeta {
52
52
  * `head: true` 时写入 `route.meta.head` 并参与扫描输出。
53
53
  */
54
54
  head?: PageHead;
55
+ /**
56
+ * Per-page select SSR. Wins over `routeRules.ssr` and the global exclude list.
57
+ * `false` = CSR without loader; `'data-only'` = loader + CSR shell; `true` = SSR.
58
+ */
59
+ ssr?: boolean | 'streaming' | 'data-only';
55
60
  }
56
61
  /** 扫描得到的单个页面(字段与旧 `ScannedPageRoute` 完全兼容)。 */
57
62
  interface ScannedPage {
package/dist/vite.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { i as ScanPagesResult, n as PageMeta, r as ScanPagesOptions, t as PageHead } from "./types-VHF1RJu2.js";
1
+ import { i as ScanPagesResult, n as PageMeta, r as ScanPagesOptions, t as PageHead } from "./types-CeX4SJit.js";
2
2
  import { Plugin } from "vite";
3
3
  //#region src/scan-pages.d.ts
4
4
  /**
@@ -20,8 +20,15 @@ declare function scanPages(options: ScanPagesOptions): Promise<ScanPagesResult>;
20
20
  //#endregion
21
21
  //#region src/virtual-pages.d.ts
22
22
  type PagesModuleInput = Pick<ScanPagesResult, 'pages' | 'layouts' | 'notFoundPage' | 'loadingPage' | 'errorPage'>;
23
+ /**
24
+ * Optional compact locale prefix (plain data — `@ubean/vue` does not import i18n).
25
+ * Example: `':locale(zh)?'` for prefix_except_default with extra locales `zh`.
26
+ */
27
+ interface LocaleRouteCompileOptions {
28
+ vueParam?: string;
29
+ }
23
30
  /** Generate the virtual module source(plain JS — no TS syntax, SSR-safe)。 */
24
- declare function generatePagesModuleSource(input: PagesModuleInput): string;
31
+ declare function generatePagesModuleSource(input: PagesModuleInput, locale?: LocaleRouteCompileOptions): string;
25
32
  /**
26
33
  * Generate the ambient module declaration d.ts for the virtual module.
27
34
  *
@@ -161,4 +168,4 @@ declare function stripDefinePageCalls(code: string): string;
161
168
  declare function scanClientPages(root: string, options?: UbeanVueViteOptions): Promise<ScanPagesResult>;
162
169
  declare function ubeanVueVite(options?: UbeanVueViteOptions): Plugin;
163
170
  //#endregion
164
- export { type ParsedRoutePath, UbeanVueViteOptions, VUE_ROUTES_MODULE_ID, ubeanVueVite as default, ubeanVueVite, extractCallObject, extractDefinePage, extractDefinePageFromCode, extractSlotAndIntercept, filePathToRoute, generateLayoutName, generatePagesModuleSource, generateRouteName, generateTypedRouter, generateVirtualModuleDts, normalizePageHead, parseMatchers, scanClientPages, scanPages, stripDefinePageCalls, stripRouteGroups };
171
+ export { type LocaleRouteCompileOptions, type PagesModuleInput, type ParsedRoutePath, UbeanVueViteOptions, VUE_ROUTES_MODULE_ID, ubeanVueVite as default, ubeanVueVite, extractCallObject, extractDefinePage, extractDefinePageFromCode, extractSlotAndIntercept, filePathToRoute, generateLayoutName, generatePagesModuleSource, generateRouteName, generateTypedRouter, generateVirtualModuleDts, normalizePageHead, parseMatchers, scanClientPages, scanPages, stripDefinePageCalls, stripRouteGroups };
package/dist/vite.js CHANGED
@@ -301,6 +301,7 @@ function extractDefinePageFromCode(code) {
301
301
  if (typeof parsed.requiresAuth === "boolean") result.requiresAuth = parsed.requiresAuth;
302
302
  if (typeof parsed.cache === "boolean") result.cache = parsed.cache;
303
303
  if (typeof parsed.transition === "string") result.transition = parsed.transition;
304
+ if (parsed.ssr === false || parsed.ssr === true || parsed.ssr === "streaming" || parsed.ssr === "data-only") result.ssr = parsed.ssr;
304
305
  const head = normalizePageHead(parsed.head);
305
306
  if (head) result.head = head;
306
307
  return result;
@@ -790,6 +791,7 @@ function buildRouteMeta(page, extra = {}) {
790
791
  reuseTarget: page.isReuse ? page.reuseTarget : void 0,
791
792
  transition: page.pageMeta?.transition,
792
793
  requiresAuth: page.pageMeta?.requiresAuth === true ? true : void 0,
794
+ ssr: page.pageMeta?.ssr,
793
795
  matchers: page.matchers && Object.keys(page.matchers).length > 0 ? page.matchers : void 0,
794
796
  head: page.pageMeta?.head,
795
797
  ...page.pageMeta?.meta,
@@ -797,8 +799,13 @@ function buildRouteMeta(page, extra = {}) {
797
799
  };
798
800
  return JSON.stringify(meta);
799
801
  }
802
+ function withLocaleParam(routerPath, vueParam) {
803
+ if (!vueParam) return routerPath;
804
+ if (routerPath === "/") return `/${vueParam}`;
805
+ return `/${vueParam}${routerPath}`;
806
+ }
800
807
  /** Generate the virtual module source(plain JS — no TS syntax, SSR-safe)。 */
801
- function generatePagesModuleSource(input) {
808
+ function generatePagesModuleSource(input, locale) {
802
809
  const { pages, layouts } = input;
803
810
  const pageLoaders = [];
804
811
  const layoutLoaders = [];
@@ -838,11 +845,15 @@ function generatePagesModuleSource(input) {
838
845
  const componentParts = [];
839
846
  if (defaultPage) componentParts.push(`default: ${varNameFor(defaultPage.name)}`);
840
847
  for (const sp of slotPages) componentParts.push(`${JSON.stringify(sp.slot)}: ${varNameFor(sp.name)}`);
841
- routeEntries.push(` { path: ${JSON.stringify(routerPath)}, name: ${JSON.stringify(primaryPage.name)}, components: { ${componentParts.join(", ")} }, meta: ${buildRouteMeta(primaryPage, { parallelSlots: slotPages.map((s) => s.slot) })} }`);
842
- } else routeEntries.push(` { path: ${JSON.stringify(routerPath)}, name: ${JSON.stringify(defaultPage.name)}, component: ${varNameFor(defaultPage.name)}, meta: ${buildRouteMeta(defaultPage)} }`);
848
+ const vuePath = withLocaleParam(routerPath, locale?.vueParam);
849
+ routeEntries.push(` { path: ${JSON.stringify(vuePath)}, name: ${JSON.stringify(primaryPage.name)}, components: { ${componentParts.join(", ")} }, meta: ${buildRouteMeta(primaryPage, { parallelSlots: slotPages.map((s) => s.slot) })} }`);
850
+ } else {
851
+ const vuePath = withLocaleParam(routerPath, locale?.vueParam);
852
+ routeEntries.push(` { path: ${JSON.stringify(vuePath)}, name: ${JSON.stringify(defaultPage.name)}, component: ${varNameFor(defaultPage.name)}, meta: ${buildRouteMeta(defaultPage)} }`);
853
+ }
843
854
  }
844
855
  for (const p of interceptPages) {
845
- const routerPath = toVueRouterPath(p.route);
856
+ const routerPath = withLocaleParam(toVueRouterPath(p.route), locale?.vueParam);
846
857
  const interceptName = `__intercept_${p.name}`;
847
858
  routeEntries.push(` { path: ${JSON.stringify(routerPath)}, name: ${JSON.stringify(interceptName)}, component: ${varNameFor(p.name)}, meta: ${buildRouteMeta(p, {
848
859
  interceptFrom: p.interceptFrom,
@@ -853,7 +864,8 @@ function generatePagesModuleSource(input) {
853
864
  let hasNotFound = false;
854
865
  if (input.notFoundPage) {
855
866
  hasNotFound = true;
856
- routeEntries.push(` { path: ${JSON.stringify("/:pathMatch(.*)*")}, name: "NotFound", component: Page_NotFound, meta: { pageName: "NotFound" } }`);
867
+ const notFoundPath = withLocaleParam("/:pathMatch(.*)*", locale?.vueParam);
868
+ routeEntries.push(` { path: ${JSON.stringify(notFoundPath)}, name: "NotFound", component: Page_NotFound, meta: { pageName: "NotFound" } }`);
857
869
  }
858
870
  let loadingLoaderName = "null";
859
871
  if (input.loadingPage) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ubean/vue",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "Lean Vue client kernel & page-routing owner for ubean — plugin-first page routing outlet, page cache (keep-alive), transitions, reload signal, definePage macro + file-routing Vite plugin (/vite: multi-dir scan, reuse routes, markdown & page-head opt-in). Runtime deps: vue + vue-router only.",
5
5
  "files": [
6
6
  "dist"
@@ -19,8 +19,8 @@
19
19
  "import": "./dist/vite.js"
20
20
  },
21
21
  "./generator": {
22
- "types": "./dist/generator/index.d.ts",
23
- "import": "./dist/generator/index.js"
22
+ "types": "./dist/generator.d.ts",
23
+ "import": "./dist/generator.js"
24
24
  }
25
25
  },
26
26
  "dependencies": {
@@ -33,14 +33,14 @@
33
33
  },
34
34
  "devDependencies": {
35
35
  "@types/node": "^26.2.0",
36
- "@unhead/vue": "^3.3.2",
36
+ "@unhead/vue": "^3.4.0",
37
37
  "@vue/server-renderer": "^3.5.41",
38
38
  "happy-dom": "^20.11.6",
39
39
  "typescript": "7.0.2",
40
40
  "vite": "npm:@voidzero-dev/vite-plus-core@0.2.9",
41
41
  "vite-plus": "0.2.9",
42
42
  "vitest": "4.1.11",
43
- "@ubean/markdown": "0.2.1"
43
+ "@ubean/markdown": "0.3.0"
44
44
  },
45
45
  "peerDependencies": {
46
46
  "@ubean/markdown": ">=0.1.0",