cucumber / cucumber-js

Cucumber for JavaScript
https://cucumber.io
MIT License
5.04k stars 1.09k forks source link

SyntaxError: Cannot use import statement outside a module #2273

Closed Sam-Levene closed 1 year ago

Sam-Levene commented 1 year ago

πŸ‘“ What did you see?

I am seeing an error in my compilation of a Node.JS and Cucumber-JS automation framework where I am expecting to see a success, the error mentions that it cannot use import statements outside of a module, then references cucumber's location as the root cause.

import { Before, Given, When, Then } from "@cucumber/cucumber";
^^^^^^

SyntaxError: Cannot use import statement outside a module
    at wrapSafe (internal/modules/cjs/loader.js:1029:16)
    at Module._compile (internal/modules/cjs/loader.js:1078:27)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1143:10)
    at Module.load (internal/modules/cjs/loader.js:979:32)
    at Function.Module._load (internal/modules/cjs/loader.js:819:12)
    at Module.require (internal/modules/cjs/loader.js:1003:19)
    at require (internal/modules/cjs/helpers.js:107:18)
    at /Users/sam.levene/QA/cbfe-playwright-automation/node_modules/@cucumber/cucumber/lib/api/support.js:18:32
    at Array.map (<anonymous>)
    at getSupportCodeLibrary (/Users/sam.levene/QA/cbfe-playwright-automation/node_modules/@cucumber/cucumber/lib/api/support.js:18:18)

βœ… What did you expect to see?

I was expecting to see cucumber accept and work alongside my TypeScript ECMAScript6-compliant code using imports

πŸ“¦ Which tool/library version are you using?

Node.JS (npm v14.21.3) Cucucmber-JS ("@cucumber/cucumber": "^9.0.1") Playwright ("@playwright/test": "^1.31.2")

πŸ”¬ How could we reproduce it?

I honestly don't know how you're going to reproduce it without trying to write some sort of Cucumber/Node.JS automation yourselves.

Quite simply here's some example code for you to try yourselves:

steps.ts

import { Before, Given, When, Then } from "@cucumber/cucumber";
import { Page, chromium } from "@playwright/test";
import { HomePage }  from "../../pages/HomePage";
import { SignInPage } from "../../pages/SignInPage";
import { SignInParameters } from "../../support/SignInParameters";

let homePage: HomePage;
let signInPage: SignInPage;
let signInParameters: SignInParameters;
let page: Page;

Before(async function() {
    var browser = await chromium.launch({
        headless: false,
    });
    var context = await browser.newContext();
    page = await context.newPage();
    homePage = new HomePage(page);
    signInPage = new SignInPage(page);
    signInParameters = new SignInParameters();
});
package.json

{
  "name": "playwright-poc",
  "version": "0.0.1",
  "description": "A Proof of Concept for Playwright",
  "scripts": {
    "test": "npx cucumber-js --require features/step_definitions/steps.ts --exit"
  },
  "type": "module",
  "keywords": [],
  "author": "Sam Levene",
  "license": "ISC",
  "devDependencies": {
    "@playwright/test": "^1.31.2",
    "@cucumber/cucumber": "^9.0.1"
  }
}

and simply try to run that npx script in the terminal of your choice.

πŸ“š Any additional context?

N/A


This text was originally generated from a template, then edited by hand. You can modify the template here.

davidjgoss commented 1 year ago

Unless there's some configuration you haven't shared here, it looks like you don't have anything set up to transpile your TypeScript code to JavaScript that Node.js can interpret.

You probably want to do one of:

There are some docs here offering a bit more detail: https://github.com/cucumber/cucumber-js/blob/main/docs/transpiling.md

Sam-Levene commented 1 year ago

Thanks David.

Can you please confirm what you mean by "if I have TypeScript configured to emit CommonJS or ES modules". How do I check which modules my TypeScript is configured to emit? (I am using VS Code)

EDIT: I tried to follow your link but it really wasn't very helpful. I tried installing ts-node and got the following error:

TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".ts" for /Users/sam.levene/QA/playwright-automation/features/step_definitions/steps.ts
    at new NodeError (internal/errors.js:322:7)
    at Loader.defaultGetFormat [as _getFormat] (internal/modules/esm/get_format.js:71:15)
    at Loader.getFormat (internal/modules/esm/loader.js:105:42)
    at Loader.getModuleJob (internal/modules/esm/loader.js:243:31)
    at async Loader.import (internal/modules/esm/loader.js:177:17)
    at async importer (/Users/sam.levene/QA/playwright-automation/node_modules/@cucumber/cucumber/lib/importer.js:10:10)
    at async getSupportCodeLibrary (/Users/sam.levene/QA/playwright-automation/node_modules/@cucumber/cucumber/src/api/support.ts:33:5)
    at async runCucumber (/Users/sam.levene/QA/cbfe-playwright-automation/node_modules/@cucumber/cucumber/src/api/run_cucumber.ts:45:9)
    at async Cli.run (/Users/sam.levene/QA/playwright-automation/node_modules/@cucumber/cucumber/src/cli/index.ts:78:25)
    at async Object.run [as default] (/Users/sam.levene/QA/playwright-automation/node_modules/@cucumber/cucumber/src/cli/run.ts:38:14)

and my updated package.json:

{
  "name": "playwright-poc",
  "version": "0.0.1",
  "description": "A Proof of Concept for Playwright",
  "scripts": {
    "test": "npx cucumber-js --require-module ts-node/register --import features/step_definitions/steps.ts --exit"
  },
  "type": "module",
  "keywords": [],
  "author": "Sam Levene",
  "license": "ISC",
  "devDependencies": {
    "@cucumber/cucumber": "^9.0.1",
    "@playwright/test": "^1.31.2",
    "ts-node": "^10.9.1"
  }
}
davidjgoss commented 1 year ago

How do I check which modules my TypeScript is configured to emit?

Normally you would have a tsconfig.json in the root of your project. In particular under the compilerOptions the combination of target and module would decide what is output. More from the TypeScript docs here https://www.typescriptlang.org/docs/handbook/2/modules.html. Without a config file, I think it would default to es5 and CommonJS modules, but I'm not sure.

davidjgoss commented 1 year ago

Consider looking at the example TypeScript project here https://github.com/cucumber/cucumber-js-examples/tree/main/examples/typescript-node

Sam-Levene commented 1 year ago

@davidjgoss - Hi David - Thanks for that link. I followed your link and added a cucumber.js file with the following:

module.exports = {
  default: [
    "--require-module ts-node/register",
    "--require features/**/*.ts",
    "--publish-quiet",
  ].join(" "),
};

and I see the following error:

ReferenceError: module is not defined in ES module scope
This file is being treated as an ES module because it has a '.js' file extension and '/Users/sam.levene/QA/playwright-automation/package.json' contains "type": "module". To treat it as a CommonJS script, rename it to use the '.cjs' file extension.
    at file:///Users/sam.levene/QA/playwright-automation/cucumber.js:1:1
    at ModuleJob.run (internal/modules/esm/module_job.js:183:25)
    at async Loader.import (internal/modules/esm/loader.js:178:24)
    at async importer (/Users/sam.levene/QA/playwright-automation/node_modules/@cucumber/cucumber/lib/importer.js:10:10)
    at async loadFile (/Users/sam.levene/QA/playwright-automation/node_modules/@cucumber/cucumber/lib/configuration/from_file.js:56:35)
    at async fromFile (/Users/sam.levene/QA/playwright-automation/node_modules/@cucumber/cucumber/lib/configuration/from_file.js:19:25)
    at async loadConfiguration (/Users/sam.levene/QA/playwright-automation/node_modules/@cucumber/cucumber/lib/api/load_configuration.js:29:11)
    at async Cli.run (/Users/sam.levene/QA/playwright-automation/node_modules/@cucumber/cucumber/lib/cli/index.js:45:71)
    at async Object.run [as default] (/Users/sam.levene/QA/playwright-automation/node_modules/@cucumber/cucumber/lib/cli/run.js:34:18)
Sam-Levene commented 1 year ago

@davidjgoss - Hi David,

I've had a look and it appears that, because I'm using an ES module, it doesn't like the module.exports config in cucumber.js and expects it to be in an ES module format as such:

export const default = [
    "--require-module ts-node/register",
    "--require features/**/*.ts",
    "--publish-quiet",
].join(" ");

