@zap-studio/fetch 0.4.4 → 0.4.6

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/CHANGELOG.md CHANGED
@@ -1,138 +1,128 @@
1
1
  # @zap-studio/fetch
2
2
 
3
- ## 0.4.4
3
+ ## 0.4.6
4
4
 
5
5
  ### Patch Changes
6
6
 
7
- - 59a8d71: Fix request body handling for JSON payloads.
7
+ - 5ea3d3b: Updated dependencies.
8
+ - @zap-studio/validation@0.3.1
8
9
 
9
- - Accept JSON values (including arrays) in `ExtendedRequestInit.body`
10
- - Auto-stringify plain JSON body values regardless of whether a response schema is provided
11
- - Set `Content-Type: application/json` only when auto-stringifying and no explicit content type is set
12
10
 
13
- ## 0.4.3
11
+ ## 0.4.5
14
12
 
15
- ### Patch Changes
13
+ ### Dependencies
16
14
 
17
- - e4542bb: Refine `standardValidate` typings so the return type depends on the `throwOnError` flag, and update `@zap-studio/fetch` to integrate with the new overloads while preserving its boolean configuration API.
18
- - Updated dependencies [e4542bb]
19
- - @zap-studio/validation@0.2.1
15
+ - f75b984: Updated dependency `@zap-studio/validation` to `0.3.0`.
20
16
 
21
- ## 0.4.2
17
+ ## 0.4.4
22
18
 
23
- ### Patch Changes
19
+ ### Fixed
24
20
 
25
- - Updated dependencies [2de8183]
26
- - @zap-studio/validation@0.2.0
21
+ - 59a8d71: Fixed JSON request body handling.
22
+ - Accept JSON values (including arrays) in `ExtendedRequestInit.body`.
23
+ - Auto-stringify plain JSON body values even when no response schema is provided.
24
+ - Set `Content-Type: application/json` only when auto-stringifying and no explicit content type is set.
27
25
 
28
- ## 0.4.1
26
+ ## 0.4.3
29
27
 
30
- ### Patch Changes
28
+ ### Changed
31
29
 
32
- - 447dbda: Extract shared Standard Schema validation utilities into `@zap-studio/validation` and update `@zap-studio/fetch` to depend on them.
33
- - Updated dependencies [447dbda]
34
- - @zap-studio/validation@0.1.0
30
+ - e4542bb: Refined `standardValidate` typings so return types depend on `throwOnError`, and updated `@zap-studio/fetch` integration while preserving the existing boolean configuration API.
35
31
 
36
- ## 0.4.0
32
+ ### Dependencies
37
33
 
38
- ### Minor Changes
34
+ - e4542bb: Updated dependency `@zap-studio/validation` to `0.2.1`.
39
35
 
40
- - 69057cd: Expose fetch defaults constants and utility helpers as public exports.
36
+ ## 0.4.2
41
37
 
42
- ## 0.3.1
38
+ ### Dependencies
43
39
 
44
- ### Patch Changes
40
+ - 2de8183: Updated dependency `@zap-studio/validation` to `0.2.0`.
45
41
 
46
- - 9919f63: Add discriminated return types based on `throwOnValidationError` option
42
+ ## 0.4.1
47
43
 
48
- The return type of `$fetch` and `api.*` methods now correctly narrows based on the `throwOnValidationError` option:
44
+ ### Changed
49
45
 
50
- - When `throwOnValidationError: true` (default) or unspecified: returns `Promise<TSchema>` (the validated data directly)
51
- - When `throwOnValidationError: false`: returns `Promise<StandardSchemaV1.Result<TSchema>>` (the result object with `value` or `issues`)
46
+ - 447dbda: Switched shared Standard Schema validation utilities to `@zap-studio/validation`.
52
47
 
53
- This improves type safety by eliminating the need for manual type narrowing when using the default behavior.
48
+ ### Dependencies
54
49
 
55
- ## 0.3.0
50
+ - 447dbda: Updated dependency `@zap-studio/validation` to `0.1.0`.
56
51
 
57
- ### Minor Changes
52
+ ## 0.4.0
58
53
 
