mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-11-22 18:00:18 +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 React, { useState } from 'react';
|
||||||
import classnames from 'classnames';
|
|
||||||
import { Tooltip as ReactTippy } from 'react-tippy';
|
|
||||||
import { readPartitionsFile } from '../../common/partitions-file';
|
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 = {
|
type Summary = {
|
||||||
numConvertedModules: number;
|
numConvertedModules: number;
|
||||||
@ -12,18 +14,87 @@ function calculatePercentageComplete(summary: Summary) {
|
|||||||
return ((summary.numConvertedModules / summary.numModules) * 100).toFixed(1);
|
return ((summary.numConvertedModules / summary.numModules) * 100).toFixed(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function App() {
|
|
||||||
const partitions = readPartitionsFile();
|
const partitions = readPartitionsFile();
|
||||||
|
|
||||||
const allModules = partitions.flatMap((partition) => {
|
const allModules = partitions.flatMap((partition) => {
|
||||||
return partition.children;
|
return partition.children;
|
||||||
});
|
});
|
||||||
|
const modulesById = allModules.reduce<Record<string, ModulePartitionChild>>(
|
||||||
|
(obj, module) => {
|
||||||
|
return { ...obj, [module.id]: module };
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
);
|
||||||
const overallTotal = {
|
const overallTotal = {
|
||||||
numConvertedModules: allModules.filter((module) => module.hasBeenConverted)
|
numConvertedModules: allModules.filter((module) => module.hasBeenConverted)
|
||||||
.length,
|
.length,
|
||||||
numModules: allModules.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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<h1 className="page-header">
|
<h1 className="page-header">
|
||||||
@ -50,17 +121,35 @@ export default function App() {
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Each box
|
Each box on this page represents a file in the codebase. Gray boxes
|
||||||
<div className="module module--inline module--to-be-converted">
|
<span className="module module--inline module--to-be-converted">
|
||||||
|
|
||||||
</div>
|
</span>
|
||||||
on this page represents a file that either we want to convert or
|
represent files that need to be converted to TypeScript. Green boxes
|
||||||
we've already converted to TypeScript (hover over a box to see the
|
<span className="module module--inline module--has-been-converted">
|
||||||
filename). Boxes that are
|
|
||||||
<div className="module module--inline module--to-be-converted module--test">
|
|
||||||
|
|
||||||
</div>
|
</span>
|
||||||
greyed out are test or Storybook files.
|
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>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
@ -103,36 +192,25 @@ export default function App() {
|
|||||||
<div key={partition.level} className="partition">
|
<div key={partition.level} className="partition">
|
||||||
<div className="partition__name">level {partition.level}</div>
|
<div className="partition__name">level {partition.level}</div>
|
||||||
<div className="partition__children">
|
<div className="partition__children">
|
||||||
{partition.children.map(({ name, hasBeenConverted }) => {
|
{partition.children.map((module) => {
|
||||||
const isTest = /\.test\.(?:js|tsx?)/u.test(name);
|
const areConnectionsVisible = activeBoxRectId === module.id;
|
||||||
const isStorybookModule = /\.stories\.(?:js|tsx?)/u.test(
|
|
||||||
name,
|
|
||||||
);
|
|
||||||
return (
|
return (
|
||||||
<ReactTippy
|
<Box
|
||||||
key={name}
|
key={module.id}
|
||||||
title={name}
|
module={module}
|
||||||
arrow={true}
|
register={registerBox}
|
||||||
animation="fade"
|
toggleConnectionsFor={toggleConnectionsFor}
|
||||||
duration={250}
|
areConnectionsVisible={areConnectionsVisible}
|
||||||
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>
|
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
{activeBoxRect === null ? null : (
|
||||||
|
<Connections activeBox={boxesByModuleId[activeBoxRect.moduleId]} />
|
||||||
|
)}
|
||||||
</div>
|
</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-350: hsl(209deg 13.7% 62.4%);
|
||||||
--blue-gray-100: hsl(209.8deg 16.5% 89%);
|
--blue-gray-100: hsl(209.8deg 16.5% 89%);
|
||||||
--green: hsl(113deg 100% 38%);
|
--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 {
|
.page-header {
|
||||||
@ -69,6 +73,10 @@
|
|||||||
border: 1px solid rgb(0 0 0 / 50%);
|
border: 1px solid rgb(0 0 0 / 50%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.partitions {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
.partition {
|
.partition {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
@ -94,8 +102,9 @@
|
|||||||
.module {
|
.module {
|
||||||
width: 1.5rem;
|
width: 1.5rem;
|
||||||
height: 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;
|
border-radius: 0.25rem;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
&--inline {
|
&--inline {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
@ -120,6 +129,55 @@
|
|||||||
&--storybook {
|
&--storybook {
|
||||||
opacity: 0.3;
|
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 */
|
/* Package overrides */
|
||||||
|
@ -9,24 +9,42 @@ import {
|
|||||||
} from './constants';
|
} 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 dependents - The modules which are imported by this module.
|
||||||
* @property level - How many modules it takes to import this module (from the
|
* @property level - How many modules it takes to import this module (from the
|
||||||
* root of the dependency tree).
|
* 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
|
* @property hasBeenConverted - Whether the module was one of the files we
|
||||||
* wanted to convert to TypeScript and has been converted.
|
* wanted to convert to TypeScript and has been converted.
|
||||||
*/
|
*/
|
||||||
type Module = {
|
type Module = {
|
||||||
id: string;
|
id: string;
|
||||||
dependents: Module[];
|
dependents: Module[];
|
||||||
|
dependencies: Module[];
|
||||||
level: number;
|
level: number;
|
||||||
isExternal: boolean;
|
isExternal: boolean;
|
||||||
hasBeenConverted: 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
|
* Represents a set of modules that sit at a certain level within the final
|
||||||
* dependency tree.
|
* dependency tree.
|
||||||
@ -34,16 +52,10 @@ type Module = {
|
|||||||
* @property level - How many modules it takes to import this module (from the
|
* @property level - How many modules it takes to import this module (from the
|
||||||
* root of the dependency tree).
|
* root of the dependency tree).
|
||||||
* @property children - The modules that share this same level.
|
* @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 = {
|
export type ModulePartition = {
|
||||||
level: number;
|
level: number;
|
||||||
children: {
|
children: ModulePartitionChild[];
|
||||||
name: string;
|
|
||||||
hasBeenConverted: boolean;
|
|
||||||
}[];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -196,10 +208,11 @@ function buildModulesWithLevels(
|
|||||||
// includes `foo.js`, so we say `foo.js` is a circular dependency of `baz.js`,
|
// includes `foo.js`, so we say `foo.js` is a circular dependency of `baz.js`,
|
||||||
// and we don't need to follow it.
|
// and we don't need to follow it.
|
||||||
|
|
||||||
const modulesToFill: Module[] = entryModuleIds.map((moduleId) => {
|
let modulesToFill: Module[] = entryModuleIds.map((moduleId) => {
|
||||||
return {
|
return {
|
||||||
id: moduleId,
|
id: moduleId,
|
||||||
dependents: [],
|
dependents: [],
|
||||||
|
dependencies: [],
|
||||||
level: 0,
|
level: 0,
|
||||||
isExternal: false,
|
isExternal: false,
|
||||||
hasBeenConverted: /\.tsx?$/u.test(moduleId),
|
hasBeenConverted: /\.tsx?$/u.test(moduleId),
|
||||||
@ -208,56 +221,69 @@ function buildModulesWithLevels(
|
|||||||
const modulesById: Record<string, Module> = {};
|
const modulesById: Record<string, Module> = {};
|
||||||
|
|
||||||
while (modulesToFill.length > 0) {
|
while (modulesToFill.length > 0) {
|
||||||
const currentModule = modulesToFill.shift() as Module;
|
const currentModule = modulesToFill[0];
|
||||||
const childModulesToFill: Module[] = [];
|
|
||||||
(dependenciesByModuleId[currentModule.id] ?? []).forEach(
|
if (currentModule.level > 100) {
|
||||||
(givenChildModuleId) => {
|
throw new Error(
|
||||||
const npmPackageMatch = givenChildModuleId.match(
|
"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,
|
/^node_modules\/(?:(@[^/]+)\/)?([^/]+)\/.+$/u,
|
||||||
);
|
);
|
||||||
|
|
||||||
let childModuleId;
|
|
||||||
if (npmPackageMatch) {
|
if (npmPackageMatch) {
|
||||||
childModuleId =
|
dependencyModuleId =
|
||||||
npmPackageMatch[1] === undefined
|
npmPackageMatch[1] === undefined
|
||||||
? `${npmPackageMatch[2]}`
|
? `${npmPackageMatch[2]}`
|
||||||
: `${npmPackageMatch[1]}/${npmPackageMatch[2]}`;
|
: `${npmPackageMatch[1]}/${npmPackageMatch[2]}`;
|
||||||
} else {
|
} else {
|
||||||
childModuleId = givenChildModuleId;
|
dependencyModuleId = nonCanonicalDependencyModuleId;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip circular dependencies
|
// Skip circular dependencies
|
||||||
if (
|
if (
|
||||||
findDirectAndIndirectDependentIdsOf(currentModule).has(childModuleId)
|
findDirectAndIndirectDependentIdsOf(currentModule).has(
|
||||||
|
dependencyModuleId,
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
return;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip files that weren't on the original list of JavaScript files to
|
// 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
|
// convert, as we don't want them to show up on the status dashboard
|
||||||
if (
|
if (
|
||||||
!npmPackageMatch &&
|
!npmPackageMatch &&
|
||||||
!allowedFilePaths.includes(childModuleId.replace(/\.tsx?$/u, '.js'))
|
!allowedFilePaths.includes(
|
||||||
|
dependencyModuleId.replace(/\.tsx?$/u, '.js'),
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
return;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const existingChildModule = modulesById[childModuleId];
|
let existingDependencyModule = modulesById[dependencyModuleId];
|
||||||
|
|
||||||
if (existingChildModule === undefined) {
|
if (existingDependencyModule === undefined) {
|
||||||
const childModule: Module = {
|
existingDependencyModule = {
|
||||||
id: childModuleId,
|
id: dependencyModuleId,
|
||||||
dependents: [currentModule],
|
dependents: [currentModule],
|
||||||
|
dependencies: [],
|
||||||
level: currentModule.level + 1,
|
level: currentModule.level + 1,
|
||||||
isExternal: Boolean(npmPackageMatch),
|
isExternal: Boolean(npmPackageMatch),
|
||||||
hasBeenConverted: /\.tsx?$/u.test(childModuleId),
|
hasBeenConverted: /\.tsx?$/u.test(dependencyModuleId),
|
||||||
};
|
};
|
||||||
childModulesToFill.push(childModule);
|
dependencyModulesToFill.push(existingDependencyModule);
|
||||||
} else {
|
} else if (currentModule.level + 1 > existingDependencyModule.level) {
|
||||||
if (currentModule.level + 1 > existingChildModule.level) {
|
existingDependencyModule.level = currentModule.level + 1;
|
||||||
existingChildModule.level = currentModule.level + 1;
|
|
||||||
// Update descendant modules' levels
|
// Update descendant modules' levels
|
||||||
childModulesToFill.push(existingChildModule);
|
dependencyModulesToFill.push(existingDependencyModule);
|
||||||
} else {
|
} else {
|
||||||
// There is no point in descending into dependencies of this module
|
// There is no point in descending into dependencies of this module
|
||||||
// if the new level of the module would be <= the existing level,
|
// if the new level of the module would be <= the existing level,
|
||||||
@ -266,18 +292,30 @@ function buildModulesWithLevels(
|
|||||||
// level.
|
// level.
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!existingChildModule.dependents.includes(currentModule)) {
|
if (
|
||||||
existingChildModule.dependents.push(currentModule);
|
!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 (childModulesToFill.length > 0) {
|
|
||||||
modulesToFill.push(...childModulesToFill);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!(currentModule.id in modulesById)) {
|
if (!(currentModule.id in modulesById)) {
|
||||||
modulesById[currentModule.id] = currentModule;
|
modulesById[currentModule.id] = currentModule;
|
||||||
}
|
}
|
||||||
|
modulesToFill = modulesToFill.slice(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
return modulesById;
|
return modulesById;
|
||||||
@ -288,20 +326,21 @@ function buildModulesWithLevels(
|
|||||||
* which import that file directly or through some other file.
|
* which import that file directly or through some other file.
|
||||||
*
|
*
|
||||||
* @param module - A module in the graph.
|
* @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.
|
* the module.
|
||||||
*/
|
*/
|
||||||
function findDirectAndIndirectDependentIdsOf(module: Module): Set<string> {
|
function findDirectAndIndirectDependentIdsOf(module: Module): Set<string> {
|
||||||
const modulesToProcess = [module];
|
let modulesToProcess = [module];
|
||||||
const allDependentIds = new Set<string>();
|
const allDependentIds = new Set<string>();
|
||||||
while (modulesToProcess.length > 0) {
|
while (modulesToProcess.length > 0) {
|
||||||
const currentModule = modulesToProcess.shift() as Module;
|
const currentModule = modulesToProcess[0];
|
||||||
currentModule.dependents.forEach((dependent) => {
|
for (const dependent of currentModule.dependents) {
|
||||||
if (!allDependentIds.has(dependent.id)) {
|
if (!allDependentIds.has(dependent.id)) {
|
||||||
allDependentIds.add(dependent.id);
|
allDependentIds.add(dependent.id);
|
||||||
modulesToProcess.push(dependent);
|
modulesToProcess.push(dependent);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
modulesToProcess = modulesToProcess.slice(1);
|
||||||
}
|
}
|
||||||
return allDependentIds;
|
return allDependentIds;
|
||||||
}
|
}
|
||||||
@ -328,8 +367,22 @@ function partitionModulesByLevel(
|
|||||||
}
|
}
|
||||||
Object.values(modulesById).forEach((module) => {
|
Object.values(modulesById).forEach((module) => {
|
||||||
modulePartitions[module.level].children.push({
|
modulePartitions[module.level].children.push({
|
||||||
name: module.id,
|
id: module.id,
|
||||||
hasBeenConverted: module.hasBeenConverted,
|
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();
|
return modulePartitions.reverse();
|
||||||
|
Loading…
Reference in New Issue
Block a user