@dudousxd/nestjs-codegen 0.26.0 → 0.27.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.
@@ -1,2 +1,2 @@
1
- export { m as ApiClientLayer, n as ApiHeaderContribution, o as ApiModuleDeps, C as CodegenExtension, p as EmittedFile, E as ExtensionContext, L as LeafModel, q as RequestModel, s as RequestShape, t as defineExtension, u as requestShape } from '../index-Bbw73Kkb.cjs';
1
+ export { m as ApiClientLayer, n as ApiHeaderContribution, o as ApiModuleDeps, C as CodegenExtension, p as EmittedFile, E as ExtensionContext, L as LeafModel, q as RequestModel, s as RequestShape, t as defineExtension, u as requestShape } from '../index-Dc4s883u.cjs';
2
2
  import 'ts-morph';
@@ -1,2 +1,2 @@
1
- export { m as ApiClientLayer, n as ApiHeaderContribution, o as ApiModuleDeps, C as CodegenExtension, p as EmittedFile, E as ExtensionContext, L as LeafModel, q as RequestModel, s as RequestShape, t as defineExtension, u as requestShape } from '../index-Bbw73Kkb.js';
1
+ export { m as ApiClientLayer, n as ApiHeaderContribution, o as ApiModuleDeps, C as CodegenExtension, p as EmittedFile, E as ExtensionContext, L as LeafModel, q as RequestModel, s as RequestShape, t as defineExtension, u as requestShape } from '../index-Dc4s883u.js';
2
2
  import 'ts-morph';