59
- - 659621c: Add `searchParams` option to `createFetch` to allow factory-level default query/search parameters. Per-request `searchParams` continue to override factory defaults.
54
+ ### Added
60
55
 
61
- ## 0.2.2
56
+ - 69057cd: Exposed fetch defaults constants and utility helpers as public exports.
62
57
 
63
- ### Patch Changes
58
+ ## 0.3.1
64
59
 
65
- - 5c3abbf: Prepare JSR publish and isolatedDeclarations support with new explicit types for `$Fetch` and `ApiMethods`
60
+ ### Changed
66
61
 
67
- ## 0.2.1
62
+ - 9919f63: Added discriminated return types based on `throwOnValidationError`.
63
+ - `throwOnValidationError: true` (default) returns `Promise<TSchema>`.
64
+ - `throwOnValidationError: false` returns `Promise<StandardSchemaV1.Result<TSchema>>`.
65
+ - This improves type safety and removes manual narrowing in default usage.
68
66
 
69
- ### Patch Changes
67
+ ## 0.3.0
70
68
 
71
- - 82bac5c: Replace regex-based slash trimming with more efficient string manipulation functions for URL normalization
69
+ ### Added
72
70
 
73
- ## 0.2.0
71
+ - 659621c: Added `searchParams` support in `createFetch` for factory-level default query parameters.
72
+ - Per-request `searchParams` still override factory defaults.
74
73
 
75
- ### Minor Changes
74
+ ## 0.2.2
75
+
76
+ ### Changed
76
77
 
77
- - 78afb76: ### Standard Schema Support
78
+ - 5c3abbf: Prepared JSR publish and `isolatedDeclarations` support with explicit `$Fetch` and `ApiMethods` types.
78
79
 
79
- Migrated from Zod-only validation to **Standard Schema v1** specification, enabling support for multiple validation libraries:
80
+ ## 0.2.1
80
81
 
81
- - Zod
82
- - Valibot
83
- - ArkType
84
- - Any Standard Schema compliant library
82
+ ### Changed
85
83
 
86
- ### New Features
84
+ - 82bac5c: Replaced regex-based slash trimming with more efficient string manipulation for URL normalization.
87
85
 
88
- - **Factory Pattern**: `createFetch()` for creating pre-configured fetch instances with `baseURL`, default `headers`, and error handling options
89
- - **Smart URL Handling**: Absolute URLs bypass `baseURL` configuration
90
- - **Auto JSON Body**: Automatic `JSON.stringify()` and `Content-Type` header when using schemas with request bodies
86
+ ## 0.2.0
91
87
 
92
- ### Breaking Changes
88
+ ### Changed
93
89
 
94
- - Schema validation now requires Standard Schema compliant libraries (Zod 3.23+, Valibot 1.0+, ArkType 2.0+)
95
- - Internal file structure reorganized (affects deep imports if any were used)
96
- - `FetchError` constructor signature changed: now requires `(message, response)`
90
+ - 78afb76: Migrated from Zod-only validation to Standard Schema v1 for broader validator compatibility.
91
+ - Supported libraries include Zod, Valibot, ArkType, and other Standard Schema-compliant validators.
97
92
 
98
- ### Migration Guide
93
+ ### Added
99
94
 
100
- ```typescript
101
- // Before (Zod-only)
102
- import { $fetch } from "@zap-studio/fetch";
103
- import { z } from "zod";
95
+ - 78afb76: Added `createFetch()` factory pattern for pre-configured instances.
96
+ - 78afb76: Added smart URL behavior so absolute URLs bypass `baseURL`.
97
+ - 78afb76: Added automatic JSON body serialization and `Content-Type` handling for schema-based requests.
104
98
 
105
- // After (Standard Schema - works the same with Zod!)
106
- import { $fetch } from "@zap-studio/fetch";
107
- import { z } from "zod"; // Zod 3.23+ is Standard Schema compliant
99
+ ### Breaking
108
100
 
109
- // Or use other libraries
110
- import * as v from "valibot";
111
- import { type } from "arktype";
112
- ```
101
+ - 78afb76: Standard Schema-compliant validator libraries are now required (for example Zod 3.23+, Valibot 1.0+, ArkType 2.0+).
102
+ - 78afb76: Internal file structure was reorganized (affects deep imports).
103
+ - 78afb76: `FetchError` constructor now requires `(message, response)`.
113
104
 
