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