sonda 0.13.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -168,7 +168,7 @@ function hasIgnoredExtension(name) {
168
168
  }
169
169
  //#endregion
170
170
  //#region package.json
171
- var version = "0.13.0";
171
+ var version = "0.14.0";
172
172
  //#endregion
173
173
  //#region src/report/formatters/Formatter.ts
174
174
  var Formatter = class {
@@ -414,6 +414,7 @@ const parentMap = {};
414
414
  * Update the report with the output assets and their sources from the source map.
415
415
  */
416
416
  function updateOutput(report, path, entrypoints) {
417
+ if (!existsSync(path)) return;
417
418
  const type = getTypeByName(path);
418
419
  RESOURCE_TYPES_TO_ANALYZE.includes(type) ? addAnalyzableType(report, path, entrypoints, type) : addNonAnalyzableType(report, path, type);
419
420
  }
@@ -539,6 +540,7 @@ const packageNameRegExp = /(.*)(?:.*node_modules\/)(@[^\/]+\/[^\/]+|[^\/]+)/;
539
540
  function updateDependencies(report) {
540
541
  const dependencies = {};
541
542
  report.resources.map((file) => packageNameRegExp.exec(file.name)).filter((match) => match !== null).forEach(([path, , name]) => {
543
+ if (!existsSync(path)) return;
542
544
  const paths = dependencies[name] ??= [];
543
545
  if (!paths.includes(path)) paths.push(path);
544
546
  });
@@ -548,6 +550,127 @@ function updateDependencies(report) {
548
550
  }));
549
551
  }
550
552
  //#endregion
553
+ //#region src/report/processors/issues.ts
554
+ const MAX_CIRCULAR_IMPORT_ISSUES = 100;
555
+ const IMPORT_CONNECTION_KINDS = /* @__PURE__ */ new Set([
556
+ "import",
557
+ "require",
558
+ "dynamic-import"
559
+ ]);
560
+ /**
561
+ * Finds common issues in the report data.
562
+ */
563
+ function updateIssues(dependencies, connections) {
564
+ return [...findDuplicatedDependencies(dependencies), ...findCircularImports(connections)];
565
+ }
566
+ function findDuplicatedDependencies(dependencies) {
567
+ return dependencies.filter((dependency) => dependency.paths.length > 1).map((dependency) => ({
568
+ type: "duplicated-dependency",
569
+ severity: "warning",
570
+ message: `Dependency "${dependency.name}" appears to be bundled from multiple paths.`,
571
+ data: {
572
+ name: dependency.name,
573
+ paths: dependency.paths
574
+ }
575
+ }));
576
+ }
577
+ function findCircularImports(connections) {
578
+ const graph = createImportGraph(connections);
579
+ return findStronglyConnectedComponents(graph).filter((component) => component.length > 1 || hasSelfReference(graph, component[0])).toSorted(compareComponents).slice(0, MAX_CIRCULAR_IMPORT_ISSUES).map((component) => {
580
+ const cycle = findCycleInComponent(graph, component);
581
+ return {
582
+ type: "circular-import",
583
+ severity: "warning",
584
+ message: `Circular import detected between ${cycle.length - 1} modules.`,
585
+ data: { cycle }
586
+ };
587
+ });
588
+ }
589
+ function createImportGraph(connections) {
590
+ const graph = /* @__PURE__ */ new Map();
591
+ for (const connection of connections) {
592
+ if (!IMPORT_CONNECTION_KINDS.has(connection.kind)) continue;
593
+ const targets = graph.get(connection.source) ?? /* @__PURE__ */ new Set();
594
+ targets.add(connection.target);
595
+ graph.set(connection.source, targets);
596
+ if (!graph.has(connection.target)) graph.set(connection.target, /* @__PURE__ */ new Set());
597
+ }
598
+ return new Map([...graph].map(([node, targets]) => [node, [...targets].toSorted()]));
599
+ }
600
+ function findStronglyConnectedComponents(graph) {
601
+ let index = 0;
602
+ const stack = [];
603
+ const indexes = /* @__PURE__ */ new Map();
604
+ const lowLinks = /* @__PURE__ */ new Map();
605
+ const stacked = /* @__PURE__ */ new Set();
606
+ const components = [];
607
+ function visit(node) {
608
+ indexes.set(node, index);
609
+ lowLinks.set(node, index);
610
+ index++;
611
+ stack.push(node);
612
+ stacked.add(node);
613
+ for (const next of graph.get(node) ?? []) if (!indexes.has(next)) {
614
+ visit(next);
615
+ lowLinks.set(node, Math.min(lowLinks.get(node), lowLinks.get(next)));
616
+ } else if (stacked.has(next)) lowLinks.set(node, Math.min(lowLinks.get(node), indexes.get(next)));
617
+ if (lowLinks.get(node) !== indexes.get(node)) return;
618
+ const component = [];
619
+ let current;
620
+ do {
621
+ current = stack.pop();
622
+ if (current === void 0) break;
623
+ stacked.delete(current);
624
+ component.push(current);
625
+ } while (current !== node);
626
+ components.push(component.toSorted());
627
+ }
628
+ for (const node of [...graph.keys()].toSorted()) if (!indexes.has(node)) visit(node);
629
+ return components;
630
+ }
631
+ function findCycleInComponent(graph, component) {
632
+ const nodes = new Set(component);
633
+ const start = component[0];
634
+ if (hasSelfReference(graph, start)) return [start, start];
635
+ for (const next of graph.get(start) ?? []) {
636
+ if (!nodes.has(next)) continue;
637
+ const path = findPath(graph, next, start, nodes);
638
+ if (path) return [start, ...path];
639
+ }
640
+ return [start, start];
641
+ }
642
+ function findPath(graph, source, target, allowedNodes) {
643
+ const queue = [source];
644
+ const visited = /* @__PURE__ */ new Set([source]);
645
+ const previous = /* @__PURE__ */ new Map();
646
+ for (let index = 0; index < queue.length; index++) {
647
+ const current = queue[index];
648
+ if (current === target) return reconstructPath(previous, source, target);
649
+ for (const next of graph.get(current) ?? []) {
650
+ if (!allowedNodes.has(next) || visited.has(next)) continue;
651
+ visited.add(next);
652
+ previous.set(next, current);
653
+ queue.push(next);
654
+ }
655
+ }
656
+ return null;
657
+ }
658
+ function reconstructPath(previous, source, target) {
659
+ const path = [target];
660
+ let current = target;
661
+ while (current !== source) {
662
+ current = previous.get(current);
663
+ path.push(current);
664
+ }
665
+ return path.reverse();
666
+ }
667
+ function hasSelfReference(graph, node) {
668
+ return graph.get(node)?.includes(node) ?? false;
669
+ }
670
+ function compareComponents(a, b) {
671
+ return a[0] < b[0] ? -1 : Number(a[0] > b[0]);
672
+ }
673
+ //#endregion
551
674
  //#region src/report/report.ts
