rpc4next 0.4.0 → 0.4.10

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/index.js ADDED
@@ -0,0 +1 @@
1
+ export*from"./rpc/server";export*from"./rpc/client";
@@ -1,5 +1,11 @@
1
1
  import type { HttpMethodFuncKey } from "./types";
2
+ /**
3
+ * Returns true if the given key represents a dynamic segment.
4
+ */
2
5
  export declare const isDynamic: (key: string) => boolean;
6
+ /**
7
+ * Returns true if the key represents a catch-all or optional catch-all segment.
8
+ */
3
9
  export declare const isCatchAllOrOptional: (key: string) => boolean;
4
10
  export declare const isHttpMethod: (value: string) => value is HttpMethodFuncKey;
5
11
  export declare const deepMerge: <T extends object, U extends object>(target: T, source: U) => T;
@@ -1 +1 @@
1
- import{DYNAMIC_PREFIX as i,CATCH_ALL_PREFIX as a,OPTIONAL_CATCH_ALL_PREFIX as p,HTTP_METHOD_FUNC_KEYS as u}from"../lib/constants";const T=t=>t.startsWith(i),l=t=>t.startsWith(a)||t.startsWith(p),y=new Set(u),A=t=>y.has(t),c=t=>typeof t=="object"&&t!==null&&!Array.isArray(t),d=(t,n)=>{const s={...t};for(const e in n)if(Object.prototype.hasOwnProperty.call(n,e)){const r=t[e],o=n[e];c(r)&&c(o)?s[e]=d(r,o):s[e]=o}return s};export{d as deepMerge,l as isCatchAllOrOptional,T as isDynamic,A as isHttpMethod};
1
+ import{DYNAMIC_PREFIX as i,CATCH_ALL_PREFIX as a,OPTIONAL_CATCH_ALL_PREFIX as p,HTTP_METHOD_FUNC_KEYS as u}from"rpc4next-shared";const T=t=>t.startsWith(i),l=t=>t.startsWith(a)||t.startsWith(p),y=new Set(u),A=t=>y.has(t),c=t=>typeof t=="object"&&t!==null&&!Array.isArray(t),d=(t,n)=>{const s={...t};for(const e in n)if(Object.prototype.hasOwnProperty.call(n,e)){const r=t[e],o=n[e];c(r)&&c(o)?s[e]=d(r,o):s[e]=o}return s};export{d as deepMerge,l as isCatchAllOrOptional,T as isDynamic,A as isHttpMethod};
@@ -1,6 +1,22 @@
1
1
  import type { FuncParams, UrlOptions, ClientOptions, BodyOptions, HeadersOptions } from "./types";
2
+ /** Local, non-breaking extension for future body shapes */
3
+ type ExtendedBodyOptions = BodyOptions & {
4
+ text?: string;
5
+ formData?: FormData;
6
+ urlencoded?: URLSearchParams;
7
+ raw?: BodyInit;
8
+ };
9
+ /**
10
+ * Build a typed HTTP method invoker.
11
+ * - Header precedence: default < options.init < methodParam.requestHeaders
12
+ * - Do NOT override user-specified `content-type`.
13
+ * - Merge cookies instead of clobbering.
14
+ * - Never attach a body for GET/HEAD.
15
+ * - Provide basic body-shape inference (json/text/formData/urlencoded) while keeping current types compatible.
16
+ */
2
17
  export declare const httpMethod: (key: string, paths: string[], params: FuncParams, dynamicKeys: string[], defaultOptions: ClientOptions) => (methodParam?: {
3
18
  url?: UrlOptions;
4
- body?: BodyOptions;
19
+ body?: ExtendedBodyOptions;
5
20
  requestHeaders?: HeadersOptions;
6
21
  }, options?: ClientOptions) => Promise<Response>;
22
+ export {};
@@ -1 +1 @@
1
- import{deepMerge as R}from"./client-utils";import{createUrl as T}from"./url";import{normalizeHeaders as p}from"../lib/headers";const W=(f,u,h,y,d)=>async(e,a)=>{let s,r;e?.body?.json&&(r="application/json",s=JSON.stringify(e.body.json));const m=e?.requestHeaders?.headers,c=e?.requestHeaders?.cookies,l=T([...u],h,y)(e?.url),H=f.replace(/^\$/,"").toUpperCase(),g=a?.fetch??d.fetch??fetch,i=d.init??{},o=a?.init??{},I=p(i.headers??i.headersInit),O=p(m??o.headers??o.headersInit),t={...I,...O};r&&(t["content-type"]=r),c&&(t.cookie=Object.entries(c).map(([j,q])=>`${j}=${q}`).join("; "));const{headers:k,headersInit:B,...b}=i,{headers:U,headersInit:_,...C}=o,n=R(b,C);return n.method=H,Object.keys(t).length>0&&(n.headers=t),s&&(n.body=s),await g(l.path,n)};export{W as httpMethod};
1
+ import{deepMerge as x}from"./client-utils";import{createUrl as R}from"./url";import{normalizeHeaders as l}from"../lib/headers";const H=n=>{if(!n)return{};const{headers:i,headersInit:m,...p}=n;return p},U=n=>"content-type"in n?!0:Object.keys(n).some(i=>i.toLowerCase()==="content-type"),B=(n,i,m,p,g)=>async(d,O)=>{const a=n.replace(/^\$/,"").toUpperCase(),I=R([...i],m,p)(d?.url),C=O?.fetch??g.fetch??fetch,f=g.init,u=O?.init,w=l(f?.headers??f?.headersInit),T=l(u?.headers??u?.headersInit),j=l(d?.requestHeaders?.headers),o={...w,...T,...j},b=o.cookie,y=d?.requestHeaders?.cookies;if(y&&Object.keys(y).length>0){const s=Object.entries(y).map(([h,k])=>`${h}=${k}`).join("; ");o.cookie=b?`${b}; ${s}`:s}let t,r;const e=d?.body;e?.json!==void 0?(t=JSON.stringify(e.json),r="application/json"):typeof e?.text=="string"?(t=e.text,r="text/plain; charset=utf-8"):e?.formData instanceof FormData?(t=e.formData,r=void 0):e?.urlencoded instanceof URLSearchParams?(t=e.urlencoded,r="application/x-www-form-urlencoded; charset=UTF-8"):e?.raw!==void 0&&(t=e.raw,r=void 0),(a==="GET"||a==="HEAD")&&(t=void 0),!U(o)&&t&&r&&(o["content-type"]=r);const c=x(H(f),H(u));c.method=a,Object.keys(o).length>0&&(c.headers=o),t!==void 0&&(c.body=t);try{return await C(I.path,c)}catch(s){const h=s instanceof Error?s.message:String(s);throw new Error(`[httpMethod] ${a} ${I.path} failed: ${h}`)}};export{B as httpMethod};
@@ -1,5 +1,12 @@
1
+ type ParamValue = string | string[] | undefined;
2
+ /**
3
+ * Match a URL (string) against a pre-defined path pattern array.
4
+ * @param paths e.g. ["/", "users", "_id"] or ["users", "_id"]
5
+ * @param dynamicKeys keys in the same order the capturing groups appear
6
+ */
1
7
  export declare const matchPath: (paths: string[], dynamicKeys: string[]) => (input: string) => {
2
- params: Record<string, string | string[] | undefined>;
8
+ params: Record<string, ParamValue>;
3
9
  query: Record<string, string | string[] | undefined>;
4
10
  hash: string | undefined;
5
11
  } | null;
