@ubean/vue 0.5.2 → 0.6.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- import { a as ScannedLayout, o as ScannedPage } from "./types-CeX4SJit.js";
1
+ import { a as ScannedLayout, o as ScannedPage } from "./types-C1B8d1Ai.js";
2
2
  //#region src/generator.d.ts
3
3
  /**
4
4
  * 实体路由文件生成器(页面路由实体文件模式的唯一所有者,原
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-CeX4SJit.js";
1
+ import { a as ScannedLayout, i as ScanPagesResult, n as PageMeta, o as ScannedPage, r as ScanPagesOptions, t as PageHead } from "./types-C1B8d1Ai.js";
2
2
  import { Component, ComputedRef, InjectionKey, Plugin, PropType, Ref, SlotsType, VNode } from "vue";
3
3
  import { NavigationGuard, RouteMeta, Router } from "vue-router";
4
4
  //#region src/view-transitions.d.ts
@@ -142,8 +142,8 @@ interface ClientOnlySlots {
142
142
  * separate app by `hydrateIslands()` (content inside an island placeholder
143
143
  * is replaced on hydration, so `<ClientOnly>` inside island children is
144
144
  * wiped with it).
145
- * - `defineClientComponent` — programmatic wrapper (always renders a
146
- * `<div data-client-only>` wrapper element).
145
+ * - `defineClientComponent` — programmatic wrapper (renders the same
146
+ * `<!--client-only-->` comment placeholder, so it works in table/list contexts).
147
147
  */
148
148
  export declare const ClientOnly: import("vue").DefineSetupFnComponent<ClientOnlyProps, {}, SlotsType<ClientOnlySlots>, ClientOnlyProps & {}, import("vue").PublicProps>;
149
149
  /**
package/dist/index.js CHANGED
@@ -659,8 +659,8 @@ const ErrorBoundary = defineComponent({
659
659
  * separate app by `hydrateIslands()` (content inside an island placeholder
660
660
  * is replaced on hydration, so `<ClientOnly>` inside island children is
661
661
  * wiped with it).
662
- * - `defineClientComponent` — programmatic wrapper (always renders a
663
- * `<div data-client-only>` wrapper element).
662
+ * - `defineClientComponent` — programmatic wrapper (renders the same
663
+ * `<!--client-only-->` comment placeholder, so it works in table/list contexts).
664
664
  */
