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
1,408 changes: 1,352 additions & 56 deletions package-lock.json

Large diffs are not rendered by default.

6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,14 @@
"@clack/prompts": "https://pkg.pr.new/bombshell-dev/clack/@clack/prompts@276",
"@publint/pack": "^0.1.2",
"fdir": "^6.4.5",
"gunshi": "^0.14.3",
"gunshi": "^0.26.3",
"module-replacements-codemods": "^1.1.0",
"package-manager-detector": "^1.1.0",
"picocolors": "^1.1.1",
"pino": "^9.7.0",
"pino-pretty": "^13.0.0",
"publint": "^0.3.9"
"publint": "^0.3.9",
"tinyglobby": "^0.2.14"
},
"devDependencies": {
"@types/node": "^22.13.10",
Expand Down
32 changes: 18 additions & 14 deletions src/analyze-dependencies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import fsSync from 'node:fs';
import path from 'node:path';
import {unpack} from '@publint/pack';
import {analyzePackageModuleType} from './compute-type.js';
import { pino } from 'pino';
import {pino} from 'pino';
import type {DependencyStats, DependencyAnalyzer} from './types.js';
import { fdir } from 'fdir';
import {fdir} from 'fdir';

// Create a logger instance with pretty printing for development
const logger = pino({
Expand All @@ -14,27 +14,27 @@ const logger = pino({
options: {
colorize: true,
translateTime: 'SYS:standard',
ignore: 'pid,hostname',
},
},
ignore: 'pid,hostname'
}
}
});

/**
* This file contains dependency analysis functionality.
*
*
* To enable debug logging for dependency analysis:
* ```typescript
* // Enable all debug logs
* logger.level = 'debug';
*
*
* // Or create a specific logger for dependency analysis
* const analyzerLogger = logger.child({ module: 'analyzer' });
* analyzerLogger.level = 'debug';
* ```
*/

// Re-export types
export type { DependencyStats, DependencyAnalyzer };
export type {DependencyStats, DependencyAnalyzer};

