Skip to content

Commit c472ad5

Browse files
authored
[lockfile-explorer] Isolate .pnpmcfile.cjs execution and add syntax highlighter (microsoft#5366)
* Enable syntax highlighting for pnpmfile.cjs tab * Highlight package.json as well * Introduce PnpmfileRunner.ts to isolate .pnpmfile.cjs execution * rush change * Disable webpack bundle size limits, since this app is served from localhost * In a Rush repo, the ".pnpmfile.cjs" tab should show Rush's file, not the generated temp file * Upgrade Prettier to support `async using` * prettier -w . * PR feedback: use "await using" * PR feedback * PR feedback * rush change * PR feedback * Fix build break * Revert `[Symbol.asyncDispose]()` because it isn't supported by Node 18
1 parent db50a61 commit c472ad5

42 files changed

Lines changed: 1525 additions & 1000 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/lockfile-explorer-web/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"dependencies": {
1515
"@reduxjs/toolkit": "~1.8.6",
1616
"@rushstack/rush-themed-ui": "workspace:*",
17+
"prism-react-renderer": "~2.4.1",
1718
"react-dom": "~17.0.2",
1819
"react-redux": "~8.0.4",
1920
"react": "~17.0.2",
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
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 React from 'react';
5+
6+
import { Highlight, themes } from 'prism-react-renderer';
7+
8+
// Generate this list by doing console.log(Object.keys(Prism.languages))
9+
// BUT THEN DELETE the APIs that are bizarrely mixed into this namespace:
10+
// "extend", "insertBefore", "DFS"
11+
export type PrismLanguage =
12+
| 'plain'
13+
| 'plaintext'
14+
| 'text'
15+
| 'txt'
16+
| 'markup'
17+
| 'html'
18+
| 'mathml'
19+
| 'svg'
20+
| 'xml'
21+
| 'ssml'
22+
| 'atom'
23+
| 'rss'
24+
| 'regex'
25+
| 'clike'
26+
| 'javascript'
27+
| 'js'
28+
| 'actionscript'
29+
| 'coffeescript'
30+
| 'coffee'
31+
| 'javadoclike'
32+
| 'css'
33+
| 'yaml'
34+
| 'yml'
35+
| 'markdown'
36+
| 'md'
37+
| 'graphql'
38+
| 'sql'
39+
| 'typescript'
40+
| 'ts'
41+
| 'jsdoc'
42+
| 'flow'
43+
| 'n4js'
44+
| 'n4jsd'
45+
| 'jsx'
46+
| 'tsx'
47+
| 'swift'
48+
| 'kotlin'
49+
| 'kt'
50+
| 'kts'
51+
| 'c'
52+
| 'objectivec'
53+
| 'objc'
54+
| 'reason'
55+
| 'rust'
56+
| 'go'
57+
| 'cpp'
58+
| 'python'
59+
| 'py'
60+
| 'json'
61+
| 'webmanifest';
62+
63+
export const CodeBox = (props: { code: string; language: PrismLanguage }): JSX.Element => {
64+
return (
65+
<Highlight theme={themes.vsLight} code={props.code} language={props.language}>
66+
{({ className, style, tokens, getLineProps, getTokenProps }) => (
67+
<pre style={style}>
68+
{tokens.map((line, i) => (
69+
<div key={i} {...getLineProps({ line })}>
70+
{line.map((token, key) => (
71+
<span key={key} {...getTokenProps({ token })} />
72+
))}
73+
</div>
74+
))}
75+
</pre>
76+
)}
77+
</Highlight>
78+
);
79+
};

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

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

44
import React, { useCallback, useEffect, useState } from 'react';
5+
56
import { readPnpmfileAsync, readPackageSpecAsync, readPackageJsonAsync } from '../../helpers/lfxApiClient';
6-
import styles from './styles.scss';
77
import { useAppDispatch, useAppSelector } from '../../store/hooks';
88
import { selectCurrentEntry } from '../../store/slices/entrySlice';
99
import type { IPackageJson } from '../../types/IPackageJson';
@@ -13,6 +13,9 @@ import { displaySpecChanges } from '../../helpers/displaySpecChanges';
1313
import { isEntryModified } from '../../helpers/isEntryModified';
1414
import { ScrollArea, Tabs, Text } from '@rushstack/rush-themed-ui';
1515
import { LfxGraphEntryKind } from '../../packlets/lfx-shared';
16+
import { CodeBox } from './CodeBox';
17+
18+
import styles from './styles.scss';
1619

1720
const PackageView: { [key: string]: string } = {
1821
PACKAGE_JSON: 'PACKAGE_JSON',
@@ -48,9 +51,9 @@ export const PackageJsonViewer = (): JSX.Element => {
4851

4952
useEffect(() => {
5053
async function loadPackageDetailsAsync(packageName: string): Promise<void> {
51-
const packageJSONFile = await readPackageJsonAsync(packageName);
54+
const packageJSONFile: IPackageJson | undefined = await readPackageJsonAsync(packageName);
5255
setPackageJSON(packageJSONFile);
53-
const parsedJSON = await readPackageSpecAsync(packageName);
56+
const parsedJSON: IPackageJson | undefined = await readPackageSpecAsync(packageName);
5457
setParsedPackageJSON(parsedJSON);
5558

5659
if (packageJSONFile && parsedJSON) {
@@ -161,7 +164,7 @@ export const PackageJsonViewer = (): JSX.Element => {
161164
Please select a Project or Package to view it&apos;s package.json
162165
</Text>
163166
);
164-
return <pre>{JSON.stringify(packageJSON, null, 2)}</pre>;
167+
return <CodeBox code={JSON.stringify(packageJSON, null, 2)} language="json" />;
165168
case PackageView.PACKAGE_SPEC:
166169
if (!pnpmfile) {
167170
return (
@@ -171,7 +174,7 @@ export const PackageJsonViewer = (): JSX.Element => {
171174
</Text>
172175
);
173176
}
174-
return <pre>{pnpmfile}</pre>;
177+
return <CodeBox code={pnpmfile} language="js" />;
175178
case PackageView.PARSED_PACKAGE_JSON:
176179
if (!parsedPackageJSON)
177180
return (

apps/lockfile-explorer-web/src/packlets/lfx-shared/IJsonLfxWorkspace.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,15 @@ export interface IJsonLfxWorkspaceRushConfig {
1212
* Otherwise this will be an empty string.
1313
*/
1414
readonly subspaceName: string;
15+
16+
/**
17+
* The path to Rush's input file `.pnpmfile.cjs`, relative to `workspaceRootFullPath`
18+
* and normalized to use forward slashes without a leading slash. In a Rush workspace,
19+
* {@link IJsonLfxWorkspace.pnpmfilePath} is a temporary file that is generated from `rushPnpmfilePath`.
20+
*
21+
* @example `"common/config/my-subspace/pnpm-lock.yaml"`
22+
*/
23+
readonly rushPnpmfilePath: string;
1524
}
1625

1726
export interface IJsonLfxWorkspace {
@@ -44,6 +53,14 @@ export interface IJsonLfxWorkspace {
4453
*/
4554
readonly pnpmLockfileFolder: string;
4655

56+
/**
57+
* The path to the `.pnpmfile.cjs` file that is loaded by PNPM. In a Rush workspace,
58+
* this is a temporary file that is generated from `rushPnpmfilePath`.
59+
*
60+
* @example `"common/temp/my-subspace/.pnpmfile.cjs"`
61+
*/
62+
readonly pnpmfilePath: string;
63+
4764
/**
4865
* This section will be defined only if this is a Rush workspace (versus a plain PNPM workspace).
4966
*/

apps/lockfile-explorer-web/webpack.config.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,11 @@ module.exports = function createConfig(env, argv) {
1717
}
1818
},
1919
performance: {
20-
hints: env.production ? 'error' : false
20+
hints: env.production ? 'error' : false,
2121
// This specifies the bundle size limit that will trigger Webpack's warning saying:
2222
// "The following entrypoint(s) combined asset size exceeds the recommended limit."
23-
// maxEntrypointSize: 500000,
24-
// maxAssetSize: 500000
23+
maxEntrypointSize: Infinity,
24+
maxAssetSize: Infinity
2525
},
2626
devServer: {
2727
port: 8096,

apps/lockfile-explorer/src/cli/explorer/ExplorerCommandLineParser.ts

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,27 @@ import cors from 'cors';
77
import process from 'process';
88
import open from 'open';
99
import updateNotifier from 'update-notifier';
10-
10+
import * as path from 'node:path';
1111
import { FileSystem, type IPackageJson, JsonFile, PackageJsonLookup } from '@rushstack/node-core-library';
1212
import { ConsoleTerminalProvider, type ITerminal, Terminal, Colorize } from '@rushstack/terminal';
1313
import {
1414
type CommandLineFlagParameter,
1515
CommandLineParser,
1616
type IRequiredCommandLineStringParameter
1717
} from '@rushstack/ts-command-line';
18+
1819
import {
1920
type LfxGraph,
2021
lfxGraphSerializer,
2122
type IAppContext,
22-
type IJsonLfxGraph
23+
type IJsonLfxGraph,
24+
type IJsonLfxWorkspace
2325
} from '../../../build/lfx-shared';
26+
import * as lockfilePath from '../../graph/lockfilePath';
2427

2528
import type { IAppState } from '../../state';
2629
import { init } from '../../utils/init';
30+
import { PnpmfileRunner } from '../../graph/PnpmfileRunner';
2731
import * as lfxGraphLoader from '../../graph/lfxGraphLoader';
2832

2933
const EXPLORER_TOOL_FILENAME: 'lockfile-explorer' = 'lockfile-explorer';
@@ -98,6 +102,8 @@ export class ExplorerCommandLineParser extends CommandLineParser {
98102
subspaceName: this._subspaceParameter.value
99103
});
100104

105+
const lfxWorkspace: IJsonLfxWorkspace = appState.lfxWorkspace;
106+
101107
// Important: This must happen after init() reads the current working directory
102108
process.chdir(appState.lockfileExplorerProjectRoot);
103109

@@ -153,7 +159,7 @@ export class ExplorerCommandLineParser extends CommandLineParser {
153159
const pnpmLockfileText: string = await FileSystem.readFileAsync(appState.pnpmLockfileLocation);
154160
const lockfile: unknown = yaml.load(pnpmLockfileText) as unknown;
155161

156-
const graph: LfxGraph = lfxGraphLoader.generateLockfileGraph(lockfile, appState.lfxWorkspace);
162+
const graph: LfxGraph = lfxGraphLoader.generateLockfileGraph(lockfile, lfxWorkspace);
157163

158164
const jsonGraph: IJsonLfxGraph = lfxGraphSerializer.serializeToJson(graph);
159165
res.type('application/json').send(jsonGraph);
@@ -183,13 +189,18 @@ export class ExplorerCommandLineParser extends CommandLineParser {
183189
);
184190

185191
app.get('/api/pnpmfile', async (req: express.Request, res: express.Response) => {
192+
const pnpmfilePath: string = lockfilePath.join(
193+
lfxWorkspace.workspaceRootFullPath,
194+
lfxWorkspace.rushConfig?.rushPnpmfilePath ?? lfxWorkspace.pnpmfilePath
195+
);
196+
186197
let pnpmfile: string;
187198
try {
188-
pnpmfile = await FileSystem.readFileAsync(appState.pnpmfileLocation);
199+
pnpmfile = await FileSystem.readFileAsync(pnpmfilePath);
189200
} catch (e) {
190201
if (FileSystem.isNotExistError(e)) {
191202
return res.status(404).send({
192-
message: `Could not load pnpmfile file in this repo.`,
203+
message: `Could not load .pnpmfile.cjs file in this repo: "${pnpmfilePath}"`,
193204
error: `No .pnpmifile.cjs found.`
194205
});
195206
} else {
@@ -218,10 +229,18 @@ export class ExplorerCommandLineParser extends CommandLineParser {
218229
}
219230
}
220231

221-
const {
222-
hooks: { readPackage }
223-
} = require(appState.pnpmfileLocation);
224-
const parsedPackage: {} = readPackage(packageJson, {});
232+
let parsedPackage: IPackageJson = packageJson;
233+
234+
const pnpmfilePath: string = path.join(lfxWorkspace.workspaceRootFullPath, lfxWorkspace.pnpmfilePath);
235+
if (await FileSystem.existsAsync(pnpmfilePath)) {
236+
const pnpmFileRunner: PnpmfileRunner = new PnpmfileRunner(pnpmfilePath);
237+
try {
238+
parsedPackage = await pnpmFileRunner.transformPackageAsync(packageJson, fileLocation);
239+
} finally {
240+
await pnpmFileRunner.disposeAsync();
241+
}
242+
}
243+
225244
res.send(parsedPackage);
226245
}
227246
);
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
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 { IPackageJson } from '@rushstack/node-core-library';
5+
6+
export interface IReadPackageContext {
7+
log: (message: string) => void;
8+
}
9+
10+
export type IReadPackageHook = (
11+
packageJson: IPackageJson,
12+
context: IReadPackageContext
13+
) => IPackageJson | Promise<IPackageJson>;
14+
15+
export interface IPnpmHooks {
16+
readPackage?: IReadPackageHook;
17+
}
18+
19+
/**
20+
* Type of the `.pnpmfile.cjs` module.
21+
*/
22+
export interface IPnpmfileModule {
23+
hooks?: IPnpmHooks;
24+
}

0 commit comments

Comments
 (0)