Skip to content

Commit

Permalink
Make big improvements and add tests.
Browse files Browse the repository at this point in the history
Fixes #3 .

This package now supports npm v7+. While Node.js v12 and v14 should be supported (with npm updated to v7+), this isn’t easy to test in GitHub Actions CI as actions/setup-node@v2 doesn’t allow the npm version to be configured, see: actions/setup-node#213 .
  • Loading branch information
jaydenseric committed Jun 4, 2021
1 parent 1635252 commit 07a350e
Show file tree
Hide file tree
Showing 74 changed files with 2,209 additions and 166 deletions.
1 change: 1 addition & 0 deletions .eslintignore
@@ -0,0 +1 @@
/test/fixtures/package-json-broken
10 changes: 9 additions & 1 deletion .github/workflows/ci.yml
Expand Up @@ -7,7 +7,15 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
node: ['12', '14', '16']
node:
# This package supports npm v7+. While Node.js v12 and v14 should be
# supported (with npm updated to v7+), this isn’t easy to test in
# GitHub Actions CI as actions/setup-node@v2 doesn’t allow the npm
# version to be configured, see:
# https://github.com/actions/setup-node/issues/213
# - '12'
# - '14'
- '16'
steps:
- uses: actions/checkout@v2
- name: Setup Node.js v${{ matrix.node }}
Expand Down
1 change: 1 addition & 0 deletions .gitignore
@@ -1,2 +1,3 @@
node_modules
.DS_Store
!/test/fixtures/**/node_modules
1 change: 1 addition & 0 deletions .prettierignore
@@ -1 +1,2 @@
package.json
/test/snapshots
21 changes: 21 additions & 0 deletions changelog.md
Expand Up @@ -5,30 +5,51 @@
### Major

