ts-loader with Typescript 4.5 beta: "Error: TypeScript emitted no output for .../index.mts"
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
}
}
}
]
}
]
}
Haven't tried this at all - am interested with how this pans out!
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?
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.
I suspect this issue may be interesting to you @JasonKleban https://github.com/webpack/webpack/issues/13252
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
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/.ctsfiles? - Do you rely on other
node12/nodenextmodule resolution features like export maps? - Do you have a mix of ESM and CJS dependencies?
- Is anyone using Webpack’s
resolve.conditionNamesand why? - Do you want/need to write import paths with complete file extensions? Why?
Thanks @andrewbranch - much appreciated!
@djcsdy - feels like this might be interesting to you given your work on https://github.com/softwareventures/resolve-typescript-plugin
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 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 Thanks
@johnnyreilly Does we support import foo from './test.ts' here before? want to understand how do not break it for other
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.
Shorty:
import "./foo"-> based on webpackresolve.extensionslogic (now developer uses [ts, 'tsx', ...])import "./foo.js"-> try to loadfoo.tsand then try to load usingresolve.extensionslogicimport "./foo.cjs"-> try to loadfoo.ctsand then try to load usingresolve.extensionslogicimport "./foo.mjs"-> try to loadfoo.mtsand then try to load usingresolve.extensionslogic
Also we should respect type in package.json like we do in https://github.com/webpack/webpack/blob/main/lib/config/defaults.js#L491
For better setup we can use dependencyType:
- typescript-auto
- typescript-commonjs
- typescript-esm
so developer can setup it better using:
resolver: {
byDependecy: {
'typescript-esm': {
extensions: ['.tsx', '...']
}
}
}
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
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
@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?
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
hm, weird, it means import foo from './foo.mjs'; should work
I’ll give it a try this afternoon.
@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 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
Thanks, I will look at this
@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
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...
I don't fully understand the error to be honest.
@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);
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 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) withdependencyType: "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.jsonoption, attempts to repeat the same logic onts-loaderside 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.getResolvewithdependencyType: "typescript", it will be cachable and faster than we have now, also it allow to avoid mixing logic between JS and TS
@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