@dudousxd/nestjs-codegen 0.26.0 → 0.27.0

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,11 +149,32 @@ 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
155
  );
156
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)."
172
+ );
173
+ }
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");
157
178
  }
158
179
  function applyDefaults(userConfig, cwd) {
159
180
  validateUserConfig(userConfig);
@@ -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)$/, "");
@@ -4145,28 +4234,28 @@ function renderFetcherRequest(req, binaryResponse) {
4145
4234
  }
4146
4235
  function emitReqHelper() {
4147
4236
  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>(",
4237
+ "/** An awaitable request handle: a lazy, memoized `Promise` that also carries `fetch()` and the members extensions add. */",
4238
+ "export type RequestHandle<R, M = {}> = Promise<R> & { fetch(): Promise<R> } & M;",
4239
+ "class __Request<R> extends Promise<R> {",
4240
+ " static override get [Symbol.species](): PromiseConstructor {",
4241
+ " return Promise;",
4242
+ " }",
4243
+ " private __run: () => Promise<R>;",
4244
+ " private __p: Promise<R> | undefined;",
4245
+ " constructor(run: () => Promise<R>) {",
4246
+ " super(() => {});",
4247
+ " this.__run = run;",
4248
+ " }",
4249
+ " override then<T1 = R, T2 = never>(",
4151
4250
  " onfulfilled?: ((value: R) => T1 | PromiseLike<T1>) | null,",
4152
4251
  " 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
- " };",
4252
+ " ): Promise<T1 | T2> {",
4253
+ " this.__p ??= this.__run();",
4254
+ " return this.__p.then(onfulfilled, onrejected);",
4255
+ " }",
4256
+ "}",
4257
+ "function __req<R, M extends object = {}>(run: () => Promise<R>, members?: M): RequestHandle<R, M> {",
4258
+ " return Object.assign(new __Request<R>(run), { fetch: run }, members) as RequestHandle<R, M>;",
4170
4259
  "}",
4171
4260
  ""
4172
4261
  ];
@@ -4184,18 +4273,26 @@ function emitFilterFieldGuard() {
4184
4273
  ];
4185
4274
  }
4186
4275
  function renderLeaf(pad, objKey, req, requestExpr, members, streamExpr) {
4187
- const lines = [`${pad}${objKey}: (input?: ${req.inputType}) => ({`];
4188
- lines.push(`${pad} ...__req<${req.responseType}>(() => ${requestExpr}),`);
4276
+ const memberLines = [];
4189
4277
  if (req.filterFieldsExpr) {
4190
- lines.push(`${pad} filterFields: ${req.filterFieldsExpr},`);
4278
+ memberLines.push(`${pad} filterFields: ${req.filterFieldsExpr},`);
4191
4279
  }
4192
4280
  if (streamExpr) {
4193
- lines.push(`${pad} stream: () => ${streamExpr},`);
4281
+ memberLines.push(`${pad} stream: () => ${streamExpr},`);
4194
4282
  }
4195
4283
  for (const [name, value] of Object.entries(members)) {
4196
- lines.push(`${pad} ${name}: ${value},`);
4197
- }
4198
- lines.push(`${pad}}),`);
4284
+ memberLines.push(`${pad} ${name}: ${value},`);
4285
+ }
4286
+ const body = [requestExpr, ...memberLines].join("\n");
4287
+ const param = /\binput\b/.test(body) ? `input?: ${req.inputType}` : "";
4288
+ const lines = [`${pad}${objKey}: (${param}) =>`];
4289
+ if (memberLines.length === 0) {
4290
+ lines.push(`${pad} __req<${req.responseType}>(() => ${requestExpr}),`);
4291
+ return lines;
4292
+ }
4293
+ lines.push(`${pad} __req(() => ${requestExpr}, {`);
4294
+ lines.push(...memberLines);
4295
+ lines.push(`${pad} }),`);
4199
4296
  return lines;
4200
4297
  }
