From c4697e621e40e9d203a15461b9f881489ef996d9 Mon Sep 17 00:00:00 2001 From: David Goss Date: Tue, 19 Jul 2022 09:50:17 +0100 Subject: [PATCH] rework handling and docs for invalid installations (#2089) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * use is-installed-globally, emit warning * disable lint rule * remove unused dependency * rough implementation of runtime check * update comment * include method name you called * tweak wording of runtime error * tweak global message * expand documentation * update link to docs * tweak wording again * rework testing a bit * tweak variable naming * update CHANGELOG.md * address review comments * fix typo Co-authored-by: Aurélien Reeves Co-authored-by: Aurélien Reeves --- CHANGELOG.md | 2 + README.md | 1 + docs/cli.md | 3 - docs/faq.md | 23 +---- docs/installation.md | 76 ++++++++++++++ features/global_install.feature | 27 ----- features/invalid_installation.feature | 28 ++++++ features/step_definitions/cli_steps.ts | 16 --- features/step_definitions/install_steps.ts | 56 +++++++++++ features/support/hooks.ts | 53 ---------- features/support/world.ts | 1 - package-lock.json | 109 +++++++++++++++++---- package.json | 3 +- src/cli/index.ts | 2 +- src/cli/install_validator.ts | 42 ++------ src/support_code_library_builder/index.ts | 30 +++++- 16 files changed, 292 insertions(+), 180 deletions(-) create mode 100644 docs/installation.md delete mode 100644 features/global_install.feature create mode 100644 features/invalid_installation.feature create mode 100644 features/step_definitions/install_steps.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index a4beed015..0019c4cee 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](https://github.com/cucumber/cucumber/blob/master/CONTRIBUTING.md) on how to contribute to Cucumber. ## [Unreleased] +### Changed +- Reworked handling for invalid installations ([#2089](https://github.com/cucumber/cucumber-js/pull/2089)) ## [8.4.0] - 2022-06-29 ### Fixed diff --git a/README.md b/README.md index 9f2860790..a0ae439a4 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,7 @@ If you learn best by example, we have [a repo with several example projects](htt The following documentation is for `main`, which might contain some unreleased features. See [documentation for older versions](./docs/older_versions.md) if you need it. +* [Installation](./docs/installation.md) * [CLI](./docs/cli.md) * [Configuration](./docs/configuration.md) * Support Code diff --git a/docs/cli.md b/docs/cli.md index e33381cba..6c5bdeeca 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -22,9 +22,6 @@ Or via [npx](https://docs.npmjs.com/cli/v8/commands/npx): $ npx cucumber-js ``` -**Note on global installs:** Cucumber does not work when installed globally because `@cucumber/cucumber` -needs to be required in your support files and globally installed modules cannot be required. - ## Options All the [standard configuration options](./configuration.md#options) can be provided via the CLI. diff --git a/docs/faq.md b/docs/faq.md index 530a6602d..1a8a79734 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -17,25 +17,4 @@ If you have similar `Given` and `Then` patterns, try adding the word “should ## Why am I seeing `The "from" argument must be of type string. Received type undefined`? -If when running cucumber-js you see an error with a stack trace like: - -``` -TypeError [ERR_INVALID_ARG_TYPE]: The "from" argument must be of type string. Received type undefined - at validateString (internal/validators.js:125:11) - at Object.relative (path.js:1162:5) - ... -``` - -This usually an effect of one of: - -- Your project depends on cucumber-js, and also has a dependency (in `node_modules`) that depends on cucumber-js at a different version -- You have a package that depends (even as a dev dependency) on cucumber-js linked (via `npm link` or `yarn link`) - -These cases can cause two different instances of cucumber-js to be in play at runtime, which causes errors. - -If removing the duplicate dependency is not possible, you can work around this by using [import-cwd](https://www.npmjs.com/package/import-cwd) so your support code always requires cucumber-js from the current working directory (i.e. your host project): - -```js -const importCwd = require('import-cwd') -const { Given, When, Then } = importCwd('@cucumber/cucumber') -``` +See [Invalid installations](./installation.md#invalid-installations) diff --git a/docs/installation.md b/docs/installation.md new file mode 100644 index 000000000..1da91bbeb --- /dev/null +++ b/docs/installation.md @@ -0,0 +1,76 @@ +# Installation + +With [npm](https://www.npmjs.com/): + +```shell +$ npm install @cucumber/cucumber +``` + +With [Yarn](https://yarnpkg.com/): + +```shell +$ yarn add @cucumber/cucumber +``` + +## Invalid installations + +If Cucumber exits with an error message like: + +``` +You're calling functions (e.g. "Given") on an instance of Cucumber that isn't running. +This means you have an invalid installation, mostly likely due to: +... +``` + +This means you have an invalid installation. + +Unlike many libraries, Cucumber is _stateful_; you call functions to register your support code, and we keep that state until it's used in the test run. Therefore, it's important that everything interacting with Cucumber in your project is interacting with the same instance. There are a few ways this can go wrong: + +### Global installation + +Some libraries with a command-line interface are designed to be installed globally. Not Cucumber though - for the reasons above, you need to install it as a dependency in your project. + +We'll emit a warning if it looks like Cucumber is installed globally. + +### Duplicate dependency + +If your project depends on `@cucumber/cucumber`, but also has another dependency that _itself_ depends on `@cucumber/cucumber` (maybe at a slightly different version), this can cause the issue with multiple instances in play at the same time. If you're familiar with React, this is a lot like [the "invalid hook call" issue](https://reactjs.org/warnings/invalid-hook-call-warning.html#duplicate-react). + +This is common where you have split some of your support code (e.g. step definitions) into a separate package for reuse across multiple projects, or are perhaps using a third-party package intended to work with Cucumber. + +You can diagnose this by running `npm why @cucumber/cucumber` in your project. You might see something like: + +``` +@cucumber/cucumber@8.4.0 dev +node_modules/@cucumber/cucumber + dev @cucumber/cucumber@"8.4.0" from the root project + +@cucumber/cucumber@8.3.0 dev +node_modules/my-shared-steps-library/node_modules/@cucumber/cucumber + dev @cucumber/cucumber@"8.3.0" from my-shared-steps-library@1.0.0 + node_modules/my-shared-steps-library + my-shared-steps-library@"1.0.0" from the root project +``` + +In this case, the fix is to change the library so `@cucumber/cucumber` is a [peer dependency](https://docs.npmjs.com/cli/v8/configuring-npm/package-json#peerdependencies) rather than a regular dependency (it probably also needs to be a dev dependency). This will remove the duplication in the host project. If you don't control the library, consider using [overrides](https://docs.npmjs.com/cli/v8/configuring-npm/package-json#overrides) (npm) or [resolutions](https://classic.yarnpkg.com/lang/en/docs/selective-version-resolutions/) (Yarn) to get it down to a single instance. + +### Deprecated package + +When looking at the duplicate dependency issue, it's worth checking whether anything in your project is depending on the old, deprecated `cucumber` package. Anything touching Cucumber [should be using](../UPGRADING.md#package-name) the newer `@cucumber/cucumber` package. + +### Linking + +With the shared library example above, even if you have `@cucumber/cucumber` correctly defined as a peer dependency, you can still hit the issue if you hook up the library locally using `npm link` or `yarn link` when developing or testing. + +This is trickier to deal with. If you run `npm link ../my-project/node_modules/@cucumber/cucumber` from the library, this should work around it (assuming `my-project` is your host project's directory, and it's adjacent to your library in the file system). + +### Notes + +In earlier versions of Cucumber, this issue would present with a more cryptic error (the causes and solutions are the same): + +``` +TypeError [ERR_INVALID_ARG_TYPE]: The "from" argument must be of type string. Received type undefined + at validateString (internal/validators.js:125:11) + at Object.relative (path.js:1162:5) + ... +``` diff --git a/features/global_install.feature b/features/global_install.feature deleted file mode 100644 index b34f049b9..000000000 --- a/features/global_install.feature +++ /dev/null @@ -1,27 +0,0 @@ -Feature: Global Installs - - @spawn @global-install - Scenario: executing cucumber from a global install error - Given a file named "features/a.feature" with: - """ - Feature: some feature - Scenario: - When a step is passing - """ - And a file named "features/step_definitions/cucumber_steps.js" with: - """ - const {When} = require('@cucumber/cucumber') - - When(/^a step is passing$/, function() {}) - """ - When I run cucumber-js (installed globally) - Then it fails - And the error output contains the text: - """ - You appear to be executing an install of cucumber (most likely a global install) - that is different from your local install (the one required in your support files). - For cucumber to work, you need to execute the same install that is required in your support files. - Please execute the locally installed version to run your tests. - """ - When I run cucumber-js (installed locally) - Then it passes diff --git a/features/invalid_installation.feature b/features/invalid_installation.feature new file mode 100644 index 000000000..9f65be4d3 --- /dev/null +++ b/features/invalid_installation.feature @@ -0,0 +1,28 @@ +Feature: Invalid installations + + @spawn + Scenario: Cucumber exits with an error when running an invalid installation + Given an invalid installation + Given a file named "features/a.feature" with: + """ + Feature: some feature + Scenario: + When a step is passing + """ + And a file named "features/step_definitions/cucumber_steps.js" with: + """ + const {When} = require('@cucumber/cucumber') + + When(/^a step is passing$/, function() {}) + """ + When I run cucumber-js + Then it fails + And the error output contains the text: + """ + You're calling functions (e.g. "When") on an instance of Cucumber that isn't running. + This means you have an invalid installation, mostly likely due to: + - Cucumber being installed globally + - A project structure where your support code is depending on a different instance of Cucumber + Either way, you'll need to address this in order for Cucumber to work. + See https://github.com/cucumber/cucumber-js/blob/main/docs/installation.md#invalid-installations + """ diff --git a/features/step_definitions/cli_steps.ts b/features/step_definitions/cli_steps.ts index 17aec0f0b..c23774d4d 100644 --- a/features/step_definitions/cli_steps.ts +++ b/features/step_definitions/cli_steps.ts @@ -76,22 +76,6 @@ When( } ) -When( - 'I run cucumber-js \\(installed locally\\)', - { timeout: 10000 }, - async function (this: World) { - return await this.run(this.localExecutablePath, []) - } -) - -When( - 'I run cucumber-js \\(installed globally\\)', - { timeout: 10000 }, - async function (this: World) { - return await this.run(this.globalExecutablePath, []) - } -) - Then('it passes', () => {}) // eslint-disable-line @typescript-eslint/no-empty-function Then('it fails', function (this: World) { diff --git a/features/step_definitions/install_steps.ts b/features/step_definitions/install_steps.ts new file mode 100644 index 000000000..dfc8335fb --- /dev/null +++ b/features/step_definitions/install_steps.ts @@ -0,0 +1,56 @@ +import { Given } from '../../' +import tmp from 'tmp' +import path from 'path' +import fs from 'fs' +import fsExtra from 'fs-extra' +import { World } from '../support/world' + +/* +Simulates something like a global install, where the Cucumber being executed +is not the one being imported by support code + */ +Given('an invalid installation', async function (this: World) { + const projectPath = path.join(__dirname, '..', '..') + const tmpObject = tmp.dirSync({ unsafeCleanup: true }) + + // Symlink everything in node_modules so the fake installation has all the dependencies it needs + const projectNodeModulesPath = path.join(projectPath, 'node_modules') + const projectNodeModulesDirs = fs.readdirSync(projectNodeModulesPath) + const installationNodeModulesPath = path.join(tmpObject.name, 'node_modules') + projectNodeModulesDirs.forEach((nodeModuleDir) => { + let pathsToLink = [nodeModuleDir] + if (nodeModuleDir[0] === '@') { + const scopeNodeModuleDirs = fs.readdirSync( + path.join(projectNodeModulesPath, nodeModuleDir) + ) + pathsToLink = scopeNodeModuleDirs.map((x) => path.join(nodeModuleDir, x)) + } + pathsToLink.forEach((pathToLink) => { + const installationPackagePath = path.join( + installationNodeModulesPath, + pathToLink + ) + const projectPackagePath = path.join(projectNodeModulesPath, pathToLink) + fsExtra.ensureSymlinkSync(projectPackagePath, installationPackagePath) + }) + }) + + const invalidInstallationCucumberPath = path.join( + installationNodeModulesPath, + '@cucumber', + 'cucumber' + ) + const itemsToCopy = ['bin', 'lib', 'package.json'] + itemsToCopy.forEach((item) => { + fsExtra.copySync( + path.join(projectPath, item), + path.join(invalidInstallationCucumberPath, item) + ) + }) + + this.localExecutablePath = path.join( + invalidInstallationCucumberPath, + 'bin', + 'cucumber.js' + ) +}) diff --git a/features/support/hooks.ts b/features/support/hooks.ts index 66d85c3de..e837bf742 100644 --- a/features/support/hooks.ts +++ b/features/support/hooks.ts @@ -1,8 +1,6 @@ import { After, Before, formatterHelpers, ITestCaseHookParameter } from '../../' -import fs from 'fs' import fsExtra from 'fs-extra' import path from 'path' -import tmp from 'tmp' import { doesHaveValue } from '../../src/value_checker' import { World } from './world' import { warnUserAboutEnablingDeveloperMode } from './warn_user_about_enabling_developer_mode' @@ -54,57 +52,6 @@ Before('@esm', function (this: World) { }) }) -Before('@global-install', function (this: World) { - const tmpObject = tmp.dirSync({ unsafeCleanup: true }) - - // Symlink everything in node_modules so the fake global install has all the dependencies it needs - const projectNodeModulesPath = path.join(projectPath, 'node_modules') - const projectNodeModulesDirs = fs.readdirSync(projectNodeModulesPath) - const globalInstallNodeModulesPath = path.join(tmpObject.name, 'node_modules') - projectNodeModulesDirs.forEach((nodeModuleDir) => { - let pathsToLink = [nodeModuleDir] - if (nodeModuleDir[0] === '@') { - const scopeNodeModuleDirs = fs.readdirSync( - path.join(projectNodeModulesPath, nodeModuleDir) - ) - pathsToLink = scopeNodeModuleDirs.map((x) => path.join(nodeModuleDir, x)) - } - pathsToLink.forEach((pathToLink) => { - const globalInstallNodeModulePath = path.join( - globalInstallNodeModulesPath, - pathToLink - ) - const projectNodeModulePath = path.join( - projectNodeModulesPath, - pathToLink - ) - fsExtra.ensureSymlinkSync( - projectNodeModulePath, - globalInstallNodeModulePath - ) - }) - }) - - const globalInstallCucumberPath = path.join( - globalInstallNodeModulesPath, - '@cucumber', - 'cucumber' - ) - const itemsToCopy = ['bin', 'lib', 'package.json'] - itemsToCopy.forEach((item) => { - fsExtra.copySync( - path.join(projectPath, item), - path.join(globalInstallCucumberPath, item) - ) - }) - - this.globalExecutablePath = path.join( - globalInstallCucumberPath, - 'bin', - 'cucumber.js' - ) -}) - After(async function (this: World) { if (this.reportServer?.started) { await this.reportServer.stop() diff --git a/features/support/world.ts b/features/support/world.ts index ea4e0bde6..e60dbe46f 100644 --- a/features/support/world.ts +++ b/features/support/world.ts @@ -36,7 +36,6 @@ export class World { public lastRun: ILastRun public verifiedLastRunError: boolean public localExecutablePath: string - public globalExecutablePath: string public reportServer: FakeReportServer parseEnvString(str: string): NodeJS.ProcessEnv { diff --git a/package-lock.json b/package-lock.json index 221f280bc..e08b47295 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,13 +30,13 @@ "glob": "^7.1.6", "has-ansi": "^4.0.1", "indent-string": "^4.0.0", + "is-installed-globally": "^0.4.0", "is-stream": "^2.0.0", "knuth-shuffle-seeded": "^1.0.6", "lodash.merge": "^4.6.2", "lodash.mergewith": "^4.6.2", "mz": "^2.7.0", "progress": "^2.0.3", - "resolve": "^1.19.0", "resolve-pkg": "^2.0.0", "semver": "7.3.7", "stack-chain": "^2.0.0", @@ -70,7 +70,6 @@ "@types/mz": "2.7.4", "@types/node": "16.11.42", "@types/progress": "2.0.5", - "@types/resolve": "1.20.2", "@types/semver": "7.3.10", "@types/sinon-chai": "3.2.8", "@types/sinonjs__fake-timers": "8.1.2", @@ -1437,12 +1436,6 @@ "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==", "dev": true }, - "node_modules/@types/resolve": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", - "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", - "dev": true - }, "node_modules/@types/semver": { "version": "7.3.10", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.10.tgz", @@ -3827,7 +3820,8 @@ "node_modules/function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true }, "node_modules/functional-red-black-tree": { "version": "1.0.1", @@ -3957,6 +3951,20 @@ "node": ">=10.13.0" } }, + "node_modules/global-dirs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.0.tgz", + "integrity": "sha512-v8ho2DS5RiCjftj1nD9NmnfaOzTdud7RRnVd9kFNOjqZbISlx5DQ+OrTkywgd0dIt7oFCvKetZSHoHcP3sDdiA==", + "dependencies": { + "ini": "2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/globals": { "version": "13.15.0", "resolved": "https://registry.npmjs.org/globals/-/globals-13.15.0.tgz", @@ -4011,6 +4019,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, "dependencies": { "function-bind": "^1.1.1" }, @@ -4226,6 +4235,14 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, + "node_modules/ini": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", + "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==", + "engines": { + "node": ">=10" + } + }, "node_modules/internal-slot": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", @@ -4329,6 +4346,7 @@ "version": "2.9.0", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.9.0.tgz", "integrity": "sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==", + "dev": true, "dependencies": { "has": "^1.0.3" }, @@ -4380,6 +4398,21 @@ "node": ">=0.10.0" } }, + "node_modules/is-installed-globally": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz", + "integrity": "sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==", + "dependencies": { + "global-dirs": "^3.0.0", + "is-path-inside": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-negative-zero": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", @@ -4416,6 +4449,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "engines": { + "node": ">=8" + } + }, "node_modules/is-plain-obj": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", @@ -5839,7 +5880,8 @@ "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true }, "node_modules/path-platform": { "version": "0.11.15", @@ -6422,6 +6464,7 @@ "version": "1.22.0", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz", "integrity": "sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==", + "dev": true, "dependencies": { "is-core-module": "^2.8.1", "path-parse": "^1.0.7", @@ -7020,6 +7063,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, "engines": { "node": ">= 0.4" }, @@ -8852,12 +8896,6 @@ "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==", "dev": true }, - "@types/resolve": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", - "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", - "dev": true - }, "@types/semver": { "version": "7.3.10", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.10.tgz", @@ -10675,7 +10713,8 @@ "function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true }, "functional-red-black-tree": { "version": "1.0.1", @@ -10768,6 +10807,14 @@ "is-glob": "^4.0.3" } }, + "global-dirs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.0.tgz", + "integrity": "sha512-v8ho2DS5RiCjftj1nD9NmnfaOzTdud7RRnVd9kFNOjqZbISlx5DQ+OrTkywgd0dIt7oFCvKetZSHoHcP3sDdiA==", + "requires": { + "ini": "2.0.0" + } + }, "globals": { "version": "13.15.0", "resolved": "https://registry.npmjs.org/globals/-/globals-13.15.0.tgz", @@ -10807,6 +10854,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, "requires": { "function-bind": "^1.1.1" } @@ -10960,6 +11008,11 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, + "ini": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", + "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==" + }, "internal-slot": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", @@ -11033,6 +11086,7 @@ "version": "2.9.0", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.9.0.tgz", "integrity": "sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==", + "dev": true, "requires": { "has": "^1.0.3" } @@ -11066,6 +11120,15 @@ "is-extglob": "^2.1.1" } }, + "is-installed-globally": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz", + "integrity": "sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==", + "requires": { + "global-dirs": "^3.0.0", + "is-path-inside": "^3.0.2" + } + }, "is-negative-zero": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", @@ -11087,6 +11150,11 @@ "has-tostringtag": "^1.0.0" } }, + "is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==" + }, "is-plain-obj": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", @@ -12181,7 +12249,8 @@ "path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true }, "path-platform": { "version": "0.11.15", @@ -12603,6 +12672,7 @@ "version": "1.22.0", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz", "integrity": "sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==", + "dev": true, "requires": { "is-core-module": "^2.8.1", "path-parse": "^1.0.7", @@ -13056,7 +13126,8 @@ "supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==" + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true }, "test-exclude": { "version": "6.0.0", diff --git a/package.json b/package.json index b08054b60..2f66a3382 100644 --- a/package.json +++ b/package.json @@ -215,13 +215,13 @@ "glob": "^7.1.6", "has-ansi": "^4.0.1", "indent-string": "^4.0.0", + "is-installed-globally": "^0.4.0", "is-stream": "^2.0.0", "knuth-shuffle-seeded": "^1.0.6", "lodash.merge": "^4.6.2", "lodash.mergewith": "^4.6.2", "mz": "^2.7.0", "progress": "^2.0.3", - "resolve": "^1.19.0", "resolve-pkg": "^2.0.0", "semver": "7.3.7", "stack-chain": "^2.0.0", @@ -252,7 +252,6 @@ "@types/mz": "2.7.4", "@types/node": "16.11.42", "@types/progress": "2.0.5", - "@types/resolve": "1.20.2", "@types/semver": "7.3.10", "@types/sinon-chai": "3.2.8", "@types/sinonjs__fake-timers": "8.1.2", diff --git a/src/cli/index.ts b/src/cli/index.ts index 614a08222..98d5d89a7 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -38,7 +38,7 @@ export default class Cli { } async run(): Promise { - await validateInstall(this.cwd) + await validateInstall() const { options, configuration: argvConfiguration } = ArgvParser.parse( this.argv ) diff --git a/src/cli/install_validator.ts b/src/cli/install_validator.ts index 6396dd5ca..94b2136e4 100644 --- a/src/cli/install_validator.ts +++ b/src/cli/install_validator.ts @@ -1,39 +1,13 @@ -import fs from 'mz/fs' -import path from 'path' -import resolve from 'resolve' -import { promisify } from 'util' +/* eslint-disable no-console */ +import isInstalledGlobally from 'is-installed-globally' -export async function validateInstall(cwd: string): Promise { - const projectPath = path.join(__dirname, '..', '..') - if (projectPath === cwd) { - return // cucumber testing itself - } - const currentCucumberPath = require.resolve(projectPath) - let localCucumberPath: string - try { - localCucumberPath = await promisify(resolve)( - '@cucumber/cucumber', - { - basedir: cwd, - } - ) - } catch (e) { - throw new Error( - '`@cucumber/cucumber` module not resolvable. Must be locally installed.' - ) - } - localCucumberPath = await fs.realpath(localCucumberPath) - if (localCucumberPath !== currentCucumberPath) { - throw new Error( +export async function validateInstall(): Promise { + if (isInstalledGlobally) + console.warn( ` - You appear to be executing an install of cucumber (most likely a global install) - that is different from your local install (the one required in your support files). - For cucumber to work, you need to execute the same install that is required in your support files. - Please execute the locally installed version to run your tests. - - Executed Path: ${currentCucumberPath} - Local Path: ${localCucumberPath} + It looks like you're running Cucumber from a global installation. + This won't work - you need to have Cucumber installed as a local dependency in your project. + See https://github.com/cucumber/cucumber-js/blob/main/docs/installation.md#invalid-installations ` ) - } } diff --git a/src/support_code_library_builder/index.ts b/src/support_code_library_builder/index.ts index 38ee3e58e..d311c573c 100644 --- a/src/support_code_library_builder/index.ts +++ b/src/support_code_library_builder/index.ts @@ -16,7 +16,7 @@ import { ParameterTypeRegistry, RegularExpression, } from '@cucumber/cucumber-expressions' -import { doesHaveValue } from '../value_checker' +import { doesHaveValue, doesNotHaveValue } from '../value_checker' import { DefineStepPattern, IDefineStepOptions, @@ -100,7 +100,7 @@ export class SupportCodeLibraryBuilder { private parallelCanAssign: ParallelAssignmentValidator constructor() { - this.methods = { + const methods: IDefineSupportCodeMethods = { After: this.defineTestCaseHook( () => this.afterTestCaseHookDefinitionConfigs ), @@ -140,6 +140,32 @@ export class SupportCodeLibraryBuilder { Then: this.defineStep('Then', () => this.stepDefinitionConfigs), When: this.defineStep('When', () => this.stepDefinitionConfigs), } + const checkInstall = (method: string) => { + if (doesNotHaveValue(this.cwd)) { + throw new Error( + ` + You're calling functions (e.g. "${method}") on an instance of Cucumber that isn't running. + This means you have an invalid installation, mostly likely due to: + - Cucumber being installed globally + - A project structure where your support code is depending on a different instance of Cucumber + Either way, you'll need to address this in order for Cucumber to work. + See https://github.com/cucumber/cucumber-js/blob/main/docs/installation.md#invalid-installations + ` + ) + } + } + this.methods = new Proxy(methods, { + get( + target: IDefineSupportCodeMethods, + method: keyof IDefineSupportCodeMethods + ): any { + return (...args: any[]) => { + checkInstall(method) + // @ts-expect-error difficult to type this correctly + return target[method](...args) + } + }, + }) } defineParameterType(options: IParameterTypeDefinition): void {