I have a simple custom Webpack loader which generates TypeScript code from a .txt
file:
txt-loader.js
module.exports = function TxtLoader(txt) {
console.log(`TxtLoader invoked on ${this.resourcePath} with content ${JSON.stringify(txt)}`)
if (txt.indexOf('Hello') < 0) {
throw new Error(`No "Hello" found`)
}
return `export const TEXT: string = ${JSON.stringify(txt)}`
}
In real life, I'm of doing some parsing on the input; in this example, let's assume that a file must contain the text Hello
to be valid.
This loader lets me import the text file like this:
index.ts
import { TEXT } from './hello.txt'
console.log(TEXT)
It all works fine, except for one thing: webpack watch
(and its cousin webpack serve
). The first compilation is fine:
$ /tmp/webpack-loader-repro/node_modules/.bin/webpack watch
TxtLoader invoked on /tmp/webpack-loader-repro/hello.txt with content "Hello world!\n"
asset main.js 250 bytes [compared for emit] [minimized] (name: main)
./index.ts 114 bytes [built] [code generated]
./hello.txt 97 bytes [built] [code generated]
webpack 5.64.3 compiled successfully in 3952 ms
But then I change the hello.txt
file:
$ touch hello.txt
And suddenly weird stuff happens:
TxtLoader invoked on /tmp/webpack-loader-repro/index.ts with content "import { TEXT } from './hello.txt'\n\nconsole.log(TEXT)\n"
TxtLoader invoked on /tmp/webpack-loader-repro/custom.d.ts with content "declare module '*.txt'\n"
[webpack-cli] Error: The loaded module contains errors
at /tmp/webpack-loader-repro/node_modules/webpack/lib/dependencies/LoaderPlugin.js:108:11
at /tmp/webpack-loader-repro/node_modules/webpack/lib/Compilation.js:1930:5
at /tmp/webpack-loader-repro/node_modules/webpack/lib/util/AsyncQueue.js:352:5
at Hook.eval [as callAsync] (eval at create (/tmp/webpack-loader-repro/node_modules/tapable/lib/HookCodeFactory.js:33:10), <anonymous>:6:1)
at AsyncQueue._handleResult (/tmp/webpack-loader-repro/node_modules/webpack/lib/util/AsyncQueue.js:322:21)
at /tmp/webpack-loader-repro/node_modules/webpack/lib/util/AsyncQueue.js:305:11
at /tmp/webpack-loader-repro/node_modules/webpack/lib/Compilation.js:1392:15
at /tmp/webpack-loader-repro/node_modules/webpack/lib/HookWebpackError.js:68:3
at Hook.eval [as callAsync] (eval at create (/tmp/webpack-loader-repro/node_modules/tapable/lib/HookCodeFactory.js:33:10), <anonymous>:6:1)
at Cache.store (/tmp/webpack-loader-repro/node_modules/webpack/lib/Cache.js:107:20)
error Command failed with exit code 2.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
It seems that Webpack decided to throw more files at my loader than specified in the configuration.
If I remove the exception throwing in the loader and return some arbitrary valid TypeScript code, the generated main.js
looks exactly the same. So it seems that these extra operations are entirely redundant. But I don't believe that the right solution is to make my loader swallow those exceptions.
The loader is configured like this:
webpack.config.js
const path = require('path')
module.exports = {
mode: 'production',
entry: './index.ts',
module: {
rules: [
{
test: /\.ts$/,
use: 'ts-loader',
},
{
test: /\.txt$/,
use: [
{
loader: 'ts-loader',
// Tell TypeScript that the input should be parsed as TypeScript,
// not JavaScript: <https://stackoverflow.com/a/47343106/14637>
options: { appendTsSuffixTo: [/\.txt$/] },
},
path.resolve('txt-loader.js'),
],
},
],
},
}
Finally, these are the necessary bits to put it all together:
custom.d.ts
declare module '*.txt'
tsconfig.json
{}
package.json
{
"name": "webpack-loader-repro",
"license": "MIT",
"private": true,
"devDependencies": {
"ts-loader": "9.2.6",
"typescript": "4.5.2",
"webpack": "5.64.3",
"webpack-cli": "4.9.1"
},
"dependencies": {}
}
For those who want to try this at home, clone this minimal repro project.
Is this a bug in Webpack? In ts-loader? In my configuration?