Skip to content

Commit 39a0b6b

Browse files
authored
[lockfile-explorer] Architecture improvents (#5357)
* Add launch.json * Rename getPackageFiles.ts -> lfxApiClient.ts * Fix broken devServer configuration * Fix broken IntelliSense due to missing dev dependency * Initial work on replacing server-only IAppState with client ILfxWorkspace * Add missing tslib dependency; bump to tslib 2.8.1 * Extract the parsing/construction from LockfileEntry.ts and LockfileDependency.ts into lfxGraphLoader.ts (preparing to later move this code from client to server) * Normalize TSDoc * Rename IDependencyType -> DependencyKind * Consolidate LockfileDependency.ts and LockfileEntry.ts into LfxGraph.ts * Implement JSON serializer/deserializer * rush change * Remove debug code * Revert version bump that broke test * PR feedback * PR feedback: eliminate path.resolve() and fix some Rush API docs * Code cleanup, fix "key in string" mistake * Revert change that didn't compile * PR feedback: Make Lfxgraph more readonly * PR feedback: Detect "6.1" without accidentally detecting "60.0" * Fix lint error
1 parent 436afaa commit 39a0b6b

39 files changed

Lines changed: 1218 additions & 712 deletions

File tree

apps/lockfile-explorer-web/package.json

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,21 @@
1212
"_phase:test": "heft run --only test -- --clean"
1313
},
1414
"dependencies": {
15-
"react": "~17.0.2",
16-
"react-dom": "~17.0.2",
1715
"@lifaon/path": "~2.1.0",
1816
"@reduxjs/toolkit": "~1.8.6",
17+
"@rushstack/rush-themed-ui": "workspace:*",
18+
"react-dom": "~17.0.2",
1919
"react-redux": "~8.0.4",
20+
"react": "~17.0.2",
2021
"redux": "~4.2.0",
21-
"@rushstack/rush-themed-ui": "workspace:*"
22+
"tslib": "~2.8.1"
2223
},
2324
"devDependencies": {
2425
"@rushstack/heft": "workspace:*",
2526
"@types/react": "17.0.74",
2627
"@types/react-dom": "17.0.25",
2728
"eslint": "~9.25.1",
28-
"local-web-rig": "workspace:*"
29+
"local-web-rig": "workspace:*",
30+
"typescript": "5.8.2"
2931
}
3032
}

apps/lockfile-explorer-web/src/components/ConnectionModal/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import React, { useCallback, useEffect, useState } from 'react';
55
import { Button, Text } from '@rushstack/rush-themed-ui';
66
import styles from './styles.scss';
77
import appStyles from '../../App.scss';
8-
import { checkAliveAsync } from '../../parsing/getPackageFiles';
8+
import { checkAliveAsync } from '../../helpers/lfxApiClient';
99
import type { ReactNull } from '../../types/ReactNull';
1010

