ts-loader icon indicating copy to clipboard operation
ts-loader copied to clipboard

ts-loader with Typescript 4.5 beta: "Error: TypeScript emitted no output for .../index.mts"

Open JasonKleban opened this issue 4 years ago • 38 comments

Anyone have luck webpack-5-ing with the new .mts extension? I'm able to output from tsc -p . fine, but through webpack I'm getting "Error: TypeScript emitted no output for .../index.mts".

Thanks!

    resolve: {
      // Add `.ts` and `.tsx` as a resolvable extension.
      extensions: [".mts", ".ts", ".tsx", ".js"]
    },
    module: {
      rules: [
        {
            test: /(\.mts$)|(\.tsx?$)/,
            use: [
                {
                    loader: 'ts-loader',
                    options: {
                        //transpileOnly: true
                        compilerOptions: {
                            noEmit: false
                        }
                    }
                }
            ]
        }
      ]
    }

JasonKleban avatar Oct 11 '21 19:10 JasonKleban

Haven't tried this at all - am interested with how this pans out!

johnnyreilly avatar Oct 11 '21 19:10 johnnyreilly

Yes, I believe it is because of the inclusion of the eventual extension.

import { foo } from "./misc" works as always import { foo } from "./misc.ts" is wrong and gives TS2691: An import path cannot end with a '.ts' extension. Consider importing './misc.js' instead.

but

import { foo } from "./misc.js"; works with tsc but fails only in webpacking with Module not found: Error: Can't resolve './misc.js' in 'C:\example\src' resolve './misc.js'

Does ts-loader need to correct these paths for resolution? Or what?

JasonKleban avatar Oct 12 '21 21:10 JasonKleban

I'm not sure - webpack may need to be configured directly to support modules. Probably worth checking that out first. It may be that ts-loader just works with appropriate config. Or tweaks may be required. Please feel free to investigate; I'll assist where I can.

johnnyreilly avatar Oct 13 '21 05:10 johnnyreilly

I suspect this issue may be interesting to you @JasonKleban https://github.com/webpack/webpack/issues/13252

johnnyreilly avatar Oct 13 '21 17:10 johnnyreilly

It's possible that @alexander-akait's advice here may be what you need:

https://github.com/webpack/webpack/issues/13252#issuecomment-828584022

Simple: we should not do this https://github.com/TypeStrong/ts-loader/blob/main/src/resolver.ts, instead we should use this.getResolve (simple example https://github.com/webpack-contrib/less-loader/blob/master/src/utils.js#L35), by default you can set extensions: [".ts", "..."], so in future developers don't need to set resolve.extension with .ts extension, it will work out of box.

Plus no problems with future major webpack versions and no need to have enhanced-resolve in deps

johnnyreilly avatar Oct 13 '21 18:10 johnnyreilly

I just want to cross-reference https://github.com/microsoft/TypeScript/issues/37582 which is where I redirected https://github.com/microsoft/TypeScript/issues/46344 to. I would love to hear feedback there from Webpack users about

  • Why do you want/need to use .mts/.cts files?
  • Do you rely on other node12/nodenext module resolution features like export maps?
  • Do you have a mix of ESM and CJS dependencies?
  • Is anyone using Webpack’s resolve.conditionNames and why?
  • Do you want/need to write import paths with complete file extensions? Why?

andrewbranch avatar Oct 13 '21 18:10 andrewbranch

Thanks @andrewbranch - much appreciated!

johnnyreilly avatar Oct 13 '21 20:10 johnnyreilly

@djcsdy - feels like this might be interesting to you given your work on https://github.com/softwareventures/resolve-typescript-plugin

johnnyreilly avatar Oct 14 '21 06:10 johnnyreilly

I think we don't need a plugin here, webpack already do the same for cjs/mjs, https://github.com/webpack/webpack/blob/main/lib/config/defaults.js#L490, somebody can shortly write problems places with resolving and what is expected and I will show/send a PR with implement

alexander-akait avatar Oct 14 '21 09:10 alexander-akait

@alexander-akait the issue is that in the new TypeScript module resolution modes, users must write import "./foo.js" even though on disk, the colocated file is ./foo.ts. This is because TypeScript expects TS code to be compiled to JS with these import paths unchanged, that way the output JS will resolve correctly at runtime. But this breaks the expectations of bundlers like Webpack, which does module resolution before a 1:1 TS→JS compilation happens. So the correct specifier for Webpack would be import "./foo.ts" but TypeScript doesn’t allow that. The correct specifier for TypeScript is import "./foo.js" but Webpack can’t resolve that by default.

andrewbranch avatar Oct 14 '21 16:10 andrewbranch

@andrewbranch Thanks

@johnnyreilly Does we support import foo from './test.ts' here before? want to understand how do not break it for other

alexander-akait avatar Oct 14 '21 16:10 alexander-akait

So people don't specify extensions when they're writing TypeScript with ts-loader at present. An import would be extensionless like so:

import { faqsPath } from './misc/FAQs';

