Skip to content

Commit

Permalink
pipe package install logs into a stream for reduced noise
Browse files Browse the repository at this point in the history
  • Loading branch information
yannbf committed May 15, 2023
1 parent b898a56 commit a670366
Show file tree
Hide file tree
Showing 11 changed files with 514 additions and 22 deletions.
4 changes: 3 additions & 1 deletion code/lib/cli/src/generators/baseGenerator.ts
Expand Up @@ -4,7 +4,7 @@ import { dedent } from 'ts-dedent';
import type { NpmOptions } from '../NpmOptions';
import type { SupportedRenderers, SupportedFrameworks, Builder } from '../project_types';
import { externalFrameworks, CoreBuilder } from '../project_types';
import { getBabelDependencies, copyTemplateFiles } from '../helpers';
import { getBabelDependencies, copyTemplateFiles, paddedLog } from '../helpers';
import { configureMain, configurePreview } from './configure';
import type { JsPackageManager } from '../js-package-manager';
import { getPackageDetails } from '../js-package-manager';
Expand Down Expand Up @@ -234,6 +234,7 @@ export async function baseGenerator(
(packageToInstall) => !installedDependencies.has(getPackageDetails(packageToInstall)[0])
);

paddedLog(`\nGetting the correct version of ${packages.length} packages`);
const versionedPackages = await packageManager.getVersionedPackages(packages);

