@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/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
- import { U as UserConfig, R as ResolvedConfig, a as RouteDescriptor, S as SchemaNode, b as RenderContext, c as SchemaModule, d as RenderedModule, e as ResolvedFormsConfig, V as ValidationAdapter, C as CodegenExtension, E as ExtensionContext, f as SerializationMode } from './index-Bbw73Kkb.cjs';
2
- export { A as AdapterUsage, g as ContractDescriptor, h as ContractSource, i as ControllerRef, N as NumberCheck, j as ScopeConfig, k as StringCheck, T as TypeRef, l as ValidationOption, r as resolveAdapter } from './index-Bbw73Kkb.cjs';
1
+ import { U as UserConfig, R as ResolvedConfig, a as RouteDescriptor, S as SchemaNode, b as RenderContext, c as SchemaModule, d as RenderedModule, e as ResolvedFormsConfig, V as ValidationAdapter, C as CodegenExtension, E as ExtensionContext, f as SerializationMode } from './index-Dc4s883u.cjs';
2
+ export { A as AdapterUsage, g as ContractDescriptor, h as ContractSource, i as ControllerRef, N as NumberCheck, j as ScopeConfig, k as StringCheck, T as TypeRef, l as ValidationOption, r as resolveAdapter } from './index-Dc4s883u.cjs';
3
3
  import { ClassDeclaration, SourceFile, Project } from 'ts-morph';
4
4
 
5
5
  declare function defineConfig(c: UserConfig): UserConfig;
@@ -382,6 +382,6 @@ interface FastDiscoveryOptions {
382
382
  }
383
383
  declare function discoverContractsFast(opts: FastDiscoveryOptions): Promise<RouteDescriptor[]>;
384
384
 
385
- declare const VERSION = "0.26.0";
385
+ declare const VERSION = "0.27.1";
386
386
 
387
387
  export { type ChainModuleRendererOptions, CodegenError, type CodegenManifest, ConfigError, DriftGuardError, type EntryPoint, type FastDiscoveryOptions, type JsonSchema, type MocksEmitOptions, type OpenApiDocument, type OpenApiEmitOptions, type OpenApiInfo, RenderContext, RenderedModule, ResolvedConfig, RouteDescriptor, SchemaModule, SchemaNode, type TsTypeContext, UserConfig, VERSION, ValidationAdapter, type WatchOptions, type Watcher, acquireLock, buildMocksFile, buildOpenApiSpec, createChainModuleRenderer, defineConfig, discoverContractsFast, emitApi, emitForms, emitMocks, emitOpenApi, emitRoutes, extractSchemaFromDto, generate, loadConfig, renderTsType, resolveConfig, schemaModuleToJsonSchema, schemaNodeToJsonSchema, toObjectKey, typeNameFor, watch };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { U as UserConfig, R as ResolvedConfig, a as RouteDescriptor, S as SchemaNode, b as RenderContext, c as SchemaModule, d as RenderedModule, e as ResolvedFormsConfig, V as ValidationAdapter, C as CodegenExtension, E as ExtensionContext, f as SerializationMode } from './index-Bbw73Kkb.js';
2
- export { A as AdapterUsage, g as ContractDescriptor, h as ContractSource, i as ControllerRef, N as NumberCheck, j as ScopeConfig, k as StringCheck, T as TypeRef, l as ValidationOption, r as resolveAdapter } from './index-Bbw73Kkb.js';
1
+ import { U as UserConfig, R as ResolvedConfig, a as RouteDescriptor, S as SchemaNode, b as RenderContext, c as SchemaModule, d as RenderedModule, e as ResolvedFormsConfig, V as ValidationAdapter, C as CodegenExtension, E as ExtensionContext, f as SerializationMode } from './index-Dc4s883u.js';
2
+ export { A as AdapterUsage, g as ContractDescriptor, h as ContractSource, i as ControllerRef, N as NumberCheck, j as ScopeConfig, k as StringCheck, T as TypeRef, l as ValidationOption, r as resolveAdapter } from './index-Dc4s883u.js';
3
3
  import { ClassDeclaration, SourceFile, Project } from 'ts-morph';
