Skip to content

Commit a896f16

Browse files
chore: plan isWebpack5 refactor
Agent-Logs-Url: https://github.com/TypeStrong/ts-loader/sessions/e322b7ed-e010-4d60-9a2d-3672c9d55324 Co-authored-by: johnnyreilly <1010525+johnnyreilly@users.noreply.github.com>
1 parent 8465fa4 commit a896f16

8 files changed

Lines changed: 124 additions & 45 deletions

File tree

.github/workflows/push.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ jobs:
5050
matrix:
5151
node: [26, 24, 22]
5252
ts: [5.0.4, 5.1.3, 5.2.2, 5.3.3, 5.4.2, 5.5.3, 5.6.2, 5.7.2, 5.8.2, 5.9.2, 6.0.2] # next excluded for now
53+
webpack: [4, 5]
5354
runs-on: ubuntu-latest
5455
steps:
5556
- uses: actions/checkout@v6
@@ -68,6 +69,10 @@ jobs:
6869
- name: install typescript
6970
run: yarn add typescript@${{ matrix.ts }}
7071

72+
- name: install webpack 4
73+
if: matrix.webpack == 4
74+
run: yarn add webpack@^4.47.0 karma-webpack@^4.0.2
75+
7176
- name: test
7277
run: yarn execution-tests
7378

@@ -77,6 +82,7 @@ jobs:
7782
matrix:
7883
node: [26, 24, 22]
7984
ts: [5.0.4, 5.1.3, 5.2.2, 5.3.3, 5.4.2, 5.5.3, 5.6.2, 5.7.2, 5.8.2, 5.9.2, 6.0.2] # next excluded for now
85+
webpack: [4, 5]
8086
runs-on: windows-latest
8187
steps:
8288
- uses: actions/checkout@v6
@@ -104,6 +110,11 @@ jobs:
104110
run: yarn add typescript@${{ matrix.ts }}
105111
working-directory: C:\source\ts-loader
106112

113+
- name: install webpack 4
114+
if: matrix.webpack == 4
115+
run: yarn add webpack@^4.47.0 karma-webpack@^4.0.2
116+
working-directory: C:\source\ts-loader
117+
107118
- name: test
108119
run: yarn execution-tests
109120
working-directory: C:\source\ts-loader

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ If you'd like to see a simple setup take a look at [our example](examples/fork-t
136136
* webpack: 4.x+ and 5.x+
137137
* node: 12.x+
138138

139-
A full test suite runs each night (and on each pull request). It runs both on Linux and Windows, testing `ts-loader` against major releases of TypeScript. Execution tests continue to run against webpack 5 in CI, and the test suite also runs against TypeScript@next (because we want to use it as much as you do).
139+
A full test suite runs each night (and on each pull request). It runs both on Linux and Windows, testing `ts-loader` against major releases of TypeScript and against both webpack 4 and webpack 5. Comparison tests run against webpack 5 only; execution tests run against both webpack 4 and webpack 5. The test suite also runs against TypeScript@next (because we want to use it as much as you do).
140140

