@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.
@@ -109,12 +109,33 @@ function validateUserConfig(userConfig) {
109
109
  "validation adapter is required \u2014 install @dudousxd/nestjs-codegen-zod and pass zodAdapter, or use @dudousxd/nestjs-codegen-valibot / -arktype"
110
110
  );
111
111
  }
112
- if (userConfig.pages && typeof userConfig.pages.glob !== "string") {
112
+ if (userConfig.pages && !isGlobInput(userConfig.pages.glob)) {
113
113
  throw new ConfigError(
114
- "Config validation failed: `pages.glob` must be a string when `pages` is set"
114
+ "Config validation failed: `pages.glob` must be a string or a non-empty array of strings when `pages` is set"
115
+ );
116
+ }
117
+ for (const [scope, scopeConfig] of Object.entries(userConfig.pages?.scopes ?? {})) {
118
+ if (scope === "default") {
119
+ throw new ConfigError(
120
+ 'Config validation failed: `pages.scopes` cannot declare "default" \u2014 the default app is `pages.glob` (InertiaModule.forRoot).'
121
+ );
122
+ }
123
+ if (!isGlobInput(scopeConfig?.glob)) {
124
+ throw new ConfigError(
125
+ `Config validation failed: \`pages.scopes.${scope}.glob\` must be a string or a non-empty array of strings`
126
+ );
127
+ }
128
+ }
129
+ if (userConfig.scopes && Object.keys(userConfig.scopes).length > 0) {
130
+ console.warn(
131
+ "[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)."
115
132
  );
116
133
  }
117
134
  }
