@jsenv/core 38.3.4 → 38.3.6

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.
@@ -1,5 +1,5 @@
1
- import { pathToFileURL, fileURLToPath } from "node:url";
2
1
  import { chmod, stat, lstat, readdir, promises, unlink, openSync, closeSync, rmdir, watch, readdirSync, statSync, writeFileSync as writeFileSync$1, mkdirSync, createReadStream, readFile, existsSync, readFileSync, realpathSync } from "node:fs";
2
+ import { pathToFileURL, fileURLToPath } from "node:url";
3
3
  import crypto, { createHash } from "node:crypto";
4
4
  import { extname } from "node:path";
5
5
  import process$1 from "node:process";
@@ -20,132 +20,603 @@ import { systemJsClientFileUrlDefault, convertJsModuleToJsClassic } from "@jsenv
20
20
  import { RUNTIME_COMPAT } from "@jsenv/runtime-compat";
21
21
  import { jsenvPluginSupervisor } from "@jsenv/plugin-supervisor";
22
22
 
23
- /*
24
- * data:[<mediatype>][;base64],<data>
25
- * https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs#syntax
26
- */
23
+ const assertUrlLike = (value, name = "url") => {
24
+ if (typeof value !== "string") {
25
+ throw new TypeError(`${name} must be a url string, got ${value}`);
26
+ }
27
+ if (isWindowsPathnameSpecifier(value)) {
28
+ throw new TypeError(
29
+ `${name} must be a url but looks like a windows pathname, got ${value}`,
30
+ );
31
+ }
32
+ if (!hasScheme$1(value)) {
33
+ throw new TypeError(
34
+ `${name} must be a url and no scheme found, got ${value}`,
35
+ );
36
+ }
37
+ };
27
38
 
28
- /* eslint-env browser, node */
39
+ const isPlainObject = (value) => {
40
+ if (value === null) {
41
+ return false;
42
+ }
43
+ if (typeof value === "object") {
44
+ if (Array.isArray(value)) {
45
+ return false;
46
+ }
47
+ return true;
48
+ }
49
+ return false;
50
+ };
29
51
 
