TypeStrong / ts-loader

TypeScript loader for webpack

Home Page:https://johnnyreilly.com/ts-loader-goes-webpack-5

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

ESM Imports

EisenbergEffect opened this issue · comments

Expected Behaviour

Webpack is able to locate a .ts file and successfully build.

Actual Behaviour

ERROR in ./src/main.ts
Module not found: Error: Can't resolve './log.js'

Steps to Reproduce the Problem

You need two files, a main.ts and a log.ts.

log.ts

export function log() {
  console.log('test');
}

main.ts

import { log } from './log.js'

log();

I'll provide the webpack config below. The above two files produce the error described. Nothing else is needed. Your first thought might be "but you just need to remove the .js file extension to get it to work". However, that isn't possible. To explain, we're trying to emit standards compliant ES Modules that can be run directly in the browser but that can also be bundled with webpack. For the code to run in a browser, the import statements must have a .js file extension. TypeScript lets us put a .js file extension in the imports and will build it correctly on its own. However, when used with webpack, the build fails as described.

I don't know if this is a problem deep in the bowels of Webpack or if it's an issue with ts-loader. Sadly, it's a ship blocker for our library ;(

We'd appreciate help on this very much. I can't find any blog post, docs, etc. about this issue and no one I've talked to has a solution.

Thanks!

webpack.config.js

const path = require('path');

module.exports = function(env, { mode }) {
  const production = mode === 'production';
  return {
    mode: production ? 'production' : 'development',
    devtool: production ? 'source-maps' : 'inline-source-map',
    entry: {
      app: ['./src/main.ts']
    },
    output: {
      filename: 'bundle.js'
    },
    resolve: {
      extensions: ['.ts', '.js'],
      modules: ['src', 'node_modules']
    },
    devServer: {
      port: 9000,
      historyApiFallback: true,
      writeToDisk: true,
      open: !process.env.CI,
      lazy: false
    },
    module: {
      rules: [
        {
          test: /\.ts$/i,
          use: [
            {
              loader: 'ts-loader'
            }
          ],
          exclude: /node_modules/
        }
      ]
    }
  }
}

Hello Rob!

Thanks for the detailed report. That's always appreciated. First and foremost, this is very much something that we'd like to support in ts-loader. If it's possible - that's to be discovered I guess.

I don't have the time to look into this myself at present. (COVID-19 has sent my working life into overdrive and I'm doing much less OSS than I'd like.). However I'm certainly game to try and assist if you want to look into this. We very much accept PRs 🥰

Potential causes as I see them:

  • webpack - does webpack support importing standards compliant ES Modules with a suffix? I suspect the answer is yes but there may be quirks. Can you advise @sokra?

  • TypeScript - given you can build with TypeScript directly maybe this is not a problem. @andrewbranch do you have any insight on this?

  • ts-loader - may not be using all of the relevant APIs in webpack / TypeScript correctly

As I say, I'm very time poor right now but I'll do what I can in terms of providing advice and reviewing any PRs. More than that I cannot offer I'm afraid. I hope it helps 🌻🥰

Hey @johnnyreilly! Thanks for the quick response. I understand the situation, so no worries. I've got a workaround for my project that I think gets us to a pretty good place. It would be cool to get this working eventually, as I know there are others that are trying to target native modules and things just don't quite work right all the time. It may be much broader than just ts-loader and might be better to wait for TS support directly at some point, but in my quest to probe all possibilities, I thought I'd drop this issue here.

You can bundle standard esm modules, but here you only have standard esm imports, but not the standard esm js files matching them. They are probably only generated as compile step for publishing.

So either consume the compiled js files via webpack.
Or create a custom resolver plugin which tries to resolve to a .ts file in addition to the .js file. That should be pretty easy, maybe there already exists one.
Or omit the .js extension and add it after the TS compile step for esm publishing.

Or create a custom resolver plugin which tries to resolve to a .ts file in addition to the .js file. That should be pretty easy, maybe there already exists one.

As @EisenbergEffect pointed out, this is how TypeScript’s module resolution works, so this sounds like the best solution. Could ts-loader itself contribute this behavior, or would it need to be an actual plugin? I know ts-loader finds ways to hook into some of the plugin APIs (like compiler hooks) already.

