vite-plugin-react-router-routecraft 0.0.0-stage → 0.1.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.
Files changed (79) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/LICENSE +21 -0
  3. package/README.md +101 -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/ai-assistants.md +61 -0
  67. package/docs/guides/configuration.md +81 -0
  68. package/docs/guides/devtools.md +44 -0
  69. package/docs/guides/lazy-and-data.md +91 -0
  70. package/docs/guides/routing-conventions.md +131 -0
  71. package/docs/guides/type-generation.md +56 -0
  72. package/docs/index.md +60 -0
  73. package/docs/maintainers/github-pages.md +45 -0
  74. package/docs/public/favicon.svg +8 -0
  75. package/docs/public/images/routecraft-compiler.svg +43 -0
  76. package/docs/public/images/routecraft-lazy-properties.svg +45 -0
  77. package/docs/public/images/routecraft-pipeline.svg +55 -0
  78. package/docs/public/images/routecraft-route-mapping.svg +37 -0
  79. package/package.json +76 -4
package/CHANGELOG.md ADDED
@@ -0,0 +1,15 @@
1
+ # 更新记录
2
+
3
+ 本文件记录 Routecraft 的面向用户变更。
4
+
5
+ ## 0.1.1
6
+
7
+ - 完善 AI 使用指南与 Skill 对已安装插件版本文档的定位说明,明确文档缺失或版本不匹配时的处理方式。
8
+
9
+ ## 0.1.0
10
+
11
+ - 实现 Routecraft 目录约定到 React Router Data Mode `RouteObject[]` 的编译。
12
+ - 支持多路由组与布局、动态段、末尾 splat、可选 loader/action、错误边界、HydrateFallback 和 middleware。
13
+ - 支持按模块逐属性选择 eager/lazy,生成 `typed-router.d.ts` 和类型化 `href` 辅助接口。
14
+ - 提供目录扫描诊断、Vite HMR、可选开发期 DevTools、中文使用指南和可构建示例。
15
+ - 添加单元、集成、类型契约及覆盖率测试;升级到已审计修复的 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,102 @@
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
+ - 以目录表达路由层级,并通过 `page.tsx`、`layout.tsx` 等 Routecraft 约定文件生成路由对象;
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
+ - [AI 编码助手](docs/guides/ai-assistants.md)
25
+ - [DevTools 指南](docs/guides/devtools.md)
26
+
27
+ ## 试用示例
28
+
29
+ 仓库中的 `examples/basic` 是用于插件自动化验证的精简夹具,不是完整产品教程。完整、具体的应用级示例请阅读[项目管理台完整示例](https://alon-wu.github.io/vite-plugin-react-router-routecraft/examples/complete-app)。
30
+
31
+ 从源码运行仓库内的集成示例:
32
+
33
+ ```sh
34
+ npm install
35
+ npm run build
36
+ cd examples/basic
37
+ npm install
38
+ npm run dev
39
+ ```
40
+
41
+ 打开 Vite 地址查看应用;开发服务器运行时可在同一主机打开 `/__routecraft` 查看路由树和诊断。应用路由入口使用方式:
42
+
43
+ ```tsx
44
+ import { createBrowserRouter } from "react-router";
45
+ import { RouterProvider } from "react-router/dom";
46
+ import { routeObjects } from "virtual:routecraft/routes";
47
+
48
+ const router = createBrowserRouter(routeObjects);
49
+ ```
50
+
51
+ `virtual:routecraft/routes` 是 Routecraft 插件提供的 Vite 虚拟模块,不是 React Router 自带 API。Vite 的 `resolveId`/`load` 钩子将该导入编译为当前路由树;TypeScript 声明则写入配置的 typesDir。
52
+
53
+ ## 在 Vite 项目中配置
54
+
55
+ ```ts
56
+ import { defineConfig } from "vite";
57
+ import react from "@vitejs/plugin-react";
58
+ import routecraft from "vite-plugin-react-router-routecraft";
59
+
60
+ export default defineConfig({
61
+ plugins: [
62
+ react(),
63
+ routecraft({
64
+ routesDir: "src/routes",
65
+ typesDir: "src/types",
66
+ defaultLazy: true,
67
+ devTools: true,
68
+ }),
69
+ ],
70
+ });
71
+ ```
72
+
73
+ | 配置 | 默认值 | 说明 |
74
+ | --- | --- | --- |
75
+ | `routesDir` | `src/routes` | 相对于 Vite 项目根目录的路由根 |
76
+ | `typesDir` | `src/types` | `typed-router.d.ts` 输出目录 |
77
+ | `defaultLazy` | `true` | 约定模块的默认懒加载策略 |
78
+ | `devTools` | `false` | 是否启用开发期 DevTools |
79
+
80
+ ## 开发与验证
81
+
82
+ ```sh
83
+ npm install
84
+ npm run typecheck
85
+ npm test
86
+ npm run test:coverage
87
+ npm run build
88
+ npm run pack:check
89
+ ```
90
+
91
+ `npm run verify` 会执行类型检查、自动化测试和构建;`npm run pack:check` 仅预览 npm 包内容,不会发布。源代码注释与项目文档以中文编写。
92
+
93
+ ## 已知边界
94
+
95
+ - `loading.tsx` 不会自动建立导航 loading UI;应用可用 React Router 的 `useNavigation()` 实现。
96
+ - `hydrate-fallback.tsx` 映射为 React Router 的 `HydrateFallback`,用于 hydration fallback;它不是应用内导航状态组件。
97
+ - 根 `middleware.ts` 位于共同合成路由根,影响所有分支。middleware 不是服务端安全边界。
98
+ - `[...parts]` 映射为 React Router splat,参数通过 `params["*"]` 获取;`[[...parts]]` 不支持。
99
+ - 不提供服务端组件、服务端操作、HTTP 请求处理器、并行/拦截式路由、框架级 metadata 或 Framework Mode typegen。
100
+ - `useLoaderData()` 的业务返回值类型不做跨文件自动推导。
101
+
102
+ 详见[目录规则与语义边界](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"}