vite-plugin-react-router-routecraft 0.0.0-stage → 0.1.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.
Files changed (78) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/LICENSE +21 -0
  3. package/README.md +100 -2
  4. package/dist/core/compile-routes.d.ts +4 -0
  5. package/dist/core/compile-routes.d.ts.map +1 -0
  6. package/dist/core/compile-routes.js +71 -0
  7. package/dist/core/config.d.ts +11 -0
  8. package/dist/core/config.d.ts.map +1 -0
  9. package/dist/core/config.js +104 -0
  10. package/dist/core/diagnostics.d.ts +15 -0
  11. package/dist/core/diagnostics.d.ts.map +1 -0
  12. package/dist/core/diagnostics.js +16 -0
  13. package/dist/core/exports.d.ts +4 -0
  14. package/dist/core/exports.d.ts.map +1 -0
  15. package/dist/core/exports.js +51 -0
  16. package/dist/core/generate-types.d.ts +4 -0
  17. package/dist/core/generate-types.d.ts.map +1 -0
  18. package/dist/core/generate-types.js +65 -0
  19. package/dist/core/ir.d.ts +31 -0
  20. package/dist/core/ir.d.ts.map +1 -0
  21. package/dist/core/ir.js +1 -0
  22. package/dist/core/route-paths.d.ts +3 -0
  23. package/dist/core/route-paths.d.ts.map +1 -0
  24. package/dist/core/route-paths.js +14 -0
  25. package/dist/core/route-tree.d.ts +7 -0
  26. package/dist/core/route-tree.d.ts.map +1 -0
  27. package/dist/core/route-tree.js +237 -0
  28. package/dist/core/scanner.d.ts +17 -0
  29. package/dist/core/scanner.d.ts.map +1 -0
  30. package/dist/core/scanner.js +141 -0
  31. package/dist/core/segments.d.ts +24 -0
  32. package/dist/core/segments.d.ts.map +1 -0
  33. package/dist/core/segments.js +58 -0
  34. package/dist/devtools/data.d.ts +31 -0
  35. package/dist/devtools/data.d.ts.map +1 -0
  36. package/dist/devtools/data.js +56 -0
  37. package/dist/devtools/html.d.ts +3 -0
  38. package/dist/devtools/html.d.ts.map +1 -0
  39. package/dist/devtools/html.js +152 -0
  40. package/dist/devtools/middleware.d.ts +8 -0
  41. package/dist/devtools/middleware.d.ts.map +1 -0
  42. package/dist/devtools/middleware.js +27 -0
  43. package/dist/index.d.ts +3 -0
  44. package/dist/index.d.ts.map +1 -0
  45. package/dist/index.js +1 -0
  46. package/dist/types.d.ts +27 -0
  47. package/dist/types.d.ts.map +1 -0
  48. package/dist/types.js +1 -0
  49. package/dist/vite/options.d.ts +12 -0
  50. package/dist/vite/options.d.ts.map +1 -0
  51. package/dist/vite/options.js +18 -0
  52. package/dist/vite/plugin.d.ts +5 -0
  53. package/dist/vite/plugin.d.ts.map +1 -0
  54. package/dist/vite/plugin.js +202 -0
  55. package/dist/vite/refresh-coordinator.d.ts +3 -0
  56. package/dist/vite/refresh-coordinator.d.ts.map +1 -0
  57. package/dist/vite/refresh-coordinator.js +23 -0
  58. package/dist/vite/virtual-modules.d.ts +7 -0
  59. package/dist/vite/virtual-modules.d.ts.map +1 -0
  60. package/dist/vite/virtual-modules.js +6 -0
  61. package/docs/architecture/index.md +94 -0
  62. package/docs/architecture/modules.md +90 -0
  63. package/docs/design/routecraft-design.md +862 -0
  64. package/docs/examples/complete-app.md +955 -0
  65. package/docs/getting-started.md +178 -0
  66. package/docs/guides/configuration.md +81 -0
  67. package/docs/guides/devtools.md +44 -0
  68. package/docs/guides/lazy-and-data.md +91 -0
  69. package/docs/guides/routing-conventions.md +191 -0
  70. package/docs/guides/type-generation.md +56 -0
  71. package/docs/index.md +60 -0
  72. package/docs/maintainers/github-pages.md +45 -0
  73. package/docs/public/favicon.svg +8 -0
  74. package/docs/public/images/routecraft-compiler.svg +43 -0
  75. package/docs/public/images/routecraft-lazy-properties.svg +45 -0
  76. package/docs/public/images/routecraft-pipeline.svg +55 -0
  77. package/docs/public/images/routecraft-route-mapping.svg +37 -0
  78. package/package.json +76 -4
