@jsenv/plugin-supervisor 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/js/supervisor.js +1088 -0
- package/dist/jsenv_plugin_supervisor.js +1185 -0
- package/package.json +6 -5
- package/src/client/supervisor.js +3 -3
- package/src/jsenv_plugin_supervisor.js +24 -22
|
@@ -0,0 +1,1185 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { applyBabelPlugins, parseHtmlString, visitHtmlNodes, analyzeScriptNode, getHtmlNodeAttribute, getHtmlNodeText, injectHtmlNodeAsEarlyAsPossible, createHtmlNode, stringifyHtmlAst, getHtmlNodePosition, setHtmlNodeText, setHtmlNodeAttributes } from "@jsenv/ast";
|
|
4
|
+
|
|
5
|
+
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
|
6
|
+
const intToChar = new Uint8Array(64); // 64 possible chars.
|
|
7
|
+
const charToInt = new Uint8Array(128); // z is 122 in ASCII
|
|
8
|
+
for (let i = 0; i < chars.length; i++) {
|
|
9
|
+
const c = chars.charCodeAt(i);
|
|
10
|
+
intToChar[i] = c;
|
|
11
|
+
charToInt[c] = i;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const require = createRequire(import.meta.url);
|
|
15
|
+
// consider using https://github.com/7rulnik/source-map-js
|
|
16
|
+
|
|
17
|
+
const requireSourcemap = () => {
|
|
18
|
+
const namespace = require("source-map-js");
|
|
19
|
+
return namespace;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/*
|
|
23
|
+
* https://github.com/mozilla/source-map#sourcemapgenerator
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
requireSourcemap();
|
|
27
|
+
|
|
28
|
+
// https://github.com/mozilla/source-map#sourcemapconsumerprototypeoriginalpositionforgeneratedposition
|
|
29
|
+
const getOriginalPosition = ({
|
|
30
|
+
sourcemap,
|
|
31
|
+
line,
|
|
32
|
+
column,
|
|
33
|
+
bias
|
|
34
|
+
}) => {
|
|
35
|
+
const {
|
|
36
|
+
SourceMapConsumer
|
|
37
|
+
} = requireSourcemap();
|
|
38
|
+
const sourceMapConsumer = new SourceMapConsumer(sourcemap);
|
|
39
|
+
const originalPosition = sourceMapConsumer.originalPositionFor({
|
|
40
|
+
line,
|
|
41
|
+
column,
|
|
42
|
+
bias
|
|
43
|
+
});
|
|
44
|
+
return originalPosition;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const urlToScheme = url => {
|
|
48
|
+
const urlString = String(url);
|
|
49
|
+
const colonIndex = urlString.indexOf(":");
|
|
50
|
+
if (colonIndex === -1) {
|
|
51
|
+
return "";
|
|
52
|
+
}
|
|
53
|
+
const scheme = urlString.slice(0, colonIndex);
|
|
54
|
+
return scheme;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const urlToResource = url => {
|
|
58
|
+
const scheme = urlToScheme(url);
|
|
59
|
+
if (scheme === "file") {
|
|
60
|
+
const urlAsStringWithoutFileProtocol = String(url).slice("file://".length);
|
|
61
|
+
return urlAsStringWithoutFileProtocol;
|
|
62
|
+
}
|
|
63
|
+
if (scheme === "https" || scheme === "http") {
|
|
64
|
+
// remove origin
|
|
65
|
+
const afterProtocol = String(url).slice(scheme.length + "://".length);
|
|
66
|
+
const pathnameSlashIndex = afterProtocol.indexOf("/", "://".length);
|
|
67
|
+
const urlAsStringWithoutOrigin = afterProtocol.slice(pathnameSlashIndex);
|
|
68
|
+
return urlAsStringWithoutOrigin;
|
|
69
|
+
}
|
|
70
|
+
const urlAsStringWithoutProtocol = String(url).slice(scheme.length + 1);
|
|
71
|
+
return urlAsStringWithoutProtocol;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const urlToPathname = url => {
|
|
75
|
+
const resource = urlToResource(url);
|
|
76
|
+
const pathname = resourceToPathname(resource);
|
|
77
|
+
return pathname;
|
|
78
|
+
};
|
|
79
|
+
const resourceToPathname = resource => {
|
|
80
|
+
const searchSeparatorIndex = resource.indexOf("?");
|
|
81
|
+
if (searchSeparatorIndex > -1) {
|
|
82
|
+
return resource.slice(0, searchSeparatorIndex);
|
|
83
|
+
}
|
|
84
|
+
const hashIndex = resource.indexOf("#");
|
|
85
|
+
if (hashIndex > -1) {
|
|
86
|
+
return resource.slice(0, hashIndex);
|
|
87
|
+
}
|
|
88
|
+
return resource;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const urlToFilename = url => {
|
|
92
|
+
const pathname = urlToPathname(url);
|
|
93
|
+
const pathnameBeforeLastSlash = pathname.endsWith("/") ? pathname.slice(0, -1) : pathname;
|
|
94
|
+
const slashLastIndex = pathnameBeforeLastSlash.lastIndexOf("/");
|
|
95
|
+
const filename = slashLastIndex === -1 ? pathnameBeforeLastSlash : pathnameBeforeLastSlash.slice(slashLastIndex + 1);
|
|
96
|
+
return filename;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const generateInlineContentUrl = ({
|
|
100
|
+
url,
|
|
101
|
+
extension,
|
|
102
|
+
basename,
|
|
103
|
+
line,
|
|
104
|
+
column,
|
|
105
|
+
lineEnd,
|
|
106
|
+
columnEnd
|
|
107
|
+
}) => {
|
|
108
|
+
let generatedName = "";
|
|
109
|
+
if (basename !== undefined) {
|
|
110
|
+
generatedName += basename;
|
|
111
|
+
}
|
|
112
|
+
if (line !== undefined && column !== undefined) {
|
|
113
|
+
generatedName = "L".concat(line, "C").concat(column);
|
|
114
|
+
if (lineEnd !== undefined && columnEnd !== undefined) {
|
|
115
|
+
generatedName += "-L".concat(lineEnd, "C").concat(columnEnd);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const filenameRaw = urlToFilename(url);
|
|
119
|
+
const filename = "".concat(filenameRaw, "@").concat(generatedName).concat(extension);
|
|
120
|
+
// ideally we should keep query params from url
|
|
121
|
+
// maybe we could use a custom scheme like "inline:"
|
|
122
|
+
const inlineContentUrl = new URL(filename, url).href;
|
|
123
|
+
return inlineContentUrl;
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
// consider switching to https://babeljs.io/docs/en/babel-code-frame
|
|
127
|
+
// https://github.com/postcss/postcss/blob/fd30d3df5abc0954a0ec642a3cdc644ab2aacf9c/lib/css-syntax-error.js#L43
|
|
128
|
+
// https://github.com/postcss/postcss/blob/fd30d3df5abc0954a0ec642a3cdc644ab2aacf9c/lib/terminal-highlight.js#L50
|
|
129
|
+
// https://github.com/babel/babel/blob/eea156b2cb8deecfcf82d52aa1b71ba4995c7d68/packages/babel-code-frame/src/index.js#L1
|
|
130
|
+
|
|
131
|
+
const stringifyUrlSite = ({
|
|
132
|
+
url,
|
|
133
|
+
line,
|
|
134
|
+
column,
|
|
135
|
+
content
|
|
136
|
+
}, {
|
|
137
|
+
showCodeFrame = true,
|
|
138
|
+
numberOfSurroundingLinesToShow,
|
|
139
|
+
lineMaxLength,
|
|
140
|
+
color
|
|
141
|
+
} = {}) => {
|
|
142
|
+
let string = url;
|
|
143
|
+
if (typeof line === "number") {
|
|
144
|
+
string += ":".concat(line);
|
|
145
|
+
if (typeof column === "number") {
|
|
146
|
+
string += ":".concat(column);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (!showCodeFrame || typeof line !== "number" || !content) {
|
|
150
|
+
return string;
|
|
151
|
+
}
|
|
152
|
+
const sourceLoc = showSourceLocation({
|
|
153
|
+
content,
|
|
154
|
+
line,
|
|
155
|
+
column,
|
|
156
|
+
numberOfSurroundingLinesToShow,
|
|
157
|
+
lineMaxLength,
|
|
158
|
+
color
|
|
159
|
+
});
|
|
160
|
+
return "".concat(string, "\n").concat(sourceLoc);
|
|
161
|
+
};
|
|
162
|
+
const showSourceLocation = ({
|
|
163
|
+
content,
|
|
164
|
+
line,
|
|
165
|
+
column,
|
|
166
|
+
numberOfSurroundingLinesToShow = 1,
|
|
167
|
+
lineMaxLength = 120
|
|
168
|
+
} = {}) => {
|
|
169
|
+
let mark = string => string;
|
|
170
|
+
let aside = string => string;
|
|
171
|
+
// if (color) {
|
|
172
|
+
// mark = (string) => ANSI.color(string, ANSI.RED)
|
|
173
|
+
// aside = (string) => ANSI.color(string, ANSI.GREY)
|
|
174
|
+
// }
|
|
175
|
+
|
|
176
|
+
const lines = content.split(/\r?\n/);
|
|
177
|
+
if (line === 0) line = 1;
|
|
178
|
+
let lineRange = {
|
|
179
|
+
start: line - 1,
|
|
180
|
+
end: line
|
|
181
|
+
};
|
|
182
|
+
lineRange = moveLineRangeUp(lineRange, numberOfSurroundingLinesToShow);
|
|
183
|
+
lineRange = moveLineRangeDown(lineRange, numberOfSurroundingLinesToShow);
|
|
184
|
+
lineRange = lineRangeWithinLines(lineRange, lines);
|
|
185
|
+
const linesToShow = lines.slice(lineRange.start, lineRange.end);
|
|
186
|
+
const endLineNumber = lineRange.end;
|
|
187
|
+
const lineNumberMaxWidth = String(endLineNumber).length;
|
|
188
|
+
if (column === 0) column = 1;
|
|
189
|
+
const columnRange = {};
|
|
190
|
+
if (column === undefined) {
|
|
191
|
+
columnRange.start = 0;
|
|
192
|
+
columnRange.end = lineMaxLength;
|
|
193
|
+
} else if (column > lineMaxLength) {
|
|
194
|
+
columnRange.start = column - Math.floor(lineMaxLength / 2);
|
|
195
|
+
columnRange.end = column + Math.ceil(lineMaxLength / 2);
|
|
196
|
+
} else {
|
|
197
|
+
columnRange.start = 0;
|
|
198
|
+
columnRange.end = lineMaxLength;
|
|
199
|
+
}
|
|
200
|
+
return linesToShow.map((lineSource, index) => {
|
|
201
|
+
const lineNumber = lineRange.start + index + 1;
|
|
202
|
+
const isMainLine = lineNumber === line;
|
|
203
|
+
const lineSourceTruncated = applyColumnRange(columnRange, lineSource);
|
|
204
|
+
const lineNumberWidth = String(lineNumber).length;
|
|
205
|
+
// ensure if line moves from 7,8,9 to 10 the display is still great
|
|
206
|
+
const lineNumberRightSpacing = " ".repeat(lineNumberMaxWidth - lineNumberWidth);
|
|
207
|
+
const asideSource = "".concat(lineNumber).concat(lineNumberRightSpacing, " |");
|
|
208
|
+
const lineFormatted = "".concat(aside(asideSource), " ").concat(lineSourceTruncated);
|
|
209
|
+
if (isMainLine) {
|
|
210
|
+
if (column === undefined) {
|
|
211
|
+
return "".concat(mark(">"), " ").concat(lineFormatted);
|
|
212
|
+
}
|
|
213
|
+
const spacing = stringToSpaces("".concat(asideSource, " ").concat(lineSourceTruncated.slice(0, column - columnRange.start - 1)));
|
|
214
|
+
return "".concat(mark(">"), " ").concat(lineFormatted, "\n ").concat(spacing).concat(mark("^"));
|
|
215
|
+
}
|
|
216
|
+
return " ".concat(lineFormatted);
|
|
217
|
+
}).join("\n");
|
|
218
|
+
};
|
|
219
|
+
const applyColumnRange = ({
|
|
220
|
+
start,
|
|
221
|
+
end
|
|
222
|
+
}, line) => {
|
|
223
|
+
if (typeof start !== "number") {
|
|
224
|
+
throw new TypeError("start must be a number, received ".concat(start));
|
|
225
|
+
}
|
|
226
|
+
if (typeof end !== "number") {
|
|
227
|
+
throw new TypeError("end must be a number, received ".concat(end));
|
|
228
|
+
}
|
|
229
|
+
if (end < start) {
|
|
230
|
+
throw new Error("end must be greater than start, but ".concat(end, " is smaller than ").concat(start));
|
|
231
|
+
}
|
|
232
|
+
const prefix = "…";
|
|
233
|
+
const suffix = "…";
|
|
234
|
+
const lastIndex = line.length;
|
|
235
|
+
if (line.length === 0) {
|
|
236
|
+
// don't show any ellipsis if the line is empty
|
|
237
|
+
// because it's not truncated in that case
|
|
238
|
+
return "";
|
|
239
|
+
}
|
|
240
|
+
const startTruncated = start > 0;
|
|
241
|
+
const endTruncated = lastIndex > end;
|
|
242
|
+
let from = startTruncated ? start + prefix.length : start;
|
|
243
|
+
let to = endTruncated ? end - suffix.length : end;
|
|
244
|
+
if (to > lastIndex) to = lastIndex;
|
|
245
|
+
if (start >= lastIndex || from === to) {
|
|
246
|
+
return "";
|
|
247
|
+
}
|
|
248
|
+
let result = "";
|
|
249
|
+
while (from < to) {
|
|
250
|
+
result += line[from];
|
|
251
|
+
from++;
|
|
252
|
+
}
|
|
253
|
+
if (result.length === 0) {
|
|
254
|
+
return "";
|
|
255
|
+
}
|
|
256
|
+
if (startTruncated && endTruncated) {
|
|
257
|
+
return "".concat(prefix).concat(result).concat(suffix);
|
|
258
|
+
}
|
|
259
|
+
if (startTruncated) {
|
|
260
|
+
return "".concat(prefix).concat(result);
|
|
261
|
+
}
|
|
262
|
+
if (endTruncated) {
|
|
263
|
+
return "".concat(result).concat(suffix);
|
|
264
|
+
}
|
|
265
|
+
return result;
|
|
266
|
+
};
|
|
267
|
+
const stringToSpaces = string => string.replace(/[^\t]/g, " ");
|
|
268
|
+
|
|
269
|
+
// const getLineRangeLength = ({ start, end }) => end - start
|
|
270
|
+
|
|
271
|
+
const moveLineRangeUp = ({
|
|
272
|
+
start,
|
|
273
|
+
end
|
|
274
|
+
}, number) => {
|
|
275
|
+
return {
|
|
276
|
+
start: start - number,
|
|
277
|
+
end
|
|
278
|
+
};
|
|
279
|
+
};
|
|
280
|
+
const moveLineRangeDown = ({
|
|
281
|
+
start,
|
|
282
|
+
end
|
|
283
|
+
}, number) => {
|
|
284
|
+
return {
|
|
285
|
+
start,
|
|
286
|
+
end: end + number
|
|
287
|
+
};
|
|
288
|
+
};
|
|
289
|
+
const lineRangeWithinLines = ({
|
|
290
|
+
start,
|
|
291
|
+
end
|
|
292
|
+
}, lines) => {
|
|
293
|
+
return {
|
|
294
|
+
start: start < 0 ? 0 : start,
|
|
295
|
+
end: end > lines.length ? lines.length : end
|
|
296
|
+
};
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
const getCommonPathname = (pathname, otherPathname) => {
|
|
300
|
+
if (pathname === otherPathname) {
|
|
301
|
+
return pathname;
|
|
302
|
+
}
|
|
303
|
+
let commonPart = "";
|
|
304
|
+
let commonPathname = "";
|
|
305
|
+
let i = 0;
|
|
306
|
+
const length = pathname.length;
|
|
307
|
+
const otherLength = otherPathname.length;
|
|
308
|
+
while (i < length) {
|
|
309
|
+
const char = pathname.charAt(i);
|
|
310
|
+
const otherChar = otherPathname.charAt(i);
|
|
311
|
+
i++;
|
|
312
|
+
if (char === otherChar) {
|
|
313
|
+
if (char === "/") {
|
|
314
|
+
commonPart += "/";
|
|
315
|
+
commonPathname += commonPart;
|
|
316
|
+
commonPart = "";
|
|
317
|
+
} else {
|
|
318
|
+
commonPart += char;
|
|
319
|
+
}
|
|
320
|
+
} else {
|
|
321
|
+
if (char === "/" && i - 1 === otherLength) {
|
|
322
|
+
commonPart += "/";
|
|
323
|
+
commonPathname += commonPart;
|
|
324
|
+
}
|
|
325
|
+
return commonPathname;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
if (length === otherLength) {
|
|
329
|
+
commonPathname += commonPart;
|
|
330
|
+
} else if (otherPathname.charAt(i) === "/") {
|
|
331
|
+
commonPathname += commonPart;
|
|
332
|
+
}
|
|
333
|
+
return commonPathname;
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
const urlToRelativeUrl = (url, baseUrl) => {
|
|
337
|
+
const urlObject = new URL(url);
|
|
338
|
+
const baseUrlObject = new URL(baseUrl);
|
|
339
|
+
if (urlObject.protocol !== baseUrlObject.protocol) {
|
|
340
|
+
const urlAsString = String(url);
|
|
341
|
+
return urlAsString;
|
|
342
|
+
}
|
|
343
|
+
if (urlObject.username !== baseUrlObject.username || urlObject.password !== baseUrlObject.password || urlObject.host !== baseUrlObject.host) {
|
|
344
|
+
const afterUrlScheme = String(url).slice(urlObject.protocol.length);
|
|
345
|
+
return afterUrlScheme;
|
|
346
|
+
}
|
|
347
|
+
const {
|
|
348
|
+
pathname,
|
|
349
|
+
hash,
|
|
350
|
+
search
|
|
351
|
+
} = urlObject;
|
|
352
|
+
if (pathname === "/") {
|
|
353
|
+
const baseUrlResourceWithoutLeadingSlash = baseUrlObject.pathname.slice(1);
|
|
354
|
+
return baseUrlResourceWithoutLeadingSlash;
|
|
355
|
+
}
|
|
356
|
+
const basePathname = baseUrlObject.pathname;
|
|
357
|
+
const commonPathname = getCommonPathname(pathname, basePathname);
|
|
358
|
+
if (!commonPathname) {
|
|
359
|
+
const urlAsString = String(url);
|
|
360
|
+
return urlAsString;
|
|
361
|
+
}
|
|
362
|
+
const specificPathname = pathname.slice(commonPathname.length);
|
|
363
|
+
const baseSpecificPathname = basePathname.slice(commonPathname.length);
|
|
364
|
+
if (baseSpecificPathname.includes("/")) {
|
|
365
|
+
const baseSpecificParentPathname = pathnameToParentPathname(baseSpecificPathname);
|
|
366
|
+
const relativeDirectoriesNotation = baseSpecificParentPathname.replace(/.*?\//g, "../");
|
|
367
|
+
const relativeUrl = "".concat(relativeDirectoriesNotation).concat(specificPathname).concat(search).concat(hash);
|
|
368
|
+
return relativeUrl;
|
|
369
|
+
}
|
|
370
|
+
const relativeUrl = "".concat(specificPathname).concat(search).concat(hash);
|
|
371
|
+
return relativeUrl;
|
|
372
|
+
};
|
|
373
|
+
const pathnameToParentPathname = pathname => {
|
|
374
|
+
const slashLastIndex = pathname.lastIndexOf("/");
|
|
375
|
+
if (slashLastIndex === -1) {
|
|
376
|
+
return "/";
|
|
377
|
+
}
|
|
378
|
+
return pathname.slice(0, slashLastIndex + 1);
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
const generateSourcemapDataUrl = sourcemap => {
|
|
382
|
+
const asBase64 = Buffer.from(JSON.stringify(sourcemap)).toString("base64");
|
|
383
|
+
return "data:application/json;charset=utf-8;base64,".concat(asBase64);
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
const SOURCEMAP = {
|
|
387
|
+
enabledOnContentType: contentType => {
|
|
388
|
+
return ["text/javascript", "text/css"].includes(contentType);
|
|
389
|
+
},
|
|
390
|
+
readComment: ({
|
|
391
|
+
contentType,
|
|
392
|
+
content
|
|
393
|
+
}) => {
|
|
394
|
+
const read = {
|
|
395
|
+
"text/javascript": parseJavaScriptSourcemapComment,
|
|
396
|
+
"text/css": parseCssSourcemapComment
|
|
397
|
+
}[contentType];
|
|
398
|
+
return read ? read(content) : null;
|
|
399
|
+
},
|
|
400
|
+
writeComment: ({
|
|
401
|
+
contentType,
|
|
402
|
+
content,
|
|
403
|
+
specifier
|
|
404
|
+
}) => {
|
|
405
|
+
const write = {
|
|
406
|
+
"text/javascript": setJavaScriptSourceMappingUrl,
|
|
407
|
+
"text/css": setCssSourceMappingUrl
|
|
408
|
+
}[contentType];
|
|
409
|
+
return write ? write(content, specifier) : content;
|
|
410
|
+
}
|
|
411
|
+
};
|
|
412
|
+
const parseJavaScriptSourcemapComment = javaScriptSource => {
|
|
413
|
+
let sourceMappingUrl;
|
|
414
|
+
replaceSourceMappingUrl(javaScriptSource, javascriptSourceMappingUrlCommentRegexp, value => {
|
|
415
|
+
sourceMappingUrl = value;
|
|
416
|
+
});
|
|
417
|
+
if (!sourceMappingUrl) {
|
|
418
|
+
return null;
|
|
419
|
+
}
|
|
420
|
+
return {
|
|
421
|
+
type: "sourcemap_comment",
|
|
422
|
+
subtype: "js",
|
|
423
|
+
// we assume it's on last line
|
|
424
|
+
line: javaScriptSource.split(/\r?\n/).length,
|
|
425
|
+
// ${"//#"} is to avoid static analysis to think there is a sourceMappingUrl for this file
|
|
426
|
+
column: "//#".concat(" sourceMappingURL=").length + 1,
|
|
427
|
+
specifier: sourceMappingUrl
|
|
428
|
+
};
|
|
429
|
+
};
|
|
430
|
+
const setJavaScriptSourceMappingUrl = (javaScriptSource, sourceMappingFileUrl) => {
|
|
431
|
+
let replaced;
|
|
432
|
+
const sourceAfterReplace = replaceSourceMappingUrl(javaScriptSource, javascriptSourceMappingUrlCommentRegexp, () => {
|
|
433
|
+
replaced = true;
|
|
434
|
+
return sourceMappingFileUrl ? writeJavaScriptSourceMappingURL(sourceMappingFileUrl) : "";
|
|
435
|
+
});
|
|
436
|
+
if (replaced) {
|
|
437
|
+
return sourceAfterReplace;
|
|
438
|
+
}
|
|
439
|
+
return sourceMappingFileUrl ? "".concat(javaScriptSource, "\n").concat(writeJavaScriptSourceMappingURL(sourceMappingFileUrl), "\n") : javaScriptSource;
|
|
440
|
+
};
|
|
441
|
+
const parseCssSourcemapComment = cssSource => {
|
|
442
|
+
let sourceMappingUrl;
|
|
443
|
+
replaceSourceMappingUrl(cssSource, cssSourceMappingUrlCommentRegExp, value => {
|
|
444
|
+
sourceMappingUrl = value;
|
|
445
|
+
});
|
|
446
|
+
if (!sourceMappingUrl) {
|
|
447
|
+
return null;
|
|
448
|
+
}
|
|
449
|
+
return {
|
|
450
|
+
type: "sourcemap_comment",
|
|
451
|
+
subtype: "css",
|
|
452
|
+
// we assume it's on last line
|
|
453
|
+
line: cssSource.split(/\r?\n/).length - 1,
|
|
454
|
+
// ${"//*#"} is to avoid static analysis to think there is a sourceMappingUrl for this file
|
|
455
|
+
column: "//*#".concat(" sourceMappingURL=").length + 1,
|
|
456
|
+
specifier: sourceMappingUrl
|
|
457
|
+
};
|
|
458
|
+
};
|
|
459
|
+
const setCssSourceMappingUrl = (cssSource, sourceMappingFileUrl) => {
|
|
460
|
+
let replaced;
|
|
461
|
+
const sourceAfterReplace = replaceSourceMappingUrl(cssSource, cssSourceMappingUrlCommentRegExp, () => {
|
|
462
|
+
replaced = true;
|
|
463
|
+
return sourceMappingFileUrl ? writeCssSourceMappingUrl(sourceMappingFileUrl) : "";
|
|
464
|
+
});
|
|
465
|
+
if (replaced) {
|
|
466
|
+
return sourceAfterReplace;
|
|
467
|
+
}
|
|
468
|
+
return sourceMappingFileUrl ? "".concat(cssSource, "\n").concat(writeCssSourceMappingUrl(sourceMappingFileUrl), "\n") : cssSource;
|
|
469
|
+
};
|
|
470
|
+
const javascriptSourceMappingUrlCommentRegexp = /\/\/ ?# ?sourceMappingURL=([^\s'"]+)/g;
|
|
471
|
+
const cssSourceMappingUrlCommentRegExp = /\/\*# ?sourceMappingURL=([^\s'"]+) \*\//g;
|
|
472
|
+
|
|
473
|
+
// ${"//#"} is to avoid a parser thinking there is a sourceMappingUrl for this file
|
|
474
|
+
const writeJavaScriptSourceMappingURL = value => "//#".concat(" sourceMappingURL=", value);
|
|
475
|
+
const writeCssSourceMappingUrl = value => "/*# sourceMappingURL=".concat(value, " */");
|
|
476
|
+
const replaceSourceMappingUrl = (source, regexp, callback) => {
|
|
477
|
+
let lastSourceMappingUrl;
|
|
478
|
+
let matchSourceMappingUrl;
|
|
479
|
+
while (matchSourceMappingUrl = regexp.exec(source)) {
|
|
480
|
+
lastSourceMappingUrl = matchSourceMappingUrl;
|
|
481
|
+
}
|
|
482
|
+
if (lastSourceMappingUrl) {
|
|
483
|
+
const index = lastSourceMappingUrl.index;
|
|
484
|
+
const before = source.slice(0, index);
|
|
485
|
+
const after = source.slice(index);
|
|
486
|
+
const mappedAfter = after.replace(regexp, (match, firstGroup) => {
|
|
487
|
+
return callback(firstGroup);
|
|
488
|
+
});
|
|
489
|
+
return "".concat(before).concat(mappedAfter);
|
|
490
|
+
}
|
|
491
|
+
return source;
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
/*
|
|
495
|
+
* ```js
|
|
496
|
+
* console.log(42)
|
|
497
|
+
* ```
|
|
498
|
+
* becomes
|
|
499
|
+
* ```js
|
|
500
|
+
* window.__supervisor__.jsClassicStart('main.html@L10-L13.js')
|
|
501
|
+
* try {
|
|
502
|
+
* console.log(42)
|
|
503
|
+
* window.__supervisor__.jsClassicEnd('main.html@L10-L13.js')
|
|
504
|
+
* } catch(e) {
|
|
505
|
+
* window.__supervisor__.jsClassicError('main.html@L10-L13.js', e)
|
|
506
|
+
* }
|
|
507
|
+
* ```
|
|
508
|
+
*
|
|
509
|
+
* ```js
|
|
510
|
+
* import value from "./file.js"
|
|
511
|
+
* console.log(value)
|
|
512
|
+
* ```
|
|
513
|
+
* becomes
|
|
514
|
+
* ```js
|
|
515
|
+
* window.__supervisor__.jsModuleStart('main.html@L10-L13.js')
|
|
516
|
+
* try {
|
|
517
|
+
* const value = await import("./file.js")
|
|
518
|
+
* console.log(value)
|
|
519
|
+
* window.__supervisor__.jsModuleEnd('main.html@L10-L13.js')
|
|
520
|
+
* } catch(e) {
|
|
521
|
+
* window.__supervisor__.jsModuleError('main.html@L10-L13.js', e)
|
|
522
|
+
* }
|
|
523
|
+
* ```
|
|
524
|
+
*
|
|
525
|
+
* -> TO KEEP IN MIND:
|
|
526
|
+
* Static import can throw errors like
|
|
527
|
+
* The requested module '/js_module_export_not_found/foo.js' does not provide an export named 'answerr'
|
|
528
|
+
* While dynamic import will work just fine
|
|
529
|
+
* and create a variable named "undefined"
|
|
530
|
+
*/
|
|
531
|
+
|
|
532
|
+
const injectSupervisorIntoJs = async ({
|
|
533
|
+
webServer,
|
|
534
|
+
content,
|
|
535
|
+
url,
|
|
536
|
+
type,
|
|
537
|
+
inlineSrc
|
|
538
|
+
}) => {
|
|
539
|
+
const babelPluginJsSupervisor = type === "js_module" ? babelPluginJsModuleSupervisor : babelPluginJsClassicSupervisor;
|
|
540
|
+
const result = await applyBabelPlugins({
|
|
541
|
+
babelPlugins: [[babelPluginJsSupervisor, {
|
|
542
|
+
inlineSrc
|
|
543
|
+
}]],
|
|
544
|
+
input: content,
|
|
545
|
+
inputIsJsModule: type === "js_module",
|
|
546
|
+
inputUrl: url
|
|
547
|
+
});
|
|
548
|
+
let code = result.code;
|
|
549
|
+
let map = result.map;
|
|
550
|
+
const sourcemapDataUrl = generateSourcemapDataUrl(map);
|
|
551
|
+
code = SOURCEMAP.writeComment({
|
|
552
|
+
contentType: "text/javascript",
|
|
553
|
+
content: code,
|
|
554
|
+
specifier: sourcemapDataUrl
|
|
555
|
+
});
|
|
556
|
+
code = "".concat(code, "\n//# sourceURL=").concat(urlToRelativeUrl(url, webServer.rootDirectoryUrl));
|
|
557
|
+
return code;
|
|
558
|
+
};
|
|
559
|
+
const babelPluginJsModuleSupervisor = babel => {
|
|
560
|
+
const t = babel.types;
|
|
561
|
+
return {
|
|
562
|
+
name: "js-module-supervisor",
|
|
563
|
+
visitor: {
|
|
564
|
+
Program: (programPath, state) => {
|
|
565
|
+
const {
|
|
566
|
+
inlineSrc
|
|
567
|
+
} = state.opts;
|
|
568
|
+
if (state.file.metadata.jsExecutionInstrumented) return;
|
|
569
|
+
state.file.metadata.jsExecutionInstrumented = true;
|
|
570
|
+
const urlNode = t.stringLiteral(inlineSrc);
|
|
571
|
+
const startCallNode = createSupervisionCall({
|
|
572
|
+
t,
|
|
573
|
+
urlNode,
|
|
574
|
+
methodName: "jsModuleStart"
|
|
575
|
+
});
|
|
576
|
+
const endCallNode = createSupervisionCall({
|
|
577
|
+
t,
|
|
578
|
+
urlNode,
|
|
579
|
+
methodName: "jsModuleEnd"
|
|
580
|
+
});
|
|
581
|
+
const errorCallNode = createSupervisionCall({
|
|
582
|
+
t,
|
|
583
|
+
urlNode,
|
|
584
|
+
methodName: "jsModuleError",
|
|
585
|
+
args: [t.identifier("e")]
|
|
586
|
+
});
|
|
587
|
+
const bodyPath = programPath.get("body");
|
|
588
|
+
const importNodes = [];
|
|
589
|
+
const topLevelNodes = [];
|
|
590
|
+
for (const topLevelNodePath of bodyPath) {
|
|
591
|
+
const topLevelNode = topLevelNodePath.node;
|
|
592
|
+
if (t.isImportDeclaration(topLevelNode)) {
|
|
593
|
+
importNodes.push(topLevelNode);
|
|
594
|
+
} else {
|
|
595
|
+
topLevelNodes.push(topLevelNode);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// replace all import nodes with dynamic imports
|
|
600
|
+
const dynamicImports = [];
|
|
601
|
+
importNodes.forEach(importNode => {
|
|
602
|
+
const dynamicImportConversion = convertStaticImportIntoDynamicImport(importNode, t);
|
|
603
|
+
if (Array.isArray(dynamicImportConversion)) {
|
|
604
|
+
dynamicImports.push(...dynamicImportConversion);
|
|
605
|
+
} else {
|
|
606
|
+
dynamicImports.push(dynamicImportConversion);
|
|
607
|
+
}
|
|
608
|
+
});
|
|
609
|
+
const tryCatchNode = t.tryStatement(t.blockStatement([...dynamicImports, ...topLevelNodes, endCallNode]), t.catchClause(t.identifier("e"), t.blockStatement([errorCallNode])));
|
|
610
|
+
programPath.replaceWith(t.program([startCallNode, tryCatchNode]));
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
};
|
|
614
|
+
};
|
|
615
|
+
const convertStaticImportIntoDynamicImport = (staticImportNode, t) => {
|
|
616
|
+
const awaitExpression = t.awaitExpression(t.callExpression(t.import(), [t.stringLiteral(staticImportNode.source.value)]));
|
|
617
|
+
|
|
618
|
+
// import "./file.js" -> await import("./file.js")
|
|
619
|
+
if (staticImportNode.specifiers.length === 0) {
|
|
620
|
+
return t.expressionStatement(awaitExpression);
|
|
621
|
+
}
|
|
622
|
+
if (staticImportNode.specifiers.length === 1) {
|
|
623
|
+
const [firstSpecifier] = staticImportNode.specifiers;
|
|
624
|
+
if (firstSpecifier.type === "ImportNamespaceSpecifier") {
|
|
625
|
+
return t.variableDeclaration("const", [t.variableDeclarator(t.identifier(firstSpecifier.local.name), awaitExpression)]);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
if (staticImportNode.specifiers.length === 2) {
|
|
629
|
+
const [first, second] = staticImportNode.specifiers;
|
|
630
|
+
if (first.type === "ImportDefaultSpecifier" && second.type === "ImportNamespaceSpecifier") {
|
|
631
|
+
const namespaceDeclaration = t.variableDeclaration("const", [t.variableDeclarator(t.identifier(second.local.name), awaitExpression)]);
|
|
632
|
+
const defaultDeclaration = t.variableDeclaration("const", [t.variableDeclarator(t.identifier(first.local.name), t.memberExpression(t.identifier(second.local.name), t.identifier("default")))]);
|
|
633
|
+
return [namespaceDeclaration, defaultDeclaration];
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// import { name } from "./file.js" -> const { name } = await import("./file.js")
|
|
638
|
+
// import toto, { name } from "./file.js" -> const { name, default as toto } = await import("./file.js")
|
|
639
|
+
const objectPattern = t.objectPattern(staticImportNode.specifiers.map(specifier => {
|
|
640
|
+
if (specifier.type === "ImportDefaultSpecifier") {
|
|
641
|
+
return t.objectProperty(t.identifier("default"), t.identifier(specifier.local.name), false,
|
|
642
|
+
// computed
|
|
643
|
+
false // shorthand
|
|
644
|
+
);
|
|
645
|
+
}
|
|
646
|
+
// if (specifier.type === "ImportNamespaceSpecifier") {
|
|
647
|
+
// return t.restElement(t.identifier(specifier.local.name))
|
|
648
|
+
// }
|
|
649
|
+
const isRenamed = specifier.imported.name !== specifier.local.name;
|
|
650
|
+
if (isRenamed) {
|
|
651
|
+
return t.objectProperty(t.identifier(specifier.imported.name), t.identifier(specifier.local.name), false,
|
|
652
|
+
// computed
|
|
653
|
+
false // shorthand
|
|
654
|
+
);
|
|
655
|
+
}
|
|
656
|
+
// shorthand must be true
|
|
657
|
+
return t.objectProperty(t.identifier(specifier.local.name), t.identifier(specifier.local.name), false,
|
|
658
|
+
// computed
|
|
659
|
+
true // shorthand
|
|
660
|
+
);
|
|
661
|
+
}));
|
|
662
|
+
|
|
663
|
+
const variableDeclarator = t.variableDeclarator(objectPattern, awaitExpression);
|
|
664
|
+
const variableDeclaration = t.variableDeclaration("const", [variableDeclarator]);
|
|
665
|
+
return variableDeclaration;
|
|
666
|
+
};
|
|
667
|
+
const babelPluginJsClassicSupervisor = babel => {
|
|
668
|
+
const t = babel.types;
|
|
669
|
+
return {
|
|
670
|
+
name: "js-classic-supervisor",
|
|
671
|
+
visitor: {
|
|
672
|
+
Program: (programPath, state) => {
|
|
673
|
+
const {
|
|
674
|
+
inlineSrc
|
|
675
|
+
} = state.opts;
|
|
676
|
+
if (state.file.metadata.jsExecutionInstrumented) return;
|
|
677
|
+
state.file.metadata.jsExecutionInstrumented = true;
|
|
678
|
+
const urlNode = t.stringLiteral(inlineSrc);
|
|
679
|
+
const startCallNode = createSupervisionCall({
|
|
680
|
+
t,
|
|
681
|
+
urlNode,
|
|
682
|
+
methodName: "jsClassicStart"
|
|
683
|
+
});
|
|
684
|
+
const endCallNode = createSupervisionCall({
|
|
685
|
+
t,
|
|
686
|
+
urlNode,
|
|
687
|
+
methodName: "jsClassicEnd"
|
|
688
|
+
});
|
|
689
|
+
const errorCallNode = createSupervisionCall({
|
|
690
|
+
t,
|
|
691
|
+
urlNode,
|
|
692
|
+
methodName: "jsClassicError",
|
|
693
|
+
args: [t.identifier("e")]
|
|
694
|
+
});
|
|
695
|
+
const topLevelNodes = programPath.node.body;
|
|
696
|
+
const tryCatchNode = t.tryStatement(t.blockStatement([...topLevelNodes, endCallNode]), t.catchClause(t.identifier("e"), t.blockStatement([errorCallNode])));
|
|
697
|
+
programPath.replaceWith(t.program([startCallNode, tryCatchNode]));
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
};
|
|
701
|
+
};
|
|
702
|
+
const createSupervisionCall = ({
|
|
703
|
+
t,
|
|
704
|
+
methodName,
|
|
705
|
+
urlNode,
|
|
706
|
+
args = []
|
|
707
|
+
}) => {
|
|
708
|
+
return t.expressionStatement(t.callExpression(t.memberExpression(t.memberExpression(t.identifier("window"), t.identifier("__supervisor__")), t.identifier(methodName)), [urlNode, ...args]), [], null);
|
|
709
|
+
};
|
|
710
|
+
|
|
711
|
+
/*
|
|
712
|
+
* Jsenv needs to track js execution in order to:
|
|
713
|
+
* 1. report errors
|
|
714
|
+
* 2. wait for all js execution inside an HTML page before killing the browser
|
|
715
|
+
*
|
|
716
|
+
* A naive approach would rely on "load" events on window but:
|
|
717
|
+
* scenario | covered by window "load"
|
|
718
|
+
* ------------------------------------------- | -------------------------
|
|
719
|
+
* js referenced by <script src> | yes
|
|
720
|
+
* js inlined into <script> | yes
|
|
721
|
+
* js referenced by <script type="module" src> | partially (not for import and top level await)
|
|
722
|
+
* js inlined into <script type="module"> | not at all
|
|
723
|
+
* Same for "error" event on window who is not enough
|
|
724
|
+
*
|
|
725
|
+
* <script src="file.js">
|
|
726
|
+
* becomes
|
|
727
|
+
* <script>
|
|
728
|
+
* window.__supervisor__.superviseScript('file.js')
|
|
729
|
+
* </script>
|
|
730
|
+
*
|
|
731
|
+
* <script>
|
|
732
|
+
* console.log(42)
|
|
733
|
+
* </script>
|
|
734
|
+
* becomes
|
|
735
|
+
* <script inlined-from-src="main.html@L10-C5.js">
|
|
736
|
+
* window.__supervisor.__superviseScript("main.html@L10-C5.js")
|
|
737
|
+
* </script>
|
|
738
|
+
*
|
|
739
|
+
* <script type="module" src="module.js"></script>
|
|
740
|
+
* becomes
|
|
741
|
+
* <script type="module">
|
|
742
|
+
* window.__supervisor__.superviseScriptTypeModule('module.js')
|
|
743
|
+
* </script>
|
|
744
|
+
*
|
|
745
|
+
* <script type="module">
|
|
746
|
+
* console.log(42)
|
|
747
|
+
* </script>
|
|
748
|
+
* becomes
|
|
749
|
+
* <script type="module" inlined-from-src="main.html@L10-C5.js">
|
|
750
|
+
* window.__supervisor__.superviseScriptTypeModule('main.html@L10-C5.js')
|
|
751
|
+
* </script>
|
|
752
|
+
*
|
|
753
|
+
* Why Inline scripts are converted to files dynamically?
|
|
754
|
+
* -> No changes required on js source code, it's only the HTML that is modified
|
|
755
|
+
* - Also allow to catch syntax errors and export missing
|
|
756
|
+
*/
|
|
757
|
+
|
|
758
|
+
const supervisorFileUrl = new URL("./js/supervisor.js", import.meta.url).href;
|
|
759
|
+
const injectSupervisorIntoHTML = async ({
|
|
760
|
+
content,
|
|
761
|
+
url
|
|
762
|
+
}, {
|
|
763
|
+
supervisorScriptSrc = supervisorFileUrl,
|
|
764
|
+
supervisorOptions,
|
|
765
|
+
webServer,
|
|
766
|
+
onInlineScript = () => {},
|
|
767
|
+
generateInlineScriptSrc = ({
|
|
768
|
+
inlineScriptUrl
|
|
769
|
+
}) => urlToRelativeUrl(inlineScriptUrl, webServer.rootDirectoryUrl),
|
|
770
|
+
inlineAsRemote
|
|
771
|
+
}) => {
|
|
772
|
+
const htmlAst = parseHtmlString(content);
|
|
773
|
+
const mutations = [];
|
|
774
|
+
const actions = [];
|
|
775
|
+
const scriptInfos = [];
|
|
776
|
+
// 1. Find inline and remote scripts
|
|
777
|
+
{
|
|
778
|
+
const handleInlineScript = (scriptNode, {
|
|
779
|
+
type,
|
|
780
|
+
extension,
|
|
781
|
+
textContent
|
|
782
|
+
}) => {
|
|
783
|
+
const {
|
|
784
|
+
line,
|
|
785
|
+
column,
|
|
786
|
+
lineEnd,
|
|
787
|
+
columnEnd,
|
|
788
|
+
isOriginal
|
|
789
|
+
} = getHtmlNodePosition(scriptNode, {
|
|
790
|
+
preferOriginal: true
|
|
791
|
+
});
|
|
792
|
+
const inlineScriptUrl = generateInlineContentUrl({
|
|
793
|
+
url,
|
|
794
|
+
extension: extension || ".js",
|
|
795
|
+
line,
|
|
796
|
+
column,
|
|
797
|
+
lineEnd,
|
|
798
|
+
columnEnd
|
|
799
|
+
});
|
|
800
|
+
const inlineScriptSrc = generateInlineScriptSrc({
|
|
801
|
+
type,
|
|
802
|
+
textContent,
|
|
803
|
+
inlineScriptUrl,
|
|
804
|
+
isOriginal,
|
|
805
|
+
line,
|
|
806
|
+
column
|
|
807
|
+
});
|
|
808
|
+
onInlineScript({
|
|
809
|
+
type,
|
|
810
|
+
textContent,
|
|
811
|
+
url: inlineScriptUrl,
|
|
812
|
+
isOriginal,
|
|
813
|
+
line,
|
|
814
|
+
column,
|
|
815
|
+
src: inlineScriptSrc
|
|
816
|
+
});
|
|
817
|
+
if (inlineAsRemote) {
|
|
818
|
+
// prefere la version src
|
|
819
|
+
scriptInfos.push({
|
|
820
|
+
type,
|
|
821
|
+
src: inlineScriptSrc
|
|
822
|
+
});
|
|
823
|
+
const remoteJsSupervised = generateCodeToSuperviseScriptWithSrc({
|
|
824
|
+
type,
|
|
825
|
+
src: inlineScriptSrc
|
|
826
|
+
});
|
|
827
|
+
mutations.push(() => {
|
|
828
|
+
setHtmlNodeText(scriptNode, remoteJsSupervised, {
|
|
829
|
+
indentation: "auto"
|
|
830
|
+
});
|
|
831
|
+
setHtmlNodeAttributes(scriptNode, {
|
|
832
|
+
"jsenv-cooked-by": "jsenv:supervisor",
|
|
833
|
+
"src": undefined,
|
|
834
|
+
"inlined-from-src": inlineScriptSrc
|
|
835
|
+
});
|
|
836
|
+
});
|
|
837
|
+
} else {
|
|
838
|
+
scriptInfos.push({
|
|
839
|
+
type,
|
|
840
|
+
src: inlineScriptSrc,
|
|
841
|
+
isInline: true
|
|
842
|
+
});
|
|
843
|
+
actions.push(async () => {
|
|
844
|
+
try {
|
|
845
|
+
const inlineJsSupervised = await injectSupervisorIntoJs({
|
|
846
|
+
webServer,
|
|
847
|
+
content: textContent,
|
|
848
|
+
url: inlineScriptUrl,
|
|
849
|
+
type,
|
|
850
|
+
inlineSrc: inlineScriptSrc
|
|
851
|
+
});
|
|
852
|
+
mutations.push(() => {
|
|
853
|
+
setHtmlNodeText(scriptNode, inlineJsSupervised, {
|
|
854
|
+
indentation: "auto"
|
|
855
|
+
});
|
|
856
|
+
setHtmlNodeAttributes(scriptNode, {
|
|
857
|
+
"jsenv-cooked-by": "jsenv:supervisor"
|
|
858
|
+
});
|
|
859
|
+
});
|
|
860
|
+
} catch (e) {
|
|
861
|
+
if (e.code === "PARSE_ERROR") {
|
|
862
|
+
// mutations.push(() => {
|
|
863
|
+
// setHtmlNodeAttributes(scriptNode, {
|
|
864
|
+
// "jsenv-cooked-by": "jsenv:supervisor",
|
|
865
|
+
// })
|
|
866
|
+
// })
|
|
867
|
+
// on touche a rien
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
870
|
+
throw e;
|
|
871
|
+
}
|
|
872
|
+
});
|
|
873
|
+
}
|
|
874
|
+
};
|
|
875
|
+
const handleScriptWithSrc = (scriptNode, {
|
|
876
|
+
type,
|
|
877
|
+
src
|
|
878
|
+
}) => {
|
|
879
|
+
scriptInfos.push({
|
|
880
|
+
type,
|
|
881
|
+
src
|
|
882
|
+
});
|
|
883
|
+
const remoteJsSupervised = generateCodeToSuperviseScriptWithSrc({
|
|
884
|
+
type,
|
|
885
|
+
src
|
|
886
|
+
});
|
|
887
|
+
mutations.push(() => {
|
|
888
|
+
setHtmlNodeText(scriptNode, remoteJsSupervised, {
|
|
889
|
+
indentation: "auto"
|
|
890
|
+
});
|
|
891
|
+
setHtmlNodeAttributes(scriptNode, {
|
|
892
|
+
"jsenv-cooked-by": "jsenv:supervisor",
|
|
893
|
+
"src": undefined,
|
|
894
|
+
"inlined-from-src": src
|
|
895
|
+
});
|
|
896
|
+
});
|
|
897
|
+
};
|
|
898
|
+
visitHtmlNodes(htmlAst, {
|
|
899
|
+
script: scriptNode => {
|
|
900
|
+
const {
|
|
901
|
+
type,
|
|
902
|
+
extension
|
|
903
|
+
} = analyzeScriptNode(scriptNode);
|
|
904
|
+
if (type !== "js_classic" && type !== "js_module") {
|
|
905
|
+
return;
|
|
906
|
+
}
|
|
907
|
+
if (getHtmlNodeAttribute(scriptNode, "jsenv-injected-by")) {
|
|
908
|
+
return;
|
|
909
|
+
}
|
|
910
|
+
const noSupervisor = getHtmlNodeAttribute(scriptNode, "no-supervisor");
|
|
911
|
+
if (noSupervisor !== undefined) {
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
const scriptNodeText = getHtmlNodeText(scriptNode);
|
|
915
|
+
if (scriptNodeText) {
|
|
916
|
+
handleInlineScript(scriptNode, {
|
|
917
|
+
type,
|
|
918
|
+
extension,
|
|
919
|
+
textContent: scriptNodeText
|
|
920
|
+
});
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
923
|
+
const src = getHtmlNodeAttribute(scriptNode, "src");
|
|
924
|
+
if (src) {
|
|
925
|
+
const urlObject = new URL(src, "http://example.com/");
|
|
926
|
+
if (urlObject.searchParams.has("inline")) {
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
handleScriptWithSrc(scriptNode, {
|
|
930
|
+
type,
|
|
931
|
+
src
|
|
932
|
+
});
|
|
933
|
+
return;
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
});
|
|
937
|
+
}
|
|
938
|
+
// 2. Inject supervisor js file + setup call
|
|
939
|
+
{
|
|
940
|
+
const setupParamsSource = stringifyParams({
|
|
941
|
+
...supervisorOptions,
|
|
942
|
+
serverIsJsenvDevServer: webServer.isJsenvDevServer,
|
|
943
|
+
rootDirectoryUrl: webServer.rootDirectoryUrl,
|
|
944
|
+
scriptInfos
|
|
945
|
+
}, " ");
|
|
946
|
+
injectHtmlNodeAsEarlyAsPossible(htmlAst, createHtmlNode({
|
|
947
|
+
tagName: "script",
|
|
948
|
+
textContent: "window.__supervisor__.setup({".concat(setupParamsSource, "})")
|
|
949
|
+
}), "jsenv:supervisor");
|
|
950
|
+
injectHtmlNodeAsEarlyAsPossible(htmlAst, createHtmlNode({
|
|
951
|
+
tagName: "script",
|
|
952
|
+
src: supervisorScriptSrc
|
|
953
|
+
}), "jsenv:supervisor");
|
|
954
|
+
}
|
|
955
|
+
// 3. Perform actions (transforming inline script content) and html mutations
|
|
956
|
+
if (actions.length > 0) {
|
|
957
|
+
await Promise.all(actions.map(action => action()));
|
|
958
|
+
}
|
|
959
|
+
mutations.forEach(mutation => mutation());
|
|
960
|
+
const htmlModified = stringifyHtmlAst(htmlAst);
|
|
961
|
+
return {
|
|
962
|
+
content: htmlModified
|
|
963
|
+
};
|
|
964
|
+
};
|
|
965
|
+
const stringifyParams = (params, prefix = "") => {
|
|
966
|
+
const source = JSON.stringify(params, null, prefix);
|
|
967
|
+
if (prefix.length) {
|
|
968
|
+
// remove leading "{\n"
|
|
969
|
+
// remove leading prefix
|
|
970
|
+
// remove trailing "\n}"
|
|
971
|
+
return source.slice(2 + prefix.length, -2);
|
|
972
|
+
}
|
|
973
|
+
// remove leading "{"
|
|
974
|
+
// remove trailing "}"
|
|
975
|
+
return source.slice(1, -1);
|
|
976
|
+
};
|
|
977
|
+
const generateCodeToSuperviseScriptWithSrc = ({
|
|
978
|
+
type,
|
|
979
|
+
src
|
|
980
|
+
}) => {
|
|
981
|
+
const srcEncoded = JSON.stringify(src);
|
|
982
|
+
if (type === "js_module") {
|
|
983
|
+
return "window.__supervisor__.superviseScriptTypeModule(".concat(srcEncoded, ", (url) => import(url));");
|
|
984
|
+
}
|
|
985
|
+
return "window.__supervisor__.superviseScript(".concat(srcEncoded, ");");
|
|
986
|
+
};
|
|
987
|
+
|
|
988
|
+
/*
|
|
989
|
+
* This plugin provides a way for jsenv to supervisor js execution:
|
|
990
|
+
* - Know how many js are executed, when they are done, collect errors, etc...
|
|
991
|
+
*/
|
|
992
|
+
|
|
993
|
+
const jsenvPluginSupervisor = ({
|
|
994
|
+
logs = false,
|
|
995
|
+
measurePerf = false,
|
|
996
|
+
errorOverlay = true,
|
|
997
|
+
openInEditor = true,
|
|
998
|
+
errorBaseUrl
|
|
999
|
+
}) => {
|
|
1000
|
+
return {
|
|
1001
|
+
name: "jsenv:supervisor",
|
|
1002
|
+
appliesDuring: "dev",
|
|
1003
|
+
serve: async serveInfo => {
|
|
1004
|
+
if (serveInfo.request.pathname.startsWith("/__get_code_frame__/")) {
|
|
1005
|
+
const {
|
|
1006
|
+
pathname,
|
|
1007
|
+
searchParams
|
|
1008
|
+
} = new URL(serveInfo.request.url);
|
|
1009
|
+
let urlWithLineAndColumn = pathname.slice("/__get_code_frame__/".length);
|
|
1010
|
+
urlWithLineAndColumn = decodeURIComponent(urlWithLineAndColumn);
|
|
1011
|
+
const match = urlWithLineAndColumn.match(/:([0-9]+):([0-9]+)$/);
|
|
1012
|
+
if (!match) {
|
|
1013
|
+
return {
|
|
1014
|
+
status: 400,
|
|
1015
|
+
body: "Missing line and column in url"
|
|
1016
|
+
};
|
|
1017
|
+
}
|
|
1018
|
+
const file = urlWithLineAndColumn.slice(0, match.index);
|
|
1019
|
+
let line = parseInt(match[1]);
|
|
1020
|
+
let column = parseInt(match[2]);
|
|
1021
|
+
const urlInfo = serveInfo.kitchen.graph.getUrlInfo(file);
|
|
1022
|
+
if (!urlInfo) {
|
|
1023
|
+
return {
|
|
1024
|
+
status: 204,
|
|
1025
|
+
headers: {
|
|
1026
|
+
"cache-control": "no-store"
|
|
1027
|
+
}
|
|
1028
|
+
};
|
|
1029
|
+
}
|
|
1030
|
+
const remap = searchParams.has("remap");
|
|
1031
|
+
if (remap) {
|
|
1032
|
+
const sourcemap = urlInfo.sourcemap;
|
|
1033
|
+
if (sourcemap) {
|
|
1034
|
+
const original = getOriginalPosition({
|
|
1035
|
+
sourcemap,
|
|
1036
|
+
url: file,
|
|
1037
|
+
line,
|
|
1038
|
+
column
|
|
1039
|
+
});
|
|
1040
|
+
if (original.line !== null) {
|
|
1041
|
+
line = original.line;
|
|
1042
|
+
if (original.column !== null) {
|
|
1043
|
+
column = original.column;
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
const codeFrame = stringifyUrlSite({
|
|
1049
|
+
url: file,
|
|
1050
|
+
line,
|
|
1051
|
+
column,
|
|
1052
|
+
content: urlInfo.originalContent
|
|
1053
|
+
});
|
|
1054
|
+
return {
|
|
1055
|
+
status: 200,
|
|
1056
|
+
headers: {
|
|
1057
|
+
"cache-control": "no-store",
|
|
1058
|
+
"content-type": "text/plain",
|
|
1059
|
+
"content-length": Buffer.byteLength(codeFrame)
|
|
1060
|
+
},
|
|
1061
|
+
body: codeFrame
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
1064
|
+
if (serveInfo.request.pathname.startsWith("/__get_error_cause__/")) {
|
|
1065
|
+
let file = serveInfo.request.pathname.slice("/__get_error_cause__/".length);
|
|
1066
|
+
file = decodeURIComponent(file);
|
|
1067
|
+
if (!file) {
|
|
1068
|
+
return {
|
|
1069
|
+
status: 400,
|
|
1070
|
+
body: "Missing file in url"
|
|
1071
|
+
};
|
|
1072
|
+
}
|
|
1073
|
+
const getErrorCauseInfo = () => {
|
|
1074
|
+
const urlInfo = serveInfo.kitchen.graph.getUrlInfo(file);
|
|
1075
|
+
if (!urlInfo) {
|
|
1076
|
+
return null;
|
|
1077
|
+
}
|
|
1078
|
+
const {
|
|
1079
|
+
error
|
|
1080
|
+
} = urlInfo;
|
|
1081
|
+
if (error) {
|
|
1082
|
+
return error;
|
|
1083
|
+
}
|
|
1084
|
+
// search in direct dependencies (404 or 500)
|
|
1085
|
+
for (const referenceToOther of urlInfo.referenceToOthersSet) {
|
|
1086
|
+
const referencedUrlInfo = referenceToOther.urlInfo;
|
|
1087
|
+
if (referencedUrlInfo.error) {
|
|
1088
|
+
return referencedUrlInfo.error;
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
return null;
|
|
1092
|
+
};
|
|
1093
|
+
const causeInfo = getErrorCauseInfo();
|
|
1094
|
+
const body = JSON.stringify(causeInfo ? {
|
|
1095
|
+
code: causeInfo.code,
|
|
1096
|
+
message: causeInfo.message,
|
|
1097
|
+
reason: causeInfo.reason,
|
|
1098
|
+
stack: errorBaseUrl ? "stack mocked for snapshot" : causeInfo.stack,
|
|
1099
|
+
codeFrame: causeInfo.traceMessage
|
|
1100
|
+
} : null, null, " ");
|
|
1101
|
+
return {
|
|
1102
|
+
status: 200,
|
|
1103
|
+
headers: {
|
|
1104
|
+
"cache-control": "no-store",
|
|
1105
|
+
"content-type": "application/json",
|
|
1106
|
+
"content-length": Buffer.byteLength(body)
|
|
1107
|
+
},
|
|
1108
|
+
body
|
|
1109
|
+
};
|
|
1110
|
+
}
|
|
1111
|
+
if (serveInfo.request.pathname.startsWith("/__open_in_editor__/")) {
|
|
1112
|
+
let file = serveInfo.request.pathname.slice("/__open_in_editor__/".length);
|
|
1113
|
+
file = decodeURIComponent(file);
|
|
1114
|
+
if (!file) {
|
|
1115
|
+
return {
|
|
1116
|
+
status: 400,
|
|
1117
|
+
body: "Missing file in url"
|
|
1118
|
+
};
|
|
1119
|
+
}
|
|
1120
|
+
const require = createRequire(import.meta.url);
|
|
1121
|
+
const launch = require("launch-editor");
|
|
1122
|
+
launch(fileURLToPath(file), () => {
|
|
1123
|
+
// ignore error for now
|
|
1124
|
+
});
|
|
1125
|
+
return {
|
|
1126
|
+
status: 200,
|
|
1127
|
+
headers: {
|
|
1128
|
+
"cache-control": "no-store"
|
|
1129
|
+
}
|
|
1130
|
+
};
|
|
1131
|
+
}
|
|
1132
|
+
return null;
|
|
1133
|
+
},
|
|
1134
|
+
transformUrlContent: {
|
|
1135
|
+
html: htmlUrlInfo => {
|
|
1136
|
+
const supervisorFileReference = htmlUrlInfo.dependencies.inject({
|
|
1137
|
+
type: "script",
|
|
1138
|
+
expectedType: "js_classic",
|
|
1139
|
+
specifier: supervisorFileUrl
|
|
1140
|
+
});
|
|
1141
|
+
return injectSupervisorIntoHTML({
|
|
1142
|
+
content: htmlUrlInfo.content,
|
|
1143
|
+
url: htmlUrlInfo.url
|
|
1144
|
+
}, {
|
|
1145
|
+
supervisorScriptSrc: supervisorFileReference.generatedSpecifier,
|
|
1146
|
+
supervisorOptions: {
|
|
1147
|
+
errorBaseUrl,
|
|
1148
|
+
logs,
|
|
1149
|
+
measurePerf,
|
|
1150
|
+
errorOverlay,
|
|
1151
|
+
openInEditor
|
|
1152
|
+
},
|
|
1153
|
+
webServer: {
|
|
1154
|
+
rootDirectoryUrl: htmlUrlInfo.context.rootDirectoryUrl,
|
|
1155
|
+
isJsenvDevServer: true
|
|
1156
|
+
},
|
|
1157
|
+
inlineAsRemote: true,
|
|
1158
|
+
generateInlineScriptSrc: ({
|
|
1159
|
+
type,
|
|
1160
|
+
textContent,
|
|
1161
|
+
inlineScriptUrl,
|
|
1162
|
+
isOriginal,
|
|
1163
|
+
line,
|
|
1164
|
+
column
|
|
1165
|
+
}) => {
|
|
1166
|
+
const inlineScriptReference = htmlUrlInfo.dependencies.foundInline({
|
|
1167
|
+
type: "script",
|
|
1168
|
+
subtype: "inline",
|
|
1169
|
+
expectedType: type,
|
|
1170
|
+
isOriginalPosition: isOriginal,
|
|
1171
|
+
specifierLine: line - 1,
|
|
1172
|
+
specifierColumn: column,
|
|
1173
|
+
specifier: inlineScriptUrl,
|
|
1174
|
+
contentType: "text/javascript",
|
|
1175
|
+
content: textContent
|
|
1176
|
+
});
|
|
1177
|
+
return inlineScriptReference.generatedSpecifier;
|
|
1178
|
+
}
|
|
1179
|
+
});
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
};
|
|
1183
|
+
};
|
|
1184
|
+
|
|
1185
|
+
export { injectSupervisorIntoHTML, jsenvPluginSupervisor, supervisorFileUrl };
|