Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ESM in webpack.config.ts isn't supported if ts-node outputs ESM #2458

Closed
fregante opened this issue Feb 20, 2021 · 24 comments · Fixed by #2584
Closed

ESM in webpack.config.ts isn't supported if ts-node outputs ESM #2458

fregante opened this issue Feb 20, 2021 · 24 comments · Fixed by #2584
Labels

Comments

@fregante
Copy link

fregante commented Feb 20, 2021

Describe the bug

If you use:

  • webpack.config.ts with ts-node
  • export default inside it
  • "module": "esnext" in your tsconfig.json, or any ES
  • regardless of your type config in package.json

ts-node will transpile the configuration into JavaScript, preserving export default, but the file is still being executed as CJS.

ESM config files have just recently been supported (#2381, v4.5.0) but my guess is that ts-node or webpack is running the transpiled file outside the type: module directory.

What is the current behavior?

[webpack-cli] Failed to load './webpack.config.ts' config
[webpack-cli] ./webpack.config.ts:2
import path from 'path';
^^^^^^

SyntaxError: Cannot use import statement outside a module

To Reproduce

Steps to reproduce the behavior:

echo '{"type": "module"}' > package.json
echo 'export default {}' > webpack.config.ts
echo 'console.log("hello world")' > index.js
echo '{"compilerOptions":{"module":"esnext", "allowSyntheticDefaultImports":true}}' > tsconfig.json
npm install webpack-cli webpack typescript ts-node
npx webpack-cli --entry ./index.js --mode production

A full project is also here: refined-github/refined-github#4002

Please paste the results of webpack-cli info here, and mention other relevant information

  System:
    OS: macOS 10.15.7
    CPU: (8) x64 Intel(R) Core(TM) i7-4850HQ CPU @ 2.30GHz
    Memory: 483.65 MB / 16.00 GB
  Binaries:
    Node: 15.2.1 - /usr/local/bin/node
    npm: 7.0.8 - /usr/local/bin/npm
  Packages:
    webpack: ^5.23.0 => 5.23.0 
    webpack-cli: ^4.5.0 => 4.5.0 
  Global Packages:
    webpack-cli: 4.5.0
    webpack: 5.23.0
@fregante
Copy link
Author

Temporary solution:

TS_NODE_COMPILER_OPTIONS='{"module": "commonjs"}' webpack --mode=production

@fregante fregante changed the title ESM in webpack.config.ts isn't supported with module: "esnext" in tsconfig ESM in webpack.config.ts isn't supported if TS outputs ESM Feb 21, 2021
@fregante fregante changed the title ESM in webpack.config.ts isn't supported if TS outputs ESM ESM in webpack.config.ts isn't supported if ts-node outputs ESM Feb 21, 2021
fregante added a commit to refined-github/refined-github that referenced this issue Feb 22, 2021
Follows #4002

Of course the instant I press "Merge" I got some new idea.

This has no effect on the build size, but it should all be dropped (except the timeout) once webpack/webpack-cli#2458 is solved.
@alexander-akait
Copy link
Member

Interesting, somebody wrap code from ts to

(function (exports, require, module, __filename, __dirname) { export default {}; }())

@alexander-akait
Copy link
Member

alexander-akait commented Feb 24, 2021

Related TypeStrong/ts-node#1007, we need allow to pass register, so you can use ts-node/esm

@alexander-akait
Copy link
Member

I'll investigate it in detail tomorrow, but to be honest look like ts-node is not ready for using import()

@alexander-akait
Copy link
Member

alexander-akait commented Mar 16, 2021

@fregante

Sorry for delay, I think nothing to fix on our side, I am using (and everything works fine):

node --loader ts-node/esm node_modules/.bin/webpack --config webpack.config.ts

We need Node.js loader for ts files, otherwise it will not work.

From ts-node docs:

Cannot be invoked as ts-node because it requires node flags; hooks cannot be enabled at runtime. This is unavoidable.

@alexander-akait
Copy link
Member

Here interesting case, we can require files with ES module syntax, very weird, but it is behavior from ts-node

@alexander-akait
Copy link
Member

@fregante friendly ping

@fregante
Copy link
Author

fregante commented Mar 22, 2021

Ping for what? Are you suggesting that using node --loader ts-node/esm node_modules/.bin/webpack --config webpack.config.ts instead of webpack is the nothing-to-fix-here solution?

@alexander-akait
Copy link
Member

alexander-akait commented Mar 22, 2021

For using ts-node and ECMA modules you need pass loader to Node.js (i.e. --loader ts-node/esm) otherwise it will not work, I just don't know what we can do here, we can't add loader in runtime, no API from Node.js here

@alexander-akait
Copy link
Member

alexander-akait commented Mar 31, 2021

@fregante what about:

NODE_OPTIONS=--loader=ts-node/esm webpack --config webpack.config.ts

@fregante
Copy link
Author

fregante commented Mar 31, 2021

I already have a way to execute it and it’s in my second comment. Ideally one should only call $ webpack without further inline settings. Unless my answer helps you find a way to implement it, I don’t see the need to try anything else.

As far as I’m concerned, this is a webpack issue of not being able to run ESM TS config, rather than a question on how to achieve that 😅

@alexander-akait
Copy link
Member

As far as I’m concerned, this is a webpack issue of not being able to run ESM TS config sweat_smile

Here problem on ts-node side, they can't handle require()/import() for ts files with ES modules output, they need inject loader (it allows to adding own logic, described in the issue).

TS_NODE_COMPILER_OPTIONS='{"module": "commonjs"}' webpack --mode=production

It generates commonjs format, in my example above ts generates ES modules format (it is different, in future some packages can drop commonjs format so you can't load these packages using require, maybe ts already handle this case and allow to require ES modules format (need test), but for long term you should avoid it in ts code) and webpack-cli loader ES format.

rather than a question on how to achieve that sweat_smile

My comment above should clarify that these are not really attempts to achieve, they are cardinally different things.

@fregante
Copy link
Author

Here problem on ts-node side,

Indeed, with so many moving parts I can understand that this is not strictly something that can be fixed locally.

TS_NODE_COMPILER_OPTIONS='{"module": "commonjs"}' webpack --mode=production

It generates commonjs format,

Yes, but it generates commonjs format of the webpack config file, not webpack’s output, which is not transpiled by ts-node. In my (only) test, the webpack output is identical byte for byte.

@alexander-akait
Copy link
Member

Yes, it is not related to webpack output, it is related to output ts-node, i.e.

webpack.config.ts -> ts-node -> webpack.config.ts with ES modules format in memory -> webpack-cli -> import('path/to/webpack.config.ts') in our code -> ts-node returns ES modules format -> run webpack with your options

webpack-cli -> import('path/to/webpack.config.ts') in our code

We support commonjs and ES format, so firstly we try to loader using require, if it fails with error.code === 'ERR_REQUIRE_ESM' (special code for this case) we try to load it using import(). But here problems, without --loader=ts-node/esm import() will not work with ts-node - need runtime hooks (described here TypeStrong/ts-node#1007 and marked not fixable).

I think you will faced with this issue in future many times for other packages with configurations.

@cspotcode
Copy link

Came across this issue and wanted to mention a couple helpful things:

Alternative to setting TS_NODE_COMPILER_OPTIONS='{"module": "commonjs"}', you can add this override to your tsconfig.json file:

  "ts-node": {
    "compilerOptions": { "module": "commonjs" }
  }

Unfortunately the typescript compiler insists on converting dynamic import() into require() when using "commonjs" and we can't override that. However, I've explained a couple workarounds here: TypeStrong/ts-node#1290

I'm not sure if webpack has special-case logic for a CommonJS config file to return a promise, though.

@fregante
Copy link
Author

fregante commented May 24, 2021

  1. The whole point using that inline setting instead of tsconfig is to avoid those errors in the real code
  2. Using eval/Function is a really poor runtime workaround. I suggest using the ENV as suggested here instead and using non-workaround code in production.

@cspotcode
Copy link

cspotcode commented May 24, 2021

The whole point using that inline setting instead of tsconfig is to avoid those errors in the real code

The tsconfig tweak I proposed is equivalent to the environment variable in that it only affects ts-node. But I see what you're saying: in refined-github, you are also using ts-node elsewhere, so you need ts-node to emit commonjs for webpack and esnext for ava. Could be done with TS_NODE_PROJECT in the ava config if you really wanted to drop the cross-env dependency, but I can't say if it's worth it. EDIT: and now I see that's already been done


This fix may reduce confusion in these situations: TypeStrong/ts-node#1229
When you try to require() a .ts file that should be ESM, we'll throw the correct ERR_REQUIRE_ESM even when --loader isn't set. At least it avoids the misleading Cannot use import statement outside a module However, it means we no longer allow .ts in an ESM directory ("type": "module") to be erroneously executed as CJS.


Using eval/Function is a really poor runtime workaround.

It's certainly not my first suggestion, and likely not relevant if "module": "commonjs" is already working. There is another suggestion which avoids Function.

All of this is ultimately working around node and typescript feature gaps, so here's hoping they fill them soon.

@MikkCZ
Copy link

MikkCZ commented Jun 16, 2022

FYI as of today for me to be able to import an ESM module (webpack plugin) from webpack.config.ts, I needed to add the following to the tsconfig.json

{
  // ...
  "ts-node": {
    // Tell ts-node CLI to install the --loader automatically, explained below
    "esm": true,
    "compilerOptions": {
      "module": "CommonJS",
      "moduleResolution": "NodeNext",
    }
  },
  // ...

It's based on your responses above. "ts-node" itself works instead of TS_NODE_COMPILER_OPTIONS as already mentioned here, plus "esm": true replaces the need for passing --loader ts-node/esm to Node (see https://typestrong.org/ts-node/docs/imports).

node: v16.14.1
ts-node: v10.8.1
typescript: 4.7.3
webpack: 5.73.0
webpack-cli: 4.10.0

MikkCZ added a commit to MikkCZ/pontoon-addon that referenced this issue Jun 16, 2022
Required addional compiler options for ts-node to be able to handle ESM
modules, which web-ext and web-ext-plugin now use. Also Webpack
configuration needed to be split for ESLint to still be able to use it.
See webpack/webpack-cli#2458 for details
regarding ts-node and ESM.
MikkCZ added a commit to MikkCZ/pontoon-addon that referenced this issue Jun 16, 2022
Required addional compiler options for ts-node to be able to handle ESM
modules, which web-ext and web-ext-plugin now use. Also Webpack
configuration needed to be split for ESLint to still be able to use it.
See webpack/webpack-cli#2458 for details
regarding ts-node and ESM.
@digiperfect-janak
Copy link

"ts-node": {
    // Tell ts-node CLI to install the --loader automatically, explained below
    "esm": true,
    "compilerOptions": {
      "module": "CommonJS",
      "moduleResolution": "NodeNext",
    }
  },

I still get TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".ts" after adding this to tsconfig.json
Any pointers please?

@MikkCZ
Copy link

MikkCZ commented Mar 9, 2023

@digiperfect-janak here is my full tsconfig.json, but I basically followed what was mentioned above my comment.

Do you have ts-node installed? Does any of the above-mentioned solutions with TS_NODE_COMPILER_OPTIONS work for you?

@digiperfect-janak
Copy link

Thanks. I will try with the extended version of tsconfig you shared. I do have ts-node installed.

TS_NODE_COMPILER_OPTIONS='{"module": "commonjs"}' node_modules/webpack/bin/webpack.js --mode=production
gave the following error:

[webpack-cli] Failed to load '/home..../webpack.config.ts' config

If I am not mistaken, bin/webpack.js is what is supposed to be invoked?

@iclegane
Copy link

iclegane commented Nov 3, 2023

in package.json:

scripts -> "start": "ts-node node_modules/webpack-dev-server/bin/webpack-dev-server.js --config urPathToWebpackConfig.ts --mode development",
"type": "module"

and my tsconfig:

  "ts-node": {
    "esm": true,
    "experimentalSpecifierResolution": "node"
  },
  "compilerOptions": {...}

This solved my problem

@wojtekmaj
Copy link

I think I came up with a bit more elegant solution. Simply prepend your commands referencing webpack with

NODE_OPTIONS='--loader ts-node/esm'

If you had

  "ts-node": {
    "compilerOptions": {
      "module": "commonjs",
      "verbatimModuleSyntax": false
    }
  },

or something similar in your tsconfig.json, you should remove it.

@stasguma
Copy link

the best solution for me was:

install cross-env and tsx packages

package.json

{
  ...,
  "type": "module",
  "scripts": {
      "start": "cross-env NODE_OPTIONS='--import=tsx' webpack serve --env development --config webpack.config.ts"
  }
}

pay attention to the equal sign after --import, because all the examples I saw were without it and it caused an error

tsconfig.json doesn't matter

Node: 20.11.0
TypeScript: 5.3.3
cross-env: 7.0.3
tsx: 4.7.0

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging a pull request may close this issue.

8 participants