1111
export const ConnectionModal = (): JSX.Element | ReactNull => {

apps/lockfile-explorer-web/src/containers/BookmarksSidebar/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import React, { useCallback } from 'react';
55
import appStyles from '../../App.scss';
66
import styles from './styles.scss';
77
import { useAppDispatch, useAppSelector } from '../../store/hooks';
8-
import type { LockfileEntry } from '../../parsing/LockfileEntry';
8+
import type { LockfileEntry } from '../../parsing/LfxGraph';
99
import { clearStackAndPush, removeBookmark } from '../../store/slices/entrySlice';
1010
import { Button, ScrollArea, Text } from '@rushstack/rush-themed-ui';
1111

apps/lockfile-explorer-web/src/containers/LockfileEntryDetailsView/index.tsx

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@ import React, { useCallback, useEffect, useState } from 'react';
55
import { ScrollArea, Text } from '@rushstack/rush-themed-ui';
66
import styles from './styles.scss';
77
import appStyles from '../../App.scss';
8-
import { IDependencyType, type LockfileDependency } from '../../parsing/LockfileDependency';
9-
import { readPackageJsonAsync } from '../../parsing/getPackageFiles';
8+
import { DependencyKind, type LockfileDependency } from '../../parsing/LfxGraph';
9+
import { readPackageJsonAsync } from '../../helpers/lfxApiClient';
1010
import { useAppDispatch, useAppSelector } from '../../store/hooks';
1111
import { pushToStack, selectCurrentEntry } from '../../store/slices/entrySlice';
1212
import { ReactNull } from '../../types/ReactNull';
13-
import type { LockfileEntry } from '../../parsing/LockfileEntry';
13+
import type { LockfileEntry } from '../../parsing/LfxGraph';
1414
import { logDiagnosticInfo } from '../../helpers/logDiagnosticInfo';
1515
import { displaySpecChanges } from '../../helpers/displaySpecChanges';
1616
import type { IPackageJson } from '../../types/IPackageJson';
@@ -80,7 +80,7 @@ export const LockfileEntryDetailsView = (): JSX.Element | ReactNull => {
8080

8181
// Check if we need to calculate influencers.
8282
// If the current dependencyToTrace is a peer dependency then we do
83-
if (dependencyToTrace.dependencyType !== IDependencyType.PEER_DEPENDENCY) {
83+
if (dependencyToTrace.dependencyType !== DependencyKind.PEER_DEPENDENCY) {
8484
return;
8585
}
8686

@@ -179,7 +179,7 @@ export const LockfileEntryDetailsView = (): JSX.Element | ReactNull => {
179179
package.json spec:{' '}
180180
</Text>
181181
<Text type="span">
182-
{inspectDependency.dependencyType === IDependencyType.PEER_DEPENDENCY
182+
{inspectDependency.dependencyType === DependencyKind.PEER_DEPENDENCY
183183
? `"${inspectDependency.peerDependencyMeta.version}" ${
184184
inspectDependency.peerDependencyMeta.optional ? 'Optional' : 'Required'
185185
} Peer`
@@ -204,7 +204,7 @@ export const LockfileEntryDetailsView = (): JSX.Element | ReactNull => {
204204
const renderPeerDependencies = (): JSX.Element | ReactNull => {
205205
if (!selectedEntry) return ReactNull;
206206
const peerDeps = selectedEntry.dependencies.filter(
207-
(d) => d.dependencyType === IDependencyType.PEER_DEPENDENCY
207+
(d) => d.dependencyType === DependencyKind.PEER_DEPENDENCY
208208
);
209209
if (!peerDeps.length) {
210210
return (
@@ -213,7 +213,7 @@ export const LockfileEntryDetailsView = (): JSX.Element | ReactNull => {
213213
</div>
214214
);
215215
}
216-
if (!inspectDependency || inspectDependency.dependencyType !== IDependencyType.PEER_DEPENDENCY) {
216+
if (!inspectDependency || inspectDependency.dependencyType !== DependencyKind.PEER_DEPENDENCY) {
217217
return (
218218
<div className={`${appStyles.ContainerCard} ${styles.InfluencerList}`}>
219219
<Text type="h5">Select a peer dependency to view its influencers</Text>
@@ -338,7 +338,7 @@ export const LockfileEntryDetailsView = (): JSX.Element | ReactNull => {
338338
>
339339
<Text type="h5" bold>
340340
Name: {dependency.name}{' '}
341-
{dependency.dependencyType === IDependencyType.PEER_DEPENDENCY
341+
{dependency.dependencyType === DependencyKind.PEER_DEPENDENCY
342342
? `${
343343
dependency.peerDependencyMeta.optional ? '(Optional)' : '(Non-optional)'
344344
} Peer Dependency`

apps/lockfile-explorer-web/src/containers/LockfileViewer/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
import React, { useCallback, useEffect, useRef, useState } from 'react';
55
import styles from './styles.scss';
6-
import { type LockfileEntry, LockfileEntryFilter } from '../../parsing/LockfileEntry';
6+
import { type LockfileEntry, LockfileEntryFilter } from '../../parsing/LfxGraph';
77
import { ReactNull } from '../../types/ReactNull';
88
import { useAppDispatch, useAppSelector } from '../../store/hooks';
99
import {
@@ -120,7 +120,7 @@ export const LockfileViewer = (): JSX.Element | ReactNull => {
120120
filteredEntries = entries.filter((entry) => entry.entryPackageName.indexOf(packageFilter) !== -1);
121121
}
122122

123-
const reducedEntries = filteredEntries.reduce((groups: { [key in string]: LockfileEntry[] }, item) => {
123+
const reducedEntries = filteredEntries.reduce((groups: { [key: string]: LockfileEntry[] }, item) => {
124124
const group = groups[item.entryPackageName] || [];
125125
group.push(item);
126126
groups[item.entryPackageName] = group;

apps/lockfile-explorer-web/src/containers/PackageJsonViewer/index.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// See LICENSE in the project root for license information.
33

44
import React, { useCallback, useEffect, useState } from 'react';
5-
import { readPnpmfileAsync, readPackageSpecAsync, readPackageJsonAsync } from '../../parsing/getPackageFiles';
5+
import { readPnpmfileAsync, readPackageSpecAsync, readPackageJsonAsync } from '../../helpers/lfxApiClient';
66
import styles from './styles.scss';
77
import { useAppDispatch, useAppSelector } from '../../store/hooks';
88
import { selectCurrentEntry } from '../../store/slices/entrySlice';
@@ -12,9 +12,9 @@ import { loadSpecChanges } from '../../store/slices/workspaceSlice';
1212
import { displaySpecChanges } from '../../helpers/displaySpecChanges';
1313
import { isEntryModified } from '../../helpers/isEntryModified';
1414
import { ScrollArea, Tabs, Text } from '@rushstack/rush-themed-ui';
15-
import { LockfileEntryFilter } from '../../parsing/LockfileEntry';
15+
import { LockfileEntryFilter } from '../../parsing/LfxGraph';
1616

17-
const PackageView: { [key in string]: string } = {
17+
const PackageView: { [key: string]: string } = {
1818
PACKAGE_JSON: 'PACKAGE_JSON',
1919
PACKAGE_SPEC: 'PACKAGE_SPEC',
2020
PARSED_PACKAGE_JSON: 'PARSED_PACKAGE_JSON'

apps/lockfile-explorer-web/src/helpers/isEntryModified.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// See LICENSE in the project root for license information.
33

44
import type { ISpecChange } from '../parsing/compareSpec';
5-
import type { LockfileEntry } from '../parsing/LockfileEntry';
5+
import type { LockfileEntry } from '../parsing/LfxGraph';
66

77
export const isEntryModified = (
88
entry: LockfileEntry | undefined,

apps/lockfile-explorer-web/src/parsing/getPackageFiles.ts renamed to apps/lockfile-explorer-web/src/helpers/lfxApiClient.ts

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,55 @@
22
// See LICENSE in the project root for license information.
33

44
import type { IPackageJson } from '../types/IPackageJson';
5+
import type { ILfxWorkspace } from '../types/lfxProtocol';
56

6-
const apiPath: string = `${window.appContext.serviceUrl}/api`;
7+
const SERVICE_URL: string = window.appContext.serviceUrl;
78

89
export async function checkAliveAsync(): Promise<boolean> {
910
try {
10-
await fetch(`${apiPath}/health`);
11+
await fetch(`${SERVICE_URL}/api/health`);
1112
return true;
1213
} catch (e) {
1314
return false;
1415
}
1516
}
1617

18+
/**
19+
* Read the contents of a text file under the workspace directory.
20+
* @param relativePath - a file path that is relative to the working directory.
21+
*/
22+
export async function readWorkspaceConfigAsync(): Promise<ILfxWorkspace> {
23+
let response: Response;
24+
25+
try {
26+
response = await fetch(`${SERVICE_URL}/api/workspace`);
27+
if (!response.ok) {
28+
const responseText: string = await response.text();
29+
const error = new Error(
30+
'The operation failed: ' + (responseText.trim() || 'An unknown error occurred')
31+
);
32+
// eslint-disable-next-line no-console
33+
console.error('readWorkspaceConfigAsync() failed: ', error);
34+
throw error;
35+
}
36+
} catch (e) {
37+
// eslint-disable-next-line no-console
38+
console.error('Network error in readWorkspaceConfigAsync(): ', e);
39+
throw new Error('Network error: ' + (e.message || 'An unknown error occurred'));
40+
}
41+
42+
const responseJson: ILfxWorkspace = await response.json();
43+
return responseJson;
44+
}
45+
1746
/**
1847
* Fetches a projects configuration files from the local file system
1948
*
2049
* @returns a json object representing a package.json or a text file to be rendered (in the case of readPnpmfile)
2150
*/
2251
export async function readPnpmfileAsync(): Promise<string> {
2352
try {
24-
const response = await fetch(`${apiPath}/pnpmfile`);
53+
const response = await fetch(`${SERVICE_URL}/api/pnpmfile`);
2554
return await response.text();
2655
} catch (e) {
2756
// eslint-disable-next-line no-console
@@ -32,7 +61,7 @@ export async function readPnpmfileAsync(): Promise<string> {
3261

3362
export async function readPackageJsonAsync(projectPath: string): Promise<IPackageJson | undefined> {
3463
try {
35-
const response = await fetch(`${apiPath}/package-json`, {
64+
const response = await fetch(`${SERVICE_URL}/api/package-json`, {
3665
method: 'POST',
3766
headers: {
3867
'Content-Type': 'application/json'
@@ -51,7 +80,7 @@ export async function readPackageJsonAsync(projectPath: string): Promise<IPackag
5180

5281
export async function readPackageSpecAsync(projectPath: string): Promise<IPackageJson | undefined> {
5382
try {
54-
const response = await fetch(`${apiPath}/package-spec`, {
83+
const response = await fetch(`${SERVICE_URL}/api/package-spec`, {
5584
method: 'POST',
5685
headers: {
5786
'Content-Type': 'application/json'

apps/lockfile-explorer-web/src/helpers/localStorage.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
22
// See LICENSE in the project root for license information.
33

4-
import { type LockfileEntry, LockfileEntryFilter } from '../parsing/LockfileEntry';
4+
import { type LockfileEntry, LockfileEntryFilter } from '../parsing/LfxGraph';
55

66
const BOOKMARK_KEY: string = 'LOCKFILE_EXPLORER_BOOKMARKS';
77

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
2+
// See LICENSE in the project root for license information.
3+
4+
import type { DependencyKind, LockfileEntryFilter } from './LfxGraph';
5+
6+
export interface IJsonPeerDependencyMeta {
7+
name?: string;
8+
version?: string;
9+
optional?: boolean;
10+
}
11+
12+
export interface IJsonLfxDependency {
13+
name: string;
14+
version: string;
15+
entryId: string;
16+
dependencyType: DependencyKind;
17+
18+
resolvedEntryJsonId?: number;
19+
20+
peerDependencyMeta: IJsonPeerDependencyMeta;
21+
}
22+
23+
export interface IJsonLfxEntry {
24+
/**
25+
* A unique ID used when serializing graph links.
26+
*
27+
* @remarks
28+
* This is just the `IJsonLfxGraph.entries` array index, but debugging is easier if we include
29+
* it in the serialized representation.
30+
*/
31+
jsonId: number;
32+
33+
kind: LockfileEntryFilter;
34+
entryId: string;
35+
rawEntryId: string;
36+
packageJsonFolderPath: string;
37+
entryPackageName: string;
38+
displayText: string;
39+
entryPackageVersion: string;
40+
entrySuffix: string;
41+
42+
// Lists
43+
dependencies: IJsonLfxDependency[];
44+
transitivePeerDependencies: string[];
45+
referrerJsonIds: number[];
46+
}
47+
48+
export interface IJsonLfxGraph {
49+
entries: IJsonLfxEntry[];
50+
}

0 commit comments

Comments
 (0)