Skip to content

Commit

Permalink
feat(boot): support loading ESM artifacts
Browse files Browse the repository at this point in the history
BREAKING CHANGE: `loadClassesFromFiles` now returns a `Promise`

see: #10744
Signed-off-by: Rifa Achrinza <[email protected]>
  • Loading branch information
achrinza committed Nov 17, 2024
1 parent 43f5d83 commit 9b7acd2
Show file tree
Hide file tree
Showing 7 changed files with 92 additions and 9 deletions.
1 change: 1 addition & 0 deletions packages/boot/src/__tests__/fixtures/cjs.artifact.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export class DummyCJSClass {}
1 change: 1 addition & 0 deletions packages/boot/src/__tests__/fixtures/esm.artifact.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export class DummyESMClass {}
24 changes: 24 additions & 0 deletions packages/boot/src/__tests__/fixtures/multiple.artifact.cts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright IBM Corp. and LoopBack contributors 2019. All Rights Reserved.
// Node module: @loopback/boot
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import {get} from '@loopback/rest';

export class ArtifactOne {
@get('/one')
one() {
return 'ControllerOne.one()';
}
}

export class ArtifactTwo {
@get('/two')
two() {
return 'ControllerTwo.two()';
}
}

export function hello() {
return 'hello world';
}
24 changes: 24 additions & 0 deletions packages/boot/src/__tests__/fixtures/multiple.artifact.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright IBM Corp. and LoopBack contributors 2019. All Rights Reserved.
// Node module: @loopback/boot
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import {get} from '@loopback/rest';

export class ArtifactOne {
@get('/one')
one() {
return 'ControllerOne.one()';
}
}

export class ArtifactTwo {
@get('/two')
two() {
return 'ControllerTwo.two()';
}
}

export function hello() {
return 'hello world';
}
40 changes: 35 additions & 5 deletions packages/boot/src/__tests__/unit/booters/booter-utils.unit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,37 @@ describe('booter-utils unit tests', () => {
const files = [resolve(sandbox.path, 'multiple.artifact.js')];
const NUM_CLASSES = 2; // Number of classes in above file

const classes = loadClassesFromFiles(files, sandbox.path);
const classes = await loadClassesFromFiles(files, sandbox.path);
expect(classes).to.have.lengthOf(NUM_CLASSES);
expect(classes[0]).to.be.a.Function();
expect(classes[1]).to.be.a.Function();
});

it('loads classes from CJS files', async () => {
const artifactFilename = 'multiple.artifact.cjs';
await sandbox.copyFile(
resolve(__dirname, '../../fixtures/multiple.artifact.cjs'),
);
const NUM_CLASSES = 2;
const classes = await loadClassesFromFiles(
[resolve(sandbox.path, artifactFilename)],
sandbox.path,
);
expect(classes).to.have.lengthOf(NUM_CLASSES);
expect(classes[0]).to.be.a.Function();
expect(classes[1]).to.be.a.Function();
});

it('loads classes from ESM files', async () => {
const artifactFilename = 'multiple.artifact.mjs';
await sandbox.copyFile(
resolve(__dirname, '../../fixtures/multiple.artifact.mjs'),
);
const NUM_CLASSES = 2;
const classes = await loadClassesFromFiles(
[resolve(sandbox.path, artifactFilename)],
sandbox.path,
);
expect(classes).to.have.lengthOf(NUM_CLASSES);
expect(classes[0]).to.be.a.Function();
expect(classes[1]).to.be.a.Function();
Expand All @@ -78,16 +108,16 @@ describe('booter-utils unit tests', () => {
);
const files = [resolve(sandbox.path, 'empty.artifact.js')];

const classes = loadClassesFromFiles(files, sandbox.path);
const classes = await loadClassesFromFiles(files, sandbox.path);
expect(classes).to.be.an.Array();
expect(classes).to.be.empty();
});

it('throws an error given a non-existent file', async () => {
const files = [resolve(sandbox.path, 'fake.artifact.js')];
expect(() => loadClassesFromFiles(files, sandbox.path)).to.throw(
/Cannot find module/,
);
await expect(
loadClassesFromFiles(files, sandbox.path),
).to.be.rejectedWith(/Cannot find module/);
});
});
});
5 changes: 4 additions & 1 deletion packages/boot/src/booters/base-artifact.booter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,9 @@ export class BaseArtifactBooter implements Booter {
* and then process the artifact classes as appropriate.
*/
async load() {
this.classes = loadClassesFromFiles(this.discovered, this.projectRoot);
this.classes = await loadClassesFromFiles(
this.discovered,
this.projectRoot,
);
}
}
6 changes: 3 additions & 3 deletions packages/boot/src/booters/booter-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,14 @@ export function isClass(target: any): target is Constructor<any> {
* @param projectRootDir - The project root directory
* @returns An array of Class constructors from a file
*/
export function loadClassesFromFiles(
export async function loadClassesFromFiles(
files: string[],
projectRootDir: string,
): Constructor<{}>[] {
): Promise<Constructor<{}>[]> {
const classes: Constructor<{}>[] = [];
for (const file of files) {
debug('Loading artifact file %j', path.relative(projectRootDir, file));
const moduleObj = require(file);
const moduleObj = await import(file);
for (const k in moduleObj) {
const exported = moduleObj[k];
if (isClass(exported)) {
Expand Down

0 comments on commit 9b7acd2

Please sign in to comment.