@zdavison/nestjs-rpc-toolkit 0.1.5 → 0.2.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.
@@ -38,7 +38,6 @@ const ts_morph_1 = require("ts-morph");
38
38
  const path = __importStar(require("path"));
39
39
  const fs = __importStar(require("fs"));
40
40
  const glob_1 = require("glob");
41
- const package_manager_utils_1 = require("../utils/package-manager.utils");
42
41
  class RpcTypesGenerator {
43
42
  constructor(options) {
44
43
  this.options = options;
@@ -49,12 +48,19 @@ class RpcTypesGenerator {
49
48
  this.packageFiles = new Map();
50
49
  this.expandedPackages = [];
51
50
  this.fileToModuleMap = new Map();
52
- // Map of type name -> package it's imported from
53
- this.typeToPackageMap = new Map();
54
- // Set of all external packages that are imported in generated files
55
- this.externalPackagesUsed = new Set();
56
- // Map of package name -> version (from source package.json files)
57
- this.packageVersionMap = new Map();
51
+ /** Maps type names to their codec fields (field name -> codec name) */
52
+ this.codecFields = new Map();
53
+ /** Pending nested type checks to resolve after all types are processed */
54
+ this.pendingNestedTypes = new Map();
55
+ /**
56
+ * Built-in codec mappings: source type -> { codecName, wireType }
57
+ * Extensible: add more entries to support additional types.
58
+ */
59
+ this.codecMappings = {
60
+ 'Date': { codecName: 'Date', wireType: 'string' },
61
+ // Future: 'BigInt': { codecName: 'BigInt', wireType: 'string' },
62
+ // Future: 'Buffer': { codecName: 'Buffer', wireType: 'string' },
63
+ };
58
64
  // Load configuration
59
65
  this.config = this.loadConfig();
60
66
  // Expand wildcard patterns in package paths
@@ -166,9 +172,33 @@ class RpcTypesGenerator {
166
172
  this.extractTypesFromFile(sourceFile);
167
173
  });
168
174
  });
175
+ // Third pass: resolve nested type references (fields that reference types with codec fields)
176
+ this.resolveNestedTypeReferences();
169
177
  // Generate the aggregated types file
170
178
  this.generateTypesFile();
171
179
  }
