Skip to content

Commit cdb63b7

Browse files
bajtosraymondfeng
authored andcommitted
feat(boot): add debug logs for better troubleshooting
Make it easier to troubleshoot the situation when a booter is not recognizing artifact files and/or classes exported by those files. To make debug logs easy to read, absolute paths are converted to project-relative paths in debug logs. This change required a refactoring of Booter design, where "projectRoot" becomes a required constructor argument. While making these changes, I changed "options" to be a required constructor argument too, and chaged both "options" and "projectRoot" to readonly properties.
1 parent 1445ebd commit cdb63b7

File tree

7 files changed

+96
-36
lines changed

7 files changed

+96
-36
lines changed

packages/boot/src/booters/base-artifact.booter.ts

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,12 @@
44
// License text available at https://opensource.org/licenses/MIT
55

66
import {Constructor} from '@loopback/context';
7+
import * as debugFactory from 'debug';
8+
import * as path from 'path';
9+
import {ArtifactOptions, Booter} from '../interfaces';
710
import {discoverFiles, loadClassesFromFiles} from './booter-utils';
8-
import {Booter, ArtifactOptions} from '../interfaces';
11+
12+
const debug = debugFactory('loopback:boot:base-artifact-booter');
913

1014
/**
1115
* This class serves as a base class for Booters which follow a pattern of
@@ -35,14 +39,27 @@ export class BaseArtifactBooter implements Booter {
3539
/**
3640
* Options being used by the Booter.
3741
*/
38-
options: ArtifactOptions;
39-
projectRoot: string;
42+
readonly options: ArtifactOptions;
43+
readonly projectRoot: string;
4044
dirs: string[];
4145
extensions: string[];
4246
glob: string;
4347
discovered: string[];
4448
classes: Array<Constructor<{}>>;
4549

50+
constructor(projectRoot: string, options: ArtifactOptions) {
51+
this.projectRoot = projectRoot;
52+
this.options = options;
53+
}
54+
55+
/**
56+
* Get the name of the artifact loaded by this booter, e.g. "Controller".
57+
* Subclasses can override the default logic based on the class name.
58+
*/
59+
get artifactName(): string {
60+
return this.constructor.name.replace(/Booter$/, '');
61+
}
62+
4663
/**
4764
* Configure the Booter by initializing the 'dirs', 'extensions' and 'glob'
4865
* properties.
@@ -78,7 +95,25 @@ export class BaseArtifactBooter implements Booter {
7895
* 'discovered' property.
7996
*/
8097
async discover() {
98+
debug(
99+
'Discovering %s artifacts in %j using glob %j',
100+
this.artifactName,
101+
this.projectRoot,
102+
this.glob,
103+
);
104+
81105
this.discovered = await discoverFiles(this.glob, this.projectRoot);
106+
107+
if (debug.enabled) {
108+
debug(
109+
'Artifact files found: %s',
110+
JSON.stringify(
111+
this.discovered.map(f => path.relative(this.projectRoot, f)),
112+
null,
113+
2,
114+
),
115+
);
116+
}
82117
}
83118

84119
/**
@@ -90,6 +125,6 @@ export class BaseArtifactBooter implements Booter {
90125
* and then process the artifact classes as appropriate.
91126
*/
92127
async load() {
93-
this.classes = loadClassesFromFiles(this.discovered);
128+
this.classes = loadClassesFromFiles(this.discovered, this.projectRoot);
94129
}
95130
}

packages/boot/src/booters/booter-utils.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,13 @@
44
// License text available at https://opensource.org/licenses/MIT
55

66
import {Constructor} from '@loopback/context';
7+
import * as debugFactory from 'debug';
8+
import * as path from 'path';
79
import {promisify} from 'util';
810
const glob = promisify(require('glob'));
911

