Skip to content

Commit 810d8c4

Browse files
authored
feat: add replacements plugin (#32)
Adds a replacements plugin which detects replaceable modules. This only checks the root `package.json` for now, and only `dependencies` (not `devDependencies`).
1 parent 8d799a8 commit 810d8c4

7 files changed

Lines changed: 128 additions & 20 deletions

File tree

src/analyze/attw.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,18 @@ import {
66
import {groupProblemsByKind} from '@arethetypeswrong/core/utils';
77
import {filterProblems, problemKindInfo} from '@arethetypeswrong/core/problems';
88
import {Message} from '../types.js';
9+
import type {FileSystem} from '../file-system.js';
10+
import {TarballFileSystem} from '../tarball-file-system.js';
911

10-
export async function runAttw(tarball: ArrayBuffer) {
12+
export async function runAttw(fileSystem: FileSystem) {
1113
const messages: Message[] = [];
1214

13-
const pkg = createPackageFromTarballData(new Uint8Array(tarball));
15+
// Only support tarballs for now
16+
if (!(fileSystem instanceof TarballFileSystem)) {
17+
return messages;
18+
}
19+
20+
const pkg = createPackageFromTarballData(new Uint8Array(fileSystem.tarball));
1421
const result = await checkPackage(pkg);
1522

1623
if (result.types === false) {

src/analyze/dependencies.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ export async function analyzeDependencies(
5353
let pkg: PackageJsonLike;
5454

5555
try {
56-
pkg = JSON.parse(await fileSystem.readFile(rootDir + '/package.json'));
56+
pkg = JSON.parse(await fileSystem.readFile('/package.json'));
5757
} catch {
5858
throw new Error('No package.json found.');
5959
}

src/analyze/publint.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
import {publint} from 'publint';
22
import {formatMessage} from 'publint/utils';
33
import {Message} from '../types.js';
4+
import type {FileSystem} from '../file-system.js';
5+
import {TarballFileSystem} from '../tarball-file-system.js';
46

5-
export async function runPublint(tarball: ArrayBuffer) {
7+
export async function runPublint(fileSystem: FileSystem) {
68
const messages: Message[] = [];
79

8-
const result = await publint({pack: {tarball}});
10+
if (!(fileSystem instanceof TarballFileSystem)) {
11+
return messages;
12+
}
13+
14+
const result = await publint({pack: {tarball: fileSystem.tarball}});
915
for (const problem of result.messages) {
1016
messages.push({
1117
severity: problem.type,

src/analyze/replacements.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import * as replacements from 'module-replacements';
2+
import {Message, PackageJsonLike} from '../types.js';
3+
import type {FileSystem} from '../file-system.js';
4+
5+
/**
6+
* Generates a standard URL to the docs of a given rule
7+
* @param {string} name Rule name
8+
* @return {string}
9+
*/
10+
export function getDocsUrl(name: string): string {
11+
return `https://github.com/es-tooling/eslint-plugin-depend/blob/main/docs/rules/${name}.md`;
12+
}
13+
14+
/**
15+
* Generates a URL for the given path on MDN
16+
* @param {string} path Docs path
17+
* @return {string}
18+
*/
19+
export function getMdnUrl(path: string): string {
20+
return `https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/${path}`;
21+
}
22+
23+
export async function runReplacements(fileSystem: FileSystem) {
24+
const messages: Message[] = [];
25+
26+
let packageJsonText: string;
27+
28+
try {
29+
packageJsonText = await fileSystem.readFile('/package.json');
30+
} catch {
31+
// No package.json found
32+
return messages;
33+
}
34+
35+
let packageJson: PackageJsonLike;
36+
37+
try {
38+
packageJson = JSON.parse(packageJsonText);
39+
} catch {
40+
// Not parseable
41+
return messages;
42+
}
43+
44+
if (!packageJson.dependencies) {
45+
// No dependencies
46+
return messages;
47+
}
48+
49+
for (const name of Object.keys(packageJson.dependencies)) {
50+
const replacement = replacements.all.moduleReplacements.find(
51+
(replacement) => replacement.moduleName === name
52+
);
53+
54+
if (!replacement) {
55+
continue;
56+
}
57+
58+
if (replacement.type === 'none') {
59+
messages.push({
60+
severity: 'warning',
61+
score: 0,
62+
message: `Module "${name}" can be removed, and native functionality used instead`
63+
});
64+
} else if (replacement.type === 'simple') {
65+
messages.push({
66+
severity: 'warning',
67+
score: 0,
68+
message: `Module "${name}" can be replaced. ${replacement.replacement}.`
69+
});
70+
} else if (replacement.type === 'native') {
71+
const mdnPath = getMdnUrl(replacement.mdnPath);
72+
// TODO (43081j): support `nodeVersion` here, check it against the
73+
// packageJson.engines field, if there is one.
74+
messages.push({
75+
severity: 'warning',
76+
score: 0,
77+
message: `Module "${name}" can be replaced with native functionality. Use "${replacement.replacement}" instead. You can read more at ${mdnPath}.`
78+
});
79+
} else if (replacement.type === 'documented') {
80+
const docUrl = getDocsUrl(replacement.docPath);
81+
messages.push({
82+
severity: 'warning',
83+
score: 0,
84+
message: `Module "${name}" can be replaced with a more performant alternative. See the list of available alternatives at ${docUrl}.`
85+
});
86+
}
87+
}
88+
89+
return messages;
90+
}

src/analyze/report.ts

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@ import type {FileSystem} from '../file-system.js';
77
import {Message, Options} from '../types.js';
88
import {runAttw} from './attw.js';
99
import {runPublint} from './publint.js';
10+
import {runReplacements} from './replacements.js';
1011

11-
export type ReportPlugin = (tarball: ArrayBuffer) => Promise<Message[]>;
12+
export type ReportPlugin = (fileSystem: FileSystem) => Promise<Message[]>;
1213

1314
export interface ReportResult {
1415
info: {
@@ -20,13 +21,11 @@ export interface ReportResult {
2021
dependencies: DependencyStats;
2122
}
2223

23-
const plugins: ReportPlugin[] = [runAttw, runPublint];
24+
const plugins: ReportPlugin[] = [runAttw, runPublint, runReplacements];
2425

2526
async function computeInfo(fileSystem: FileSystem) {
26-
const rootDir = await fileSystem.getRootDir();
27-
2827
try {
29-
const pkgJson = await fileSystem.readFile(rootDir + '/package.json');
28+
const pkgJson = await fileSystem.readFile('/package.json');
3029
const pkg = JSON.parse(pkgJson);
3130
return {
3231
name: pkg.name,
@@ -56,13 +55,13 @@ export async function report(options: Options) {
5655
}
5756

5857
fileSystem = new TarballFileSystem(tarball);
58+
}
5959

60-
for (const plugin of plugins) {
61-
const result = await plugin(tarball);
60+
for (const plugin of plugins) {
61+
const result = await plugin(fileSystem);
6262

63-
for (const message of result) {
64-
messages.push(message);
65-
}
63+
for (const message of result) {
64+
messages.push(message);
6665
}
6766
}
6867

src/local-file-system.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,8 @@ export class LocalFileSystem implements FileSystem {
4343
}
4444
}
4545

46-
async readFile(path: string): Promise<string> {
47-
return await readFile(path, 'utf8');
46+
async readFile(filePath: string): Promise<string> {
47+
return await readFile(path.join(this.#root, filePath), 'utf8');
4848
}
4949

5050
async getInstallSize(): Promise<number> {

src/tarball-file-system.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
import {unpack, type UnpackResult} from '@publint/pack';
22
import type {FileSystem} from './file-system.js';
3+
import path from 'node:path';
34

45
export class TarballFileSystem implements FileSystem {
56
#tarball: ArrayBuffer;
67
#unpackResult?: UnpackResult;
78

9+
public get tarball(): ArrayBuffer {
10+
return this.#tarball;
11+
}
12+
813
constructor(tarball: ArrayBuffer) {
914
this.#tarball = tarball;
1015
}
@@ -29,11 +34,12 @@ export class TarballFileSystem implements FileSystem {
2934
.map((file) => file.name);
3035
}
3136

32-
async readFile(path: string): Promise<string> {
37+
async readFile(filePath: string): Promise<string> {
3338
const {files} = await this.#getUnpackResult();
34-
const file = files.find((f) => f.name === path);
39+
const fullPath = path.join(await this.getRootDir(), filePath);
40+
const file = files.find((f) => f.name === fullPath);
3541
if (!file) {
36-
throw new Error(`File not found: ${path}`);
42+
throw new Error(`File not found: ${filePath}`);
3743
}
3844
return new TextDecoder().decode(file.data);
3945
}

0 commit comments

Comments
 (0)