180
+ /**
181
+ * Resolve pending nested type references.
182
+ * For each type, check if any of its fields reference other types that have codec fields.
183
+ * If so, add a nested type reference (prefixed with @) to the codec fields.
184
+ */
185
+ resolveNestedTypeReferences() {
186
+ this.pendingNestedTypes.forEach((pendingChecks, typeName) => {
187
+ pendingChecks.forEach(({ propName, typeName: nestedTypeName }) => {
188
+ // Check if the referenced type has codec fields
189
+ if (this.codecFields.has(nestedTypeName)) {
190
+ // Add a nested type reference to the parent type's codec fields
191
+ let typeCodecFields = this.codecFields.get(typeName);
192
+ if (!typeCodecFields) {
193
+ typeCodecFields = new Map();
194
+ this.codecFields.set(typeName, typeCodecFields);
195
+ }
196
+ // Use @ prefix to indicate a nested type reference
197
+ typeCodecFields.set(propName, `@${nestedTypeName}`);
198
+ }
199
+ });
200
+ });
201
+ }
172
202
  scanForRpcMethods(sourceFile) {
173
203
  sourceFile.forEachDescendant((node) => {
174
204
  if (node.getKind() === ts_morph_1.ts.SyntaxKind.MethodDeclaration) {
@@ -190,8 +220,6 @@ class RpcTypesGenerator {
190
220
  });
191
221
  }
192
222
  extractTypesFromFile(sourceFile) {
193
- // First, extract import information
194
- this.extractImports(sourceFile);
195
223
  sourceFile.forEachDescendant((node) => {
196
224
  if (node.getKind() === ts_morph_1.ts.SyntaxKind.InterfaceDeclaration) {
197
225
  this.extractInterface(node, sourceFile);
@@ -207,64 +235,47 @@ class RpcTypesGenerator {
207
235
  }
208
236
  });
209
237
  }
210
- extractImports(sourceFile) {
211
- const importDeclarations = sourceFile.getImportDeclarations();
212
- importDeclarations.forEach(importDecl => {
213
- const moduleSpecifier = importDecl.getModuleSpecifierValue();
214
- // Only track imports from packages (not relative imports)
215
- if (!moduleSpecifier.startsWith('.') && !moduleSpecifier.startsWith('/')) {
216
- const namedImports = importDecl.getNamedImports();
217
- namedImports.forEach(namedImport => {
218
- const importedName = namedImport.getName();
219
- this.typeToPackageMap.set(importedName, moduleSpecifier);
220
- });
221
- // Try to resolve package version from the source file's package.json
222
- if (!this.packageVersionMap.has(moduleSpecifier)) {
223
- const version = this.resolvePackageVersion(sourceFile.getFilePath(), moduleSpecifier);
224
- if (version) {
225
- this.packageVersionMap.set(moduleSpecifier, version);
226
- }
227
- }
228
- }
229
- });
230
- }
231
- resolvePackageVersion(sourceFilePath, packageName) {
232
- // Walk up from the source file to find package.json
233
- let currentDir = path.dirname(sourceFilePath);
234
- while (currentDir !== path.dirname(currentDir)) { // Stop at root
235
- const packageJsonPath = path.join(currentDir, 'package.json');
236
- if (fs.existsSync(packageJsonPath)) {
237
- try {
238
- const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
239
- // Check dependencies and devDependencies
240
- const version = packageJson.dependencies?.[packageName] ||
241
- packageJson.devDependencies?.[packageName];
242
- if (version) {
243
- return version;
244
- }
245
- }
246
- catch (error) {
247
- // Ignore and continue searching
248
- }
249
- }
250
- currentDir = path.dirname(currentDir);
251
- }
252
- return null;
253
- }
254
238
  extractInterface(interfaceDeclaration, sourceFile) {
255
239
  const name = interfaceDeclaration.getName();
256
- const jsDoc = this.extractJsDoc(interfaceDeclaration);
257
- let source = interfaceDeclaration.getText();
258
- // Prepend JSDoc if available and not already in source
259
- if (jsDoc && !source.startsWith('/**')) {
260
- source = `${jsDoc}\n${source}`;
261
- }
262
- // Ensure the source has export keyword
263
- if (!source.includes('export interface')) {
264
- source = source.replace(/^(\/\*\*[\s\S]*?\*\/\n)?interface/, '$1export interface');
265
- }
266
240
  const moduleName = this.getModuleForFile(sourceFile.getFilePath());
241
+ const jsDoc = this.extractJsDoc(interfaceDeclaration);
267
242
  if (name && this.isRelevantInterface(name) && !this.isInternalType(name)) {
243
+ // Track codec fields and transform the interface source
244
+ const typeCodecFields = new Map();
245
+ // Track nested type references (for fields whose type has codec fields)
246
+ const pendingNestedTypeChecks = [];
247
+ const properties = interfaceDeclaration.getProperties();
248
+ let source = interfaceDeclaration.getText();
249
+ // Check each property for codec-handled types
250
+ properties.forEach((prop) => {
251
+ const propName = prop.getName();
252
+ const typeNode = prop.getTypeNode();
253
+ if (typeNode) {
254
+ const propType = typeNode.getText();
255
+ const codecInfo = this.getCodecForType(propType);
256
+ if (codecInfo) {
257
+ typeCodecFields.set(propName, codecInfo.codecName);
258
+ // Replace the type with wire type in the source
259
+ const newType = codecInfo.wireType;
260
+ source = source.replace(new RegExp(`(${propName}\\s*[?]?\\s*:\\s*)${propType.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`, 'g'), `$1${newType}`);
261
+ }
262
+ else {
263
+ // Check if this is a reference to another type (potential nested type)
264
+ const extractedType = this.extractMainTypeName(propType);
265
+ if (extractedType && !this.isBuiltInType(extractedType)) {
266
+ pendingNestedTypeChecks.push({ propName, typeName: extractedType });
267
+ }
268
+ }
269
+ }
270
+ });
271
+ // Store codec fields for this type
272
+ if (typeCodecFields.size > 0) {
273
+ this.codecFields.set(name, typeCodecFields);
274
+ }
275
+ // Store pending nested type checks to resolve after all types are processed
276
+ if (pendingNestedTypeChecks.length > 0) {
277
+ this.pendingNestedTypes.set(name, pendingNestedTypeChecks);
278
+ }
268
279
  this.interfaces.set(name, {
269
280
  name,
270
281
  source,
@@ -294,6 +305,10 @@ class RpcTypesGenerator {
294
305
  return result;
295
306
  }).join(', ')}>`
296
307
  : '';
308
+ // Track codec fields for this type
309
+ const typeCodecFields = new Map();
310
+ // Track nested type references (for fields whose type has codec fields)
311
+ const pendingNestedTypeChecks = [];
297
312
  // Extract DTO classes as interfaces
298
313
  const properties = classDeclaration.getProperties()
299
314
  .filter((prop) => !prop.hasModifier(ts_morph_1.ts.SyntaxKind.PrivateKeyword))
@@ -311,11 +326,32 @@ class RpcTypesGenerator {
311
326
  // Clean up the type string - remove import paths and keep it simple
312
327
  propType = this.cleanTypeString(fullType);
313
328
  }
329
+ // Check if this type needs a codec and convert to wire type
330
+ const codecInfo = this.getCodecForType(propType);
331
+ if (codecInfo) {
332
+ typeCodecFields.set(propName, codecInfo.codecName);
333
+ propType = codecInfo.wireType;
334
+ }
335
+ else {
336
+ // Check if this is a reference to another type (potential nested type)
337
+ const extractedType = this.extractMainTypeName(propType);
338
+ if (extractedType && !this.isBuiltInType(extractedType)) {
339
+ pendingNestedTypeChecks.push({ propName, typeName: extractedType });
340
+ }
341
+ }
314
342
  // Extract JSDoc for the property
315
343
  const propJsDoc = this.extractJsDoc(prop);
316
344
  const propJsDocStr = propJsDoc ? `${propJsDoc}\n` : '';
317
345
  return `${propJsDocStr} ${propName}: ${propType};`;
318
346
  });
347
+ // Store codec fields for this type
348
+ if (typeCodecFields.size > 0) {
349
+ this.codecFields.set(name, typeCodecFields);
350
+ }
351
+ // Store pending nested type checks to resolve after all types are processed
352
+ if (pendingNestedTypeChecks.length > 0) {
353
+ this.pendingNestedTypes.set(name, pendingNestedTypeChecks);
354
+ }
319
355
  if (properties.length > 0) {
320
356
  // Extract JSDoc for the class
321
357
  const classJsDoc = this.extractJsDoc(classDeclaration);
@@ -330,6 +366,27 @@ class RpcTypesGenerator {
330
366
  });
331
367
  }
332
368
  }
369
+ /**
370
+ * Check if a type needs a codec and return codec info.
371
+ * Returns undefined if the type doesn't need a codec.
372
+ */
373
+ getCodecForType(typeStr) {
374
+ const normalized = typeStr.replace(/\s/g, '');
375
+ // Check each known codec type
376
+ for (const [sourceType, codecInfo] of Object.entries(this.codecMappings)) {
377
+ // Match exact type or union with null/undefined
378
+ if (normalized === sourceType ||
379
+ normalized.includes(`${sourceType}|`) ||
380
+ normalized.includes(`|${sourceType}`)) {
381
+ return {
382
+ codecName: codecInfo.codecName,
383
+ // Replace the source type with wire type, preserving unions
384
+ wireType: typeStr.replace(new RegExp(`\\b${sourceType}\\b`, 'g'), codecInfo.wireType),
385
+ };
386
+ }
387
+ }
388
+ return undefined;
389
+ }
333
390
  extractTypeAlias(typeAliasDeclaration, sourceFile) {
334
391
  const name = typeAliasDeclaration.getName();
335
392
  let source = typeAliasDeclaration.getText();
@@ -401,54 +458,6 @@ class RpcTypesGenerator {
401
458
  name === 'RpcGenerationConfig' ||
402
459
  name === 'GeneratorOptions';
403
460
  }
404
- collectExternalImports(referencedTypes, genericTypeParamNames) {
405
- // Map of package name -> Set of type names to import from that package
406
- const externalImports = new Map();
407
- const typesToCheck = new Set(referencedTypes);
408
- const checkedTypes = new Set();
409
- // Recursively collect all external types and their dependencies
410
- while (typesToCheck.size > 0) {
411
- const currentType = Array.from(typesToCheck)[0];
412
- typesToCheck.delete(currentType);
413
- checkedTypes.add(currentType);
414
- // Skip if it's a built-in type, generic parameter, or internal type
415
- if (this.isBuiltInType(currentType) || genericTypeParamNames.has(currentType) || this.isInternalType(currentType)) {
416
- continue;
417
- }
418
- // Check if this type is defined locally (in our interfaces or enums)
419
- const isLocalType = this.interfaces.has(currentType) || this.enums.has(currentType);
420
- if (!isLocalType && this.typeToPackageMap.has(currentType)) {
421
- // This is an external type - add to imports
422
- const packageName = this.typeToPackageMap.get(currentType);
423
- if (!externalImports.has(packageName)) {
424
- externalImports.set(packageName, new Set());
425
- }
426
- externalImports.get(packageName).add(currentType);
427
- // Check if any of our source interfaces reference this type and extract nested types
428
- this.interfaces.forEach(interfaceDef => {
429
- if (interfaceDef.source.includes(currentType)) {
430
- this.extractTypeNames(interfaceDef.source).forEach(nestedType => {
431
- if (!checkedTypes.has(nestedType) && !genericTypeParamNames.has(nestedType)) {
432
- typesToCheck.add(nestedType);
433
- }
434
- });
435
- }
436
- });
437
- }
438
- else if (isLocalType) {
439
- // This is a local type - check if it references other external types
440
- const localDef = this.interfaces.get(currentType) || this.enums.get(currentType);
441
- if (localDef) {
442
- this.extractTypeNames(localDef.source).forEach(nestedType => {
443
- if (!checkedTypes.has(nestedType) && !genericTypeParamNames.has(nestedType)) {
444
- typesToCheck.add(nestedType);
445
- }
446
- });
447
- }
448
- }
449
- }
450
- return externalImports;
451
- }
452
461
  processMethod(method, sourceFile) {
453
462
  // Check for @RpcMethod decorator
454
463
  const rpcDecorator = method.getDecorators().find(decorator => {
@@ -568,7 +577,7 @@ class RpcTypesGenerator {
568
577
  // Generate the main types file that composes all modules
569
578
  this.generateMainTypesFile(moduleGroups);
570
579
  }
571
- generateModuleTypesFile(moduleName, methods, _interfaces, _enums) {
580
+ generateModuleTypesFile(moduleName, methods, interfaces, enums) {
572
581
  // Collect all type names referenced in RPC methods
573
582
  const referencedTypes = new Set();
574
583
  const genericTypeParamNames = new Set();
@@ -606,62 +615,61 @@ class RpcTypesGenerator {
606
615
  });
607
616
  }
608
617
  });
609
- // Recursively collect all transitive type dependencies (interfaces, type aliases, and enums)
610
- // Keep iterating until no new types are discovered
611
- const collectedTypes = new Set();
612
- let typesToProcess = new Set(referencedTypes);
613
- while (typesToProcess.size > 0) {
614
- const newTypesToProcess = new Set();
615
- typesToProcess.forEach(typeName => {
616
- if (collectedTypes.has(typeName) || genericTypeParamNames.has(typeName)) {
617
- return;
618
- }
619
- collectedTypes.add(typeName);
620
- // Check if this type is defined locally (interface or type alias)
621
- const interfaceDef = this.interfaces.get(typeName);
622
- if (interfaceDef) {
623
- // Extract all type references from this interface/type alias source
624
- this.extractTypeNames(interfaceDef.source).forEach(nestedType => {
625
- if (!collectedTypes.has(nestedType) && !genericTypeParamNames.has(nestedType)) {
626
- newTypesToProcess.add(nestedType);
627
- }
628
- });
629
- }
630
- // Check if this type is an enum
631
- const enumDef = this.enums.get(typeName);
632
- if (enumDef) {
633
- // Enums don't have nested type references, but mark as collected
634
- }
635
- });
636
- typesToProcess = newTypesToProcess;
637
- }
638
- // Update referencedTypes with all collected types
639
- collectedTypes.forEach(t => referencedTypes.add(t));
640
- // Collect external type imports needed
641
- const externalImports = this.collectExternalImports(referencedTypes, genericTypeParamNames);
642
618
  // Include enums that are actually referenced, from this module or others
643
619
  const referencedEnums = [];
644
- // Add all referenced enums
620
+ // First add enums from this module
621
+ enums.filter(enumDef => referencedTypes.has(enumDef.name)).forEach(enumDef => referencedEnums.push(enumDef));
622
+ // Then add enums from other modules that are referenced
645
623
  this.enums.forEach(enumDef => {
646
624
  if (referencedTypes.has(enumDef.name) &&
625
+ enumDef.module !== moduleName &&
647
626
  !referencedEnums.some(existing => existing.name === enumDef.name)) {
648
627
  referencedEnums.push(enumDef);
649
628
  }
650
629
  });
651
- // Include interfaces/type aliases that are actually referenced, from this module or others
630
+ // Include interfaces that are actually referenced, from this module or others
652
631
  const referencedInterfaces = [];
653
- // Add all referenced interfaces/type aliases
632
+ // First add interfaces from this module
633
+ interfaces.filter(interfaceDef => referencedTypes.has(interfaceDef.name)).forEach(interfaceDef => referencedInterfaces.push(interfaceDef));
634
+ // Then add interfaces from other modules that are referenced
654
635
  this.interfaces.forEach(interfaceDef => {
655
636
  if (referencedTypes.has(interfaceDef.name) &&
637
+ interfaceDef.module !== moduleName &&
656
638
  !referencedInterfaces.some(existing => existing.name === interfaceDef.name)) {
657
639
  referencedInterfaces.push(interfaceDef);
658
640
  }
659
641
  });
660
- // Sort interfaces/type aliases topologically so dependencies come before dependents
661
- const sortedInterfaces = this.topologicalSortTypes(referencedInterfaces, genericTypeParamNames);
642
+ // Recursively collect type dependencies from referenced interfaces
643
+ // Keep scanning until no new types are found
644
+ let prevSize = 0;
645
+ while (referencedTypes.size !== prevSize) {
646
+ prevSize = referencedTypes.size;
647
+ // Scan referenced interfaces for additional type dependencies
648
+ referencedInterfaces.forEach(interfaceDef => {
649
+ this.extractTypeNames(interfaceDef.source).forEach(typeName => {
650
+ if (!genericTypeParamNames.has(typeName)) {
651
+ referencedTypes.add(typeName);
652
+ }
653
+ });
654
+ });
655
+ // Re-check interfaces after scanning for dependencies (for nested types)
656
+ this.interfaces.forEach(interfaceDef => {
657
+ if (referencedTypes.has(interfaceDef.name) &&
658
+ !referencedInterfaces.some(existing => existing.name === interfaceDef.name)) {
659
+ referencedInterfaces.push(interfaceDef);
660
+ }
661
+ });
662
+ }
663
+ // Re-check enums after scanning interfaces for dependencies
664
+ this.enums.forEach(enumDef => {
665
+ if (referencedTypes.has(enumDef.name) &&
666
+ !referencedEnums.some(existing => existing.name === enumDef.name)) {
667
+ referencedEnums.push(enumDef);
668
+ }
669
+ });
662
670
  // Enums should come before interfaces that use them
663
671
  const moduleEnums = referencedEnums.map(enumDef => enumDef.source).join('\n\n');
664
- const moduleInterfaces = sortedInterfaces.map(interfaceDef => interfaceDef.source).join('\n\n');
672
+ const moduleInterfaces = referencedInterfaces.map(interfaceDef => interfaceDef.source).join('\n\n');
665
673
  // Generate domain interface for this module
666
674
  const domainMethodDefinitions = methods.map(method => {
667
675
  const methodNameWithoutModule = method.methodName;
@@ -678,39 +686,136 @@ ${domainMethodDefinitions}
678
686
  }`;
679
687
  // Build file content with enums before interfaces
680
688
  const typesSection = [moduleEnums, moduleInterfaces].filter(section => section.length > 0).join('\n\n');
681
- // Generate import statements for external types
682
- const importStatements = [];
683
- externalImports.forEach((types, packageName) => {
684
- const sortedTypes = Array.from(types).sort();
685
- importStatements.push(`import { ${sortedTypes.join(', ')} } from '${packageName}';`);
686
- // Track that this external package is used
687
- this.externalPackagesUsed.add(packageName);
688
- });
689
- const importsSection = importStatements.length > 0 ? importStatements.join('\n') + '\n\n' : '';
689
+ // Generate codec field metadata for types in this module only
690
+ const moduleCodecFields = this.generateCodecFieldsMetadata(moduleName);
690
691
  const fileContent = `// Auto-generated RPC types for ${moduleName.charAt(0).toUpperCase() + moduleName.slice(1)} module
691
692
  // Do not edit this file manually - it will be overwritten
692
693
  //
693
694
  // IMPORTANT: All types must be JSON-serializable for TCP transport when extracted to microservices
694
695
 
695
- ${importsSection}// ${moduleName.charAt(0).toUpperCase() + moduleName.slice(1)} module types
696
+ // ${moduleName.charAt(0).toUpperCase() + moduleName.slice(1)} module types
696
697
  ${typesSection}
697
698
 
698
699
  ${domainInterface}
700
+ ${moduleCodecFields}
699
701
  `;
700
702
  // Write to configured output directory
701
703
  const outputPath = path.join(this.options.rootDir, this.config.outputDir, `${moduleName}.rpc.gen.ts`);
702
704
  fs.writeFileSync(outputPath, fileContent, 'utf8');
703
705
  }
706
+ /** Generate codec metadata for types belonging to this module only */
707
+ generateCodecFieldsMetadata(moduleName) {
708
+ const codecEntries = [];
709
+ // Only include types that belong to this module
710
+ this.codecFields.forEach((fields, typeName) => {
711
+ const interfaceDef = this.interfaces.get(typeName);
712
+ if (interfaceDef && interfaceDef.module === moduleName && fields.size > 0) {
713
+ const fieldEntries = Array.from(fields.entries())
714
+ .map(([fieldName, codecName]) => ` ${fieldName}: '${codecName}'`)
715
+ .join(',\n');
716
+ codecEntries.push(` ${typeName}: {\n${fieldEntries}\n }`);
717
+ }
718
+ });
719
+ if (codecEntries.length === 0) {
720
+ return '';
721
+ }
722
+ return `
723
+ // Type metadata for automatic codec transformation
724
+ // Maps type names to field -> codec name mappings
725
+ // Used by RPC client for transparent serialization (Date <-> string, etc.)
726
+ export const RpcTypeInfo = {
727
+ ${codecEntries.join(',\n')}
728
+ } as const;
729
+ `;
730
+ }
731
+ /** Generate function metadata for RPC patterns (params and returns) */
732
+ generateRpcFunctionInfo(moduleGroups) {
733
+ const entries = [];
734
+ Object.values(moduleGroups).forEach(methods => {
735
+ methods.forEach(method => {
736
+ // Extract param types that have codec fields
737
+ const paramEntries = [];
738
+ method.paramTypes.forEach(param => {
739
+ const typeName = this.extractMainTypeName(param.type);
740
+ if (typeName && this.codecFields.has(typeName)) {
741
+ paramEntries.push(` ${param.name}: '${typeName}'`);
742
+ }
743
+ });
744
+ // Extract return type if it has codec fields
745
+ const returnType = this.extractMainTypeName(method.returnType);
746
+ const hasReturnCodec = returnType && this.codecFields.has(returnType);
747
+ // Only include if there are codec fields in params or return
748
+ if (paramEntries.length > 0 || hasReturnCodec) {
749
+ const paramsObj = paramEntries.length > 0
750
+ ? `{\n${paramEntries.join(',\n')}\n }`
751
+ : '{}';
752
+ const returnsValue = hasReturnCodec ? `'${returnType}'` : 'undefined';
753
+ entries.push(` '${method.pattern}': {\n params: ${paramsObj},\n returns: ${returnsValue}\n }`);
754
+ }
755
+ });
756
+ });
757
+ if (entries.length === 0) {
758
+ return '';
759
+ }
760
+ return `
761
+ // Function metadata for RPC patterns
762
+ // Maps patterns to their parameter and return type names for codec transformation
763
+ export const RpcFunctionInfo = {
764
+ ${entries.join(',\n')}
765
+ } as const;
766
+
767
+ export type RpcFunctionInfoType = typeof RpcFunctionInfo;
768
+ `;
769
+ }
770
+ /** Extract the main type name from a type string (e.g., "User" from "Promise<User>") */
771
+ extractMainTypeName(typeStr) {
772
+ // Remove array brackets
773
+ let type = typeStr.replace(/\[\]/g, '');
774
+ // Remove Promise wrapper
775
+ const promiseMatch = type.match(/Promise<(.+)>/);
776
+ if (promiseMatch) {
777
+ type = promiseMatch[1];
778
+ }
779
+ // Get the first capitalized identifier
780
+ const match = type.match(/\b([A-Z][a-zA-Z0-9]*)\b/);
781
+ return match ? match[1] : undefined;
782
+ }
783
+ /** Generate imports and re-exports for codec fields from module files */
784
+ generateCodecFieldReExports(moduleGroups) {
785
+ const modulesWithCodecFields = [];
786
+ // Check which modules have codec fields
787
+ Object.keys(moduleGroups).forEach(moduleName => {
788
+ // Check if any types in this module have codec fields
789
+ const moduleTypes = Array.from(this.interfaces.entries())
790
+ .filter(([_, def]) => def.module === moduleName)
791
+ .map(([name]) => name);
792
+ const hasCodecFields = moduleTypes.some(typeName => this.codecFields.has(typeName));
793
+ if (hasCodecFields) {
794
+ modulesWithCodecFields.push(moduleName);
795
+ }
796
+ });
797
+ if (modulesWithCodecFields.length === 0) {
798
+ return { imports: '', exports: '' };
799
+ }
800
+ // Generate imports with aliases to avoid conflicts
801
+ const imports = modulesWithCodecFields
802
+ .map(mod => `import { RpcTypeInfo as ${this.toCamelCase(mod)}TypeInfo } from './${mod}.rpc.gen';`)
803
+ .join('\n');
804
+ // Generate merged export
805
+ const mergeEntries = modulesWithCodecFields
806
+ .map(mod => ` ...${this.toCamelCase(mod)}TypeInfo`)
807
+ .join(',\n');
808
+ const exports = `
809
+ // Merged type metadata from all modules
810
+ export const RpcTypeInfo = {
811
+ ${mergeEntries}
812
+ } as const;
813
+ `;
814
+ return { imports, exports };
815
+ }
704
816
  generateMainTypesFile(moduleGroups) {
705
817
  const hasModules = Object.keys(moduleGroups).length > 0;
706
- // Helper to check if a type is external (imported from an npm package)
707
- const isExternalType = (typeName) => {
708
- return this.typeToPackageMap.has(typeName) &&
709
- !this.interfaces.has(typeName) &&
710
- !this.enums.has(typeName);
711
- };
712
818
  // Generate imports from module files - include domain interfaces and types
713
- // but EXCLUDE external types (they're imported in module files, not exported)
714
819
  const moduleImports = Object.keys(moduleGroups).map(moduleName => {
715
820
  // Collect all types referenced in this module's methods
716
821
  const referencedTypes = new Set();
@@ -745,8 +850,7 @@ ${domainInterface}
745
850
  });
746
851
  }
747
852
  });
748
- // Filter out built-in types, internal types, AND external types
749
- const typesList = Array.from(referencedTypes).filter(type => !this.isBuiltInType(type) && !this.isInternalType(type) && !isExternalType(type));
853
+ const typesList = Array.from(referencedTypes).filter(type => !this.isBuiltInType(type) && !this.isInternalType(type));
750
854
  const imports = [`${this.toCamelCase(moduleName)}Domain`];
751
855
  if (typesList.length > 0) {
752
856
  imports.push(...typesList);
@@ -754,7 +858,6 @@ ${domainInterface}
754
858
  return `import { ${imports.join(', ')} } from './${moduleName}.rpc.gen';`;
755
859
  }).join('\n');
756
860
  // Generate selective re-exports to avoid type conflicts
757
- // EXCLUDE external types - they should be imported directly from their packages
758
861
  const moduleReExports = Object.keys(moduleGroups).map(moduleName => {
759
862
  // Collect all types referenced in this module's methods
760
863
  const referencedTypes = new Set();
@@ -789,8 +892,7 @@ ${domainInterface}
789
892
  });
790
893
  }