export class LocalDependencyAnalyzer implements DependencyAnalyzer {
async analyzeDependencies(root: string): Promise<DependencyStats> {
Expand Down Expand Up @@ -66,7 +66,9 @@ export class LocalDependencyAnalyzer implements DependencyAnalyzer {
await this.walkNodeModules(nodeModulesPath, {
onPackage: (pkgJson) => {
const type = analyzePackageModuleType(pkgJson);
logger.debug(`Package ${pkgJson.name}: ${type} (type=${pkgJson.type}, main=${pkgJson.main}, exports=${JSON.stringify(pkgJson.exports)})`);
logger.debug(
`Package ${pkgJson.name}: ${type} (type=${pkgJson.type}, main=${pkgJson.main}, exports=${JSON.stringify(pkgJson.exports)})`
);

if (type === 'cjs') cjsDependencies++;
if (type === 'esm') esmDependencies++;
Expand Down Expand Up @@ -118,10 +120,7 @@ export class LocalDependencyAnalyzer implements DependencyAnalyzer {
seenPackages = new Set<string>()
) {
try {
const crawler = new fdir()
.withFullPaths()
.withSymlinks()
.crawl(dir);
const crawler = new fdir().withFullPaths().withSymlinks().crawl(dir);

const files = await crawler.withPromise();

Expand All @@ -135,7 +134,12 @@ export class LocalDependencyAnalyzer implements DependencyAnalyzer {
logger.debug('Detected package:', pkgJson.name, 'at', filePath);
callbacks.onPackage(pkgJson);
} else {
logger.debug('Already seen package:', pkgJson.name, 'at', filePath);
logger.debug(
'Already seen package:',
pkgJson.name,
'at',
filePath
);
}
} catch {
logger.debug('Error reading package.json:', filePath);
Expand Down
227 changes: 28 additions & 199 deletions src/cli.ts
Original file line number Diff line number Diff line change
@@ -1,219 +1,48 @@
import fs from 'node:fs/promises';
import {createRequire} from 'node:module';
import {cli, define} from 'gunshi';
import {cli, define, lazy, type LazyCommand} from 'gunshi';
import * as prompts from '@clack/prompts';
import c from 'picocolors';
import {report} from './index.js';
import type {PackType} from './types.js';
import {LocalDependencyAnalyzer} from './analyze-dependencies.js';
import { pino } from 'pino';
import {meta as analyzeMeta} from './commands/analyze.meta.js';
import {meta as migrateMeta} from './commands/migrate.meta.js';

const version = createRequire(import.meta.url)('../package.json').version;
const allowedPackTypes: PackType[] = ['auto', 'npm', 'yarn', 'pnpm', 'bun'];

// Create a logger instance with pretty printing for development
const logger = pino({
transport: {
target: 'pino-pretty',
options: {
colorize: true,
translateTime: 'SYS:standard',
ignore: 'pid,hostname',
},
},
});

const defaultCommand = define({
options: {
pack: {
type: 'string',
default: 'auto',
description: `Package manager to use for packing ('auto' | 'npm' | 'yarn' | 'pnpm' | 'bun')`
},
'log-level': {
type: 'string',
default: 'info',
description: 'Set the log level (debug | info | warn | error)'
}
},
async run(ctx) {
const root = ctx.positionals[0];
let pack = ctx.values.pack as PackType;
const logLevel = ctx.values['log-level'];

// Set the logger level based on the option
logger.level = logLevel;

prompts.intro('Generating report...');

if (typeof pack === 'string' && !allowedPackTypes.includes(pack)) {
prompts.cancel(
`Invalid '--pack' option. Allowed values are: ${allowedPackTypes.join(', ')}`
);
process.exit(1);
}

// If a path is passed, it must be a tarball file
let isTarball = false;
if (root) {
try {
const stat = await fs.stat(root);
if (stat.isFile()) {
const buffer = await fs.readFile(root);
pack = {tarball: buffer.buffer};
isTarball = true;
} else {
// Not a file, exit
prompts.cancel(
`When '--pack file' is used, a path to a tarball file must be passed.`
);
process.exit(1);
}
} catch (error) {
prompts.cancel(
`Failed to read tarball file: ${error instanceof Error ? error.message : String(error)}`
);
process.exit(1);
}
}

// Only run local analysis if the root is not a tarball file
if (!isTarball) {
const resolvedRoot = root || process.cwd();
const localAnalyzer = new LocalDependencyAnalyzer();
const localStats = await localAnalyzer.analyzeDependencies(resolvedRoot);

prompts.log.info('Local Analysis');
prompts.log.message(
`${c.cyan('Total deps ')} ${localStats.totalDependencies}`,
{spacing: 0}
);
prompts.log.message(
`${c.cyan('Direct deps ')} ${localStats.directDependencies}`,
{spacing: 0}
);
prompts.log.message(
`${c.cyan('Dev deps ')} ${localStats.devDependencies}`,
{spacing: 0}
);
prompts.log.message(
`${c.cyan('CJS deps ')} ${localStats.cjsDependencies}`,
{spacing: 0}
);
prompts.log.message(
`${c.cyan('ESM deps ')} ${localStats.esmDependencies}`,
{spacing: 0}
);
prompts.log.message(
`${c.cyan('Install size ')} ${formatBytes(localStats.installSize)}`,
{spacing: 0}
);
prompts.log.message(
c.yellowBright(
'Dependency type analysis is based on your installed node_modules.'
),
{spacing: 1}
);
prompts.log.message('', {spacing: 0});

// Display package info
prompts.log.info('Package info');
prompts.log.message(`${c.cyan('Name ')} ${localStats.packageName}`, {spacing: 0});
prompts.log.message(`${c.cyan('Version')} ${localStats.version}`, {spacing: 0});
prompts.log.message('', {spacing: 0});
}

// Then analyze the tarball
const {dependencies, messages} = await report({root, pack});

// Show files in tarball as debug output
if (Array.isArray(dependencies.tarballFiles)) {
logger.debug('Files in tarball:');
for (const file of dependencies.tarballFiles) {
logger.debug(` - ${file}`);
}
}

prompts.log.info('Tarball Analysis');
args: {},
async run() {
prompts.intro('Please choose a command to run:');
prompts.log.message(
`${c.cyan('Total deps ')} ${dependencies.totalDependencies}`,
{spacing: 0}
[
`--analyze (${c.dim('analyzes the package for warnings/errors')})`,
`--migrate (${c.dim('migrates packages to their suggested alternatives')})`
].join('\n')
);
prompts.log.message(
`${c.cyan('Direct deps ')} ${dependencies.directDependencies}`,
{spacing: 0}
prompts.outro(
'Use `<command> --help` to read more about a specific command'
);
prompts.log.message(
`${c.cyan('Dev deps ')} ${dependencies.devDependencies}`,
{spacing: 0}
);
prompts.log.message(`${c.cyan('CJS deps ')} N/A`, {spacing: 0});
prompts.log.message(`${c.cyan('ESM deps ')} N/A`, {spacing: 0});
prompts.log.message(
`${c.cyan('Install size ')} ${formatBytes(dependencies.installSize)}`,
{spacing: 0}
);
prompts.log.message(
c.yellowBright(
'Dependency type analysis is only available for local analysis, as tarballs do not include dependencies.'
),
{spacing: 1}
);

prompts.log.info('Package report');
prompts.log.message('', {spacing: 0});
// Display tool analysis results
if (messages.length > 0) {
const errorMessages = messages.filter(m => m.severity === 'error');
const warningMessages = messages.filter(m => m.severity === 'warning');
const suggestionMessages = messages.filter(m => m.severity === 'suggestion');

// Display errors
if (errorMessages.length > 0) {
prompts.log.message(c.red('Errors:'), {spacing: 0});
for (const msg of errorMessages) {
prompts.log.message(` ${c.red('•')} ${msg.message}`, {spacing: 0});
}
prompts.log.message('', {spacing: 0});
}

// Display warnings
if (warningMessages.length > 0) {
prompts.log.message(c.yellow('Warnings:'), {spacing: 0});
for (const msg of warningMessages) {
prompts.log.message(` ${c.yellow('•')} ${msg.message}`, {spacing: 0});
}
prompts.log.message('', {spacing: 0});
}

// Display suggestions
if (suggestionMessages.length > 0) {
prompts.log.message(c.blue('Suggestions:'), {spacing: 0});
for (const msg of suggestionMessages) {
prompts.log.message(` ${c.blue('•')} ${msg.message}`, {spacing: 0});
}
prompts.log.message('', {spacing: 0});
}
}
prompts.outro('Report generated successfully!');
}
});

function formatBytes(bytes: number) {
const units = ['B', 'KB', 'MB', 'GB'];
let size = bytes;
let unitIndex = 0;

while (size >= 1024 && unitIndex < units.length - 1) {
size /= 1024;
unitIndex++;
}

return `${size.toFixed(1)} ${units[unitIndex]}`;
}
const analyzeCommand = async () => {
const {run} = await import('./commands/analyze.js');
return run;
};
const migrateCommand = async () => {
const {run} = await import('./commands/migrate.js');
return run;
};

const subCommands = new Map<string, LazyCommand<any>>([
// TODO (43081j): get rid of these casts
['analyze', lazy(analyzeCommand, analyzeMeta) as LazyCommand<any>],
['migrate', lazy(migrateCommand, migrateMeta) as LazyCommand<any>]
]);

cli(process.argv.slice(2), defaultCommand, {
name: 'e18e-report',
version,
description: 'Generate a performance report for your package.'
description: `${c.cyan('e18e CLI')}`,
subCommands
});
20 changes: 20 additions & 0 deletions src/commands/analyze.meta.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type {PackType} from '../types.js';

export const meta = {
name: 'analyze',
description: 'Analyze the project for any warnings or errors',
args: {
pack: {
type: 'enum',
choices: ['auto', 'npm', 'yarn', 'pnpm', 'bun'] satisfies PackType[],
default: 'auto',
description: `Package manager to use for packing ('auto' | 'npm' | 'yarn' | 'pnpm' | 'bun')`
},
'log-level': {
type: 'enum',
choices: ['debug', 'info', 'warn', 'error'],
default: 'info',
description: 'Set the log level (debug | info | warn | error)'
}
}
} as const;
Loading