4201
4298
  function renderStreamExpr(req) {
@@ -4329,6 +4426,23 @@ var EMPTY_PATH_NAMESPACE = [
4329
4426
  function isClientCallable(route) {
4330
4427
  return route.method.toUpperCase() !== "ALL";
4331
4428
  }
4429
+ var ROUTES_IMPORT_PLACEHOLDER = "/* @@routes-import@@ */";
4430
+ var ROUTES_EXPORTS = [
4431
+ // Called, never mentioned in prose: match the call so a comment saying "route" doesn't count.
4432
+ { name: "route", typeOnly: false },
4433
+ { name: "ROUTES", typeOnly: false },
4434
+ { name: "RouteName", typeOnly: true },
4435
+ { name: "ExtractParams", typeOnly: true },
4436
+ { name: "RouteParams", typeOnly: true }
4437
+ ];
4438
+ function finishRoutesImport(lines) {
4439
+ const body = lines.filter((l) => l !== ROUTES_IMPORT_PLACEHOLDER).join("\n");
4440
+ const used = ROUTES_EXPORTS.filter(
4441
+ ({ name }) => new RegExp(name === "route" ? "\\broute\\(" : `\\b${name}\\b`).test(body)
4442
+ ).map(({ name, typeOnly }) => typeOnly ? `type ${name}` : name);
4443
+ const importLine = used.length ? `import { ${used.join(", ")} } from './routes.js';` : null;
4444
+ return lines.flatMap((l) => l === ROUTES_IMPORT_PLACEHOLDER ? importLine ? [importLine] : [] : [l]).join("\n");
4445
+ }
4332
4446
  function buildApiFile(routes, outDir, opts = {}) {
4333
4447
  const fetcherImportPath = opts.fetcherImportPath;
4334
4448
  const serialization = opts.serialization ?? "json";
@@ -4388,9 +4502,7 @@ function buildApiFile(routes, outDir, opts = {}) {
4388
4502
  for (const imp of ext.apiHeader?.(ctx)?.imports ?? []) pushImport(imp);
4389
4503
  }
4390
4504
  lines.push(...extImports);
4391
- lines.push(
4392
- "import { route, ROUTES, type RouteName, type ExtractParams, type RouteParams } from './routes.js';"
4393
- );
4505
+ lines.push(ROUTES_IMPORT_PLACEHOLDER);
4394
4506
  const runtimeImport = fetcherImportPath ?? "@dudousxd/nestjs-client";
4395
4507
  lines.push(`import type { Fetcher } from '${runtimeImport}';`);
4396
4508
  if (serialization === "json" && contracted.length > 0) {
@@ -4441,7 +4553,7 @@ function buildApiFile(routes, outDir, opts = {}) {
4441
4553
  lines.push(...statements, "");
4442
4554
  }
4443
4555
  }
4444
- return lines.join("\n");
4556
+ return finishRoutesImport(lines);
4445
4557
  }
4446
4558
  const tree = /* @__PURE__ */ new Map();
4447
4559
  for (const r of contracted) {
@@ -4494,7 +4606,7 @@ function buildApiFile(routes, outDir, opts = {}) {
4494
4606
  lines.push(...statements, "");
4495
4607
  }
4496
4608
  }
4497
- return lines.join("\n");
4609
+ return finishRoutesImport(lines);
4498
4610
  }
4499
4611
 
4500
4612
  // src/emit/emit-cache.ts
@@ -4507,6 +4619,7 @@ async function emitCache(pages, outDir) {
4507
4619
  const s = await (0, import_promises4.stat)(p.absolutePath);
4508
4620
  return {
4509
4621
  name: p.name,
4622
+ scope: p.scope ?? "default",
4510
4623
  relativePath: p.relativePath,
4511
4624
  mtime: s.mtime.toISOString()
4512
4625
  };
@@ -5301,10 +5414,11 @@ async function emitPages(pages, outDir, _options = {}) {
5301
5414
  }).join("\n");
5302
5415
  const propsHelper = "\nexport type InertiaProps<K extends InertiaPageName> = import('@dudousxd/nestjs-inertia').InertiaPages[K];\n";
5303
5416
  const sharedPropsBlock = buildSharedPropsBlock(_options.sharedProps ?? null);
5417
+ const scopesBlock = buildScopesBlock(pages);
5304
5418
  const content = `// Generated by @dudousxd/nestjs-codegen. Do not edit.
5305
5419
 
5306
5420
  export type InertiaPageName = ${pageNameUnion};
5307
- ${propsHelper}
5421
+ ${propsHelper}${scopesBlock}
5308
5422
  declare module '@dudousxd/nestjs-inertia' {
5309
5423
  interface InertiaPages {
5310
5424
  ${augBody}
@@ -5313,6 +5427,31 @@ ${sharedPropsBlock}}
5313
5427
  `;
5314
5428
  await (0, import_promises9.writeFile)((0, import_node_path13.join)(outDir, "pages.d.ts"), content, "utf8");
5315
5429
  }
