diff --git a/scripts/lib/mocha.js b/scripts/lib/mocha.js index 79ea5bff1d..25c6931769 100644 --- a/scripts/lib/mocha.js +++ b/scripts/lib/mocha.js @@ -20,10 +20,6 @@ const runMocha = (args, execMochaOptions = {}, coverageEnabled) => { shell.echo(`\nSetting mocha timeout from env var: ${MOCHA_TIMEOUT}\n`); } - // Pass testdouble node loader to support ESM module mocking and - // transpiling on the fly the tests modules. - binArgs.push('-n="loader=testdouble"'); - const res = spawnSync(binPath, binArgs, { ...execMochaOptions, env: { diff --git a/src/config.js b/src/config.js index ea76479863..3b9f3f9b13 100644 --- a/src/config.js +++ b/src/config.js @@ -12,29 +12,6 @@ import { UsageError, WebExtError } from './errors.js'; const log = createLogger(import.meta.url); -// NOTE: this error message is used in an interpolated string (while the other two help -// messages are being logged as is). -export const WARN_LEGACY_JS_EXT = [ - 'should be renamed to ".cjs" or ".mjs" file extension to ensure its format is not ambiguous.', - 'Config files with the ".js" file extension are deprecated and will not be loaded anymore', - 'in a future web-ext major version.', -].join(' '); - -export const HELP_ERR_MODULE_FROM_ESM = [ - 'This config file belongs to a package.json file with "type" set to "module".', - 'Change the file extension to ".cjs" or rewrite it as an ES module and use the ".mjs" file extension.', -].join(' '); - -export const HELP_ERR_IMPORTEXPORT_CJS = [ - 'This config file is defined as an ES module, but it belongs to either a project directory', - 'with a package.json file with "type" set to "commonjs" or one without any package.json file.', - 'Change the file extension to ".mjs" to fix the config loading error.', -].join(' '); - -const ERR_IMPORT_FROM_CJS = 'Cannot use import statement outside a module'; -const ERR_EXPORT_FROM_CJS = "Unexpected token 'export'"; -const ERR_MODULE_FROM_ESM = 'module is not defined in ES module scope'; - export function applyConfigToArgv({ argv, argvFromCLI, @@ -145,12 +122,18 @@ export async function loadJSConfigFile(filePath) { `Loading JS config file: "${filePath}" ` + `(resolved to "${resolvedFilePath}")`, ); + if (filePath.endsWith('.js')) { - log.warn(`WARNING: config file ${filePath} ${WARN_LEGACY_JS_EXT}`); + throw new UsageError( + ` Invalid config file "${resolvedFilePath}": the file extension should be` + + '".cjs" or ".mjs". More information at: https://mzl.la/web-ext-config-file', + ); } + let configObject; try { const nonce = `${Date.now()}-${Math.random()}`; + let configModule; if (resolvedFilePath.endsWith('package.json')) { configModule = parseJSON( @@ -165,35 +148,36 @@ export async function loadJSConfigFile(filePath) { // ES modules may expose both a default and named exports and so // we merge the named exports on top of what may have been set in // the default export. + if (filePath.endsWith('.cjs')) { + // Remove the additional 'module.exports' named export that Node.js >= + // 24 is returning from the dynamic import call (in addition to being + // also set on the default property as in Node.js < 24). + delete esmConfigMod['module.exports']; + } configObject = { ...configDefault, ...esmConfigMod }; } else { configObject = { ...configModule }; } } catch (error) { - log.debug('Handling error:', error); - let errorMessage = error.message; - if (error.message.startsWith(ERR_MODULE_FROM_ESM)) { - errorMessage = HELP_ERR_MODULE_FROM_ESM; - } else if ( - [ERR_IMPORT_FROM_CJS, ERR_EXPORT_FROM_CJS].includes(error.message) - ) { - errorMessage = HELP_ERR_IMPORTEXPORT_CJS; - } - throw new UsageError( - `Cannot read config file: ${resolvedFilePath}\n` + - `Error: ${errorMessage}`, + const configFileError = new UsageError( + `Cannot read config file "${resolvedFilePath}":\n${error}`, ); + configFileError.cause = error; + throw configFileError; } + if (filePath.endsWith('package.json')) { log.debug('Looking for webExt key inside package.json file'); configObject = configObject.webExt || {}; } + if (Object.keys(configObject).length === 0) { log.debug( `Config file ${resolvedFilePath} did not define any options. ` + 'Did you set module.exports = {...}?', ); } + return configObject; } diff --git a/src/program.js b/src/program.js index 992c608b0e..0c276557ad 100644 --- a/src/program.js +++ b/src/program.js @@ -331,6 +331,9 @@ export class Program { if (error.code) { log.error(`Error code: ${error.code}\n`); } + if (error.cause && adjustedArgv.verbose) { + log.error(`Error cause: ${error.cause.stack}\n`); + } log.debug(`Command executed: ${cmd}`); diff --git a/tests/functional/test.cli.sign.js b/tests/functional/test.cli.sign.js index 408f332efd..fd492a3f51 100644 --- a/tests/functional/test.cli.sign.js +++ b/tests/functional/test.cli.sign.js @@ -13,11 +13,11 @@ import { reportCommandErrors, } from './common.js'; -// Put this as "web-ext-config.js" in the current directory, and replace +// Put this as "web-ext-config.mjs" in the current directory, and replace // "FAKEAPIKEY" and "FAKEAPISECRET" with the actual values to enable // "web-ext sign" without passing those values via the CLI parameters. const GOOD_EXAMPLE_OF_WEB_EXT_CONFIG_JS = ` -module.exports = { +export default { sign: { apiKey: "FAKEAPIKEY", apiSecret: "FAKEAPISECRET", @@ -27,7 +27,7 @@ module.exports = { // Do NOT use this to specify the API key and secret. It won't work. const BAD_EXAMPLE_OF_WEB_EXT_CONFIG_JS = ` -module.exports = { +export default { // Bad config: those should be under the "sign" key. apiKey: "FAKEAPIKEY", apiSecret: "FAKEAPISECRET", @@ -86,7 +86,7 @@ describe('web-ext sign', () => { it('should use config file if required parameters are not in the arguments', () => withTempAddonDir({ addonPath: minimalAddonPath }, (srcDir, tmpDir) => { writeFileSync( - path.join(tmpDir, 'web-ext-config.js'), + path.join(tmpDir, 'web-ext-config.mjs'), GOOD_EXAMPLE_OF_WEB_EXT_CONFIG_JS, ); @@ -120,7 +120,7 @@ describe('web-ext sign', () => { it('should show an error message if the api-key is not set in the config', () => withTempAddonDir({ addonPath: minimalAddonPath }, (srcDir, tmpDir) => { - const configFilePath = path.join(tmpDir, 'web-ext-config.js'); + const configFilePath = path.join(tmpDir, 'web-ext-config.mjs'); writeFileSync(configFilePath, BAD_EXAMPLE_OF_WEB_EXT_CONFIG_JS); const argv = [ 'sign', @@ -135,7 +135,7 @@ describe('web-ext sign', () => { assert.notEqual(exitCode, 0); assert.match( stderr, - /web-ext-config.js specified an unknown option: "apiKey"/, + /web-ext-config.mjs specified an unknown option: "apiKey"/, ); }); })); diff --git a/tests/unit/test.config.js b/tests/unit/test.config.js index ba43962036..f77b4736f5 100644 --- a/tests/unit/test.config.js +++ b/tests/unit/test.config.js @@ -11,9 +11,6 @@ import { applyConfigToArgv, discoverConfigFiles, loadJSConfigFile, - HELP_ERR_IMPORTEXPORT_CJS, - HELP_ERR_MODULE_FROM_ESM, - WARN_LEGACY_JS_EXT, } from '../../src/config.js'; import { withTempDir } from '../../src/util/temp-dir.js'; import { UsageError, WebExtError } from '../../src/errors.js'; @@ -48,7 +45,7 @@ function makeArgv({ const applyConf = (params) => applyConfigToArgv({ - configFileName: 'some/path/to/config.js', + configFileName: 'some/path/to/config.mjs', ...params, }); @@ -433,7 +430,7 @@ describe('config', () => { }, UsageError, 'The config file ' + - 'at some/path/to/config.js specified an unknown option: "randomDir"', + 'at some/path/to/config.mjs specified an unknown option: "randomDir"', ); }); @@ -452,7 +449,7 @@ describe('config', () => { assert.throws( () => applyConf({ ...params, configObject }), UsageError, - 'The config file at some/path/to/config.js specified the ' + + 'The config file at some/path/to/config.mjs specified the ' + 'type of "retries" incorrectly as "string" (expected type "number")', ); }); @@ -474,7 +471,7 @@ describe('config', () => { applyConf({ ...params, configObject }); }, UsageError, - 'The config file at some/path/to/config.js ' + + 'The config file at some/path/to/config.mjs ' + 'specified the type of "sourceDir" incorrectly', ); }); @@ -709,7 +706,7 @@ describe('config', () => { assert.throws( () => applyConf({ ...params, configObject }), UsageError, - 'The config file at some/path/to/config.js ' + + 'The config file at some/path/to/config.mjs ' + 'specified the type of "apiUrl" incorrectly', ); }); @@ -766,7 +763,7 @@ describe('config', () => { }, UsageError, 'The config file at ' + - 'some/path/to/config.js specified an unknown option: "randomOption"', + 'some/path/to/config.mjs specified an unknown option: "randomOption"', ); }); @@ -793,7 +790,7 @@ describe('config', () => { applyConf({ ...params, configObject }); }, UsageError, - 'The config file at some/path/to/config.js ' + + 'The config file at some/path/to/config.mjs ' + 'specified the type of "apiUrl" incorrectly', ); }); @@ -830,7 +827,7 @@ describe('config', () => { applyConf({ ...params, configObject }); }, UsageError, - 'The config file at some/path/to/config.js ' + + 'The config file at some/path/to/config.mjs ' + 'specified the type of "apiUrl" incorrectly', ); }, @@ -897,7 +894,7 @@ describe('config', () => { }); }, UsageError, - 'The config file at some/path/to/config.js ' + + 'The config file at some/path/to/config.mjs ' + 'specified the type of "apiUrl" incorrectly as "number"' + ' (expected type "string")', ); @@ -908,7 +905,7 @@ describe('config', () => { it('throws an error if the config file does not exist', () => { return withTempDir(async (tmpDir) => { const promise = loadJSConfigFile( - path.join(tmpDir.path(), 'non-existant-config.js'), + path.join(tmpDir.path(), 'non-existant-config.mjs'), ); await assert.isRejected(promise, UsageError); await assert.isRejected(promise, /Cannot read config file/); @@ -917,11 +914,11 @@ describe('config', () => { it('throws an error if the config file has syntax errors', () => { return withTempDir(async (tmpDir) => { - const configFilePath = path.join(tmpDir.path(), 'config.js'); + const configFilePath = path.join(tmpDir.path(), 'config.mjs'); writeFileSync( configFilePath, - // missing = in two places - `module.exports { + // missing = + `export default { sourceDir 'path/to/fake/source/dir', };`, ); @@ -929,61 +926,7 @@ describe('config', () => { }); }); - it('provides help message on load failure due to .js ESM config file and no package', () => { - return withTempDir(async (tmpDir) => { - const configFilePath = path.join(tmpDir.path(), 'config.js'); - writeFileSync( - configFilePath, - 'export default { sourceDir: "fake/dir" };', - ); - const promise = loadJSConfigFile(configFilePath); - await assert.isRejected(promise, UsageError); - await assert.isRejected(promise, new RegExp(HELP_ERR_IMPORTEXPORT_CJS)); - }); - }); - - it('provides help message on load failure due to .js CJS config file and type module package', () => - withTempDir(async (tmpDir) => { - const cfgFilePath = path.join(tmpDir.path(), 'config.js'); - const pkgFilePath = path.join(tmpDir.path(), 'package.json'); - writeFileSync(cfgFilePath, 'module.exports = {};'); - writeFileSync(pkgFilePath, JSON.stringify({ type: 'module' })); - const promise = loadJSConfigFile(cfgFilePath); - await assert.isRejected(promise, UsageError); - await assert.isRejected(promise, new RegExp(HELP_ERR_MODULE_FROM_ESM)); - })); - - it('provides help message on load failure due to .js ESM config file and type commonjs package', () => - withTempDir(async (tmpDir) => { - const cfgWithExportFilePath = path.join( - tmpDir.path(), - 'config-with-export.js', - ); - const cfgWithImportFilePath = path.join( - tmpDir.path(), - 'config-with-import.js', - ); - const pkgFilePath = path.join(tmpDir.path(), 'package.json'); - writeFileSync(cfgWithExportFilePath, 'export default {}'); - writeFileSync(cfgWithImportFilePath, 'import test from "./test.js";'); - writeFileSync(pkgFilePath, JSON.stringify({ type: 'commonjs' })); - - const promiseErrOnExport = loadJSConfigFile(cfgWithExportFilePath); - await assert.isRejected(promiseErrOnExport, UsageError); - await assert.isRejected( - promiseErrOnExport, - new RegExp(HELP_ERR_IMPORTEXPORT_CJS), - ); - - const promiseErrOnImport = loadJSConfigFile(cfgWithImportFilePath); - await assert.isRejected(promiseErrOnImport, UsageError); - await assert.isRejected( - promiseErrOnImport, - new RegExp(HELP_ERR_IMPORTEXPORT_CJS), - ); - })); - - it('parses successfully .js file as CommonJS config file', () => { + it('does not parse .js files', () => { return withTempDir(async (tmpDir) => { const configFilePath = path.join(tmpDir.path(), 'config.js'); writeFileSync( @@ -996,15 +939,8 @@ describe('config', () => { consoleStream.startCapturing(); const promise = loadJSConfigFile(configFilePath); - - const { capturedMessages } = consoleStream; - consoleStream.stopCapturing(); - - await assert.becomes(promise, { sourceDir: 'fake/dir' }); - assert.include( - capturedMessages.join('\n'), - `WARNING: config file ${configFilePath} ${WARN_LEGACY_JS_EXT}`, - ); + await assert.isRejected(promise, UsageError); + await assert.isRejected(promise, /the file extension should be/); }); }); @@ -1047,7 +983,7 @@ describe('config', () => { it('does not throw an error for an empty config', () => { return withTempDir(async (tmpDir) => { - const configFilePath = path.join(tmpDir.path(), 'config.js'); + const configFilePath = path.join(tmpDir.path(), 'config.cjs'); writeFileSync(configFilePath, 'module.exports = {};'); await loadJSConfigFile(configFilePath); }); @@ -1084,7 +1020,7 @@ describe('config', () => { // will be discovered because it's inside current working // directory const packageJSON = path.join(process.cwd(), 'package.json'); - const homeDirConfig = path.join(tmpDir.path(), '.web-ext-config.js'); + const homeDirConfig = path.join(tmpDir.path(), '.web-ext-config.cjs'); await fs.writeFile(homeDirConfig, 'module.exports = {}'); assert.deepEqual( // Stub out getHomeDir() so that it returns tmpDir.path() @@ -1103,7 +1039,7 @@ describe('config', () => { process.chdir(tmpDir.path()); try { const expectedConfig = path.resolve( - path.join(process.cwd(), '.web-ext-config.js'), + path.join(process.cwd(), '.web-ext-config.cjs'), ); await fs.writeFile(expectedConfig, 'module.exports = {}'); @@ -1127,13 +1063,9 @@ describe('config', () => { const globalConfigCjs = path.resolve( path.join(fakeHomeDir, '.web-ext-config.cjs'), ); - const globalConfig = path.resolve( - path.join(fakeHomeDir, '.web-ext-config.js'), - ); await fs.writeFile(globalConfigMjs, 'export default {}'); await fs.writeFile(globalConfigCjs, 'module.exports = {}'); - await fs.writeFile(globalConfig, 'module.exports = {}'); const packageJSONConfig = path.resolve( path.join(process.cwd(), 'package.json'), @@ -1153,13 +1085,9 @@ describe('config', () => { const projectConfigCjs = path.resolve( path.join(process.cwd(), '.web-ext-config.cjs'), ); - const projectConfig = path.resolve( - path.join(process.cwd(), '.web-ext-config.js'), - ); await fs.writeFile(projectConfigMjs, 'export default {}'); await fs.writeFile(projectConfigCjs, 'module.exports = {}'); - await fs.writeFile(projectConfig, 'module.exports = {}'); const projectConfigUndottedMjs = path.resolve( path.join(process.cwd(), 'web-ext-config.mjs'), @@ -1167,13 +1095,9 @@ describe('config', () => { const projectConfigUndottedCjs = path.resolve( path.join(process.cwd(), 'web-ext-config.cjs'), ); - const projectConfigUndotted = path.resolve( - path.join(process.cwd(), 'web-ext-config.js'), - ); await fs.writeFile(projectConfigUndottedMjs, 'export default {}'); await fs.writeFile(projectConfigUndottedCjs, 'module.exports = {}'); - await fs.writeFile(projectConfigUndotted, 'module.exports = {}'); assert.deepEqual( await _discoverConfigFiles({ @@ -1182,14 +1106,11 @@ describe('config', () => { [ globalConfigMjs, globalConfigCjs, - globalConfig, packageJSONConfig, projectConfigUndottedMjs, projectConfigUndottedCjs, - projectConfigUndotted, projectConfigMjs, projectConfigCjs, - projectConfig, ], ); } finally { diff --git a/tests/unit/test.program.js b/tests/unit/test.program.js index a064145190..7eebe2f2e3 100644 --- a/tests/unit/test.program.js +++ b/tests/unit/test.program.js @@ -638,7 +638,7 @@ describe('program.main', () => { return configObject; }); - await execProgram(['lint', '--config', 'path/to/web-ext-config.js'], { + await execProgram(['lint', '--config', 'path/to/web-ext-config.mjs'], { commands: fakeCommands, runOptions: { loadJSConfigFile: fakeLoadJSConfigFile, @@ -665,7 +665,7 @@ describe('program.main', () => { return configObject; }); - const discoveredFile = 'fake/config.js'; + const discoveredFile = 'fake/config.mjs'; await execProgram(['lint'], { commands: fakeCommands, runOptions: { @@ -703,9 +703,9 @@ describe('program.main', () => { lint: () => Promise.resolve(), }); - const globalConfig = 'home/dir/.web-ext-config.js'; - const projectConfig = 'project/dir/web-ext-config.js'; - const customConfig = path.resolve('custom/web-ext-config.js'); + const globalConfig = 'home/dir/.web-ext-config.mjs'; + const projectConfig = 'project/dir/web-ext-config.mjs'; + const customConfig = path.resolve('custom/web-ext-config.mjs'); const loadJSConfigFile = makeConfigLoader({ configObjects: { @@ -756,8 +756,8 @@ describe('program.main', () => { lint: () => Promise.resolve(), }); - const globalConfig = path.resolve('home/dir/.web-ext-config.js'); - const customConfig = path.resolve('custom/web-ext-config.js'); + const globalConfig = path.resolve('home/dir/.web-ext-config.mjs'); + const customConfig = path.resolve('custom/web-ext-config.mjs'); const finalSourceDir = path.resolve('final/source-dir'); const loadJSConfigFile = makeConfigLoader({ @@ -792,7 +792,7 @@ describe('program.main', () => { lint: () => Promise.resolve(), }); - const customConfig = path.resolve('custom/web-ext-config.js'); + const customConfig = path.resolve('custom/web-ext-config.mjs'); const loadJSConfigFile = makeConfigLoader({ configObjects: {