12+
const debug = debugFactory('loopback:boot:booter-utils');
13+
1014
/**
1115
* Returns all files matching the given glob pattern relative to root
1216
*
@@ -42,16 +46,23 @@ export function isClass(target: any): target is Constructor<any> {
4246
* @param files An array of string of absolute file paths
4347
* @returns {Constructor<{}>[]} An array of Class constructors from a file
4448
*/
45-
export function loadClassesFromFiles(files: string[]): Constructor<{}>[] {
49+
export function loadClassesFromFiles(
50+
files: string[],
51+
projectRootDir: string,
52+
): Constructor<{}>[] {
4653
const classes: Array<Constructor<{}>> = [];
4754
for (const file of files) {
55+
debug('Loading artifact file %j', path.relative(projectRootDir, file));
4856
const moduleObj = require(file);
4957
// WORKAROUND: use `for in` instead of Object.values().
5058
// See https://github.com/nodejs/node/issues/20278
5159
for (const k in moduleObj) {
5260
const exported = moduleObj[k];
5361
if (isClass(exported)) {
62+
debug(' add %s (class %s)', k, exported.name);
5463
classes.push(exported);
64+
} else {
65+
debug(' skip non-class %s', k);
5566
}
5667
}
5768
}

packages/boot/src/booters/controller.booter.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,15 @@ import {BootBindings} from '../keys';
2222
export class ControllerBooter extends BaseArtifactBooter {
2323
constructor(
2424
@inject(CoreBindings.APPLICATION_INSTANCE) public app: Application,
25-
@inject(BootBindings.PROJECT_ROOT) public projectRoot: string,
25+
@inject(BootBindings.PROJECT_ROOT) projectRoot: string,
2626
@inject(`${BootBindings.BOOT_OPTIONS}#controllers`)
2727
public controllerConfig: ArtifactOptions = {},
2828
) {
29-
super();
30-
// Set Controller Booter Options if passed in via bootConfig
31-
this.options = Object.assign({}, ControllerDefaults, controllerConfig);
29+
super(
30+
projectRoot,
31+
// Set Controller Booter Options if passed in via bootConfig
32+
Object.assign({}, ControllerDefaults, controllerConfig),
33+
);
3234
}
3335

3436
/**

packages/boot/src/booters/datasource.booter.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,15 @@ export class DataSourceBooter extends BaseArtifactBooter {
2828
constructor(
2929
@inject(CoreBindings.APPLICATION_INSTANCE)
3030
public app: ApplicationWithRepositories,
31-
@inject(BootBindings.PROJECT_ROOT) public projectRoot: string,
31+
@inject(BootBindings.PROJECT_ROOT) projectRoot: string,
3232
@inject(`${BootBindings.BOOT_OPTIONS}#datasources`)
3333
public datasourceConfig: ArtifactOptions = {},
3434
) {
35-
super();
36-
// Set DataSource Booter Options if passed in via bootConfig
37-
this.options = Object.assign({}, DataSourceDefaults, datasourceConfig);
35+
super(
36+
projectRoot,
37+
// Set DataSource Booter Options if passed in via bootConfig
38+
Object.assign({}, DataSourceDefaults, datasourceConfig),
39+
);
3840
}
3941

4042
/**

packages/boot/src/booters/repository.booter.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,15 @@ export class RepositoryBooter extends BaseArtifactBooter {
2525
constructor(
2626
@inject(CoreBindings.APPLICATION_INSTANCE)
2727
public app: ApplicationWithRepositories,
28-
@inject(BootBindings.PROJECT_ROOT) public projectRoot: string,
28+
@inject(BootBindings.PROJECT_ROOT) projectRoot: string,
2929
@inject(`${BootBindings.BOOT_OPTIONS}#repositories`)
3030
public repositoryOptions: ArtifactOptions = {},
3131
) {
32-
super();
33-
// Set Repository Booter Options if passed in via bootConfig
34-
this.options = Object.assign({}, RepositoryDefaults, repositoryOptions);
32+
super(
33+
projectRoot,
34+
// Set Repository Booter Options if passed in via bootConfig
35+
Object.assign({}, RepositoryDefaults, repositoryOptions),
36+
);
3537
}
3638

3739
/**

packages/boot/test/unit/booters/base-artifact.booter.unit.ts

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,51 +6,56 @@
66
import {BaseArtifactBooter} from '../../../index';
77
import {expect} from '@loopback/testlab';
88
import {resolve} from 'path';
9+
import {ArtifactOptions} from '../../../src';
910

1011
describe('base-artifact booter unit tests', () => {
11-
let booterInst: BaseArtifactBooter;
12-
13-
beforeEach(getBaseBooter);
12+
const TEST_OPTIONS = {
13+
dirs: ['test', 'test2'],
14+
extensions: ['.test.js', 'test2.js'],
15+
nested: false,
16+
};
1417

1518
describe('configure()', () => {
16-
const options = {
17-
dirs: ['test', 'test2'],
18-
extensions: ['.test.js', 'test2.js'],
19-
nested: false,
20-
};
21-
2219
it(`sets 'dirs' / 'extensions' properties as an array if a string`, async () => {
23-
booterInst.options = {dirs: 'test', extensions: '.test.js', nested: true};
20+
const booterInst = givenBaseBooter({
21+
dirs: 'test',
22+
extensions: '.test.js',
23+
nested: true,
24+
});
2425
await booterInst.configure();
2526
expect(booterInst.dirs).to.be.eql(['test']);
2627
expect(booterInst.extensions).to.be.eql(['.test.js']);
2728
});
2829

2930
it(`creates and sets 'glob' pattern`, async () => {
30-
booterInst.options = options;
31+
const booterInst = givenBaseBooter();
3132
const expected = '/@(test|test2)/*@(.test.js|test2.js)';
3233
await booterInst.configure();
3334
expect(booterInst.glob).to.equal(expected);
3435
});
3536

3637
it(`creates and sets 'glob' pattern (nested)`, async () => {
37-
booterInst.options = Object.assign({}, options, {nested: true});
38+
const booterInst = givenBaseBooter(
39+
Object.assign({}, TEST_OPTIONS, {nested: true}),
40+
);
3841
const expected = '/@(test|test2)/**/*@(.test.js|test2.js)';
3942
await booterInst.configure();
4043
expect(booterInst.glob).to.equal(expected);
4144
});
4245

4346
it(`sets 'glob' pattern to options.glob if present`, async () => {
4447
const expected = '/**/*.glob';
45-
booterInst.options = Object.assign({}, options, {glob: expected});
48+
const booterInst = givenBaseBooter(
49+
Object.assign({}, TEST_OPTIONS, {glob: expected}),
50+
);
4651
await booterInst.configure();
4752
expect(booterInst.glob).to.equal(expected);
4853
});
4954
});
5055

