diff --git a/CHANGELOG.md b/CHANGELOG.md index 1901bfbd2..f5f19bfe9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/). Please see [CONTRIBUTING.md](./CONTRIBUTING.md) on how to contribute to Cucumber. ## [Unreleased] +### Added +- Formatters create sub-directory automatically instead of failing ([#2266](https://github.com/cucumber/cucumber-js/pull/2266)) ## [9.0.1] - 2023-03-15 ### Fixed diff --git a/docs/formatters.md b/docs/formatters.md index a8fc802ab..bbb1647b3 100644 --- a/docs/formatters.md +++ b/docs/formatters.md @@ -16,7 +16,7 @@ For each value you provide, `TYPE` should be one of: * A relative path to a local formatter implementation e.g. `./my-customer-formatter.js` * An absolute path to a local formatter implementation in the form of a `file://` URL -If `PATH` is supplied, the formatter prints to the given file, otherwise it prints to `stdout`. +If `PATH` is supplied, the formatter prints to the given file, otherwise it prints to `stdout`. If the path includes directories that do not yet exist they will be created. For example, this configuration would give you a progress bar as you run, plus JSON and HTML report files: @@ -30,7 +30,7 @@ Some notes on specifying Formatters: ## Options -Many formatters, including the built-in ones, support some configurability via options. You can provide this data as an object literal via the `formatOptions` configuration option, like this: +Many formatters, including the built-in ones, support some configuration via options. You can provide this data as an object literal via the `formatOptions` configuration option, like this: - In a configuration file `{ formatOptions: { someOption: true } }` - On the CLI `$ cucumber-js --format-options '{"someOption":true}'` diff --git a/features/formatter_paths.feature b/features/formatter_paths.feature index 886f7a8e3..4bab17b3b 100644 --- a/features/formatter_paths.feature +++ b/features/formatter_paths.feature @@ -33,10 +33,12 @@ Feature: Formatter Paths """ - Scenario: Invalid path - When I run cucumber-js with `-f summary:invalid/summary.txt` - Then it fails - And the error output contains the text: + Scenario: Created relative path + When I run cucumber-js with `-f summary:some/long/path/for/reports/summary.txt` + Then the file "some/long/path/for/reports/summary.txt" has the text: """ - ENOENT + 1 scenario (1 passed) + 1 step (1 passed) + """ + diff --git a/package-lock.json b/package-lock.json index dda8187cc..7dd3bf39b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35,6 +35,7 @@ "lodash.merge": "^4.6.2", "lodash.mergewith": "^4.6.2", "luxon": "3.2.1", + "mkdirp": "^2.1.5", "mz": "^2.7.0", "progress": "^2.0.3", "resolve-pkg": "^2.0.0", @@ -5253,6 +5254,20 @@ "node": ">= 6" } }, + "node_modules/mkdirp": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-2.1.5.tgz", + "integrity": "sha512-jbjfql+shJtAPrFoKxHOXip4xS+kul9W3OzfzzrqueWK2QMGon2bFH2opl6W9EagBThjEz+iysyi/swOoVfB/w==", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/mocha": { "version": "10.2.0", "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.2.0.tgz", @@ -11926,6 +11941,11 @@ "kind-of": "^6.0.3" } }, + "mkdirp": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-2.1.5.tgz", + "integrity": "sha512-jbjfql+shJtAPrFoKxHOXip4xS+kul9W3OzfzzrqueWK2QMGon2bFH2opl6W9EagBThjEz+iysyi/swOoVfB/w==" + }, "mocha": { "version": "10.2.0", "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.2.0.tgz", diff --git a/package.json b/package.json index 959f44040..10030282c 100644 --- a/package.json +++ b/package.json @@ -232,6 +232,7 @@ "lodash.merge": "^4.6.2", "lodash.mergewith": "^4.6.2", "luxon": "3.2.1", + "mkdirp": "^2.1.5", "mz": "^2.7.0", "progress": "^2.0.3", "resolve-pkg": "^2.0.0", diff --git a/src/api/formatters.ts b/src/api/formatters.ts index c1d3e6be1..6055fbb09 100644 --- a/src/api/formatters.ts +++ b/src/api/formatters.ts @@ -9,6 +9,7 @@ import fs from 'mz/fs' import path from 'path' import { IRunOptionsFormats } from './types' import { ILogger } from '../logger' +import { mkdirp } from 'mkdirp' export async function initializeFormatters({ env, @@ -70,12 +71,28 @@ export async function initializeFormatters({ await initializeFormatter(stdout, 'stdout', configuration.stdout) ) - for (const [target, type] of Object.entries(configuration.files)) { - const stream: IFormatterStream = fs.createWriteStream(null, { - fd: await fs.open(path.resolve(cwd, target), 'w'), - }) - formatters.push(await initializeFormatter(stream, target, type)) - } + const streamPromises: Promise[] = [] + + Object.entries(configuration.files).forEach(([target, type]) => { + streamPromises.push( + (async (target, type) => { + const absoluteTarget = path.resolve(cwd, target) + + try { + await mkdirp(path.dirname(absoluteTarget)) + } catch (error) { + logger.warn('Failed to ensure directory for formatter target exists') + } + + const stream: IFormatterStream = fs.createWriteStream(null, { + fd: await fs.open(absoluteTarget, 'w'), + }) + formatters.push(await initializeFormatter(stream, target, type)) + })(target, type) + ) + }) + + await Promise.all(streamPromises) return async function () { await Promise.all(formatters.map(async (f) => await f.finished())) diff --git a/src/formatter/builder.ts b/src/formatter/builder.ts index 264ecaa5d..82e80b581 100644 --- a/src/formatter/builder.ts +++ b/src/formatter/builder.ts @@ -102,13 +102,13 @@ const FormatterBuilder = { descriptor: string, cwd: string ) { - let normalised: URL | string = descriptor + let normalized: URL | string = descriptor if (descriptor.startsWith('.')) { - normalised = pathToFileURL(path.resolve(cwd, descriptor)) + normalized = pathToFileURL(path.resolve(cwd, descriptor)) } else if (descriptor.startsWith('file://')) { - normalised = new URL(descriptor) + normalized = new URL(descriptor) } - let CustomClass = await FormatterBuilder.loadFile(normalised) + let CustomClass = await FormatterBuilder.loadFile(normalized) CustomClass = FormatterBuilder.resolveConstructor(CustomClass) if (doesHaveValue(CustomClass)) { return CustomClass