Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export default [
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-unused-vars': [
'error',
{varsIgnorePattern: '^[A-Z_]'}
{varsIgnorePattern: '^[A-Z_]', argsIgnorePattern: '^_'}
]
}
}
Expand Down
5 changes: 3 additions & 2 deletions src/analyze/attw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ import {
} from '@arethetypeswrong/core';
import {groupProblemsByKind} from '@arethetypeswrong/core/utils';
import {filterProblems, problemKindInfo} from '@arethetypeswrong/core/problems';
import {ReportPluginResult} from '../types.js';
import {ReportPluginResult, type Options} from '../types.js';
import type {FileSystem} from '../file-system.js';
import {TarballFileSystem} from '../tarball-file-system.js';

export async function runAttw(
fileSystem: FileSystem
fileSystem: FileSystem,
_options?: Options
): Promise<ReportPluginResult> {
const result: ReportPluginResult = {
messages: []
Expand Down
6 changes: 4 additions & 2 deletions src/analyze/dependencies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import type {
PackageJsonLike,
ReportPluginResult,
Message,
Stats
Stats,
Options
} from '../types.js';
import {FileSystem} from '../file-system.js';
import {normalizePath} from '../utils/path.js';
Expand Down Expand Up @@ -147,7 +148,8 @@ async function parsePackageJson(

// Keep the existing tarball analysis for backward compatibility
export async function runDependencyAnalysis(
fileSystem: FileSystem
fileSystem: FileSystem,
_options?: Options
): Promise<ReportPluginResult> {
const packageFiles = await fileSystem.listPackageFiles();
const rootDir = await fileSystem.getRootDir();
Expand Down
5 changes: 3 additions & 2 deletions src/analyze/publint.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import {publint} from 'publint';
import {formatMessage} from 'publint/utils';
import {ReportPluginResult} from '../types.js';
import {ReportPluginResult, type Options} from '../types.js';
import type {FileSystem} from '../file-system.js';
import {TarballFileSystem} from '../tarball-file-system.js';

export async function runPublint(
fileSystem: FileSystem
fileSystem: FileSystem,
_options?: Options
): Promise<ReportPluginResult> {
const result: ReportPluginResult = {
messages: []
Expand Down
73 changes: 66 additions & 7 deletions src/analyze/replacements.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
import * as replacements from 'module-replacements';
import type {ManifestModule, ModuleReplacement} from 'module-replacements';
import {ReportPluginResult} from '../types.js';
import type {FileSystem} from '../file-system.js';
import {getPackageJson} from '../file-system-utils.js';
import semverSatisfies from 'semver/functions/satisfies.js';
import semverLessThan from 'semver/ranges/ltr.js';
import {minVersion, validRange} from 'semver';
import {resolve, dirname, basename} from 'node:path';
import {
satisfies as semverSatisfies,
ltr as semverLessThan,
minVersion,
validRange
} from 'semver';
import {LocalFileSystem} from '../local-file-system.js';
import type {Options} from '../types.js';

/**
* Generates a standard URL to the docs of a given rule
Expand All @@ -24,6 +31,42 @@ export function getMdnUrl(path: string): string {
return `https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/${path}`;
}

async function loadCustomManifests(
manifestPaths?: string[]
): Promise<ModuleReplacement[]> {
if (!manifestPaths || manifestPaths.length === 0) {
return [];
}

const customReplacements: ModuleReplacement[] = [];

for (const manifestPath of manifestPaths) {
try {
const absolutePath = resolve(manifestPath);
const manifestDir = dirname(absolutePath);
const manifestFileName = basename(absolutePath);
const localFileSystem = new LocalFileSystem(manifestDir);
const manifestContent = await localFileSystem.readFile(
`/${manifestFileName}`
);
const manifest: ManifestModule = JSON.parse(manifestContent);

if (
manifest.moduleReplacements &&
Array.isArray(manifest.moduleReplacements)
) {
customReplacements.push(...manifest.moduleReplacements);
}
} catch (error) {
console.warn(
`Warning: Failed to load custom manifest from ${manifestPath}: ${error}`
);
}
}

return customReplacements;
}

function isNodeEngineCompatible(
requiredNode: string,
enginesNode: string
Expand All @@ -47,7 +90,8 @@ function isNodeEngineCompatible(
}

export async function runReplacements(
fileSystem: FileSystem
fileSystem: FileSystem,
options?: Options
): Promise<ReportPluginResult> {
const result: ReportPluginResult = {
messages: []
Expand All @@ -60,15 +104,26 @@ export async function runReplacements(
return result;
}

// Load custom manifests
const customReplacements = await loadCustomManifests(options?.manifest);

// Combine custom and built-in replacements
const allReplacements = [
...customReplacements,
...replacements.all.moduleReplacements
];

for (const name of Object.keys(packageJson.dependencies)) {
const replacement = replacements.all.moduleReplacements.find(
// Find replacement (custom replacements take precedence due to order)
const replacement = allReplacements.find(
(replacement) => replacement.moduleName === name
);

if (!replacement) {
continue;
}

// Handle each replacement type using the same logic for both custom and built-in
if (replacement.type === 'none') {
result.messages.push({
severity: 'warning',
Expand Down Expand Up @@ -101,17 +156,21 @@ export async function runReplacements(
replacement.nodeVersion && !enginesNode
? ` Required Node >= ${replacement.nodeVersion}.`
: '';
const message = `Module "${name}" can be replaced with native functionality. Use "${replacement.replacement}" instead.${requires}`;
const fullMessage = `${message} You can read more at ${mdnPath}.`;
result.messages.push({
severity: 'warning',
score: 0,
message: `Module "${name}" can be replaced with native functionality. Use "${replacement.replacement}" instead. You can read more at ${mdnPath}.${requires}`
message: fullMessage
});
} else if (replacement.type === 'documented') {
const docUrl = getDocsUrl(replacement.docPath);
const message = `Module "${name}" can be replaced with a more performant alternative.`;
const fullMessage = `${message} See the list of available alternatives at ${docUrl}.`;
result.messages.push({
severity: 'warning',
score: 0,
message: `Module "${name}" can be replaced with a more performant alternative. See the list of available alternatives at ${docUrl}.`
message: fullMessage
});
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/analyze/report.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export async function report(options: Options) {
}

for (const plugin of plugins) {
const result = await plugin(fileSystem);
const result = await plugin(fileSystem, options);

for (const message of result.messages) {
messages.push(message);
Expand Down
6 changes: 6 additions & 0 deletions src/commands/analyze.meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ export const meta = {
choices: ['debug', 'info', 'warn', 'error'],
default: 'info',
description: 'Set the log level (debug | info | warn | error)'
},
manifest: {
type: 'string',
array: true,
description:
'Path(s) to custom manifest file(s) for module replacements analysis'
}
}
} as const;
18 changes: 16 additions & 2 deletions src/commands/analyze.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,22 @@ export async function run(ctx: CommandContext<typeof meta.args>) {
}
}

// Analyze
const {stats, messages} = await report({root, pack});
// Then analyze the tarball
const rawCustomManifests = ctx.values['manifest'];

// NOTE: Gunshi quirk - array arguments are sometimes returned as single strings
// when only one value is provided, so we need to handle both cases
const customManifests = Array.isArray(rawCustomManifests)
? rawCustomManifests
: rawCustomManifests
? [rawCustomManifests]
: [];

const {stats, messages} = await report({
root,
pack,
manifest: customManifests
});

prompts.log.info('Summary');

Expand Down
106 changes: 106 additions & 0 deletions src/test/__snapshots__/custom-manifests.test.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`Custom Manifests > should handle invalid manifest files gracefully 1`] = `[]`;

exports[`Custom Manifests > should load and use custom manifest files 1`] = `
[
{
"message": "Module "@e18e/fake-0" can be replaced. Use picocolors, kleur, or native console styling.",
"score": 0,
"severity": "warning",
},
{
"message": "Module "@e18e/fake-1" can be replaced. Use native JavaScript methods or specific lodash functions.",
"score": 0,
"severity": "warning",
},
{
"message": "Module "@e18e/fake-2" can be replaced with native functionality. Use "Intl.DateTimeFormat or Date methods" instead. Required Node >= 12.0.0. You can read more at https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat.",
"score": 0,
"severity": "warning",
},
{
"message": "Module "@e18e/fake-3" can be replaced with a more performant alternative. See the list of available alternatives at https://github.com/es-tooling/eslint-plugin-depend/blob/main/docs/rules/request-alternatives.md.",
"score": 0,
"severity": "warning",
},
{
"message": "Module "@e18e/fake-4" can be removed, and native functionality used instead",
"score": 0,
"severity": "warning",
},
]
`;

exports[`Custom Manifests > should load multiple manifest files 1`] = `
[
{
"message": "Module "@e18e/fake-0" can be replaced. Use picocolors, kleur, or native console styling.",
"score": 0,
"severity": "warning",
},
{
"message": "Module "@e18e/fake-1" can be replaced. Use native JavaScript methods or specific lodash functions.",
"score": 0,
"severity": "warning",
},
{
"message": "Module "@e18e/fake-2" can be replaced with native functionality. Use "Intl.DateTimeFormat or Date methods" instead. Required Node >= 12.0.0. You can read more at https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat.",
"score": 0,
"severity": "warning",
},
{
"message": "Module "@e18e/fake-3" can be replaced with a more performant alternative. See the list of available alternatives at https://github.com/es-tooling/eslint-plugin-depend/blob/main/docs/rules/request-alternatives.md.",
"score": 0,
"severity": "warning",
},
{
"message": "Module "@e18e/fake-4" can be removed, and native functionality used instead",
"score": 0,
"severity": "warning",
},
{
"message": "Module "@e18e/fake-5" can be replaced. Use Fastify, Koa, or native Node.js http module.",
"score": 0,
"severity": "warning",
},
{
"message": "Module "@e18e/fake-6" can be removed, and native functionality used instead",
"score": 0,
"severity": "warning",
},
]
`;

exports[`Custom Manifests > should prioritize custom replacements over built-in ones 1`] = `
{
"withCustom": [
{
"message": "Module "@e18e/fake-0" can be replaced. Use picocolors, kleur, or native console styling.",
"score": 0,
"severity": "warning",
},
{
"message": "Module "@e18e/fake-1" can be replaced. Use native JavaScript methods or specific lodash functions.",
"score": 0,
"severity": "warning",
},
{
"message": "Module "@e18e/fake-2" can be replaced with native functionality. Use "Intl.DateTimeFormat or Date methods" instead. Required Node >= 12.0.0. You can read more at https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat.",
"score": 0,
"severity": "warning",
},
{
"message": "Module "@e18e/fake-3" can be replaced with a more performant alternative. See the list of available alternatives at https://github.com/es-tooling/eslint-plugin-depend/blob/main/docs/rules/request-alternatives.md.",
"score": 0,
"severity": "warning",
},
{
"message": "Module "@e18e/fake-4" can be removed, and native functionality used instead",
"score": 0,
"severity": "warning",
},
],
"withoutCustom": [],
}
`;
Loading