Skip to content

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

Closed

Description

@fregante

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

Activity

  1. fregante commented on Feb 20, 2021

    @fregante
    Author

    Temporary solution:

    TS_NODE_COMPILER_OPTIONS='{"module": "commonjs"}' webpack --mode=production
    
  2. 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[/+] on Feb 21, 2021
  3. 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[/+] on Feb 21, 2021
  4. added a commit that references this issue on Feb 22, 2021
  5. alexander-akait commented on Feb 24, 2021

    @alexander-akait
    Member

    Interesting, somebody wrap code from ts to

    (function (exports, require, module, __filename, __dirname) { export default {}; }())
  6. alexander-akait commented on Feb 24, 2021

    @alexander-akait
    Member

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

  7. alexander-akait commented on Feb 24, 2021

    @alexander-akait
    Member

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

  8. alexander-akait commented on Mar 16, 2021

    @alexander-akait
    Member

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

  9. alexander-akait commented on Mar 16, 2021

    @alexander-akait
    Member

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

  10. alexander-akait commented on Mar 22, 2021

    @alexander-akait
    Member

    @fregante friendly ping

  11. fregante commented on Mar 22, 2021

    @fregante
    Author

    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?

  12. alexander-akait commented on Mar 22, 2021

    @alexander-akait
    Member

    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

  13. 3 remaining items

  14. fregante commented on Mar 31, 2021

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

  15. alexander-akait commented on Mar 31, 2021

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

  16. cspotcode commented on May 23, 2021

    @cspotcode

    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.

  17. fregante commented on May 24, 2021

    @fregante
    Author
    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.
  18. cspotcode commented on May 24, 2021

    @cspotcode

    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.

  19. MikkCZ commented on Jun 16, 2022

    @MikkCZ

    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

  20. digiperfect-tech commented on Mar 9, 2023

    @digiperfect-tech
    "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?

  21. MikkCZ commented on Mar 9, 2023

    @MikkCZ

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

  22. digiperfect-tech commented on Mar 9, 2023

    @digiperfect-tech

    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?

  23. iclegane commented on Nov 3, 2023

    @iclegane

    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

  24. wojtekmaj commented on Nov 16, 2023

    @wojtekmaj

    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.

  25. stasguma commented on Jan 30, 2024

    @stasguma

    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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions