From 246b8b5f8d0c733f6b2b7ecde933acfb0bfd15ae Mon Sep 17 00:00:00 2001 From: sculpt0r Date: Thu, 9 Mar 2023 23:11:41 +0100 Subject: [PATCH 01/16] feat: handle complex `options.require` config property --- lib/api.js | 23 +++++++++++++++++++---- lib/worker/base.js | 9 ++++++++- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/lib/api.js b/lib/api.js index eaf223650..bea272b98 100644 --- a/lib/api.js +++ b/lib/api.js @@ -24,13 +24,28 @@ import serializeError from './serialize-error.js'; function resolveModules(modules) { return arrify(modules).map(name => { - const modulePath = resolveCwd.silent(name); + if (typeof name === 'string') { + const modulePath = resolveCwd.silent(name); - if (modulePath === undefined) { - throw new Error(`Could not resolve required module ’${name}’`); + if (modulePath === undefined) { + throw new Error(`Could not resolve required module ’${name}’`); + } + + return modulePath; + } + + if (Array.isArray(name) && name.length > 0) { + const modulePath = resolveCwd.silent(name[0]); + + if (modulePath === undefined) { + throw new Error(`Could not resolve required module ’${name[0]}’`); + } + + name[0] = modulePath; + return name; } - return modulePath; + return name; }); } diff --git a/lib/worker/base.js b/lib/worker/base.js index cdd3c4a1a..2cf4bcd1c 100644 --- a/lib/worker/base.js +++ b/lib/worker/base.js @@ -176,7 +176,14 @@ const run = async options => { try { for await (const ref of (options.require || [])) { - await load(ref); + if (typeof ref === 'string') { + await load(ref); + } else if (Array.isArray(ref)) { + const [path, options] = ref; + + const mod = await load(path); + mod.apply(null, ...options); + } } // Install dependency tracker after the require configuration has been evaluated From b6f93cde9474c7729b56ca4a958bc52ddacc2ebe Mon Sep 17 00:00:00 2001 From: sculpt0r Date: Mon, 13 Mar 2023 18:01:31 +0100 Subject: [PATCH 02/16] feat: handle call function from loaded `option.require` - call directly - fallback to `default` --- lib/worker/base.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/worker/base.js b/lib/worker/base.js index 2cf4bcd1c..40478776a 100644 --- a/lib/worker/base.js +++ b/lib/worker/base.js @@ -181,8 +181,15 @@ const run = async options => { } else if (Array.isArray(ref)) { const [path, options] = ref; - const mod = await load(path); - mod.apply(null, ...options); + const loadedModule = await load(path); + + if (typeof loadedModule === 'function') { + loadedModule.apply(null, ...options); + } else if (typeof loadedModule.default === 'function') { + loadedModule.default.apply(null, ...options); + } else { + channel.send({type: 'non-invokable-require-option'}); + } } } From 1143fa3912fbd440b2819b251d645bc51073bc56 Mon Sep 17 00:00:00 2001 From: sculpt0r Date: Wed, 15 Mar 2023 13:50:02 +0100 Subject: [PATCH 03/16] fix: await required functions & allow to pass `this` --- lib/worker/base.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/worker/base.js b/lib/worker/base.js index 40478776a..e6abb5334 100644 --- a/lib/worker/base.js +++ b/lib/worker/base.js @@ -184,9 +184,9 @@ const run = async options => { const loadedModule = await load(path); if (typeof loadedModule === 'function') { - loadedModule.apply(null, ...options); + await loadedModule.apply(...options); } else if (typeof loadedModule.default === 'function') { - loadedModule.default.apply(null, ...options); + await loadedModule.default.apply(...options); } else { channel.send({type: 'non-invokable-require-option'}); } From 9b2ff87fc72d69226e5ac2adfb9c6ea1f3675f33 Mon Sep 17 00:00:00 2001 From: sculpt0r Date: Mon, 27 Mar 2023 07:02:27 +0200 Subject: [PATCH 04/16] refactor: handle loaded module --- lib/worker/base.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/worker/base.js b/lib/worker/base.js index e6abb5334..ed7c0ec8d 100644 --- a/lib/worker/base.js +++ b/lib/worker/base.js @@ -184,9 +184,10 @@ const run = async options => { const loadedModule = await load(path); if (typeof loadedModule === 'function') { - await loadedModule.apply(...options); + await loadedModule(...options); } else if (typeof loadedModule.default === 'function') { - await loadedModule.default.apply(...options); + const {default: fn} = loadedModule; + await fn(...options); } else { channel.send({type: 'non-invokable-require-option'}); } From 1b017a358f61dc2dbc1d07fdaaf960bacab6bf5a Mon Sep 17 00:00:00 2001 From: sculpt0r Date: Tue, 16 May 2023 23:09:53 +0200 Subject: [PATCH 05/16] test(options): add experimental files with failing test suite --- .../fixtures/required-default/ava.config.js | 7 +++++++ .../fixtures/required-default/package.json | 3 +++ test/config-require/fixtures/required-default/req.js | 4 ++++ test/config-require/fixtures/required-default/test.js | 6 ++++++ test/config-require/test.js | 10 ++++++++++ 5 files changed, 30 insertions(+) create mode 100644 test/config-require/fixtures/required-default/ava.config.js create mode 100644 test/config-require/fixtures/required-default/package.json create mode 100644 test/config-require/fixtures/required-default/req.js create mode 100644 test/config-require/fixtures/required-default/test.js create mode 100644 test/config-require/test.js diff --git a/test/config-require/fixtures/required-default/ava.config.js b/test/config-require/fixtures/required-default/ava.config.js new file mode 100644 index 000000000..21832d3f1 --- /dev/null +++ b/test/config-require/fixtures/required-default/ava.config.js @@ -0,0 +1,7 @@ +export default async () => ({ + require: [ + ["req.js", { + "ignore": ["test/*"] + }] + ] +}); diff --git a/test/config-require/fixtures/required-default/package.json b/test/config-require/fixtures/required-default/package.json new file mode 100644 index 000000000..bedb411a9 --- /dev/null +++ b/test/config-require/fixtures/required-default/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/test/config-require/fixtures/required-default/req.js b/test/config-require/fixtures/required-default/req.js new file mode 100644 index 000000000..079978a89 --- /dev/null +++ b/test/config-require/fixtures/required-default/req.js @@ -0,0 +1,4 @@ + +export default function(args){ + console.log(args) +} diff --git a/test/config-require/fixtures/required-default/test.js b/test/config-require/fixtures/required-default/test.js new file mode 100644 index 000000000..af8013cf2 --- /dev/null +++ b/test/config-require/fixtures/required-default/test.js @@ -0,0 +1,6 @@ +import test from 'ava'; + +test('always pass', t => { + t.fail(); +}) + diff --git a/test/config-require/test.js b/test/config-require/test.js new file mode 100644 index 000000000..d171478cc --- /dev/null +++ b/test/config-require/test.js @@ -0,0 +1,10 @@ +import test from '@ava/test'; + +import {fixture} from '../helpers/exec.js'; + +test.only('load sculpt0r', async t => { + const result = await fixture(['required-default/test.js']); + const files = new Set(result.stats.passed.map(({file}) => file)); + + t.true(files.has('required-default/test.js')); +}); From cec80a948240a62d33b32fd068f47b93d8a97828 Mon Sep 17 00:00:00 2001 From: Mark Wubben Date: Sun, 21 May 2023 17:05:56 +0200 Subject: [PATCH 06/16] Throw module resolution error when require option is an empty array --- lib/api.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/api.js b/lib/api.js index bea272b98..f3fea28ad 100644 --- a/lib/api.js +++ b/lib/api.js @@ -34,8 +34,8 @@ function resolveModules(modules) { return modulePath; } - if (Array.isArray(name) && name.length > 0) { - const modulePath = resolveCwd.silent(name[0]); + if (Array.isArray(name)) { + const modulePath = resolveCwd.silent(name[0] ?? ''); if (modulePath === undefined) { throw new Error(`Could not resolve required module ’${name[0]}’`); From 5592ebc430d8779a63f634b0e6d270c932e380fd Mon Sep 17 00:00:00 2001 From: Mark Wubben Date: Sun, 21 May 2023 17:07:40 +0200 Subject: [PATCH 07/16] Collect multiple options --- lib/worker/base.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/worker/base.js b/lib/worker/base.js index ed7c0ec8d..4945e25b5 100644 --- a/lib/worker/base.js +++ b/lib/worker/base.js @@ -179,7 +179,7 @@ const run = async options => { if (typeof ref === 'string') { await load(ref); } else if (Array.isArray(ref)) { - const [path, options] = ref; + const [path, ...options] = ref; const loadedModule = await load(path); From 0b4317be3caa4281e54fb90010502274ab022f16 Mon Sep 17 00:00:00 2001 From: Mark Wubben Date: Sun, 21 May 2023 18:32:11 +0200 Subject: [PATCH 08/16] Modernize & fix tests * Resolve from the project directory by joining paths, this means you can only provide relative files and not dependencies, which is a breaking change * Remove Babel recipe since it's now incompatible * Fix tests and add additional cases * Some refactoring --- docs/06-configuration.md | 86 +++++++++++++++++-- docs/recipes/babel.md | 21 ----- lib/api.js | 26 +----- lib/worker/base.js | 25 ++---- test-tap/api.js | 19 ---- test-tap/fixture/install-global.cjs | 2 - .../fixture/validate-installed-global.cjs | 3 - .../fixtures/exports-default/package.json | 6 ++ .../fixtures/exports-default/required.cjs | 5 ++ .../fixtures/exports-default/test.js | 7 ++ .../fixtures/failed-import/package.json | 6 ++ .../fixtures/failed-import/test.js | 5 ++ .../fixtures/required-default/ava.config.js | 7 -- .../fixtures/required-default/req.js | 4 - .../fixtures/required-default/test.js | 6 -- .../fixtures/single-argument/package.json | 6 ++ .../fixtures/single-argument/required.js | 5 ++ .../fixtures/single-argument/test.js | 7 ++ .../fixtures/with-arguments/ava.config.js | 7 ++ .../package.json | 0 .../fixtures/with-arguments/required.cjs | 7 ++ .../fixtures/with-arguments/required.mjs | 5 ++ .../fixtures/with-arguments/side-effect.js | 1 + .../fixtures/with-arguments/test.js | 15 ++++ test/config-require/test.js | 23 +++-- 25 files changed, 192 insertions(+), 112 deletions(-) delete mode 100644 docs/recipes/babel.md delete mode 100644 test-tap/fixture/install-global.cjs delete mode 100644 test-tap/fixture/validate-installed-global.cjs create mode 100644 test/config-require/fixtures/exports-default/package.json create mode 100644 test/config-require/fixtures/exports-default/required.cjs create mode 100644 test/config-require/fixtures/exports-default/test.js create mode 100644 test/config-require/fixtures/failed-import/package.json create mode 100644 test/config-require/fixtures/failed-import/test.js delete mode 100644 test/config-require/fixtures/required-default/ava.config.js delete mode 100644 test/config-require/fixtures/required-default/req.js delete mode 100644 test/config-require/fixtures/required-default/test.js create mode 100644 test/config-require/fixtures/single-argument/package.json create mode 100644 test/config-require/fixtures/single-argument/required.js create mode 100644 test/config-require/fixtures/single-argument/test.js create mode 100644 test/config-require/fixtures/with-arguments/ava.config.js rename test/config-require/fixtures/{required-default => with-arguments}/package.json (100%) create mode 100644 test/config-require/fixtures/with-arguments/required.cjs create mode 100644 test/config-require/fixtures/with-arguments/required.mjs create mode 100644 test/config-require/fixtures/with-arguments/side-effect.js create mode 100644 test/config-require/fixtures/with-arguments/test.js diff --git a/docs/06-configuration.md b/docs/06-configuration.md index dc92f54c7..82959d4a7 100644 --- a/docs/06-configuration.md +++ b/docs/06-configuration.md @@ -55,7 +55,7 @@ Arguments passed to the CLI will always take precedence over the CLI options con - `verbose`: if `true`, enables verbose output (though there currently non-verbose output is not supported) - `snapshotDir`: specifies a fixed location for storing snapshot files. Use this if your snapshots are ending up in the wrong location - `extensions`: extensions of test files. Setting this overrides the default `["cjs", "mjs", "js"]` value, so make sure to include those extensions in the list. [Experimentally you can configure how files are loaded](#configuring-module-formats) -- `require`: extra modules to require before tests are run. Modules are required in the [worker processes](./01-writing-tests.md#test-isolation) +- `require`: [extra modules to load before test files](#requiring-extra-modules) - `timeout`: Timeouts in AVA behave differently than in other test frameworks. AVA resets a timer after each test, forcing tests to quit if no new test results were received within the specified timeout. This can be used to handle stalled tests. See our [timeout documentation](./07-test-timeouts.md) for more options. - `nodeArguments`: Configure Node.js arguments used to launch worker processes. - `sortTestFiles`: A comparator function to sort test files with. Available only when using a `ava.config.*` file. See an example use case [here](recipes/splitting-tests-ci.md). @@ -84,14 +84,14 @@ The default export can either be a plain object or a factory function which retu ```js export default { - require: ['./_my-test-helper'] + require: ['./_my-test-helper.js'] }; ``` ```js export default function factory() { return { - require: ['./_my-test-helper'] + require: ['./_my-test-helper.js'] }; }; ``` @@ -120,14 +120,14 @@ The module export can either be a plain object or a factory function which retur ```js module.exports = { - require: ['./_my-test-helper'] + require: ['./_my-test-helper.js'] }; ``` ```js module.exports = () => { return { - require: ['./_my-test-helper'] + require: ['./_my-test-helper.js'] }; }; ``` @@ -154,14 +154,14 @@ The default export can either be a plain object or a factory function which retu ```js export default { - require: ['./_my-test-helper'] + require: ['./_my-test-helper.js'] }; ``` ```js export default function factory() { return { - require: ['./_my-test-helper'] + require: ['./_my-test-helper.js'] }; }; ``` @@ -258,6 +258,78 @@ export default { }; ``` +## Requiring extra modules + +Use the `require` configuration to load extra modules before test files are loaded. **Accepts relative paths only**. Paths are resolved against the project directory. You may specify a single value, or an array of values: + +`ava.config.js`: +```js +export default { + require: './_my-test-helper.js' +} +``` +```js +export default { + require: ['./_my-test-helper.js'] +} +``` + +If the module exports a function, it is called and awaited: + +`_my-test-helper.js`: +```js +export default function () { + // Additional setup +} +``` + +`_my-test-helper.cjs`: +```js +module.exports = function () { + // Additional setup +} +``` + +In CJS files, a `default` export is also supported: + +```js +exports.default = function () { + // Never called +} +``` + +You can provide arguments: + +`ava.config.js`: +```js +export default { + require: [ + ['./_my-test-helper.js', 'my', 'arguments'] + ] +} +``` + +`_my-test-helper.js`: +```js +export default function (first, second) { // 'my', 'arguments' + // Additional setup +} +``` + +To load another dependency you need to wrap it in a helper file. This ensures that your dependencies are resolved from your project, not from within AVA: + +`ava.config.js`: +```js +export default { + require: './_register-babel.cjs' +} +``` + +`_register-babel.cjs`: +``` +require('@babel/register') +``` + ## Node arguments The `nodeArguments` configuration may be used to specify additional arguments for launching worker processes. These are combined with `--node-arguments` passed on the CLI and any arguments passed to the `node` binary when starting AVA. diff --git a/docs/recipes/babel.md b/docs/recipes/babel.md deleted file mode 100644 index 5e41bb7d0..000000000 --- a/docs/recipes/babel.md +++ /dev/null @@ -1,21 +0,0 @@ -# Configuring Babel with AVA - -Translations: [Français](https://github.com/avajs/ava-docs/blob/main/fr_FR/docs/recipes/babel.md) - -You can enable Babel support by installing [`@babel/register`](https://babeljs.io/docs/en/babel-register) and `@babel/core`, and then in AVA's configuration requiring `@babel/register`: - -**`package.json`:** - -```json -{ - "ava": { - "require": [ - "@babel/register" - ] - } -} -``` - -`@babel/register` is compatible with CommonJS only. It intercepts `require()` calls and compiles files on the fly. This will compile source, helper and test files. - -For more information visit the [Babel documentation](https://babeljs.io/docs/en/babel-register). diff --git a/lib/api.js b/lib/api.js index f3fea28ad..ace789d08 100644 --- a/lib/api.js +++ b/lib/api.js @@ -9,7 +9,6 @@ import commonPathPrefix from 'common-path-prefix'; import Emittery from 'emittery'; import ms from 'ms'; import pMap from 'p-map'; -import resolveCwd from 'resolve-cwd'; import tempDir from 'temp-dir'; import fork from './fork.js'; @@ -22,27 +21,10 @@ import RunStatus from './run-status.js'; import scheduler from './scheduler.js'; import serializeError from './serialize-error.js'; -function resolveModules(modules) { - return arrify(modules).map(name => { +function normalizeRequireOption(require) { + return arrify(require).map(name => { if (typeof name === 'string') { - const modulePath = resolveCwd.silent(name); - - if (modulePath === undefined) { - throw new Error(`Could not resolve required module ’${name}’`); - } - - return modulePath; - } - - if (Array.isArray(name)) { - const modulePath = resolveCwd.silent(name[0] ?? ''); - - if (modulePath === undefined) { - throw new Error(`Could not resolve required module ’${name[0]}’`); - } - - name[0] = modulePath; - return name; + return arrify(name); } return name; @@ -96,7 +78,7 @@ export default class Api extends Emittery { super(); this.options = {match: [], moduleTypes: {}, ...options}; - this.options.require = resolveModules(this.options.require); + this.options.require = normalizeRequireOption(this.options.require); this._cacheDir = null; this._interruptHandler = () => {}; diff --git a/lib/worker/base.js b/lib/worker/base.js index 4945e25b5..a142b83b2 100644 --- a/lib/worker/base.js +++ b/lib/worker/base.js @@ -1,4 +1,5 @@ import {createRequire} from 'node:module'; +import {resolve as resolvePath} from 'node:path'; import process from 'node:process'; import {pathToFileURL} from 'node:url'; import {workerData} from 'node:worker_threads'; @@ -175,22 +176,14 @@ const run = async options => { }; try { - for await (const ref of (options.require || [])) { - if (typeof ref === 'string') { - await load(ref); - } else if (Array.isArray(ref)) { - const [path, ...options] = ref; - - const loadedModule = await load(path); - - if (typeof loadedModule === 'function') { - await loadedModule(...options); - } else if (typeof loadedModule.default === 'function') { - const {default: fn} = loadedModule; - await fn(...options); - } else { - channel.send({type: 'non-invokable-require-option'}); - } + for await (const [ref, ...args] of (options.require ?? [])) { + const loadedModule = await load(resolvePath(projectDir, ref)); + + if (typeof loadedModule === 'function') { // CJS module + await loadedModule(...args); + } else if (typeof loadedModule.default === 'function') { // ES module, or exports.default from CJS + const {default: fn} = loadedModule; + await fn(...args); } } diff --git a/test-tap/api.js b/test-tap/api.js index b395d81f8..0697b2257 100644 --- a/test-tap/api.js +++ b/test-tap/api.js @@ -359,25 +359,6 @@ for (const opt of options) { }); }); - test(`Node.js-style --require CLI argument - workerThreads: ${opt.workerThreads}`, async t => { - const requirePath = './' + path.relative('.', path.join(__dirname, 'fixture/install-global.cjs')).replace(/\\/g, '/'); - - const api = await apiCreator({ - ...opt, - require: [requirePath], - }); - - return api.run({files: [path.join(__dirname, 'fixture/validate-installed-global.cjs')]}) - .then(runStatus => { - t.equal(runStatus.stats.passedTests, 1); - }); - }); - - test(`Node.js-style --require CLI argument module not found - workerThreads: ${opt.workerThreads}`, t => { - t.rejects(apiCreator({...opt, require: ['foo-bar']}), /^Could not resolve required module ’foo-bar’$/); - t.end(); - }); - test(`caching is enabled by default - workerThreads: ${opt.workerThreads}`, async t => { fs.rmSync(path.join(__dirname, 'fixture/caching/node_modules'), {recursive: true, force: true}); diff --git a/test-tap/fixture/install-global.cjs b/test-tap/fixture/install-global.cjs deleted file mode 100644 index 22de4db13..000000000 --- a/test-tap/fixture/install-global.cjs +++ /dev/null @@ -1,2 +0,0 @@ -'use strict'; -global.foo = 'bar'; diff --git a/test-tap/fixture/validate-installed-global.cjs b/test-tap/fixture/validate-installed-global.cjs deleted file mode 100644 index f1cab146a..000000000 --- a/test-tap/fixture/validate-installed-global.cjs +++ /dev/null @@ -1,3 +0,0 @@ -const test = require('../../entrypoints/main.cjs'); - -test('test', t => t.is(global.foo, 'bar')); diff --git a/test/config-require/fixtures/exports-default/package.json b/test/config-require/fixtures/exports-default/package.json new file mode 100644 index 000000000..62b13ac45 --- /dev/null +++ b/test/config-require/fixtures/exports-default/package.json @@ -0,0 +1,6 @@ +{ + "type": "module", + "ava": { + "require": "./required.cjs" + } +} diff --git a/test/config-require/fixtures/exports-default/required.cjs b/test/config-require/fixtures/exports-default/required.cjs new file mode 100644 index 000000000..8ceb435bf --- /dev/null +++ b/test/config-require/fixtures/exports-default/required.cjs @@ -0,0 +1,5 @@ +exports.called = false; + +exports.default = function () { + exports.called = true; +}; diff --git a/test/config-require/fixtures/exports-default/test.js b/test/config-require/fixtures/exports-default/test.js new file mode 100644 index 000000000..3ea5bc6b9 --- /dev/null +++ b/test/config-require/fixtures/exports-default/test.js @@ -0,0 +1,7 @@ +import test from 'ava'; + +import required from './required.cjs'; + +test('exports.default is called', t => { + t.true(required.called); +}); diff --git a/test/config-require/fixtures/failed-import/package.json b/test/config-require/fixtures/failed-import/package.json new file mode 100644 index 000000000..c79b571ec --- /dev/null +++ b/test/config-require/fixtures/failed-import/package.json @@ -0,0 +1,6 @@ +{ + "type": "module", + "ava": { + "require": "@babel/register" + } +} diff --git a/test/config-require/fixtures/failed-import/test.js b/test/config-require/fixtures/failed-import/test.js new file mode 100644 index 000000000..4be1a9985 --- /dev/null +++ b/test/config-require/fixtures/failed-import/test.js @@ -0,0 +1,5 @@ +import test from 'ava'; + +test('should not make it this far', t => { + t.fail(); +}); diff --git a/test/config-require/fixtures/required-default/ava.config.js b/test/config-require/fixtures/required-default/ava.config.js deleted file mode 100644 index 21832d3f1..000000000 --- a/test/config-require/fixtures/required-default/ava.config.js +++ /dev/null @@ -1,7 +0,0 @@ -export default async () => ({ - require: [ - ["req.js", { - "ignore": ["test/*"] - }] - ] -}); diff --git a/test/config-require/fixtures/required-default/req.js b/test/config-require/fixtures/required-default/req.js deleted file mode 100644 index 079978a89..000000000 --- a/test/config-require/fixtures/required-default/req.js +++ /dev/null @@ -1,4 +0,0 @@ - -export default function(args){ - console.log(args) -} diff --git a/test/config-require/fixtures/required-default/test.js b/test/config-require/fixtures/required-default/test.js deleted file mode 100644 index af8013cf2..000000000 --- a/test/config-require/fixtures/required-default/test.js +++ /dev/null @@ -1,6 +0,0 @@ -import test from 'ava'; - -test('always pass', t => { - t.fail(); -}) - diff --git a/test/config-require/fixtures/single-argument/package.json b/test/config-require/fixtures/single-argument/package.json new file mode 100644 index 000000000..4f81b98d1 --- /dev/null +++ b/test/config-require/fixtures/single-argument/package.json @@ -0,0 +1,6 @@ +{ + "type": "module", + "ava": { + "require": "./required.js" + } +} diff --git a/test/config-require/fixtures/single-argument/required.js b/test/config-require/fixtures/single-argument/required.js new file mode 100644 index 000000000..d363e318d --- /dev/null +++ b/test/config-require/fixtures/single-argument/required.js @@ -0,0 +1,5 @@ +export let required = false; // eslint-disable-line import/no-mutable-exports + +export default function () { + required = true; +} diff --git a/test/config-require/fixtures/single-argument/test.js b/test/config-require/fixtures/single-argument/test.js new file mode 100644 index 000000000..b12c733dd --- /dev/null +++ b/test/config-require/fixtures/single-argument/test.js @@ -0,0 +1,7 @@ +import test from 'ava'; + +import {required} from './required.js'; + +test('loads when given as a single argument', t => { + t.true(required); +}); diff --git a/test/config-require/fixtures/with-arguments/ava.config.js b/test/config-require/fixtures/with-arguments/ava.config.js new file mode 100644 index 000000000..b497f6dfd --- /dev/null +++ b/test/config-require/fixtures/with-arguments/ava.config.js @@ -0,0 +1,7 @@ +export default { + require: [ + ['./required.mjs', 'hello', 'world'], + ['./required.cjs', 'goodbye'], + './side-effect.js', + ], +}; diff --git a/test/config-require/fixtures/required-default/package.json b/test/config-require/fixtures/with-arguments/package.json similarity index 100% rename from test/config-require/fixtures/required-default/package.json rename to test/config-require/fixtures/with-arguments/package.json diff --git a/test/config-require/fixtures/with-arguments/required.cjs b/test/config-require/fixtures/with-arguments/required.cjs new file mode 100644 index 000000000..ad5c055e6 --- /dev/null +++ b/test/config-require/fixtures/with-arguments/required.cjs @@ -0,0 +1,7 @@ +function init(...args) { + init.receivedArgs = args; +} + +init.receivedArgs = null; + +module.exports = init; diff --git a/test/config-require/fixtures/with-arguments/required.mjs b/test/config-require/fixtures/with-arguments/required.mjs new file mode 100644 index 000000000..0a9e1fb51 --- /dev/null +++ b/test/config-require/fixtures/with-arguments/required.mjs @@ -0,0 +1,5 @@ +export let receivedArgs = null; // eslint-disable-line import/no-mutable-exports + +export default function (...args) { + receivedArgs = args; +} diff --git a/test/config-require/fixtures/with-arguments/side-effect.js b/test/config-require/fixtures/with-arguments/side-effect.js new file mode 100644 index 000000000..549233956 --- /dev/null +++ b/test/config-require/fixtures/with-arguments/side-effect.js @@ -0,0 +1 @@ +export default Date.now(); diff --git a/test/config-require/fixtures/with-arguments/test.js b/test/config-require/fixtures/with-arguments/test.js new file mode 100644 index 000000000..3004851c2 --- /dev/null +++ b/test/config-require/fixtures/with-arguments/test.js @@ -0,0 +1,15 @@ +import test from 'ava'; + +import cjs from './required.cjs'; +import {receivedArgs} from './required.mjs'; + +test('receives arguments from config', t => { + t.deepEqual(receivedArgs, ['hello', 'world']); + t.deepEqual(cjs.receivedArgs, ['goodbye']); +}); + +test('ok to load for side-effects', async t => { + const now = Date.now(); + const sideEffect = await import('./side-effect.js'); + t.true(sideEffect.default < now); +}); diff --git a/test/config-require/test.js b/test/config-require/test.js index d171478cc..8de1a97b1 100644 --- a/test/config-require/test.js +++ b/test/config-require/test.js @@ -1,10 +1,23 @@ import test from '@ava/test'; -import {fixture} from '../helpers/exec.js'; +import {cwd, fixture} from '../helpers/exec.js'; -test.only('load sculpt0r', async t => { - const result = await fixture(['required-default/test.js']); - const files = new Set(result.stats.passed.map(({file}) => file)); +test('loads required modules with arguments', async t => { + const result = await fixture([], {cwd: cwd('with-arguments')}); + t.is(result.stats.passed.length, 2); +}); + +test('loads required modules, not as an array', async t => { + const result = await fixture([], {cwd: cwd('single-argument')}); + t.is(result.stats.passed.length, 1); +}); + +test('calls exports.default (CJS)', async t => { + const result = await fixture([], {cwd: cwd('exports-default')}); + t.is(result.stats.passed.length, 1); +}); - t.true(files.has('required-default/test.js')); +test('crashes if module cannot be loaded', async t => { + const result = await t.throwsAsync(fixture([], {cwd: cwd('failed-import')})); + t.is(result.stats.uncaughtExceptions.length, 1); }); From 29e401bb3b32d9c917297298d89ea3c7d400f25e Mon Sep 17 00:00:00 2001 From: sculpt0r Date: Wed, 31 May 2023 16:19:04 +0200 Subject: [PATCH 09/16] test(refactor): renamed test case --- test/config-require/fixtures/with-arguments/test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/config-require/fixtures/with-arguments/test.js b/test/config-require/fixtures/with-arguments/test.js index 3004851c2..8c1e1b43f 100644 --- a/test/config-require/fixtures/with-arguments/test.js +++ b/test/config-require/fixtures/with-arguments/test.js @@ -8,7 +8,7 @@ test('receives arguments from config', t => { t.deepEqual(cjs.receivedArgs, ['goodbye']); }); -test('ok to load for side-effects', async t => { +test('side-effects are execute when tests loaded, before test code', async t => { const now = Date.now(); const sideEffect = await import('./side-effect.js'); t.true(sideEffect.default < now); From b88c6dffba1bc0561a03c5386a11d9264047ef93 Mon Sep 17 00:00:00 2001 From: Mark Wubben Date: Mon, 19 Jun 2023 17:22:54 +0200 Subject: [PATCH 10/16] Add test for argument serialization --- test/config-require/fixtures/non-json/ava.config.js | 5 +++++ test/config-require/fixtures/non-json/package.json | 3 +++ test/config-require/fixtures/non-json/required.mjs | 5 +++++ test/config-require/fixtures/non-json/test.js | 7 +++++++ test/config-require/test.js | 10 ++++++++++ 5 files changed, 30 insertions(+) create mode 100644 test/config-require/fixtures/non-json/ava.config.js create mode 100644 test/config-require/fixtures/non-json/package.json create mode 100644 test/config-require/fixtures/non-json/required.mjs create mode 100644 test/config-require/fixtures/non-json/test.js diff --git a/test/config-require/fixtures/non-json/ava.config.js b/test/config-require/fixtures/non-json/ava.config.js new file mode 100644 index 000000000..34aa3ff57 --- /dev/null +++ b/test/config-require/fixtures/non-json/ava.config.js @@ -0,0 +1,5 @@ +export default { + require: [ + ['./required.mjs', new Map([['hello', 'world']])], + ], +}; diff --git a/test/config-require/fixtures/non-json/package.json b/test/config-require/fixtures/non-json/package.json new file mode 100644 index 000000000..bedb411a9 --- /dev/null +++ b/test/config-require/fixtures/non-json/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/test/config-require/fixtures/non-json/required.mjs b/test/config-require/fixtures/non-json/required.mjs new file mode 100644 index 000000000..0a9e1fb51 --- /dev/null +++ b/test/config-require/fixtures/non-json/required.mjs @@ -0,0 +1,5 @@ +export let receivedArgs = null; // eslint-disable-line import/no-mutable-exports + +export default function (...args) { + receivedArgs = args; +} diff --git a/test/config-require/fixtures/non-json/test.js b/test/config-require/fixtures/non-json/test.js new file mode 100644 index 000000000..c2607362e --- /dev/null +++ b/test/config-require/fixtures/non-json/test.js @@ -0,0 +1,7 @@ +import test from 'ava'; + +import {receivedArgs} from './required.mjs'; + +test('non-JSON arguments can be provided', t => { + t.deepEqual(receivedArgs, [new Map([['hello', 'world']])]); +}); diff --git a/test/config-require/test.js b/test/config-require/test.js index 8de1a97b1..60d1808ba 100644 --- a/test/config-require/test.js +++ b/test/config-require/test.js @@ -7,6 +7,16 @@ test('loads required modules with arguments', async t => { t.is(result.stats.passed.length, 2); }); +test('non-JSON arguments can be provided (worker threads)', async t => { + const result = await fixture([], {cwd: cwd('non-json')}); + t.is(result.stats.passed.length, 1); +}); + +test.failing('non-JSON arguments can be provided (child process)', async t => { + const result = await fixture(['--no-worker-threads'], {cwd: cwd('non-json')}); + t.is(result.stats.passed.length, 1); +}); + test('loads required modules, not as an array', async t => { const result = await fixture([], {cwd: cwd('single-argument')}); t.is(result.stats.passed.length, 1); From db50d3097219f23dcccfddf7c700f06f7bc5f50c Mon Sep 17 00:00:00 2001 From: Mark Wubben Date: Mon, 19 Jun 2023 17:32:04 +0200 Subject: [PATCH 11/16] Use advanced serialization for child processes This means options are (de)serialized the same way as with worker threads, enabling us to use maps etc for require arguments. --- lib/fork.js | 1 + test/config-require/test.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/fork.js b/lib/fork.js index 7630baa39..d551ef84c 100644 --- a/lib/fork.js +++ b/lib/fork.js @@ -53,6 +53,7 @@ const createWorker = (options, execArgv) => { silent: true, env: {NODE_ENV: 'test', ...process.env, ...options.environmentVariables}, execArgv: [...execArgv, ...additionalExecArgv], + serialization: 'advanced', }); postMessage = controlFlow(worker); close = async () => worker.kill(); diff --git a/test/config-require/test.js b/test/config-require/test.js index 60d1808ba..6f1998ae0 100644 --- a/test/config-require/test.js +++ b/test/config-require/test.js @@ -12,7 +12,7 @@ test('non-JSON arguments can be provided (worker threads)', async t => { t.is(result.stats.passed.length, 1); }); -test.failing('non-JSON arguments can be provided (child process)', async t => { +test('non-JSON arguments can be provided (child process)', async t => { const result = await fixture(['--no-worker-threads'], {cwd: cwd('non-json')}); t.is(result.stats.passed.length, 1); }); From 36bb93573882dc08691e27c0183c4a80c125abb2 Mon Sep 17 00:00:00 2001 From: Mark Wubben Date: Wed, 21 Jun 2023 17:29:35 +0200 Subject: [PATCH 12/16] Document that local files are loaded through providers --- docs/06-configuration.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/06-configuration.md b/docs/06-configuration.md index 82959d4a7..e3a756163 100644 --- a/docs/06-configuration.md +++ b/docs/06-configuration.md @@ -260,7 +260,9 @@ export default { ## Requiring extra modules -Use the `require` configuration to load extra modules before test files are loaded. **Accepts relative paths only**. Paths are resolved against the project directory. You may specify a single value, or an array of values: +Use the `require` configuration to load extra modules before test files are loaded. **Accepts relative paths only**. Paths are resolved against the project directory and can be loaded through `@ava/typescript`. + +You may specify a single value, or an array of values: `ava.config.js`: ```js From 345bc31ca635dee6510d25039a30c7d63a7f66da Mon Sep 17 00:00:00 2001 From: Mark Wubben Date: Wed, 21 Jun 2023 17:30:22 +0200 Subject: [PATCH 13/16] Document use of structured clone --- docs/06-configuration.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/06-configuration.md b/docs/06-configuration.md index e3a756163..3cab37878 100644 --- a/docs/06-configuration.md +++ b/docs/06-configuration.md @@ -318,6 +318,8 @@ export default function (first, second) { // 'my', 'arguments' } ``` +Arguments are copied using the [structured clone algorithm](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm). This means `Map` values survive, but a `Buffer` will come out as a `Uint8Array`. + To load another dependency you need to wrap it in a helper file. This ensures that your dependencies are resolved from your project, not from within AVA: `ava.config.js`: From 0ff6c93c9cc50d8d0c47c0376f76836512a7362d Mon Sep 17 00:00:00 2001 From: Mark Wubben Date: Wed, 21 Jun 2023 17:31:02 +0200 Subject: [PATCH 14/16] Regain ability to require dependencies Write a file into node_modules to give us a relative import. --- docs/06-configuration.md | 11 ++--- docs/recipes/babel.md | 21 +++++++++ lib/worker/base.js | 44 ++++++++++++++++++- .../fixtures/require-dependency/.gitignore | 1 + .../node_modules/@ava/stub/index.js | 5 +++ .../node_modules/@ava/stub/package.json | 4 ++ .../fixtures/require-dependency/package.json | 8 ++++ .../fixtures/require-dependency/test.js | 7 +++ test/config-require/test.js | 5 +++ 9 files changed, 97 insertions(+), 9 deletions(-) create mode 100644 docs/recipes/babel.md create mode 100644 test/config-require/fixtures/require-dependency/.gitignore create mode 100644 test/config-require/fixtures/require-dependency/node_modules/@ava/stub/index.js create mode 100644 test/config-require/fixtures/require-dependency/node_modules/@ava/stub/package.json create mode 100644 test/config-require/fixtures/require-dependency/package.json create mode 100644 test/config-require/fixtures/require-dependency/test.js diff --git a/docs/06-configuration.md b/docs/06-configuration.md index 3cab37878..a7b3fe457 100644 --- a/docs/06-configuration.md +++ b/docs/06-configuration.md @@ -260,7 +260,7 @@ export default { ## Requiring extra modules -Use the `require` configuration to load extra modules before test files are loaded. **Accepts relative paths only**. Paths are resolved against the project directory and can be loaded through `@ava/typescript`. +Use the `require` configuration to load extra modules before test files are loaded. Relative paths are resolved against the project directory and can be loaded through `@ava/typescript`. Otherwise, modules are loaded from within the `node_modules` directory inside the project. You may specify a single value, or an array of values: @@ -320,19 +320,16 @@ export default function (first, second) { // 'my', 'arguments' Arguments are copied using the [structured clone algorithm](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm). This means `Map` values survive, but a `Buffer` will come out as a `Uint8Array`. -To load another dependency you need to wrap it in a helper file. This ensures that your dependencies are resolved from your project, not from within AVA: +You can load dependencies installed in your project: `ava.config.js`: ```js export default { - require: './_register-babel.cjs' + require: '@babel/register' } ``` -`_register-babel.cjs`: -``` -require('@babel/register') -``` +These may also export a function which is then invoked, and can receive arguments. ## Node arguments diff --git a/docs/recipes/babel.md b/docs/recipes/babel.md new file mode 100644 index 000000000..5e41bb7d0 --- /dev/null +++ b/docs/recipes/babel.md @@ -0,0 +1,21 @@ +# Configuring Babel with AVA + +Translations: [Français](https://github.com/avajs/ava-docs/blob/main/fr_FR/docs/recipes/babel.md) + +You can enable Babel support by installing [`@babel/register`](https://babeljs.io/docs/en/babel-register) and `@babel/core`, and then in AVA's configuration requiring `@babel/register`: + +**`package.json`:** + +```json +{ + "ava": { + "require": [ + "@babel/register" + ] + } +} +``` + +`@babel/register` is compatible with CommonJS only. It intercepts `require()` calls and compiles files on the fly. This will compile source, helper and test files. + +For more information visit the [Babel documentation](https://babeljs.io/docs/en/babel-register). diff --git a/lib/worker/base.js b/lib/worker/base.js index a142b83b2..037b345f3 100644 --- a/lib/worker/base.js +++ b/lib/worker/base.js @@ -1,10 +1,12 @@ import {createRequire} from 'node:module'; -import {resolve as resolvePath} from 'node:path'; +import {mkdir} from 'node:fs/promises'; +import {join as joinPath, resolve as resolvePath} from 'node:path'; import process from 'node:process'; import {pathToFileURL} from 'node:url'; import {workerData} from 'node:worker_threads'; import setUpCurrentlyUnhandled from 'currently-unhandled'; +import writeFileAtomic from 'write-file-atomic'; import {set as setChalk} from '../chalk.js'; import nowAndTimers from '../now-and-timers.cjs'; @@ -174,10 +176,48 @@ const run = async options => { // We still support require() since it's more easily monkey-patched. return require(ref); }; + const loadRequiredModule = async ref => { + // If the provider can load the module, assume it's a local file and not a + // dependency. + for (const provider of providers) { + if (provider.canLoad(ref)) { + return provider.load(ref, {requireFn: require}); + } + } + + // Try to load the module as a file, relative to the project directory. + // Match load() behavior. + const fullPath = resolvePath(projectDir, ref); + try { + for (const extension of extensionsToLoadAsModules) { + if (fullPath.endsWith(`.${extension}`)) { + return await import(pathToFileURL(fullPath)); + } + } + + return require(fullPath); + } catch (error) { + // If the module could not be found, assume it's not a file but a dependency. + if (error.code === 'ERR_MODULE_NOT_FOUND' || error.code === 'MODULE_NOT_FOUND') { + return importFromProject(ref); + } + + throw error; + } + } + let importFromProject = async ref => { + // Do not use the cacheDir since it's not guaranteed to be inside node_modules. + const avaCacheDir = joinPath(projectDir, 'node_modules', '.cache', 'ava'); + await mkdir(avaCacheDir, {recursive: true}); + const stubPath = joinPath(avaCacheDir, 'import-from-project.mjs'); + await writeFileAtomic(stubPath, 'export const importFromProject = ref => import(ref);\n'); + ({ importFromProject } = await import(stubPath)); + return importFromProject(ref); + } try { for await (const [ref, ...args] of (options.require ?? [])) { - const loadedModule = await load(resolvePath(projectDir, ref)); + const loadedModule = await loadRequiredModule(ref); if (typeof loadedModule === 'function') { // CJS module await loadedModule(...args); diff --git a/test/config-require/fixtures/require-dependency/.gitignore b/test/config-require/fixtures/require-dependency/.gitignore new file mode 100644 index 000000000..4bae8d8f6 --- /dev/null +++ b/test/config-require/fixtures/require-dependency/.gitignore @@ -0,0 +1 @@ +!node_modules/@ava diff --git a/test/config-require/fixtures/require-dependency/node_modules/@ava/stub/index.js b/test/config-require/fixtures/require-dependency/node_modules/@ava/stub/index.js new file mode 100644 index 000000000..d363e318d --- /dev/null +++ b/test/config-require/fixtures/require-dependency/node_modules/@ava/stub/index.js @@ -0,0 +1,5 @@ +export let required = false; // eslint-disable-line import/no-mutable-exports + +export default function () { + required = true; +} diff --git a/test/config-require/fixtures/require-dependency/node_modules/@ava/stub/package.json b/test/config-require/fixtures/require-dependency/node_modules/@ava/stub/package.json new file mode 100644 index 000000000..c9094cfa8 --- /dev/null +++ b/test/config-require/fixtures/require-dependency/node_modules/@ava/stub/package.json @@ -0,0 +1,4 @@ +{ + "name": "@ava/stub", + "type": "module" +} diff --git a/test/config-require/fixtures/require-dependency/package.json b/test/config-require/fixtures/require-dependency/package.json new file mode 100644 index 000000000..cf1e31bb7 --- /dev/null +++ b/test/config-require/fixtures/require-dependency/package.json @@ -0,0 +1,8 @@ +{ + "type": "module", + "ava": { + "require": [ + "@ava/stub" + ] + } +} diff --git a/test/config-require/fixtures/require-dependency/test.js b/test/config-require/fixtures/require-dependency/test.js new file mode 100644 index 000000000..c3da6babd --- /dev/null +++ b/test/config-require/fixtures/require-dependency/test.js @@ -0,0 +1,7 @@ +import test from 'ava'; + +import {required} from '@ava/stub'; + +test('loads dependencies', t => { + t.true(required); +}); diff --git a/test/config-require/test.js b/test/config-require/test.js index 6f1998ae0..f67844cdf 100644 --- a/test/config-require/test.js +++ b/test/config-require/test.js @@ -27,6 +27,11 @@ test('calls exports.default (CJS)', async t => { t.is(result.stats.passed.length, 1); }); +test('loads dependencies', async t => { + const result = await fixture([], {cwd: cwd('require-dependency')}); + t.is(result.stats.passed.length, 1); +}); + test('crashes if module cannot be loaded', async t => { const result = await t.throwsAsync(fixture([], {cwd: cwd('failed-import')})); t.is(result.stats.uncaughtExceptions.length, 1); From 2c5635ec1cbf63a715fa0afeeeb9fb2c6bd42be1 Mon Sep 17 00:00:00 2001 From: Mark Wubben Date: Wed, 21 Jun 2023 17:36:16 +0200 Subject: [PATCH 15/16] Fix linting issues --- lib/worker/base.js | 12 +++++++----- .../fixtures/require-dependency/test.js | 3 +-- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/lib/worker/base.js b/lib/worker/base.js index 037b345f3..e3939b754 100644 --- a/lib/worker/base.js +++ b/lib/worker/base.js @@ -1,5 +1,5 @@ -import {createRequire} from 'node:module'; import {mkdir} from 'node:fs/promises'; +import {createRequire} from 'node:module'; import {join as joinPath, resolve as resolvePath} from 'node:path'; import process from 'node:process'; import {pathToFileURL} from 'node:url'; @@ -176,6 +176,7 @@ const run = async options => { // We still support require() since it's more easily monkey-patched. return require(ref); }; + const loadRequiredModule = async ref => { // If the provider can load the module, assume it's a local file and not a // dependency. @@ -191,7 +192,7 @@ const run = async options => { try { for (const extension of extensionsToLoadAsModules) { if (fullPath.endsWith(`.${extension}`)) { - return await import(pathToFileURL(fullPath)); + return await import(pathToFileURL(fullPath)); // eslint-disable-line no-await-in-loop } } @@ -204,16 +205,17 @@ const run = async options => { throw error; } - } + }; + let importFromProject = async ref => { // Do not use the cacheDir since it's not guaranteed to be inside node_modules. const avaCacheDir = joinPath(projectDir, 'node_modules', '.cache', 'ava'); await mkdir(avaCacheDir, {recursive: true}); const stubPath = joinPath(avaCacheDir, 'import-from-project.mjs'); await writeFileAtomic(stubPath, 'export const importFromProject = ref => import(ref);\n'); - ({ importFromProject } = await import(stubPath)); + ({importFromProject} = await import(stubPath)); return importFromProject(ref); - } + }; try { for await (const [ref, ...args] of (options.require ?? [])) { diff --git a/test/config-require/fixtures/require-dependency/test.js b/test/config-require/fixtures/require-dependency/test.js index c3da6babd..8f82ee74d 100644 --- a/test/config-require/fixtures/require-dependency/test.js +++ b/test/config-require/fixtures/require-dependency/test.js @@ -1,6 +1,5 @@ -import test from 'ava'; - import {required} from '@ava/stub'; +import test from 'ava'; test('loads dependencies', t => { t.true(required); From 6b5ce306bad8c44b8d0cdf870838df4fe2ff95b3 Mon Sep 17 00:00:00 2001 From: Mark Wubben Date: Wed, 21 Jun 2023 17:54:06 +0200 Subject: [PATCH 16/16] Hopefully make it work on Windows --- lib/worker/base.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/worker/base.js b/lib/worker/base.js index e3939b754..63c8a7db5 100644 --- a/lib/worker/base.js +++ b/lib/worker/base.js @@ -213,7 +213,7 @@ const run = async options => { await mkdir(avaCacheDir, {recursive: true}); const stubPath = joinPath(avaCacheDir, 'import-from-project.mjs'); await writeFileAtomic(stubPath, 'export const importFromProject = ref => import(ref);\n'); - ({importFromProject} = await import(stubPath)); + ({importFromProject} = await import(pathToFileURL(stubPath))); return importFromProject(ref); };