5430
+ function buildScopesBlock(pages) {
5431
+ const byScope = /* @__PURE__ */ new Map();
5432
+ for (const page of pages) {
5433
+ const scope = page.scope ?? "default";
5434
+ const names = byScope.get(scope) ?? [];
5435
+ names.push(JSON.stringify(page.name));
5436
+ byScope.set(scope, names);
5437
+ }
5438
+ if (![...byScope.keys()].some((scope) => scope !== "default")) return "";
5439
+ if (!byScope.has("default")) byScope.set("default", []);
5440
+ const scopes = ["default", ...[...byScope.keys()].filter((s) => s !== "default")];
5441
+ const body = scopes.map((scope) => {
5442
+ const key = needsQuotes(scope) ? JSON.stringify(scope) : scope;
5443
+ const names = byScope.get(scope) ?? [];
5444
+ return ` ${key}: ${names.length ? names.join(" | ") : "never"};`;
5445
+ }).join("\n");
5446
+ return `
5447
+ /** The page names of each Inertia app: \`default\` (forRoot) and every \`pages.scopes\` entry. */
5448
+ export interface InertiaScopePages {
5449
+ ${body}
5450
+ }
5451
+
5452
+ export type InertiaScopePageName<S extends keyof InertiaScopePages> = InertiaScopePages[S];
5453
+ `;
5454
+ }
5316
5455
  function buildSharedPropsBlock(sharedProps) {
5317
5456
  if (!sharedProps) return "";
5318
5457
  if (sharedProps.isImportRef) {
@@ -5520,8 +5659,7 @@ function diffConfigKeyHashes(previous, current) {
5520
5659
  return [...keys].filter((key) => previous[key] !== current[key]).sort();
5521
5660
  }
5522
5661
  async function discoverInputFiles(config) {
5523
- const globs = [config.contracts.glob, config.forms.watch];
5524
- if (config.pages) globs.push(config.pages.glob);
5662
+ const globs = [config.contracts.glob, config.forms.watch, ...allPageGlobs(config)];
5525
5663
  const cwd = config.codegen.cwd;
5526
5664
  const matched = await (0, import_fast_glob3.default)(globs, { cwd, absolute: true, onlyFiles: true });
5527
5665
  return [...new Set(matched)].sort();
@@ -5652,12 +5790,7 @@ async function generate(config, inputRoutes = [], entryPoint = "cli") {
5652
5790
  }
5653
5791
  if (config.pages) {
5654
5792
  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
- });
5793
+ const pages = await discoverPageScopes(config);
5661
5794
  const sharedProps = discoverSharedPropsFromConfig(config);
5662
5795
  await emitPages(pages, config.codegen.outDir, {
5663
5796
  propsExport: pagesConfig.propsExport,
@@ -5832,12 +5965,17 @@ async function watch(config, onChange, options = {}) {
5832
5965
  await runInitialPass();
5833
5966
  }
5834
5967
  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
- });
5968
+ const pageGlobs = allPageGlobs(config);
5969
+ const pagesWatcher = import_chokidar.default.watch(
5970
+ (pageGlobs.length ? pageGlobs : [".nestjs-codegen-no-pages"]).map(
5971
+ (glob) => (0, import_node_path18.join)(config.codegen.cwd, glob)
5972
+ ),
5973
+ {
5974
+ ignoreInitial: true,
5975
+ persistent: true,
5976
+ awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
5977
+ }
5978
+ );
5841
5979
  function schedulePagesRegenerate() {
5842
5980
  if (pagesDebounceTimer !== void 0) {
5843
5981
  clearTimeout(pagesDebounceTimer);
@@ -6000,7 +6138,7 @@ function createChainModuleRenderer(opts) {
6000
6138
  }
6001
6139
 
6002
6140
  // src/index.ts
6003
- var VERSION = "0.26.0";
6141
+ var VERSION = "0.27.0";
6004
6142
  // Annotate the CommonJS export names for ESM import in node:
6005
6143
  0 && (module.exports = {
6006
6144
  CodegenError,