Skip to content

Commit c35f001

Browse files
add ESM support (take 2) (#1649)
* Revert "temporarily revert ESM change (#1647)" This reverts commit 084c1f2. * 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 <aurelien.reeves@smartbear.com> * Update features/esm.feature Co-authored-by: Aurélien Reeves <aurelien.reeves@smartbear.com> * 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 <aurelien.reeves@smartbear.com>
1 parent 6e958f1 commit c35f001

24 files changed

Lines changed: 313 additions & 75 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ See the [migration guide](./docs/migration.md) for details of how to migrate fro
1919

2020
### Added
2121

22+
* Add support for user code as native ES modules
2223
* `BeforeStep` and `AfterStep` hook functions now have access to the `pickleStep` in their argument object.
2324
* `--config` option to the CLI. It allows you to specify a configuration file other than `cucumber.js`.
2425
See [docs/profiles.md](./docs/profiles.md#using-another-file-than-cucumberjs) for more info.

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ The following documentation is for master. See below the documentation for older
6767
* [Attachments](/docs/support_files/attachments.md)
6868
* [API Reference](/docs/support_files/api_reference.md)
6969
* Guides
70+
* [ES Modules](./docs/esm.md)
7071
* [Running in parallel](./docs/parallel.md)
7172
* [Retrying failing scenarios](./docs/retry.md)
7273
* [Profiles](./docs/profiles.md)

dependency-lint.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ requiredModules:
4444
- 'dist/**/*'
4545
- 'lib/**/*'
4646
- 'node_modules/**/*'
47+
- 'src/importer.js'
4748
- 'tmp/**/*'
4849
root: '**/*.{js,ts}'
4950
stripLoaders: false

docs/esm.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# ES Modules (experimental)
2+
3+
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.
4+
5+
Example (adapted from [our original example](./nodejs_example.md)):
6+
7+
```javascript
8+
// features/support/steps.mjs
9+
import { Given, When, Then } from '@cucumber/cucumber'
10+
import { strict as assert } from 'assert'
11+
12+
Given('a variable set to {int}', function (number) {
13+
this.setTo(number)
14+
})
15+
16+
When('I increment the variable by {int}', function (number) {
17+
this.incrementBy(number)
18+
})
19+
20+
Then('the variable should contain {int}', function (number) {
21+
assert.equal(this.variable, number)
22+
})
23+
```
24+
25+
As well as support code, these things can also be in ES modules syntax:
26+
27+
- Custom formatters
28+
- Custom snippets
29+
30+
You can use ES modules selectively/incrementally - so you can have a mixture of CommonJS and ESM in the same project.
31+
32+
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).
33+
34+
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.

features/direct_imports.feature

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,20 @@ Feature: Core feature elements execution using direct imports
4444
"""
4545
features/step_definitions/cucumber_steps.js:3
4646
"""
47+
48+
Scenario: deep imports don't break everything
49+
Given a file named "features/a.feature" with:
50+
"""
51+
Feature: some feature
52+
Scenario: some scenario
53+
Given a step passes
54+
"""
55+
And a file named "features/step_definitions/cucumber_steps.js" with:
56+
"""
57+
const {Given} = require('@cucumber/cucumber')
58+
const TestCaseHookDefinition = require('@cucumber/cucumber/lib/models/test_case_hook_definition')
59+
60+
Given(/^a step passes$/, function() {});
61+
"""
62+
When I run cucumber-js
63+
Then it passes

features/esm.feature

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
@esm
2+
Feature: ES modules support
3+
4+
cucumber-js works with native ES modules
5+
6+
Scenario Outline: native module syntax works in support code, formatters and snippets
7+
Given a file named "features/a.feature" with:
8+
"""
9+
Feature:
10+
Scenario: one
11+
Given a step passes
12+
13+
Scenario: two
14+
Given a step passes
15+
"""
16+
And a file named "features/step_definitions/cucumber_steps.js" with:
17+
"""
18+
import {Given} from '@cucumber/cucumber'
19+
20+
Given(/^a step passes$/, function() {});
21+
"""
22+
And a file named "custom-formatter.js" with:
23+
"""
24+
import {SummaryFormatter} from '@cucumber/cucumber'
25+
26+
export default class CustomFormatter extends SummaryFormatter {}
27+
"""
28+
And a file named "custom-snippet-syntax.js" with:
29+
"""
30+
export default class CustomSnippetSyntax {
31+
build(opts) {
32+
return 'hello world'
33+
}
34+
}
35+
"""
36+
And a file named "cucumber.cjs" with:
37+
"""
38+
module.exports = {
39+
'default': '--format summary'
40+
}
41+
"""
42+
When I run cucumber-js with `<options> --format ./custom-formatter.js --format-options '{"snippetSyntax": "./custom-snippet-syntax.js"}' <args>`
43+
Then it passes
44+
Examples:
45+
| args |
46+
| |
47+
| --parallel 2 |
48+
49+
Scenario: .mjs support code files are matched by default
50+
Given a file named "features/a.feature" with:
51+
"""
52+
Feature:
53+
Scenario:
54+
Given a step passes
55+
"""
56+
And a file named "features/step_definitions/cucumber_steps.mjs" with:
57+
"""
58+
import {Given} from '@cucumber/cucumber'
59+
60+
Given(/^a step passes$/, function() {});
61+
"""
62+
When I run cucumber-js
63+
Then it passes

features/profiles.feature

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,3 +91,7 @@ Feature: default command line arguments
9191
| OPT |
9292
| -c |
9393
| --config |
94+
95+
Scenario: specifying a configuration file that doesn't exist
96+
When I run cucumber-js with `--config doesntexist.js`
97+
Then it fails

features/support/hooks.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ Before('@debug', function (this: World) {
1313
this.debug = true
1414
})
1515

16-
Before('@spawn', function (this: World) {
16+
Before('@spawn or @esm', function (this: World) {
1717
this.spawn = true
1818
})
1919

@@ -43,6 +43,13 @@ Before(function (
4343
this.localExecutablePath = path.join(projectPath, 'bin', 'cucumber-js')
4444
})
4545

46+
Before('@esm', function (this: World) {
47+
fsExtra.writeJSONSync(path.join(this.tmpDir, 'package.json'), {
48+
name: 'feature-test-pickle',
49+
type: 'module',
50+
})
51+
})
52+
4653
Before('@global-install', function (this: World) {
4754
const tmpObject = tmp.dirSync({ unsafeCleanup: true })
4855

package-lock.json

Lines changed: 4 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,15 @@
163163
"lib": "./lib"
164164
},
165165
"main": "./lib/index.js",
166+
"exports": {
167+
".": {
168+
"import": "./lib/wrapper.mjs",
169+
"require": "./lib/index.js"
170+
},
171+
"./lib/*": {
172+
"require": "./lib/*.js"
173+
}
174+
},
166175
"types": "./lib/index.d.ts",
167176
"engines": {
168177
"node": ">=12"
@@ -180,7 +189,6 @@
180189
"cli-table3": "^0.6.0",
181190
"colors": "^1.4.0",
182191
"commander": "^8.0.0",
183-
"create-require": "^1.1.1",
184192
"duration": "^0.2.2",
185193
"durations": "^3.4.2",
186194
"figures": "^3.2.0",
@@ -252,7 +260,7 @@
252260
"typescript": "4.4.2"
253261
},
254262
"scripts": {
255-
"build-local": "tsc --build tsconfig.node.json",
263+
"build-local": "tsc --build tsconfig.node.json && cp src/importer.js lib/ && cp src/wrapper.mjs lib/",
256264
"cck-test": "mocha 'compatibility/**/*_spec.ts'",
257265
"feature-test": "node ./bin/cucumber-js",
258266
"html-formatter": "node ./bin/cucumber-js --profile htmlFormatter",

0 commit comments

Comments
 (0)