repo-dive 0.16.4 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -68,7 +68,7 @@ Collectors so far:
68
68
  - **commit-meta** — identities, dates, parents, subject and trailers (incl. AI co-authors)
69
69
  - **churn** — lines added/deleted per commit, by file extension
70
70
  - **file-types** — file count and bytes per extension at each commit's tree
71
- - **directives** — eslint-disable comments by rule (block disables tracked as gray areas) and `@ts-ignore`/`@ts-expect-error`/`@ts-nocheck`
71
+ - **directives** — `eslint-disable` and `oxlint-disable` comments by linter and rule (block disables tracked as gray areas) and `@ts-ignore`/`@ts-expect-error`/`@ts-nocheck`
72
72
  - **dependencies** — resolved package totals from lockfiles, per package manager (pnpm, npm, yarn classic and yarn berry), plus direct/dev/optional dependencies and manifest counts read straight from `package.json` files; version-aware, monorepo-aware and extensible to more managers
73
73
  - **todo-comments** — TODO/FIXME/HACK/XXX counts
74
74
  - **languages** — lines and file count per language across a commit's source files (lockfiles, minified bundles and generated data excluded)
package/dist/cli.js CHANGED
@@ -42805,7 +42805,7 @@ var runWith = (command, config) => {
42805
42805
  };
