next-zero-rpc 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 +527 -0
- package/bin/cli.mjs +212 -0
- package/package.json +35 -0
- package/templates/apiClient.ts +114 -0
- package/templates/apiRegistry.ts +50 -0
- package/templates/responses.ts +257 -0
- package/templates/update-api-registry.mjs +225 -0
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
|
|
4
|
+
function detectBaseDir() {
|
|
5
|
+
const cwd = process.cwd();
|
|
6
|
+
return fs.existsSync(path.join(cwd, "src")) ? "src" : ".";
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const BASE_DIR = detectBaseDir();
|
|
10
|
+
const API_DIR = path.join(process.cwd(), BASE_DIR, "app/api");
|
|
11
|
+
const REGISTRY_FILE = path.join(process.cwd(), BASE_DIR, "lib/next-zero-rpc/apiRegistry.ts");
|
|
12
|
+
const BRACKET_DOT_REGEX = /[\[\].()]/g;
|
|
13
|
+
|
|
14
|
+
function getRouteFiles(dir, fileList = []) {
|
|
15
|
+
if (!fs.existsSync(dir)) return fileList;
|
|
16
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
17
|
+
for (let i = 0; i < entries.length; i++) {
|
|
18
|
+
const entry = entries[i];
|
|
19
|
+
if (entry.isDirectory()) {
|
|
20
|
+
getRouteFiles(path.join(dir, entry.name), fileList);
|
|
21
|
+
} else if (entry.name === "route.ts") {
|
|
22
|
+
fileList.push(path.join(dir, entry.name));
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return fileList;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function updateApiRegistry() {
|
|
29
|
+
const routeFiles = getRouteFiles(API_DIR);
|
|
30
|
+
|
|
31
|
+
const routes = [];
|
|
32
|
+
|
|
33
|
+
for (let i = 0; i < routeFiles.length; i++) {
|
|
34
|
+
const filePath = routeFiles[i];
|
|
35
|
+
const fileContent = fs.readFileSync(filePath, "utf-8");
|
|
36
|
+
if (fileContent.indexOf("export") === -1) continue;
|
|
37
|
+
|
|
38
|
+
const relativePath = path.relative(API_DIR, filePath);
|
|
39
|
+
const routeDir = path.dirname(relativePath);
|
|
40
|
+
|
|
41
|
+
// Normalize path separators for Windows/Unix compatibility
|
|
42
|
+
const posixRouteDir = routeDir.split(path.sep).join("/");
|
|
43
|
+
|
|
44
|
+
// Construct route path, ignoring Next.js route groups like (groupName)
|
|
45
|
+
const urlSegments = posixRouteDir
|
|
46
|
+
.split("/")
|
|
47
|
+
.filter((segment) => !(segment.startsWith("(") && segment.endsWith(")")));
|
|
48
|
+
const urlRouteDir = urlSegments.join("/");
|
|
49
|
+
const routePath = urlRouteDir === "" || urlRouteDir === "." ? "/api" : `/api/${urlRouteDir}`;
|
|
50
|
+
|
|
51
|
+
// Construct import name: e.g. /api/admin/prom/verifications/[id]/presign -> AdminPromVerificationsIdPresignRoute
|
|
52
|
+
const parts = urlRouteDir.split("/");
|
|
53
|
+
let importName = "";
|
|
54
|
+
for (let j = 0; j < parts.length; j++) {
|
|
55
|
+
const cleanPart = parts[j].replace(BRACKET_DOT_REGEX, "");
|
|
56
|
+
const words = cleanPart.split("-");
|
|
57
|
+
for (let k = 0; k < words.length; k++) {
|
|
58
|
+
const word = words[k];
|
|
59
|
+
if (word) {
|
|
60
|
+
importName += word.charAt(0).toUpperCase() + word.slice(1);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
importName += "Route";
|
|
65
|
+
|
|
66
|
+
const importPath =
|
|
67
|
+
posixRouteDir === "." ? "@/app/api/route" : `@/app/api/${posixRouteDir}/route`;
|
|
68
|
+
|
|
69
|
+
routes.push({ importName, importPath, routePath });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Sort for types by routePath
|
|
73
|
+
const typeRoutes = routes.slice().sort((a, b) => (a.routePath < b.routePath ? -1 : 1));
|
|
74
|
+
|
|
75
|
+
// Sort for imports by importPath using simple string comparison (matches prettier-plugin-sort-imports)
|
|
76
|
+
const importRoutes = routes.slice().sort((a, b) => (a.importPath < b.importPath ? -1 : 1));
|
|
77
|
+
|
|
78
|
+
const importLines = [];
|
|
79
|
+
let currentImportGroup = "";
|
|
80
|
+
for (let i = 0; i < importRoutes.length; i++) {
|
|
81
|
+
const r = importRoutes[i];
|
|
82
|
+
const group = r.routePath.split("/")[2] || "root";
|
|
83
|
+
if (group !== currentImportGroup) {
|
|
84
|
+
if (currentImportGroup !== "") importLines.push("");
|
|
85
|
+
importLines.push(`// /api/${group}`);
|
|
86
|
+
currentImportGroup = group;
|
|
87
|
+
}
|
|
88
|
+
importLines.push(`import type * as ${r.importName} from "${r.importPath}";`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const typeLines = [];
|
|
92
|
+
let currentTypeGroup = "";
|
|
93
|
+
for (let i = 0; i < typeRoutes.length; i++) {
|
|
94
|
+
const r = typeRoutes[i];
|
|
95
|
+
const group = r.routePath.split("/")[2] || "root";
|
|
96
|
+
if (group !== currentTypeGroup) {
|
|
97
|
+
if (currentTypeGroup !== "") typeLines.push("");
|
|
98
|
+
typeLines.push(` // /api/${group}`);
|
|
99
|
+
currentTypeGroup = group;
|
|
100
|
+
}
|
|
101
|
+
typeLines.push(` "${r.routePath}": typeof ${r.importName};`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const generatedBlock = `// --- BEGIN GENERATED API REGISTRY ---
|
|
105
|
+
// This section is auto-generated. Do not edit manually.
|
|
106
|
+
// Run your dev server or \`node ${BASE_DIR === "." ? "" : BASE_DIR + "/"}lib/next-zero-rpc/update-api-registry.mjs\` to regenerate.
|
|
107
|
+
${importLines.join("\n")}
|
|
108
|
+
|
|
109
|
+
${typeLines.length === 0 ? "// eslint-disable-next-line @typescript-eslint/no-empty-object-type" : ""}
|
|
110
|
+
export type KnownRoutes = {
|
|
111
|
+
// Static Routes & Autocomplete Hints
|
|
112
|
+
${typeLines.join("\n")}
|
|
113
|
+
};
|
|
114
|
+
// --- END GENERATED API REGISTRY ---`.replace(/\n\n+/g, "\n\n");
|
|
115
|
+
|
|
116
|
+
const staticTypes = [
|
|
117
|
+
"",
|
|
118
|
+
"type Split<S extends string> = S extends `${infer Head}/${infer Tail}`",
|
|
119
|
+
" ? [Head, ...Split<Tail>]",
|
|
120
|
+
" : [S];",
|
|
121
|
+
"",
|
|
122
|
+
"type MatchSegment<P extends string, K extends string> = K extends `[${string}]`",
|
|
123
|
+
' ? P extends ""',
|
|
124
|
+
" ? false",
|
|
125
|
+
" : true",
|
|
126
|
+
" : K extends P",
|
|
127
|
+
" ? true",
|
|
128
|
+
" : false;",
|
|
129
|
+
"",
|
|
130
|
+
"type MatchSegments<P extends string[], K extends string[]> = K extends []",
|
|
131
|
+
" ? P extends []",
|
|
132
|
+
" ? true",
|
|
133
|
+
" : false",
|
|
134
|
+
" : K extends [`[...${string}]`]",
|
|
135
|
+
" ? true",
|
|
136
|
+
" : [P, K] extends [",
|
|
137
|
+
" [infer PH extends string, ...infer PT extends string[]],",
|
|
138
|
+
" [infer KH extends string, ...infer KT extends string[]],",
|
|
139
|
+
" ]",
|
|
140
|
+
" ? MatchSegment<PH, KH> extends true",
|
|
141
|
+
" ? MatchSegments<PT, KT>",
|
|
142
|
+
" : false",
|
|
143
|
+
" : false;",
|
|
144
|
+
"",
|
|
145
|
+
"type StripQuery<Path extends string> = Path extends `${infer Base}?${string}` ? Base : Path;",
|
|
146
|
+
"",
|
|
147
|
+
"export type FindMatchingRoute<Path extends string> = {",
|
|
148
|
+
" [K in keyof KnownRoutes & string]: MatchSegments<Split<StripQuery<Path>>, Split<K>> extends true",
|
|
149
|
+
" ? K",
|
|
150
|
+
" : never;",
|
|
151
|
+
"}[keyof KnownRoutes & string];",
|
|
152
|
+
"",
|
|
153
|
+
'export type CheckPath<Path extends string> = Path extends ""',
|
|
154
|
+
" ? keyof KnownRoutes",
|
|
155
|
+
" : FindMatchingRoute<Path> extends never",
|
|
156
|
+
" ? keyof KnownRoutes",
|
|
157
|
+
" : Path;",
|
|
158
|
+
].join("\n");
|
|
159
|
+
|
|
160
|
+
if (!fs.existsSync(REGISTRY_FILE)) {
|
|
161
|
+
console.log(`[API Registry] File not found. Creating apiRegistry.ts...`);
|
|
162
|
+
fs.writeFileSync(REGISTRY_FILE, generatedBlock + "\n" + staticTypes, "utf-8");
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const registryContent = fs.readFileSync(REGISTRY_FILE, "utf-8");
|
|
167
|
+
const startMarker = "// --- BEGIN GENERATED API REGISTRY ---";
|
|
168
|
+
const endMarker = "// --- END GENERATED API REGISTRY ---";
|
|
169
|
+
|
|
170
|
+
const startIndex = registryContent.indexOf(startMarker);
|
|
171
|
+
let newContent;
|
|
172
|
+
|
|
173
|
+
if (startIndex !== -1) {
|
|
174
|
+
const endIndex = registryContent.indexOf(endMarker, startIndex);
|
|
175
|
+
if (endIndex !== -1) {
|
|
176
|
+
newContent =
|
|
177
|
+
registryContent.slice(0, startIndex) +
|
|
178
|
+
generatedBlock +
|
|
179
|
+
registryContent.slice(endIndex + endMarker.length);
|
|
180
|
+
} else {
|
|
181
|
+
console.warn(`[API Registry] End marker missing in apiRegistry.ts. Rebuilding file...`);
|
|
182
|
+
newContent = generatedBlock + "\n" + staticTypes;
|
|
183
|
+
}
|
|
184
|
+
} else {
|
|
185
|
+
console.warn(`[API Registry] Start marker missing in apiRegistry.ts. Rebuilding file...`);
|
|
186
|
+
newContent = generatedBlock + "\n" + staticTypes;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (newContent !== registryContent) {
|
|
190
|
+
fs.writeFileSync(REGISTRY_FILE, newContent, "utf-8");
|
|
191
|
+
console.log(`[API Registry] Updated known routes in apiRegistry.ts`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Execute directly if run via CLI (e.g. from package.json script)
|
|
196
|
+
if (process.argv[1] && process.argv[1].endsWith("update-api-registry.mjs")) {
|
|
197
|
+
updateApiRegistry();
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Next.js plugin to automatically generate the API registry during development and build.
|
|
202
|
+
* Usage in next.config.ts:
|
|
203
|
+
* export default withApiRegistry(nextConfig);
|
|
204
|
+
*/
|
|
205
|
+
export function withApiRegistry(nextConfig = {}) {
|
|
206
|
+
// Only run once per process (prevents duplicate watchers in Turbopack/Webpack)
|
|
207
|
+
if (!globalThis.__apiRegistryWatcherSetup) {
|
|
208
|
+
globalThis.__apiRegistryWatcherSetup = true;
|
|
209
|
+
updateApiRegistry();
|
|
210
|
+
|
|
211
|
+
// Only setup the watcher in development mode
|
|
212
|
+
if (process.env.NODE_ENV !== "production") {
|
|
213
|
+
if (fs.existsSync(API_DIR)) {
|
|
214
|
+
let timeout;
|
|
215
|
+
fs.watch(API_DIR, { recursive: true }, () => {
|
|
216
|
+
clearTimeout(timeout);
|
|
217
|
+
timeout = setTimeout(() => {
|
|
218
|
+
updateApiRegistry();
|
|
219
|
+
}, 100);
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return nextConfig;
|
|
225
|
+
}
|