@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,6 +1,6 @@
1
1
  import * as _nestjs_common from '@nestjs/common';
2
2
  import { DynamicModule, OnApplicationBootstrap, OnModuleDestroy, ExecutionContext } from '@nestjs/common';
3
- import { U as UserConfig } from '../index-Bbw73Kkb.cjs';
3
+ import { U as UserConfig } from '../index-Dc4s883u.cjs';
4
4
  import 'ts-morph';
5
5
 
6
6
  /**
@@ -1,6 +1,6 @@
1
1
  import * as _nestjs_common from '@nestjs/common';
2
2
  import { DynamicModule, OnApplicationBootstrap, OnModuleDestroy, ExecutionContext } from '@nestjs/common';
3
- import { U as UserConfig } from '../index-Bbw73Kkb.js';
3
+ import { U as UserConfig } from '../index-Dc4s883u.js';
4
4
  import 'ts-morph';
5
5
 
6
6
  /**
@@ -70,12 +70,33 @@ function validateUserConfig(userConfig) {
70
70
  "validation adapter is required \u2014 install @dudousxd/nestjs-codegen-zod and pass zodAdapter, or use @dudousxd/nestjs-codegen-valibot / -arktype"
71
71
  );
72
72
  }
73
- if (userConfig.pages && typeof userConfig.pages.glob !== "string") {
73
+ if (userConfig.pages && !isGlobInput(userConfig.pages.glob)) {
74
74
  throw new ConfigError(
75
- "Config validation failed: `pages.glob` must be a string when `pages` is set"
75
+ "Config validation failed: `pages.glob` must be a string or a non-empty array of strings when `pages` is set"
76
+ );
77
+ }
78
+ for (const [scope, scopeConfig] of Object.entries(userConfig.pages?.scopes ?? {})) {
79
+ if (scope === "default") {
80
+ throw new ConfigError(
81
+ 'Config validation failed: `pages.scopes` cannot declare "default" \u2014 the default app is `pages.glob` (InertiaModule.forRoot).'
82
+ );
83
+ }
84
+ if (!isGlobInput(scopeConfig?.glob)) {
85
+ throw new ConfigError(
86
+ `Config validation failed: \`pages.scopes.${scope}.glob\` must be a string or a non-empty array of strings`
87
+ );
88
+ }
89
+ }
90
+ if (userConfig.scopes && Object.keys(userConfig.scopes).length > 0) {
91
+ console.warn(
92
+ "[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)."
76
93
  );
77
94
  }
78
95
  }
96
+ function isGlobInput(value) {
97
+ if (typeof value === "string") return true;
98
+ return Array.isArray(value) && value.length > 0 && value.every((glob) => typeof glob === "string");
99
+ }
79
100
  function applyDefaults(userConfig, cwd) {
80
101
  validateUserConfig(userConfig);
81
102
  const outDir = userConfig.codegen?.outDir ? resolveAbsolute(cwd, userConfig.codegen.outDir) : join(cwd, ".nestjs-codegen");
@@ -102,7 +123,17 @@ function applyDefaults(userConfig, cwd) {
102
123
  pages: userConfig.pages ? {
103
124
  glob: userConfig.pages.glob,
104
125
  propsExport: userConfig.pages.propsExport ?? "ComponentProps",
105
- componentNameStrategy: userConfig.pages.componentNameStrategy ?? "relative-no-ext"
126
+ componentNameStrategy: userConfig.pages.componentNameStrategy ?? "relative-no-ext",
127
+ scopes: Object.fromEntries(
128
+ Object.entries(userConfig.pages.scopes ?? {}).map(([scope, scopeConfig]) => [
129
+ scope,
130
+ {
131
+ glob: scopeConfig.glob,
132
+ prefix: scopeConfig.prefix ?? `${scope}/`,
133
+ componentNameStrategy: scopeConfig.componentNameStrategy ?? userConfig.pages?.componentNameStrategy ?? "relative-no-ext"
134
+ }
135
+ ])
136
+ )
106
137
  } : null,
107
138
  contracts: {
108
139
  glob: userConfig.contracts?.glob ?? "src/**/*.controller.ts",
