Skip to content

Commit 9315855

Browse files
authored
map input source map (#1626)
* map input source map * code review changes * reformat * remove usage of async/await * free memory of source map consumers * code review changes * copy input source map before modifying * fix comment * fix: crash because source map is not always generated by ts-compiler * chore: update yarn.lock * fix linting * add test * test * test * add new source maps * add readme * update bundle * link test package * install dependencies in during test execution * update package lock * update expected output * bump version and add changelog entry
1 parent 02c2069 commit 9315855

File tree

21 files changed

+39739
-3966
lines changed

21 files changed

+39739
-3966
lines changed

Diff for: CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# Changelog
22

3+
## 9.5.0
4+
* [Feature: map the input source map in case ts-loader is used in a loader pipeline](https://github.com/TypeStrong/ts-loader/pull/1626) - thanks @Ka0o0 and @bojanv55
5+
36
## 9.4.4
47
* [Bug fix: let users override skipLibCheck](https://github.com/TypeStrong/ts-loader/pull/1617) - thanks @haakonflatval-cognite
58

Diff for: package.json

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "ts-loader",
3-
"version": "9.4.4",
3+
"version": "9.5.0",
44
"description": "TypeScript loader for webpack",
55
"main": "index.js",
66
"types": "dist",
@@ -57,7 +57,8 @@
5757
"chalk": "^4.1.0",
5858
"enhanced-resolve": "^5.0.0",
5959
"micromatch": "^4.0.0",
60-
"semver": "^7.3.4"
60+
"semver": "^7.3.4",
61+
"source-map": "^0.7.4"
6162
},
6263
"devDependencies": {
6364
"@types/micromatch": "^4.0.0",

Diff for: src/index.ts

+85-6
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,19 @@ import {
2525
formatErrors,
2626
isReferencedFile,
2727
} from './utils';
28+
import type { RawSourceMap } from 'source-map';
29+
import { SourceMapConsumer, SourceMapGenerator } from 'source-map';
2830

2931
const loaderOptionsCache: LoaderOptionsCache = {};
3032

3133
/**
3234
* The entry point for ts-loader
3335
*/
34-
function loader(this: webpack.LoaderContext<LoaderOptions>, contents: string) {
36+
function loader(
37+
this: webpack.LoaderContext<LoaderOptions>,
38+
contents: string,
39+
inputSourceMap?: Record<string, any>
40+
) {
3541
this.cacheable && this.cacheable();
3642
const callback = this.async();
3743
const options = getLoaderOptions(this);
@@ -43,14 +49,15 @@ function loader(this: webpack.LoaderContext<LoaderOptions>, contents: string) {
4349
}
4450
const instance = instanceOrError.instance!;
4551
buildSolutionReferences(instance, this);
46-
successLoader(this, contents, callback, instance);
52+
successLoader(this, contents, callback, instance, inputSourceMap);
4753
}
4854

4955
function successLoader(
5056
loaderContext: webpack.LoaderContext<LoaderOptions>,
5157
contents: string,
5258
callback: ReturnType<webpack.LoaderContext<LoaderOptions>['async']>,
53-
instance: TSInstance
59+
instance: TSInstance,
60+
inputSourceMap?: Record<string, any>
5461
) {
5562
initializeInstance(loaderContext, instance);
5663
reportTranspileErrors(instance, loaderContext);
@@ -78,6 +85,8 @@ function successLoader(
7885
? getTranspilationEmit(filePath, contents, instance, loaderContext)
7986
: getEmit(rawFilePath, filePath, instance, loaderContext);
8087

88+
// the following function is async, which means it will immediately return and run in the "background"
89+
// Webpack will be notified when it's finished when the function calls the `callback` method
8190
makeSourceMapAndFinish(
8291
sourceMapText,
8392
outputText,
@@ -86,7 +95,8 @@ function successLoader(
8695
loaderContext,
8796
fileVersion,
8897
callback,
89-
instance
98+
instance,
99+
inputSourceMap
90100
);
91101
}
92102

@@ -98,7 +108,8 @@ function makeSourceMapAndFinish(
98108
loaderContext: webpack.LoaderContext<LoaderOptions>,
99109
fileVersion: number,
100110
callback: ReturnType<webpack.LoaderContext<LoaderOptions>['async']>,
101-
instance: TSInstance
111+
instance: TSInstance,
112+
inputSourceMap?: Record<string, any>
102113
) {
103114
if (outputText === null || outputText === undefined) {
104115
setModuleMeta(loaderContext, instance, fileVersion);
@@ -130,7 +141,27 @@ function makeSourceMapAndFinish(
130141
);
131142

132143
setModuleMeta(loaderContext, instance, fileVersion);
133-
callback(null, output, sourceMap);
144+
145+
// there are two cases where we don't need to perform input source map mapping:
146+
// - either the ts-compiler did not generate a source map (tsconfig had `sourceMap` set to false)
147+
// - or we did not get an input source map
148+
//
149+
// in the first case, we simply return undefined.
150+
// in the second case we only need to return the newly generated source map
151+
// this avoids that we have to make a possibly expensive call to the source-map lib
152+
if (sourceMap === undefined || inputSourceMap === undefined) {
153+
callback(null, output, sourceMap);
154+
return;
155+
}
156+
157+
// otherwise we have to make a mapping to the input source map which is asynchronous
158+
mapToInputSourceMap(sourceMap, loaderContext, inputSourceMap as RawSourceMap)
159+
.then(mappedSourceMap => {
160+
callback(null, output, mappedSourceMap);
161+
})
162+
.catch((e: Error) => {
163+
callback(e);
164+
});
134165
}
135166

136167
function setModuleMeta(
@@ -661,6 +692,54 @@ function makeSourceMap(
661692
};
662693
}
663694

695+
/**
696+
* This method maps the newly generated @param{sourceMap} to the input source map.
697+
* This is required when ts-loader is not the first loader in the Webpack loader chain.
698+
*/
699+
function mapToInputSourceMap(
700+
sourceMap: RawSourceMap,
701+
loaderContext: webpack.LoaderContext<LoaderOptions>,
702+
inputSourceMap: RawSourceMap
703+
): Promise<RawSourceMap> {
704+
return new Promise<RawSourceMap>((resolve, reject) => {
705+
const inMap: RawSourceMap = {
706+
file: loaderContext.remainingRequest,
707+
mappings: inputSourceMap.mappings,
708+
names: inputSourceMap.names,
709+
sources: inputSourceMap.sources,
710+
sourceRoot: inputSourceMap.sourceRoot,
711+
sourcesContent: inputSourceMap.sourcesContent,
712+
version: inputSourceMap.version,
713+
};
714+
Promise.all([
715+
new SourceMapConsumer(inMap),
716+
new SourceMapConsumer(sourceMap),
717+
])
718+
.then(sourceMapConsumers => {
719+
try {
720+
const generator = SourceMapGenerator.fromSourceMap(
721+
sourceMapConsumers[1]
722+
);
723+
generator.applySourceMap(sourceMapConsumers[0]);
724+
const mappedSourceMap = generator.toJSON();
725+
726+
// before resolving, we free memory by calling destroy on the source map consumers
727+
sourceMapConsumers.forEach(sourceMapConsumer =>
728+
sourceMapConsumer.destroy()
729+
);
730+
resolve(mappedSourceMap);
731+
} catch (e) {
732+
//before rejecting, we free memory by calling destroy on the source map consumers
733+
sourceMapConsumers.forEach(sourceMapConsumer =>
734+
sourceMapConsumer.destroy()
735+
);
736+
reject(e);
737+
}
738+
})
739+
.catch(reject);
740+
});
741+
}
742+
664743
export = loader;
665744

666745
/**

Diff for: test/comparison-tests/create-and-execute-test.js

+4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
const assert = require("assert");
22
const os = require('os');
33
const fs = require('fs-extra');
4+
const execSync = require('child_process').execSync;
45
const path = require('path');
56
const mkdirp = require('mkdirp');
67
const rimraf = require('rimraf');
@@ -110,6 +111,9 @@ function createTest(test, testPath, options) {
110111
const program = getProgram(path.resolve(paths.testStagingPath, "lib/tsconfig.json"), { newLine: typescript.NewLineKind.LineFeed });
111112
program.emit();
112113
}
114+
if(test === "sourceMapsShouldConsiderInputSourceMap") {
115+
execSync("npm ci", { cwd: paths.testStagingPath, stdio: 'inherit' });
116+
}
113117

114118
// ensure output directories
115119
mkdirp.sync(paths.actualOutput);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<template>
2+
<h1>Hello World!</h1>
3+
</template>
4+
5+
<script lang="ts">
6+
/* eslint-disable import/no-extraneous-dependencies */
7+
import { defineComponent } from "vue";
8+
9+
export default defineComponent({
10+
components: {
11+
},
12+
async created() {
13+
console.log("Hello World!");
14+
},
15+
});
16+
</script>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# sourceMapsShouldConsiderInputSourceMap
2+
3+
This test represents a typical Vue project which is configured to compile using the [vue-loader](https://github.com/vuejs/vue-loader) webpack loader.
4+
In this test we expect that `ts-loader` is considering the input source map which is generated by the `vue-loader`.
5+
6+
## Background Information
7+
8+
A Vue single file component (SFC) contains different parts of the component: the HTML template, the component's script (can be TypeScript) and sometimes CSS.
9+
The `vue-loader` is extracting the different parts of those SFCs and "sends" the different parts back to webpack together with a appropriate source map.
10+
Webpack then forwards those parts to the each different loaders that are next in the loader chain, which in the case for the script part is the `ts-loader`.
11+
`ts-loader` receives the isolated TypeScript code together with a source map and then further compiles the TypeScript code with the `tsc` and maps the newly generated source map to the input source map from `vue-loader`.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { createApp } from "vue";
2+
import App from "./App.vue";
3+
4+
const app = createApp(App);
5+
6+
app.mount("#app");

0 commit comments

Comments
 (0)