@@ -238,9 +238,22 @@ interface UserConfig {
238
238
  validation: ValidationOption;
239
239
  /** Inertia page discovery. Omit when you don't use Inertia. */
240
240
  pages?: {
241
- glob: string;
241
+ /**
242
+ * Page files of the default Inertia app (`InertiaModule.forRoot`), relative to `cwd`. One
243
+ * glob or several; a page is named relative to the static base of the glob that matched it
244
+ * (`inertia/pages/users/Show.tsx` under `inertia/pages/**\/*.tsx` → `users/Show`).
245
+ */
246
+ glob: string | string[];
242
247
  propsExport?: string;
243
- componentNameStrategy?: 'relative-no-ext' | 'kebab' | ((path: string) => string);
248
+ componentNameStrategy?: PageNameStrategy;
249
+ /**
250
+ * Further Inertia apps, one per `InertiaModule.forFeature({ scope })`, keyed by scope name.
251
+ * Each has its own page glob(s); its page names get a prefix (default `'<scope>/'`, so
252
+ * `minimal/pages/Home.tsx` → `minimal/Home`, the name its `@Inertia('minimal/Home')`
253
+ * handlers render). Every page of every scope lands in `InertiaPages`/`InertiaPageName`;
254
+ * `InertiaScopePages` lists each scope's page names.
255
+ */
256
+ scopes?: Record<string, PageScopeConfig>;
244
257
  };
245
258
  contracts?: {
246
259
  /** Glob pattern (relative to cwd) for controller files. Default: `'src/**\/\*.controller.ts'` */
@@ -248,6 +261,11 @@ interface UserConfig {
248
261
  /** Debounce delay in ms before re-running route discovery. Default: `500` */
249
262
  debounceMs?: number;
250
263
  };
264
+ /**
265
+ * @deprecated Has no effect: discovery never read it. For several Inertia apps use
266
+ * `pages.scopes`; for several controller sets use one `contracts.glob` (brace/extglob).
267
+ * A config that sets it gets a warning.
268
+ */
251
269
  scopes?: Record<string, ScopeConfig>;
252
270
  codegen?: {
253
271
  outDir?: string;
@@ -344,14 +362,33 @@ interface UserConfig {
344
362
  */
345
363
  driftGuard?: boolean;
346
364
  }
365
+ /** @deprecated See `UserConfig.scopes`. */
347
366
  interface ScopeConfig {
348
367
  glob: string;
349
368
  prefix?: string;
350
369
  }
370
+ /** How a page file's path (relative to its glob's static base) becomes its component name. */
371
+ type PageNameStrategy = 'relative-no-ext' | 'kebab' | ((path: string) => string);
372
+ /** One more Inertia app (an `InertiaModule.forFeature({ scope })`) — see `pages.scopes`. */
373
+ interface PageScopeConfig {
374
+ /** Page files of this app, relative to `cwd`. */
375
+ glob: string | string[];
376
+ /** Prepended to every page name of this scope. Default `'<scope>/'`; `''` for none. */
377
+ prefix?: string;
378
+ /** Naming strategy for this scope's pages. Default: `pages.componentNameStrategy`. */
379
+ componentNameStrategy?: PageNameStrategy;
380
+ }
381
+ interface ResolvedPageScopeConfig {
382
+ glob: string | string[];
383
+ prefix: string;
384
+ componentNameStrategy: PageNameStrategy;
385
+ }
351
386
  interface ResolvedPagesConfig {
352
- glob: string;
387
+ glob: string | string[];
353
388
  propsExport: string;
354
- componentNameStrategy: 'relative-no-ext' | 'kebab' | ((path: string) => string);
389
+ componentNameStrategy: PageNameStrategy;
390
+ /** Feature scopes (not including the default app), with defaults applied. */
391
+ scopes: Record<string, ResolvedPageScopeConfig>;
355
392
  }
356
393
  interface ResolvedContractsConfig {
357
394
  /** Glob pattern relative to `codegen.cwd` for watching controller files. */
@@ -238,9 +238,22 @@ interface UserConfig {
238
238
  validation: ValidationOption;
239
239
  /** Inertia page discovery. Omit when you don't use Inertia. */
240
240
  pages?: {
241
- glob: string;
241
+ /**
242
+ * Page files of the default Inertia app (`InertiaModule.forRoot`), relative to `cwd`. One
243
+ * glob or several; a page is named relative to the static base of the glob that matched it
244
+ * (`inertia/pages/users/Show.tsx` under `inertia/pages/**\/*.tsx` → `users/Show`).
245
+ */
246
+ glob: string | string[];
242
247
  propsExport?: string;
243
- componentNameStrategy?: 'relative-no-ext' | 'kebab' | ((path: string) => string);
248
+ componentNameStrategy?: PageNameStrategy;
249
+ /**
250
+ * Further Inertia apps, one per `InertiaModule.forFeature({ scope })`, keyed by scope name.
251
+ * Each has its own page glob(s); its page names get a prefix (default `'<scope>/'`, so
252
+ * `minimal/pages/Home.tsx` → `minimal/Home`, the name its `@Inertia('minimal/Home')`
253
+ * handlers render). Every page of every scope lands in `InertiaPages`/`InertiaPageName`;
254
+ * `InertiaScopePages` lists each scope's page names.
255
+ */
256
+ scopes?: Record<string, PageScopeConfig>;
244
257
  };
245
258
  contracts?: {
246
259
  /** Glob pattern (relative to cwd) for controller files. Default: `'src/**\/\*.controller.ts'` */
@@ -248,6 +261,11 @@ interface UserConfig {
248
261
  /** Debounce delay in ms before re-running route discovery. Default: `500` */
249
262
  debounceMs?: number;
250
263
  };
264
+ /**
265
+ * @deprecated Has no effect: discovery never read it. For several Inertia apps use
266
+ * `pages.scopes`; for several controller sets use one `contracts.glob` (brace/extglob).
267
+ * A config that sets it gets a warning.
268
+ */
251
269
  scopes?: Record<string, ScopeConfig>;
252
270
  codegen?: {
253
271
  outDir?: string;
@@ -344,14 +362,33 @@ interface UserConfig {
344
362
  */
345
363
  driftGuard?: boolean;
346
364
  }
365
+ /** @deprecated See `UserConfig.scopes`. */
347
366
  interface ScopeConfig {
348
367
  glob: string;
349
368
  prefix?: string;
350
369
  }
370
+ /** How a page file's path (relative to its glob's static base) becomes its component name. */
371
+ type PageNameStrategy = 'relative-no-ext' | 'kebab' | ((path: string) => string);
372
+ /** One more Inertia app (an `InertiaModule.forFeature({ scope })`) — see `pages.scopes`. */
373
+ interface PageScopeConfig {
374
+ /** Page files of this app, relative to `cwd`. */
375
+ glob: string | string[];
376
+ /** Prepended to every page name of this scope. Default `'<scope>/'`; `''` for none. */
377
+ prefix?: string;
378
+ /** Naming strategy for this scope's pages. Default: `pages.componentNameStrategy`. */
379
+ componentNameStrategy?: PageNameStrategy;
380
+ }
381
+ interface ResolvedPageScopeConfig {
382
+ glob: string | string[];
383
+ prefix: string;
384
+ componentNameStrategy: PageNameStrategy;
385
+ }
351
386
  interface ResolvedPagesConfig {
352
- glob: string;
387
+ glob: string | string[];
353
388
  propsExport: string;
354
- componentNameStrategy: 'relative-no-ext' | 'kebab' | ((path: string) => string);
389
+ componentNameStrategy: PageNameStrategy;
390
+ /** Feature scopes (not including the default app), with defaults applied. */
391
+ scopes: Record<string, ResolvedPageScopeConfig>;
355
392
  }
356
393
  interface ResolvedContractsConfig {
357
394
  /** Glob pattern relative to `codegen.cwd` for watching controller files. */
package/dist/index.cjs CHANGED
@@ -149,12 +149,33 @@ function validateUserConfig(userConfig) {
149
149
  "validation adapter is required \u2014 install @dudousxd/nestjs-codegen-zod and pass zodAdapter, or use @dudousxd/nestjs-codegen-valibot / -arktype"
150
150
  );
151
151
  }
152
- if (userConfig.pages && typeof userConfig.pages.glob !== "string") {
152
+ if (userConfig.pages && !isGlobInput(userConfig.pages.glob)) {
153
153
  throw new ConfigError(
154
- "Config validation failed: `pages.glob` must be a string when `pages` is set"
154
+ "Config validation failed: `pages.glob` must be a string or a non-empty array of strings when `pages` is set"
155
+ );
156
+ }
157
+ for (const [scope, scopeConfig] of Object.entries(userConfig.pages?.scopes ?? {})) {
158
+ if (scope === "default") {
159
+ throw new ConfigError(
160
+ 'Config validation failed: `pages.scopes` cannot declare "default" \u2014 the default app is `pages.glob` (InertiaModule.forRoot).'
161
+ );
162
+ }
163
+ if (!isGlobInput(scopeConfig?.glob)) {
164
+ throw new ConfigError(
165
+ `Config validation failed: \`pages.scopes.${scope}.glob\` must be a string or a non-empty array of strings`
166
+ );
167
+ }
168
+ }
169
+ if (userConfig.scopes && Object.keys(userConfig.scopes).length > 0) {
170
+ console.warn(
171
+ "[nestjs-codegen] The top-level `scopes` option has no effect and is deprecated. For several Inertia apps use `pages.scopes` (one entry per InertiaModule.forFeature scope); for several controller sets use one `contracts.glob` (e.g. a brace glob)."
155
172
  );
156
173
  }
157
174
  }
175
+ function isGlobInput(value) {
176
+ if (typeof value === "string") return true;
177
+ return Array.isArray(value) && value.length > 0 && value.every((glob) => typeof glob === "string");
178
+ }
158
179
  function applyDefaults(userConfig, cwd) {
159
180
  validateUserConfig(userConfig);
160
181
  const outDir = userConfig.codegen?.outDir ? resolveAbsolute(cwd, userConfig.codegen.outDir) : (0, import_node_path.join)(cwd, ".nestjs-codegen");
@@ -181,7 +202,17 @@ function applyDefaults(userConfig, cwd) {
181
202
  pages: userConfig.pages ? {
182
203
  glob: userConfig.pages.glob,
183
204
  propsExport: userConfig.pages.propsExport ?? "ComponentProps",
184
- componentNameStrategy: userConfig.pages.componentNameStrategy ?? "relative-no-ext"
205
+ componentNameStrategy: userConfig.pages.componentNameStrategy ?? "relative-no-ext",
206
+ scopes: Object.fromEntries(
207
+ Object.entries(userConfig.pages.scopes ?? {}).map(([scope, scopeConfig]) => [
208
+ scope,
209
+ {
210
+ glob: scopeConfig.glob,
211
+ prefix: scopeConfig.prefix ?? `${scope}/`,
212
+ componentNameStrategy: scopeConfig.componentNameStrategy ?? userConfig.pages?.componentNameStrategy ?? "relative-no-ext"
213
+ }
214
+ ])
215
+ )
185
216
  } : null,
186
217
  contracts: {
187
218
  glob: userConfig.contracts?.glob ?? "src/**/*.controller.ts",
@@ -258,23 +289,81 @@ var import_promises2 = require("fs/promises");
258
289
  var import_node_path2 = require("path");
259
290
  var import_fast_glob = __toESM(require("fast-glob"), 1);
260
291
  var NON_PAGE_FILE_RE = /\.(?:test|spec|stories|story)\.(?:tsx?|jsx?|vue|svelte)$/i;
292
+ var MAGIC_SEGMENT_RE = /[*?{}[\]()!]/;
293
+ function globStaticBase(glob) {
294
+ const segments = glob.split("/");
295
+ const staticSegments = [];
296
+ for (const segment of segments.slice(0, -1)) {
297
+ if (MAGIC_SEGMENT_RE.test(segment)) break;
298
+ staticSegments.push(segment);
299
+ }
300
+ return staticSegments.join("/");
301
+ }
261
302
  async function discoverPages(opts) {
262
- const allFiles = await (0, import_fast_glob.default)(opts.glob, { cwd: opts.cwd, absolute: true });
263
- const files = allFiles.filter((f) => !NON_PAGE_FILE_RE.test(f));
264
- files.sort();
265
- const globStatic = opts.glob.split("*")[0]?.replace(/\/$/, "") ?? "";
266
- const pagesBase = (0, import_node_path2.join)(opts.cwd, globStatic);
303
+ const globs = typeof opts.glob === "string" ? [opts.glob] : opts.glob;
304
+ const prefix = opts.prefix ?? "";
305
+ const scope = opts.scope ?? "default";
306
+ const seenFiles = /* @__PURE__ */ new Set();
267
307
  const out = [];
268
- for (const file of files) {
269
- const rel = (0, import_node_path2.relative)(opts.cwd, file);
270
- const nameRel = (0, import_node_path2.relative)(pagesBase, file);
271
- const name = computeName(nameRel, opts.componentNameStrategy);
272
- const source = await (0, import_promises2.readFile)(file, "utf8");
273
- const propsSource = extractPropsSource(source, opts.propsExport);
274
- out.push({ name, absolutePath: file, relativePath: rel, propsSource });
308
+ for (const glob of globs) {
309
+ const files = (await (0, import_fast_glob.default)(glob, { cwd: opts.cwd, absolute: true })).filter((f) => !NON_PAGE_FILE_RE.test(f) && !seenFiles.has(f)).sort();
310
+ const pagesBase = (0, import_node_path2.join)(opts.cwd, globStaticBase(glob));
311
+ for (const file of files) {
312
+ seenFiles.add(file);
313
+ const rel = (0, import_node_path2.relative)(opts.cwd, file);
314
+ const nameRel = (0, import_node_path2.relative)(pagesBase, file).split(import_node_path2.sep).join("/");
315
+ const name = `${prefix}${computeName(nameRel, opts.componentNameStrategy)}`;
316
+ const source = await (0, import_promises2.readFile)(file, "utf8");
317
+ const propsSource = extractPropsSource(source, opts.propsExport);
318
+ out.push({ name, absolutePath: file, relativePath: rel, propsSource, scope });
319
+ }
320
+ }
321
+ assertUniqueNames(out);
322
+ return out;
323
+ }
324
+ async function discoverPageScopes(config) {
325
+ const pages = config.pages;
326
+ if (!pages) return [];
327
+ const common = { cwd: config.codegen.cwd, propsExport: pages.propsExport };
328
+ const out = await discoverPages({
329
+ ...common,
330
+ glob: pages.glob,
331
+ componentNameStrategy: pages.componentNameStrategy
332
+ });
333
+ for (const [scope, scopeConfig] of Object.entries(pages.scopes ?? {})) {
334
+ out.push(
335
+ ...await discoverPages({
336
+ ...common,
337
+ glob: scopeConfig.glob,
338
+ componentNameStrategy: scopeConfig.componentNameStrategy,
339
+ prefix: scopeConfig.prefix,
340
+ scope
341
+ })
342
+ );
275
343
  }
344
+ assertUniqueNames(out);
276
345
  return out;
277
346
  }
347
+ function allPageGlobs(config) {
348
+ if (!config.pages) return [];
349
+ const asList = (glob) => typeof glob === "string" ? [glob] : glob;
350
+ return [
351
+ ...asList(config.pages.glob),
352
+ ...Object.values(config.pages.scopes ?? {}).flatMap((scope) => asList(scope.glob))
353
+ ];
354
+ }
355
+ function assertUniqueNames(pages) {
356
+ const byName = /* @__PURE__ */ new Map();
357
+ for (const page of pages) {
358
+ const previous = byName.get(page.name);
359
+ if (previous) {
360
+ throw new Error(
361
+ `Two Inertia pages resolve to the component name "${page.name}": ${previous.relativePath} (scope "${previous.scope}") and ${page.relativePath} (scope "${page.scope}"). Give one of them another name, directory or scope prefix.`
362
+ );
363
+ }
364
+ byName.set(page.name, page);
365
+ }
366
+ }
278
367
  function computeName(rel, strat) {
279
368
  if (typeof strat === "function") return strat(rel);
280
369
  const noExt = rel.replace(/\.(tsx?|vue|svelte)$/, "");
@@ -686,10 +775,11 @@ function resolveModuleSpecifier(moduleSpecifier, sourceFile, project) {
686
775
  }
687
776
  return [];
688
777
  }
689
- function resolveImportedType(name, sourceFile, project) {
778
+ function resolveImportedType(name, sourceFile, project, seen = /* @__PURE__ */ new Set()) {
690
779
  for (const importDecl of sourceFile.getImportDeclarations()) {
691
- const namedImport = importDecl.getNamedImports().find((n) => n.getName() === name);
780
+ const namedImport = importDecl.getNamedImports().find((n) => (n.getAliasNode()?.getText() ?? n.getName()) === name);
692
781
  if (!namedImport) continue;
782
+ const sourceName = namedImport.getName();
693
783
  const moduleSpecifier = importDecl.getModuleSpecifierValue();
694
784
  const candidates = resolveModuleSpecifier(moduleSpecifier, sourceFile, project);
695
785
  for (const candidate of candidates) {
@@ -701,17 +791,17 @@ function resolveImportedType(name, sourceFile, project) {
701
791
  continue;
702
792
  }
703
793
  }
704
- const result = findTypeInFile(name, importedFile);
794
+ const result = findTypeInFile(sourceName, importedFile);
705
795
  if (result) return result;
706
- const viaReExport = resolveReExportedType(name, importedFile, project, /* @__PURE__ */ new Set());
796
+ const viaReExport = resolveReExportedType(sourceName, importedFile, project, /* @__PURE__ */ new Set());
707
797
  if (viaReExport) return viaReExport;
708
798
  }
709
799
  if (candidates.length === 0) {
710
- const viaCompiler = resolveBareSpecifierType(name, importDecl, project);
800
+ const viaCompiler = resolveBareSpecifierType(sourceName, importDecl, project);
711
801
  if (viaCompiler) return viaCompiler;
712
802
  }
713
803
  }
714
- return resolveReExportedType(name, sourceFile, project, /* @__PURE__ */ new Set());
804
+ return resolveReExportedType(name, sourceFile, project, seen);
715
805
  }
716
806
  function resolveBareSpecifierType(name, importDecl, project) {
717
807
  let target;
@@ -726,9 +816,9 @@ function resolveBareSpecifierType(name, importDecl, project) {
726
816
  return resolveReExportedType(name, target, project, /* @__PURE__ */ new Set());
727
817
  }
728
818
  function resolveReExportedType(name, file, project, seen) {
729
- const filePath = file.getFilePath();
730
- if (seen.has(filePath)) return null;
731
- seen.add(filePath);
819
+ const visit = `${file.getFilePath()}#${name}`;
820
+ if (seen.has(visit)) return null;
821
+ seen.add(visit);
732
822
  for (const exportDecl of file.getExportDeclarations()) {
733
823
  const moduleSpecifier = exportDecl.getModuleSpecifierValue();
734
824
  const namedExports = exportDecl.getNamedExports();
@@ -750,7 +840,7 @@ function resolveReExportedType(name, file, project, seen) {
750
840
  const sourceName = namedExports.find((n) => (n.getAliasNode()?.getText() ?? n.getName()) === name)?.getName() ?? name;
751
841
  const local = findTypeInFile(sourceName, file);
752
842
  if (local) return local;
753
- const imported = resolveImportedType(sourceName, file, project);
843
+ const imported = resolveImportedType(sourceName, file, project, seen);
754
844
  if (imported) return imported;
755
845
  }
756
846
  return null;
@@ -4145,28 +4235,28 @@ function renderFetcherRequest(req, binaryResponse) {
4145
4235
  }
4146
4236
  function emitReqHelper() {
4147
4237
  return [
4148
- "/** Awaitable request handle. `await api.x.y({...})` runs the fetch; extensions add query/mutation helpers. */",
4149
- "type __Req<R> = {",
4150
- " then<T1 = R, T2 = never>(",
4238
+ "/** An awaitable request handle: a lazy, memoized `Promise` that also carries `fetch()` and the members extensions add. */",
4239
+ "export type RequestHandle<R, M = {}> = Promise<R> & { fetch(): Promise<R> } & M;",
4240
+ "class __Request<R> extends Promise<R> {",
4241
+ " static override get [Symbol.species](): PromiseConstructor {",
4242
+ " return Promise;",
4243
+ " }",
4244
+ " private __run: () => Promise<R>;",
4245
+ " private __p: Promise<R> | undefined;",
4246
+ " constructor(run: () => Promise<R>) {",
4247
+ " super(() => {});",
4248
+ " this.__run = run;",
4249
+ " }",
4250
+ " override then<T1 = R, T2 = never>(",
4151
4251
  " onfulfilled?: ((value: R) => T1 | PromiseLike<T1>) | null,",
4152
4252
  " onrejected?: ((reason: unknown) => T2 | PromiseLike<T2>) | null,",
4153
- " ): Promise<T1 | T2>;",
4154
- " catch<T = never>(onrejected?: ((reason: unknown) => T | PromiseLike<T>) | null): Promise<R | T>;",
4155
- " finally(onfinally?: (() => void) | null): Promise<R>;",
4156
- " fetch(): Promise<R>;",
4157
- "};",
4158
- "function __req<R>(run: () => Promise<R>): __Req<R> {",
4159
- " let __p: Promise<R> | undefined;",
4160
- " const __promise = () => {",
4161
- " __p ??= run();",
4162
- " return __p;",
4163
- " };",
4164
- " return {",
4165
- " then: (onfulfilled, onrejected) => __promise().then(onfulfilled, onrejected),",
4166
- " catch: (onrejected) => __promise().catch(onrejected),",
4167
- " finally: (onfinally) => __promise().finally(onfinally),",
4168
- " fetch: run,",
4169
- " };",
4253
+ " ): Promise<T1 | T2> {",
4254
+ " this.__p ??= this.__run();",
4255
+ " return this.__p.then(onfulfilled, onrejected);",
4256
+ " }",
4257
+ "}",
4258
+ "function __req<R, M extends object = {}>(run: () => Promise<R>, members?: M): RequestHandle<R, M> {",
4259
+ " return Object.assign(new __Request<R>(run), { fetch: run }, members) as RequestHandle<R, M>;",
4170
4260
  "}",
4171
4261
  ""
4172
4262
  ];
@@ -4184,18 +4274,26 @@ function emitFilterFieldGuard() {
4184
4274
  ];
4185
4275
  }
4186
4276
  function renderLeaf(pad, objKey, req, requestExpr, members, streamExpr) {
4187
- const lines = [`${pad}${objKey}: (input?: ${req.inputType}) => ({`];
4188
- lines.push(`${pad} ...__req<${req.responseType}>(() => ${requestExpr}),`);
4277
+ const memberLines = [];
4189
4278
  if (req.filterFieldsExpr) {
4190
- lines.push(`${pad} filterFields: ${req.filterFieldsExpr},`);
4279
+ memberLines.push(`${pad} filterFields: ${req.filterFieldsExpr},`);
4191
4280
  }
4192
4281
  if (streamExpr) {
4193
- lines.push(`${pad} stream: () => ${streamExpr},`);
4282
+ memberLines.push(`${pad} stream: () => ${streamExpr},`);
4194
4283
  }
4195
4284
  for (const [name, value] of Object.entries(members)) {
4196
- lines.push(`${pad} ${name}: ${value},`);
4197
- }
4198
- lines.push(`${pad}}),`);
4285
+ memberLines.push(`${pad} ${name}: ${value},`);
4286
+ }
4287
+ const body = [requestExpr, ...memberLines].join("\n");
4288
+ const param = /\binput\b/.test(body) ? `input?: ${req.inputType}` : "";
4289
+ const lines = [`${pad}${objKey}: (${param}) =>`];
4290
+ if (memberLines.length === 0) {
4291
+ lines.push(`${pad} __req<${req.responseType}>(() => ${requestExpr}),`);
4292
+ return lines;
4293
+ }
4294
+ lines.push(`${pad} __req(() => ${requestExpr}, {`);
4295
+ lines.push(...memberLines);
4296
+ lines.push(`${pad} }),`);
4199
4297
  return lines;
4200
4298
  }
4201
4299
  function renderStreamExpr(req) {
@@ -4329,6 +4427,23 @@ var EMPTY_PATH_NAMESPACE = [
4329
4427
  function isClientCallable(route) {
4330
4428
  return route.method.toUpperCase() !== "ALL";
4331
4429
  }
4430
+ var ROUTES_IMPORT_PLACEHOLDER = "/* @@routes-import@@ */";
4431
+ var ROUTES_EXPORTS = [
4432
+ // Called, never mentioned in prose: match the call so a comment saying "route" doesn't count.
4433
+ { name: "route", typeOnly: false },
4434
+ { name: "ROUTES", typeOnly: false },
4435
+ { name: "RouteName", typeOnly: true },
4436
+ { name: "ExtractParams", typeOnly: true },
4437
+ { name: "RouteParams", typeOnly: true }
4438
+ ];
4439
+ function finishRoutesImport(lines) {
4440
+ const body = lines.filter((l) => l !== ROUTES_IMPORT_PLACEHOLDER).join("\n");
4441
+ const used = ROUTES_EXPORTS.filter(
4442
+ ({ name }) => new RegExp(name === "route" ? "\\broute\\(" : `\\b${name}\\b`).test(body)
4443
+ ).map(({ name, typeOnly }) => typeOnly ? `type ${name}` : name);
4444
+ const importLine = used.length ? `import { ${used.join(", ")} } from './routes.js';` : null;
4445
+ return lines.flatMap((l) => l === ROUTES_IMPORT_PLACEHOLDER ? importLine ? [importLine] : [] : [l]).join("\n");
4446
+ }
4332
4447
  function buildApiFile(routes, outDir, opts = {}) {
4333
4448
  const fetcherImportPath = opts.fetcherImportPath;
4334
4449
  const serialization = opts.serialization ?? "json";
@@ -4388,9 +4503,7 @@ function buildApiFile(routes, outDir, opts = {}) {
4388
4503
  for (const imp of ext.apiHeader?.(ctx)?.imports ?? []) pushImport(imp);
4389
4504
  }
4390
4505
  lines.push(...extImports);
4391
- lines.push(
4392
- "import { route, ROUTES, type RouteName, type ExtractParams, type RouteParams } from './routes.js';"
4393
- );
4506
+ lines.push(ROUTES_IMPORT_PLACEHOLDER);
4394
4507
  const runtimeImport = fetcherImportPath ?? "@dudousxd/nestjs-client";
4395
4508
  lines.push(`import type { Fetcher } from '${runtimeImport}';`);
4396
4509
  if (serialization === "json" && contracted.length > 0) {
@@ -4441,7 +4554,7 @@ function buildApiFile(routes, outDir, opts = {}) {
4441
4554
  lines.push(...statements, "");
4442
4555
  }
4443
4556
  }
4444
- return lines.join("\n");
4557
+ return finishRoutesImport(lines);
4445
4558
  }
4446
4559
  const tree = /* @__PURE__ */ new Map();
4447
4560
  for (const r of contracted) {
@@ -4494,7 +4607,7 @@ function buildApiFile(routes, outDir, opts = {}) {
4494
4607
  lines.push(...statements, "");
4495
4608
  }
4496
4609
  }
4497
- return lines.join("\n");
4610
+ return finishRoutesImport(lines);
4498
4611
  }
4499
4612
 
4500
4613
  // src/emit/emit-cache.ts
@@ -4507,6 +4620,7 @@ async function emitCache(pages, outDir) {
4507
4620
  const s = await (0, import_promises4.stat)(p.absolutePath);
4508
4621
  return {
4509
4622
  name: p.name,
4623
+ scope: p.scope ?? "default",
4510
4624
  relativePath: p.relativePath,
4511
4625
  mtime: s.mtime.toISOString()
4512
4626
  };
@@ -5301,10 +5415,11 @@ async function emitPages(pages, outDir, _options = {}) {
5301
5415
  }).join("\n");
5302
5416
  const propsHelper = "\nexport type InertiaProps<K extends InertiaPageName> = import('@dudousxd/nestjs-inertia').InertiaPages[K];\n";
5303
5417
  const sharedPropsBlock = buildSharedPropsBlock(_options.sharedProps ?? null);
5418
+ const scopesBlock = buildScopesBlock(pages);
5304
5419
  const content = `// Generated by @dudousxd/nestjs-codegen. Do not edit.
5305
5420
 
5306
5421
  export type InertiaPageName = ${pageNameUnion};
5307
- ${propsHelper}
5422
+ ${propsHelper}${scopesBlock}
5308
5423
  declare module '@dudousxd/nestjs-inertia' {
5309
5424
  interface InertiaPages {
5310
5425
  ${augBody}
@@ -5313,6 +5428,31 @@ ${sharedPropsBlock}}
5313
5428
  `;
5314
5429
  await (0, import_promises9.writeFile)((0, import_node_path13.join)(outDir, "pages.d.ts"), content, "utf8");
5315
5430
  }
5431
+ function buildScopesBlock(pages) {
5432
+ const byScope = /* @__PURE__ */ new Map();
5433
+ for (const page of pages) {
5434
+ const scope = page.scope ?? "default";
5435
+ const names = byScope.get(scope) ?? [];
5436
+ names.push(JSON.stringify(page.name));
5437
+ byScope.set(scope, names);
5438
+ }
5439
+ if (![...byScope.keys()].some((scope) => scope !== "default")) return "";
5440
+ if (!byScope.has("default")) byScope.set("default", []);
5441
+ const scopes = ["default", ...[...byScope.keys()].filter((s) => s !== "default")];
5442
+ const body = scopes.map((scope) => {
5443
+ const key = needsQuotes(scope) ? JSON.stringify(scope) : scope;
5444
+ const names = byScope.get(scope) ?? [];
5445
+ return ` ${key}: ${names.length ? names.join(" | ") : "never"};`;
5446
+ }).join("\n");
5447
+ return `
5448
+ /** The page names of each Inertia app: \`default\` (forRoot) and every \`pages.scopes\` entry. */
5449
+ export interface InertiaScopePages {
5450
+ ${body}
5451
+ }
5452
+
5453
+ export type InertiaScopePageName<S extends keyof InertiaScopePages> = InertiaScopePages[S];
5454
+ `;
5455
+ }
5316
5456
  function buildSharedPropsBlock(sharedProps) {
5317
5457
  if (!sharedProps) return "";
5318
5458
  if (sharedProps.isImportRef) {
@@ -5520,8 +5660,7 @@ function diffConfigKeyHashes(previous, current) {
5520
5660
  return [...keys].filter((key) => previous[key] !== current[key]).sort();
5521
5661
  }
5522
5662
  async function discoverInputFiles(config) {
5523
- const globs = [config.contracts.glob, config.forms.watch];
5524
- if (config.pages) globs.push(config.pages.glob);
5663
+ const globs = [config.contracts.glob, config.forms.watch, ...allPageGlobs(config)];
5525
5664
  const cwd = config.codegen.cwd;
5526
5665
  const matched = await (0, import_fast_glob3.default)(globs, { cwd, absolute: true, onlyFiles: true });
5527
5666
  return [...new Set(matched)].sort();
@@ -5652,12 +5791,7 @@ async function generate(config, inputRoutes = [], entryPoint = "cli") {
5652
5791
  }
5653
5792
  if (config.pages) {
5654
5793
  const pagesConfig = config.pages;
5655
- const pages = await discoverPages({
5656
- glob: pagesConfig.glob,
5657
- cwd: config.codegen.cwd,
5658
- propsExport: pagesConfig.propsExport,
5659
- componentNameStrategy: pagesConfig.componentNameStrategy
5660
- });
5794
+ const pages = await discoverPageScopes(config);
5661
5795
  const sharedProps = discoverSharedPropsFromConfig(config);
5662
5796
  await emitPages(pages, config.codegen.outDir, {
5663
5797
  propsExport: pagesConfig.propsExport,
@@ -5832,12 +5966,17 @@ async function watch(config, onChange, options = {}) {
5832
5966
  await runInitialPass();
5833
5967
  }
5834
5968
  let pagesDebounceTimer;
5835
- const pagesGlob = config.pages?.glob ?? ".nestjs-codegen-no-pages";
5836
- const pagesWatcher = import_chokidar.default.watch((0, import_node_path18.join)(config.codegen.cwd, pagesGlob), {
5837
- ignoreInitial: true,
5838
- persistent: true,
5839
- awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
5840
- });
5969
+ const pageGlobs = allPageGlobs(config);
5970
+ const pagesWatcher = import_chokidar.default.watch(
5971
+ (pageGlobs.length ? pageGlobs : [".nestjs-codegen-no-pages"]).map(
5972
+ (glob) => (0, import_node_path18.join)(config.codegen.cwd, glob)
5973
+ ),
5974
+ {
5975
+ ignoreInitial: true,
5976
+ persistent: true,
5977
+ awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
5978
+ }
5979
+ );
5841
5980
  function schedulePagesRegenerate() {
5842
5981
  if (pagesDebounceTimer !== void 0) {
5843
5982
  clearTimeout(pagesDebounceTimer);
@@ -6000,7 +6139,7 @@ function createChainModuleRenderer(opts) {
6000
6139
  }
6001
6140
 
6002
6141
  // src/index.ts
6003
- var VERSION = "0.26.0";
6142
+ var VERSION = "0.27.1";
6004
6143
  // Annotate the CommonJS export names for ESM import in node:
6005
6144
  0 && (module.exports = {
6006
6145
  CodegenError,