From c35f00184db12161f30f02fd4b44dc780c754935 Mon Sep 17 00:00:00 2001 From: David Goss Date: Sat, 18 Sep 2021 10:35:24 +0100 Subject: [PATCH] add ESM support (take 2) (#1649) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Revert "temporarily revert ESM change (#1647)" This reverts commit 084c1f258097d1e424c3ecddf8bd980d8d8f5229. * add failing scenario for deep imports * define entry point with dot * make deep imports work via export patterns * move doc to own file * link to doc from readme * add changelog entry * add example to doc * remove confusing comment * remove cli option, use import by default * update documentation * remove redundant describe * fix ordering * Update features/esm.feature Co-authored-by: Aurélien Reeves * Update features/esm.feature Co-authored-by: Aurélien Reeves * simplify tagging * use import only if a javascript file * add note about no transpilers * inline to avoid confusing reassignment * whoops, re-add try/catch * use require with transpilers; import otherwise * remove pointless return * support .cjs config file * type and import the importer * actually dont import - causes issues Co-authored-by: Aurélien Reeves --- CHANGELOG.md | 1 + README.md | 1 + dependency-lint.yml | 1 + docs/esm.md | 34 +++++++++++ features/direct_imports.feature | 17 ++++++ features/esm.feature | 63 +++++++++++++++++++ features/profiles.feature | 4 ++ features/support/hooks.ts | 9 ++- package-lock.json | 7 ++- package.json | 12 +++- src/cli/argv_parser.ts | 6 +- src/cli/configuration_builder.ts | 2 +- src/cli/configuration_builder_spec.ts | 8 ++- src/cli/index.ts | 22 ++++--- src/cli/profile_loader.ts | 31 ++++++---- src/cli/profile_loader_spec.ts | 34 ++++++++--- src/formatter/builder.ts | 64 +++++++++++++------- src/formatter/helpers/issue_helpers_spec.ts | 2 +- src/formatter/progress_bar_formatter_spec.ts | 4 +- src/importer.js | 13 ++++ src/runtime/parallel/worker.ts | 11 +++- src/wrapper.mjs | 38 ++++++++++++ test/formatter_helpers.ts | 2 +- tsconfig.json | 2 +- 24 files changed, 313 insertions(+), 75 deletions(-) create mode 100644 docs/esm.md create mode 100644 features/esm.feature create mode 100644 src/importer.js create mode 100644 src/wrapper.mjs diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a90d06ee..797d09cbd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ See the [migration guide](./docs/migration.md) for details of how to migrate fro ### Added +* Add support for user code as native ES modules * `BeforeStep` and `AfterStep` hook functions now have access to the `pickleStep` in their argument object. * `--config` option to the CLI. It allows you to specify a configuration file other than `cucumber.js`. See [docs/profiles.md](./docs/profiles.md#using-another-file-than-cucumberjs) for more info. diff --git a/README.md b/README.md index cb3d87a9d..5c7ac86cc 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,7 @@ The following documentation is for master. See below the documentation for older * [Attachments](/docs/support_files/attachments.md) * [API Reference](/docs/support_files/api_reference.md) * Guides + * [ES Modules](./docs/esm.md) * [Running in parallel](./docs/parallel.md) * [Retrying failing scenarios](./docs/retry.md) * [Profiles](./docs/profiles.md) diff --git a/dependency-lint.yml b/dependency-lint.yml index 581dc9c05..c1fe0f844 100644 --- a/dependency-lint.yml +++ b/dependency-lint.yml @@ -44,6 +44,7 @@ requiredModules: - 'dist/**/*' - 'lib/**/*' - 'node_modules/**/*' + - 'src/importer.js' - 'tmp/**/*' root: '**/*.{js,ts}' stripLoaders: false diff --git a/docs/esm.md b/docs/esm.md new file mode 100644 index 000000000..9e7e2a54e --- /dev/null +++ b/docs/esm.md @@ -0,0 +1,34 @@ +# ES Modules (experimental) + +You can optionally write your support code (steps, hooks, etc) with native ES modules syntax - i.e. using `import` and `export` statements without transpiling. This is enabled without any additional configuration, and you can use either of the `.js` or `.mjs` file extensions. + +Example (adapted from [our original example](./nodejs_example.md)): + +```javascript +// features/support/steps.mjs +import { Given, When, Then } from '@cucumber/cucumber' +import { strict as assert } from 'assert' + +Given('a variable set to {int}', function (number) { + this.setTo(number) +}) + +When('I increment the variable by {int}', function (number) { + this.incrementBy(number) +}) + +Then('the variable should contain {int}', function (number) { + assert.equal(this.variable, number) +}) +``` + +As well as support code, these things can also be in ES modules syntax: + +- Custom formatters +- Custom snippets + +You can use ES modules selectively/incrementally - so you can have a mixture of CommonJS and ESM in the same project. + +When using a transpiler for e.g. TypeScript, ESM isn't supported - you'll need to configure your transpiler to output modules in CommonJS syntax (for now). + +The config file referenced for [Profiles](./profiles.md) can only be in CommonJS syntax. In a project with `type=module`, you can name the file `cucumber.cjs`, since Node expects `.js` files to be in ESM syntax in such projects. diff --git a/features/direct_imports.feature b/features/direct_imports.feature index c01905136..a9ebd7250 100644 --- a/features/direct_imports.feature +++ b/features/direct_imports.feature @@ -44,3 +44,20 @@ Feature: Core feature elements execution using direct imports """ features/step_definitions/cucumber_steps.js:3 """ + + Scenario: deep imports don't break everything + Given a file named "features/a.feature" with: + """ + Feature: some feature + Scenario: some scenario + Given a step passes + """ + And a file named "features/step_definitions/cucumber_steps.js" with: + """ + const {Given} = require('@cucumber/cucumber') + const TestCaseHookDefinition = require('@cucumber/cucumber/lib/models/test_case_hook_definition') + + Given(/^a step passes$/, function() {}); + """ + When I run cucumber-js + Then it passes \ No newline at end of file diff --git a/features/esm.feature b/features/esm.feature new file mode 100644 index 000000000..0191a59cf --- /dev/null +++ b/features/esm.feature @@ -0,0 +1,63 @@ +@esm +Feature: ES modules support + + cucumber-js works with native ES modules + + Scenario Outline: native module syntax works in support code, formatters and snippets + Given a file named "features/a.feature" with: + """ + Feature: + Scenario: one + Given a step passes + + Scenario: two + Given a step passes + """ + And a file named "features/step_definitions/cucumber_steps.js" with: + """ + import {Given} from '@cucumber/cucumber' + + Given(/^a step passes$/, function() {}); + """ + And a file named "custom-formatter.js" with: + """ + import {SummaryFormatter} from '@cucumber/cucumber' + + export default class CustomFormatter extends SummaryFormatter {} + """ + And a file named "custom-snippet-syntax.js" with: + """ + export default class CustomSnippetSyntax { + build(opts) { + return 'hello world' + } + } + """ + And a file named "cucumber.cjs" with: + """ + module.exports = { + 'default': '--format summary' + } + """ + When I run cucumber-js with ` --format ./custom-formatter.js --format-options '{"snippetSyntax": "./custom-snippet-syntax.js"}' ` + Then it passes + Examples: + | args | + | | + | --parallel 2 | + + Scenario: .mjs support code files are matched by default + Given a file named "features/a.feature" with: + """ + Feature: + Scenario: + Given a step passes + """ + And a file named "features/step_definitions/cucumber_steps.mjs" with: + """ + import {Given} from '@cucumber/cucumber' + + Given(/^a step passes$/, function() {}); + """ + When I run cucumber-js + Then it passes diff --git a/features/profiles.feature b/features/profiles.feature index c229ca4e8..fca00d0fc 100644 --- a/features/profiles.feature +++ b/features/profiles.feature @@ -91,3 +91,7 @@ Feature: default command line arguments | OPT | | -c | | --config | + + Scenario: specifying a configuration file that doesn't exist + When I run cucumber-js with `--config doesntexist.js` + Then it fails diff --git a/features/support/hooks.ts b/features/support/hooks.ts index 04d5bee06..650a50ee4 100644 --- a/features/support/hooks.ts +++ b/features/support/hooks.ts @@ -13,7 +13,7 @@ Before('@debug', function (this: World) { this.debug = true }) -Before('@spawn', function (this: World) { +Before('@spawn or @esm', function (this: World) { this.spawn = true }) @@ -43,6 +43,13 @@ Before(function ( this.localExecutablePath = path.join(projectPath, 'bin', 'cucumber-js') }) +Before('@esm', function (this: World) { + fsExtra.writeJSONSync(path.join(this.tmpDir, 'package.json'), { + name: 'feature-test-pickle', + type: 'module', + }) +}) + Before('@global-install', function (this: World) { const tmpObject = tmp.dirSync({ unsafeCleanup: true }) diff --git a/package-lock.json b/package-lock.json index 487ae9e68..7b9f07064 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,7 +21,6 @@ "cli-table3": "^0.6.0", "colors": "^1.4.0", "commander": "^8.0.0", - "create-require": "^1.1.1", "duration": "^0.2.2", "durations": "^3.4.2", "figures": "^3.2.0", @@ -2089,7 +2088,8 @@ "node_modules/create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", - "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==" + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true }, "node_modules/cross-spawn": { "version": "7.0.3", @@ -8825,7 +8825,8 @@ "create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", - "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==" + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true }, "cross-spawn": { "version": "7.0.3", diff --git a/package.json b/package.json index a1c574820..e3328267f 100644 --- a/package.json +++ b/package.json @@ -163,6 +163,15 @@ "lib": "./lib" }, "main": "./lib/index.js", + "exports": { + ".": { + "import": "./lib/wrapper.mjs", + "require": "./lib/index.js" + }, + "./lib/*": { + "require": "./lib/*.js" + } + }, "types": "./lib/index.d.ts", "engines": { "node": ">=12" @@ -180,7 +189,6 @@ "cli-table3": "^0.6.0", "colors": "^1.4.0", "commander": "^8.0.0", - "create-require": "^1.1.1", "duration": "^0.2.2", "durations": "^3.4.2", "figures": "^3.2.0", @@ -252,7 +260,7 @@ "typescript": "4.4.2" }, "scripts": { - "build-local": "tsc --build tsconfig.node.json", + "build-local": "tsc --build tsconfig.node.json && cp src/importer.js lib/ && cp src/wrapper.mjs lib/", "cck-test": "mocha 'compatibility/**/*_spec.ts'", "feature-test": "node ./bin/cucumber-js", "html-formatter": "node ./bin/cucumber-js --profile htmlFormatter", diff --git a/src/cli/argv_parser.ts b/src/cli/argv_parser.ts index cdc787b9e..9b1ac0bc9 100644 --- a/src/cli/argv_parser.ts +++ b/src/cli/argv_parser.ts @@ -107,11 +107,7 @@ const ArgvParser = { .usage('[options] [...]') .version(version, '-v, --version') .option('-b, --backtrace', 'show full backtrace for errors') - .option( - '-c, --config ', - 'specify configuration file', - 'cucumber.js' - ) + .option('-c, --config ', 'specify configuration file') .option( '-d, --dry-run', 'invoke formatters without executing steps', diff --git a/src/cli/configuration_builder.ts b/src/cli/configuration_builder.ts index 1673e396c..8e79306a7 100644 --- a/src/cli/configuration_builder.ts +++ b/src/cli/configuration_builder.ts @@ -76,7 +76,7 @@ export default class ConfigurationBuilder { } supportCodePaths = await this.expandPaths( unexpandedSupportCodePaths, - '.js' + '.@(js|mjs)' ) } return { diff --git a/src/cli/configuration_builder_spec.ts b/src/cli/configuration_builder_spec.ts index 5d1d2c6b5..63c58b5aa 100644 --- a/src/cli/configuration_builder_spec.ts +++ b/src/cli/configuration_builder_spec.ts @@ -71,8 +71,10 @@ describe('Configuration', () => { const relativeFeaturePath = path.join('features', 'a.feature') const featurePath = path.join(cwd, relativeFeaturePath) await fsExtra.outputFile(featurePath, '') - const supportCodePath = path.join(cwd, 'features', 'a.js') - await fsExtra.outputFile(supportCodePath, '') + const jsSupportCodePath = path.join(cwd, 'features', 'a.js') + await fsExtra.outputFile(jsSupportCodePath, '') + const esmSupportCodePath = path.join(cwd, 'features', 'a.mjs') + await fsExtra.outputFile(esmSupportCodePath, '') const argv = baseArgv.concat([relativeFeaturePath]) // Act @@ -82,7 +84,7 @@ describe('Configuration', () => { // Assert expect(featurePaths).to.eql([featurePath]) expect(pickleFilterOptions.featurePaths).to.eql([relativeFeaturePath]) - expect(supportCodePaths).to.eql([supportCodePath]) + expect(supportCodePaths).to.eql([jsSupportCodePath, esmSupportCodePath]) }) it('deduplicates the .feature files before returning', async function () { diff --git a/src/cli/index.ts b/src/cli/index.ts index 6a42d82c0..929fe03b4 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -29,7 +29,10 @@ import { IParsedArgvFormatOptions } from './argv_parser' import HttpStream from '../formatter/http_stream' import { promisify } from 'util' import { Writable } from 'stream' +import { pathToFileURL } from 'url' +// eslint-disable-next-line @typescript-eslint/no-var-requires +const { importer } = require('../importer') const { incrementing, uuid } = IdGenerator export interface ICliRunResult { @@ -143,7 +146,7 @@ export default class Cli { ) type = 'progress' } - return FormatterBuilder.build(type, typeOptions) + return await FormatterBuilder.build(type, typeOptions) }) ) return async function () { @@ -151,21 +154,20 @@ export default class Cli { } } - getSupportCodeLibrary({ + async getSupportCodeLibrary({ newId, supportCodeRequiredModules, supportCodePaths, - }: IGetSupportCodeLibraryRequest): ISupportCodeLibrary { + }: IGetSupportCodeLibraryRequest): Promise { supportCodeRequiredModules.map((module) => require(module)) supportCodeLibraryBuilder.reset(this.cwd, newId) - supportCodePaths.forEach((codePath) => { - try { + for (const codePath of supportCodePaths) { + if (supportCodeRequiredModules.length) { require(codePath) - } catch (e) { - console.error(e.stack) - console.error('codepath: ' + codePath) + } else { + await importer(pathToFileURL(codePath)) } - }) + } return supportCodeLibraryBuilder.finalize() } @@ -184,7 +186,7 @@ export default class Cli { configuration.predictableIds && configuration.parallel <= 1 ? incrementing() : uuid() - const supportCodeLibrary = this.getSupportCodeLibrary({ + const supportCodeLibrary = await this.getSupportCodeLibrary({ newId, supportCodePaths: configuration.supportCodePaths, supportCodeRequiredModules: configuration.supportCodeRequiredModules, diff --git a/src/cli/profile_loader.ts b/src/cli/profile_loader.ts index 4543e71f0..8cd97d499 100644 --- a/src/cli/profile_loader.ts +++ b/src/cli/profile_loader.ts @@ -3,24 +3,31 @@ import path from 'path' import stringArgv from 'string-argv' import { doesHaveValue, doesNotHaveValue } from '../value_checker' -export default class ProfileLoader { - private readonly directory: string +const DEFAULT_FILENAMES = ['cucumber.cjs', 'cucumber.js'] - constructor(directory: string) { - this.directory = directory - } +export default class ProfileLoader { + constructor(private readonly directory: string) {} async getDefinitions(configFile?: string): Promise> { - const definitionsFilePath: string = path.join( - this.directory, - configFile || 'cucumber.js' + if (configFile) { + return this.loadFile(configFile) + } + + const defaultFile = DEFAULT_FILENAMES.find((filename) => + fs.existsSync(path.join(this.directory, filename)) ) - const exists = await fs.exists(definitionsFilePath) - if (!exists) { - return {} + if (defaultFile) { + return this.loadFile(defaultFile) } - const definitions = require(definitionsFilePath) // eslint-disable-line @typescript-eslint/no-var-requires + + return {} + } + + loadFile(configFile: string): Record { + const definitionsFilePath: string = path.join(this.directory, configFile) + // eslint-disable-next-line @typescript-eslint/no-var-requires + const definitions = require(definitionsFilePath) if (typeof definitions !== 'object') { throw new Error(`${definitionsFilePath} does not export an object`) } diff --git a/src/cli/profile_loader_spec.ts b/src/cli/profile_loader_spec.ts index 909dbb281..fcbe4c035 100644 --- a/src/cli/profile_loader_spec.ts +++ b/src/cli/profile_loader_spec.ts @@ -9,8 +9,9 @@ import { doesHaveValue, valueOrDefault } from '../value_checker' interface TestProfileLoaderOptions { definitionsFileContent?: string + definitionsFileName?: string profiles?: string[] - configFile?: string + configOption?: string } async function testProfileLoader( @@ -19,15 +20,11 @@ async function testProfileLoader( const cwd = await promisify(tmp.dir)({ unsafeCleanup: true, }) - let configurationFileName = 'cucumber.js' - - if (doesHaveValue(opts.configFile)) { - configurationFileName = opts.configFile - } + const definitionsFileName = opts.definitionsFileName ?? 'cucumber.js' if (doesHaveValue(opts.definitionsFileContent)) { await fs.writeFile( - path.join(cwd, configurationFileName), + path.join(cwd, definitionsFileName), opts.definitionsFileContent ) } @@ -35,7 +32,7 @@ async function testProfileLoader( const profileLoader = new ProfileLoader(cwd) return await profileLoader.getArgv( valueOrDefault(opts.profiles, []), - opts.configFile + opts.configOption ) } @@ -171,13 +168,32 @@ describe('ProfileLoader', () => { // Act const result = await testProfileLoader({ definitionsFileContent, + definitionsFileName: '.cucumber-rc.js', profiles: ['profile3'], - configFile: '.cucumber-rc.js', + configOption: '.cucumber-rc.js', }) // Assert expect(result).to.eql(['--opt3', '--opt4']) }) + + it('throws when the file doesnt exist', async () => { + // Arrange + const definitionsFileContent = + 'module.exports = {profile3: "--opt3 --opt4"}' + + // Act + try { + await testProfileLoader({ + definitionsFileContent, + profiles: [], + configOption: 'doesntexist.js', + }) + expect.fail('should throw') + } catch (e) { + expect(e.message).to.contain('Cannot find module') + } + }) }) }) }) diff --git a/src/formatter/builder.ts b/src/formatter/builder.ts index 27da244eb..72862b074 100644 --- a/src/formatter/builder.ts +++ b/src/formatter/builder.ts @@ -20,7 +20,9 @@ import { Writable as WritableStream } from 'stream' import { IParsedArgvFormatOptions } from '../cli/argv_parser' import { SnippetInterface } from './step_definition_snippet_builder/snippet_syntax' import HtmlFormatter from './html_formatter' -import createRequire from 'create-require' +import { pathToFileURL } from 'url' +// eslint-disable-next-line @typescript-eslint/no-var-requires +const { importer } = require('../importer') interface IGetStepDefinitionSnippetBuilderOptions { cwd: string @@ -41,18 +43,19 @@ export interface IBuildOptions { } const FormatterBuilder = { - build(type: string, options: IBuildOptions): Formatter { - const FormatterConstructor = FormatterBuilder.getConstructorByType( + async build(type: string, options: IBuildOptions): Promise { + const FormatterConstructor = await FormatterBuilder.getConstructorByType( type, options.cwd ) const colorFns = getColorFns(options.parsedArgvOptions.colorsEnabled) - const snippetBuilder = FormatterBuilder.getStepDefinitionSnippetBuilder({ - cwd: options.cwd, - snippetInterface: options.parsedArgvOptions.snippetInterface, - snippetSyntax: options.parsedArgvOptions.snippetSyntax, - supportCodeLibrary: options.supportCodeLibrary, - }) + const snippetBuilder = + await FormatterBuilder.getStepDefinitionSnippetBuilder({ + cwd: options.cwd, + snippetInterface: options.parsedArgvOptions.snippetInterface, + snippetSyntax: options.parsedArgvOptions.snippetSyntax, + supportCodeLibrary: options.supportCodeLibrary, + }) return new FormatterConstructor({ colorFns, snippetBuilder, @@ -60,7 +63,10 @@ const FormatterBuilder = { }) }, - getConstructorByType(type: string, cwd: string): typeof Formatter { + async getConstructorByType( + type: string, + cwd: string + ): Promise { switch (type) { case 'json': return JsonFormatter @@ -83,11 +89,11 @@ const FormatterBuilder = { case 'usage-json': return UsageJsonFormatter default: - return FormatterBuilder.loadCustomFormatter(type, cwd) + return await FormatterBuilder.loadCustomFormatter(type, cwd) } }, - getStepDefinitionSnippetBuilder({ + async getStepDefinitionSnippetBuilder({ cwd, snippetInterface, snippetSyntax, @@ -99,7 +105,9 @@ const FormatterBuilder = { let Syntax = JavascriptSnippetSyntax if (doesHaveValue(snippetSyntax)) { const fullSyntaxPath = path.resolve(cwd, snippetSyntax) - Syntax = require(fullSyntaxPath) // eslint-disable-line @typescript-eslint/no-var-requires + Syntax = FormatterBuilder.resolveConstructor( + await importer(pathToFileURL(fullSyntaxPath)) + ) } return new StepDefinitionSnippetBuilder({ snippetSyntax: new Syntax(snippetInterface), @@ -107,20 +115,30 @@ const FormatterBuilder = { }) }, - loadCustomFormatter(customFormatterPath: string, cwd: string) { - const CustomFormatter = createRequire(cwd)(customFormatterPath) - - if (typeof CustomFormatter === 'function') { + async loadCustomFormatter(customFormatterPath: string, cwd: string) { + let CustomFormatter = customFormatterPath.startsWith(`.`) + ? await importer(pathToFileURL(path.resolve(cwd, customFormatterPath))) + : await importer(customFormatterPath) + CustomFormatter = FormatterBuilder.resolveConstructor(CustomFormatter) + if (doesHaveValue(CustomFormatter)) { return CustomFormatter + } else { + throw new Error( + `Custom formatter (${customFormatterPath}) does not export a function` + ) + } + }, + + resolveConstructor(ImportedCode: any) { + if (typeof ImportedCode === 'function') { + return ImportedCode } else if ( - doesHaveValue(CustomFormatter) && - typeof CustomFormatter.default === 'function' + doesHaveValue(ImportedCode) && + typeof ImportedCode.default === 'function' ) { - return CustomFormatter.default + return ImportedCode.default } - throw new Error( - `Custom formatter (${customFormatterPath}) does not export a function` - ) + return null }, } diff --git a/src/formatter/helpers/issue_helpers_spec.ts b/src/formatter/helpers/issue_helpers_spec.ts index e7e895bbe..f60cfed10 100644 --- a/src/formatter/helpers/issue_helpers_spec.ts +++ b/src/formatter/helpers/issue_helpers_spec.ts @@ -24,7 +24,7 @@ async function testFormatIssue(sourceData: string): Promise { cwd: 'project/', colorFns: getColorFns(false), number: 1, - snippetBuilder: FormatterBuilder.getStepDefinitionSnippetBuilder({ + snippetBuilder: await FormatterBuilder.getStepDefinitionSnippetBuilder({ cwd: 'project/', supportCodeLibrary, }), diff --git a/src/formatter/progress_bar_formatter_spec.ts b/src/formatter/progress_bar_formatter_spec.ts index ce1fd9a3c..c0709331a 100644 --- a/src/formatter/progress_bar_formatter_spec.ts +++ b/src/formatter/progress_bar_formatter_spec.ts @@ -58,7 +58,7 @@ async function testProgressBarFormatter({ output += data } const passThrough = new PassThrough() - const progressBarFormatter = FormatterBuilder.build('progress-bar', { + const progressBarFormatter = (await FormatterBuilder.build('progress-bar', { cwd: '', eventBroadcaster, eventDataCollector: new EventDataCollector(eventBroadcaster), @@ -67,7 +67,7 @@ async function testProgressBarFormatter({ stream: passThrough, cleanup: promisify(passThrough.end.bind(passThrough)), supportCodeLibrary, - }) as ProgressBarFormatter + })) as ProgressBarFormatter let mocked = false for (const envelope of envelopes) { eventBroadcaster.emit('envelope', envelope) diff --git a/src/importer.js b/src/importer.js new file mode 100644 index 000000000..5347a38ca --- /dev/null +++ b/src/importer.js @@ -0,0 +1,13 @@ +/** + * Provides the async `import()` function to source code that needs it, + * without having it transpiled down to commonjs `require()` by TypeScript. + * See https://github.com/microsoft/TypeScript/issues/43329. + * + * @param {any} descriptor - A URL or path for the module to load + * @return {Promise} Promise that resolves to the loaded module + */ +async function importer(descriptor) { + return await import(descriptor) +} + +module.exports = { importer } diff --git a/src/runtime/parallel/worker.ts b/src/runtime/parallel/worker.ts index 629bf4a2d..b8b42b38b 100644 --- a/src/runtime/parallel/worker.ts +++ b/src/runtime/parallel/worker.ts @@ -18,7 +18,10 @@ import { doesHaveValue, valueOrDefault } from '../../value_checker' import { IRuntimeOptions } from '../index' import { PredictableTestRunStopwatch, RealTestRunStopwatch } from '../stopwatch' import { duration } from 'durations' +import { pathToFileURL } from 'url' +// eslint-disable-next-line @typescript-eslint/no-var-requires +const { importer } = require('../../importer') const { uuid } = IdGenerator type IExitFunction = (exitCode: number, error?: Error, message?: string) => void @@ -72,7 +75,13 @@ export default class Worker { }: IWorkerCommandInitialize): Promise { supportCodeRequiredModules.map((module) => require(module)) supportCodeLibraryBuilder.reset(this.cwd, this.newId) - supportCodePaths.forEach((codePath) => require(codePath)) + for (const codePath of supportCodePaths) { + if (supportCodeRequiredModules.length) { + require(codePath) + } else { + await importer(pathToFileURL(codePath)) + } + } this.supportCodeLibrary = supportCodeLibraryBuilder.finalize(supportCodeIds) this.worldParameters = options.worldParameters diff --git a/src/wrapper.mjs b/src/wrapper.mjs new file mode 100644 index 000000000..360d5eee7 --- /dev/null +++ b/src/wrapper.mjs @@ -0,0 +1,38 @@ +import cucumber from './index.js' + +export const Cli = cucumber.Cli +export const parseGherkinMessageStream = cucumber.parseGherkinMessageStream +export const PickleFilter = cucumber.PickleFilter +export const Runtime = cucumber.Runtime +export const supportCodeLibraryBuilder = cucumber.supportCodeLibraryBuilder +export const Status = cucumber.Status +export const DataTable = cucumber.DataTable + +export const Formatter = cucumber.Formatter +export const FormatterBuilder = cucumber.FormatterBuilder +export const JsonFormatter = cucumber.JsonFormatter +export const ProgressFormatter = cucumber.ProgressFormatter +export const RerunFormatter = cucumber.RerunFormatter +export const SnippetsFormatter = cucumber.SnippetsFormatter +export const SummaryFormatter = cucumber.SummaryFormatter +export const UsageFormatter = cucumber.UsageFormatter +export const UsageJsonFormatter = cucumber.UsageJsonFormatter +export const formatterHelpers = cucumber.formatterHelpers + +export const After = cucumber.After +export const AfterAll = cucumber.AfterAll +export const AfterStep = cucumber.AfterStep +export const Before = cucumber.Before +export const BeforeAll = cucumber.BeforeAll +export const BeforeStep = cucumber.BeforeStep +export const defineParameterType = cucumber.defineParameterType +export const defineStep = cucumber.defineStep +export const Given = cucumber.Given +export const setDefaultTimeout = cucumber.setDefaultTimeout +export const setDefinitionFunctionWrapper = cucumber.setDefinitionFunctionWrapper +export const setWorldConstructor = cucumber.setWorldConstructor +export const Then = cucumber.Then +export const When = cucumber.When + +export const World = cucumber.World + diff --git a/test/formatter_helpers.ts b/test/formatter_helpers.ts index 6b1005de4..8e2825529 100644 --- a/test/formatter_helpers.ts +++ b/test/formatter_helpers.ts @@ -60,7 +60,7 @@ export async function testFormatter({ output += data } const passThrough = new PassThrough() - FormatterBuilder.build(type, { + await FormatterBuilder.build(type, { cwd: '', eventBroadcaster, eventDataCollector, diff --git a/tsconfig.json b/tsconfig.json index b08e6ccef..b9d4b64ad 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,7 +9,7 @@ "resolveJsonModule": true, "sourceMap": true, "inlineSources": true, - "target": "es2017", + "target": "es2018", "typeRoots": [ "./node_modules/@types", "./src/types"