Skip to content

Commit c87a027

Browse files
author
DavertMik
committed
improved loading esm/cjs style modules
1 parent 8646b72 commit c87a027

File tree

2 files changed

+86
-34
lines changed

2 files changed

+86
-34
lines changed

lib/container.js

Lines changed: 85 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
const glob = require('glob');
22
const path = require('path');
3+
const { isPromise } = require('util/types');
34
const { MetaStep } = require('./step');
4-
const { fileExists, isFunction, isAsyncFunction } = require('./utils');
5+
const { fileExists, isFunction, isAsyncFunction, installedLocally } = require('./utils');
56
const Translation = require('./translation');
67
const MochaFactory = require('./mochaFactory');
78
const recorder = require('./recorder');
@@ -155,45 +156,42 @@ module.exports = Container;
155156

156157
function createHelpers(config) {
157158
const helpers = {};
158-
let moduleName;
159-
for (const helperName in config) {
159+
for (let helperName in config) {
160160
try {
161-
if (config[helperName].require) {
162-
if (config[helperName].require.startsWith('.')) {
163-
moduleName = path.resolve(global.codecept_dir, config[helperName].require); // custom helper
164-
} else {
165-
moduleName = config[helperName].require; // plugin helper
166-
}
167-
} else {
168-
moduleName = `./helper/${helperName}`; // built-in helper
161+
let HelperClass;
162+
163+
// ESM import
164+
if (helperName?.constructor === Function && helperName.prototype) {
165+
HelperClass = helperName;
166+
helperName = HelperClass.constructor.name;
169167
}
170168

171-
// @ts-ignore
172-
let HelperClass;
173-
// check if the helper is the built-in, use the require() syntax.
174-
if (moduleName.startsWith('./helper/')) {
175-
HelperClass = require(moduleName);
176-
} else {
177-
// check if the new syntax export default HelperName is used and loads the Helper, otherwise loads the module that used old syntax export = HelperName.
178-
HelperClass = require(moduleName).default || require(moduleName);
169+
// classical require
170+
if (!HelperClass) {
171+
HelperClass = requireHelperFromModule(helperName, config);
179172
}
180173

181-
if (HelperClass._checkRequirements) {
182-
const requirements = HelperClass._checkRequirements();
183-
if (requirements) {
184-
let install;
185-
if (require('./utils').installedLocally()) {
186-
install = `npm install --save-dev ${requirements.join(' ')}`;
187-
} else {
188-
console.log('WARNING: CodeceptJS is not installed locally. It is recommended to switch to local installation');
189-
install = `[sudo] npm install -g ${requirements.join(' ')}`;
174+
// handle async CJS modules that use dynamic import
175+
if (isPromise(HelperClass) && typeof HelperClass.then === 'function') {
176+
HelperClass = HelperClass.then(ResolvedHelperClass => {
177+
// Check if ResolvedHelperClass is a constructor function
178+
if (typeof ResolvedHelperClass !== 'function' || !ResolvedHelperClass.prototype) {
179+
throw new Error(`Helper class from module '${helperName}' is not a constructor. Use CJS async module syntax.`);
190180
}
191-
throw new Error(`Required modules are not installed.\n\nRUN: ${install}`);
192-
}
181+
try {
182+
helpers[helperName] = new ResolvedHelperClass(config[helperName]);
183+
} catch (err) {
184+
throw new Error(`Could not load helper ${helperName} as async module (${err.message})`);
185+
}
186+
});
187+
continue;
193188
}
189+
190+
checkHelperRequirements(HelperClass);
191+
194192
helpers[helperName] = new HelperClass(config[helperName]);
195193
} catch (err) {
196-
throw new Error(`Could not load helper ${helperName} from module '${moduleName}':\n${err.message}\n${err.stack}`);
194+
throw new Error(`Could not load helper ${helperName} (${err.message})`);
197195
}
198196
}
199197

@@ -203,6 +201,44 @@ function createHelpers(config) {
203201
return helpers;
204202
}
205203

204+
function checkHelperRequirements(HelperClass) {
205+
if (HelperClass._checkRequirements) {
206+
const requirements = HelperClass._checkRequirements();
207+
if (requirements) {
208+
let install;
209+
if (installedLocally()) {
210+
install = `npm install --save-dev ${requirements.join(' ')}`;
211+
} else {
212+
console.log('WARNING: CodeceptJS is not installed locally. It is recommended to switch to local installation');
213+
install = `[sudo] npm install -g ${requirements.join(' ')}`;
214+
}
215+
throw new Error(`Required modules are not installed.\n\nRUN: ${install}`);
216+
}
217+
}
218+
}
219+
220+
function requireHelperFromModule(helperName, config, HelperClass) {
221+
const moduleName = getHelperModuleName(helperName, config);
222+
if (moduleName.startsWith('./helper/')) {
223+
HelperClass = require(moduleName);
224+
} else {
225+
// check if the new syntax export default HelperName is used and loads the Helper, otherwise loads the module that used old syntax export = HelperName.
226+
try {
227+
const mod = require(moduleName);
228+
if (!mod && !mod.default) {
229+
throw new Error(`Helper module '${moduleName}' was not found. Make sure you have installed the package correctly.`);
230+
}
231+
HelperClass = mod.default || mod;
232+
} catch (err) {
233+
if (err.code === 'MODULE_NOT_FOUND') {
234+
throw new Error(`Helper module '${moduleName}' was not found. Make sure you have installed the package correctly.`);
235+
}
236+
throw err;
237+
}
238+
}
239+
return HelperClass;
240+
}
241+
206242
function createSupportObjects(config) {
207243
const objects = {};
208244

@@ -440,3 +476,21 @@ function loadTranslation(locale, vocabularies) {
440476

441477
return translation;
442478
}
479+
480+
function getHelperModuleName(helperName, config) {
481+
// classical require
482+
if (config[helperName].require) {
483+
if (config[helperName].require.startsWith('.')) {
484+
return path.resolve(global.codecept_dir, config[helperName].require); // custom helper
485+
}
486+
return config[helperName].require; // plugin helper
487+
}
488+
489+
// built-in helpers
490+
if (helperName.startsWith('@codeceptjs/')) {
491+
return helperName;
492+
}
493+
494+
// built-in helpers
495+
return `./helper/${helperName}`;
496+
}

test/acceptance/codecept.Playwright.js

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,7 @@ module.exports.config = {
2323
require: '../support/ScreenshotSessionHelper.js',
2424
outputPath: 'test/acceptance/output',
2525
},
26-
Expect: {
27-
require: '@codeceptjs/expect-helper',
28-
},
26+
'@codeceptjs/expect-helper': {},
2927
},
3028
include: {},
3129
bootstrap: false,

0 commit comments

Comments
 (0)