@jsenv/test 1.6.9 → 1.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- import { uneval, executeUsingDynamicImport } from "./js/execute_using_dynamic_import.js";
1
+ import { createException, executeUsingDynamicImport } from "./js/execute_using_dynamic_import.js";
2
2
  import "node:fs";
3
3
  import "node:inspector";
4
4
  import "node:perf_hooks";
@@ -28,20 +28,13 @@ const ACTION_RESPONSE_STATUS_FAILED = "action-failed";
28
28
  const ACTION_RESPONSE_STATUS_COMPLETED = "action-completed";
29
29
 
30
30
  const sendActionFailed = (error) => {
31
- if (error.hasOwnProperty("toString")) {
32
- delete error.toString;
33
- }
31
+ const exception = createException(error);
34
32
  sendToParent(
35
33
  ACTION_RESPONSE_EVENT_NAME,
36
- // process.send algorithm does not send non enumerable values
37
- // so use @jsenv/uneval
38
- uneval(
39
- {
40
- status: ACTION_RESPONSE_STATUS_FAILED,
41
- value: error,
42
- },
43
- { ignoreSymbols: true },
44
- ),
34
+ JSON.stringify({
35
+ status: ACTION_RESPONSE_STATUS_FAILED,
36
+ value: exception,
37
+ }),
45
38
  );
46
39
  };
47
40
 
@@ -1,5 +1,5 @@
1
1
  import { parentPort } from "node:worker_threads";
2
- import { uneval, executeUsingDynamicImport } from "./js/execute_using_dynamic_import.js";
2
+ import { createException, executeUsingDynamicImport } from "./js/execute_using_dynamic_import.js";
3
3
  import "node:fs";
4
4
  import "node:inspector";
5
5
  import "node:perf_hooks";
@@ -13,20 +13,13 @@ const ACTION_RESPONSE_STATUS_FAILED = "action-failed";
13
13
  const ACTION_RESPONSE_STATUS_COMPLETED = "action-completed";
14
14
 
15
15
  const sendActionFailed = (error) => {
16
- if (error.hasOwnProperty("toString")) {
17
- delete error.toString;
18
- }
16
+ const exception = createException(error);
19
17
  sendToParent(
20
18
  ACTION_RESPONSE_EVENT_NAME,
21
- // process.send algorithm does not send non enumerable values
22
- // so use @jsenv/uneval
23
- uneval(
24
- {
25
- status: ACTION_RESPONSE_STATUS_FAILED,
26
- value: error,
27
- },
28
- { ignoreSymbols: true },
29
- ),
19
+ JSON.stringify({
20
+ status: ACTION_RESPONSE_STATUS_FAILED,
21
+ value: exception,
22
+ }),
30
23
  );
31
24
  };
32
25
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/test",
3
- "version": "1.6.9",
3
+ "version": "1.7.1",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -37,14 +37,14 @@
37
37
  "@jsenv/ast": "5.1.4",
38
38
  "@jsenv/filesystem": "4.3.2",
39
39
  "@jsenv/log": "3.4.1",
40
- "@jsenv/plugin-supervisor": "1.3.4",
40
+ "@jsenv/plugin-supervisor": "1.3.5",
41
41
  "@jsenv/urls": "2.2.1",
42
42
  "@jsenv/url-meta": "8.1.0",
43
- "@jsenv/uneval": "1.6.0",
44
43
  "@jsenv/utils": "2.0.1",
45
- "v8-to-istanbul": "9.1.0",
44
+ "@jsenv/github-check-run": "0.1.0",
45
+ "v8-to-istanbul": "9.1.3",
46
46
  "istanbul-lib-coverage": "3.2.0",
47
- "istanbul-lib-instrument": "6.0.0",
47
+ "istanbul-lib-instrument": "6.0.1",
48
48
  "istanbul-lib-report": "3.0.1",
49
49
  "istanbul-reports": "3.1.6",
50
50
  "@c88/v8-coverage": "0.1.1",
