@@ -13,6 +13,12 @@ imports `./services/Files.js`, while the tree-shaking transform currently emits
1313is always the capitalized service property plus ` Service ` , and it always strips
1414source import extensions from operation import specifiers.
1515
16+ The same ESM-extension concern applies to other imports emitted by the
17+ transform: the React context import for generated context clients and the
18+ options factory import for pre-created clients. Those imports should receive
19+ their own explicit extension configuration because their public modules may live
20+ under a different layout from service files.
21+
1622## Current Behavior
1723
1824For a usage such as:
@@ -42,11 +48,33 @@ That path is wrong when the generated OpenAPI Qraft client was created with a
4248custom service postfix, for example ` --postfix-services "" ` , because generated
4349files are named ` Files.ts ` and their ESM imports use ` Files.js ` .
4450
51+ When optimizing context-backed generated clients, the transform may also emit a
52+ configured React context import from ` reactContext.moduleSpecifier ` . That
53+ specifier is currently inserted as configured, with no way to request an ESM
54+ extension when the config uses a path-like source specifier.
55+
56+ When optimizing pre-created clients, the transform emits an options factory
57+ import from ` optionsFactory.moduleSpecifier ` . Path-like options factory imports
58+ can be recomposed relative to the transformed source file, but the helper still
59+ strips source extensions and offers no way to append ` .js ` .
60+
4561## Public API
4662
4763Extend ` ServicesTarget ` with two optional fields:
4864
4965``` ts
66+ export type ReactContextTarget = {
67+ exportName: string ;
68+ moduleSpecifier? : string ;
69+ importExtension? : string ;
70+ };
71+
72+ export type OptionsFactoryTarget = {
73+ exportName: string ;
74+ moduleSpecifier: string ;
75+ importExtension? : string ;
76+ };
77+
5078export type ServicesTarget = {
5179 moduleSpecifierBase? : string ;
5280 directory? : string ;
@@ -55,9 +83,26 @@ export type ServicesTarget = {
5583};
5684```
5785
58- Normalize these fields into ` ServicesConfig ` :
86+ Use ` OptionsFactoryTarget ` for
87+ ` QraftPrecreatedClientEntrypointConfig.optionsFactory ` . Do not add
88+ ` importExtension ` to ` factory ` or ` client ` ; those module specifiers are
89+ primarily matching and resolution inputs, not newly emitted optimized imports.
90+
91+ Normalize these fields into internal config objects:
5992
6093``` ts
94+ export type ReactContextConfig = {
95+ exportName: string ;
96+ moduleSpecifier: string ;
97+ importExtension: string ;
98+ };
99+
100+ export type ImportTarget = {
101+ exportName: string ;
102+ moduleSpecifier: string ;
103+ importExtension? : string ;
104+ };
105+
61106export type ServicesConfig = {
62107 moduleSpecifierBase: string ;
63108 directory: string ;
@@ -71,6 +116,9 @@ Defaults preserve existing behavior:
71116- ` fileNamePostfix: "Service" ` ;
72117- ` importExtension: "" ` .
73118
119+ All new import extension fields default to an empty string. Empty string means
120+ "do not append an extension".
121+
74122The motivating fixture can opt in with:
75123
76124``` ts
@@ -86,6 +134,30 @@ This emits:
86134import { getFileList } from " ./fixtures/files-api/services/Files.js" ;
87135```
88136
137+ Context imports can opt in independently:
138+
139+ ``` ts
140+ reactContext : {
141+ exportName : " InternalReactAPIClientContext" ,
142+ moduleSpecifier : " ./fixtures/files-api/InternalReactAPIClientContext" ,
143+ importExtension : " .js" ,
144+ }
145+ ```
146+
147+ Pre-created options imports can opt in independently:
148+
149+ ``` ts
150+ optionsFactory : {
151+ exportName : " createOptions" ,
152+ moduleSpecifier : " ./client-options" ,
153+ importExtension : " .js" ,
154+ }
155+ ```
156+
157+ The transform should not provide a top-level ` importExtension ` shortcut in this
158+ design. Service files, context modules, and pre-created options modules can have
159+ different public layouts, so each emitted import target owns its extension.
160+
89161## Non-Goals
90162
91163Do not infer service file names from ` services/index.ts ` in this change. That
@@ -99,6 +171,12 @@ Do not change generated client output. The generator already exposes
99171` --postfix-services ` and ` --explicit-import-extensions ` ; this design only lets
100172the tree-shaking plugin mirror those choices when rewriting user code.
101173
174+ Do not mutate bare module specifiers with import extensions. A configured
175+ specifier such as ` @api/my-api ` or ` @scope/client/options ` must stay exactly as
176+ configured, even when it contains a package subpath. Users who need an
177+ extension in a package subpath should provide that exact module specifier
178+ themselves.
179+
102180Do not alter runtime callback selection, generated source ownership checks,
103181resolver behavior, diagnostics levels, or standalone project file discovery.
104182
@@ -107,7 +185,11 @@ resolver behavior, diagnostics levels, or standalone project file discovery.
107185Keep defaults in ` normalizeServices(...) ` so the rest of the transform receives
108186a fully normalized service import configuration.
109187
110- The new fields flow through the existing model:
188+ Keep React context extension defaults in ` normalizeReactContext(...) ` . Keep
189+ pre-created options factory extension defaults while normalizing pre-created
190+ entrypoints.
191+
192+ The new service fields flow through the existing model:
111193
112194``` txt
113195ServicesTarget
@@ -142,10 +224,36 @@ composeServiceOperationImportPath("./api", "./services", "./Files.ts", ".js")
142224// "./api/services/Files.js"
143225```
144226
227+ For context imports, ` GeneratedClientInfo.contextImportPath ` should hold the
228+ fully emitted context import specifier. If ` reactContext.importExtension ` is
229+ configured and ` reactContext.moduleSpecifier ` is path-like, append the extension
230+ unless the specifier already ends with that extension.
231+
232+ For pre-created options imports, ` resolvePrecreatedOptionsImportPath(...) `
233+ should accept the configured ` optionsFactory.importExtension ` . It should keep
234+ bare specifiers unchanged, preserve the existing relative recomposition behavior
235+ for path-like specifiers, and append the configured extension to the emitted
236+ path-like specifier when needed.
237+
238+ Use one shared path-rendering helper for appending configured extensions to
239+ path-like import specifiers. The helper should:
240+
241+ - leave bare specifiers unchanged;
242+ - leave empty extensions unchanged;
243+ - avoid duplicating an extension that is already present;
244+ - strip TypeScript source extensions before appending the configured extension;
245+ - preserve query/hash handling already used by resolved source paths.
246+
145247## Entrypoint Keys And Caching
146248
147- Include ` fileNamePostfix ` and ` importExtension ` in normalized entrypoint keys.
148- Different service-file layouts must not share cached generated metadata or
249+ Include the new normalized extension fields in entrypoint keys:
250+
251+ - ` services.fileNamePostfix ` ;
252+ - ` services.importExtension ` ;
253+ - ` reactContext.importExtension ` ;
254+ - ` optionsFactory.importExtension ` .
255+
256+ Different emitted import layouts must not share cached generated metadata or
149257operation import information.
150258
151259Operation import cache keys should also include the normalized postfix and
@@ -162,13 +270,20 @@ Add focused coverage at three levels.
162270- omitted service fields normalize to ` fileNamePostfix: "Service" ` and
163271 ` importExtension: "" ` ;
164272- explicit ` fileNamePostfix ` and ` importExtension ` are preserved;
165- - normalized keys include both new fields.
273+ - omitted ` reactContext.importExtension ` and
274+ ` optionsFactory.importExtension ` normalize to ` "" ` ;
275+ - explicit ` reactContext.importExtension ` and
276+ ` optionsFactory.importExtension ` are preserved;
277+ - normalized keys include all new fields.
166278
167279` path-rendering.test.ts ` :
168280
169281- service import paths can append ` .js ` ;
170282- configured extension replaces stripped ` .ts ` /` .tsx ` /` .mts ` /` .cts ` source
171283 extensions instead of duplicating them;
284+ - path-like context and options imports can append ` .js ` ;
285+ - bare context and options module specifiers are unchanged;
286+ - existing ` .js ` specifiers do not become ` .js.js ` ;
172287- default empty extension preserves current snapshots.
173288
174289Core transform coverage, in ` create-api-client-fn.test.ts ` unless a more local
@@ -178,9 +293,20 @@ existing case is extended:
178293 ` services: { fileNamePostfix: "", importExtension: ".js" } ` rewrites
179294 ` api.pets.getPets.useQuery() ` to import the operation from
180295 ` ./api/services/Pets.js ` ;
296+ - a context-backed generated factory with
297+ ` reactContext: { moduleSpecifier: "./api/APIClientContext", importExtension: ".js" } `
298+ emits the context import from ` ./api/APIClientContext.js ` ;
181299- existing default snapshots continue to emit ` PetsService ` without an
182300 extension.
183301
302+ Pre-created transform coverage, in ` precreated-api-client.test.ts ` :
303+
304+ - a pre-created client entrypoint with
305+ ` optionsFactory: { moduleSpecifier: "./client-options", importExtension: ".js" } `
306+ emits the options import from ` ./client-options.js ` ;
307+ - a bare options factory module specifier stays bare when ` importExtension ` is
308+ configured.
309+
184310Standalone coverage can be added by extending the project transform smoke test
185311only if the implementation touches standalone-specific behavior. The public
186312standalone config uses the same ` QraftTreeShakeOptions ` type, so core transform
@@ -196,11 +322,18 @@ embedded files API entrypoint with:
196322services : {
197323 fileNamePostfix : " " ,
198324 importExtension : " .js" ,
325+ },
326+ reactContext : {
327+ exportName : " InternalReactAPIClientContext" ,
328+ moduleSpecifier : ` ${filesApiModule }/index ` ,
329+ importExtension : " .js" ,
199330}
200331```
201332
202333This mirrors the generated fixture shape where files live at
203334` services/Files.ts ` and public ESM imports point at ` services/Files.js ` .
335+ The context module also needs an emitted ` .js ` specifier when configured through
336+ a path-like generated-client module.
204337
205338## Success Criteria
206339
@@ -209,5 +342,10 @@ This mirrors the generated fixture shape where files live at
209342- The standalone transform can rewrite
210343 ` generatedEmbeddedCustomContextApi.files.getFileList ` to import
211344 ` getFileList ` from ` ./fixtures/files-api/services/Files.js ` .
345+ - Context-backed rewrites can import a configured path-like React context with
346+ ` .js ` .
347+ - Pre-created client rewrites can import a configured path-like options factory
348+ with ` .js ` .
212349- Existing users with default generated service names continue to get
213- ` *Service ` imports with no explicit extension unless they opt in.
350+ ` *Service ` , context, and options imports with no explicit extension unless
351+ they opt in.
0 commit comments