791
894
  });
792
- // Filter out built-in types, internal types, AND external types
793
- const typesList = Array.from(referencedTypes).filter(type => !this.isBuiltInType(type) && !this.isInternalType(type) && !isExternalType(type));
895
+ const typesList = Array.from(referencedTypes).filter(type => !this.isBuiltInType(type) && !this.isInternalType(type));
794
896
  const exports = [`${this.toCamelCase(moduleName)}Domain`];
795
897
  if (typesList.length > 0) {
796
898
  exports.push(...typesList);
@@ -799,58 +901,6 @@ ${domainInterface}
799
901
  }).join('\n');
800
902
  // Generate common type re-exports from their original modules
801
903
  const commonTypeExports = this.generateCommonTypeExports(moduleGroups);
802
- // Collect all external types used across all modules for direct import/re-export
803
- const externalTypesUsed = new Map(); // package -> types
804
- Object.values(moduleGroups).forEach(methods => {
805
- methods.forEach(method => {
806
- const genericTypeParamNames = new Set();
807
- if (method.typeParameters) {
808
- method.typeParameters.forEach(typeParam => {
809
- genericTypeParamNames.add(typeParam.split(' ')[0]);
810
- });
811
- }
812
- // Collect types from params and return type
813
- const allTypes = new Set();
814
- method.paramTypes.forEach(param => {
815
- this.extractTypeNames(param.type).forEach(t => {
816
- if (!genericTypeParamNames.has(t))
817
- allTypes.add(t);
818
- });
819
- });
820
- this.extractTypeNames(method.returnType).forEach(t => {
821
- if (!genericTypeParamNames.has(t))
822
- allTypes.add(t);
823
- });
824
- // Check which are external types
825
- allTypes.forEach(typeName => {
826
- if (isExternalType(typeName)) {
827
- const packageName = this.typeToPackageMap.get(typeName);
828
- if (!externalTypesUsed.has(packageName)) {
829
- externalTypesUsed.set(packageName, new Set());
830
- }
831
- externalTypesUsed.get(packageName).add(typeName);
832
- }
833
- });
834
- });
835
- });
836
- // Generate import statements for external types
837
- const externalImportStatements = [];
838
- externalTypesUsed.forEach((types, packageName) => {
839
- const sortedTypes = Array.from(types).sort();
840
- externalImportStatements.push(`import type { ${sortedTypes.join(', ')} } from '${packageName}';`);
841
- });
842
- const externalImportsSection = externalImportStatements.length > 0
843
- ? externalImportStatements.join('\n') + '\n'
844
- : '';
845
- // Generate re-export statements for external types
846
- const externalReExportStatements = [];
847
- externalTypesUsed.forEach((types, packageName) => {
848
- const sortedTypes = Array.from(types).sort();
849
- externalReExportStatements.push(`export type { ${sortedTypes.join(', ')} } from '${packageName}';`);
850
- });
851
- const externalReExportsSection = externalReExportStatements.length > 0
852
- ? '\n// Re-export external types from their source packages\n' + externalReExportStatements.join('\n')
853
- : '';
854
904
  // Generate AllRpcMethods type for MessageBus