552
675
  const formatters = {
553
676
  html: HtmlFormatter,
@@ -602,8 +725,9 @@ var Report = class {
602
725
  this.assets[name] = entrypoints;
603
726
  }
604
727
  async generate() {
605
- Object.entries(this.assets).filter(([_, entrypoints]) => !!entrypoints).forEach(([path, entrypoints]) => updateOutput(this, path, entrypoints));
728
+ Object.entries(this.assets).filter(([_, entrypoints]) => entrypoints !== void 0).forEach(([path, entrypoints]) => updateOutput(this, path, entrypoints));
606
729
  this.dependencies = updateDependencies(this);
730
+ this.issues = updateIssues(this.dependencies, this.connections);
607
731
  const outputs = [];
608
732
  for (const format of this.config.format) {
609
733
  const path = await new formatters[format](this.config).write(this.#getFormattedData());
@@ -625,7 +749,7 @@ var Report = class {
625
749
  resources: sortByKey(this.resources, "name"),
626
750
  connections: this.connections,
627
751
  dependencies: sortByKey(this.dependencies, "name"),
628
- issues: this.issues,
752
+ issues: sortByKey(this.issues, "message"),
629
753
  sourcemaps: this.sourcemaps
630
754
  };
631
755
  }
@@ -733,7 +857,7 @@ function SondaRollupPlugin(userOptions = {}) {
733
857
  },
734
858
  async writeBundle({ dir, file }, bundle) {
735
859
  const outputDir = resolve(process.cwd(), dir ?? dirname(file));
736
- for (const [path, asset] of Object.entries(bundle)) report.addAsset(resolve(outputDir, path), asset.type === "chunk" && asset.facadeModuleId ? [asset.facadeModuleId] : void 0);
860
+ for (const [path, asset] of Object.entries(bundle)) report.addAsset(resolve(outputDir, path), asset.type === "chunk" ? asset.facadeModuleId ? [asset.facadeModuleId] : [] : void 0);
737
861
  },
738
862
  async closeBundle() {
739
863
  const reportPaths = await report.generate();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sonda",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
4
4
  "description": "Universal bundle analyzer and visualizer that works with most popular bundlers and frameworks.",
5
5
  "keywords": [
6
6
  "analyzer",