-
Notifications
You must be signed in to change notification settings - Fork 21
/
index.js
290 lines (260 loc) · 8.56 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
//@ts-check
const fs = require('graceful-fs');
const path = require('path');
const SOURCEMAP_URL_REGEX = /^\/\/#\s*sourceMappingURL=/;
const CHARSET_REGEX = /^;charset=([^;]+);/;
/**
* @param {*} logger
* @param {karmaSourcemapLoader.Config} config
* @returns {karmaSourcemapLoader.Preprocessor}
*/
function createSourceMapLocatorPreprocessor(logger, config) {
const options = (config && config.sourceMapLoader) || {};
const remapPrefixes = options.remapPrefixes;
const remapSource = options.remapSource;
const useSourceRoot = options.useSourceRoot;
const onlyWithURL = options.onlyWithURL;
const strict = options.strict;
const needsUpdate = remapPrefixes || remapSource || useSourceRoot;
const log = logger.create('preprocessor.sourcemap');
/**
* @param {string[]} sources
*/
function remapSources(sources) {
const all = sources.length;
let remapped = 0;
/** @type {Record<string, boolean>} */
const remappedPrefixes = {};
let remappedSource = false;
/**
* Replaces source path prefixes using a key:value map
* @param {string} source
* @returns {string | undefined}
*/
function handlePrefixes(source) {
if (!remapPrefixes) {
return undefined;
}
let sourcePrefix, targetPrefix, target;
for (sourcePrefix in remapPrefixes) {
targetPrefix = remapPrefixes[sourcePrefix];
if (source.startsWith(sourcePrefix)) {
target = targetPrefix + source.substring(sourcePrefix.length);
++remapped;
// Log only one remapping as an example for each prefix to prevent
// flood of messages on the console
if (!remappedPrefixes[sourcePrefix]) {
remappedPrefixes[sourcePrefix] = true;
log.debug(' ', source, '>>', target);
}
return target;
}
}
}
// Replaces source paths using a custom function
/**
* @param {string} source
* @returns {string | undefined}
*/
function handleMapper(source) {
if (!remapSource) {
return undefined;
}
const target = remapSource(source);
// Remapping is considered happenned only if the handler returns
// a non-empty path different from the existing one
if (target && target !== source) {
++remapped;
// Log only one remapping as an example to prevent flooding the console
if (!remappedSource) {
remappedSource = true;
log.debug(' ', source, '>>', target);
}
return target;
}
}
const result = sources.map((rawSource) => {
const source = rawSource.replace(/\\/g, '/');
const sourceWithRemappedPrefixes = handlePrefixes(source);
if (sourceWithRemappedPrefixes) {
// One remapping is enough; if a prefix was replaced, do not let
// the handler below check the source path any more
return sourceWithRemappedPrefixes;
}
return handleMapper(source) || source;
});
if (remapped) {
log.debug(' ...');
log.debug(' ', remapped, 'sources from', all, 'were remapped');
}
return result;
}
return function karmaSourcemapLoaderPreprocessor(content, file, done) {
/**
* Parses a string with source map as JSON and handles errors
* @param {string} data
* @returns {karmaSourcemapLoader.SourceMap | false | undefined}
*/
function parseMap(data) {
try {
return JSON.parse(data);
} catch (err) {
if (strict) {
done(new Error('malformed source map for' + file.originalPath + '\nError: ' + err));
// Returning `false` will make the caller abort immediately
return false;
}
log.warn('malformed source map for', file.originalPath);
log.warn('Error:', err);
}
}
/**
* Sets the sourceRoot property to a fixed or computed value
* @param {karmaSourcemapLoader.SourceMap} sourceMap
*/
function setSourceRoot(sourceMap) {
const sourceRoot = typeof useSourceRoot === 'function' ? useSourceRoot(file) : useSourceRoot;
if (sourceRoot) {
sourceMap.sourceRoot = sourceRoot;
}
}
/**
* Performs configured updates of the source map content
* @param {karmaSourcemapLoader.SourceMap} sourceMap
*/
function updateSourceMap(sourceMap) {
if (remapPrefixes || remapSource) {
sourceMap.sources = remapSources(sourceMap.sources);
}
if (useSourceRoot) {
setSourceRoot(sourceMap);
}
}
/**
* @param {string} data
* @returns {void}
*/
function sourceMapData(data) {
const sourceMap = parseMap(data);
if (sourceMap) {
// Perform the remapping only if there is a configuration for it
if (needsUpdate) {
updateSourceMap(sourceMap);
}
file.sourceMap = sourceMap;
} else if (sourceMap === false) {
return;
}
done(content);
}
/**
* @param {string} inlineData
*/
function inlineMap(inlineData) {
let charset = 'utf-8';
if (CHARSET_REGEX.test(inlineData)) {
const matches = inlineData.match(CHARSET_REGEX);
if (matches && matches.length === 2) {
charset = matches[1];
inlineData = inlineData.slice(matches[0].length - 1);
}
}
if (/^;base64,/.test(inlineData)) {
// base64-encoded JSON string
log.debug('base64-encoded source map for', file.originalPath);
const buffer = Buffer.from(inlineData.slice(';base64,'.length), 'base64');
//@ts-ignore Assume the parsed charset is supported by Buffer.
sourceMapData(buffer.toString(charset));
} else if (inlineData.startsWith(',')) {
// straight-up URL-encoded JSON string
log.debug('raw inline source map for', file.originalPath);
sourceMapData(decodeURIComponent(inlineData.slice(1)));
} else {
if (strict) {
done(new Error('invalid source map in ' + file.originalPath));
} else {
log.warn('invalid source map in', file.originalPath);
done(content);
}
}
}
/**
* @param {string} mapPath
* @param {boolean} optional
*/
function fileMap(mapPath, optional) {
fs.readFile(mapPath, function (err, data) {
// File does not exist
if (err && err.code === 'ENOENT') {
if (!optional) {
if (strict) {
done(new Error('missing external source map for ' + file.originalPath));
return;
} else {
log.warn('missing external source map for', file.originalPath);
}
}
done(content);
return;
}
// Error while reading the file
if (err) {
if (strict) {
done(
new Error('reading external source map failed for ' + file.originalPath + '\n' + err)
);
} else {
log.warn('reading external source map failed for', file.originalPath);
log.warn(err);
done(content);
}
return;
}
log.debug('external source map exists for', file.originalPath);
sourceMapData(data.toString());
});
}
// Remap source paths in a directly served source map
function convertMap() {
let sourceMap;
// Perform the remapping only if there is a configuration for it
if (needsUpdate) {
log.debug('processing source map', file.originalPath);
sourceMap = parseMap(content);
if (sourceMap) {
updateSourceMap(sourceMap);
content = JSON.stringify(sourceMap);
} else if (sourceMap === false) {
return;
}
}
done(content);
}
if (file.path.endsWith('.map')) {
return convertMap();
}
const lines = content.split(/\n/);
let lastLine = lines.pop();
while (typeof lastLine === 'string' && /^\s*$/.test(lastLine)) {
lastLine = lines.pop();
}
const mapUrl =
lastLine && SOURCEMAP_URL_REGEX.test(lastLine) && lastLine.replace(SOURCEMAP_URL_REGEX, '');
if (!mapUrl) {
if (onlyWithURL) {
done(content);
} else {
fileMap(file.path + '.map', true);
}
} else if (/^data:application\/json/.test(mapUrl)) {
inlineMap(mapUrl.slice('data:application/json'.length));
} else {
fileMap(path.resolve(path.dirname(file.path), mapUrl), false);
}
};
}
createSourceMapLocatorPreprocessor.$inject = ['logger', 'config'];
// PUBLISH DI MODULE
module.exports = {
'preprocessor:sourcemap': ['factory', createSourceMapLocatorPreprocessor],
};