Skip to content

Commit 73c5b0e

Browse files
authored
Convert npm and yarn helpers to TypeScript & enforce prettier (#14493)
* Convert npm and yarn helpers to typescript * Remove unused npm6:update helper * Use type packages when available * Use provided types in pnpm helper * Reduce usage of any * Apply prettier & check formatting in CI * Update README with new dev guidance * Properly lint typescript files * Remove unnecessary npm build files from the built image * Revert "Remove unnecessary npm build files from the built image"
1 parent a8005e1 commit 73c5b0e

62 files changed

Lines changed: 2991 additions & 1426 deletions

Some content is hidden

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

npm_and_yarn/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@
55
/dependabot-*.gem
66
/helpers/node_modules
77
/helpers/install-dir
8+
/helpers/dist
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
dist/
2+
test/pnpm/fixtures/**/pnpm-lock.yaml
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"trailingComma": "es5"
3+
}

npm_and_yarn/helpers/README.md

Lines changed: 57 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,70 @@
1-
Native JavaScript helpers
2-
-------------------------
1+
## Native TypeScript helpers
32

4-
This directory contains helper functions for npm and yarn, natively written in
5-
Javascript so that we can utilize the package managers internal APIs and other
3+
This directory contains helper functions for npm, yarn, and pnpm, written in
4+
TypeScript, so that we can utilize the package managers' internal APIs and other
65
native tooling for these ecosystems.
76

8-
These helpers are called from the Ruby code via `run.js`, they are passed
7+
These helpers are called from the Ruby code via `run.ts`, they are passed
98
arguments via stdin and return JSON data to stdout.
109

11-
## Testing
10+
## Development
11+
12+
Install dependencies:
13+
14+
```
15+
npm install
16+
```
17+
18+
### Building
19+
20+
The helpers are compiled from TypeScript to JavaScript before being used at
21+
runtime. To build:
22+
23+
```
24+
npm run build
25+
```
26+
27+
The compiled output goes to `dist/`.
28+
29+
### Type checking
30+
31+
Run the TypeScript compiler in check-only mode (no output):
32+
33+
```
34+
npm run typecheck
35+
```
36+
37+
### Linting
38+
39+
ESLint is used for code quality checks:
40+
41+
```
42+
npm run lint
43+
```
44+
45+
### Formatting
46+
47+
Prettier is used for code formatting. To check for formatting issues:
48+
49+
```
50+
npm run format
51+
```
52+
53+
To auto-fix formatting:
54+
55+
```
56+
npm run format:fix
57+
```
58+
59+
### Testing
1260

1361
When working on these helpers, it's convenient to write some high level tests in
14-
JavaScript to make it easier to debug the code.
62+
TypeScript to make it easier to debug the code.
1563

16-
You can now run the tests from this directory by running:
64+
Run the tests from this directory:
1765

1866
```
19-
yarn test path/to/test.js
67+
npm test
2068
```
2169

2270
### Debugging

npm_and_yarn/helpers/build

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ helpers_dir="$(dirname "${BASH_SOURCE[0]}")"
1414
cp -r \
1515
"$helpers_dir/lib" \
1616
"$helpers_dir/test" \
17-
"$helpers_dir/run.js" \
17+
"$helpers_dir/run.ts" \
18+
"$helpers_dir/tsconfig.json" \
19+
"$helpers_dir/tsconfig.build.json" \
1820
"$helpers_dir/eslint.config.js" \
1921
"$helpers_dir/jest.config.js" \
2022
"$helpers_dir/package.json" \
@@ -24,3 +26,4 @@ cp -r \
2426

2527
cd "$install_dir"
2628
npm ci --fetch-timeout=600000 --fetch-retries=5
29+
npm run build
Lines changed: 29 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,37 @@
11
const globals = require("globals");
22
const { defineConfig } = require("eslint/config");
33
const js = require("@eslint/js");
4+
const tseslint = require("typescript-eslint");
45
const eslintConfigPrettier = require("eslint-config-prettier/flat");
56

