Skip to content

Commit

Permalink
map input source map (#1626)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
Ka0o0 authored Oct 7, 2023
1 parent 02c2069 commit 9315855
Show file tree
Hide file tree
Showing 21 changed files with 39,739 additions and 3,966 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Changelog

## 9.5.0
* [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

## 9.4.4
* [Bug fix: let users override skipLibCheck](https://github.com/TypeStrong/ts-loader/pull/1617) - thanks @haakonflatval-cognite

Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ts-loader",
"version": "9.4.4",
"version": "9.5.0",
"description": "TypeScript loader for webpack",
"main": "index.js",
"types": "dist",
Expand Down Expand Up @@ -57,7 +57,8 @@
"chalk": "^4.1.0",
"enhanced-resolve": "^5.0.0",
"micromatch": "^4.0.0",
"semver": "^7.3.4"
"semver": "^7.3.4",
"source-map": "^0.7.4"
},
"devDependencies": {
"@types/micromatch": "^4.0.0",
Expand Down
91 changes: 85 additions & 6 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,19 @@ import {
formatErrors,
isReferencedFile,
} from './utils';
import type { RawSourceMap } from 'source-map';
import { SourceMapConsumer, SourceMapGenerator } from 'source-map';

const loaderOptionsCache: LoaderOptionsCache = {};

/**
* The entry point for ts-loader
*/
function loader(this: webpack.LoaderContext<LoaderOptions>, contents: string) {
function loader(
this: webpack.LoaderContext<LoaderOptions>,
contents: string,
inputSourceMap?: Record<string, any>
) {
this.cacheable && this.cacheable();
const callback = this.async();
const options = getLoaderOptions(this);
Expand All @@ -43,14 +49,15 @@ function loader(this: webpack.LoaderContext<LoaderOptions>, contents: string) {
}
const instance = instanceOrError.instance!;
buildSolutionReferences(instance, this);
successLoader(this, contents, callback, instance);
successLoader(this, contents, callback, instance, inputSourceMap);
}

function successLoader(
loaderContext: webpack.LoaderContext<LoaderOptions>,
contents: string,
callback: ReturnType<webpack.LoaderContext<LoaderOptions>['async']>,
instance: TSInstance
instance: TSInstance,
inputSourceMap?: Record<string, any>
) {
initializeInstance(loaderContext, instance);
reportTranspileErrors(instance, loaderContext);
Expand Down Expand Up @@ -78,6 +85,8 @@ function successLoader(
? getTranspilationEmit(filePath, contents, instance, loaderContext)
: getEmit(rawFilePath, filePath, instance, loaderContext);

// the following function is async, which means it will immediately return and run in the "background"
// Webpack will be notified when it's finished when the function calls the `callback` method
makeSourceMapAndFinish(
sourceMapText,
outputText,
Expand All @@ -86,7 +95,8 @@ function successLoader(
loaderContext,
fileVersion,
callback,
instance
instance,
inputSourceMap
);
}

Expand All @@ -98,7 +108,8 @@ function makeSourceMapAndFinish(
loaderContext: webpack.LoaderContext<LoaderOptions>,
fileVersion: number,
callback: ReturnType<webpack.LoaderContext<LoaderOptions>['async']>,
instance: TSInstance
instance: TSInstance,
inputSourceMap?: Record<string, any>
) {
if (outputText === null || outputText === undefined) {
setModuleMeta(loaderContext, instance, fileVersion);
Expand Down Expand Up @@ -130,7 +141,27 @@ function makeSourceMapAndFinish(
);

setModuleMeta(loaderContext, instance, fileVersion);
callback(null, output, sourceMap);

// there are two cases where we don't need to perform input source map mapping:
// - either the ts-compiler did not generate a source map (tsconfig had `sourceMap` set to false)
// - or we did not get an input source map
//
// in the first case, we simply return undefined.
// in the second case we only need to return the newly generated source map
// this avoids that we have to make a possibly expensive call to the source-map lib
if (sourceMap === undefined || inputSourceMap === undefined) {
callback(null, output, sourceMap);
return;
}

// otherwise we have to make a mapping to the input source map which is asynchronous
mapToInputSourceMap(sourceMap, loaderContext, inputSourceMap as RawSourceMap)
.then(mappedSourceMap => {
callback(null, output, mappedSourceMap);
})
.catch((e: Error) => {
callback(e);
});
}

function setModuleMeta(
Expand Down Expand Up @@ -661,6 +692,54 @@ function makeSourceMap(
};
}

/**
* This method maps the newly generated @param{sourceMap} to the input source map.
* This is required when ts-loader is not the first loader in the Webpack loader chain.
*/
function mapToInputSourceMap(
sourceMap: RawSourceMap,
loaderContext: webpack.LoaderContext<LoaderOptions>,
inputSourceMap: RawSourceMap
): Promise<RawSourceMap> {
return new Promise<RawSourceMap>((resolve, reject) => {
const inMap: RawSourceMap = {
file: loaderContext.remainingRequest,
mappings: inputSourceMap.mappings,
names: inputSourceMap.names,
sources: inputSourceMap.sources,
sourceRoot: inputSourceMap.sourceRoot,
sourcesContent: inputSourceMap.sourcesContent,
version: inputSourceMap.version,
};
Promise.all([
new SourceMapConsumer(inMap),
new SourceMapConsumer(sourceMap),
])
.then(sourceMapConsumers => {
try {
const generator = SourceMapGenerator.fromSourceMap(
sourceMapConsumers[1]
);
generator.applySourceMap(sourceMapConsumers[0]);
const mappedSourceMap = generator.toJSON();

// before resolving, we free memory by calling destroy on the source map consumers
sourceMapConsumers.forEach(sourceMapConsumer =>
sourceMapConsumer.destroy()
);
resolve(mappedSourceMap);
} catch (e) {
//before rejecting, we free memory by calling destroy on the source map consumers
sourceMapConsumers.forEach(sourceMapConsumer =>
sourceMapConsumer.destroy()
);
reject(e);
}
})
.catch(reject);
});
}

export = loader;

/**
Expand Down
4 changes: 4 additions & 0 deletions test/comparison-tests/create-and-execute-test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const assert = require("assert");
const os = require('os');
const fs = require('fs-extra');
const execSync = require('child_process').execSync;
const path = require('path');
const mkdirp = require('mkdirp');
const rimraf = require('rimraf');
Expand Down Expand Up @@ -110,6 +111,9 @@ function createTest(test, testPath, options) {
const program = getProgram(path.resolve(paths.testStagingPath, "lib/tsconfig.json"), { newLine: typescript.NewLineKind.LineFeed });
program.emit();
}
if(test === "sourceMapsShouldConsiderInputSourceMap") {
execSync("npm ci", { cwd: paths.testStagingPath, stdio: 'inherit' });
}

// ensure output directories
mkdirp.sync(paths.actualOutput);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<template>
<h1>Hello World!</h1>
</template>

<script lang="ts">
/* eslint-disable import/no-extraneous-dependencies */
import { defineComponent } from "vue";
export default defineComponent({
components: {
},
async created() {
console.log("Hello World!");
},
});
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# sourceMapsShouldConsiderInputSourceMap

This test represents a typical Vue project which is configured to compile using the [vue-loader](https://github.com/vuejs/vue-loader) webpack loader.
In this test we expect that `ts-loader` is considering the input source map which is generated by the `vue-loader`.

## Background Information

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.
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.
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`.
`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 number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { createApp } from "vue";
import App from "./App.vue";

const app = createApp(App);

app.mount("#app");
Loading

0 comments on commit 9315855

Please sign in to comment.