From b00a4320a71d2aca6c4a50e8df04a82fdd8a0971 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 19 May 2026 22:49:06 +0200 Subject: [PATCH] core-fetch: baseUrl accepts () => string for lazy resolution MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lets consumers pass `() => import.meta.env.BASE_URL` without orval's CJS bundle path blowing up on the top-level `import.meta` reference. The getter is invoked at request time rather than factory time, so the mutator file can be loaded by orval (which bundles to CJS) without evaluating `import.meta`. Closes uwe.admin/webapp-template#21 (scaffold side). Bump to v0.5.0 — additive change (string form still works) but new shape for `CoreFetchOptions.baseUrl` is a public API change. Co-Authored-By: Claude Opus 4.7 (1M context) --- package-lock.json | 4 ++-- package.json | 2 +- src/core-fetch.test.ts | 19 +++++++++++++++++++ src/core-fetch.ts | 20 ++++++++++++++++---- 4 files changed, 38 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index 50c4870..6d9ac52 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@uschuster/webapp-scaffold", - "version": "0.3.6", + "version": "0.5.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@uschuster/webapp-scaffold", - "version": "0.3.6", + "version": "0.5.0", "license": "UNLICENSED", "bin": { "webapp-scaffold-fetch-openapi": "bin/fetch-openapi.sh", diff --git a/package.json b/package.json index efa5653..6194764 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@uschuster/webapp-scaffold", - "version": "0.4.2", + "version": "0.5.0", "description": "Shared build scripts + Vite config factories for webapp-template-derived projects.", "type": "module", "bin": { diff --git a/src/core-fetch.test.ts b/src/core-fetch.test.ts index 0b6890b..9d3cb6b 100644 --- a/src/core-fetch.test.ts +++ b/src/core-fetch.test.ts @@ -173,4 +173,23 @@ describe('createCoreFetch — request shape', () => { await cf('/v1/x'); expect(captured).toBe('https://api.example.com/v1/x'); }); + + it('baseUrl getter is resolved at request time, not factory time', async () => { + const captured: string[] = []; + let dynamicBase = 'https://first.example.com'; + const cf = createCoreFetch({ + baseUrl: () => dynamicBase, + fetchImpl: ((url: string) => { + captured.push(url); + return Promise.resolve(jsonResponse({})); + }) as unknown as typeof fetch, + }); + await cf('/v1/x'); + dynamicBase = 'https://second.example.com/'; + await cf('/v1/y'); + expect(captured).toEqual([ + 'https://first.example.com/v1/x', + 'https://second.example.com/v1/y', + ]); + }); }); diff --git a/src/core-fetch.ts b/src/core-fetch.ts index 3dc5afc..23a0b39 100644 --- a/src/core-fetch.ts +++ b/src/core-fetch.ts @@ -28,8 +28,16 @@ export interface CoreFetchRequest { } export interface CoreFetchOptions { - /** Prefix for every request; strip trailing slash. */ - baseUrl?: string; + /** + * Prefix for every request; trailing slash stripped at resolve time. + * + * Accepts a string OR a getter so consumers that derive the base from + * `import.meta.env.BASE_URL` (or any other late-bound source) can pass + * the getter form. The getter is invoked at request time, never at + * factory-construction time — which keeps orval's CJS bundling of the + * mutator file from blowing up on top-level `import.meta` references. + */ + baseUrl?: string | (() => string); /** `'body'` returns the parsed body (default); `'wrapped'` returns { data, status, headers }. */ responseShape?: ResponseShape; @@ -97,7 +105,11 @@ export type CoreFetch = CoreFetchWrapped & CoreFetchBody; const MUTATING = new Set(['POST', 'PUT', 'PATCH', 'DELETE']); export function createCoreFetch(opts: CoreFetchOptions = {}): CoreFetch { - const base = (opts.baseUrl ?? '').replace(/\/$/, ''); + // Lazy base resolution — see CoreFetchOptions.baseUrl JSDoc. + const resolveBase = (): string => { + const raw = typeof opts.baseUrl === 'function' ? opts.baseUrl() : (opts.baseUrl ?? ''); + return raw.replace(/\/$/, ''); + }; // Resolve fetch at call time, not construction time, so tests that // `vi.stubGlobal('fetch', ...)` after module import see the stub. const callFetch: typeof fetch = opts.fetchImpl @@ -135,7 +147,7 @@ export function createCoreFetch(opts: CoreFetchOptions = {}): CoreFetch { let r: Response; try { - r = await callFetch(`${base}${url}`, { + r = await callFetch(`${resolveBase()}${url}`, { credentials: 'include', ...init, headers,