42806
42806
  var package_default = {
42807
42807
  name: "repo-dive",
42808
- version: "0.16.4",
42808
+ version: "0.17.0",
42809
42809
  description: "Dive into a git repo's history: per-commit snapshots, an indexed metrics catalog and an interactive dashboard",
42810
42810
  keywords: [
42811
42811
  "git",
@@ -49682,6 +49682,36 @@ var scanTreeWithBlobCache = fnUntraced(function* ({ repoRoot, catalogPath, sha,
49682
49682
  });
49683
49683
  //#endregion
49684
49684
  //#region src/cli/shared/collectors/directives.ts
49685
+ /**
49686
+ * The linters whose suppression comments are counted, by comment prefix.
49687
+ * oxlint honours `eslint-*` comments too, so the prefix records how a comment
49688
+ * is spelled, not which tool reads it.
49689
+ */
49690
+ var linters = ["eslint", "oxlint"];
49691
+ /**
49692
+ * Where each linter's counts sit in {@link DirectivesOutput}. Flat rather than
49693
+ * nested by linter, so outputs collected before oxlint was counted (eslint
49694
+ * keys only) still normalize.
49695
+ */
49696
+ var outputKeys = {
49697
+ eslint: {
49698
+ nextLine: "eslintNextLine",
49699
+ line: "eslintLine",
49700
+ blocks: "eslintBlocks"
49701
+ },
49702
+ oxlint: {
49703
+ nextLine: "oxlintNextLine",
49704
+ line: "oxlintLine",
49705
+ blocks: "oxlintBlocks"
49706
+ }
49707
+ };
49708
+ var emptyBlockCounts = () => ({
49709
+ count: 0,
49710
+ closedCount: 0,
49711
+ unboundedCount: 0,
49712
+ coveredLines: 0,
49713
+ byRule: {}
49714
+ });
49685
49715
  var emptyOutput = () => ({
49686
49716
  eslintNextLine: {
49687
49717
  count: 0,
@@ -49691,24 +49721,25 @@ var emptyOutput = () => ({
49691
49721
  count: 0,
49692
49722
  byRule: {}
49693
49723
  },
49694
- eslintBlocks: {
49724
+ eslintBlocks: emptyBlockCounts(),
49725
+ oxlintNextLine: {
49726
+ count: 0,
49727
+ byRule: {}
49728
+ },
49729
+ oxlintLine: {
49695
49730
  count: 0,
49696
- closedCount: 0,
49697
- unboundedCount: 0,
49698
- coveredLines: 0,
49699
49731
  byRule: {}
49700
49732
  },
49733
+ oxlintBlocks: emptyBlockCounts(),
49701
49734
  tsDirectives: {
49702
49735
  ignore: 0,
49703
49736
  expectError: 0,
49704
49737
  nocheck: 0
49705
49738
  }
49706
49739
  });
49707
- /** Extracts rule names following a directive keyword, stopping at a comment terminator or `--`. */
49708
- var parseRules = (content, keyword) => {
49709
- const keywordIndex = content.indexOf(keyword);
49710
- if (keywordIndex === -1) return [];
49711
- let rest = content.slice(keywordIndex + keyword.length);
49740
+ /** Extracts the rule names after a directive, stopping at a comment terminator or `--`. */
49741
+ var parseRules = (afterDirective) => {
49742
+ let rest = afterDirective;
49712
49743
  for (const terminator of [
49713
49744
  "*/",
49714
49745
  "-->",
@@ -49722,34 +49753,59 @@ var parseRules = (content, keyword) => {
49722
49753
  var addRules = (byRule, rules) => {
49723
49754
  for (const rule of rules.length > 0 ? rules : ["(all)"]) byRule[rule] = (byRule[rule] ?? 0) + 1;
49724
49755
  };
49725
- var quickPattern = /eslint-disable|eslint-enable|@ts-(?:ignore|expect-error|nocheck)/;
49756
+ var quickPattern = /(?:eslint|oxlint)-(?:disable|enable)|@ts-(?:ignore|expect-error|nocheck)/;
49757
+ /**
49758
+ * A lint directive and its kind. The first match on a line wins, so a comment
49759
+ * that mentions the other linter's directive in its rationale is still
49760
+ * attributed to the linter it opens with.
49761
+ */
49762
+ var lintDirectivePattern = /(eslint|oxlint)-(disable-next-line|disable-line|disable|enable)/;
49726
49763
  /** Scans one file's content; results are cached per blob by the tree scanner. */
49727
49764
  var scanFileForDirectives = (content) => {
49728
49765
  const output = emptyOutput();
49729
49766
  if (!quickPattern.test(content)) return output;
49730
- let openBlock;
49767
+ const openBlockLines = /* @__PURE__ */ new Map();
49731
49768
  for (const [index, line] of content.split("\n").entries()) {
49732
49769
  if (!quickPattern.test(line)) continue;
49770
+ const lintDirective = lintDirectivePattern.exec(line);
49733
49771
  if (line.includes("@ts-ignore")) output.tsDirectives.ignore += 1;
49734
49772
  else if (line.includes("@ts-expect-error")) output.tsDirectives.expectError += 1;
49735
49773
  else if (line.includes("@ts-nocheck")) output.tsDirectives.nocheck += 1;
49736
- else if (line.includes("eslint-disable-next-line")) {
49737
- output.eslintNextLine.count += 1;
49738
- addRules(output.eslintNextLine.byRule, parseRules(line, "eslint-disable-next-line"));
49739
- } else if (line.includes("eslint-disable-line")) {
49740
- output.eslintLine.count += 1;
49741
- addRules(output.eslintLine.byRule, parseRules(line, "eslint-disable-line"));
49742
- } else if (line.includes("eslint-disable")) {
49743
- output.eslintBlocks.count += 1;
49744
- addRules(output.eslintBlocks.byRule, parseRules(line, "eslint-disable"));
49745
- openBlock = { line: index };
49746
- } else if (openBlock && line.includes("eslint-enable")) {
49747
- output.eslintBlocks.closedCount += 1;
49748
- output.eslintBlocks.coveredLines += Math.max(0, index - openBlock.line - 1);
49749
- openBlock = void 0;
49750
- }
49751
- }
49752
- output.eslintBlocks.unboundedCount = output.eslintBlocks.count - output.eslintBlocks.closedCount;
49774
+ else if (lintDirective !== null) {
49775
+ const [directive, prefix, kind] = lintDirective;
49776
+ const linter = prefix === "oxlint" ? "oxlint" : "eslint";
49777
+ const keys = outputKeys[linter];
49778
+ const rules = parseRules(line.slice(lintDirective.index + directive.length));
49779
+ switch (kind) {
49780
+ case "disable-next-line":
49781
+ output[keys.nextLine].count += 1;
49782
+ addRules(output[keys.nextLine].byRule, rules);
49783
+ break;
49784
+ case "disable-line":
49785
+ output[keys.line].count += 1;
49786
+ addRules(output[keys.line].byRule, rules);
49787
+ break;
49788
+ case "disable":
49789
+ output[keys.blocks].count += 1;
49790
+ addRules(output[keys.blocks].byRule, rules);
49791
+ openBlockLines.set(linter, index);
49792
+ break;
49793
+ case "enable": {
49794
+ const openBlockLine = openBlockLines.get(linter);
49795
+ if (openBlockLine !== void 0) {
49796
+ output[keys.blocks].closedCount += 1;
49797
+ output[keys.blocks].coveredLines += Math.max(0, index - openBlockLine - 1);
49798
+ openBlockLines.delete(linter);
49799
+ }
49800
+ break;
49801
+ }
49802
+ }
49803
+ }
49804
+ }
49805
+ for (const linter of linters) {
49806
+ const blocks = output[outputKeys[linter].blocks];
49807
+ blocks.unboundedCount = blocks.count - blocks.closedCount;
49808
+ }
49753
49809
  return output;
49754
49810
  };
49755
49811
  var mergeRuleCounts = (target, source) => {
@@ -49759,18 +49815,21 @@ var mergeRuleCounts = (target, source) => {
49759
49815
  var mergeDirectives = (fileResults) => {
49760
49816
  const merged = emptyOutput();
49761
49817
  for (const result of fileResults) {
49762
- const nextLine = recordAt(result, "eslintNextLine");
49763
- merged.eslintNextLine.count += numberAt(nextLine, "count");
49764
- mergeRuleCounts(merged.eslintNextLine.byRule, nextLine["byRule"]);
49765
- const line = recordAt(result, "eslintLine");
49766
- merged.eslintLine.count += numberAt(line, "count");
49767
- mergeRuleCounts(merged.eslintLine.byRule, line["byRule"]);
49768
- const blocks = recordAt(result, "eslintBlocks");
49769
- merged.eslintBlocks.count += numberAt(blocks, "count");
49770
- merged.eslintBlocks.closedCount += numberAt(blocks, "closedCount");
49771
- merged.eslintBlocks.unboundedCount += numberAt(blocks, "unboundedCount");
49772
- merged.eslintBlocks.coveredLines += numberAt(blocks, "coveredLines");
49773
- mergeRuleCounts(merged.eslintBlocks.byRule, blocks["byRule"]);
49818
+ for (const linter of linters) {
49819
+ const keys = outputKeys[linter];
49820
+ for (const key of [keys.nextLine, keys.line]) {
49821
+ const counts = recordAt(result, key);
49822
+ merged[key].count += numberAt(counts, "count");
49823
+ mergeRuleCounts(merged[key].byRule, counts["byRule"]);
49824
+ }
49825
+ const blocks = recordAt(result, keys.blocks);
49826
+ const mergedBlocks = merged[keys.blocks];
49827
+ mergedBlocks.count += numberAt(blocks, "count");
49828
+ mergedBlocks.closedCount += numberAt(blocks, "closedCount");
49829
+ mergedBlocks.unboundedCount += numberAt(blocks, "unboundedCount");
49830
+ mergedBlocks.coveredLines += numberAt(blocks, "coveredLines");
49831
+ mergeRuleCounts(mergedBlocks.byRule, blocks["byRule"]);
49832
+ }
49774
49833
  const ts = recordAt(result, "tsDirectives");
49775
49834
  merged.tsDirectives.ignore += numberAt(ts, "ignore");
49776
49835
  merged.tsDirectives.expectError += numberAt(ts, "expectError");
@@ -49780,8 +49839,8 @@ var mergeDirectives = (fileResults) => {
49780
49839
  };
49781
49840
  var directivesCollector = {
49782
49841
  name: "directives",
49783
- description: "ESLint suppression comments (by rule; block disables as gray areas) and @ts-ignore/@ts-expect-error/@ts-nocheck counts",
49784
- version: "1",
49842
+ description: "ESLint and oxlint suppression comments (by linter and rule; block disables as gray areas) and @ts-ignore/@ts-expect-error/@ts-nocheck counts",
49843
+ version: "2",
49785
49844
  strategy: "tree",
49786
49845
  defaultSampling: "all",
49787
49846
  collect: ({ repoRoot, catalogPath, sha, cacheKey }) => scanTreeWithBlobCache({
@@ -49794,22 +49853,28 @@ var directivesCollector = {
49794
49853
  }).pipe(map$6((files) => mergeDirectives(files.map((file) => file.result)))),
49795
49854
  normalize: (raw) => {
49796
49855
  const facts = [];
49797
- for (const [key, type] of [
49798
- ["eslintNextLine", "next-line"],
49799
- ["eslintLine", "line"],
49800
- ["eslintBlocks", "block"]
49801
- ]) for (const [rule, count] of Object.entries(recordAt(recordAt(raw, key), "byRule"))) if (typeof count === "number") facts.push({
49802
- metric: "directives.eslint",
49803
- value: count,
49804
- categories: {
49805
- type,
49806
- rule
49807
- }
49808
- });
49809
- facts.push({
49810
- metric: "directives.eslintBlockCoveredLines",
49811
- value: numberAt(recordAt(raw, "eslintBlocks"), "coveredLines")
49812
- });
49856
+ for (const linter of linters) {
49857
+ const keys = outputKeys[linter];
49858
+ const kinds = [
49859
+ [keys.nextLine, "next-line"],
49860
+ [keys.line, "line"],
49861
+ [keys.blocks, "block"]
49862
+ ];
49863
+ for (const [key, type] of kinds) for (const [rule, count] of Object.entries(recordAt(recordAt(raw, key), "byRule"))) if (typeof count === "number") facts.push({
49864
+ metric: "directives.lint",
49865
+ value: count,
49866
+ categories: {
49867
+ linter,
49868
+ type,
49869
+ rule
49870
+ }
49871
+ });
49872
+ facts.push({
49873
+ metric: "directives.lintBlockCoveredLines",
49874
+ value: numberAt(recordAt(raw, keys.blocks), "coveredLines"),
49875
+ categories: { linter }
49876
+ });
49877
+ }
49813
49878
  const ts = recordAt(raw, "tsDirectives");
49814
49879
  for (const type of [
49815
49880
  "ignore",
@@ -52054,15 +52119,17 @@ var sumMetric = (commit, metric, filter) => {
52054
52119
  for (const facts of commit.factsByCollector.values()) for (const fact of facts) if (fact.metric === metric && (filter === void 0 || filter(fact.categories ?? {}))) total += fact.value;
52055
52120
  return total;
52056
52121
  };
52057
- var groupMetric = (commit, metric, categoryKey) => {
52122
+ /** Sums a metric's values by a key derived from each fact's categories. */
52123
+ var groupMetricBy = (commit, metric, keyOf) => {
52058
52124
  const grouped = {};
52059
52125
  for (const facts of commit.factsByCollector.values()) for (const fact of facts) {
52060
52126
  if (fact.metric !== metric) continue;
52061
- const key = fact.categories?.[categoryKey] ?? "(unknown)";
52127
+ const key = keyOf(fact.categories ?? {});
52062
52128
  grouped[key] = (grouped[key] ?? 0) + fact.value;
52063
52129
  }
52064
52130
  return grouped;
52065
52131
  };
52132
+ var groupMetric = (commit, metric, categoryKey) => groupMetricBy(commit, metric, (categories) => categories[categoryKey] ?? "(unknown)");
52066
52133
  var hasMetric = (commit, metric) => commit.factsByCollector.values().some((facts) => facts.some((fact) => fact.metric === metric));
52067
52134
  /** Re-keys a numeric record, summing values whose new keys collide. */
52068
52135
  var sumByKey = (record, keyOf) => {
@@ -52127,15 +52194,6 @@ var buildDashboardData = (repoRoot, commits, config, remoteUrl, lineageOf) => {
52127
52194
  };
52128
52195
  };
52129
52196
  const span = spanOf(commits);
52130
- const commitRows = commits.map((commit) => ({
52131
- sha: commit.sha.slice(0, 10),
52132
- date: commit.authoredAt,
52133
- author: commit.authorEmail,
52134
- kind: config.resolveContributor(commit.authorEmail, commit.authorName).kind,
52135
- ai: coAuthorsOf(commit).some((identity) => resolveCoAuthor(identity).resolved.kind === "ai"),
52136
- added: sumMetric(commit, "churn.added"),
52137
- deleted: sumMetric(commit, "churn.deleted")
52138
- }));
52139
52197
  const languages = composeSeries((commit) => hasMetric(commit, "languages.lines"), (commit) => ({
52140
52198
  sha: commit.sha.slice(0, 10),
52141
52199
  date: commit.committedAt,
@@ -52149,15 +52207,18 @@ var buildDashboardData = (repoRoot, commits, config, remoteUrl, lineageOf) => {
52149
52207
  totalBytes: sumMetric(commit, "files.bytes")
52150
52208
  }));
52151
52209
  const directives = composeSeries((commit) => hasMetric(commit, "directives.ts"), (commit) => {
52152
- const byType = groupMetric(commit, "directives.eslint", "type");
52210
+ const lint = groupMetricBy(commit, "directives.lint", (categories) => `${categories["linter"]} ${categories["type"]}`);
52153
52211
  const ts = groupMetric(commit, "directives.ts", "type");
52154
52212
  return {
52155
52213
  sha: commit.sha.slice(0, 10),
52156
52214
  date: commit.committedAt,
52157
- eslintNextLine: byType["next-line"] ?? 0,
52158
- eslintLine: byType["line"] ?? 0,
52159
- eslintBlocks: byType["block"] ?? 0,
52160
- blockCoveredLines: sumMetric(commit, "directives.eslintBlockCoveredLines"),
52215
+ eslintNextLine: lint["eslint next-line"] ?? 0,
52216
+ eslintLine: lint["eslint line"] ?? 0,
52217
+ eslintBlocks: lint["eslint block"] ?? 0,
52218
+ oxlintNextLine: lint["oxlint next-line"] ?? 0,
52219
+ oxlintLine: lint["oxlint line"] ?? 0,
52220
+ oxlintBlocks: lint["oxlint block"] ?? 0,
52221
+ blockCoveredLines: sumMetric(commit, "directives.lintBlockCoveredLines"),
52161
52222
  tsIgnore: ts["ignore"] ?? 0,
52162
52223
  tsExpectError: ts["expectError"] ?? 0,
52163
52224
  tsNocheck: ts["nocheck"] ?? 0,
@@ -52177,8 +52238,8 @@ var buildDashboardData = (repoRoot, commits, config, remoteUrl, lineageOf) => {
52177
52238
  byPackageManager: groupMetric(commit, "dependencies.resolved", "packageManager")
52178
52239
  };
52179
52240
  });
52180
- const latestWithDirectives = commits.findLast((commit) => hasMetric(commit, "directives.eslint"));
52181
- const topRules = latestWithDirectives ? Object.entries(groupMetric(latestWithDirectives, "directives.eslint", "rule")).toSorted(([, left], [, right]) => right - left).slice(0, 20).map(([rule, count]) => ({
52241
+ const latestWithDirectives = commits.findLast((commit) => hasMetric(commit, "directives.lint"));
52242
+ const topRules = latestWithDirectives ? Object.entries(groupMetric(latestWithDirectives, "directives.lint", "rule")).toSorted(([, left], [, right]) => right - left).slice(0, 20).map(([rule, count]) => ({
52182
52243
  rule,
52183
52244
  count
52184
52245
  })) : [];
@@ -52243,7 +52304,7 @@ var buildDashboardData = (repoRoot, commits, config, remoteUrl, lineageOf) => {
52243
52304
  const fileSurvival = survivalSeriesOf("survival.files");
52244
52305
  const contributorMap = /* @__PURE__ */ new Map();
52245
52306
  /**
52246
- * Fetches or creates a contributor's bucket.
52307
+ * Fetches or creates a contributor's entry.
52247
52308
  *
52248
52309
  * Humans key off their canonical email alone, so an alias group folds every
52249
52310
  * spelling of one person together. Bots and AI agents key off their name too:
@@ -52252,65 +52313,44 @@ var buildDashboardData = (repoRoot, commits, config, remoteUrl, lineageOf) => {
52252
52313
  * identity worth telling apart. Giving such a group a `displayName` in the
52253
52314
  * config merges them back into one.
52254
52315
  */
52255
- const bucketFor = (resolved, observedName) => {
52316
+ const contributorFor = (resolved, observedName) => {
52256
52317
  const name = nameOf(resolved, observedName);
52257
52318
  const email = resolved.canonicalEmail.toLowerCase();
52258
52319
  const key = resolved.kind === "human" ? email : `${name.toLowerCase()}\u{1F}${email}`;
52259
- const bucket = contributorMap.get(key) ?? {
52320
+ const contributor = contributorMap.get(key) ?? {
52321
+ index: contributorMap.size,
52260
52322
  email: resolved.canonicalEmail,
52261
52323
  name,
52262
52324
  url: resolved.url,
52263
- kind: resolved.kind,
52264
- commits: 0,
52265
- added: 0,
52266
- deleted: 0,
52267
- assistedBy: {},
52268
- assisted: {}
52325
+ kind: resolved.kind
52269
52326
  };
52270
- bucket.name = name;
52271
- contributorMap.set(key, bucket);
52272
- return bucket;
52327
+ contributor.name = name;
52328
+ contributorMap.set(key, contributor);
52329
+ return contributor;
52273
52330
  };
52274
- for (const [index, commit] of commits.entries()) {
52275
- const row = commitRows[index];
52276
- if (!row) continue;
52331
+ const commitRows = commits.map((commit) => {
52277
52332
  const author = config.resolveContributor(commit.authorEmail, commit.authorName);
52278
- const authorBucket = bucketFor(author, commit.authorName);
52279
- authorBucket.commits += 1;
52280
- authorBucket.added += row.added;
52281
- authorBucket.deleted += row.deleted;
52333
+ const contributor = contributorFor(author, commit.authorName).index;
52334
+ const coAuthors = coAuthorsOf(commit).map(resolveCoAuthor);
52282
52335
  const helpers = /* @__PURE__ */ new Set();
52283
- for (const identity of coAuthorsOf(commit)) {
52284
- const { name, resolved } = resolveCoAuthor(identity);
52285
- if (resolved.kind !== author.kind) helpers.add(bucketFor(resolved, name));
52286
- }
52287
- const helperKinds = /* @__PURE__ */ new Set();
52288
- for (const helper of helpers) {
52289
- helper.assisted[author.kind] = (helper.assisted[author.kind] ?? 0) + 1;
52290
- helperKinds.add(helper.kind);
52291
- }
52292
- for (const kind of helperKinds) authorBucket.assistedBy[kind] = (authorBucket.assistedBy[kind] ?? 0) + 1;
52293
- }
52294
- const sumCounts = (counts) => Object.values(counts).reduce((total, count) => total + count, 0);
52295
- /** Ranking weight: authoring and helping both count as taking part. */
52296
- const involvementOf = (bucket) => bucket.commits + sumCounts(bucket.assisted);
52297
- /** Emitted as absent rather than `{}` so the JSON stays lean. */
52298
- const countsOrUndefined = (counts) => Object.keys(counts).length === 0 ? void 0 : counts;
52299
- const perKindCap = config.maxInCharts * 2;
52300
- const keptPerKind = {
52301
- human: 0,
52302
- bot: 0,
52303
- ai: 0
52304
- };
52305
- const contributors = contributorMap.values().toArray().toSorted((left, right) => involvementOf(right) - involvementOf(left)).filter((bucket) => {
52306
- if (keptPerKind[bucket.kind] >= perKindCap) return false;
52307
- keptPerKind[bucket.kind] += 1;
52308
- return true;
52309
- }).map((bucket) => ({
52310
- ...bucket,
52311
- assistedBy: countsOrUndefined(bucket.assistedBy),
52312
- assisted: countsOrUndefined(bucket.assisted)
52313
- }));
52336
+ for (const { name, resolved } of coAuthors) if (resolved.kind !== author.kind) helpers.add(contributorFor(resolved, name).index);
52337
+ return {
52338
+ sha: commit.sha.slice(0, 10),
52339
+ date: commit.authoredAt,
52340
+ contributor,
52341
+ kind: author.kind,
52342
+ ai: coAuthors.some(({ resolved }) => resolved.kind === "ai"),
52343
+ added: sumMetric(commit, "churn.added"),
52344
+ deleted: sumMetric(commit, "churn.deleted"),
52345
+ ...helpers.size > 0 && { helpers: [...helpers] }
52346
+ };
52347
+ });
52348
+ const contributors = contributorMap.values().map(({ email, name, url, kind }) => ({
52349
+ email,
52350
+ name,
52351
+ url,
52352
+ kind
52353
+ })).toArray();
52314
52354
  return {
52315
52355
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
52316
52356
  config: {