Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support controllers glob for dynamic imports #396

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
14 changes: 14 additions & 0 deletions README.MD
Expand Up @@ -213,6 +213,7 @@ by defining it in your tsoa.json configuration. Route paths are generated based

### Consuming generated routes

#### Using imports in app entry file
```typescript
import * as methodOverride from 'method-override';
import * as express from 'express';
Expand All @@ -231,6 +232,19 @@ RegisterRoutes(app);

app.listen(3000);

```
#### Using automatic controllers discovery
To tell `tsoa` to use your automatic controllers discovery you need to add to theconfig file (e.g. `tsoa.json`):
The controllers globs pattern:
```json
{
"routes": {
"entryFile": "...",
"routesDir": "...",
"middleware": "...",
"controllerPathGlobs": ["./dir-with-controllers/*", "./recursive-dir/**/", "./custom-filerecursive-dir/**/*.controller.ts"]
}
}
```

### Get access to the request object of express (or koa) in Controllers
Expand Down
5 changes: 5 additions & 0 deletions src/config.ts
Expand Up @@ -146,4 +146,9 @@ export interface RoutesConfig {
* Authentication Module for express, hapi and koa
*/
authenticationModule?: string;

/**
* Controllers glob path, contains directories with wild card to file names.
*/
controllerPathGlobs?: string[];
}
39 changes: 28 additions & 11 deletions src/metadataGeneration/metadataGenerator.ts
@@ -1,6 +1,8 @@
import * as mm from 'minimatch';
import * as ts from 'typescript';
import { importClassesFromDirectories } from '../utils/importClassesFromDirectories';
import { ControllerGenerator } from './controllerGenerator';
import { GenerateMetadataError } from './exceptions';
import { Tsoa } from './tsoa';

export class MetadataGenerator {
Expand All @@ -12,12 +14,36 @@ export class MetadataGenerator {

public IsExportedNode(node: ts.Node) { return true; }

constructor(entryFile: string, compilerOptions?: ts.CompilerOptions, private readonly ignorePaths?: string[]) {
this.program = ts.createProgram([entryFile], compilerOptions || {});
constructor(entryFile: string, private readonly compilerOptions?: ts.CompilerOptions, private readonly ignorePaths?: string[], controllers?: string[]) {
this.program = !!controllers ?
this.setProgramToDynamicControllersFiles(controllers) :
ts.createProgram([entryFile], compilerOptions || {});
this.typeChecker = this.program.getTypeChecker();
}

public Generate(): Tsoa.Metadata {
this.extractNodeFromProgramSourceFiles();

const controllers = this.buildControllers();

this.circularDependencyResolvers.forEach((c) => c(this.referenceTypeMap));

return {
controllers,
referenceTypeMap: this.referenceTypeMap,
};
}

private setProgramToDynamicControllersFiles(controllers) {
const allGlobFiles = importClassesFromDirectories(controllers);
if (allGlobFiles.length === 0) {
throw new GenerateMetadataError(`[${controllers.join(', ')}] globs found 0 controllers.`);
}

return ts.createProgram(allGlobFiles, this.compilerOptions || {});
}

private extractNodeFromProgramSourceFiles() {
this.program.getSourceFiles().forEach((sf) => {
if (this.ignorePaths && this.ignorePaths.length) {
for (const path of this.ignorePaths) {
Expand All @@ -31,15 +57,6 @@ export class MetadataGenerator {
this.nodes.push(node);
});
});

const controllers = this.buildControllers();

this.circularDependencyResolvers.forEach((c) => c(this.referenceTypeMap));

return {
controllers,
referenceTypeMap: this.referenceTypeMap,
};
}

public TypeChecker() {
Expand Down
1 change: 1 addition & 0 deletions src/module/generate-routes.ts
Expand Up @@ -19,6 +19,7 @@ export const generateRoutes = async (
routesConfig.entryFile,
compilerOptions,
ignorePaths,
routesConfig.controllerPathGlobs,
).Generate();
}

Expand Down
16 changes: 16 additions & 0 deletions src/utils/importClassesFromDirectories.ts
@@ -0,0 +1,16 @@
import * as path from 'path';

/**
* Loads all exported classes from the given directory.
*/
export function importClassesFromDirectories(directories: string[], formats = ['.ts']): string[] {
const allFiles = directories.reduce((allDirs, dir) => {
return allDirs.concat(require('glob').sync(path.normalize(dir)));
}, [] as string[]);

return allFiles
joni7777 marked this conversation as resolved.
Show resolved Hide resolved
.filter(file => {
const dtsExtension = file.substring(file.length - 5, file.length);
return formats.indexOf(path.extname(file)) !== -1 && dtsExtension !== '.d.ts';
});
}
29 changes: 29 additions & 0 deletions tests/fixtures/express-dynamic-controllers/server.ts
@@ -0,0 +1,29 @@
import * as bodyParser from 'body-parser';
import * as express from 'express';
import * as methodOverride from 'method-override';

import { RegisterRoutes } from './routes';

export const app: express.Express = express();
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
app.use(methodOverride());
app.use((req: any, res: any, next: any) => {
req.stringValue = 'fancyStringForContext';
next();
});
RegisterRoutes(app);

// It's important that this come after the main routes are registered
app.use((err: any, req: express.Request, res: express.Response, next: express.NextFunction) => {
const status = err.status || 500;
const body: any = {
fields: err.fields || undefined,
message: err.message || 'An error occurred during the request.',
name: err.name,
status,
};
res.status(status).json(body);
});

app.listen(3000);