Could ts-loader itself contribute this behavior, or would it need to be an actual plugin?

Thanks for commenting @andrewbranch! On where this functionality might live I guess it's a choice and I don't feel super strongly either way. I figure that how often this functionality would be used is a good rationale.

Obviously right now this is super niche. But if this is the future of the web™️ then it being out-of-the-box functionality entirely makes sense.

My question is more whether it’s possible to affect Webpack’s module resolution from a loader. It’s definitely something we’d want since ts-loader aims to mirror tsc’s behavior as closely as possible.

Yeah - I'm not sure. @sokra can you advise?

Loaders shouldn't do that, but you can put it next to the ts-loader in the module.rules item.

test: /\.ts$/,
use: "ts-loader",
resolve: { plugins: [
  new TsResolvePlugin()
] }
commented

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

commented

Closing as stale. Please reopen if you'd like to work on this further.

I guess this is still an issue? @sokra does the TsResolvePlugin already exist?

Hmmm... This is still an issue I think.

TypeScript resolves foo.js as foo.ts (or foo.tsz) fine but WebPack breaks.

This is not so good because it implies that we need to handle different syntax and import styles for ESM modules included in the app and the app itself...

Maybe the solution is to build the app as an ESM module and just import 'app' inside the the app loader. Not sure what this does to optimisations though.

I also encountered this issue trying to set up webpack to resolve modules in accordance with ESM. My goal was to have webpack resolve modules as close as possible to node, ts-node, and browsers, for consistency and to reduce developer confusion. Setting fullySpecified: true got me 90% of the way there, but TypeScript was a sticking point as described here.

I've developed a simple webpack plugin (actually an enhanced-resolve plugin, but enhanced-resolve is part of webpack) to try to solve the problem. I'm not sure of my approach and I'm seeking community feedback, so if anyone interested in this problem, and especially anyone involved with webpack internals, could take a look and let me know what they think I'd really appreciate it. See this issue for submitting feedback.

To use the plugin, install resolve-typescript-plugin from npm and then configure webpack something like this:

const ResolveTypeScriptPlugin = require("resolve-typescript-plugin").default;

exports = {
    module: {
        rules: [
            {
                test: /\.tsx?$/,
                use: "ts-loader"
            }
        ]
    },
    resolve: {
        fullySpecfied: true,
        plugins: [new ResolveTypeScriptPlugin()]
    }
};

I hope it's ok to mention this here. My goal is to draw attention to my attempt at solving this problem to anyone who is interested. If you have any feedback for me about the plugin please respond in the plugin repository and not here, to avoid excess noise for the developers of ts-loader.

EDIT: Note that the actual code in the GitHub repository for the plugin is all in the alpha branch.

Thanks for sharing - it looks like the https://github.com/softwareventures/resolve-typescript-plugin link doesn't work?

D'oh, sorry about that. I set it private by mistake. The link should work now.

I think there might still be something wrong...

https://github.com/softwareventures/resolve-typescript-plugin/blob/main/index.ts

There's no code in index.ts

@johnnyreilly The code is in the alpha branch. Sorry for the confusion.

Edit: I've set the primary branch to alpha for now to make it more obvious where the code is.

Have you considered a side career in the word search and crossword industry? 😉

Heads up there is a proper release of the above plugin now, and the code is all on the main branch where you would expect to find it :-).

Heads up to anyone facing this issue that Webpack itself provides a proper solution now.

module.exports = {
  //...
  resolve: {
    extensionAlias: {
      '.js': ['.ts', '.js'],
      '.mjs': ['.mts', '.mjs'],
    },
  },
};

See: https://webpack.js.org/configuration/resolve/#resolveextensionalias

I also recommend:

module.exports = {
  //...
  resolve: {
    enforceExtension: true,
  },
};

to enforce use of file extensions for full compatibility with ES Modules.

If for some reason you can't use a recent version of Webpack that includes these options, use https://github.com/softwareventures/resolve-typescript-plugin

Thanks for this @djcsdy - do you fancy submitting a README.md docs PR to include these points?