avajs / ava

Node.js test runner that lets you develop with confidence 🚀
MIT License
20.74k stars 1.41k forks source link

Update TypeScript recipe for ESM support #2593

Closed novemberborn closed 1 year ago

novemberborn commented 4 years ago

@FallingSnow shared how to (experimentally) configure AVA and Node.js so that AVA can load TypeScript-based ESM files:

"ava": {
    "extensions": {
      "ts": "module"
    },
    "nonSemVerExperiments": {
      "configurableModuleFormat": true
    },
    "nodeArguments": [
      "--loader=ts-node/esm",
      "--experimental-specifier-resolution=node"
    ],
    "files": [
      "test/**/*.spec.ts"
    ]
}

It'd be great to add this to our TypeScript recipe.

thasmo commented 3 years ago

Update Somehow I missed to also declare --experimental-specifier-resolution=node which was the issue after all. Thanks!


Trying to run TypeScript tests with ESM support, but failed to do so with the configuration above, adapted to my setup.

export default {
    nonSemVerExperiments: {
        nextGenConfig: true,
        configurableModuleFormat: true,
    },
    nodeArguments: [
        '--loader=ts-node/esm',
        '--experimental-specifier-resolution=node',
    ],
    extensions: {
        ts: 'module',
    },
    require: [
        'ts-node/register/transpile-only',
    ],
    files: [
        'packages/*/test/**/*.ts',
    ],
}

I'm running it with Node 14 and TypeScript 4, which results in this errors:

➜ npm test

> test
> ava

âš  Experiments are enabled. These are unsupported and may change or be removed at any time.

(node:13990) ExperimentalWarning: --experimental-loader is an experimental feature. This feature could change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
(node:13996) ExperimentalWarning: --experimental-loader is an experimental feature. This feature could change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
(node:13997) ExperimentalWarning: --experimental-loader is an experimental feature. This feature could change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
(node:14010) ExperimentalWarning: --experimental-loader is an experimental feature. This feature could change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
(node:14004) ExperimentalWarning: --experimental-loader is an experimental feature. This feature could change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
(node:14025) ExperimentalWarning: --experimental-loader is an experimental feature. This feature could change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
(node:14018) ExperimentalWarning: --experimental-loader is an experimental feature. This feature could change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
(node:14031) ExperimentalWarning: --experimental-loader is an experimental feature. This feature could change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
(node:14038) ExperimentalWarning: --experimental-loader is an experimental feature. This feature could change at any time
(Use `node --trace-warnings ...` to show where the warning was created)

  ✖ No tests found in packages/pack-cache/test/pack.ts
  ✖ No tests found in packages/pack-environment/test/pack.ts
  ✖ No tests found in packages/pack-watch/test/pack.ts
  ✖ No tests found in packages/pack-less/test/pack.ts
  ✖ No tests found in packages/core/test/packs/optimization.ts
  ✖ No tests found in packages/core/test/packmule.ts
  ✖ No tests found in packages/pack-log/test/pack.ts
  ✖ No tests found in packages/core/test/packs/base.ts
  ✖ No tests found in packages/core/test/packs/minification.ts

  ─

Uncaught exception in packages/pack-cache/test/pack.ts

Error: ERR_UNSUPPORTED_DIR_IMPORT /mnt/c/Users/thasmo/Projects/packmule.packmule/packages/pack-cache/src/ /mnt/c/Users/thasmo/Projects/packmule.packmule/packages/pack-cache/test/pack.ts

  › finalizeResolution (node_modules/ts-node/dist-raw/node-esm-resolve-implementation.js:370:17)
  › moduleResolve (node_modules/ts-node/dist-raw/node-esm-resolve-implementation.js:809:10)
  › Object.defaultResolve (node_modules/ts-node/dist-raw/node-esm-resolve-implementation.js:920:11)
  › node_modules/ts-node/src/esm.ts:55:38
  › Generator.next (<anonymous>)
  › node_modules/ts-node/dist/esm.js:8:71
  › __awaiter (node_modules/ts-node/dist/esm.js:4:12)
  › resolve (node_modules/ts-node/dist/esm.js:31:16)
fregante commented 3 years ago

I think the configuration above needs to be paired with a tsconfig.json containing:

{
    "compilerOptions": {
        "target": "es2020",
        "module": "commonjs", // Without this, TS will still generate ESM and fail
        "esModuleInterop": true
    }
}

Without that module, I get the regular old error:

  import test from 'ava';
  ^^^^^^

  SyntaxError: Cannot use import statement outside a module

