@ubean/vue 0.2.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/LICENSE +21 -0
- package/README.md +493 -0
- package/README.zh-CN.md +500 -0
- package/dist/generator/index.d.ts +125 -0
- package/dist/generator/index.js +369 -0
- package/dist/index.d.ts +782 -0
- package/dist/index.js +1073 -0
- package/dist/types-VHF1RJu2.d.ts +132 -0
- package/dist/vite.d.ts +164 -0
- package/dist/vite.js +1189 -0
- package/package.json +67 -0
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
//#region src/generator/index.ts
|
|
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
|
+
async function generateRouteFiles(scan, options) {
|
|
4
|
+
return new RouteFileGenerator(options).generate(scan);
|
|
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
|
+
srcDir: options.srcDir ?? joinPosix(options.cwd, "src"),
|
|
13
|
+
outDir: options.outDir,
|
|
14
|
+
dtsPath,
|
|
15
|
+
generateRoutes: options.generateRoutes ?? true,
|
|
16
|
+
generateImports: options.generateImports ?? true,
|
|
17
|
+
generateDts: options.generateDts ?? true,
|
|
18
|
+
routeLazy: options.routeLazy ?? true,
|
|
19
|
+
layoutLazy: options.layoutLazy ?? true,
|
|
20
|
+
headerComment: options.headerComment ?? "// @generated by @ubean/vue/generator — do not edit manually.\n// This file is regenerated on every dev start / build.",
|
|
21
|
+
getRouteMeta: options.getRouteMeta,
|
|
22
|
+
getImportPath: options.getImportPath,
|
|
23
|
+
getLayoutImportPath: options.getLayoutImportPath
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
async generate(scan) {
|
|
27
|
+
const { cwd, outDir, dtsPath, generateRoutes, generateImports, generateDts } = this.opts;
|
|
28
|
+
const routesPath = generateRoutes ? joinPosix(outDir, "routes.ts") : void 0;
|
|
29
|
+
const importsPath = generateImports ? joinPosix(outDir, "imports.ts") : void 0;
|
|
30
|
+
const finalDtsPath = generateDts ? dtsPath : void 0;
|
|
31
|
+
await Promise.all([
|
|
32
|
+
routesPath ? writeFile(resolvePath(cwd, routesPath), this.renderRoutesFile(scan)) : null,
|
|
33
|
+
importsPath ? writeFile(resolvePath(cwd, importsPath), this.renderImportsFile(scan)) : null,
|
|
34
|
+
finalDtsPath ? writeFile(resolvePath(cwd, finalDtsPath), this.renderDtsFile(scan)) : null
|
|
35
|
+
]);
|
|
36
|
+
return {
|
|
37
|
+
routesPath,
|
|
38
|
+
importsPath,
|
|
39
|
+
dtsPath: finalDtsPath,
|
|
40
|
+
routeCount: scan.pages.length,
|
|
41
|
+
layoutCount: scan.layouts.length
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
renderRoutesFile(scan) {
|
|
45
|
+
return `${this.opts.headerComment}
|
|
46
|
+
|
|
47
|
+
import type { RouteRecord } from './imports';
|
|
48
|
+
|
|
49
|
+
export const routes: RouteRecord[] = [
|
|
50
|
+
${scan.pages.map((p) => this.renderRouteRecord(p)).join(",\n")}
|
|
51
|
+
];
|
|
52
|
+
`;
|
|
53
|
+
}
|
|
54
|
+
renderRouteRecord(page) {
|
|
55
|
+
const parts = [];
|
|
56
|
+
parts.push(` name: ${JSON.stringify(page.name)}`);
|
|
57
|
+
parts.push(` path: ${JSON.stringify(page.route)}`);
|
|
58
|
+
const componentKey = page.isReuse && page.reuseTarget ? page.reuseTarget : page.name;
|
|
59
|
+
parts.push(` component: ${JSON.stringify(componentKey)}`);
|
|
60
|
+
if (page.layout !== void 0) {
|
|
61
|
+
let layoutVal;
|
|
62
|
+
if (page.layout === false) layoutVal = "false";
|
|
63
|
+
else if (Array.isArray(page.layout)) layoutVal = JSON.stringify(page.layout);
|
|
64
|
+
else layoutVal = JSON.stringify(page.layout);
|
|
65
|
+
parts.push(` layout: ${layoutVal}`);
|
|
66
|
+
}
|
|
67
|
+
if (page.isReuse) parts.push(` reuse: true`);
|
|
68
|
+
const meta = this.computeMeta(page);
|
|
69
|
+
if (meta && Object.keys(meta).length > 0) parts.push(` meta: ${JSON.stringify(meta)}`);
|
|
70
|
+
if (page.pageMeta?.cache === true) parts.push(` cache: true`);
|
|
71
|
+
if (page.pageMeta?.requiresAuth === true) parts.push(` requiresAuth: true`);
|
|
72
|
+
return ` {\n${parts.join(",\n")}\n }`;
|
|
73
|
+
}
|
|
74
|
+
computeMeta(page) {
|
|
75
|
+
const base = page.pageMeta?.meta ?? {};
|
|
76
|
+
const merged = {
|
|
77
|
+
...this.opts.getRouteMeta?.(page) ?? {},
|
|
78
|
+
...base
|
|
79
|
+
};
|
|
80
|
+
if (page.matchers && Object.keys(page.matchers).length > 0) merged.matchers = page.matchers;
|
|
81
|
+
return Object.keys(merged).length > 0 ? merged : null;
|
|
82
|
+
}
|
|
83
|
+
renderImportsFile(scan) {
|
|
84
|
+
const header = this.opts.headerComment;
|
|
85
|
+
const layouts = scan.layouts.map((l) => this.renderLayoutImport(l)).join(",\n");
|
|
86
|
+
const views = scan.pages.filter((p) => !p.isReuse).map((p) => this.renderViewImport(p)).join(",\n");
|
|
87
|
+
const layoutType = scan.layouts.length > 0 ? `'${scan.layouts.map((l) => l.name).join("' | '")}'` : "string";
|
|
88
|
+
const filePages = scan.pages.filter((p) => !p.isReuse);
|
|
89
|
+
const reusePages = scan.pages.filter((p) => p.isReuse);
|
|
90
|
+
return `${header}
|
|
91
|
+
|
|
92
|
+
export type LayoutKey = ${layoutType};
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Route keys that have a corresponding source file (\`.vue\` / \`.md\`).
|
|
96
|
+
* Excludes reuse routes and builtin routes.
|
|
97
|
+
*
|
|
98
|
+
* The \`views\` map is keyed by this type — \`views[RouteFileKey]\` resolves
|
|
99
|
+
* to the actual component loader.
|
|
100
|
+
*/
|
|
101
|
+
export type RouteFileKey = ${filePages.length > 0 ? `'${filePages.map((p) => p.name).join("' | '")}'` : "never"};
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Route keys declared via \`.reuse.ts\` / \`.reuse.js\` metadata files.
|
|
105
|
+
* They reference another route's component via \`reuseTarget\`.
|
|
106
|
+
*/
|
|
107
|
+
export type RouteReuseKey = ${reusePages.length > 0 ? `'${reusePages.map((p) => p.name).join("' | '")}'` : "never"};
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Builtin route keys (framework-provided, e.g. \`root\` / \`not_found\`).
|
|
111
|
+
* Currently unused — reserved for future builtin pages.
|
|
112
|
+
*/
|
|
113
|
+
export type BuiltinRouteKey = never;
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* All page route names. Union of {@link RouteFileKey}, {@link RouteReuseKey}
|
|
117
|
+
* and {@link BuiltinRouteKey}.
|
|
118
|
+
*/
|
|
119
|
+
export type RouteKey = ${scan.pages.length > 0 ? `'${scan.pages.map((p) => p.name).join("' | '")}'` : "never"};
|
|
120
|
+
|
|
121
|
+
export type Lazy<T> = () => Promise<T>;
|
|
122
|
+
export type RawRouteComponent = import('vue').Component | Lazy<import('vue').Component>;
|
|
123
|
+
|
|
124
|
+
export interface RouteRecord {
|
|
125
|
+
name: RouteKey;
|
|
126
|
+
path: string;
|
|
127
|
+
/**
|
|
128
|
+
* Key into the \`views\` map — resolve the actual component via
|
|
129
|
+
* \`views[route.component]\`.
|
|
130
|
+
*
|
|
131
|
+
* For reuse routes, this is the \`reuseTarget\`'s \`RouteFileKey\`
|
|
132
|
+
* (not the reuse route's own name), so the same component is reused.
|
|
133
|
+
*/
|
|
134
|
+
component: RouteFileKey;
|
|
135
|
+
layout?: LayoutKey | LayoutKey[] | false;
|
|
136
|
+
reuse?: boolean;
|
|
137
|
+
meta?: Record<string, unknown>;
|
|
138
|
+
cache?: boolean;
|
|
139
|
+
requiresAuth?: boolean;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export const layouts: Record<LayoutKey, RawRouteComponent> = {
|
|
143
|
+
${layouts}
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
export const views: Record<RouteFileKey, RawRouteComponent> = {
|
|
147
|
+
${views}
|
|
148
|
+
};
|
|
149
|
+
`;
|
|
150
|
+
}
|
|
151
|
+
renderLayoutImport(layout) {
|
|
152
|
+
const importPath = this.opts.getLayoutImportPath?.(layout) ?? defaultLayoutImportPath(layout, this.opts.srcDir);
|
|
153
|
+
if (typeof this.opts.layoutLazy === "function" ? this.opts.layoutLazy(layout) : this.opts.layoutLazy) return ` ${layout.name}: () => import(${JSON.stringify(importPath)})`;
|
|
154
|
+
const importName = `__layout_${layout.name}`;
|
|
155
|
+
return ` ${layout.name}: ${importName}, // import ${importName} from ${JSON.stringify(importPath)}`;
|
|
156
|
+
}
|
|
157
|
+
renderViewImport(page) {
|
|
158
|
+
const importPath = this.opts.getImportPath?.(page) ?? defaultPageImportPath(page, this.opts.srcDir);
|
|
159
|
+
if (typeof this.opts.routeLazy === "function" ? this.opts.routeLazy(page) : this.opts.routeLazy) return ` ${page.name}: () => import(${JSON.stringify(importPath)})`;
|
|
160
|
+
return ` ${page.name}: ${page.name}, // import ${page.name} from ${JSON.stringify(importPath)}`;
|
|
161
|
+
}
|
|
162
|
+
renderDtsFile(scan) {
|
|
163
|
+
const header = this.opts.headerComment;
|
|
164
|
+
const pathMapEntries = scan.pages.length > 0 ? scan.pages.map((p) => ` "${p.name}": "${p.route}";`).join("\n") : " // (no pages scanned)";
|
|
165
|
+
const reuseRoutes = scan.pages.filter((p) => p.isReuse);
|
|
166
|
+
const reuseKeys = reuseRoutes.length > 0 ? reuseRoutes.map((p) => `"${p.name}"`).join(" | ") : "never";
|
|
167
|
+
return `${header}
|
|
168
|
+
|
|
169
|
+
declare module '@ubean/scan' {
|
|
170
|
+
/**
|
|
171
|
+
* Route layout keys scanned from \`src/layouts/\`.
|
|
172
|
+
*/
|
|
173
|
+
export type RouteLayoutKey = ${scan.layouts.length > 0 ? scan.layouts.map((l) => `"${l.name}"`).join(" | ") : "never"};
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Map of route name → route path, scanned from \`src/pages/\`.
|
|
177
|
+
*/
|
|
178
|
+
export interface RoutePathMap {
|
|
179
|
+
${pathMapEntries}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Route names (keys of {@link RoutePathMap}).
|
|
184
|
+
*/
|
|
185
|
+
export type RouteKey = keyof RoutePathMap;
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Route paths (values of {@link RoutePathMap}).
|
|
189
|
+
*/
|
|
190
|
+
export type RoutePath = RoutePathMap[RouteKey];
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Reuse route keys (pages using \`.reuse.ts\` / \`.reuse.js\` metadata files).
|
|
194
|
+
*/
|
|
195
|
+
export type ReuseRouteKey = ${reuseKeys};
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Builtin route keys (framework-provided, e.g. \`root\` / \`not_found\`).
|
|
199
|
+
* Currently unused — reserved for future builtin pages.
|
|
200
|
+
*/
|
|
201
|
+
export type BuiltinRouteKey = never;
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Route keys that have a corresponding source file (\`.vue\` / \`.md\`).
|
|
205
|
+
* Excludes reuse routes and builtin routes.
|
|
206
|
+
*
|
|
207
|
+
* Use this to index the \`views\` map for component lookup.
|
|
208
|
+
*/
|
|
209
|
+
export type RouteFileKey = Exclude<RouteKey, ReuseRouteKey | BuiltinRouteKey>;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
declare module 'vue-router/auto-routes' {
|
|
213
|
+
import type { RouteRecordInfo, ParamValue, ParamValueZeroOrOne } from 'vue-router';
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Route named map — enables typed \`useRoute<Name>(name)\` via vue-router's
|
|
217
|
+
* \`TypesConfig\` augmentation below.
|
|
218
|
+
*
|
|
219
|
+
* Each entry describes a route's name, path, and the types of its path
|
|
220
|
+
* params (raw input vs. resolved value). For routes without params,
|
|
221
|
+
* \`Record<never, never>\` is used.
|
|
222
|
+
*
|
|
223
|
+
* @see https://router.vuejs.org/api/interfaces/RouteRecordInfo.html
|
|
224
|
+
*/
|
|
225
|
+
export interface RouteNamedMap {
|
|
226
|
+
${scan.pages.length > 0 ? scan.pages.map((p) => ` "${p.name}": ${this.renderRouteRecordInfo(p)};`).join("\n") : " // (no pages scanned)"}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
declare module 'vue-router' {
|
|
231
|
+
export interface TypesConfig {
|
|
232
|
+
RouteNamedMap: import('vue-router/auto-routes').RouteNamedMap;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export type { RouteLayoutKey, RoutePathMap, RouteKey, RoutePath, ReuseRouteKey, BuiltinRouteKey, RouteFileKey } from '@ubean/scan';
|
|
237
|
+
`;
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* 渲染单条路由的 `RouteRecordInfo<Name, Path, ParamsRaw, Params>`。
|
|
241
|
+
*
|
|
242
|
+
* 参数类型映射:
|
|
243
|
+
* - 必需参数(`/users/:id`) → `{ id: ParamValue<true> }` / `{ id: ParamValue<false> }`
|
|
244
|
+
* - 可选参数(`/users/:id?`) → `{ id?: ParamValueZeroOrOne<true> }` / `{ id?: ParamValueZeroOrOne<false> }`
|
|
245
|
+
* - 无参数(`/about`) → `Record<never, never>`
|
|
246
|
+
*
|
|
247
|
+
* `ParamValue<true>` 表示原始输入类型(必需为 string),`ParamValue<false>` 表示
|
|
248
|
+
* 解析后类型(已编码/解码,可能为 `string | undefined`)。这是 vue-router 的约定。
|
|
249
|
+
*/
|
|
250
|
+
renderRouteRecordInfo(page) {
|
|
251
|
+
const params = extractRouteParams(page.route);
|
|
252
|
+
const paramsRaw = renderParamsType(params, true);
|
|
253
|
+
const paramsResolved = renderParamsType(params, false);
|
|
254
|
+
return `RouteRecordInfo<${JSON.stringify(page.name)}, ${JSON.stringify(page.route)}, ${paramsRaw}, ${paramsResolved}>`;
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
/**
|
|
258
|
+
* Extensions stripped when computing the default import specifier.
|
|
259
|
+
*
|
|
260
|
+
* `.vue` / `.md` / `.mdx` are KEPT — Vite dispatches them to dedicated
|
|
261
|
+
* transforms (vue-loader / markdown) based on the extension, so the
|
|
262
|
+
* explicit suffix is required for correct resolution at runtime.
|
|
263
|
+
*
|
|
264
|
+
* `.ts` / `.tsx` / `.js` / `.jsx` are STRIPPED — TypeScript/Vite convention
|
|
265
|
+
* is to import script modules without their extension.
|
|
266
|
+
*/
|
|
267
|
+
const STRIPPABLE_IMPORT_EXT = /\.(tsx?|jsx?)$/;
|
|
268
|
+
/**
|
|
269
|
+
* Compute the default import specifier for a page.
|
|
270
|
+
*
|
|
271
|
+
* Uses `srcDir` (not `cwd`) as the relative base so that the resulting
|
|
272
|
+
* `@/<rel>` path matches the common `@/` → `src/` path alias. For example,
|
|
273
|
+
* a page at `<cwd>/src/pages/about.vue` becomes `@/pages/about.vue`
|
|
274
|
+
* (NOT `@/src/pages/about.vue`, which would double the `src/` prefix and
|
|
275
|
+
* fail to resolve under the standard alias).
|
|
276
|
+
*/
|
|
277
|
+
function defaultPageImportPath(page, srcDir) {
|
|
278
|
+
return `@/${relativePosix(srcDir, page.fullPath).replace(STRIPPABLE_IMPORT_EXT, "")}`;
|
|
279
|
+
}
|
|
280
|
+
function defaultLayoutImportPath(layout, srcDir) {
|
|
281
|
+
return `@/${relativePosix(srcDir, layout.fullPath).replace(STRIPPABLE_IMPORT_EXT, "")}`;
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* 从 vue-router 风格路径中提取参数信息。
|
|
285
|
+
*
|
|
286
|
+
* 支持的语法:
|
|
287
|
+
* - `:name` — 必需参数(如 `/users/:id`)
|
|
288
|
+
* - `:name?` — 可选参数(如 `/users/:id?`)
|
|
289
|
+
* - `:name*` — 重复参数(零或多个,如 `/files/:path*`)— 当前当作可选处理
|
|
290
|
+
* - `:name+` — 重复参数(一个或多个,如 `/files/:path+`)— 当前当作必需处理
|
|
291
|
+
* - `:name(...)` — 带自定义正则的参数(如 `:id(\\d+)`)— 提取 `name`,忽略正则
|
|
292
|
+
*
|
|
293
|
+
* 不识别 `:pathMatch(.*)*`(catch-all),由 vue-router 内部处理,在
|
|
294
|
+
* `RouteRecordInfo` 中视为无参数(`Record<never, never>`)。
|
|
295
|
+
*
|
|
296
|
+
* @returns 参数数组,顺序与 path 中出现顺序一致
|
|
297
|
+
*/
|
|
298
|
+
function extractRouteParams(path) {
|
|
299
|
+
const params = [];
|
|
300
|
+
const paramRegex = /:([A-Za-z_][A-Za-z0-9_]*)(?:\([^)]*\))?([?*+]?)/g;
|
|
301
|
+
let match;
|
|
302
|
+
while ((match = paramRegex.exec(path)) !== null) {
|
|
303
|
+
const name = match[1];
|
|
304
|
+
const modifier = match[2];
|
|
305
|
+
const optional = modifier === "?" || modifier === "*";
|
|
306
|
+
if (!params.some((p) => p.name === name)) params.push({
|
|
307
|
+
name,
|
|
308
|
+
optional
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
return params;
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* 渲染参数类型字面量,用于 `RouteRecordInfo` 的 `ParamsRaw` / `Params` 类型参数位置。
|
|
315
|
+
*
|
|
316
|
+
* @param params `extractRouteParams` 返回的参数数组
|
|
317
|
+
* @param isRaw `true` 渲染原始输入类型(`ParamValue<true>` / `ParamValueZeroOrOne<true>`),
|
|
318
|
+
* `false` 渲染解析后类型(`ParamValue<false>` / `ParamValueZeroOrOne<false>`)
|
|
319
|
+
* @returns 类型字面量字符串,如 `{ id: ParamValue<true> }` 或 `Record<never, never>`
|
|
320
|
+
*/
|
|
321
|
+
function renderParamsType(params, isRaw) {
|
|
322
|
+
if (params.length === 0) return "Record<never, never>";
|
|
323
|
+
return `{\n${params.map((p) => {
|
|
324
|
+
const optionalMarker = p.optional ? "?" : "";
|
|
325
|
+
const type = p.optional ? `ParamValueZeroOrOne<${isRaw ? "true" : "false"}>` : `ParamValue<${isRaw ? "true" : "false"}>`;
|
|
326
|
+
return ` ${p.name}${optionalMarker}: ${type}`;
|
|
327
|
+
}).join(",\n")}\n }`;
|
|
328
|
+
}
|
|
329
|
+
function joinPosix(...parts) {
|
|
330
|
+
return parts.map((p) => p.replace(/\\/g, "/")).join("/").replace(/\/+/g, "/");
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* 将 `cwd` 与 `target` 合并为最终写入路径。
|
|
334
|
+
*
|
|
335
|
+
* 与 `joinPosix` 不同,此函数会识别 `target` 是否为绝对路径:
|
|
336
|
+
* - 如果 `target` 以 `/` 开头(POSIX 绝对路径)或盘符开头(Windows,如 `C:`),
|
|
337
|
+
* 直接使用 `target`,忽略 `cwd`
|
|
338
|
+
* - 否则,使用 `joinPosix(cwd, target)` 拼接
|
|
339
|
+
*
|
|
340
|
+
* 这是为了支持消费方传入 `path.resolve()` 计算出的绝对路径,
|
|
341
|
+
* 避免出现 `<cwd>/<absolute-target>` 这种重复前缀的错误路径(参见
|
|
342
|
+
* builder `maybeGenerateRouteFiles` 中 `resolve(config.rootDir, ...)` 的调用)。
|
|
343
|
+
*/
|
|
344
|
+
function resolvePath(cwd, target) {
|
|
345
|
+
const normalized = target.replace(/\\/g, "/");
|
|
346
|
+
if (normalized.startsWith("/") || /^[a-zA-Z]:[\\/]/.test(normalized)) return normalized;
|
|
347
|
+
return joinPosix(cwd, normalized);
|
|
348
|
+
}
|
|
349
|
+
function relativePosix(from, to) {
|
|
350
|
+
const fromParts = resolvePosix(from).split("/").filter(Boolean);
|
|
351
|
+
const toParts = resolvePosix(to).split("/").filter(Boolean);
|
|
352
|
+
let i = 0;
|
|
353
|
+
while (i < fromParts.length && i < toParts.length && fromParts[i] === toParts[i]) i++;
|
|
354
|
+
const up = fromParts.slice(i).map(() => "..");
|
|
355
|
+
const down = toParts.slice(i);
|
|
356
|
+
return [...up, ...down].join("/") || ".";
|
|
357
|
+
}
|
|
358
|
+
function resolvePosix(p) {
|
|
359
|
+
if (p.startsWith("/")) return p.replace(/\\/g, "/");
|
|
360
|
+
return p.replace(/\\/g, "/");
|
|
361
|
+
}
|
|
362
|
+
async function writeFile(path, content) {
|
|
363
|
+
const { mkdir, writeFile: fsWriteFile } = await import("node:fs/promises");
|
|
364
|
+
const { dirname } = await import("node:path");
|
|
365
|
+
await mkdir(dirname(path), { recursive: true });
|
|
366
|
+
await fsWriteFile(path, content, "utf-8");
|
|
367
|
+
}
|
|
368
|
+
//#endregion
|
|
369
|
+
export { DEFAULT_HEADER_COMMENT, RouteFileGenerator, generateRouteFiles };
|