package/CHANGELOG.md ADDED
@@ -0,0 +1,11 @@
1
+ # 更新记录
2
+
3
+ 本文件记录 Routecraft 的面向用户变更。
4
+
5
+ ## 0.1.0
6
+
7
+ - 实现 Next.js App Router 风格目录约定到 React Router Data Mode `RouteObject[]` 的编译。
8
+ - 支持多路由组与布局、动态段、末尾 splat、可选 loader/action、错误边界、HydrateFallback 和 middleware。
9
+ - 支持按模块逐属性选择 eager/lazy,生成 `typed-router.d.ts` 和类型化 `href` 辅助接口。
10
+ - 提供目录扫描诊断、Vite HMR、可选开发期 DevTools、中文使用指南和可构建示例。
11
+ - 添加单元、集成、类型契约及覆盖率测试;升级到已审计修复的 Vitest 5.1.0。
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Routecraft contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,101 @@
1
- # Temporary Holding Version
1
+ # Routecraft
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ **Routecraft** 是一个 Vite 插件:按约定目录生成 React Router v8 Data Mode 的 `RouteObject[]`,自动更新路由类型声明,并在开发期提供独立 DevTools 页面。
4
+
5
+ > Routecraft 面向 React Router Data Mode;接入前请确认项目使用的 React Router 与 Vite 版本符合 peer dependency 要求。Routecraft 不实现 React Router Framework Mode,也不提供服务端鉴权或数据运行时。
6
+
7
+ **文档站:**[Routecraft 文档](https://alon-wu.github.io/vite-plugin-react-router-routecraft/) · [完整示例](https://alon-wu.github.io/vite-plugin-react-router-routecraft/examples/complete-app) · [架构设计](https://alon-wu.github.io/vite-plugin-react-router-routecraft/architecture/)
8
+
9
+ ## 特性
10
+
11
+ - Next.js App Router 风格的目录与 `page.tsx`、`layout.tsx` 约定,但仅实现可对应到 React Router Data Mode 的语义;
12
+ - 路由组 `(group)`、私有目录 `_private`、`[param]` 动态段及末尾 `[...parts]` splat;
13
+ - 多分支、多布局;不同 route group 可各自拥有 layout;
14
+ - 组件、loader、action、错误边界与 hydration fallback 逐属性选择 eager/lazy;
15
+ - 不存在的 loader/action 不生成空属性或虚假导入;
16
+ - 生成 `typed-router.d.ts`,声明路由 ID、URL 参数及类型化 `href` 辅助;
17
+ - 可关闭的开发期路由树、加载方式和诊断 UI。
18
+
19
+ ## 仓库内文档
20
+
21
+ - [灵机一动:项目起点与原始设计方案](docs/design/routecraft-design.md)
22
+ - [配置指南](docs/guides/configuration.md)
23
+ - [目录路由约定](docs/guides/routing-conventions.md)
24
+ - [DevTools 指南](docs/guides/devtools.md)
25
+
26
+ ## 试用示例
27
+
28
+ 仓库中的 `examples/basic` 是用于插件自动化验证的精简夹具,不是完整产品教程。完整、具体的应用级示例请阅读[项目管理台完整示例](https://alon-wu.github.io/vite-plugin-react-router-routecraft/examples/complete-app)。
29
+
30
+ 从源码运行仓库内的集成示例:
31
+
32
+ ```sh
33
+ npm install
34
+ npm run build
35
+ cd examples/basic
36
+ npm install
37
+ npm run dev
38
+ ```
39
+
40
+ 打开 Vite 地址查看应用;开发服务器运行时可在同一主机打开 `/__routecraft` 查看路由树和诊断。应用路由入口使用方式:
41
+
42
+ ```tsx
43
+ import { createBrowserRouter } from "react-router";
44
+ import { RouterProvider } from "react-router/dom";
45
+ import { routeObjects } from "virtual:routecraft/routes";
46
+
47
+ const router = createBrowserRouter(routeObjects);
48
+ ```
49
+
50
+ `virtual:routecraft/routes` 是 Routecraft 插件提供的 Vite 虚拟模块,不是 React Router 自带 API。Vite 的 `resolveId`/`load` 钩子将该导入编译为当前路由树;TypeScript 声明则写入配置的 typesDir。
51
+
52
+ ## 在 Vite 项目中配置
53
+
54
+ ```ts
55
+ import { defineConfig } from "vite";
56
+ import react from "@vitejs/plugin-react";
57
+ import routecraft from "vite-plugin-react-router-routecraft";
58
+
59
+ export default defineConfig({
60
+ plugins: [
61
+ react(),
62
+ routecraft({
63
+ routesDir: "src/routes",
64
+ typesDir: "src/types",
65
+ defaultLazy: true,
66
+ devTools: true,
67
+ }),
68
+ ],
69
+ });
70
+ ```
71
+
72
+ | 配置 | 默认值 | 说明 |
73
+ | --- | --- | --- |
74
+ | `routesDir` | `src/routes` | 相对于 Vite 项目根目录的路由根 |
75
+ | `typesDir` | `src/types` | `typed-router.d.ts` 输出目录 |
76
+ | `defaultLazy` | `true` | 约定模块的默认懒加载策略 |
77
+ | `devTools` | `false` | 是否启用开发期 DevTools |
78
+
79
+ ## 开发与验证
80
+
81
+ ```sh
82
+ npm install
83
+ npm run typecheck
84
+ npm test
85
+ npm run test:coverage
86
+ npm run build
87
+ npm run pack:check
88
+ ```
89
+
90
+ `npm run verify` 会执行类型检查、自动化测试和构建;`npm run pack:check` 仅预览 npm 包内容,不会发布。源代码注释与项目文档以中文编写。
91
+
92
+ ## 已知边界
93
+
94
+ - `loading.tsx` 不会自动建立导航 loading UI;应用可用 React Router 的 `useNavigation()` 实现。
95
+ - `hydrate-fallback.tsx` 只用于 React Router 初始 hydration fallback,不等于 Next.js `loading.tsx`。
96
+ - 根 `middleware.ts` 位于共同合成路由根,影响所有分支。middleware 不是服务端安全边界。
97
+ - `[...parts]` 使用 React Router splat 的 `params["*"]` 字符串语义,不等价于 Next.js catch-all 字符串数组;`[[...parts]]` 不支持。
98
+ - 不提供 Next.js Server Components、Server Actions、HTTP Route Handler、Parallel/Intercepting Routes、Next metadata 或 Framework Mode typegen。
99
+ - `useLoaderData()` 的业务返回值类型不做跨文件自动推导。
100
+
101
+ 详见[目录规则与语义边界](docs/guides/routing-conventions.md)。
@@ -0,0 +1,4 @@
1
+ import type { RouteNode } from "./ir.js";
2
+ /** 将 Route IR 编译为 Vite 虚拟模块可加载的 TypeScript 源码。 */
3
+ export declare function compileRouteObjects(root: RouteNode): string;
4
+ //# sourceMappingURL=compile-routes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compile-routes.d.ts","sourceRoot":"","sources":["../../src/core/compile-routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAGV,SAAS,EAEV,MAAM,SAAS,CAAC;AAEjB,kDAAkD;AAClD,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,SAAS,GAAG,MAAM,CAkB3D"}
@@ -0,0 +1,71 @@
1
+ /** 将 Route IR 编译为 Vite 虚拟模块可加载的 TypeScript 源码。 */
2
+ export function compileRouteObjects(root) {
3
+ const modules = collectModules(root);
4
+ const imports = modules.map((module, index) => `import * as __routecraft_module_${index} from ${JSON.stringify(module.file)};`);
5
+ const moduleIndexes = new Map(modules.map((module, index) => [module.file, index]));
6
+ const routeObjects = `[${compileNode(root, 1, moduleIndexes)}]`;
7
+ return [
8
+ "// 由 Routecraft 根据路由目录自动生成;请勿手工编辑。",
9
+ ...imports,
10
+ "",
11
+ `export const routeObjects = ${routeObjects};`,
12
+ "",
13
+ ].join("\n");
14
+ }
15
+ function compileNode(node, depth, moduleIndexes) {
16
+ const indentation = " ".repeat(depth);
17
+ const childIndentation = " ".repeat(depth + 1);
18
+ const fields = [`id: ${JSON.stringify(node.id)}`];
19
+ if (node.path !== undefined)
20
+ fields.push(`path: ${JSON.stringify(node.path)}`);
21
+ if (node.index)
22
+ fields.push("index: true");
23
+ const eagerModules = Object.values(node.modules).filter((module) => Boolean(module && !module.lazy));
24
+ for (const module of eagerModules) {
25
+ fields.push(`${module.property}: ${moduleReference(module, moduleIndexes)}.${module.exportName}`);
26
+ }
27
+ if (node.middleware) {
28
+ fields.push(`middleware: [${middlewareReference(node.middleware, moduleIndexes)}.middleware]`);
29
+ }
30
+ const lazyModules = Object.values(node.modules).filter((module) => Boolean(module?.lazy));
31
+ if (lazyModules.length > 0) {
32
+ const lazyFields = lazyModules.map((module) => `${module.property}: async () => (await import(${JSON.stringify(module.file)})).${module.exportName}`);
33
+ fields.push(`lazy: { ${lazyFields.join(", ")} }`);
34
+ }
35
+ if (node.children.length > 0) {
36
+ const children = node.children
37
+ .map((child) => compileNode(child, depth + 2, moduleIndexes))
38
+ .join(",\n");
39
+ fields.push(`children: [\n${children}\n${childIndentation}]`);
40
+ }
41
+ return `{\n${fields.map((field) => `${childIndentation}${field}`).join(",\n")}\n${indentation}}`;
42
+ }
43
+ function collectModules(root) {
44
+ const byFile = new Map();
45
+ const visit = (node) => {
46
+ for (const module of Object.values(node.modules)) {
47
+ if (module && !module.lazy)
48
+ byFile.set(module.file, module);
49
+ }
50
+ if (node.middleware)
51
+ byFile.set(node.middleware.file, node.middleware);
52
+ for (const child of node.children)
53
+ visit(child);
54
+ };
55
+ visit(root);
56
+ return [...byFile.values()].sort((left, right) => left.file.localeCompare(right.file));
57
+ }
58
+ function moduleReference(module, moduleIndexes) {
59
+ const index = moduleIndexes.get(module.file);
60
+ if (index === undefined) {
61
+ throw new Error(`Routecraft 编译错误:找不到模块 ${module.file} 的导入编号。`);
62
+ }
63
+ return `__routecraft_module_${index}`;
64
+ }
65
+ function middlewareReference(module, moduleIndexes) {
66
+ const index = moduleIndexes.get(module.file);
67
+ if (index === undefined) {
68
+ throw new Error(`Routecraft 编译错误:找不到 middleware 模块 ${module.file} 的导入编号。`);
69
+ }
70
+ return `__routecraft_module_${index}`;
71
+ }
@@ -0,0 +1,11 @@
1
+ import { type RoutecraftDiagnostic } from "./diagnostics.js";
2
+ import type { RouteLazyConfig } from "../types.js";
3
+ export interface ParsedRouteConfig {
4
+ lazy: RouteLazyConfig;
5
+ diagnostics: RoutecraftDiagnostic[];
6
+ }
7
+ /** 读取并静态分析目录配置;不执行用户模块。 */
8
+ export declare function readRouteConfig(filePath: string): ParsedRouteConfig;
9
+ /** 从源码直接解析静态配置,便于单元测试和调用方复用。 */
10
+ export declare function parseRouteConfigSource(source: string, filePath?: string): ParsedRouteConfig;
11
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/core/config.ts"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,oBAAoB,EAC1B,MAAM,kBAAkB,CAAC;AAC1B,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAEnD,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,eAAe,CAAC;IACtB,WAAW,EAAE,oBAAoB,EAAE,CAAC;CACrC;AAaD,2BAA2B;AAC3B,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,iBAAiB,CAEnE;AAED,gCAAgC;AAChC,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,MAAM,EACd,QAAQ,SAAoB,GAC3B,iBAAiB,CAiJnB"}
@@ -0,0 +1,104 @@
1
+ import { readFileSync } from "node:fs";
2
+ import ts from "typescript";
3
+ import { errorDiagnostic, } from "./diagnostics.js";
4
+ const supportedKeys = new Set([
5
+ "layout",
6
+ "layoutLoader",
7
+ "layoutAction",
8
+ "page",
9
+ "loader",
10
+ "action",
11
+ "error",
12
+ "hydrateFallback",
13
+ ]);
14
+ /** 读取并静态分析目录配置;不执行用户模块。 */
15
+ export function readRouteConfig(filePath) {
16
+ return parseRouteConfigSource(readFileSync(filePath, "utf8"), filePath);
17
+ }
18
+ /** 从源码直接解析静态配置,便于单元测试和调用方复用。 */
19
+ export function parseRouteConfigSource(source, filePath = "route.config.ts") {
20
+ const sourceFile = ts.createSourceFile(filePath, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
21
+ const diagnostics = [];
22
+ for (const statement of sourceFile.statements) {
23
+ if (ts.isImportDeclaration(statement) && !statement.importClause?.isTypeOnly) {
24
+ diagnostics.push(errorDiagnostic("RC_CONFIG_RUNTIME_IMPORT", "route.config.ts 仅允许 import type,不允许运行时导入。", filePath));
25
+ }
26
+ }
27
+ const assignment = sourceFile.statements.find((statement) => ts.isExportAssignment(statement) && !statement.isExportEquals);
28
+ if (!assignment) {
29
+ return {
30
+ lazy: {},
31
+ diagnostics: [
32
+ errorDiagnostic("RC_CONFIG_DEFAULT_EXPORT", "route.config.ts 必须默认导出一个静态配置对象。", filePath),
33
+ ],
34
+ };
35
+ }
36
+ const expression = unwrapExpression(assignment.expression);
37
+ if (!ts.isObjectLiteralExpression(expression)) {
38
+ return {
39
+ lazy: {},
40
+ diagnostics: [
41
+ errorDiagnostic("RC_CONFIG_STATIC_ONLY", "route.config.ts 的默认导出必须是对象字面量,不能使用变量、函数或运行时表达式。", filePath),
42
+ ],
43
+ };
44
+ }
45
+ const lazyProperty = expression.properties.find((property) => ts.isPropertyAssignment(property) && getPropertyName(property.name) === "lazy");
46
+ if (expression.properties.filter((property) => getPropertyName(property.name) === "lazy").length > 1) {
47
+ diagnostics.push(errorDiagnostic("RC_CONFIG_DUPLICATE_KEY", "route.config.ts 中 lazy 配置项不能重复。", filePath));
48
+ }
49
+ const otherProperties = expression.properties.filter((property) => getPropertyName(property.name) !== "lazy");
50
+ if (otherProperties.length > 0) {
51
+ diagnostics.push(errorDiagnostic("RC_CONFIG_UNKNOWN_KEY", `不支持 route.config.ts 配置项:${otherProperties
52
+ .map((property) => getPropertyName(property.name) ?? "未知属性")
53
+ .join(", ")}。`, filePath));
54
+ }
55
+ if (!lazyProperty)
56
+ return { lazy: {}, diagnostics };
57
+ const lazyExpression = unwrapExpression(lazyProperty.initializer);
58
+ if (!ts.isObjectLiteralExpression(lazyExpression)) {
59
+ diagnostics.push(errorDiagnostic("RC_CONFIG_LAZY_OBJECT", "route.config.ts 的 lazy 配置必须是对象字面量。", filePath));
60
+ return { lazy: {}, diagnostics };
61
+ }
62
+ const lazy = {};
63
+ const seenKeys = new Set();
64
+ for (const property of lazyExpression.properties) {
65
+ if (!ts.isPropertyAssignment(property)) {
66
+ diagnostics.push(errorDiagnostic("RC_CONFIG_STATIC_ONLY", "lazy 配置仅支持直接的布尔属性,不支持展开、方法或访问器。", filePath));
67
+ continue;
68
+ }
69
+ const name = getPropertyName(property.name);
70
+ if (!name || !supportedKeys.has(name)) {
71
+ diagnostics.push(errorDiagnostic("RC_CONFIG_UNKNOWN_KEY", `不支持 lazy 配置项“${name ?? "未知属性"}”。`, filePath));
72
+ continue;
73
+ }
74
+ if (seenKeys.has(name)) {
75
+ diagnostics.push(errorDiagnostic("RC_CONFIG_DUPLICATE_KEY", `lazy 配置项“${name}”不能重复。`, filePath));
76
+ continue;
77
+ }
78
+ seenKeys.add(name);
79
+ const value = unwrapExpression(property.initializer);
80
+ if (value.kind !== ts.SyntaxKind.TrueKeyword && value.kind !== ts.SyntaxKind.FalseKeyword) {
81
+ diagnostics.push(errorDiagnostic("RC_CONFIG_BOOLEAN", `lazy.${name} 必须是 true 或 false 字面量。`, filePath));
82
+ continue;
83
+ }
84
+ lazy[name] = value.kind === ts.SyntaxKind.TrueKeyword;
85
+ }
86
+ return { lazy, diagnostics };
87
+ }
88
+ function unwrapExpression(expression) {
89
+ if (ts.isParenthesizedExpression(expression) ||
90
+ ts.isAsExpression(expression) ||
91
+ ts.isTypeAssertionExpression(expression) ||
92
+ ts.isSatisfiesExpression(expression)) {
93
+ return unwrapExpression(expression.expression);
94
+ }
95
+ return expression;
96
+ }
97
+ function getPropertyName(name) {
98
+ if (!name)
99
+ return undefined;
100
+ if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name)) {
101
+ return name.text;
102
+ }
103
+ return undefined;
104
+ }
@@ -0,0 +1,15 @@
1
+ export type DiagnosticLevel = "error" | "warning" | "info";
2
+ export interface RoutecraftDiagnostic {
3
+ code: string;
4
+ level: DiagnosticLevel;
5
+ message: string;
6
+ file?: string;
7
+ routePath?: string;
8
+ suggestion?: string;
9
+ }
10
+ export declare class RoutecraftError extends Error {
11
+ readonly diagnostics: RoutecraftDiagnostic[];
12
+ constructor(diagnostics: RoutecraftDiagnostic[]);
13
+ }
14
+ export declare function errorDiagnostic(code: string, message: string, file?: string): RoutecraftDiagnostic;
15
+ //# sourceMappingURL=diagnostics.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diagnostics.d.ts","sourceRoot":"","sources":["../../src/core/diagnostics.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,eAAe,GAAG,OAAO,GAAG,SAAS,GAAG,MAAM,CAAC;AAE3D,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,eAAe,CAAC;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,qBAAa,eAAgB,SAAQ,KAAK;IACxC,QAAQ,CAAC,WAAW,EAAE,oBAAoB,EAAE,CAAC;gBAEjC,WAAW,EAAE,oBAAoB,EAAE;CAKhD;AAED,wBAAgB,eAAe,CAC7B,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,IAAI,CAAC,EAAE,MAAM,GACZ,oBAAoB,CAOtB"}
@@ -0,0 +1,16 @@
1
+ export class RoutecraftError extends Error {
2
+ diagnostics;
3
+ constructor(diagnostics) {
4
+ super(diagnostics.map((item) => `${item.code}: ${item.message}`).join("\n"));
5
+ this.name = "RoutecraftError";
6
+ this.diagnostics = diagnostics;
7
+ }
8
+ }
9
+ export function errorDiagnostic(code, message, file) {
10
+ return {
11
+ code,
12
+ level: "error",
13
+ message,
14
+ ...(file === undefined ? {} : { file }),
15
+ };
16
+ }
@@ -0,0 +1,4 @@
1
+ import { type RoutecraftDiagnostic } from "./diagnostics.js";
2
+ /** 通过 TypeScript AST 检查所需导出,不执行用户模块代码。 */
3
+ export declare function analyzeModuleExports(source: string, filePath: string, requiredExports: readonly string[]): RoutecraftDiagnostic[];
4
+ //# sourceMappingURL=exports.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"exports.d.ts","sourceRoot":"","sources":["../../src/core/exports.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,KAAK,oBAAoB,EAC1B,MAAM,kBAAkB,CAAC;AAE1B,0CAA0C;AAC1C,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,eAAe,EAAE,SAAS,MAAM,EAAE,GACjC,oBAAoB,EAAE,CAmBxB"}
@@ -0,0 +1,51 @@
1
+ import ts from "typescript";
2
+ import { errorDiagnostic, } from "./diagnostics.js";
3
+ /** 通过 TypeScript AST 检查所需导出,不执行用户模块代码。 */
4
+ export function analyzeModuleExports(source, filePath, requiredExports) {
5
+ const sourceFile = ts.createSourceFile(filePath, source, ts.ScriptTarget.Latest, true, filePath.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS);
6
+ const exports = collectExports(sourceFile);
7
+ return requiredExports
8
+ .filter((name) => !exports.has(name))
9
+ .map((name) => errorDiagnostic("RC_REQUIRED_EXPORT", `路由模块必须导出“${name}”。`, filePath));
10
+ }
11
+ function collectExports(sourceFile) {
12
+ const exports = new Set();
13
+ for (const statement of sourceFile.statements) {
14
+ if (ts.isExportAssignment(statement) && !statement.isExportEquals) {
15
+ exports.add("default");
16
+ continue;
17
+ }
18
+ if (ts.isFunctionDeclaration(statement) || ts.isClassDeclaration(statement)) {
19
+ if (!hasModifier(statement, ts.SyntaxKind.ExportKeyword))
20
+ continue;
21
+ if (hasModifier(statement, ts.SyntaxKind.DefaultKeyword)) {
22
+ exports.add("default");
23
+ }
24
+ else if (statement.name) {
25
+ exports.add(statement.name.text);
26
+ }
27
+ continue;
28
+ }
29
+ if (ts.isVariableStatement(statement)) {
30
+ if (!hasModifier(statement, ts.SyntaxKind.ExportKeyword))
31
+ continue;
32
+ for (const declaration of statement.declarationList.declarations) {
33
+ if (ts.isIdentifier(declaration.name))
34
+ exports.add(declaration.name.text);
35
+ }
36
+ continue;
37
+ }
38
+ if (ts.isExportDeclaration(statement) && statement.exportClause) {
39
+ if (ts.isNamedExports(statement.exportClause)) {
40
+ for (const element of statement.exportClause.elements) {
41
+ exports.add(element.name.text);
42
+ }
43
+ }
44
+ }
45
+ }
46
+ return exports;
47
+ }
48
+ function hasModifier(node, kind) {
49
+ return (ts.canHaveModifiers(node) &&
50
+ (ts.getModifiers(node)?.some((modifier) => modifier.kind === kind) ?? false));
51
+ }
@@ -0,0 +1,4 @@
1
+ import type { RouteIR } from "./ir.js";
2
+ /** 根据同一份 Route IR 生成虚拟模块声明和类型化 href 契约。 */
3
+ export declare function generateTypedRouter(ir: RouteIR): string;
4
+ //# sourceMappingURL=generate-types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"generate-types.d.ts","sourceRoot":"","sources":["../../src/core/generate-types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAa,MAAM,SAAS,CAAC;AAElD,2CAA2C;AAC3C,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,OAAO,GAAG,MAAM,CAgCvD"}
@@ -0,0 +1,65 @@
1
+ /** 根据同一份 Route IR 生成虚拟模块声明和类型化 href 契约。 */
2
+ export function generateTypedRouter(ir) {
3
+ const routePaths = collectPagePaths(ir.root);
4
+ const routeIds = [...new Set(ir.routeIds)].sort();
5
+ const paramEntries = routePaths
6
+ .map((path) => ` ${JSON.stringify(path)}: ${formatParams(path)};`)
7
+ .join("\n");
8
+ return [
9
+ "// Routecraft 自动生成的声明文件;请勿手工编辑。",
10
+ 'declare module "virtual:routecraft/routes" {',
11
+ ' import type { RouteObject } from "react-router";',
12
+ "",
13
+ " export const routeObjects: RouteObject[];",
14
+ ` export type RouteId = ${union(routeIds)};`,
15
+ ` export type RoutePath = ${union(routePaths)};`,
16
+ "",
17
+ " type RouteParamMap = {",
18
+ paramEntries,
19
+ " };",
20
+ " export type ParamsFor<P extends RoutePath> = RouteParamMap[P];",
21
+ "}",
22
+ "",
23
+ 'declare module "virtual:routecraft" {',
24
+ ' import type { ParamsFor, RoutePath } from "virtual:routecraft/routes";',
25
+ "",
26
+ " export function href<P extends RoutePath>(",
27
+ " path: P,",
28
+ " ...args: keyof ParamsFor<P> extends never ? [] : [params: ParamsFor<P>]",
29
+ " ): string;",
30
+ "}",
31
+ "",
32
+ ].join("\n");
33
+ }
34
+ function collectPagePaths(root) {
35
+ const paths = new Set();
36
+ const visit = (node) => {
37
+ if (node.kind === "index")
38
+ paths.add(node.routePath);
39
+ for (const child of node.children)
40
+ visit(child);
41
+ };
42
+ visit(root);
43
+ return [...paths].sort();
44
+ }
45
+ function formatParams(path) {
46
+ const parameters = path
47
+ .split("/")
48
+ .flatMap((part) => {
49
+ if (part.startsWith(":"))
50
+ return [part.slice(1)];
51
+ if (part === "*")
52
+ return ["*"];
53
+ return [];
54
+ });
55
+ if (parameters.length === 0)
56
+ return "{}";
57
+ return `{ ${parameters
58
+ .map((parameter) => `${JSON.stringify(parameter)}: string`)
59
+ .join("; ")} }`;
60
+ }
61
+ function union(values) {
62
+ return values.length === 0
63
+ ? "never"
64
+ : values.map((value) => JSON.stringify(value)).join(" | ");
65
+ }
@@ -0,0 +1,31 @@
1
+ import type { RoutecraftDiagnostic } from "./diagnostics.js";
2
+ export type RouteNodeKind = "root" | "segment" | "index" | "not-found";
3
+ export type RouteProperty = "Component" | "loader" | "action" | "ErrorBoundary" | "HydrateFallback";
4
+ export interface RouteModuleRef {
5
+ file: string;
6
+ exportName: string;
7
+ property: RouteProperty;
8
+ lazy: boolean;
9
+ }
10
+ export interface MiddlewareModuleRef {
11
+ file: string;
12
+ exportName: "middleware";
13
+ }
14
+ export interface RouteNode {
15
+ id: string;
16
+ kind: RouteNodeKind;
17
+ sourcePath: string;
18
+ routePath: string;
19
+ path?: string;
20
+ index?: true;
21
+ modules: Partial<Record<RouteProperty, RouteModuleRef>>;
22
+ middleware?: MiddlewareModuleRef;
23
+ children: RouteNode[];
24
+ }
25
+ export interface RouteIR {
26
+ root: RouteNode;
27
+ routeIds: string[];
28
+ routePaths: string[];
29
+ diagnostics: RoutecraftDiagnostic[];
30
+ }
31
+ //# sourceMappingURL=ir.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ir.d.ts","sourceRoot":"","sources":["../../src/core/ir.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAE7D,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,SAAS,GAAG,OAAO,GAAG,WAAW,CAAC;AACvE,MAAM,MAAM,aAAa,GACrB,WAAW,GACX,QAAQ,GACR,QAAQ,GACR,eAAe,GACf,iBAAiB,CAAC;AAEtB,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,aAAa,CAAC;IACxB,IAAI,EAAE,OAAO,CAAC;CACf;AAED,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,YAAY,CAAC;CAC1B;AAED,MAAM,WAAW,SAAS;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,aAAa,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,IAAI,CAAC;IACb,OAAO,EAAE,OAAO,CAAC,MAAM,CAAC,aAAa,EAAE,cAAc,CAAC,CAAC,CAAC;IACxD,UAAU,CAAC,EAAE,mBAAmB,CAAC;IACjC,QAAQ,EAAE,SAAS,EAAE,CAAC;CACvB;AAED,MAAM,WAAW,OAAO;IACtB,IAAI,EAAE,SAAS,CAAC;IAChB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,WAAW,EAAE,oBAAoB,EAAE,CAAC;CACrC"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,3 @@
1
+ /** 生成 href 虚拟模块源码;路径插值交由 React Router 原生实现。 */
2
+ export declare function createHrefModuleSource(): string;
3
+ //# sourceMappingURL=route-paths.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"route-paths.d.ts","sourceRoot":"","sources":["../../src/core/route-paths.ts"],"names":[],"mappings":"AAAA,+CAA+C;AAC/C,wBAAgB,sBAAsB,IAAI,MAAM,CAY/C"}
@@ -0,0 +1,14 @@
1
+ /** 生成 href 虚拟模块源码;路径插值交由 React Router 原生实现。 */
2
+ export function createHrefModuleSource() {
3
+ return [
4
+ "// Routecraft 路径辅助模块;路径规则由 React Router 处理。",
5
+ 'import { generatePath } from "react-router";',
6
+ "",
7
+ "export function href(path, params) {",
8
+ " return params === undefined",
9
+ " ? generatePath(path)",
10
+ " : generatePath(path, params);",
11
+ "}",
12
+ "",
13
+ ].join("\n");
14
+ }
@@ -0,0 +1,7 @@
1
+ import type { RouteIR } from "./ir.js";
2
+ export interface RouteTreeOptions {
3
+ defaultLazy: boolean;
4
+ }
5
+ /** 从扫描快照构建统一路由 IR,并校验模块契约与 URL 冲突。 */
6
+ export declare function buildRouteTree(routesDir: string, options: RouteTreeOptions): RouteIR;
7
+ //# sourceMappingURL=route-tree.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"route-tree.d.ts","sourceRoot":"","sources":["../../src/core/route-tree.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAEV,OAAO,EAIR,MAAM,SAAS,CAAC;AAKjB,MAAM,WAAW,gBAAgB;IAC/B,WAAW,EAAE,OAAO,CAAC;CACtB;AAED,sCAAsC;AACtC,wBAAgB,cAAc,CAC5B,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,gBAAgB,GACxB,OAAO,CA+FT"}