114
105
  ## 0.1.2
115
106
 
116
- ### Patch Changes
107
+ ### Changed
117
108
 
118
- - 69c2b21: Change `safeFetch` to `$fetch` syntax and make sure `safeFetch` can also be used for legacy
109
+ - 69c2b21: Renamed `safeFetch` to `$fetch` while preserving `safeFetch` compatibility for legacy usage.
119
110
 
120
111
  ## 0.1.1
121
112
 
122
- ### Patch Changes
113
+ ### Changed
123
114
 
124
- - 5f1812b: Change files field in package.json to distribute only necessary artifacts
115
+ - 5f1812b: Updated `files` in `package.json` to publish only required artifacts.
125
116
 
126
117
  ## 0.1.0
127
118
 
128
- ### Minor Changes
129
-
130
- - 1644006: Comprehensive description of the initial release features including:
119
+ ### Added
131
120
 
132
- - Type-safe HTTP requests with Zod validation
133
- - Automatic content-type handling
134
- - Multiple response type support
135
- - Convenient API methods (GET, POST, PUT, PATCH, DELETE)
136
- - Flexible error handling
137
- - Custom FetchError class
138
- - Full TypeScript support
121
+ - 1644006: Initial release of `@zap-studio/fetch`.
122
+ - Type-safe HTTP requests with Zod validation.
123
+ - Automatic content-type handling.
124
+ - Multiple response type support.
125
+ - API methods for GET, POST, PUT, PATCH, and DELETE.
126
+ - Flexible error handling.
127
+ - Custom `FetchError` class.
128
+ - Full TypeScript support.
package/dist/utils.mjs CHANGED
@@ -156,8 +156,8 @@ async function fetchInternal(resource, schema, options, defaults) {
156
156
  if (throwOnFetchError && !response.ok) throw new FetchError(`HTTP ${response.status}: ${response.statusText}`, response);
157
157
  if (schema) {
158
158
  const raw = await response.json();
159
- if (throwOnValidationError) return standardValidate(schema, raw, true);
160
- return standardValidate(schema, raw, false);
159
+ if (throwOnValidationError) return standardValidate(schema, raw, { throwOnError: true });
160
+ return standardValidate(schema, raw, { throwOnError: false });
161
161
  }
162
162
  return response;
163
163
  }
