ava icon indicating copy to clipboard operation
ava copied to clipboard

Update TypeScript recipe for ESM support

Open novemberborn opened this issue 3 years ago • 15 comments

@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.

novemberborn avatar Oct 03 '20 15:10 novemberborn

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)

thasmo avatar Feb 05 '21 17:02 thasmo

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 avatar Feb 20 '21 07:02 fregante

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
}

fregante avatar Feb 20 '21 19:02 fregante

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;

mikob avatar Jun 09 '21 00:06 mikob

@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.

novemberborn avatar Jun 09 '21 07:06 novemberborn

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.

mikob avatar Jun 12 '21 00:06 mikob

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

novemberborn avatar Jun 13 '21 14:06 novemberborn

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

  • https://github.com/TypeStrong/ts-node/issues/1007#issuecomment-917677011

fregante avatar Sep 13 '21 08:09 fregante

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
  }
}

Akiyamka avatar Nov 24 '21 12:11 Akiyamka

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

Cobertos avatar Dec 08 '21 01:12 Cobertos

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

rrichardson avatar Dec 14 '21 18:12 rrichardson

I'm getting:

✖ nonSemVerExperiments.configurableModuleFormat from undefined is not a supported experiment

mesqueeb avatar Jan 20 '22 13:01 mesqueeb

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

novemberborn avatar Jan 20 '22 14:01 novemberborn

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.

Grawl avatar Jul 08 '22 08:07 Grawl

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 🎉

  • https://github.com/refined-github/github-url-detection/pull/143

fregante avatar Jul 28 '22 07:07 fregante

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",
  ]
}

plantain-00 avatar Dec 10 '22 04:12 plantain-00

@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)

fregante avatar Feb 03 '23 07:02 fregante

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?

novemberborn avatar Feb 06 '23 20:02 novemberborn

~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

Sparticuz avatar Feb 08 '23 21:02 Sparticuz

@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.

MartynasZilinskas avatar Mar 13 '23 20:03 MartynasZilinskas

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

ondreian avatar Mar 31 '23 13:03 ondreian

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.

novemberborn avatar Apr 10 '23 14:04 novemberborn

@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.

Sparticuz avatar Apr 14 '23 13:04 Sparticuz

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

novemberborn avatar Apr 16 '23 17:04 novemberborn

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.

{
  "scripts": {
    "ava:run": "NODE_OPTIONS='--loader=tsx --no-warnings' ava"
  },
  "ava": {
    "extensions": {
      "ts": "module"
    }
  }
}

LangLangBart avatar Apr 27 '23 06:04 LangLangBart

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.

punkpeye avatar May 07 '23 00:05 punkpeye

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'

firxworx avatar Mar 25 '24 17:03 firxworx