- Updated Node.js support to `^12.20 || >= 14.13`.
- Updated npm support to `>= 7`.
- Updated dependencies, some of which require newer Node.js versions than were previously supported.
- Published modules are now ESM in `.mjs` files instead of CJS in `.js` files.
- Added a package `exports` field.
- Stop displaying the execution time in CLI output.
- Added tests with 100% code coverage, using ESM in `.mjs` files, a new package `test:api` script, and new dev dependencies:
- [`coverage-node`](https://npm.im/coverage-node)
- [`snapshot-assertion`](https://npm.im/snapshot-assertion)
- [`test-director`](https://npm.im/test-director)

### Minor

- Added a package `sideEffects` field.
- Added a package `main` field and a public JS API.
- Added a new “unknown” age category to `audit-age` command output, and correctly handle packages without a published date (e.g. it’s a local file or Git dependency).
- Handle errors and display them with nice formatting in CLI output.
- Setup [GitHub Sponsors funding](https://github.com/sponsors/jaydenseric):
- Added `.github/funding.yml` to display a sponsor button in GitHub.
- Added a package `funding` field to enable npm CLI funding features.

### Patch

- Use [`duration-relativetimeformat`](https://npm.im/duration-relativetimeformat) instead of [`moment`](https://npm.im/moment) to format durations.
- Use [`kleur`](https://npm.im/kleur) instead of [`chalk`](https://npm.im/chalk) to format CLI output with ANSI colors.
- Stop using [`husky`](https://npm.im/husky) and [`lint-staged`](https://npm.im/lint-staged).
- Removed the [`cli-table3`](https://npm.im/cli-table3) dependency and manually align CLI output instead.
- Use [`jsdoc-md`](https://npm.im/jsdoc-md) to generate and check the new readme section “API” via new package `jsdoc` and `test:jsdoc` scripts.
- Updated the package description.
- Updated the package `keywords` field.
- Removed the package `engines.npm` field.
- More specific package `bin` field.
- Improved the package scripts.
- Moved dev config from `package.json` to separate files, for a leaner install size.
- Only audit deduped packages once to massively speedup audits and simplify output.
- Made the `audit-age` CLI output the audit in smaller `stdout` chunks instead of all at once, fixing [#3](https://github.com/jaydenseric/audit-age/issues/3).
- Use the more efficient Node.js `child_process` function `execFile` instead of `exec` when running the npm CLI.
- Removed redundant `--only production` args from the `npm ls` command used to get the dependency tree for the package being audited.
- Configured Prettier option `semi` to the default, `true`.
- Use GitHub Actions instead of Travis for CI.
- Updated the EditorConfig.
- Removed `package-lock.json` from the `.gitignore` and `.prettierignore` files as it’s disabled in `.npmrc` anyway.
- Removed `npm-debug.log` from the `.gitignore` file as npm [v4.2.0](https://github.com/npm/npm/releases/tag/v4.2.0)+ doesn’t create it in the current working directory.
- Use [Badgen](https://badgen.net) instead of [Shields](https://shields.io) for the readme npm version badge.
- Removed the readme section “Demo”.
- Improved documentation.
- Amended the changelog entry for v0.1.1.

## 0.1.1
Expand Down
265 changes: 112 additions & 153 deletions cli/audit-age.mjs
@@ -1,169 +1,128 @@
#!/usr/bin/env node

import { exec } from 'child_process';
import { promisify } from 'util';
import chalk from 'chalk';
import Table from 'cli-table3';
import moment from 'moment';

const asyncExec = promisify(exec);
const startTime = new Date();
const thresholds = [
{
label: 'Day',
count: 0,
ms: 8.64e7,
color: 'green',
},
{
label: 'Week',
count: 0,
ms: 6.048e8,
color: 'cyan',
},
{
label: 'Month',
count: 0,
ms: 2.628e9,
color: 'magenta',
},
{
label: 'Year',
count: 0,
ms: 3.154e10,
color: 'yellow',
},
{
label: 'Year+',
count: 0,
ms: Infinity,
color: 'red',
},
];
const clearTableChars = {
top: '',
'top-mid': '',
'top-left': '',
'top-right': '',
bottom: '',
'bottom-mid': '',
'bottom-left': '',
'bottom-right': '',
left: '',
'left-mid': '',
mid: '',
'mid-mid': '',
right: '',
'right-mid': '',
middle: '',
};
import createFormatDuration from 'duration-relativetimeformat';
import kleur from 'kleur';
import reportCliError from '../private/reportCliError.mjs';
import auditAge from '../public/auditAge.mjs';

const formatDuration = createFormatDuration('en-US');

/**
* Audits the age of installed npm packages.
* @private
* Runs the `audit-age` CLI.
* @kind function
* @name auditAgeCli
* @returns {Promise<void>} Resolves once the operation is done.
* @ignore
*/
async function auditAge() {
const { stdout: rawTree } = await asyncExec(
'npm ls --prod --only production --json'
);
const tree = JSON.parse(rawTree);
const lookups = [];

/**
* Recurses dependencies to prepare the report.
* @param {Array<object>} dependencies Dependencies nested at the current level.
* @param {Array<string>} ancestorPath How the dependency is nested.
*/
const recurse = (dependencies, ancestorPath = []) => {
Object.entries(dependencies).forEach(
([name, { version, dependencies }]) => {
const path = [...ancestorPath, `${name}@${version}`];
lookups.push(
asyncExec(`npm view ${name} time --json`).then(
({ stdout: rawTimes }) => {
const times = JSON.parse(rawTimes);
const published = moment(times[version]);
const msDiff = moment(startTime).diff(published);
const threshold = thresholds.find(({ ms }) => msDiff < ms);
threshold.count++;
return {
path,
name,
version,
published,
threshold,
};
}
)
);
if (dependencies) recurse(dependencies, path);
}
);
};

recurse(tree.dependencies);

// eslint-disable-next-line no-console
console.log(`\nFetching ${lookups.length} package ages...\n`);
async function auditAgeCli() {
try {
console.info('Auditing the age of installed production npm packages…');

const list = await Promise.all(lookups);
const sorted = list.sort((a, b) => a.published - b.published);
const packagesTable = new Table({
chars: {
...clearTableChars,
mid: '─',
'mid-mid': '─',
},
});

sorted.forEach(({ path, published, threshold }) =>
packagesTable.push([
const dateAudit = new Date();
const audit = await auditAge();
const unknownCategory = {
label: 'Unknown',
color: 'grey',
count: 0,
};
const thresholdCategories = [
{
vAlign: 'bottom',
content: path.reduce((tree, item, index) => {
if (index > 0) tree += `\n${' '.repeat(index - 1)}└─ `;
return (index === path.length - 1 ? chalk.dim(tree) : tree) + item;
}, ''),
label: 'Day',
ms: 8.64e7,
color: 'green',
count: 0,
},
{
hAlign: 'right',
vAlign: 'bottom',
content: `${chalk[threshold.color](
published.fromNow()
)}\n${published.format('lll')}`,
label: 'Week',
ms: 6.048e8,
color: 'cyan',
count: 0,
},
])
);

// eslint-disable-next-line no-console
console.log(`${packagesTable.toString()}\n\n`);

const summaryTable = new Table({
chars: clearTableChars,
});

thresholds.reverse().forEach(({ color, label, count }) =>
summaryTable.push([
{
hAlign: 'right',
content: chalk[color](label),
label: 'Month',
ms: 2.628e9,
color: 'magenta',
count: 0,
},
{
hAlign: 'right',
content: count,
label: 'Year',
ms: 3.154e10,
color: 'yellow',
count: 0,
},
])
);

// eslint-disable-next-line no-console
console.log(`${summaryTable.toString()}\n`);

// eslint-disable-next-line no-console
console.log(
`Audited ${lookups.length} package ages in ${
(new Date() - startTime) / 1000
}s.\n`
);
{
label: 'Year+',
ms: Infinity,
color: 'red',
count: 0,
},
];

for (const { path, datePublished } of audit) {
let category;

if (datePublished) {
const msDiff = dateAudit - datePublished;

category = thresholdCategories.find(({ ms }) => msDiff < ms);
} else category = unknownCategory;

category.count++;

let dependencyTree = '';

path.forEach(({ name, version }, index) => {
if (index)
dependencyTree += `
${' '.repeat(index - 1)}└─ `;

if (index === path.length - 1)
dependencyTree = kleur.dim(dependencyTree);

dependencyTree += name;

if (version) dependencyTree += `@${version}`;
});

console.info(`
${dependencyTree}
${kleur[category.color](
`${kleur.dim(datePublished ? datePublished.toISOString() : 'Unavailable')} (${
datePublished ? formatDuration(datePublished, dateAudit) : 'unknown age'
})`
)}`);
}

const allCategories = [...thresholdCategories.reverse(), unknownCategory];

// This is needed to align the category count column.
const longestCategoryLabelLength = Math.max(
...allCategories.map(({ label }) => label.length)
);

let outputSummary = '';

for (const { label, color, count } of allCategories)
outputSummary += `
${' '.repeat(longestCategoryLabelLength - label.length)}${kleur[color](
label
)} ${count}`;

outputSummary += `
${kleur.bold(
`Audited the age of ${audit.length} installed production npm package${
audit.length === 1 ? '' : 's'
}.`
)}
`;

console.info(outputSummary);
} catch (error) {
reportCliError('audit-age', error);

process.exitCode = 1;
}
}

auditAge();
auditAgeCli();

0 comments on commit 07a350e

Please sign in to comment.