@ubean/vue 0.5.1 → 0.6.0-beta.1
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/dist/generator.d.ts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +22 -9
- package/dist/{types-CeX4SJit.d.ts → types-C1B8d1Ai.d.ts} +0 -3
- package/dist/vite.d.ts +5 -9
- package/dist/vite.js +28 -42
- package/package.json +2 -2
package/dist/generator.d.ts
CHANGED
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-
|
|
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
|
package/dist/index.js
CHANGED
|
@@ -977,12 +977,25 @@ function isActiveRoute(currentPath, targetHref, exact = false) {
|
|
|
977
977
|
//#endregion
|
|
978
978
|
//#region src/matchers.ts
|
|
979
979
|
/**
|
|
980
|
-
*
|
|
980
|
+
* matcher 注册表 —— **真·进程单例**(挂在 `globalThis` 上)。
|
|
981
981
|
*
|
|
982
|
-
*
|
|
983
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
|
1023
|
+
return getMatcherRegistry().get(name);
|
|
1011
1024
|
}
|
|
1012
1025
|
/**
|
|
1013
1026
|
* 判断指定名称的 matcher 是否已注册。
|
|
1014
1027
|
*/
|
|
1015
1028
|
function hasMatcher(name) {
|
|
1016
|
-
return
|
|
1029
|
+
return getMatcherRegistry().has(name);
|
|
1017
1030
|
}
|
|
1018
1031
|
/**
|
|
1019
1032
|
* 获取所有已注册 matcher 的名称列表(主要用于调试 / DevTools)。
|
|
1020
1033
|
*/
|
|
1021
1034
|
function listMatcherNames() {
|
|
1022
|
-
return [...
|
|
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
|
-
|
|
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-
|
|
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
|
|
5
|
+
* Extract parallel route slot name from a relative file path.
|
|
6
6
|
*
|
|
7
7
|
* Parallel routes: `@slotName/page.vue` → slot = 'slotName'
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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
|
|
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/`
|
|
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
|
-
*
|
|
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
|
-
*
|
|
503
|
-
*
|
|
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
|
|
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 (
|
|
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
|
-
|
|
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
|
|
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,
|
|
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.
|
|
3
|
+
"version": "0.6.0-beta.1",
|
|
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"
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"@types/node": "^26.5.0",
|
|
36
|
-
"@ubean/markdown": "0.
|
|
36
|
+
"@ubean/markdown": "0.6.0-beta.1",
|
|
37
37
|
"@unhead/vue": "^3.4.0",
|
|
38
38
|
"@vue/server-renderer": "^3.5.42",
|
|
39
39
|
"happy-dom": "^20.14.5",
|