135
+ function isGlobInput(value) {
136
+ if (typeof value === "string") return true;
137
+ return Array.isArray(value) && value.length > 0 && value.every((glob) => typeof glob === "string");
138
+ }
118
139
  function applyDefaults(userConfig, cwd) {
119
140
  validateUserConfig(userConfig);
120
141
  const outDir = userConfig.codegen?.outDir ? resolveAbsolute(cwd, userConfig.codegen.outDir) : (0, import_node_path.join)(cwd, ".nestjs-codegen");
@@ -141,7 +162,17 @@ function applyDefaults(userConfig, cwd) {
141
162
  pages: userConfig.pages ? {
142
163
  glob: userConfig.pages.glob,
143
164
  propsExport: userConfig.pages.propsExport ?? "ComponentProps",
144
- componentNameStrategy: userConfig.pages.componentNameStrategy ?? "relative-no-ext"
165
+ componentNameStrategy: userConfig.pages.componentNameStrategy ?? "relative-no-ext",
166
+ scopes: Object.fromEntries(
167
+ Object.entries(userConfig.pages.scopes ?? {}).map(([scope, scopeConfig]) => [
168
+ scope,
169
+ {
170
+ glob: scopeConfig.glob,
171
+ prefix: scopeConfig.prefix ?? `${scope}/`,
172
+ componentNameStrategy: scopeConfig.componentNameStrategy ?? userConfig.pages?.componentNameStrategy ?? "relative-no-ext"
173
+ }
174
+ ])
175
+ )
145
176
  } : null,
146
177
  contracts: {
147
178
  glob: userConfig.contracts?.glob ?? "src/**/*.controller.ts",
@@ -3365,32 +3396,86 @@ function extractFromSourceFile(sourceFile, project) {
3365
3396
  return routes;
3366
3397
  }
3367
3398
 
3368
- // src/generate.ts
3369
- var import_promises11 = require("fs/promises");
3370
- var import_node_path15 = require("path");
3371
-
3372
3399
  // src/discovery/pages.ts
3373
3400
  var import_promises2 = require("fs/promises");
3374
3401
  var import_node_path4 = require("path");
3375
3402
  var import_fast_glob2 = __toESM(require("fast-glob"), 1);
3376
3403
  var NON_PAGE_FILE_RE = /\.(?:test|spec|stories|story)\.(?:tsx?|jsx?|vue|svelte)$/i;
3404
+ var MAGIC_SEGMENT_RE = /[*?{}[\]()!]/;
3405
+ function globStaticBase(glob) {
3406
+ const segments = glob.split("/");
3407
+ const staticSegments = [];
3408
+ for (const segment of segments.slice(0, -1)) {
3409
+ if (MAGIC_SEGMENT_RE.test(segment)) break;
3410
+ staticSegments.push(segment);
3411
+ }
3412
+ return staticSegments.join("/");
3413
+ }
3377
3414
  async function discoverPages(opts) {
3378
- const allFiles = await (0, import_fast_glob2.default)(opts.glob, { cwd: opts.cwd, absolute: true });
3379
- const files = allFiles.filter((f) => !NON_PAGE_FILE_RE.test(f));
3380
- files.sort();
3381
- const globStatic = opts.glob.split("*")[0]?.replace(/\/$/, "") ?? "";
3382
- const pagesBase = (0, import_node_path4.join)(opts.cwd, globStatic);
3415
+ const globs = typeof opts.glob === "string" ? [opts.glob] : opts.glob;
3416
+ const prefix = opts.prefix ?? "";
3417
+ const scope = opts.scope ?? "default";
3418
+ const seenFiles = /* @__PURE__ */ new Set();
3383
3419
  const out = [];
3384
- for (const file of files) {
3385
- const rel = (0, import_node_path4.relative)(opts.cwd, file);
3386
- const nameRel = (0, import_node_path4.relative)(pagesBase, file);
3387
- const name = computeName(nameRel, opts.componentNameStrategy);
3388
- const source = await (0, import_promises2.readFile)(file, "utf8");
3389
- const propsSource = extractPropsSource(source, opts.propsExport);
3390
- out.push({ name, absolutePath: file, relativePath: rel, propsSource });
3420
+ for (const glob of globs) {
3421
+ const files = (await (0, import_fast_glob2.default)(glob, { cwd: opts.cwd, absolute: true })).filter((f) => !NON_PAGE_FILE_RE.test(f) && !seenFiles.has(f)).sort();
3422
+ const pagesBase = (0, import_node_path4.join)(opts.cwd, globStaticBase(glob));
3423
+ for (const file of files) {
3424
+ seenFiles.add(file);
3425
+ const rel = (0, import_node_path4.relative)(opts.cwd, file);
3426
+ const nameRel = (0, import_node_path4.relative)(pagesBase, file).split(import_node_path4.sep).join("/");
3427
+ const name = `${prefix}${computeName(nameRel, opts.componentNameStrategy)}`;
3428
+ const source = await (0, import_promises2.readFile)(file, "utf8");
3429
+ const propsSource = extractPropsSource(source, opts.propsExport);
3430
+ out.push({ name, absolutePath: file, relativePath: rel, propsSource, scope });
3431
+ }
3432
+ }
3433
+ assertUniqueNames(out);
3434
+ return out;
3435
+ }
3436
+ async function discoverPageScopes(config) {
3437
+ const pages = config.pages;
3438
+ if (!pages) return [];
3439
+ const common = { cwd: config.codegen.cwd, propsExport: pages.propsExport };
3440
+ const out = await discoverPages({
3441
+ ...common,
3442
+ glob: pages.glob,
3443
+ componentNameStrategy: pages.componentNameStrategy
3444
+ });
3445
+ for (const [scope, scopeConfig] of Object.entries(pages.scopes ?? {})) {
3446
+ out.push(
3447
+ ...await discoverPages({
3448
+ ...common,
3449
+ glob: scopeConfig.glob,
3450
+ componentNameStrategy: scopeConfig.componentNameStrategy,
3451
+ prefix: scopeConfig.prefix,
3452
+ scope
3453
+ })
3454
+ );
3391
3455
  }
3456
+ assertUniqueNames(out);
3392
3457
  return out;
3393
3458
  }
3459
+ function allPageGlobs(config) {
3460
+ if (!config.pages) return [];
3461
+ const asList = (glob) => typeof glob === "string" ? [glob] : glob;
3462
+ return [
3463
+ ...asList(config.pages.glob),
3464
+ ...Object.values(config.pages.scopes ?? {}).flatMap((scope) => asList(scope.glob))
3465
+ ];
3466
+ }
3467
+ function assertUniqueNames(pages) {
3468
+ const byName = /* @__PURE__ */ new Map();
3469
+ for (const page of pages) {
3470
+ const previous = byName.get(page.name);
3471
+ if (previous) {
3472
+ throw new Error(
3473
+ `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.`
3474
+ );
3475
+ }
3476
+ byName.set(page.name, page);
3477
+ }
3478
+ }
3394
3479
  function computeName(rel, strat) {
3395
3480
  if (typeof strat === "function") return strat(rel);
3396
3481
  const noExt = rel.replace(/\.(tsx?|vue|svelte)$/, "");
@@ -3421,6 +3506,10 @@ function extractPropsSource(source, exportName) {
3421
3506
  return source.slice(start);
3422
3507
  }
3423
3508
 
3509
+ // src/generate.ts
3510
+ var import_promises11 = require("fs/promises");
3511
+ var import_node_path15 = require("path");
3512
+
3424
3513
  // src/discovery/shared-props.ts
3425
3514
  var import_node_path5 = require("path");
3426
3515
  var import_ts_morph12 = require("ts-morph");
@@ -4055,28 +4144,28 @@ function renderFetcherRequest(req, binaryResponse) {
4055
4144
  }
4056
4145
  function emitReqHelper() {
4057
4146
  return [
4058
- "/** Awaitable request handle. `await api.x.y({...})` runs the fetch; extensions add query/mutation helpers. */",
4059
- "type __Req<R> = {",
4060
- " then<T1 = R, T2 = never>(",
4147
+ "/** An awaitable request handle: a lazy, memoized `Promise` that also carries `fetch()` and the members extensions add. */",
4148
+ "export type RequestHandle<R, M = {}> = Promise<R> & { fetch(): Promise<R> } & M;",
4149
+ "class __Request<R> extends Promise<R> {",
4150
+ " static override get [Symbol.species](): PromiseConstructor {",
4151
+ " return Promise;",
4152
+ " }",
4153
+ " private __run: () => Promise<R>;",
4154
+ " private __p: Promise<R> | undefined;",
4155
+ " constructor(run: () => Promise<R>) {",
4156
+ " super(() => {});",
4157
+ " this.__run = run;",
4158
+ " }",
4159
+ " override then<T1 = R, T2 = never>(",
4061
4160
  " onfulfilled?: ((value: R) => T1 | PromiseLike<T1>) | null,",
4062
4161
  " onrejected?: ((reason: unknown) => T2 | PromiseLike<T2>) | null,",
4063
- " ): Promise<T1 | T2>;",
4064
- " catch<T = never>(onrejected?: ((reason: unknown) => T | PromiseLike<T>) | null): Promise<R | T>;",
4065
- " finally(onfinally?: (() => void) | null): Promise<R>;",
4066
- " fetch(): Promise<R>;",
4067
- "};",
4068
- "function __req<R>(run: () => Promise<R>): __Req<R> {",
4069
- " let __p: Promise<R> | undefined;",
4070
- " const __promise = () => {",
4071
- " __p ??= run();",
4072
- " return __p;",
4073
- " };",
4074
- " return {",
4075
- " then: (onfulfilled, onrejected) => __promise().then(onfulfilled, onrejected),",
4076
- " catch: (onrejected) => __promise().catch(onrejected),",
4077
- " finally: (onfinally) => __promise().finally(onfinally),",
4078
- " fetch: run,",
4079
- " };",
4162
+ " ): Promise<T1 | T2> {",
4163
+ " this.__p ??= this.__run();",
4164
+ " return this.__p.then(onfulfilled, onrejected);",
4165
+ " }",
4166
+ "}",
4167
+ "function __req<R, M extends object = {}>(run: () => Promise<R>, members?: M): RequestHandle<R, M> {",
4168
+ " return Object.assign(new __Request<R>(run), { fetch: run }, members) as RequestHandle<R, M>;",
4080
4169
  "}",
4081
4170
  ""
4082
4171
  ];
@@ -4094,18 +4183,26 @@ function emitFilterFieldGuard() {
4094
4183
  ];
4095
4184
  }
4096
4185
  function renderLeaf(pad, objKey, req, requestExpr, members, streamExpr) {
4097
- const lines = [`${pad}${objKey}: (input?: ${req.inputType}) => ({`];
4098
- lines.push(`${pad} ...__req<${req.responseType}>(() => ${requestExpr}),`);
4186
+ const memberLines = [];
4099
4187
  if (req.filterFieldsExpr) {
4100
- lines.push(`${pad} filterFields: ${req.filterFieldsExpr},`);
4188
+ memberLines.push(`${pad} filterFields: ${req.filterFieldsExpr},`);
4101
4189
  }
4102
4190
  if (streamExpr) {
4103
- lines.push(`${pad} stream: () => ${streamExpr},`);
4191
+ memberLines.push(`${pad} stream: () => ${streamExpr},`);
4104
4192
  }
4105
4193
  for (const [name, value] of Object.entries(members)) {
4106
- lines.push(`${pad} ${name}: ${value},`);
4107
- }
4108
- lines.push(`${pad}}),`);
4194
+ memberLines.push(`${pad} ${name}: ${value},`);
4195
+ }
4196
+ const body = [requestExpr, ...memberLines].join("\n");
4197
+ const param = /\binput\b/.test(body) ? `input?: ${req.inputType}` : "";
4198
+ const lines = [`${pad}${objKey}: (${param}) =>`];
4199
+ if (memberLines.length === 0) {
4200
+ lines.push(`${pad} __req<${req.responseType}>(() => ${requestExpr}),`);
4201
+ return lines;
4202
+ }
4203
+ lines.push(`${pad} __req(() => ${requestExpr}, {`);
4204
+ lines.push(...memberLines);
4205
+ lines.push(`${pad} }),`);
4109
4206
  return lines;
4110
4207
  }
4111
4208
  function renderStreamExpr(req) {
@@ -4239,6 +4336,23 @@ var EMPTY_PATH_NAMESPACE = [
4239
4336
  function isClientCallable(route) {
4240
4337
  return route.method.toUpperCase() !== "ALL";
4241
4338
  }
4339
+ var ROUTES_IMPORT_PLACEHOLDER = "/* @@routes-import@@ */";
4340
+ var ROUTES_EXPORTS = [
4341
+ // Called, never mentioned in prose: match the call so a comment saying "route" doesn't count.
4342
+ { name: "route", typeOnly: false },
4343
+ { name: "ROUTES", typeOnly: false },
4344
+ { name: "RouteName", typeOnly: true },
4345
+ { name: "ExtractParams", typeOnly: true },
4346
+ { name: "RouteParams", typeOnly: true }
4347
+ ];
4348
+ function finishRoutesImport(lines) {
4349
+ const body = lines.filter((l) => l !== ROUTES_IMPORT_PLACEHOLDER).join("\n");
4350
+ const used = ROUTES_EXPORTS.filter(
4351
+ ({ name }) => new RegExp(name === "route" ? "\\broute\\(" : `\\b${name}\\b`).test(body)
4352
+ ).map(({ name, typeOnly }) => typeOnly ? `type ${name}` : name);
4353
+ const importLine = used.length ? `import { ${used.join(", ")} } from './routes.js';` : null;
4354
+ return lines.flatMap((l) => l === ROUTES_IMPORT_PLACEHOLDER ? importLine ? [importLine] : [] : [l]).join("\n");
4355
+ }
4242
4356
  function buildApiFile(routes, outDir, opts = {}) {
4243
4357
  const fetcherImportPath = opts.fetcherImportPath;
4244
4358
  const serialization = opts.serialization ?? "json";
@@ -4298,9 +4412,7 @@ function buildApiFile(routes, outDir, opts = {}) {
4298
4412
  for (const imp of ext.apiHeader?.(ctx)?.imports ?? []) pushImport(imp);
4299
4413
  }
4300
4414
  lines.push(...extImports);
4301
- lines.push(
4302
- "import { route, ROUTES, type RouteName, type ExtractParams, type RouteParams } from './routes.js';"
4303
- );
4415
+ lines.push(ROUTES_IMPORT_PLACEHOLDER);
4304
4416
  const runtimeImport = fetcherImportPath ?? "@dudousxd/nestjs-client";
4305
4417
  lines.push(`import type { Fetcher } from '${runtimeImport}';`);
4306
4418
  if (serialization === "json" && contracted.length > 0) {
@@ -4351,7 +4463,7 @@ function buildApiFile(routes, outDir, opts = {}) {
4351
4463
  lines.push(...statements, "");
4352
4464
  }
4353
4465
  }
4354
- return lines.join("\n");
4466
+ return finishRoutesImport(lines);
4355
4467
  }
4356
4468
  const tree = /* @__PURE__ */ new Map();
4357
4469
  for (const r of contracted) {
@@ -4404,7 +4516,7 @@ function buildApiFile(routes, outDir, opts = {}) {
4404
4516
  lines.push(...statements, "");
4405
4517
  }
4406
4518
  }
4407
- return lines.join("\n");
4519
+ return finishRoutesImport(lines);
4408
4520
  }
4409
4521
 
4410
4522
  // src/emit/emit-cache.ts
@@ -4417,6 +4529,7 @@ async function emitCache(pages, outDir) {
4417
4529
  const s = await (0, import_promises4.stat)(p.absolutePath);
4418
4530
  return {
4419
4531
  name: p.name,
4532
+ scope: p.scope ?? "default",
4420
4533
  relativePath: p.relativePath,
4421
4534
  mtime: s.mtime.toISOString()
4422
4535
  };
@@ -5208,10 +5321,11 @@ async function emitPages(pages, outDir, _options = {}) {
5208
5321
  }).join("\n");
5209
5322
  const propsHelper = "\nexport type InertiaProps<K extends InertiaPageName> = import('@dudousxd/nestjs-inertia').InertiaPages[K];\n";
5210
5323
  const sharedPropsBlock = buildSharedPropsBlock(_options.sharedProps ?? null);
5324
+ const scopesBlock = buildScopesBlock(pages);
5211
5325
  const content = `// Generated by @dudousxd/nestjs-codegen. Do not edit.
5212
5326
 
5213
5327
  export type InertiaPageName = ${pageNameUnion};
5214
- ${propsHelper}
5328
+ ${propsHelper}${scopesBlock}
5215
5329
  declare module '@dudousxd/nestjs-inertia' {
5216
5330
  interface InertiaPages {
5217
5331
  ${augBody}
@@ -5220,6 +5334,31 @@ ${sharedPropsBlock}}
5220
5334
  `;
5221
5335
  await (0, import_promises9.writeFile)((0, import_node_path13.join)(outDir, "pages.d.ts"), content, "utf8");
5222
5336
  }
5337
+ function buildScopesBlock(pages) {
5338
+ const byScope = /* @__PURE__ */ new Map();
5339
+ for (const page of pages) {
5340
+ const scope = page.scope ?? "default";
5341
+ const names = byScope.get(scope) ?? [];
5342
+ names.push(JSON.stringify(page.name));
5343
+ byScope.set(scope, names);
5344
+ }
5345
+ if (![...byScope.keys()].some((scope) => scope !== "default")) return "";
5346
+ if (!byScope.has("default")) byScope.set("default", []);
5347
+ const scopes = ["default", ...[...byScope.keys()].filter((s) => s !== "default")];
5348
+ const body = scopes.map((scope) => {
5349
+ const key = needsQuotes(scope) ? JSON.stringify(scope) : scope;
5350
+ const names = byScope.get(scope) ?? [];
5351
+ return ` ${key}: ${names.length ? names.join(" | ") : "never"};`;
5352
+ }).join("\n");
5353
+ return `
5354
+ /** The page names of each Inertia app: \`default\` (forRoot) and every \`pages.scopes\` entry. */
5355
+ export interface InertiaScopePages {
5356
+ ${body}
5357
+ }
5358
+
5359
+ export type InertiaScopePageName<S extends keyof InertiaScopePages> = InertiaScopePages[S];
5360
+ `;
5361
+ }
5223
5362
  function buildSharedPropsBlock(sharedProps) {
5224
5363
  if (!sharedProps) return "";
5225
5364
  if (sharedProps.isImportRef) {
@@ -5401,12 +5540,7 @@ async function generate(config, inputRoutes = [], entryPoint = "cli") {
5401
5540
  }
5402
5541
  if (config.pages) {
5403
5542
  const pagesConfig = config.pages;
5404
- const pages = await discoverPages({
5405
- glob: pagesConfig.glob,
5406
- cwd: config.codegen.cwd,
5407
- propsExport: pagesConfig.propsExport,
5408
- componentNameStrategy: pagesConfig.componentNameStrategy
5409
- });
5543
+ const pages = await discoverPageScopes(config);
5410
5544
  const sharedProps = discoverSharedPropsFromConfig(config);
5411
5545
  await emitPages(pages, config.codegen.outDir, {
5412
5546
  propsExport: pagesConfig.propsExport,
@@ -5581,12 +5715,17 @@ async function watch(config, onChange, options = {}) {
5581
5715
  await runInitialPass();
5582
5716
  }
5583
5717
  let pagesDebounceTimer;
5584
- const pagesGlob = config.pages?.glob ?? ".nestjs-codegen-no-pages";
5585
- const pagesWatcher = import_chokidar.default.watch((0, import_node_path17.join)(config.codegen.cwd, pagesGlob), {
5586
- ignoreInitial: true,
5587
- persistent: true,
5588
- awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
5589
- });
5718
+ const pageGlobs = allPageGlobs(config);
5719
+ const pagesWatcher = import_chokidar.default.watch(
5720
+ (pageGlobs.length ? pageGlobs : [".nestjs-codegen-no-pages"]).map(
5721
+ (glob) => (0, import_node_path17.join)(config.codegen.cwd, glob)
5722
+ ),
5723
+ {
5724
+ ignoreInitial: true,
5725
+ persistent: true,
5726
+ awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
5727
+ }
5728
+ );
5590
5729
  function schedulePagesRegenerate() {
5591
5730
  if (pagesDebounceTimer !== void 0) {
5592
5731
  clearTimeout(pagesDebounceTimer);
@@ -5666,7 +5805,7 @@ async function watch(config, onChange, options = {}) {
5666
5805
  }
5667
5806
 
5668
5807
  // src/index.ts
5669
- var VERSION = "0.26.0";
5808
+ var VERSION = "0.27.0";
5670
5809
 
5671
5810
  // src/generate-manifest.ts
5672
5811
  var MANIFEST_FILE = ".codegen-manifest.json";
@@ -5726,8 +5865,7 @@ function diffConfigKeyHashes(previous, current) {
5726
5865
  return [...keys].filter((key) => previous[key] !== current[key]).sort();
5727
5866
  }
5728
5867
  async function discoverInputFiles(config) {
5729
- const globs = [config.contracts.glob, config.forms.watch];
5730
- if (config.pages) globs.push(config.pages.glob);
5868
+ const globs = [config.contracts.glob, config.forms.watch, ...allPageGlobs(config)];
5731
5869
  const cwd = config.codegen.cwd;
5732
5870
  const matched = await (0, import_fast_glob3.default)(globs, { cwd, absolute: true, onlyFiles: true });
5733
5871
  return [...new Set(matched)].sort();