@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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,43 @@
1
1
  # @dudousxd/nestjs-codegen
2
2
 
3
+ ## 0.27.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [#103](https://github.com/DavideCarvalho/nestjs-codegen/pull/103) [`9489714`](https://github.com/DavideCarvalho/nestjs-codegen/commit/94897149bb7133c61de43d66ab6c1bdc5de98cd9) Thanks [@DavideCarvalho](https://github.com/DavideCarvalho)! - Type discovery no longer overflows the stack on bundled declaration files. A `.d.ts` chunk that imports a type under an alias and re-publishes it bare (`import { p as ToolKind } from './chunk'; export { ToolKind }` — what tsup/rollup emit) used to be matched by the import's source name, miss, and re-enter the same file with a fresh cycle guard until `Maximum call stack size exceeded` aborted the whole `codegen` run (seen with `@dudousxd/nestjs-agent-core` 0.29 through `@dudousxd/nestjs-agent-codegen`). Imports are now matched by their local name and followed under their source name, and the cycle guard is carried through the walk (keyed by file and name).
8
+
9
+ ## 0.27.0
10
+
11
+ ### Minor Changes
12
+
13
+ - [#101](https://github.com/DavideCarvalho/nestjs-codegen/pull/101) [`eb98ec8`](https://github.com/DavideCarvalho/nestjs-codegen/commit/eb98ec87be042b56a068bff9d4d71c35b6e67283) Thanks [@DavideCarvalho](https://github.com/DavideCarvalho)! - Several page globs and several Inertia apps (scopes).
14
+
15
+ - `pages.glob` takes one glob or an array. Each page is named relative to the static base of
16
+ the glob that matched it (brace globs included: the base stops at the first segment with
17
+ glob syntax), and two files resolving to the same page name are an error.
18
+ - `pages.scopes` declares more Inertia apps, one per `InertiaModule.forFeature({ scope })`:
19
+ `{ glob, prefix?, componentNameStrategy? }`. A scope's page names get the prefix
20
+ `'<scope>/'` by default (`minimal/pages/Home.tsx` → `minimal/Home`). Every page lands in
21
+ `InertiaPages`/`InertiaPageName`; `pages.d.ts` also exports `InertiaScopePages` and
22
+ `InertiaScopePageName<S>`, `components.json` records each page's `scope`, and the watcher and
23
+ the skip-if-unchanged hash cover every scope's pages.
24
+ - The top-level `scopes` option never had an effect; it is deprecated and now warns.
25
+
26
+ - [#100](https://github.com/DavideCarvalho/nestjs-codegen/pull/100) [`1cc8944`](https://github.com/DavideCarvalho/nestjs-codegen/commit/1cc8944126b794cdd5748e2417f448cd1e157395) Thanks [@DavideCarvalho](https://github.com/DavideCarvalho)! - Request handles are real Promises, and `api.ts` compiles under strict compiler flags.
27
+
28
+ - A generated leaf call (`api.users.create({ body })`) now returns a `RequestHandle<R>`: a
29
+ lazy, memoized `Promise<R>` that also carries `fetch()` and the extension members
30
+ (`queryKey`, `queryOptions`, `mutationOptions`, …). It can be returned from a TanStack
31
+ `mutationFn`/`queryFn`, passed to `Promise.all` or assigned to `Promise<T>` without calling
32
+ `.fetch()` first. Nothing is sent until it is awaited, so building `queryOptions()` or
33
+ reading `queryKey()` never fires a request. `.fetch()` keeps working.
34
+ - `api.ts` imports from `routes.ts` only the names it uses (no unused `ROUTES`/`RouteName`/…),
35
+ a leaf with no input to read takes no parameter, and `infiniteQueryOptions()` no longer
36
+ passes `getPreviousPageParam: undefined`. The output is clean under `noUnusedLocals`,
37
+ `noUnusedParameters`, `exactOptionalPropertyTypes`, `noImplicitOverride`,
38
+ `noUncheckedIndexedAccess` and `noPropertyAccessFromIndexSignature`; a fixture test compiles
39
+ it with all of them.
40
+
3
41
  ## 0.26.0
4
42
 
5
43
  ### Minor Changes
package/README.md CHANGED
@@ -53,8 +53,8 @@ export class AppModule {}
53
53
  ```
54
54
 
55
55
  The generated `api.ts` exports a `createApi(fetcher)` factory — create the client once,
56
- injecting your fetcher. Each endpoint is a **unified awaitable handle**: `await` it to run
57
- the request.
56
+ injecting your fetcher. Each endpoint is a **unified awaitable handle**: a lazy, memoized
57
+ `Promise` — `await` it (or return it from a TanStack `mutationFn`) to run the request.
58
58
 
59
59
  ```ts title="src/lib/api.ts"
60
60
  import { createApi } from '../generated/api';
package/dist/cli/main.cjs CHANGED
@@ -117,12 +117,33 @@ function validateUserConfig(userConfig) {
117
117
  "validation adapter is required \u2014 install @dudousxd/nestjs-codegen-zod and pass zodAdapter, or use @dudousxd/nestjs-codegen-valibot / -arktype"
118
118
  );
119
119
  }
120
- if (userConfig.pages && typeof userConfig.pages.glob !== "string") {
120
+ if (userConfig.pages && !isGlobInput(userConfig.pages.glob)) {
121
121
  throw new ConfigError(
122
- "Config validation failed: `pages.glob` must be a string when `pages` is set"
122
+ "Config validation failed: `pages.glob` must be a string or a non-empty array of strings when `pages` is set"
123
+ );
124
+ }
125
+ for (const [scope, scopeConfig] of Object.entries(userConfig.pages?.scopes ?? {})) {
126
+ if (scope === "default") {
127
+ throw new ConfigError(
128
+ 'Config validation failed: `pages.scopes` cannot declare "default" \u2014 the default app is `pages.glob` (InertiaModule.forRoot).'
129
+ );
130
+ }
131
+ if (!isGlobInput(scopeConfig?.glob)) {
132
+ throw new ConfigError(
133
+ `Config validation failed: \`pages.scopes.${scope}.glob\` must be a string or a non-empty array of strings`
134
+ );
135
+ }
136
+ }
137
+ if (userConfig.scopes && Object.keys(userConfig.scopes).length > 0) {
138
+ console.warn(
139
+ "[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)."
123
140
  );
124
141
  }
125
142
  }
143
+ function isGlobInput(value) {
144
+ if (typeof value === "string") return true;
145
+ return Array.isArray(value) && value.length > 0 && value.every((glob) => typeof glob === "string");
146
+ }
126
147
  function applyDefaults(userConfig, cwd) {
127
148
  validateUserConfig(userConfig);
128
149
  const outDir = userConfig.codegen?.outDir ? resolveAbsolute(cwd, userConfig.codegen.outDir) : (0, import_node_path.join)(cwd, ".nestjs-codegen");
@@ -149,7 +170,17 @@ function applyDefaults(userConfig, cwd) {
149
170
  pages: userConfig.pages ? {
150
171
  glob: userConfig.pages.glob,
151
172
  propsExport: userConfig.pages.propsExport ?? "ComponentProps",
152
- componentNameStrategy: userConfig.pages.componentNameStrategy ?? "relative-no-ext"
173
+ componentNameStrategy: userConfig.pages.componentNameStrategy ?? "relative-no-ext",
174
+ scopes: Object.fromEntries(
175
+ Object.entries(userConfig.pages.scopes ?? {}).map(([scope, scopeConfig]) => [
176
+ scope,
177
+ {
178
+ glob: scopeConfig.glob,
179
+ prefix: scopeConfig.prefix ?? `${scope}/`,
180
+ componentNameStrategy: scopeConfig.componentNameStrategy ?? userConfig.pages?.componentNameStrategy ?? "relative-no-ext"
181
+ }
182
+ ])
183
+ )
153
184
  } : null,
154
185
  contracts: {
155
186
  glob: userConfig.contracts?.glob ?? "src/**/*.controller.ts",
@@ -226,23 +257,81 @@ var import_promises2 = require("fs/promises");
226
257
  var import_node_path2 = require("path");
227
258
  var import_fast_glob = __toESM(require("fast-glob"), 1);
228
259
  var NON_PAGE_FILE_RE = /\.(?:test|spec|stories|story)\.(?:tsx?|jsx?|vue|svelte)$/i;
260
+ var MAGIC_SEGMENT_RE = /[*?{}[\]()!]/;
261
+ function globStaticBase(glob) {
262
+ const segments = glob.split("/");
263
+ const staticSegments = [];
264
+ for (const segment of segments.slice(0, -1)) {
265
+ if (MAGIC_SEGMENT_RE.test(segment)) break;
266
+ staticSegments.push(segment);
267
+ }
268
+ return staticSegments.join("/");
269
+ }
229
270
  async function discoverPages(opts) {
230
- const allFiles = await (0, import_fast_glob.default)(opts.glob, { cwd: opts.cwd, absolute: true });
231
- const files = allFiles.filter((f) => !NON_PAGE_FILE_RE.test(f));
232
- files.sort();
233
- const globStatic = opts.glob.split("*")[0]?.replace(/\/$/, "") ?? "";
234
- const pagesBase = (0, import_node_path2.join)(opts.cwd, globStatic);
271
+ const globs = typeof opts.glob === "string" ? [opts.glob] : opts.glob;
272
+ const prefix = opts.prefix ?? "";
273
+ const scope = opts.scope ?? "default";
274
+ const seenFiles = /* @__PURE__ */ new Set();
235
275
  const out = [];
236
- for (const file of files) {
237
- const rel = (0, import_node_path2.relative)(opts.cwd, file);
238
- const nameRel = (0, import_node_path2.relative)(pagesBase, file);
239
- const name = computeName(nameRel, opts.componentNameStrategy);
240
- const source = await (0, import_promises2.readFile)(file, "utf8");
241
- const propsSource = extractPropsSource(source, opts.propsExport);
242
- out.push({ name, absolutePath: file, relativePath: rel, propsSource });
276
+ for (const glob of globs) {
277
+ 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();
278
+ const pagesBase = (0, import_node_path2.join)(opts.cwd, globStaticBase(glob));
279
+ for (const file of files) {
280
+ seenFiles.add(file);
281
+ const rel = (0, import_node_path2.relative)(opts.cwd, file);
282
+ const nameRel = (0, import_node_path2.relative)(pagesBase, file).split(import_node_path2.sep).join("/");
283
+ const name = `${prefix}${computeName(nameRel, opts.componentNameStrategy)}`;
284
+ const source = await (0, import_promises2.readFile)(file, "utf8");
285
+ const propsSource = extractPropsSource(source, opts.propsExport);
286
+ out.push({ name, absolutePath: file, relativePath: rel, propsSource, scope });
287
+ }
288
+ }
289
+ assertUniqueNames(out);
290
+ return out;
291
+ }
292
+ async function discoverPageScopes(config) {
293
+ const pages = config.pages;
294
+ if (!pages) return [];
295
+ const common = { cwd: config.codegen.cwd, propsExport: pages.propsExport };
296
+ const out = await discoverPages({
297
+ ...common,
298
+ glob: pages.glob,
299
+ componentNameStrategy: pages.componentNameStrategy
300
+ });
301
+ for (const [scope, scopeConfig] of Object.entries(pages.scopes ?? {})) {
302
+ out.push(
303
+ ...await discoverPages({
304
+ ...common,
305
+ glob: scopeConfig.glob,
306
+ componentNameStrategy: scopeConfig.componentNameStrategy,
307
+ prefix: scopeConfig.prefix,
308
+ scope
309
+ })
310
+ );
243
311
  }
312
+ assertUniqueNames(out);
244
313
  return out;
245
314
  }
315
+ function allPageGlobs(config) {
316
+ if (!config.pages) return [];
317
+ const asList = (glob) => typeof glob === "string" ? [glob] : glob;
318
+ return [
319
+ ...asList(config.pages.glob),
320
+ ...Object.values(config.pages.scopes ?? {}).flatMap((scope) => asList(scope.glob))
321
+ ];
322
+ }
323
+ function assertUniqueNames(pages) {
324
+ const byName = /* @__PURE__ */ new Map();
325
+ for (const page of pages) {
326
+ const previous = byName.get(page.name);
327
+ if (previous) {
328
+ throw new Error(
329
+ `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.`
330
+ );
331
+ }
332
+ byName.set(page.name, page);
333
+ }
334
+ }
246
335
  function computeName(rel, strat) {
247
336
  if (typeof strat === "function") return strat(rel);
248
337
  const noExt = rel.replace(/\.(tsx?|vue|svelte)$/, "");
@@ -654,10 +743,11 @@ function resolveModuleSpecifier(moduleSpecifier, sourceFile, project) {
654
743
  }
655
744
  return [];
656
745
  }
657
- function resolveImportedType(name, sourceFile, project) {
746
+ function resolveImportedType(name, sourceFile, project, seen = /* @__PURE__ */ new Set()) {
658
747
  for (const importDecl of sourceFile.getImportDeclarations()) {
659
- const namedImport = importDecl.getNamedImports().find((n) => n.getName() === name);
748
+ const namedImport = importDecl.getNamedImports().find((n) => (n.getAliasNode()?.getText() ?? n.getName()) === name);
660
749
  if (!namedImport) continue;
750
+ const sourceName = namedImport.getName();
661
751
  const moduleSpecifier = importDecl.getModuleSpecifierValue();
662
752
  const candidates = resolveModuleSpecifier(moduleSpecifier, sourceFile, project);
663
753
  for (const candidate of candidates) {
@@ -669,17 +759,17 @@ function resolveImportedType(name, sourceFile, project) {
669
759
  continue;
670
760
  }
671
761
  }
672
- const result = findTypeInFile(name, importedFile);
762
+ const result = findTypeInFile(sourceName, importedFile);
673
763
  if (result) return result;
674
- const viaReExport = resolveReExportedType(name, importedFile, project, /* @__PURE__ */ new Set());
764
+ const viaReExport = resolveReExportedType(sourceName, importedFile, project, /* @__PURE__ */ new Set());
675
765
  if (viaReExport) return viaReExport;
676
766
  }
677
767
  if (candidates.length === 0) {
678
- const viaCompiler = resolveBareSpecifierType(name, importDecl, project);
768
+ const viaCompiler = resolveBareSpecifierType(sourceName, importDecl, project);
679
769
  if (viaCompiler) return viaCompiler;
680
770
  }
681
771
  }
682
- return resolveReExportedType(name, sourceFile, project, /* @__PURE__ */ new Set());
772
+ return resolveReExportedType(name, sourceFile, project, seen);
683
773
  }
684
774
  function resolveBareSpecifierType(name, importDecl, project) {
685
775
  let target;
@@ -694,9 +784,9 @@ function resolveBareSpecifierType(name, importDecl, project) {
694
784
  return resolveReExportedType(name, target, project, /* @__PURE__ */ new Set());
695
785
  }
696
786
  function resolveReExportedType(name, file, project, seen) {
697
- const filePath = file.getFilePath();
698
- if (seen.has(filePath)) return null;
699
- seen.add(filePath);
787
+ const visit = `${file.getFilePath()}#${name}`;
788
+ if (seen.has(visit)) return null;
789
+ seen.add(visit);
700
790
  for (const exportDecl of file.getExportDeclarations()) {
701
791
  const moduleSpecifier = exportDecl.getModuleSpecifierValue();
702
792
  const namedExports = exportDecl.getNamedExports();
@@ -718,7 +808,7 @@ function resolveReExportedType(name, file, project, seen) {
718
808
  const sourceName = namedExports.find((n) => (n.getAliasNode()?.getText() ?? n.getName()) === name)?.getName() ?? name;
719
809
  const local = findTypeInFile(sourceName, file);
720
810
  if (local) return local;
721
- const imported = resolveImportedType(sourceName, file, project);
811
+ const imported = resolveImportedType(sourceName, file, project, seen);
722
812
  if (imported) return imported;
723
813
  }
724
814
  return null;
@@ -4113,28 +4203,28 @@ function renderFetcherRequest(req, binaryResponse) {
4113
4203
  }
4114
4204
  function emitReqHelper() {
4115
4205
  return [
4116
- "/** Awaitable request handle. `await api.x.y({...})` runs the fetch; extensions add query/mutation helpers. */",
4117
- "type __Req<R> = {",
4118
- " then<T1 = R, T2 = never>(",
4206
+ "/** An awaitable request handle: a lazy, memoized `Promise` that also carries `fetch()` and the members extensions add. */",
4207
+ "export type RequestHandle<R, M = {}> = Promise<R> & { fetch(): Promise<R> } & M;",
4208
+ "class __Request<R> extends Promise<R> {",
4209
+ " static override get [Symbol.species](): PromiseConstructor {",
4210
+ " return Promise;",
4211
+ " }",
4212
+ " private __run: () => Promise<R>;",
4213
+ " private __p: Promise<R> | undefined;",
4214
+ " constructor(run: () => Promise<R>) {",
4215
+ " super(() => {});",
4216
+ " this.__run = run;",
4217
+ " }",
4218
+ " override then<T1 = R, T2 = never>(",
4119
4219
  " onfulfilled?: ((value: R) => T1 | PromiseLike<T1>) | null,",
4120
4220
  " onrejected?: ((reason: unknown) => T2 | PromiseLike<T2>) | null,",
4121
- " ): Promise<T1 | T2>;",
4122
- " catch<T = never>(onrejected?: ((reason: unknown) => T | PromiseLike<T>) | null): Promise<R | T>;",
4123
- " finally(onfinally?: (() => void) | null): Promise<R>;",
4124
- " fetch(): Promise<R>;",
4125
- "};",
4126
- "function __req<R>(run: () => Promise<R>): __Req<R> {",
4127
- " let __p: Promise<R> | undefined;",
4128
- " const __promise = () => {",
4129
- " __p ??= run();",
4130
- " return __p;",
4131
- " };",
4132
- " return {",
4133
- " then: (onfulfilled, onrejected) => __promise().then(onfulfilled, onrejected),",
4134
- " catch: (onrejected) => __promise().catch(onrejected),",
4135
- " finally: (onfinally) => __promise().finally(onfinally),",
4136
- " fetch: run,",
4137
- " };",
4221
+ " ): Promise<T1 | T2> {",
4222
+ " this.__p ??= this.__run();",
4223
+ " return this.__p.then(onfulfilled, onrejected);",
4224
+ " }",
4225
+ "}",
4226
+ "function __req<R, M extends object = {}>(run: () => Promise<R>, members?: M): RequestHandle<R, M> {",
4227
+ " return Object.assign(new __Request<R>(run), { fetch: run }, members) as RequestHandle<R, M>;",
4138
4228
  "}",
4139
4229
  ""
4140
4230
  ];
@@ -4152,18 +4242,26 @@ function emitFilterFieldGuard() {
4152
4242
  ];
4153
4243
  }
4154
4244
  function renderLeaf(pad, objKey, req, requestExpr, members, streamExpr) {
4155
- const lines = [`${pad}${objKey}: (input?: ${req.inputType}) => ({`];
4156
- lines.push(`${pad} ...__req<${req.responseType}>(() => ${requestExpr}),`);
4245
+ const memberLines = [];
4157
4246
  if (req.filterFieldsExpr) {
4158
- lines.push(`${pad} filterFields: ${req.filterFieldsExpr},`);
4247
+ memberLines.push(`${pad} filterFields: ${req.filterFieldsExpr},`);
4159
4248
  }
4160
4249
  if (streamExpr) {
4161
- lines.push(`${pad} stream: () => ${streamExpr},`);
4250
+ memberLines.push(`${pad} stream: () => ${streamExpr},`);
4162
4251
  }
4163
4252
  for (const [name, value] of Object.entries(members)) {
4164
- lines.push(`${pad} ${name}: ${value},`);
4165
- }
4166
- lines.push(`${pad}}),`);
4253
+ memberLines.push(`${pad} ${name}: ${value},`);
4254
+ }
4255
+ const body = [requestExpr, ...memberLines].join("\n");
4256
+ const param = /\binput\b/.test(body) ? `input?: ${req.inputType}` : "";
4257
+ const lines = [`${pad}${objKey}: (${param}) =>`];
4258
+ if (memberLines.length === 0) {
4259
+ lines.push(`${pad} __req<${req.responseType}>(() => ${requestExpr}),`);
4260
+ return lines;
4261
+ }
4262
+ lines.push(`${pad} __req(() => ${requestExpr}, {`);
4263
+ lines.push(...memberLines);
4264
+ lines.push(`${pad} }),`);
4167
4265
  return lines;
4168
4266
  }
4169
4267
  function renderStreamExpr(req) {
@@ -4297,6 +4395,23 @@ var EMPTY_PATH_NAMESPACE = [
4297
4395
  function isClientCallable(route) {
4298
4396
  return route.method.toUpperCase() !== "ALL";
4299
4397
  }
4398
+ var ROUTES_IMPORT_PLACEHOLDER = "/* @@routes-import@@ */";
4399
+ var ROUTES_EXPORTS = [
4400
+ // Called, never mentioned in prose: match the call so a comment saying "route" doesn't count.
4401
+ { name: "route", typeOnly: false },
4402
+ { name: "ROUTES", typeOnly: false },
4403
+ { name: "RouteName", typeOnly: true },
4404
+ { name: "ExtractParams", typeOnly: true },
4405
+ { name: "RouteParams", typeOnly: true }
4406
+ ];
4407
+ function finishRoutesImport(lines) {
4408
+ const body = lines.filter((l) => l !== ROUTES_IMPORT_PLACEHOLDER).join("\n");
4409
+ const used = ROUTES_EXPORTS.filter(
4410
+ ({ name }) => new RegExp(name === "route" ? "\\broute\\(" : `\\b${name}\\b`).test(body)
4411
+ ).map(({ name, typeOnly }) => typeOnly ? `type ${name}` : name);
4412
+ const importLine = used.length ? `import { ${used.join(", ")} } from './routes.js';` : null;
4413
+ return lines.flatMap((l) => l === ROUTES_IMPORT_PLACEHOLDER ? importLine ? [importLine] : [] : [l]).join("\n");
4414
+ }
4300
4415
  function buildApiFile(routes, outDir, opts = {}) {
4301
4416
  const fetcherImportPath = opts.fetcherImportPath;
4302
4417
  const serialization = opts.serialization ?? "json";
@@ -4356,9 +4471,7 @@ function buildApiFile(routes, outDir, opts = {}) {
4356
4471
  for (const imp of ext.apiHeader?.(ctx)?.imports ?? []) pushImport(imp);
4357
4472
  }
4358
4473
  lines.push(...extImports);
4359
- lines.push(
4360
- "import { route, ROUTES, type RouteName, type ExtractParams, type RouteParams } from './routes.js';"
4361
- );
4474
+ lines.push(ROUTES_IMPORT_PLACEHOLDER);
4362
4475
  const runtimeImport = fetcherImportPath ?? "@dudousxd/nestjs-client";
4363
4476
  lines.push(`import type { Fetcher } from '${runtimeImport}';`);
4364
4477
  if (serialization === "json" && contracted.length > 0) {
@@ -4409,7 +4522,7 @@ function buildApiFile(routes, outDir, opts = {}) {
4409
4522
  lines.push(...statements, "");
4410
4523
  }
4411
4524
  }
4412
- return lines.join("\n");
4525
+ return finishRoutesImport(lines);
4413
4526
  }
4414
4527
  const tree = /* @__PURE__ */ new Map();
4415
4528
  for (const r of contracted) {
@@ -4462,7 +4575,7 @@ function buildApiFile(routes, outDir, opts = {}) {
4462
4575
  lines.push(...statements, "");
4463
4576
  }
4464
4577
  }
4465
- return lines.join("\n");
4578
+ return finishRoutesImport(lines);
4466
4579
  }
4467
4580
 
4468
4581
  // src/emit/emit-cache.ts
@@ -4475,6 +4588,7 @@ async function emitCache(pages, outDir) {
4475
4588
  const s = await (0, import_promises4.stat)(p.absolutePath);
4476
4589
  return {
4477
4590
  name: p.name,
4591
+ scope: p.scope ?? "default",
4478
4592
  relativePath: p.relativePath,
4479
4593
  mtime: s.mtime.toISOString()
4480
4594
  };
@@ -5266,10 +5380,11 @@ async function emitPages(pages, outDir, _options = {}) {
5266
5380
  }).join("\n");
5267
5381
  const propsHelper = "\nexport type InertiaProps<K extends InertiaPageName> = import('@dudousxd/nestjs-inertia').InertiaPages[K];\n";
5268
5382
  const sharedPropsBlock = buildSharedPropsBlock(_options.sharedProps ?? null);
5383
+ const scopesBlock = buildScopesBlock(pages);
5269
5384
  const content = `// Generated by @dudousxd/nestjs-codegen. Do not edit.
5270
5385
 
5271
5386
  export type InertiaPageName = ${pageNameUnion};
5272
- ${propsHelper}
5387
+ ${propsHelper}${scopesBlock}
5273
5388
  declare module '@dudousxd/nestjs-inertia' {
5274
5389
  interface InertiaPages {
5275
5390
  ${augBody}
@@ -5278,6 +5393,31 @@ ${sharedPropsBlock}}
5278
5393
  `;
5279
5394
  await (0, import_promises9.writeFile)((0, import_node_path13.join)(outDir, "pages.d.ts"), content, "utf8");
5280
5395
  }
5396
+ function buildScopesBlock(pages) {
5397
+ const byScope = /* @__PURE__ */ new Map();
5398
+ for (const page of pages) {
5399
+ const scope = page.scope ?? "default";
5400
+ const names = byScope.get(scope) ?? [];
5401
+ names.push(JSON.stringify(page.name));
5402
+ byScope.set(scope, names);
5403
+ }
5404
+ if (![...byScope.keys()].some((scope) => scope !== "default")) return "";
5405
+ if (!byScope.has("default")) byScope.set("default", []);
5406
+ const scopes = ["default", ...[...byScope.keys()].filter((s) => s !== "default")];
5407
+ const body = scopes.map((scope) => {
5408
+ const key = needsQuotes(scope) ? JSON.stringify(scope) : scope;
5409
+ const names = byScope.get(scope) ?? [];
5410
+ return ` ${key}: ${names.length ? names.join(" | ") : "never"};`;
5411
+ }).join("\n");
5412
+ return `
5413
+ /** The page names of each Inertia app: \`default\` (forRoot) and every \`pages.scopes\` entry. */
5414
+ export interface InertiaScopePages {
5415
+ ${body}
5416
+ }
5417
+
5418
+ export type InertiaScopePageName<S extends keyof InertiaScopePages> = InertiaScopePages[S];
5419
+ `;
5420
+ }
5281
5421
  function buildSharedPropsBlock(sharedProps) {
5282
5422
  if (!sharedProps) return "";
5283
5423
  if (sharedProps.isImportRef) {
@@ -5485,8 +5625,7 @@ function diffConfigKeyHashes(previous, current) {
5485
5625
  return [...keys].filter((key) => previous[key] !== current[key]).sort();
5486
5626
  }
5487
5627
  async function discoverInputFiles(config) {
5488
- const globs = [config.contracts.glob, config.forms.watch];
5489
- if (config.pages) globs.push(config.pages.glob);
5628
+ const globs = [config.contracts.glob, config.forms.watch, ...allPageGlobs(config)];
5490
5629
  const cwd = config.codegen.cwd;
5491
5630
  const matched = await (0, import_fast_glob3.default)(globs, { cwd, absolute: true, onlyFiles: true });
5492
5631
  return [...new Set(matched)].sort();
@@ -5617,12 +5756,7 @@ async function generate(config, inputRoutes = [], entryPoint = "cli") {
5617
5756
  }
5618
5757
  if (config.pages) {
5619
5758
  const pagesConfig = config.pages;
5620
- const pages = await discoverPages({
5621
- glob: pagesConfig.glob,
5622
- cwd: config.codegen.cwd,
5623
- propsExport: pagesConfig.propsExport,
5624
- componentNameStrategy: pagesConfig.componentNameStrategy
5625
- });
5759
+ const pages = await discoverPageScopes(config);
5626
5760
  const sharedProps = discoverSharedPropsFromConfig(config);
5627
5761
  await emitPages(pages, config.codegen.outDir, {
5628
5762
  propsExport: pagesConfig.propsExport,
@@ -5797,12 +5931,17 @@ async function watch(config, onChange, options = {}) {
5797
5931
  await runInitialPass();
5798
5932
  }
5799
5933
  let pagesDebounceTimer;
5800
- const pagesGlob = config.pages?.glob ?? ".nestjs-codegen-no-pages";
5801
- const pagesWatcher = import_chokidar.default.watch((0, import_node_path18.join)(config.codegen.cwd, pagesGlob), {
5802
- ignoreInitial: true,
5803
- persistent: true,
5804
- awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
5805
- });
5934
+ const pageGlobs = allPageGlobs(config);
5935
+ const pagesWatcher = import_chokidar.default.watch(
5936
+ (pageGlobs.length ? pageGlobs : [".nestjs-codegen-no-pages"]).map(
5937
+ (glob) => (0, import_node_path18.join)(config.codegen.cwd, glob)
5938
+ ),
5939
+ {
5940
+ ignoreInitial: true,
5941
+ persistent: true,
5942
+ awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
5943
+ }
5944
+ );
5806
5945
  function schedulePagesRegenerate() {
5807
5946
  if (pagesDebounceTimer !== void 0) {
5808
5947
  clearTimeout(pagesDebounceTimer);
@@ -5882,7 +6021,7 @@ async function watch(config, onChange, options = {}) {
5882
6021
  }
5883
6022
 
5884
6023
  // src/index.ts
5885
- var VERSION = "0.26.0";
6024
+ var VERSION = "0.27.1";
5886
6025
 
5887
6026
  // src/cli/codegen.ts
5888
6027
  async function runCodegen(opts = {}) {