From 3c93cbfeb4c67b01421ba62dcd26e88f0344f603 Mon Sep 17 00:00:00 2001 From: David Goss Date: Fri, 3 May 2024 09:18:47 +0100 Subject: [PATCH] refactor to allow custom formatter plugins --- features/custom_formatter.feature | 21 ++++++++++++++++++ src/formatter/builder.ts | 14 ++++-------- src/formatter/import_code.ts | 12 ++++++++++ src/formatter/resolve_class_or_plugin.ts | 28 ++++++++++++++++++++++++ src/formatter/resolve_implementation.ts | 12 ++++++++-- 5 files changed, 75 insertions(+), 12 deletions(-) create mode 100644 src/formatter/import_code.ts create mode 100644 src/formatter/resolve_class_or_plugin.ts diff --git a/features/custom_formatter.feature b/features/custom_formatter.feature index 8acb1959f..9666d6b5f 100644 --- a/features/custom_formatter.feature +++ b/features/custom_formatter.feature @@ -120,6 +120,27 @@ Feature: custom formatter """ + Scenario: formatter plugins + Given a file named "simple_formatter.js" with: + """ + module.exports = { + type: 'formatter', + formatter({ on, write }) { + on('message', (message) => { + if (message.testRunFinished) { + write('Test run finished!') + } + }) + } + } + """ + When I run cucumber-js with `--format ./simple_formatter.js` + Then it fails + And it outputs the text: + """ + Test run finished! + """ + Scenario Outline: supported module formats Given a file named "features/step_definitions/cucumber_steps.js" with: """ diff --git a/src/formatter/builder.ts b/src/formatter/builder.ts index 567520396..5ee857101 100644 --- a/src/formatter/builder.ts +++ b/src/formatter/builder.ts @@ -1,7 +1,5 @@ -import path from 'node:path' import { EventEmitter } from 'node:events' import { Writable as WritableStream } from 'node:stream' -import { pathToFileURL } from 'node:url' import { doesHaveValue, doesNotHaveValue } from '../value_checker' import { SupportCodeLibrary } from '../support_code_library_builder/types' import { SnippetInterface } from './step_definition_snippet_builder/snippet_syntax' @@ -10,6 +8,7 @@ import StepDefinitionSnippetBuilder from './step_definition_snippet_builder' import JavascriptSnippetSyntax from './step_definition_snippet_builder/javascript_snippet_syntax' import getColorFns from './get_color_fns' import Formatters from './helpers/formatters' +import { importCode } from './import_code' import Formatter, { FormatOptions, IFormatterCleanupFn, @@ -105,14 +104,9 @@ const FormatterBuilder = { descriptor: string, cwd: string ) { - let normalized: URL | string = descriptor - if (descriptor.startsWith('.')) { - normalized = pathToFileURL(path.resolve(cwd, descriptor)) - } else if (descriptor.startsWith('file://')) { - normalized = new URL(descriptor) - } - let CustomClass = await FormatterBuilder.loadFile(normalized) - CustomClass = FormatterBuilder.resolveConstructor(CustomClass) + const CustomClass = FormatterBuilder.resolveConstructor( + await importCode(descriptor, cwd) + ) if (doesHaveValue(CustomClass)) { return CustomClass } else { diff --git a/src/formatter/import_code.ts b/src/formatter/import_code.ts new file mode 100644 index 000000000..25e4515f2 --- /dev/null +++ b/src/formatter/import_code.ts @@ -0,0 +1,12 @@ +import { pathToFileURL } from 'node:url' +import path from 'node:path' + +export async function importCode(specifier: string, cwd: string): Promise { + let normalized: URL | string = specifier + if (specifier.startsWith('.')) { + normalized = pathToFileURL(path.resolve(cwd, specifier)) + } else if (specifier.startsWith('file://')) { + normalized = new URL(specifier) + } + return await import(normalized.toString()) +} diff --git a/src/formatter/resolve_class_or_plugin.ts b/src/formatter/resolve_class_or_plugin.ts new file mode 100644 index 000000000..939be3312 --- /dev/null +++ b/src/formatter/resolve_class_or_plugin.ts @@ -0,0 +1,28 @@ +import { doesNotHaveValue } from '../value_checker' + +export function resolveClassOrPlugin(imported: any) { + if (doesNotHaveValue(imported)) { + return null + } + if (typeof imported === 'function') { + return imported + } else if (typeof imported === 'object' && imported.type === 'formatter') { + return imported + } else if ( + typeof imported === 'object' && + typeof imported.default === 'function' + ) { + return imported.default + } else if ( + typeof imported.default === 'object' && + imported.default.type === 'formatter' + ) { + return imported.default + } else if ( + typeof imported.default === 'object' && + typeof imported.default.default === 'function' + ) { + return imported.default.default + } + return null +} diff --git a/src/formatter/resolve_implementation.ts b/src/formatter/resolve_implementation.ts index f9c98cee6..edf708747 100644 --- a/src/formatter/resolve_implementation.ts +++ b/src/formatter/resolve_implementation.ts @@ -1,5 +1,6 @@ -import FormatterBuilder from './builder' import builtin from './builtin' +import { importCode } from './import_code' +import { resolveClassOrPlugin } from './resolve_class_or_plugin' import { FormatterImplementation } from './index' export async function resolveImplementation( @@ -9,6 +10,13 @@ export async function resolveImplementation( if (builtin[specifier]) { return builtin[specifier] } else { - return await FormatterBuilder.loadCustomClass('formatter', specifier, cwd) + const imported = await importCode(specifier, cwd) + const resolved = resolveClassOrPlugin(imported) + if (!resolved) { + throw new Error( + `Custom formatter (${specifier}) does not export a function/class` + ) + } + return resolved } }