@jsenv/test 2.1.3 → 3.0.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.
Files changed (46) hide show
  1. package/README.md +2 -1
  2. package/dist/js/exception.js +908 -0
  3. package/dist/js/execute_using_dynamic_import.js +76 -167
  4. package/dist/js/v8_coverage.js +4 -482
  5. package/dist/jsenv_test.js +4608 -2917
  6. package/dist/node_child_process_controlled.mjs +95 -76
  7. package/dist/node_worker_thread_controlled.mjs +72 -54
  8. package/package.json +23 -14
  9. package/src/coverage/generate_coverage.js +186 -0
  10. package/src/coverage/list_files_not_covered.js +2 -2
  11. package/src/coverage/missing_coverage.js +2 -2
  12. package/src/coverage/report_coverage_as_html.js +45 -0
  13. package/src/coverage/report_coverage_as_json.js +20 -0
  14. package/src/coverage/{coverage_reporter_text_log.js → report_coverage_in_console.js} +13 -7
  15. package/src/coverage/v8_and_istanbul.js +4 -4
  16. package/src/coverage/v8_coverage.js +2 -2
  17. package/src/execution/exception.js +151 -68
  18. package/src/execution/execute.js +24 -11
  19. package/src/execution/execute_test_plan.js +834 -750
  20. package/src/execution/format_error_for_terminal.js +187 -0
  21. package/src/execution/github_annotation_from_error.js +3 -3
  22. package/src/execution/junit_xml_file/report_as_junit_xml.js +149 -0
  23. package/src/execution/junit_xml_file/xml_generator.js +141 -0
  24. package/src/execution/reporters/reporter_list.js +687 -0
  25. package/src/execution/run.js +92 -30
  26. package/src/execution/web_server/start_using_command.js +2 -2
  27. package/src/execution/web_server_param.js +5 -5
  28. package/src/helpers/cpu_usage.js +142 -0
  29. package/src/helpers/cpu_usage.test_manual.js +7 -0
  30. package/src/main.js +8 -0
  31. package/src/runtime_browsers/chromium.js +1 -3
  32. package/src/runtime_browsers/firefox.js +0 -3
  33. package/src/runtime_browsers/middleware_istanbul.js +2 -2
  34. package/src/runtime_browsers/using_playwright.js +296 -220
  35. package/src/runtime_browsers/webkit.js +0 -3
  36. package/src/runtime_node/execute_using_dynamic_import.js +75 -28
  37. package/src/runtime_node/node_child_process.js +250 -139
  38. package/src/runtime_node/node_child_process_controlled.mjs +94 -75
  39. package/src/runtime_node/node_worker_thread.js +206 -120
  40. package/src/runtime_node/node_worker_thread_controlled.mjs +70 -53
  41. package/src/coverage/coverage_reporter_html_directory.js +0 -32
  42. package/src/coverage/coverage_reporter_json_file.js +0 -17
  43. package/src/coverage/report_to_coverage.js +0 -198
  44. package/src/execution/execution_colors.js +0 -10
  45. package/src/execution/execution_steps.js +0 -65
  46. package/src/execution/logs_file_execution.js +0 -495