5156
describe('discover()', () => {
5257
it(`sets 'discovered' property`, async () => {
53-
booterInst.projectRoot = __dirname;
58+
const booterInst = givenBaseBooter();
5459
// Fake glob pattern so we get an empty array
5560
booterInst.glob = '/abc.xyz';
5661
await booterInst.discover();
@@ -60,6 +65,7 @@ describe('base-artifact booter unit tests', () => {
6065

6166
describe('load()', () => {
6267
it(`sets 'classes' property to Classes from a file`, async () => {
68+
const booterInst = givenBaseBooter();
6369
booterInst.discovered = [
6470
resolve(__dirname, '../../fixtures/multiple.artifact.js'),
6571
];
@@ -69,7 +75,7 @@ describe('base-artifact booter unit tests', () => {
6975
});
7076
});
7177

72-
async function getBaseBooter() {
73-
booterInst = new BaseArtifactBooter();
78+
function givenBaseBooter(options?: ArtifactOptions) {
79+
return new BaseArtifactBooter(__dirname, options || TEST_OPTIONS);
7480
}
7581
});

packages/boot/test/unit/booters/booter-utils.unit.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ describe('booter-utils unit tests', () => {
6767
const files = [resolve(SANDBOX_PATH, 'multiple.artifact.js')];
6868
const NUM_CLASSES = 2; // Number of classes in above file
6969

70-
const classes = loadClassesFromFiles(files);
70+
const classes = loadClassesFromFiles(files, sandbox.path);
7171
expect(classes).to.have.lengthOf(NUM_CLASSES);
7272
expect(classes[0]).to.be.a.Function();
7373
expect(classes[1]).to.be.a.Function();
@@ -79,14 +79,16 @@ describe('booter-utils unit tests', () => {
7979
);
8080
const files = [resolve(SANDBOX_PATH, 'empty.artifact.js')];
8181

82-
const classes = loadClassesFromFiles(files);
82+
const classes = loadClassesFromFiles(files, sandbox.path);
8383
expect(classes).to.be.an.Array();
8484
expect(classes).to.be.empty();
8585
});
8686

8787
it('throws an error given a non-existent file', async () => {
8888
const files = [resolve(SANDBOX_PATH, 'fake.artifact.js')];
89-
expect(() => loadClassesFromFiles(files)).to.throw(/Cannot find module/);
89+
expect(() => loadClassesFromFiles(files, sandbox.path)).to.throw(
90+
/Cannot find module/,
91+
);
9092
});
9193
});
9294
});

0 commit comments

Comments
 (0)