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
28 changes: 16 additions & 12 deletions src/analyze/attw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,45 +5,49 @@ import {
} from '@arethetypeswrong/core';
import {groupProblemsByKind} from '@arethetypeswrong/core/utils';
import {filterProblems, problemKindInfo} from '@arethetypeswrong/core/problems';
import {Message} from '../types.js';
import {ReportPluginResult} from '../types.js';
import type {FileSystem} from '../file-system.js';
import {TarballFileSystem} from '../tarball-file-system.js';

export async function runAttw(fileSystem: FileSystem) {
const messages: Message[] = [];
export async function runAttw(
fileSystem: FileSystem
): Promise<ReportPluginResult> {
const result: ReportPluginResult = {
messages: []
};

// Only support tarballs for now
if (!(fileSystem instanceof TarballFileSystem)) {
return messages;
return result;
}

const pkg = createPackageFromTarballData(new Uint8Array(fileSystem.tarball));
const result = await checkPackage(pkg);
const attwResult = await checkPackage(pkg);

if (result.types === false) {
messages.push({
if (attwResult.types === false) {
result.messages.push({
severity: 'suggestion',
score: 0,
message: `No type definitions found.`
});
} else {
const subpaths = Object.keys(result.entrypoints);
const subpaths = Object.keys(attwResult.entrypoints);

for (const subpath of subpaths) {
const resolutions = result.entrypoints[subpath].resolutions;
const resolutions = attwResult.entrypoints[subpath].resolutions;

for (const resolutionKind in resolutions) {
const problemsForMatrix = Object.entries(
groupProblemsByKind(
filterProblems(result, {
filterProblems(attwResult, {
resolutionKind: resolutionKind as ResolutionKind,
entrypoint: subpath
})
)
);
for (const [_kind, problems] of problemsForMatrix) {
for (const problem of problems) {
messages.push({
result.messages.push({
severity: 'error',
score: 0,
message:
Expand All @@ -56,5 +60,5 @@ export async function runAttw(fileSystem: FileSystem) {
}
}

return messages;
return result;
}
115 changes: 91 additions & 24 deletions src/analyze/dependencies.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,30 @@
import colors from 'picocolors';
import {analyzePackageModuleType} from '../compute-type.js';
import type {
DependencyStats,
DependencyAnalyzer,
PackageJsonLike,
DependencyNode,
DuplicateDependency
ReportPluginResult,
Message,
Stat
} from '../types.js';
import {FileSystem} from '../file-system.js';

/**
* This file contains dependency analysis functionality.
*/
interface DependencyNode {
name: string;
version: string;
// TODO (43081j): make this an array or something structured one day
path: string; // Path in dependency tree (e.g., "root > package-a > package-b")
parent?: string; // Parent package name
depth: number; // Depth in dependency tree
packagePath: string; // File system path to package.json
}

// Re-export types
export type {DependencyStats, DependencyAnalyzer};
interface DuplicateDependency {
name: string;
versions: DependencyNode[];
severity: 'exact' | 'conflict' | 'resolvable';
potentialSavings?: number;
suggestions?: string[];
}

/**
* Detects duplicate dependencies from a list of dependency nodes
Expand Down Expand Up @@ -134,11 +145,12 @@ async function parsePackageJson(
}

// Keep the existing tarball analysis for backward compatibility
export async function analyzeDependencies(
export async function runDependencyAnalysis(
fileSystem: FileSystem
): Promise<DependencyStats> {
): Promise<ReportPluginResult> {
const packageFiles = await fileSystem.listPackageFiles();
const rootDir = await fileSystem.getRootDir();
const messages: Message[] = [];

// Find root package.json
const pkg = await parsePackageJson(fileSystem, '/package.json');
Expand All @@ -148,8 +160,35 @@ export async function analyzeDependencies(
}

const installSize = await fileSystem.getInstallSize();
const directDependencies = Object.keys(pkg.dependencies || {}).length;
const prodDependencies = Object.keys(pkg.dependencies || {}).length;
const devDependencies = Object.keys(pkg.devDependencies || {}).length;
const stats: Stat[] = [
{
name: 'packageName',
label: 'Package Name',
value: pkg.name
},
{
name: 'version',
label: 'Version',
value: pkg.version
},
{
name: 'installSize',
label: 'Install Size',
value: installSize
},
{
name: 'prodDependencies',
label: 'Prod. Dependencies',
value: prodDependencies
},
{
name: 'devDependencies',
label: 'Dev. Dependencies',
value: devDependencies
}
];

let cjsDependencies = 0;
let esmDependencies = 0;
Expand Down Expand Up @@ -268,20 +307,48 @@ export async function analyzeDependencies(
// Detect duplicates from the collected dependency nodes
const duplicateDependencies = detectDuplicates(dependencyNodes);

const result: DependencyStats = {
totalDependencies: directDependencies + devDependencies,
directDependencies,
devDependencies,
cjsDependencies,
esmDependencies,
installSize,
packageName: pkg.name,
version: pkg.version
};
stats.push({
name: 'cjsDependencies',
label: 'CJS Dependencies',
value: cjsDependencies
});
stats.push({
name: 'esmDependencies',
label: 'ESM Dependencies',
value: esmDependencies
});

if (duplicateDependencies.length > 0) {
result.duplicateDependencies = duplicateDependencies;
stats.push({
name: 'duplicateDependencies',
label: 'Duplicate Dependencies',
value: duplicateDependencies.length
});

for (const duplicate of duplicateDependencies) {
const severityColor =
duplicate.severity === 'exact' ? colors.blue : colors.yellow;

let message = `${severityColor('[duplicate dependency]')} ${colors.bold(duplicate.name)} has ${duplicate.versions.length} installed versions:`;

for (const version of duplicate.versions) {
message += `\n ${colors.gray(version.version)} via ${colors.gray(version.path)}`;
}

if (duplicate.suggestions && duplicate.suggestions.length > 0) {
message += '\nSuggestions:';
for (const suggestion of duplicate.suggestions) {
message += ` ${colors.blue('💡')} ${colors.gray(suggestion)}`;
}
}

messages.push({
message,
severity: 'warning',
score: 0
});
}
}

return result;
return {stats, messages};
}
22 changes: 13 additions & 9 deletions src/analyze/publint.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,28 @@
import {publint} from 'publint';
import {formatMessage} from 'publint/utils';
import {Message} from '../types.js';
import {ReportPluginResult} from '../types.js';
import type {FileSystem} from '../file-system.js';
import {TarballFileSystem} from '../tarball-file-system.js';

export async function runPublint(fileSystem: FileSystem) {
const messages: Message[] = [];
export async function runPublint(
fileSystem: FileSystem
): Promise<ReportPluginResult> {
const result: ReportPluginResult = {
messages: []
};

if (!(fileSystem instanceof TarballFileSystem)) {
return messages;
return result;
}

const result = await publint({pack: {tarball: fileSystem.tarball}});
for (const problem of result.messages) {
messages.push({
const publintResult = await publint({pack: {tarball: fileSystem.tarball}});
for (const problem of publintResult.messages) {
result.messages.push({
severity: problem.type,
score: 0,
message: formatMessage(problem, result.pkg) ?? ''
message: formatMessage(problem, publintResult.pkg) ?? ''
});
}

return messages;
return result;
}
26 changes: 15 additions & 11 deletions src/analyze/replacements.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as replacements from 'module-replacements';
import {Message, PackageJsonLike} from '../types.js';
import {PackageJsonLike, ReportPluginResult} from '../types.js';
import type {FileSystem} from '../file-system.js';

/**
Expand All @@ -20,16 +20,20 @@ export function getMdnUrl(path: string): string {
return `https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/${path}`;
}

export async function runReplacements(fileSystem: FileSystem) {
const messages: Message[] = [];
export async function runReplacements(
fileSystem: FileSystem
): Promise<ReportPluginResult> {
const result: ReportPluginResult = {
messages: []
};

let packageJsonText: string;

try {
packageJsonText = await fileSystem.readFile('/package.json');
} catch {
// No package.json found
return messages;
return result;
}

let packageJson: PackageJsonLike;
Expand All @@ -38,12 +42,12 @@ export async function runReplacements(fileSystem: FileSystem) {
packageJson = JSON.parse(packageJsonText);
} catch {
// Not parseable
return messages;
return result;
}

if (!packageJson.dependencies) {
// No dependencies
return messages;
return result;
}

for (const name of Object.keys(packageJson.dependencies)) {
Expand All @@ -56,13 +60,13 @@ export async function runReplacements(fileSystem: FileSystem) {
}

if (replacement.type === 'none') {
messages.push({
result.messages.push({
severity: 'warning',
score: 0,
message: `Module "${name}" can be removed, and native functionality used instead`
});
} else if (replacement.type === 'simple') {
messages.push({
result.messages.push({
severity: 'warning',
score: 0,
message: `Module "${name}" can be replaced. ${replacement.replacement}.`
Expand All @@ -71,20 +75,20 @@ export async function runReplacements(fileSystem: FileSystem) {
const mdnPath = getMdnUrl(replacement.mdnPath);
// TODO (43081j): support `nodeVersion` here, check it against the
// packageJson.engines field, if there is one.
messages.push({
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}.`
});
} else if (replacement.type === 'documented') {
const docUrl = getDocsUrl(replacement.docPath);
messages.push({
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}.`
});
}
}

return messages;
return result;
}
Loading