@@ -0,0 +1,137 @@
1
+ /*
2
+ * Exception are objects used to wrap a value that is thrown
3
+ * Usually they wrap error but the value can be anything as
4
+ * throw "toto" can be used in JS
5
+ *
6
+ * - provide a common API to interact with value that can be thrown
7
+ * - enrich usual errors with a bit more information (line, column)
8
+ * - normalize error properties
9
+ * - exception.stackTrace: only the stack trace as string (no error.name or error.message)
10
+ * - error.stack: error.name + error.message + error.stackTrace
11
+ *
12
+ * It is used mostly internally by jsenv but can also be found
13
+ * value returned by "executeTestPlan" public export (for failed executions)
14
+ * and value returned by "execute" public export (when execution fails)
15
+ *
16
+ * This file is responsible to wrap error hapenning in Node.js runtime
17
+ * The browser part can be found in "supervisor.js"
18
+ */
19
+
20
+ export const createException = (reason) => {
21
+ const exception = {
22
+ isException: true,
23
+ isError: false,
24
+ name: "",
25
+ message: "",
26
+ stack: "",
27
+ stackTrace: "",
28
+ site: null,
29
+ };
30
+
31
+ if (reason === undefined) {
32
+ exception.message = "undefined";
33
+ return exception;
34
+ }
35
+ if (reason === undefined) {
36
+ exception.message = "undefined";
37
+ return exception;
38
+ }
39
+ if (typeof reason === "string") {
40
+ exception.message = reason;
41
+ return exception;
42
+ }
43
+ if (reason instanceof Error) {
44
+ exception.isError = true;
45
+ exception.name = reason.name;
46
+ exception.message = reason.message;
47
+ const stackInfo = getStackInfo(reason);
48
+ if (stackInfo) {
49
+ const { stackTrace, stackObject } = stackInfo;
50
+ exception.stackTrace = stackTrace;
51
+ const [firstCallSite] = stackObject;
52
+ writePropertiesFromCallSite(exception, firstCallSite);
53
+ }
54
+ exception.stack = reason.stack;
55
+ return exception;
56
+ }
57
+ if (typeof reason === "object") {
58
+ exception.code = reason.code;
59
+ exception.message = reason.message;
60
+ exception.stack = reason.stack;
61
+ return exception;
62
+ }
63
+ exception.message = JSON.stringify(reason);
64
+ return exception;
65
+ };
66
+
67
+ const getStackInfo = (reason) => {
68
+ if (reason instanceof Error) {
69
+ const { prepareStackTrace } = Error;
70
+ let stackTrace;
71
+ let stackObject;
72
+ Error.prepareStackTrace = (e, secondArg) => {
73
+ Error.prepareStackTrace = prepareStackTrace;
74
+ stackObject = secondArg;
75
+ stackTrace = secondArg.map((callSite) => ` at ${callSite}`).join("\n");
76
+ const name = e.name || "Error";
77
+ const message = e.message || "";
78
+ let stack = `${name}: ${message}`;
79
+ if (stackTrace) {
80
+ stack += `\n${stackTrace}`;
81
+ }
82
+ return stack;
83
+ };
84
+ // eslint-disable-next-line no-unused-expressions
85
+ reason.stack;
86
+ if (stackTrace === undefined) {
87
+ return null;
88
+ }
89
+ return {
90
+ stackTrace,
91
+ stackObject,
92
+ };
93
+ }
94
+ return null;
95
+ };
96
+ const writePropertiesFromCallSite = (exception, callSite) => {
97
+ const source = callSite.getFileName() || callSite.getScriptNameOrSourceURL();
98
+ if (source) {
99
+ const line = callSite.getLineNumber();
100
+ const column = callSite.getColumnNumber() - 1;
101
+ exception.site = {
102
+ url: source,
103
+ line,
104
+ column,
105
+ };
106
+ return;
107
+ }
108
+ // Code called using eval() needs special handling
109
+ if (callSite.isEval()) {
110
+ const origin = callSite.getEvalOrigin();
111
+ if (origin) {
112
+ writePropertiesFromEvalOrigin(exception, origin);
113
+ }
114
+ }
115
+ };
116
+ const writePropertiesFromEvalOrigin = (exception, origin) => {
117
+ // Most eval() calls are in this format
118
+ const topLevelEvalMatch = /^eval at ([^(]+) \((.+):(\d+):(\d+)\)$/.exec(
119
+ origin,
120
+ );
121
+ if (topLevelEvalMatch) {
122
+ const source = topLevelEvalMatch[2];
123
+ const line = Number(topLevelEvalMatch[3]);
124
+ const column = topLevelEvalMatch[4] - 1;
125
+ exception.site = {
126
+ url: source,
127
+ line,
128
+ column,
129
+ };
130
+ return;
131
+ }
132
+ // Parse nested eval() calls using recursion
133
+ const nestedEvalMatch = /^eval at ([^(]+) \((.+)\)$/.exec(origin);
134
+ if (nestedEvalMatch) {
135
+ writePropertiesFromEvalOrigin(exception, nestedEvalMatch[2]);
136
+ }
137
+ };
@@ -10,7 +10,7 @@ import { ensureEmptyDirectory, writeFileSync } from "@jsenv/filesystem";
10
10
  import { reportToCoverage } from "../coverage/report_to_coverage.js";
11
11
  import { run } from "./run.js";
12
12
  import { ensureGlobalGc } from "./gc.js";
13
- import { createExecutionLog, createSummaryLog } from "./logs_file_execution.js";
13
+ import { createExecutionLog, formatSummaryLog } from "./logs_file_execution.js";
14
14
 
15
15
  export const executeSteps = async (
16
16
  executionSteps,
@@ -177,6 +177,9 @@ export const executeSteps = async (
177
177
  executionResult: {
178
178
  status: "executing",
179
179
  },
180
+ counters,
181
+ timeEllapsed: Date.now() - startMs,
182
+ memoryHeap: memoryUsage().heapUsed,
180
183
  };
181
184
  if (typeof executionParams.allocatedMs === "function") {
182
185
  executionParams.allocatedMs =
@@ -188,17 +191,10 @@ export const executeSteps = async (
188
191
  log: executionLog,
189
192
  render: () => {
190
193
  return createExecutionLog(beforeExecutionInfo, {
191
- counters,
192
194
  logRuntime,
193
195
  logEachDuration,
194
- ...(logTimeUsage
195
- ? {
196
- timeEllapsed: Date.now() - startMs,
197
- }
198
- : {}),
199
- ...(logMemoryHeapUsage
200
- ? { memoryHeap: memoryUsage().heapUsed }
201
- : {}),
196
+ logTimeUsage,
197
+ logMemoryHeapUsage,
202
198
  });
203
199
  },
204
200
  });
@@ -249,7 +245,6 @@ export const executeSteps = async (
249
245
  endMs: Date.now(),
250
246
  executionResult,
251
247
  };
252
- afterExecutionCallback(afterExecutionInfo);
253
248
 
254
249
  if (executionResult.status === "aborted") {
255
250
  counters.aborted++;
@@ -266,7 +261,6 @@ export const executeSteps = async (
266
261
  if (executionLogsEnabled) {
267
262
  const log = createExecutionLog(afterExecutionInfo, {
268
263
  logShortForCompletedExecutions,
269
- counters,
270
264
  logRuntime,
271
265
  logEachDuration,
272
266
  ...(logTimeUsage
@@ -294,6 +288,7 @@ export const executeSteps = async (
294
288
  executionLog = createLog({ newLine: "" });
295
289
  }
296
290
  }
291
+ afterExecutionCallback(afterExecutionInfo);
297
292
  const isLastExecutionLog = executionIndex === executionSteps.length - 1;
298
293
  const cancelRemaining =
299
294
  failFast &&
@@ -321,7 +316,7 @@ export const executeSteps = async (
321
316
  duration: Date.now() - startMs,
322
317
  };
323
318
  if (logSummary) {
324
- const summaryLog = createSummaryLog(summary);
319
+ const summaryLog = formatSummaryLog(summary);
325
320
  rawOutput += stripAnsi(summaryLog);
326
321
  logger.info(summaryLog);
327
322
  }
@@ -1,4 +1,5 @@
1
1
  import { existsSync } from "node:fs";
2
+ import stripAnsi from "strip-ansi";
2
3
  import { Abort, raceProcessTeardownEvents } from "@jsenv/abort";
3
4
  import { URL_META } from "@jsenv/url-meta";
4
5
  import { urlToFileSystemPath, urlToRelativeUrl } from "@jsenv/urls";
@@ -7,7 +8,11 @@ import {
7
8
  assertAndNormalizeDirectoryUrl,
8
9
  assertAndNormalizeFileUrl,
9
10
  } from "@jsenv/filesystem";
10
- import { createLogger, createDetailedMessage } from "@jsenv/log";
11
+ import { createLogger, createDetailedMessage, UNICODE } from "@jsenv/log";
12
+ import {
13
+ startGithubCheckRun,
14
+ readGitHubWorkflowEnv,
15
+ } from "@jsenv/github-check-run";
11
16
 
12
17
  import { createTeardown } from "../helpers/teardown.js";
13
18
  import { generateCoverageJsonFile } from "../coverage/coverage_reporter_json_file.js";
@@ -16,6 +21,8 @@ import { generateCoverageTextLog } from "../coverage/coverage_reporter_text_log.
16
21
  import { assertAndNormalizeWebServer } from "./web_server_param.js";
17
22
  import { executionStepsFromTestPlan } from "./execution_steps.js";
18
23
  import { executeSteps } from "./execute_steps.js";
24
+ import { formatExecutionLabel, formatSummary } from "./logs_file_execution.js";
25
+ import { githubAnnotationFromError } from "./github_annotation_from_error.js";
19
26
 
20
27
  /**
21
28
  * Execute a list of files and log how it goes.
@@ -64,6 +71,15 @@ export const executeTestPlan = async ({
64
71
  cooldownBetweenExecutions = 0,
65
72
  gcBetweenExecutions = logMemoryHeapUsage,
66
73
 
74
+ githubCheckEnabled = Boolean(process.env.GITHUB_WORKFLOW),
75
+ githubCheckLogLevel,
76
+ githubCheckName = "Jsenv tests",
77
+ githubCheckTitle,
78
+ githubCheckToken,
79
+ githubCheckRepositoryOwner,
80
+ githubCheckRepositoryName,
81
+ githubCheckCommitSha,
82
+
67
83
  coverageEnabled = process.argv.includes("--coverage"),
68
84
  coverageConfig = {
69
85
  "file:///**/node_modules/": false,
@@ -178,6 +194,47 @@ export const executeTestPlan = async ({
178
194
  });
179
195
  }
180
196
 
197
+ if (githubCheckEnabled && !process.env.GITHUB_TOKEN) {
198
+ githubCheckEnabled = false;
199
+ const suggestions = [];
200
+ if (process.env.GITHUB_WORKFLOW_REF) {
201
+ const workflowFileRef = process.env.GITHUB_WORKFLOW_REF;
202
+ const refsIndex = workflowFileRef.indexOf("@refs/");
203
+ // see "GITHUB_WORKFLOW_REF" in https://docs.github.com/en/actions/learn-github-actions/variables#default-environment-variables
204
+ const workflowFilePath =
205
+ refsIndex === -1
206
+ ? workflowFileRef
207
+ : workflowFileRef.slice(0, refsIndex);
208
+ suggestions.push(`Pass github token in ${workflowFilePath} during job "${process.env.GITHUB_JOB}"
209
+ \`\`\`yml
210
+ env:
211
+ GITHUB_TOKEN: \${{ secrets.GITHUB_TOKEN }}
212
+ \`\`\``);
213
+ }
214
+ suggestions.push(`Disable github check with githubCheckEnabled: false`);
215
+ logger.warn(
216
+ `${
217
+ UNICODE.WARNING
218
+ } githubCheckEnabled but process.env.GITHUB_TOKEN is missing.
219
+ Integration with Github check API is disabled
220
+ To fix this warning:
221
+ - ${suggestions.join("\n- ")}
222
+ `,
223
+ );
224
+ }
225
+ if (githubCheckEnabled) {
226
+ const githubCheckInfoFromEnv = process.env.GITHUB_WORKFLOW
227
+ ? readGitHubWorkflowEnv()
228
+ : {};
229
+ githubCheckToken = githubCheckToken || githubCheckInfoFromEnv.githubToken;
230
+ githubCheckRepositoryOwner =
231
+ githubCheckRepositoryOwner || githubCheckInfoFromEnv.repositoryOwner;
232
+ githubCheckRepositoryName =
233
+ githubCheckRepositoryName || githubCheckInfoFromEnv.repositoryName;
234
+ githubCheckCommitSha =
235
+ githubCheckCommitSha || githubCheckInfoFromEnv.commitSha;
236
+ }
237
+
181
238
  if (coverageEnabled) {
182
239
  if (coverageMethodForBrowsers === undefined) {
183
240
  coverageMethodForBrowsers = someHasCoverageV8
@@ -311,6 +368,61 @@ export const executeTestPlan = async ({
311
368
  rootDirectoryUrl,
312
369
  });
313
370
  logger.debug(`${executionSteps.length} executions planned`);
371
+ let beforeExecutionCallback;
372
+ let afterExecutionCallback;
373
+ let afterAllExecutionCallback = () => {};
374
+ if (githubCheckEnabled) {
375
+ const githubCheckRun = await startGithubCheckRun({
376
+ logLevel: githubCheckLogLevel,
377
+ githubToken: githubCheckToken,
378
+ repositoryOwner: githubCheckRepositoryOwner,
379
+ repositoryName: githubCheckRepositoryName,
380
+ commitSha: githubCheckCommitSha,
381
+ checkName: githubCheckName,
382
+ checkTitle: `Tests executions`,
383
+ checkSummary: `${executionSteps.length} files will be executed`,
384
+ });
385
+ afterExecutionCallback = (afterExecutionInfo) => {
386
+ const summary = stripAnsi(
387
+ formatExecutionLabel(afterExecutionInfo, {
388
+ logTimeUsage,
389
+ logMemoryHeapUsage,
390
+ }),
391
+ );
392
+ const annotations = [];
393
+ const { executionResult } = afterExecutionInfo;
394
+ const { errors = [] } = executionResult;
395
+ for (const error of errors) {
396
+ const annotation = githubAnnotationFromError(error, {
397
+ rootDirectoryUrl,
398
+ executionInfo: afterExecutionInfo,
399
+ });
400
+ annotations.push(annotation);
401
+ }
402
+ githubCheckRun.progress({
403
+ title: "Jsenv test executions",
404
+ summary,
405
+ annotations,
406
+ });
407
+ };
408
+ afterAllExecutionCallback = async ({ testPlanSummary }) => {
409
+ const title = "Jsenv test results";
410
+ const summary = stripAnsi(formatSummary(testPlanSummary));
411
+ if (
412
+ testPlanSummary.counters.total !== testPlanSummary.counters.completed
413
+ ) {
414
+ await githubCheckRun.fail({
415
+ title,
416
+ summary,
417
+ });
418
+ return;
419
+ }
420
+ await githubCheckRun.pass({
421
+ title,
422
+ summary,
423
+ });
424
+ };
425
+ }
314
426
 
315
427
  const result = await executeSteps(executionSteps, {
316
428
  signal,
@@ -335,6 +447,14 @@ export const executeTestPlan = async ({
335
447
  cooldownBetweenExecutions,
336
448
  gcBetweenExecutions,
337
449
 
450
+ githubCheckEnabled,
451
+ githubCheckName,
452
+ githubCheckTitle,
453
+ githubCheckToken,
454
+ githubCheckRepositoryOwner,
455
+ githubCheckRepositoryName,
456
+ githubCheckCommitSha,
457
+
338
458
  coverageEnabled,
339
459
  coverageConfig,
340
460
  coverageIncludeMissing,
@@ -342,11 +462,14 @@ export const executeTestPlan = async ({
342
462
  coverageMethodForNodeJs,
343
463
  coverageV8ConflictWarning,
344
464
  coverageTempDirectoryUrl,
465
+
466
+ beforeExecutionCallback,
467
+ afterExecutionCallback,
345
468
  });
346
- if (
347
- updateProcessExitCode &&
348
- result.planSummary.counters.total !== result.planSummary.counters.completed
349
- ) {
469
+
470
+ const hasFailed =
471
+ result.planSummary.counters.total !== result.planSummary.counters.completed;
472
+ if (updateProcessExitCode && hasFailed) {
350
473
  process.exitCode = 1;
351
474
  }
352
475
  const planCoverage = result.planCoverage;
@@ -393,10 +516,13 @@ export const executeTestPlan = async ({
393
516
  }
394
517
  await Promise.all(promises);
395
518
  }
396
- return {
519
+
520
+ const returnValue = {
397
521
  testPlanAborted: result.aborted,
398
522
  testPlanSummary: result.planSummary,
399
523
  testPlanReport: result.planReport,
400
524
  testPlanCoverage: planCoverage,
401
525
  };
526
+ await afterAllExecutionCallback(returnValue);
527
+ return returnValue;
402
528
  };
@@ -0,0 +1,132 @@
1
+ import { urlIsInsideOf, urlToRelativeUrl } from "@jsenv/urls";
2
+
3
+ export const githubAnnotationFromError = (
4
+ error,
5
+ { rootDirectoryUrl, executionInfo },
6
+ ) => {
7
+ const annotation = {
8
+ annotation_level: "failure",
9
+ path: executionInfo.fileRelativeUrl,
10
+ start_line: 1,
11
+ end_line: 1,
12
+ title: `Error while executing ${executionInfo.fileRelativeUrl} on ${executionInfo.runtimeName}@${executionInfo.runtimeVersion}`,
13
+ };
14
+ const exception = asException(error, { rootDirectoryUrl });
15
+ if (typeof exception.site.line === "number") {
16
+ annotation.path = urlToRelativeUrl(exception.site.url, rootDirectoryUrl);
17
+ annotation.start_line = exception.site.line;
18
+ annotation.end_line = exception.site.line;
19
+ annotation.start_column = exception.site.column;
20
+ annotation.end_column = exception.site.column;
21
+ }
22
+ annotation.message = exception.stack;
23
+ return annotation;
24
+ };
25
+
26
+ const asException = (error, { rootDirectoryUrl }) => {
27
+ const exception = {
28
+ isException: true,
29
+ stack: "",
30
+ site: {},
31
+ };
32
+ if (error === null || error === undefined || typeof error === "string") {
33
+ exception.message = String(error);
34
+ return exception;
35
+ }
36
+ if (error) {
37
+ exception.message = error.message;
38
+ if (error.isException) {
39
+ Object.assign(exception, error);
40
+ exception.stack = replaceUrls(
41
+ error.stack,
42
+ ({ match, url, line = 1, column = 1 }) => {
43
+ if (urlIsInsideOf(url, rootDirectoryUrl)) {
44
+ const relativeUrl = urlToRelativeUrl(url, rootDirectoryUrl);
45
+ match = stringifyUrlSite({ url: relativeUrl, line, column });
46
+ }
47
+ return match;
48
+ },
49
+ );
50
+ } else if (error.stack) {
51
+ let firstSite = true;
52
+ exception.stack = replaceUrls(
53
+ error.stack,
54
+ ({ match, url, line = 1, column = 1 }) => {
55
+ if (urlIsInsideOf(url, rootDirectoryUrl)) {
56
+ const relativeUrl = urlToRelativeUrl(url, rootDirectoryUrl);
57
+ match = stringifyUrlSite({ url: relativeUrl, line, column });
58
+ }
59
+ if (firstSite) {
60
+ firstSite = false;
61
+ exception.site.url = url;
62
+ exception.site.line = line;
63
+ exception.site.column = column;
64
+ }
65
+ return match;
66
+ },
67
+ );
68
+ }
69
+
70
+ return exception;
71
+ }
72
+ exception.message = error;
73
+ return exception;
74
+ };
75
+
76
+ const stringifyUrlSite = ({ url, line, column }) => {
77
+ let string = url;
78
+ if (typeof line === "number") {
79
+ string += `:${line}`;
80
+ if (typeof column === "number") {
81
+ string += `:${column}`;
82
+ }
83
+ }
84
+ return string;
85
+ };
86
+
87
+ const replaceUrls = (source, replace) => {
88
+ return source.replace(/(?:https?|ftp|file):\/\/\S+/gm, (match) => {
89
+ let replacement = "";
90
+ const lastChar = match[match.length - 1];
91
+
92
+ // hotfix because our url regex sucks a bit
93
+ const endsWithSeparationChar = lastChar === ")" || lastChar === ":";
94
+ if (endsWithSeparationChar) {
95
+ match = match.slice(0, -1);
96
+ }
97
+
98
+ const lineAndColumnPattern = /:([0-9]+):([0-9]+)$/;
99
+ const lineAndColumMatch = match.match(lineAndColumnPattern);
100
+ if (lineAndColumMatch) {
101
+ const lineAndColumnString = lineAndColumMatch[0];
102
+ const lineString = lineAndColumMatch[1];
103
+ const columnString = lineAndColumMatch[2];
104
+ replacement = replace({
105
+ match: lineAndColumMatch,
106
+ url: match.slice(0, -lineAndColumnString.length),
107
+ line: lineString ? parseInt(lineString) : undefined,
108
+ column: columnString ? parseInt(columnString) : undefined,
109
+ });
110
+ } else {
111
+ const linePattern = /:([0-9]+)$/;
112
+ const lineMatch = match.match(linePattern);
113
+ if (lineMatch) {
114
+ const lineString = lineMatch[0];
115
+ replacement = replace({
116
+ match: lineMatch,
117
+ url: match.slice(0, -lineString.length),
118
+ line: lineString ? parseInt(lineString) : undefined,
119
+ });
120
+ } else {
121
+ replacement = replace({
122
+ match: lineMatch,
123
+ url: match,
124
+ });
125
+ }
126
+ }
127
+ if (endsWithSeparationChar) {
128
+ return `${replacement}${lastChar}`;
129
+ }
130
+ return replacement;
131
+ });
132
+ };