diff --git a/CHANGELOG.md b/CHANGELOG.md index 363637700..deed8b5a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/). Please see [CONTRIBUTING.md](./CONTRIBUTING.md) on how to contribute to Cucumber. ## [Unreleased] +### Added +- Allow loading config files in TypeScript format ([#2709](https://github.com/cucumber/cucumber-js/pull/2709)) + ### Changed - Compress report content with gzip before publishing ([#2687](https://github.com/cucumber/cucumber-js/pull/2687)) diff --git a/docs/configuration.md b/docs/configuration.md index cb71470fc..a00ab876a 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -71,11 +71,18 @@ module.exports = { (If you're wondering why the configuration sits within a "default" property, that's to allow for [Profiles](./profiles.md).) -### Type checking +### TypeScript -If you want to type check your configuration, we export two types that can help with that: -- `IProfiles` represents the dictionary of profile names to configuration objects exported for CommonJS -- `IConfiguration` represents a single configuration object exported with named exports for ESM (`Partial` will be more useful in practise) +You can also write your configuration file in TypeScript, with a `.ts`, `.mts` or `.cts` extension. These files are loaded with [Node.js built-in TypeScript support](https://nodejs.org/api/typescript.html), which has several caveats and limitations, mostly that your `tsconfig.json` won't be honoured and that you need to be explicit about type imports. Here's an example: + +```typescript +import type { IConfiguration } from '@cucumber/cucumber' + +export default { + parallel: 2, + format: ['html:cucumber-report.html'] +} satisfies Partial +``` ## Options diff --git a/package-lock.json b/package-lock.json index 10374d8ab..0c11a6c9f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -169,7 +169,6 @@ "integrity": "sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", @@ -454,6 +453,7 @@ "resolved": "https://registry.npmjs.org/@cucumber/cucumber/-/cucumber-12.1.0.tgz", "integrity": "sha512-F72uKHUtHFFn4OAw6r6MoS+PWSMomQoIpW4+W/dcwNYY9oAKVAGyGJkEPIDXX00YtZY+EH3RUcNPlkD+f+dI0Q==", "license": "MIT", + "peer": true, "dependencies": { "@cucumber/ci-environment": "10.0.1", "@cucumber/cucumber-expressions": "18.0.1", @@ -518,7 +518,8 @@ "version": "10.0.1", "resolved": "https://registry.npmjs.org/@cucumber/ci-environment/-/ci-environment-10.0.1.tgz", "integrity": "sha512-/+ooDMPtKSmvcPMDYnMZt4LuoipfFfHaYspStI4shqw8FyKcfQAmekz6G+QKWjQQrvM+7Hkljwx58MEwPCwwzg==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@cucumber/cucumber/node_modules/@cucumber/gherkin": { "version": "32.2.0", @@ -535,6 +536,7 @@ "resolved": "https://registry.npmjs.org/@cucumber/gherkin-streams/-/gherkin-streams-5.0.1.tgz", "integrity": "sha512-/7VkIE/ASxIP/jd4Crlp4JHXqdNFxPGQokqWqsaCCiqBiu5qHoKMxcWNlp9njVL/n9yN4S08OmY3ZR8uC5x74Q==", "license": "MIT", + "peer": true, "dependencies": { "commander": "9.1.0", "source-map-support": "0.5.21" @@ -553,6 +555,7 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-9.1.0.tgz", "integrity": "sha512-i0/MaqBtdbnJ4XQs4Pmyb+oFQl+q0lsAmokVUH92SlSw4fkeAcG3bVon+Qt7hmtF+u3Het6o4VgrcY3qAoEB6w==", "license": "MIT", + "peer": true, "engines": { "node": "^12.20.0 || >=14" } @@ -562,6 +565,7 @@ "resolved": "https://registry.npmjs.org/@cucumber/gherkin-utils/-/gherkin-utils-9.2.0.tgz", "integrity": "sha512-3nmRbG1bUAZP3fAaUBNmqWO0z0OSkykZZotfLjyhc8KWwDSOrOmMJlBTd474lpA8EWh4JFLAX3iXgynBqBvKzw==", "license": "MIT", + "peer": true, "dependencies": { "@cucumber/gherkin": "^31.0.0", "@cucumber/messages": "^27.0.0", @@ -578,6 +582,7 @@ "resolved": "https://registry.npmjs.org/@cucumber/gherkin/-/gherkin-31.0.0.tgz", "integrity": "sha512-wlZfdPif7JpBWJdqvHk1Mkr21L5vl4EfxVUOS4JinWGf3FLRV6IKUekBv5bb5VX79fkDcfDvESzcQ8WQc07Wgw==", "license": "MIT", + "peer": true, "dependencies": { "@cucumber/messages": ">=19.1.4 <=26" } @@ -587,6 +592,7 @@ "resolved": "https://registry.npmjs.org/@cucumber/messages/-/messages-26.0.1.tgz", "integrity": "sha512-DIxSg+ZGariumO+Lq6bn4kOUIUET83A4umrnWmidjGFl8XxkBieUZtsmNbLYgH/gnsmP07EfxxdTr0hOchV1Sg==", "license": "MIT", + "peer": true, "dependencies": { "@types/uuid": "10.0.0", "class-transformer": "0.5.1", @@ -599,6 +605,7 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", "license": "MIT", + "peer": true, "engines": { "node": ">=18" } @@ -612,6 +619,7 @@ "https://github.com/sponsors/ctavan" ], "license": "MIT", + "peer": true, "bin": { "uuid": "dist/bin/uuid" } @@ -621,6 +629,7 @@ "resolved": "https://registry.npmjs.org/@cucumber/html-formatter/-/html-formatter-21.13.0.tgz", "integrity": "sha512-/zkBZNGZca7AeY4hSMMu3PBqZBZtZ45qhynZC++LAstlyhXQrzl6zmjVLZMX7jIbdF1Lb+TjN4PWiGtS5VOM6g==", "license": "MIT", + "peer": true, "peerDependencies": { "@cucumber/messages": ">=18" } @@ -630,6 +639,7 @@ "resolved": "https://registry.npmjs.org/@cucumber/junit-xml-formatter/-/junit-xml-formatter-0.7.1.tgz", "integrity": "sha512-AzhX+xFE/3zfoYeqkT7DNq68wAQfBcx4Dk9qS/ocXM2v5tBv6eFQ+w8zaSfsktCjYzu4oYRH/jh4USD1CYHfaQ==", "license": "MIT", + "peer": true, "dependencies": { "@cucumber/query": "^13.0.2", "@teppeis/multimaps": "^3.0.0", @@ -658,6 +668,7 @@ "resolved": "https://registry.npmjs.org/@cucumber/query/-/query-13.6.0.tgz", "integrity": "sha512-tiDneuD5MoWsJ9VKPBmQok31mSX9Ybl+U4wqDoXeZgsXHDURqzM3rnpWVV3bC34y9W6vuFxrlwF/m7HdOxwqRw==", "license": "MIT", + "peer": true, "dependencies": { "@teppeis/multimaps": "3.0.0", "lodash.sortby": "^4.7.0" @@ -670,13 +681,15 @@ "version": "6.2.0", "resolved": "https://registry.npmjs.org/@cucumber/tag-expressions/-/tag-expressions-6.2.0.tgz", "integrity": "sha512-KIF0eLcafHbWOuSDWFw0lMmgJOLdDRWjEL1kfXEWrqHmx2119HxVAr35WuEd9z542d3Yyg+XNqSr+81rIKqEdg==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@cucumber/cucumber/node_modules/commander": { "version": "10.0.1", "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", "license": "MIT", + "peer": true, "engines": { "node": ">=14" } @@ -686,6 +699,7 @@ "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", "license": "ISC", + "peer": true, "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" @@ -702,6 +716,7 @@ "resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz", "integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==", "license": "BlueOak-1.0.0", + "peer": true, "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", @@ -725,6 +740,7 @@ "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", "integrity": "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==", "license": "ISC", + "peer": true, "dependencies": { "lru-cache": "^10.0.1" }, @@ -736,13 +752,15 @@ "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "license": "ISC" + "license": "ISC", + "peer": true }, "node_modules/@cucumber/cucumber/node_modules/luxon": { "version": "3.6.1", "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.6.1.tgz", "integrity": "sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=12" } @@ -752,6 +770,7 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", "license": "BlueOak-1.0.0", + "peer": true, "dependencies": { "@isaacs/brace-expansion": "^5.0.0" }, @@ -767,6 +786,7 @@ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-2.1.6.tgz", "integrity": "sha512-+hEnITedc8LAtIP9u3HJDFIdcLV2vXP33sqLLIzkv1Db1zO/1OxbvYf0Y1OC/S/Qo5dxHXepofhmxL02PsKe+A==", "license": "MIT", + "peer": true, "bin": { "mkdirp": "dist/cjs/src/bin.js" }, @@ -782,6 +802,7 @@ "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.2.tgz", "integrity": "sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g==", "license": "BSD-2-Clause", + "peer": true, "dependencies": { "hosted-git-info": "^7.0.0", "semver": "^7.3.5", @@ -796,6 +817,7 @@ "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz", "integrity": "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==", "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.26.2", "index-to-position": "^1.1.0", @@ -813,6 +835,7 @@ "resolved": "https://registry.npmjs.org/read-package-up/-/read-package-up-11.0.0.tgz", "integrity": "sha512-MbgfoNPANMdb4oRBNg5eqLbB2t2r+o5Ua1pNt8BqGp4I0FJZhuVSOj3PaBPni4azWuSzEdNn2evevzVmEk1ohQ==", "license": "MIT", + "peer": true, "dependencies": { "find-up-simple": "^1.0.0", "read-pkg": "^9.0.0", @@ -830,6 +853,7 @@ "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-9.0.1.tgz", "integrity": "sha512-9viLL4/n1BJUCT1NXVTdS1jtm80yDEgR5T4yCelII49Mbj0v1rZdKqj7zCiYdbB0CuCgdrvHcNogAKTFPBocFA==", "license": "MIT", + "peer": true, "dependencies": { "@types/normalize-package-data": "^2.4.3", "normalize-package-data": "^6.0.0", @@ -849,6 +873,7 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "license": "ISC", + "peer": true, "bin": { "semver": "bin/semver.js" }, @@ -861,6 +886,7 @@ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", "license": "ISC", + "peer": true, "engines": { "node": ">=14" }, @@ -877,6 +903,7 @@ "https://github.com/sponsors/ctavan" ], "license": "MIT", + "peer": true, "bin": { "uuid": "dist/esm/bin/uuid" } @@ -886,6 +913,7 @@ "resolved": "https://registry.npmjs.org/yup/-/yup-1.6.1.tgz", "integrity": "sha512-JED8pB50qbA4FOkDol0bYF/p60qSEDQqBD0/qeIrUCG1KbPBIQ776fCUNb9ldbPcSTxA69g/47XTo4TqWiuXOA==", "license": "MIT", + "peer": true, "dependencies": { "property-expr": "^2.0.5", "tiny-case": "^1.0.3", @@ -898,6 +926,7 @@ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", "license": "(MIT OR CC0-1.0)", + "peer": true, "engines": { "node": ">=12.20" }, @@ -910,7 +939,6 @@ "resolved": "https://registry.npmjs.org/@cucumber/gherkin/-/gherkin-37.0.0.tgz", "integrity": "sha512-vKJVJ6h4HCktG870wgYUUskNpFxbFI0WmAkVLPTz1LlLwJX7/KOBqFcr2/L3u0pPoHjbLRW+IpbiXLT2T13/wg==", "license": "MIT", - "peer": true, "dependencies": { "@cucumber/messages": ">=31.0.0 <32" } @@ -1008,7 +1036,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/@cucumber/message-streams/-/message-streams-4.0.1.tgz", "integrity": "sha512-Kxap9uP5jD8tHUZVjTWgzxemi/0uOsbGjd4LBOSxcJoOCRbESFwemUzilJuzNTB8pcTQUh8D5oudUyxfkJOKmA==", - "peer": true, "peerDependencies": { "@cucumber/messages": ">=17.1.1" } @@ -1018,7 +1045,6 @@ "resolved": "https://registry.npmjs.org/@cucumber/messages/-/messages-31.0.0.tgz", "integrity": "sha512-Dqhatp4AjMsH9SREfWz3Q8nlGuwJMTW7YAW5L3OzRId86ZUEu/a8vIL1RO2c0agQefuBS2SVH9fEZ66ovrMYRA==", "license": "MIT", - "peer": true, "dependencies": { "class-transformer": "0.5.1", "reflect-metadata": "0.2.2" @@ -1832,7 +1858,6 @@ "integrity": "sha512-PRA911Blj99jR5RMeTunVbNXMF6Lp4vZXnk5GQjcnUWUTsrXtekg/pnmFFI2u/I36Y/2bITGS30GZCXei6uNkA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "json-schema-traverse": "^1.0.0", @@ -2341,7 +2366,6 @@ "integrity": "sha512-yCAeZl7a0DxgNVteXFHt9+uyFbqXGy/ShC4BlcHkoE0AfGXYv/BUiplV72DjMYXHDBXFjhvr6DD1NiRVfB4j8g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -2512,7 +2536,6 @@ "integrity": "sha512-vxXJV1hVFx3IXz/oy2sICsJukaBrtDEQSBiV48/YIV5KWjX1dO+bcIr/kCPrW6weKXvsaGKFNlwH0v2eYdRRbA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.34.0", "@typescript-eslint/types": "8.34.0", @@ -2756,7 +2779,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", "dev": true, - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3265,7 +3287,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.19", "caniuse-lite": "^1.0.30001751", @@ -3470,7 +3491,6 @@ "integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "assertion-error": "^1.1.0", "check-error": "^1.0.3", @@ -4474,7 +4494,6 @@ "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -6814,6 +6833,7 @@ "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz", "integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==", "license": "BlueOak-1.0.0", + "peer": true, "dependencies": { "@isaacs/cliui": "^8.0.2" }, @@ -10487,7 +10507,6 @@ "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -10534,6 +10553,7 @@ "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, diff --git a/src/configuration/from_file.ts b/src/configuration/from_file.ts index 9629e033c..d9a3d6d59 100644 --- a/src/configuration/from_file.ts +++ b/src/configuration/from_file.ts @@ -8,6 +8,18 @@ import { IConfiguration } from './types' import { mergeConfigurations } from './merge_configurations' import { parseConfiguration } from './parse_configuration' +const SUPPORTED_EXTENSIONS = [ + '.json', + '.yaml', + '.yml', + '.js', + '.cjs', + '.mjs', + '.ts', + '.cts', + '.mts', +] + export async function fromFile( logger: ILogger, cwd: string, @@ -79,58 +91,80 @@ async function loadFile( ): Promise> { const filePath: string = path.join(cwd, file) const extension = path.extname(filePath) + if (!SUPPORTED_EXTENSIONS.includes(extension)) { + throw new Error(`Unsupported configuration file extension "${extension}"`) + } let definitions - switch (extension) { - case '.json': - definitions = JSON.parse( - await promisify(fs.readFile)(filePath, { encoding: 'utf-8' }) - ) - break - case '.yaml': - case '.yml': - definitions = YAML.parse( - await promisify(fs.readFile)(filePath, { encoding: 'utf-8' }) - ) - break - case '.cjs': - logger.debug( - `Loading configuration file "${file}" as CommonJS based on extension` - ) - // eslint-disable-next-line @typescript-eslint/no-require-imports - definitions = require(filePath) - break - case '.mjs': - logger.debug( - `Loading configuration file "${file}" as ESM based on extension` - ) - definitions = await import(pathToFileURL(filePath).toString()) - break - case '.js': - { - const parentPackage = await readPackageJson(filePath) - if (!parentPackage) { - logger.debug( - `Loading configuration file "${file}" as CommonJS based on absence of a parent package` - ) - // eslint-disable-next-line @typescript-eslint/no-require-imports - definitions = require(filePath) - } else if (parentPackage.type === 'module') { - logger.debug( - `Loading configuration file "${file}" as ESM based on "${parentPackage.name}" package type` - ) - definitions = await import(pathToFileURL(filePath).toString()) - } else { - logger.debug( - `Loading configuration file "${file}" as CommonJS based on "${parentPackage.name}" package type` - ) - // eslint-disable-next-line @typescript-eslint/no-require-imports - definitions = require(filePath) + try { + switch (extension) { + case '.json': + definitions = JSON.parse( + await promisify(fs.readFile)(filePath, { encoding: 'utf-8' }) + ) + break + case '.yaml': + case '.yml': + definitions = YAML.parse( + await promisify(fs.readFile)(filePath, { encoding: 'utf-8' }) + ) + break + case '.cjs': + logger.debug( + `Loading configuration file "${file}" as CommonJS based on extension` + ) + // eslint-disable-next-line @typescript-eslint/no-require-imports + definitions = require(filePath) + break + case '.cts': + logger.debug( + `Loading configuration file "${file}" as TypeScript based on extension` + ) + // eslint-disable-next-line @typescript-eslint/no-require-imports + definitions = require(filePath) + break + case '.mjs': + logger.debug( + `Loading configuration file "${file}" as ESM based on extension` + ) + definitions = await import(pathToFileURL(filePath).toString()) + break + case '.mts': + case '.ts': + logger.debug( + `Loading configuration file "${file}" as TypeScript based on extension` + ) + definitions = await import(pathToFileURL(filePath).toString()) + break + case '.js': + { + const parentPackage = await readPackageJson(filePath) + if (!parentPackage) { + logger.debug( + `Loading configuration file "${file}" as CommonJS based on absence of a parent package` + ) + // eslint-disable-next-line @typescript-eslint/no-require-imports + definitions = require(filePath) + } else if (parentPackage.type === 'module') { + logger.debug( + `Loading configuration file "${file}" as ESM based on "${parentPackage.name}" package type` + ) + definitions = await import(pathToFileURL(filePath).toString()) + } else { + logger.debug( + `Loading configuration file "${file}" as CommonJS based on "${parentPackage.name}" package type` + ) + // eslint-disable-next-line @typescript-eslint/no-require-imports + definitions = require(filePath) + } } - } - break - default: - throw new Error(`Unsupported configuration file extension "${extension}"`) + break + } + } catch (error) { + throw new Error(`Configuration file "${file}" failed to load/parse`, { + cause: error, + }) } + if (typeof definitions !== 'object') { throw new Error(`Configuration file ${filePath} does not export an object`) } diff --git a/src/configuration/from_file_spec.ts b/src/configuration/from_file_spec.ts index bc3f116f9..3a5efb715 100644 --- a/src/configuration/from_file_spec.ts +++ b/src/configuration/from_file_spec.ts @@ -3,6 +3,7 @@ import fs from 'node:fs' import path from 'node:path' import tmp, { DirOptions } from 'tmp' import { expect } from 'chai' +import semver from 'semver' import { FakeLogger } from '../../test/fake_logger' import { fromFile } from './from_file' @@ -231,5 +232,61 @@ p1: ) } }) + + it('should throw when a supported format fails to load or parse', async () => { + const { logger, cwd } = await setup('cucumber.js', `nope!`) + try { + await fromFile(logger, cwd, 'cucumber.js', ['p1']) + expect.fail('should have thrown') + } catch (error) { + expect(error.message).to.eq( + 'Configuration file "cucumber.js" failed to load/parse' + ) + } + }) + + describe('typescript', function () { + if (!semver.satisfies(process.version, '>=22.0.0')) { + return + } + + it('should work with .mts', async () => { + const { logger, cwd } = await setup( + 'cucumber.mts', + `import type { IConfiguration } from '@cucumber/cucumber' + +export default {} + +export const p1 = {paths: ['other/path/*.feature']}` + ) + + const result = await fromFile(logger, cwd, 'cucumber.mts', ['p1']) + expect(result).to.deep.eq({ paths: ['other/path/*.feature'] }) + }) + + it('should work with .ts', async () => { + const { logger, cwd } = await setup( + 'cucumber.ts', + `import type { IConfiguration } from '@cucumber/cucumber' + +export default {} + +export const p1 = {paths: ['other/path/*.feature']}` + ) + + const result = await fromFile(logger, cwd, 'cucumber.ts', ['p1']) + expect(result).to.deep.eq({ paths: ['other/path/*.feature'] }) + }) + + it('should work with .cts', async () => { + const { logger, cwd } = await setup( + 'cucumber.cts', + `module.exports = { default: {}, p1: { paths: ['other/path/*.feature'] } }` + ) + + const result = await fromFile(logger, cwd, 'cucumber.cts', ['p1']) + expect(result).to.deep.eq({ paths: ['other/path/*.feature'] }) + }) + }) }) })