4
4
 
5
5
  declare function defineConfig(c: UserConfig): UserConfig;
@@ -382,6 +382,6 @@ interface FastDiscoveryOptions {
382
382
  }
383
383
  declare function discoverContractsFast(opts: FastDiscoveryOptions): Promise<RouteDescriptor[]>;
384
384
 
385
- declare const VERSION = "0.26.0";
385
+ declare const VERSION = "0.27.1";
386
386
 
387
387
  export { type ChainModuleRendererOptions, CodegenError, type CodegenManifest, ConfigError, DriftGuardError, type EntryPoint, type FastDiscoveryOptions, type JsonSchema, type MocksEmitOptions, type OpenApiDocument, type OpenApiEmitOptions, type OpenApiInfo, RenderContext, RenderedModule, ResolvedConfig, RouteDescriptor, SchemaModule, SchemaNode, type TsTypeContext, UserConfig, VERSION, ValidationAdapter, type WatchOptions, type Watcher, acquireLock, buildMocksFile, buildOpenApiSpec, createChainModuleRenderer, defineConfig, discoverContractsFast, emitApi, emitForms, emitMocks, emitOpenApi, emitRoutes, extractSchemaFromDto, generate, loadConfig, renderTsType, resolveConfig, schemaModuleToJsonSchema, schemaNodeToJsonSchema, toObjectKey, typeNameFor, watch };
package/dist/index.js CHANGED
@@ -88,12 +88,33 @@ function validateUserConfig(userConfig) {
88
88
  "validation adapter is required \u2014 install @dudousxd/nestjs-codegen-zod and pass zodAdapter, or use @dudousxd/nestjs-codegen-valibot / -arktype"
89
89
  );
90
90
  }
91
- if (userConfig.pages && typeof userConfig.pages.glob !== "string") {
91
+ if (userConfig.pages && !isGlobInput(userConfig.pages.glob)) {
92
92
  throw new ConfigError(
93
- "Config validation failed: `pages.glob` must be a string when `pages` is set"
93
+ "Config validation failed: `pages.glob` must be a string or a non-empty array of strings when `pages` is set"
94
+ );
95
+ }
96
+ for (const [scope, scopeConfig] of Object.entries(userConfig.pages?.scopes ?? {})) {
97
+ if (scope === "default") {
98
+ throw new ConfigError(
99
+ 'Config validation failed: `pages.scopes` cannot declare "default" \u2014 the default app is `pages.glob` (InertiaModule.forRoot).'
100
+ );
101
+ }
102
+ if (!isGlobInput(scopeConfig?.glob)) {
103
+ throw new ConfigError(
104
+ `Config validation failed: \`pages.scopes.${scope}.glob\` must be a string or a non-empty array of strings`
105
+ );
106
+ }
107
+ }
108
+ if (userConfig.scopes && Object.keys(userConfig.scopes).length > 0) {
109
+ console.warn(
110
+ "[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)."
94
111
  );
95
112
  }
96
113
  }