However, I still can't get this to work with external ES Module dependencies, because it appears that everything is treated as CJS and my import 'some-esm-module' becomes require('some-esm-module') and therefore I get:

  Error [ERR_REQUIRE_ESM]: Must use import to load ES Module: /Users/rico/Web/projects-extensions/refined-github/node_modules/select-dom/index.js
  require() of ES modules is not supported.
  require() of ./node_modules/select-dom/index.js from ./file/index.ts is an ES module file as it is a .js file whose nearest parent package.json contains "type": "module" which defines all .js files in that package scope as ES modules.
  Instead rename index.js to end in .cjs, change the requiring code to use import(), or remove "type": "module" from ./node_modules/select-dom/package.json.

It seems to me, as it was last year, that if you use both CJS and ESM dependencies there's still no support in AVA + TS + mixed dependencies. This is simply because TypeScript won't leave imports/requires alone, it will always change them to whatever module is.

That's the problem in my experience.

fregante commented 3 years ago

I can confirm that the first configuration works, even without files:

// package.json
{
    "ava": {
        "extensions": {
            "ts": "module"
        },
        "nodeArguments": [
            "--loader=ts-node/esm",
            "--experimental-specifier-resolution=node"
        ],
        "nonSemVerExperiments": {
            "configurableModuleFormat": true
        }
    }
}

As long as it's paired with:

// tsconfig.json
{
    "compilerOptions": {
        "module": "ESNext", // Or any ES version
        "esModuleInterop": true // Or else TypeScript will complain about CJS packages, even if Node can `import` them
    }
}

and

// package.json
{
    "type": "module" // Because the TS output is now ESM
}
mikob commented 3 years ago

This doesn't work with default exports for me.

e.g.

import Visitor from "@swc/core/Visitor.js";
console.log('Visitor', Visitor);

prints: Visitor { default: [class Visitor] }

@swc/core/Visitor looks like this:

"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
class Visitor { ... }
exports.default = Visitor;
novemberborn commented 3 years ago

@mikob that's a typical interoperability pattern to make CJS behave like ESM. However now that you're using actual ESM, it does not behave the way you expect. See https://nodejs.org/api/esm.html#esm_import_statements:

When importing CommonJS modules, the module.exports object is provided as the default export. Named exports may be available, provided by static analysis as a convenience for better ecosystem compatibility.

You might be able to import { default as Visitor } from … but default is a special member so that probably won't work.

mikob commented 3 years ago

You might be able to import { default as Visitor } from … but default is a special member so that probably won't work.

Tried that as well without success. In the end I reverted to using "esm" (https://github.com/standard-things/esm) in the "requires" property for ava.

novemberborn commented 3 years ago

That might paper over the cracks for now, yes. AVA 4 removes the special treatment for that package though.

fregante commented 3 years ago

Node 16.10 or later will break the ESM loader, not sure if this affects this recipe.

Akiyamka commented 2 years ago

At this moment ts-node/esm can't handle some edge-cases . More stable alternative is esbuild-node-loader. Es-build work in transpile only mode, but work much faster.

"ava": {
  "extensions": {
    "ts": "module"
  },
  "nodeArguments": [
    "--loader=esbuild-node-loader",
    "--experimental-specifier-resolution=node"
  ],
  "nonSemVerExperiments": {
    "configurableModuleFormat": true
  }
}
Cobertos commented 2 years ago

The above config is much better.

The default installation using ts-node/esm was very slow. I had to increase the timeout for my tests.

I was also getting very odd failures. I narrowed down a couple problem tests and found that ES6 features that were working previously (in js before migrating to TypeScript) were behaving differently. Awaiting an object that wasn't a Promise was giving unexpected values. for...of loops over a custom Iterable would always received undefined, but if I called .next() on them myself, they seemed to work just fine.

After switching to the above (esbuild-node-loader) all my tests passed and it was much faster

rrichardson commented 2 years ago

I created a small PR to update the typescript recipe per the above (I independently found the solution.. searching the issues here would have saved me some time :) )

https://github.com/avajs/ava/pull/2910

mesqueeb commented 2 years ago

I'm getting:

✖ nonSemVerExperiments.configurableModuleFormat from undefined is not a supported experiment

novemberborn commented 2 years ago

@mesqueeb see https://github.com/avajs/ava/issues/2945.

Grawl commented 2 years ago

just started to work with AVA, and I want to replace Jest with it on my regular Webpack project (with Quasar Framework under the hood).

I have a lot of .ts files written with import/export and a few tests using some of them.

No one guide helped me with setup because I have Typescript files written in ESM. I tried a few configs from issues here and from guides of AVA and @ava/typescript and each of them led me to a problem like "AVA cannot run ESM" or "cannot run ESM outside of a module" or "unknown extension: typescript".

