rpc4next 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 (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +130 -0
  3. package/dist/cli/cache.d.ts +4 -0
  4. package/dist/cli/cache.js +24 -0
  5. package/dist/cli/cli.d.ts +2 -0
  6. package/dist/cli/cli.js +81 -0
  7. package/dist/cli/constants.d.ts +12 -0
  8. package/dist/cli/constants.js +20 -0
  9. package/dist/cli/debounce.d.ts +1 -0
  10. package/dist/cli/debounce.js +14 -0
  11. package/dist/cli/generate-path-structure.d.ts +7 -0
  12. package/dist/cli/generate-path-structure.js +37 -0
  13. package/dist/cli/path-utils.d.ts +1 -0
  14. package/dist/cli/path-utils.js +19 -0
  15. package/dist/cli/route-scanner.d.ts +21 -0
  16. package/dist/cli/route-scanner.js +167 -0
  17. package/dist/cli/scan-utils.d.ts +20 -0
  18. package/dist/cli/scan-utils.js +52 -0
  19. package/dist/cli/type-utils.d.ts +6 -0
  20. package/dist/cli/type-utils.js +26 -0
  21. package/dist/cli/types.d.ts +2 -0
  22. package/dist/cli/types.js +2 -0
  23. package/dist/helper/client/http-method.d.ts +2 -0
  24. package/dist/helper/client/http-method.js +68 -0
  25. package/dist/helper/client/index.d.ts +2 -0
  26. package/dist/helper/client/index.js +6 -0
  27. package/dist/helper/client/match.d.ts +1 -0
  28. package/dist/helper/client/match.js +33 -0
  29. package/dist/helper/client/rpc.d.ts +55 -0
  30. package/dist/helper/client/rpc.js +100 -0
  31. package/dist/helper/client/types.d.ts +119 -0
  32. package/dist/helper/client/types.js +2 -0
  33. package/dist/helper/client/url.d.ts +8 -0
  34. package/dist/helper/client/url.js +57 -0
  35. package/dist/helper/client/utils.d.ts +5 -0
  36. package/dist/helper/client/utils.js +31 -0
  37. package/dist/helper/server/create-handler.d.ts +2 -0
  38. package/dist/helper/server/create-handler.js +10 -0
  39. package/dist/helper/server/create-route-context.d.ts +5 -0
  40. package/dist/helper/server/create-route-context.js +27 -0
  41. package/dist/helper/server/index.d.ts +2 -0
  42. package/dist/helper/server/index.js +5 -0
  43. package/dist/helper/server/route-handler-factory.d.ts +53 -0
  44. package/dist/helper/server/route-handler-factory.js +67 -0
  45. package/dist/helper/server/search-params-to-object.d.ts +1 -0
  46. package/dist/helper/server/search-params-to-object.js +18 -0
  47. package/dist/helper/server/types.d.ts +250 -0
  48. package/dist/helper/server/types.js +4 -0
  49. package/dist/helper/server/validators/zod/index.d.ts +1 -0
  50. package/dist/helper/server/validators/zod/index.js +5 -0
  51. package/dist/helper/server/validators/zod/zod-validator.d.ts +6 -0
  52. package/dist/helper/server/validators/zod/zod-validator.js +44 -0
  53. package/dist/index.d.ts +2 -0
  54. package/dist/index.js +18 -0
  55. package/dist/lib/constants.d.ts +5 -0
  56. package/dist/lib/constants.js +9 -0
  57. package/dist/lib/types.d.ts +1 -0
  58. package/dist/lib/types.js +2 -0
  59. package/package.json +81 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 watanabe-1
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 ADDED
@@ -0,0 +1,130 @@
1
+ # rpc4next
2
+
3
+ Lightweight, type-safe RPC system for Next.js App Router projects.
4
+
5
+ Inspired by Hono RPC and Pathpida, **rpc4next** automatically generates a type-safe client for your existing `route.ts` **and** `page.tsx` files, enabling seamless server-client communication with full type inference.
6
+
7
+ ---
8
+
9
+ ## ✨ Features
10
+
11
+ - ✅ 既存の `app/**/route.ts` および `app/**/page.tsx` を活用するため、新たなハンドラファイルの作成は不要
12
+ - ✅ ルート、パラメータ、リクエストボディ、レスポンスの型安全なクライアント生成
13
+ - ✅ 最小限のセットアップで、カスタムサーバー不要
14
+ - ✅ 動的ルート(`[id]`、`[...slug]` など)に対応
15
+ - ✅ CLI による自動クライアント用型定義生成
16
+
17
+ > **注意**
18
+ > RPCとしてresponseの戻り値の推論が機能するのは、対象となる `route.ts` の HTTPメソッドハンドラ内で`NextResponse.json()` をしている物のみになります。
19
+
20
+ ---
21
+
22
+ ## 🚀 Getting Started
23
+
24
+ ### 1. Install rpc4next
25
+
26
+ ```bash
27
+ npm install rpc4next
28
+ ```
29
+
30
+ ### 2. Define API Routes in Next.js
31
+
32
+ Next.js プロジェクト内の既存の `app/**/route.ts` と `app/**/page.tsx` ファイルをそのまま利用できます。
33
+
34
+ ```ts
35
+ // app/api/user/[id]/route.ts
36
+ import { NextRequest, NextResponse } from "next/server";
37
+
38
+ export async function GET(
39
+ req: NextRequest,
40
+ segmentData: { params: Promise<{ id: string }> }
41
+ ) {
42
+ const { id } = await segmentData.params;
43
+ // RPCとしてresponseの戻り値の推論が機能するのは、対象となる `route.ts` の HTTPメソッドハンドラ内で`NextResponse.json()` をしている物のみになります
44
+ return NextResponse.json({ name: `User ${id}` });
45
+ }
46
+ ```
47
+
48
+ - `GET`、`POST` などのハンドラを定義
49
+ - **RPCとして利用するには、`NextResponse.json()` によるレスポンス返却が必須です**
50
+
51
+ ---
52
+
53
+ ### 3. Generate Type Definitions with CLI
54
+
55
+ CLI を利用して、Next.js のルート構造から型安全な RPC クライアントの定義を自動生成します。
56
+ 以下のコマンドを実行すると、`route.ts` と `page.tsx` ファイルの両方を走査し、型定義ファイル(outputPathに指定したファイル)が生成されます。
57
+
58
+ ```bash
59
+ npx rpc4next <baseDir> <outputPath>
60
+ ```
61
+
62
+ - `<baseDir>`: Next.js の Appルータが配置されたベースディレクトリ
63
+ - `<outputPath>`: 生成された型定義ファイルの出力先
64
+
65
+ #### オプション
66
+
67
+ - **ウォッチモード**
68
+ ファイル変更を検知して自動的に再生成する場合は `--watch` オプションを付けます。
69
+
70
+ ```bash
71
+ npx rpc4next <baseDir> <outputPath> --watch
72
+ ```
73
+
74
+ - **パラメータ型ファイルの生成**
75
+ 各ルートに対して個別のパラメータ型定義ファイルを生成する場合は、`--generate-params-types` オプションにファイル名を指定します。
76
+
77
+ ```bash
78
+ npx rpc4next <baseDir> <outputPath> --generate-params-types <paramsFileName>
79
+ ```
80
+
81
+ ※ このオプションを指定する際は、必ずファイル名をセットしてください。ファイル名が指定されない場合、エラーが発生します。
82
+
83
+ ---
84
+
85
+ ### 4. Create Your RPC Client
86
+
87
+ 生成された型定義ファイルを基に、RPC クライアントを作成します。
88
+
89
+ ```ts
90
+ // lib/rpcClient.ts
91
+ import { createClient } from "rpc4next/client";
92
+ import type { PathStructure } from "あなたが生成した型定義ファイル";
93
+
94
+ export const rpc = createClient<PathStructure>();
95
+ ```
96
+
97
+ ---
98
+
99
+ ### 5. Use It in Your Components
100
+
101
+ コンポーネント内で生成された RPC クライアントを使用します。
102
+
103
+ ```tsx
104
+ // app/page.tsx
105
+ import { rpc } from "@/lib/rpcClient";
106
+
107
+ export default async function Page() {
108
+ const res = await rpc.api.user._id("123").$get();
109
+ const json = await res.json();
110
+ return <div>{json.name}</div>;
111
+ }
112
+ ```
113
+
114
+ - エディタの補完機能により、利用可能なエンドポイントが自動的に表示されます。
115
+ - リクエストの構造(params, searchParams)はサーバーコードから推論され、レスポンスも型安全に扱えます。
116
+
117
+ ---
118
+
119
+ ## 🚧 Requirements
120
+
121
+ - Next.js 14+ (App Router 使用)
122
+ - Node.js 18+
123
+
124
+ ---
125
+
126
+ ## 💼 License
127
+
128
+ MIT
129
+
130
+ ---
@@ -0,0 +1,4 @@
1
+ export declare const visitedDirsCache: Map<string, boolean>;
2
+ export declare const cntCache: Record<string, number>;
3
+ export declare const clearCntCache: () => void;
4
+ export declare const clearVisitedDirsCacheAbove: (targetPath: string) => void;
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.clearVisitedDirsCacheAbove = exports.clearCntCache = exports.cntCache = exports.visitedDirsCache = void 0;
7
+ const path_1 = __importDefault(require("path"));
8
+ exports.visitedDirsCache = new Map();
9
+ exports.cntCache = {};
10
+ const clearCntCache = () => {
11
+ Object.keys(exports.cntCache).forEach((key) => delete exports.cntCache[key]);
12
+ };
13
+ exports.clearCntCache = clearCntCache;
14
+ const clearVisitedDirsCacheAbove = (targetPath) => {
15
+ const basePath = path_1.default.resolve(targetPath);
16
+ for (const key of exports.visitedDirsCache.keys()) {
17
+ const normalizedKey = path_1.default.resolve(key);
18
+ if (normalizedKey === basePath ||
19
+ basePath.startsWith(normalizedKey + path_1.default.sep)) {
20
+ exports.visitedDirsCache.delete(key);
21
+ }
22
+ }
23
+ };
24
+ exports.clearVisitedDirsCacheAbove = clearVisitedDirsCacheAbove;
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __importDefault = (this && this.__importDefault) || function (mod) {
4
+ return (mod && mod.__esModule) ? mod : { "default": mod };
5
+ };
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const chalk_1 = __importDefault(require("chalk"));
10
+ const chokidar_1 = __importDefault(require("chokidar"));
11
+ const commander_1 = require("commander");
12
+ const cache_1 = require("./cache");
13
+ const debounce_1 = require("./debounce");
14
+ const generate_path_structure_1 = require("./generate-path-structure");
15
+ const program = new commander_1.Command();
16
+ program
17
+ .description("Generate RPC client type definitions based on the Next.js path structure.")
18
+ .argument("<baseDir>", "Base directory containing Next.js paths for type generation")
19
+ .argument("<outputPath>", "Output path for the generated type definitions")
20
+ .option("-w, --watch", "Watch mode: regenerate on file changes")
21
+ .option("--generate-params-types [filename]", "Generate params types file with specified filename")
22
+ .action((baseDir, outputPath, options) => {
23
+ const resolvedBaseDir = path_1.default.resolve(baseDir).replace(/\\/g, "/");
24
+ const resolvedOutputPath = path_1.default.resolve(outputPath).replace(/\\/g, "/");
25
+ const paramsFileName = typeof options.generateParamsTypes === "string"
26
+ ? options.generateParamsTypes
27
+ : null;
28
+ if (options.generateParamsTypes !== undefined && !paramsFileName) {
29
+ console.error(chalk_1.default.red("Error: --generate-params-types requires a filename (e.g., params.ts) when specified."));
30
+ process.exit(1);
31
+ }
32
+ const log = (msg) => {
33
+ const time = new Date().toLocaleTimeString();
34
+ console.log(`${chalk_1.default.gray(`[${time}]`)} ${msg}`);
35
+ };
36
+ const generate = () => {
37
+ log(chalk_1.default.cyan("Generating..."));
38
+ const { pathStructure: outputContent, paramsTypes } = (0, generate_path_structure_1.generatePages)(resolvedOutputPath, resolvedBaseDir);
39
+ fs_1.default.writeFileSync(resolvedOutputPath, outputContent);
40
+ log(chalk_1.default.green(`RPC client type definitions generated at ${resolvedOutputPath}`));
41
+ if (paramsFileName) {
42
+ paramsTypes.forEach(({ paramsType, dirPath }) => {
43
+ const filePath = `${dirPath}/${paramsFileName}`;
44
+ fs_1.default.writeFileSync(filePath, paramsType);
45
+ });
46
+ log(chalk_1.default.green(`Params type files have been generated as '${paramsFileName}' alongside each route/page.`));
47
+ }
48
+ };
49
+ generate();
50
+ if (options.watch) {
51
+ log(chalk_1.default.yellow(`Watching ${resolvedBaseDir} for changes...`));
52
+ const isTargetFiles = (path) => path.endsWith("route.ts") || path.endsWith("page.tsx");
53
+ const changedPaths = new Set();
54
+ // Once the debounced function starts executing, no new watcher events will be processed until it completes.
55
+ // This is due to JavaScript's single-threaded event loop: the current debounced function runs to completion,
56
+ // and any new change events are queued until the execution finishes.
57
+ const debouncedGenerate = (0, debounce_1.debounce)(() => {
58
+ changedPaths.forEach((path) => {
59
+ (0, cache_1.clearVisitedDirsCacheAbove)(path);
60
+ });
61
+ changedPaths.clear();
62
+ (0, cache_1.clearCntCache)();
63
+ generate();
64
+ }, 300);
65
+ const watcher = chokidar_1.default.watch(resolvedBaseDir, {
66
+ ignoreInitial: true,
67
+ // If we exclude everything except files using ignored, the watch mode will terminate, so we added "only files" to the exclusion condition.
68
+ ignored: (path, stats) => !!(stats === null || stats === void 0 ? void 0 : stats.isFile()) && !isTargetFiles(path),
69
+ });
70
+ watcher.on("ready", () => {
71
+ watcher.on("all", (event, path) => {
72
+ if (isTargetFiles(path)) {
73
+ log(`${chalk_1.default.magenta(`[${event}]`)} ${chalk_1.default.blue(path)}`);
74
+ changedPaths.add(path);
75
+ debouncedGenerate();
76
+ }
77
+ });
78
+ });
79
+ }
80
+ });
81
+ program.parse(process.argv);
@@ -0,0 +1,12 @@
1
+ export declare const END_POINT_FILE_NAMES: readonly ["page.tsx", "route.ts"];
2
+ export declare const QUERY_TYPES: readonly ["Query", "OptionalQuery"];
3
+ export declare const INDENT = " ";
4
+ export declare const NEWLINE = "\n";
5
+ export declare const STATEMENT_TERMINATOR = ";";
6
+ export declare const TYPE_SEPARATOR = ";";
7
+ export declare const TYPE_END_POINT = "Endpoint";
8
+ export declare const TYPE_KEY_QUERY = "QueryKey";
9
+ export declare const TYPE_KEY_OPTIONAL_QUERY = "OptionalQueryKey";
10
+ export declare const TYPE_KEY_PARAMS = "ParamsKey";
11
+ export declare const TYPE_KEYS: string[];
12
+ export declare const RPC4NEXT_CLIENT_IMPORT_PATH = "rpc4next/client";
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RPC4NEXT_CLIENT_IMPORT_PATH = exports.TYPE_KEYS = exports.TYPE_KEY_PARAMS = exports.TYPE_KEY_OPTIONAL_QUERY = exports.TYPE_KEY_QUERY = exports.TYPE_END_POINT = exports.TYPE_SEPARATOR = exports.STATEMENT_TERMINATOR = exports.NEWLINE = exports.INDENT = exports.QUERY_TYPES = exports.END_POINT_FILE_NAMES = void 0;
4
+ exports.END_POINT_FILE_NAMES = ["page.tsx", "route.ts"];
5
+ exports.QUERY_TYPES = ["Query", "OptionalQuery"];
6
+ exports.INDENT = " ";
7
+ exports.NEWLINE = "\n";
8
+ exports.STATEMENT_TERMINATOR = ";";
9
+ exports.TYPE_SEPARATOR = ";";
10
+ exports.TYPE_END_POINT = "Endpoint";
11
+ exports.TYPE_KEY_QUERY = "QueryKey";
12
+ exports.TYPE_KEY_OPTIONAL_QUERY = "OptionalQueryKey";
13
+ exports.TYPE_KEY_PARAMS = "ParamsKey";
14
+ exports.TYPE_KEYS = [
15
+ exports.TYPE_END_POINT,
16
+ exports.TYPE_KEY_OPTIONAL_QUERY,
17
+ exports.TYPE_KEY_PARAMS,
18
+ exports.TYPE_KEY_QUERY,
19
+ ];
20
+ exports.RPC4NEXT_CLIENT_IMPORT_PATH = "rpc4next/client";
@@ -0,0 +1 @@
1
+ export declare const debounce: <T extends (...args: any[]) => void>(func: T, delay: number) => (...args: Parameters<T>) => void;
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.debounce = void 0;
4
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
5
+ const debounce = (func, delay) => {
6
+ let timer;
7
+ return (...args) => {
8
+ clearTimeout(timer);
9
+ timer = setTimeout(() => {
10
+ func(...args);
11
+ }, delay);
12
+ };
13
+ };
14
+ exports.debounce = debounce;
@@ -0,0 +1,7 @@
1
+ export declare const generatePages: (outputPath: string, baseDir: string) => {
2
+ pathStructure: string;
3
+ paramsTypes: {
4
+ paramsType: string;
5
+ dirPath: string;
6
+ }[];
7
+ };
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.generatePages = void 0;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const constants_1 = require("./constants");
10
+ const route_scanner_1 = require("./route-scanner");
11
+ const type_utils_1 = require("./type-utils");
12
+ const generatePages = (outputPath, baseDir) => {
13
+ const { pathStructure, imports, paramsTypes } = (0, route_scanner_1.scanAppDir)(outputPath, baseDir);
14
+ const pathStructureType = `export type PathStructure = ${pathStructure}${constants_1.STATEMENT_TERMINATOR}`;
15
+ const importsStr = imports.length
16
+ ? `${imports
17
+ .sort((a, b) => a.path.localeCompare(b.path, undefined, { numeric: true }))
18
+ .map((v) => v.statement)
19
+ .join(constants_1.NEWLINE)}`
20
+ : "";
21
+ const keyTypes = constants_1.TYPE_KEYS.filter((type) => pathStructure.includes(type));
22
+ const keyTypesImportStr = (0, type_utils_1.createImport)(keyTypes.join(" ,"), constants_1.RPC4NEXT_CLIENT_IMPORT_PATH);
23
+ const dirParamsTypes = paramsTypes.map(({ paramsType, path: filePath }) => {
24
+ const stats = fs_1.default.statSync(filePath);
25
+ const dirPath = stats.isFile() ? path_1.default.dirname(filePath) : filePath;
26
+ const params = `export type Params = ${paramsType}${constants_1.STATEMENT_TERMINATOR}`;
27
+ return {
28
+ paramsType: params,
29
+ dirPath,
30
+ };
31
+ });
32
+ return {
33
+ pathStructure: `${keyTypesImportStr}${constants_1.NEWLINE}${importsStr}${constants_1.NEWLINE}${constants_1.NEWLINE}${pathStructureType}`,
34
+ paramsTypes: dirParamsTypes,
35
+ };
36
+ };
37
+ exports.generatePages = generatePages;
@@ -0,0 +1 @@
1
+ export declare const createRelativeImportPath: (outputFile: string, inputFile: string) => string;
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.createRelativeImportPath = void 0;
7
+ const path_1 = __importDefault(require("path"));
8
+ const createRelativeImportPath = (outputFile, inputFile) => {
9
+ let relativePath = path_1.default
10
+ .relative(path_1.default.dirname(outputFile), inputFile)
11
+ .replace(/\\/g, "/")
12
+ .replace(/\.tsx?$/, "");
13
+ // Add "./" if the file is in the same directory
14
+ if (!relativePath.startsWith("../")) {
15
+ relativePath = "./" + relativePath;
16
+ }
17
+ return relativePath;
18
+ };
19
+ exports.createRelativeImportPath = createRelativeImportPath;
@@ -0,0 +1,21 @@
1
+ export declare const hasTargetFiles: (dirPath: string) => boolean;
2
+ export declare const scanAppDir: (output: string, input: string, indent?: string, parentParams?: {
3
+ paramName: string;
4
+ routeType: {
5
+ isDynamic: boolean;
6
+ isCatchAll: boolean;
7
+ isOptionalCatchAll: boolean;
8
+ isGroup: boolean;
9
+ isParallel: boolean;
10
+ };
11
+ }[]) => {
12
+ pathStructure: string;
13
+ imports: {
14
+ statement: string;
15
+ path: string;
16
+ }[];
17
+ paramsTypes: {
18
+ paramsType: string;
19
+ path: string;
20
+ }[];
21
+ };
@@ -0,0 +1,167 @@
1
+ "use strict";
2
+ // Inspired by pathpida (https://github.com/aspida/pathpida)
3
+ // Some parts of this code are based on or adapted from the pathpida project
4
+ var __importDefault = (this && this.__importDefault) || function (mod) {
5
+ return (mod && mod.__esModule) ? mod : { "default": mod };
6
+ };
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.scanAppDir = exports.hasTargetFiles = void 0;
9
+ const fs_1 = __importDefault(require("fs"));
10
+ const path_1 = __importDefault(require("path"));
11
+ const cache_1 = require("./cache");
12
+ const constants_1 = require("./constants");
13
+ const scan_utils_1 = require("./scan-utils");
14
+ const type_utils_1 = require("./type-utils");
15
+ const constants_2 = require("../lib/constants");
16
+ const endPointFileNames = new Set(constants_1.END_POINT_FILE_NAMES);
17
+ const hasTargetFiles = (dirPath) => {
18
+ // Return cached result if available
19
+ if (cache_1.visitedDirsCache.has(dirPath))
20
+ return cache_1.visitedDirsCache.get(dirPath);
21
+ const entries = fs_1.default.readdirSync(dirPath, { withFileTypes: true });
22
+ for (const entry of entries) {
23
+ const { name } = entry;
24
+ const entryPath = path_1.default.join(dirPath, name);
25
+ if (name === "node_modules" ||
26
+ // privete
27
+ name.startsWith("_") ||
28
+ // intercepts
29
+ name.startsWith("(.)") ||
30
+ name.startsWith("(..)") ||
31
+ name.startsWith("(...)")) {
32
+ cache_1.visitedDirsCache.set(dirPath, false);
33
+ return false;
34
+ }
35
+ if (entry.isFile() && endPointFileNames.has(name)) {
36
+ cache_1.visitedDirsCache.set(dirPath, true);
37
+ return true;
38
+ }
39
+ if (entry.isDirectory()) {
40
+ if ((0, exports.hasTargetFiles)(entryPath)) {
41
+ cache_1.visitedDirsCache.set(dirPath, true);
42
+ return true;
43
+ }
44
+ }
45
+ }
46
+ cache_1.visitedDirsCache.set(dirPath, false);
47
+ return false;
48
+ };
49
+ exports.hasTargetFiles = hasTargetFiles;
50
+ const scanAppDir = (output, input, indent = "", parentParams = []) => {
51
+ indent += constants_1.INDENT;
52
+ const pathStructures = [];
53
+ const imports = [];
54
+ const types = [];
55
+ const params = [...parentParams];
56
+ const paramsTypes = [];
57
+ const entries = fs_1.default
58
+ .readdirSync(input, { withFileTypes: true })
59
+ .filter((entry) => {
60
+ const { name } = entry;
61
+ if (entry.isDirectory()) {
62
+ const dirPath = path_1.default.join(input, name);
63
+ return (0, exports.hasTargetFiles)(dirPath);
64
+ }
65
+ return endPointFileNames.has(entry.name);
66
+ })
67
+ .sort();
68
+ entries.forEach((entry) => {
69
+ const fullPath = path_1.default.join(input, entry.name);
70
+ const nameWithoutExt = entry.isFile()
71
+ ? entry.name.replace(/\.[^/.]+$/, "")
72
+ : entry.name;
73
+ if (entry.isDirectory()) {
74
+ const isGroup = nameWithoutExt.startsWith("(") && nameWithoutExt.endsWith(")");
75
+ const isParallel = nameWithoutExt.startsWith("@");
76
+ const isOptionalCatchAll = nameWithoutExt.startsWith("[[...") && nameWithoutExt.endsWith("]]");
77
+ const isCatchAll = nameWithoutExt.startsWith("[...") && nameWithoutExt.endsWith("]");
78
+ const isDynamic = nameWithoutExt.startsWith("[") && nameWithoutExt.endsWith("]");
79
+ const { paramName, keyName } = (() => {
80
+ let param = nameWithoutExt;
81
+ // Remove []
82
+ if (isDynamic) {
83
+ param = param.replace(/^\[+|\]+$/g, "");
84
+ }
85
+ // Remove ...
86
+ if (isCatchAll || isOptionalCatchAll) {
87
+ param = param.replace(/^\.{3}/, "");
88
+ }
89
+ const prefix = isOptionalCatchAll
90
+ ? constants_2.OPTIONAL_CATCH_ALL_PREFIX
91
+ : isCatchAll
92
+ ? constants_2.CATCH_ALL_PREFIX
93
+ : isDynamic
94
+ ? constants_2.DYNAMIC_PREFIX
95
+ : "";
96
+ return { paramName: param, keyName: `${prefix}${param}` };
97
+ })();
98
+ if (isDynamic || isCatchAll || isOptionalCatchAll) {
99
+ const routeType = {
100
+ isGroup,
101
+ isParallel,
102
+ isOptionalCatchAll,
103
+ isCatchAll,
104
+ isDynamic,
105
+ };
106
+ params.push({ paramName, routeType });
107
+ }
108
+ const isSkipDir = isGroup || isParallel;
109
+ const { pathStructure: childPathStructure, imports: childImports, paramsTypes: childParamsTypes, } = (0, exports.scanAppDir)(output, fullPath, isSkipDir ? indent.replace(constants_1.INDENT, "") : indent, [...params]);
110
+ imports.push(...childImports);
111
+ paramsTypes.push(...childParamsTypes);
112
+ if (isSkipDir) {
113
+ // Extract the contents inside {}
114
+ const match = childPathStructure.match(/^\s*\{([\s\S]*)\}\s*$/);
115
+ const childStr = match ? match[1].trim() : null;
116
+ if (childStr) {
117
+ pathStructures.push(`${indent}${childStr}`);
118
+ }
119
+ }
120
+ else {
121
+ pathStructures.push(`${indent}"${keyName}": ${childPathStructure}`);
122
+ }
123
+ }
124
+ else {
125
+ const queryDef = (0, scan_utils_1.scanQuery)(output, fullPath);
126
+ if (queryDef) {
127
+ const { importStatement: statement, importPath: path, type } = queryDef;
128
+ imports.push({ statement, path });
129
+ types.push(type);
130
+ }
131
+ constants_2.HTTP_METHODS_EXCLUDE_OPTIONS.forEach((method) => {
132
+ const routeDef = (0, scan_utils_1.scanRoute)(output, fullPath, method);
133
+ if (routeDef) {
134
+ const { importStatement: statement, importPath: path, type, } = routeDef;
135
+ imports.push({ statement, path });
136
+ types.push(type);
137
+ }
138
+ });
139
+ types.push(constants_1.TYPE_END_POINT);
140
+ if (params.length > 0) {
141
+ const fields = params.map((param) => {
142
+ const { paramName, routeType } = param;
143
+ const { isCatchAll, isOptionalCatchAll } = routeType;
144
+ const paramType = isCatchAll
145
+ ? "string[]"
146
+ : isOptionalCatchAll
147
+ ? "string[] | undefined"
148
+ : "string";
149
+ return { name: paramName, type: paramType };
150
+ });
151
+ const paramsType = (0, type_utils_1.createObjectType)(fields);
152
+ paramsTypes.push({ paramsType, path: fullPath.replace(/\\/g, "/") });
153
+ types.push((0, type_utils_1.createRecodeType)(constants_1.TYPE_KEY_PARAMS, paramsType));
154
+ }
155
+ }
156
+ });
157
+ const typeString = types.join(" & ");
158
+ const pathStructure = pathStructures.length > 0
159
+ ? `${typeString}${typeString ? " & " : ""}{${constants_1.NEWLINE}${pathStructures.join(`,${constants_1.NEWLINE}`)}${constants_1.NEWLINE}${indent.replace(constants_1.INDENT, "")}}`
160
+ : typeString;
161
+ return {
162
+ pathStructure,
163
+ imports,
164
+ paramsTypes,
165
+ };
166
+ };
167
+ exports.scanAppDir = scanAppDir;
@@ -0,0 +1,20 @@
1
+ import { HttpMethod } from "../lib/types";
2
+ export declare const createImportAlias: (type: string, key: string) => string;
3
+ export declare const scanFile: <T extends string | undefined>(outputFile: string, inputFile: string, findCallBack: (fileContents: string) => T, typeCallBack: (type: NonNullable<T>, importAlias: string) => string) => {
4
+ importName: string;
5
+ importPath: string;
6
+ importStatement: string;
7
+ type: string;
8
+ } | undefined;
9
+ export declare const scanQuery: (outputFile: string, inputFile: string) => {
10
+ importName: string;
11
+ importPath: string;
12
+ importStatement: string;
13
+ type: string;
14
+ } | undefined;
15
+ export declare const scanRoute: (outputFile: string, inputFile: string, httpMethod: HttpMethod) => {
16
+ importName: string;
17
+ importPath: string;
18
+ importStatement: string;
19
+ type: string;
20
+ } | undefined;
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.scanRoute = exports.scanQuery = exports.scanFile = exports.createImportAlias = void 0;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const cache_1 = require("./cache");
9
+ const constants_1 = require("./constants");
10
+ const path_utils_1 = require("./path-utils");
11
+ const type_utils_1 = require("./type-utils");
12
+ // 連番付与
13
+ const createImportAlias = (type, key) => {
14
+ if (!cache_1.cntCache[key]) {
15
+ cache_1.cntCache[key] = 0;
16
+ }
17
+ return `${type}_${cache_1.cntCache[key]++}`;
18
+ };
19
+ exports.createImportAlias = createImportAlias;
20
+ const scanFile = (outputFile, inputFile, findCallBack, typeCallBack) => {
21
+ const fileContents = fs_1.default.readFileSync(inputFile, "utf8");
22
+ const type = findCallBack(fileContents);
23
+ if (!type)
24
+ return;
25
+ const relativeImportPath = (0, path_utils_1.createRelativeImportPath)(outputFile, inputFile);
26
+ const importAlias = (0, exports.createImportAlias)(type, type);
27
+ return {
28
+ importName: importAlias,
29
+ importPath: relativeImportPath,
30
+ importStatement: (0, type_utils_1.createImport)(type, relativeImportPath, importAlias),
31
+ type: typeCallBack(type, importAlias),
32
+ };
33
+ };
34
+ exports.scanFile = scanFile;
35
+ // query定義作成
36
+ const scanQuery = (outputFile, inputFile) => {
37
+ return (0, exports.scanFile)(outputFile, inputFile, (fileContents) => {
38
+ return constants_1.QUERY_TYPES.find((type) => new RegExp(`export (interface ${type} ?{|type ${type} ?=)`).test(fileContents));
39
+ }, (type, importAlias) => type === "Query"
40
+ ? (0, type_utils_1.createRecodeType)(constants_1.TYPE_KEY_QUERY, importAlias)
41
+ : (0, type_utils_1.createRecodeType)(constants_1.TYPE_KEY_OPTIONAL_QUERY, importAlias));
42
+ };
43
+ exports.scanQuery = scanQuery;
44
+ // route定義作成
45
+ const scanRoute = (outputFile, inputFile, httpMethod) => {
46
+ return (0, exports.scanFile)(outputFile, inputFile, (fileContents) => {
47
+ return [httpMethod].find((method) => new RegExp(`export (async )?(function ${method} ?\\(|const ${method} ?=|\\{[^}]*\\b${method}\\b[^}]*\\} ?=|const \\{[^}]*\\b${method}\\b[^}]*\\} ?=|\\{[^}]*\\b${method}\\b[^}]*\\} from)`).test(fileContents));
48
+ }, (type, importAlias) => (0, type_utils_1.createObjectType)([
49
+ { name: `$${type.toLowerCase()}`, type: `typeof ${importAlias}` },
50
+ ]));
51
+ };
52
+ exports.scanRoute = scanRoute;
@@ -0,0 +1,6 @@
1
+ export declare const createRecodeType: (key: string, value: string) => string;
2
+ export declare const createObjectType: (fields: {
3
+ name: string;
4
+ type: string;
5
+ }[]) => string;
6
+ export declare const createImport: (type: string, path: string, importAlias?: string) => string;