@spoosh/core 0.8.2 → 0.9.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/README.md CHANGED
@@ -50,6 +50,9 @@ import { createClient } from "@spoosh/core";
50
50
  const api = createClient<ApiSchema>({
51
51
  baseUrl: "/api",
52
52
  });
53
+
54
+ // Import body wrappers for explicit serialization
55
+ import { json, form, urlencoded } from "@spoosh/core";
53
56
  ```
54
57
 
55
58
  ### Make API Calls
@@ -79,14 +82,14 @@ const { data: updated } = await api("users/:id").PUT({
79
82
  // DELETE /api/users/123
80
83
  await api("users/:id").DELETE({ params: { id: 123 } });
81
84
 
82
- // POST with file upload (auto FormData when File detected)
85
+ // POST with file upload (using form() wrapper for multipart/form-data)
83
86
  const { data: uploaded } = await api("upload").POST({
84
- body: { file: myFile },
87
+ body: form({ file: myFile }),
85
88
  });
86
89
 
87
- // POST with form data
90
+ // POST with form data (using urlencoded() wrapper)
88
91
  const { data: payment } = await api("payments").POST({
89
- body: { amount: 1000, currency: "usd" },
92
+ body: urlencoded({ amount: 1000, currency: "usd" }),
90
93
  });
91
94
  ```
92
95
 
@@ -172,12 +175,12 @@ const updatedContext = await applyMiddlewares(context, middlewares, "before");
172
175
 
173
176
  ## Schema Types
174
177
 
175
- | Field | Description | Example |
176
- | ------- | --------------------------------------- | ------------------------------------------------ |
177
- | `data` | Response data type | `GET: { data: User[] }` |
178
- | `body` | Request body type (files auto-detected) | `POST: { data: User; body: CreateUserBody }` |
179
- | `query` | Query parameters type | `GET: { data: User[]; query: { page: number } }` |
180
- | `error` | Typed error type | `GET: { data: User; error: ApiError }` |
178
+ | Field | Description | Example |
179
+ | ------- | --------------------- | ------------------------------------------------ |
180
+ | `data` | Response data type | `GET: { data: User[] }` |
181
+ | `body` | Request body type | `POST: { data: User; body: CreateUserBody }` |
182
+ | `query` | Query parameters type | `GET: { data: User[]; query: { page: number } }` |
183
+ | `error` | Typed error type | `GET: { data: User; error: ApiError }` |
181
184
 
182
185
  Path parameters are defined using `:param` syntax in the path key (e.g., `"users/:id"`).
183
186
 
package/dist/index.d.mts CHANGED
@@ -1,4 +1,8 @@
1
1
  type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
2
+ type WriteMethod = "POST" | "PUT" | "PATCH" | "DELETE";
3
+ type Simplify<T> = {
4
+ [K in keyof T]: T[K];
5
+ } & {};
2
6
 
3
7
  type MiddlewarePhase = "before" | "after";