Even though the underlying file is FAQs.tsx in this case.

johnnyreilly avatar Oct 14 '21 16:10 johnnyreilly

Shorty:

  1. import "./foo" -> based on webpack resolve.extensions logic (now developer uses [ts, 'tsx', ...])
  2. import "./foo.js" -> try to load foo.ts and then try to load using resolve.extensions logic
  3. import "./foo.cjs" -> try to load foo.cts and then try to load using resolve.extensions logic
  4. import "./foo.mjs" -> try to load foo.mts and then try to load using resolve.extensions logic

Also we should respect type in package.json like we do in https://github.com/webpack/webpack/blob/main/lib/config/defaults.js#L491

alexander-akait avatar Oct 14 '21 16:10 alexander-akait

For better setup we can use dependencyType:

  1. typescript-auto
  2. typescript-commonjs
  3. typescript-esm

so developer can setup it better using:

resolver: {
  byDependecy: {
    'typescript-esm': {
      extensions: ['.tsx', '...']
    }
  }
}

alexander-akait avatar Oct 14 '21 17:10 alexander-akait

Ideally we should go away from the common resolve.extensions option, it is really weird when developers use ['.ts', '.tsx', '.css', '.js', '.cjs', '.mjs'], because resolving logic is different for different dependencies types

alexander-akait avatar Oct 14 '21 17:10 alexander-akait

Here only one edge case, if developer has type: 'module' before and we will implement typescript-esm logic, but he uses non extensions imports, we can break build

alexander-akait avatar Oct 14 '21 17:10 alexander-akait

@andersekdahl @johnnyreilly And another question, is typescript exports resolution logic (like function or class), because we can emulate it here, but in future, it can be changed more, maybe we can reuse resolution logic and avoid any problems in future?

alexander-akait avatar Oct 14 '21 17:10 alexander-akait

It’s internal API, but it looks like it’s already used here: https://github.com/TypeStrong/ts-loader/blob/8bc7b8b58b9592ef998491b69b00e390d4f110a5/src/servicesHost.ts#L1290

andrewbranch avatar Oct 14 '21 17:10 andrewbranch

hm, weird, it means import foo from './foo.mjs'; should work

alexander-akait avatar Oct 14 '21 17:10 alexander-akait

I’ll give it a try this afternoon.

andrewbranch avatar Oct 14 '21 17:10 andrewbranch

@andrewbranch Feel free to ping me, I some busy today/tomorrow, but will be free on this weekends, and look at this deeply, will be glad to any investigations so we can fix it faster

alexander-akait avatar Oct 14 '21 17:10 alexander-akait

@alexander-akait I put up a super simple repro showing that .js resolution to .ts doesn’t work, but haven’t had time to start seeing how ts-loader could be involved here. Honestly my expectation was that this would not work; I’m not super familiar with how ts-loader contributes to Webpack’s resolution, but my impression was that this would be a Webpack-level thing, not a ts-loader thing. https://github.com/andrewbranch/nodenext-webpack

andrewbranch avatar Oct 14 '21 23:10 andrewbranch

Thanks, I will look at this

alexander-akait avatar Oct 15 '21 10:10 alexander-akait

@johnnyreilly We have a bug inside ts-loader, webpack resolve broken with error:

Error: context argument is not an object
    at Resolver.resolve (/home/evilebottnawi/IdeaProjects/nodenext-webpackzxzx/node_modules/enhanced-resolve/lib/Resolver.js:261:20)
    at Resolver.resolveSync (/home/evilebottnawi/IdeaProjects/nodenext-webpackzxzx/node_modules/enhanced-resolve/lib/Resolver.js:236:8)
    at /home/evilebottnawi/IdeaProjects/nodenext-webpackzxzx/node_modules/enhanced-resolve/lib/index.js:94:19
    at resolveModule (/home/evilebottnawi/IdeaProjects/nodenext-webpackzxzx/node_modules/ts-loader/dist/servicesHost.js:724:34)
    at /home/evilebottnawi/IdeaProjects/nodenext-webpackzxzx/node_modules/ts-loader/dist/servicesHost.js:123:63
    at Array.map (<anonymous>)
    at Object.resolveModuleNames (/home/evilebottnawi/IdeaProjects/nodenext-webpackzxzx/node_modules/ts-loader/dist/servicesHost.js:123:45)
    at actualResolveModuleNamesWorker (/home/evilebottnawi/IdeaProjects/nodenext-webpackzxzx/node_modules/typescript/lib/typescript.js:113491:153)
    at resolveModuleNamesWorker (/home/evilebottnawi/IdeaProjects/nodenext-webpackzxzx/node_modules/typescript/lib/typescript.js:113760:26)
    at resolveModuleNamesReusingOldState (/home/evilebottnawi/IdeaProjects/nodenext-webpackzxzx/node_modules/typescript/lib/typescript.js:113856:24)

Just put console.log https://github.com/TypeStrong/ts-loader/blob/main/src/servicesHost.ts#L1242 here, it is really very big problem, we did not encounter this often because ts resolution is very close with JS resolution logic and so webpack logic