665
665
  const ClientOnly = defineComponent((props, { slots }) => {
666
666
  const mounted = ref(false);
@@ -977,12 +977,25 @@ function isActiveRoute(currentPath, targetHref, exact = false) {
977
977
  //#endregion
978
978
  //#region src/matchers.ts
979
979
  /**
980
- * 全局 matcher 注册表(进程单例)。
980
+ * matcher 注册表 —— **真·进程单例**(挂在 `globalThis` 上)。
981
981
  *
982
- * server / client 运行时各自维护一份;dev 模式下 HMR 不会自动清理注册表,
983
- * 但 `defineMatcher` 同名覆盖,因此重新加载 matcher 文件会自动更新函数引用。
982
+ * 为什么不能用模块级 `Map`:模块级状态只在**同一个模块实例**内共享,而同一个进程里常常有第二份
983
+ * `@ubean/vue`(dev 的 SSR 图会把 `ubean` 内联、却把 `@ubean/vue` 外部化;pnpm 也可能装出两份)。
984
+ * 那样「用户注册」与「router 校验」读的是两个 Map,表现为**所有 `[id=numeric]` 路由一律 404**
985
+ * (实测:`validateParams` 对未注册名保守返回 false)。i18n 的 ALS 与 `@ubean/build` 的模块注册表
986
+ * 都出于同一理由挂在 `globalThis` 上(见 `packages/builder/src/registry.ts`)。
987
+ *
988
+ * 同名重复注册会覆盖 —— HMR 重新求值 matcher 文件时因此能拿到最新函数引用。
984
989
  */
985
- const matcherRegistry = /* @__PURE__ */ new Map();
990
+ const MATCHERS_KEY = "__ubean_route_matchers__";
991
+ function getMatcherRegistry() {
992
+ const store = globalThis;
993
+ const existing = store[MATCHERS_KEY];
994
+ if (existing instanceof Map) return existing;
995
+ const created = /* @__PURE__ */ new Map();
996
+ store[MATCHERS_KEY] = created;
997
+ return created;
998
+ }
986
999
  /**
987
1000
  * 定义并注册一个命名 route matcher。
988
1001
  *
@@ -1000,26 +1013,26 @@ const matcherRegistry = /* @__PURE__ */ new Map();
1000
1013
  function defineMatcher(name, fn) {
1001
1014
  if (typeof name !== "string" || name.length === 0) throw new TypeError(`[ubean] defineMatcher: name must be a non-empty string, got: ${typeof name}`);
1002
1015
  if (typeof fn !== "function") throw new TypeError(`[ubean] defineMatcher: fn must be a function, got: ${typeof fn}`);
1003
- matcherRegistry.set(name, fn);
1016
+ getMatcherRegistry().set(name, fn);
1004
1017
  return fn;
1005
1018
  }
1006
1019
  /**
1007
1020
  * 按名称获取已注册的 matcher。未注册时返回 `undefined`。
1008
1021
  */
1009
1022
  function getMatcher(name) {
1010
- return matcherRegistry.get(name);
1023
+ return getMatcherRegistry().get(name);
1011
1024
  }
1012
1025
  /**
1013
1026
  * 判断指定名称的 matcher 是否已注册。
1014
1027
  */
1015
1028
  function hasMatcher(name) {
1016
- return matcherRegistry.has(name);
1029
+ return getMatcherRegistry().has(name);
1017
1030
  }
1018
1031
  /**
1019
1032
  * 获取所有已注册 matcher 的名称列表(主要用于调试 / DevTools)。
1020
1033
  */
1021
1034
  function listMatcherNames() {
1022
- return [...matcherRegistry.keys()];
1035
+ return [...getMatcherRegistry().keys()];
1023
1036
  }
1024
1037
  /**
1025
1038
  * 清空所有已注册的 matcher。
@@ -1027,7 +1040,7 @@ function listMatcherNames() {
1027
1040
  * **仅供测试使用** —— 应用代码不应调用,避免误删其他模块注册的 matcher。
1028
1041
  */
1029
1042
  function clearMatchers() {
1030
- matcherRegistry.clear();
1043
+ getMatcherRegistry().clear();
1031
1044
  }
1032
1045
  /**
1033
1046
  * 校验一组路由参数是否通过对应的 matcher。
@@ -76,9 +76,6 @@ interface ScannedPage {
76
76
  frontmatter?: Record<string, unknown>;
77
77
  /** Parallel route slot name(`@slotName/` 目录约定)。 */
78
78
  slot?: string;
79
- /** Intercepting route:从 `interceptFrom` 导航到 `interceptTarget` 时拦截。 */
80
- interceptFrom?: string;
81
- interceptTarget?: string;
82
79
  /** 路由参数 → matcher 名称映射(`[param=matcher]` 语法)。 */
83
80
  matchers?: Record<string, string>;
84
81
  }
package/dist/vite.d.ts CHANGED
@@ -1,20 +1,16 @@
1
- import { i as ScanPagesResult, n as PageMeta, r as ScanPagesOptions, t as PageHead } from "./types-CeX4SJit.js";
1
+ import { i as ScanPagesResult, n as PageMeta, r as ScanPagesOptions, t as PageHead } from "./types-C1B8d1Ai.js";
2
2
  import { Plugin } from "vite";
3
3
  //#region src/scan-pages.d.ts
4
4
  /**
5
- * Extract parallel route slot name and intercept info from a relative file path.
5
+ * Extract parallel route slot name from a relative file path.
6
6
  *
7
7
  * Parallel routes: `@slotName/page.vue` → slot = 'slotName'
8
- * Intercepting routes:
9
- * `(..)target/page.vue` → intercept from parent, target = 'target'
10
- * `(.)target/page.vue` → intercept from same level, target = 'target'
11
- * `(...)target/page.vue` → intercept from root, target = 'target'
8
+ *
9
+ * 拦截路由标记段会让本函数抛错(见 `INTERCEPT_MARKER_RE`)。
12
10
  */
13
- export declare function extractSlotAndIntercept(fileBase: string): {
11
+ export declare function extractSlotFromPath(fileBase: string): {
14
12
  cleanedBase: string;
15
13
  slot?: string;
16
- interceptFrom?: string;
17
- interceptTarget?: string;
18
14
  };
19
15
  export declare function scanPages(options: ScanPagesOptions): Promise<ScanPagesResult>;
20
16
  //#endregion
package/dist/vite.js CHANGED
@@ -457,9 +457,12 @@ function filePathToRoute(filePath, prefix = "/") {
457
457
  * - markdown 页面(opt-in,`@ubean/markdown` 按需加载解析 frontmatter)
458
458
  * - 页面级 head(opt-in,`definePage({ head })` / frontmatter `head`)
459
459
  * - 特殊页:`404` / `loading` / `error`(仅页面目录根级)
460
- * - 并行路由 `@slot/` 与拦截路由 `(..)target/` `(.)target/` `(...)target/`
460
+ * - 并行路由 `@slot/`
461
461
  * - `[param=matcher]` 语法 → matchers 映射
462
462
  *
463
+ * 拦截路由(`(.)` / `(..)` / `(...)` 目录约定)**刻意不支持** —— 扫到标记段直接抛错,见
464
+ * `extractSlotFromPath()` 的说明与 [docs/adr/0010]。
465
+ *
463
466
  * `@ubean/scan` 聚合层的 `scanProject` 委托本模块。
464
467
  */
465
468
  /** 轻量告警(去重)—— 保持本包零 `@ubean/*` 硬依赖,不用 @ubean/logger。 */
@@ -496,43 +499,42 @@ function buildMarkdownHead(fm, enabled) {
496
499
  return Object.keys(head).length > 0 ? head : void 0;
497
500
  }
498
501
  /**
499
- * Extract parallel route slot name and intercept info from a relative file path.
502
+ * 拦截路由的标记段(Next 的 `(.)target` / `(..)target` / `(...)target` 目录约定)。
503
+ *
504
+ * **刻意不支持**([docs/adr/0010] 的刻意不做清单)。本仓一度按约定扫出 `interceptFrom` /
505
+ * `interceptTarget` 元数据、把页面注册成 `__intercept_*` 路由,但**全仓没有消费者**:
506
+ * 「从 X 导航到 Y 时渲染拦截页」从未发生,拦截页只能落在它被清理后的路径上(一个谁也不会
507
+ * 访问的废 URL)。而且真要接线,代价在运行时而非约定 —— 需要守卫 + 同 URL 双记录 + 故意的
508
+ * SSR/客户端分叉(与本仓「两侧一致」的原则冲突),且背景页会重挂载(Next 原生保留上下文)。
509
+ * 核心价值(URL 可分享、back 关闭对话框)用已有的并行路由 + 守卫在应用侧就能拿到,
510
+ * 所以按「单独『竞品有』→ 刻意不做」处理。
511
+ *
512
+ * 因此扫到标记段**响亮失败**而不是静默降级:不拦的话它会退化成 `/feed/(.)photo/:id`
513
+ * 这种字面垃圾路径(路由组正则 `\(([^(/\\]+)\)[/\\]` 只吞整段 `(group)/`,不吞 `(.)photo`)。
514
+ */
515
+ const INTERCEPT_MARKER_RE = /^\((\.{1,3})\)(.+)$/;
516
+ /**
517
+ * Extract parallel route slot name from a relative file path.
500
518
  *
501
519
  * Parallel routes: `@slotName/page.vue` → slot = 'slotName'
502
- * Intercepting routes:
503
- * `(..)target/page.vue` → intercept from parent, target = 'target'
504
- * `(.)target/page.vue` → intercept from same level, target = 'target'
505
- * `(...)target/page.vue` → intercept from root, target = 'target'
520
+ *
521
+ * 拦截路由标记段会让本函数抛错(见 `INTERCEPT_MARKER_RE`)。
506
522
  */
507
- function extractSlotAndIntercept(fileBase) {
523
+ function extractSlotFromPath(fileBase) {
508
524
  const segments = fileBase.split("/");
509
525
  let slot;
510
- let interceptFrom;
511
- let interceptTarget;
512
526
  const cleanedSegments = [];
513
- for (let i = 0; i < segments.length; i++) {
514
- const seg = segments[i];
527
+ for (const seg of segments) {
515
528
  if (seg.startsWith("@")) {
516
529
  slot = seg.slice(1);
517
530
  continue;
518
531
  }
519
- const interceptMatch = seg.match(/^\((\.{1,3})\)(.+)$/);
520
- if (interceptMatch) {
521
- const dots = interceptMatch[1];
522
- interceptTarget = interceptMatch[2];
523
- const prefixSegments = cleanedSegments.slice(0, i);
524
- if (dots === "..") interceptFrom = `/${prefixSegments.slice(0, -1).join("/")}`;
525
- else if (dots === "...") interceptFrom = "/";
526
- else interceptFrom = `/${prefixSegments.join("/")}`;
527
- continue;
528
- }
532
+ if (INTERCEPT_MARKER_RE.test(seg)) throw new Error(`[ubean] 拦截路由目录约定不被支持:发现了 "${seg}"(文件:${fileBase})。ubean 刻意不做这个约定(见 docs/adr/0010 的刻意不做清单)—— 它需要故意的 SSR/客户端分叉,而核心价值(URL 可分享、back 关闭)用并行路由就能拿到。请改写为「并行路由 + <SlotView>」:把对话框页面放进 @dialog/ 目录(如 src/pages/@dialog/photo/[id].vue),在布局里用 <SlotView name="dialog" /> 承载,用查询标记之类的条件决定它何时出现(站点文档 guide/pages-routing 的 "Dialogs and shareable URLs")。`);
529
533
  cleanedSegments.push(seg);
530
534
  }
531
535
  return {
532
536
  cleanedBase: cleanedSegments.join("/"),
533
- slot,
534
- interceptFrom,
535
- interceptTarget
537
+ slot
536
538
  };
537
539
  }
538
540
  /**
@@ -609,7 +611,7 @@ async function scanPages(options) {
609
611
  const isReuse = !isMarkdown && /\.reuse\.(ts|js)$/.test(relativePath);
610
612
  const pageBase = isReuse ? base.slice(0, -6) : base;
611
613
  const dirPart = dirname(relativePath) === "." ? "" : dirname(relativePath);
612
- const { cleanedBase, slot, interceptFrom, interceptTarget } = extractSlotAndIntercept(dirPart ? `${dirPart}/${pageBase}` : pageBase);
614
+ const { cleanedBase, slot } = extractSlotFromPath(dirPart ? `${dirPart}/${pageBase}` : pageBase);
613
615
  const fileBase = cleanedBase;
614
616
  if (!isReuse && dirPart === "" && (pageBase === "404" || pageBase === "loading" || pageBase === "error")) {
615
617
  const { route: specialRoute, cleaned: specialCleaned } = filePathToRoute(fileBase);
@@ -679,8 +681,6 @@ async function scanPages(options) {
679
681
  pageMeta: pageMeta || void 0,
680
682
  frontmatter,
681
683
  slot,
682
- interceptFrom,
683
- interceptTarget,
684
684
  matchers
685
685
  });
686
686
  }
@@ -821,12 +821,7 @@ function generatePagesModuleSource(input, locale) {
821
821
  if (input.notFoundPage) pageLoaders.push(`const Page_NotFound = () => import(${JSON.stringify(input.notFoundPage.fullPath)}).then(m => m.default || m);`);
822
822
  const namedPages = input.notFoundPage ? [...sortedPages, input.notFoundPage] : sortedPages;
823
823
  const routeGroups = /* @__PURE__ */ new Map();
824
- const interceptPages = [];
825
824
  for (const p of sortedPages) {
826
- if (p.interceptTarget) {
827
- interceptPages.push(p);
828
- continue;
829
- }
830
825
  const routerPath = toVueRouterPath(p.route);
831
826
  let group = routeGroups.get(routerPath);
832
827
  if (!group) {
@@ -852,15 +847,6 @@ function generatePagesModuleSource(input, locale) {
852
847
  routeEntries.push(` { path: ${JSON.stringify(vuePath)}, name: ${JSON.stringify(defaultPage.name)}, component: ${varNameFor(defaultPage.name)}, meta: ${buildRouteMeta(defaultPage)} }`);
853
848
  }
854
849
  }
855
- for (const p of interceptPages) {
856
- const routerPath = withLocaleParam(toVueRouterPath(p.route), locale?.vueParam);
857
- const interceptName = `__intercept_${p.name}`;
858
- routeEntries.push(` { path: ${JSON.stringify(routerPath)}, name: ${JSON.stringify(interceptName)}, component: ${varNameFor(p.name)}, meta: ${buildRouteMeta(p, {
859
- interceptFrom: p.interceptFrom,
860
- interceptTarget: p.interceptTarget,
861
- isIntercepting: true
862
- })} }`);
863
- }
864
850
  let hasNotFound = false;
865
851
  if (input.notFoundPage) {
866
852
  hasNotFound = true;
@@ -1198,4 +1184,4 @@ function ubeanVueVite(options = {}) {
1198
1184
  };
1199
1185
  }
1200
1186
  //#endregion
1201
- export { VUE_ROUTES_MODULE_ID, ubeanVueVite as default, ubeanVueVite, extractCallObject, extractDefinePage, extractDefinePageFromCode, extractSlotAndIntercept, filePathToRoute, generateLayoutName, generatePagesModuleSource, generateRouteName, generateTypedRouter, generateVirtualModuleDts, normalizePageHead, parseMatchers, scanClientPages, scanPages, stripDefinePageCalls, stripRouteGroups };
1187
+ export { VUE_ROUTES_MODULE_ID, ubeanVueVite as default, ubeanVueVite, extractCallObject, extractDefinePage, extractDefinePageFromCode, extractSlotFromPath, filePathToRoute, generateLayoutName, generatePagesModuleSource, generateRouteName, generateTypedRouter, generateVirtualModuleDts, normalizePageHead, parseMatchers, scanClientPages, scanPages, stripDefinePageCalls, stripRouteGroups };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ubean/vue",
3
- "version": "0.5.2",
3
+ "version": "0.6.0-beta.2",
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"
@@ -28,19 +28,19 @@
28
28
  "scule": "^1.3.0",
29
29
  "tinyglobby": "^0.2.17",
30
30
  "ufo": "^1.6.4",
31
- "vue": "^3.5.42",
31
+ "vue": "^3.5.43",
32
32
  "vue-router": "^5.3.1"
33
33
  },
34
34
  "devDependencies": {
35
- "@types/node": "^26.5.0",
36
- "@ubean/markdown": "0.5.2",
37
- "@unhead/vue": "^3.4.0",
38
- "@vue/server-renderer": "^3.5.42",
35
+ "@types/node": "^26.6.1",
36
+ "@ubean/markdown": "0.6.0-beta.2",
37
+ "@unhead/vue": "^3.4.1",
38
+ "@vue/server-renderer": "^3.5.43",
39
39
  "happy-dom": "^20.14.5",
40
40
  "typescript": "npm:typescript-native-bridge@latest",
41
- "vite": "npm:@voidzero-dev/vite-plus-core@0.3.1",
42
- "vite-plus": "0.3.1",
43
- "vitest": "5.0.0"
41
+ "vite": "npm:@voidzero-dev/vite-plus-core@0.3.3",
42
+ "vite-plus": "0.3.3",
43
+ "vitest": "5.0.1"
44
44
  },
45
45
  "peerDependencies": {
46
46
  "@ubean/markdown": ">=0.1.0",