30
- const DATA_URL = {
31
- parse: (string) => {
32
- const afterDataProtocol = string.slice("data:".length);
33
- const commaIndex = afterDataProtocol.indexOf(",");
34
- const beforeComma = afterDataProtocol.slice(0, commaIndex);
52
+ const isWindowsPathnameSpecifier = (specifier) => {
53
+ const firstChar = specifier[0];
54
+ if (!/[a-zA-Z]/.test(firstChar)) return false;
55
+ const secondChar = specifier[1];
56
+ if (secondChar !== ":") return false;
57
+ const thirdChar = specifier[2];
58
+ return thirdChar === "/" || thirdChar === "\\";
59
+ };
35
60
 
36
- let contentType;
37
- let base64Flag;
38
- if (beforeComma.endsWith(`;base64`)) {
39
- contentType = beforeComma.slice(0, -`;base64`.length);
40
- base64Flag = true;
61
+ const hasScheme$1 = (specifier) => /^[a-zA-Z]+:/.test(specifier);
62
+
63
+ const resolveAssociations = (associations, baseUrl) => {
64
+ assertUrlLike(baseUrl, "baseUrl");
65
+ const associationsResolved = {};
66
+ Object.keys(associations).forEach((key) => {
67
+ const value = associations[key];
68
+ if (typeof value === "object" && value !== null) {
69
+ const valueMapResolved = {};
70
+ Object.keys(value).forEach((pattern) => {
71
+ const valueAssociated = value[pattern];
72
+ const patternResolved = normalizeUrlPattern(pattern, baseUrl);
73
+ valueMapResolved[patternResolved] = valueAssociated;
74
+ });
75
+ associationsResolved[key] = valueMapResolved;
41
76
  } else {
42
- contentType = beforeComma;
43
- base64Flag = false;
77
+ associationsResolved[key] = value;
44
78
  }
79
+ });
80
+ return associationsResolved;
81
+ };
45
82
 
46
- contentType =
47
- contentType === "" ? "text/plain;charset=US-ASCII" : contentType;
48
- const afterComma = afterDataProtocol.slice(commaIndex + 1);
49
- return {
50
- contentType,
51
- base64Flag,
52
- data: afterComma,
53
- };
54
- },
83
+ const normalizeUrlPattern = (urlPattern, baseUrl) => {
84
+ try {
85
+ return String(new URL(urlPattern, baseUrl));
86
+ } catch (e) {
87
+ // it's not really an url, no need to perform url resolution nor encoding
88
+ return urlPattern;
89
+ }
90
+ };
55
91
 
56
- stringify: ({ contentType, base64Flag = true, data }) => {
57
- if (!contentType || contentType === "text/plain;charset=US-ASCII") {
58
- // can be a buffer or a string, hence check on data.length instead of !data or data === ''
59
- if (data.length === 0) {
60
- return `data:,`;
61
- }
62
- if (base64Flag) {
63
- return `data:;base64,${data}`;
64
- }
65
- return `data:,${data}`;
66
- }
67
- if (base64Flag) {
68
- return `data:${contentType};base64,${data}`;
92
+ const asFlatAssociations = (associations) => {
93
+ if (!isPlainObject(associations)) {
94
+ throw new TypeError(
95
+ `associations must be a plain object, got ${associations}`,
96
+ );
97
+ }
98
+ const flatAssociations = {};
99
+ Object.keys(associations).forEach((associationName) => {
100
+ const associationValue = associations[associationName];
101
+ if (isPlainObject(associationValue)) {
102
+ Object.keys(associationValue).forEach((pattern) => {
103
+ const patternValue = associationValue[pattern];
104
+ const previousValue = flatAssociations[pattern];
105
+ if (isPlainObject(previousValue)) {
106
+ flatAssociations[pattern] = {
107
+ ...previousValue,
108
+ [associationName]: patternValue,
109
+ };
110
+ } else {
111
+ flatAssociations[pattern] = {
112
+ [associationName]: patternValue,
113
+ };
114
+ }
115
+ });
69
116
  }
70
- return `data:${contentType},${data}`;
71
- },
117
+ });
118
+ return flatAssociations;
72
119
  };
73
120
 
74
- // consider switching to https://babeljs.io/docs/en/babel-code-frame
75
- // https://github.com/postcss/postcss/blob/fd30d3df5abc0954a0ec642a3cdc644ab2aacf9c/lib/css-syntax-error.js#L43
76
- // https://github.com/postcss/postcss/blob/fd30d3df5abc0954a0ec642a3cdc644ab2aacf9c/lib/terminal-highlight.js#L50
77
- // https://github.com/babel/babel/blob/eea156b2cb8deecfcf82d52aa1b71ba4995c7d68/packages/babel-code-frame/src/index.js#L1
121
+ /*
122
+ * Link to things doing pattern matching:
123
+ * https://git-scm.com/docs/gitignore
124
+ * https://github.com/kaelzhang/node-ignore
125
+ */
78
126
 
79
127
 
80
- const stringifyUrlSite = (
81
- { url, line, column, content },
82
- {
83
- showCodeFrame = true,
84
- numberOfSurroundingLinesToShow,
85
- lineMaxLength,
86
- color,
87
- } = {},
88
- ) => {
89
- let string = url;
128
+ /** @module jsenv_url_meta **/
129
+ /**
130
+ * An object representing the result of applying a pattern to an url
131
+ * @typedef {Object} MatchResult
132
+ * @property {boolean} matched Indicates if url matched pattern
133
+ * @property {number} patternIndex Index where pattern stopped matching url, otherwise pattern.length
134
+ * @property {number} urlIndex Index where url stopped matching pattern, otherwise url.length
135
+ * @property {Array} matchGroups Array of strings captured during pattern matching
136
+ */
90
137
 
91
- if (typeof line === "number") {
92
- string += `:${line}`;
93
- if (typeof column === "number") {
94
- string += `:${column}`;
138
+ /**
139
+ * Apply a pattern to an url
140
+ * @param {Object} applyPatternMatchingParams
141
+ * @param {string} applyPatternMatchingParams.pattern "*", "**" and trailing slash have special meaning
142
+ * @param {string} applyPatternMatchingParams.url a string representing an url
143
+ * @return {MatchResult}
144
+ */
145
+ const applyPatternMatching = ({ url, pattern }) => {
146
+ assertUrlLike(pattern, "pattern");
147
+ assertUrlLike(url, "url");
148
+ const { matched, patternIndex, index, groups } = applyMatching(pattern, url);
149
+ const matchGroups = [];
150
+ let groupIndex = 0;
151
+ groups.forEach((group) => {
152
+ if (group.name) {
153
+ matchGroups[group.name] = group.string;
154
+ } else {
155
+ matchGroups[groupIndex] = group.string;
156
+ groupIndex++;
95
157
  }
96
- }
97
-
98
- if (!showCodeFrame || typeof line !== "number" || !content) {
99
- return string;
100
- }
101
-
102
- const sourceLoc = showSourceLocation({
103
- content,
104
- line,
105
- column,
106
- numberOfSurroundingLinesToShow,
107
- lineMaxLength,
108
- color,
109
158
  });
110
-
111
- return `${string}
112
- ${sourceLoc}`;
159
+ return {
160
+ matched,
161
+ patternIndex,
162
+ urlIndex: index,
163
+ matchGroups,
164
+ };
113
165
  };
114
166
 
115
- const showSourceLocation = ({
116
- content,
117
- line,
118
- column,
119
- numberOfSurroundingLinesToShow = 1,
120
- lineMaxLength = 120,
121
- } = {}) => {
122
- let mark = (string) => string;
123
- let aside = (string) => string;
124
- // if (color) {
125
- // mark = (string) => ANSI.color(string, ANSI.RED)
126
- // aside = (string) => ANSI.color(string, ANSI.GREY)
127
- // }
167
+ const applyMatching = (pattern, string) => {
168
+ const groups = [];
169
+ let patternIndex = 0;
170
+ let index = 0;
171
+ let remainingPattern = pattern;
172
+ let remainingString = string;
173
+ let restoreIndexes = true;
128
174
 
129
- const lines = content.split(/\r?\n/);
130
- if (line === 0) line = 1;
131
- let lineRange = {
132
- start: line - 1,
133
- end: line,
175
+ const consumePattern = (count) => {
176
+ const subpattern = remainingPattern.slice(0, count);
177
+ remainingPattern = remainingPattern.slice(count);
178
+ patternIndex += count;
179
+ return subpattern;
180
+ };
181
+ const consumeString = (count) => {
182
+ const substring = remainingString.slice(0, count);
183
+ remainingString = remainingString.slice(count);
184
+ index += count;
185
+ return substring;
186
+ };
187
+ const consumeRemainingString = () => {
188
+ return consumeString(remainingString.length);
134
189
  };
135
- lineRange = moveLineRangeUp(lineRange, numberOfSurroundingLinesToShow);
136
- lineRange = moveLineRangeDown(lineRange, numberOfSurroundingLinesToShow);
137
- lineRange = lineRangeWithinLines(lineRange, lines);
138
- const linesToShow = lines.slice(lineRange.start, lineRange.end);
139
- const endLineNumber = lineRange.end;
140
- const lineNumberMaxWidth = String(endLineNumber).length;
141
-
142
- if (column === 0) column = 1;
143
190
 
144
- const columnRange = {};
145
- if (column === undefined) {
146
- columnRange.start = 0;
147
- columnRange.end = lineMaxLength;
148
- } else if (column > lineMaxLength) {
191
+ let matched;
192
+ const iterate = () => {
193
+ const patternIndexBefore = patternIndex;
194
+ const indexBefore = index;
195
+ matched = matchOne();
196
+ if (matched === undefined) {
197
+ consumePattern(1);
198
+ consumeString(1);
199
+ iterate();
200
+ return;
201
+ }
202
+ if (matched === false && restoreIndexes) {
203
+ patternIndex = patternIndexBefore;
204
+ index = indexBefore;
205
+ }
206
+ };
207
+ const matchOne = () => {
208
+ // pattern consumed and string consumed
209
+ if (remainingPattern === "" && remainingString === "") {
210
+ return true; // string fully matched pattern
211
+ }
212
+ // pattern consumed, string not consumed
213
+ if (remainingPattern === "" && remainingString !== "") {
214
+ return false; // fails because string longer than expected
215
+ }
216
+ // -- from this point pattern is not consumed --
217
+ // string consumed, pattern not consumed
218
+ if (remainingString === "") {
219
+ if (remainingPattern === "**") {
220
+ // trailing "**" is optional
221
+ consumePattern(2);
222
+ return true;
223
+ }
224
+ if (remainingPattern === "*") {
225
+ groups.push({ string: "" });
226
+ }
227
+ return false; // fail because string shorter than expected
228
+ }
229
+ // -- from this point pattern and string are not consumed --
230
+ // fast path trailing slash
231
+ if (remainingPattern === "/") {
232
+ if (remainingString[0] === "/") {
233
+ // trailing slash match remaining
234
+ consumePattern(1);
235
+ groups.push({ string: consumeRemainingString() });
236
+ return true;
237
+ }
238
+ return false;
239
+ }
240
+ // fast path trailing '**'
241
+ if (remainingPattern === "**") {
242
+ consumePattern(2);
243
+ consumeRemainingString();
244
+ return true;
245
+ }
246
+ // pattern leading **
247
+ if (remainingPattern.slice(0, 2) === "**") {
248
+ consumePattern(2); // consumes "**"
249
+ let skipAllowed = true;
250
+ if (remainingPattern[0] === "/") {
251
+ consumePattern(1); // consumes "/"
252
+ // when remainingPattern was preceeded by "**/"
253
+ // and remainingString have no "/"
254
+ // then skip is not allowed, a regular match will be performed
255
+ if (!remainingString.includes("/")) {
256
+ skipAllowed = false;
257
+ }
258
+ }
259
+ // pattern ending with "**" or "**/" match remaining string
260
+ if (remainingPattern === "") {
261
+ consumeRemainingString();
262
+ return true;
263
+ }
264
+ if (skipAllowed) {
265
+ const skipResult = skipUntilMatch({
266
+ pattern: remainingPattern,
267
+ string: remainingString,
268
+ canSkipSlash: true,
269
+ });
270
+ groups.push(...skipResult.groups);
271
+ consumePattern(skipResult.patternIndex);
272
+ consumeRemainingString();
273
+ restoreIndexes = false;
274
+ return skipResult.matched;
275
+ }
276
+ }
277
+ if (remainingPattern[0] === "*") {
278
+ consumePattern(1); // consumes "*"
279
+ if (remainingPattern === "") {
280
+ // matches everything except "/"
281
+ const slashIndex = remainingString.indexOf("/");
282
+ if (slashIndex === -1) {
283
+ groups.push({ string: consumeRemainingString() });
284
+ return true;
285
+ }
286
+ groups.push({ string: consumeString(slashIndex) });
287
+ return false;
288
+ }
289
+ // the next char must not the one expected by remainingPattern[0]
290
+ // because * is greedy and expect to skip at least one char
291
+ if (remainingPattern[0] === remainingString[0]) {
292
+ groups.push({ string: "" });
293
+ patternIndex = patternIndex - 1;
294
+ return false;
295
+ }
296
+ const skipResult = skipUntilMatch({
297
+ pattern: remainingPattern,
298
+ string: remainingString,
299
+ canSkipSlash: false,
300
+ });
301
+ groups.push(skipResult.group, ...skipResult.groups);
302
+ consumePattern(skipResult.patternIndex);
303
+ consumeString(skipResult.index);
304
+ restoreIndexes = false;
305
+ return skipResult.matched;
306
+ }
307
+ if (remainingPattern[0] !== remainingString[0]) {
308
+ return false;
309
+ }
310
+ return undefined;
311
+ };
312
+ iterate();
313
+
314
+ return {
315
+ matched,
316
+ patternIndex,
317
+ index,
318
+ groups,
319
+ };
320
+ };
321
+
322
+ const skipUntilMatch = ({ pattern, string, canSkipSlash }) => {
323
+ let index = 0;
324
+ let remainingString = string;
325
+ let longestAttemptRange = null;
326
+ let isLastAttempt = false;
327
+
328
+ const failure = () => {
329
+ return {
330
+ matched: false,
331
+ patternIndex: longestAttemptRange.patternIndex,
332
+ index: longestAttemptRange.index + longestAttemptRange.length,
333
+ groups: longestAttemptRange.groups,
334
+ group: {
335
+ string: string.slice(0, longestAttemptRange.index),
336
+ },
337
+ };
338
+ };
339
+
340
+ const tryToMatch = () => {
341
+ const matchAttempt = applyMatching(pattern, remainingString);
342
+ if (matchAttempt.matched) {
343
+ return {
344
+ matched: true,
345
+ patternIndex: matchAttempt.patternIndex,
346
+ index: index + matchAttempt.index,
347
+ groups: matchAttempt.groups,
348
+ group: {
349
+ string:
350
+ remainingString === ""
351
+ ? string
352
+ : string.slice(0, -remainingString.length),
353
+ },
354
+ };
355
+ }
356
+ const attemptIndex = matchAttempt.index;
357
+ const attemptRange = {
358
+ patternIndex: matchAttempt.patternIndex,
359
+ index,
360
+ length: attemptIndex,
361
+ groups: matchAttempt.groups,
362
+ };
363
+ if (
364
+ !longestAttemptRange ||
365
+ longestAttemptRange.length < attemptRange.length
366
+ ) {
367
+ longestAttemptRange = attemptRange;
368
+ }
369
+ if (isLastAttempt) {
370
+ return failure();
371
+ }
372
+ const nextIndex = attemptIndex + 1;
373
+ if (nextIndex >= remainingString.length) {
374
+ return failure();
375
+ }
376
+ if (remainingString[0] === "/") {
377
+ if (!canSkipSlash) {
378
+ return failure();
379
+ }
380
+ // when it's the last slash, the next attempt is the last
381
+ if (remainingString.indexOf("/", 1) === -1) {
382
+ isLastAttempt = true;
383
+ }
384
+ }
385
+ // search against the next unattempted string
386
+ index += nextIndex;
387
+ remainingString = remainingString.slice(nextIndex);
388
+ return tryToMatch();
389
+ };
390
+ return tryToMatch();
391
+ };
392
+
393
+ const applyAssociations = ({ url, associations }) => {
394
+ assertUrlLike(url);
395
+ const flatAssociations = asFlatAssociations(associations);
396
+ return Object.keys(flatAssociations).reduce((previousValue, pattern) => {
397
+ const { matched } = applyPatternMatching({
398
+ pattern,
399
+ url,
400
+ });
401
+ if (matched) {
402
+ const value = flatAssociations[pattern];
403
+ if (isPlainObject(previousValue) && isPlainObject(value)) {
404
+ return {
405
+ ...previousValue,
406
+ ...value,
407
+ };
408
+ }
409
+ return value;
410
+ }
411
+ return previousValue;
412
+ }, {});
413
+ };
414
+
415
+ const applyAliases = ({ url, aliases }) => {
416
+ let aliasFullMatchResult;
417
+ const aliasMatchingKey = Object.keys(aliases).find((key) => {
418
+ const aliasMatchResult = applyPatternMatching({
419
+ pattern: key,
420
+ url,
421
+ });
422
+ if (aliasMatchResult.matched) {
423
+ aliasFullMatchResult = aliasMatchResult;
424
+ return true;
425
+ }
426
+ return false;
427
+ });
428
+ if (!aliasMatchingKey) {
429
+ return url;
430
+ }
431
+ const { matchGroups } = aliasFullMatchResult;
432
+ const alias = aliases[aliasMatchingKey];
433
+ const parts = alias.split("*");
434
+ const newUrl = parts.reduce((previous, value, index) => {
435
+ return `${previous}${value}${
436
+ index === parts.length - 1 ? "" : matchGroups[index]
437
+ }`;
438
+ }, "");
439
+ return newUrl;
440
+ };
441
+
442
+ const urlChildMayMatch = ({ url, associations, predicate }) => {
443
+ assertUrlLike(url, "url");
444
+ // the function was meants to be used on url ending with '/'
445
+ if (!url.endsWith("/")) {
446
+ throw new Error(`url should end with /, got ${url}`);
447
+ }
448
+ if (typeof predicate !== "function") {
449
+ throw new TypeError(`predicate must be a function, got ${predicate}`);
450
+ }
451
+ const flatAssociations = asFlatAssociations(associations);
452
+ // for full match we must create an object to allow pattern to override previous ones
453
+ let fullMatchMeta = {};
454
+ let someFullMatch = false;
455
+ // for partial match, any meta satisfying predicate will be valid because
456
+ // we don't know for sure if pattern will still match for a file inside pathname
457
+ const partialMatchMetaArray = [];
458
+ Object.keys(flatAssociations).forEach((pattern) => {
459
+ const value = flatAssociations[pattern];
460
+ const matchResult = applyPatternMatching({
461
+ pattern,
462
+ url,
463
+ });
464
+ if (matchResult.matched) {
465
+ someFullMatch = true;
466
+ if (isPlainObject(fullMatchMeta) && isPlainObject(value)) {
467
+ fullMatchMeta = {
468
+ ...fullMatchMeta,
469
+ ...value,
470
+ };
471
+ } else {
472
+ fullMatchMeta = value;
473
+ }
474
+ } else if (someFullMatch === false && matchResult.urlIndex >= url.length) {
475
+ partialMatchMetaArray.push(value);
476
+ }
477
+ });
478
+ if (someFullMatch) {
479
+ return Boolean(predicate(fullMatchMeta));
480
+ }
481
+ return partialMatchMetaArray.some((partialMatchMeta) =>
482
+ predicate(partialMatchMeta),
483
+ );
484
+ };
485
+
486
+ const URL_META = {
487
+ resolveAssociations,
488
+ applyAssociations,
489
+ urlChildMayMatch,
490
+ applyPatternMatching,
491
+ applyAliases,
492
+ };
493
+
494
+ /*
495
+ * data:[<mediatype>][;base64],<data>
496
+ * https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs#syntax
497
+ */
498
+
499
+ /* eslint-env browser, node */
500
+
501
+ const DATA_URL = {
502
+ parse: (string) => {
503
+ const afterDataProtocol = string.slice("data:".length);
504
+ const commaIndex = afterDataProtocol.indexOf(",");
505
+ const beforeComma = afterDataProtocol.slice(0, commaIndex);
506
+
507
+ let contentType;
508
+ let base64Flag;
509
+ if (beforeComma.endsWith(`;base64`)) {
510
+ contentType = beforeComma.slice(0, -`;base64`.length);
511
+ base64Flag = true;
512
+ } else {
513
+ contentType = beforeComma;
514
+ base64Flag = false;
515
+ }
516
+
517
+ contentType =
518
+ contentType === "" ? "text/plain;charset=US-ASCII" : contentType;
519
+ const afterComma = afterDataProtocol.slice(commaIndex + 1);
520
+ return {
521
+ contentType,
522
+ base64Flag,
523
+ data: afterComma,
524
+ };
525
+ },
526
+
527
+ stringify: ({ contentType, base64Flag = true, data }) => {
528
+ if (!contentType || contentType === "text/plain;charset=US-ASCII") {
529
+ // can be a buffer or a string, hence check on data.length instead of !data or data === ''
530
+ if (data.length === 0) {
531
+ return `data:,`;
532
+ }
533
+ if (base64Flag) {
534
+ return `data:;base64,${data}`;
535
+ }
536
+ return `data:,${data}`;
537
+ }
538
+ if (base64Flag) {
539
+ return `data:${contentType};base64,${data}`;
540
+ }
541
+ return `data:${contentType},${data}`;
542
+ },
543
+ };
544
+
545
+ // consider switching to https://babeljs.io/docs/en/babel-code-frame
546
+ // https://github.com/postcss/postcss/blob/fd30d3df5abc0954a0ec642a3cdc644ab2aacf9c/lib/css-syntax-error.js#L43
547
+ // https://github.com/postcss/postcss/blob/fd30d3df5abc0954a0ec642a3cdc644ab2aacf9c/lib/terminal-highlight.js#L50
548
+ // https://github.com/babel/babel/blob/eea156b2cb8deecfcf82d52aa1b71ba4995c7d68/packages/babel-code-frame/src/index.js#L1
549
+
550
+
551
+ const stringifyUrlSite = (
552
+ { url, line, column, content },
553
+ {
554
+ showCodeFrame = true,
555
+ numberOfSurroundingLinesToShow,
556
+ lineMaxLength,
557
+ color,
558
+ } = {},
559
+ ) => {
560
+ let string = url;
561
+
562
+ if (typeof line === "number") {
563
+ string += `:${line}`;
564
+ if (typeof column === "number") {
565
+ string += `:${column}`;
566
+ }
567
+ }
568
+
569
+ if (!showCodeFrame || typeof line !== "number" || !content) {
570
+ return string;
571
+ }
572
+
573
+ const sourceLoc = showSourceLocation({
574
+ content,
575
+ line,
576
+ column,
577
+ numberOfSurroundingLinesToShow,
578
+ lineMaxLength,
579
+ color,
580
+ });
581
+
582
+ return `${string}
583
+ ${sourceLoc}`;
584
+ };
585
+
586
+ const showSourceLocation = ({
587
+ content,
588
+ line,
589
+ column,
590
+ numberOfSurroundingLinesToShow = 1,
591
+ lineMaxLength = 120,
592
+ } = {}) => {
593
+ let mark = (string) => string;
594
+ let aside = (string) => string;
595
+ // if (color) {
596
+ // mark = (string) => ANSI.color(string, ANSI.RED)
597
+ // aside = (string) => ANSI.color(string, ANSI.GREY)
598
+ // }
599
+
600
+ const lines = content.split(/\r?\n/);
601
+ if (line === 0) line = 1;
602
+ let lineRange = {
603
+ start: line - 1,
604
+ end: line,
605
+ };
606
+ lineRange = moveLineRangeUp(lineRange, numberOfSurroundingLinesToShow);
607
+ lineRange = moveLineRangeDown(lineRange, numberOfSurroundingLinesToShow);
608
+ lineRange = lineRangeWithinLines(lineRange, lines);
609
+ const linesToShow = lines.slice(lineRange.start, lineRange.end);
610
+ const endLineNumber = lineRange.end;
611
+ const lineNumberMaxWidth = String(endLineNumber).length;
612
+
613
+ if (column === 0) column = 1;
614
+
615
+ const columnRange = {};
616
+ if (column === undefined) {
617
+ columnRange.start = 0;
618
+ columnRange.end = lineMaxLength;
619
+ } else if (column > lineMaxLength) {
149
620
  columnRange.start = column - Math.floor(lineMaxLength / 2);
150
621
  columnRange.end = column + Math.ceil(lineMaxLength / 2);
151
622
  } else {
@@ -1022,989 +1493,518 @@ const readStat = (
1022
1493
  * - Buffer documentation on Node.js
1023
1494
  * https://nodejs.org/docs/latest-v13.x/api/buffer.html
1024
1495
  * - eTag documentation on MDN
1025
- * https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag
1026
- */
1027
-
1028
-
1029
- const ETAG_FOR_EMPTY_CONTENT$1 = '"0-2jmj7l5rSw0yVb/vlWAYkK/YBwk"';
1030
-
1031
- const bufferToEtag$1 = (buffer) => {
1032
- if (!Buffer.isBuffer(buffer)) {
1033
- throw new TypeError(`buffer expected, got ${buffer}`);
1034
- }
1035
-
1036
- if (buffer.length === 0) {
1037
- return ETAG_FOR_EMPTY_CONTENT$1;
1038
- }
1039
-
1040
- const hash = createHash("sha1");
1041
- hash.update(buffer, "utf8");
1042
-
1043
- const hashBase64String = hash.digest("base64");
1044
- const hashBase64StringSubset = hashBase64String.slice(0, 27);
1045
- const length = buffer.length;
1046
-
1047
- return `"${length.toString(16)}-${hashBase64StringSubset}"`;
1048
- };
1049
-
1050
- /*
1051
- * See callback_race.md
1052
- */
1053
-
1054
- const raceCallbacks = (raceDescription, winnerCallback) => {
1055
- let cleanCallbacks = [];
1056
- let status = "racing";
1057
-
1058
- const clean = () => {
1059
- cleanCallbacks.forEach((clean) => {
1060
- clean();
1061
- });
1062
- cleanCallbacks = null;
1063
- };
1064
-
1065
- const cancel = () => {
1066
- if (status !== "racing") {
1067
- return;
1068
- }
1069
- status = "cancelled";
1070
- clean();
1071
- };
1072
-
1073
- Object.keys(raceDescription).forEach((candidateName) => {
1074
- const register = raceDescription[candidateName];
1075
- const returnValue = register((data) => {
1076
- if (status !== "racing") {
1077
- return;
1078
- }
1079
- status = "done";
1080
- clean();
1081
- winnerCallback({
1082
- name: candidateName,
1083
- data,
1084
- });
1085
- });
1086
- if (typeof returnValue === "function") {
1087
- cleanCallbacks.push(returnValue);
1088
- }
1089
- });
1090
-
1091
- return cancel;
1092
- };
1093
-
1094
- const createCallbackListNotifiedOnce = () => {
1095
- let callbacks = [];
1096
- let status = "waiting";
1097
- let currentCallbackIndex = -1;
1098
-
1099
- const callbackListOnce = {};
1100
-
1101
- const add = (callback) => {
1102
- if (status !== "waiting") {
1103
- emitUnexpectedActionWarning({ action: "add", status });
1104
- return removeNoop;
1105
- }
1106
-
1107
- if (typeof callback !== "function") {
1108
- throw new Error(`callback must be a function, got ${callback}`);
1109
- }
1110
-
1111
- // don't register twice
1112
- const existingCallback = callbacks.find((callbackCandidate) => {
1113
- return callbackCandidate === callback;
1114
- });
1115
- if (existingCallback) {
1116
- emitCallbackDuplicationWarning();
1117
- return removeNoop;
1118
- }
1119
-
1120
- callbacks.push(callback);
1121
- return () => {
1122
- if (status === "notified") {
1123
- // once called removing does nothing
1124
- // as the callbacks array is frozen to null
1125
- return;
1126
- }
1127
-
1128
- const index = callbacks.indexOf(callback);
1129
- if (index === -1) {
1130
- return;
1131
- }
1132
-
1133
- if (status === "looping") {
1134
- if (index <= currentCallbackIndex) {
1135
- // The callback was already called (or is the current callback)
1136
- // We don't want to mutate the callbacks array
1137
- // or it would alter the looping done in "call" and the next callback
1138
- // would be skipped
1139
- return;
1140
- }
1141
-
1142
- // Callback is part of the next callback to call,
1143
- // we mutate the callbacks array to prevent this callback to be called
1144
- }
1145
-
1146
- callbacks.splice(index, 1);
1147
- };
1148
- };
1149
-
1150
- const notify = (param) => {
1151
- if (status !== "waiting") {
1152
- emitUnexpectedActionWarning({ action: "call", status });
1153
- return [];
1154
- }
1155
- status = "looping";
1156
- const values = callbacks.map((callback, index) => {
1157
- currentCallbackIndex = index;
1158
- return callback(param);
1159
- });
1160
- callbackListOnce.notified = true;
1161
- status = "notified";
1162
- // we reset callbacks to null after looping
1163
- // so that it's possible to remove during the loop
1164
- callbacks = null;
1165
- currentCallbackIndex = -1;
1166
-
1167
- return values;
1168
- };
1169
-
1170
- callbackListOnce.notified = false;
1171
- callbackListOnce.add = add;
1172
- callbackListOnce.notify = notify;
1173
-
1174
- return callbackListOnce;
1175
- };
1176
-
1177
- const emitUnexpectedActionWarning = ({ action, status }) => {
1178
- if (typeof process.emitWarning === "function") {
1179
- process.emitWarning(
1180
- `"${action}" should not happen when callback list is ${status}`,
1181
- {
1182
- CODE: "UNEXPECTED_ACTION_ON_CALLBACK_LIST",
1183
- detail: `Code is potentially executed when it should not`,
1184
- },
1185
- );
1186
- } else {
1187
- console.warn(
1188
- `"${action}" should not happen when callback list is ${status}`,
1189
- );
1190
- }
1191
- };
1192
-
1193
- const emitCallbackDuplicationWarning = () => {
1194
- if (typeof process.emitWarning === "function") {
1195
- process.emitWarning(`Trying to add a callback already in the list`, {
1196
- CODE: "CALLBACK_DUPLICATION",
1197
- detail: `Code is potentially executed more than it should`,
1198
- });
1199
- } else {
1200
- console.warn(`Trying to add same callback twice`);
1201
- }
1202
- };
1203
-
1204
- const removeNoop = () => {};
1205
-
1206
- /*
1207
- * https://github.com/whatwg/dom/issues/920
1208
- */
1209
-
1210
-
1211
- const Abort = {
1212
- isAbortError: (error) => {
1213
- return error && error.name === "AbortError";
1214
- },
1496
+ * https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag
1497
+ */
1215
1498
 
1216
- startOperation: () => {
1217
- return createOperation();
1218
- },
1219
1499
 
1220
- throwIfAborted: (signal) => {
1221
- if (signal.aborted) {
1222
- const error = new Error(`The operation was aborted`);
1223
- error.name = "AbortError";
1224
- error.type = "aborted";
1225
- throw error;
1226
- }
1227
- },
1228
- };
1500
+ const ETAG_FOR_EMPTY_CONTENT$1 = '"0-2jmj7l5rSw0yVb/vlWAYkK/YBwk"';
1229
1501
 
1230
- const createOperation = () => {
1231
- const operationAbortController = new AbortController();
1232
- // const abortOperation = (value) => abortController.abort(value)
1233
- const operationSignal = operationAbortController.signal;
1502
+ const bufferToEtag$1 = (buffer) => {
1503
+ if (!Buffer.isBuffer(buffer)) {
1504
+ throw new TypeError(`buffer expected, got ${buffer}`);
1505
+ }
1234
1506
 
1235
- // abortCallbackList is used to ignore the max listeners warning from Node.js
1236
- // this warning is useful but becomes problematic when it's expected
1237
- // (a function doing 20 http call in parallel)
1238
- // To be 100% sure we don't have memory leak, only Abortable.asyncCallback
1239
- // uses abortCallbackList to know when something is aborted
1240
- const abortCallbackList = createCallbackListNotifiedOnce();
1241
- const endCallbackList = createCallbackListNotifiedOnce();
1507
+ if (buffer.length === 0) {
1508
+ return ETAG_FOR_EMPTY_CONTENT$1;
1509
+ }
1242
1510
 
1243
- let isAbortAfterEnd = false;
1511
+ const hash = createHash("sha1");
1512
+ hash.update(buffer, "utf8");
1244
1513
 
1245
- operationSignal.onabort = () => {
1246
- operationSignal.onabort = null;
1514
+ const hashBase64String = hash.digest("base64");
1515
+ const hashBase64StringSubset = hashBase64String.slice(0, 27);
1516
+ const length = buffer.length;
1247
1517
 
1248
- const allAbortCallbacksPromise = Promise.all(abortCallbackList.notify());
1249
- if (!isAbortAfterEnd) {
1250
- addEndCallback(async () => {
1251
- await allAbortCallbacksPromise;
1252
- });
1253
- }
1254
- };
1518
+ return `"${length.toString(16)}-${hashBase64StringSubset}"`;
1519
+ };
1255
1520
 
1256
- const throwIfAborted = () => {
1257
- Abort.throwIfAborted(operationSignal);
1258
- };
1521
+ /*
1522
+ * See callback_race.md
1523
+ */
1259
1524
 
1260
- // add a callback called on abort
1261
- // differences with signal.addEventListener('abort')
1262
- // - operation.end awaits the return value of this callback
1263
- // - It won't increase the count of listeners for "abort" that would
1264
- // trigger max listeners warning when count > 10
1265
- const addAbortCallback = (callback) => {
1266
- // It would be painful and not super redable to check if signal is aborted
1267
- // before deciding if it's an abort or end callback
1268
- // with pseudo-code below where we want to stop server either
1269
- // on abort or when ended because signal is aborted
1270
- // operation[operation.signal.aborted ? 'addAbortCallback': 'addEndCallback'](async () => {
1271
- // await server.stop()
1272
- // })
1273
- if (operationSignal.aborted) {
1274
- return addEndCallback(callback);
1275
- }
1276
- return abortCallbackList.add(callback);
1277
- };
1525
+ const raceCallbacks = (raceDescription, winnerCallback) => {
1526
+ let cleanCallbacks = [];
1527
+ let status = "racing";
1278
1528
 
1279
- const addEndCallback = (callback) => {
1280
- return endCallbackList.add(callback);
1529
+ const clean = () => {
1530
+ cleanCallbacks.forEach((clean) => {
1531
+ clean();
1532
+ });
1533
+ cleanCallbacks = null;
1281
1534
  };
1282
1535
 
1283
- const end = async ({ abortAfterEnd = false } = {}) => {
1284
- await Promise.all(endCallbackList.notify());
1285
-
1286
- // "abortAfterEnd" can be handy to ensure "abort" callbacks
1287
- // added with { once: true } are removed
1288
- // It might also help garbage collection because
1289
- // runtime implementing AbortSignal (Node.js, browsers) can consider abortSignal
1290
- // as settled and clean up things
1291
- if (abortAfterEnd) {
1292
- // because of operationSignal.onabort = null
1293
- // + abortCallbackList.clear() this won't re-call
1294
- // callbacks
1295
- if (!operationSignal.aborted) {
1296
- isAbortAfterEnd = true;
1297
- operationAbortController.abort();
1298
- }
1536
+ const cancel = () => {
1537
+ if (status !== "racing") {
1538
+ return;
1299
1539
  }
1540
+ status = "cancelled";
1541
+ clean();
1300
1542
  };
1301
1543
 
1302
- const addAbortSignal = (
1303
- signal,
1304
- { onAbort = callbackNoop, onRemove = callbackNoop } = {},
1305
- ) => {
1306
- const applyAbortEffects = () => {
1307
- const onAbortCallback = onAbort;
1308
- onAbort = callbackNoop;
1309
- onAbortCallback();
1310
- };
1311
- const applyRemoveEffects = () => {
1312
- const onRemoveCallback = onRemove;
1313
- onRemove = callbackNoop;
1314
- onAbort = callbackNoop;
1315
- onRemoveCallback();
1316
- };
1317
-
1318
- if (operationSignal.aborted) {
1319
- applyAbortEffects();
1320
- applyRemoveEffects();
1321
- return callbackNoop;
1544
+ Object.keys(raceDescription).forEach((candidateName) => {
1545
+ const register = raceDescription[candidateName];
1546
+ const returnValue = register((data) => {
1547
+ if (status !== "racing") {
1548
+ return;
1549
+ }
1550
+ status = "done";
1551
+ clean();
1552
+ winnerCallback({
1553
+ name: candidateName,
1554
+ data,
1555
+ });
1556
+ });
1557
+ if (typeof returnValue === "function") {
1558
+ cleanCallbacks.push(returnValue);
1322
1559
  }
1560
+ });
1323
1561
 
1324
- if (signal.aborted) {
1325
- operationAbortController.abort();
1326
- applyAbortEffects();
1327
- applyRemoveEffects();
1328
- return callbackNoop;
1329
- }
1562
+ return cancel;
1563
+ };
1330
1564
 
1331
- const cancelRace = raceCallbacks(
1332
- {
1333
- operation_abort: (cb) => {
1334
- return addAbortCallback(cb);
1335
- },
1336
- operation_end: (cb) => {
1337
- return addEndCallback(cb);
1338
- },
1339
- child_abort: (cb) => {
1340
- return addEventListener(signal, "abort", cb);
1341
- },
1342
- },
1343
- (winner) => {
1344
- const raceEffects = {
1345
- // Both "operation_abort" and "operation_end"
1346
- // means we don't care anymore if the child aborts.
1347
- // So we can:
1348
- // - remove "abort" event listener on child (done by raceCallback)
1349
- // - remove abort callback on operation (done by raceCallback)
1350
- // - remove end callback on operation (done by raceCallback)
1351
- // - call any custom cancel function
1352
- operation_abort: () => {
1353
- applyAbortEffects();
1354
- applyRemoveEffects();
1355
- },
1356
- operation_end: () => {
1357
- // Exists to
1358
- // - remove abort callback on operation
1359
- // - remove "abort" event listener on child
1360
- // - call any custom cancel function
1361
- applyRemoveEffects();
1362
- },
1363
- child_abort: () => {
1364
- applyAbortEffects();
1365
- operationAbortController.abort();
1366
- },
1367
- };
1368
- raceEffects[winner.name](winner.value);
1369
- },
1370
- );
1565
+ const createCallbackListNotifiedOnce = () => {
1566
+ let callbacks = [];
1567
+ let status = "waiting";
1568
+ let currentCallbackIndex = -1;
1371
1569
 
1372
- return () => {
1373
- cancelRace();
1374
- applyRemoveEffects();
1375
- };
1376
- };
1570
+ const callbackListOnce = {};
1377
1571
 
1378
- const addAbortSource = (abortSourceCallback) => {
1379
- const abortSource = {
1380
- cleaned: false,
1381
- signal: null,
1382
- remove: callbackNoop,
1383
- };
1384
- const abortSourceController = new AbortController();
1385
- const abortSourceSignal = abortSourceController.signal;
1386
- abortSource.signal = abortSourceSignal;
1387
- if (operationSignal.aborted) {
1388
- return abortSource;
1572
+ const add = (callback) => {
1573
+ if (status !== "waiting") {
1574
+ emitUnexpectedActionWarning({ action: "add", status });
1575
+ return removeNoop;
1389
1576
  }
1390
- const returnValue = abortSourceCallback((value) => {
1391
- abortSourceController.abort(value);
1392
- });
1393
- const removeAbortSignal = addAbortSignal(abortSourceSignal, {
1394
- onRemove: () => {
1395
- if (typeof returnValue === "function") {
1396
- returnValue();
1397
- }
1398
- abortSource.cleaned = true;
1399
- },
1400
- });
1401
- abortSource.remove = removeAbortSignal;
1402
- return abortSource;
1403
- };
1404
-
1405
- const timeout = (ms) => {
1406
- return addAbortSource((abort) => {
1407
- const timeoutId = setTimeout(abort, ms);
1408
- // an abort source return value is called when:
1409
- // - operation is aborted (by an other source)
1410
- // - operation ends
1411
- return () => {
1412
- clearTimeout(timeoutId);
1413
- };
1414
- });
1415
- };
1416
1577
 
1417
- const withSignal = async (asyncCallback) => {
1418
- const abortController = new AbortController();
1419
- const signal = abortController.signal;
1420
- const removeAbortSignal = addAbortSignal(signal, {
1421
- onAbort: () => {
1422
- abortController.abort();
1423
- },
1424
- });
1425
- try {
1426
- const value = await asyncCallback(signal);
1427
- removeAbortSignal();
1428
- return value;
1429
- } catch (e) {
1430
- removeAbortSignal();
1431
- throw e;
1578
+ if (typeof callback !== "function") {
1579
+ throw new Error(`callback must be a function, got ${callback}`);
1432
1580
  }
1433
- };
1434
1581
 
1435
- const withSignalSync = (callback) => {
1436
- const abortController = new AbortController();
1437
- const signal = abortController.signal;
1438
- const removeAbortSignal = addAbortSignal(signal, {
1439
- onAbort: () => {
1440
- abortController.abort();
1441
- },
1582
+ // don't register twice
1583
+ const existingCallback = callbacks.find((callbackCandidate) => {
1584
+ return callbackCandidate === callback;
1442
1585
  });
1443
- try {
1444
- const value = callback(signal);
1445
- removeAbortSignal();
1446
- return value;
1447
- } catch (e) {
1448
- removeAbortSignal();
1449
- throw e;
1586
+ if (existingCallback) {
1587
+ emitCallbackDuplicationWarning();
1588
+ return removeNoop;
1450
1589
  }
1451
- };
1452
-
1453
- return {
1454
- // We could almost hide the operationSignal
1455
- // But it can be handy for 2 things:
1456
- // - know if operation is aborted (operation.signal.aborted)
1457
- // - forward the operation.signal directly (not using "withSignal" or "withSignalSync")
1458
- signal: operationSignal,
1459
1590
 
1460
- throwIfAborted,
1461
- addAbortCallback,
1462
- addAbortSignal,
1463
- addAbortSource,
1464
- timeout,
1465
- withSignal,
1466
- withSignalSync,
1467
- addEndCallback,
1468
- end,
1469
- };
1470
- };
1591
+ callbacks.push(callback);
1592
+ return () => {
1593
+ if (status === "notified") {
1594
+ // once called removing does nothing
1595
+ // as the callbacks array is frozen to null
1596
+ return;
1597
+ }
1471
1598
 
1472
- const callbackNoop = () => {};
1599
+ const index = callbacks.indexOf(callback);
1600
+ if (index === -1) {
1601
+ return;
1602
+ }
1473
1603
 
1474
- const addEventListener = (target, eventName, cb) => {
1475
- target.addEventListener(eventName, cb);
1476
- return () => {
1477
- target.removeEventListener(eventName, cb);
1478
- };
1479
- };
1604
+ if (status === "looping") {
1605
+ if (index <= currentCallbackIndex) {
1606
+ // The callback was already called (or is the current callback)
1607
+ // We don't want to mutate the callbacks array
1608
+ // or it would alter the looping done in "call" and the next callback
1609
+ // would be skipped
1610
+ return;
1611
+ }
1480
1612
 
1481
- const raceProcessTeardownEvents = (processTeardownEvents, callback) => {
1482
- return raceCallbacks(
1483
- {
1484
- ...(processTeardownEvents.SIGHUP ? SIGHUP_CALLBACK : {}),
1485
- ...(processTeardownEvents.SIGTERM ? SIGTERM_CALLBACK : {}),
1486
- ...(processTeardownEvents.SIGINT ? SIGINT_CALLBACK : {}),
1487
- ...(processTeardownEvents.beforeExit ? BEFORE_EXIT_CALLBACK : {}),
1488
- ...(processTeardownEvents.exit ? EXIT_CALLBACK : {}),
1489
- },
1490
- callback,
1491
- );
1492
- };
1613
+ // Callback is part of the next callback to call,
1614
+ // we mutate the callbacks array to prevent this callback to be called
1615
+ }
1493
1616
 
1494
- const SIGHUP_CALLBACK = {
1495
- SIGHUP: (cb) => {
1496
- process.on("SIGHUP", cb);
1497
- return () => {
1498
- process.removeListener("SIGHUP", cb);
1617
+ callbacks.splice(index, 1);
1499
1618
  };
1500
- },
1501
- };
1619
+ };
1502
1620
 
1503
- const SIGTERM_CALLBACK = {
1504
- SIGTERM: (cb) => {
1505
- process.on("SIGTERM", cb);
1506
- return () => {
1507
- process.removeListener("SIGTERM", cb);
1508
- };
1509
- },
1510
- };
1621
+ const notify = (param) => {
1622
+ if (status !== "waiting") {
1623
+ emitUnexpectedActionWarning({ action: "call", status });
1624
+ return [];
1625
+ }
1626
+ status = "looping";
1627
+ const values = callbacks.map((callback, index) => {
1628
+ currentCallbackIndex = index;
1629
+ return callback(param);
1630
+ });
1631
+ callbackListOnce.notified = true;
1632
+ status = "notified";
1633
+ // we reset callbacks to null after looping
1634
+ // so that it's possible to remove during the loop
1635
+ callbacks = null;
1636
+ currentCallbackIndex = -1;
1511
1637
 
1512
- const BEFORE_EXIT_CALLBACK = {
1513
- beforeExit: (cb) => {
1514
- process.on("beforeExit", cb);
1515
- return () => {
1516
- process.removeListener("beforeExit", cb);
1517
- };
1518
- },
1519
- };
1638
+ return values;
1639
+ };
1520
1640
 
1521
- const EXIT_CALLBACK = {
1522
- exit: (cb) => {
1523
- process.on("exit", cb);
1524
- return () => {
1525
- process.removeListener("exit", cb);
1526
- };
1527
- },
1528
- };
1641
+ callbackListOnce.notified = false;
1642
+ callbackListOnce.add = add;
1643
+ callbackListOnce.notify = notify;
1529
1644
 
1530
- const SIGINT_CALLBACK = {
1531
- SIGINT: (cb) => {
1532
- process.on("SIGINT", cb);
1533
- return () => {
1534
- process.removeListener("SIGINT", cb);
1535
- };
1536
- },
1645
+ return callbackListOnce;
1537
1646
  };
1538
1647
 
1539
- const assertUrlLike = (value, name = "url") => {
1540
- if (typeof value !== "string") {
1541
- throw new TypeError(`${name} must be a url string, got ${value}`);
1542
- }
1543
- if (isWindowsPathnameSpecifier(value)) {
1544
- throw new TypeError(
1545
- `${name} must be a url but looks like a windows pathname, got ${value}`,
1648
+ const emitUnexpectedActionWarning = ({ action, status }) => {
1649
+ if (typeof process.emitWarning === "function") {
1650
+ process.emitWarning(
1651
+ `"${action}" should not happen when callback list is ${status}`,
1652
+ {
1653
+ CODE: "UNEXPECTED_ACTION_ON_CALLBACK_LIST",
1654
+ detail: `Code is potentially executed when it should not`,
1655
+ },
1546
1656
  );
1547
- }
1548
- if (!hasScheme$1(value)) {
1549
- throw new TypeError(
1550
- `${name} must be a url and no scheme found, got ${value}`,
1657
+ } else {
1658
+ console.warn(
1659
+ `"${action}" should not happen when callback list is ${status}`,
1551
1660
  );
1552
1661
  }
1553
1662
  };
1554
1663
 
1555
- const isPlainObject = (value) => {
1556
- if (value === null) {
1557
- return false;
1558
- }
1559
- if (typeof value === "object") {
1560
- if (Array.isArray(value)) {
1561
- return false;
1562
- }
1563
- return true;
1664
+ const emitCallbackDuplicationWarning = () => {
1665
+ if (typeof process.emitWarning === "function") {
1666
+ process.emitWarning(`Trying to add a callback already in the list`, {
1667
+ CODE: "CALLBACK_DUPLICATION",
1668
+ detail: `Code is potentially executed more than it should`,
1669
+ });
1670
+ } else {
1671
+ console.warn(`Trying to add same callback twice`);
1564
1672
  }
1565
- return false;
1566
1673
  };
1567
1674
 
1568
- const isWindowsPathnameSpecifier = (specifier) => {
1569
- const firstChar = specifier[0];
1570
- if (!/[a-zA-Z]/.test(firstChar)) return false;
1571
- const secondChar = specifier[1];
1572
- if (secondChar !== ":") return false;
1573
- const thirdChar = specifier[2];
1574
- return thirdChar === "/" || thirdChar === "\\";
1575
- };
1675
+ const removeNoop = () => {};
1576
1676
 
1577
- const hasScheme$1 = (specifier) => /^[a-zA-Z]+:/.test(specifier);
1677
+ /*
1678
+ * https://github.com/whatwg/dom/issues/920
1679
+ */
1578
1680
 
1579
- const resolveAssociations = (associations, baseUrl) => {
1580
- assertUrlLike(baseUrl, "baseUrl");
1581
- const associationsResolved = {};
1582
- Object.keys(associations).forEach((key) => {
1583
- const value = associations[key];
1584
- if (typeof value === "object" && value !== null) {
1585
- const valueMapResolved = {};
1586
- Object.keys(value).forEach((pattern) => {
1587
- const valueAssociated = value[pattern];
1588
- const patternResolved = normalizeUrlPattern(pattern, baseUrl);
1589
- valueMapResolved[patternResolved] = valueAssociated;
1590
- });
1591
- associationsResolved[key] = valueMapResolved;
1592
- } else {
1593
- associationsResolved[key] = value;
1594
- }
1595
- });
1596
- return associationsResolved;
1597
- };
1598
1681
 
1599
- const normalizeUrlPattern = (urlPattern, baseUrl) => {
1600
- try {
1601
- return String(new URL(urlPattern, baseUrl));
1602
- } catch (e) {
1603
- // it's not really an url, no need to perform url resolution nor encoding
1604
- return urlPattern;
1605
- }
1606
- };
1682
+ const Abort = {
1683
+ isAbortError: (error) => {
1684
+ return error && error.name === "AbortError";
1685
+ },
1607
1686
 
1608
- const asFlatAssociations = (associations) => {
1609
- if (!isPlainObject(associations)) {
1610
- throw new TypeError(
1611
- `associations must be a plain object, got ${associations}`,
1612
- );
1613
- }
1614
- const flatAssociations = {};
1615
- Object.keys(associations).forEach((associationName) => {
1616
- const associationValue = associations[associationName];
1617
- if (isPlainObject(associationValue)) {
1618
- Object.keys(associationValue).forEach((pattern) => {
1619
- const patternValue = associationValue[pattern];
1620
- const previousValue = flatAssociations[pattern];
1621
- if (isPlainObject(previousValue)) {
1622
- flatAssociations[pattern] = {
1623
- ...previousValue,
1624
- [associationName]: patternValue,
1625
- };
1626
- } else {
1627
- flatAssociations[pattern] = {
1628
- [associationName]: patternValue,
1629
- };
1630
- }
1631
- });
1687
+ startOperation: () => {
1688
+ return createOperation();
1689
+ },
1690
+
1691
+ throwIfAborted: (signal) => {
1692
+ if (signal.aborted) {
1693
+ const error = new Error(`The operation was aborted`);
1694
+ error.name = "AbortError";
1695
+ error.type = "aborted";
1696
+ throw error;
1632
1697
  }
1633
- });
1634
- return flatAssociations;
1698
+ },
1635
1699
  };
1636
1700
 
1637
- /*
1638
- * Link to things doing pattern matching:
1639
- * https://git-scm.com/docs/gitignore
1640
- * https://github.com/kaelzhang/node-ignore
1641
- */
1701
+ const createOperation = () => {
1702
+ const operationAbortController = new AbortController();
1703
+ // const abortOperation = (value) => abortController.abort(value)
1704
+ const operationSignal = operationAbortController.signal;
1642
1705
 
1706
+ // abortCallbackList is used to ignore the max listeners warning from Node.js
1707
+ // this warning is useful but becomes problematic when it's expected
1708
+ // (a function doing 20 http call in parallel)
1709
+ // To be 100% sure we don't have memory leak, only Abortable.asyncCallback
1710
+ // uses abortCallbackList to know when something is aborted
1711
+ const abortCallbackList = createCallbackListNotifiedOnce();
1712
+ const endCallbackList = createCallbackListNotifiedOnce();
1643
1713
 
1644
- /** @module jsenv_url_meta **/
1645
- /**
1646
- * An object representing the result of applying a pattern to an url
1647
- * @typedef {Object} MatchResult
1648
- * @property {boolean} matched Indicates if url matched pattern
1649
- * @property {number} patternIndex Index where pattern stopped matching url, otherwise pattern.length
1650
- * @property {number} urlIndex Index where url stopped matching pattern, otherwise url.length
1651
- * @property {Array} matchGroups Array of strings captured during pattern matching
1652
- */
1714
+ let isAbortAfterEnd = false;
1653
1715
 
1654
- /**
1655
- * Apply a pattern to an url
1656
- * @param {Object} applyPatternMatchingParams
1657
- * @param {string} applyPatternMatchingParams.pattern "*", "**" and trailing slash have special meaning
1658
- * @param {string} applyPatternMatchingParams.url a string representing an url
1659
- * @return {MatchResult}
1660
- */
1661
- const applyPatternMatching = ({ url, pattern }) => {
1662
- assertUrlLike(pattern, "pattern");
1663
- assertUrlLike(url, "url");
1664
- const { matched, patternIndex, index, groups } = applyMatching(pattern, url);
1665
- const matchGroups = [];
1666
- let groupIndex = 0;
1667
- groups.forEach((group) => {
1668
- if (group.name) {
1669
- matchGroups[group.name] = group.string;
1670
- } else {
1671
- matchGroups[groupIndex] = group.string;
1672
- groupIndex++;
1716
+ operationSignal.onabort = () => {
1717
+ operationSignal.onabort = null;
1718
+
1719
+ const allAbortCallbacksPromise = Promise.all(abortCallbackList.notify());
1720
+ if (!isAbortAfterEnd) {
1721
+ addEndCallback(async () => {
1722
+ await allAbortCallbacksPromise;
1723
+ });
1673
1724
  }
1674
- });
1675
- return {
1676
- matched,
1677
- patternIndex,
1678
- urlIndex: index,
1679
- matchGroups,
1680
1725
  };
1681
- };
1682
-
1683
- const applyMatching = (pattern, string) => {
1684
- const groups = [];
1685
- let patternIndex = 0;
1686
- let index = 0;
1687
- let remainingPattern = pattern;
1688
- let remainingString = string;
1689
- let restoreIndexes = true;
1690
1726
 
1691
- const consumePattern = (count) => {
1692
- const subpattern = remainingPattern.slice(0, count);
1693
- remainingPattern = remainingPattern.slice(count);
1694
- patternIndex += count;
1695
- return subpattern;
1696
- };
1697
- const consumeString = (count) => {
1698
- const substring = remainingString.slice(0, count);
1699
- remainingString = remainingString.slice(count);
1700
- index += count;
1701
- return substring;
1702
- };
1703
- const consumeRemainingString = () => {
1704
- return consumeString(remainingString.length);
1727
+ const throwIfAborted = () => {
1728
+ Abort.throwIfAborted(operationSignal);
1705
1729
  };
1706
1730
 
1707
- let matched;
1708
- const iterate = () => {
1709
- const patternIndexBefore = patternIndex;
1710
- const indexBefore = index;
1711
- matched = matchOne();
1712
- if (matched === undefined) {
1713
- consumePattern(1);
1714
- consumeString(1);
1715
- iterate();
1716
- return;
1717
- }
1718
- if (matched === false && restoreIndexes) {
1719
- patternIndex = patternIndexBefore;
1720
- index = indexBefore;
1731
+ // add a callback called on abort
1732
+ // differences with signal.addEventListener('abort')
1733
+ // - operation.end awaits the return value of this callback
1734
+ // - It won't increase the count of listeners for "abort" that would
1735
+ // trigger max listeners warning when count > 10
1736
+ const addAbortCallback = (callback) => {
1737
+ // It would be painful and not super redable to check if signal is aborted
1738
+ // before deciding if it's an abort or end callback
1739
+ // with pseudo-code below where we want to stop server either
1740
+ // on abort or when ended because signal is aborted
1741
+ // operation[operation.signal.aborted ? 'addAbortCallback': 'addEndCallback'](async () => {
1742
+ // await server.stop()
1743
+ // })
1744
+ if (operationSignal.aborted) {
1745
+ return addEndCallback(callback);
1721
1746
  }
1747
+ return abortCallbackList.add(callback);
1722
1748
  };
1723
- const matchOne = () => {
1724
- // pattern consumed and string consumed
1725
- if (remainingPattern === "" && remainingString === "") {
1726
- return true; // string fully matched pattern
1727
- }
1728
- // pattern consumed, string not consumed
1729
- if (remainingPattern === "" && remainingString !== "") {
1730
- return false; // fails because string longer than expected
1731
- }
1732
- // -- from this point pattern is not consumed --
1733
- // string consumed, pattern not consumed
1734
- if (remainingString === "") {
1735
- if (remainingPattern === "**") {
1736
- // trailing "**" is optional
1737
- consumePattern(2);
1738
- return true;
1739
- }
1740
- if (remainingPattern === "*") {
1741
- groups.push({ string: "" });
1742
- }
1743
- return false; // fail because string shorter than expected
1744
- }
1745
- // -- from this point pattern and string are not consumed --
1746
- // fast path trailing slash
1747
- if (remainingPattern === "/") {
1748
- if (remainingString[0] === "/") {
1749
- // trailing slash match remaining
1750
- consumePattern(1);
1751
- groups.push({ string: consumeRemainingString() });
1752
- return true;
1753
- }
1754
- return false;
1755
- }
1756
- // fast path trailing '**'
1757
- if (remainingPattern === "**") {
1758
- consumePattern(2);
1759
- consumeRemainingString();
1760
- return true;
1761
- }
1762
- // pattern leading **
1763
- if (remainingPattern.slice(0, 2) === "**") {
1764
- consumePattern(2); // consumes "**"
1765
- let skipAllowed = true;
1766
- if (remainingPattern[0] === "/") {
1767
- consumePattern(1); // consumes "/"
1768
- // when remainingPattern was preceeded by "**/"
1769
- // and remainingString have no "/"
1770
- // then skip is not allowed, a regular match will be performed
1771
- if (!remainingString.includes("/")) {
1772
- skipAllowed = false;
1773
- }
1774
- }
1775
- // pattern ending with "**" or "**/" match remaining string
1776
- if (remainingPattern === "") {
1777
- consumeRemainingString();
1778
- return true;
1779
- }
1780
- if (skipAllowed) {
1781
- const skipResult = skipUntilMatch({
1782
- pattern: remainingPattern,
1783
- string: remainingString,
1784
- canSkipSlash: true,
1785
- });
1786
- groups.push(...skipResult.groups);
1787
- consumePattern(skipResult.patternIndex);
1788
- consumeRemainingString();
1789
- restoreIndexes = false;
1790
- return skipResult.matched;
1791
- }
1792
- }
1793
- if (remainingPattern[0] === "*") {
1794
- consumePattern(1); // consumes "*"
1795
- if (remainingPattern === "") {
1796
- // matches everything except "/"
1797
- const slashIndex = remainingString.indexOf("/");
1798
- if (slashIndex === -1) {
1799
- groups.push({ string: consumeRemainingString() });
1800
- return true;
1801
- }
1802
- groups.push({ string: consumeString(slashIndex) });
1803
- return false;
1804
- }
1805
- // the next char must not the one expected by remainingPattern[0]
1806
- // because * is greedy and expect to skip at least one char
1807
- if (remainingPattern[0] === remainingString[0]) {
1808
- groups.push({ string: "" });
1809
- patternIndex = patternIndex - 1;
1810
- return false;
1749
+
1750
+ const addEndCallback = (callback) => {
1751
+ return endCallbackList.add(callback);
1752
+ };
1753
+
1754
+ const end = async ({ abortAfterEnd = false } = {}) => {
1755
+ await Promise.all(endCallbackList.notify());
1756
+
1757
+ // "abortAfterEnd" can be handy to ensure "abort" callbacks
1758
+ // added with { once: true } are removed
1759
+ // It might also help garbage collection because
1760
+ // runtime implementing AbortSignal (Node.js, browsers) can consider abortSignal
1761
+ // as settled and clean up things
1762
+ if (abortAfterEnd) {
1763
+ // because of operationSignal.onabort = null
1764
+ // + abortCallbackList.clear() this won't re-call
1765
+ // callbacks
1766
+ if (!operationSignal.aborted) {
1767
+ isAbortAfterEnd = true;
1768
+ operationAbortController.abort();
1811
1769
  }
1812
- const skipResult = skipUntilMatch({
1813
- pattern: remainingPattern,
1814
- string: remainingString,
1815
- canSkipSlash: false,
1816
- });
1817
- groups.push(skipResult.group, ...skipResult.groups);
1818
- consumePattern(skipResult.patternIndex);
1819
- consumeString(skipResult.index);
1820
- restoreIndexes = false;
1821
- return skipResult.matched;
1822
- }
1823
- if (remainingPattern[0] !== remainingString[0]) {
1824
- return false;
1825
1770
  }
1826
- return undefined;
1827
1771
  };
1828
- iterate();
1829
1772
 
1830
- return {
1831
- matched,
1832
- patternIndex,
1833
- index,
1834
- groups,
1835
- };
1836
- };
1773
+ const addAbortSignal = (
1774
+ signal,
1775
+ { onAbort = callbackNoop, onRemove = callbackNoop } = {},
1776
+ ) => {
1777
+ const applyAbortEffects = () => {
1778
+ const onAbortCallback = onAbort;
1779
+ onAbort = callbackNoop;
1780
+ onAbortCallback();
1781
+ };
1782
+ const applyRemoveEffects = () => {
1783
+ const onRemoveCallback = onRemove;
1784
+ onRemove = callbackNoop;
1785
+ onAbort = callbackNoop;
1786
+ onRemoveCallback();
1787
+ };
1788
+
1789
+ if (operationSignal.aborted) {
1790
+ applyAbortEffects();
1791
+ applyRemoveEffects();
1792
+ return callbackNoop;
1793
+ }
1837
1794
 
1838
- const skipUntilMatch = ({ pattern, string, canSkipSlash }) => {
1839
- let index = 0;
1840
- let remainingString = string;
1841
- let longestAttemptRange = null;
1842
- let isLastAttempt = false;
1795
+ if (signal.aborted) {
1796
+ operationAbortController.abort();
1797
+ applyAbortEffects();
1798
+ applyRemoveEffects();
1799
+ return callbackNoop;
1800
+ }
1843
1801
 
1844
- const failure = () => {
1845
- return {
1846
- matched: false,
1847
- patternIndex: longestAttemptRange.patternIndex,
1848
- index: longestAttemptRange.index + longestAttemptRange.length,
1849
- groups: longestAttemptRange.groups,
1850
- group: {
1851
- string: string.slice(0, longestAttemptRange.index),
1802
+ const cancelRace = raceCallbacks(
1803
+ {
1804
+ operation_abort: (cb) => {
1805
+ return addAbortCallback(cb);
1806
+ },
1807
+ operation_end: (cb) => {
1808
+ return addEndCallback(cb);
1809
+ },
1810
+ child_abort: (cb) => {
1811
+ return addEventListener(signal, "abort", cb);
1812
+ },
1813
+ },
1814
+ (winner) => {
1815
+ const raceEffects = {
1816
+ // Both "operation_abort" and "operation_end"
1817
+ // means we don't care anymore if the child aborts.
1818
+ // So we can:
1819
+ // - remove "abort" event listener on child (done by raceCallback)
1820
+ // - remove abort callback on operation (done by raceCallback)
1821
+ // - remove end callback on operation (done by raceCallback)
1822
+ // - call any custom cancel function
1823
+ operation_abort: () => {
1824
+ applyAbortEffects();
1825
+ applyRemoveEffects();
1826
+ },
1827
+ operation_end: () => {
1828
+ // Exists to
1829
+ // - remove abort callback on operation
1830
+ // - remove "abort" event listener on child
1831
+ // - call any custom cancel function
1832
+ applyRemoveEffects();
1833
+ },
1834
+ child_abort: () => {
1835
+ applyAbortEffects();
1836
+ operationAbortController.abort();
1837
+ },
1838
+ };
1839
+ raceEffects[winner.name](winner.value);
1852
1840
  },
1841
+ );
1842
+
1843
+ return () => {
1844
+ cancelRace();
1845
+ applyRemoveEffects();
1853
1846
  };
1854
1847
  };
1855
1848
 
1856
- const tryToMatch = () => {
1857
- const matchAttempt = applyMatching(pattern, remainingString);
1858
- if (matchAttempt.matched) {
1859
- return {
1860
- matched: true,
1861
- patternIndex: matchAttempt.patternIndex,
1862
- index: index + matchAttempt.index,
1863
- groups: matchAttempt.groups,
1864
- group: {
1865
- string:
1866
- remainingString === ""
1867
- ? string
1868
- : string.slice(0, -remainingString.length),
1869
- },
1870
- };
1871
- }
1872
- const attemptIndex = matchAttempt.index;
1873
- const attemptRange = {
1874
- patternIndex: matchAttempt.patternIndex,
1875
- index,
1876
- length: attemptIndex,
1877
- groups: matchAttempt.groups,
1849
+ const addAbortSource = (abortSourceCallback) => {
1850
+ const abortSource = {
1851
+ cleaned: false,
1852
+ signal: null,
1853
+ remove: callbackNoop,
1878
1854
  };
1879
- if (
1880
- !longestAttemptRange ||
1881
- longestAttemptRange.length < attemptRange.length
1882
- ) {
1883
- longestAttemptRange = attemptRange;
1884
- }
1885
- if (isLastAttempt) {
1886
- return failure();
1887
- }
1888
- const nextIndex = attemptIndex + 1;
1889
- if (nextIndex >= remainingString.length) {
1890
- return failure();
1855
+ const abortSourceController = new AbortController();
1856
+ const abortSourceSignal = abortSourceController.signal;
1857
+ abortSource.signal = abortSourceSignal;
1858
+ if (operationSignal.aborted) {
1859
+ return abortSource;
1891
1860
  }
1892
- if (remainingString[0] === "/") {
1893
- if (!canSkipSlash) {
1894
- return failure();
1895
- }
1896
- // when it's the last slash, the next attempt is the last
1897
- if (remainingString.indexOf("/", 1) === -1) {
1898
- isLastAttempt = true;
1899
- }
1861
+ const returnValue = abortSourceCallback((value) => {
1862
+ abortSourceController.abort(value);
1863
+ });
1864
+ const removeAbortSignal = addAbortSignal(abortSourceSignal, {
1865
+ onRemove: () => {
1866
+ if (typeof returnValue === "function") {
1867
+ returnValue();
1868
+ }
1869
+ abortSource.cleaned = true;
1870
+ },
1871
+ });
1872
+ abortSource.remove = removeAbortSignal;
1873
+ return abortSource;
1874
+ };
1875
+
1876
+ const timeout = (ms) => {
1877
+ return addAbortSource((abort) => {
1878
+ const timeoutId = setTimeout(abort, ms);
1879
+ // an abort source return value is called when:
1880
+ // - operation is aborted (by an other source)
1881
+ // - operation ends
1882
+ return () => {
1883
+ clearTimeout(timeoutId);
1884
+ };
1885
+ });
1886
+ };
1887
+
1888
+ const withSignal = async (asyncCallback) => {
1889
+ const abortController = new AbortController();
1890
+ const signal = abortController.signal;
1891
+ const removeAbortSignal = addAbortSignal(signal, {
1892
+ onAbort: () => {
1893
+ abortController.abort();
1894
+ },
1895
+ });
1896
+ try {
1897
+ const value = await asyncCallback(signal);
1898
+ removeAbortSignal();
1899
+ return value;
1900
+ } catch (e) {
1901
+ removeAbortSignal();
1902
+ throw e;
1900
1903
  }
1901
- // search against the next unattempted string
1902
- index += nextIndex;
1903
- remainingString = remainingString.slice(nextIndex);
1904
- return tryToMatch();
1905
1904
  };
1906
- return tryToMatch();
1907
- };
1908
1905
 
1909
- const applyAssociations = ({ url, associations }) => {
1910
- assertUrlLike(url);
1911
- const flatAssociations = asFlatAssociations(associations);
1912
- return Object.keys(flatAssociations).reduce((previousValue, pattern) => {
1913
- const { matched } = applyPatternMatching({
1914
- pattern,
1915
- url,
1906
+ const withSignalSync = (callback) => {
1907
+ const abortController = new AbortController();
1908
+ const signal = abortController.signal;
1909
+ const removeAbortSignal = addAbortSignal(signal, {
1910
+ onAbort: () => {
1911
+ abortController.abort();
1912
+ },
1916
1913
  });
1917
- if (matched) {
1918
- const value = flatAssociations[pattern];
1919
- if (isPlainObject(previousValue) && isPlainObject(value)) {
1920
- return {
1921
- ...previousValue,
1922
- ...value,
1923
- };
1924
- }
1914
+ try {
1915
+ const value = callback(signal);
1916
+ removeAbortSignal();
1925
1917
  return value;
1918
+ } catch (e) {
1919
+ removeAbortSignal();
1920
+ throw e;
1926
1921
  }
1927
- return previousValue;
1928
- }, {});
1922
+ };
1923
+
1924
+ return {
1925
+ // We could almost hide the operationSignal
1926
+ // But it can be handy for 2 things:
1927
+ // - know if operation is aborted (operation.signal.aborted)
1928
+ // - forward the operation.signal directly (not using "withSignal" or "withSignalSync")
1929
+ signal: operationSignal,
1930
+
1931
+ throwIfAborted,
1932
+ addAbortCallback,
1933
+ addAbortSignal,
1934
+ addAbortSource,
1935
+ timeout,
1936
+ withSignal,
1937
+ withSignalSync,
1938
+ addEndCallback,
1939
+ end,
1940
+ };
1941
+ };
1942
+
1943
+ const callbackNoop = () => {};
1944
+
1945
+ const addEventListener = (target, eventName, cb) => {
1946
+ target.addEventListener(eventName, cb);
1947
+ return () => {
1948
+ target.removeEventListener(eventName, cb);
1949
+ };
1950
+ };
1951
+
1952
+ const raceProcessTeardownEvents = (processTeardownEvents, callback) => {
1953
+ return raceCallbacks(
1954
+ {
1955
+ ...(processTeardownEvents.SIGHUP ? SIGHUP_CALLBACK : {}),
1956
+ ...(processTeardownEvents.SIGTERM ? SIGTERM_CALLBACK : {}),
1957
+ ...(processTeardownEvents.SIGINT ? SIGINT_CALLBACK : {}),
1958
+ ...(processTeardownEvents.beforeExit ? BEFORE_EXIT_CALLBACK : {}),
1959
+ ...(processTeardownEvents.exit ? EXIT_CALLBACK : {}),
1960
+ },
1961
+ callback,
1962
+ );
1963
+ };
1964
+
1965
+ const SIGHUP_CALLBACK = {
1966
+ SIGHUP: (cb) => {
1967
+ process.on("SIGHUP", cb);
1968
+ return () => {
1969
+ process.removeListener("SIGHUP", cb);
1970
+ };
1971
+ },
1929
1972
  };
1930
1973
 
1931
- const applyAliases = ({ url, aliases }) => {
1932
- let aliasFullMatchResult;
1933
- const aliasMatchingKey = Object.keys(aliases).find((key) => {
1934
- const aliasMatchResult = applyPatternMatching({
1935
- pattern: key,
1936
- url,
1937
- });
1938
- if (aliasMatchResult.matched) {
1939
- aliasFullMatchResult = aliasMatchResult;
1940
- return true;
1941
- }
1942
- return false;
1943
- });
1944
- if (!aliasMatchingKey) {
1945
- return url;
1946
- }
1947
- const { matchGroups } = aliasFullMatchResult;
1948
- const alias = aliases[aliasMatchingKey];
1949
- const parts = alias.split("*");
1950
- const newUrl = parts.reduce((previous, value, index) => {
1951
- return `${previous}${value}${
1952
- index === parts.length - 1 ? "" : matchGroups[index]
1953
- }`;
1954
- }, "");
1955
- return newUrl;
1974
+ const SIGTERM_CALLBACK = {
1975
+ SIGTERM: (cb) => {
1976
+ process.on("SIGTERM", cb);
1977
+ return () => {
1978
+ process.removeListener("SIGTERM", cb);
1979
+ };
1980
+ },
1956
1981
  };
1957
1982
 
1958
- const urlChildMayMatch = ({ url, associations, predicate }) => {
1959
- assertUrlLike(url, "url");
1960
- // the function was meants to be used on url ending with '/'
1961
- if (!url.endsWith("/")) {
1962
- throw new Error(`url should end with /, got ${url}`);
1963
- }
1964
- if (typeof predicate !== "function") {
1965
- throw new TypeError(`predicate must be a function, got ${predicate}`);
1966
- }
1967
- const flatAssociations = asFlatAssociations(associations);
1968
- // for full match we must create an object to allow pattern to override previous ones
1969
- let fullMatchMeta = {};
1970
- let someFullMatch = false;
1971
- // for partial match, any meta satisfying predicate will be valid because
1972
- // we don't know for sure if pattern will still match for a file inside pathname
1973
- const partialMatchMetaArray = [];
1974
- Object.keys(flatAssociations).forEach((pattern) => {
1975
- const value = flatAssociations[pattern];
1976
- const matchResult = applyPatternMatching({
1977
- pattern,
1978
- url,
1979
- });
1980
- if (matchResult.matched) {
1981
- someFullMatch = true;
1982
- if (isPlainObject(fullMatchMeta) && isPlainObject(value)) {
1983
- fullMatchMeta = {
1984
- ...fullMatchMeta,
1985
- ...value,
1986
- };
1987
- } else {
1988
- fullMatchMeta = value;
1989
- }
1990
- } else if (someFullMatch === false && matchResult.urlIndex >= url.length) {
1991
- partialMatchMetaArray.push(value);
1992
- }
1993
- });
1994
- if (someFullMatch) {
1995
- return Boolean(predicate(fullMatchMeta));
1996
- }
1997
- return partialMatchMetaArray.some((partialMatchMeta) =>
1998
- predicate(partialMatchMeta),
1999
- );
1983
+ const BEFORE_EXIT_CALLBACK = {
1984
+ beforeExit: (cb) => {
1985
+ process.on("beforeExit", cb);
1986
+ return () => {
1987
+ process.removeListener("beforeExit", cb);
1988
+ };
1989
+ },
2000
1990
  };
2001
1991
 
2002
- const URL_META = {
2003
- resolveAssociations,
2004
- applyAssociations,
2005
- urlChildMayMatch,
2006
- applyPatternMatching,
2007
- applyAliases,
1992
+ const EXIT_CALLBACK = {
1993
+ exit: (cb) => {
1994
+ process.on("exit", cb);
1995
+ return () => {
1996
+ process.removeListener("exit", cb);
1997
+ };
1998
+ },
1999
+ };
2000
+
2001
+ const SIGINT_CALLBACK = {
2002
+ SIGINT: (cb) => {
2003
+ process.on("SIGINT", cb);
2004
+ return () => {
2005
+ process.removeListener("SIGINT", cb);
2006
+ };
2007
+ },
2008
2008
  };
2009
2009
 
2010
2010
  const readDirectory = async (url, { emfileMaxWait = 1000 } = {}) => {
@@ -3761,6 +3761,7 @@ const createLog = ({
3761
3761
  const { columns = 80, rows = 24 } = stream;
3762
3762
 
3763
3763
  const log = {
3764
+ destroyed: false,
3764
3765
  onVerticalOverflow: () => {},
3765
3766
  };
3766
3767
 
@@ -3825,6 +3826,9 @@ const createLog = ({
3825
3826
  };
3826
3827
 
3827
3828
  const write = (string, outputFromOutside = streamOutputSpy()) => {
3829
+ if (log.destroyed) {
3830
+ throw new Error("Cannot write log after destroy");
3831
+ }
3828
3832
  if (!lastOutput) {
3829
3833
  doWrite(string);
3830
3834
  return;
@@ -3845,6 +3849,7 @@ const createLog = ({
3845
3849
  };
3846
3850
 
3847
3851
  const destroy = () => {
3852
+ log.destroyed = true;
3848
3853
  if (streamOutputSpy) {
3849
3854
  streamOutputSpy(); // this uninstalls the spy
3850
3855
  streamOutputSpy = null;
@@ -8088,6 +8093,102 @@ const generateAccessControlHeaders = ({
8088
8093
  };
8089
8094
  };
8090
8095
 
8096
+ const WEB_URL_CONVERTER = {
8097
+ asWebUrl: (fileUrl, webServer) => {
8098
+ if (urlIsInsideOf(fileUrl, webServer.rootDirectoryUrl)) {
8099
+ return moveUrl({
8100
+ url: fileUrl,
8101
+ from: webServer.rootDirectoryUrl,
8102
+ to: `${webServer.origin}/`,
8103
+ });
8104
+ }
8105
+ const fsRootUrl = ensureWindowsDriveLetter("file:///", fileUrl);
8106
+ return `${webServer.origin}/@fs/${fileUrl.slice(fsRootUrl.length)}`;
8107
+ },
8108
+ asFileUrl: (webUrl, webServer) => {
8109
+ const { pathname, search } = new URL(webUrl);
8110
+ if (pathname.startsWith("/@fs/")) {
8111
+ const fsRootRelativeUrl = pathname.slice("/@fs/".length);
8112
+ return `file:///${fsRootRelativeUrl}${search}`;
8113
+ }
8114
+ return moveUrl({
8115
+ url: webUrl,
8116
+ from: `${webServer.origin}/`,
8117
+ to: webServer.rootDirectoryUrl,
8118
+ });
8119
+ },
8120
+ };
8121
+
8122
+ const watchSourceFiles = (
8123
+ sourceDirectoryUrl,
8124
+ callback,
8125
+ { sourceFileConfig = {}, keepProcessAlive, cooldownBetweenFileEvents },
8126
+ ) => {
8127
+ // Project should use a dedicated directory (usually "src/")
8128
+ // passed to the dev server via "sourceDirectoryUrl" param
8129
+ // In that case all files inside the source directory should be watched
8130
+ // But some project might want to use their root directory as source directory
8131
+ // In that case source directory might contain files matching "node_modules/*" or ".git/*"
8132
+ // And jsenv should not consider these as source files and watch them (to not hurt performances)
8133
+ const watchPatterns = {
8134
+ "**/*": true, // by default watch everything inside the source directory
8135
+ // line below is commented until @jsenv/url-meta fixes the fact that is matches
8136
+ // any file with an extension
8137
+ "**/.*": false, // file starting with a dot -> do not watch
8138
+ "**/.*/": false, // directory starting with a dot -> do not watch
8139
+ "**/node_modules/": false, // node_modules directory -> do not watch
8140
+ ...sourceFileConfig,
8141
+ };
8142
+ const stopWatchingSourceFiles = registerDirectoryLifecycle(
8143
+ sourceDirectoryUrl,
8144
+ {
8145
+ watchPatterns,
8146
+ cooldownBetweenFileEvents,
8147
+ keepProcessAlive,
8148
+ recursive: true,
8149
+ added: ({ relativeUrl }) => {
8150
+ callback({
8151
+ url: new URL(relativeUrl, sourceDirectoryUrl).href,
8152
+ event: "added",
8153
+ });
8154
+ },
8155
+ updated: ({ relativeUrl }) => {
8156
+ callback({
8157
+ url: new URL(relativeUrl, sourceDirectoryUrl).href,
8158
+ event: "modified",
8159
+ });
8160
+ },
8161
+ removed: ({ relativeUrl }) => {
8162
+ callback({
8163
+ url: new URL(relativeUrl, sourceDirectoryUrl).href,
8164
+ event: "removed",
8165
+ });
8166
+ },
8167
+ },
8168
+ );
8169
+ stopWatchingSourceFiles.watchPatterns = watchPatterns;
8170
+ return stopWatchingSourceFiles;
8171
+ };
8172
+
8173
+ const createEventEmitter = () => {
8174
+ const callbackSet = new Set();
8175
+ const on = (callback) => {
8176
+ callbackSet.add(callback);
8177
+ return () => {
8178
+ callbackSet.delete(callback);
8179
+ };
8180
+ };
8181
+ const off = (callback) => {
8182
+ callbackSet.delete(callback);
8183
+ };
8184
+ const emit = (...args) => {
8185
+ callbackSet.forEach((callback) => {
8186
+ callback(...args);
8187
+ });
8188
+ };
8189
+ return { on, off, emit };
8190
+ };
8191
+
8091
8192
  const lookupPackageDirectory = (currentUrl) => {
8092
8193
  if (currentUrl === "file:///") {
8093
8194
  return null;
@@ -11023,58 +11124,7 @@ const jsenvPluginTranspilation = ({
11023
11124
  : []),
11024
11125
 
11025
11126
  ...(css ? [jsenvPluginCssTranspilation()] : []),
11026
- ];
11027
- };
11028
-
11029
- const watchSourceFiles = (
11030
- sourceDirectoryUrl,
11031
- callback,
11032
- { sourceFileConfig = {}, keepProcessAlive, cooldownBetweenFileEvents },
11033
- ) => {
11034
- // Project should use a dedicated directory (usually "src/")
11035
- // passed to the dev server via "sourceDirectoryUrl" param
11036
- // In that case all files inside the source directory should be watched
11037
- // But some project might want to use their root directory as source directory
11038
- // In that case source directory might contain files matching "node_modules/*" or ".git/*"
11039
- // And jsenv should not consider these as source files and watch them (to not hurt performances)
11040
- const watchPatterns = {
11041
- "**/*": true, // by default watch everything inside the source directory
11042
- // line below is commented until @jsenv/url-meta fixes the fact that is matches
11043
- // any file with an extension
11044
- "**/.*": false, // file starting with a dot -> do not watch
11045
- "**/.*/": false, // directory starting with a dot -> do not watch
11046
- "**/node_modules/": false, // node_modules directory -> do not watch
11047
- ...sourceFileConfig,
11048
- };
11049
- const stopWatchingSourceFiles = registerDirectoryLifecycle(
11050
- sourceDirectoryUrl,
11051
- {
11052
- watchPatterns,
11053
- cooldownBetweenFileEvents,
11054
- keepProcessAlive,
11055
- recursive: true,
11056
- added: ({ relativeUrl }) => {
11057
- callback({
11058
- url: new URL(relativeUrl, sourceDirectoryUrl).href,
11059
- event: "added",
11060
- });
11061
- },
11062
- updated: ({ relativeUrl }) => {
11063
- callback({
11064
- url: new URL(relativeUrl, sourceDirectoryUrl).href,
11065
- event: "modified",
11066
- });
11067
- },
11068
- removed: ({ relativeUrl }) => {
11069
- callback({
11070
- url: new URL(relativeUrl, sourceDirectoryUrl).href,
11071
- event: "removed",
11072
- });
11073
- },
11074
- },
11075
- );
11076
- stopWatchingSourceFiles.watchPatterns = watchPatterns;
11077
- return stopWatchingSourceFiles;
11127
+ ];
11078
11128
  };
11079
11129
 
11080
11130
  const GRAPH_VISITOR = {};
@@ -11209,25 +11259,6 @@ GRAPH_VISITOR.forEachUrlInfoStronglyReferenced = (initialUrlInfo, callback) => {
11209
11259
  seen.clear();
11210
11260
  };
11211
11261
 
11212
- const createEventEmitter = () => {
11213
- const callbackSet = new Set();
11214
- const on = (callback) => {
11215
- callbackSet.add(callback);
11216
- return () => {
11217
- callbackSet.delete(callback);
11218
- };
11219
- };
11220
- const off = (callback) => {
11221
- callbackSet.delete(callback);
11222
- };
11223
- const emit = (...args) => {
11224
- callbackSet.forEach((callback) => {
11225
- callback(...args);
11226
- });
11227
- };
11228
- return { on, off, emit };
11229
- };
11230
-
11231
11262
  const urlSpecifierEncoding = {
11232
11263
  encode: (reference) => {
11233
11264
  const { generatedSpecifier } = reference;
@@ -14967,7 +14998,7 @@ const jsenvPluginDataUrlsAnalysis = () => {
14967
14998
  contentType: urlInfo.contentType,
14968
14999
  base64Flag: urlInfo.data.base64Flag,
14969
15000
  data: urlInfo.data.base64Flag
14970
- ? dataToBase64(urlInfo.content)
15001
+ ? dataToBase64$1(urlInfo.content)
14971
15002
  : String(urlInfo.content),
14972
15003
  });
14973
15004
  return specifier;
@@ -15001,8 +15032,9 @@ const jsenvPluginDataUrlsAnalysis = () => {
15001
15032
  data: urlData,
15002
15033
  } = DATA_URL.parse(urlInfo.url);
15003
15034
  urlInfo.data.base64Flag = base64Flag;
15035
+ const content = contentFromUrlData({ contentType, base64Flag, urlData });
15004
15036
  return {
15005
- content: contentFromUrlData({ contentType, base64Flag, urlData }),
15037
+ content,
15006
15038
  contentType,
15007
15039
  };
15008
15040
  },
@@ -15025,7 +15057,7 @@ const contentFromUrlData = ({ contentType, base64Flag, urlData }) => {
15025
15057
  const base64ToBuffer = (base64String) => Buffer.from(base64String, "base64");
15026
15058
  const base64ToString = (base64String) =>
15027
15059
  Buffer.from(base64String, "base64").toString("utf8");
15028
- const dataToBase64 = (data) => Buffer.from(data).toString("base64");
15060
+ const dataToBase64$1 = (data) => Buffer.from(data).toString("base64");
15029
15061
 
15030
15062
  // duplicated from @jsenv/log to avoid the dependency
15031
15063
  const createDetailedMessage = (message, details = {}) => {
@@ -18386,9 +18418,11 @@ const jsenvPluginInliningAsDataUrl = () => {
18386
18418
  return (async () => {
18387
18419
  await urlInfoInlined.cook();
18388
18420
  const base64Url = DATA_URL.stringify({
18389
- mediaType: urlInfoInlined.contentType,
18421
+ contentType: urlInfoInlined.contentType,
18390
18422
  base64Flag: true,
18391
- data: urlInfoInlined.content,
18423
+ data: urlInfoInlined.data.base64Flag
18424
+ ? urlInfoInlined.content
18425
+ : dataToBase64(urlInfoInlined.content),
18392
18426
  });
18393
18427
  return base64Url;
18394
18428
  })();
@@ -18403,6 +18437,7 @@ const jsenvPluginInliningAsDataUrl = () => {
18403
18437
  const contentAsBase64 = Buffer.from(
18404
18438
  withoutBase64ParamUrlInfo.content,
18405
18439
  ).toString("base64");
18440
+ urlInfo.data.base64Flag = true;
18406
18441
  return {
18407
18442
  originalContent: withoutBase64ParamUrlInfo.originalContent,
18408
18443
  content: contentAsBase64,
@@ -18412,6 +18447,8 @@ const jsenvPluginInliningAsDataUrl = () => {
18412
18447
  };
18413
18448
  };
18414
18449
 
18450
+ const dataToBase64 = (data) => Buffer.from(data).toString("base64");
18451
+
18415
18452
  const jsenvPluginInliningIntoHtml = () => {
18416
18453
  return {
18417
18454
  name: "jsenv:inlining_into_html",
@@ -22102,32 +22139,6 @@ build ${entryPointKeys.length} entry points`);
22102
22139
  return stopWatchingSourceFiles;
22103
22140
  };
22104
22141
 
22105
- const WEB_URL_CONVERTER = {
22106
- asWebUrl: (fileUrl, webServer) => {
22107
- if (urlIsInsideOf(fileUrl, webServer.rootDirectoryUrl)) {
22108
- return moveUrl({
22109
- url: fileUrl,
22110
- from: webServer.rootDirectoryUrl,
22111
- to: `${webServer.origin}/`,
22112
- });
22113
- }
22114
- const fsRootUrl = ensureWindowsDriveLetter("file:///", fileUrl);
22115
- return `${webServer.origin}/@fs/${fileUrl.slice(fsRootUrl.length)}`;
22116
- },
22117
- asFileUrl: (webUrl, webServer) => {
22118
- const { pathname, search } = new URL(webUrl);
22119
- if (pathname.startsWith("/@fs/")) {
22120
- const fsRootRelativeUrl = pathname.slice("/@fs/".length);
22121
- return `file:///${fsRootRelativeUrl}${search}`;
22122
- }
22123
- return moveUrl({
22124
- url: webUrl,
22125
- from: `${webServer.origin}/`,
22126
- to: webServer.rootDirectoryUrl,
22127
- });
22128
- },
22129
- };
22130
-
22131
22142
  /*
22132
22143
  * This plugin is very special because it is here
22133
22144
  * to provide "serverEvents" used by other plugins
@@ -22178,432 +22189,31 @@ const memoizeByFirstArgument = (compute) => {
22178
22189
  fnWithMemoization.forget = () => {
22179
22190
  urlCache.clear();
22180
22191
  };
22181
-
22182
- return fnWithMemoization;
22183
- };
22184
-
22185
- const requireFromJsenv = createRequire(import.meta.url);
22186
-
22187
- const parseUserAgentHeader = memoizeByFirstArgument((userAgent) => {
22188
- if (userAgent.includes("node-fetch/")) {
22189
- // it's not really node and conceptually we can't assume the node version
22190
- // but good enough for now
22191
- return {
22192
- runtimeName: "node",
22193
- runtimeVersion: process.version.slice(1),
22194
- };
22195
- }
22196
- const UA = requireFromJsenv("@financial-times/polyfill-useragent-normaliser");
22197
- const { ua } = new UA(userAgent);
22198
- const { family, major, minor, patch } = ua;
22199
- return {
22200
- runtimeName: family.toLowerCase(),
22201
- runtimeVersion:
22202
- family === "Other" ? "unknown" : `${major}.${minor}${patch}`,
22203
- };
22204
- });
22205
-
22206
- const createFileService = ({
22207
- signal,
22208
- logLevel,
22209
- serverStopCallbacks,
22210
- serverEventsDispatcher,
22211
- kitchenCache,
22212
- onKitchenCreated = () => {},
22213
-
22214
- sourceDirectoryUrl,
22215
- sourceMainFilePath,
22216
- ignore,
22217
- sourceFilesConfig,
22218
- runtimeCompat,
22219
-
22220
- plugins,
22221
- referenceAnalysis,
22222
- nodeEsmResolution,
22223
- magicExtensions,
22224
- magicDirectoryIndex,
22225
- supervisor,
22226
- injections,
22227
- transpilation,
22228
- clientAutoreload,
22229
- cacheControl,
22230
- ribbon,
22231
- sourcemaps,
22232
- sourcemapsSourcesContent,
22233
- outDirectoryUrl,
22234
- }) => {
22235
- if (clientAutoreload === true) {
22236
- clientAutoreload = {};
22237
- }
22238
- if (clientAutoreload === false) {
22239
- clientAutoreload = { enabled: false };
22240
- }
22241
- const clientFileChangeEventEmitter = createEventEmitter();
22242
- const clientFileDereferencedEventEmitter = createEventEmitter();
22243
-
22244
- clientAutoreload = {
22245
- enabled: true,
22246
- clientServerEventsConfig: {},
22247
- clientFileChangeEventEmitter,
22248
- clientFileDereferencedEventEmitter,
22249
- ...clientAutoreload,
22250
- };
22251
-
22252
- const stopWatchingSourceFiles = watchSourceFiles(
22253
- sourceDirectoryUrl,
22254
- (fileInfo) => {
22255
- clientFileChangeEventEmitter.emit(fileInfo);
22256
- },
22257
- {
22258
- sourceFilesConfig,
22259
- keepProcessAlive: false,
22260
- cooldownBetweenFileEvents: clientAutoreload.cooldownBetweenFileEvents,
22261
- },
22262
- );
22263
- serverStopCallbacks.push(stopWatchingSourceFiles);
22264
-
22265
- const getOrCreateKitchen = (request) => {
22266
- const { runtimeName, runtimeVersion } = parseUserAgentHeader(
22267
- request.headers["user-agent"] || "",
22268
- );
22269
- const runtimeId = `${runtimeName}@${runtimeVersion}`;
22270
- const existing = kitchenCache.get(runtimeId);
22271
- if (existing) {
22272
- return existing;
22273
- }
22274
- const watchAssociations = URL_META.resolveAssociations(
22275
- { watch: stopWatchingSourceFiles.watchPatterns },
22276
- sourceDirectoryUrl,
22277
- );
22278
- let kitchen;
22279
- clientFileChangeEventEmitter.on(({ url }) => {
22280
- const urlInfo = kitchen.graph.getUrlInfo(url);
22281
- if (urlInfo) {
22282
- urlInfo.onModified();
22283
- }
22284
- });
22285
- const clientRuntimeCompat = { [runtimeName]: runtimeVersion };
22286
-
22287
- kitchen = createKitchen({
22288
- name: runtimeId,
22289
- signal,
22290
- logLevel,
22291
- rootDirectoryUrl: sourceDirectoryUrl,
22292
- mainFilePath: sourceMainFilePath,
22293
- ignore,
22294
- dev: true,
22295
- runtimeCompat,
22296
- clientRuntimeCompat,
22297
- plugins: [
22298
- ...plugins,
22299
- ...getCorePlugins({
22300
- rootDirectoryUrl: sourceDirectoryUrl,
22301
- runtimeCompat,
22302
-
22303
- referenceAnalysis,
22304
- nodeEsmResolution,
22305
- magicExtensions,
22306
- magicDirectoryIndex,
22307
- supervisor,
22308
- injections,
22309
- transpilation,
22310
-
22311
- clientAutoreload,
22312
- cacheControl,
22313
- ribbon,
22314
- }),
22315
- ],
22316
- supervisor,
22317
- minification: false,
22318
- sourcemaps,
22319
- sourcemapsSourcesContent,
22320
- outDirectoryUrl: outDirectoryUrl
22321
- ? new URL(`${runtimeName}@${runtimeVersion}/`, outDirectoryUrl)
22322
- : undefined,
22323
- });
22324
- kitchen.graph.urlInfoCreatedEventEmitter.on((urlInfoCreated) => {
22325
- const { watch } = URL_META.applyAssociations({
22326
- url: urlInfoCreated.url,
22327
- associations: watchAssociations,
22328
- });
22329
- urlInfoCreated.isWatched = watch;
22330
- // when an url depends on many others, we check all these (like package.json)
22331
- urlInfoCreated.isValid = () => {
22332
- if (!urlInfoCreated.url.startsWith("file:")) {
22333
- return false;
22334
- }
22335
- if (urlInfoCreated.content === undefined) {
22336
- // urlInfo content is undefined when:
22337
- // - url info content never fetched
22338
- // - it is considered as modified because undelying file is watched and got saved
22339
- // - it is considered as modified because underlying file content
22340
- // was compared using etag and it has changed
22341
- return false;
22342
- }
22343
- if (!watch) {
22344
- // file is not watched, check the filesystem
22345
- let fileContentAsBuffer;
22346
- try {
22347
- fileContentAsBuffer = readFileSync(new URL(urlInfoCreated.url));
22348
- } catch (e) {
22349
- if (e.code === "ENOENT") {
22350
- urlInfoCreated.onModified();
22351
- return false;
22352
- }
22353
- return false;
22354
- }
22355
- const fileContentEtag = bufferToEtag$1(fileContentAsBuffer);
22356
- if (fileContentEtag !== urlInfoCreated.originalContentEtag) {
22357
- urlInfoCreated.onModified();
22358
- // restore content to be able to compare it again later
22359
- urlInfoCreated.kitchen.urlInfoTransformer.setContent(
22360
- urlInfoCreated,
22361
- String(fileContentAsBuffer),
22362
- {
22363
- contentEtag: fileContentEtag,
22364
- },
22365
- );
22366
- return false;
22367
- }
22368
- }
22369
- for (const implicitUrl of urlInfoCreated.implicitUrlSet) {
22370
- const implicitUrlInfo = urlInfoCreated.graph.getUrlInfo(implicitUrl);
22371
- if (implicitUrlInfo && !implicitUrlInfo.isValid()) {
22372
- return false;
22373
- }
22374
- }
22375
- return true;
22376
- };
22377
- });
22378
- kitchen.graph.urlInfoDereferencedEventEmitter.on(
22379
- (urlInfoDereferenced, lastReferenceFromOther) => {
22380
- clientFileDereferencedEventEmitter.emit(
22381
- urlInfoDereferenced,
22382
- lastReferenceFromOther,
22383
- );
22384
- },
22385
- );
22386
-
22387
- serverStopCallbacks.push(() => {
22388
- kitchen.pluginController.callHooks("destroy", kitchen.context);
22389
- });
22390
- {
22391
- const allServerEvents = {};
22392
- kitchen.pluginController.plugins.forEach((plugin) => {
22393
- const { serverEvents } = plugin;
22394
- if (serverEvents) {
22395
- Object.keys(serverEvents).forEach((serverEventName) => {
22396
- // we could throw on serverEvent name conflict
22397
- // we could throw if serverEvents[serverEventName] is not a function
22398
- allServerEvents[serverEventName] = serverEvents[serverEventName];
22399
- });
22400
- }
22401
- });
22402
- const serverEventNames = Object.keys(allServerEvents);
22403
- if (serverEventNames.length > 0) {
22404
- Object.keys(allServerEvents).forEach((serverEventName) => {
22405
- const serverEventInfo = {
22406
- ...kitchen.context,
22407
- sendServerEvent: (data) => {
22408
- serverEventsDispatcher.dispatch({
22409
- type: serverEventName,
22410
- data,
22411
- });
22412
- },
22413
- };
22414
- const serverEventInit = allServerEvents[serverEventName];
22415
- serverEventInit(serverEventInfo);
22416
- });
22417
- // "pushPlugin" so that event source client connection can be put as early as possible in html
22418
- kitchen.pluginController.pushPlugin(
22419
- jsenvPluginServerEventsClientInjection(
22420
- clientAutoreload.clientServerEventsConfig,
22421
- ),
22422
- );
22423
- }
22424
- }
22425
-
22426
- kitchenCache.set(runtimeId, kitchen);
22427
- onKitchenCreated(kitchen);
22428
- return kitchen;
22429
- };
22430
-
22431
- return async (request) => {
22432
- const kitchen = getOrCreateKitchen(request);
22433
- const serveHookInfo = {
22434
- ...kitchen.context,
22435
- request,
22436
- };
22437
- const responseFromPlugin =
22438
- await kitchen.pluginController.callAsyncHooksUntil(
22439
- "serve",
22440
- serveHookInfo,
22441
- );
22442
- if (responseFromPlugin) {
22443
- return responseFromPlugin;
22444
- }
22445
- const { referer } = request.headers;
22446
- const parentUrl = referer
22447
- ? WEB_URL_CONVERTER.asFileUrl(referer, {
22448
- origin: request.origin,
22449
- rootDirectoryUrl: sourceDirectoryUrl,
22450
- })
22451
- : sourceDirectoryUrl;
22452
- let reference = kitchen.graph.inferReference(request.resource, parentUrl);
22453
- if (!reference) {
22454
- reference =
22455
- kitchen.graph.rootUrlInfo.dependencies.createResolveAndFinalize({
22456
- trace: { message: parentUrl },
22457
- type: "http_request",
22458
- specifier: request.resource,
22459
- });
22460
- }
22461
- const urlInfo = reference.urlInfo;
22462
- const ifNoneMatch = request.headers["if-none-match"];
22463
- const urlInfoTargetedByCache = urlInfo.findParentIfInline() || urlInfo;
22464
-
22465
- try {
22466
- if (!urlInfo.error && ifNoneMatch) {
22467
- const [clientOriginalContentEtag, clientContentEtag] =
22468
- ifNoneMatch.split("_");
22469
- if (
22470
- urlInfoTargetedByCache.originalContentEtag ===
22471
- clientOriginalContentEtag &&
22472
- urlInfoTargetedByCache.contentEtag === clientContentEtag &&
22473
- urlInfoTargetedByCache.isValid()
22474
- ) {
22475
- const headers = {
22476
- "cache-control": `private,max-age=0,must-revalidate`,
22477
- };
22478
- Object.keys(urlInfo.headers).forEach((key) => {
22479
- if (key !== "content-length") {
22480
- headers[key] = urlInfo.headers[key];
22481
- }
22482
- });
22483
- return {
22484
- status: 304,
22485
- headers,
22486
- };
22487
- }
22488
- }
22489
-
22490
- await urlInfo.cook({ request, reference });
22491
- let { response } = urlInfo;
22492
- if (response) {
22493
- return response;
22494
- }
22495
- response = {
22496
- url: reference.url,
22497
- status: 200,
22498
- headers: {
22499
- // when we send eTag to the client the next request to the server
22500
- // will send etag in request headers.
22501
- // If they match jsenv bypass cooking and returns 304
22502
- // This must not happen when a plugin uses "no-store" or "no-cache" as it means
22503
- // plugin logic wants to happens for every request to this url
22504
- ...(urlInfo.headers["cache-control"] === "no-store" ||
22505
- urlInfo.headers["cache-control"] === "no-cache"
22506
- ? {}
22507
- : {
22508
- "cache-control": `private,max-age=0,must-revalidate`,
22509
- // it's safe to use "_" separator because etag is encoded with base64 (see https://stackoverflow.com/a/13195197)
22510
- "eTag": `${urlInfoTargetedByCache.originalContentEtag}_${urlInfoTargetedByCache.contentEtag}`,
22511
- }),
22512
- ...urlInfo.headers,
22513
- "content-type": urlInfo.contentType,
22514
- "content-length": urlInfo.contentLength,
22515
- },
22516
- body: urlInfo.content,
22517
- timing: urlInfo.timing,
22518
- };
22519
- const augmentResponseInfo = {
22520
- ...kitchen.context,
22521
- reference,
22522
- urlInfo,
22523
- };
22524
- kitchen.pluginController.callHooks(
22525
- "augmentResponse",
22526
- augmentResponseInfo,
22527
- (returnValue) => {
22528
- response = composeTwoResponses(response, returnValue);
22529
- },
22530
- );
22531
- return response;
22532
- } catch (e) {
22533
- urlInfo.error = e;
22534
- const originalError = e ? e.cause || e : e;
22535
- if (originalError.asResponse) {
22536
- return originalError.asResponse();
22537
- }
22538
- const code = originalError.code;
22539
- if (code === "PARSE_ERROR") {
22540
- // when possible let browser re-throw the syntax error
22541
- // it's not possible to do that when url info content is not available
22542
- // (happens for js_module_fallback for instance)
22543
- if (urlInfo.content !== undefined) {
22544
- kitchen.context.logger.error(`Error while handling ${request.url}:
22545
- ${originalError.reasonCode || originalError.code}
22546
- ${e.traceMessage}`);
22547
- return {
22548
- url: reference.url,
22549
- status: 200,
22550
- // reason becomes the http response statusText, it must not contain invalid chars
22551
- // https://github.com/nodejs/node/blob/0c27ca4bc9782d658afeaebcec85ec7b28f1cc35/lib/_http_common.js#L221
22552
- statusText: e.reason,
22553
- statusMessage: originalError.message,
22554
- headers: {
22555
- "content-type": urlInfo.contentType,
22556
- "content-length": urlInfo.contentLength,
22557
- "cache-control": "no-store",
22558
- },
22559
- body: urlInfo.content,
22560
- };
22561
- }
22562
- return {
22563
- url: reference.url,
22564
- status: 500,
22565
- statusText: e.reason,
22566
- statusMessage: originalError.message,
22567
- headers: {
22568
- "cache-control": "no-store",
22569
- },
22570
- body: urlInfo.content,
22571
- };
22572
- }
22573
- if (code === "DIRECTORY_REFERENCE_NOT_ALLOWED") {
22574
- return serveDirectory(reference.url, {
22575
- headers: {
22576
- accept: "text/html",
22577
- },
22578
- canReadDirectory: true,
22579
- rootDirectoryUrl: sourceDirectoryUrl,
22580
- });
22581
- }
22582
- if (code === "NOT_ALLOWED") {
22583
- return {
22584
- url: reference.url,
22585
- status: 403,
22586
- statusText: originalError.reason,
22587
- };
22588
- }
22589
- if (code === "NOT_FOUND") {
22590
- return {
22591
- url: reference.url,
22592
- status: 404,
22593
- statusText: originalError.reason,
22594
- statusMessage: originalError.message,
22595
- };
22596
- }
22597
- return {
22598
- url: reference.url,
22599
- status: 500,
22600
- statusText: e.reason,
22601
- statusMessage: e.stack,
22602
- };
22603
- }
22604
- };
22192
+
22193
+ return fnWithMemoization;
22605
22194
  };
22606
22195
 
22196
+ const requireFromJsenv = createRequire(import.meta.url);
22197
+
22198
+ const parseUserAgentHeader = memoizeByFirstArgument((userAgent) => {
22199
+ if (userAgent.includes("node-fetch/")) {
22200
+ // it's not really node and conceptually we can't assume the node version
22201
+ // but good enough for now
22202
+ return {
22203
+ runtimeName: "node",
22204
+ runtimeVersion: process.version.slice(1),
22205
+ };
22206
+ }
22207
+ const UA = requireFromJsenv("@financial-times/polyfill-useragent-normaliser");
22208
+ const { ua } = new UA(userAgent);
22209
+ const { family, major, minor, patch } = ua;
22210
+ return {
22211
+ runtimeName: family.toLowerCase(),
22212
+ runtimeVersion:
22213
+ family === "Other" ? "unknown" : `${major}.${minor}${patch}`,
22214
+ };
22215
+ });
22216
+
22607
22217
  /**
22608
22218
  * Start a server for source files:
22609
22219
  * - cook source files according to jsenv plugins
@@ -22689,6 +22299,16 @@ const startDevServer = async ({
22689
22299
  }
22690
22300
  }
22691
22301
 
22302
+ // params normalization
22303
+ {
22304
+ if (clientAutoreload === true) {
22305
+ clientAutoreload = {};
22306
+ }
22307
+ if (clientAutoreload === false) {
22308
+ clientAutoreload = { enabled: false };
22309
+ }
22310
+ }
22311
+
22692
22312
  const logger = createLogger({ logLevel });
22693
22313
  const operation = Abort.startOperation();
22694
22314
  operation.addAbortSignal(signal);
@@ -22712,48 +22332,38 @@ const startDevServer = async ({
22712
22332
  serverEventsDispatcher.destroy();
22713
22333
  });
22714
22334
  const kitchenCache = new Map();
22715
- const server = await startServer({
22716
- signal,
22717
- stopOnExit: false,
22718
- stopOnSIGINT: handleSIGINT,
22719
- stopOnInternalError: false,
22720
- keepProcessAlive: process.env.IMPORTED_BY_TEST_PLAN
22721
- ? false
22722
- : keepProcessAlive,
22723
- logLevel: serverLogLevel,
22724
- startLog: false,
22725
22335
 
22726
- https,
22727
- http2,
22728
- acceptAnyIp,
22729
- hostname,
22730
- port,
22731
- requestWaitingMs: 60_000,
22732
- services: [
22733
- {
22734
- handleRequest: (request) => {
22735
- if (request.headers["x-server-inspect"]) {
22736
- return { status: 200 };
22737
- }
22738
- if (request.pathname === "/__params__.json") {
22739
- const json = JSON.stringify({
22740
- sourceDirectoryUrl,
22741
- });
22742
- return {
22743
- status: 200,
22744
- headers: {
22745
- "content-type": "application/json",
22746
- "content-length": Buffer.byteLength(json),
22747
- },
22748
- body: json,
22749
- };
22750
- }
22751
- return null;
22752
- },
22753
- injectResponseHeaders: () => {
22754
- return { server: "jsenv_dev_server/1" };
22755
- },
22336
+ const finalServices = [];
22337
+ // x-server-inspect service
22338
+ {
22339
+ finalServices.push({
22340
+ handleRequest: (request) => {
22341
+ if (request.headers["x-server-inspect"]) {
22342
+ return { status: 200 };
22343
+ }
22344
+ if (request.pathname === "/__params__.json") {
22345
+ const json = JSON.stringify({
22346
+ sourceDirectoryUrl,
22347
+ });
22348
+ return {
22349
+ status: 200,
22350
+ headers: {
22351
+ "content-type": "application/json",
22352
+ "content-length": Buffer.byteLength(json),
22353
+ },
22354
+ body: json,
22355
+ };
22356
+ }
22357
+ return null;
22358
+ },
22359
+ injectResponseHeaders: () => {
22360
+ return { server: "jsenv_dev_server/1" };
22756
22361
  },
22362
+ });
22363
+ }
22364
+ // cors service
22365
+ {
22366
+ finalServices.push(
22757
22367
  jsenvServiceCORS({
22758
22368
  accessControlAllowRequestOrigin: true,
22759
22369
  accessControlAllowRequestMethod: true,
@@ -22765,86 +22375,453 @@ const startDevServer = async ({
22765
22375
  accessControlAllowCredentials: true,
22766
22376
  timingAllowOrigin: true,
22767
22377
  }),
22768
- ...services,
22378
+ );
22379
+ }
22380
+ // custom services
22381
+ {
22382
+ finalServices.push(...services);
22383
+ }
22384
+ // file_service
22385
+ {
22386
+ const clientFileChangeEventEmitter = createEventEmitter();
22387
+ const clientFileDereferencedEventEmitter = createEventEmitter();
22388
+ clientAutoreload = {
22389
+ enabled: true,
22390
+ clientServerEventsConfig: {},
22391
+ clientFileChangeEventEmitter,
22392
+ clientFileDereferencedEventEmitter,
22393
+ ...clientAutoreload,
22394
+ };
22395
+ const stopWatchingSourceFiles = watchSourceFiles(
22396
+ sourceDirectoryUrl,
22397
+ (fileInfo) => {
22398
+ clientFileChangeEventEmitter.emit(fileInfo);
22399
+ },
22769
22400
  {
22770
- name: "jsenv:omega_file_service",
22771
- handleRequest: createFileService({
22772
- signal,
22773
- logLevel,
22774
- serverStopCallbacks,
22775
- serverEventsDispatcher,
22776
- kitchenCache,
22777
- onKitchenCreated,
22778
-
22779
- sourceDirectoryUrl,
22780
- sourceMainFilePath,
22781
- ignore,
22782
- sourceFilesConfig,
22783
- runtimeCompat,
22401
+ sourceFilesConfig,
22402
+ keepProcessAlive: false,
22403
+ cooldownBetweenFileEvents: clientAutoreload.cooldownBetweenFileEvents,
22404
+ },
22405
+ );
22406
+ serverStopCallbacks.push(stopWatchingSourceFiles);
22784
22407
 
22785
- plugins,
22786
- referenceAnalysis,
22787
- nodeEsmResolution,
22788
- magicExtensions,
22789
- magicDirectoryIndex,
22790
- supervisor,
22791
- injections,
22792
- transpilation,
22793
- clientAutoreload,
22794
- cacheControl,
22795
- ribbon,
22796
- sourcemaps,
22797
- sourcemapsSourcesContent,
22798
- outDirectoryUrl,
22799
- }),
22800
- handleWebsocket: (websocket, { request }) => {
22801
- if (request.headers["sec-websocket-protocol"] === "jsenv") {
22802
- serverEventsDispatcher.addWebsocket(websocket, request);
22408
+ const getOrCreateKitchen = (request) => {
22409
+ const { runtimeName, runtimeVersion } = parseUserAgentHeader(
22410
+ request.headers["user-agent"] || "",
22411
+ );
22412
+ const runtimeId = `${runtimeName}@${runtimeVersion}`;
22413
+ const existing = kitchenCache.get(runtimeId);
22414
+ if (existing) {
22415
+ return existing;
22416
+ }
22417
+ const watchAssociations = URL_META.resolveAssociations(
22418
+ { watch: stopWatchingSourceFiles.watchPatterns },
22419
+ sourceDirectoryUrl,
22420
+ );
22421
+ let kitchen;
22422
+ clientFileChangeEventEmitter.on(({ url }) => {
22423
+ const urlInfo = kitchen.graph.getUrlInfo(url);
22424
+ if (urlInfo) {
22425
+ urlInfo.onModified();
22426
+ }
22427
+ });
22428
+ const clientRuntimeCompat = { [runtimeName]: runtimeVersion };
22429
+
22430
+ kitchen = createKitchen({
22431
+ name: runtimeId,
22432
+ signal,
22433
+ logLevel,
22434
+ rootDirectoryUrl: sourceDirectoryUrl,
22435
+ mainFilePath: sourceMainFilePath,
22436
+ ignore,
22437
+ dev: true,
22438
+ runtimeCompat,
22439
+ clientRuntimeCompat,
22440
+ plugins: [
22441
+ ...plugins,
22442
+ ...getCorePlugins({
22443
+ rootDirectoryUrl: sourceDirectoryUrl,
22444
+ runtimeCompat,
22445
+
22446
+ referenceAnalysis,
22447
+ nodeEsmResolution,
22448
+ magicExtensions,
22449
+ magicDirectoryIndex,
22450
+ supervisor,
22451
+ injections,
22452
+ transpilation,
22453
+
22454
+ clientAutoreload,
22455
+ cacheControl,
22456
+ ribbon,
22457
+ }),
22458
+ ],
22459
+ supervisor,
22460
+ minification: false,
22461
+ sourcemaps,
22462
+ sourcemapsSourcesContent,
22463
+ outDirectoryUrl: outDirectoryUrl
22464
+ ? new URL(`${runtimeName}@${runtimeVersion}/`, outDirectoryUrl)
22465
+ : undefined,
22466
+ });
22467
+ kitchen.graph.urlInfoCreatedEventEmitter.on((urlInfoCreated) => {
22468
+ const { watch } = URL_META.applyAssociations({
22469
+ url: urlInfoCreated.url,
22470
+ associations: watchAssociations,
22471
+ });
22472
+ urlInfoCreated.isWatched = watch;
22473
+ // when an url depends on many others, we check all these (like package.json)
22474
+ urlInfoCreated.isValid = () => {
22475
+ if (!urlInfoCreated.url.startsWith("file:")) {
22476
+ return false;
22477
+ }
22478
+ if (urlInfoCreated.content === undefined) {
22479
+ // urlInfo content is undefined when:
22480
+ // - url info content never fetched
22481
+ // - it is considered as modified because undelying file is watched and got saved
22482
+ // - it is considered as modified because underlying file content
22483
+ // was compared using etag and it has changed
22484
+ return false;
22485
+ }
22486
+ if (!watch) {
22487
+ // file is not watched, check the filesystem
22488
+ let fileContentAsBuffer;
22489
+ try {
22490
+ fileContentAsBuffer = readFileSync(new URL(urlInfoCreated.url));
22491
+ } catch (e) {
22492
+ if (e.code === "ENOENT") {
22493
+ urlInfoCreated.onModified();
22494
+ return false;
22495
+ }
22496
+ return false;
22497
+ }
22498
+ const fileContentEtag = bufferToEtag$1(fileContentAsBuffer);
22499
+ if (fileContentEtag !== urlInfoCreated.originalContentEtag) {
22500
+ urlInfoCreated.onModified();
22501
+ // restore content to be able to compare it again later
22502
+ urlInfoCreated.kitchen.urlInfoTransformer.setContent(
22503
+ urlInfoCreated,
22504
+ String(fileContentAsBuffer),
22505
+ {
22506
+ contentEtag: fileContentEtag,
22507
+ },
22508
+ );
22509
+ return false;
22510
+ }
22511
+ }
22512
+ for (const implicitUrl of urlInfoCreated.implicitUrlSet) {
22513
+ const implicitUrlInfo =
22514
+ urlInfoCreated.graph.getUrlInfo(implicitUrl);
22515
+ if (implicitUrlInfo && !implicitUrlInfo.isValid()) {
22516
+ return false;
22517
+ }
22803
22518
  }
22519
+ return true;
22520
+ };
22521
+ });
22522
+ kitchen.graph.urlInfoDereferencedEventEmitter.on(
22523
+ (urlInfoDereferenced, lastReferenceFromOther) => {
22524
+ clientFileDereferencedEventEmitter.emit(
22525
+ urlInfoDereferenced,
22526
+ lastReferenceFromOther,
22527
+ );
22804
22528
  },
22805
- },
22529
+ );
22530
+
22531
+ serverStopCallbacks.push(() => {
22532
+ kitchen.pluginController.callHooks("destroy", kitchen.context);
22533
+ });
22806
22534
  {
22807
- name: "jsenv:omega_error_handler",
22808
- handleError: (error) => {
22809
- const getResponseForError = () => {
22810
- if (error && error.asResponse) {
22811
- return error.asResponse();
22812
- }
22535
+ const allServerEvents = {};
22536
+ kitchen.pluginController.plugins.forEach((plugin) => {
22537
+ const { serverEvents } = plugin;
22538
+ if (serverEvents) {
22539
+ Object.keys(serverEvents).forEach((serverEventName) => {
22540
+ // we could throw on serverEvent name conflict
22541
+ // we could throw if serverEvents[serverEventName] is not a function
22542
+ allServerEvents[serverEventName] = serverEvents[serverEventName];
22543
+ });
22544
+ }
22545
+ });
22546
+ const serverEventNames = Object.keys(allServerEvents);
22547
+ if (serverEventNames.length > 0) {
22548
+ Object.keys(allServerEvents).forEach((serverEventName) => {
22549
+ const serverEventInfo = {
22550
+ ...kitchen.context,
22551
+ sendServerEvent: (data) => {
22552
+ serverEventsDispatcher.dispatch({
22553
+ type: serverEventName,
22554
+ data,
22555
+ });
22556
+ },
22557
+ };
22558
+ const serverEventInit = allServerEvents[serverEventName];
22559
+ serverEventInit(serverEventInfo);
22560
+ });
22561
+ // "pushPlugin" so that event source client connection can be put as early as possible in html
22562
+ kitchen.pluginController.pushPlugin(
22563
+ jsenvPluginServerEventsClientInjection(
22564
+ clientAutoreload.clientServerEventsConfig,
22565
+ ),
22566
+ );
22567
+ }
22568
+ }
22569
+
22570
+ kitchenCache.set(runtimeId, kitchen);
22571
+ onKitchenCreated(kitchen);
22572
+ return kitchen;
22573
+ };
22574
+
22575
+ finalServices.push({
22576
+ name: "jsenv:omega_file_service",
22577
+ handleRequest: async (request) => {
22578
+ const kitchen = getOrCreateKitchen(request);
22579
+ const serveHookInfo = {
22580
+ ...kitchen.context,
22581
+ request,
22582
+ };
22583
+ const responseFromPlugin =
22584
+ await kitchen.pluginController.callAsyncHooksUntil(
22585
+ "serve",
22586
+ serveHookInfo,
22587
+ );
22588
+ if (responseFromPlugin) {
22589
+ return responseFromPlugin;
22590
+ }
22591
+ const { referer } = request.headers;
22592
+ const parentUrl = referer
22593
+ ? WEB_URL_CONVERTER.asFileUrl(referer, {
22594
+ origin: request.origin,
22595
+ rootDirectoryUrl: sourceDirectoryUrl,
22596
+ })
22597
+ : sourceDirectoryUrl;
22598
+ let reference = kitchen.graph.inferReference(
22599
+ request.resource,
22600
+ parentUrl,
22601
+ );
22602
+ if (!reference) {
22603
+ reference =
22604
+ kitchen.graph.rootUrlInfo.dependencies.createResolveAndFinalize({
22605
+ trace: { message: parentUrl },
22606
+ type: "http_request",
22607
+ specifier: request.resource,
22608
+ });
22609
+ }
22610
+ const urlInfo = reference.urlInfo;
22611
+ const ifNoneMatch = request.headers["if-none-match"];
22612
+ const urlInfoTargetedByCache = urlInfo.findParentIfInline() || urlInfo;
22613
+
22614
+ try {
22615
+ if (!urlInfo.error && ifNoneMatch) {
22616
+ const [clientOriginalContentEtag, clientContentEtag] =
22617
+ ifNoneMatch.split("_");
22813
22618
  if (
22814
- error &&
22815
- error.statusText === "Unexpected directory operation"
22619
+ urlInfoTargetedByCache.originalContentEtag ===
22620
+ clientOriginalContentEtag &&
22621
+ urlInfoTargetedByCache.contentEtag === clientContentEtag &&
22622
+ urlInfoTargetedByCache.isValid()
22816
22623
  ) {
22624
+ const headers = {
22625
+ "cache-control": `private,max-age=0,must-revalidate`,
22626
+ };
22627
+ Object.keys(urlInfo.headers).forEach((key) => {
22628
+ if (key !== "content-length") {
22629
+ headers[key] = urlInfo.headers[key];
22630
+ }
22631
+ });
22817
22632
  return {
22818
- status: 403,
22633
+ status: 304,
22634
+ headers,
22819
22635
  };
22820
22636
  }
22821
- return convertFileSystemErrorToResponseProperties(error);
22822
- };
22823
- const response = getResponseForError();
22824
- if (!response) {
22825
- return null;
22826
22637
  }
22827
- const body = JSON.stringify({
22828
- status: response.status,
22829
- statusText: response.statusText,
22830
- headers: response.headers,
22831
- body: response.body,
22832
- });
22833
- return {
22638
+
22639
+ await urlInfo.cook({ request, reference });
22640
+ let { response } = urlInfo;
22641
+ if (response) {
22642
+ return response;
22643
+ }
22644
+ response = {
22645
+ url: reference.url,
22834
22646
  status: 200,
22835
22647
  headers: {
22836
- "content-type": "application/json",
22837
- "content-length": Buffer.byteLength(body),
22648
+ // when we send eTag to the client the next request to the server
22649
+ // will send etag in request headers.
22650
+ // If they match jsenv bypass cooking and returns 304
22651
+ // This must not happen when a plugin uses "no-store" or "no-cache" as it means
22652
+ // plugin logic wants to happens for every request to this url
22653
+ ...(urlInfo.headers["cache-control"] === "no-store" ||
22654
+ urlInfo.headers["cache-control"] === "no-cache"
22655
+ ? {}
22656
+ : {
22657
+ "cache-control": `private,max-age=0,must-revalidate`,
22658
+ // it's safe to use "_" separator because etag is encoded with base64 (see https://stackoverflow.com/a/13195197)
22659
+ "eTag": `${urlInfoTargetedByCache.originalContentEtag}_${urlInfoTargetedByCache.contentEtag}`,
22660
+ }),
22661
+ ...urlInfo.headers,
22662
+ "content-type": urlInfo.contentType,
22663
+ "content-length": urlInfo.contentLength,
22838
22664
  },
22839
- body,
22665
+ body: urlInfo.content,
22666
+ timing: urlInfo.timing,
22840
22667
  };
22841
- },
22668
+ const augmentResponseInfo = {
22669
+ ...kitchen.context,
22670
+ reference,
22671
+ urlInfo,
22672
+ };
22673
+ kitchen.pluginController.callHooks(
22674
+ "augmentResponse",
22675
+ augmentResponseInfo,
22676
+ (returnValue) => {
22677
+ response = composeTwoResponses(response, returnValue);
22678
+ },
22679
+ );
22680
+ return response;
22681
+ } catch (e) {
22682
+ urlInfo.error = e;
22683
+ const originalError = e ? e.cause || e : e;
22684
+ if (originalError.asResponse) {
22685
+ return originalError.asResponse();
22686
+ }
22687
+ const code = originalError.code;
22688
+ if (code === "PARSE_ERROR") {
22689
+ // when possible let browser re-throw the syntax error
22690
+ // it's not possible to do that when url info content is not available
22691
+ // (happens for js_module_fallback for instance)
22692
+ if (urlInfo.content !== undefined) {
22693
+ kitchen.context.logger.error(`Error while handling ${request.url}:
22694
+ ${originalError.reasonCode || originalError.code}
22695
+ ${e.traceMessage}`);
22696
+ return {
22697
+ url: reference.url,
22698
+ status: 200,
22699
+ // reason becomes the http response statusText, it must not contain invalid chars
22700
+ // https://github.com/nodejs/node/blob/0c27ca4bc9782d658afeaebcec85ec7b28f1cc35/lib/_http_common.js#L221
22701
+ statusText: e.reason,
22702
+ statusMessage: originalError.message,
22703
+ headers: {
22704
+ "content-type": urlInfo.contentType,
22705
+ "content-length": urlInfo.contentLength,
22706
+ "cache-control": "no-store",
22707
+ },
22708
+ body: urlInfo.content,
22709
+ };
22710
+ }
22711
+ return {
22712
+ url: reference.url,
22713
+ status: 500,
22714
+ statusText: e.reason,
22715
+ statusMessage: originalError.message,
22716
+ headers: {
22717
+ "cache-control": "no-store",
22718
+ },
22719
+ body: urlInfo.content,
22720
+ };
22721
+ }
22722
+ if (code === "DIRECTORY_REFERENCE_NOT_ALLOWED") {
22723
+ return serveDirectory(reference.url, {
22724
+ headers: {
22725
+ accept: "text/html",
22726
+ },
22727
+ canReadDirectory: true,
22728
+ rootDirectoryUrl: sourceDirectoryUrl,
22729
+ });
22730
+ }
22731
+ if (code === "NOT_ALLOWED") {
22732
+ return {
22733
+ url: reference.url,
22734
+ status: 403,
22735
+ statusText: originalError.reason,
22736
+ };
22737
+ }
22738
+ if (code === "NOT_FOUND") {
22739
+ return {
22740
+ url: reference.url,
22741
+ status: 404,
22742
+ statusText: originalError.reason,
22743
+ statusMessage: originalError.message,
22744
+ };
22745
+ }
22746
+ return {
22747
+ url: reference.url,
22748
+ status: 500,
22749
+ statusText: e.reason,
22750
+ statusMessage: e.stack,
22751
+ };
22752
+ }
22753
+ },
22754
+ handleWebsocket: (websocket, { request }) => {
22755
+ if (request.headers["sec-websocket-protocol"] === "jsenv") {
22756
+ serverEventsDispatcher.addWebsocket(websocket, request);
22757
+ }
22758
+ },
22759
+ });
22760
+ }
22761
+ // jsenv error handler service
22762
+ {
22763
+ finalServices.push({
22764
+ name: "jsenv:omega_error_handler",
22765
+ handleError: (error) => {
22766
+ const getResponseForError = () => {
22767
+ if (error && error.asResponse) {
22768
+ return error.asResponse();
22769
+ }
22770
+ if (error && error.statusText === "Unexpected directory operation") {
22771
+ return {
22772
+ status: 403,
22773
+ };
22774
+ }
22775
+ return convertFileSystemErrorToResponseProperties(error);
22776
+ };
22777
+ const response = getResponseForError();
22778
+ if (!response) {
22779
+ return null;
22780
+ }
22781
+ const body = JSON.stringify({
22782
+ status: response.status,
22783
+ statusText: response.statusText,
22784
+ headers: response.headers,
22785
+ body: response.body,
22786
+ });
22787
+ return {
22788
+ status: 200,
22789
+ headers: {
22790
+ "content-type": "application/json",
22791
+ "content-length": Buffer.byteLength(body),
22792
+ },
22793
+ body,
22794
+ };
22842
22795
  },
22843
- // default error handling
22796
+ });
22797
+ }
22798
+ // default error handler
22799
+ {
22800
+ finalServices.push(
22844
22801
  jsenvServiceErrorHandler({
22845
22802
  sendErrorDetails: true,
22846
22803
  }),
22847
- ],
22804
+ );
22805
+ }
22806
+
22807
+ const server = await startServer({
22808
+ signal,
22809
+ stopOnExit: false,
22810
+ stopOnSIGINT: handleSIGINT,
22811
+ stopOnInternalError: false,
22812
+ keepProcessAlive: process.env.IMPORTED_BY_TEST_PLAN
22813
+ ? false
22814
+ : keepProcessAlive,
22815
+ logLevel: serverLogLevel,
22816
+ startLog: false,
22817
+
22818
+ https,
22819
+ http2,
22820
+ acceptAnyIp,
22821
+ hostname,
22822
+ port,
22823
+ requestWaitingMs: 60_000,
22824
+ services: finalServices,
22848
22825
  });
22849
22826
  server.stoppedPromise.then((reason) => {
22850
22827
  onStop();