Skip to content

Commit

Permalink
Revert "temporarily revert ESM change (#1647)"
Browse files Browse the repository at this point in the history
This reverts commit 084c1f2.
  • Loading branch information
davidjgoss committed Apr 21, 2021
1 parent 02c0245 commit d268a8d
Show file tree
Hide file tree
Showing 20 changed files with 346 additions and 68 deletions.
1 change: 1 addition & 0 deletions dependency-lint.yml
Expand Up @@ -43,6 +43,7 @@ requiredModules:
- 'dist/**/*'
- 'lib/**/*'
- 'node_modules/**/*'
- 'src/importers.js'
- 'tmp/**/*'
root: '**/*.{js,ts}'
stripLoaders: false
Expand Down
16 changes: 16 additions & 0 deletions docs/cli.md
Expand Up @@ -81,6 +81,22 @@ You can pass in format options with `--format-options <JSON>`. The JSON string m

* Suggested use: add with profiles so you can define an object and use `JSON.stringify` instead of writing `JSON` manually.

## ES Modules (experimental) (Node.js 12+)

You can optionally write your support code (steps, hooks, etc) with native ES modules syntax - i.e. using `import` and `export` statements without transpiling.

To enable this, run with the `--esm` flag.

This will also expand the default glob for support files to include the `.mjs` file extension.

As well as support code, these things can also be in ES modules syntax:

- Custom formatters
- Custom snippets
- Your `cucumber.js` config file

You can use ES modules selectively/incrementally - the module loading strategy that the `--esm` flag activates supports both ES modules and CommonJS.

## Colors

Colors can be disabled with `--format-options '{"colorsEnabled": false}'`
Expand Down
80 changes: 80 additions & 0 deletions features/esm.feature
@@ -0,0 +1,80 @@
Feature: ES modules support

cucumber-js works with native ES modules, via a Cli flag `--esm`

