@ubean/routing 0.1.2 → 0.1.4
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/index.d.ts +89 -0
- package/dist/generator/index.js +300 -0
- package/dist/index.d.ts +75 -0
- package/dist/index.js +1057 -0
- package/dist/types.d.ts +157 -0
- package/dist/types.js +19 -0
- package/package.json +5 -5
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { ScanResult, ScannedLayout, ScannedPageRoute } from "../types.js";
|
|
2
|
+
//#region src/generator/index.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* Options for the physical route file generator.
|
|
5
|
+
*
|
|
6
|
+
* The generator produces 3 files under `outDir`:
|
|
7
|
+
* - `routes.ts` — flat route records (`RouteRecord[]`) with name/path/component/layout/meta
|
|
8
|
+
* - `imports.ts` — lazy `views` and `layouts` records (for `import.meta.glob`-free consumption)
|
|
9
|
+
* - `typed-router.d.ts` — type definitions:
|
|
10
|
+
* - `@ubean/routing` 模块增强:`RouteKey`、`RoutePath`、`RouteLayoutKey`、`ReuseRouteKey`、`RoutePathMap`
|
|
11
|
+
* - `vue-router/auto-routes` 模块增强:`RouteNamedMap`(name → `RouteRecordInfo<Name, Path, ParamsRaw, Params>`)
|
|
12
|
+
* - `vue-router` 模块增强:`TypesConfig.RouteNamedMap` 引用上面的 `RouteNamedMap`
|
|
13
|
+
*
|
|
14
|
+
* `RouteNamedMap` 让 `vue-router` 的 `useRoute<Name>(name)` 能根据路由名推断
|
|
15
|
+
* `route.params` 的类型(动态路由 `:id` / `:id?` 自动类型化),无需用户手动断言。
|
|
16
|
+
* 这与 elegant-router 的 `typed-router.d.ts` 行为对齐。
|
|
17
|
+
*/
|
|
18
|
+
interface GeneratorOptions {
|
|
19
|
+
/** Project root directory (absolute). */
|
|
20
|
+
cwd: string;
|
|
21
|
+
/** Output directory for `routes.ts` / `imports.ts` (absolute or relative to `cwd`). */
|
|
22
|
+
outDir: string;
|
|
23
|
+
/** Path for `typed-router.d.ts` (absolute or relative to `cwd`). Default: `<outDir>/typed-router.d.ts`. */
|
|
24
|
+
dtsPath?: string;
|
|
25
|
+
/** Whether to generate `routes.ts`. Default: `true`. */
|
|
26
|
+
generateRoutes?: boolean;
|
|
27
|
+
/** Whether to generate `imports.ts`. Default: `true`. */
|
|
28
|
+
generateImports?: boolean;
|
|
29
|
+
/** Whether to generate `typed-router.d.ts`. Default: `true`. */
|
|
30
|
+
generateDts?: boolean;
|
|
31
|
+
/** Use lazy `() => import(...)` for views. Default: `true`. */
|
|
32
|
+
routeLazy?: boolean | ((page: ScannedPageRoute) => boolean);
|
|
33
|
+
/** Use lazy `() => import(...)` for layouts. Default: `true`. */
|
|
34
|
+
layoutLazy?: boolean | ((layout: ScannedLayout) => boolean);
|
|
35
|
+
/**
|
|
36
|
+
* Compute extra `meta` for a route. The returned object is merged
|
|
37
|
+
* on top of the page's existing `pageMeta.meta` (from `definePage` / frontmatter).
|
|
38
|
+
* Return `null` to skip. Existing keys are NOT overwritten.
|
|
39
|
+
*/
|
|
40
|
+
getRouteMeta?: (page: ScannedPageRoute) => Record<string, unknown> | null;
|
|
41
|
+
/**
|
|
42
|
+
* Compute the import specifier for a page file. Receives the absolute
|
|
43
|
+
* `fullPath` of the page; should return a module specifier usable from
|
|
44
|
+
* `imports.ts`. Defaults to a project-relative path prefixed with `@/`.
|
|
45
|
+
*/
|
|
46
|
+
getImportPath?: (page: ScannedPageRoute) => string;
|
|
47
|
+
/** Same as `getImportPath` but for layouts. */
|
|
48
|
+
getLayoutImportPath?: (layout: ScannedLayout) => string;
|
|
49
|
+
/**
|
|
50
|
+
* Header comment prepended to every generated file. Default: a
|
|
51
|
+
* `// @generated by ubean-routing — do not edit` notice with a timestamp.
|
|
52
|
+
*/
|
|
53
|
+
headerComment?: string;
|
|
54
|
+
}
|
|
55
|
+
interface GeneratorResult {
|
|
56
|
+
routesPath?: string;
|
|
57
|
+
importsPath?: string;
|
|
58
|
+
dtsPath?: string;
|
|
59
|
+
routeCount: number;
|
|
60
|
+
layoutCount: number;
|
|
61
|
+
}
|
|
62
|
+
declare const DEFAULT_HEADER_COMMENT: string;
|
|
63
|
+
declare function generateRouteFiles(scanResult: ScanResult, options: GeneratorOptions): Promise<GeneratorResult>;
|
|
64
|
+
declare class RouteFileGenerator {
|
|
65
|
+
private readonly opts;
|
|
66
|
+
constructor(options: GeneratorOptions);
|
|
67
|
+
generate(scanResult: ScanResult): Promise<GeneratorResult>;
|
|
68
|
+
renderRoutesFile(scanResult: ScanResult): string;
|
|
69
|
+
private renderRouteRecord;
|
|
70
|
+
private computeMeta;
|
|
71
|
+
renderImportsFile(scanResult: ScanResult): string;
|
|
72
|
+
private renderLayoutImport;
|
|
73
|
+
private renderViewImport;
|
|
74
|
+
renderDtsFile(scanResult: ScanResult): string;
|
|
75
|
+
/**
|
|
76
|
+
* 渲染单条路由的 `RouteRecordInfo<Name, Path, ParamsRaw, Params>`。
|
|
77
|
+
*
|
|
78
|
+
* 参数类型映射:
|
|
79
|
+
* - 必需参数(`/users/:id`) → `{ id: ParamValue<true> }` / `{ id: ParamValue<false> }`
|
|
80
|
+
* - 可选参数(`/users/:id?`) → `{ id?: ParamValueZeroOrOne<true> }` / `{ id?: ParamValueZeroOrOne<false> }`
|
|
81
|
+
* - 无参数(`/about`) → `Record<never, never>`
|
|
82
|
+
*
|
|
83
|
+
* `ParamValue<true>` 表示原始输入类型(必需为 string),`ParamValue<false>` 表示
|
|
84
|
+
* 解析后类型(已编码/解码,可能为 `string | undefined`)。这是 vue-router 的约定。
|
|
85
|
+
*/
|
|
86
|
+
private renderRouteRecordInfo;
|
|
87
|
+
}
|
|
88
|
+
//#endregion
|
|
89
|
+
export { DEFAULT_HEADER_COMMENT, GeneratorOptions, GeneratorResult, RouteFileGenerator, generateRouteFiles };
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
//#region src/generator/index.ts
|
|
2
|
+
const DEFAULT_HEADER_COMMENT = "// @generated by @ubean/routing/generator — do not edit manually.\n// This file is regenerated on every dev start / build.";
|
|
3
|
+
async function generateRouteFiles(scanResult, options) {
|
|
4
|
+
return new RouteFileGenerator(options).generate(scanResult);
|
|
5
|
+
}
|
|
6
|
+
var RouteFileGenerator = class {
|
|
7
|
+
opts;
|
|
8
|
+
constructor(options) {
|
|
9
|
+
const dtsPath = options.dtsPath ?? joinPosix(options.outDir, "typed-router.d.ts");
|
|
10
|
+
this.opts = {
|
|
11
|
+
cwd: options.cwd,
|
|
12
|
+
outDir: options.outDir,
|
|
13
|
+
dtsPath,
|
|
14
|
+
generateRoutes: options.generateRoutes ?? true,
|
|
15
|
+
generateImports: options.generateImports ?? true,
|
|
16
|
+
generateDts: options.generateDts ?? true,
|
|
17
|
+
routeLazy: options.routeLazy ?? true,
|
|
18
|
+
layoutLazy: options.layoutLazy ?? true,
|
|
19
|
+
headerComment: options.headerComment ?? "// @generated by @ubean/routing/generator — do not edit manually.\n// This file is regenerated on every dev start / build.",
|
|
20
|
+
getRouteMeta: options.getRouteMeta,
|
|
21
|
+
getImportPath: options.getImportPath,
|
|
22
|
+
getLayoutImportPath: options.getLayoutImportPath
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
async generate(scanResult) {
|
|
26
|
+
const { cwd, outDir, dtsPath, generateRoutes, generateImports, generateDts } = this.opts;
|
|
27
|
+
const routesPath = generateRoutes ? joinPosix(outDir, "routes.ts") : void 0;
|
|
28
|
+
const importsPath = generateImports ? joinPosix(outDir, "imports.ts") : void 0;
|
|
29
|
+
const finalDtsPath = generateDts ? dtsPath : void 0;
|
|
30
|
+
await Promise.all([
|
|
31
|
+
routesPath ? writeFile(resolvePath(cwd, routesPath), this.renderRoutesFile(scanResult)) : null,
|
|
32
|
+
importsPath ? writeFile(resolvePath(cwd, importsPath), this.renderImportsFile(scanResult)) : null,
|
|
33
|
+
finalDtsPath ? writeFile(resolvePath(cwd, finalDtsPath), this.renderDtsFile(scanResult)) : null
|
|
34
|
+
]);
|
|
35
|
+
return {
|
|
36
|
+
routesPath,
|
|
37
|
+
importsPath,
|
|
38
|
+
dtsPath: finalDtsPath,
|
|
39
|
+
routeCount: scanResult.pages.length,
|
|
40
|
+
layoutCount: scanResult.layouts.length
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
renderRoutesFile(scanResult) {
|
|
44
|
+
return `${this.opts.headerComment}
|
|
45
|
+
|
|
46
|
+
import type { RouteRecord } from './imports';
|
|
47
|
+
|
|
48
|
+
export const routes: RouteRecord[] = [
|
|
49
|
+
${scanResult.pages.map((p) => this.renderRouteRecord(p)).join(",\n")}
|
|
50
|
+
];
|
|
51
|
+
`;
|
|
52
|
+
}
|
|
53
|
+
renderRouteRecord(page) {
|
|
54
|
+
const parts = [];
|
|
55
|
+
parts.push(` name: ${JSON.stringify(page.name)}`);
|
|
56
|
+
parts.push(` path: ${JSON.stringify(page.route)}`);
|
|
57
|
+
const component = this.opts.getImportPath?.(page) ?? defaultPageImportPath(page, this.opts.cwd);
|
|
58
|
+
parts.push(` component: ${JSON.stringify(component)}`);
|
|
59
|
+
if (page.layout !== void 0) {
|
|
60
|
+
const layoutVal = page.layout === false ? "false" : JSON.stringify(page.layout);
|
|
61
|
+
parts.push(` layout: ${layoutVal}`);
|
|
62
|
+
}
|
|
63
|
+
if (page.isReuse) parts.push(` reuse: true`);
|
|
64
|
+
const meta = this.computeMeta(page);
|
|
65
|
+
if (meta && Object.keys(meta).length > 0) parts.push(` meta: ${JSON.stringify(meta)}`);
|
|
66
|
+
if (page.pageMeta?.cache === true) parts.push(` cache: true`);
|
|
67
|
+
if (page.pageMeta?.requiresAuth === true) parts.push(` requiresAuth: true`);
|
|
68
|
+
if (page.pageMeta?.middleware) {
|
|
69
|
+
const mw = page.pageMeta.middleware;
|
|
70
|
+
const mwVal = Array.isArray(mw) ? JSON.stringify(mw) : JSON.stringify(mw);
|
|
71
|
+
parts.push(` middleware: ${mwVal}`);
|
|
72
|
+
}
|
|
73
|
+
return ` {\n${parts.join(",\n")}\n }`;
|
|
74
|
+
}
|
|
75
|
+
computeMeta(page) {
|
|
76
|
+
const base = page.pageMeta?.meta ?? {};
|
|
77
|
+
const merged = {
|
|
78
|
+
...this.opts.getRouteMeta?.(page) ?? {},
|
|
79
|
+
...base
|
|
80
|
+
};
|
|
81
|
+
return Object.keys(merged).length > 0 ? merged : null;
|
|
82
|
+
}
|
|
83
|
+
renderImportsFile(scanResult) {
|
|
84
|
+
const header = this.opts.headerComment;
|
|
85
|
+
const layouts = scanResult.layouts.map((l) => this.renderLayoutImport(l)).join(",\n");
|
|
86
|
+
const views = scanResult.pages.filter((p) => !p.isReuse).map((p) => this.renderViewImport(p)).join(",\n");
|
|
87
|
+
return `${header}
|
|
88
|
+
|
|
89
|
+
export type LayoutKey = ${scanResult.layouts.length > 0 ? `'${scanResult.layouts.map((l) => l.name).join("' | '")}'` : "string"};
|
|
90
|
+
export type RouteKey = ${scanResult.pages.length > 0 ? `'${scanResult.pages.map((p) => p.name).join("' | '")}'` : "string"};
|
|
91
|
+
|
|
92
|
+
export type Lazy<T> = () => Promise<T>;
|
|
93
|
+
export type RawRouteComponent = import('vue').Component | Lazy<import('vue').Component>;
|
|
94
|
+
|
|
95
|
+
export interface RouteRecord {
|
|
96
|
+
name: RouteKey;
|
|
97
|
+
path: string;
|
|
98
|
+
component: string;
|
|
99
|
+
layout?: LayoutKey | false;
|
|
100
|
+
reuse?: boolean;
|
|
101
|
+
meta?: Record<string, unknown>;
|
|
102
|
+
cache?: boolean;
|
|
103
|
+
requiresAuth?: boolean;
|
|
104
|
+
middleware?: string | string[];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export const layouts: Record<LayoutKey, RawRouteComponent> = {
|
|
108
|
+
${layouts}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
export const views: Record<RouteKey, RawRouteComponent> = {
|
|
112
|
+
${views}
|
|
113
|
+
};
|
|
114
|
+
`;
|
|
115
|
+
}
|
|
116
|
+
renderLayoutImport(layout) {
|
|
117
|
+
const importPath = this.opts.getLayoutImportPath?.(layout) ?? defaultLayoutImportPath(layout, this.opts.cwd);
|
|
118
|
+
if (typeof this.opts.layoutLazy === "function" ? this.opts.layoutLazy(layout) : this.opts.layoutLazy) return ` ${layout.name}: () => import(${JSON.stringify(importPath)})`;
|
|
119
|
+
const importName = `__layout_${layout.name}`;
|
|
120
|
+
return ` ${layout.name}: ${importName}, // import ${importName} from ${JSON.stringify(importPath)}`;
|
|
121
|
+
}
|
|
122
|
+
renderViewImport(page) {
|
|
123
|
+
const importPath = this.opts.getImportPath?.(page) ?? defaultPageImportPath(page, this.opts.cwd);
|
|
124
|
+
if (typeof this.opts.routeLazy === "function" ? this.opts.routeLazy(page) : this.opts.routeLazy) return ` ${page.name}: () => import(${JSON.stringify(importPath)})`;
|
|
125
|
+
return ` ${page.name}: ${page.name}, // import ${page.name} from ${JSON.stringify(importPath)}`;
|
|
126
|
+
}
|
|
127
|
+
renderDtsFile(scanResult) {
|
|
128
|
+
const header = this.opts.headerComment;
|
|
129
|
+
const pathMapEntries = scanResult.pages.length > 0 ? scanResult.pages.map((p) => ` "${p.name}": "${p.route}";`).join("\n") : " // (no pages scanned)";
|
|
130
|
+
const reuseRoutes = scanResult.pages.filter((p) => p.isReuse);
|
|
131
|
+
const reuseKeys = reuseRoutes.length > 0 ? reuseRoutes.map((p) => `"${p.name}"`).join(" | ") : "never";
|
|
132
|
+
return `${header}
|
|
133
|
+
|
|
134
|
+
declare module '@ubean/routing' {
|
|
135
|
+
/**
|
|
136
|
+
* Route layout keys scanned from \`src/layouts/\`.
|
|
137
|
+
*/
|
|
138
|
+
export type RouteLayoutKey = ${scanResult.layouts.length > 0 ? scanResult.layouts.map((l) => `"${l.name}"`).join(" | ") : "never"};
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Map of route name → route path, scanned from \`src/pages/\`.
|
|
142
|
+
*/
|
|
143
|
+
export interface RoutePathMap {
|
|
144
|
+
${pathMapEntries}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Route names (keys of {@link RoutePathMap}).
|
|
149
|
+
*/
|
|
150
|
+
export type RouteKey = keyof RoutePathMap;
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Route paths (values of {@link RoutePathMap}).
|
|
154
|
+
*/
|
|
155
|
+
export type RoutePath = RoutePathMap[RouteKey];
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Reuse route keys (pages using \`.reuse.vue\` suffix).
|
|
159
|
+
*/
|
|
160
|
+
export type ReuseRouteKey = ${reuseKeys};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
declare module 'vue-router/auto-routes' {
|
|
164
|
+
import type { RouteRecordInfo, ParamValue, ParamValueZeroOrOne } from 'vue-router';
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Route named map — enables typed \`useRoute<Name>(name)\` via vue-router's
|
|
168
|
+
* \`TypesConfig\` augmentation below.
|
|
169
|
+
*
|
|
170
|
+
* Each entry describes a route's name, path, and the types of its path
|
|
171
|
+
* params (raw input vs. resolved value). For routes without params,
|
|
172
|
+
* \`Record<never, never>\` is used.
|
|
173
|
+
*
|
|
174
|
+
* @see https://router.vuejs.org/api/interfaces/RouteRecordInfo.html
|
|
175
|
+
*/
|
|
176
|
+
export interface RouteNamedMap {
|
|
177
|
+
${scanResult.pages.length > 0 ? scanResult.pages.map((p) => ` "${p.name}": ${this.renderRouteRecordInfo(p)};`).join("\n") : " // (no pages scanned)"}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
declare module 'vue-router' {
|
|
182
|
+
export interface TypesConfig {
|
|
183
|
+
RouteNamedMap: import('vue-router/auto-routes').RouteNamedMap;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export type { RouteLayoutKey, RoutePathMap, RouteKey, RoutePath, ReuseRouteKey } from '@ubean/routing';
|
|
188
|
+
`;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* 渲染单条路由的 `RouteRecordInfo<Name, Path, ParamsRaw, Params>`。
|
|
192
|
+
*
|
|
193
|
+
* 参数类型映射:
|
|
194
|
+
* - 必需参数(`/users/:id`) → `{ id: ParamValue<true> }` / `{ id: ParamValue<false> }`
|
|
195
|
+
* - 可选参数(`/users/:id?`) → `{ id?: ParamValueZeroOrOne<true> }` / `{ id?: ParamValueZeroOrOne<false> }`
|
|
196
|
+
* - 无参数(`/about`) → `Record<never, never>`
|
|
197
|
+
*
|
|
198
|
+
* `ParamValue<true>` 表示原始输入类型(必需为 string),`ParamValue<false>` 表示
|
|
199
|
+
* 解析后类型(已编码/解码,可能为 `string | undefined`)。这是 vue-router 的约定。
|
|
200
|
+
*/
|
|
201
|
+
renderRouteRecordInfo(page) {
|
|
202
|
+
const params = extractRouteParams(page.route);
|
|
203
|
+
const paramsRaw = renderParamsType(params, true);
|
|
204
|
+
const paramsResolved = renderParamsType(params, false);
|
|
205
|
+
return `RouteRecordInfo<${JSON.stringify(page.name)}, ${JSON.stringify(page.route)}, ${paramsRaw}, ${paramsResolved}>`;
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
function defaultPageImportPath(page, cwd) {
|
|
209
|
+
return `@/${relativePosix(cwd, page.fullPath).replace(/\.(vue|tsx?|jsx?|mdx?)$/, "")}`;
|
|
210
|
+
}
|
|
211
|
+
function defaultLayoutImportPath(layout, cwd) {
|
|
212
|
+
return `@/${relativePosix(cwd, layout.fullPath).replace(/\.(vue|tsx?|jsx?)$/, "")}`;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* 从 vue-router 风格路径中提取参数信息。
|
|
216
|
+
*
|
|
217
|
+
* 支持的语法:
|
|
218
|
+
* - `:name` — 必需参数(如 `/users/:id`)
|
|
219
|
+
* - `:name?` — 可选参数(如 `/users/:id?`)
|
|
220
|
+
* - `:name*` — 重复参数(零或多个,如 `/files/:path*`)— 当前当作可选处理
|
|
221
|
+
* - `:name+` — 重复参数(一个或多个,如 `/files/:path+`)— 当前当作必需处理
|
|
222
|
+
* - `:name(...)` — 带自定义正则的参数(如 `:id(\\d+)`)— 提取 `name`,忽略正则
|
|
223
|
+
*
|
|
224
|
+
* 不识别 `:pathMatch(.*)*`(catch-all),由 vue-router 内部处理,在
|
|
225
|
+
* `RouteRecordInfo` 中视为无参数(`Record<never, never>`)。
|
|
226
|
+
*
|
|
227
|
+
* @returns 参数数组,顺序与 path 中出现顺序一致
|
|
228
|
+
*/
|
|
229
|
+
function extractRouteParams(path) {
|
|
230
|
+
const params = [];
|
|
231
|
+
const paramRegex = /:([A-Za-z_][A-Za-z0-9_]*)(?:\([^)]*\))?([?*+]?)/g;
|
|
232
|
+
let match;
|
|
233
|
+
while ((match = paramRegex.exec(path)) !== null) {
|
|
234
|
+
const name = match[1];
|
|
235
|
+
const modifier = match[2];
|
|
236
|
+
const optional = modifier === "?" || modifier === "*";
|
|
237
|
+
if (!params.some((p) => p.name === name)) params.push({
|
|
238
|
+
name,
|
|
239
|
+
optional
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
return params;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* 渲染参数类型字面量,用于 `RouteRecordInfo` 的 `ParamsRaw` / `Params` 类型参数位置。
|
|
246
|
+
*
|
|
247
|
+
* @param params `extractRouteParams` 返回的参数数组
|
|
248
|
+
* @param isRaw `true` 渲染原始输入类型(`ParamValue<true>` / `ParamValueZeroOrOne<true>`),
|
|
249
|
+
* `false` 渲染解析后类型(`ParamValue<false>` / `ParamValueZeroOrOne<false>`)
|
|
250
|
+
* @returns 类型字面量字符串,如 `{ id: ParamValue<true> }` 或 `Record<never, never>`
|
|
251
|
+
*/
|
|
252
|
+
function renderParamsType(params, isRaw) {
|
|
253
|
+
if (params.length === 0) return "Record<never, never>";
|
|
254
|
+
return `{\n${params.map((p) => {
|
|
255
|
+
const optionalMarker = p.optional ? "?" : "";
|
|
256
|
+
const type = p.optional ? `ParamValueZeroOrOne<${isRaw ? "true" : "false"}>` : `ParamValue<${isRaw ? "true" : "false"}>`;
|
|
257
|
+
return ` ${p.name}${optionalMarker}: ${type}`;
|
|
258
|
+
}).join(",\n")}\n }`;
|
|
259
|
+
}
|
|
260
|
+
function joinPosix(...parts) {
|
|
261
|
+
return parts.map((p) => p.replace(/\\/g, "/")).join("/").replace(/\/+/g, "/");
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* 将 `cwd` 与 `target` 合并为最终写入路径。
|
|
265
|
+
*
|
|
266
|
+
* 与 `joinPosix` 不同,此函数会识别 `target` 是否为绝对路径:
|
|
267
|
+
* - 如果 `target` 以 `/` 开头(POSIX 绝对路径)或盘符开头(Windows,如 `C:`),
|
|
268
|
+
* 直接使用 `target`,忽略 `cwd`
|
|
269
|
+
* - 否则,使用 `joinPosix(cwd, target)` 拼接
|
|
270
|
+
*
|
|
271
|
+
* 这是为了支持消费方(`@ubean/build`)传入 `path.resolve()` 计算出的绝对路径,
|
|
272
|
+
* 避免出现 `<cwd>/<absolute-target>` 这种重复前缀的错误路径(参见
|
|
273
|
+
* `maybeGenerateRouteFiles` 中 `resolve(config.rootDir, ...)` 的调用)。
|
|
274
|
+
*/
|
|
275
|
+
function resolvePath(cwd, target) {
|
|
276
|
+
const normalized = target.replace(/\\/g, "/");
|
|
277
|
+
if (normalized.startsWith("/") || /^[a-zA-Z]:[\\/]/.test(normalized)) return normalized;
|
|
278
|
+
return joinPosix(cwd, normalized);
|
|
279
|
+
}
|
|
280
|
+
function relativePosix(from, to) {
|
|
281
|
+
const fromParts = resolvePosix(from).split("/").filter(Boolean);
|
|
282
|
+
const toParts = resolvePosix(to).split("/").filter(Boolean);
|
|
283
|
+
let i = 0;
|
|
284
|
+
while (i < fromParts.length && i < toParts.length && fromParts[i] === toParts[i]) i++;
|
|
285
|
+
const up = fromParts.slice(i).map(() => "..");
|
|
286
|
+
const down = toParts.slice(i);
|
|
287
|
+
return [...up, ...down].join("/") || ".";
|
|
288
|
+
}
|
|
289
|
+
function resolvePosix(p) {
|
|
290
|
+
if (p.startsWith("/")) return p.replace(/\\/g, "/");
|
|
291
|
+
return p.replace(/\\/g, "/");
|
|
292
|
+
}
|
|
293
|
+
async function writeFile(path, content) {
|
|
294
|
+
const { mkdir, writeFile: fsWriteFile } = await import("node:fs/promises");
|
|
295
|
+
const { dirname } = await import("node:path");
|
|
296
|
+
await mkdir(dirname(path), { recursive: true });
|
|
297
|
+
await fsWriteFile(path, content, "utf-8");
|
|
298
|
+
}
|
|
299
|
+
//#endregion
|
|
300
|
+
export { DEFAULT_HEADER_COMMENT, RouteFileGenerator, generateRouteFiles };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { AppEntry, DefineMetaResult, HttpMethod, PageMeta, RouteMeta, ScanOptions, ScanResult, ScannedApiRoute, ScannedAppEntry, ScannedCronTask, ScannedFile, ScannedLayout, ScannedLocale, ScannedMiddleware, ScannedPageRoute, ScannedPlugin, ScannedQueue, ScannedServerEntry } from "./types.js";
|
|
2
|
+
import { filePathToRoute, stripRouteGroups } from "@ubean/utils";
|
|
3
|
+
//#region src/scan.d.ts
|
|
4
|
+
declare function scanProject(options: ScanOptions): Promise<ScanResult>;
|
|
5
|
+
//#endregion
|
|
6
|
+
//#region src/detect-exports.d.ts
|
|
7
|
+
interface DetectExportsResult {
|
|
8
|
+
exports: string[];
|
|
9
|
+
httpMethods: Lowercase<HttpMethod>[];
|
|
10
|
+
hasMeta: boolean;
|
|
11
|
+
fileMeta?: RouteMeta;
|
|
12
|
+
}
|
|
13
|
+
declare function detectHttpExports(filePath: string): Promise<DetectExportsResult>;
|
|
14
|
+
declare function detectHttpExportsFromCode(code: string): DetectExportsResult;
|
|
15
|
+
//#endregion
|
|
16
|
+
//#region src/route-name.d.ts
|
|
17
|
+
declare function generateRouteName(routePath: string): string;
|
|
18
|
+
declare function generateLayoutName(layoutPath: string): string;
|
|
19
|
+
declare function generateApiRouteId(method: string, routePath: string): string;
|
|
20
|
+
//#endregion
|
|
21
|
+
//#region src/define-page.d.ts
|
|
22
|
+
declare function extractDefinePageFromCode(code: string): PageMeta | null;
|
|
23
|
+
declare function extractDefineMetaFromCode(code: string): DefineMetaResult | null;
|
|
24
|
+
declare function extractDefinePage(filePath: string): Promise<PageMeta | null>;
|
|
25
|
+
declare function extractDefineMeta(filePath: string): Promise<DefineMetaResult | null>;
|
|
26
|
+
//#endregion
|
|
27
|
+
//#region src/router.d.ts
|
|
28
|
+
interface CompiledRoute {
|
|
29
|
+
method: string;
|
|
30
|
+
path: string;
|
|
31
|
+
id: string;
|
|
32
|
+
filePath: string;
|
|
33
|
+
meta?: Record<string, unknown>;
|
|
34
|
+
}
|
|
35
|
+
interface CompiledMiddleware {
|
|
36
|
+
path: string;
|
|
37
|
+
filePath: string;
|
|
38
|
+
order: number;
|
|
39
|
+
global: boolean;
|
|
40
|
+
}
|
|
41
|
+
interface CompiledPage {
|
|
42
|
+
name: string;
|
|
43
|
+
path: string;
|
|
44
|
+
filePath: string;
|
|
45
|
+
layout?: string | false;
|
|
46
|
+
reuseTarget?: string;
|
|
47
|
+
}
|
|
48
|
+
interface CompiledLayout {
|
|
49
|
+
name: string;
|
|
50
|
+
filePath: string;
|
|
51
|
+
isDefault: boolean;
|
|
52
|
+
}
|
|
53
|
+
declare class UbeanRouter {
|
|
54
|
+
private apiContext;
|
|
55
|
+
private middlewares;
|
|
56
|
+
private pages;
|
|
57
|
+
private layouts;
|
|
58
|
+
constructor();
|
|
59
|
+
addApiRoute(route: ScannedApiRoute): void;
|
|
60
|
+
addMiddleware(mw: ScannedMiddleware): void;
|
|
61
|
+
addPage(page: ScannedPageRoute): void;
|
|
62
|
+
addLayout(layout: ScannedLayout): void;
|
|
63
|
+
matchApi(method: string, path: string): CompiledRoute | undefined;
|
|
64
|
+
getMiddlewares(): CompiledMiddleware[];
|
|
65
|
+
getPages(): CompiledPage[];
|
|
66
|
+
getPage(name: string): CompiledPage | undefined;
|
|
67
|
+
getLayout(name: string): CompiledLayout | undefined;
|
|
68
|
+
getDefaultLayout(): CompiledLayout | undefined;
|
|
69
|
+
getLayouts(): CompiledLayout[];
|
|
70
|
+
getPageRouteNames(): string[];
|
|
71
|
+
}
|
|
72
|
+
declare function useRouter(): UbeanRouter;
|
|
73
|
+
declare function createUbeanRouter(): UbeanRouter;
|
|
74
|
+
//#endregion
|
|
75
|
+
export { type AppEntry, type CompiledLayout, type CompiledMiddleware, type CompiledPage, type CompiledRoute, type DefineMetaResult, type HttpMethod, type PageMeta, type ScanOptions, type ScanResult, type ScannedApiRoute, type ScannedAppEntry, type ScannedCronTask, type ScannedFile, type ScannedLayout, type ScannedLocale, type ScannedMiddleware, type ScannedPageRoute, type ScannedPlugin, type ScannedQueue, type ScannedServerEntry, UbeanRouter, createUbeanRouter, detectHttpExports, detectHttpExportsFromCode, extractDefineMeta, extractDefineMetaFromCode, extractDefinePage, extractDefinePageFromCode, filePathToRoute, generateApiRouteId, generateLayoutName, generateRouteName, scanProject, stripRouteGroups, useRouter };
|