12
+ export {};
@@ -1 +1 @@
1
- import{isCatchAllOrOptional as g}from"./client-utils";import{replaceDynamicSegments as R}from"./url";import{searchParamsToObject as C}from"../lib/search-params";const U=(r,o)=>c=>{const e=new URL(c,"http://dummy"),s=e.pathname,m=`/${r.slice(1).join("/")}`,i=R(m,{optionalCatchAll:"(?:/(.*))?",catchAll:"/([^/]+(?:/[^/]+)*)",dynamic:"/([^/]+)"}),t=new RegExp(`^${i}/?$`).exec(s);if(!t)return null;const l=o.reduce((h,a,d)=>{const u=a.replace(/^_+/,""),n=t[d+1],f=g(a)?n===void 0||n===""?void 0:n.split("/").filter(Boolean).map(decodeURIComponent):decodeURIComponent(n);return{...h,[u]:f}},{}),p=C(e.searchParams);return{params:l,query:p,hash:e.hash?decodeURIComponent(e.hash.slice(1)):void 0}};export{U as matchPath};
1
+ import{isCatchAllOrOptional as w}from"./client-utils";import{replaceDynamicSegments as x}from"./url";import{searchParamsToObject as L}from"../lib/search-params";const o=n=>{if(n!=null)try{return decodeURIComponent(n)}catch{return n}},$=n=>{const e=n.join("/").replace(/\/+/g,"/");return(e.startsWith("/")?e:`/${e}`).replace(/\/+$/,"")||"/"},O=(n,e)=>{const l=$(n),u=x(l,{optionalCatchAll:"(?:/(.*))?",catchAll:"/([^/]+(?:/[^/]+)*)",dynamic:"/([^/]+)"}),m=new RegExp(`^${u}(?:/)?$`);return p=>{let t;try{t=new URL(p,"http://dummy")}catch{return null}const f=t.pathname,d=m.exec(f);if(!d)return null;const a={};for(let s=0;s<e.length;s++){const h=e[s],i=h.replace(/^_+/,""),r=d[s+1];if(w(h))if(r==null||r==="")a[i]=void 0;else{const R=r.split("/").filter(Boolean).map(c=>o(c)).filter(c=>c!==void 0);a[i]=R}else a[i]=o(r)}const g=L(t.searchParams),P=t.hash?t.hash.slice(1):void 0,y=o(P);return{params:a,query:g,hash:y}}};export{O as matchPath};
@@ -2,4 +2,4 @@
2
2
  * Inspired by the design of Hono (https://github.com/honojs/hono)
3
3
  * and pathpida (https://github.com/aspida/pathpida)
4
4
  * particularly their routing structures and developer experience.
5
- */import{isDynamic as f}from"./client-utils";const s=(n,e,t,c,o)=>new Proxy(u=>{const r=t.at(-1),i=o.at(-1);if(u===void 0)throw new Error(`Missing value for dynamic parameter: ${i}`);if(r&&i&&f(r))return s(n,e,[...t],{...c,[i]:u},o);throw new Error(`${r} is not dynamic`)},{get(u,r){const i=n(r,{paths:t,params:c,dynamicKeys:o,options:e});return i!==void 0?i:f(r)?s(n,e,[...t,r],c,[...o,r]):s(n,e,[...t,r],c,o)}}),l=n=>(e="/",t={})=>s(n,t,[e],{},[]);export{l as makeCreateRpc};
5
+ */import{isDynamic as g}from"./client-utils";const l=(r,e,n,s,o)=>{const p=function(){};return new Proxy(p,{apply(a,u,t){const i=t[0],c=n[n.length-1],f=o[o.length-1];if(!c||!g(c))throw new Error(`Cannot apply a value: "${c??""}" is not a dynamic segment.`);if(i===void 0){const d=f??c;throw new Error(`Missing value for dynamic parameter: ${String(d)}`)}return l(r,e,n,{...s,[f]:i},o)},get(a,u){if(u==="then"||typeof u=="symbol")return;const t=String(u),i=r(t,{paths:n,params:s,dynamicKeys:o,options:e});return i!==void 0?i:g(t)?l(r,e,[...n,t],s,[...o,t]):l(r,e,[...n,t],s,o)}})},T=r=>(e="/",n={})=>l(r,n,[e],{},[]);export{T as makeCreateRpc};
@@ -1,10 +1,10 @@
1
- import type { OPTIONAL_CATCH_ALL_PREFIX, CATCH_ALL_PREFIX, DYNAMIC_PREFIX, HTTP_METHOD_FUNC_KEYS } from "../lib/constants";
2
1
  import type { ContentType } from "../lib/content-type-types";
3
2
  import type { HttpRequestHeaders } from "../lib/http-request-headers-types";
4
3
  import type { HttpStatusCode } from "../lib/http-status-code-types";
5
4
  import type { RouteHandlerResponse, RouteResponse, ValidationSchema } from "../server/route-types";
6
5
  import type { TypedNextResponse, ValidationInputFor } from "../server/types";
7
6
  import type { NextResponse } from "next/server";
7
+ import type { OPTIONAL_CATCH_ALL_PREFIX, CATCH_ALL_PREFIX, DYNAMIC_PREFIX, HTTP_METHOD_FUNC_KEYS } from "rpc4next-shared";
8
8
  type DistributeOmit<T, K extends keyof any> = T extends any ? Omit<T, K> : never;
9
9
  /**
10
10
  * Extension of the standard `RequestInit` interface with strongly typed headers.
@@ -1,8 +1,126 @@
1
1
  import type { UrlOptions, UrlResult, FuncParams } from "./types";
2
+ /**
3
+ * Builds a URL suffix string from optional query and hash values.
4
+ *
5
+ * This utility generates the trailing portion of a URL, consisting of:
6
+ * - A query string (e.g. `?key=value&foo=bar`)
7
+ * - A hash fragment (e.g. `#section`)
8
+ *
9
+ * If both are present, they are concatenated in the order:
10
+ * `?query#hash`.
11
+ *
12
+ * If `url` is `undefined` or neither `query` nor `hash` is provided,
13
+ * an empty string is returned.
14
+ *
15
+ * @example
16
+ * buildUrlSuffix({
17
+ * query: { page: "1", sort: "asc" },
18
+ * hash: "top"
19
+ * });
20
+ * // => "?page=1&sort=asc#top"
21
+ *
22
+ * @example
23
+ * buildUrlSuffix({ hash: "section" });
24
+ * // => "#section"
25
+ *
26
+ * @example
27
+ * buildUrlSuffix();
28
+ * // => ""
29
+ *
30
+ * @param url - Optional URL options containing query parameters and/or a hash fragment.
31
+ * @returns A URL suffix string beginning with `?` and/or `#`, or an empty string if none exist.
32
+ */
2
33
  export declare const buildUrlSuffix: (url?: UrlOptions) => string;
34
+ /**
35
+ * Replaces dynamic route segment markers in a base path string.
36
+ *
37
+ * This utility transforms special placeholder patterns in `basePath`
38
+ * into concrete replacement strings.
39
+ *
40
+ * It supports three types of dynamic segments:
41
+ *
42
+ * - **Optional catch-all**: `"/_____<name>"` (5 underscores)
43
+ * - **Catch-all**: `"/___<name>"` (3 underscores)
44
+ * - **Dynamic segment**: `"/_<name>"` (1 underscore)
45
+ *
46
+ * Each matched segment (including its leading slash) is replaced
47
+ * with the corresponding value from the `replacements` object.
48
+ *
49
+ * Replacement is performed in the following order:
50
+ * 1. optionalCatchAll
51
+ * 2. catchAll
52
+ * 3. dynamic
53
+ *
54
+ * This ordering ensures that longer patterns are processed before
55
+ * shorter ones to avoid partial matching.
56
+ *
57
+ * @example
58
+ * ```ts
59
+ * replaceDynamicSegments(
60
+ * "/users/_id/posts/___slug/files/_____path",
61
+ * {
62
+ * dynamic: ":id",
63
+ * catchAll: "*",
64
+ * optionalCatchAll: "**",
65
+ * }
66
+ * );
67
+ * ```
68
+ *
69
+ * @param basePath - The path string containing dynamic segment markers.
70
+ * @param replacements - Replacement strings for each dynamic segment type.
71
+ * @param replacements.optionalCatchAll - Replacement for `"/_____<name>"` segments.
72
+ * @param replacements.catchAll - Replacement for `"/___<name>"` segments.
73
+ * @param replacements.dynamic - Replacement for `"/_<name>"` segments.
74
+ *
75
+ * @returns A new path string with all dynamic segment markers replaced.
76
+ */
3
77
  export declare const replaceDynamicSegments: (basePath: string, replacements: {
4
78
  optionalCatchAll: string;
5
79
  catchAll: string;
6
80
  dynamic: string;
7
81
  }) => string;
82
+ /**
83
+ * Creates a URL builder for a route definition.
84
+ *
85
+ * This function takes a list of path segments (e.g. `["api", "users", "[id]"]`),
86
+ * a params object, and a list of dynamic keys used in the path, then returns a
87
+ * function that can build a concrete URL string plus related metadata.
88
+ *
89
+ * Dynamic segments are replaced using `dynamicKeys`:
90
+ * - If `params[key]` is an array, it is treated as a catch-all segment and each
91
+ * element is URL-encoded then joined by `/`.
92
+ * - If `params[key]` is `undefined`, the segment is removed (useful for optional
93
+ * segments).
94
+ * - Otherwise, the value is URL-encoded and substituted into the segment.
95
+ *
96
+ * The returned builder optionally appends a suffix (e.g. query/hash) via
97
+ * `buildUrlSuffix(url)` and also produces a Next.js-style `pathname` by converting
98
+ * dynamic markers using `replaceDynamicSegments`.
99
+ *
100
+ * In addition, params keys are normalized by removing leading underscores
101
+ * (e.g. `__id` -> `id`) in the returned `params`.
102
+ *
103
+ * @param paths - Path segments. The first element is treated as an optional base
104
+ * URL, and the remaining segments form the route path.
105
+ * @param params - Parameters used to replace dynamic segments. Values may be
106
+ * `string`, `string[]`, or `undefined` depending on the segment type.
107
+ * @param dynamicKeys - Keys representing dynamic segments present in the path,
108
+ * used to substitute values from `params`.
109
+ * @returns A builder function that accepts URL options and returns a `UrlResult`.
110
+ *
111
+ * @example
112
+ * ```ts
113
+ * const build = createUrl(
114
+ * ["https://example.com", "users", "id"],
115
+ * { id: "a b" },
116
+ * ["id"],
117
+ * );
118
+ *
119
+ * const result = build({ query: { page: "1" } });
120
+ * // result.path: "https://example.com/users/a%20b?page=1"
121
+ * // result.relativePath: "/users/a%20b?page=1"
122
+ * // result.pathname: "/users/[$1]" (depending on replaceDynamicSegments behavior)
123
+ * // result.params: { id: "a b" }
124
+ * ```
125
+ */
8
126
  export declare const createUrl: (paths: string[], params: FuncParams, dynamicKeys: string[]) => (url?: UrlOptions) => UrlResult;
@@ -1 +1,36 @@
1
+ /**
2
+ * Normalizes various `HeadersInit` formats into a plain object
3
+ * with lower-cased header names.
4
+ *
5
+ * This utility converts `Headers`, an array of `[key, value]` tuples,
6
+ * or a plain object into a `Record<string, string>`.
7
+ *
8
+ * All header names are converted to lowercase to ensure
9
+ * case-insensitive consistency when accessing header values.
10
+ *
11
+ * @param headers - A `HeadersInit` value (`Headers`, `[string, string][]`, or `Record<string, string>`).
12
+ * If `undefined`, an empty object is returned.
13
+ *
14
+ * @returns A plain object whose keys are lower-cased header names
15
+ * and whose values are header strings.
16
+ *
17
+ * @example
18
+ * ```ts
19
+ * const headers = new Headers({
20
+ * "Content-Type": "application/json",
21
+ * "Authorization": "Bearer token",
22
+ * });
23
+ *
24
+ * const normalized = normalizeHeaders(headers);
25
+ * // {
26
+ * // "content-type": "application/json",
27
+ * // "authorization": "Bearer token"
28
+ * // }
29
+ * ```
30
+ *
31
+ * @remarks
32
+ * - If duplicate header names exist (ignoring case),
33
+ * the last encountered value will overwrite previous ones.
34
+ * - This function does not merge multiple values for the same header.
35
+ */
1
36
  export declare const normalizeHeaders: (headers?: HeadersInit) => Record<string, string>;
@@ -1 +1,35 @@
1
+ /**
2
+ * Converts a `URLSearchParams` instance into a plain object.
3
+ *
4
+ * This utility aggregates query parameters into an object where:
5
+ * - The first occurrence of a key becomes a `string`
6
+ * - If the same key appears multiple times, the value becomes a `string[]`
7
+ *
8
+ * The returned object is cast to the generic type `T`. This is a **type-level**
9
+ * convenience: the runtime output only contains `string` or `string[]` values
10
+ * (no `undefined` keys are produced).
11
+ *
12
+ * @typeParam T - The desired return shape. Must be a record of `string` keys
13
+ * with values `string | string[] | undefined`. Note: `undefined` is allowed in
14
+ * the type to support downstream optional fields, but is not produced at runtime.
15
+ *
16
+ * @param searchParams - The `URLSearchParams` to convert.
17
+ * @returns A plain object containing the query parameters.
18
+ *
19
+ * @example
20
+ * ```ts
21
+ * const sp = new URLSearchParams("q=chatgpt&tag=a&tag=b");
22
+ * const obj = searchParamsToObject(sp);
23
+ * // => { q: "chatgpt", tag: ["a", "b"] }
24
+ * ```
25
+ *
26
+ * @example
27
+ * ```ts
28
+ * type Params = { q?: string; tag?: string[] };
29
+ * const sp = new URLSearchParams("q=hello&tag=a&tag=b");
30
+ * const obj = searchParamsToObject<Params>(sp);
31
+ * // obj.q is typed as string | undefined
32
+ * // obj.tag is typed as string[] | undefined
33
+ * ```
34
+ */
1
35
  export declare const searchParamsToObject: <T extends Record<string, string | string[] | undefined>>(searchParams: URLSearchParams) => T;
@@ -1,2 +1 @@
1
- import type { HTTP_METHODS } from "./constants";
2
- export type HttpMethod = (typeof HTTP_METHODS)[number];
1
+ export type { HttpMethod } from "rpc4next-shared";
@@ -1,4 +1,4 @@
1
1
  import type { ValidationSchema, RouteResponse, Handler } from "./route-types";
2
2
  import type { Params, Query } from "./types";
3
- import type { HttpMethod } from "../lib/types";
3
+ import type { HttpMethod } from "rpc4next-shared";
4
4
  export declare const createHandler: <THttpMethod extends HttpMethod, TParams extends Params, TQuery extends Query, TValidationSchema extends ValidationSchema>() => <TRouteResponse extends RouteResponse>(handler: Handler<THttpMethod, TParams, TQuery, TValidationSchema, TRouteResponse>) => Handler<THttpMethod, TParams, TQuery, TValidationSchema, TRouteResponse>;
@@ -1,4 +1,4 @@
1
1
  /*!
2
2
  * Inspired by Hono (https://github.com/honojs/hono),
3
3
  * particularly its routing design and handler interface.
4
- */import{createRouteContext as d}from"./route-context";const i=(o,e)=>async(n,s)=>{const a=d(n,s);try{for(const t of o){const r=await t(a);if(r instanceof Response)return r}throw new Error("No handler returned a response")}catch(t){return await e(t,a)}},T=o=>()=>{const e=n=>(...s)=>{const t=i(s,o??((r,p)=>{throw r}));return{[n]:t}};return{get:e("GET"),post:e("POST"),put:e("PUT"),delete:e("DELETE"),patch:e("PATCH"),head:e("HEAD"),options:e("OPTIONS")}};export{T as routeHandlerFactory};
4
+ */import{createRouteContext as d}from"./route-context";const i=(o,e)=>async(n,s)=>{const a=d(n,s);try{for(const r of o){const t=await r(a);if(t instanceof Response)return t}throw new Error("No handler returned a response")}catch(r){return await e(r,a)}},T=o=>()=>{const e=n=>((...s)=>{const r=i(s,o??((t,p)=>{throw t}));return{[n]:r}});return{get:e("GET"),post:e("POST"),put:e("PUT"),delete:e("DELETE"),patch:e("PATCH"),head:e("HEAD"),options:e("OPTIONS")}};export{T as routeHandlerFactory};
@@ -7,8 +7,8 @@
7
7
  * Hono is licensed under the MIT License.
8
8
  */
9
9
  import type { TypedNextResponse, Query, RouteContext, Params } from "./types";
10
- import type { HttpMethod } from "../lib/types";
11
10
  import type { NextRequest } from "next/server";
11
+ import type { HttpMethod } from "rpc4next-shared";
12
12
  export type RouteResponse = TypedNextResponse | Promise<TypedNextResponse | void>;
13
13
  export type RequiredRouteResponse = TypedNextResponse | Promise<TypedNextResponse>;
14
14
  export interface RouteBindings {
@@ -2,8 +2,8 @@ import type { ValidationSchema } from "./route-types";
2
2
  import type { ContentType } from "../lib/content-type-types";
3
3
  import type { HttpResponseHeaders } from "../lib/http-response-headers-types";
4
4
  import type { HttpStatusCode, RedirectionHttpStatusCode, SuccessfulHttpStatusCode } from "../lib/http-status-code-types";
5
- import type { HttpMethod } from "../lib/types";
6
5
  import type { NextResponse, NextRequest } from "next/server";
6
+ import type { HttpMethod } from "rpc4next-shared";
7
7
  /**
8
8
  * Represents the result of an HTTP response status check.
9
9
  *
@@ -6,7 +6,7 @@
6
6
  * Original copyright belongs to Yusuke Wada and the Hono project contributors.
7
7
  * Hono is licensed under the MIT License.
8
8
  */
9
- import type { HttpMethod } from "../../lib/types";
10
9
  import type { ValidationSchema } from "../route-types";
11
10
  import type { Params, Query, RouteContext, TypedNextResponse, ValidatedData, ValidationTarget } from "../types";
11
+ import type { HttpMethod } from "rpc4next-shared";
12
12
  export declare const validator: <THttpMethod extends HttpMethod, TValidationTarget extends ValidationTarget<THttpMethod>, TParams extends Params, TQuery extends Query, TValidationSchema extends ValidationSchema>() => <TTypedNextResponse extends TypedNextResponse>(target: TValidationTarget, validateHandler: (value: object, routeContext: RouteContext<TParams, TQuery, TValidationSchema>) => Promise<ValidatedData | TTypedNextResponse>) => import("../route-types").Handler<THttpMethod, TParams, TQuery, TValidationSchema, Promise<TTypedNextResponse | undefined>>;
@@ -5,4 +5,4 @@
5
5
  * This code has been adapted and modified for this project.
6
6
  * Original copyright belongs to Yusuke Wada and the Hono project contributors.
7
7
  * Hono is licensed under the MIT License.
8
- */import{createHandler as i}from"../handler";import{getCookiesObject as n,getHeadersObject as s}from"./validator-utils";const T=()=>(e,r)=>i()(async t=>{const o=await(async()=>{if(e==="params")return await t.req.params();if(e==="query")return t.req.query();if(e==="json")return t.req.json();if(e==="headers")return await s();if(e==="cookies")return await n();throw new Error(`Unexpected target: ${e}`)})(),a=await r(o,t);if(a instanceof Response)return a;t.req.addValidatedData(e,a)});export{T as validator};
8
+ */import{createHandler as n}from"../handler";import{getCookiesObject as i,getHeadersObject as s}from"./validator-utils";const T=()=>(e,r)=>n()(async t=>{const o=await(async()=>{if(e==="params")return await t.req.params();if(e==="query")return t.req.query();if(e==="json")return t.req.json();if(e==="headers")return await s();if(e==="cookies")return await i();throw new Error(`Unexpected target: ${e}`)})(),a=await r(o,t);if(a instanceof Response)return a;t.req.addValidatedData(e,a)});export{T as validator};
@@ -6,11 +6,11 @@
6
6
  * Original copyright belongs to Yusuke Wada and the Hono project contributors.
7
7
  * Hono is licensed under the MIT License.
8
8
  */
9
- import type { HttpMethod } from "../../../lib/types";
10
9
  import type { ValidationSchema } from "../../route-types";
11
10
  import type { RouteContext, Params, Query, TypedNextResponse, ConditionalValidationInput, ValidationTarget } from "../../types";
11
+ import type { HttpMethod } from "rpc4next-shared";
12
12
  import type { z, ZodSchema } from "zod";
13
13
  export declare const zValidator: <THttpMethod extends HttpMethod, TValidationTarget extends ValidationTarget<THttpMethod>, TSchema extends ZodSchema<any>, TParams extends ConditionalValidationInput<TValidationTarget, "params", TValidationSchema, Params> & Params, TQuery extends ConditionalValidationInput<TValidationTarget, "query", TValidationSchema, Query> & Query, TInput = z.input<TSchema>, TOutput = z.output<TSchema>, TValidationSchema extends ValidationSchema = {
14
14
  input: Record<TValidationTarget, TInput>;
15
15
  output: Record<TValidationTarget, TOutput>;
16
- }, THookReturn extends TypedNextResponse | void = TypedNextResponse<z.SafeParseError<TInput>, 400, "application/json"> | void>(target: TValidationTarget, schema: TSchema, hook?: (result: z.SafeParseReturnType<TInput, TOutput>, routeContext: RouteContext<TParams, TQuery, TValidationSchema>) => THookReturn) => import("../../route-types").Handler<THttpMethod, TParams, TQuery, TValidationSchema, Promise<Exclude<THookReturn, void> | undefined>>;
16
+ }, THookReturn extends TypedNextResponse | void = TypedNextResponse<z.ZodSafeParseError<TInput>, 400, "application/json"> | void>(target: TValidationTarget, schema: TSchema, hook?: (result: z.ZodSafeParseResult<TOutput>, routeContext: RouteContext<TParams, TQuery, TValidationSchema>) => THookReturn) => import("../../route-types").Handler<THttpMethod, TParams, TQuery, TValidationSchema, Promise<Exclude<THookReturn, void> | undefined>>;
package/package.json CHANGED
@@ -1,22 +1,25 @@
1
1
  {
2
2
  "name": "rpc4next",
3
- "version": "0.4.0",
4
3
  "description": "Inspired by Hono RPC and Pathpida, rpc4next brings a lightweight and intuitive RPC solution to Next.js, making server-client communication seamless",
5
4
  "author": "watanabe-1",
6
5
  "license": "MIT",
6
+ "version": "0.4.10",
7
7
  "repository": {
8
8
  "type": "git",
9
- "url": "https://github.com/watanabe-1/rpc4next"
9
+ "url": "git+https://github.com/watanabe-1/rpc4next.git",
10
+ "directory": "packages/rpc4next"
10
11
  },
11
12
  "homepage": "https://github.com/watanabe-1/rpc4next#readme",
12
13
  "bugs": {
13
14
  "url": "https://github.com/watanabe-1/rpc4next/issues"
14
15
  },
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
15
19
  "keywords": [
16
20
  "next.js",
17
21
  "rpc",
18
- "typescript",
19
- "cli"
22
+ "typescript"
20
23
  ],
21
24
  "type": "module",
22
25
  "module": "dist/index.js",
@@ -24,10 +27,17 @@
24
27
  "files": [
25
28
  "dist"
26
29
  ],
27
- "bin": {
28
- "rpc4next": "dist/rpc/cli/index.js"
29
- },
30
30
  "exports": {
31
+ ".": {
32
+ "import": {
33
+ "types": "./dist/index.d.ts",
34
+ "default": "./dist/index.js"
35
+ },
36
+ "default": {
37
+ "types": "./dist/index.d.ts",
38
+ "default": "./dist/index.js"
39
+ }
40
+ },
31
41
  "./client": {
32
42
  "import": {
33
43
  "types": "./dist/rpc/client/index.d.ts",
@@ -57,44 +67,42 @@
57
67
  "types": "./dist/rpc/server/validators/zod/index.d.ts",
58
68
  "default": "./dist/rpc/server/validators/zod/index.js"
59
69
  }
70
+ },
71
+ "./lib/constants": {
72
+ "import": {
73
+ "types": "./dist/rpc/lib/constants.d.ts",
74
+ "default": "./dist/rpc/lib/constants.js"
75
+ },
76
+ "default": {
77
+ "types": "./dist/rpc/lib/constants.d.ts",
78
+ "default": "./dist/rpc/lib/constants.js"
79
+ }
80
+ },
81
+ "./lib/types": {
82
+ "import": {
83
+ "types": "./dist/rpc/lib/types.d.ts",
84
+ "default": "./dist/rpc/lib/types.js"
85
+ },
86
+ "default": {
87
+ "types": "./dist/rpc/lib/types.d.ts",
88
+ "default": "./dist/rpc/lib/types.js"
89
+ }
60
90
  }
61
91
  },
62
92
  "scripts": {
63
93
  "build": "bun run clean && bun build.ts && bun run build:types",
64
- "build:types": "tsc -p tsconfig.build-types.json",
65
- "clean": "bun -e \"require('fs').rmSync('dist', { recursive: true, force: true })\"",
94
+ "build:types": "tsc -b --force tsconfig.build-types.json",
95
+ "clean": "bun -e \"const fs=require('fs'); fs.rmSync('dist',{recursive:true,force:true}); fs.rmSync('tsconfig.build-types.tsbuildinfo',{force:true}); fs.rmSync('tsconfig.tsbuildinfo',{force:true});\"",
66
96
  "test": "vitest run",
67
97
  "test:coverage": "vitest run --coverage.enabled true",
68
98
  "test:ui": "vitest --ui --coverage.enabled true",
69
99
  "test:watch": "vitest --watch",
70
- "lint": "eslint \"**/*.ts\"",
71
- "lint:fix": "eslint \"**/*.ts\" --fix",
72
- "typecheck": "tsc --noEmit"
100
+ "lint": "eslint \"src/**/*.ts\"",
101
+ "lint:fix": "eslint \"src/**/*.ts\" --fix",
102
+ "typecheck": "tsc -b tsconfig.build.json --noEmit"
73
103
  },
74
104
  "dependencies": {
75
- "chalk": "^5.4.1",
76
- "chokidar": "^4.0.3",
77
- "commander": "^13.1.0"
78
- },
79
- "devDependencies": {
80
- "@types/node": "^22.15.3",
81
- "@vitest/coverage-v8": "^3.1.2",
82
- "@vitest/eslint-plugin": "^1.1.43",
83
- "@vitest/ui": "^3.1.2",
84
- "esbuild": "^0.25.3",
85
- "eslint": "^9.25.1",
86
- "eslint-config-prettier": "^10.1.2",
87
- "eslint-plugin-import": "^2.31.0",
88
- "eslint-plugin-unused-imports": "^4.1.4",
89
- "mock-fs": "^5.5.0",
90
- "msw": "^2.7.5",
91
- "next": "15.3.1",
92
- "prettier": "^3.5.3",
93
- "ts-node": "^10.9.2",
94
- "typescript": "^5.8.3",
95
- "typescript-eslint": "^8.31.1",
96
- "vitest": "^3.1.2",
97
- "zod": "^3.24.3"
105
+ "rpc4next-shared": "workspace:*"
98
106
  },
99
107
  "peerDependencies": {
100
108
  "next": "^14.0.0 || ^15.0.0"
package/LICENSE DELETED
@@ -1,21 +0,0 @@
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 DELETED
@@ -1,320 +0,0 @@
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
- - ✅ ルート、パラメータ、クエリパラメータ、 リクエストボディ、レスポンスの型安全なクライアント生成
12
- - ✅ 既存の `app/**/route.ts` および `app/**/page.tsx` を活用するため、新たなハンドラファイルの作成は不要
13
- - ✅ 最小限のセットアップで、カスタムサーバー不要
14
- - ✅ 動的ルート(`[id]`、`[...slug]` など)に対応
15
- - ✅ CLI による自動クライアント用型定義生成
16
-
17
- ---
18
-
19
- ## 🚀 Getting Started
20
-
21
- ### 1. Install rpc4next
22
-
23
- ```bash
24
- npm install rpc4next
25
- ```
26
-
27
- ### 2. Define API Routes in Next.js
28
-
29
- Next.js プロジェクト内の既存の `app/**/route.ts` と `app/**/page.tsx` ファイルをそのまま利用できます。
30
- さらに、クエリパラメータ(searchParams)の型安全性を有効にするには、対象のファイル内で `Query` 型を定義し、`export` してください。
31
-
32
- ```ts
33
- // app/api/user/[id]/route.ts
34
- import { NextRequest, NextResponse } from "next/server";
35
-
36
- // searchParams用の型定義
37
- export type Query = {
38
- q: string; // 必須
39
- page?: number; // 任意
40
- };
41
-
42
- export async function GET(
43
- req: NextRequest,
44
- segmentData: { params: Promise<{ id: string }> }
45
- ) {
46
- const { id } = await segmentData.params;
47
- const q = req.nextUrl.searchParams.get("q");
48
- return NextResponse.json({ id, q });
49
- }
50
- ```
51
-
52
- 🚩 Query 型を export することで、searchParams の型も自動的にクライアントに反映されます。
53
-
54
- - **RPCとしてresponseの戻り値の推論が機能するのは、対象となる `route.ts` の HTTPメソッドハンドラ内で`NextResponse.json()` をしている関数のみになります**
55
-
56
- ---
57
-
58
- ### 3. Generate Type Definitions with CLI
59
-
60
- CLI を利用して、Next.js のルート構造から型安全な RPC クライアントの定義を自動生成します。
61
-
62
- ```bash
63
- npx rpc4next <baseDir> <outputPath>
64
- ```
65
-
66
- - `<baseDir>`: Next.js の Appルータが配置されたベースディレクトリ
67
- - `<outputPath>`: 生成された型定義ファイルの出力先
68
-
69
- #### オプション
70
-
71
- - **ウォッチモード**
72
- ファイル変更を検知して自動的に再生成する場合は `--watch` or `-w` オプションを付けます。
73
-
74
- ```bash
75
- npx rpc4next <baseDir> <outputPath> --watch
76
- ```
77
-
78
- - **パラメータ型ファイルの生成**
79
- 各ルートに対して個別のパラメータ型定義ファイルを生成する場合は、`--params-file` or `-p` オプションにファイル名を指定します。
80
-
81
- ```bash
82
- npx rpc4next <baseDir> <outputPath> --generate-params-types <paramsFileName>
83
- ```
84
-
85
- ---
86
-
87
- ### 4. Create Your RPC Client
88
-
89
- 生成された型定義ファイルを基に、RPC クライアントを作成します。
90
-
91
- ```ts
92
- // lib/rpcClient.ts
93
- import { createClient } from "rpc4next/client";
94
- import type { PathStructure } from "あなたが生成した型定義ファイル";
95
-
96
- export const rpc = createClient<PathStructure>();
97
- ```
98
-
99
- ---
100
-
101
- ### 5. Use It in Your Components
102
-
103
- コンポーネント内で生成された RPC クライアントを使用します。
104
-
105
- ```tsx
106
- // app/page.tsx
107
- import { rpc } from "@/lib/rpcClient";
108
-
109
- export default async function Page() {
110
- const res = await rpc.api.user._id("123").$get({
111
- query: { q: "hello", page: 1 },
112
- });
113
- const json = await res.json();
114
- return <div>{json.q}</div>;
115
- }
116
- ```
117
-
118
- - エディタの補完機能により、利用可能なエンドポイントが自動的に表示されます。
119
- - リクエストの構造(params, query)はサーバーコードから推論され、レスポンスも型安全に扱えます。
120
-
121
- ---
122
-
123
- ## ✅ さらに型安全にしたい場合 `createRouteHandler` による Next.js の型安全強化
124
-
125
- ### 📌 主なメリット
126
-
127
- 1. **レスポンス型安全**
128
-
129
- - ステータス、Content-Type、Body がすべて型で保証される
130
- - クライアントは受け取るレスポンス型を完全に推論可能
131
-
132
- 2. **クライアント側補完強化**
133
-
134
- - `status`, `content-type`, `json()`, `text()` などが適切に補完される
135
-
136
- 3. **サーバー側 params / query も型安全**
137
- - `createRouteHandler()` + `zValidator()` を使えば、`params`, `query`, `headers`, `cookies`, `json` も型推論・バリデーション可能
138
- - `createRouteHandler()` + `zValidator()` を使えば、`Query` 型もexportする必要なし
139
-
140
- ---
141
-
142
- ### ✅ 基本的な使い方
143
-
144
- ```ts
145
- const createRouteHandler = routeHandlerFactory((err, rc) =>
146
- rc.text("error", { status: 400 })
147
- );
148
-
149
- const { POST } = createRouteHandler().post(async (rc) => rc.text("plain text"));
150
- ```
151
-
152
- これだけで、POSTリクエストの返り値に、レスポンスの内容 (`json`, `text`など)、`status`, `content-type` が型付けされるようになります。
153
-
154
- ---
155
-
156
- ### 👤 サーバー側でのより型安全なルート作成
157
-
158
- `createRouteHandler()` と `zValidator()` を使うことで、各リクエストパーツに対して **型安全なバリデーション** をかけられます。
159
-
160
- #### シンプルな例
161
-
162
- ```ts
163
- import { createRouteHandler } from "@/path/to/createRouteHandler";
164
- import { zValidator } from "@/path/to/zValidator";
165
- import { z } from "zod";
166
-
167
- // Zodスキーマを定義
168
- const paramsSchema = z.object({
169
- userId: z.string(),
170
- });
171
-
172
- // バリデーション付きルートハンドラを作成
173
- export const { GET } = createRouteHandler<{
174
- params: z.infer<typeof paramsSchema>;
175
- }>().get(
176
- zValidator("params", paramsSchema), // paramsを検証
177
- async (rc) => {
178
- const params = rc.req.valid("params"); // バリデーション済みparamsを取得
179
- return rc.json({ message: `User ID is ${params.userId}` });
180
- }
181
- );
182
- ```
183
-
184
- ## ✅ サポートされているバリデーションターゲット
185
-
186
- サーバー側では,次のリクエスト部分を型安全に検証できます:
187
-
188
- | ターゲット | 説明 |
189
- | :--------- | :-------------------------------------------------- |
190
- | `params` | URLパラメータ ( `/user/:id` の `id`など) |
191
- | `query` | クエリパラメータ (`?q=xxx&page=1`など) |
192
- | `headers` | リクエストヘッダー |
193
- | `cookies` | クッキー |
194
- | `json` | リクエストボディ (Content-Type: `application/json`) |
195
-
196
- ---
197
-
198
- ### 🔥 複数ターゲットを同時に検証する例
199
-
200
- ```ts
201
- import { createRouteHandler } from "@/path/to/createRouteHandler";
202
- import { zValidator } from "@/path/to/zValidator";
203
- import { z } from "zod";
204
-
205
- const querySchema = z.object({
206
- page: z.string().regex(/^\d+$/),
207
- });
208
-
209
- const jsonSchema = z.object({
210
- name: z.string(),
211
- age: z.number(),
212
- });
213
-
214
- export const { POST } = createRouteHandler<{
215
- query: z.infer<typeof querySchema>;
216
- }>().post(
217
- zValidator("query", querySchema),
218
- zValidator("json", jsonSchema),
219
- async (rc) => {
220
- const query = rc.req.valid("query");
221
- const body = rc.req.valid("json");
222
- return rc.json({ query, body });
223
- }
224
- );
225
- ```
226
-
227
- - `query`と`json`を別々のスキーマで検証
228
- - **成功時は型安全に取得可能** (`rc.req.valid('query')`, `rc.req.valid('json')`)
229
-
230
- ---
231
-
232
- これにより,クライアント側とサーバー側が、全面的に**型でつながる**ので,ミスを何次も防げ,開発体験を大幅に向上できます。
233
-
234
- ---
235
-
236
- ### ⚡ バリデーション失敗時のカスタムエラーハンドリング
237
-
238
- - デフォルトでは、バリデーション失敗時に自動で `400 Bad Request` を返します。
239
- - 必要に応じて、**カスタムフック**でエラー対応を制御できます。
240
-
241
- ```ts
242
- zValidator("params", paramsSchema, (result, rc) => {
243
- if (!result.success) {
244
- return rc.json({ error: result.error.errors }, { status: 422 });
245
- }
246
- });
247
- ```
248
-
249
- > (フック内でレスポンスを返さない場合は、通常通り例外がスローされます)
250
-
251
- ---
252
-
253
- ## 📡 クライアント側での使い方
254
-
255
- `rpc4next`で作成したクライアントは、`createRouteHandler` と `zValidator` で作成したルートハンドラの内容にしたがって **params, query, headers, cookies, json** を型安全に送信できます。
256
-
257
- 例:
258
-
259
- ```ts
260
- import { createRpcClient } from "@/path/to/rpc-client";
261
- import type { PathStructure } from "@/path/to/generated-types";
262
-
263
- const client = createRpcClient<PathStructure>("http://localhost:3000");
264
-
265
- async function callUserApi() {
266
- const res = await client.api.menu.test.$post({
267
- body: { json: { age: 20, name: "foo" } },
268
- url: { query: { page: "1" } },
269
- });
270
-
271
- if (res.ok) {
272
- const json = await res.json();
273
-
274
- // ✅ 正常時は次の型が推論されます
275
- // const json: {
276
- // query: {
277
- // page: string;
278
- // };
279
- // body: {
280
- // name: string;
281
- // age: number;
282
- // };
283
- // }
284
- } else {
285
- const error = await res.json();
286
-
287
- // ⚠️ バリデーションエラー時は次の型が推論されます
288
- // const error:
289
- // | SafeParseError<{
290
- // page: string;
291
- // }>
292
- // | SafeParseError<{
293
- // name: string;
294
- // age: number;
295
- // }>;
296
- }
297
- }
298
- ```
299
-
300
- - エディタの補完機能により、送信できるターゲットが明示されます
301
- - サーバー側の型定義に基づいて、**型のズレを防止**できます
302
-
303
- ---
304
-
305
- これらのように、リクエスト時にはさまざまなターゲット (`params`, `query`, `headers`, `cookies`, `json`) を送信できます。
306
-
307
- さらに、サーバー側では、これらを**個別に型付け、バリデーション**できます。
308
-
309
- ---
310
-
311
- ## 🚧 Requirements
312
-
313
- - Next.js 14+ (App Router 使用)
314
- - Node.js 18+
315
-
316
- ---
317
-
318
- ## 💼 License
319
-
320
- MIT
@@ -1,7 +0,0 @@
1
- #!/usr/bin/env node
2
- import{Command as Wt}from"commander";import xt from"path";var I=["page.tsx","route.ts"],w=0,$=1,M=1,W=20,Y="\u2192";import U from"path";var tt=(t,r)=>{let e=S(U.relative(U.dirname(t),r)).replace(/\.tsx?$/,"");return e.startsWith("../")||(e="./"+e),e},S=t=>t.replace(/\\/g,"/"),C=t=>U.relative(process.cwd(),t);import dt from"fs";import wt from"path";var et=["Query"],R=" ",h=`
3
- `,_=";",H=";",j="Endpoint",X="QueryKey",G="ParamsKey",rt=[j,G,X],ot="rpc4next/client";import ht from"fs";import b from"path";import k from"path";var g=new Map,N=new Map,nt=(t,r)=>{let e=k.resolve(r);[...t.keys()].forEach(o=>{let n=k.resolve(o);(n===e||e.startsWith(n+k.sep))&&t.delete(o)})},st=t=>{nt(g,t)},it=t=>{nt(N,t)};import bt from"fs";import Dt from"crypto";var at=(t,r)=>{let e=Dt.createHash("md5").update(`${t}::${r}`).digest("hex").slice(0,16);return`${r}_${e}`};var L=(t,r)=>!t||!r?"":`Record<${t}, ${r}>`,O=t=>t.length===0||t.some(({name:r,type:e})=>!r||!e)?"":`{ ${t.map(({name:r,type:e})=>`"${r}": ${e}`).join(`${H} `)}${t.length>1?H:""} }`,D=(t,r,e)=>!t||!r?"":e?`import type { ${t} as ${e} } from "${r}"${_}`:`import type { ${t} } from "${r}"${_}`;var pt=(t,r,e,o)=>{let n=bt.readFileSync(r,"utf8"),i=e(n);if(!i)return;let s=tt(t,r),p=at(s,i);return{importName:p,importPath:s,importStatement:D(i,s,p),type:o(i,p)}},ct=(t,r)=>pt(t,r,e=>et.find(o=>new RegExp(`export (interface ${o} ?{|type ${o} ?=)`).test(e)),(e,o)=>L(X,o)),mt=(t,r,e)=>pt(t,r,o=>[e].find(n=>new RegExp(`export (async )?(function ${n} ?\\(|const ${n} ?=|\\{[^}]*\\b${n}\\b[^}]*\\} ?=|const \\{[^}]*\\b${n}\\b[^}]*\\} ?=|\\{[^}]*\\b${n}\\b[^}]*\\} from)`).test(o)),(o,n)=>O([{name:`$${o.toLowerCase()}`,type:`typeof ${n}`}]));var vt=["GET","HEAD","OPTIONS","POST","PUT","DELETE","PATCH"],lt="_____",ut="___",ft="_",K=vt.filter(t=>t!=="OPTIONS"),re=K.map(t=>`$${t.toLowerCase()}`);var Tt=new Set(I),Et=t=>{if(g.has(t))return g.get(t);let r=ht.readdirSync(t,{withFileTypes:!0});for(let e of r){let{name:o}=e,n=b.join(t,o);if(o==="node_modules"||o.startsWith("_")||o.startsWith("(.)")||o.startsWith("(..)")||o.startsWith("(...)"))return g.set(t,!1),!1;if(e.isFile()&&Tt.has(o))return g.set(t,!0),!0;if(e.isDirectory()&&Et(n))return g.set(t,!0),!0}return g.set(t,!1),!1},Ft=(t,{isDynamic:r,isCatchAll:e,isOptionalCatchAll:o})=>{let n=t;return r&&(n=n.replace(/^\[+|\]+$/g,"")),(e||o)&&(n=n.replace(/^\.{3}/,"")),{paramName:n,keyName:`${o?lt:e?ut:r?ft:""}${n}`}},Q=(t,r,e="",o=[])=>{if(N.has(r))return N.get(r);let n=e,i=e+R,s=[],p=[],a=[],m=[],c=[...o],x=ht.readdirSync(r,{withFileTypes:!0}).filter(y=>{if(y.isDirectory()){let P=b.join(r,y.name);return Et(P)}return Tt.has(y.name)}).sort();for(let y of x){let P=S(b.join(r,y.name));if(y.isDirectory()){let l=y.name,T=l.startsWith("(")&&l.endsWith(")"),u=l.startsWith("@"),f=l.startsWith("[[...")&&l.endsWith("]]"),E=l.startsWith("[...")&&l.endsWith("]"),d=l.startsWith("[")&&l.endsWith("]"),{paramName:At,keyName:It}=Ft(l,{isDynamic:d,isCatchAll:E,isOptionalCatchAll:f}),Rt=d||E||f?[...c,{paramName:At,routeType:{isDynamic:d,isCatchAll:E,isOptionalCatchAll:f,isGroup:T,isParallel:u}}]:c,z=T||u,{pathStructure:J,imports:Lt,paramsTypes:Ot}=Q(t,P,z?n:i,Rt);if(p.push(...Lt),m.push(...Ot),z){let Z=J.match(/^\s*\{([\s\S]*)\}\s*$/);Z&&s.push(`${i}${Z[1].trim()}`)}else s.push(`${i}"${It}": ${J}`)}else{let l=ct(t,P);if(l){let{importStatement:T,importPath:u,type:f}=l;p.push({statement:T,path:u}),a.push(f)}if(K.forEach(T=>{let u=mt(t,P,T);if(u){let{importStatement:f,importPath:E,type:d}=u;p.push({statement:f,path:E}),a.push(d)}}),a.push(j),c.length>0){let T=c.map(({paramName:f,routeType:E})=>{let d=E.isCatchAll?"string[]":E.isOptionalCatchAll?"string[] | undefined":"string";return{name:f,type:d}}),u=O(T);m.push({paramsType:u,dirPath:b.dirname(P)}),a.push(L(G,u))}}}let A=a.join(" & "),F=s.length>0?`{${h}${s.join(`,${h}`)}${h}${n}}`:"",q={pathStructure:A&&F?`${A} & ${F}`:A||F,imports:p,paramsTypes:m};return N.set(r,q),q};var gt=(t,r)=>{let{pathStructure:e,imports:o,paramsTypes:n}=Q(t,r),i=`export type PathStructure = ${e}${_}`,s=o.length?`${o.sort((c,x)=>c.path.localeCompare(x.path,void 0,{numeric:!0})).map(c=>c.statement).join(h)}`:"",p=rt.filter(c=>e.includes(c)),a=D(p.join(" ,"),ot),m=n.map(({paramsType:c,dirPath:x})=>({paramsType:`export type Params = ${c}${_}`,dirPath:x}));return{pathStructure:`${a}${h}${s}${h}${h}${i}`,paramsTypes:m}};import v from"chalk";var B=(t,r,e="\u2192",o=24)=>t.padEnd(o)+` ${e} ${r}`,V=(t=0)=>R.repeat(t),yt=()=>({info:(t,r={})=>{let{indentLevel:e=0,event:o}=r,n=o?`${v.cyan(`[${o}]`)} `:"";console.log(`${V(e)}${n}${t}`)},success:(t,r={})=>{let{indentLevel:e=0}=r;console.log(`${V(e)}${v.green("\u2713")} ${t}`)},error:(t,r={})=>{let{indentLevel:e=0}=r;console.error(`${V(e)}${v.red("\u2717")} ${v.red(t)}`)}});var Pt=({baseDir:t,outputPath:r,paramsFileName:e,logger:o})=>{o.info("Generating types...",{event:"generate"});let{pathStructure:n,paramsTypes:i}=gt(r,t);dt.writeFileSync(r,n),o.success(B("Path structure type",C(r),Y,W),{indentLevel:M}),e&&(i.forEach(({paramsType:s,dirPath:p})=>{let a=wt.join(p,e);dt.writeFileSync(a,s)}),o.success(B("Params types",e,Y,W),{indentLevel:M}))};import Mt from"chokidar";var St=(t,r)=>{let e=null,o=!1,n=null,i=async(...s)=>{o=!0;try{await t(...s)}finally{if(o=!1,n){let p=n;n=null,i(...p)}}};return(...s)=>{e&&clearTimeout(e),e=setTimeout(()=>{if(o){n=s;return}i(...s)},r)}};var _t=(t,r,e)=>{e.info(`${C(t)}`,{event:"watch"});let o=a=>I.some(m=>a.endsWith(m)),n=new Set,i=St(()=>{n.forEach(a=>{st(a),it(a)}),n.clear(),r()},300),s=Mt.watch(t,{ignoreInitial:!0,ignored:(a,m)=>!!m?.isFile()&&!o(a)});s.on("ready",()=>{i(),s.on("all",(a,m)=>{if(o(m)){let c=C(m);e.info(c,{event:a}),n.add(m),i()}})}),s.on("error",a=>{a instanceof Error?e.error(`Watcher error: ${a.message}`):e.error(`Unknown watcher error: ${String(a)}`)});let p=()=>{s.close().then(()=>{e.info("Watcher closed.",{event:"watch"})}).catch(a=>{e.error(`Failed to close watcher: ${a.message}`)})};process.on("SIGINT",p),process.on("SIGTERM",p)};var $t=(t,r,e,o)=>{try{return Pt({baseDir:t,outputPath:r,paramsFileName:e,logger:o}),w}catch(n){return n instanceof Error?o.error(`Failed to generate: ${n.message}`):o.error(`Unknown error occurred during generate: ${String(n)}`),$}},Ct=(t,r,e,o)=>{let n=S(xt.resolve(t)),i=S(xt.resolve(r)),s=typeof e.paramsFile=="string"?e.paramsFile:null;return e.paramsFile!==void 0&&!s?(o.error("Error: --params-file requires a filename."),$):e.watch?(_t(n,()=>{$t(n,i,s,o)},o),w):$t(n,i,s,o)};var Nt=(t,r=yt())=>{let e=new Wt;e.description("Generate RPC client type definitions based on the Next.js path structure.").argument("<baseDir>","Base directory containing Next.js paths for type generation").argument("<outputPath>","Output path for the generated type definitions").option("-w, --watch","Watch mode: regenerate on file changes").option("-p, --params-file [filename]","Generate params types file with specified filename").action(async(o,n,i)=>{try{let s=await Ct(o,n,i,r);i.watch||process.exit(s)}catch(s){r.error(`Unexpected error occurred:${s instanceof Error?s.message:String(s)}`),process.exit($)}}),e.parse(t)};Nt(process.argv);
4
- /*!
5
- * Inspired by pathpida (https://github.com/aspida/pathpida),
6
- * especially the design and UX of its CLI.
7
- */
@@ -1,6 +0,0 @@
1
- export declare const HTTP_METHODS: readonly ["GET", "HEAD", "OPTIONS", "POST", "PUT", "DELETE", "PATCH"];
2
- export declare const OPTIONAL_CATCH_ALL_PREFIX = "_____";
3
- export declare const CATCH_ALL_PREFIX = "___";
4
- export declare const DYNAMIC_PREFIX = "_";
5
- export declare const HTTP_METHODS_EXCLUDE_OPTIONS: ("GET" | "HEAD" | "POST" | "PUT" | "DELETE" | "PATCH")[];
6
- export declare const HTTP_METHOD_FUNC_KEYS: ("$get" | "$head" | "$post" | "$put" | "$delete" | "$patch")[];
@@ -1 +0,0 @@
1
- const T=["GET","HEAD","OPTIONS","POST","PUT","DELETE","PATCH"],t="_____",E="___",e="_",o=T.filter(_=>_!=="OPTIONS"),P=o.map(_=>`$${_.toLowerCase()}`);export{E as CATCH_ALL_PREFIX,e as DYNAMIC_PREFIX,T as HTTP_METHODS,o as HTTP_METHODS_EXCLUDE_OPTIONS,P as HTTP_METHOD_FUNC_KEYS,t as OPTIONAL_CATCH_ALL_PREFIX};