855
905
  const allRpcMethodsType = hasModules
856
906
  ? this.generateAllRpcMethodsType(moduleGroups)
@@ -868,19 +918,23 @@ ${Object.keys(moduleGroups).map(moduleName => ` ${moduleName}: ${this.toCamelCa
868
918
  export interface IRpcClient {
869
919
  // No RPC domains available
870
920
  }`;
921
+ // Generate RPC function info (params and returns) for codec transformation
922
+ const rpcFunctionInfo = this.generateRpcFunctionInfo(moduleGroups);
923
+ // Generate codec field imports and re-exports from module files
924
+ const codecFieldImportsAndExports = this.generateCodecFieldReExports(moduleGroups);
871
925
  const fileContent = `// Auto-generated RPC types from all modules
872
926
  // Do not edit this file manually - it will be overwritten
873
927
  //
874
928
  // SERIALIZATION REQUIREMENTS:
875
929
  // All @RpcMethod parameters and return types must be JSON-serializable for TCP transport.
876
930
  // Avoid: functions, callbacks, Buffer, Map/Set, DOM elements, class instances, undefined
877
- // Prefer: primitives, plain objects, arrays, null (instead of undefined)
931
+ // Prefer: primitives, plain objects, arrays, null (instead of undefined), Date (auto-converted)
878
932
 
879
- ${externalImportsSection}${moduleImports}
933
+ ${moduleImports}
934
+ ${codecFieldImportsAndExports.imports}
880
935
 
881
936
  // Re-export domain interfaces and types
882
937
  ${moduleReExports}
883
- ${externalReExportsSection}
884
938
 
885
939
  // Re-export common types from their primary modules
886
940
  ${commonTypeExports}
@@ -888,12 +942,16 @@ ${commonTypeExports}
888
942
  ${allRpcMethodsType}
889
943
 
890
944
  ${rpcClientInterface}
891
-
945
+ ${codecFieldImportsAndExports.exports}${rpcFunctionInfo}
892
946
  // Usage examples:
893
- // import { TypedRpcClient } from '@modular-monolith/rpc';
947
+ // import { RpcTypeInfo, RpcFunctionInfo } from '@your-org/lib-rpc';
948
+ // import { createRpcClientProxy } from '@zdavison/nestjs-rpc-toolkit';
894
949
  //
895
- // const user = await rpc.user.findOne({ id: 'user123' });
896
- // const products = await rpc.product.findByOwner({ ownerId: 'user123' });
950
+ // const rpc = createRpcClientProxy(client, {
951
+ // typeInfo: RpcTypeInfo,
952
+ // functionInfo: RpcFunctionInfo,
953
+ // });
954
+ // const user = await rpc.user.create({ ... }); // Dates auto-converted
897
955
  `;
898
956
  // Write to configured output directory
899
957
  const outputPath = path.join(this.options.rootDir, this.config.outputDir, 'all.rpc.gen.ts');
@@ -915,64 +973,6 @@ ${rpcClientInterface}
915
973
  console.log(` 📄 ${module}: ${methods.length} methods`);
916
974
  });
