Skip to content

Commit 5c5d5f8

Browse files
Jasper De Moordevongovett
authored andcommitted
JS Source Maps support (#506)
* run prettier * initial sourcemap * improve sourcemap constructor * also support SourceMapConsumer as addMap input * some cleanup and fixes * add lineOffset and 1:1 sourcemaps * add lineOffset and 1:1 sourcemaps * slight sourcemaputil improvement * use babel-generator instead of double generating * add 1:1 sourcemap for untranspiled assets * update offset * fix source paths * Better offset solution * fix sourceContent not being set by babel-generator * fix small offset bug * add sourcemap option and offset when globals are added * add cli option to disable sourcemaps * ts & coffeescript support * fix most tests * All tests (except hmr) fixed + bugfix for ts * fix hmr tests * comment out error throwing in tests * fix windows tests * add sourcemap tests * add comment to why throwing errors is commented out in tests * update with circular fix, now throws on test failures * fix generator for invalid maps * fix tests * fix tiny issues * small performance improvement * rewrite sourcemap handling, less generator constructing overhead and some more improvements * extendSourceMap bugfix * Use babel rawMappings to remove encoding step * small performance fixes * improve linecounter * small performance improvement and bugfix * small improvement * change for source-map 0.7 compatibility, improves performance a lil * switch to official npm release * tiny improvement * only install source-map 0.7 if possible * remove optional 0.7 dep * Minor refactorings * Store precomputed line count as part of source map * Clean up * Last cleanup
1 parent dba3d49 commit 5c5d5f8

37 files changed

Lines changed: 1005 additions & 83 deletions

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,4 @@ lib
1111
!test/**/node_modules
1212
.vscode/
1313
.idea/
14-
*.min.js
14+
*.min.js

package.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
"babylon-walk": "^1.0.2",
1717
"browser-resolve": "^1.11.2",
1818
"chalk": "^2.1.0",
19-
"child-process-promise": "^2.2.1",
2019
"chokidar": "^1.7.0",
2120
"commander": "^2.11.0",
2221
"cross-spawn": "^5.1.0",
@@ -40,6 +39,7 @@
4039
"resolve": "^1.4.0",
4140
"sanitize-filename": "^1.6.1",
4241
"serve-static": "^1.12.4",
42+
"source-map": "0.6.1",
4343
"uglify-es": "^3.2.1",
4444
"v8-compile-cache": "^1.1.0",
4545
"worker-farm": "^1.4.1",
@@ -49,6 +49,7 @@
4949
"babel-cli": "^6.26.0",
5050
"babel-preset-env": "^1.6.1",
5151
"bsb-js": "^1.0.1",
52+
"codecov": "^3.0.0",
5253
"coffeescript": "^2.0.3",
5354
"cross-env": "^5.1.1",
5455
"eslint": "^4.13.0",
@@ -66,9 +67,9 @@
6667
"posthtml-include": "^1.1.0",
6768
"prettier": "^1.9.1",
6869
"rimraf": "^2.6.1",
70+
"sourcemap-validator": "^1.0.6",
6971
"stylus": "^0.54.5",
70-
"typescript": "^2.6.2",
71-
"codecov": "^3.0.0"
72+
"typescript": "^2.6.2"
7273
},
7374
"scripts": {
7475
"test": "cross-env NODE_ENV=test mocha",

src/Asset.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ class Asset {
1919
this.id = ASSET_ID++;
2020
this.name = name;
2121
this.basename = path.basename(this.name);
22+
this.relativeName = path.relative(options.rootDir, this.name);
2223
this.package = pkg || {};
2324
this.options = options;
2425
this.encoding = 'utf8';
@@ -125,7 +126,7 @@ class Asset {
125126
// do nothing by default
126127
}
127128

128-
generate() {
129+
async generate() {
129130
return {
130131
[this.type]: this.contents
131132
};
@@ -137,7 +138,7 @@ class Asset {
137138
await this.pretransform();
138139
await this.getDependencies();
139140
await this.transform();
140-
this.generated = this.generate();
141+
this.generated = await this.generate();
141142
this.hash = this.generateHash();
142143
}
143144

src/Bundle.js

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@ class Bundle {
1515
this.entryAsset = null;
1616
this.assets = new Set();
1717
this.childBundles = new Set();
18-
this.siblingBundles = new Set;
18+
this.siblingBundles = new Set();
1919
this.siblingBundlesMap = new Map();
20+
this.offsets = new Map();
2021
}
2122

2223
static createWithAsset(asset, parentBundle) {
@@ -41,6 +42,14 @@ class Bundle {
4142
this.assets.delete(asset);
4243
}
4344

45+
addOffset(asset, line) {
46+
this.offsets.set(asset, line);
47+
}
48+
49+
getOffset(asset) {
50+
return this.offsets.get(asset) || 0;
51+
}
52+
4453
getSiblingBundle(type) {
4554
if (!type || type === this.type) {
4655
return this;
@@ -89,15 +98,23 @@ class Bundle {
8998
newHashes.set(this.name, hash);
9099

91100
let promises = [];
101+
let mappings = [];
92102
if (!oldHashes || oldHashes.get(this.name) !== hash) {
93103
promises.push(this._package(bundler));
94104
}
95105

96106
for (let bundle of this.childBundles.values()) {
97-
promises.push(bundle.package(bundler, oldHashes, newHashes));
107+
if (bundle.type === 'map') {
108+
mappings.push(bundle);
109+
} else {
110+
promises.push(bundle.package(bundler, oldHashes, newHashes));
111+
}
98112
}
99113

100114
await Promise.all(promises);
115+
for (let bundle of mappings) {
116+
await bundle.package(bundler, oldHashes, newHashes);
117+
}
101118
return newHashes;
102119
}
103120

src/Bundler.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,11 @@ class Bundler extends EventEmitter {
8080
logLevel: typeof options.logLevel === 'number' ? options.logLevel : 3,
8181
mainFile: this.mainFile,
8282
hmrPort: options.hmrPort || 0,
83+
rootDir: Path.dirname(this.mainFile),
84+
sourceMaps:
85+
typeof options.sourceMaps === 'boolean'
86+
? options.sourceMaps
87+
: !isProduction,
8388
hmrHostname: options.hmrHostname || ''
8489
};
8590
}
@@ -204,6 +209,8 @@ class Bundler extends EventEmitter {
204209

205210
if (process.env.NODE_ENV === 'production') {
206211
process.exitCode = 1;
212+
} else if (process.env.NODE_ENV === 'test' && !this.hmr) {
213+
throw err;
207214
}
208215
} finally {
209216
this.pending = false;

src/SourceMap.js

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
const {SourceMapConsumer, SourceMapGenerator} = require('source-map');
2+
const lineCounter = require('./utils/lineCounter');
3+
4+
class SourceMap {
5+
constructor(mappings, sources) {
6+
this.mappings = mappings || [];
7+
this.sources = sources || {};
8+
this.lineCount = null;
9+
}
10+
11+
async getConsumer(map) {
12+
if (map instanceof SourceMapConsumer) {
13+
return map;
14+
}
15+
16+
return await new SourceMapConsumer(map);
17+
}
18+
19+
async addMap(map, lineOffset = 0, columnOffset = 0) {
20+
if (!(map instanceof SourceMap) && map.version) {
21+
let consumer = await this.getConsumer(map);
22+
23+
consumer.eachMapping(mapping => {
24+
this.addConsumerMapping(mapping, lineOffset, columnOffset);
25+
if (!this.sources[mapping.source]) {
26+
this.sources[mapping.source] = consumer.sourceContentFor(
27+
mapping.source,
28+
true
29+
);
30+
}
31+
});
32+
33+
if (consumer.destroy) {
34+
// Only needs to happen in source-map 0.7
35+
consumer.destroy();
36+
}
37+
} else {
38+
if (!map.eachMapping) {
39+
map = new SourceMap(map.mappings, map.sources);
40+
}
41+
42+
if (lineOffset === 0 && columnOffset === 0) {
43+
this.mappings = this.mappings.concat(map.mappings);
44+
} else {
45+
map.eachMapping(mapping => {
46+
this.addMapping(mapping, lineOffset, columnOffset);
47+
});
48+
}
49+
50+
Object.keys(map.sources).forEach(sourceName => {
51+
if (!this.sources[sourceName]) {
52+
this.sources[sourceName] = map.sources[sourceName];
53+
}
54+
});
55+
}
56+
57+
return this;
58+
}
59+
60+
addMapping(mapping, lineOffset = 0, columnOffset = 0) {
61+
mapping.generated = {
62+
line: mapping.generated.line + lineOffset,
63+
column: mapping.generated.column + columnOffset
64+
};
65+
66+
this.mappings.push(mapping);
67+
}
68+
69+
addConsumerMapping(mapping, lineOffset = 0, columnOffset = 0) {
70+
if (
71+
!mapping.source ||
72+
!mapping.originalLine ||
73+
(!mapping.originalColumn && mapping.originalColumn !== 0)
74+
) {
75+
return;
76+
}
77+
78+
this.mappings.push({
79+
source: mapping.source,
80+
original: {
81+
line: mapping.originalLine,
82+
column: mapping.originalColumn
83+
},
84+
generated: {
85+
line: mapping.generatedLine + lineOffset,
86+
column: mapping.generatedColumn + columnOffset
87+
},
88+
name: mapping.name
89+
});
90+
}
91+
92+
eachMapping(callback) {
93+
this.mappings.forEach(callback);
94+
}
95+
96+
generateEmptyMap(sourceName, sourceContent) {
97+
this.sources[sourceName] = sourceContent;
98+
99+
this.lineCount = lineCounter(sourceContent);
100+
for (let line = 1; line < this.lineCount + 1; line++) {
101+
this.addMapping({
102+
source: sourceName,
103+
original: {
104+
line: line,
105+
column: 0
106+
},
107+
generated: {
108+
line: line,
109+
column: 0
110+
}
111+
});
112+
}
113+
114+
return this;
115+
}
116+
117+
async extendSourceMap(original, extension) {
118+
if (!(extension instanceof SourceMap)) {
119+
throw new Error(
120+
'[SOURCEMAP] Type of extension should be a SourceMap instance!'
121+
);
122+
}
123+
124+
original = await this.getConsumer(original);
125+
extension.eachMapping(mapping => {
126+
let originalMapping = original.originalPositionFor({
127+
line: mapping.original.line,
128+
column: mapping.original.column
129+
});
130+
131+
if (!originalMapping.line) {
132+
return false;
133+
}
134+
135+
this.addMapping({
136+
source: originalMapping.source,
137+
name: originalMapping.name,
138+
original: {
139+
line: originalMapping.line,
140+
column: originalMapping.column
141+
},
142+
generated: {
143+
line: mapping.generated.line,
144+
column: mapping.generated.column
145+
}
146+
});
147+
148+
if (!this.sources[originalMapping.source]) {
149+
this.sources[originalMapping.source] = original.sourceContentFor(
150+
originalMapping.source,
151+
true
152+
);
153+
}
154+
});
155+
156+
if (original.destroy) {
157+
// Only needs to happen in source-map 0.7
158+
original.destroy();
159+
}
160+
161+
return this;
162+
}
163+
164+
offset(lineOffset = 0, columnOffset = 0) {
165+
this.mappings.map(mapping => {
166+
mapping.generated.line = mapping.generated.line + lineOffset;
167+
mapping.generated.column = mapping.generated.column + columnOffset;
168+
return mapping;
169+
});
170+
171+
if (this.lineCount != null) {
172+
this.lineCount += lineOffset;
173+
}
174+
}
175+
176+
stringify(file) {
177+
let generator = new SourceMapGenerator({
178+
file: file
179+
});
180+
181+
this.eachMapping(mapping => generator.addMapping(mapping));
182+
Object.keys(this.sources).forEach(sourceName =>
183+
generator.setSourceContent(sourceName, this.sources[sourceName])
184+
);
185+
186+
return generator.toString();
187+
}
188+
}
189+
190+
module.exports = SourceMap;

src/assets/CoffeeScriptAsset.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,17 @@ class CoffeeScriptAsset extends JSAsset {
77
let coffee = await localRequire('coffeescript', this.name);
88

99
// Transpile Module using CoffeeScript and parse result as ast format through babylon
10-
this.contents = coffee.compile(code, {});
10+
let transpiled = coffee.compile(code, {
11+
sourceMap: this.options.sourceMaps
12+
});
13+
14+
if (transpiled.sourceMap) {
15+
this.sourceMap = transpiled.sourceMap.generate();
16+
this.sourceMap.sources = [this.relativeName];
17+
this.sourceMap.sourcesContent = [this.contents];
18+
}
19+
20+
this.contents = this.options.sourceMaps ? transpiled.js : transpiled;
1121
return await super.parse(this.contents);
1222
}
1323
}

0 commit comments

Comments
 (0)