Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions apps/lockfile-explorer-web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,21 @@
"_phase:test": "heft run --only test -- --clean"
},
"dependencies": {
"react": "~17.0.2",
"react-dom": "~17.0.2",
"@lifaon/path": "~2.1.0",
"@reduxjs/toolkit": "~1.8.6",
"@rushstack/rush-themed-ui": "workspace:*",
"react-dom": "~17.0.2",
"react-redux": "~8.0.4",
"react": "~17.0.2",
"redux": "~4.2.0",
"@rushstack/rush-themed-ui": "workspace:*"
"tslib": "~2.8.1"
},
"devDependencies": {
"@rushstack/heft": "workspace:*",
"@types/react": "17.0.74",
"@types/react-dom": "17.0.25",
"eslint": "~9.25.1",
"local-web-rig": "workspace:*"
"local-web-rig": "workspace:*",
"typescript": "5.8.2"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

Copy link
Member

@iclanton iclanton Sep 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That isn't a problem in other Rush repos. VSCode should be able to fall back to TypeScript that it ships with.

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import React, { useCallback, useEffect, useState } from 'react';
import { Button, Text } from '@rushstack/rush-themed-ui';
import styles from './styles.scss';
import appStyles from '../../App.scss';
import { checkAliveAsync } from '../../parsing/getPackageFiles';
import { checkAliveAsync } from '../../helpers/lfxApiClient';
import type { ReactNull } from '../../types/ReactNull';

export const ConnectionModal = (): JSX.Element | ReactNull => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import React, { useCallback } from 'react';
import appStyles from '../../App.scss';
import styles from './styles.scss';
import { useAppDispatch, useAppSelector } from '../../store/hooks';
import type { LockfileEntry } from '../../parsing/LockfileEntry';
import type { LockfileEntry } from '../../parsing/LfxGraph';
import { clearStackAndPush, removeBookmark } from '../../store/slices/entrySlice';
import { Button, ScrollArea, Text } from '@rushstack/rush-themed-ui';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ import React, { useCallback, useEffect, useState } from 'react';
import { ScrollArea, Text } from '@rushstack/rush-themed-ui';
import styles from './styles.scss';
import appStyles from '../../App.scss';
import { IDependencyType, type LockfileDependency } from '../../parsing/LockfileDependency';
import { readPackageJsonAsync } from '../../parsing/getPackageFiles';
import { DependencyKind, type LockfileDependency } from '../../parsing/LfxGraph';
import { readPackageJsonAsync } from '../../helpers/lfxApiClient';
import { useAppDispatch, useAppSelector } from '../../store/hooks';
import { pushToStack, selectCurrentEntry } from '../../store/slices/entrySlice';
import { ReactNull } from '../../types/ReactNull';
import type { LockfileEntry } from '../../parsing/LockfileEntry';
import type { LockfileEntry } from '../../parsing/LfxGraph';
import { logDiagnosticInfo } from '../../helpers/logDiagnosticInfo';
import { displaySpecChanges } from '../../helpers/displaySpecChanges';
import type { IPackageJson } from '../../types/IPackageJson';
Expand Down Expand Up @@ -80,7 +80,7 @@ export const LockfileEntryDetailsView = (): JSX.Element | ReactNull => {

// Check if we need to calculate influencers.
// If the current dependencyToTrace is a peer dependency then we do
if (dependencyToTrace.dependencyType !== IDependencyType.PEER_DEPENDENCY) {
if (dependencyToTrace.dependencyType !== DependencyKind.PEER_DEPENDENCY) {
return;
}

Expand Down Expand Up @@ -179,7 +179,7 @@ export const LockfileEntryDetailsView = (): JSX.Element | ReactNull => {
package.json spec:{' '}
</Text>
<Text type="span">
{inspectDependency.dependencyType === IDependencyType.PEER_DEPENDENCY
{inspectDependency.dependencyType === DependencyKind.PEER_DEPENDENCY
? `"${inspectDependency.peerDependencyMeta.version}" ${
inspectDependency.peerDependencyMeta.optional ? 'Optional' : 'Required'
} Peer`
Expand All @@ -204,7 +204,7 @@ export const LockfileEntryDetailsView = (): JSX.Element | ReactNull => {
const renderPeerDependencies = (): JSX.Element | ReactNull => {
if (!selectedEntry) return ReactNull;
const peerDeps = selectedEntry.dependencies.filter(
(d) => d.dependencyType === IDependencyType.PEER_DEPENDENCY
(d) => d.dependencyType === DependencyKind.PEER_DEPENDENCY
);
if (!peerDeps.length) {
return (
Expand All @@ -213,7 +213,7 @@ export const LockfileEntryDetailsView = (): JSX.Element | ReactNull => {
</div>
);
}
if (!inspectDependency || inspectDependency.dependencyType !== IDependencyType.PEER_DEPENDENCY) {
if (!inspectDependency || inspectDependency.dependencyType !== DependencyKind.PEER_DEPENDENCY) {
return (
<div className={`${appStyles.ContainerCard} ${styles.InfluencerList}`}>
<Text type="h5">Select a peer dependency to view its influencers</Text>
Expand Down Expand Up @@ -338,7 +338,7 @@ export const LockfileEntryDetailsView = (): JSX.Element | ReactNull => {
>
<Text type="h5" bold>
Name: {dependency.name}{' '}
{dependency.dependencyType === IDependencyType.PEER_DEPENDENCY
{dependency.dependencyType === DependencyKind.PEER_DEPENDENCY
? `${
dependency.peerDependencyMeta.optional ? '(Optional)' : '(Non-optional)'
} Peer Dependency`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

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

const reducedEntries = filteredEntries.reduce((groups: { [key in string]: LockfileEntry[] }, item) => {
const reducedEntries = filteredEntries.reduce((groups: { [key: string]: LockfileEntry[] }, item) => {
const group = groups[item.entryPackageName] || [];
group.push(item);
groups[item.entryPackageName] = group;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// See LICENSE in the project root for license information.

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

const PackageView: { [key in string]: string } = {
const PackageView: { [key: string]: string } = {
PACKAGE_JSON: 'PACKAGE_JSON',
PACKAGE_SPEC: 'PACKAGE_SPEC',
PARSED_PACKAGE_JSON: 'PARSED_PACKAGE_JSON'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// See LICENSE in the project root for license information.

import type { ISpecChange } from '../parsing/compareSpec';
import type { LockfileEntry } from '../parsing/LockfileEntry';
import type { LockfileEntry } from '../parsing/LfxGraph';

export const isEntryModified = (
entry: LockfileEntry | undefined,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,55 @@
// See LICENSE in the project root for license information.

import type { IPackageJson } from '../types/IPackageJson';
import type { ILfxWorkspace } from '../types/lfxProtocol';

const apiPath: string = `${window.appContext.serviceUrl}/api`;
const SERVICE_URL: string = window.appContext.serviceUrl;

export async function checkAliveAsync(): Promise<boolean> {
try {
await fetch(`${apiPath}/health`);
await fetch(`${SERVICE_URL}/api/health`);
return true;
} catch (e) {
return false;
}
}

/**
* Read the contents of a text file under the workspace directory.
* @param relativePath - a file path that is relative to the working directory.
*/
export async function readWorkspaceConfigAsync(): Promise<ILfxWorkspace> {
let response: Response;

try {
response = await fetch(`${SERVICE_URL}/api/workspace`);
if (!response.ok) {
const responseText: string = await response.text();
const error = new Error(
'The operation failed: ' + (responseText.trim() || 'An unknown error occurred')
);
// eslint-disable-next-line no-console
console.error('readWorkspaceConfigAsync() failed: ', error);
throw error;
}
} catch (e) {
// eslint-disable-next-line no-console
console.error('Network error in readWorkspaceConfigAsync(): ', e);
throw new Error('Network error: ' + (e.message || 'An unknown error occurred'));
}

const responseJson: ILfxWorkspace = await response.json();
return responseJson;
}

/**
* Fetches a projects configuration files from the local file system
*
* @returns a json object representing a package.json or a text file to be rendered (in the case of readPnpmfile)
*/
export async function readPnpmfileAsync(): Promise<string> {
try {
const response = await fetch(`${apiPath}/pnpmfile`);
const response = await fetch(`${SERVICE_URL}/api/pnpmfile`);
return await response.text();
} catch (e) {
// eslint-disable-next-line no-console
Expand All @@ -32,7 +61,7 @@ export async function readPnpmfileAsync(): Promise<string> {

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

export async function readPackageSpecAsync(projectPath: string): Promise<IPackageJson | undefined> {
try {
const response = await fetch(`${apiPath}/package-spec`, {
const response = await fetch(`${SERVICE_URL}/api/package-spec`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
Expand Down
2 changes: 1 addition & 1 deletion apps/lockfile-explorer-web/src/helpers/localStorage.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.

import { type LockfileEntry, LockfileEntryFilter } from '../parsing/LockfileEntry';
import { type LockfileEntry, LockfileEntryFilter } from '../parsing/LfxGraph';

const BOOKMARK_KEY: string = 'LOCKFILE_EXPLORER_BOOKMARKS';

Expand Down
50 changes: 50 additions & 0 deletions apps/lockfile-explorer-web/src/parsing/JsonLfxGraph.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.

import type { DependencyKind, LockfileEntryFilter } from './LfxGraph';

export interface IJsonPeerDependencyMeta {
name?: string;
version?: string;
optional?: boolean;
}

export interface IJsonLfxDependency {
name: string;
version: string;
entryId: string;
dependencyType: DependencyKind;

resolvedEntryJsonId?: number;

peerDependencyMeta: IJsonPeerDependencyMeta;
}

export interface IJsonLfxEntry {
/**
* A unique ID used when serializing graph links.
*
* @remarks
* This is just the `IJsonLfxGraph.entries` array index, but debugging is easier if we include
* it in the serialized representation.
*/
jsonId: number;

kind: LockfileEntryFilter;
entryId: string;
rawEntryId: string;
packageJsonFolderPath: string;
entryPackageName: string;
displayText: string;
entryPackageVersion: string;
entrySuffix: string;

// Lists
dependencies: IJsonLfxDependency[];
transitivePeerDependencies: string[];
referrerJsonIds: number[];
}

export interface IJsonLfxGraph {
entries: IJsonLfxEntry[];
}
Loading
Loading