917
975
  }
918
- // Update output package.json with missing dependencies
919
- this.updateOutputPackageJson();
920
- }
921
- updateOutputPackageJson() {
922
- if (this.externalPackagesUsed.size === 0) {
923
- return; // No external packages to add
924
- }
925
- // Find the package.json for the output directory
926
- const outputDir = path.join(this.options.rootDir, this.config.outputDir);
927
- const packageJsonPath = this.findPackageJsonForOutput(outputDir);
928
- if (!packageJsonPath) {
929
- console.log(`⚠️ Could not find package.json for output directory ${this.config.outputDir}`);
930
- console.log(` External packages used: ${Array.from(this.externalPackagesUsed).join(', ')}`);
931
- return;
932
- }
933
- try {
934
- const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
935
- const dependencies = packageJson.dependencies || {};
936
- const missingDeps = [];
937
- const addedDeps = {};
938
- // Check which external packages are missing
939
- this.externalPackagesUsed.forEach(packageName => {
940
- if (!dependencies[packageName]) {
941
- missingDeps.push(packageName);
942
- const version = this.packageVersionMap.get(packageName) || 'workspace:*';
943
- addedDeps[packageName] = version;
944
- dependencies[packageName] = version;
945
- }
946
- });
947
- if (missingDeps.length > 0) {
948
- // Update package.json with new dependencies
949
- packageJson.dependencies = dependencies;
950
- // Write back to file with proper formatting
951
- fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n', 'utf-8');
952
- console.log(`📦 Updated ${path.relative(this.options.rootDir, packageJsonPath)} with missing dependencies:`);
953
- missingDeps.forEach(dep => {
954
- console.log(` ✓ ${dep}@${addedDeps[dep]}`);
955
- });
956
- // Detect package manager and show appropriate install command
957
- const packageManager = (0, package_manager_utils_1.detectPackageManager)(this.options.rootDir);
958
- console.log(`\n⚠️ Please run '${packageManager} install' to install the new dependencies before building.\n`);
959
- }
960
- }
961
- catch (error) {
962
- console.error(`❌ Error updating package.json: ${error instanceof Error ? error.message : String(error)}`);
963
- }
964
- }
965
- findPackageJsonForOutput(outputDir) {
966
- // Walk up from output directory to find package.json
967
- let currentDir = outputDir;
968
- while (currentDir !== path.dirname(currentDir)) { // Stop at root
969
- const packageJsonPath = path.join(currentDir, 'package.json');
970
- if (fs.existsSync(packageJsonPath)) {
971
- return packageJsonPath;
972
- }
973
- currentDir = path.dirname(currentDir);
974
- }
975
- return null;
976
976
  }