@@ -1 +1 @@
1
- {"version":3,"file":"utils.mjs","names":["hash","url: string"],"sources":["../src/utils.ts"],"sourcesContent":["import type { StandardSchemaV1 } from \"@standard-schema/spec\";\nimport { isStandardSchema, standardValidate } from \"@zap-studio/validation\";\nimport { FetchError } from \"./errors\";\nimport type {\n $Fetch,\n ExtendedRequestInit,\n FetchDefaults,\n SearchParams,\n} from \"./types\";\n\n/**\n * Merges two HeadersInit objects, with the second one taking precedence\n *\n * @example\n * const baseHeaders = { \"Authorization\": \"Bearer token\", \"Content-Type\": \"application/json\" };\n * const overrideHeaders = { \"Content-Type\": \"application/xml\", \"X-Custom-Header\": \"value\" };\n *\n * const merged = mergeHeaders(baseHeaders, overrideHeaders);\n *\n * // Resulting headers:\n * // {\n * // \"Authorization\": \"Bearer token\",\n * // \"Content-Type\": \"application/xml\",\n * // \"X-Custom-Header\": \"value\"\n * // }\n */\nexport function mergeHeaders(\n base: HeadersInit | undefined,\n override: HeadersInit | undefined\n): Headers | undefined {\n if (!(base || override)) {\n return;\n }\n\n const merged = new Headers(base);\n if (override) {\n const overrideHeaders = new Headers(override);\n for (const [key, value] of overrideHeaders.entries()) {\n merged.set(key, value);\n }\n }\n return merged;\n}\n\n/**\n * Removes trailing slashes from a string\n */\nfunction trimTrailingSlashes(str: string): string {\n let end = str.length;\n while (end > 0 && str[end - 1] === \"/\") {\n end -= 1;\n }\n return str.slice(0, end);\n}\n\n/**\n * Removes leading slashes from a string\n */\nfunction trimLeadingSlashes(str: string): string {\n let start = 0;\n while (start < str.length && str[start] === \"/\") {\n start += 1;\n }\n return str.slice(start);\n}\n\nconst ABSOLUTE_URL_PATTERN = /^(?:https?:)?\\/\\//i;\n\n/**\n * Checks if a URL is absolute (starts with http://, https://, or //)\n */\nfunction isAbsoluteURL(url: string): boolean {\n return ABSOLUTE_URL_PATTERN.test(url);\n}\n\n/**\n * Normalizes search parameters into a URLSearchParams object.\n */\nfunction normalizeSearchParams(\n input: SearchParams | undefined\n): URLSearchParams {\n if (input === undefined || input === null) {\n return new URLSearchParams();\n }\n\n if (input instanceof URLSearchParams) {\n return new URLSearchParams(input);\n }\n\n if (typeof input === \"string\") {\n return new URLSearchParams(input);\n }\n\n if (Array.isArray(input)) {\n return new URLSearchParams(input);\n }\n\n const params = new URLSearchParams();\n for (const [k, v] of Object.entries(input)) {\n params.set(k, v);\n }\n\n return params;\n}\n\n/**\n * Parses a URL string into its path, query, and hash components.\n * Handles both absolute URLs (using URL constructor) and relative paths.\n */\nfunction parseUrlComponents(url: string): {\n pathOnly: string;\n existingQuery: string;\n hash: string;\n} {\n // Try parsing as absolute URL first\n if (isAbsoluteURL(url)) {\n try {\n const parsed = new URL(url);\n const pathOnly = `${parsed.origin}${parsed.pathname}`;\n // parsed.search includes the leading \"?\", so strip it\n const existingQuery = parsed.search.startsWith(\"?\")\n ? parsed.search.slice(1)\n : parsed.search;\n // URL constructor normalizes away empty hash, if original URL had # at end, preserve it\n let hash = parsed.hash;\n if (hash === \"\" && url.endsWith(\"#\")) {\n hash = \"#\";\n }\n return { pathOnly, existingQuery, hash };\n } catch {\n // Fall through to manual parsing if URL constructor fails\n }\n }\n\n // Manual parsing for relative URLs or if URL constructor failed\n let hash = \"\";\n let urlWithoutHash = url;\n const hashIndex = url.indexOf(\"#\");\n if (hashIndex !== -1) {\n hash = url.slice(hashIndex);\n urlWithoutHash = url.slice(0, hashIndex);\n }\n\n // Find the first \"?\" to split path and query\n const queryIndex = urlWithoutHash.indexOf(\"?\");\n if (queryIndex === -1) {\n return { pathOnly: urlWithoutHash, existingQuery: \"\", hash };\n }\n\n const pathOnly = urlWithoutHash.slice(0, queryIndex);\n const existingQuery = urlWithoutHash.slice(queryIndex + 1);\n return { pathOnly, existingQuery, hash };\n}\n\n/**\n * Builds a URL with merged search parameters from factory defaults,\n * request options, and existing query parameters.\n *\n * This function takes a base URL, factory search parameters,\n * and request search parameters, and combines them into a single URL.\n *\n * Priority (highest to lowest):\n * 1. Request search parameters (highest priority, overwrites all others)\n * 2. Existing query parameters from the URL (overwrites factory defaults)\n * 3. Factory search parameters (lowest priority, overwritten by all others)\n *\n * If no search parameters are provided, the URL will not have a query string.\n * Any trailing hash/fragment is preserved.\n */\nfunction buildUrlWithMergedSearchParams(\n url: string,\n factorySearch: FetchDefaults[\"searchParams\"] | undefined,\n requestSearch: ExtendedRequestInit[\"searchParams\"] | undefined\n): string {\n const { pathOnly, existingQuery, hash } = parseUrlComponents(url);\n\n // Early return if no search params to merge\n const hasSearchParams = factorySearch ?? requestSearch ?? existingQuery;\n if (!hasSearchParams) {\n return url;\n }\n\n const mergedParams = new URLSearchParams();\n\n const resourceParams = new URLSearchParams(existingQuery);\n const factoryParams = normalizeSearchParams(factorySearch);\n const reqParams = normalizeSearchParams(requestSearch);\n\n for (const [k, v] of factoryParams.entries()) {\n mergedParams.set(k, v);\n }\n\n for (const [k, v] of resourceParams.entries()) {\n mergedParams.set(k, v);\n }\n\n for (const [k, v] of reqParams.entries()) {\n mergedParams.set(k, v);\n }\n\n const queryString = mergedParams.toString();\n if (queryString) {\n return `${pathOnly}?${queryString}${hash}`;\n }\n\n return `${pathOnly}${hash}`;\n}\n\n/**\n * Internal fetch implementation used by both $fetch and createFetch\n */\nexport async function fetchInternal(\n resource: string,\n schema: StandardSchemaV1 | undefined,\n options: ExtendedRequestInit | undefined,\n defaults: FetchDefaults\n): Promise<unknown> {\n const {\n throwOnValidationError = defaults.throwOnValidationError,\n throwOnFetchError = defaults.throwOnFetchError,\n headers: requestHeaders,\n searchParams: requestSearchParams,\n ...rest\n } = options || {};\n\n const mergedHeaders = mergeHeaders(defaults.headers, requestHeaders);\n const init = { ...rest, headers: mergedHeaders } as RequestInit;\n\n const isBodyInit =\n typeof init.body === \"string\" ||\n init.body instanceof FormData ||\n init.body instanceof URLSearchParams ||\n init.body instanceof Blob ||\n init.body instanceof ArrayBuffer ||\n ArrayBuffer.isView(init.body) ||\n init.body instanceof ReadableStream;\n\n // Auto-stringify plain JSON bodies and set default Content-Type\n if (init.body != null && !isBodyInit) {\n init.body = JSON.stringify(init.body);\n\n const existingHeaders = new Headers(init.headers);\n if (!existingHeaders.has(\"Content-Type\")) {\n existingHeaders.set(\"Content-Type\", \"application/json\");\n }\n\n init.headers = existingHeaders;\n }\n\n // For absolute URLs, ignore baseURL entirely\n let url: string;\n if (isAbsoluteURL(resource)) {\n url = resource;\n } else {\n // Normalize URL by avoiding double slashes between baseURL and resource\n const base = trimTrailingSlashes(defaults.baseURL);\n const path = trimLeadingSlashes(resource);\n url = base ? `${base}/${path}` : resource;\n }\n\n // Merge query/search params\n url = buildUrlWithMergedSearchParams(\n url,\n defaults.searchParams,\n requestSearchParams\n );\n\n const response = await fetch(url, init);\n\n if (throwOnFetchError && !response.ok) {\n throw new FetchError(\n `HTTP ${response.status}: ${response.statusText}`,\n response\n );\n }\n\n // For json with schema, validate\n if (schema) {\n const raw = await response.json();\n if (throwOnValidationError) {\n return standardValidate(schema, raw, true);\n }\n return standardValidate(schema, raw, false);\n }\n\n // No validation, return raw response data\n return response;\n}\n\n/**\n * Creates an HTTP method helper bound to a fetch function\n */\nexport function createMethod<TFetch extends $Fetch>(\n fetchFn: TFetch,\n method: string\n): $Fetch {\n function methodFetch<TSchema extends StandardSchemaV1>(\n resource: string,\n schema: TSchema,\n options: Omit<ExtendedRequestInit<false>, \"method\">\n ): Promise<StandardSchemaV1.Result<StandardSchemaV1.InferOutput<TSchema>>>;\n\n function methodFetch<TSchema extends StandardSchemaV1>(\n resource: string,\n schema: TSchema,\n options?: Omit<ExtendedRequestInit<true | undefined>, \"method\">\n ): Promise<StandardSchemaV1.InferOutput<TSchema>>;\n\n function methodFetch(\n resource: string,\n options?: Omit<ExtendedRequestInit, \"method\">\n ): Promise<Response>;\n\n function methodFetch(\n resource: string,\n schemaOrOptions?: StandardSchemaV1 | Omit<ExtendedRequestInit, \"method\">,\n optionsOrUndefined?: Omit<ExtendedRequestInit, \"method\">\n ): Promise<unknown> {\n const [schema, options] = isStandardSchema(schemaOrOptions)\n ? [schemaOrOptions, optionsOrUndefined]\n : [undefined, schemaOrOptions];\n\n return (fetchFn as (...args: unknown[]) => Promise<unknown>)(\n resource,\n schema,\n { ...options, method }\n );\n }\n\n return methodFetch;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"utils.mjs","names":["hash","url: string"],"sources":["../src/utils.ts"],"sourcesContent":["import type { StandardSchemaV1 } from \"@standard-schema/spec\";\nimport { isStandardSchema, standardValidate } from \"@zap-studio/validation\";\nimport { FetchError } from \"./errors\";\nimport type {\n $Fetch,\n ExtendedRequestInit,\n FetchDefaults,\n SearchParams,\n} from \"./types\";\n\n/**\n * Merges two HeadersInit objects, with the second one taking precedence\n *\n * @example\n * const baseHeaders = { \"Authorization\": \"Bearer token\", \"Content-Type\": \"application/json\" };\n * const overrideHeaders = { \"Content-Type\": \"application/xml\", \"X-Custom-Header\": \"value\" };\n *\n * const merged = mergeHeaders(baseHeaders, overrideHeaders);\n *\n * // Resulting headers:\n * // {\n * // \"Authorization\": \"Bearer token\",\n * // \"Content-Type\": \"application/xml\",\n * // \"X-Custom-Header\": \"value\"\n * // }\n */\nexport function mergeHeaders(\n base: HeadersInit | undefined,\n override: HeadersInit | undefined\n): Headers | undefined {\n if (!(base || override)) {\n return;\n }\n\n const merged = new Headers(base);\n if (override) {\n const overrideHeaders = new Headers(override);\n for (const [key, value] of overrideHeaders.entries()) {\n merged.set(key, value);\n }\n }\n return merged;\n}\n\n/**\n * Removes trailing slashes from a string\n */\nfunction trimTrailingSlashes(str: string): string {\n let end = str.length;\n while (end > 0 && str[end - 1] === \"/\") {\n end -= 1;\n }\n return str.slice(0, end);\n}\n\n/**\n * Removes leading slashes from a string\n */\nfunction trimLeadingSlashes(str: string): string {\n let start = 0;\n while (start < str.length && str[start] === \"/\") {\n start += 1;\n }\n return str.slice(start);\n}\n\nconst ABSOLUTE_URL_PATTERN = /^(?:https?:)?\\/\\//i;\n\n/**\n * Checks if a URL is absolute (starts with http://, https://, or //)\n */\nfunction isAbsoluteURL(url: string): boolean {\n return ABSOLUTE_URL_PATTERN.test(url);\n}\n\n/**\n * Normalizes search parameters into a URLSearchParams object.\n */\nfunction normalizeSearchParams(\n input: SearchParams | undefined\n): URLSearchParams {\n if (input === undefined || input === null) {\n return new URLSearchParams();\n }\n\n if (input instanceof URLSearchParams) {\n return new URLSearchParams(input);\n }\n\n if (typeof input === \"string\") {\n return new URLSearchParams(input);\n }\n\n if (Array.isArray(input)) {\n return new URLSearchParams(input);\n }\n\n const params = new URLSearchParams();\n for (const [k, v] of Object.entries(input)) {\n params.set(k, v);\n }\n\n return params;\n}\n\n/**\n * Parses a URL string into its path, query, and hash components.\n * Handles both absolute URLs (using URL constructor) and relative paths.\n */\nfunction parseUrlComponents(url: string): {\n pathOnly: string;\n existingQuery: string;\n hash: string;\n} {\n // Try parsing as absolute URL first\n if (isAbsoluteURL(url)) {\n try {\n const parsed = new URL(url);\n const pathOnly = `${parsed.origin}${parsed.pathname}`;\n // parsed.search includes the leading \"?\", so strip it\n const existingQuery = parsed.search.startsWith(\"?\")\n ? parsed.search.slice(1)\n : parsed.search;\n // URL constructor normalizes away empty hash, if original URL had # at end, preserve it\n let hash = parsed.hash;\n if (hash === \"\" && url.endsWith(\"#\")) {\n hash = \"#\";\n }\n return { pathOnly, existingQuery, hash };\n } catch {\n // Fall through to manual parsing if URL constructor fails\n }\n }\n\n // Manual parsing for relative URLs or if URL constructor failed\n let hash = \"\";\n let urlWithoutHash = url;\n const hashIndex = url.indexOf(\"#\");\n if (hashIndex !== -1) {\n hash = url.slice(hashIndex);\n urlWithoutHash = url.slice(0, hashIndex);\n }\n\n // Find the first \"?\" to split path and query\n const queryIndex = urlWithoutHash.indexOf(\"?\");\n if (queryIndex === -1) {\n return { pathOnly: urlWithoutHash, existingQuery: \"\", hash };\n }\n\n const pathOnly = urlWithoutHash.slice(0, queryIndex);\n const existingQuery = urlWithoutHash.slice(queryIndex + 1);\n return { pathOnly, existingQuery, hash };\n}\n\n/**\n * Builds a URL with merged search parameters from factory defaults,\n * request options, and existing query parameters.\n *\n * This function takes a base URL, factory search parameters,\n * and request search parameters, and combines them into a single URL.\n *\n * Priority (highest to lowest):\n * 1. Request search parameters (highest priority, overwrites all others)\n * 2. Existing query parameters from the URL (overwrites factory defaults)\n * 3. Factory search parameters (lowest priority, overwritten by all others)\n *\n * If no search parameters are provided, the URL will not have a query string.\n * Any trailing hash/fragment is preserved.\n */\nfunction buildUrlWithMergedSearchParams(\n url: string,\n factorySearch: FetchDefaults[\"searchParams\"] | undefined,\n requestSearch: ExtendedRequestInit[\"searchParams\"] | undefined\n): string {\n const { pathOnly, existingQuery, hash } = parseUrlComponents(url);\n\n // Early return if no search params to merge\n const hasSearchParams = factorySearch ?? requestSearch ?? existingQuery;\n if (!hasSearchParams) {\n return url;\n }\n\n const mergedParams = new URLSearchParams();\n\n const resourceParams = new URLSearchParams(existingQuery);\n const factoryParams = normalizeSearchParams(factorySearch);\n const reqParams = normalizeSearchParams(requestSearch);\n\n for (const [k, v] of factoryParams.entries()) {\n mergedParams.set(k, v);\n }\n\n for (const [k, v] of resourceParams.entries()) {\n mergedParams.set(k, v);\n }\n\n for (const [k, v] of reqParams.entries()) {\n mergedParams.set(k, v);\n }\n\n const queryString = mergedParams.toString();\n if (queryString) {\n return `${pathOnly}?${queryString}${hash}`;\n }\n\n return `${pathOnly}${hash}`;\n}\n\n/**\n * Internal fetch implementation used by both $fetch and createFetch\n */\nexport async function fetchInternal(\n resource: string,\n schema: StandardSchemaV1 | undefined,\n options: ExtendedRequestInit | undefined,\n defaults: FetchDefaults\n): Promise<unknown> {\n const {\n throwOnValidationError = defaults.throwOnValidationError,\n throwOnFetchError = defaults.throwOnFetchError,\n headers: requestHeaders,\n searchParams: requestSearchParams,\n ...rest\n } = options || {};\n\n const mergedHeaders = mergeHeaders(defaults.headers, requestHeaders);\n const init = { ...rest, headers: mergedHeaders } as RequestInit;\n\n const isBodyInit =\n typeof init.body === \"string\" ||\n init.body instanceof FormData ||\n init.body instanceof URLSearchParams ||\n init.body instanceof Blob ||\n init.body instanceof ArrayBuffer ||\n ArrayBuffer.isView(init.body) ||\n init.body instanceof ReadableStream;\n\n // Auto-stringify plain JSON bodies and set default Content-Type\n if (init.body != null && !isBodyInit) {\n init.body = JSON.stringify(init.body);\n\n const existingHeaders = new Headers(init.headers);\n if (!existingHeaders.has(\"Content-Type\")) {\n existingHeaders.set(\"Content-Type\", \"application/json\");\n }\n\n init.headers = existingHeaders;\n }\n\n // For absolute URLs, ignore baseURL entirely\n let url: string;\n if (isAbsoluteURL(resource)) {\n url = resource;\n } else {\n // Normalize URL by avoiding double slashes between baseURL and resource\n const base = trimTrailingSlashes(defaults.baseURL);\n const path = trimLeadingSlashes(resource);\n url = base ? `${base}/${path}` : resource;\n }\n\n // Merge query/search params\n url = buildUrlWithMergedSearchParams(\n url,\n defaults.searchParams,\n requestSearchParams\n );\n\n const response = await fetch(url, init);\n\n if (throwOnFetchError && !response.ok) {\n throw new FetchError(\n `HTTP ${response.status}: ${response.statusText}`,\n response\n );\n }\n\n // For json with schema, validate\n if (schema) {\n const raw = await response.json();\n if (throwOnValidationError) {\n return standardValidate(schema, raw, { throwOnError: true });\n }\n return standardValidate(schema, raw, { throwOnError: false });\n }\n\n // No validation, return raw response data\n return response;\n}\n\n/**\n * Creates an HTTP method helper bound to a fetch function\n */\nexport function createMethod<TFetch extends $Fetch>(\n fetchFn: TFetch,\n method: string\n): $Fetch {\n function methodFetch<TSchema extends StandardSchemaV1>(\n resource: string,\n schema: TSchema,\n options: Omit<ExtendedRequestInit<false>, \"method\">\n ): Promise<StandardSchemaV1.Result<StandardSchemaV1.InferOutput<TSchema>>>;\n\n function methodFetch<TSchema extends StandardSchemaV1>(\n resource: string,\n schema: TSchema,\n options?: Omit<ExtendedRequestInit<true | undefined>, \"method\">\n ): Promise<StandardSchemaV1.InferOutput<TSchema>>;\n\n function methodFetch(\n resource: string,\n options?: Omit<ExtendedRequestInit, \"method\">\n ): Promise<Response>;\n\n function methodFetch(\n resource: string,\n schemaOrOptions?: StandardSchemaV1 | Omit<ExtendedRequestInit, \"method\">,\n optionsOrUndefined?: Omit<ExtendedRequestInit, \"method\">\n ): Promise<unknown> {\n const [schema, options] = isStandardSchema(schemaOrOptions)\n ? [schemaOrOptions, optionsOrUndefined]\n : [undefined, schemaOrOptions];\n\n return (fetchFn as (...args: unknown[]) => Promise<unknown>)(\n resource,\n schema,\n { ...options, method }\n );\n }\n\n return methodFetch;\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zap-studio/fetch",
3
- "version": "0.4.4",
3
+ "version": "0.4.6",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -39,7 +39,7 @@
39
39
  ],
40
40
  "dependencies": {
41
41
  "@standard-schema/spec": "^1.1.0",
42
- "@zap-studio/validation": "0.2.1"
42
+ "@zap-studio/validation": "0.3.1"
43
43
  },
44
44
  "devDependencies": {
45
45
  "@types/node": "^25.0.2",
@@ -50,9 +50,9 @@
50
50
  "valibot": "^1.2.0",
51
51
  "vitest": "^4.0.18",
52
52
  "zod": "^4.2.0",
53
+ "@zap-studio/vitest-config": "0.0.0",
53
54
  "@zap-studio/typescript-config": "0.0.0",
54
- "@zap-studio/tsdown-config": "0.0.0",
55
- "@zap-studio/vitest-config": "0.0.0"
55
+ "@zap-studio/tsdown-config": "0.0.0"
56
56
  },
57
57
  "exports": {
58
58
  ".": "./dist/index.mjs",
@@ -67,7 +67,7 @@
67
67
  "types": "./dist/index.d.mts",
68
68
  "scripts": {
69
69
  "build": "tsdown --config tsdown.config.ts",
70
- "check": "tsc --noEmit",
70
+ "typecheck": "tsc --noEmit",
71
71
  "test": "vitest run",
72
72
  "test:watch": "vitest --watch"
73
73
  }