alexander-akait avatar Nov 13 '21 13:11 alexander-akait

Also interesting when I use "module": "nodenext", ts resolver resolves .ts file successfully, you can check it here https://github.com/TypeStrong/ts-loader/blob/main/src/servicesHost.ts#L1244...

alexander-akait avatar Nov 13 '21 13:11 alexander-akait

I don't fully understand the error to be honest.

johnnyreilly avatar Nov 13 '21 14:11 johnnyreilly

@johnnyreilly

Solved:

import { fileURLToPath } from "url";
import { dirname, join } from "path";

class NodeNextTSPlugin {
  apply(resolver) {
    const target = resolver.ensureHook("file");
    
    resolver
        .getHook("raw-file")
        .tapAsync("NodeNextTSPlugin", (request, resolveContext, callback) => {
          const obj = {
            ...request,
            path: request.path.replace(/\.js(x?)/, '.ts$1'),
            relativePath:
                request.relativePath && request.relativePath.replace(/\.js(x?)/, '.ts$1')
          };

          resolver.doResolve(
              target,
              obj,
              this.appending,
              resolveContext,
              callback
          );
        });
  }
}

/** @type {import("webpack").Configuration} */
const config = {
  mode: "development",
  entry: "./src/main.ts",
  output: {
    path: join(dirname(fileURLToPath(import.meta.url)), "dist"),
  },
  module: {
    rules: [{ test: /\.(m|c)?ts$/, use: "ts-loader" }]
  },
  resolve: {
    plugins: [new NodeNextTSPlugin()],
    extensions: [],
  },
};

export default config;

Also we can put it inside ts-loader and allow to import it from loader (for better DX), like

import { NodeNextTSPlugin } from "ts-loader"

export default {
  // Developer options
  resolver: {
    plugins: [ new NodeNextTSPlugin() ].
  }.
};

Regarding Error: context argument is not an object, you pass invalid arguments to resolveSync (https://github.com/TypeStrong/ts-loader/blob/main/src/servicesHost.ts#L1229), first argument should be context, not undefined, just change it on const originalFileName = resolveSync(path.normalize(path.dirname(containingFile)), moduleName);

alexander-akait avatar Nov 13 '21 14:11 alexander-akait

That's fast work! I think we'd be open to a PR that added this in. However I think work is still ongoing on how nodenext etc works in TypeScript and maybe we should be holding on for that? @andrewbranch may have a view

Regarding Error: context argument is not an object, you pass invalid arguments to resolveSync

If you wanted to PR this fix now there's no reason we couldn't land it I think

johnnyreilly avatar Nov 13 '21 14:11 johnnyreilly

@johnnyreilly Anyway I strongly recommend do not use create.sync https://github.com/TypeStrong/ts-loader/blob/main/src/resolver.ts#L8 and remove this code, Why?

  • You do extra resolve call using webpack built-in resolved, it decrease perf very well (just comment code https://github.com/TypeStrong/ts-loader/blob/main/src/servicesHost.ts#L1228-L1242) on big project and you will see how it faster, I do experiment, on one big project and removing it reduce time on 2,6 second, it is very big number
  • Mixing resolution logic between JS and TS is not good idea (yes, they are same, but if they are same why we need do extra resolution call (see before)? If developer need do own strange logic, it will be better to use loaderContext.getResolve (https://webpack.js.org/api/loaders/#thisgetresolve) with dependencyType: "typescript" and developer can set logic using:

https://webpack.js.org/configuration/resolve/#resolvebydependency

module.exports = {
  // ...
  resolve: {
    byDependency: {
      // ...
      typescript: {
        // Other options
        extensions: ['.my-strange-extension-for-ts'],
      },
    },
  },
};

also it is cachable, now we spend time on creating resolve for every ts file

  • ts does good job with resolving using tsconfig.json option, attempts to repeat the same logic on ts-loader side is not good direction.

So my solutions:

  • Сompletely remove sync webpack resolving logic, if developer really need weird resolving (I think it is bad idea and you should drink coffee and relax, and think again - do you really need this,), but if you really want to do it, we can allow define custom https://github.com/microsoft/TypeScript-wiki/blob/main/Using-the-Compiler-API.md#customizing-module-resolution, and developer can do it, we will speed up the time very very well
  • Migrate from sync webpack resolving logic on loaderContext.getResolve with dependencyType: "typescript", it will be cachable and faster than we have now, also it allow to avoid mixing logic between JS and TS

alexander-akait avatar Nov 13 '21 14:11 alexander-akait

@johnnyreilly Unfortunately, I don't have enough time right now (I am working on some great rust based things, in the near future I will do tweet about it, so I didn't give the solution so quickly here), I would be glad if someone does it, I can review code, because these changes are not hard

Anyway we can put solution in README (even it doesn't finished yet), so developer can start to use extensions in their imports like in ESM

alexander-akait avatar Nov 13 '21 14:11 alexander-akait