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.
- package/LICENSE +21 -0
- package/README.md +130 -0
- package/dist/cli/cache.d.ts +4 -0
- package/dist/cli/cache.js +24 -0
- package/dist/cli/cli.d.ts +2 -0
- package/dist/cli/cli.js +81 -0
- package/dist/cli/constants.d.ts +12 -0
- package/dist/cli/constants.js +20 -0
- package/dist/cli/debounce.d.ts +1 -0
- package/dist/cli/debounce.js +14 -0
- package/dist/cli/generate-path-structure.d.ts +7 -0
- package/dist/cli/generate-path-structure.js +37 -0
- package/dist/cli/path-utils.d.ts +1 -0
- package/dist/cli/path-utils.js +19 -0
- package/dist/cli/route-scanner.d.ts +21 -0
- package/dist/cli/route-scanner.js +167 -0
- package/dist/cli/scan-utils.d.ts +20 -0
- package/dist/cli/scan-utils.js +52 -0
- package/dist/cli/type-utils.d.ts +6 -0
- package/dist/cli/type-utils.js +26 -0
- package/dist/cli/types.d.ts +2 -0
- package/dist/cli/types.js +2 -0
- package/dist/helper/client/http-method.d.ts +2 -0
- package/dist/helper/client/http-method.js +68 -0
- package/dist/helper/client/index.d.ts +2 -0
- package/dist/helper/client/index.js +6 -0
- package/dist/helper/client/match.d.ts +1 -0
- package/dist/helper/client/match.js +33 -0
- package/dist/helper/client/rpc.d.ts +55 -0
- package/dist/helper/client/rpc.js +100 -0
- package/dist/helper/client/types.d.ts +119 -0
- package/dist/helper/client/types.js +2 -0
- package/dist/helper/client/url.d.ts +8 -0
- package/dist/helper/client/url.js +57 -0
- package/dist/helper/client/utils.d.ts +5 -0
- package/dist/helper/client/utils.js +31 -0
- package/dist/helper/server/create-handler.d.ts +2 -0
- package/dist/helper/server/create-handler.js +10 -0
- package/dist/helper/server/create-route-context.d.ts +5 -0
- package/dist/helper/server/create-route-context.js +27 -0
- package/dist/helper/server/index.d.ts +2 -0
- package/dist/helper/server/index.js +5 -0
- package/dist/helper/server/route-handler-factory.d.ts +53 -0
- package/dist/helper/server/route-handler-factory.js +67 -0
- package/dist/helper/server/search-params-to-object.d.ts +1 -0
- package/dist/helper/server/search-params-to-object.js +18 -0
- package/dist/helper/server/types.d.ts +250 -0
- package/dist/helper/server/types.js +4 -0
- package/dist/helper/server/validators/zod/index.d.ts +1 -0
- package/dist/helper/server/validators/zod/index.js +5 -0
- package/dist/helper/server/validators/zod/zod-validator.d.ts +6 -0
- package/dist/helper/server/validators/zod/zod-validator.js +44 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +18 -0
- package/dist/lib/constants.d.ts +5 -0
- package/dist/lib/constants.js +9 -0
- package/dist/lib/types.d.ts +1 -0
- package/dist/lib/types.js +2 -0
- 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,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;
|
package/dist/cli/cli.js
ADDED
|
@@ -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,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;
|