the only issue is that default is a reserved word in Node.JS and it doesn't let me compile the config. Can I use a different name here and can Cucumber pick up on it if I do?

davidjgoss commented 1 year ago

For the default profile you'd just need to do export default followed by the value. Also consider using JSON or YAML.

https://github.com/cucumber/cucumber-js/blob/main/docs/configuration.md#files

Sam-Levene commented 1 year ago

Hi @davidjgoss - when I try that, I get the same error as yesterday with an unknown file extension.ts

cucumber.yml

default:
  require-module: ts-node/register
  import: ["features/**/*.ts"]

or

cucumber.js

export default [
    "--require-module ts-node/register",
    "--import features/**/*.ts",
    "--publish-quiet",
].join(" ");

Error when running script:

TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".ts" for /Users/sam.levene/QA/playwright-automation/features/step_definitions/steps.ts
    at new NodeError (internal/errors.js:322:7)
    at Loader.defaultGetFormat [as _getFormat] (internal/modules/esm/get_format.js:71:15)
    at Loader.getFormat (internal/modules/esm/loader.js:105:42)
    at Loader.getModuleJob (internal/modules/esm/loader.js:243:31)
    at async Loader.import (internal/modules/esm/loader.js:177:17)
    at async importer (/Users/sam.levene/QA/playwright-automation/node_modules/@cucumber/cucumber/lib/importer.js:10:10)
    at async getSupportCodeLibrary (/Users/sam.levene/QA/playwright-automation/node_modules/@cucumber/cucumber/src/api/support.ts:33:5)
    at async runCucumber (/Users/sam.levene/QA/playwright-automation/node_modules/@cucumber/cucumber/src/api/run_cucumber.ts:45:9)
    at async Cli.run (/Users/sam.levene/QA/playwright-automation/node_modules/@cucumber/cucumber/src/cli/index.ts:78:25)
    at async Object.run [as default] (/Users/sam.levene/QA/playwright-automation/node_modules/@cucumber/cucumber/src/cli/run.ts:38:14)

EDIT: Forgot to mention here's my tsconfig file:

tsconfig.json

{
    "allowSyntheticDefaultImports": true,
    "resolveJsonModule": true,
    "compilerOptions": {
        "module": "ESNext",
        "moduleResolution": "node"
    }
}
davidjgoss commented 1 year ago

You can't combine --require-module with --import. Since your tsconfig is emitting ESM, you should remove the --require-module and instead use the ESM loader (which you have to set outside of the process e.g. NODE_OPTIONS="--loader ts-node/esm" npx cucumber-js)

Sam-Levene commented 1 year ago

That seems to get me somewhere, now I'm stuck somewhere elsem, to do with esm-resolve and not cuke, so thanks @davidjgoss

apazureck commented 10 months ago

Hi,

I had the same problem. The suggested answers did not work for me. I am also using a shared package.json with my angular project. Finally I found a fix, which works for me and wanted to share it:

  1. Add a tsconfig file to your test folder with the following content:
    {
    "compilerOptions": {
    "module": "NodeNext",
    "target": "ES2022",
    "allowSyntheticDefaultImports": true,
    "resolveJsonModule": true,
    "moduleResolution": "NodeNext"
    }
    }
  2. Go to the folder with this tsconfig.json
  3. Start cucumber-js using the --resolve-module flag set to ts-node/register or put it in your cucumber config file.

This will pick up the tsconfig from the current working directory.

If you have, like me, a project where the modules are resolved from the workspace root, you can set the baseurl in the tsconfi accordingly. So if your cucumber tsconfig is in <root>/e2etest folder for example, just set your baseurl to "../". My IDE (VSCode) autocompletes the imports using the baseurl in my "main" tsconfig.json.

So I hope this helps somebody.

My current versions are:

node: v20.5.0
npm: 9.8.0
"@cucumber/cucumber": "^10.0.1",
"ts-node": "^10.9.1",
"typescript": "~5.1.3",

Here is my current cucumber config (as yaml)

default:
  features:
    - "features/**/*.feature"
  requireModule:
    - "ts-node/register"
  require:
    - "steps/**/*.ts"
    - "setup/**/*.ts"
  format:
    - progress-bar