From c97252d7ba74b45aa7a6b098e3047c118f8fcf29 Mon Sep 17 00:00:00 2001 From: David Goss Date: Wed, 6 Apr 2022 09:18:18 +0100 Subject: [PATCH] fix: try require first for custom formatters so transpiled ones work (#1985) * try require first when loadinh formatters * prove that transpiled ones work too * simplify logic to resolve ctor * update CHANGELOG.md * extend feature test to prove transpiled formatters work --- CHANGELOG.md | 1 + features/custom_formatter.feature | 1 + src/formatter/builder.ts | 34 +++++++++---- src/formatter/builder_spec.ts | 72 ++++++++++++---------------- src/formatter/fixtures/typescript.ts | 1 + 5 files changed, 59 insertions(+), 50 deletions(-) create mode 100644 src/formatter/fixtures/typescript.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index dfe08b342..0d7c5dbfa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ Please see [CONTRIBUTING.md](https://github.com/cucumber/cucumber/blob/master/CO ## [Unreleased] ### Fixed - Allow `file://` URLs to be used as formatter/snippet paths in options ([#1963](https://github.com/cucumber/cucumber-js/pull/1963) [#1920](https://github.com/cucumber/cucumber-js/issues/1920)) +- Allow custom formatters to rely on `--require-module` transpilers ([#1985](https://github.com/cucumber/cucumber-js/pull/1985)) ### Changed - Emit a warning when using a Node.js version that's untested with Cucumber ([#1959](https://github.com/cucumber/cucumber-js/pull/1959)) diff --git a/features/custom_formatter.feature b/features/custom_formatter.feature index daff0b374..22cd33e7c 100644 --- a/features/custom_formatter.feature +++ b/features/custom_formatter.feature @@ -139,6 +139,7 @@ Feature: custom formatter Then it passes Examples: | EXT | IMPORT_STATEMENT | EXPORT_STATEMENT | + | .ts | import {Formatter} from '@cucumber/cucumber' | export default CustomFormatter | | .mjs | import {Formatter} from '@cucumber/cucumber' | export default CustomFormatter | | .js | const {Formatter} = require('@cucumber/cucumber') | module.exports = CustomFormatter | | .js | const {Formatter} = require('@cucumber/cucumber') | exports.default = CustomFormatter | diff --git a/src/formatter/builder.ts b/src/formatter/builder.ts index 3d906acd9..ac0419efd 100644 --- a/src/formatter/builder.ts +++ b/src/formatter/builder.ts @@ -13,7 +13,7 @@ import { EventEmitter } from 'events' import EventDataCollector from './helpers/event_data_collector' import { Writable as WritableStream } from 'stream' import { SnippetInterface } from './step_definition_snippet_builder/snippet_syntax' -import { pathToFileURL } from 'url' +import { fileURLToPath, pathToFileURL } from 'url' import Formatters from './helpers/formatters' // eslint-disable-next-line @typescript-eslint/no-var-requires const { importer } = require('../importer') @@ -97,9 +97,13 @@ const FormatterBuilder = { descriptor: string, cwd: string ) { - let CustomClass = descriptor.startsWith(`.`) - ? await importer(pathToFileURL(path.resolve(cwd, descriptor))) - : await importer(descriptor) + let normalised: URL | string = descriptor + if (descriptor.startsWith('.')) { + normalised = pathToFileURL(path.resolve(cwd, descriptor)) + } else if (descriptor.startsWith('file://')) { + normalised = new URL(descriptor) + } + let CustomClass = await FormatterBuilder.loadFile(normalised) CustomClass = FormatterBuilder.resolveConstructor(CustomClass) if (doesHaveValue(CustomClass)) { return CustomClass @@ -110,6 +114,23 @@ const FormatterBuilder = { } }, + async loadFile(urlOrName: URL | string) { + let result + try { + // eslint-disable-next-line @typescript-eslint/no-var-requires + result = require(typeof urlOrName === 'string' + ? urlOrName + : fileURLToPath(urlOrName)) + } catch (error) { + if (error.code === 'ERR_REQUIRE_ESM') { + result = await importer(urlOrName) + } else { + throw error + } + } + return result + }, + resolveConstructor(ImportedCode: any) { if (doesNotHaveValue(ImportedCode)) { return null @@ -121,11 +142,6 @@ const FormatterBuilder = { typeof ImportedCode.default === 'function' ) { return ImportedCode.default - } else if ( - typeof ImportedCode.default === 'object' && - typeof ImportedCode.default.default === 'function' - ) { - return ImportedCode.default.default } return null }, diff --git a/src/formatter/builder_spec.ts b/src/formatter/builder_spec.ts index a6b7ea54d..215575dfc 100644 --- a/src/formatter/builder_spec.ts +++ b/src/formatter/builder_spec.ts @@ -4,46 +4,36 @@ import { pathToFileURL } from 'url' import path from 'path' describe('custom class loading', () => { - it('should handle a file:// url', async () => { - const fileUrl = pathToFileURL( - path.resolve(__dirname, './fixtures/module_dot_exports.cjs') - ).toString() - const CustomClass = await FormatterBuilder.loadCustomClass( - 'formatter', - fileUrl, - __dirname - ) - - expect(typeof CustomClass).to.eq('function') - }) - - it('should handle cjs module.exports', async () => { - const CustomClass = await FormatterBuilder.loadCustomClass( - 'formatter', - './fixtures/module_dot_exports.cjs', - __dirname - ) - - expect(typeof CustomClass).to.eq('function') - }) - - it('should handle cjs exports.default', async () => { - const CustomClass = await FormatterBuilder.loadCustomClass( - 'formatter', - './fixtures/exports_dot_default.cjs', - __dirname - ) - - expect(typeof CustomClass).to.eq('function') - }) - - it('should handle esm default export', async () => { - const CustomClass = await FormatterBuilder.loadCustomClass( - 'formatter', - './fixtures/esm.mjs', - __dirname - ) - - expect(typeof CustomClass).to.eq('function') + const varieties = [ + 'esm.mjs', + 'exports_dot_default.cjs', + 'module_dot_exports.cjs', + 'typescript.ts', + ] + varieties.forEach((filename) => { + describe(filename, () => { + it('should handle a relative path', async () => { + const CustomClass = await FormatterBuilder.loadCustomClass( + 'formatter', + `./fixtures/${filename}`, + __dirname + ) + + expect(typeof CustomClass).to.eq('function') + }) + + it('should handle a file:// url', async () => { + const fileUrl = pathToFileURL( + path.resolve(__dirname, `./fixtures/${filename}`) + ).toString() + const CustomClass = await FormatterBuilder.loadCustomClass( + 'formatter', + fileUrl, + __dirname + ) + + expect(typeof CustomClass).to.eq('function') + }) + }) }) }) diff --git a/src/formatter/fixtures/typescript.ts b/src/formatter/fixtures/typescript.ts new file mode 100644 index 000000000..2174f613f --- /dev/null +++ b/src/formatter/fixtures/typescript.ts @@ -0,0 +1 @@ +export default class Formatter {}