mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-11-22 09:57:02 +01:00
[TS dashboard] Draw dependencies between files (#17338)
If you're thinking about converting a file to TypeScript, you might want to know what files import that file and what files it imports. This commit makes it so that if you click on a box in the visualization, it will draw lines between that box and the other boxes which represent its dependents and dependencies. Co-authored-by: legobeat <109787230+legobeat@users.noreply.github.com>
This commit is contained in:
parent
d45c4ed497
commit
3c622cd395
@ -1,7 +1,9 @@
|
||||
import React from 'react';
|
||||
import classnames from 'classnames';
|
||||
import { Tooltip as ReactTippy } from 'react-tippy';
|
||||
import React, { useState } from 'react';
|
||||
import { readPartitionsFile } from '../../common/partitions-file';
|
||||
import type { ModulePartitionChild } from '../../common/build-module-partitions';
|
||||
import Box from './Box';
|
||||
import Connections from './Connections';
|
||||
import type { BoxRect, BoxModel } from './types';
|
||||
|
||||
type Summary = {
|
||||
numConvertedModules: number;
|
||||
@ -12,16 +14,85 @@ function calculatePercentageComplete(summary: Summary) {
|
||||
return ((summary.numConvertedModules / summary.numModules) * 100).toFixed(1);
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
const partitions = readPartitionsFile();
|
||||
const partitions = readPartitionsFile();
|
||||
const allModules = partitions.flatMap((partition) => {
|
||||
return partition.children;
|
||||
});
|
||||
const modulesById = allModules.reduce<Record<string, ModulePartitionChild>>(
|
||||
(obj, module) => {
|
||||
return { ...obj, [module.id]: module };
|
||||
},
|
||||
{},
|
||||
);
|
||||
const overallTotal = {
|
||||
numConvertedModules: allModules.filter((module) => module.hasBeenConverted)
|
||||
.length,
|
||||
numModules: allModules.length,
|
||||
};
|
||||
|
||||
const allModules = partitions.flatMap((partition) => {
|
||||
return partition.children;
|
||||
});
|
||||
const overallTotal = {
|
||||
numConvertedModules: allModules.filter((module) => module.hasBeenConverted)
|
||||
.length,
|
||||
numModules: allModules.length,
|
||||
export default function App() {
|
||||
const [boxRectsByModuleId, setBoxRectsById] = useState<Record<
|
||||
string,
|
||||
BoxRect
|
||||
> | null>(null);
|
||||
const boxesByModuleId =
|
||||
boxRectsByModuleId === null
|
||||
? {}
|
||||
: Object.values(boxRectsByModuleId).reduce<Record<string, BoxModel>>(
|
||||
(obj, boxRect) => {
|
||||
const module = modulesById[boxRect.moduleId];
|
||||
|
||||
const dependencyBoxRects = module.dependencyIds.reduce<BoxRect[]>(
|
||||
(dependencyBoxes, dependencyId) => {
|
||||
if (boxRectsByModuleId[dependencyId] === undefined) {
|
||||
return dependencyBoxes;
|
||||
}
|
||||
return [...dependencyBoxes, boxRectsByModuleId[dependencyId]];
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const dependentBoxRects = module.dependentIds.reduce<BoxRect[]>(
|
||||
(dependentBoxes, dependentId) => {
|
||||
if (boxRectsByModuleId[dependentId] === undefined) {
|
||||
return dependentBoxes;
|
||||
}
|
||||
return [...dependentBoxes, boxRectsByModuleId[dependentId]];
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return {
|
||||
...obj,
|
||||
[boxRect.moduleId]: {
|
||||
rect: boxRect,
|
||||
dependencyBoxRects,
|
||||
dependentBoxRects,
|
||||
},
|
||||
};
|
||||
},
|
||||
{},
|
||||
);
|
||||
const [activeBoxRectId, setActiveBoxRectId] = useState<string | null>(null);
|
||||
const activeBoxRect =
|
||||
boxRectsByModuleId === null || activeBoxRectId === null
|
||||
? null
|
||||
: boxRectsByModuleId[activeBoxRectId];
|
||||
|
||||
const registerBox = (id: string, boxRect: BoxRect) => {
|
||||
setBoxRectsById((existingBoxRectsById) => {
|
||||
if (existingBoxRectsById === undefined) {
|
||||
return { [id]: boxRect };
|
||||
}
|
||||
return { ...existingBoxRectsById, [id]: boxRect };
|
||||
});
|
||||
};
|
||||
const toggleConnectionsFor = (id: string) => {
|
||||
if (activeBoxRectId !== undefined && activeBoxRectId === id) {
|
||||
setActiveBoxRectId(null);
|
||||
} else {
|
||||
setActiveBoxRectId(id);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
@ -50,17 +121,35 @@ export default function App() {
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Each box
|
||||
<div className="module module--inline module--to-be-converted">
|
||||
Each box on this page represents a file in the codebase. Gray boxes
|
||||
<span className="module module--inline module--to-be-converted">
|
||||
|
||||
</div>
|
||||
on this page represents a file that either we want to convert or
|
||||
we've already converted to TypeScript (hover over a box to see the
|
||||
filename). Boxes that are
|
||||
<div className="module module--inline module--to-be-converted module--test">
|
||||
</span>
|
||||
represent files that need to be converted to TypeScript. Green boxes
|
||||
<span className="module module--inline module--has-been-converted">
|
||||
|
||||
</div>
|
||||
greyed out are test or Storybook files.
|
||||
</span>
|
||||
are files that have already been converted. Faded boxes
|
||||
<span className="module module--inline module--to-be-converted module--test">
|
||||
|
||||
</span>
|
||||
<span
|
||||
className="module module--inline module--has-been-converted module--test"
|
||||
style={{ marginLeft: 0 }}
|
||||
>
|
||||
|
||||
</span>
|
||||
are test or Storybook files.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
You can hover over a box to see the name of the file that it
|
||||
represents. You can also click on a box to see connections between
|
||||
other files;{' '}
|
||||
<strong className="module-connection__dependency">red</strong> lines
|
||||
lead to dependencies (other files that import the file);{' '}
|
||||
<strong className="module-connection__dependent">blue</strong> lines
|
||||
lead to dependents (other files that are imported by the file).
|
||||
</p>
|
||||
|
||||
<p>
|
||||
@ -103,36 +192,25 @@ export default function App() {
|
||||
<div key={partition.level} className="partition">
|
||||
<div className="partition__name">level {partition.level}</div>
|
||||
<div className="partition__children">
|
||||
{partition.children.map(({ name, hasBeenConverted }) => {
|
||||
const isTest = /\.test\.(?:js|tsx?)/u.test(name);
|
||||
const isStorybookModule = /\.stories\.(?:js|tsx?)/u.test(
|
||||
name,
|
||||
);
|
||||
{partition.children.map((module) => {
|
||||
const areConnectionsVisible = activeBoxRectId === module.id;
|
||||
return (
|
||||
<ReactTippy
|
||||
key={name}
|
||||
title={name}
|
||||
arrow={true}
|
||||
animation="fade"
|
||||
duration={250}
|
||||
className="module__tooltipped"
|
||||
style={{ display: 'block' }}
|
||||
>
|
||||
<div
|
||||
className={classnames('module', {
|
||||
'module--has-been-converted': hasBeenConverted,
|
||||
'module--to-be-converted': !hasBeenConverted,
|
||||
'module--test': isTest,
|
||||
'module--storybook': isStorybookModule,
|
||||
})}
|
||||
/>
|
||||
</ReactTippy>
|
||||
<Box
|
||||
key={module.id}
|
||||
module={module}
|
||||
register={registerBox}
|
||||
toggleConnectionsFor={toggleConnectionsFor}
|
||||
areConnectionsVisible={areConnectionsVisible}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{activeBoxRect === null ? null : (
|
||||
<Connections activeBox={boxesByModuleId[activeBoxRect.moduleId]} />
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
69
development/ts-migration-dashboard/app/components/Box.tsx
Normal file
69
development/ts-migration-dashboard/app/components/Box.tsx
Normal file
@ -0,0 +1,69 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import classnames from 'classnames';
|
||||
import { Tooltip as ReactTippy } from 'react-tippy';
|
||||
import type { ModulePartitionChild } from '../../common/build-module-partitions';
|
||||
import type { BoxRect } from './types';
|
||||
|
||||
export default function Box({
|
||||
module,
|
||||
register,
|
||||
toggleConnectionsFor,
|
||||
areConnectionsVisible,
|
||||
}: {
|
||||
module: ModulePartitionChild;
|
||||
register: (id: string, boxRect: BoxRect) => void;
|
||||
toggleConnectionsFor: (id: string) => void;
|
||||
areConnectionsVisible: boolean;
|
||||
}) {
|
||||
const isTest = /\.test\.(?:js|tsx?)/u.test(module.id);
|
||||
const isStorybookModule = /\.stories\.(?:js|tsx?)/u.test(module.id);
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (ref.current?.offsetParent) {
|
||||
const rect = ref.current.getBoundingClientRect();
|
||||
const offsetParentRect = ref.current.offsetParent.getBoundingClientRect();
|
||||
const top = rect.top - offsetParentRect.top;
|
||||
const left = rect.left - offsetParentRect.left;
|
||||
const centerX = left + rect.width / 2;
|
||||
const centerY = top + rect.height / 2;
|
||||
register(module.id, {
|
||||
moduleId: module.id,
|
||||
top,
|
||||
left,
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
centerX,
|
||||
centerY,
|
||||
});
|
||||
}
|
||||
}, [ref]);
|
||||
|
||||
const onClick = (event: React.MouseEvent<HTMLDivElement>) => {
|
||||
event.preventDefault();
|
||||
toggleConnectionsFor(module.id);
|
||||
};
|
||||
|
||||
return (
|
||||
<ReactTippy
|
||||
title={module.id}
|
||||
arrow={true}
|
||||
animation="fade"
|
||||
duration={250}
|
||||
className="module__tooltipped"
|
||||
style={{ display: 'block' }}
|
||||
>
|
||||
<div
|
||||
ref={ref}
|
||||
onClick={onClick}
|
||||
className={classnames('module', {
|
||||
'module--has-been-converted': module.hasBeenConverted,
|
||||
'module--to-be-converted': !module.hasBeenConverted,
|
||||
'module--test': isTest,
|
||||
'module--storybook': isStorybookModule,
|
||||
'module--active': areConnectionsVisible,
|
||||
})}
|
||||
/>
|
||||
</ReactTippy>
|
||||
);
|
||||
}
|
@ -0,0 +1,143 @@
|
||||
import React from 'react';
|
||||
import type { BoxModel } from './types';
|
||||
|
||||
function buildShapePoints(coordinates: [number, number][]): string {
|
||||
return coordinates.map(([x, y]) => `${x},${y}`).join(' ');
|
||||
}
|
||||
|
||||
function buildPathD(coordinates: [number, number][]): string {
|
||||
return coordinates
|
||||
.map(([x, y], index) => {
|
||||
if (index === 0) {
|
||||
return `M ${x},${y}`;
|
||||
}
|
||||
return `L ${x},${y}`;
|
||||
})
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
function Arrowhead({
|
||||
type,
|
||||
x,
|
||||
y,
|
||||
}: {
|
||||
type: 'dependency' | 'dependent';
|
||||
x: number;
|
||||
y: number;
|
||||
}) {
|
||||
return (
|
||||
<polygon
|
||||
className={`module-connection__${type}-arrowhead`}
|
||||
points={buildShapePoints([
|
||||
[x - 6, y - 6],
|
||||
[x + 6, y - 6],
|
||||
[x, y],
|
||||
])}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function Line({
|
||||
type,
|
||||
originX,
|
||||
originY,
|
||||
originYOffset = 0,
|
||||
destinationX,
|
||||
destinationY,
|
||||
destinationYOffset = 0,
|
||||
}: {
|
||||
type: 'dependency' | 'dependent';
|
||||
originX: number;
|
||||
originY: number;
|
||||
originYOffset?: number;
|
||||
destinationX: number;
|
||||
destinationY: number;
|
||||
destinationYOffset?: number;
|
||||
}) {
|
||||
const coordinates: [number, number][] =
|
||||
type === 'dependency'
|
||||
? [
|
||||
[originX, originY],
|
||||
[originX, originY + originYOffset],
|
||||
[destinationX, originY + originYOffset],
|
||||
[destinationX, destinationY],
|
||||
]
|
||||
: [
|
||||
[originX, originY],
|
||||
[originX, destinationY - destinationYOffset],
|
||||
[destinationX, destinationY - destinationYOffset],
|
||||
[destinationX, destinationY],
|
||||
];
|
||||
return (
|
||||
<path
|
||||
className={`module-connection__${type}`}
|
||||
d={buildPathD(coordinates)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function LineStart({
|
||||
type,
|
||||
x,
|
||||
y,
|
||||
}: {
|
||||
type: 'dependency' | 'dependent';
|
||||
x: number;
|
||||
y: number;
|
||||
}) {
|
||||
return (
|
||||
<circle className={`module-connection__${type}-point`} cx={x} cy={y} />
|
||||
);
|
||||
}
|
||||
|
||||
export default function Connections({ activeBox }: { activeBox: BoxModel }) {
|
||||
return (
|
||||
<svg className="module-connections">
|
||||
{activeBox.dependencyBoxRects.length === 0 ? null : (
|
||||
<Arrowhead
|
||||
type="dependency"
|
||||
x={activeBox.rect.centerX}
|
||||
y={activeBox.rect.centerY}
|
||||
/>
|
||||
)}
|
||||
{activeBox.dependencyBoxRects.map((dependencyBoxRect) => {
|
||||
return (
|
||||
<React.Fragment key={dependencyBoxRect.moduleId}>
|
||||
<Line
|
||||
type="dependency"
|
||||
originX={dependencyBoxRect.centerX}
|
||||
originY={dependencyBoxRect.centerY}
|
||||
originYOffset={dependencyBoxRect.height / 2 + 7}
|
||||
destinationX={activeBox.rect.centerX}
|
||||
destinationY={activeBox.rect.centerY}
|
||||
/>
|
||||
<LineStart
|
||||
type="dependency"
|
||||
x={dependencyBoxRect.centerX}
|
||||
y={dependencyBoxRect.centerY}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
{activeBox.dependentBoxRects.map((dependentBoxRect) => {
|
||||
return (
|
||||
<React.Fragment key={dependentBoxRect.moduleId}>
|
||||
<Line
|
||||
type="dependent"
|
||||
originX={activeBox.rect.centerX}
|
||||
originY={activeBox.rect.centerY}
|
||||
destinationX={dependentBoxRect.centerX}
|
||||
destinationY={dependentBoxRect.centerY}
|
||||
destinationYOffset={dependentBoxRect.height / 2 + 7}
|
||||
/>
|
||||
<Arrowhead
|
||||
type="dependent"
|
||||
x={dependentBoxRect.centerX}
|
||||
y={dependentBoxRect.centerY}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</svg>
|
||||
);
|
||||
}
|
15
development/ts-migration-dashboard/app/components/types.ts
Normal file
15
development/ts-migration-dashboard/app/components/types.ts
Normal file
@ -0,0 +1,15 @@
|
||||
export type BoxRect = {
|
||||
moduleId: string;
|
||||
top: number;
|
||||
left: number;
|
||||
width: number;
|
||||
height: number;
|
||||
centerX: number;
|
||||
centerY: number;
|
||||
};
|
||||
|
||||
export type BoxModel = {
|
||||
rect: BoxRect;
|
||||
dependencyBoxRects: BoxRect[];
|
||||
dependentBoxRects: BoxRect[];
|
||||
};
|
@ -2,6 +2,10 @@
|
||||
--blue-gray-350: hsl(209deg 13.7% 62.4%);
|
||||
--blue-gray-100: hsl(209.8deg 16.5% 89%);
|
||||
--green: hsl(113deg 100% 38%);
|
||||
--red: hsl(13deg 98% 61%);
|
||||
--blue: hsl(246deg 97% 55%);
|
||||
--light-cyan: hsl(178deg 100% 85%);
|
||||
--cyan: hsl(178deg 100% 42%);
|
||||
}
|
||||
|
||||
.page-header {
|
||||
@ -69,6 +73,10 @@
|
||||
border: 1px solid rgb(0 0 0 / 50%);
|
||||
}
|
||||
|
||||
.partitions {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.partition {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
@ -94,8 +102,9 @@
|
||||
.module {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
border: 1px solid rgba(0 0 0 / 25%);
|
||||
border: 1px solid hsla(0deg 0% 0% / 25%);
|
||||
border-radius: 0.25rem;
|
||||
cursor: pointer;
|
||||
|
||||
&--inline {
|
||||
display: inline-block;
|
||||
@ -120,6 +129,55 @@
|
||||
&--storybook {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
&--active {
|
||||
border-color: var(--cyan);
|
||||
background-color: var(--light-cyan);
|
||||
border-width: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.module-connections {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.module-connection {
|
||||
&__dependency-arrowhead {
|
||||
fill: var(--red);
|
||||
}
|
||||
|
||||
&__dependency {
|
||||
fill: none;
|
||||
stroke: var(--red);
|
||||
color: var(--red);
|
||||
stroke-width: 2px;
|
||||
}
|
||||
|
||||
&__dependency-point {
|
||||
fill: var(--red);
|
||||
r: 3px;
|
||||
}
|
||||
|
||||
&__dependent-arrowhead {
|
||||
fill: var(--blue);
|
||||
}
|
||||
|
||||
&__dependent {
|
||||
fill: none;
|
||||
stroke: var(--blue);
|
||||
color: var(--blue);
|
||||
stroke-width: 2px;
|
||||
}
|
||||
|
||||
&__dependent-point {
|
||||
fill: var(--blue);
|
||||
r: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Package overrides */
|
||||
|
@ -9,24 +9,42 @@ import {
|
||||
} from './constants';
|
||||
|
||||
/**
|
||||
* Represents a module that has been imported somewhere in the codebase.
|
||||
* Represents a module that has been imported somewhere in the codebase, whether
|
||||
* it is local to the project or an NPM package.
|
||||
*
|
||||
* @property id - The name of a file or NPM module.
|
||||
* @property id - If an NPM package, then the name of the package; otherwise the
|
||||
* path to a file within the project.
|
||||
* @property dependents - The modules which are imported by this module.
|
||||
* @property level - How many modules it takes to import this module (from the
|
||||
* root of the dependency tree).
|
||||
* @property isExternal - Whether the module refers to a NPM module.
|
||||
* @property isExternal - Whether the module refers to a NPM package.
|
||||
* @property hasBeenConverted - Whether the module was one of the files we
|
||||
* wanted to convert to TypeScript and has been converted.
|
||||
*/
|
||||
type Module = {
|
||||
id: string;
|
||||
dependents: Module[];
|
||||
dependencies: Module[];
|
||||
level: number;
|
||||
isExternal: boolean;
|
||||
hasBeenConverted: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Represents a module that belongs to a certain level within the dependency
|
||||
* graph, displayed as a box in the UI.
|
||||
*
|
||||
* @property id - The id of the module.
|
||||
* @property hasBeenConverted - Whether or not the module has been converted to
|
||||
* TypeScript.
|
||||
*/
|
||||
export type ModulePartitionChild = {
|
||||
id: string;
|
||||
hasBeenConverted: boolean;
|
||||
dependentIds: string[];
|
||||
dependencyIds: string[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Represents a set of modules that sit at a certain level within the final
|
||||
* dependency tree.
|
||||
@ -34,16 +52,10 @@ type Module = {
|
||||
* @property level - How many modules it takes to import this module (from the
|
||||
* root of the dependency tree).
|
||||
* @property children - The modules that share this same level.
|
||||
* @property children[].name - The name of the item being imported.
|
||||
* @property children[].hasBeenConverted - Whether or not the module (assuming
|
||||
* that it is a file in our codebase) has been converted to TypeScript.
|
||||
*/
|
||||
export type ModulePartition = {
|
||||
level: number;
|
||||
children: {
|
||||
name: string;
|
||||
hasBeenConverted: boolean;
|
||||
}[];
|
||||
children: ModulePartitionChild[];
|
||||
};
|
||||
|
||||
/**
|
||||
@ -196,10 +208,11 @@ function buildModulesWithLevels(
|
||||
// includes `foo.js`, so we say `foo.js` is a circular dependency of `baz.js`,
|
||||
// and we don't need to follow it.
|
||||
|
||||
const modulesToFill: Module[] = entryModuleIds.map((moduleId) => {
|
||||
let modulesToFill: Module[] = entryModuleIds.map((moduleId) => {
|
||||
return {
|
||||
id: moduleId,
|
||||
dependents: [],
|
||||
dependencies: [],
|
||||
level: 0,
|
||||
isExternal: false,
|
||||
hasBeenConverted: /\.tsx?$/u.test(moduleId),
|
||||
@ -208,76 +221,101 @@ function buildModulesWithLevels(
|
||||
const modulesById: Record<string, Module> = {};
|
||||
|
||||
while (modulesToFill.length > 0) {
|
||||
const currentModule = modulesToFill.shift() as Module;
|
||||
const childModulesToFill: Module[] = [];
|
||||
(dependenciesByModuleId[currentModule.id] ?? []).forEach(
|
||||
(givenChildModuleId) => {
|
||||
const npmPackageMatch = givenChildModuleId.match(
|
||||
/^node_modules\/(?:(@[^/]+)\/)?([^/]+)\/.+$/u,
|
||||
);
|
||||
const currentModule = modulesToFill[0];
|
||||
|
||||
let childModuleId;
|
||||
if (npmPackageMatch) {
|
||||
childModuleId =
|
||||
npmPackageMatch[1] === undefined
|
||||
? `${npmPackageMatch[2]}`
|
||||
: `${npmPackageMatch[1]}/${npmPackageMatch[2]}`;
|
||||
} else {
|
||||
childModuleId = givenChildModuleId;
|
||||
}
|
||||
|
||||
// Skip circular dependencies
|
||||
if (
|
||||
findDirectAndIndirectDependentIdsOf(currentModule).has(childModuleId)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip files that weren't on the original list of JavaScript files to
|
||||
// convert, as we don't want them to show up on the status dashboard
|
||||
if (
|
||||
!npmPackageMatch &&
|
||||
!allowedFilePaths.includes(childModuleId.replace(/\.tsx?$/u, '.js'))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const existingChildModule = modulesById[childModuleId];
|
||||
|
||||
if (existingChildModule === undefined) {
|
||||
const childModule: Module = {
|
||||
id: childModuleId,
|
||||
dependents: [currentModule],
|
||||
level: currentModule.level + 1,
|
||||
isExternal: Boolean(npmPackageMatch),
|
||||
hasBeenConverted: /\.tsx?$/u.test(childModuleId),
|
||||
};
|
||||
childModulesToFill.push(childModule);
|
||||
} else {
|
||||
if (currentModule.level + 1 > existingChildModule.level) {
|
||||
existingChildModule.level = currentModule.level + 1;
|
||||
// Update descendant modules' levels
|
||||
childModulesToFill.push(existingChildModule);
|
||||
} else {
|
||||
// There is no point in descending into dependencies of this module
|
||||
// if the new level of the module would be <= the existing level,
|
||||
// because all of the dependencies from this point on are guaranteed
|
||||
// to have a higher level and are therefore already at the right
|
||||
// level.
|
||||
}
|
||||
|
||||
if (!existingChildModule.dependents.includes(currentModule)) {
|
||||
existingChildModule.dependents.push(currentModule);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
if (childModulesToFill.length > 0) {
|
||||
modulesToFill.push(...childModulesToFill);
|
||||
if (currentModule.level > 100) {
|
||||
throw new Error(
|
||||
"Can't build module partitions, as the dependency graph is being traversed ad infinitum.",
|
||||
);
|
||||
}
|
||||
|
||||
const dependencyModulesToFill: Module[] = [];
|
||||
for (const nonCanonicalDependencyModuleId of dependenciesByModuleId[
|
||||
currentModule.id
|
||||
]) {
|
||||
// If the file path of the module is located in `node_modules`, use the
|
||||
// name of the package as the ID
|
||||
let dependencyModuleId;
|
||||
const npmPackageMatch = nonCanonicalDependencyModuleId.match(
|
||||
/^node_modules\/(?:(@[^/]+)\/)?([^/]+)\/.+$/u,
|
||||
);
|
||||
if (npmPackageMatch) {
|
||||
dependencyModuleId =
|
||||
npmPackageMatch[1] === undefined
|
||||
? `${npmPackageMatch[2]}`
|
||||
: `${npmPackageMatch[1]}/${npmPackageMatch[2]}`;
|
||||
} else {
|
||||
dependencyModuleId = nonCanonicalDependencyModuleId;
|
||||
}
|
||||
|
||||
// Skip circular dependencies
|
||||
if (
|
||||
findDirectAndIndirectDependentIdsOf(currentModule).has(
|
||||
dependencyModuleId,
|
||||
)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip files that weren't on the original list of JavaScript files to
|
||||
// convert, as we don't want them to show up on the status dashboard
|
||||
if (
|
||||
!npmPackageMatch &&
|
||||
!allowedFilePaths.includes(
|
||||
dependencyModuleId.replace(/\.tsx?$/u, '.js'),
|
||||
)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let existingDependencyModule = modulesById[dependencyModuleId];
|
||||
|
||||
if (existingDependencyModule === undefined) {
|
||||
existingDependencyModule = {
|
||||
id: dependencyModuleId,
|
||||
dependents: [currentModule],
|
||||
dependencies: [],
|
||||
level: currentModule.level + 1,
|
||||
isExternal: Boolean(npmPackageMatch),
|
||||
hasBeenConverted: /\.tsx?$/u.test(dependencyModuleId),
|
||||
};
|
||||
dependencyModulesToFill.push(existingDependencyModule);
|
||||
} else if (currentModule.level + 1 > existingDependencyModule.level) {
|
||||
existingDependencyModule.level = currentModule.level + 1;
|
||||
// Update descendant modules' levels
|
||||
dependencyModulesToFill.push(existingDependencyModule);
|
||||
} else {
|
||||
// There is no point in descending into dependencies of this module
|
||||
// if the new level of the module would be <= the existing level,
|
||||
// because all of the dependencies from this point on are guaranteed
|
||||
// to have a higher level and are therefore already at the right
|
||||
// level.
|
||||
}
|
||||
|
||||
if (
|
||||
!existingDependencyModule.dependents.some(
|
||||
(m) => m.id === currentModule.id,
|
||||
)
|
||||
) {
|
||||
existingDependencyModule.dependents.push(currentModule);
|
||||
}
|
||||
|
||||
if (
|
||||
!currentModule.dependencies.some(
|
||||
(m) => m.id === existingDependencyModule.id,
|
||||
)
|
||||
) {
|
||||
currentModule.dependencies.push(existingDependencyModule);
|
||||
}
|
||||
}
|
||||
if (dependencyModulesToFill.length > 0) {
|
||||
modulesToFill.push(...dependencyModulesToFill);
|
||||
}
|
||||
|
||||
if (!(currentModule.id in modulesById)) {
|
||||
modulesById[currentModule.id] = currentModule;
|
||||
}
|
||||
modulesToFill = modulesToFill.slice(1);
|
||||
}
|
||||
|
||||
return modulesById;
|
||||
@ -288,20 +326,21 @@ function buildModulesWithLevels(
|
||||
* which import that file directly or through some other file.
|
||||
*
|
||||
* @param module - A module in the graph.
|
||||
* @returns The ids of the modules which are incoming connections to
|
||||
* @returns The ids of the modules which are direct and indirect dependents of
|
||||
* the module.
|
||||
*/
|
||||
function findDirectAndIndirectDependentIdsOf(module: Module): Set<string> {
|
||||
const modulesToProcess = [module];
|
||||
let modulesToProcess = [module];
|
||||
const allDependentIds = new Set<string>();
|
||||
while (modulesToProcess.length > 0) {
|
||||
const currentModule = modulesToProcess.shift() as Module;
|
||||
currentModule.dependents.forEach((dependent) => {
|
||||
const currentModule = modulesToProcess[0];
|
||||
for (const dependent of currentModule.dependents) {
|
||||
if (!allDependentIds.has(dependent.id)) {
|
||||
allDependentIds.add(dependent.id);
|
||||
modulesToProcess.push(dependent);
|
||||
}
|
||||
});
|
||||
}
|
||||
modulesToProcess = modulesToProcess.slice(1);
|
||||
}
|
||||
return allDependentIds;
|
||||
}
|
||||
@ -328,8 +367,22 @@ function partitionModulesByLevel(
|
||||
}
|
||||
Object.values(modulesById).forEach((module) => {
|
||||
modulePartitions[module.level].children.push({
|
||||
name: module.id,
|
||||
id: module.id,
|
||||
hasBeenConverted: module.hasBeenConverted,
|
||||
dependentIds: module.dependents.map((dependent) => dependent.id).sort(),
|
||||
dependencyIds: module.dependencies
|
||||
.map((dependency) => dependency.id)
|
||||
.sort(),
|
||||
});
|
||||
});
|
||||
Object.values(modulePartitions).forEach((partition) => {
|
||||
partition.children.sort((a, b) => {
|
||||
if (a.id < b.id) {
|
||||
return -1;
|
||||
} else if (a.id > b.id) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
});
|
||||
return modulePartitions.reverse();
|
||||
|
Loading…
Reference in New Issue
Block a user