6-
// Rules not included before upgrading to ESLint 9. Can be enabled later
7-
const temporaryDisabledRules = {
7+
module.exports = defineConfig([
8+
js.configs.recommended,
9+
tseslint.configs.recommended,
10+
{
11+
languageOptions: {
12+
globals: {
13+
...globals.node,
14+
...globals.jest,
15+
},
16+
},
17+
},
18+
{
819
rules: {
9-
"no-unused-vars": "off",
10-
"no-extra-boolean-cast": "off",
11-
"no-undef": "off"
20+
"@typescript-eslint/no-unused-vars": [
21+
"error",
22+
{ argsIgnorePattern: "^_", destructuredArrayIgnorePattern: "^_" },
23+
],
24+
"@typescript-eslint/consistent-type-imports": "error",
1225
},
13-
}
14-
15-
module.exports = defineConfig([
16-
js.configs.recommended,
17-
{
18-
languageOptions: {
19-
globals: {
20-
...globals.node,
21-
...globals.jest
22-
}
23-
},
26+
},
27+
eslintConfigPrettier,
28+
{
29+
files: ["**/*.js"],
30+
rules: {
31+
"@typescript-eslint/no-require-imports": "off",
2432
},
25-
temporaryDisabledRules,
26-
eslintConfigPrettier
27-
])
33+
},
34+
{
35+
ignores: ["dist/**"],
36+
},
37+
]);

npm_and_yarn/helpers/jest.config.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,11 @@ module.exports = {
22
verbose: true,
33
rootDir: "test",
44
testEnvironment: "node",
5+
transform: {
6+
"^.+\\.ts$": "ts-jest",
7+
},
8+
moduleFileExtensions: ["ts", "js", "json"],
9+
moduleNameMapper: {
10+
"^(\\.{1,2}/.*)\\.js$": "$1",
11+
},
512
};

npm_and_yarn/helpers/lib/npm/conflicting-dependency-parser.js

Lines changed: 0 additions & 78 deletions
This file was deleted.
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/* Conflicting dependency parser for npm
2+
*
3+
* Inputs:
4+
* - directory containing a package.json and a yarn.lock
5+
* - dependency name
6+
* - target dependency version
7+
*
8+
* Outputs:
9+
* - An array of objects with conflicting dependencies
10+
*/
11+
12+
import Arborist from "@npmcli/arborist";
13+
import semver from "semver";
14+
15+
interface ConflictingDependency {
16+
explanation: string;
17+
name: string;
18+
version: string;
19+
requirement: string;
20+
}
21+
22+
export async function findConflictingDependencies(
23+
directory: string,
24+
depName: string,
25+
targetVersion: string
26+
): Promise<ConflictingDependency[]> {
27+
const arb = new Arborist({
28+
path: directory,
29+
dryRun: true,
30+
ignoreScripts: true,
31+
});
32+
33+
return await arb.loadVirtual().then((tree) => {
34+
const parents: ConflictingDependency[] = [];
35+
for (const node of tree.inventory.query("name", depName)) {
36+
for (const edge of node.edgesIn) {
37+
if (!semver.satisfies(targetVersion, edge.spec)) {
38+
findTopLevelEdges(edge).forEach((topLevel) => {
39+
const explanation = buildExplanation(node, edge, topLevel);
40+
41+
parents.push({
42+
explanation: explanation,
43+
name: edge.from!.name,
44+
version: edge.from!.version,
45+
requirement: edge.spec,
46+
});
47+
});
48+
}
49+
}
50+
}
51+
52+
return parents;
53+
});
54+
}
55+
56+
function buildExplanation(
57+
node: Arborist.Node,
58+
directEdge: Arborist.Edge,
59+
topLevelEdge: Arborist.Edge
60+
): string {
61+
if (directEdge.from === topLevelEdge.to) {
62+
// The nodes parent is top-level
63+
return `${directEdge.from!.name}@${directEdge.from!.version} requires ${directEdge.to!.name}@${directEdge.spec}`;
64+
} else if (topLevelEdge.to!.edgesOut.has(directEdge.from!.name)) {
65+
// The nodes parent is a direct dependency of the top-level dependency
66+
return (
67+
`${topLevelEdge.to!.name}@${topLevelEdge.to!.version} requires ${directEdge.to!.name}@${directEdge.spec} ` +
68+
`via ${directEdge.from!.name}@${directEdge.from!.version}`
69+
);
70+
} else {
71+
// The nodes parent is a transitive dependency of the top-level dependency
72+
return (
73+
`${topLevelEdge.to!.name}@${topLevelEdge.to!.version} requires ${directEdge.to!.name}@${directEdge.spec} ` +
74+
`via a transitive dependency on ${directEdge.from!.name}@${directEdge.from!.version}`
75+
);
76+
}
77+
}
78+
79+
function findTopLevelEdges(
80+
edge: Arborist.Edge,
81+
parents: Arborist.Edge[] = []
82+
): Arborist.Edge[] {
83+
edge.from!.edgesIn.forEach((parent) => {
84+
if (parent.from!.edgesIn.size === 0) {
85+
if (!parents.includes(parent)) {
86+
parents.push(parent);
87+
}
88+
} else {
89+
findTopLevelEdges(parent, parents);
90+
}
91+
});
92+
93+
return parents;
94+
}

npm_and_yarn/helpers/lib/npm/index.js

Lines changed: 0 additions & 9 deletions
This file was deleted.

0 commit comments

Comments
 (0)