@jsenv/plugin-supervisor 1.0.1 → 1.1.1

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