@@ -3344,32 +3375,86 @@ function extractFromSourceFile(sourceFile, project) {
3344
3375
  return routes;
3345
3376
  }
3346
3377
 
3347
- // src/generate.ts
3348
- import { mkdir as mkdir9, writeFile as writeFile9 } from "fs/promises";
3349
- import { dirname as dirname4, join as join13 } from "path";
3350
-
3351
3378
  // src/discovery/pages.ts
3352
3379
  import { readFile } from "fs/promises";
3353
- import { join as join3, relative as relative2 } from "path";
3380
+ import { join as join3, relative as relative2, sep as sep2 } from "path";
3354
3381
  import fg2 from "fast-glob";
3355
3382
  var NON_PAGE_FILE_RE = /\.(?:test|spec|stories|story)\.(?:tsx?|jsx?|vue|svelte)$/i;
3383
+ var MAGIC_SEGMENT_RE = /[*?{}[\]()!]/;
3384
+ function globStaticBase(glob) {
3385
+ const segments = glob.split("/");
3386
+ const staticSegments = [];
3387
+ for (const segment of segments.slice(0, -1)) {
3388
+ if (MAGIC_SEGMENT_RE.test(segment)) break;
3389
+ staticSegments.push(segment);
3390
+ }
3391
+ return staticSegments.join("/");
3392
+ }
3356
3393
  async function discoverPages(opts) {
3357
- const allFiles = await fg2(opts.glob, { cwd: opts.cwd, absolute: true });
3358
- const files = allFiles.filter((f) => !NON_PAGE_FILE_RE.test(f));
3359
- files.sort();
3360
- const globStatic = opts.glob.split("*")[0]?.replace(/\/$/, "") ?? "";
3361
- const pagesBase = join3(opts.cwd, globStatic);
3394
+ const globs = typeof opts.glob === "string" ? [opts.glob] : opts.glob;
3395
+ const prefix = opts.prefix ?? "";
3396
+ const scope = opts.scope ?? "default";
3397
+ const seenFiles = /* @__PURE__ */ new Set();
3362
3398
  const out = [];
3363
- for (const file of files) {
3364
- const rel = relative2(opts.cwd, file);
3365
- const nameRel = relative2(pagesBase, file);
3366
- const name = computeName(nameRel, opts.componentNameStrategy);
3367
- const source = await readFile(file, "utf8");
3368
- const propsSource = extractPropsSource(source, opts.propsExport);
3369
- out.push({ name, absolutePath: file, relativePath: rel, propsSource });
3399
+ for (const glob of globs) {
3400
+ const files = (await fg2(glob, { cwd: opts.cwd, absolute: true })).filter((f) => !NON_PAGE_FILE_RE.test(f) && !seenFiles.has(f)).sort();
3401
+ const pagesBase = join3(opts.cwd, globStaticBase(glob));
3402
+ for (const file of files) {
3403
+ seenFiles.add(file);
3404
+ const rel = relative2(opts.cwd, file);
3405
+ const nameRel = relative2(pagesBase, file).split(sep2).join("/");
3406
+ const name = `${prefix}${computeName(nameRel, opts.componentNameStrategy)}`;
3407
+ const source = await readFile(file, "utf8");
3408
+ const propsSource = extractPropsSource(source, opts.propsExport);
3409
+ out.push({ name, absolutePath: file, relativePath: rel, propsSource, scope });
3410
+ }
3411
+ }
3412
+ assertUniqueNames(out);
3413
+ return out;
3414
+ }
3415
+ async function discoverPageScopes(config) {
3416
+ const pages = config.pages;
3417
+ if (!pages) return [];
3418
+ const common = { cwd: config.codegen.cwd, propsExport: pages.propsExport };
3419
+ const out = await discoverPages({
3420
+ ...common,
3421
+ glob: pages.glob,
3422
+ componentNameStrategy: pages.componentNameStrategy
3423
+ });
3424
+ for (const [scope, scopeConfig] of Object.entries(pages.scopes ?? {})) {
3425
+ out.push(
3426
+ ...await discoverPages({
3427
+ ...common,
3428
+ glob: scopeConfig.glob,
3429
+ componentNameStrategy: scopeConfig.componentNameStrategy,
3430
+ prefix: scopeConfig.prefix,
3431
+ scope
3432
+ })
3433
+ );
3370
3434
  }
3435
+ assertUniqueNames(out);
3371
3436
  return out;
3372
3437
  }
3438
+ function allPageGlobs(config) {
3439
+ if (!config.pages) return [];
3440
+ const asList = (glob) => typeof glob === "string" ? [glob] : glob;
3441
+ return [
3442
+ ...asList(config.pages.glob),
3443
+ ...Object.values(config.pages.scopes ?? {}).flatMap((scope) => asList(scope.glob))
3444
+ ];
3445
+ }
3446
+ function assertUniqueNames(pages) {
3447
+ const byName = /* @__PURE__ */ new Map();
3448
+ for (const page of pages) {
3449
+ const previous = byName.get(page.name);
3450
+ if (previous) {
3451
+ throw new Error(
3452
+ `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.`
3453
+ );
3454
+ }
3455
+ byName.set(page.name, page);
3456
+ }
3457
+ }
3373
3458
  function computeName(rel, strat) {
3374
3459
  if (typeof strat === "function") return strat(rel);
3375
3460
  const noExt = rel.replace(/\.(tsx?|vue|svelte)$/, "");
@@ -3400,6 +3485,10 @@ function extractPropsSource(source, exportName) {
3400
3485
  return source.slice(start);
3401
3486
  }
3402
3487
 
3488
+ // src/generate.ts
3489
+ import { mkdir as mkdir9, writeFile as writeFile9 } from "fs/promises";
3490
+ import { dirname as dirname4, join as join13 } from "path";
3491
+
3403
3492
  // src/discovery/shared-props.ts
3404
3493
  import { join as join4 } from "path";
3405
3494
  import { Node as Node11, Project as Project4, SyntaxKind as SyntaxKind4 } from "ts-morph";
@@ -4034,28 +4123,28 @@ function renderFetcherRequest(req, binaryResponse) {
4034
4123
  }
4035
4124
  function emitReqHelper() {
4036
4125
  return [
4037
- "/** Awaitable request handle. `await api.x.y({...})` runs the fetch; extensions add query/mutation helpers. */",
4038
- "type __Req<R> = {",
4039
- " then<T1 = R, T2 = never>(",
4126
+ "/** An awaitable request handle: a lazy, memoized `Promise` that also carries `fetch()` and the members extensions add. */",
4127
+ "export type RequestHandle<R, M = {}> = Promise<R> & { fetch(): Promise<R> } & M;",
4128
+ "class __Request<R> extends Promise<R> {",
4129
+ " static override get [Symbol.species](): PromiseConstructor {",
4130
+ " return Promise;",
4131
+ " }",
4132
+ " private __run: () => Promise<R>;",
4133
+ " private __p: Promise<R> | undefined;",
4134
+ " constructor(run: () => Promise<R>) {",
4135
+ " super(() => {});",
4136
+ " this.__run = run;",
4137
+ " }",
4138
+ " override then<T1 = R, T2 = never>(",
4040
4139
  " onfulfilled?: ((value: R) => T1 | PromiseLike<T1>) | null,",
4041
4140
  " onrejected?: ((reason: unknown) => T2 | PromiseLike<T2>) | null,",
4042
- " ): Promise<T1 | T2>;",
4043
- " catch<T = never>(onrejected?: ((reason: unknown) => T | PromiseLike<T>) | null): Promise<R | T>;",
4044
- " finally(onfinally?: (() => void) | null): Promise<R>;",
4045
- " fetch(): Promise<R>;",
4046
- "};",
4047
- "function __req<R>(run: () => Promise<R>): __Req<R> {",
4048
- " let __p: Promise<R> | undefined;",
4049
- " const __promise = () => {",
4050
- " __p ??= run();",
4051
- " return __p;",
4052
- " };",
4053
- " return {",
4054
- " then: (onfulfilled, onrejected) => __promise().then(onfulfilled, onrejected),",
4055
- " catch: (onrejected) => __promise().catch(onrejected),",
4056
- " finally: (onfinally) => __promise().finally(onfinally),",
4057
- " fetch: run,",
4058
- " };",
4141
+ " ): Promise<T1 | T2> {",
4142
+ " this.__p ??= this.__run();",
4143
+ " return this.__p.then(onfulfilled, onrejected);",
4144
+ " }",
4145
+ "}",
4146
+ "function __req<R, M extends object = {}>(run: () => Promise<R>, members?: M): RequestHandle<R, M> {",
4147
+ " return Object.assign(new __Request<R>(run), { fetch: run }, members) as RequestHandle<R, M>;",
4059
4148
  "}",
4060
4149
  ""
4061
4150
  ];
@@ -4073,18 +4162,26 @@ function emitFilterFieldGuard() {
4073
4162
  ];
4074
4163
  }
4075
4164
  function renderLeaf(pad, objKey, req, requestExpr, members, streamExpr) {
4076
- const lines = [`${pad}${objKey}: (input?: ${req.inputType}) => ({`];
4077
- lines.push(`${pad} ...__req<${req.responseType}>(() => ${requestExpr}),`);
4165
+ const memberLines = [];
4078
4166
  if (req.filterFieldsExpr) {
4079
- lines.push(`${pad} filterFields: ${req.filterFieldsExpr},`);
4167
+ memberLines.push(`${pad} filterFields: ${req.filterFieldsExpr},`);
4080
4168
  }
4081
4169
  if (streamExpr) {
4082
- lines.push(`${pad} stream: () => ${streamExpr},`);
4170
+ memberLines.push(`${pad} stream: () => ${streamExpr},`);
4083
4171
  }
4084
4172
  for (const [name, value] of Object.entries(members)) {
4085
- lines.push(`${pad} ${name}: ${value},`);
4086
- }
4087
- lines.push(`${pad}}),`);
4173
+ memberLines.push(`${pad} ${name}: ${value},`);
4174
+ }
4175
+ const body = [requestExpr, ...memberLines].join("\n");
4176
+ const param = /\binput\b/.test(body) ? `input?: ${req.inputType}` : "";
4177
+ const lines = [`${pad}${objKey}: (${param}) =>`];
4178
+ if (memberLines.length === 0) {
4179
+ lines.push(`${pad} __req<${req.responseType}>(() => ${requestExpr}),`);
4180
+ return lines;
4181
+ }
4182
+ lines.push(`${pad} __req(() => ${requestExpr}, {`);
4183
+ lines.push(...memberLines);
4184
+ lines.push(`${pad} }),`);
4088
4185
  return lines;
4089
4186
  }
4090
4187
  function renderStreamExpr(req) {
@@ -4218,6 +4315,23 @@ var EMPTY_PATH_NAMESPACE = [
4218
4315
  function isClientCallable(route) {
4219
4316
  return route.method.toUpperCase() !== "ALL";
4220
4317
  }
4318
+ var ROUTES_IMPORT_PLACEHOLDER = "/* @@routes-import@@ */";
4319
+ var ROUTES_EXPORTS = [
4320
+ // Called, never mentioned in prose: match the call so a comment saying "route" doesn't count.
4321
+ { name: "route", typeOnly: false },
4322
+ { name: "ROUTES", typeOnly: false },
4323
+ { name: "RouteName", typeOnly: true },
4324
+ { name: "ExtractParams", typeOnly: true },
4325
+ { name: "RouteParams", typeOnly: true }
4326
+ ];
4327
+ function finishRoutesImport(lines) {
4328
+ const body = lines.filter((l) => l !== ROUTES_IMPORT_PLACEHOLDER).join("\n");
4329
+ const used = ROUTES_EXPORTS.filter(
4330
+ ({ name }) => new RegExp(name === "route" ? "\\broute\\(" : `\\b${name}\\b`).test(body)
4331
+ ).map(({ name, typeOnly }) => typeOnly ? `type ${name}` : name);
4332
+ const importLine = used.length ? `import { ${used.join(", ")} } from './routes.js';` : null;
4333
+ return lines.flatMap((l) => l === ROUTES_IMPORT_PLACEHOLDER ? importLine ? [importLine] : [] : [l]).join("\n");
4334
+ }
4221
4335
  function buildApiFile(routes, outDir, opts = {}) {
4222
4336
  const fetcherImportPath = opts.fetcherImportPath;
4223
4337
  const serialization = opts.serialization ?? "json";
@@ -4277,9 +4391,7 @@ function buildApiFile(routes, outDir, opts = {}) {
4277
4391
  for (const imp of ext.apiHeader?.(ctx)?.imports ?? []) pushImport(imp);
4278
4392
  }
4279
4393
  lines.push(...extImports);
4280
- lines.push(
4281
- "import { route, ROUTES, type RouteName, type ExtractParams, type RouteParams } from './routes.js';"
4282
- );
4394
+ lines.push(ROUTES_IMPORT_PLACEHOLDER);
4283
4395
  const runtimeImport = fetcherImportPath ?? "@dudousxd/nestjs-client";
4284
4396
  lines.push(`import type { Fetcher } from '${runtimeImport}';`);
4285
4397
  if (serialization === "json" && contracted.length > 0) {
@@ -4330,7 +4442,7 @@ function buildApiFile(routes, outDir, opts = {}) {
4330
4442
  lines.push(...statements, "");
4331
4443
  }
4332
4444
  }
4333
- return lines.join("\n");
4445
+ return finishRoutesImport(lines);
4334
4446
  }
4335
4447
  const tree = /* @__PURE__ */ new Map();
4336
4448
  for (const r of contracted) {
@@ -4383,7 +4495,7 @@ function buildApiFile(routes, outDir, opts = {}) {
4383
4495
  lines.push(...statements, "");
4384
4496
  }
4385
4497
  }
4386
- return lines.join("\n");
4498
+ return finishRoutesImport(lines);
4387
4499
  }
4388
4500
 
4389
4501
  // src/emit/emit-cache.ts
@@ -4396,6 +4508,7 @@ async function emitCache(pages, outDir) {
4396
4508
  const s = await stat(p.absolutePath);
4397
4509
  return {
4398
4510
  name: p.name,
4511
+ scope: p.scope ?? "default",
4399
4512
  relativePath: p.relativePath,
4400
4513
  mtime: s.mtime.toISOString()
4401
4514
  };
@@ -5187,10 +5300,11 @@ async function emitPages(pages, outDir, _options = {}) {
5187
5300
  }).join("\n");
5188
5301
  const propsHelper = "\nexport type InertiaProps<K extends InertiaPageName> = import('@dudousxd/nestjs-inertia').InertiaPages[K];\n";
5189
5302
  const sharedPropsBlock = buildSharedPropsBlock(_options.sharedProps ?? null);
5303
+ const scopesBlock = buildScopesBlock(pages);
5190
5304
  const content = `// Generated by @dudousxd/nestjs-codegen. Do not edit.
5191
5305
 
5192
5306
  export type InertiaPageName = ${pageNameUnion};
5193
- ${propsHelper}
5307
+ ${propsHelper}${scopesBlock}
5194
5308
  declare module '@dudousxd/nestjs-inertia' {
5195
5309
  interface InertiaPages {
5196
5310
  ${augBody}
@@ -5199,6 +5313,31 @@ ${sharedPropsBlock}}
5199
5313
  `;
5200
5314
  await writeFile7(join11(outDir, "pages.d.ts"), content, "utf8");
5201
5315
  }
5316
+ function buildScopesBlock(pages) {
5317
+ const byScope = /* @__PURE__ */ new Map();
5318
+ for (const page of pages) {
5319
+ const scope = page.scope ?? "default";
5320
+ const names = byScope.get(scope) ?? [];
5321
+ names.push(JSON.stringify(page.name));
5322
+ byScope.set(scope, names);
5323
+ }
5324
+ if (![...byScope.keys()].some((scope) => scope !== "default")) return "";
5325
+ if (!byScope.has("default")) byScope.set("default", []);
5326
+ const scopes = ["default", ...[...byScope.keys()].filter((s) => s !== "default")];
5327
+ const body = scopes.map((scope) => {
5328
+ const key = needsQuotes(scope) ? JSON.stringify(scope) : scope;
5329
+ const names = byScope.get(scope) ?? [];
5330
+ return ` ${key}: ${names.length ? names.join(" | ") : "never"};`;
5331
+ }).join("\n");
5332
+ return `
5333
+ /** The page names of each Inertia app: \`default\` (forRoot) and every \`pages.scopes\` entry. */
5334
+ export interface InertiaScopePages {
5335
+ ${body}
5336
+ }
5337
+
5338
+ export type InertiaScopePageName<S extends keyof InertiaScopePages> = InertiaScopePages[S];
5339
+ `;
5340
+ }
5202
5341
  function buildSharedPropsBlock(sharedProps) {
5203
5342
  if (!sharedProps) return "";
5204
5343
  if (sharedProps.isImportRef) {
@@ -5380,12 +5519,7 @@ async function generate(config, inputRoutes = [], entryPoint = "cli") {
5380
5519
  }
5381
5520
  if (config.pages) {
5382
5521
  const pagesConfig = config.pages;
5383
- const pages = await discoverPages({
5384
- glob: pagesConfig.glob,
5385
- cwd: config.codegen.cwd,
5386
- propsExport: pagesConfig.propsExport,
5387
- componentNameStrategy: pagesConfig.componentNameStrategy
5388
- });
5522
+ const pages = await discoverPageScopes(config);
5389
5523
  const sharedProps = discoverSharedPropsFromConfig(config);
5390
5524
  await emitPages(pages, config.codegen.outDir, {
5391
5525
  propsExport: pagesConfig.propsExport,
@@ -5560,12 +5694,17 @@ async function watch(config, onChange, options = {}) {
5560
5694
  await runInitialPass();
5561
5695
  }
5562
5696
  let pagesDebounceTimer;
5563
- const pagesGlob = config.pages?.glob ?? ".nestjs-codegen-no-pages";
5564
- const pagesWatcher = chokidar.watch(join15(config.codegen.cwd, pagesGlob), {
5565
- ignoreInitial: true,
5566
- persistent: true,
5567
- awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
5568
- });
5697
+ const pageGlobs = allPageGlobs(config);
5698
+ const pagesWatcher = chokidar.watch(
5699
+ (pageGlobs.length ? pageGlobs : [".nestjs-codegen-no-pages"]).map(
5700
+ (glob) => join15(config.codegen.cwd, glob)
5701
+ ),
5702
+ {
5703
+ ignoreInitial: true,
5704
+ persistent: true,
5705
+ awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
5706
+ }
5707
+ );
5569
5708
  function schedulePagesRegenerate() {
5570
5709
  if (pagesDebounceTimer !== void 0) {
5571
5710
  clearTimeout(pagesDebounceTimer);
@@ -5645,7 +5784,7 @@ async function watch(config, onChange, options = {}) {
5645
5784
  }
5646
5785
 
5647
5786
  // src/index.ts
5648
- var VERSION = "0.26.0";
5787
+ var VERSION = "0.27.0";
5649
5788
 
5650
5789
  // src/generate-manifest.ts
5651
5790
  var MANIFEST_FILE = ".codegen-manifest.json";
@@ -5705,8 +5844,7 @@ function diffConfigKeyHashes(previous, current) {
5705
5844
  return [...keys].filter((key) => previous[key] !== current[key]).sort();
5706
5845
  }
5707
5846
  async function discoverInputFiles(config) {
5708
- const globs = [config.contracts.glob, config.forms.watch];
5709
- if (config.pages) globs.push(config.pages.glob);
5847
+ const globs = [config.contracts.glob, config.forms.watch, ...allPageGlobs(config)];
5710
5848
  const cwd = config.codegen.cwd;
5711
5849
  const matched = await fg3(globs, { cwd, absolute: true, onlyFiles: true });
5712
5850
  return [...new Set(matched)].sort();