executable-stories-formatters 0.7.6 → 0.7.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // src/index.ts
2
2
  import "fs";
3
- import * as path6 from "path";
3
+ import * as path7 from "path";
4
4
  import * as fsPromises from "fs/promises";
5
5
 
6
6
  // src/converters/acl/status.ts
@@ -15002,8 +15002,8 @@ function extractDocAttachments(step) {
15002
15002
  }
15003
15003
  return attachments;
15004
15004
  }
15005
- function guessMediaType(path7) {
15006
- const lower = path7.toLowerCase();
15005
+ function guessMediaType(path8) {
15006
+ const lower = path8.toLowerCase();
15007
15007
  if (lower.endsWith(".png")) return "image/png";
15008
15008
  if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg";
15009
15009
  if (lower.endsWith(".gif")) return "image/gif";
@@ -15144,11 +15144,11 @@ var CucumberHtmlFormatter = class {
15144
15144
  for (const envelope of envelopes) {
15145
15145
  const accepted = htmlStream.write(envelope);
15146
15146
  if (!accepted) {
15147
- await new Promise((resolve5) => htmlStream.once("drain", resolve5));
15147
+ await new Promise((resolve7) => htmlStream.once("drain", resolve7));
15148
15148
  }
15149
15149
  }
15150
- await new Promise((resolve5, reject) => {
15151
- collector.on("finish", resolve5);
15150
+ await new Promise((resolve7, reject) => {
15151
+ collector.on("finish", resolve7);
15152
15152
  collector.on("error", reject);
15153
15153
  htmlStream.end();
15154
15154
  });
@@ -16150,6 +16150,177 @@ function replaceAssetRef(html, original, replacement) {
16150
16150
  return html;
16151
16151
  }
16152
16152
 
16153
+ // src/formatters/astro.ts
16154
+ var AstroFormatter = class _AstroFormatter {
16155
+ markdownFormatter;
16156
+ title;
16157
+ constructor(options = {}) {
16158
+ this.title = options.markdown?.title ?? "User Stories";
16159
+ this.markdownFormatter = new MarkdownFormatter({
16160
+ ...options.markdown,
16161
+ title: this.title,
16162
+ stepStyle: "gherkin",
16163
+ includeFrontMatter: false,
16164
+ includeSummaryTable: false,
16165
+ includeMetadata: false
16166
+ });
16167
+ }
16168
+ format(run) {
16169
+ const markdown = this.markdownFormatter.format(run);
16170
+ const body = markdown.replace(/^# .+\n\n?/, "");
16171
+ const frontmatter = this.buildFrontmatter(run);
16172
+ return `${frontmatter}
16173
+ ${body}`;
16174
+ }
16175
+ buildFrontmatter(run) {
16176
+ const badge = _AstroFormatter.computeBadge(run.testCases);
16177
+ const count = run.testCases.length;
16178
+ const description = `${count} scenario${count !== 1 ? "s" : ""} \u2014 ${badge.text.toLowerCase()}`;
16179
+ const lines = [
16180
+ "---",
16181
+ `title: ${this.title}`,
16182
+ `description: ${description}`,
16183
+ "sidebar:",
16184
+ " badge:",
16185
+ ` text: ${badge.text}`,
16186
+ ` variant: ${badge.variant}`,
16187
+ "---"
16188
+ ];
16189
+ return lines.join("\n");
16190
+ }
16191
+ static computeBadge(testCases) {
16192
+ const statuses = new Set(testCases.map((tc) => tc.status));
16193
+ if (statuses.has("failed")) return { text: "Failed", variant: "danger" };
16194
+ if (statuses.has("pending")) return { text: "Pending", variant: "caution" };
16195
+ if (statuses.has("skipped") && !statuses.has("passed")) return { text: "Skipped", variant: "caution" };
16196
+ return { text: "Passed", variant: "success" };
16197
+ }
16198
+ };
16199
+
16200
+ // src/formatters/astro-assets.ts
16201
+ import * as fs4 from "fs";
16202
+ import * as path4 from "path";
16203
+ var SKIP_PREFIXES = ["http://", "https://", "data:", "#"];
16204
+ function isLocalPath(src) {
16205
+ const trimmed = src.trim();
16206
+ if (SKIP_PREFIXES.some((prefix) => trimmed.startsWith(prefix))) {
16207
+ return false;
16208
+ }
16209
+ return !path4.posix.isAbsolute(trimmed) && !path4.win32.isAbsolute(trimmed);
16210
+ }
16211
+ function stripCodeContent(markdown) {
16212
+ let result = markdown.replace(/^[ \t]*(`{3,}|~{3,})[^\n]*\n[\s\S]*?^[ \t]*\1\s*$/gm, "");
16213
+ result = result.replace(/(`+)(?:(?!\1).)+\1/g, "");
16214
+ result = result.replace(/<pre\b[^>]*>[\s\S]*?<\/pre>/gi, "");
16215
+ result = result.replace(/<code\b[^>]*>[\s\S]*?<\/code>/gi, "");
16216
+ return result;
16217
+ }
16218
+ function scanMarkdownAssets(markdown) {
16219
+ const found = /* @__PURE__ */ new Set();
16220
+ const stripped = stripCodeContent(markdown);
16221
+ const mdImageRe = /!\[[^\]]*\]\(([^)"'\s]+)(?:\s+["'][^"']*["'])?\s*\)/g;
16222
+ let match;
16223
+ while ((match = mdImageRe.exec(stripped)) !== null) {
16224
+ const src = match[1].trim();
16225
+ if (isLocalPath(src)) {
16226
+ found.add(src);
16227
+ }
16228
+ }
16229
+ const htmlSrcRe = /<(?:img|source|video)[^>]+\bsrc=["']([^"']+)["'][^>]*>/gi;
16230
+ while ((match = htmlSrcRe.exec(stripped)) !== null) {
16231
+ const src = match[1].trim();
16232
+ if (isLocalPath(src)) {
16233
+ found.add(src);
16234
+ }
16235
+ }
16236
+ return Array.from(found);
16237
+ }
16238
+ function splitByCode(markdown) {
16239
+ const codeRe = /^[ \t]*(`{3,}|~{3,})[^\n]*\n[\s\S]*?^[ \t]*\1\s*$|<pre\b[^>]*>[\s\S]*?<\/pre>|<code\b[^>]*>[\s\S]*?<\/code>|(`+)(?:(?!\2).)+\2/gim;
16240
+ const segments = [];
16241
+ let lastIndex = 0;
16242
+ for (const match of markdown.matchAll(codeRe)) {
16243
+ if (match.index > lastIndex) {
16244
+ segments.push(markdown.slice(lastIndex, match.index));
16245
+ }
16246
+ segments.push(match[0]);
16247
+ lastIndex = match.index + match[0].length;
16248
+ }
16249
+ if (lastIndex < markdown.length) {
16250
+ segments.push(markdown.slice(lastIndex));
16251
+ }
16252
+ return segments;
16253
+ }
16254
+ function isCode(segment) {
16255
+ const trimmed = segment.trimStart();
16256
+ return trimmed.startsWith("`") || trimmed.startsWith("~") || trimmed.startsWith("<pre") || trimmed.startsWith("<code");
16257
+ }
16258
+ function rewriteProseSegment(prose, assetsBaseUrl, pathMap) {
16259
+ let result = prose;
16260
+ result = result.replace(
16261
+ /(!\[[^\]]*\]\()([^)"'\s]+)((?:\s+["'][^"']*["'])?\s*\))/g,
16262
+ (full, pre, src, post) => {
16263
+ const trimmed = src.trim();
16264
+ if (!isLocalPath(trimmed)) return full;
16265
+ if (pathMap) {
16266
+ const mapped = pathMap.get(trimmed);
16267
+ if (mapped === void 0) return full;
16268
+ return `${pre}${assetsBaseUrl}/${mapped}${post}`;
16269
+ }
16270
+ return `${pre}${assetsBaseUrl}/${trimmed}${post}`;
16271
+ }
16272
+ );
16273
+ result = result.replace(
16274
+ /(<(?:img|source|video)[^>]+\bsrc=["'])([^"']+)(["'][^>]*>)/gi,
16275
+ (full, pre, src, post) => {
16276
+ const trimmed = src.trim();
16277
+ if (!isLocalPath(trimmed)) return full;
16278
+ if (pathMap) {
16279
+ const mapped = pathMap.get(trimmed);
16280
+ if (mapped === void 0) return full;
16281
+ return `${pre}${assetsBaseUrl}/${mapped}${post}`;
16282
+ }
16283
+ return `${pre}${assetsBaseUrl}/${trimmed}${post}`;
16284
+ }
16285
+ );
16286
+ return result;
16287
+ }
16288
+ function rewriteAssetPaths(markdown, assetsBaseUrl, pathMap) {
16289
+ return splitByCode(markdown).map((seg) => isCode(seg) ? seg : rewriteProseSegment(seg, assetsBaseUrl, pathMap)).join("");
16290
+ }
16291
+ function copyMarkdownAssets(options) {
16292
+ const {
16293
+ markdown,
16294
+ markdownDir,
16295
+ assetsDir,
16296
+ assetsBaseUrl,
16297
+ allowMissing = false
16298
+ } = options;
16299
+ const refs = scanMarkdownAssets(markdown);
16300
+ const pathMap = /* @__PURE__ */ new Map();
16301
+ const missing = [];
16302
+ for (const ref of refs) {
16303
+ const absPath = path4.resolve(markdownDir, ref);
16304
+ if (!fs4.existsSync(absPath)) {
16305
+ if (!allowMissing) {
16306
+ throw new Error(`Asset not found: ${absPath}`);
16307
+ }
16308
+ missing.push(ref);
16309
+ continue;
16310
+ }
16311
+ const relativeCopied = copyAsset(absPath, assetsDir);
16312
+ const fileName = relativeCopied.replace(/^assets\//, "");
16313
+ pathMap.set(ref, fileName);
16314
+ }
16315
+ const rewritten = rewriteAssetPaths(markdown, assetsBaseUrl, pathMap);
16316
+ return {
16317
+ markdown: rewritten,
16318
+ copiedCount: pathMap.size,
16319
+ missingCount: missing.length,
16320
+ missing
16321
+ };
16322
+ }
16323
+
16153
16324
  // src/converters/adapters/jest.ts
16154
16325
  function mapJestStatus(status) {
16155
16326
  switch (status) {
@@ -16917,27 +17088,27 @@ function pickleStepArgumentToDocs(ps) {
16917
17088
  }
16918
17089
 
16919
17090
  // src/utils/git-info.ts
16920
- import * as fs4 from "fs";
16921
- import * as path4 from "path";
17091
+ import * as fs5 from "fs";
17092
+ import * as path5 from "path";
16922
17093
  function readGitSha(cwd = process.cwd()) {
16923
17094
  const envSha = process.env.GITHUB_SHA || process.env.GIT_COMMIT || process.env.CI_COMMIT_SHA;
16924
17095
  if (envSha) return envSha;
16925
17096
  const gitDir = findGitDir(cwd);
16926
17097
  if (!gitDir) return void 0;
16927
17098
  try {
16928
- const headPath = path4.join(gitDir, "HEAD");
16929
- const head = fs4.readFileSync(headPath, "utf8").trim();
17099
+ const headPath = path5.join(gitDir, "HEAD");
17100
+ const head = fs5.readFileSync(headPath, "utf8").trim();
16930
17101
  if (!head.startsWith("ref:")) {
16931
17102
  return head;
16932
17103
  }
16933
17104
  const refPath = head.replace("ref:", "").trim();
16934
- const refFile = path4.join(gitDir, refPath);
16935
- if (fs4.existsSync(refFile)) {
16936
- return fs4.readFileSync(refFile, "utf8").trim();
17105
+ const refFile = path5.join(gitDir, refPath);
17106
+ if (fs5.existsSync(refFile)) {
17107
+ return fs5.readFileSync(refFile, "utf8").trim();
16937
17108
  }
16938
- const packedRefs = path4.join(gitDir, "packed-refs");
16939
- if (fs4.existsSync(packedRefs)) {
16940
- const content = fs4.readFileSync(packedRefs, "utf8");
17109
+ const packedRefs = path5.join(gitDir, "packed-refs");
17110
+ if (fs5.existsSync(packedRefs)) {
17111
+ const content = fs5.readFileSync(packedRefs, "utf8");
16941
17112
  for (const line of content.split("\n")) {
16942
17113
  if (!line || line.startsWith("#") || line.startsWith("^")) continue;
16943
17114
  const [sha, ref] = line.split(" ");
@@ -16952,19 +17123,19 @@ function readGitSha(cwd = process.cwd()) {
16952
17123
  function findGitDir(start) {
16953
17124
  let current = start;
16954
17125
  while (true) {
16955
- const candidate = path4.join(current, ".git");
16956
- if (fs4.existsSync(candidate)) {
16957
- const stat = fs4.statSync(candidate);
17126
+ const candidate = path5.join(current, ".git");
17127
+ if (fs5.existsSync(candidate)) {
17128
+ const stat = fs5.statSync(candidate);
16958
17129
  if (stat.isFile()) {
16959
- const content = fs4.readFileSync(candidate, "utf8").trim();
17130
+ const content = fs5.readFileSync(candidate, "utf8").trim();
16960
17131
  const match = content.match(/^gitdir: (.+)$/);
16961
17132
  if (match) {
16962
- return path4.resolve(current, match[1]);
17133
+ return path5.resolve(current, match[1]);
16963
17134
  }
16964
17135
  }
16965
17136
  return candidate;
16966
17137
  }
16967
- const parent = path4.dirname(current);
17138
+ const parent = path5.dirname(current);
16968
17139
  if (parent === current) return void 0;
16969
17140
  current = parent;
16970
17141
  }
@@ -16975,8 +17146,8 @@ function readBranchName(cwd = process.cwd()) {
16975
17146
  const gitDir = findGitDir(cwd);
16976
17147
  if (!gitDir) return void 0;
16977
17148
  try {
16978
- const headPath = path4.join(gitDir, "HEAD");
16979
- const head = fs4.readFileSync(headPath, "utf8").trim();
17149
+ const headPath = path5.join(gitDir, "HEAD");
17150
+ const head = fs5.readFileSync(headPath, "utf8").trim();
16980
17151
  if (head.startsWith("ref:")) {
16981
17152
  const refPath = head.replace("ref:", "").trim();
16982
17153
  const match = refPath.match(/^refs\/heads\/(.+)$/);
@@ -17013,8 +17184,8 @@ function nanosecondsToMs(ns) {
17013
17184
  }
17014
17185
 
17015
17186
  // src/utils/metadata.ts
17016
- import * as fs5 from "fs";
17017
- import * as path5 from "path";
17187
+ import * as fs6 from "fs";
17188
+ import * as path6 from "path";
17018
17189
  var versionCache = /* @__PURE__ */ new Map();
17019
17190
  function readPackageVersion(root) {
17020
17191
  if (versionCache.has(root)) {
@@ -17025,18 +17196,18 @@ function readPackageVersion(root) {
17025
17196
  return version;
17026
17197
  }
17027
17198
  function findPackageVersion(startDir) {
17028
- let current = path5.resolve(startDir);
17199
+ let current = path6.resolve(startDir);
17029
17200
  while (true) {
17030
- const pkgPath = path5.join(current, "package.json");
17201
+ const pkgPath = path6.join(current, "package.json");
17031
17202
  try {
17032
- if (fs5.existsSync(pkgPath)) {
17033
- const raw = fs5.readFileSync(pkgPath, "utf8");
17203
+ if (fs6.existsSync(pkgPath)) {
17204
+ const raw = fs6.readFileSync(pkgPath, "utf8");
17034
17205
  const parsed = JSON.parse(raw);
17035
17206
  return parsed.version;
17036
17207
  }
17037
17208
  } catch {
17038
17209
  }
17039
- const parent = path5.dirname(current);
17210
+ const parent = path6.dirname(current);
17040
17211
  if (parent === current) {
17041
17212
  return void 0;
17042
17213
  }
@@ -17961,6 +18132,7 @@ function listScenarios(args, _deps) {
17961
18132
 
17962
18133
  // src/index.ts
17963
18134
  var FORMAT_EXTENSIONS = {
18135
+ astro: ".md",
17964
18136
  markdown: ".md",
17965
18137
  html: ".html",
17966
18138
  "cucumber-html": ".cucumber.html",
@@ -17993,11 +18165,11 @@ function computeOutputPath(sourceFile, format, mode, colocatedStyle, baseOutputD
17993
18165
  const ext = FORMAT_EXTENSIONS[format];
17994
18166
  const effectiveName = outputName + (outputNameSuffix ?? "");
17995
18167
  if (mode === "aggregated") {
17996
- return toPosix(path6.join(baseOutputDir, `${effectiveName}${ext}`));
18168
+ return toPosix(path7.join(baseOutputDir, `${effectiveName}${ext}`));
17997
18169
  }
17998
18170
  const normalizedSource = toPosix(sourceFile);
17999
- const dirOfSource = path6.posix.dirname(normalizedSource);
18000
- let baseName = path6.posix.basename(normalizedSource);
18171
+ const dirOfSource = path7.posix.dirname(normalizedSource);
18172
+ let baseName = path7.posix.basename(normalizedSource);
18001
18173
  for (const testExt of TEST_EXTENSIONS) {
18002
18174
  if (baseName.endsWith(testExt)) {
18003
18175
  baseName = baseName.slice(0, -testExt.length);
@@ -18006,9 +18178,9 @@ function computeOutputPath(sourceFile, format, mode, colocatedStyle, baseOutputD
18006
18178
  }
18007
18179
  const fileName = `${baseName}.${effectiveName}${ext}`;
18008
18180
  if (colocatedStyle === "adjacent") {
18009
- return toPosix(path6.posix.join(dirOfSource, fileName));
18181
+ return toPosix(path7.posix.join(dirOfSource, fileName));
18010
18182
  }
18011
- return toPosix(path6.posix.join(baseOutputDir, dirOfSource, fileName));
18183
+ return toPosix(path7.posix.join(baseOutputDir, dirOfSource, fileName));
18012
18184
  }
18013
18185
  function groupTestCasesByOutput(testCases, format, options, logger, outputNameSuffix) {
18014
18186
  const groups = /* @__PURE__ */ new Map();
@@ -18134,6 +18306,24 @@ var ReportGenerator = class {
18134
18306
  includeSourceLinks: options.markdown?.includeSourceLinks ?? true,
18135
18307
  customRenderers: options.markdown?.customRenderers
18136
18308
  },
18309
+ astro: {
18310
+ assetsDir: options.astro?.assetsDir ?? "public/stories/assets",
18311
+ assetsBaseUrl: options.astro?.assetsBaseUrl ?? "/stories/assets",
18312
+ markdown: {
18313
+ title: options.astro?.markdown?.title ?? "User Stories",
18314
+ includeStatusIcons: options.astro?.markdown?.includeStatusIcons ?? true,
18315
+ includeErrors: options.astro?.markdown?.includeErrors ?? true,
18316
+ scenarioHeadingLevel: options.astro?.markdown?.scenarioHeadingLevel ?? 3,
18317
+ groupBy: options.astro?.markdown?.groupBy ?? "file",
18318
+ sortScenarios: options.astro?.markdown?.sortScenarios ?? "source",
18319
+ suiteSeparator: options.astro?.markdown?.suiteSeparator ?? " - ",
18320
+ includeSourceLinks: options.astro?.markdown?.includeSourceLinks ?? true,
18321
+ permalinkBaseUrl: options.astro?.markdown?.permalinkBaseUrl,
18322
+ ticketUrlTemplate: options.astro?.markdown?.ticketUrlTemplate,
18323
+ traceUrlTemplate: options.astro?.markdown?.traceUrlTemplate,
18324
+ customRenderers: options.astro?.markdown?.customRenderers
18325
+ }
18326
+ },
18137
18327
  assetMode: options.assetMode ?? "none",
18138
18328
  allowMissingAssets: options.allowMissingAssets ?? false
18139
18329
  };
@@ -18171,6 +18361,24 @@ var ReportGenerator = class {
18171
18361
  });
18172
18362
  }
18173
18363
  }
18364
+ const astroPaths = results.get("astro");
18365
+ if (astroPaths) {
18366
+ for (const mdPath of astroPaths) {
18367
+ const content = await fsPromises.readFile(mdPath, "utf8");
18368
+ const mdDir = path7.dirname(mdPath);
18369
+ const assetsDir = path7.resolve(this.options.astro.assetsDir);
18370
+ const result = copyMarkdownAssets({
18371
+ markdown: content,
18372
+ markdownDir: mdDir,
18373
+ assetsDir,
18374
+ assetsBaseUrl: this.options.astro.assetsBaseUrl,
18375
+ allowMissing: this.options.allowMissingAssets
18376
+ });
18377
+ if (result.copiedCount > 0 || result.missingCount > 0) {
18378
+ await this.deps.writeFile(mdPath, result.markdown);
18379
+ }
18380
+ }
18381
+ }
18174
18382
  }
18175
18383
  return results;
18176
18384
  }
@@ -18189,9 +18397,9 @@ var ReportGenerator = class {
18189
18397
  if (groups.size === 0 && this.options.output.mode === "aggregated") {
18190
18398
  const ext = FORMAT_EXTENSIONS[format];
18191
18399
  const effectiveName = this.options.outputName + (outputNameSuffix ?? "");
18192
- const outputPath = toPosix(path6.join(this.options.outputDir, `${effectiveName}${ext}`));
18400
+ const outputPath = toPosix(path7.join(this.options.outputDir, `${effectiveName}${ext}`));
18193
18401
  const content = await this.formatContent(run, format);
18194
- const dir = path6.dirname(outputPath);
18402
+ const dir = path7.dirname(outputPath);
18195
18403
  await fsPromises.mkdir(dir, { recursive: true });
18196
18404
  await this.deps.writeFile(outputPath, content);
18197
18405
  return [outputPath];
@@ -18203,7 +18411,7 @@ var ReportGenerator = class {
18203
18411
  testCases
18204
18412
  };
18205
18413
  const content = await this.formatContent(groupRun, format);
18206
- const dir = path6.dirname(outputPath);
18414
+ const dir = path7.dirname(outputPath);
18207
18415
  await fsPromises.mkdir(dir, { recursive: true });
18208
18416
  await this.deps.writeFile(outputPath, content);
18209
18417
  writtenPaths.push(outputPath);
@@ -18266,6 +18474,13 @@ var ReportGenerator = class {
18266
18474
  });
18267
18475
  return formatter.formatToString(run);
18268
18476
  }
18477
+ case "astro": {
18478
+ const formatter = new AstroFormatter({
18479
+ assetsBaseUrl: this.options.astro.assetsBaseUrl,
18480
+ markdown: this.options.astro.markdown
18481
+ });
18482
+ return formatter.format(run);
18483
+ }
18269
18484
  case "markdown": {
18270
18485
  const formatter = new MarkdownFormatter({
18271
18486
  title: this.options.markdown.title,
@@ -18303,7 +18518,7 @@ async function generateRunComparison(args) {
18303
18518
  await fsPromises.mkdir(outputDir, { recursive: true });
18304
18519
  for (const format of args.formats) {
18305
18520
  const ext = format === "html" ? ".html" : ".md";
18306
- const outputPath = toPosix(path6.join(outputDir, `${outputName}${ext}`));
18521
+ const outputPath = toPosix(path7.join(outputDir, `${outputName}${ext}`));
18307
18522
  const content = format === "html" ? new RunDiffHtmlFormatter({ title: args.title }).format(diff) : new RunDiffMarkdownFormatter({ title: args.title }).format(diff);
18308
18523
  await fsPromises.writeFile(outputPath, content, "utf8");
18309
18524
  files.push(outputPath);
@@ -18323,6 +18538,7 @@ function normalizePlaywrightResults(testResults, adapterOptions, canonicalizeOpt
18323
18538
  return canonicalizeRun(raw, canonicalizeOptions);
18324
18539
  }
18325
18540
  export {
18541
+ AstroFormatter,
18326
18542
  CucumberHtmlFormatter,
18327
18543
  CucumberJsonFormatter,
18328
18544
  CucumberMessagesFormatter,
@@ -18346,6 +18562,7 @@ export {
18346
18562
  canonicalizeRun,
18347
18563
  clearVersionCache,
18348
18564
  computeTestMetrics,
18565
+ copyMarkdownAssets,
18349
18566
  createPrCommentSummary,
18350
18567
  createReportGenerator,
18351
18568
  deriveStepResults,
@@ -18378,6 +18595,7 @@ export {
18378
18595
  resolveAttachments,
18379
18596
  resolveTheme,
18380
18597
  resolveTraceUrl,
18598
+ rewriteAssetPaths,
18381
18599
  saveHistory,
18382
18600
  sendNotifications,
18383
18601
  sendSlackNotification,