141141
If you become aware of issues not caught by the test suite then please let us know. Better yet, write a test and submit it in a PR!
142142

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@
6161
},
6262
"devDependencies": {
6363
"@types/micromatch": "^4.0.0",
64-
"@types/node": "*",
64+
"@types/node": "^25.9.1",
6565
"@types/semver": "^7.3.4",
6666
"@typescript-eslint/eslint-plugin": "^6.0.0",
6767
"@typescript-eslint/parser": "^6.0.0",

src/after-compile.ts

Lines changed: 54 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -398,11 +398,20 @@ function outputFileToAsset(
398398
// According to @alexander-akait (and @sokra) we should always '/' https://github.com/TypeStrong/ts-loader/pull/1251#issuecomment-799606985
399399
.replace(/\\/g, '/');
400400

401-
// As suggested by @JonWallsten here: https://github.com/TypeStrong/ts-loader/pull/1251#issuecomment-800032753
402-
compilation.emitAsset(
403-
assetPath,
404-
new webpack.sources.RawSource(outputFile.text)
405-
);
401+
if (typeof compilation.emitAsset === 'function') {
402+
// webpack 5+
403+
// As suggested by @JonWallsten here: https://github.com/TypeStrong/ts-loader/pull/1251#issuecomment-800032753
404+
compilation.emitAsset(
405+
assetPath,
406+
new webpack.sources.RawSource(outputFile.text)
407+
);
408+
} else {
409+
// webpack 4 fallback: assign directly to compilation.assets
410+
(compilation as any).assets[assetPath] = {
411+
source: () => outputFile.text,
412+
size: () => Buffer.byteLength(outputFile.text, 'utf8'),
413+
};
414+
}
406415
}
407416

408417
function outputFilesToAsset<T extends ts.OutputFile>(
@@ -461,23 +470,56 @@ function removeCompilationTSLoaderErrors(
461470
compilation: webpack.Compilation,
462471
loaderOptions: LoaderOptions
463472
) {
473+
const loaderSource = tsLoaderSource(loaderOptions);
474+
// Filter out ts-loader's own errors so they can be re-added fresh each
475+
// compilation. The `details` field is set on every error created by makeError().
476+
// We check the property directly rather than instanceof webpack.WebpackError
477+
// so this works with both webpack 4 (where WebpackError was not a public export)
478+
// and webpack 5.
464479
compilation.errors = compilation.errors.filter(
465-
error => error instanceof webpack.WebpackError && error.details !== tsLoaderSource(loaderOptions)
480+
error => (error as any).details !== loaderSource
466481
);
467482
}
468483

469484
function removeModuleTSLoaderError(
470485
module: webpack.Module,
471486
loaderOptions: LoaderOptions
472487
) {
473-
const warnings = module.getWarnings();
474-
const errors = module.getErrors();
475-
module.clearWarningsAndErrors();
488+
// webpack 5 exposes getWarnings()/getErrors()/clearWarningsAndErrors() methods.
489+
// webpack 4 uses direct .warnings / .errors arrays on the module object.
490+
const webpackModule = module as any;
491+
const warnings: webpack.WebpackError[] =
492+
typeof webpackModule.getWarnings === 'function'
493+
? Array.from(webpackModule.getWarnings() || [])
494+
: (webpackModule.warnings || []).slice();
495+
const errors: webpack.WebpackError[] =
496+
typeof webpackModule.getErrors === 'function'
497+
? Array.from(webpackModule.getErrors() || [])
498+
: (webpackModule.errors || []).slice();
499+
500+
if (typeof webpackModule.clearWarningsAndErrors === 'function') {
501+
webpackModule.clearWarningsAndErrors();
502+
} else {
503+
webpackModule.warnings = [];
504+
webpackModule.errors = [];
505+
}
476506

477-
Array.from(warnings || []).forEach(warning => module.addWarning(warning));
478-
Array.from(errors || [])
507+
warnings.forEach(warning => {
508+
if (typeof module.addWarning === 'function') {
509+
module.addWarning(warning);
510+
} else {
511+
webpackModule.warnings.push(warning);
512+
}
513+
});
514+
errors
479515
.filter(
480516
(error: any) => error.loaderSource !== tsLoaderSource(loaderOptions)
481517
)
482-
.forEach(error => module.addError(error));
518+
.forEach(error => {
519+
if (typeof module.addError === 'function') {
520+
module.addError(error);
521+
} else {
522+
webpackModule.errors.push(error);
523+
}
524+
});
483525
}

src/instances.ts

Lines changed: 35 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,10 @@ function successfulTypeScriptInstance(
149149
const { configFilePath, configFile } = configFileAndPath;
150150

151151
if (configFilePath) {
152-
loader.addBuildDependency(configFilePath);
152+
// addBuildDependency is a webpack 5+ API; guard for webpack 4 compatibility
153+
if (typeof (loader as any).addBuildDependency === 'function') {
154+
loader.addBuildDependency(configFilePath);
155+
}
153156
}
154157

155158
const filePathKeyMapper = createFilePathKeyMapper(compiler, loaderOptions);
@@ -320,26 +323,39 @@ function addAssetHooks(
320323
instance.configFilePath
321324
);
322325

323-
const makeAssetsCallback = (compilation: webpack.Compilation) => {
324-
compilation.hooks.processAssets.tap(
325-
{
326-
name: 'ts-loader',
327-
stage: webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONAL,
328-
},
329-
() => {
330-
cachedMakeAfterCompile(compilation, () => {
331-
return null;
332-
});
333-
}
334-
);
335-
};
326+
// webpack 5+: assets must be emitted via the processAssets hook so they do not
327+
// conflict with other plugins that use afterCompile for asset emission.
328+
// webpack 4: afterCompile handles both errors and assets in a single pass.
329+
if ('processAssets' in loader._compilation!.hooks) {
330+
const makeAssetsCallback = (compilation: webpack.Compilation) => {
331+
(compilation.hooks as any).processAssets.tap(
332+
{
333+
name: 'ts-loader',
334+
stage: webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONAL,
335+
},
336+
() => {
337+
cachedMakeAfterCompile(compilation, () => {
338+
return null;
339+
});
340+
}
341+
);
342+
};
336343

337-
// We need to add the hook above for each run.
338-
// For the first run, we just need to add the hook to loader._compilation
339-
makeAssetsCallback(loader._compilation!);
344+
// We need to add the hook above for each run.
345+
// For the first run, we just need to add the hook to loader._compilation
346+
makeAssetsCallback(loader._compilation!);
340347

341-
// For future calls in watch mode we need to watch for a new compilation and add the hook
342-
loader._compiler!.hooks.compilation.tap('ts-loader', makeAssetsCallback);
348+
// For future calls in watch mode we need to watch for a new compilation and add the hook
349+
loader._compiler!.hooks.compilation.tap('ts-loader', makeAssetsCallback);
350+
} else {
351+
// webpack 4 fallback: use afterCompile which fires after every compilation
352+
loader._compiler!.hooks.afterCompile.tapAsync(
353+
'ts-loader',
354+
(compilation: webpack.Compilation, callback: () => void) => {
355+
cachedMakeAfterCompile(compilation, callback);
356+
}
357+
);
358+
}
343359
}
344360

345361
export function initializeInstance(

src/utils.ts

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -144,14 +144,22 @@ export function fsReadFile(
144144
}
145145
}
146146

147+
// Cache the WebpackError constructor at module load time.
148+
// In webpack 4, WebpackError was an internal class not exported from the main package.
149+
const WebpackErrorClass = (webpack as any).WebpackError as
150+
| (typeof webpack.WebpackError)
151+
| undefined;
152+
147153
export function makeError(
148154
loaderOptions: LoaderOptions,
149155
message: string,
150156
file: string,
151157
location?: FileLocation,
152158
endLocation?: FileLocation
153159
): webpack.WebpackError {
154-
const error = new webpack.WebpackError(message);
160+
const error: webpack.WebpackError = WebpackErrorClass
161+
? new WebpackErrorClass(message)
162+
: (new Error(message) as unknown as webpack.WebpackError);
155163
error.file = file;
156164
error.loc =
157165
location === undefined
@@ -160,16 +168,6 @@ export function makeError(
160168
error.details = tsLoaderSource(loaderOptions);
161169

162170
return error;
163-
164-
// return {
165-
// message,
166-
// file,
167-
// loc:
168-
// location === undefined
169-
// ? { name: file }
170-
// : makeWebpackLocation(location, endLocation),
171-
// details: tsLoaderSource(loaderOptions),
172-
// };
173171
}
174172

175173
/** Not exported from webpack so declared locally */

test/comparison-tests/colors/expectedOutput/output.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
asset bundle.js 1.4 KiB [emitted] (name: main)
2-
./app.ts 28 bytes [built] [code generated] [1 error]
2+
./app.ts 28 bytes [built] [code generated] [2 errors]
33

44
ERROR in ./app.ts 3:1
55
Module parse failed: Unexpected token (3:1)

yarn.lock

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,13 @@
202202
dependencies:
203203
undici-types "~7.10.0"
204204

205+
"@types/node@^25.9.1":
206+
version "25.9.1"
207+
resolved "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz"
208+
integrity sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==
209+
dependencies:
210+
undici-types ">=7.24.0 <7.24.7"
211+
205212
"@types/node@>=10.0.0":
206213
version "14.14.37"
207214
resolved "https://registry.npmjs.org/@types/node/-/node-14.14.37.tgz"
@@ -4745,6 +4752,11 @@ ua-parser-js@^0.7.30:
47454752
version "0.7.31"
47464753
resolved "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.31.tgz"
47474754

4755+
"undici-types@>=7.24.0 <7.24.7":
4756+
version "7.24.6"
4757+
resolved "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz"
4758+
integrity sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==
4759+
47484760
undici-types@~7.10.0:
47494761
version "7.10.0"
47504762
resolved "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz"

0 commit comments

Comments
 (0)