tag-rpc 1.0.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/dist/chunk-XFEVFUJZ.js +100 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +29 -0
- package/dist/index.d.ts +58 -0
- package/dist/index.js +59 -0
- package/dist/scanner.d.ts +3 -0
- package/dist/scanner.js +6 -0
- package/package.json +33 -0
- package/readme.md +90 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// src/scanner.ts
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
function runScanner() {
|
|
5
|
+
const API_DIR = path.resolve("app/api");
|
|
6
|
+
const OUTPUT_FILE = path.resolve("rpc/api-registry.ts");
|
|
7
|
+
function extractRouteTypes(fileContent) {
|
|
8
|
+
const results = {};
|
|
9
|
+
const regex = /<(\w+)>(.*?)<\1>|<(\w+)>(.*?)<\/\3>/gs;
|
|
10
|
+
let match;
|
|
11
|
+
while ((match = regex.exec(fileContent)) !== null) {
|
|
12
|
+
const tagName = match[1] || match[3];
|
|
13
|
+
const content = match[2] || match[4];
|
|
14
|
+
if (tagName && content) {
|
|
15
|
+
results[tagName] = content.trim();
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return results;
|
|
19
|
+
}
|
|
20
|
+
function getRoutes(dir, routeList = []) {
|
|
21
|
+
if (!fs.existsSync(dir)) return routeList;
|
|
22
|
+
const files = fs.readdirSync(dir);
|
|
23
|
+
files.forEach((file) => {
|
|
24
|
+
const filePath = path.join(dir, file);
|
|
25
|
+
if (fs.statSync(filePath).isDirectory()) {
|
|
26
|
+
getRoutes(filePath, routeList);
|
|
27
|
+
} else if (file === "route.ts") {
|
|
28
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
29
|
+
const methods = ["GET", "POST", "PUT", "PATCH", "DELETE"].filter(
|
|
30
|
+
(m) => new RegExp(`export\\s+(async\\s+)?(function|const|let|var)\\s+${m}\\b`).test(content)
|
|
31
|
+
);
|
|
32
|
+
if (methods.length > 0) {
|
|
33
|
+
const routePath = dir.replace(path.resolve("app"), "").replace(/\\/g, "/");
|
|
34
|
+
const pathParamKeys = (routePath.match(/\[(.*?)\]/g) || []).map(
|
|
35
|
+
(key) => key.replace(/[\[\]]/g, "")
|
|
36
|
+
);
|
|
37
|
+
const extractedTypes = extractRouteTypes(content);
|
|
38
|
+
routeList.push({
|
|
39
|
+
routePath,
|
|
40
|
+
methods,
|
|
41
|
+
pathParamKeys,
|
|
42
|
+
extractedTypes
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
return routeList;
|
|
48
|
+
}
|
|
49
|
+
const routes = getRoutes(API_DIR);
|
|
50
|
+
const sdkApiObject = routes.map((r) => {
|
|
51
|
+
return r.methods.map((m) => {
|
|
52
|
+
const methodTitle = m.charAt(0).toUpperCase() + m.slice(1).toLowerCase();
|
|
53
|
+
const customName = r.extractedTypes[`${methodTitle}RouteName`];
|
|
54
|
+
if (!customName) return null;
|
|
55
|
+
return ` /** {@link <@/app${r.routePath}/route.ts>} */
|
|
56
|
+
${customName}: { path: "${r.routePath}", method: "${m}" }`;
|
|
57
|
+
}).filter(Boolean).join(",\n");
|
|
58
|
+
}).filter(Boolean).join(",\n");
|
|
59
|
+
const registryContent = `/** Generated by TagRPC - Do not edit manually */
|
|
60
|
+
|
|
61
|
+
export const namedRoutes = {
|
|
62
|
+
${sdkApiObject}
|
|
63
|
+
} as const;
|
|
64
|
+
|
|
65
|
+
export interface ApiRoutes {
|
|
66
|
+
${routes.map((r) => ` "${r.routePath}": {
|
|
67
|
+
PathParams: ${r.pathParamKeys.length > 0 ? `{ ${r.pathParamKeys.map((k) => `${k}: string | number`).join("; ")} }` : "never"};
|
|
68
|
+
Methods: {
|
|
69
|
+
${r.methods.map((m) => {
|
|
70
|
+
const methodTitle = m.charAt(0).toUpperCase() + m.slice(1).toLowerCase();
|
|
71
|
+
const body = r.extractedTypes[`${methodTitle}RequestType`] || "any";
|
|
72
|
+
const resp = r.extractedTypes[`${methodTitle}ResponseType`] || "any";
|
|
73
|
+
const query = r.extractedTypes[`${methodTitle}QueryType`] || "any";
|
|
74
|
+
return ` ${m}: {
|
|
75
|
+
Response: ${resp};
|
|
76
|
+
Body: ${body};
|
|
77
|
+
Query: ${query};
|
|
78
|
+
};`;
|
|
79
|
+
}).join("\n")}
|
|
80
|
+
};
|
|
81
|
+
};`).join("\n")}
|
|
82
|
+
}
|
|
83
|
+
`;
|
|
84
|
+
let shouldWrite = true;
|
|
85
|
+
if (fs.existsSync(OUTPUT_FILE)) {
|
|
86
|
+
const existingContent = fs.readFileSync(OUTPUT_FILE, "utf8");
|
|
87
|
+
if (existingContent === registryContent) {
|
|
88
|
+
shouldWrite = false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (shouldWrite) {
|
|
92
|
+
fs.mkdirSync(path.dirname(OUTPUT_FILE), { recursive: true });
|
|
93
|
+
fs.writeFileSync(OUTPUT_FILE, registryContent);
|
|
94
|
+
console.log(`\u2705 [TagRPC] Registry updated at ${OUTPUT_FILE}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export {
|
|
99
|
+
runScanner
|
|
100
|
+
};
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
runScanner
|
|
4
|
+
} from "./chunk-XFEVFUJZ.js";
|
|
5
|
+
|
|
6
|
+
// src/cli.ts
|
|
7
|
+
import chokidar from "chokidar";
|
|
8
|
+
var args = process.argv.slice(2);
|
|
9
|
+
var isWatchMode = args.includes("--watch");
|
|
10
|
+
if (isWatchMode) {
|
|
11
|
+
console.log("\u{1F440} TagRPC is watching for changes in /app...");
|
|
12
|
+
const watcher = chokidar.watch("./app", {
|
|
13
|
+
ignored: /(^|[\/\\])\../,
|
|
14
|
+
// ignore dotfiles
|
|
15
|
+
persistent: true
|
|
16
|
+
});
|
|
17
|
+
let timeout;
|
|
18
|
+
watcher.on("change", (path) => {
|
|
19
|
+
if (path.endsWith("route.ts")) {
|
|
20
|
+
clearTimeout(timeout);
|
|
21
|
+
timeout = setTimeout(() => {
|
|
22
|
+
console.log(`File changed: ${path}. Regenerating...`);
|
|
23
|
+
runScanner();
|
|
24
|
+
}, 300);
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
} else {
|
|
28
|
+
runScanner();
|
|
29
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base types that describe the shape of the generated registry.
|
|
3
|
+
* This allows the package to compile without knowing the exact routes yet.
|
|
4
|
+
*/
|
|
5
|
+
interface BaseApiRoutes {
|
|
6
|
+
[path: string]: {
|
|
7
|
+
PathParams: any;
|
|
8
|
+
Methods: {
|
|
9
|
+
[method: string]: {
|
|
10
|
+
Response: any;
|
|
11
|
+
Body: any;
|
|
12
|
+
Query: any;
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
interface BaseNamedRoutes {
|
|
18
|
+
[key: string]: {
|
|
19
|
+
path: string;
|
|
20
|
+
method: string;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Types for the fetcher options, extracted to be reusable.
|
|
25
|
+
*/
|
|
26
|
+
type TagRPCOptions<T extends BaseApiRoutes, TPath extends keyof T, TMethod extends keyof T[TPath]["Methods"]> = Omit<RequestInit, "method" | "body"> & (T[TPath]["PathParams"] extends never ? {
|
|
27
|
+
pathParams?: never;
|
|
28
|
+
} : {
|
|
29
|
+
pathParams: T[TPath]["PathParams"];
|
|
30
|
+
}) & (T[TPath]["Methods"][TMethod]["Body"] extends never ? {
|
|
31
|
+
body?: never;
|
|
32
|
+
} : {
|
|
33
|
+
body: T[TPath]["Methods"][TMethod]["Body"];
|
|
34
|
+
}) & (T[TPath]["Methods"][TMethod]["Query"] extends never ? {
|
|
35
|
+
query?: never;
|
|
36
|
+
} : {
|
|
37
|
+
query?: T[TPath]["Methods"][TMethod]["Query"];
|
|
38
|
+
});
|
|
39
|
+
/**
|
|
40
|
+
* Custom Error Class for API failures
|
|
41
|
+
*/
|
|
42
|
+
declare class TAG_API_ERROR extends Error {
|
|
43
|
+
status: number;
|
|
44
|
+
statusText: string;
|
|
45
|
+
body: any;
|
|
46
|
+
url: string;
|
|
47
|
+
constructor(status: number, statusText: string, body: any, url: string);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* THE MAIN EXPORT: This creates the proxy-based SDK.
|
|
51
|
+
* Users will call this and pass in their generated types/objects.
|
|
52
|
+
*/
|
|
53
|
+
declare function createTagRPC<T extends BaseApiRoutes, N extends BaseNamedRoutes>(namedRoutes: N): { [K in keyof N]: (options: TagRPCOptions<T, N[K]["path"] & keyof T, N[K]["method"] & keyof T[N[K]["path"] & keyof T]["Methods"]>) => Promise<T[N[K]["path"] & keyof T]["Methods"][N[K]["method"] & keyof T[N[K]["path"] & keyof T]["Methods"]]["Response"]>; } & {
|
|
54
|
+
fetch: <P extends keyof T, M extends keyof T[P]["Methods"] & string>(path: P, method: M, options: TagRPCOptions<T, P, M>) => Promise<T[P]["Methods"][M]["Response"]>;
|
|
55
|
+
routes: N;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export { type BaseApiRoutes, type BaseNamedRoutes, TAG_API_ERROR, type TagRPCOptions, createTagRPC };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
var TAG_API_ERROR = class extends Error {
|
|
3
|
+
constructor(status, statusText, body, url) {
|
|
4
|
+
super(`API Error ${status}: ${statusText}`);
|
|
5
|
+
this.status = status;
|
|
6
|
+
this.statusText = statusText;
|
|
7
|
+
this.body = body;
|
|
8
|
+
this.url = url;
|
|
9
|
+
this.name = "TagApiError";
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
async function fetchData(path, method, options) {
|
|
13
|
+
let url = path;
|
|
14
|
+
if (options.pathParams) {
|
|
15
|
+
for (const [key, value] of Object.entries(options.pathParams)) {
|
|
16
|
+
url = url.replace(`[${key}]`, encodeURIComponent(String(value)));
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
if (options.query) {
|
|
20
|
+
const qs = new URLSearchParams(options.query).toString();
|
|
21
|
+
if (qs) url += `?${qs}`;
|
|
22
|
+
}
|
|
23
|
+
const res = await fetch(url, {
|
|
24
|
+
...options,
|
|
25
|
+
method,
|
|
26
|
+
body: options.body ? JSON.stringify(options.body) : void 0,
|
|
27
|
+
headers: { "Content-Type": "application/json", ...options.headers }
|
|
28
|
+
});
|
|
29
|
+
if (!res.ok) {
|
|
30
|
+
let errorBody;
|
|
31
|
+
try {
|
|
32
|
+
errorBody = await res.json();
|
|
33
|
+
} catch {
|
|
34
|
+
errorBody = { message: "Unknown error occurred" };
|
|
35
|
+
}
|
|
36
|
+
throw new TAG_API_ERROR(res.status, res.statusText, errorBody, url);
|
|
37
|
+
}
|
|
38
|
+
return res.json();
|
|
39
|
+
}
|
|
40
|
+
function createTagRPC(namedRoutes) {
|
|
41
|
+
const baseFetcher = {
|
|
42
|
+
fetch: (path, method, options) => fetchData(path, method, options),
|
|
43
|
+
routes: namedRoutes
|
|
44
|
+
};
|
|
45
|
+
return new Proxy(baseFetcher, {
|
|
46
|
+
get(target, prop) {
|
|
47
|
+
if (prop in target) return target[prop];
|
|
48
|
+
const route = namedRoutes[prop];
|
|
49
|
+
if (route) {
|
|
50
|
+
return (options) => fetchData(route.path, route.method, options);
|
|
51
|
+
}
|
|
52
|
+
return void 0;
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
export {
|
|
57
|
+
TAG_API_ERROR,
|
|
58
|
+
createTagRPC
|
|
59
|
+
};
|
package/dist/scanner.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "tag-rpc",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "End-to-end type safety for Next.js Route Handlers",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"module": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"bin": {
|
|
9
|
+
"tag-rpc": "./dist/cli.js"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"test": "echo \"Error: no test specified\" && exit 1",
|
|
13
|
+
"build": "tsup src/index.ts src/cli.ts src/scanner.ts --format esm --dts --clean",
|
|
14
|
+
"dev": "tsup src/index.ts src/cli.ts src/scanner.ts --format esm --watch",
|
|
15
|
+
"prepublishOnly": "npm run build"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [],
|
|
18
|
+
"author": "Kelvin Mitau",
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"type": "module",
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"chokidar": "^5.0.0"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@types/node": "^25.1.0",
|
|
26
|
+
"tsup": "^8.0.0",
|
|
27
|
+
"typescript": "^5.0.0"
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"dist",
|
|
31
|
+
"README.md"
|
|
32
|
+
]
|
|
33
|
+
}
|
package/readme.md
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
->TagRPC
|
|
2
|
+
End-to-end type safety for Next.js Route Handlers. TagRPC bridges the gap between your Next.js API routes and your frontend with zero boilerplate and 100% type safety using simple XML-like tags.
|
|
3
|
+
|
|
4
|
+
->Features
|
|
5
|
+
1.Zero Schemas: No Zod or Valibot required (though you can use them!). Just define your types in your route.ts.
|
|
6
|
+
|
|
7
|
+
2.Auto-Generation: A watcher that tracks your app/api folder and updates types instantly.
|
|
8
|
+
|
|
9
|
+
3.Named Routes: Call your APIs by name (e.g., api.getUser()) instead of messy strings like "/api/users/[id]".
|
|
10
|
+
|
|
11
|
+
4.Ultra-Lightweight: Zero runtime dependencies in your production bundle.
|
|
12
|
+
|
|
13
|
+
->Installation
|
|
14
|
+
Bash
|
|
15
|
+
npm install tag-rpc
|
|
16
|
+
|
|
17
|
+
->Setup
|
|
18
|
+
|
|
19
|
+
1. Tag Your Routes
|
|
20
|
+
Inside any app/api/\*\*/route.ts file, add tags inside a backtick string to define your route's metadata:
|
|
21
|
+
|
|
22
|
+
Example:
|
|
23
|
+
// app/api/users/[id]/route.ts
|
|
24
|
+
|
|
25
|
+
` <GetRouteName>
|
|
26
|
+
getUser
|
|
27
|
+
</GetRouteName>
|
|
28
|
+
<GetResponseType>
|
|
29
|
+
id: string,;
|
|
30
|
+
name: string,
|
|
31
|
+
email?:string
|
|
32
|
+
</GetResponseType>
|
|
33
|
+
<GetRequestType>
|
|
34
|
+
id: string,
|
|
35
|
+
name: string
|
|
36
|
+
</GetRequestType>
|
|
37
|
+
<GetQueryType>
|
|
38
|
+
includeemail:string
|
|
39
|
+
</GetQueryType>`
|
|
40
|
+
export async function GET(req: Request, { params }: { params: { id: string } }) {
|
|
41
|
+
// ... your logic
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
2. Run the Scanner
|
|
45
|
+
Add the scanner to your package.json scripts:
|
|
46
|
+
|
|
47
|
+
Example:
|
|
48
|
+
|
|
49
|
+
"scripts": {
|
|
50
|
+
"rpc:watch": "tag-rpc --watch",
|
|
51
|
+
"build": "tag-rpc && next build"
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
3. Initialize the Client
|
|
55
|
+
Create a file (e.g., lib/api.ts) to initialize your type-safe client:
|
|
56
|
+
|
|
57
|
+
Example:
|
|
58
|
+
|
|
59
|
+
import { createTagRPC } from 'tag-rpc';
|
|
60
|
+
import { ApiRoutes, namedRoutes } from '@/rpc/api-registry'; // Generated file
|
|
61
|
+
|
|
62
|
+
export const api = createTagRPC<ApiRoutes, typeof namedRoutes>(namedRoutes);
|
|
63
|
+
|
|
64
|
+
Usage:
|
|
65
|
+
Now, enjoy full autocomplete and type safety in your React components!
|
|
66
|
+
|
|
67
|
+
TypeScript
|
|
68
|
+
// components/UserCard.tsx
|
|
69
|
+
import { api } from '@/lib/api';
|
|
70
|
+
|
|
71
|
+
const UserCard = async ({ id }: { id: string }) => {
|
|
72
|
+
// Fully typed: knows path params, query params, and response shape!
|
|
73
|
+
const user = await api.getUser({
|
|
74
|
+
pathParams: { id }
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
return <div>{user.name}</div>;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
->Configuration (Optional)
|
|
81
|
+
Create a tagrpc.config.mjs in your root directory to customize paths:
|
|
82
|
+
|
|
83
|
+
JavaScript
|
|
84
|
+
export default {
|
|
85
|
+
appDir: 'app', // Where your Next.js app folder is
|
|
86
|
+
outputFile: 'rpc/registry.ts' // Where to save the generated types
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
->License
|
|
90
|
+
MIT © [Kelvin Mitau]
|