But I do not want to compile tests before running, and I cannot add "type": "module" to my package.json because Webpack and other tools goes crazy with it.

Only one working solution it using esbuild like @Akiyamka offers. Here's my working config:

module.exports = {
    'extensions': {
        'ts': 'module',
    },
    'nodeArguments': [
        '--loader=esbuild-node-loader',
        '--experimental-specifier-resolution=node',
    ],
}

Also I have to yarn add -D esbuild-node-loader but it's fast and safe I think.

fregante commented 2 years ago

I'm getting ERR_IMPORT_ASSERTION_TYPE_MISSING when importing a JSON file in Node 16.16 but Node 16.14 works fine 😅

Replacing ts-node/esm with esbuild-node-loader worked 🎉

plantain-00 commented 1 year ago

esbuild-node-loader is deprecated: https://github.com/antfu/esbuild-node-loader I use tsx(https://github.com/esbuild-kit/tsx) instead and it works fine for me:

{
  "extensions": {
    "ts": "module"
  },
  "nodeArguments": [
    "--loader=tsx",
  ]
}
fregante commented 1 year ago

@novemberborn can you update the first post with @plantain-00’s and hide the outdated comments? Thankfully now the amount of config is reduced (tested on Node 18.13)

Or maybe this issue should be closed since https://github.com/avajs/ava/blob/main/docs/recipes/typescript.md seems to be up to date (although it uses ts-node)

novemberborn commented 1 year ago

Or maybe this issue should be closed since main/docs/recipes/typescript.md seems to be up to date (although it uses ts-node)

That works for me, though you're suggesting it's not ideal?

Sparticuz commented 1 year ago

~ts-node seems to be unmaintained~, @plantain-00's solution should probably be what should be on https://github.com/avajs/ava/blob/main/docs/recipes/typescript.md

Maybe including a link to this page would be helpful to determine which loader the user would want to use https://github.com/privatenumber/ts-runtime-comparison

edit: I was looking at the original repo, not typestrongs

MartynasZilinskas commented 1 year ago

@Sparticuz for the performance reasons tsx looks like a good option. But has no support for emitDecoratorMetadata. I think ts-node has only drawback, that is performance.

I agree that we should have a link for other possible loaders, so you could choose for a specific case.

ondreian commented 1 year ago

After several hours of head scratching and hair pulling trying to setup ava on a serverless project, the tsx example provided by @plantain-00 is the only loader that works correctly with Typescript setups that cannot use "type": "module".

This should probably be added to the documentation as the current boilerplate does not work at all and always seems to error with:

  Uncaught exception in test/api.spec.ts

  <project>/test/api.spec.ts:1
  import anyTest, {TestFn} from "ava"
  ^^^^^^

  SyntaxError: Cannot use import statement outside a module
novemberborn commented 1 year ago

This should probably be added to the documentation as the current boilerplate does not work at all

A PR would be welcome!

Personally I use https://github.com/avajs/typescript in my projects and compile TypeScript separately, which removes the dependency on various loaders and complications with the module systems.

Sparticuz commented 1 year ago

@novemberborn Does running ava on your compiled files affect coverage? I guess when I was compiling to cjs there were extra code paths that made it difficult to cover, but I'm not sure I've tried since switching to esm.

novemberborn commented 1 year ago

@Sparticuz https://github.com/bcoe/c8 does the trick.

LangLangBart commented 1 year ago

esbuild-node-loader is deprecated: https://github.com/antfu/esbuild-node-loader I use tsx(https://github.com/esbuild-kit/tsx) instead and it works fine for me:

{
  "extensions": {
    "ts": "module"
  },
  "nodeArguments": [
    "--loader=tsx",
  ]
}

The solution above worked for me up to nodejs/node version v19.9.0.


Starting with node v20.0.0 (18/Apr/23) there are a lot of errors.

TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".ts"

The workaround was to tweak the package.json as follows and start ava with npm run ava:run.

punkpeye commented 1 year ago

Note that in the above, setting nodeArguments: ['--loader=tsx'], does not work as a replacement for NODE_OPTIONS. You must start ava using NODE_OPTIONS just how @LangLangBart demonstrated.

firxworx commented 7 months ago

I stumbled on this while searching about an issue with node + esm loading nothing to do with ava...

Above there is a good suggestion for workaround to set the environment: NODE_OPTIONS='--loader=tsx --no-warnings' (make sure you have tsx installed as a dev dependency!) -- https://github.com/avajs/ava/issues/2593#issuecomment-1524846453

I wanted to add for the sake of anyone else who ends up here that --loader only works for earlier versions of Node 20 and for later versions like the current LTS you would need to use --import in its place: NODE_OPTIONS='--import=tsx --no-warnings'