Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ describe('Router Decorator', () => {
class TestRouter {}

const metadata = Reflect.getMetadata(ROUTER_METADATA_KEY, TestRouter);
expect(metadata).toStrictEqual({alias: undefined, path: __filename})
expect(metadata.alias).toBeUndefined();
expect(metadata.path[0]).toBe(__filename)
});

it('should set router metadata with alias', () => {
Expand All @@ -18,7 +19,8 @@ describe('Router Decorator', () => {
class TestRouter {}

const metadata = Reflect.getMetadata(ROUTER_METADATA_KEY, TestRouter);
expect(metadata).toStrictEqual({alias, path: __filename})
expect(metadata.alias).toBe(alias);
expect(metadata.path[0]).toBe(__filename)
});

it('should not affect class methods', () => {
Expand All @@ -41,7 +43,10 @@ describe('Router Decorator', () => {
const metadata1 = Reflect.getMetadata(ROUTER_METADATA_KEY, TestRouter1);
const metadata2 = Reflect.getMetadata(ROUTER_METADATA_KEY, TestRouter2);

expect(metadata1).toEqual({ alias: 'router1', path: __filename });
expect(metadata2).toEqual({ alias: 'router2', path: __filename });
expect(metadata1.alias).toBe("router1");
expect(metadata1.path[0]).toBe(__filename);

expect(metadata2.alias).toBe('router2');
expect(metadata2.path[0]).toBe(__filename);
});
});
4 changes: 2 additions & 2 deletions packages/nestjs-trpc/lib/factories/middleware.factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import { RouterFactory } from './router.factory';
import { ProcedureFactory } from './procedure.factory';
import { isEqual, uniqWith } from 'lodash';

interface MiddlewareMetadata {
path: string;
export interface MiddlewareMetadata {
path: string[];
instance: Class<TRPCMiddleware> | Constructor<TRPCMiddleware>;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ describe('MiddlewareGenerator', () => {

describe('getMiddlewareInterface', () => {
it('should return null if middleware class name is not defined', async () => {
const result = await middlewareGenerator.getMiddlewareInterface('routerPath', {} as any, project);
const result = await middlewareGenerator.getMiddlewareInterface(['routerPath'], {} as any, project);
expect(result).toBeNull();
});

Expand All @@ -58,7 +58,7 @@ describe('MiddlewareGenerator', () => {

jest.spyOn(project, 'addSourceFileAtPath').mockReturnValue(sourceFile);

const result = await middlewareGenerator.getMiddlewareInterface('routerPath', TestMiddleware, project);
const result = await middlewareGenerator.getMiddlewareInterface(['routerPath'], TestMiddleware, project);
expect(result).toEqual({
name: 'TestMiddleware',
properties: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { Project, SourceFile } from 'ts-morph';
import { RoutersFactoryMetadata, } from '../../interfaces/factory.interface';
import {
DecoratorGeneratorMetadata,
ProcedureGeneratorMetadata,
RouterGeneratorMetadata,
} from '../../interfaces/generator.interface';
import { Query, Mutation } from '../../decorators';
Expand Down Expand Up @@ -85,12 +84,12 @@ describe('RouterGenerator', () => {
const mockRouter: RoutersFactoryMetadata = {
name: 'TestRouter',
alias: 'test',
path: 'testPath',
path: ['testPath'],
instance: {
name: "TestRouter",
instance: jest.fn(),
alias: 'test',
path:"testPath",
path: ["testPath"],
middlewares: []
},
procedures: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,18 @@ import { RouterGenerator } from '../router.generator';
import { MiddlewareGenerator } from '../middleware.generator';
import { ContextGenerator } from '../context.generator';
import { RouterFactory } from '../../factories/router.factory';
import { MiddlewareFactory } from '../../factories/middleware.factory';
import { MiddlewareFactory, MiddlewareMetadata } from '../../factories/middleware.factory';
import { ProcedureFactory } from '../../factories/procedure.factory';
import { ClassDeclaration, Project, SourceFile } from 'ts-morph';
import * as fileUtil from '../../utils/ts-morph.util';
import { ProcedureFactoryMetadata } from '../../interfaces/factory.interface';
import { ProcedureFactoryMetadata, RouterInstance } from '../../interfaces/factory.interface';
import { MiddlewareOptions, MiddlewareResponse, TRPCContext, TRPCMiddleware } from '../../interfaces';
import { CreateExpressContextOptions } from '@trpc/server/adapters/express';
import { TRPC_GENERATOR_OPTIONS, TRPC_MODULE_CALLER_FILE_PATH } from '../../trpc.constants';
import { TYPESCRIPT_APP_ROUTER_SOURCE_FILE, TYPESCRIPT_PROJECT } from '../generator.constants';
import { StaticGenerator } from '../static.generator';
import { ImportsScanner } from '../../scanners/imports.scanner';
import { SourceFileImportsMap } from '../../interfaces/generator.interface';
import { RouterGeneratorMetadata, SourceFileImportsMap } from '../../interfaces/generator.interface';

jest.mock('../../utils/ts-morph.util');

Expand Down Expand Up @@ -128,7 +128,7 @@ describe('TRPCGenerator', () => {

describe('generateSchemaFile', () => {
it('should generate schema file', async () => {
const mockRouters = [{ name: 'TestRouter', instance: {}, alias: 'test', path: 'testPath', middlewares: [] }];
const mockRouters: Array<RouterInstance> = [{ name: 'TestRouter', instance: {}, alias: 'test', path: ['testPath'], middlewares: [] }];
const mockProcedures: Array<ProcedureFactoryMetadata> = [{
name: 'testProcedure',
implementation: jest.fn(),
Expand All @@ -138,7 +138,7 @@ describe('TRPCGenerator', () => {
params: [],
middlewares: [],
}];
const mockRoutersMetadata = [{ name: 'TestRouter', alias: 'test', procedures: [{ name: 'testProcedure', decorators: [] }], path: 'testPath'}];
const mockRoutersMetadata: Array<RouterGeneratorMetadata> = [{ name: 'TestRouter', alias: 'test', procedures: [{ name: 'testProcedure', decorators: [] }]}];

routerFactory.getRouters.mockReturnValue(mockRouters);
procedureFactory.getProcedures.mockReturnValue(mockProcedures);
Expand Down Expand Up @@ -185,7 +185,7 @@ describe('TRPCGenerator', () => {
}
}

const mockMiddlewares = [{ instance: TestMiddleware, path: 'testPath' }];
const mockMiddlewares: Array<MiddlewareMetadata> = [{ instance: TestMiddleware, path: ['testPath'] }];
const mockMiddlewareInterface = { name: 'TestMiddleware', properties: [{ name: 'test', type: 'string' }] };
const mockImportsMap = new Map<string, SourceFileImportsMap>([
[TestContext.name, {sourceFile, initializer: sourceFile.getClass(TestContext.name) as ClassDeclaration}]
Expand Down
25 changes: 7 additions & 18 deletions packages/nestjs-trpc/lib/generators/middleware.generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,12 @@ import {
import { Injectable } from '@nestjs/common';
import { TRPCMiddleware } from '../interfaces';
import type { Class } from 'type-fest';
import { findClassInPath } from '../utils/find-class-in-path';

@Injectable()
export class MiddlewareGenerator {
public async getMiddlewareInterface(
routerFilePath: string,
routerFilePath: string[],
middleware: Class<TRPCMiddleware>,
project: Project,
): Promise<{
Expand All @@ -34,18 +35,17 @@ export class MiddlewareGenerator {
return null;
}

const contextSourceFile = project.addSourceFileAtPath(routerFilePath);

const classDeclaration = this.getClassDeclaration(
contextSourceFile,
const classFromPath = findClassInPath(
project,
routerFilePath,
middleware.name,
);

if (!classDeclaration) {
if (!classFromPath) {
return null;
}

const useMethod = classDeclaration.getMethod('use');
const useMethod = classFromPath.classDeclaration.getMethod('use');
if (!useMethod) {
return null;
}
Expand Down Expand Up @@ -99,17 +99,6 @@ export class MiddlewareGenerator {
return ctxProperty.getInitializer()?.getType() || null;
}

private getClassDeclaration(
sourceFile: SourceFile,
className: string,
): ClassDeclaration | undefined {
const classDeclaration = sourceFile.getClass(className);
if (classDeclaration) {
return classDeclaration;
}
return undefined;
}

private typeToProperties(
type: Type,
): Array<OptionalKind<PropertySignatureStructure>> {
Expand Down
11 changes: 6 additions & 5 deletions packages/nestjs-trpc/lib/generators/router.generator.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Project } from 'ts-morph';
import { ClassDeclaration, Project, SourceFile } from 'ts-morph';
import {
RouterGeneratorMetadata,
ProcedureGeneratorMetadata,
Expand All @@ -11,6 +11,7 @@ import { DecoratorGenerator } from './decorator.generator';
import { Inject, Injectable } from '@nestjs/common';
import { camelCase } from 'lodash';
import { ProcedureGenerator } from './procedure.generator';
import { findClassInPath } from '../utils/find-class-in-path';

@Injectable()
export class RouterGenerator {
Expand Down Expand Up @@ -43,18 +44,18 @@ export class RouterGenerator {
}

private serializeRouterProcedures(
routerFilePath: string,
routerFilePath: string[],
procedure: ProcedureFactoryMetadata,
routerName: string,
project: Project,
): ProcedureGeneratorMetadata {
const sourceFile = project.addSourceFileAtPath(routerFilePath);
const classDeclaration = sourceFile.getClass(routerName);
const classFromPath = findClassInPath(project, routerFilePath, routerName);

if (!classDeclaration) {
if (!classFromPath) {
throw new Error(`Could not find router ${routerName} class declaration.`);
}

const { sourceFile, classDeclaration } = classFromPath;
const methodDeclaration = classDeclaration.getMethod(procedure.name);

if (!methodDeclaration) {
Expand Down
4 changes: 2 additions & 2 deletions packages/nestjs-trpc/lib/interfaces/factory.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,15 +57,15 @@ export interface CustomProcedureFactoryMetadata {

export interface RouterInstance {
name: string;
path: string;
path: string[];
instance: unknown;
middlewares: Array<Class<TRPCMiddleware> | Constructor<TRPCMiddleware>>;
alias?: string;
}

export interface RoutersFactoryMetadata {
name: string;
path: string;
path: string[];
alias?: string;
instance: RouterInstance;
procedures: Array<ProcedureFactoryMetadata>;
Expand Down
46 changes: 28 additions & 18 deletions packages/nestjs-trpc/lib/scanners/file.scanner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { SourceMapping } from '../interfaces/scanner.interface';
*/
@Injectable()
export class FileScanner {
public getCallerFilePath(skip: number = 2): string {
public getCallerFilePath(skip: number = 2): string[] {
const originalPrepareStackTrace = Error.prepareStackTrace;

Error.prepareStackTrace = (_, stack) => stack;
Expand All @@ -18,28 +18,38 @@ export class FileScanner {

Error.prepareStackTrace = originalPrepareStackTrace;

const caller = stack[skip];
const jsFilePath = caller?.getFileName();
const callers = stack.slice(skip);
const jsFilePaths: string[] = [];

if (jsFilePath == null) {
throw new Error(`Could not find caller file: ${caller}`);
for (const caller of callers) {
const fileName = caller.getFileName();

if (fileName && !fileName.startsWith('node:')) {
jsFilePaths.push(fileName);
}
}

try {
// Attempt to find the source map file and extract the original TypeScript path
const sourceMap = this.getSourceMapFromJSPath(jsFilePath);
return this.normalizePath(
path.resolve(jsFilePath, '..', sourceMap.sources[0]),
);
} catch (error) {
// Suppress the warning if in test environment
if (process.env.NODE_ENV !== 'test') {
console.warn(
`Warning: Could not resolve source map for ${jsFilePath}. Falling back to default path resolution.`,
if (!jsFilePaths || jsFilePaths.length === 0) {
throw new Error(`Could not find caller file: ${callers}`);
}

return jsFilePaths.map((jsFilePath) => {
try {
// Attempt to find the source map file and extract the original TypeScript path
const sourceMap = this.getSourceMapFromJSPath(jsFilePath);
return this.normalizePath(
path.resolve(jsFilePath, '..', sourceMap.sources[0]),
);
} catch (error) {
// Suppress the warning if in test environment
if (process.env.NODE_ENV !== 'test' && jsFilePath.endsWith('.ts')) {
console.warn(
`Warning: Could not resolve source map for ${jsFilePath}. Falling back to default path resolution.`,
);
}
return this.normalizePath(jsFilePath);
}
return this.normalizePath(jsFilePath);
}
});
}

private normalizePath(p: string): string {
Expand Down
3 changes: 2 additions & 1 deletion packages/nestjs-trpc/lib/trpc.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,11 @@ export class TRPCModule implements OnModuleInit {
if (options.autoSchemaFile != null) {
const fileScanner = new FileScanner();
const callerFilePath = fileScanner.getCallerFilePath();
const rootModuleFilePath = callerFilePath[0];
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[note] It seemed to make sense to just leave this usage as the root file

imports.push(
GeneratorModule.forRoot({
outputDirPath: options.autoSchemaFile,
rootModuleFilePath: callerFilePath,
rootModuleFilePath: rootModuleFilePath,
schemaFileImports: options.schemaFileImports,
context: options.context,
}),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { Project, SourceFile } from 'ts-morph';
import { findClassInPath } from '../find-class-in-path';

describe('find-class-in-path', () => {
let project: Project;
const routerName = "TestRouter"
const routerFileName = "test.router.ts"
let routerSourceFile: SourceFile;
const decoratorFileName = "test.decorator.ts"
let decoratorSourceFile: SourceFile;

beforeEach(async () => {
project = new Project();

decoratorSourceFile = project.createSourceFile(decoratorFileName,
`
import { applyDecorators } from '@nestjs/common';
import { UseMiddlewares, Router } from 'nestjs-trpc';
import { Middleware1, Middleware2 } from "./middlewares;

export const MyCustomRoute = (options: { alias: string }) => {
return applyDecorators(
Router({ alias: options.alias }),
UseMiddlewares(Middleware1, Middleware2),
);
};
`, { overwrite: true });

routerSourceFile = project.createSourceFile(
routerFileName,
`
@MyCustomRoute({ alias: 'classTest' })
export class ${routerName} {}
`, { overwrite: true }
);
});

it('should return undefined when path is empty', () => {
expect(findClassInPath(project, [], "SomeClass")).toBeUndefined();
});

it('should return undefined when the path has no classes', () => {
const path = [decoratorFileName]
jest.spyOn(project, 'addSourceFileAtPath').mockReturnValueOnce(decoratorSourceFile);

const foundClass = findClassInPath(project, path, routerName);
expect(foundClass).toBeUndefined();
});

it('should return a class at start of the path call stack', () => {
const path = [routerFileName]
jest.spyOn(project, 'addSourceFileAtPath').mockReturnValueOnce(routerSourceFile);

const foundClass = findClassInPath(project, path, routerName);
expect(foundClass).toBeDefined()
expect(foundClass?.classDeclaration).toBeDefined()
expect(foundClass?.sourceFile).toEqual(routerSourceFile)
});

it('should return a class that is further down the path call stack', () => {
const path = [decoratorFileName, routerFileName]
jest.spyOn(project, 'addSourceFileAtPath').mockReturnValueOnce(decoratorSourceFile);
jest.spyOn(project, 'addSourceFileAtPath').mockReturnValueOnce(routerSourceFile);

const foundClass = findClassInPath(project, path, routerName);
expect(foundClass).toBeDefined()
expect(foundClass?.classDeclaration).toBeDefined()
expect(foundClass?.sourceFile).toEqual(routerSourceFile)
expect(foundClass?.sourceFile.getFilePath()).not.toContain(decoratorFileName);
expect(foundClass?.sourceFile.getFilePath()).toContain(routerFileName);
});

});
Loading