977
977
  generateParamsType(params) {
978
978
  if (params.length === 0)
@@ -1001,15 +1001,15 @@ ${rpcClientInterface}
1001
1001
  }
1002
1002
  extractTypeNames(typeString) {
1003
1003
  const typeNames = new Set();
1004
- // Remove JSDoc comments and single-line comments to avoid matching words in comments
1005
- const codeWithoutComments = typeString
1006
- .replace(/\/\*\*[\s\S]*?\*\//g, '') // Remove JSDoc comments
1007
- .replace(/\/\*[\s\S]*?\*\//g, '') // Remove multi-line comments
1008
- .replace(/\/\/.*$/gm, ''); // Remove single-line comments
1004
+ // Strip JSDoc comments before extracting type names
1005
+ // This prevents matching words in comments (e.g., "User" in "from a user")
1006
+ const withoutJsDoc = typeString
1007
+ .replace(/\/\*\*[\s\S]*?\*\//g, '') // Remove /** ... */ comments
1008
+ .replace(/\/\/[^\n]*/g, ''); // Remove // comments
1009
1009
  // Match type names (letters, numbers, underscore, $)
1010
1010
  // This regex will match identifiers that could be type names
1011
1011
  const typeNameRegex = /\b[A-Z][a-zA-Z0-9_$]*\b/g;
1012
- const matches = codeWithoutComments.match(typeNameRegex);
1012
+ const matches = withoutJsDoc.match(typeNameRegex);
1013
1013
  if (matches) {
1014
1014
  matches.forEach(match => {
1015
1015
  // Exclude built-in types and common generic types
@@ -1131,73 +1131,6 @@ export type AllRpcMethods = {
1131
1131
  ${methodEntries.join('\n')}
1132
1132
  };`;
1133
1133
  }
1134
- /**
1135
- * Topologically sort types so that dependencies come before dependents.
1136
- * This ensures type aliases and interfaces are defined before they are used.
1137
- */
1138
- topologicalSortTypes(types, genericTypeParamNames) {
1139
- if (types.length === 0)
1140
- return [];
1141
- // Build a dependency graph
1142
- const typeNames = new Set(types.map(t => t.name));
1143
- const dependencies = new Map();
1144
- types.forEach(typeDef => {
1145
- const deps = new Set();
1146
- this.extractTypeNames(typeDef.source).forEach(depName => {
1147
- // Only consider dependencies that are in our type set and not generic params
1148
- if (typeNames.has(depName) && depName !== typeDef.name && !genericTypeParamNames.has(depName)) {
1149
- deps.add(depName);
1150
- }
1151
- });
1152
- dependencies.set(typeDef.name, deps);
1153
- });
1154
- // Kahn's algorithm for topological sort
1155
- // We want dependencies to come BEFORE dependents
1156
- const sorted = [];
1157
- const typeMap = new Map(types.map(t => [t.name, t]));
1158
- // In-degree = number of dependencies a type has (within our type set)
1159
- // Types with 0 dependencies should be output first
1160
- const inDegree = new Map();
1161
- typeNames.forEach(name => {
1162
- const deps = dependencies.get(name) || new Set();
1163
- inDegree.set(name, deps.size);
1164
- });
1165
- // Start with types that have no dependencies
1166
- const queue = [];
1167
- inDegree.forEach((degree, name) => {
1168
- if (degree === 0) {
1169
- queue.push(name);
1170
- }
1171
- });
1172
- while (queue.length > 0) {
1173
- const name = queue.shift();
1174
- const typeDef = typeMap.get(name);
1175
- if (typeDef) {
1176
- sorted.push(typeDef);
1177
- }
1178
- // For each type that depends on this one, decrement its in-degree
1179
- // (because one of its dependencies has now been processed)
1180
- typeNames.forEach(dependentName => {
1181
- const deps = dependencies.get(dependentName);
1182
- if (deps && deps.has(name)) {
1183
- const newDegree = (inDegree.get(dependentName) || 1) - 1;
1184
- inDegree.set(dependentName, newDegree);
1185
- if (newDegree === 0) {
1186
- queue.push(dependentName);
1187
- }
1188
- }
1189
- });
1190
- }
1191
- // If there's a cycle, just append remaining types (they have circular deps)
1192
- if (sorted.length < types.length) {
1193
- types.forEach(t => {
1194
- if (!sorted.includes(t)) {
1195
- sorted.push(t);
1196
- }
1197
- });
1198
- }
1199
- return sorted;
1200
- }
1201
1134
  }
1202
1135
  exports.RpcTypesGenerator = RpcTypesGenerator;
1203
1136
  //# sourceMappingURL=rpc-types-generator.js.map