@spoosh/core 0.9.0 → 0.9.1

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.d.mts CHANGED
@@ -124,7 +124,7 @@ type MethodOptionsMap<TQueryOptions = object, TMutationOptions = object> = {
124
124
  DELETE: TMutationOptions;
125
125
  };
126
126
  type ExtractMethodOptions<TOptionsMap, TMethod extends HttpMethod> = TOptionsMap extends MethodOptionsMap<infer TQuery, infer TMutation> ? TMethod extends "GET" ? TQuery : TMutation : TOptionsMap;
127
- type FetchExecutor<TOptions = SpooshOptions, TRequestOptions = AnyRequestOptions> = <TData, TError>(baseUrl: string, path: string[], method: HttpMethod, defaultOptions: TOptions, requestOptions?: TRequestOptions, nextTags?: boolean) => Promise<SpooshResponse<TData, TError>>;
127
+ type FetchExecutor<TOptions = SpooshOptions, TRequestOptions = AnyRequestOptions> = <TData, TError>(baseUrl: string, path: string[], method: HttpMethod, defaultOptions: TOptions, requestOptions?: TRequestOptions, nextTags?: boolean, tagPath?: string[]) => Promise<SpooshResponse<TData, TError>>;
128
128
  type TypedParamsOption<TParamNames extends string> = [TParamNames] extends [
129
129
  never
130
130
  ] ? object : {
@@ -251,6 +251,11 @@ type PluginContext<TData = unknown, TError = unknown> = {
251
251
  readonly requestTimestamp: number;
252
252
  /** Unique identifier for the hook instance. Persists across queryKey changes within the same hook. */
253
253
  readonly hookId?: string;
254
+ /**
255
+ * Prefix to strip from tags. Configured at the Spoosh instance level.
256
+ * Plugins can use this to normalize tags before emitting events.
257
+ */
258
+ readonly stripTagPrefix?: string;
254
259
  requestOptions: AnyRequestOptions;
255
260
  state: OperationState<TData, TError>;
256
261
  response?: SpooshResponse<TData, TError>;
@@ -596,7 +601,10 @@ type PluginExecutor = {
596
601
  /** Creates a full PluginContext with plugins accessor injected */
597
602
  createContext: <TData, TError>(input: PluginContextInput<TData, TError>) => PluginContext<TData, TError>;
598
603
  };
599
- declare function createPluginExecutor(initialPlugins?: SpooshPlugin[]): PluginExecutor;
604
+ type PluginExecutorOptions = {
605
+ stripTagPrefix?: string;
606
+ };
607
+ declare function createPluginExecutor(initialPlugins?: SpooshPlugin[], options?: PluginExecutorOptions): PluginExecutor;
600
608
 
601
609
  /**
602
610
  * Resolves plugin option types based on the full context.
@@ -863,6 +871,33 @@ type HasReadMethod<TSchema, TPath extends string> = FindMatchingKey<TSchema, TPa
863
871
  * Check if a schema path has any write methods.
864
872
  */
865
873
  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;
874
+ type NormalizePrefix<T extends string> = T extends `/${infer Rest}` ? NormalizePrefix<Rest> : T extends `${infer Rest}/` ? NormalizePrefix<Rest> : T;
875
+ type StripPrefixFromPath<TPath extends string, TPrefix extends string> = TPath extends TPrefix ? "" : TPath extends `${TPrefix}/${infer Rest}` ? Rest : TPath;
876
+ /**
877
+ * Strips a prefix from all path keys in a schema.
878
+ * Works with any schema (Elysia, Hono, or manual).
879
+ *
880
+ * @example
881
+ * ```ts
882
+ * type FullSchema = {
883
+ * "api": { GET: { data: string } };
884
+ * "api/users": { GET: { data: User[] } };
885
+ * "api/posts/:id": { GET: { data: Post } };
886
+ * "health": { GET: { data: { status: string } } };
887
+ * };
888
+ *
889
+ * type ApiSchema = StripPrefix<FullSchema, "api">;
890
+ * // {
891
+ * // "": { GET: { data: string } };
892
+ * // "users": { GET: { data: User[] } };
893
+ * // "posts/:id": { GET: { data: Post } };
894
+ * // "health": { GET: { data: { status: string } } };
895
+ * // }
896
+ * ```
897
+ */
898
+ type StripPrefix<TSchema, TPrefix extends string> = TPrefix extends "" ? TSchema : {
899
+ [K in keyof TSchema as K extends string ? StripPrefixFromPath<K, NormalizePrefix<TPrefix>> : K]: TSchema[K];
900
+ };
866
901
 
867
902
  type IsNever<T> = [T] extends [never] ? true : false;
868
903
  type EndpointRequestOptions<TEndpoint, TPath extends string> = (IsNever<ExtractBody$1<TEndpoint>> extends true ? object : {
@@ -1056,6 +1091,33 @@ type WritePathMethods<TSchema, TPath extends string, TDefaultError> = FindMatchi
1056
1091
  type WriteClient<TSchema, TDefaultError = unknown> = <TPath extends WritePaths<TSchema> | (string & {})>(path: TPath) => HasWriteMethod<TSchema, TPath> extends true ? WritePathMethods<TSchema, TPath, TDefaultError> : never;
1057
1092
 
1058
1093
  type PluginArray = readonly SpooshPlugin<PluginTypeConfig>[];
1094
+ /**
1095
+ * Configuration options for Spoosh runtime behavior.
1096
+ */
1097
+ type SpooshConfigOptions = {
1098
+ /**
1099
+ * Prefix to strip from tag generation.
1100
+ *
1101
+ * URL prefix stripping always auto-detects from baseUrl.
1102
+ * This option only affects tag generation for cache invalidation.
1103
+ *
1104
+ * - `undefined`: Auto-detect from baseUrl (default)
1105
+ * - `string`: Explicit prefix to strip from tags
1106
+ *
1107
+ * @example
1108
+ * ```ts
1109
+ * // Default: auto-detect from baseUrl
1110
+ * // baseUrl="/api", schema="api/posts" → tags: ["posts"]
1111
+ * new Spoosh<Schema>('https://localhost:3000/api')
1112
+ *
1113
+ * // Explicit prefix (when baseUrl doesn't have it)
1114
+ * // baseUrl="/", schema="api/v1/posts" → tags: ["posts"]
1115
+ * new Spoosh<Schema>('http://localhost:3000')
1116
+ * .configure({ stripTagPrefix: "api/v1" })
1117
+ * ```
1118
+ */
1119
+ stripTagPrefix?: string;
1120
+ };
1059
1121
  interface SpooshConfig<TPlugins extends PluginArray = PluginArray> {
1060
1122
  baseUrl: string;
1061
1123
  defaultOptions?: SpooshOptions;
@@ -1069,6 +1131,8 @@ type SpooshInstance<TSchema = unknown, TDefaultError = unknown, TPlugins extends
1069
1131
  config: {
1070
1132
  baseUrl: string;
1071
1133
  defaultOptions: SpooshOptions;
1134
+ /** Resolved prefix to strip from tags (used for cache invalidation matching) */
1135
+ stripTagPrefix?: string;
1072
1136
  };
1073
1137
  _types: {
1074
1138
  schema: TSchema;
@@ -1120,12 +1184,14 @@ declare class Spoosh<TSchema = unknown, TError = unknown, TPlugins extends Plugi
1120
1184
  private baseUrl;
1121
1185
  private defaultOptions;
1122
1186
  private _plugins;
1187
+ private _config;
1123
1188
  /**
1124
1189
  * Creates a new Spoosh instance.
1125
1190
  *
1126
1191
  * @param baseUrl - The base URL for all API requests (e.g., '/api' or 'https://api.example.com')
1127
1192
  * @param defaultOptions - Optional default options applied to all requests (headers, credentials, etc.)
1128
1193
  * @param plugins - Internal parameter used by the `.use()` method. Do not pass directly.
1194
+ * @param configOptions - Internal parameter used by the `.config()` method. Do not pass directly.
1129
1195
  *
1130
1196
  * @example
1131
1197
  * ```ts
@@ -1138,7 +1204,7 @@ declare class Spoosh<TSchema = unknown, TError = unknown, TPlugins extends Plugi
1138
1204
  * });
1139
1205
  * ```
1140
1206
  */
1141
- constructor(baseUrl: string, defaultOptions?: SpooshOptions, plugins?: TPlugins);
1207
+ constructor(baseUrl: string, defaultOptions?: SpooshOptions, plugins?: TPlugins, configOptions?: SpooshConfigOptions);
1142
1208
  /**
1143
1209
  * Adds plugins to the Spoosh instance.
1144
1210
  *
@@ -1174,6 +1240,34 @@ declare class Spoosh<TSchema = unknown, TError = unknown, TPlugins extends Plugi
1174
1240
  * ```
1175
1241
  */
1176
1242
  use<const TNewPlugins extends PluginArray>(plugins: TNewPlugins): Spoosh<TSchema, TError, TNewPlugins>;
1243
+ /**
1244
+ * Configures runtime options for the Spoosh instance.
1245
+ *
1246
+ * Returns a **new** Spoosh instance with the updated configuration (immutable pattern).
1247
+ * Configuration is preserved across `.use()` calls.
1248
+ *
1249
+ * URL prefix stripping always auto-detects from baseUrl.
1250
+ * Tag prefix stripping defaults to URL prefix but can be overridden.
1251
+ *
1252
+ * @param options - Configuration options
1253
+ * @returns A new Spoosh instance with the specified configuration
1254
+ *
1255
+ * @example Default behavior (auto-detect from baseUrl for both URL and tags)
1256
+ * ```ts
1257
+ * // baseUrl="/api", schema="api/posts"
1258
+ * // URL: /api/posts, Tags: ["posts"]
1259
+ * const client = new Spoosh<Schema, Error>('https://localhost:3000/api');
1260
+ * ```
1261
+ *
1262
+ * @example Override tag prefix (when baseUrl doesn't have the prefix you want to strip from tags)
1263
+ * ```ts
1264
+ * // baseUrl="/", schema="api/v1/posts"
1265
+ * // URL: /api/v1/posts, Tags: ["posts"] (strips "api/v1" from tags only)
1266
+ * const client = new Spoosh<Schema, Error>('http://localhost:3000')
1267
+ * .configure({ stripTagPrefix: "api/v1" });
1268
+ * ```
1269
+ */
1270
+ configure(options: SpooshConfigOptions): Spoosh<TSchema, TError, TPlugins>;
1177
1271
  /**
1178
1272
  * Cached instance of the underlying SpooshInstance.
1179
1273
  * Created lazily on first property access.
@@ -1273,6 +1367,7 @@ declare class Spoosh<TSchema = unknown, TError = unknown, TPlugins extends Plugi
1273
1367
  get config(): {
1274
1368
  baseUrl: string;
1275
1369
  defaultOptions: SpooshOptions;
1370
+ stripTagPrefix?: string;
1276
1371
  };
1277
1372
  /**
1278
1373
  * Type information carrier for generic type inference.
@@ -1291,6 +1386,16 @@ type SpooshClientConfig = {
1291
1386
  baseUrl: string;
1292
1387
  defaultOptions?: SpooshOptions;
1293
1388
  middlewares?: SpooshMiddleware[];
1389
+ /**
1390
+ * Prefix to strip from tag generation.
1391
+ *
1392
+ * URL prefix stripping always auto-detects from baseUrl.
1393
+ * This option only affects tag generation for cache invalidation.
1394
+ *
1395
+ * - `undefined`: Auto-detect from baseUrl (default, same as URL prefix)
1396
+ * - `string`: Explicit prefix to strip from tags
1397
+ */
1398
+ stripTagPrefix?: string;
1294
1399
  };
1295
1400
  /**
1296
1401
  * Creates a lightweight type-safe API client for vanilla JavaScript/TypeScript usage.
@@ -1384,11 +1489,61 @@ type TagOptions = {
1384
1489
  declare function resolveTags(options: TagOptions | undefined, resolvedPath: string[]): string[];
1385
1490
  declare function resolvePath(path: string[], params: Record<string, string | number> | undefined): string[];
1386
1491
 
1492
+ /**
1493
+ * Extracts the path prefix from a base URL.
1494
+ *
1495
+ * @param baseUrl - The base URL (absolute or relative)
1496
+ * @returns The path portion without leading/trailing slashes
1497
+ *
1498
+ * @example
1499
+ * ```ts
1500
+ * extractPrefixFromBaseUrl("https://localhost:3000/api"); // "api"
1501
+ * extractPrefixFromBaseUrl("/api/v1"); // "api/v1"
1502
+ * extractPrefixFromBaseUrl("api"); // "api"
1503
+ * ```
1504
+ */
1505
+ declare function extractPrefixFromBaseUrl(baseUrl: string): string;
1506
+ /**
1507
+ * Strips a prefix from path segments if the path starts with that prefix.
1508
+ *
1509
+ * @param pathSegments - Array of path segments
1510
+ * @param prefix - Prefix to strip (e.g., "api" or "api/v1")
1511
+ * @returns Path segments with prefix removed
1512
+ *
1513
+ * @example
1514
+ * ```ts
1515
+ * stripPrefixFromPath(["api", "posts"], "api"); // ["posts"]
1516
+ * stripPrefixFromPath(["api", "v1", "users"], "api/v1"); // ["users"]
1517
+ * stripPrefixFromPath(["posts"], "api"); // ["posts"] (no match, unchanged)
1518
+ * ```
1519
+ */
1520
+ declare function stripPrefixFromPath(pathSegments: string[], prefix: string): string[];
1521
+ /**
1522
+ * Resolves the strip prefix value based on configuration.
1523
+ *
1524
+ * @param stripPathPrefix - Configuration value (boolean, string, or undefined)
1525
+ * @param baseUrl - The base URL to extract prefix from when true
1526
+ * @returns The resolved prefix string to strip
1527
+ *
1528
+ * @example
1529
+ * ```ts
1530
+ * resolveStripPrefix(true, "https://localhost:3000/api"); // "api"
1531
+ * resolveStripPrefix("api/v1", "https://localhost:3000/api"); // "api/v1"
1532
+ * resolveStripPrefix(false, "https://localhost:3000/api"); // ""
1533
+ * resolveStripPrefix(undefined, "https://localhost:3000/api"); // ""
1534
+ * ```
1535
+ */
1536
+ declare function resolveStripPrefix(stripPathPrefix: boolean | string | undefined, baseUrl: string): string;
1537
+
1387
1538
  type ProxyHandlerConfig<TOptions = SpooshOptions> = {
1388
1539
  baseUrl: string;
1389
1540
  defaultOptions: TOptions;
1390
1541
  fetchExecutor?: FetchExecutor<TOptions, AnyRequestOptions>;
1391
1542
  nextTags?: boolean;
1543
+ /** Prefix to strip from URL path (auto-detected from baseUrl, always applied) */
1544
+ urlPrefix?: string;
1545
+ /** Prefix to strip from tag generation (defaults to urlPrefix) */
1546
+ tagPrefix?: string;
1392
1547
  };
1393
1548
  /**
1394
1549
  * Creates an API client proxy that uses path strings instead of chained property access.
@@ -1515,7 +1670,7 @@ declare function extractPathFromSelector(fn: unknown): string;
1515
1670
  */
1516
1671
  declare function extractMethodFromSelector(fn: unknown): string | undefined;
1517
1672
 
1518
- declare function executeFetch<TData, TError>(baseUrl: string, path: string[], method: HttpMethod, defaultOptions: SpooshOptions & SpooshOptionsExtra, requestOptions?: AnyRequestOptions, nextTags?: boolean): Promise<SpooshResponse<TData, TError>>;
1673
+ declare function executeFetch<TData, TError>(baseUrl: string, path: string[], method: HttpMethod, defaultOptions: SpooshOptions & SpooshOptionsExtra, requestOptions?: AnyRequestOptions, nextTags?: boolean, tagPath?: string[]): Promise<SpooshResponse<TData, TError>>;
1519
1674
 
1520
1675
  declare function createMiddleware<TData = unknown, TError = unknown>(name: string, phase: MiddlewarePhase, handler: SpooshMiddleware<TData, TError>["handler"]): SpooshMiddleware<TData, TError>;
1521
1676
  declare function applyMiddlewares<TData = unknown, TError = unknown>(context: MiddlewareContext<TData, TError>, middlewares: SpooshMiddleware<TData, TError>[], phase: MiddlewarePhase): Promise<MiddlewareContext<TData, TError>>;
@@ -1609,4 +1764,4 @@ type CreateInfiniteReadOptions<TData, TItem, TError, TRequest> = {
1609
1764
  };
1610
1765
  declare function createInfiniteReadController<TData, TItem, TError, TRequest extends InfiniteRequestOptions = InfiniteRequestOptions>(options: CreateInfiniteReadOptions<TData, TItem, TError, TRequest>): InfiniteReadController<TData, TItem, TError>;
1611
1766
 
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 };
1767
+ 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 PluginExecutorOptions, 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 SpooshConfigOptions, type SpooshInstance, type SpooshMiddleware, type SpooshOptions, type SpooshOptionsExtra, type SpooshPlugin, type SpooshResponse, type SpooshSchema, type StateManager, type StripPrefix, 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, extractPrefixFromBaseUrl, form, generateTags, getContentType, isJsonBody, isSpooshBody, json, mergeHeaders, objectToFormData, objectToUrlEncoded, resolveHeadersToRecord, resolvePath, resolveRequestBody, resolveStripPrefix, resolveTags, setHeaders, sortObjectKeys, stripPrefixFromPath, urlencoded };
package/dist/index.d.ts CHANGED
@@ -124,7 +124,7 @@ type MethodOptionsMap<TQueryOptions = object, TMutationOptions = object> = {
124
124
  DELETE: TMutationOptions;
125
125
  };
126
126
  type ExtractMethodOptions<TOptionsMap, TMethod extends HttpMethod> = TOptionsMap extends MethodOptionsMap<infer TQuery, infer TMutation> ? TMethod extends "GET" ? TQuery : TMutation : TOptionsMap;
127
- type FetchExecutor<TOptions = SpooshOptions, TRequestOptions = AnyRequestOptions> = <TData, TError>(baseUrl: string, path: string[], method: HttpMethod, defaultOptions: TOptions, requestOptions?: TRequestOptions, nextTags?: boolean) => Promise<SpooshResponse<TData, TError>>;
127
+ type FetchExecutor<TOptions = SpooshOptions, TRequestOptions = AnyRequestOptions> = <TData, TError>(baseUrl: string, path: string[], method: HttpMethod, defaultOptions: TOptions, requestOptions?: TRequestOptions, nextTags?: boolean, tagPath?: string[]) => Promise<SpooshResponse<TData, TError>>;
128
128
  type TypedParamsOption<TParamNames extends string> = [TParamNames] extends [
129
129
  never
130
130
  ] ? object : {
@@ -251,6 +251,11 @@ type PluginContext<TData = unknown, TError = unknown> = {
251
251
  readonly requestTimestamp: number;
252
252
  /** Unique identifier for the hook instance. Persists across queryKey changes within the same hook. */
253
253
  readonly hookId?: string;
254
+ /**
255
+ * Prefix to strip from tags. Configured at the Spoosh instance level.
256
+ * Plugins can use this to normalize tags before emitting events.
257
+ */
258
+ readonly stripTagPrefix?: string;
254
259
  requestOptions: AnyRequestOptions;
255
260
  state: OperationState<TData, TError>;
256
261
  response?: SpooshResponse<TData, TError>;
@@ -596,7 +601,10 @@ type PluginExecutor = {
596
601
  /** Creates a full PluginContext with plugins accessor injected */
597
602
  createContext: <TData, TError>(input: PluginContextInput<TData, TError>) => PluginContext<TData, TError>;
598
603
  };
599
- declare function createPluginExecutor(initialPlugins?: SpooshPlugin[]): PluginExecutor;
604
+ type PluginExecutorOptions = {
605
+ stripTagPrefix?: string;
606
+ };
607
+ declare function createPluginExecutor(initialPlugins?: SpooshPlugin[], options?: PluginExecutorOptions): PluginExecutor;
600
608
 
601
609
  /**
602
610
  * Resolves plugin option types based on the full context.
@@ -863,6 +871,33 @@ type HasReadMethod<TSchema, TPath extends string> = FindMatchingKey<TSchema, TPa
863
871
  * Check if a schema path has any write methods.
864
872
  */
865
873
  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;
874
+ type NormalizePrefix<T extends string> = T extends `/${infer Rest}` ? NormalizePrefix<Rest> : T extends `${infer Rest}/` ? NormalizePrefix<Rest> : T;
875
+ type StripPrefixFromPath<TPath extends string, TPrefix extends string> = TPath extends TPrefix ? "" : TPath extends `${TPrefix}/${infer Rest}` ? Rest : TPath;
876
+ /**
877
+ * Strips a prefix from all path keys in a schema.
878
+ * Works with any schema (Elysia, Hono, or manual).
879
+ *
880
+ * @example
881
+ * ```ts
882
+ * type FullSchema = {
883
+ * "api": { GET: { data: string } };
884
+ * "api/users": { GET: { data: User[] } };
885
+ * "api/posts/:id": { GET: { data: Post } };
886
+ * "health": { GET: { data: { status: string } } };
887
+ * };
888
+ *
889
+ * type ApiSchema = StripPrefix<FullSchema, "api">;
890
+ * // {
891
+ * // "": { GET: { data: string } };
892
+ * // "users": { GET: { data: User[] } };
893
+ * // "posts/:id": { GET: { data: Post } };
894
+ * // "health": { GET: { data: { status: string } } };
895
+ * // }
896
+ * ```
897
+ */
898
+ type StripPrefix<TSchema, TPrefix extends string> = TPrefix extends "" ? TSchema : {
899
+ [K in keyof TSchema as K extends string ? StripPrefixFromPath<K, NormalizePrefix<TPrefix>> : K]: TSchema[K];
900
+ };
866
901
 
867
902
  type IsNever<T> = [T] extends [never] ? true : false;
868
903
  type EndpointRequestOptions<TEndpoint, TPath extends string> = (IsNever<ExtractBody$1<TEndpoint>> extends true ? object : {
@@ -1056,6 +1091,33 @@ type WritePathMethods<TSchema, TPath extends string, TDefaultError> = FindMatchi
1056
1091
  type WriteClient<TSchema, TDefaultError = unknown> = <TPath extends WritePaths<TSchema> | (string & {})>(path: TPath) => HasWriteMethod<TSchema, TPath> extends true ? WritePathMethods<TSchema, TPath, TDefaultError> : never;
1057
1092
 
1058
1093
  type PluginArray = readonly SpooshPlugin<PluginTypeConfig>[];
1094
+ /**
1095
+ * Configuration options for Spoosh runtime behavior.
1096
+ */
1097
+ type SpooshConfigOptions = {
1098
+ /**
1099
+ * Prefix to strip from tag generation.
1100
+ *
1101
+ * URL prefix stripping always auto-detects from baseUrl.
1102
+ * This option only affects tag generation for cache invalidation.
1103
+ *
1104
+ * - `undefined`: Auto-detect from baseUrl (default)
1105
+ * - `string`: Explicit prefix to strip from tags
1106
+ *
1107
+ * @example
1108
+ * ```ts
1109
+ * // Default: auto-detect from baseUrl
1110
+ * // baseUrl="/api", schema="api/posts" → tags: ["posts"]
1111
+ * new Spoosh<Schema>('https://localhost:3000/api')
1112
+ *
1113
+ * // Explicit prefix (when baseUrl doesn't have it)
1114
+ * // baseUrl="/", schema="api/v1/posts" → tags: ["posts"]
1115
+ * new Spoosh<Schema>('http://localhost:3000')
1116
+ * .configure({ stripTagPrefix: "api/v1" })
1117
+ * ```
1118
+ */
1119
+ stripTagPrefix?: string;
1120
+ };
1059
1121
  interface SpooshConfig<TPlugins extends PluginArray = PluginArray> {
1060
1122
  baseUrl: string;
1061
1123
  defaultOptions?: SpooshOptions;
@@ -1069,6 +1131,8 @@ type SpooshInstance<TSchema = unknown, TDefaultError = unknown, TPlugins extends
1069
1131
  config: {
1070
1132
  baseUrl: string;
1071
1133
  defaultOptions: SpooshOptions;
1134
+ /** Resolved prefix to strip from tags (used for cache invalidation matching) */
1135
+ stripTagPrefix?: string;
1072
1136
  };
1073
1137
  _types: {
1074
1138
  schema: TSchema;
@@ -1120,12 +1184,14 @@ declare class Spoosh<TSchema = unknown, TError = unknown, TPlugins extends Plugi
1120
1184
  private baseUrl;
1121
1185
  private defaultOptions;
1122
1186
  private _plugins;
1187
+ private _config;
1123
1188
  /**
1124
1189
  * Creates a new Spoosh instance.
1125
1190
  *
1126
1191
  * @param baseUrl - The base URL for all API requests (e.g., '/api' or 'https://api.example.com')
1127
1192
  * @param defaultOptions - Optional default options applied to all requests (headers, credentials, etc.)
1128
1193
  * @param plugins - Internal parameter used by the `.use()` method. Do not pass directly.
1194
+ * @param configOptions - Internal parameter used by the `.config()` method. Do not pass directly.
1129
1195
  *
1130
1196
  * @example
1131
1197
  * ```ts
@@ -1138,7 +1204,7 @@ declare class Spoosh<TSchema = unknown, TError = unknown, TPlugins extends Plugi
1138
1204
  * });
1139
1205
  * ```
1140
1206
  */
1141
- constructor(baseUrl: string, defaultOptions?: SpooshOptions, plugins?: TPlugins);
1207
+ constructor(baseUrl: string, defaultOptions?: SpooshOptions, plugins?: TPlugins, configOptions?: SpooshConfigOptions);
1142
1208
  /**
1143
1209
  * Adds plugins to the Spoosh instance.
1144
1210
  *
@@ -1174,6 +1240,34 @@ declare class Spoosh<TSchema = unknown, TError = unknown, TPlugins extends Plugi
1174
1240
  * ```
1175
1241
  */
1176
1242
  use<const TNewPlugins extends PluginArray>(plugins: TNewPlugins): Spoosh<TSchema, TError, TNewPlugins>;
1243
+ /**
1244
+ * Configures runtime options for the Spoosh instance.
1245
+ *
1246
+ * Returns a **new** Spoosh instance with the updated configuration (immutable pattern).
1247
+ * Configuration is preserved across `.use()` calls.
1248
+ *
1249
+ * URL prefix stripping always auto-detects from baseUrl.
1250
+ * Tag prefix stripping defaults to URL prefix but can be overridden.
1251
+ *
1252
+ * @param options - Configuration options
1253
+ * @returns A new Spoosh instance with the specified configuration
1254
+ *
1255
+ * @example Default behavior (auto-detect from baseUrl for both URL and tags)
1256
+ * ```ts
1257
+ * // baseUrl="/api", schema="api/posts"
1258
+ * // URL: /api/posts, Tags: ["posts"]
1259
+ * const client = new Spoosh<Schema, Error>('https://localhost:3000/api');
1260
+ * ```
1261
+ *
1262
+ * @example Override tag prefix (when baseUrl doesn't have the prefix you want to strip from tags)
1263
+ * ```ts
1264
+ * // baseUrl="/", schema="api/v1/posts"
1265
+ * // URL: /api/v1/posts, Tags: ["posts"] (strips "api/v1" from tags only)
1266
+ * const client = new Spoosh<Schema, Error>('http://localhost:3000')
1267
+ * .configure({ stripTagPrefix: "api/v1" });
1268
+ * ```
1269
+ */
1270
+ configure(options: SpooshConfigOptions): Spoosh<TSchema, TError, TPlugins>;
1177
1271
  /**
1178
1272
  * Cached instance of the underlying SpooshInstance.
1179
1273
  * Created lazily on first property access.
@@ -1273,6 +1367,7 @@ declare class Spoosh<TSchema = unknown, TError = unknown, TPlugins extends Plugi
1273
1367
  get config(): {
1274
1368
  baseUrl: string;
1275
1369
  defaultOptions: SpooshOptions;
1370
+ stripTagPrefix?: string;
1276
1371
  };
1277
1372
  /**
1278
1373
  * Type information carrier for generic type inference.
@@ -1291,6 +1386,16 @@ type SpooshClientConfig = {
1291
1386
  baseUrl: string;
1292
1387
  defaultOptions?: SpooshOptions;
1293
1388
  middlewares?: SpooshMiddleware[];
1389
+ /**
1390
+ * Prefix to strip from tag generation.
1391
+ *
1392
+ * URL prefix stripping always auto-detects from baseUrl.
1393
+ * This option only affects tag generation for cache invalidation.
1394
+ *
1395
+ * - `undefined`: Auto-detect from baseUrl (default, same as URL prefix)
1396
+ * - `string`: Explicit prefix to strip from tags
1397
+ */
1398
+ stripTagPrefix?: string;
1294
1399
  };
1295
1400
  /**
1296
1401
  * Creates a lightweight type-safe API client for vanilla JavaScript/TypeScript usage.
@@ -1384,11 +1489,61 @@ type TagOptions = {
1384
1489
  declare function resolveTags(options: TagOptions | undefined, resolvedPath: string[]): string[];
1385
1490
  declare function resolvePath(path: string[], params: Record<string, string | number> | undefined): string[];
1386
1491
 
1492
+ /**
1493
+ * Extracts the path prefix from a base URL.
1494
+ *
1495
+ * @param baseUrl - The base URL (absolute or relative)
1496
+ * @returns The path portion without leading/trailing slashes
1497
+ *
1498
+ * @example
1499
+ * ```ts
1500
+ * extractPrefixFromBaseUrl("https://localhost:3000/api"); // "api"
1501
+ * extractPrefixFromBaseUrl("/api/v1"); // "api/v1"
1502
+ * extractPrefixFromBaseUrl("api"); // "api"
1503
+ * ```
1504
+ */
1505
+ declare function extractPrefixFromBaseUrl(baseUrl: string): string;
1506
+ /**
1507
+ * Strips a prefix from path segments if the path starts with that prefix.
1508
+ *
1509
+ * @param pathSegments - Array of path segments
1510
+ * @param prefix - Prefix to strip (e.g., "api" or "api/v1")
1511
+ * @returns Path segments with prefix removed
1512
+ *
1513
+ * @example
1514
+ * ```ts
1515
+ * stripPrefixFromPath(["api", "posts"], "api"); // ["posts"]
1516
+ * stripPrefixFromPath(["api", "v1", "users"], "api/v1"); // ["users"]
1517
+ * stripPrefixFromPath(["posts"], "api"); // ["posts"] (no match, unchanged)
1518
+ * ```
1519
+ */
1520
+ declare function stripPrefixFromPath(pathSegments: string[], prefix: string): string[];
1521
+ /**
1522
+ * Resolves the strip prefix value based on configuration.
1523
+ *
1524
+ * @param stripPathPrefix - Configuration value (boolean, string, or undefined)
1525
+ * @param baseUrl - The base URL to extract prefix from when true
1526
+ * @returns The resolved prefix string to strip
1527
+ *
1528
+ * @example
1529
+ * ```ts
1530
+ * resolveStripPrefix(true, "https://localhost:3000/api"); // "api"
1531
+ * resolveStripPrefix("api/v1", "https://localhost:3000/api"); // "api/v1"
1532
+ * resolveStripPrefix(false, "https://localhost:3000/api"); // ""
1533
+ * resolveStripPrefix(undefined, "https://localhost:3000/api"); // ""
1534
+ * ```
1535
+ */
1536
+ declare function resolveStripPrefix(stripPathPrefix: boolean | string | undefined, baseUrl: string): string;
1537
+
1387
1538
  type ProxyHandlerConfig<TOptions = SpooshOptions> = {
1388
1539
  baseUrl: string;
1389
1540
  defaultOptions: TOptions;
1390
1541
  fetchExecutor?: FetchExecutor<TOptions, AnyRequestOptions>;
1391
1542
  nextTags?: boolean;
1543
+ /** Prefix to strip from URL path (auto-detected from baseUrl, always applied) */
1544
+ urlPrefix?: string;
1545
+ /** Prefix to strip from tag generation (defaults to urlPrefix) */
1546
+ tagPrefix?: string;
1392
1547
  };
1393
1548
  /**
1394
1549
  * Creates an API client proxy that uses path strings instead of chained property access.
@@ -1515,7 +1670,7 @@ declare function extractPathFromSelector(fn: unknown): string;
1515
1670
  */
1516
1671
  declare function extractMethodFromSelector(fn: unknown): string | undefined;
1517
1672
 
1518
- declare function executeFetch<TData, TError>(baseUrl: string, path: string[], method: HttpMethod, defaultOptions: SpooshOptions & SpooshOptionsExtra, requestOptions?: AnyRequestOptions, nextTags?: boolean): Promise<SpooshResponse<TData, TError>>;
1673
+ declare function executeFetch<TData, TError>(baseUrl: string, path: string[], method: HttpMethod, defaultOptions: SpooshOptions & SpooshOptionsExtra, requestOptions?: AnyRequestOptions, nextTags?: boolean, tagPath?: string[]): Promise<SpooshResponse<TData, TError>>;
1519
1674
 
1520
1675
  declare function createMiddleware<TData = unknown, TError = unknown>(name: string, phase: MiddlewarePhase, handler: SpooshMiddleware<TData, TError>["handler"]): SpooshMiddleware<TData, TError>;
1521
1676
  declare function applyMiddlewares<TData = unknown, TError = unknown>(context: MiddlewareContext<TData, TError>, middlewares: SpooshMiddleware<TData, TError>[], phase: MiddlewarePhase): Promise<MiddlewareContext<TData, TError>>;
@@ -1609,4 +1764,4 @@ type CreateInfiniteReadOptions<TData, TItem, TError, TRequest> = {
1609
1764
  };
1610
1765
  declare function createInfiniteReadController<TData, TItem, TError, TRequest extends InfiniteRequestOptions = InfiniteRequestOptions>(options: CreateInfiniteReadOptions<TData, TItem, TError, TRequest>): InfiniteReadController<TData, TItem, TError>;
1611
1766
 
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 };
1767
+ 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 PluginExecutorOptions, 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 SpooshConfigOptions, type SpooshInstance, type SpooshMiddleware, type SpooshOptions, type SpooshOptionsExtra, type SpooshPlugin, type SpooshResponse, type SpooshSchema, type StateManager, type StripPrefix, 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, extractPrefixFromBaseUrl, form, generateTags, getContentType, isJsonBody, isSpooshBody, json, mergeHeaders, objectToFormData, objectToUrlEncoded, resolveHeadersToRecord, resolvePath, resolveRequestBody, resolveStripPrefix, resolveTags, setHeaders, sortObjectKeys, stripPrefixFromPath, urlencoded };
package/dist/index.js CHANGED
@@ -41,6 +41,7 @@ __export(src_exports, {
41
41
  executeFetch: () => executeFetch,
42
42
  extractMethodFromSelector: () => extractMethodFromSelector,
43
43
  extractPathFromSelector: () => extractPathFromSelector,
44
+ extractPrefixFromBaseUrl: () => extractPrefixFromBaseUrl,
44
45
  form: () => form,
45
46
  generateTags: () => generateTags,
46
47
  getContentType: () => getContentType,
@@ -53,9 +54,11 @@ __export(src_exports, {
53
54
  resolveHeadersToRecord: () => resolveHeadersToRecord,
54
55
  resolvePath: () => resolvePath,
55
56
  resolveRequestBody: () => resolveRequestBody,
57
+ resolveStripPrefix: () => resolveStripPrefix,
56
58
  resolveTags: () => resolveTags,
57
59
  setHeaders: () => setHeaders,
58
60
  sortObjectKeys: () => sortObjectKeys,
61
+ stripPrefixFromPath: () => stripPrefixFromPath,
59
62
  urlencoded: () => urlencoded
60
63
  });
61
64
  module.exports = __toCommonJS(src_exports);
@@ -368,11 +371,41 @@ function resolvePath(path, params) {
368
371
  });
369
372
  }
370
373
 
374
+ // src/utils/stripPathPrefix.ts
375
+ function extractPrefixFromBaseUrl(baseUrl) {
376
+ const isAbsolute = /^https?:\/\//.test(baseUrl);
377
+ if (isAbsolute) {
378
+ try {
379
+ const url = new URL(baseUrl);
380
+ return url.pathname.replace(/^\/|\/$/g, "");
381
+ } catch {
382
+ return "";
383
+ }
384
+ }
385
+ return baseUrl.replace(/^\/|\/$/g, "");
386
+ }
387
+ function stripPrefixFromPath(pathSegments, prefix) {
388
+ if (!prefix) return pathSegments;
389
+ const prefixSegments = prefix.split("/").filter(Boolean);
390
+ if (prefixSegments.length === 0) return pathSegments;
391
+ const startsWithPrefix = prefixSegments.every(
392
+ (seg, i) => pathSegments[i] === seg
393
+ );
394
+ return startsWithPrefix ? pathSegments.slice(prefixSegments.length) : pathSegments;
395
+ }
396
+ function resolveStripPrefix(stripPathPrefix, baseUrl) {
397
+ if (!stripPathPrefix) return "";
398
+ if (stripPathPrefix === true) {
399
+ return extractPrefixFromBaseUrl(baseUrl);
400
+ }
401
+ return stripPathPrefix.replace(/^\/|\/$/g, "");
402
+ }
403
+
371
404
  // src/fetch.ts
372
405
  var delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
373
406
  var isNetworkError = (err) => err instanceof TypeError;
374
407
  var isAbortError = (err) => err instanceof DOMException && err.name === "AbortError";
375
- async function executeFetch(baseUrl, path, method, defaultOptions, requestOptions, nextTags) {
408
+ async function executeFetch(baseUrl, path, method, defaultOptions, requestOptions, nextTags, tagPath) {
376
409
  const middlewares = defaultOptions.middlewares ?? [];
377
410
  let context = {
378
411
  baseUrl,
@@ -392,7 +425,8 @@ async function executeFetch(baseUrl, path, method, defaultOptions, requestOption
392
425
  defaultOptions: context.defaultOptions,
393
426
  requestOptions: context.requestOptions,
394
427
  middlewareFetchInit: context.fetchInit,
395
- nextTags
428
+ nextTags,
429
+ tagPath
396
430
  });
397
431
  context.response = response;
398
432
  if (middlewares.length > 0) {
@@ -424,7 +458,8 @@ async function executeCoreFetch(config) {
424
458
  defaultOptions,
425
459
  requestOptions,
426
460
  middlewareFetchInit,
427
- nextTags
461
+ nextTags,
462
+ tagPath
428
463
  } = config;
429
464
  const {
430
465
  middlewares: _,
@@ -449,7 +484,7 @@ async function executeCoreFetch(config) {
449
484
  }
450
485
  fetchInit.cache = requestOptions?.cache ?? fetchDefaults?.cache;
451
486
  if (nextTags) {
452
- const autoTags = generateTags(path);
487
+ const autoTags = generateTags(tagPath ?? path);
453
488
  const userNext = requestOptions?.next;
454
489
  fetchInit.next = {
455
490
  tags: userNext?.tags ?? autoTags,
@@ -540,8 +575,11 @@ function createProxyHandler(config) {
540
575
  baseUrl,
541
576
  defaultOptions,
542
577
  fetchExecutor = executeFetch,
543
- nextTags
578
+ nextTags,
579
+ urlPrefix,
580
+ tagPrefix
544
581
  } = config;
582
+ const effectiveTagPrefix = tagPrefix ?? urlPrefix;
545
583
  return ((path) => {
546
584
  return new Proxy(
547
585
  {},
@@ -554,13 +592,16 @@ function createProxyHandler(config) {
554
592
  }
555
593
  return (options) => {
556
594
  const resolvedPath = resolvePath2(path, options?.params);
595
+ const urlPath = urlPrefix ? stripPrefixFromPath(resolvedPath, urlPrefix) : resolvedPath;
596
+ const tagPath = effectiveTagPrefix ? stripPrefixFromPath(resolvedPath, effectiveTagPrefix) : resolvedPath;
557
597
  return fetchExecutor(
558
598
  baseUrl,
559
- resolvedPath,
599
+ urlPath,
560
600
  method,
561
601
  defaultOptions,
562
602
  options,
563
- nextTags
603
+ nextTags,
604
+ tagPath
564
605
  );
565
606
  };
566
607
  }
@@ -843,7 +884,8 @@ function sortByDependencies(plugins) {
843
884
  }
844
885
  return sorted;
845
886
  }
846
- function createPluginExecutor(initialPlugins = []) {
887
+ function createPluginExecutor(initialPlugins = [], options = {}) {
888
+ const { stripTagPrefix } = options;
847
889
  validateDependencies(initialPlugins);
848
890
  const plugins = sortByDependencies(initialPlugins);
849
891
  const frozenPlugins = Object.freeze([...plugins]);
@@ -927,6 +969,9 @@ function createPluginExecutor(initialPlugins = []) {
927
969
  ctx.headers = { ...ctx.headers, ...newHeaders };
928
970
  ctx.requestOptions.headers = ctx.headers;
929
971
  };
972
+ if (stripTagPrefix) {
973
+ ctx.stripTagPrefix = stripTagPrefix;
974
+ }
930
975
  return ctx;
931
976
  }
932
977
  };
@@ -945,12 +990,14 @@ var Spoosh = class _Spoosh {
945
990
  baseUrl;
946
991
  defaultOptions;
947
992
  _plugins;
993
+ _config;
948
994
  /**
949
995
  * Creates a new Spoosh instance.
950
996
  *
951
997
  * @param baseUrl - The base URL for all API requests (e.g., '/api' or 'https://api.example.com')
952
998
  * @param defaultOptions - Optional default options applied to all requests (headers, credentials, etc.)
953
999
  * @param plugins - Internal parameter used by the `.use()` method. Do not pass directly.
1000
+ * @param configOptions - Internal parameter used by the `.config()` method. Do not pass directly.
954
1001
  *
955
1002
  * @example
956
1003
  * ```ts
@@ -963,10 +1010,11 @@ var Spoosh = class _Spoosh {
963
1010
  * });
964
1011
  * ```
965
1012
  */
966
- constructor(baseUrl, defaultOptions, plugins) {
1013
+ constructor(baseUrl, defaultOptions, plugins, configOptions) {
967
1014
  this.baseUrl = baseUrl;
968
1015
  this.defaultOptions = defaultOptions || {};
969
1016
  this._plugins = plugins || [];
1017
+ this._config = configOptions || {};
970
1018
  }
971
1019
  /**
972
1020
  * Adds plugins to the Spoosh instance.
@@ -1006,7 +1054,43 @@ var Spoosh = class _Spoosh {
1006
1054
  return new _Spoosh(
1007
1055
  this.baseUrl,
1008
1056
  this.defaultOptions,
1009
- plugins
1057
+ plugins,
1058
+ this._config
1059
+ );
1060
+ }
1061
+ /**
1062
+ * Configures runtime options for the Spoosh instance.
1063
+ *
1064
+ * Returns a **new** Spoosh instance with the updated configuration (immutable pattern).
1065
+ * Configuration is preserved across `.use()` calls.
1066
+ *
1067
+ * URL prefix stripping always auto-detects from baseUrl.
1068
+ * Tag prefix stripping defaults to URL prefix but can be overridden.
1069
+ *
1070
+ * @param options - Configuration options
1071
+ * @returns A new Spoosh instance with the specified configuration
1072
+ *
1073
+ * @example Default behavior (auto-detect from baseUrl for both URL and tags)
1074
+ * ```ts
1075
+ * // baseUrl="/api", schema="api/posts"
1076
+ * // URL: /api/posts, Tags: ["posts"]
1077
+ * const client = new Spoosh<Schema, Error>('https://localhost:3000/api');
1078
+ * ```
1079
+ *
1080
+ * @example Override tag prefix (when baseUrl doesn't have the prefix you want to strip from tags)
1081
+ * ```ts
1082
+ * // baseUrl="/", schema="api/v1/posts"
1083
+ * // URL: /api/v1/posts, Tags: ["posts"] (strips "api/v1" from tags only)
1084
+ * const client = new Spoosh<Schema, Error>('http://localhost:3000')
1085
+ * .configure({ stripTagPrefix: "api/v1" });
1086
+ * ```
1087
+ */
1088
+ configure(options) {
1089
+ return new _Spoosh(
1090
+ this.baseUrl,
1091
+ this.defaultOptions,
1092
+ this._plugins,
1093
+ { ...this._config, ...options }
1010
1094
  );
1011
1095
  }
1012
1096
  /**
@@ -1022,13 +1106,19 @@ var Spoosh = class _Spoosh {
1022
1106
  */
1023
1107
  getInstance() {
1024
1108
  if (!this._instance) {
1109
+ const urlPrefix = extractPrefixFromBaseUrl(this.baseUrl) || void 0;
1110
+ const tagPrefix = this._config.stripTagPrefix ?? urlPrefix;
1025
1111
  const api = createProxyHandler({
1026
1112
  baseUrl: this.baseUrl,
1027
- defaultOptions: this.defaultOptions
1113
+ defaultOptions: this.defaultOptions,
1114
+ urlPrefix,
1115
+ tagPrefix
1028
1116
  });
1029
1117
  const stateManager = createStateManager();
1030
1118
  const eventEmitter = createEventEmitter();
1031
- const pluginExecutor = createPluginExecutor([...this._plugins]);
1119
+ const pluginExecutor = createPluginExecutor([...this._plugins], {
1120
+ stripTagPrefix: tagPrefix
1121
+ });
1032
1122
  this._instance = {
1033
1123
  api,
1034
1124
  stateManager,
@@ -1036,7 +1126,8 @@ var Spoosh = class _Spoosh {
1036
1126
  pluginExecutor,
1037
1127
  config: {
1038
1128
  baseUrl: this.baseUrl,
1039
- defaultOptions: this.defaultOptions
1129
+ defaultOptions: this.defaultOptions,
1130
+ stripTagPrefix: tagPrefix
1040
1131
  },
1041
1132
  _types: {
1042
1133
  schema: void 0,
@@ -1155,12 +1246,21 @@ var Spoosh = class _Spoosh {
1155
1246
 
1156
1247
  // src/createClient.ts
1157
1248
  function createClient(config) {
1158
- const { baseUrl, defaultOptions = {}, middlewares = [] } = config;
1249
+ const {
1250
+ baseUrl,
1251
+ defaultOptions = {},
1252
+ middlewares = [],
1253
+ stripTagPrefix
1254
+ } = config;
1159
1255
  const optionsWithMiddlewares = { ...defaultOptions, middlewares };
1256
+ const urlPrefix = extractPrefixFromBaseUrl(baseUrl) || void 0;
1257
+ const tagPrefix = stripTagPrefix ?? urlPrefix;
1160
1258
  return createProxyHandler({
1161
1259
  baseUrl,
1162
1260
  defaultOptions: optionsWithMiddlewares,
1163
- nextTags: true
1261
+ nextTags: true,
1262
+ urlPrefix,
1263
+ tagPrefix
1164
1264
  });
1165
1265
  }
1166
1266
 
package/dist/index.mjs CHANGED
@@ -306,11 +306,41 @@ function resolvePath(path, params) {
306
306
  });
307
307
  }
308
308
 
309
+ // src/utils/stripPathPrefix.ts
310
+ function extractPrefixFromBaseUrl(baseUrl) {
311
+ const isAbsolute = /^https?:\/\//.test(baseUrl);
312
+ if (isAbsolute) {
313
+ try {
314
+ const url = new URL(baseUrl);
315
+ return url.pathname.replace(/^\/|\/$/g, "");
316
+ } catch {
317
+ return "";
318
+ }
319
+ }
320
+ return baseUrl.replace(/^\/|\/$/g, "");
321
+ }
322
+ function stripPrefixFromPath(pathSegments, prefix) {
323
+ if (!prefix) return pathSegments;
324
+ const prefixSegments = prefix.split("/").filter(Boolean);
325
+ if (prefixSegments.length === 0) return pathSegments;
326
+ const startsWithPrefix = prefixSegments.every(
327
+ (seg, i) => pathSegments[i] === seg
328
+ );
329
+ return startsWithPrefix ? pathSegments.slice(prefixSegments.length) : pathSegments;
330
+ }
331
+ function resolveStripPrefix(stripPathPrefix, baseUrl) {
332
+ if (!stripPathPrefix) return "";
333
+ if (stripPathPrefix === true) {
334
+ return extractPrefixFromBaseUrl(baseUrl);
335
+ }
336
+ return stripPathPrefix.replace(/^\/|\/$/g, "");
337
+ }
338
+
309
339
  // src/fetch.ts
310
340
  var delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
311
341
  var isNetworkError = (err) => err instanceof TypeError;
312
342
  var isAbortError = (err) => err instanceof DOMException && err.name === "AbortError";
313
- async function executeFetch(baseUrl, path, method, defaultOptions, requestOptions, nextTags) {
343
+ async function executeFetch(baseUrl, path, method, defaultOptions, requestOptions, nextTags, tagPath) {
314
344
  const middlewares = defaultOptions.middlewares ?? [];
315
345
  let context = {
316
346
  baseUrl,
@@ -330,7 +360,8 @@ async function executeFetch(baseUrl, path, method, defaultOptions, requestOption
330
360
  defaultOptions: context.defaultOptions,
331
361
  requestOptions: context.requestOptions,
332
362
  middlewareFetchInit: context.fetchInit,
333
- nextTags
363
+ nextTags,
364
+ tagPath
334
365
  });
335
366
  context.response = response;
336
367
  if (middlewares.length > 0) {
@@ -362,7 +393,8 @@ async function executeCoreFetch(config) {
362
393
  defaultOptions,
363
394
  requestOptions,
364
395
  middlewareFetchInit,
365
- nextTags
396
+ nextTags,
397
+ tagPath
366
398
  } = config;
367
399
  const {
368
400
  middlewares: _,
@@ -387,7 +419,7 @@ async function executeCoreFetch(config) {
387
419
  }
388
420
  fetchInit.cache = requestOptions?.cache ?? fetchDefaults?.cache;
389
421
  if (nextTags) {
390
- const autoTags = generateTags(path);
422
+ const autoTags = generateTags(tagPath ?? path);
391
423
  const userNext = requestOptions?.next;
392
424
  fetchInit.next = {
393
425
  tags: userNext?.tags ?? autoTags,
@@ -478,8 +510,11 @@ function createProxyHandler(config) {
478
510
  baseUrl,
479
511
  defaultOptions,
480
512
  fetchExecutor = executeFetch,
481
- nextTags
513
+ nextTags,
514
+ urlPrefix,
515
+ tagPrefix
482
516
  } = config;
517
+ const effectiveTagPrefix = tagPrefix ?? urlPrefix;
483
518
  return ((path) => {
484
519
  return new Proxy(
485
520
  {},
@@ -492,13 +527,16 @@ function createProxyHandler(config) {
492
527
  }
493
528
  return (options) => {
494
529
  const resolvedPath = resolvePath2(path, options?.params);
530
+ const urlPath = urlPrefix ? stripPrefixFromPath(resolvedPath, urlPrefix) : resolvedPath;
531
+ const tagPath = effectiveTagPrefix ? stripPrefixFromPath(resolvedPath, effectiveTagPrefix) : resolvedPath;
495
532
  return fetchExecutor(
496
533
  baseUrl,
497
- resolvedPath,
534
+ urlPath,
498
535
  method,
499
536
  defaultOptions,
500
537
  options,
501
- nextTags
538
+ nextTags,
539
+ tagPath
502
540
  );
503
541
  };
504
542
  }
@@ -781,7 +819,8 @@ function sortByDependencies(plugins) {
781
819
  }
782
820
  return sorted;
783
821
  }
784
- function createPluginExecutor(initialPlugins = []) {
822
+ function createPluginExecutor(initialPlugins = [], options = {}) {
823
+ const { stripTagPrefix } = options;
785
824
  validateDependencies(initialPlugins);
786
825
  const plugins = sortByDependencies(initialPlugins);
787
826
  const frozenPlugins = Object.freeze([...plugins]);
@@ -865,6 +904,9 @@ function createPluginExecutor(initialPlugins = []) {
865
904
  ctx.headers = { ...ctx.headers, ...newHeaders };
866
905
  ctx.requestOptions.headers = ctx.headers;
867
906
  };
907
+ if (stripTagPrefix) {
908
+ ctx.stripTagPrefix = stripTagPrefix;
909
+ }
868
910
  return ctx;
869
911
  }
870
912
  };
@@ -883,12 +925,14 @@ var Spoosh = class _Spoosh {
883
925
  baseUrl;
884
926
  defaultOptions;
885
927
  _plugins;
928
+ _config;
886
929
  /**
887
930
  * Creates a new Spoosh instance.
888
931
  *
889
932
  * @param baseUrl - The base URL for all API requests (e.g., '/api' or 'https://api.example.com')
890
933
  * @param defaultOptions - Optional default options applied to all requests (headers, credentials, etc.)
891
934
  * @param plugins - Internal parameter used by the `.use()` method. Do not pass directly.
935
+ * @param configOptions - Internal parameter used by the `.config()` method. Do not pass directly.
892
936
  *
893
937
  * @example
894
938
  * ```ts
@@ -901,10 +945,11 @@ var Spoosh = class _Spoosh {
901
945
  * });
902
946
  * ```
903
947
  */
904
- constructor(baseUrl, defaultOptions, plugins) {
948
+ constructor(baseUrl, defaultOptions, plugins, configOptions) {
905
949
  this.baseUrl = baseUrl;
906
950
  this.defaultOptions = defaultOptions || {};
907
951
  this._plugins = plugins || [];
952
+ this._config = configOptions || {};
908
953
  }
909
954
  /**
910
955
  * Adds plugins to the Spoosh instance.
@@ -944,7 +989,43 @@ var Spoosh = class _Spoosh {
944
989
  return new _Spoosh(
945
990
  this.baseUrl,
946
991
  this.defaultOptions,
947
- plugins
992
+ plugins,
993
+ this._config
994
+ );
995
+ }
996
+ /**
997
+ * Configures runtime options for the Spoosh instance.
998
+ *
999
+ * Returns a **new** Spoosh instance with the updated configuration (immutable pattern).
1000
+ * Configuration is preserved across `.use()` calls.
1001
+ *
1002
+ * URL prefix stripping always auto-detects from baseUrl.
1003
+ * Tag prefix stripping defaults to URL prefix but can be overridden.
1004
+ *
1005
+ * @param options - Configuration options
1006
+ * @returns A new Spoosh instance with the specified configuration
1007
+ *
1008
+ * @example Default behavior (auto-detect from baseUrl for both URL and tags)
1009
+ * ```ts
1010
+ * // baseUrl="/api", schema="api/posts"
1011
+ * // URL: /api/posts, Tags: ["posts"]
1012
+ * const client = new Spoosh<Schema, Error>('https://localhost:3000/api');
1013
+ * ```
1014
+ *
1015
+ * @example Override tag prefix (when baseUrl doesn't have the prefix you want to strip from tags)
1016
+ * ```ts
1017
+ * // baseUrl="/", schema="api/v1/posts"
1018
+ * // URL: /api/v1/posts, Tags: ["posts"] (strips "api/v1" from tags only)
1019
+ * const client = new Spoosh<Schema, Error>('http://localhost:3000')
1020
+ * .configure({ stripTagPrefix: "api/v1" });
1021
+ * ```
1022
+ */
1023
+ configure(options) {
1024
+ return new _Spoosh(
1025
+ this.baseUrl,
1026
+ this.defaultOptions,
1027
+ this._plugins,
1028
+ { ...this._config, ...options }
948
1029
  );
949
1030
  }
950
1031
  /**
@@ -960,13 +1041,19 @@ var Spoosh = class _Spoosh {
960
1041
  */
961
1042
  getInstance() {
962
1043
  if (!this._instance) {
1044
+ const urlPrefix = extractPrefixFromBaseUrl(this.baseUrl) || void 0;
1045
+ const tagPrefix = this._config.stripTagPrefix ?? urlPrefix;
963
1046
  const api = createProxyHandler({
964
1047
  baseUrl: this.baseUrl,
965
- defaultOptions: this.defaultOptions
1048
+ defaultOptions: this.defaultOptions,
1049
+ urlPrefix,
1050
+ tagPrefix
966
1051
  });
967
1052
  const stateManager = createStateManager();
968
1053
  const eventEmitter = createEventEmitter();
969
- const pluginExecutor = createPluginExecutor([...this._plugins]);
1054
+ const pluginExecutor = createPluginExecutor([...this._plugins], {
1055
+ stripTagPrefix: tagPrefix
1056
+ });
970
1057
  this._instance = {
971
1058
  api,
972
1059
  stateManager,
@@ -974,7 +1061,8 @@ var Spoosh = class _Spoosh {
974
1061
  pluginExecutor,
975
1062
  config: {
976
1063
  baseUrl: this.baseUrl,
977
- defaultOptions: this.defaultOptions
1064
+ defaultOptions: this.defaultOptions,
1065
+ stripTagPrefix: tagPrefix
978
1066
  },
979
1067
  _types: {
980
1068
  schema: void 0,
@@ -1093,12 +1181,21 @@ var Spoosh = class _Spoosh {
1093
1181
 
1094
1182
  // src/createClient.ts
1095
1183
  function createClient(config) {
1096
- const { baseUrl, defaultOptions = {}, middlewares = [] } = config;
1184
+ const {
1185
+ baseUrl,
1186
+ defaultOptions = {},
1187
+ middlewares = [],
1188
+ stripTagPrefix
1189
+ } = config;
1097
1190
  const optionsWithMiddlewares = { ...defaultOptions, middlewares };
1191
+ const urlPrefix = extractPrefixFromBaseUrl(baseUrl) || void 0;
1192
+ const tagPrefix = stripTagPrefix ?? urlPrefix;
1098
1193
  return createProxyHandler({
1099
1194
  baseUrl,
1100
1195
  defaultOptions: optionsWithMiddlewares,
1101
- nextTags: true
1196
+ nextTags: true,
1197
+ urlPrefix,
1198
+ tagPrefix
1102
1199
  });
1103
1200
  }
1104
1201
 
@@ -1681,6 +1778,7 @@ export {
1681
1778
  executeFetch,
1682
1779
  extractMethodFromSelector,
1683
1780
  extractPathFromSelector,
1781
+ extractPrefixFromBaseUrl,
1684
1782
  form,
1685
1783
  generateTags,
1686
1784
  getContentType,
@@ -1693,8 +1791,10 @@ export {
1693
1791
  resolveHeadersToRecord,
1694
1792
  resolvePath,
1695
1793
  resolveRequestBody,
1794
+ resolveStripPrefix,
1696
1795
  resolveTags,
1697
1796
  setHeaders,
1698
1797
  sortObjectKeys,
1798
+ stripPrefixFromPath,
1699
1799
  urlencoded
1700
1800
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spoosh/core",
3
- "version": "0.9.0",
3
+ "version": "0.9.1",
4
4
  "license": "MIT",
5
5
  "description": "Type-safe API client with plugin middleware system",
6
6
  "keywords": [