Skip to content

Commit 13b8c7d

Browse files
committed
docs: extend import extension design
1 parent 40b8e34 commit 13b8c7d

1 file changed

Lines changed: 144 additions & 6 deletions

File tree

docs/superpowers/specs/2026-06-17-tree-shaking-service-file-import-config-design.md

Lines changed: 144 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@ imports `./services/Files.js`, while the tree-shaking transform currently emits
1313
is always the capitalized service property plus `Service`, and it always strips
1414
source 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

1824
For a usage such as:
@@ -42,11 +48,33 @@ That path is wrong when the generated OpenAPI Qraft client was created with a
4248
custom service postfix, for example `--postfix-services ""`, because generated
4349
files 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

4763
Extend `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+
5078
export 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+
61106
export 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+
74122
The motivating fixture can opt in with:
75123

76124
```ts
@@ -86,6 +134,30 @@ This emits:
86134
import { 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

91163
Do 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
100172
the 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+
102180
Do not alter runtime callback selection, generated source ownership checks,
103181
resolver behavior, diagnostics levels, or standalone project file discovery.
104182

@@ -107,7 +185,11 @@ resolver behavior, diagnostics levels, or standalone project file discovery.
107185
Keep defaults in `normalizeServices(...)` so the rest of the transform receives
108186
a 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
113195
ServicesTarget
@@ -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
149257
operation import information.
150258

151259
Operation 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

174289
Core 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+
184310
Standalone coverage can be added by extending the project transform smoke test
185311
only if the implementation touches standalone-specific behavior. The public
186312
standalone config uses the same `QraftTreeShakeOptions` type, so core transform
@@ -196,11 +322,18 @@ embedded files API entrypoint with:
196322
services: {
197323
fileNamePostfix: "",
198324
importExtension: ".js",
325+
},
326+
reactContext: {
327+
exportName: "InternalReactAPIClientContext",
328+
moduleSpecifier: `${filesApiModule}/index`,
329+
importExtension: ".js",
199330
}
200331
```
201332

202333
This 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

Comments
 (0)