blob: 626d82f81e4b1f7a6dd9d7a5a72f7b6f68ddaf79 [file] [log] [blame]
Leo Repp58b9f112021-11-22 11:57:47 +01001'use strict';
2
3var path = require('path');
4
5var fs = require('graceful-fs');
6var nal = require('now-and-later');
7var File = require('vinyl');
8var convert = require('convert-source-map');
9var removeBOM = require('remove-bom-buffer');
10var appendBuffer = require('append-buffer');
11var normalizePath = require('normalize-path');
12
13var urlRegex = /^(https?|webpack(-[^:]+)?):\/\//;
14
15function isRemoteSource(source) {
16 return source.match(urlRegex);
17}
18
19function parse(data) {
20 try {
21 return JSON.parse(removeBOM(data));
22 } catch (err) {
23 // TODO: should this log a debug?
24 }
25}
26
27function loadSourceMap(file, state, callback) {
28 // Try to read inline source map
29 state.map = convert.fromSource(state.content);
30
31 if (state.map) {
32 state.map = state.map.toObject();
33 // Sources in map are relative to the source file
34 state.path = file.dirname;
35 state.content = convert.removeComments(state.content);
36 // Remove source map comment from source
37 file.contents = new Buffer(state.content, 'utf8');
38 return callback();
39 }
40
41 // Look for source map comment referencing a source map file
42 var mapComment = convert.mapFileCommentRegex.exec(state.content);
43
44 var mapFile;
45 if (mapComment) {
46 mapFile = path.resolve(file.dirname, mapComment[1] || mapComment[2]);
47 state.content = convert.removeMapFileComments(state.content);
48 // Remove source map comment from source
49 file.contents = new Buffer(state.content, 'utf8');
50 } else {
51 // If no comment try map file with same name as source file
52 mapFile = file.path + '.map';
53 }
54
55 // Sources in external map are relative to map file
56 state.path = path.dirname(mapFile);
57
58 fs.readFile(mapFile, onRead);
59
60 function onRead(err, data) {
61 if (err) {
62 return callback();
63 }
64 state.map = parse(data);
65 callback();
66 }
67}
68
69// Fix source paths and sourceContent for imported source map
70function fixImportedSourceMap(file, state, callback) {
71 if (!state.map) {
72 return callback();
73 }
74
75 state.map.sourcesContent = state.map.sourcesContent || [];
76
77 nal.map(state.map.sources, normalizeSourcesAndContent, callback);
78
79 function assignSourcesContent(sourceContent, idx) {
80 state.map.sourcesContent[idx] = sourceContent;
81 }
82
83 function normalizeSourcesAndContent(sourcePath, idx, cb) {
84 var sourceRoot = state.map.sourceRoot || '';
85 var sourceContent = state.map.sourcesContent[idx] || null;
86
87 if (isRemoteSource(sourcePath)) {
88 assignSourcesContent(sourceContent, idx);
89 return cb();
90 }
91
92 if (state.map.sourcesContent[idx]) {
93 return cb();
94 }
95
96 if (sourceRoot && isRemoteSource(sourceRoot)) {
97 assignSourcesContent(sourceContent, idx);
98 return cb();
99 }
100
101 var basePath = path.resolve(file.base, sourceRoot);
102 var absPath = path.resolve(state.path, sourceRoot, sourcePath);
103 var relPath = path.relative(basePath, absPath);
104 var unixRelPath = normalizePath(relPath);
105
106 state.map.sources[idx] = unixRelPath;
107
108 if (absPath !== file.path) {
109 // Load content from file async
110 return fs.readFile(absPath, onRead);
111 }
112
113 // If current file: use content
114 assignSourcesContent(state.content, idx);
115 cb();
116
117 function onRead(err, data) {
118 if (err) {
119 assignSourcesContent(null, idx);
120 return cb();
121 }
122 assignSourcesContent(removeBOM(data).toString('utf8'), idx);
123 cb();
124 }
125 }
126}
127
128function mapsLoaded(file, state, callback) {
129
130 if (!state.map) {
131 state.map = {
132 version: 3,
133 names: [],
134 mappings: '',
135 sources: [normalizePath(file.relative)],
136 sourcesContent: [state.content],
137 };
138 }
139
140 state.map.file = normalizePath(file.relative);
141 file.sourceMap = state.map;
142
143 callback();
144}
145
146function addSourceMaps(file, state, callback) {
147 var tasks = [
148 loadSourceMap,
149 fixImportedSourceMap,
150 mapsLoaded,
151 ];
152
153 function apply(fn, key, cb) {
154 fn(file, state, cb);
155 }
156
157 nal.mapSeries(tasks, apply, done);
158
159 function done() {
160 callback(null, file);
161 }
162}
163
164/* Write Helpers */
165function createSourceMapFile(opts) {
166 return new File({
167 cwd: opts.cwd,
168 base: opts.base,
169 path: opts.path,
170 contents: new Buffer(JSON.stringify(opts.content)),
171 stat: {
172 isFile: function() {
173 return true;
174 },
175 isDirectory: function() {
176 return false;
177 },
178 isBlockDevice: function() {
179 return false;
180 },
181 isCharacterDevice: function() {
182 return false;
183 },
184 isSymbolicLink: function() {
185 return false;
186 },
187 isFIFO: function() {
188 return false;
189 },
190 isSocket: function() {
191 return false;
192 },
193 },
194 });
195}
196
197var needsMultiline = ['.css'];
198
199function getCommentOptions(extname) {
200 var opts = {
201 multiline: (needsMultiline.indexOf(extname) !== -1),
202 };
203
204 return opts;
205}
206
207function writeSourceMaps(file, destPath, callback) {
208 var sourceMapFile;
209 var commentOpts = getCommentOptions(file.extname);
210
211 var comment;
212 if (destPath == null) {
213 // Encode source map into comment
214 comment = convert.fromObject(file.sourceMap).toComment(commentOpts);
215 } else {
216 var mapFile = path.join(destPath, file.relative) + '.map';
217 var sourceMapPath = path.join(file.base, mapFile);
218
219 // Create new sourcemap File
220 sourceMapFile = createSourceMapFile({
221 cwd: file.cwd,
222 base: file.base,
223 path: sourceMapPath,
224 content: file.sourceMap,
225 });
226
227 var sourcemapLocation = path.relative(file.dirname, sourceMapPath);
228
229 sourcemapLocation = normalizePath(sourcemapLocation);
230
231 comment = convert.generateMapFileComment(sourcemapLocation, commentOpts);
232 }
233
234 // Append source map comment
235 file.contents = appendBuffer(file.contents, comment);
236
237 callback(null, file, sourceMapFile);
238}
239
240module.exports = {
241 addSourceMaps: addSourceMaps,
242 writeSourceMaps: writeSourceMaps,
243};