114
+ function isGlobInput(value) {
115
+ if (typeof value === "string") return true;
116
+ return Array.isArray(value) && value.length > 0 && value.every((glob) => typeof glob === "string");
117
+ }
97
118
  function applyDefaults(userConfig, cwd) {
98
119
  validateUserConfig(userConfig);
99
120
  const outDir = userConfig.codegen?.outDir ? resolveAbsolute(cwd, userConfig.codegen.outDir) : join(cwd, ".nestjs-codegen");
@@ -120,7 +141,17 @@ function applyDefaults(userConfig, cwd) {
120
141
  pages: userConfig.pages ? {
121
142
  glob: userConfig.pages.glob,
122
143
  propsExport: userConfig.pages.propsExport ?? "ComponentProps",
123
- componentNameStrategy: userConfig.pages.componentNameStrategy ?? "relative-no-ext"
144
+ componentNameStrategy: userConfig.pages.componentNameStrategy ?? "relative-no-ext",
145
+ scopes: Object.fromEntries(
146
+ Object.entries(userConfig.pages.scopes ?? {}).map(([scope, scopeConfig]) => [
147
+ scope,
148
+ {
149
+ glob: scopeConfig.glob,
150
+ prefix: scopeConfig.prefix ?? `${scope}/`,
151
+ componentNameStrategy: scopeConfig.componentNameStrategy ?? userConfig.pages?.componentNameStrategy ?? "relative-no-ext"
152
+ }
153
+ ])
154
+ )
124
155
  } : null,
125
156
  contracts: {
126
157
  glob: userConfig.contracts?.glob ?? "src/**/*.controller.ts",
@@ -194,26 +225,84 @@ import { dirname as dirname4, join as join14 } from "path";
194
225
 
195
226
  // src/discovery/pages.ts
196
227
  import { readFile } from "fs/promises";
197
- import { join as join2, relative as relative2 } from "path";
228
+ import { join as join2, relative as relative2, sep as sep2 } from "path";
198
229
  import fg from "fast-glob";
199
230
  var NON_PAGE_FILE_RE = /\.(?:test|spec|stories|story)\.(?:tsx?|jsx?|vue|svelte)$/i;
231
+ var MAGIC_SEGMENT_RE = /[*?{}[\]()!]/;
232
+ function globStaticBase(glob) {
233
+ const segments = glob.split("/");
234
+ const staticSegments = [];
235
+ for (const segment of segments.slice(0, -1)) {
236
+ if (MAGIC_SEGMENT_RE.test(segment)) break;
237
+ staticSegments.push(segment);
238
+ }
239
+ return staticSegments.join("/");
240
+ }
200
241
  async function discoverPages(opts) {
201
- const allFiles = await fg(opts.glob, { cwd: opts.cwd, absolute: true });
202
- const files = allFiles.filter((f) => !NON_PAGE_FILE_RE.test(f));
203
- files.sort();
204
- const globStatic = opts.glob.split("*")[0]?.replace(/\/$/, "") ?? "";
205
- const pagesBase = join2(opts.cwd, globStatic);
242
+ const globs = typeof opts.glob === "string" ? [opts.glob] : opts.glob;
243
+ const prefix = opts.prefix ?? "";
244
+ const scope = opts.scope ?? "default";
245
+ const seenFiles = /* @__PURE__ */ new Set();
206
246
  const out = [];
207
- for (const file of files) {
208
- const rel = relative2(opts.cwd, file);
209
- const nameRel = relative2(pagesBase, file);
210
- const name = computeName(nameRel, opts.componentNameStrategy);
211
- const source = await readFile(file, "utf8");
212
- const propsSource = extractPropsSource(source, opts.propsExport);
213
- out.push({ name, absolutePath: file, relativePath: rel, propsSource });
247
+ for (const glob of globs) {
248
+ const files = (await fg(glob, { cwd: opts.cwd, absolute: true })).filter((f) => !NON_PAGE_FILE_RE.test(f) && !seenFiles.has(f)).sort();
249
+ const pagesBase = join2(opts.cwd, globStaticBase(glob));
250
+ for (const file of files) {
251
+ seenFiles.add(file);
252
+ const rel = relative2(opts.cwd, file);
253
+ const nameRel = relative2(pagesBase, file).split(sep2).join("/");
254
+ const name = `${prefix}${computeName(nameRel, opts.componentNameStrategy)}`;
255
+ const source = await readFile(file, "utf8");
256
+ const propsSource = extractPropsSource(source, opts.propsExport);
257
+ out.push({ name, absolutePath: file, relativePath: rel, propsSource, scope });
258
+ }
259
+ }
260
+ assertUniqueNames(out);
261
+ return out;
262
+ }
263
+ async function discoverPageScopes(config) {
264
+ const pages = config.pages;
265
+ if (!pages) return [];
266
+ const common = { cwd: config.codegen.cwd, propsExport: pages.propsExport };
267
+ const out = await discoverPages({
268
+ ...common,
269
+ glob: pages.glob,
270
+ componentNameStrategy: pages.componentNameStrategy
271
+ });
272
+ for (const [scope, scopeConfig] of Object.entries(pages.scopes ?? {})) {
273
+ out.push(
274
+ ...await discoverPages({
275
+ ...common,
276
+ glob: scopeConfig.glob,
277
+ componentNameStrategy: scopeConfig.componentNameStrategy,
278
+ prefix: scopeConfig.prefix,
279
+ scope
280
+ })
281
+ );
214
282
  }
283
+ assertUniqueNames(out);
215
284
  return out;
216
285
  }
286
+ function allPageGlobs(config) {
287
+ if (!config.pages) return [];
288
+ const asList = (glob) => typeof glob === "string" ? [glob] : glob;
289
+ return [
290
+ ...asList(config.pages.glob),
291
+ ...Object.values(config.pages.scopes ?? {}).flatMap((scope) => asList(scope.glob))
292
+ ];
293
+ }
294
+ function assertUniqueNames(pages) {
295
+ const byName = /* @__PURE__ */ new Map();
296
+ for (const page of pages) {
297
+ const previous = byName.get(page.name);
298
+ if (previous) {
299
+ throw new Error(
300
+ `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.`
301
+ );
302
+ }
303
+ byName.set(page.name, page);
304
+ }
305
+ }
217
306
  function computeName(rel, strat) {
218
307
  if (typeof strat === "function") return strat(rel);
219
308
  const noExt = rel.replace(/\.(tsx?|vue|svelte)$/, "");
@@ -636,10 +725,11 @@ function resolveModuleSpecifier(moduleSpecifier, sourceFile, project) {
636
725
  }
637
726
  return [];
638
727
  }
639
- function resolveImportedType(name, sourceFile, project) {
728
+ function resolveImportedType(name, sourceFile, project, seen = /* @__PURE__ */ new Set()) {
640
729
  for (const importDecl of sourceFile.getImportDeclarations()) {
641
- const namedImport = importDecl.getNamedImports().find((n) => n.getName() === name);
730
+ const namedImport = importDecl.getNamedImports().find((n) => (n.getAliasNode()?.getText() ?? n.getName()) === name);
642
731
  if (!namedImport) continue;
732
+ const sourceName = namedImport.getName();
643
733
  const moduleSpecifier = importDecl.getModuleSpecifierValue();
644
734
  const candidates = resolveModuleSpecifier(moduleSpecifier, sourceFile, project);
645
735
  for (const candidate of candidates) {
@@ -651,17 +741,17 @@ function resolveImportedType(name, sourceFile, project) {
651
741
  continue;
652
742
  }
653
743
  }
654
- const result = findTypeInFile(name, importedFile);
744
+ const result = findTypeInFile(sourceName, importedFile);
655
745
  if (result) return result;
656
- const viaReExport = resolveReExportedType(name, importedFile, project, /* @__PURE__ */ new Set());
746
+ const viaReExport = resolveReExportedType(sourceName, importedFile, project, /* @__PURE__ */ new Set());
657
747
  if (viaReExport) return viaReExport;
658
748
  }
659
749
  if (candidates.length === 0) {
660
- const viaCompiler = resolveBareSpecifierType(name, importDecl, project);
750
+ const viaCompiler = resolveBareSpecifierType(sourceName, importDecl, project);
661
751
  if (viaCompiler) return viaCompiler;
662
752
  }
663
753
  }
664
- return resolveReExportedType(name, sourceFile, project, /* @__PURE__ */ new Set());
754
+ return resolveReExportedType(name, sourceFile, project, seen);
665
755
  }
666
756
  function resolveBareSpecifierType(name, importDecl, project) {
667
757
  let target;
@@ -676,9 +766,9 @@ function resolveBareSpecifierType(name, importDecl, project) {
676
766
  return resolveReExportedType(name, target, project, /* @__PURE__ */ new Set());
677
767
  }
678
768
  function resolveReExportedType(name, file, project, seen) {
679
- const filePath = file.getFilePath();
680
- if (seen.has(filePath)) return null;
681
- seen.add(filePath);
769
+ const visit = `${file.getFilePath()}#${name}`;
770
+ if (seen.has(visit)) return null;
771
+ seen.add(visit);
682
772
  for (const exportDecl of file.getExportDeclarations()) {
683
773
  const moduleSpecifier = exportDecl.getModuleSpecifierValue();
684
774
  const namedExports = exportDecl.getNamedExports();
@@ -700,7 +790,7 @@ function resolveReExportedType(name, file, project, seen) {
700
790
  const sourceName = namedExports.find((n) => (n.getAliasNode()?.getText() ?? n.getName()) === name)?.getName() ?? name;
701
791
  const local = findTypeInFile(sourceName, file);
702
792
  if (local) return local;
703
- const imported = resolveImportedType(sourceName, file, project);
793
+ const imported = resolveImportedType(sourceName, file, project, seen);
704
794
  if (imported) return imported;
705
795
  }
706
796
  return null;
@@ -4102,28 +4192,28 @@ function renderFetcherRequest(req, binaryResponse) {
4102
4192
  }
4103
4193
  function emitReqHelper() {
4104
4194
  return [
4105
- "/** Awaitable request handle. `await api.x.y({...})` runs the fetch; extensions add query/mutation helpers. */",
4106
- "type __Req<R> = {",
4107
- " then<T1 = R, T2 = never>(",
4195
+ "/** An awaitable request handle: a lazy, memoized `Promise` that also carries `fetch()` and the members extensions add. */",
4196
+ "export type RequestHandle<R, M = {}> = Promise<R> & { fetch(): Promise<R> } & M;",
4197
+ "class __Request<R> extends Promise<R> {",
4198
+ " static override get [Symbol.species](): PromiseConstructor {",
4199
+ " return Promise;",
4200
+ " }",
4201
+ " private __run: () => Promise<R>;",
4202
+ " private __p: Promise<R> | undefined;",
4203
+ " constructor(run: () => Promise<R>) {",
4204
+ " super(() => {});",
4205
+ " this.__run = run;",
4206
+ " }",
4207
+ " override then<T1 = R, T2 = never>(",
4108
4208
  " onfulfilled?: ((value: R) => T1 | PromiseLike<T1>) | null,",
4109
4209
  " onrejected?: ((reason: unknown) => T2 | PromiseLike<T2>) | null,",
4110
- " ): Promise<T1 | T2>;",
4111
- " catch<T = never>(onrejected?: ((reason: unknown) => T | PromiseLike<T>) | null): Promise<R | T>;",
4112
- " finally(onfinally?: (() => void) | null): Promise<R>;",
4113
- " fetch(): Promise<R>;",
4114
- "};",
4115
- "function __req<R>(run: () => Promise<R>): __Req<R> {",
4116
- " let __p: Promise<R> | undefined;",
4117
- " const __promise = () => {",
4118
- " __p ??= run();",
4119
- " return __p;",
4120
- " };",
4121
- " return {",
4122
- " then: (onfulfilled, onrejected) => __promise().then(onfulfilled, onrejected),",
4123
- " catch: (onrejected) => __promise().catch(onrejected),",
4124
- " finally: (onfinally) => __promise().finally(onfinally),",
4125
- " fetch: run,",
4126
- " };",
4210
+ " ): Promise<T1 | T2> {",
4211
+ " this.__p ??= this.__run();",
4212
+ " return this.__p.then(onfulfilled, onrejected);",
4213
+ " }",
4214
+ "}",
4215
+ "function __req<R, M extends object = {}>(run: () => Promise<R>, members?: M): RequestHandle<R, M> {",
4216
+ " return Object.assign(new __Request<R>(run), { fetch: run }, members) as RequestHandle<R, M>;",
4127
4217
  "}",
4128
4218
  ""
4129
4219
  ];
@@ -4141,18 +4231,26 @@ function emitFilterFieldGuard() {
4141
4231
  ];
4142
4232
  }
4143
4233
  function renderLeaf(pad, objKey, req, requestExpr, members, streamExpr) {
4144
- const lines = [`${pad}${objKey}: (input?: ${req.inputType}) => ({`];
4145
- lines.push(`${pad} ...__req<${req.responseType}>(() => ${requestExpr}),`);
4234
+ const memberLines = [];
4146
4235
  if (req.filterFieldsExpr) {
4147
- lines.push(`${pad} filterFields: ${req.filterFieldsExpr},`);
4236
+ memberLines.push(`${pad} filterFields: ${req.filterFieldsExpr},`);
4148
4237
  }
4149
4238
  if (streamExpr) {
4150
- lines.push(`${pad} stream: () => ${streamExpr},`);
4239
+ memberLines.push(`${pad} stream: () => ${streamExpr},`);
4151
4240
  }
4152
4241
  for (const [name, value] of Object.entries(members)) {
4153
- lines.push(`${pad} ${name}: ${value},`);
4154
- }
4155
- lines.push(`${pad}}),`);
4242
+ memberLines.push(`${pad} ${name}: ${value},`);
4243
+ }
4244
+ const body = [requestExpr, ...memberLines].join("\n");
4245
+ const param = /\binput\b/.test(body) ? `input?: ${req.inputType}` : "";
4246
+ const lines = [`${pad}${objKey}: (${param}) =>`];
4247
+ if (memberLines.length === 0) {
4248
+ lines.push(`${pad} __req<${req.responseType}>(() => ${requestExpr}),`);
4249
+ return lines;
4250
+ }
4251
+ lines.push(`${pad} __req(() => ${requestExpr}, {`);
4252
+ lines.push(...memberLines);
4253
+ lines.push(`${pad} }),`);
4156
4254
  return lines;
4157
4255
  }
4158
4256
  function renderStreamExpr(req) {
@@ -4286,6 +4384,23 @@ var EMPTY_PATH_NAMESPACE = [
4286
4384
  function isClientCallable(route) {
4287
4385
  return route.method.toUpperCase() !== "ALL";
4288
4386
  }
4387
+ var ROUTES_IMPORT_PLACEHOLDER = "/* @@routes-import@@ */";
4388
+ var ROUTES_EXPORTS = [
4389
+ // Called, never mentioned in prose: match the call so a comment saying "route" doesn't count.
4390
+ { name: "route", typeOnly: false },
4391
+ { name: "ROUTES", typeOnly: false },
4392
+ { name: "RouteName", typeOnly: true },
4393
+ { name: "ExtractParams", typeOnly: true },
4394
+ { name: "RouteParams", typeOnly: true }
4395
+ ];
4396
+ function finishRoutesImport(lines) {
4397
+ const body = lines.filter((l) => l !== ROUTES_IMPORT_PLACEHOLDER).join("\n");
4398
+ const used = ROUTES_EXPORTS.filter(
4399
+ ({ name }) => new RegExp(name === "route" ? "\\broute\\(" : `\\b${name}\\b`).test(body)
4400
+ ).map(({ name, typeOnly }) => typeOnly ? `type ${name}` : name);
4401
+ const importLine = used.length ? `import { ${used.join(", ")} } from './routes.js';` : null;
4402
+ return lines.flatMap((l) => l === ROUTES_IMPORT_PLACEHOLDER ? importLine ? [importLine] : [] : [l]).join("\n");
4403
+ }
4289
4404
  function buildApiFile(routes, outDir, opts = {}) {
4290
4405
  const fetcherImportPath = opts.fetcherImportPath;
4291
4406
  const serialization = opts.serialization ?? "json";
@@ -4345,9 +4460,7 @@ function buildApiFile(routes, outDir, opts = {}) {
4345
4460
  for (const imp of ext.apiHeader?.(ctx)?.imports ?? []) pushImport(imp);
4346
4461
  }
4347
4462
  lines.push(...extImports);
4348
- lines.push(
4349
- "import { route, ROUTES, type RouteName, type ExtractParams, type RouteParams } from './routes.js';"
4350
- );
4463
+ lines.push(ROUTES_IMPORT_PLACEHOLDER);
4351
4464
  const runtimeImport = fetcherImportPath ?? "@dudousxd/nestjs-client";
4352
4465
  lines.push(`import type { Fetcher } from '${runtimeImport}';`);
4353
4466
  if (serialization === "json" && contracted.length > 0) {
@@ -4398,7 +4511,7 @@ function buildApiFile(routes, outDir, opts = {}) {
4398
4511
  lines.push(...statements, "");
4399
4512
  }
4400
4513
  }
4401
- return lines.join("\n");
4514
+ return finishRoutesImport(lines);
4402
4515
  }
4403
4516
  const tree = /* @__PURE__ */ new Map();
4404
4517
  for (const r of contracted) {
@@ -4451,7 +4564,7 @@ function buildApiFile(routes, outDir, opts = {}) {
4451
4564
  lines.push(...statements, "");
4452
4565
  }
4453
4566
  }
4454
- return lines.join("\n");
4567
+ return finishRoutesImport(lines);
4455
4568
  }
4456
4569
 
4457
4570
  // src/emit/emit-cache.ts
@@ -4464,6 +4577,7 @@ async function emitCache(pages, outDir) {
4464
4577
  const s = await stat(p.absolutePath);
4465
4578
  return {
4466
4579
  name: p.name,
4580
+ scope: p.scope ?? "default",
4467
4581
  relativePath: p.relativePath,
4468
4582
  mtime: s.mtime.toISOString()
4469
4583
  };
@@ -5258,10 +5372,11 @@ async function emitPages(pages, outDir, _options = {}) {
5258
5372
  }).join("\n");
5259
5373
  const propsHelper = "\nexport type InertiaProps<K extends InertiaPageName> = import('@dudousxd/nestjs-inertia').InertiaPages[K];\n";
5260
5374
  const sharedPropsBlock = buildSharedPropsBlock(_options.sharedProps ?? null);
5375
+ const scopesBlock = buildScopesBlock(pages);
5261
5376
  const content = `// Generated by @dudousxd/nestjs-codegen. Do not edit.
5262
5377
 
5263
5378
  export type InertiaPageName = ${pageNameUnion};
5264
- ${propsHelper}
5379
+ ${propsHelper}${scopesBlock}
5265
5380
  declare module '@dudousxd/nestjs-inertia' {
5266
5381
  interface InertiaPages {
5267
5382
  ${augBody}
@@ -5270,6 +5385,31 @@ ${sharedPropsBlock}}
5270
5385
  `;
5271
5386
  await writeFile7(join11(outDir, "pages.d.ts"), content, "utf8");
5272
5387
  }
5388
+ function buildScopesBlock(pages) {
5389
+ const byScope = /* @__PURE__ */ new Map();
5390
+ for (const page of pages) {
5391
+ const scope = page.scope ?? "default";
5392
+ const names = byScope.get(scope) ?? [];
5393
+ names.push(JSON.stringify(page.name));
5394
+ byScope.set(scope, names);
5395
+ }
5396
+ if (![...byScope.keys()].some((scope) => scope !== "default")) return "";
5397
+ if (!byScope.has("default")) byScope.set("default", []);
5398
+ const scopes = ["default", ...[...byScope.keys()].filter((s) => s !== "default")];
5399
+ const body = scopes.map((scope) => {
5400
+ const key = needsQuotes(scope) ? JSON.stringify(scope) : scope;
5401
+ const names = byScope.get(scope) ?? [];
5402
+ return ` ${key}: ${names.length ? names.join(" | ") : "never"};`;
5403
+ }).join("\n");
5404
+ return `
5405
+ /** The page names of each Inertia app: \`default\` (forRoot) and every \`pages.scopes\` entry. */
5406
+ export interface InertiaScopePages {
5407
+ ${body}
5408
+ }
5409
+
5410
+ export type InertiaScopePageName<S extends keyof InertiaScopePages> = InertiaScopePages[S];
5411
+ `;
5412
+ }
5273
5413
  function buildSharedPropsBlock(sharedProps) {
5274
5414
  if (!sharedProps) return "";
5275
5415
  if (sharedProps.isImportRef) {
@@ -5477,8 +5617,7 @@ function diffConfigKeyHashes(previous, current) {
5477
5617
  return [...keys].filter((key) => previous[key] !== current[key]).sort();
5478
5618
  }
5479
5619
  async function discoverInputFiles(config) {
5480
- const globs = [config.contracts.glob, config.forms.watch];
5481
- if (config.pages) globs.push(config.pages.glob);
5620
+ const globs = [config.contracts.glob, config.forms.watch, ...allPageGlobs(config)];
5482
5621
  const cwd = config.codegen.cwd;
5483
5622
  const matched = await fg3(globs, { cwd, absolute: true, onlyFiles: true });
5484
5623
  return [...new Set(matched)].sort();
@@ -5609,12 +5748,7 @@ async function generate(config, inputRoutes = [], entryPoint = "cli") {
5609
5748
  }
5610
5749
  if (config.pages) {
5611
5750
  const pagesConfig = config.pages;
5612
- const pages = await discoverPages({
5613
- glob: pagesConfig.glob,
5614
- cwd: config.codegen.cwd,
5615
- propsExport: pagesConfig.propsExport,
5616
- componentNameStrategy: pagesConfig.componentNameStrategy
5617
- });
5751
+ const pages = await discoverPageScopes(config);
5618
5752
  const sharedProps = discoverSharedPropsFromConfig(config);
5619
5753
  await emitPages(pages, config.codegen.outDir, {
5620
5754
  propsExport: pagesConfig.propsExport,
@@ -5789,12 +5923,17 @@ async function watch(config, onChange, options = {}) {
5789
5923
  await runInitialPass();
5790
5924
  }
5791
5925
  let pagesDebounceTimer;
5792
- const pagesGlob = config.pages?.glob ?? ".nestjs-codegen-no-pages";
5793
- const pagesWatcher = chokidar.watch(join16(config.codegen.cwd, pagesGlob), {
5794
- ignoreInitial: true,
5795
- persistent: true,
5796
- awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
5797
- });
5926
+ const pageGlobs = allPageGlobs(config);
5927
+ const pagesWatcher = chokidar.watch(
5928
+ (pageGlobs.length ? pageGlobs : [".nestjs-codegen-no-pages"]).map(
5929
+ (glob) => join16(config.codegen.cwd, glob)
5930
+ ),
5931
+ {
5932
+ ignoreInitial: true,
5933
+ persistent: true,
5934
+ awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
5935
+ }
5936
+ );
5798
5937
  function schedulePagesRegenerate() {
5799
5938
  if (pagesDebounceTimer !== void 0) {
5800
5939
  clearTimeout(pagesDebounceTimer);
@@ -5957,7 +6096,7 @@ function createChainModuleRenderer(opts) {
5957
6096
  }
5958
6097
 
5959
6098
  // src/index.ts
5960
- var VERSION = "0.26.0";
6099
+ var VERSION = "0.27.1";
5961
6100
  export {
5962
6101
  CodegenError,
5963
6102
  ConfigError,