await fse.ensureDir(`./${storybookConfigFolder}`);
Expand Down Expand Up @@ -274,6 +275,7 @@ export async function baseGenerator(
const depsToInstall = [...versionedPackages, ...babelDependencies];

if (depsToInstall.length > 0) {
paddedLog('Installing Storybook dependencies');
await packageManager.addDependencies({ ...npmOptions, packageJson }, depsToInstall);
}

Expand Down
3 changes: 2 additions & 1 deletion code/lib/cli/src/js-package-manager/JsPackageManager.ts
Expand Up @@ -218,7 +218,7 @@ export abstract class JsPackageManager {
try {
await this.runAddDeps(dependencies, options.installAsDevDependencies);
} catch (e) {
logger.error('An error occurred while installing dependencies.');
logger.error('\nAn error occurred while installing dependencies:');
logger.log(e.message);
throw new HandledError(e);
}
Expand Down Expand Up @@ -440,6 +440,7 @@ export abstract class JsPackageManager {
stdio?: 'inherit' | 'pipe'
): string;
public abstract findInstallations(pattern?: string[]): Promise<InstallationMetadata | undefined>;
public abstract parseErrorFromLogs(logs?: string): string;

public executeCommandSync({
command,
Expand Down
68 changes: 68 additions & 0 deletions code/lib/cli/src/js-package-manager/NPMProxy.test.ts
@@ -1,5 +1,15 @@
import { NPMProxy } from './NPMProxy';

// mock createLogStream
jest.mock('../utils', () => ({
createLogStream: jest.fn(() => ({
logStream: '',
readLogFile: jest.fn(),
moveLogFile: jest.fn(),
removeLogFile: jest.fn(),
})),
}));

describe('NPM Proxy', () => {
let npmProxy: NPMProxy;

Expand Down Expand Up @@ -426,4 +436,62 @@ describe('NPM Proxy', () => {
`);
});
});

describe('parseErrors', () => {
it('should parse npm errors', () => {
const NPM_ERROR_SAMPLE = `
npm ERR! code ERESOLVE
npm ERR! ERESOLVE unable to resolve dependency tree
npm ERR!
npm ERR! While resolving: before-storybook@1.0.0
npm ERR! Found: react@undefined
npm ERR! node_modules/react
npm ERR! react@"30" from the root project
npm ERR!
npm ERR! Could not resolve dependency:
npm ERR! peer react@"^16.8.0 || ^17.0.0 || ^18.0.0" from @storybook/react@7.1.0-alpha.17
npm ERR! node_modules/@storybook/react
npm ERR! dev @storybook/react@"^7.1.0-alpha.17" from the root project
npm ERR!
npm ERR! Fix the upstream dependency conflict, or retry
npm ERR! this command with --force or --legacy-peer-deps
npm ERR! to accept an incorrect (and potentially broken) dependency resolution.
npm ERR!
npm ERR!
npm ERR! For a full report see:
npm ERR! /Users/yannbraga/.npm/_logs/2023-05-12T08_38_18_464Z-eresolve-report.txt
npm ERR! A complete log of this run can be found in:
npm ERR! /Users/yannbraga/.npm/_logs/2023-05-12T08_38_18_464Z-debug-0.log
`;

expect(npmProxy.parseErrorFromLogs(NPM_ERROR_SAMPLE)).toEqual(
'ERESOLVE: Dependency resolution error.'
);
});

it('should show unknown npm error', () => {
const NPM_ERROR_SAMPLE = `
npm ERR!
npm ERR! While resolving: before-storybook@1.0.0
npm ERR! Found: react@undefined
npm ERR! node_modules/react
npm ERR! react@"30" from the root project
`;

expect(npmProxy.parseErrorFromLogs(NPM_ERROR_SAMPLE)).toEqual(`Unknown NPM error`);
});

it('should show unknown npm error with code if it at least matches the pattern', () => {
const NPM_ERROR_SAMPLE = `
npm ERR! code ESOMETHING
npm ERR! ESOMETHING something something
npm ERR!
`;

expect(npmProxy.parseErrorFromLogs(NPM_ERROR_SAMPLE)).toEqual(
`Unknown NPM error: ESOMETHING`
);
});
});
});
82 changes: 77 additions & 5 deletions code/lib/cli/src/js-package-manager/NPMProxy.ts
@@ -1,8 +1,10 @@
import sort from 'semver/functions/sort';
import { platform } from 'os';
import dedent from 'ts-dedent';
import { JsPackageManager } from './JsPackageManager';
import type { PackageJson } from './PackageJson';
import type { InstallationMetadata, PackageMetadata } from './types';
import { createLogStream } from '../utils';

type NpmDependency = {
version: string;
Expand All @@ -19,6 +21,41 @@ export type NpmListOutput = {
dependencies: NpmDependencies;
};

const NPM_ERROR_REGEX = /\bERR! code\s+([A-Z]+)\b/;
const NPM_ERROR_CODES = {
E401: 'Authentication failed or is required.',
E403: 'Access to the resource is forbidden.',
E404: 'Requested resource not found.',
EACCES: 'Permission issue.',
EAI_FAIL: 'DNS lookup failed.',
EBADENGINE: 'Engine compatibility check failed.',
EBADPLATFORM: 'Platform not supported.',
ECONNREFUSED: 'Connection refused.',
ECONNRESET: 'Connection reset.',
EEXIST: 'File or directory already exists.',
EINVALIDTYPE: 'Invalid type encountered.',
EISGIT: 'Git operation failed or conflicts with an existing file.',
EJSONPARSE: 'Error parsing JSON data.',
EMISSINGARG: 'Required argument missing.',
ENEEDAUTH: 'Authentication needed.',
ENOAUDIT: 'No audit available.',
ENOENT: 'File or directory does not exist.',
ENOGIT: 'Git not found or failed to run.',
ENOLOCK: 'Lockfile missing.',
ENOSPC: 'Insufficient disk space.',
ENOTFOUND: 'Resource not found.',
EOTP: 'One-time password required.',
EPERM: 'Permission error.',
EPUBLISHCONFLICT: 'Conflict during package publishing.',
ERESOLVE: 'Dependency resolution error.',
EROFS: 'File system is read-only.',
ERR_SOCKET_TIMEOUT: 'Socket timed out.',
ETARGET: 'Package target not found.',
ETIMEDOUT: 'Operation timed out.',
ETOOMANYARGS: 'Too many arguments provided.',
EUNKNOWNTYPE: 'Unknown type encountered.',
};

export class NPMProxy extends JsPackageManager {
readonly type = 'npm';

Expand Down Expand Up @@ -104,17 +141,38 @@ export class NPMProxy extends JsPackageManager {
}

protected async runAddDeps(dependencies: string[], installAsDevDependencies: boolean) {
const { logStream, readLogFile, moveLogFile, removeLogFile } = await createLogStream(
'init-storybook.log'
);
let args = [...dependencies];

if (installAsDevDependencies) {
args = ['-D', ...args];
}

await this.executeCommand({
command: 'npm',
args: ['install', ...this.getInstallArgs(), ...args],
stdio: 'inherit',
});
logStream.write(`\n THIS IS CUSTOM! Installing dependencies:\n${args.join('\n')}\n\n`);

try {
await this.executeCommand({
command: 'npm',
args: ['install', ...args, ...this.getInstallArgs()],
stdio: ['ignore', logStream, logStream],
});
} catch (err) {
const stdout = await readLogFile();

const errorMessage = this.parseErrorFromLogs(stdout);

await moveLogFile();

throw new Error(
dedent`${errorMessage}
Please check the logfile generated at ./init-install.log for troubleshooting and try again.`
);
}

await removeLogFile();
}

protected async runRemoveDeps(dependencies: string[]) {
Expand Down Expand Up @@ -191,4 +249,18 @@ export class NPMProxy extends JsPackageManager {
infoCommand: 'npm ls --depth=1',
};
}

public parseErrorFromLogs(logs: string): string {
const match = logs.match(NPM_ERROR_REGEX);
let errorCode;
if (match) {
errorCode = match[1] as keyof typeof NPM_ERROR_CODES;
const errorMessage = NPM_ERROR_CODES[errorCode];
if (errorCode && errorMessage) {
return `${errorCode}: ${errorMessage}`.trim();
}
}

return `Unknown NPM error${errorCode ? `: ${errorCode}` : ''}`;
}
}
38 changes: 38 additions & 0 deletions code/lib/cli/src/js-package-manager/PNPMProxy.test.ts
Expand Up @@ -375,4 +375,42 @@ describe('NPM Proxy', () => {
`);
});
});

describe('parseErrors', () => {
it('should parse pnpm errors', () => {
const PNPM_ERROR_SAMPLE = `
ERR_PNPM_NO_MATCHING_VERSION No matching version found for react@29.2.0
This error happened while installing a direct dependency of /Users/yannbraga/open-source/sandboxes/react-vite/default-js/before-storybook
The latest release of react is "18.2.0".
`;

expect(pnpmProxy.parseErrorFromLogs(PNPM_ERROR_SAMPLE)).toEqual(
'ERR_PNPM_NO_MATCHING_VERSION: No matching version found for react@29.2.0'
);
});

it('should show unknown pnpm error', () => {
const PNPM_ERROR_SAMPLE = `
This error happened while installing a direct dependency of /Users/yannbraga/open-source/sandboxes/react-vite/default-js/before-storybook
The latest release of react is "18.2.0".
`;

expect(pnpmProxy.parseErrorFromLogs(PNPM_ERROR_SAMPLE)).toEqual(`Unknown PNPM error`);
});

it('should show unknown pnpm error with code if it at least matches the pattern', () => {
const PNPM_ERROR_SAMPLE = `
ERR_PNPM_SOMETHING No matching version found for react@29.2.0
This error happened while installing a direct dependency of /Users/yannbraga/open-source/sandboxes/react-vite/default-js/before-storybook
`;

expect(pnpmProxy.parseErrorFromLogs(PNPM_ERROR_SAMPLE)).toEqual(
`Unknown PNPM error: ERR_PNPM_SOMETHING`
);
});
});
});
65 changes: 60 additions & 5 deletions code/lib/cli/src/js-package-manager/PNPMProxy.ts
@@ -1,7 +1,9 @@
import { pathExistsSync } from 'fs-extra';
import dedent from 'ts-dedent';
import { JsPackageManager } from './JsPackageManager';
import type { PackageJson } from './PackageJson';
import type { InstallationMetadata, PackageMetadata } from './types';
import { createLogStream } from '../utils';

type PnpmDependency = {
from: string;
Expand All @@ -22,6 +24,26 @@ type PnpmListItem = {

export type PnpmListOutput = PnpmListItem[];

const PNPM_ERROR_REGEX = /(ELIFECYCLE|ERR_PNPM_[A-Z_]+)\s+(.*)/i;
const PNPM_ERROR_CODES = {
ELIFECYCLE: 'Lifecycle error',
ERR_PNPM_BAD_TARBALL_SIZE: 'Bad tarball size error',
ERR_PNPM_DEDUPE_CHECK_ISSUES: 'Dedupe check issues error',
ERR_PNPM_FETCH_401: 'Fetch 401 error',
ERR_PNPM_FETCH_403: 'Fetch 403 error',
ERR_PNPM_LOCKFILE_BREAKING_CHANGE: 'Lockfile breaking change error',
ERR_PNPM_MODIFIED_DEPENDENCY: 'Modified dependency error',
ERR_PNPM_MODULES_BREAKING_CHANGE: 'Modules breaking change error',
ERR_PNPM_NO_MATCHING_VERSION: 'No matching version error',
ERR_PNPM_PEER_DEP_ISSUES: 'Peer dependency issues error',
ERR_PNPM_RECURSIVE_FAIL: 'Recursive command failed error',
ERR_PNPM_RECURSIVE_RUN_NO_SCRIPT: 'Recursive run no script error',
ERR_PNPM_STORE_BREAKING_CHANGE: 'Store breaking change error',
ERR_PNPM_UNEXPECTED_STORE: 'Unexpected store error',
ERR_PNPM_UNEXPECTED_VIRTUAL_STORE: 'Unexpected virtual store error',
ERR_PNPM_UNSUPPORTED_ENGINE: 'Unsupported engine error',
};

export class PNPMProxy extends JsPackageManager {
readonly type = 'pnpm';

Expand Down Expand Up @@ -126,12 +148,31 @@ export class PNPMProxy extends JsPackageManager {
if (installAsDevDependencies) {
args = ['-D', ...args];
}
const { logStream, readLogFile, moveLogFile, removeLogFile } = await createLogStream(
'init-storybook.log'
);

await this.executeCommand({
command: 'pnpm',
args: ['add', ...args, ...this.getInstallArgs()],
stdio: 'inherit',
});
try {
await this.executeCommand({
command: 'pnpm',
args: ['add', ...args, ...this.getInstallArgs()],
stdio: ['ignore', logStream, logStream],
});
} catch (err) {
const stdout = await readLogFile();

const errorMessage = this.parseErrorFromLogs(stdout);

await moveLogFile();

throw new Error(
dedent`${errorMessage}
Please check the logfile generated at ./init-install.log for troubleshooting and try again.`
);
}

await removeLogFile();
}

protected async runRemoveDeps(dependencies: string[]) {
Expand Down Expand Up @@ -212,4 +253,18 @@ export class PNPMProxy extends JsPackageManager {
infoCommand: 'pnpm list --depth=1',
};
}

public parseErrorFromLogs(logs: string): string {
const match = logs.match(PNPM_ERROR_REGEX);
let errorCode;
if (match) {
errorCode = match[1] as keyof typeof PNPM_ERROR_CODES;
const errorMessage = match[2];
const errorType = PNPM_ERROR_CODES[errorCode];
if (errorType && errorMessage) {
return `${errorCode}: ${errorMessage}`.trim();
}
}
return `Unknown PNPM error${errorCode ? `: ${errorCode}` : ''}`;
}
}

0 comments on commit a670366

Please sign in to comment.