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.
@@ -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
+ }
@@ -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
+ };
@@ -0,0 +1,3 @@
1
+ declare function runScanner(): void;
2
+
3
+ export { runScanner as default };
@@ -0,0 +1,6 @@
1
+ import {
2
+ runScanner
3
+ } from "./chunk-XFEVFUJZ.js";
4
+ export {
5
+ runScanner as default
6
+ };
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]