@@ -0,0 +1,908 @@
1
+ /*
2
+ * Link to things doing pattern matching:
3
+ * https://git-scm.com/docs/gitignore
4
+ * https://github.com/kaelzhang/node-ignore
5
+ */
6
+
7
+ /** @module jsenv_url_meta **/
8
+ /**
9
+ * An object representing the result of applying a pattern to an url
10
+ * @typedef {Object} MatchResult
11
+ * @property {boolean} matched Indicates if url matched pattern
12
+ * @property {number} patternIndex Index where pattern stopped matching url, otherwise pattern.length
13
+ * @property {number} urlIndex Index where url stopped matching pattern, otherwise url.length
14
+ * @property {Array} matchGroups Array of strings captured during pattern matching
15
+ */
16
+
17
+ /**
18
+ * Apply a pattern to an url
19
+ * @param {Object} applyPatternMatchingParams
20
+ * @param {string} applyPatternMatchingParams.pattern "*", "**" and trailing slash have special meaning
21
+ * @param {string} applyPatternMatchingParams.url a string representing an url
22
+ * @return {MatchResult}
23
+ */
24
+ const applyPattern = ({ url, pattern }) => {
25
+ const { matched, patternIndex, index, groups } = applyMatching(pattern, url);
26
+ const matchGroups = [];
27
+ let groupIndex = 0;
28
+ for (const group of groups) {
29
+ if (group.name) {
30
+ matchGroups[group.name] = group.string;
31
+ } else {
32
+ matchGroups[groupIndex] = group.string;
33
+ groupIndex++;
34
+ }
35
+ }
36
+ return {
37
+ matched,
38
+ patternIndex,
39
+ urlIndex: index,
40
+ matchGroups,
41
+ };
42
+ };
43
+
44
+ const applyMatching = (pattern, string) => {
45
+ const groups = [];
46
+ let patternIndex = 0;
47
+ let index = 0;
48
+ let remainingPattern = pattern;
49
+ let remainingString = string;
50
+ let restoreIndexes = true;
51
+
52
+ const consumePattern = (count) => {
53
+ const subpattern = remainingPattern.slice(0, count);
54
+ remainingPattern = remainingPattern.slice(count);
55
+ patternIndex += count;
56
+ return subpattern;
57
+ };
58
+ const consumeString = (count) => {
59
+ const substring = remainingString.slice(0, count);
60
+ remainingString = remainingString.slice(count);
61
+ index += count;
62
+ return substring;
63
+ };
64
+ const consumeRemainingString = () => {
65
+ return consumeString(remainingString.length);
66
+ };
67
+
68
+ let matched;
69
+ const iterate = () => {
70
+ const patternIndexBefore = patternIndex;
71
+ const indexBefore = index;
72
+ matched = matchOne();
73
+ if (matched === undefined) {
74
+ consumePattern(1);
75
+ consumeString(1);
76
+ iterate();
77
+ return;
78
+ }
79
+ if (matched === false && restoreIndexes) {
80
+ patternIndex = patternIndexBefore;
81
+ index = indexBefore;
82
+ }
83
+ };
84
+ const matchOne = () => {
85
+ // pattern consumed and string consumed
86
+ if (remainingPattern === "" && remainingString === "") {
87
+ return true; // string fully matched pattern
88
+ }
89
+ // pattern consumed, string not consumed
90
+ if (remainingPattern === "" && remainingString !== "") {
91
+ return false; // fails because string longer than expected
92
+ }
93
+ // -- from this point pattern is not consumed --
94
+ // string consumed, pattern not consumed
95
+ if (remainingString === "") {
96
+ if (remainingPattern === "**") {
97
+ // trailing "**" is optional
98
+ consumePattern(2);
99
+ return true;
100
+ }
101
+ if (remainingPattern === "*") {
102
+ groups.push({ string: "" });
103
+ }
104
+ return false; // fail because string shorter than expected
105
+ }
106
+ // -- from this point pattern and string are not consumed --
107
+ // fast path trailing slash
108
+ if (remainingPattern === "/") {
109
+ if (remainingString[0] === "/") {
110
+ // trailing slash match remaining
111
+ consumePattern(1);
112
+ groups.push({ string: consumeRemainingString() });
113
+ return true;
114
+ }
115
+ return false;
116
+ }
117
+ // fast path trailing '**'
118
+ if (remainingPattern === "**") {
119
+ consumePattern(2);
120
+ consumeRemainingString();
121
+ return true;
122
+ }
123
+ // pattern leading **
124
+ if (remainingPattern.slice(0, 2) === "**") {
125
+ consumePattern(2); // consumes "**"
126
+ let skipAllowed = true;
127
+ if (remainingPattern[0] === "/") {
128
+ consumePattern(1); // consumes "/"
129
+ // when remainingPattern was preceeded by "**/"
130
+ // and remainingString have no "/"
131
+ // then skip is not allowed, a regular match will be performed
132
+ if (!remainingString.includes("/")) {
133
+ skipAllowed = false;
134
+ }
135
+ }
136
+ // pattern ending with "**" or "**/" match remaining string
137
+ if (remainingPattern === "") {
138
+ consumeRemainingString();
139
+ return true;
140
+ }
141
+ if (skipAllowed) {
142
+ const skipResult = skipUntilMatch({
143
+ pattern: remainingPattern,
144
+ string: remainingString,
145
+ canSkipSlash: true,
146
+ });
147
+ groups.push(...skipResult.groups);
148
+ consumePattern(skipResult.patternIndex);
149
+ consumeRemainingString();
150
+ restoreIndexes = false;
151
+ return skipResult.matched;
152
+ }
153
+ }
154
+ if (remainingPattern[0] === "*") {
155
+ consumePattern(1); // consumes "*"
156
+ if (remainingPattern === "") {
157
+ // matches everything except "/"
158
+ const slashIndex = remainingString.indexOf("/");
159
+ if (slashIndex === -1) {
160
+ groups.push({ string: consumeRemainingString() });
161
+ return true;
162
+ }
163
+ groups.push({ string: consumeString(slashIndex) });
164
+ return false;
165
+ }
166
+ // the next char must not the one expected by remainingPattern[0]
167
+ // because * is greedy and expect to skip at least one char
168
+ if (remainingPattern[0] === remainingString[0]) {
169
+ groups.push({ string: "" });
170
+ patternIndex = patternIndex - 1;
171
+ return false;
172
+ }
173
+ const skipResult = skipUntilMatch({
174
+ pattern: remainingPattern,
175
+ string: remainingString,
176
+ canSkipSlash: false,
177
+ });
178
+ groups.push(skipResult.group, ...skipResult.groups);
179
+ consumePattern(skipResult.patternIndex);
180
+ consumeString(skipResult.index);
181
+ restoreIndexes = false;
182
+ return skipResult.matched;
183
+ }
184
+ if (remainingPattern[0] !== remainingString[0]) {
185
+ return false;
186
+ }
187
+ return undefined;
188
+ };
189
+ iterate();
190
+
191
+ return {
192
+ matched,
193
+ patternIndex,
194
+ index,
195
+ groups,
196
+ };
197
+ };
198
+
199
+ const skipUntilMatch = ({ pattern, string, canSkipSlash }) => {
200
+ let index = 0;
201
+ let remainingString = string;
202
+ let longestAttemptRange = null;
203
+ let isLastAttempt = false;
204
+
205
+ const failure = () => {
206
+ return {
207
+ matched: false,
208
+ patternIndex: longestAttemptRange.patternIndex,
209
+ index: longestAttemptRange.index + longestAttemptRange.length,
210
+ groups: longestAttemptRange.groups,
211
+ group: {
212
+ string: string.slice(0, longestAttemptRange.index),
213
+ },
214
+ };
215
+ };
216
+
217
+ const tryToMatch = () => {
218
+ const matchAttempt = applyMatching(pattern, remainingString);
219
+ if (matchAttempt.matched) {
220
+ return {
221
+ matched: true,
222
+ patternIndex: matchAttempt.patternIndex,
223
+ index: index + matchAttempt.index,
224
+ groups: matchAttempt.groups,
225
+ group: {
226
+ string:
227
+ remainingString === ""
228
+ ? string
229
+ : string.slice(0, -remainingString.length),
230
+ },
231
+ };
232
+ }
233
+ const attemptIndex = matchAttempt.index;
234
+ const attemptRange = {
235
+ patternIndex: matchAttempt.patternIndex,
236
+ index,
237
+ length: attemptIndex,
238
+ groups: matchAttempt.groups,
239
+ };
240
+ if (
241
+ !longestAttemptRange ||
242
+ longestAttemptRange.length < attemptRange.length
243
+ ) {
244
+ longestAttemptRange = attemptRange;
245
+ }
246
+ if (isLastAttempt) {
247
+ return failure();
248
+ }
249
+ const nextIndex = attemptIndex + 1;
250
+ if (nextIndex >= remainingString.length) {
251
+ return failure();
252
+ }
253
+ if (remainingString[0] === "/") {
254
+ if (!canSkipSlash) {
255
+ return failure();
256
+ }
257
+ // when it's the last slash, the next attempt is the last
258
+ if (remainingString.indexOf("/", 1) === -1) {
259
+ isLastAttempt = true;
260
+ }
261
+ }
262
+ // search against the next unattempted string
263
+ index += nextIndex;
264
+ remainingString = remainingString.slice(nextIndex);
265
+ return tryToMatch();
266
+ };
267
+ return tryToMatch();
268
+ };
269
+
270
+ const applyPatternMatching = ({ url, pattern }) => {
271
+ assertUrlLike(pattern, "pattern");
272
+ if (url && typeof url.href === "string") url = url.href;
273
+ assertUrlLike(url, "url");
274
+ return applyPattern({ url, pattern });
275
+ };
276
+
277
+ const resolveAssociations = (associations, baseUrl) => {
278
+ if (baseUrl && typeof baseUrl.href === "string") baseUrl = baseUrl.href;
279
+ assertUrlLike(baseUrl, "baseUrl");
280
+
281
+ const associationsResolved = {};
282
+ for (const key of Object.keys(associations)) {
283
+ const value = associations[key];
284
+ if (typeof value === "object" && value !== null) {
285
+ const valueMapResolved = {};
286
+ for (const pattern of Object.keys(value)) {
287
+ const valueAssociated = value[pattern];
288
+ let patternResolved;
289
+ try {
290
+ patternResolved = String(new URL(pattern, baseUrl));
291
+ } catch (e) {
292
+ // it's not really an url, no need to perform url resolution nor encoding
293
+ patternResolved = pattern;
294
+ }
295
+
296
+ valueMapResolved[patternResolved] = valueAssociated;
297
+ }
298
+ associationsResolved[key] = valueMapResolved;
299
+ } else {
300
+ associationsResolved[key] = value;
301
+ }
302
+ }
303
+ return associationsResolved;
304
+ };
305
+
306
+ const asFlatAssociations = (associations) => {
307
+ if (!isPlainObject(associations)) {
308
+ throw new TypeError(
309
+ `associations must be a plain object, got ${associations}`,
310
+ );
311
+ }
312
+ const flatAssociations = {};
313
+ for (const associationName of Object.keys(associations)) {
314
+ const associationValue = associations[associationName];
315
+ if (!isPlainObject(associationValue)) {
316
+ continue;
317
+ }
318
+ for (const pattern of Object.keys(associationValue)) {
319
+ const patternValue = associationValue[pattern];
320
+ const previousValue = flatAssociations[pattern];
321
+ if (isPlainObject(previousValue)) {
322
+ flatAssociations[pattern] = {
323
+ ...previousValue,
324
+ [associationName]: patternValue,
325
+ };
326
+ } else {
327
+ flatAssociations[pattern] = {
328
+ [associationName]: patternValue,
329
+ };
330
+ }
331
+ }
332
+ }
333
+ return flatAssociations;
334
+ };
335
+
336
+ const applyAssociations = ({ url, associations }) => {
337
+ if (url && typeof url.href === "string") url = url.href;
338
+ assertUrlLike(url);
339
+ const flatAssociations = asFlatAssociations(associations);
340
+ let associatedValue = {};
341
+ for (const pattern of Object.keys(flatAssociations)) {
342
+ const { matched } = applyPatternMatching({
343
+ pattern,
344
+ url,
345
+ });
346
+ if (matched) {
347
+ const value = flatAssociations[pattern];
348
+ associatedValue = deepAssign(associatedValue, value);
349
+ }
350
+ }
351
+ return associatedValue;
352
+ };
353
+
354
+ const deepAssign = (firstValue, secondValue) => {
355
+ if (!isPlainObject(firstValue) || !isPlainObject(secondValue)) {
356
+ return secondValue;
357
+ }
358
+ for (const key of Object.keys(secondValue)) {
359
+ const leftValue = firstValue[key];
360
+ const rightValue = secondValue[key];
361
+ firstValue[key] = deepAssign(leftValue, rightValue);
362
+ }
363
+ return firstValue;
364
+ };
365
+
366
+ const urlChildMayMatch = ({ url, associations, predicate }) => {
367
+ if (url && typeof url.href === "string") url = url.href;
368
+ assertUrlLike(url, "url");
369
+ // the function was meants to be used on url ending with '/'
370
+ if (!url.endsWith("/")) {
371
+ throw new Error(`url should end with /, got ${url}`);
372
+ }
373
+ if (typeof predicate !== "function") {
374
+ throw new TypeError(`predicate must be a function, got ${predicate}`);
375
+ }
376
+ const flatAssociations = asFlatAssociations(associations);
377
+ // for full match we must create an object to allow pattern to override previous ones
378
+ let fullMatchMeta = {};
379
+ let someFullMatch = false;
380
+ // for partial match, any meta satisfying predicate will be valid because
381
+ // we don't know for sure if pattern will still match for a file inside pathname
382
+ const partialMatchMetaArray = [];
383
+ for (const pattern of Object.keys(flatAssociations)) {
384
+ const value = flatAssociations[pattern];
385
+ const matchResult = applyPatternMatching({
386
+ pattern,
387
+ url,
388
+ });
389
+ if (matchResult.matched) {
390
+ someFullMatch = true;
391
+ if (isPlainObject(fullMatchMeta) && isPlainObject(value)) {
392
+ fullMatchMeta = {
393
+ ...fullMatchMeta,
394
+ ...value,
395
+ };
396
+ } else {
397
+ fullMatchMeta = value;
398
+ }
399
+ } else if (someFullMatch === false && matchResult.urlIndex >= url.length) {
400
+ partialMatchMetaArray.push(value);
401
+ }
402
+ }
403
+ if (someFullMatch) {
404
+ return Boolean(predicate(fullMatchMeta));
405
+ }
406
+ return partialMatchMetaArray.some((partialMatchMeta) =>
407
+ predicate(partialMatchMeta),
408
+ );
409
+ };
410
+
411
+ const applyAliases = ({ url, aliases }) => {
412
+ let aliasFullMatchResult;
413
+ const aliasMatchingKey = Object.keys(aliases).find((key) => {
414
+ const aliasMatchResult = applyPatternMatching({
415
+ pattern: key,
416
+ url,
417
+ });
418
+ if (aliasMatchResult.matched) {
419
+ aliasFullMatchResult = aliasMatchResult;
420
+ return true;
421
+ }
422
+ return false;
423
+ });
424
+ if (!aliasMatchingKey) {
425
+ return url;
426
+ }
427
+ const { matchGroups } = aliasFullMatchResult;
428
+ const alias = aliases[aliasMatchingKey];
429
+ const parts = alias.split("*");
430
+ let newUrl = "";
431
+ let index = 0;
432
+ for (const part of parts) {
433
+ newUrl += `${part}`;
434
+ if (index < parts.length - 1) {
435
+ newUrl += matchGroups[index];
436
+ }
437
+ index++;
438
+ }
439
+ return newUrl;
440
+ };
441
+
442
+ const matches = (url, patterns) => {
443
+ return Boolean(
444
+ applyAssociations({
445
+ url,
446
+ associations: {
447
+ yes: patterns,
448
+ },
449
+ }).yes,
450
+ );
451
+ };
452
+
453
+ // const assertSpecifierMetaMap = (value, checkComposition = true) => {
454
+ // if (!isPlainObject(value)) {
455
+ // throw new TypeError(
456
+ // `specifierMetaMap must be a plain object, got ${value}`,
457
+ // );
458
+ // }
459
+ // if (checkComposition) {
460
+ // const plainObject = value;
461
+ // Object.keys(plainObject).forEach((key) => {
462
+ // assertUrlLike(key, "specifierMetaMap key");
463
+ // const value = plainObject[key];
464
+ // if (value !== null && !isPlainObject(value)) {
465
+ // throw new TypeError(
466
+ // `specifierMetaMap value must be a plain object or null, got ${value} under key ${key}`,
467
+ // );
468
+ // }
469
+ // });
470
+ // }
471
+ // };
472
+ const assertUrlLike = (value, name = "url") => {
473
+ if (typeof value !== "string") {
474
+ throw new TypeError(`${name} must be a url string, got ${value}`);
475
+ }
476
+ if (isWindowsPathnameSpecifier(value)) {
477
+ throw new TypeError(
478
+ `${name} must be a url but looks like a windows pathname, got ${value}`,
479
+ );
480
+ }
481
+ if (!hasScheme(value)) {
482
+ throw new TypeError(
483
+ `${name} must be a url and no scheme found, got ${value}`,
484
+ );
485
+ }
486
+ };
487
+ const isPlainObject = (value) => {
488
+ if (value === null) {
489
+ return false;
490
+ }
491
+ if (typeof value === "object") {
492
+ if (Array.isArray(value)) {
493
+ return false;
494
+ }
495
+ return true;
496
+ }
497
+ return false;
498
+ };
499
+ const isWindowsPathnameSpecifier = (specifier) => {
500
+ const firstChar = specifier[0];
501
+ if (!/[a-zA-Z]/.test(firstChar)) return false;
502
+ const secondChar = specifier[1];
503
+ if (secondChar !== ":") return false;
504
+ const thirdChar = specifier[2];
505
+ return thirdChar === "/" || thirdChar === "\\";
506
+ };
507
+ const hasScheme = (specifier) => /^[a-zA-Z]+:/.test(specifier);
508
+
509
+ const URL_META = {
510
+ resolveAssociations,
511
+ applyAssociations,
512
+ applyAliases,
513
+ applyPatternMatching,
514
+ urlChildMayMatch,
515
+ matches,
516
+ };
517
+
518
+ function createRawFrame(raw) {
519
+ return {
520
+ column: -1,
521
+ fileName: "",
522
+ line: -1,
523
+ name: "",
524
+ raw: raw,
525
+ sourceColumn: -1,
526
+ sourceFileName: "",
527
+ sourceLine: -1,
528
+ type: "",
529
+ };
530
+ }
531
+ var FIREFOX_WEBKIT = /([^@]+|^)@(.*):(\d+):(\d+)/;
532
+ var WEBKIT_ADDRESS_UNNAMED = /^(http(s)?:\/\/.*):(\d+):(\d+)$/;
533
+ var WEBKIT_NATIVE_UNNAMED = "[native code]";
534
+ function parsWebkit(str) {
535
+ if (str.includes(WEBKIT_NATIVE_UNNAMED)) {
536
+ return {
537
+ line: -1,
538
+ column: -1,
539
+ type: "native",
540
+ fileName: "",
541
+ name: "",
542
+ raw: str,
543
+ sourceColumn: -1,
544
+ sourceFileName: "",
545
+ sourceLine: -1,
546
+ };
547
+ }
548
+ var match = str.match(WEBKIT_ADDRESS_UNNAMED);
549
+ if (match) {
550
+ var line = match[3] ? +match[3] : -1;
551
+ var column = match[4] ? +match[4] : -1;
552
+ var fileName = match[1] ? match[1] : "";
553
+ return {
554
+ line: line,
555
+ column: column,
556
+ type: "",
557
+ fileName: fileName,
558
+ name: "",
559
+ raw: str,
560
+ sourceColumn: -1,
561
+ sourceFileName: "",
562
+ sourceLine: -1,
563
+ };
564
+ }
565
+ return createRawFrame(str);
566
+ }
567
+ function parseFirefoxWebkit(lines) {
568
+ return lines.map(function (str) {
569
+ var match = str.match(FIREFOX_WEBKIT);
570
+ if (!match) {
571
+ return parsWebkit(str);
572
+ }
573
+ var line = match[3] ? +match[3] : -1;
574
+ var column = match[4] ? +match[4] : -1;
575
+ var fileName = match[2] ? match[2] : "";
576
+ return {
577
+ line: line,
578
+ column: column,
579
+ type: match[0] ? "" : "native",
580
+ fileName: fileName,
581
+ name: (match[1] || "").trim(),
582
+ raw: str,
583
+ sourceColumn: -1,
584
+ sourceFileName: "",
585
+ sourceLine: -1,
586
+ };
587
+ });
588
+ }
589
+ // foo.bar.js:123:39
590
+ // foo.bar.js:123:39 <- original.js:123:34
591
+ var CHROME_MAPPED = /(.*?):(\d+):(\d+)(\s<-\s(.+):(\d+):(\d+))?/;
592
+ function parseMapped(frame, maybeMapped) {
593
+ var match = maybeMapped.match(CHROME_MAPPED);
594
+ if (match) {
595
+ frame.fileName = match[1];
596
+ frame.line = +match[2];
597
+ frame.column = +match[3];
598
+ if (match[5])
599
+ frame.sourceFileName = match[5];
600
+ if (match[6])
601
+ frame.sourceLine = +match[6];
602
+ if (match[7])
603
+ frame.sourceColumn = +match[7];
604
+ }
605
+ }
606
+ // at <SomeFramework>
607
+ var CHROME_IE_NATIVE_NO_LINE = /^at\s(<.*>)$/;
608
+ // at <SomeFramework>:123:39
609
+ var CHROME_IE_NATIVE = /^\s*at\s(<.*>):(\d+):(\d+)$/;
610
+ // at foo.bar(bob) (foo.bar.js:123:39)
611
+ // at foo.bar(bob) (foo.bar.js:123:39 <- original.js:123:34)
612
+ var CHROME_IE_FUNCTION = /^at\s(.*)\s\((.*)\)$/;
613
+ // >= Chrome 88
614
+ // spy() at Component.Foo [as constructor] (original.js:123:34)
615
+ // spy() at Component.Foo [as constructor] (foo.bar.js:123:39 <- original.js:123:34)
616
+ var CHROME_IE_FUNCTION_WITH_CALL = /^([\w\W]*)\s\((.*)\)/;
617
+ var CHROME_IE_DETECTOR = /\s*at\s.+/;
618
+ // at about:blank:1:7
619
+ // >= Chrome 99
620
+ // at /projects/foo.test.js:689:1 <- /projects/foo.test.js:10:1
621
+ var CHROME_BLANK = /\s*at\s(.*):(\d+):(\d+)$/;
622
+ function parseChromeIe(lines) {
623
+ // Many frameworks mess with error.stack. So we use this check
624
+ // to find the first line of the actual stack
625
+ var start = lines.findIndex(function (line) { return CHROME_IE_DETECTOR.test(line); });
626
+ if (start === -1) {
627
+ return [];
628
+ }
629
+ var frames = [];
630
+ for (var i = start; i < lines.length; i++) {
631
+ var str = lines[i].replace(/^\s+|\s+$/g, "");
632
+ var frame = createRawFrame(lines[i]);
633
+ var nativeNoLine = str.match(CHROME_IE_NATIVE_NO_LINE);
634
+ if (nativeNoLine) {
635
+ frame.fileName = nativeNoLine[1];
636
+ frame.type = "native";
637
+ frames.push(frame);
638
+ continue;
639
+ }
640
+ var native = str.match(CHROME_IE_NATIVE);
641
+ if (native) {
642
+ frame.fileName = native[1];
643
+ frame.type = "native";
644
+ if (native[2])
645
+ frame.line = +native[2];
646
+ if (native[3])
647
+ frame.column = +native[3];
648
+ frames.push(frame);
649
+ continue;
650
+ }
651
+ var withFn = str.match(CHROME_IE_FUNCTION);
652
+ if (withFn) {
653
+ frame.name = withFn[1];
654
+ parseMapped(frame, withFn[2]);
655
+ frames.push(frame);
656
+ continue;
657
+ }
658
+ var blank = str.match(CHROME_BLANK);
659
+ if (blank) {
660
+ frame.fileName = blank[1];
661
+ frame.line = +blank[2];
662
+ frame.column = +blank[3];
663
+ parseMapped(frame, blank[1] + ":" + blank[2] + ":" + blank[3]);
664
+ frames.push(frame);
665
+ continue;
666
+ }
667
+ var withFnCall = str.match(CHROME_IE_FUNCTION_WITH_CALL);
668
+ if (withFnCall) {
669
+ frame.name = withFnCall[1];
670
+ parseMapped(frame, withFnCall[2]);
671
+ frames.push(frame);
672
+ continue;
673
+ }
674
+ frames.push(frame);
675
+ }
676
+ return frames;
677
+ }
678
+ function parseStackTrace(stack) {
679
+ if (!stack)
680
+ return [];
681
+ var lines = stack.split("\n").filter(Boolean);
682
+ // Libraries like node's "assert" module mess with the stack trace by
683
+ // prepending custom data. So we need to do a precheck, to determine
684
+ // which browser the trace is coming from.
685
+ if (lines.some(function (line) { return CHROME_IE_DETECTOR.test(line); })) {
686
+ return parseChromeIe(lines);
687
+ }
688
+ return parseFirefoxWebkit(lines);
689
+ }
690
+
691
+ /*
692
+ * Exception are objects used to wrap a value that is thrown
693
+ * Usually they wrap error but the value can be anything as
694
+ * throw "toto" can be used in JS
695
+ *
696
+ * - provide a common API to interact with value that can be thrown
697
+ * - enrich usual errors with a bit more information (line, column)
698
+ * - normalize error properties
699
+ * - exception.stackTrace: only the stack trace as string (no error.name or error.message)
700
+ * - error.stack: error.name + error.message + error.stackTrace
701
+ *
702
+ * It is used mostly internally by jsenv but can also be found
703
+ * value returned by "executeTestPlan" public export (for failed executions)
704
+ * and value returned by "execute" public export (when execution fails)
705
+ *
706
+ * This file is responsible to wrap error hapenning in Node.js runtime
707
+ * The browser part can be found in "supervisor.js"
708
+ */
709
+
710
+
711
+ const isDev = process.execArgv.includes("--conditions=development");
712
+ const jsenvCoreDirectoryUrl = new URL("file:///Users/d.maillard/dev/jsenv/jsenv-core/", import.meta.url);
713
+
714
+ const createException = (reason, { rootDirectoryUrl } = {}) => {
715
+ const exception = {
716
+ isException: true,
717
+ isError: false,
718
+ name: "",
719
+ message: "",
720
+ stack: "",
721
+ stackTrace: "",
722
+ stackFrames: undefined,
723
+ site: null,
724
+ };
725
+
726
+ if (reason === undefined) {
727
+ exception.message = "undefined";
728
+ return exception;
729
+ }
730
+ if (reason === null) {
731
+ exception.message = "null";
732
+ return exception;
733
+ }
734
+ if (typeof reason === "string") {
735
+ exception.message = reason;
736
+ return exception;
737
+ }
738
+ if (typeof reason !== "object") {
739
+ exception.message = JSON.stringify(reason);
740
+ return exception;
741
+ }
742
+ if (reason.stackFrames === undefined && "stack" in reason) {
743
+ let stackFrames;
744
+
745
+ const { prepareStackTrace } = Error;
746
+ Error.prepareStackTrace = (e, callSites) => {
747
+ Error.prepareStackTrace = prepareStackTrace;
748
+
749
+ stackFrames = [];
750
+ for (const callSite of callSites) {
751
+ const stackFrame = {
752
+ raw: ` at ${String(callSite)}`,
753
+ url: callSite.getFileName() || callSite.getScriptNameOrSourceURL(),
754
+ line: callSite.getLineNumber(),
755
+ column: callSite.getColumnNumber(),
756
+ functionName: callSite.getFunctionName(),
757
+ isNative: callSite.isNative(),
758
+ isEval: callSite.isEval(),
759
+ isConstructor: callSite.isConstructor(),
760
+ isAsync: callSite.isAsync(),
761
+ evalSite: null,
762
+ };
763
+ if (stackFrame.isEval) {
764
+ const evalOrigin = stackFrame.getEvalOrigin();
765
+ if (evalOrigin) {
766
+ stackFrame.evalSite = getPropertiesFromEvalOrigin(evalOrigin);
767
+ }
768
+ }
769
+ stackFrames.push(stackFrame);
770
+ }
771
+ return "";
772
+ };
773
+ exception.stack = reason.stack;
774
+ if (stackFrames === undefined) {
775
+ // Error.prepareStackTrace not trigerred
776
+ // - reason is not an error
777
+ // - reason.stack already get
778
+ Error.prepareStackTrace = prepareStackTrace;
779
+
780
+ const calls = parseStackTrace(reason.stack);
781
+ stackFrames = [];
782
+ for (const call of calls) {
783
+ if (call.fileName === "") {
784
+ continue;
785
+ }
786
+ stackFrames.push({
787
+ raw: call.raw,
788
+ functionName: call.name,
789
+ url: call.fileName,
790
+ line: call.line,
791
+ column: call.column,
792
+ native: call.type === "native",
793
+ });
794
+ }
795
+ }
796
+
797
+ const stackFramesNonNative = [];
798
+ for (const stackFrame of stackFrames) {
799
+ if (stackFrame.url.startsWith("node:")) {
800
+ stackFrame.native = "node";
801
+ continue;
802
+ }
803
+ if (stackFrame.url.startsWith("file:")) {
804
+ if (rootDirectoryUrl && stackFrame.url.startsWith(rootDirectoryUrl)) {
805
+ stackFramesNonNative.push(stackFrame);
806
+ continue;
807
+ }
808
+
809
+ if (isDev) {
810
+ // while developing jsenv itself we want to exclude any
811
+ // - src/*
812
+ // - packages/**/src/
813
+ // for the users of jsenv it's easier, we want to exclude
814
+ // - **/node_modules/@jsenv/**
815
+ if (
816
+ URL_META.matches(stackFrame.url, {
817
+ [`${jsenvCoreDirectoryUrl}src/`]: true,
818
+ [`${jsenvCoreDirectoryUrl}packages/**/src/`]: true,
819
+ })
820
+ ) {
821
+ stackFrame.native = "jsenv";
822
+ continue;
823
+ }
824
+ } else if (
825
+ URL_META.matches(stackFrame.url, {
826
+ "file:///**/node_modules/@jsenv/core/": true,
827
+ })
828
+ ) {
829
+ stackFrame.native = "jsenv";
830
+ continue;
831
+ }
832
+ }
833
+ stackFramesNonNative.push(stackFrame);
834
+ }
835
+ if (stackFramesNonNative.length) {
836
+ stackFrames = stackFramesNonNative;
837
+ }
838
+
839
+ reason.stackFrames = stackFrames;
840
+
841
+ let stackTrace = "";
842
+ for (const stackFrame of stackFrames) {
843
+ if (stackTrace) stackTrace += "\n";
844
+ stackTrace += stackFrame.raw;
845
+ }
846
+ reason.stackTrace = stackTrace;
847
+ let stack = "";
848
+ const name = reason.name || "Error";
849
+ const message = reason.message || "";
850
+ stack += `${name}: ${message}`;
851
+ if (stackTrace) {
852
+ stack += `\n${stackTrace}`;
853
+ }
854
+ reason.stack = stack;
855
+
856
+ const [firstCallFrame] = stackFrames;
857
+ if (firstCallFrame && (!reason.site || !reason.site.isInline)) {
858
+ reason.site = firstCallFrame.url
859
+ ? {
860
+ url: firstCallFrame.url,
861
+ line: firstCallFrame.line,
862
+ column: firstCallFrame.column,
863
+ }
864
+ : firstCallFrame.evalSite;
865
+ }
866
+ }
867
+ // getOwnPropertyNames to catch non enumerable properties on reason
868
+ // (happens mostly when reason is instanceof Error)
869
+ // like .stack, .message
870
+ // some properties are even on the prototype like .name
871
+ for (const ownPropertyName of Object.getOwnPropertyNames(reason)) {
872
+ exception[ownPropertyName] = reason[ownPropertyName];
873
+ }
874
+ const isError = reason instanceof Error;
875
+ exception.isError = isError;
876
+ if (isError) {
877
+ // getOwnPropertyNames is not enough to copy .name and .message
878
+ // on error instances
879
+ exception.name = reason.name;
880
+ exception.message = reason.message;
881
+ }
882
+ return exception;
883
+ };
884
+
885
+ const getPropertiesFromEvalOrigin = (origin) => {
886
+ // Most eval() calls are in this format
887
+ const topLevelEvalMatch = /^eval at ([^(]+) \((.+):(\d+):(\d+)\)$/.exec(
888
+ origin,
889
+ );
890
+ if (topLevelEvalMatch) {
891
+ const source = topLevelEvalMatch[2];
892
+ const line = Number(topLevelEvalMatch[3]);
893
+ const column = topLevelEvalMatch[4] - 1;
894
+ return {
895
+ url: source,
896
+ line,
897
+ column,
898
+ };
899
+ }
900
+ // Parse nested eval() calls using recursion
901
+ const nestedEvalMatch = /^eval at ([^(]+) \((.+)\)$/.exec(origin);
902
+ if (nestedEvalMatch) {
903
+ return getPropertiesFromEvalOrigin(nestedEvalMatch[2]);
904
+ }
905
+ return null;
906
+ };
907
+
908
+ export { URL_META, createException };