@esm
Scenario Outline: native module syntax works when using --esm
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 "cucumber.js" with:
"""
export default {
'default': '--format message:messages.ndjson',
}
"""
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'
}
}
"""
When I run cucumber-js with `<options> --format ./custom-formatter.js --format-options '{"snippetSyntax": "./custom-snippet-syntax.js"}'`
Then it passes
Examples:
| options |
| --esm |
| --esm --parallel 2 |

@esm
Scenario: .mjs support code files are matched by default when using --esm
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 with `--esm`
Then it passes

Scenario: native module syntax doesn't work without --esm
Given a file named "features/a.feature" with:
"""
Feature:
Scenario:
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() {});
"""
When I run cucumber-js
Then it fails
14 changes: 13 additions & 1 deletion features/support/hooks.ts
Expand Up @@ -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
})

Expand Down Expand Up @@ -43,6 +43,18 @@ Before(function (
this.localExecutablePath = path.join(projectPath, 'bin', 'cucumber-js')
})

Before('@esm', function (this: World) {
const [majorVersion] = process.versions.node.split('.')
if (Number(majorVersion) < 12) {
return 'skipped'
}
fsExtra.writeJSONSync(path.join(this.tmpDir, 'package.json'), {
name: 'feature-test-pickle',
type: 'module',
})
return undefined
})

Before('@global-install', function (this: World) {
const tmpObject = tmp.dirSync({ unsafeCleanup: true })

Expand Down
7 changes: 5 additions & 2 deletions package.json
Expand Up @@ -163,6 +163,10 @@
"lib": "./lib"
},
"main": "./lib/index.js",
"exports": {
"import": "./lib/wrapper.mjs",
"require": "./lib/index.js"
},
"types": "./lib/index.d.ts",
"engines": {
"node": ">=10"
Expand All @@ -182,7 +186,6 @@
"cli-table3": "^0.6.0",
"colors": "^1.4.0",
"commander": "^7.0.0",
"create-require": "^1.1.1",
"duration": "^0.2.2",
"durations": "^3.4.2",
"figures": "^3.2.0",
Expand Down Expand Up @@ -257,7 +260,7 @@
"typescript": "4.2.3"
},
"scripts": {
"build-local": "tsc -p tsconfig.node.json",
"build-local": "tsc -p tsconfig.node.json && cp src/importers.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",
Expand Down
2 changes: 2 additions & 0 deletions src/cli/argv_parser.ts
Expand Up @@ -22,6 +22,7 @@ export interface IParsedArgvFormatOptions {
export interface IParsedArgvOptions {
backtrace: boolean
dryRun: boolean
esm: boolean
exit: boolean
failFast: boolean
format: string[]
Expand Down Expand Up @@ -112,6 +113,7 @@ const ArgvParser = {
'invoke formatters without executing steps',
false
)
.option('--esm', 'import support code via ES module imports', false)
.option(
'--exit',
'force shutdown of the event loop when the test run has finished: cucumber will call process.exit',
Expand Down
4 changes: 3 additions & 1 deletion src/cli/configuration_builder.ts
Expand Up @@ -19,6 +19,7 @@ export interface IConfigurationFormat {
}

export interface IConfiguration {
esm: boolean
featureDefaultLanguage: string
featurePaths: string[]
formats: IConfigurationFormat[]
Expand Down Expand Up @@ -80,10 +81,11 @@ export default class ConfigurationBuilder {
}
supportCodePaths = await this.expandPaths(
unexpandedSupportCodePaths,
'.js'
this.options.esm ? '.@(js|mjs)' : '.js'
)
}
return {
esm: this.options.esm,
featureDefaultLanguage: this.options.language,
featurePaths,
formats: this.getFormats(),
Expand Down
91 changes: 72 additions & 19 deletions src/cli/configuration_builder_spec.ts
Expand Up @@ -29,6 +29,7 @@ describe('Configuration', () => {

// Assert
expect(result).to.eql({
esm: false,
featureDefaultLanguage: 'en',
featurePaths: [],
formatOptions: {},
Expand Down Expand Up @@ -65,27 +66,79 @@ describe('Configuration', () => {
})

describe('path to a feature', () => {
it('returns the appropriate feature and support code paths', async function () {
// Arrange
const cwd = await buildTestWorkingDirectory()
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 argv = baseArgv.concat([relativeFeaturePath])
describe('without esm', () => {
it('returns the appropriate feature and support code paths', async function () {
// Arrange
const cwd = await buildTestWorkingDirectory()
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 argv = baseArgv.concat([relativeFeaturePath])

// Act
const {
featurePaths,
pickleFilterOptions,
supportCodePaths,
} = await ConfigurationBuilder.build({ argv, cwd })

// Assert
expect(featurePaths).to.eql([featurePath])
expect(pickleFilterOptions.featurePaths).to.eql([relativeFeaturePath])
expect(supportCodePaths).to.eql([supportCodePath])
})
})

// Act
const {
featurePaths,
pickleFilterOptions,
supportCodePaths,
} = await ConfigurationBuilder.build({ argv, cwd })
describe('with esm and js support files', () => {
it('returns the appropriate feature and support code paths', async function () {
// Arrange
const cwd = await buildTestWorkingDirectory()
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 argv = baseArgv.concat([relativeFeaturePath, '--esm'])

// Act
const {
featurePaths,
pickleFilterOptions,
supportCodePaths,
} = await ConfigurationBuilder.build({ argv, cwd })

// Assert
expect(featurePaths).to.eql([featurePath])
expect(pickleFilterOptions.featurePaths).to.eql([relativeFeaturePath])
expect(supportCodePaths).to.eql([supportCodePath])
})
})

// Assert
expect(featurePaths).to.eql([featurePath])
expect(pickleFilterOptions.featurePaths).to.eql([relativeFeaturePath])
expect(supportCodePaths).to.eql([supportCodePath])
describe('with esm and mjs support files', () => {
it('returns the appropriate feature and support code paths', async function () {
// Arrange
const cwd = await buildTestWorkingDirectory()
const relativeFeaturePath = path.join('features', 'a.feature')
const featurePath = path.join(cwd, relativeFeaturePath)
await fsExtra.outputFile(featurePath, '')
const supportCodePath = path.join(cwd, 'features', 'a.mjs')
await fsExtra.outputFile(supportCodePath, '')
const argv = baseArgv.concat([relativeFeaturePath, '--esm'])

// Act
const {
featurePaths,
pickleFilterOptions,
supportCodePaths,
} = await ConfigurationBuilder.build({ argv, cwd })

// Assert
expect(featurePaths).to.eql([featurePath])
expect(pickleFilterOptions.featurePaths).to.eql([relativeFeaturePath])
expect(supportCodePaths).to.eql([supportCodePath])
})
})
})

Expand Down
8 changes: 7 additions & 1 deletion src/cli/helpers.ts
Expand Up @@ -16,6 +16,9 @@ import TestCaseHookDefinition from '../models/test_case_hook_definition'
import TestRunHookDefinition from '../models/test_run_hook_definition'
import { builtinParameterTypes } from '../support_code_library_builder'

// eslint-disable-next-line @typescript-eslint/no-var-requires
const importers = require('../importers')

const StepDefinitionPatternType =
messages.StepDefinition.StepDefinitionPattern.StepDefinitionPatternType

Expand All @@ -29,8 +32,11 @@ export async function getExpandedArgv({
cwd,
}: IGetExpandedArgvRequest): Promise<string[]> {
const { options } = ArgvParser.parse(argv)
const importer = options.esm ? importers.esm : importers.legacy
let fullArgv = argv
const profileArgv = await new ProfileLoader(cwd).getArgv(options.profile)
const profileArgv = await new ProfileLoader(cwd, importer).getArgv(
options.profile
)
if (profileArgv.length > 0) {
fullArgv = _.concat(argv.slice(0, 2), profileArgv, argv.slice(2))
}
Expand Down

0 comments on commit d268a8d

Please sign in to comment.