4
8
  type MiddlewareContext<TData = unknown, TError = unknown> = {
@@ -62,6 +66,20 @@ type SpooshOptionsExtra<TData = unknown, TError = unknown> = {
62
66
  middlewares?: SpooshMiddleware<TData, TError>[];
63
67
  };
64
68
 
69
+ type SpooshBody<T = unknown> = {
70
+ readonly __spooshBody: true;
71
+ readonly kind: "form" | "json" | "urlencoded";
72
+ readonly value: T;
73
+ };
74
+ declare function isSpooshBody(value: unknown): value is SpooshBody;
75
+ declare function form<T>(value: T): SpooshBody<T>;
76
+ declare function json<T>(value: T): SpooshBody<T>;
77
+ declare function urlencoded<T>(value: T): SpooshBody<T>;
78
+ declare function resolveRequestBody(rawBody: unknown): {
79
+ body: BodyInit;
80
+ headers?: Record<string, string>;
81
+ } | undefined;
82
+
65
83
  type RetryConfig = {
66
84
  retries?: number | false;
67
85
  retryDelay?: number;
@@ -76,9 +94,9 @@ type BaseRequestOptions$1 = {
76
94
  signal?: AbortSignal;
77
95
  };
78
96
  type BodyOption$1<TBody> = [TBody] extends [never] ? object : undefined extends TBody ? {
79
- body?: Exclude<TBody, undefined>;
97
+ body?: Exclude<TBody, undefined> | SpooshBody<Exclude<TBody, undefined>>;
80
98
  } : {
81
- body: TBody;
99
+ body: TBody | SpooshBody<TBody>;
82
100
  };
83
101
  type QueryOption$1<TQuery> = [TQuery] extends [never] ? object : undefined extends TQuery ? {
84
102
  query?: Exclude<TQuery, undefined>;
@@ -825,10 +843,27 @@ type ExtractParamNames<T extends string> = T extends `${string}:${infer Param}/$
825
843
  * ```
826
844
  */
827
845
  type HasParams<T extends string> = T extends `${string}:${string}` ? true : false;
846
+ /**
847
+ * Extract paths that have GET methods.
848
+ */
849
+ type ReadPaths<TSchema> = {
850
+ [K in keyof TSchema & string]: "GET" extends keyof TSchema[K] ? K : never;
851
+ }[keyof TSchema & string];
852
+ /**
853
+ * Extract paths that have write methods (POST, PUT, PATCH, DELETE).
854
+ */
855
+ type WritePaths<TSchema> = {
856
+ [K in keyof TSchema & string]: Extract<keyof TSchema[K], WriteMethod> extends never ? never : K;
857
+ }[keyof TSchema & string];
858
+ /**
859
+ * Check if a schema path has a GET method.
860
+ */
861
+ type HasReadMethod<TSchema, TPath extends string> = FindMatchingKey<TSchema, TPath> extends infer TKey ? TKey extends keyof TSchema ? "GET" extends keyof TSchema[TKey] ? true : false : false : false;
862
+ /**
863
+ * Check if a schema path has any write methods.
864
+ */
865
+ type HasWriteMethod<TSchema, TPath extends string> = FindMatchingKey<TSchema, TPath> extends infer TKey ? TKey extends keyof TSchema ? WriteMethod extends never ? false : Extract<keyof TSchema[TKey], WriteMethod> extends never ? false : true : false : false;
828
866
 
829
- type Simplify$1<T> = {
830
- [K in keyof T]: T[K];
831
- } & {};
832
867
  type IsNever<T> = [T] extends [never] ? true : false;
833
868
  type EndpointRequestOptions<TEndpoint, TPath extends string> = (IsNever<ExtractBody$1<TEndpoint>> extends true ? object : {
834
869
  body: ExtractBody$1<TEndpoint>;
@@ -837,14 +872,10 @@ type EndpointRequestOptions<TEndpoint, TPath extends string> = (IsNever<ExtractB
837
872
  }) & (HasParams<TPath> extends true ? {
838
873
  params: Record<ExtractParamNames<TPath>, string | number>;
839
874
  } : object);
840
- type EndpointMethodFn<TEndpoint, TPath extends string> = (options?: Simplify$1<EndpointRequestOptions<TEndpoint, TPath>>) => Promise<SpooshResponse<ExtractData<TEndpoint>, unknown, EndpointRequestOptions<TEndpoint, TPath>>>;
841
- type QueryPathMethods<TSchema, TPath extends string> = FindMatchingKey<TSchema, TPath> extends infer TKey ? TKey extends keyof TSchema ? "GET" extends keyof TSchema[TKey] ? Simplify$1<{
875
+ type EndpointMethodFn<TEndpoint, TPath extends string> = (options?: Simplify<EndpointRequestOptions<TEndpoint, TPath>>) => Promise<SpooshResponse<ExtractData<TEndpoint>, unknown, EndpointRequestOptions<TEndpoint, TPath>>>;
876
+ type ReadPathMethods$1<TSchema, TPath extends string> = FindMatchingKey<TSchema, TPath> extends infer TKey ? TKey extends keyof TSchema ? "GET" extends keyof TSchema[TKey] ? Simplify<{
842
877
  GET: EndpointMethodFn<TSchema[TKey]["GET"], TPath>;
843
878
  }> : never : never : never;
844
- type ReadPaths$1<TSchema> = {
845
- [K in keyof TSchema & string]: "GET" extends keyof TSchema[K] ? K : never;
846
- }[keyof TSchema & string];
847
- type HasGetMethod$1<TSchema, TPath extends string> = FindMatchingKey<TSchema, TPath> extends infer TKey ? TKey extends keyof TSchema ? "GET" extends keyof TSchema[TKey] ? true : false : false : false;
848
879
  /**
849
880
  * Schema navigation helper for plugins that need type-safe API schema access.
850
881
  *
@@ -858,7 +889,7 @@ type HasGetMethod$1<TSchema, TPath extends string> = FindMatchingKey<TSchema, TP
858
889
  * ```ts
859
890
  * // Define your plugin's callback type
860
891
  * type MyCallbackFn<TSchema = unknown> = (
861
- * api: QuerySchemaHelper<TSchema>
892
+ * api: ReadSchemaHelper<TSchema>
862
893
  * ) => unknown;
863
894
  *
864
895
  * // Usage in plugin options
@@ -886,25 +917,17 @@ type HasGetMethod$1<TSchema, TPath extends string> = FindMatchingKey<TSchema, TP
886
917
  * });
887
918
  * ```
888
919
  */
889
- type QuerySchemaHelper<TSchema> = <TPath extends ReadPaths$1<TSchema> | (string & {})>(path: TPath) => HasGetMethod$1<TSchema, TPath> extends true ? QueryPathMethods<TSchema, TPath> : never;
890
- type MutationMethod$1 = "POST" | "PUT" | "PATCH" | "DELETE";
891
- type MutationPathMethods<TSchema, TPath extends string> = FindMatchingKey<TSchema, TPath> extends infer TKey ? TKey extends keyof TSchema ? Simplify$1<{
892
- [M in MutationMethod$1 as M extends keyof TSchema[TKey] ? M : never]: M extends keyof TSchema[TKey] ? EndpointMethodFn<TSchema[TKey][M], TPath> : never;
920
+ type ReadSchemaHelper<TSchema> = <TPath extends ReadPaths<TSchema> | (string & {})>(path: TPath) => HasReadMethod<TSchema, TPath> extends true ? ReadPathMethods$1<TSchema, TPath> : never;
921
+ type WritePathMethods$1<TSchema, TPath extends string> = FindMatchingKey<TSchema, TPath> extends infer TKey ? TKey extends keyof TSchema ? Simplify<{
922
+ [M in WriteMethod as M extends keyof TSchema[TKey] ? M : never]: M extends keyof TSchema[TKey] ? EndpointMethodFn<TSchema[TKey][M], TPath> : never;
893
923
  }> : never : never;
894
- type WritePaths$1<TSchema> = {
895
- [K in keyof TSchema & string]: Extract<keyof TSchema[K], MutationMethod$1> extends never ? never : K;
896
- }[keyof TSchema & string];
897
- type HasMutationMethod$1<TSchema, TPath extends string> = FindMatchingKey<TSchema, TPath> extends infer TKey ? TKey extends keyof TSchema ? MutationMethod$1 extends never ? false : Extract<keyof TSchema[TKey], MutationMethod$1> extends never ? false : true : false : false;
898
924
  /**
899
925
  * Schema navigation helper for plugins that need type-safe API schema access for mutations.
900
926
  *
901
- * Similar to QuerySchemaHelper but exposes mutation methods (POST, PUT, PATCH, DELETE).
927
+ * Similar to ReadSchemaHelper but exposes write methods (POST, PUT, PATCH, DELETE).
902
928
  */
903
- type MutationSchemaHelper<TSchema> = <TPath extends WritePaths$1<TSchema> | (string & {})>(path: TPath) => HasMutationMethod$1<TSchema, TPath> extends true ? MutationPathMethods<TSchema, TPath> : never;
929
+ type WriteSchemaHelper<TSchema> = <TPath extends WritePaths<TSchema> | (string & {})>(path: TPath) => HasWriteMethod<TSchema, TPath> extends true ? WritePathMethods$1<TSchema, TPath> : never;
904
930
 
905
- type Simplify<T> = {
906
- [K in keyof T]: T[K];
907
- } & {};
908
931
  /**
909
932
  * Base request options available on all methods.
910
933
  */
@@ -941,9 +964,9 @@ type IsQueryRequired<T> = T extends {
941
964
  * Build the options type for a method.
942
965
  */
943
966
  type BodyOption<T> = [ExtractBody<T>] extends [never] ? {} : IsBodyRequired<T> extends true ? {
944
- body: ExtractBody<T>;
967
+ body: ExtractBody<T> | SpooshBody<ExtractBody<T>>;
945
968
  } : {
946
- body?: ExtractBody<T>;
969
+ body?: ExtractBody<T> | SpooshBody<ExtractBody<T>>;
947
970
  };
948
971
  type QueryOption<T> = [ExtractQuery<T>] extends [never] ? {} : IsQueryRequired<T> extends true ? {
949
972
  query: ExtractQuery<T>;
@@ -1014,47 +1037,23 @@ type SpooshClient<TSchema, TDefaultError = unknown> = <TPath extends SchemaPaths
1014
1037
  type ReadPathMethods<TSchema, TPath extends string, TDefaultError> = FindMatchingKey<TSchema, TPath> extends infer TKey ? TKey extends keyof TSchema ? "GET" extends keyof TSchema[TKey] ? Simplify<{
1015
1038
  GET: MethodFn<TSchema[TKey]["GET"], TDefaultError, TPath>;
1016
1039
  }> : never : never : never;
1017
- /**
1018
- * Check if a schema path has a GET method.
1019
- */
1020
- type HasGetMethod<TSchema, TPath extends string> = FindMatchingKey<TSchema, TPath> extends infer TKey ? TKey extends keyof TSchema ? "GET" extends keyof TSchema[TKey] ? true : false : false : false;
1021
- /**
1022
- * Extract paths that have GET methods.
1023
- */
1024
- type ReadPaths<TSchema> = {
1025
- [K in keyof TSchema & string]: "GET" extends keyof TSchema[K] ? K : never;
1026
- }[keyof TSchema & string];
1027
1040
  /**
1028
1041
  * A read-only API client that only exposes GET methods.
1029
1042
  * Used by useRead and injectRead hooks.
1030
1043
  */
1031
- type ReadClient<TSchema, TDefaultError = unknown> = <TPath extends ReadPaths<TSchema> | (string & {})>(path: TPath) => HasGetMethod<TSchema, TPath> extends true ? ReadPathMethods<TSchema, TPath, TDefaultError> : never;
1032
- /**
1033
- * Mutation methods (non-GET).
1034
- */
1035
- type MutationMethod = "POST" | "PUT" | "PATCH" | "DELETE";
1044
+ type ReadClient<TSchema, TDefaultError = unknown> = <TPath extends ReadPaths<TSchema> | (string & {})>(path: TPath) => HasReadMethod<TSchema, TPath> extends true ? ReadPathMethods<TSchema, TPath, TDefaultError> : never;
1036
1045
  /**
1037
1046
  * Write-only client type that only exposes mutation methods (POST, PUT, PATCH, DELETE).
1038
1047
  * Used by useWrite/injectWrite hooks to ensure only mutation operations are selected.
1039
1048
  */
1040
1049
  type WritePathMethods<TSchema, TPath extends string, TDefaultError> = FindMatchingKey<TSchema, TPath> extends infer TKey ? TKey extends keyof TSchema ? Simplify<{
1041
- [M in MutationMethod as M extends keyof TSchema[TKey] ? M : never]: M extends keyof TSchema[TKey] ? MethodFn<TSchema[TKey][M], TDefaultError, TPath> : never;
1050
+ [M in WriteMethod as M extends keyof TSchema[TKey] ? M : never]: M extends keyof TSchema[TKey] ? MethodFn<TSchema[TKey][M], TDefaultError, TPath> : never;
1042
1051
  }> : never : never;
1043
- /**
1044
- * Check if a schema path has any mutation methods.
1045
- */
1046
- type HasMutationMethod<TSchema, TPath extends string> = FindMatchingKey<TSchema, TPath> extends infer TKey ? TKey extends keyof TSchema ? MutationMethod extends never ? false : Extract<keyof TSchema[TKey], MutationMethod> extends never ? false : true : false : false;
1047
- /**
1048
- * Extract paths that have mutation methods.
1049
- */
1050
- type WritePaths<TSchema> = {
1051
- [K in keyof TSchema & string]: Extract<keyof TSchema[K], MutationMethod> extends never ? never : K;
1052
- }[keyof TSchema & string];
1053
1052
  /**
1054
1053
  * A write-only API client that only exposes mutation methods (POST, PUT, PATCH, DELETE).
1055
1054
  * Used by useWrite and injectWrite hooks.
1056
1055
  */
1057
- type WriteClient<TSchema, TDefaultError = unknown> = <TPath extends WritePaths<TSchema> | (string & {})>(path: TPath) => HasMutationMethod<TSchema, TPath> extends true ? WritePathMethods<TSchema, TPath, TDefaultError> : never;
1056
+ type WriteClient<TSchema, TDefaultError = unknown> = <TPath extends WritePaths<TSchema> | (string & {})>(path: TPath) => HasWriteMethod<TSchema, TPath> extends true ? WritePathMethods<TSchema, TPath, TDefaultError> : never;
1058
1057
 
1059
1058
  type PluginArray = readonly SpooshPlugin<PluginTypeConfig>[];
1060
1059
  interface SpooshConfig<TPlugins extends PluginArray = PluginArray> {
@@ -1334,6 +1333,8 @@ declare function createClient<TSchema, TDefaultError = unknown>(config: SpooshCl
1334
1333
 
1335
1334
  declare function buildUrl(baseUrl: string, path: string[], query?: Record<string, string | number | boolean | undefined>): string;
1336
1335
 
1336
+ declare function __DEV__(): boolean;
1337
+
1337
1338
  /**
1338
1339
  * Generate cache tags from URL path segments.
1339
1340
  * e.g., ['posts', '1'] → ['posts', 'posts/1']
@@ -1608,4 +1609,4 @@ type CreateInfiniteReadOptions<TData, TItem, TError, TRequest> = {
1608
1609
  };
1609
1610
  declare function createInfiniteReadController<TData, TItem, TError, TRequest extends InfiniteRequestOptions = InfiniteRequestOptions>(options: CreateInfiniteReadOptions<TData, TItem, TError, TRequest>): InfiniteReadController<TData, TItem, TError>;
1610
1611
 
1611
- export { type AnyRequestOptions, type ApiSchema, type BuiltInEvents, type CacheEntry, type CacheEntryWithKey, type CapturedCall, type SpooshClientConfig as ClientConfig, type ComputeRequestOptions, type CoreRequestOptionsBase, type CreateInfiniteReadOptions, type CreateOperationOptions, type DataAwareCallback, type DataAwareTransform, type EventEmitter, type ExtractBody$1 as ExtractBody, type ExtractData, type ExtractError, type ExtractMethodOptions, type ExtractParamNames, type ExtractQuery$1 as ExtractQuery, type FetchDirection, type FetchExecutor, type FindMatchingKey, HTTP_METHODS, type HasParams, type HeadersInitOrGetter, type HttpMethod, type HttpMethodKey, type InfiniteReadController, type InfiniteReadState, type InfiniteRequestOptions, type InstanceApiContext, type InstanceApiResolvers, type InstancePluginExecutor, type LifecyclePhase, type MergePluginInstanceApi, type MergePluginOptions, type MergePluginResults, type MethodOptionsMap, type MiddlewareContext, type MiddlewareHandler, type MiddlewarePhase, type MutationSchemaHelper, type OperationController, type OperationState, type OperationType, type PageContext, type PluginAccessor, type PluginArray, type PluginContext, type PluginContextInput, type PluginExecutor, type PluginExportsRegistry, type PluginFactory, type PluginHandler, type PluginLifecycle, type PluginMiddleware, type PluginRegistry, type PluginResolvers, type PluginResponseHandler, type PluginResultResolvers, type PluginTypeConfig, type PluginUpdateHandler, type QuerySchemaHelper, type ReadClient, type RefetchEvent, type RequestOptions$1 as RequestOptions, type ResolveInstanceApi, type ResolveResultTypes, type ResolveSchemaTypes, type ResolveTypes, type ResolverContext, type RetryConfig, type RouteToPath, type SchemaPaths, type SelectedEndpoint, type SelectorFunction, type SelectorResult, Spoosh, type SpooshClient, type SpooshConfig, type SpooshInstance, type SpooshMiddleware, type SpooshOptions, type SpooshOptionsExtra, type SpooshPlugin, type SpooshResponse, type SpooshSchema, type StateManager, type TagMode, type TagOptions, type WriteClient, applyMiddlewares, buildUrl, composeMiddlewares, containsFile, createClient, createEventEmitter, createInfiniteReadController, createInitialState, createMiddleware, createOperationController, createPluginExecutor, createPluginRegistry, createProxyHandler, createSelectorProxy, createStateManager, executeFetch, extractMethodFromSelector, extractPathFromSelector, generateTags, getContentType, isJsonBody, mergeHeaders, objectToFormData, objectToUrlEncoded, resolveHeadersToRecord, resolvePath, resolveTags, setHeaders, sortObjectKeys };
1612
+ export { type AnyRequestOptions, type ApiSchema, type BuiltInEvents, type CacheEntry, type CacheEntryWithKey, type CapturedCall, type SpooshClientConfig as ClientConfig, type ComputeRequestOptions, type CoreRequestOptionsBase, type CreateInfiniteReadOptions, type CreateOperationOptions, type DataAwareCallback, type DataAwareTransform, type EventEmitter, type ExtractBody$1 as ExtractBody, type ExtractData, type ExtractError, type ExtractMethodOptions, type ExtractParamNames, type ExtractQuery$1 as ExtractQuery, type FetchDirection, type FetchExecutor, type FindMatchingKey, HTTP_METHODS, type HasParams, type HasReadMethod, type HasWriteMethod, type HeadersInitOrGetter, type HttpMethod, type HttpMethodKey, type InfiniteReadController, type InfiniteReadState, type InfiniteRequestOptions, type InstanceApiContext, type InstanceApiResolvers, type InstancePluginExecutor, type LifecyclePhase, type MergePluginInstanceApi, type MergePluginOptions, type MergePluginResults, type MethodOptionsMap, type MiddlewareContext, type MiddlewareHandler, type MiddlewarePhase, type OperationController, type OperationState, type OperationType, type PageContext, type PluginAccessor, type PluginArray, type PluginContext, type PluginContextInput, type PluginExecutor, type PluginExportsRegistry, type PluginFactory, type PluginHandler, type PluginLifecycle, type PluginMiddleware, type PluginRegistry, type PluginResolvers, type PluginResponseHandler, type PluginResultResolvers, type PluginTypeConfig, type PluginUpdateHandler, type ReadClient, type ReadPaths, type ReadSchemaHelper, type RefetchEvent, type RequestOptions$1 as RequestOptions, type ResolveInstanceApi, type ResolveResultTypes, type ResolveSchemaTypes, type ResolveTypes, type ResolverContext, type RetryConfig, type RouteToPath, type SchemaPaths, type SelectedEndpoint, type SelectorFunction, type SelectorResult, type Simplify, Spoosh, type SpooshBody, type SpooshClient, type SpooshConfig, type SpooshInstance, type SpooshMiddleware, type SpooshOptions, type SpooshOptionsExtra, type SpooshPlugin, type SpooshResponse, type SpooshSchema, type StateManager, type TagMode, type TagOptions, type WriteClient, type WriteMethod, type WritePaths, type WriteSchemaHelper, __DEV__, applyMiddlewares, buildUrl, composeMiddlewares, containsFile, createClient, createEventEmitter, createInfiniteReadController, createInitialState, createMiddleware, createOperationController, createPluginExecutor, createPluginRegistry, createProxyHandler, createSelectorProxy, createStateManager, executeFetch, extractMethodFromSelector, extractPathFromSelector, form, generateTags, getContentType, isJsonBody, isSpooshBody, json, mergeHeaders, objectToFormData, objectToUrlEncoded, resolveHeadersToRecord, resolvePath, resolveRequestBody, resolveTags, setHeaders, sortObjectKeys, urlencoded };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,8 @@
1
1
  type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
2
+ type WriteMethod = "POST" | "PUT" | "PATCH" | "DELETE";
3
+ type Simplify<T> = {
4
+ [K in keyof T]: T[K];
5
+ } & {};
2
6
 
3
7
  type MiddlewarePhase = "before" | "after";
4
8
  type MiddlewareContext<TData = unknown, TError = unknown> = {
@@ -62,6 +66,20 @@ type SpooshOptionsExtra<TData = unknown, TError = unknown> = {
62
66
  middlewares?: SpooshMiddleware<TData, TError>[];
63
67
  };
64
68
 
69
+ type SpooshBody<T = unknown> = {
70
+ readonly __spooshBody: true;
71
+ readonly kind: "form" | "json" | "urlencoded";
72
+ readonly value: T;
73
+ };
74
+ declare function isSpooshBody(value: unknown): value is SpooshBody;
75
+ declare function form<T>(value: T): SpooshBody<T>;
76
+ declare function json<T>(value: T): SpooshBody<T>;
77
+ declare function urlencoded<T>(value: T): SpooshBody<T>;
78
+ declare function resolveRequestBody(rawBody: unknown): {
79
+ body: BodyInit;
80
+ headers?: Record<string, string>;
81
+ } | undefined;
82
+
65
83
  type RetryConfig = {
66
84
  retries?: number | false;
67
85
  retryDelay?: number;
@@ -76,9 +94,9 @@ type BaseRequestOptions$1 = {
76
94
  signal?: AbortSignal;
77
95
  };
78
96
  type BodyOption$1<TBody> = [TBody] extends [never] ? object : undefined extends TBody ? {
79
- body?: Exclude<TBody, undefined>;
97
+ body?: Exclude<TBody, undefined> | SpooshBody<Exclude<TBody, undefined>>;
80
98
  } : {
81
- body: TBody;
99
+ body: TBody | SpooshBody<TBody>;
82
100
  };
83
101
  type QueryOption$1<TQuery> = [TQuery] extends [never] ? object : undefined extends TQuery ? {
84
102
  query?: Exclude<TQuery, undefined>;
@@ -825,10 +843,27 @@ type ExtractParamNames<T extends string> = T extends `${string}:${infer Param}/$
825
843
  * ```
826
844
  */
827
845
  type HasParams<T extends string> = T extends `${string}:${string}` ? true : false;
846
+ /**
847
+ * Extract paths that have GET methods.
848
+ */
849
+ type ReadPaths<TSchema> = {
850
+ [K in keyof TSchema & string]: "GET" extends keyof TSchema[K] ? K : never;
851
+ }[keyof TSchema & string];
852
+ /**
853
+ * Extract paths that have write methods (POST, PUT, PATCH, DELETE).
854
+ */
855
+ type WritePaths<TSchema> = {
856
+ [K in keyof TSchema & string]: Extract<keyof TSchema[K], WriteMethod> extends never ? never : K;
857
+ }[keyof TSchema & string];
858
+ /**
859
+ * Check if a schema path has a GET method.
860
+ */
861
+ type HasReadMethod<TSchema, TPath extends string> = FindMatchingKey<TSchema, TPath> extends infer TKey ? TKey extends keyof TSchema ? "GET" extends keyof TSchema[TKey] ? true : false : false : false;
862
+ /**
863
+ * Check if a schema path has any write methods.
864
+ */
865
+ type HasWriteMethod<TSchema, TPath extends string> = FindMatchingKey<TSchema, TPath> extends infer TKey ? TKey extends keyof TSchema ? WriteMethod extends never ? false : Extract<keyof TSchema[TKey], WriteMethod> extends never ? false : true : false : false;
828
866
 
829
- type Simplify$1<T> = {
830
- [K in keyof T]: T[K];
831
- } & {};
832
867
  type IsNever<T> = [T] extends [never] ? true : false;
833
868
  type EndpointRequestOptions<TEndpoint, TPath extends string> = (IsNever<ExtractBody$1<TEndpoint>> extends true ? object : {
834
869
  body: ExtractBody$1<TEndpoint>;
@@ -837,14 +872,10 @@ type EndpointRequestOptions<TEndpoint, TPath extends string> = (IsNever<ExtractB
837
872
  }) & (HasParams<TPath> extends true ? {
838
873
  params: Record<ExtractParamNames<TPath>, string | number>;
839
874
  } : object);
840
- type EndpointMethodFn<TEndpoint, TPath extends string> = (options?: Simplify$1<EndpointRequestOptions<TEndpoint, TPath>>) => Promise<SpooshResponse<ExtractData<TEndpoint>, unknown, EndpointRequestOptions<TEndpoint, TPath>>>;
841
- type QueryPathMethods<TSchema, TPath extends string> = FindMatchingKey<TSchema, TPath> extends infer TKey ? TKey extends keyof TSchema ? "GET" extends keyof TSchema[TKey] ? Simplify$1<{
875
+ type EndpointMethodFn<TEndpoint, TPath extends string> = (options?: Simplify<EndpointRequestOptions<TEndpoint, TPath>>) => Promise<SpooshResponse<ExtractData<TEndpoint>, unknown, EndpointRequestOptions<TEndpoint, TPath>>>;
876
+ type ReadPathMethods$1<TSchema, TPath extends string> = FindMatchingKey<TSchema, TPath> extends infer TKey ? TKey extends keyof TSchema ? "GET" extends keyof TSchema[TKey] ? Simplify<{
842
877
  GET: EndpointMethodFn<TSchema[TKey]["GET"], TPath>;
843
878
  }> : never : never : never;
844
- type ReadPaths$1<TSchema> = {
845
- [K in keyof TSchema & string]: "GET" extends keyof TSchema[K] ? K : never;
846
- }[keyof TSchema & string];
847
- type HasGetMethod$1<TSchema, TPath extends string> = FindMatchingKey<TSchema, TPath> extends infer TKey ? TKey extends keyof TSchema ? "GET" extends keyof TSchema[TKey] ? true : false : false : false;
848
879
  /**
849
880
  * Schema navigation helper for plugins that need type-safe API schema access.
850
881
  *
@@ -858,7 +889,7 @@ type HasGetMethod$1<TSchema, TPath extends string> = FindMatchingKey<TSchema, TP
858
889
  * ```ts
859
890
  * // Define your plugin's callback type
860
891
  * type MyCallbackFn<TSchema = unknown> = (
861
- * api: QuerySchemaHelper<TSchema>
892
+ * api: ReadSchemaHelper<TSchema>
862
893
  * ) => unknown;
863
894
  *
864
895
  * // Usage in plugin options
@@ -886,25 +917,17 @@ type HasGetMethod$1<TSchema, TPath extends string> = FindMatchingKey<TSchema, TP
886
917
  * });
887
918
  * ```
888
919
  */
889
- type QuerySchemaHelper<TSchema> = <TPath extends ReadPaths$1<TSchema> | (string & {})>(path: TPath) => HasGetMethod$1<TSchema, TPath> extends true ? QueryPathMethods<TSchema, TPath> : never;
890
- type MutationMethod$1 = "POST" | "PUT" | "PATCH" | "DELETE";
891
- type MutationPathMethods<TSchema, TPath extends string> = FindMatchingKey<TSchema, TPath> extends infer TKey ? TKey extends keyof TSchema ? Simplify$1<{
892
- [M in MutationMethod$1 as M extends keyof TSchema[TKey] ? M : never]: M extends keyof TSchema[TKey] ? EndpointMethodFn<TSchema[TKey][M], TPath> : never;
920
+ type ReadSchemaHelper<TSchema> = <TPath extends ReadPaths<TSchema> | (string & {})>(path: TPath) => HasReadMethod<TSchema, TPath> extends true ? ReadPathMethods$1<TSchema, TPath> : never;
921
+ type WritePathMethods$1<TSchema, TPath extends string> = FindMatchingKey<TSchema, TPath> extends infer TKey ? TKey extends keyof TSchema ? Simplify<{
922
+ [M in WriteMethod as M extends keyof TSchema[TKey] ? M : never]: M extends keyof TSchema[TKey] ? EndpointMethodFn<TSchema[TKey][M], TPath> : never;
893
923
  }> : never : never;
894
- type WritePaths$1<TSchema> = {
895
- [K in keyof TSchema & string]: Extract<keyof TSchema[K], MutationMethod$1> extends never ? never : K;
896
- }[keyof TSchema & string];
897
- type HasMutationMethod$1<TSchema, TPath extends string> = FindMatchingKey<TSchema, TPath> extends infer TKey ? TKey extends keyof TSchema ? MutationMethod$1 extends never ? false : Extract<keyof TSchema[TKey], MutationMethod$1> extends never ? false : true : false : false;
898
924
  /**
899
925
  * Schema navigation helper for plugins that need type-safe API schema access for mutations.
900
926
  *
901
- * Similar to QuerySchemaHelper but exposes mutation methods (POST, PUT, PATCH, DELETE).
927
+ * Similar to ReadSchemaHelper but exposes write methods (POST, PUT, PATCH, DELETE).
902
928
  */
903
- type MutationSchemaHelper<TSchema> = <TPath extends WritePaths$1<TSchema> | (string & {})>(path: TPath) => HasMutationMethod$1<TSchema, TPath> extends true ? MutationPathMethods<TSchema, TPath> : never;
929
+ type WriteSchemaHelper<TSchema> = <TPath extends WritePaths<TSchema> | (string & {})>(path: TPath) => HasWriteMethod<TSchema, TPath> extends true ? WritePathMethods$1<TSchema, TPath> : never;
904
930
 
905
- type Simplify<T> = {
906
- [K in keyof T]: T[K];
907
- } & {};
908
931
  /**
909
932
  * Base request options available on all methods.
910
933
  */
@@ -941,9 +964,9 @@ type IsQueryRequired<T> = T extends {
941
964
  * Build the options type for a method.
942
965
  */
943
966
  type BodyOption<T> = [ExtractBody<T>] extends [never] ? {} : IsBodyRequired<T> extends true ? {
944
- body: ExtractBody<T>;
967
+ body: ExtractBody<T> | SpooshBody<ExtractBody<T>>;
945
968
  } : {
946
- body?: ExtractBody<T>;
969
+ body?: ExtractBody<T> | SpooshBody<ExtractBody<T>>;
947
970
  };
948
971
  type QueryOption<T> = [ExtractQuery<T>] extends [never] ? {} : IsQueryRequired<T> extends true ? {
949
972
  query: ExtractQuery<T>;
@@ -1014,47 +1037,23 @@ type SpooshClient<TSchema, TDefaultError = unknown> = <TPath extends SchemaPaths
1014
1037
  type ReadPathMethods<TSchema, TPath extends string, TDefaultError> = FindMatchingKey<TSchema, TPath> extends infer TKey ? TKey extends keyof TSchema ? "GET" extends keyof TSchema[TKey] ? Simplify<{
1015
1038
  GET: MethodFn<TSchema[TKey]["GET"], TDefaultError, TPath>;
1016
1039
  }> : never : never : never;
1017
- /**
1018
- * Check if a schema path has a GET method.
1019
- */
1020
- type HasGetMethod<TSchema, TPath extends string> = FindMatchingKey<TSchema, TPath> extends infer TKey ? TKey extends keyof TSchema ? "GET" extends keyof TSchema[TKey] ? true : false : false : false;
1021
- /**
1022
- * Extract paths that have GET methods.
1023
- */
1024
- type ReadPaths<TSchema> = {
1025
- [K in keyof TSchema & string]: "GET" extends keyof TSchema[K] ? K : never;
1026
- }[keyof TSchema & string];
1027
1040
  /**
1028
1041
  * A read-only API client that only exposes GET methods.
1029
1042
  * Used by useRead and injectRead hooks.
1030
1043
  */
1031
- type ReadClient<TSchema, TDefaultError = unknown> = <TPath extends ReadPaths<TSchema> | (string & {})>(path: TPath) => HasGetMethod<TSchema, TPath> extends true ? ReadPathMethods<TSchema, TPath, TDefaultError> : never;
1032
- /**
1033
- * Mutation methods (non-GET).
1034
- */
1035
- type MutationMethod = "POST" | "PUT" | "PATCH" | "DELETE";
1044
+ type ReadClient<TSchema, TDefaultError = unknown> = <TPath extends ReadPaths<TSchema> | (string & {})>(path: TPath) => HasReadMethod<TSchema, TPath> extends true ? ReadPathMethods<TSchema, TPath, TDefaultError> : never;
1036
1045
  /**
1037
1046
  * Write-only client type that only exposes mutation methods (POST, PUT, PATCH, DELETE).
1038
1047
  * Used by useWrite/injectWrite hooks to ensure only mutation operations are selected.
1039
1048
  */
1040
1049
  type WritePathMethods<TSchema, TPath extends string, TDefaultError> = FindMatchingKey<TSchema, TPath> extends infer TKey ? TKey extends keyof TSchema ? Simplify<{
1041
- [M in MutationMethod as M extends keyof TSchema[TKey] ? M : never]: M extends keyof TSchema[TKey] ? MethodFn<TSchema[TKey][M], TDefaultError, TPath> : never;
1050
+ [M in WriteMethod as M extends keyof TSchema[TKey] ? M : never]: M extends keyof TSchema[TKey] ? MethodFn<TSchema[TKey][M], TDefaultError, TPath> : never;
1042
1051
  }> : never : never;
1043
- /**
1044
- * Check if a schema path has any mutation methods.
1045
- */
1046
- type HasMutationMethod<TSchema, TPath extends string> = FindMatchingKey<TSchema, TPath> extends infer TKey ? TKey extends keyof TSchema ? MutationMethod extends never ? false : Extract<keyof TSchema[TKey], MutationMethod> extends never ? false : true : false : false;
1047
- /**
1048
- * Extract paths that have mutation methods.
1049
- */
1050
- type WritePaths<TSchema> = {
1051
- [K in keyof TSchema & string]: Extract<keyof TSchema[K], MutationMethod> extends never ? never : K;
1052
- }[keyof TSchema & string];
1053
1052
  /**
1054
1053
  * A write-only API client that only exposes mutation methods (POST, PUT, PATCH, DELETE).
1055
1054
  * Used by useWrite and injectWrite hooks.
1056
1055
  */
1057
- type WriteClient<TSchema, TDefaultError = unknown> = <TPath extends WritePaths<TSchema> | (string & {})>(path: TPath) => HasMutationMethod<TSchema, TPath> extends true ? WritePathMethods<TSchema, TPath, TDefaultError> : never;
1056
+ type WriteClient<TSchema, TDefaultError = unknown> = <TPath extends WritePaths<TSchema> | (string & {})>(path: TPath) => HasWriteMethod<TSchema, TPath> extends true ? WritePathMethods<TSchema, TPath, TDefaultError> : never;
1058
1057
 
1059
1058
  type PluginArray = readonly SpooshPlugin<PluginTypeConfig>[];
1060
1059
  interface SpooshConfig<TPlugins extends PluginArray = PluginArray> {
@@ -1334,6 +1333,8 @@ declare function createClient<TSchema, TDefaultError = unknown>(config: SpooshCl
1334
1333
 
1335
1334
  declare function buildUrl(baseUrl: string, path: string[], query?: Record<string, string | number | boolean | undefined>): string;
1336
1335
 
1336
+ declare function __DEV__(): boolean;
1337
+
1337
1338
  /**
1338
1339
  * Generate cache tags from URL path segments.
1339
1340
  * e.g., ['posts', '1'] → ['posts', 'posts/1']
@@ -1608,4 +1609,4 @@ type CreateInfiniteReadOptions<TData, TItem, TError, TRequest> = {
1608
1609
  };
1609
1610
  declare function createInfiniteReadController<TData, TItem, TError, TRequest extends InfiniteRequestOptions = InfiniteRequestOptions>(options: CreateInfiniteReadOptions<TData, TItem, TError, TRequest>): InfiniteReadController<TData, TItem, TError>;
1610
1611
 
1611
- export { type AnyRequestOptions, type ApiSchema, type BuiltInEvents, type CacheEntry, type CacheEntryWithKey, type CapturedCall, type SpooshClientConfig as ClientConfig, type ComputeRequestOptions, type CoreRequestOptionsBase, type CreateInfiniteReadOptions, type CreateOperationOptions, type DataAwareCallback, type DataAwareTransform, type EventEmitter, type ExtractBody$1 as ExtractBody, type ExtractData, type ExtractError, type ExtractMethodOptions, type ExtractParamNames, type ExtractQuery$1 as ExtractQuery, type FetchDirection, type FetchExecutor, type FindMatchingKey, HTTP_METHODS, type HasParams, type HeadersInitOrGetter, type HttpMethod, type HttpMethodKey, type InfiniteReadController, type InfiniteReadState, type InfiniteRequestOptions, type InstanceApiContext, type InstanceApiResolvers, type InstancePluginExecutor, type LifecyclePhase, type MergePluginInstanceApi, type MergePluginOptions, type MergePluginResults, type MethodOptionsMap, type MiddlewareContext, type MiddlewareHandler, type MiddlewarePhase, type MutationSchemaHelper, type OperationController, type OperationState, type OperationType, type PageContext, type PluginAccessor, type PluginArray, type PluginContext, type PluginContextInput, type PluginExecutor, type PluginExportsRegistry, type PluginFactory, type PluginHandler, type PluginLifecycle, type PluginMiddleware, type PluginRegistry, type PluginResolvers, type PluginResponseHandler, type PluginResultResolvers, type PluginTypeConfig, type PluginUpdateHandler, type QuerySchemaHelper, type ReadClient, type RefetchEvent, type RequestOptions$1 as RequestOptions, type ResolveInstanceApi, type ResolveResultTypes, type ResolveSchemaTypes, type ResolveTypes, type ResolverContext, type RetryConfig, type RouteToPath, type SchemaPaths, type SelectedEndpoint, type SelectorFunction, type SelectorResult, Spoosh, type SpooshClient, type SpooshConfig, type SpooshInstance, type SpooshMiddleware, type SpooshOptions, type SpooshOptionsExtra, type SpooshPlugin, type SpooshResponse, type SpooshSchema, type StateManager, type TagMode, type TagOptions, type WriteClient, applyMiddlewares, buildUrl, composeMiddlewares, containsFile, createClient, createEventEmitter, createInfiniteReadController, createInitialState, createMiddleware, createOperationController, createPluginExecutor, createPluginRegistry, createProxyHandler, createSelectorProxy, createStateManager, executeFetch, extractMethodFromSelector, extractPathFromSelector, generateTags, getContentType, isJsonBody, mergeHeaders, objectToFormData, objectToUrlEncoded, resolveHeadersToRecord, resolvePath, resolveTags, setHeaders, sortObjectKeys };
1612
+ export { type AnyRequestOptions, type ApiSchema, type BuiltInEvents, type CacheEntry, type CacheEntryWithKey, type CapturedCall, type SpooshClientConfig as ClientConfig, type ComputeRequestOptions, type CoreRequestOptionsBase, type CreateInfiniteReadOptions, type CreateOperationOptions, type DataAwareCallback, type DataAwareTransform, type EventEmitter, type ExtractBody$1 as ExtractBody, type ExtractData, type ExtractError, type ExtractMethodOptions, type ExtractParamNames, type ExtractQuery$1 as ExtractQuery, type FetchDirection, type FetchExecutor, type FindMatchingKey, HTTP_METHODS, type HasParams, type HasReadMethod, type HasWriteMethod, type HeadersInitOrGetter, type HttpMethod, type HttpMethodKey, type InfiniteReadController, type InfiniteReadState, type InfiniteRequestOptions, type InstanceApiContext, type InstanceApiResolvers, type InstancePluginExecutor, type LifecyclePhase, type MergePluginInstanceApi, type MergePluginOptions, type MergePluginResults, type MethodOptionsMap, type MiddlewareContext, type MiddlewareHandler, type MiddlewarePhase, type OperationController, type OperationState, type OperationType, type PageContext, type PluginAccessor, type PluginArray, type PluginContext, type PluginContextInput, type PluginExecutor, type PluginExportsRegistry, type PluginFactory, type PluginHandler, type PluginLifecycle, type PluginMiddleware, type PluginRegistry, type PluginResolvers, type PluginResponseHandler, type PluginResultResolvers, type PluginTypeConfig, type PluginUpdateHandler, type ReadClient, type ReadPaths, type ReadSchemaHelper, type RefetchEvent, type RequestOptions$1 as RequestOptions, type ResolveInstanceApi, type ResolveResultTypes, type ResolveSchemaTypes, type ResolveTypes, type ResolverContext, type RetryConfig, type RouteToPath, type SchemaPaths, type SelectedEndpoint, type SelectorFunction, type SelectorResult, type Simplify, Spoosh, type SpooshBody, type SpooshClient, type SpooshConfig, type SpooshInstance, type SpooshMiddleware, type SpooshOptions, type SpooshOptionsExtra, type SpooshPlugin, type SpooshResponse, type SpooshSchema, type StateManager, type TagMode, type TagOptions, type WriteClient, type WriteMethod, type WritePaths, type WriteSchemaHelper, __DEV__, applyMiddlewares, buildUrl, composeMiddlewares, containsFile, createClient, createEventEmitter, createInfiniteReadController, createInitialState, createMiddleware, createOperationController, createPluginExecutor, createPluginRegistry, createProxyHandler, createSelectorProxy, createStateManager, executeFetch, extractMethodFromSelector, extractPathFromSelector, form, generateTags, getContentType, isJsonBody, isSpooshBody, json, mergeHeaders, objectToFormData, objectToUrlEncoded, resolveHeadersToRecord, resolvePath, resolveRequestBody, resolveTags, setHeaders, sortObjectKeys, urlencoded };
package/dist/index.js CHANGED
@@ -22,6 +22,7 @@ var src_exports = {};
22
22
  __export(src_exports, {
23
23
  HTTP_METHODS: () => HTTP_METHODS,
24
24
  Spoosh: () => Spoosh,
25
+ __DEV__: () => __DEV__,
25
26
  applyMiddlewares: () => applyMiddlewares,
26
27
  buildUrl: () => buildUrl,
27
28
  composeMiddlewares: () => composeMiddlewares,
@@ -40,17 +41,22 @@ __export(src_exports, {
40
41
  executeFetch: () => executeFetch,
41
42
  extractMethodFromSelector: () => extractMethodFromSelector,
42
43
  extractPathFromSelector: () => extractPathFromSelector,
44
+ form: () => form,
43
45
  generateTags: () => generateTags,
44
46
  getContentType: () => getContentType,
45
47
  isJsonBody: () => isJsonBody,
48
+ isSpooshBody: () => isSpooshBody,
49
+ json: () => json,
46
50
  mergeHeaders: () => mergeHeaders,
47
51
  objectToFormData: () => objectToFormData,
48
52
  objectToUrlEncoded: () => objectToUrlEncoded,
49
53
  resolveHeadersToRecord: () => resolveHeadersToRecord,
50
54
  resolvePath: () => resolvePath,
55
+ resolveRequestBody: () => resolveRequestBody,
51
56
  resolveTags: () => resolveTags,
52
57
  setHeaders: () => setHeaders,
53
- sortObjectKeys: () => sortObjectKeys
58
+ sortObjectKeys: () => sortObjectKeys,
59
+ urlencoded: () => urlencoded
54
60
  });
55
61
  module.exports = __toCommonJS(src_exports);
56
62
 
@@ -99,6 +105,11 @@ function buildUrl(baseUrl, path, query) {
99
105
  return `${cleanBase}${pathStr}${queryStr ? `?${queryStr}` : ""}`;
100
106
  }
101
107
 
108
+ // src/utils/env.ts
109
+ function __DEV__() {
110
+ return typeof process !== "undefined" && true;
111
+ }
112
+
102
113
  // src/utils/generateTags.ts
103
114
  function generateTags(path) {
104
115
  return path.map((_, i) => path.slice(0, i + 1).join("/"));
@@ -122,7 +133,6 @@ function isJsonBody(body) {
122
133
  if (body instanceof ReadableStream) return false;
123
134
  if (typeof body === "string") return false;
124
135
  if (typeof body === "object") {
125
- if (containsFile(body)) return false;
126
136
  return true;
127
137
  }
128
138
  return false;
@@ -246,6 +256,67 @@ function sortObjectKeys(obj, seen = /* @__PURE__ */ new WeakSet()) {
246
256
  );
247
257
  }
248
258
 
259
+ // src/utils/body.ts
260
+ function isSpooshBody(value) {
261
+ return typeof value === "object" && value !== null && "__spooshBody" in value && value.__spooshBody === true;
262
+ }
263
+ function form(value) {
264
+ return Object.freeze({
265
+ __spooshBody: true,
266
+ kind: "form",
267
+ value
268
+ });
269
+ }
270
+ function json(value) {
271
+ return Object.freeze({
272
+ __spooshBody: true,
273
+ kind: "json",
274
+ value
275
+ });
276
+ }
277
+ function urlencoded(value) {
278
+ return Object.freeze({
279
+ __spooshBody: true,
280
+ kind: "urlencoded",
281
+ value
282
+ });
283
+ }
284
+ function resolveRequestBody(rawBody) {
285
+ if (rawBody === void 0 || rawBody === null) {
286
+ return void 0;
287
+ }
288
+ if (isSpooshBody(rawBody)) {
289
+ switch (rawBody.kind) {
290
+ case "form":
291
+ return {
292
+ body: objectToFormData(rawBody.value)
293
+ };
294
+ case "json":
295
+ return {
296
+ body: JSON.stringify(rawBody.value),
297
+ headers: { "Content-Type": "application/json" }
298
+ };
299
+ case "urlencoded":
300
+ return {
301
+ body: objectToUrlEncoded(rawBody.value),
302
+ headers: { "Content-Type": "application/x-www-form-urlencoded" }
303
+ };
304
+ }
305
+ }
306
+ if (isJsonBody(rawBody)) {
307
+ if (__DEV__() && containsFile(rawBody)) {
308
+ console.warn(
309
+ "[spoosh] Plain object body contains File/Blob. Use form() wrapper for multipart upload."
310
+ );
311
+ }
312
+ return {
313
+ body: JSON.stringify(rawBody),
314
+ headers: { "Content-Type": "application/json" }
315
+ };
316
+ }
317
+ return { body: rawBody };
318
+ }
319
+
249
320
  // src/utils/path-utils.ts
250
321
  function resolveTagMode(mode, path) {
251
322
  switch (mode) {
@@ -391,25 +462,15 @@ async function executeCoreFetch(config) {
391
462
  fetchInit.signal = requestOptions.signal;
392
463
  }
393
464
  if (requestOptions?.body !== void 0) {
394
- const contentType = getContentType(headers);
395
- if (contentType?.includes("application/x-www-form-urlencoded")) {
396
- fetchInit.body = objectToUrlEncoded(
397
- requestOptions.body
398
- );
399
- } else if (contentType?.includes("multipart/form-data") || containsFile(requestOptions.body)) {
400
- fetchInit.body = objectToFormData(
401
- requestOptions.body
402
- );
403
- } else if (isJsonBody(requestOptions.body)) {
404
- fetchInit.body = JSON.stringify(requestOptions.body);
405
- headers = await mergeHeaders(headers, {
406
- "Content-Type": "application/json"
407
- });
408
- if (headers) {
409
- fetchInit.headers = headers;
465
+ const resolved = resolveRequestBody(requestOptions.body);
466
+ if (resolved) {
467
+ fetchInit.body = resolved.body;
468
+ if (resolved.headers) {
469
+ headers = await mergeHeaders(headers, resolved.headers);
470
+ if (headers) {
471
+ fetchInit.headers = headers;
472
+ }
410
473
  }
411
- } else {
412
- fetchInit.body = requestOptions.body;
413
474
  }
414
475
  }
415
476
  let lastError;
package/dist/index.mjs CHANGED
@@ -43,6 +43,11 @@ function buildUrl(baseUrl, path, query) {
43
43
  return `${cleanBase}${pathStr}${queryStr ? `?${queryStr}` : ""}`;
44
44
  }
45
45
 
46
+ // src/utils/env.ts
47
+ function __DEV__() {
48
+ return typeof process !== "undefined" && true;
49
+ }
50
+
46
51
  // src/utils/generateTags.ts
47
52
  function generateTags(path) {
48
53
  return path.map((_, i) => path.slice(0, i + 1).join("/"));
@@ -66,7 +71,6 @@ function isJsonBody(body) {
66
71
  if (body instanceof ReadableStream) return false;
67
72
  if (typeof body === "string") return false;
68
73
  if (typeof body === "object") {
69
- if (containsFile(body)) return false;
70
74
  return true;
71
75
  }
72
76
  return false;
@@ -190,6 +194,67 @@ function sortObjectKeys(obj, seen = /* @__PURE__ */ new WeakSet()) {
190
194
  );
191
195
  }
192
196
 
197
+ // src/utils/body.ts
198
+ function isSpooshBody(value) {
199
+ return typeof value === "object" && value !== null && "__spooshBody" in value && value.__spooshBody === true;
200
+ }
201
+ function form(value) {
202
+ return Object.freeze({
203
+ __spooshBody: true,
204
+ kind: "form",
205
+ value
206
+ });
207
+ }
208
+ function json(value) {
209
+ return Object.freeze({
210
+ __spooshBody: true,
211
+ kind: "json",
212
+ value
213
+ });
214
+ }
215
+ function urlencoded(value) {
216
+ return Object.freeze({
217
+ __spooshBody: true,
218
+ kind: "urlencoded",
219
+ value
220
+ });
221
+ }
222
+ function resolveRequestBody(rawBody) {
223
+ if (rawBody === void 0 || rawBody === null) {
224
+ return void 0;
225
+ }
226
+ if (isSpooshBody(rawBody)) {
227
+ switch (rawBody.kind) {
228
+ case "form":
229
+ return {
230
+ body: objectToFormData(rawBody.value)
231
+ };
232
+ case "json":
233
+ return {
234
+ body: JSON.stringify(rawBody.value),
235
+ headers: { "Content-Type": "application/json" }
236
+ };
237
+ case "urlencoded":
238
+ return {
239
+ body: objectToUrlEncoded(rawBody.value),
240
+ headers: { "Content-Type": "application/x-www-form-urlencoded" }
241
+ };
242
+ }
243
+ }
244
+ if (isJsonBody(rawBody)) {
245
+ if (__DEV__() && containsFile(rawBody)) {
246
+ console.warn(
247
+ "[spoosh] Plain object body contains File/Blob. Use form() wrapper for multipart upload."
248
+ );
249
+ }
250
+ return {
251
+ body: JSON.stringify(rawBody),
252
+ headers: { "Content-Type": "application/json" }
253
+ };
254
+ }
255
+ return { body: rawBody };
256
+ }
257
+
193
258
  // src/utils/path-utils.ts
194
259
  function resolveTagMode(mode, path) {
195
260
  switch (mode) {
@@ -335,25 +400,15 @@ async function executeCoreFetch(config) {
335
400
  fetchInit.signal = requestOptions.signal;
336
401
  }
337
402
  if (requestOptions?.body !== void 0) {
338
- const contentType = getContentType(headers);
339
- if (contentType?.includes("application/x-www-form-urlencoded")) {
340
- fetchInit.body = objectToUrlEncoded(
341
- requestOptions.body
342
- );
343
- } else if (contentType?.includes("multipart/form-data") || containsFile(requestOptions.body)) {
344
- fetchInit.body = objectToFormData(
345
- requestOptions.body
346
- );
347
- } else if (isJsonBody(requestOptions.body)) {
348
- fetchInit.body = JSON.stringify(requestOptions.body);
349
- headers = await mergeHeaders(headers, {
350
- "Content-Type": "application/json"
351
- });
352
- if (headers) {
353
- fetchInit.headers = headers;
403
+ const resolved = resolveRequestBody(requestOptions.body);
404
+ if (resolved) {
405
+ fetchInit.body = resolved.body;
406
+ if (resolved.headers) {
407
+ headers = await mergeHeaders(headers, resolved.headers);
408
+ if (headers) {
409
+ fetchInit.headers = headers;
410
+ }
354
411
  }
355
- } else {
356
- fetchInit.body = requestOptions.body;
357
412
  }
358
413
  }
359
414
  let lastError;
@@ -1607,6 +1662,7 @@ function createInfiniteReadController(options) {
1607
1662
  export {
1608
1663
  HTTP_METHODS,
1609
1664
  Spoosh,
1665
+ __DEV__,
1610
1666
  applyMiddlewares,
1611
1667
  buildUrl,
1612
1668
  composeMiddlewares,
@@ -1625,15 +1681,20 @@ export {
1625
1681
  executeFetch,
1626
1682
  extractMethodFromSelector,
1627
1683
  extractPathFromSelector,
1684
+ form,
1628
1685
  generateTags,
1629
1686
  getContentType,
1630
1687
  isJsonBody,
1688
+ isSpooshBody,
1689
+ json,
1631
1690
  mergeHeaders,
1632
1691
  objectToFormData,
1633
1692
  objectToUrlEncoded,
1634
1693
  resolveHeadersToRecord,
1635
1694
  resolvePath,
1695
+ resolveRequestBody,
1636
1696
  resolveTags,
1637
1697
  setHeaders,
1638
- sortObjectKeys
1698
+ sortObjectKeys,
1699
+ urlencoded
1639
1700
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spoosh/core",
3
- "version": "0.8.2",
3
+ "version": "0.9.0",
4
4
  "license": "MIT",
5
5
  "description": "Type-safe API client with plugin middleware system",
6
6
  "keywords": [