Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

esm: use specific option for import vs require for support code #1931

Merged
merged 17 commits into from
Feb 26, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 5 additions & 2 deletions compatibility/cck_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,11 @@ describe('Cucumber Compatibility Kit', () => {
paths: [`${CCK_FEATURES_PATH}/${suiteName}/${suiteName}${extension}`],
},
support: {
transpileWith: ['ts-node/register'],
paths: [`${CCK_IMPLEMENTATIONS_PATH}/${suiteName}/${suiteName}.ts`],
requireModules: ['ts-node/register'],
requirePaths: [
`${CCK_IMPLEMENTATIONS_PATH}/${suiteName}/${suiteName}.ts`,
],
importPaths: [],
},
formats: {
stdout: 'message',
Expand Down
19 changes: 12 additions & 7 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,19 +26,24 @@ needs to be required in your support files and globally installed modules cannot
* To escape special regex characters in scenario name, use backslash e.g., `\(Scenario Name\)`
* Use [Tags](#tags)

## Requiring support files
## Loading support files

By default, the following files are required:
By default, the following files are loaded:
* If the features live in a `features` directory (at any level)
* `features/**/*.js`
* `features/**/*.(js|mjs)`
* Otherwise
* `<DIR>/**/*.js` for each directory containing the selected features
* `<DIR>/**/*.(js|mjs)` for each directory containing the selected features

Alternatively, you can use `--require <GLOB|DIR|FILE>` to explicitly require support files before executing the features. Uses [glob](https://github.com/isaacs/node-glob) patterns.
With the defaults described above, `.js` files are loaded via `require()`, whereas `.mjs` files are loaded via `import()`.

This option may be used multiple times in order to e.g. require files from several different locations.
Alternatively, you can use either or both of these options to explicitly load support files before executing the features:

_Note that once you specify any `--require` options, the defaults described above are no longer applied._
- `--require <GLOB|DIR|FILE>` - loads via `require()` (legacy)
- `--import <GLOB|DIR|FILE>` - loads via `import()`

Both options use [glob](https://github.com/isaacs/node-glob) patterns and may be used multiple times in order to e.g. load files from several different locations.

_Note that once you specify any `--require` or `--import` options, the defaults described above are no longer applied._
davidjgoss marked this conversation as resolved.
Show resolved Hide resolved

## Formats

Expand Down
4 changes: 3 additions & 1 deletion docs/esm.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# ES Modules (experimental)
aurelien-reeves marked this conversation as resolved.
Show resolved Hide resolved

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

If your support code is written as ESM, you'll need to use the `--import` option to specify your files, rather than the `--require` option.

**Important**: please note that your configuration file referenced for [Profiles](./profiles.md) - aka `cucumber.js` file - must remain a CommonJS file. 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.

Expand Down
41 changes: 31 additions & 10 deletions features/esm.feature
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ Feature: ES modules support

cucumber-js works with native ES modules

Scenario Outline: native module syntax works in support code, formatters and snippets
Scenario: native module syntax works in support code, formatters and snippets
Given a file named "features/a.feature" with:
"""
Feature:
Expand Down Expand Up @@ -39,18 +39,38 @@ Feature: ES modules support
'default': '--format summary'
}
"""
When I run cucumber-js with `<options> --format ./custom-formatter.js --format-options '{"snippetSyntax": "./custom-snippet-syntax.js"}' <args>`
Then it passes
Examples:
| args |
| |
| --parallel 2 |
When I run cucumber-js with `--import features/**/*.js --format ./custom-formatter.js --format-options '{"snippetSyntax": "./custom-snippet-syntax.js"}'`
Then it runs 2 scenarios
And it passes

Scenario: .mjs support code files are matched by default
Scenario: native modules work with parallel runtime
Given a file named "features/a.feature" with:
"""
Feature:
Scenario:
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() {});
"""
When I run cucumber-js with `--import features/**/*.js' --parallel 2`
Then it runs 2 scenarios
And it passes

Scenario: .mjs support code files are discovered automatically if no requires or imports specified
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.mjs" with:
Expand All @@ -60,4 +80,5 @@ Feature: ES modules support
Given(/^a step passes$/, function() {});
"""
When I run cucumber-js
Then it passes
Then it runs 2 scenarios
And it passes
7 changes: 7 additions & 0 deletions src/cli/argv_parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export interface IParsedArgvOptions {
formatOptions: IParsedArgvFormatOptions
i18nKeywords: string
i18nLanguages: boolean
import: string[]
language: string
name: string[]
order: PickleOrder
Expand Down Expand Up @@ -140,6 +141,12 @@ const ArgvParser = {
''
)
.option('--i18n-languages', 'list languages', false)
.option(
'--import <GLOB|DIR|FILE>',
'import files before executing features (repeatable)',
ArgvParser.collect,
[]
)
.option(
'--language <ISO 639-1>',
'provide the default language for feature files',
Expand Down
5 changes: 3 additions & 2 deletions src/cli/configuration_builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ export async function buildConfiguration(
order: options.order,
},
support: {
transpileWith: options.requireModule,
paths: options.require,
requireModules: options.requireModule,
requirePaths: options.require,
importPaths: options.import,
},
runtime: {
dryRun: options.dryRun,
Expand Down
5 changes: 3 additions & 2 deletions src/cli/configuration_builder_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,9 @@ describe('buildConfiguration', () => {
tagExpression: '',
},
support: {
paths: [],
transpileWith: [],
requireModules: [],
requirePaths: [],
importPaths: [],
},
})
})
Expand Down
8 changes: 0 additions & 8 deletions src/cli/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,14 +107,6 @@ export function orderPickleIds(pickleIds: string[], order: PickleOrder): void {
}
}

export function isJavaScript(filePath: string): boolean {
return (
filePath.endsWith('.js') ||
filePath.endsWith('.mjs') ||
filePath.endsWith('.cjs')
)
}

export async function emitMetaMessage(
eventBroadcaster: EventEmitter,
env: NodeJS.ProcessEnv
Expand Down
11 changes: 0 additions & 11 deletions src/cli/helpers_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { expect } from 'chai'
import {
emitMetaMessage,
emitSupportCodeMessages,
isJavaScript,
parseGherkinMessageStream,
PickleOrder,
} from './helpers'
Expand Down Expand Up @@ -88,16 +87,6 @@ function testEmitSupportCodeMessages(
}

describe('helpers', () => {
describe('isJavaScript', () => {
it('should identify a native javascript file path that can be `import()`ed', () => {
expect(isJavaScript('foo/bar.js')).to.be.true()
expect(isJavaScript('foo/bar.mjs')).to.be.true()
expect(isJavaScript('foo/bar.cjs')).to.be.true()
expect(isJavaScript('foo/bar.ts')).to.be.false()
expect(isJavaScript('foo/bar.coffee')).to.be.false()
})
})

describe('emitMetaMessage', () => {
it('emits a meta message', async () => {
const envelopes: messages.Envelope[] = []
Expand Down
5 changes: 3 additions & 2 deletions src/configuration/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ export interface IRunConfiguration {
order?: PickleOrder
}
support: {
transpileWith: string[]
paths: string[]
requireModules: string[]
requirePaths: string[]
importPaths: string[]
}
runtime?: Partial<IRuntimeOptions> & { parallel?: number }
formats?: IFormatterConfiguration
Expand Down
46 changes: 37 additions & 9 deletions src/run/paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ export async function resolvePaths(
): Promise<{
unexpandedFeaturePaths: string[]
featurePaths: string[]
supportCodePaths: string[]
requirePaths: string[]
importPaths: string[]
}> {
const unexpandedFeaturePaths = await getUnexpandedFeaturePaths(
cwd,
Expand All @@ -20,19 +21,17 @@ export async function resolvePaths(
cwd,
unexpandedFeaturePaths
)
let unexpandedSupportCodePaths = configuration.support.paths ?? []
if (unexpandedSupportCodePaths.length === 0) {
unexpandedSupportCodePaths = getFeatureDirectoryPaths(cwd, featurePaths)
}
const supportCodePaths = await expandPaths(
const { requirePaths, importPaths } = await deriveSupportPaths(
cwd,
unexpandedSupportCodePaths,
'.@(js|mjs)'
featurePaths,
configuration.support.requirePaths,
configuration.support.importPaths
)
return {
unexpandedFeaturePaths,
featurePaths,
supportCodePaths,
requirePaths,
importPaths,
}
}

Expand Down Expand Up @@ -114,3 +113,32 @@ async function expandFeaturePaths(
featurePaths = [...new Set(featurePaths)] // Deduplicate the feature files
return await expandPaths(cwd, featurePaths, '.feature')
}

async function deriveSupportPaths(
cwd: string,
featurePaths: string[],
unexpandedRequirePaths: string[],
unexpandedImportPaths: string[]
): Promise<{
requirePaths: string[]
importPaths: string[]
}> {
if (
unexpandedRequirePaths.length === 0 &&
unexpandedImportPaths.length === 0
) {
const defaultPaths = getFeatureDirectoryPaths(cwd, featurePaths)
const requirePaths = await expandPaths(cwd, defaultPaths, '.js')
const importPaths = await expandPaths(cwd, defaultPaths, '.mjs')
davidjgoss marked this conversation as resolved.
Show resolved Hide resolved
return { requirePaths, importPaths }
}
const requirePaths =
unexpandedRequirePaths.length > 0
? await expandPaths(cwd, unexpandedRequirePaths, '.js')
: []
const importPaths =
unexpandedImportPaths.length > 0
? await expandPaths(cwd, unexpandedImportPaths, '.@(js|cjs|mjs)')
: []
return { requirePaths, importPaths }
}