diff --git a/code/lib/cli/src/generators/baseGenerator.ts b/code/lib/cli/src/generators/baseGenerator.ts index 9a44785b7146..c61e2e7fae0e 100644 --- a/code/lib/cli/src/generators/baseGenerator.ts +++ b/code/lib/cli/src/generators/baseGenerator.ts @@ -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'; @@ -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}`); @@ -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); } diff --git a/code/lib/cli/src/js-package-manager/JsPackageManager.ts b/code/lib/cli/src/js-package-manager/JsPackageManager.ts index 3e0b16a1b25c..a33e64353967 100644 --- a/code/lib/cli/src/js-package-manager/JsPackageManager.ts +++ b/code/lib/cli/src/js-package-manager/JsPackageManager.ts @@ -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); } @@ -440,6 +440,7 @@ export abstract class JsPackageManager { stdio?: 'inherit' | 'pipe' ): string; public abstract findInstallations(pattern?: string[]): Promise; + public abstract parseErrorFromLogs(logs?: string): string; public executeCommandSync({ command, diff --git a/code/lib/cli/src/js-package-manager/NPMProxy.test.ts b/code/lib/cli/src/js-package-manager/NPMProxy.test.ts index c0c8cb63be52..8524a1113f55 100644 --- a/code/lib/cli/src/js-package-manager/NPMProxy.test.ts +++ b/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; @@ -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` + ); + }); + }); }); diff --git a/code/lib/cli/src/js-package-manager/NPMProxy.ts b/code/lib/cli/src/js-package-manager/NPMProxy.ts index 5dd0d63bd262..ff9a1c675490 100644 --- a/code/lib/cli/src/js-package-manager/NPMProxy.ts +++ b/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; @@ -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'; @@ -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[]) { @@ -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}` : ''}`; + } } diff --git a/code/lib/cli/src/js-package-manager/PNPMProxy.test.ts b/code/lib/cli/src/js-package-manager/PNPMProxy.test.ts index eb82f1a06465..2300c5dc3a7c 100644 --- a/code/lib/cli/src/js-package-manager/PNPMProxy.test.ts +++ b/code/lib/cli/src/js-package-manager/PNPMProxy.test.ts @@ -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` + ); + }); + }); }); diff --git a/code/lib/cli/src/js-package-manager/PNPMProxy.ts b/code/lib/cli/src/js-package-manager/PNPMProxy.ts index 8e8a4dce793e..14c08067bee0 100644 --- a/code/lib/cli/src/js-package-manager/PNPMProxy.ts +++ b/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; @@ -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'; @@ -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[]) { @@ -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}` : ''}`; + } } diff --git a/code/lib/cli/src/js-package-manager/Yarn1Proxy.test.ts b/code/lib/cli/src/js-package-manager/Yarn1Proxy.test.ts index fb9edaef3cc0..0da683dfadb8 100644 --- a/code/lib/cli/src/js-package-manager/Yarn1Proxy.test.ts +++ b/code/lib/cli/src/js-package-manager/Yarn1Proxy.test.ts @@ -1,3 +1,4 @@ +import dedent from 'ts-dedent'; import { Yarn1Proxy } from './Yarn1Proxy'; describe('Yarn 1 Proxy', () => { @@ -275,4 +276,29 @@ describe('Yarn 1 Proxy', () => { `); }); }); + + describe('parseErrors', () => { + it('should parse yarn1 errors', () => { + const YARN1_ERROR_SAMPLE = dedent` + yarn add v1.22.19 + [1/4] Resolving packages... + error Couldn't find any versions for "react" that matches "28.2.0" + info Visit https://yarnpkg.com/en/docs/cli/add for documentation about this command. + `; + + expect(yarn1Proxy.parseErrorFromLogs(YARN1_ERROR_SAMPLE)).toEqual( + `YARN1: Couldn't find any versions for "react" that matches "28.2.0"` + ); + }); + + it('should show unknown yarn1 error', () => { + const YARN1_ERROR_SAMPLE = dedent` + yarn install v1.22.19 + [1/4] 🔍 Resolving packages... + info Visit https://yarnpkg.com/en/docs/cli/install for documentation about this command. + `; + + expect(yarn1Proxy.parseErrorFromLogs(YARN1_ERROR_SAMPLE)).toEqual(`Unknown Yarn1 error`); + }); + }); }); diff --git a/code/lib/cli/src/js-package-manager/Yarn1Proxy.ts b/code/lib/cli/src/js-package-manager/Yarn1Proxy.ts index 00de17ded091..6683ff014b9c 100644 --- a/code/lib/cli/src/js-package-manager/Yarn1Proxy.ts +++ b/code/lib/cli/src/js-package-manager/Yarn1Proxy.ts @@ -1,3 +1,5 @@ +import dedent from 'ts-dedent'; +import { createLogStream } from '../utils'; import { JsPackageManager } from './JsPackageManager'; import type { PackageJson } from './PackageJson'; import type { InstallationMetadata, PackageMetadata } from './types'; @@ -18,6 +20,8 @@ export type Yarn1ListOutput = { data: Yarn1ListData; }; +const YARN1_ERROR_REGEX = /^error\s(.*)$/gm; + export class Yarn1Proxy extends JsPackageManager { readonly type = 'yarn1'; @@ -93,11 +97,31 @@ export class Yarn1Proxy extends JsPackageManager { args = ['-D', ...args]; } - await this.executeCommand({ - command: 'yarn', - args: ['add', ...this.getInstallArgs(), ...args], - stdio: 'inherit', - }); + const { logStream, readLogFile, moveLogFile, removeLogFile } = await createLogStream( + 'init-storybook.log' + ); + + try { + await this.executeCommand({ + command: 'yarn', + args: ['add', ...this.getInstallArgs(), ...args], + 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[]) { @@ -170,4 +194,16 @@ export class Yarn1Proxy extends JsPackageManager { throw new Error('Something went wrong while parsing yarn output'); } + + public parseErrorFromLogs(logs: string): string { + const match = logs.match(YARN1_ERROR_REGEX); + if (match) { + const errorMessage = match[0].replace(/^error\s(.*)$/, '$1'); + if (errorMessage) { + return `YARN1: ${errorMessage}`.trim(); + } + } + + return `Unknown Yarn1 error`; + } } diff --git a/code/lib/cli/src/js-package-manager/Yarn2Proxy.test.ts b/code/lib/cli/src/js-package-manager/Yarn2Proxy.test.ts index f875254a6858..521cdf1241ea 100644 --- a/code/lib/cli/src/js-package-manager/Yarn2Proxy.test.ts +++ b/code/lib/cli/src/js-package-manager/Yarn2Proxy.test.ts @@ -1,3 +1,4 @@ +import dedent from 'ts-dedent'; import { Yarn2Proxy } from './Yarn2Proxy'; describe('Yarn 2 Proxy', () => { @@ -271,4 +272,37 @@ describe('Yarn 2 Proxy', () => { `); }); }); + + describe('parseErrors', () => { + it('should parse yarn2 errors', () => { + const YARN2_ERROR_SAMPLE = ` + ➤ YN0000: ┌ Resolution step + ➤ YN0001: │ Error: react@npm:28.2.0: No candidates found + at ge (/Users/yannbraga/.cache/node/corepack/yarn/3.5.1/yarn.js:439:8124) + at process.processTicksAndRejections (node:internal/process/task_queues:95:5) + at async Promise.allSettled (index 8) + at async io (/Users/yannbraga/.cache/node/corepack/yarn/3.5.1/yarn.js:390:10398) + ➤ YN0000: └ Completed in 2s 369ms + ➤ YN0000: Failed with errors in 2s 372ms + ➤ YN0032: fsevents@npm:2.3.2: Implicit dependencies on node-gyp are discouraged + ➤ YN0061: @npmcli/move-file@npm:2.0.1 is deprecated: This functionality has been moved to @npmcli/fs + `; + + expect(yarn2Proxy.parseErrorFromLogs(YARN2_ERROR_SAMPLE)).toEqual( + 'YN0001 - EXCEPTION: react@npm:28.2.0: No candidates found' + ); + }); + + it('should show unknown yarn2 error', () => { + const YARN2_ERROR_SAMPLE = dedent` + ➤ YN0000: ┌ Resolution step + ➤ YN0000: └ Completed in 2s 369ms + ➤ YN0000: Failed with errors in 2s 372ms + ➤ YN0032: fsevents@npm:2.3.2: Implicit dependencies on node-gyp are discouraged + ➤ YN0061: @npmcli/move-file@npm:2.0.1 is deprecated: This functionality has been moved to @npmcli/fs + `; + + expect(yarn2Proxy.parseErrorFromLogs(YARN2_ERROR_SAMPLE)).toEqual(`Unknown Yarn2 error`); + }); + }); }); diff --git a/code/lib/cli/src/js-package-manager/Yarn2Proxy.ts b/code/lib/cli/src/js-package-manager/Yarn2Proxy.ts index 7005052b0d0a..57a9d02eff0d 100644 --- a/code/lib/cli/src/js-package-manager/Yarn2Proxy.ts +++ b/code/lib/cli/src/js-package-manager/Yarn2Proxy.ts @@ -1,8 +1,65 @@ +import dedent from 'ts-dedent'; +import { createLogStream } from '../utils'; import { JsPackageManager } from './JsPackageManager'; import type { PackageJson } from './PackageJson'; import type { InstallationMetadata, PackageMetadata } from './types'; import { parsePackageData } from './util'; +const YARN2_ERROR_REGEX = /(YN\d{4}):.*?Error:\s+(.*)/i; +const YARN2_ERROR_CODES = { + YN0000: 'UNNAMED', + YN0001: 'EXCEPTION', + YN0002: 'MISSING_PEER_DEPENDENCY', + YN0003: 'CYCLIC_DEPENDENCIES', + YN0004: 'DISABLED_BUILD_SCRIPTS', + YN0005: 'BUILD_DISABLED', + YN0006: 'SOFT_LINK_BUILD', + YN0007: 'MUST_BUILD', + YN0008: 'MUST_REBUILD', + YN0009: 'BUILD_FAILED', + YN0010: 'RESOLVER_NOT_FOUND', + YN0011: 'FETCHER_NOT_FOUND', + YN0012: 'LINKER_NOT_FOUND', + YN0013: 'FETCH_NOT_CACHED', + YN0014: 'YARN_IMPORT_FAILED', + YN0015: 'REMOTE_INVALID', + YN0016: 'REMOTE_NOT_FOUND', + YN0017: 'RESOLUTION_PACK', + YN0018: 'CACHE_CHECKSUM_MISMATCH', + YN0019: 'UNUSED_CACHE_ENTRY', + YN0020: 'MISSING_LOCKFILE_ENTRY', + YN0021: 'WORKSPACE_NOT_FOUND', + YN0022: 'TOO_MANY_MATCHING_WORKSPACES', + YN0023: 'CONSTRAINTS_MISSING_DEPENDENCY', + YN0024: 'CONSTRAINTS_INCOMPATIBLE_DEPENDENCY', + YN0025: 'CONSTRAINTS_EXTRANEOUS_DEPENDENCY', + YN0026: 'CONSTRAINTS_INVALID_DEPENDENCY', + YN0027: 'CANT_SUGGEST_RESOLUTIONS', + YN0028: 'FROZEN_LOCKFILE_EXCEPTION', + YN0029: 'CROSS_DRIVE_VIRTUAL_LOCAL', + YN0030: 'FETCH_FAILED', + YN0031: 'DANGEROUS_NODE_MODULES', + YN0032: 'NODE_GYP_INJECTED', + YN0046: 'AUTOMERGE_FAILED_TO_PARSE', + YN0047: 'AUTOMERGE_IMMUTABLE', + YN0048: 'AUTOMERGE_SUCCESS', + YN0049: 'AUTOMERGE_REQUIRED', + YN0050: 'DEPRECATED_CLI_SETTINGS', + YN0059: 'INVALID_RANGE_PEER_DEPENDENCY', + YN0060: 'INCOMPATIBLE_PEER_DEPENDENCY', + YN0061: 'DEPRECATED_PACKAGE', + YN0062: 'INCOMPATIBLE_OS', + YN0063: 'INCOMPATIBLE_CPU', + YN0068: 'UNUSED_PACKAGE_EXTENSION', + YN0069: 'REDUNDANT_PACKAGE_EXTENSION', + YN0071: 'NM_CANT_INSTALL_EXTERNAL_SOFT_LINK', + YN0072: 'NM_PRESERVE_SYMLINKS_REQUIRED', + YN0074: 'NM_HARDLINKS_MODE_DOWNGRADED', + YN0075: 'PROLOG_INSTANTIATION_ERROR', + YN0076: 'INCOMPATIBLE_ARCHITECTURE', + YN0077: 'GHOST_ARCHITECTURE', +}; + // This encompasses both yarn 2 and yarn 3 export class Yarn2Proxy extends JsPackageManager { readonly type = 'yarn2'; @@ -84,11 +141,31 @@ export class Yarn2Proxy extends JsPackageManager { args = ['-D', ...args]; } - await this.executeCommand({ - command: 'yarn', - args: ['add', ...this.getInstallArgs(), ...args], - stdio: 'inherit', - }); + const { logStream, readLogFile, moveLogFile, removeLogFile } = await createLogStream( + 'init-storybook.log' + ); + + try { + await this.executeCommand({ + command: 'yarn', + args: ['add', ...this.getInstallArgs(), ...args], + 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[]) { @@ -153,4 +230,20 @@ export class Yarn2Proxy extends JsPackageManager { infoCommand: 'yarn why', }; } + + public parseErrorFromLogs(logs: string): string { + const match = logs.match(YARN2_ERROR_REGEX); + let errorCode; + + if (match) { + errorCode = match[1] as keyof typeof YARN2_ERROR_CODES; + const errorMessage = match[2]; + const errorType = YARN2_ERROR_CODES[errorCode]; + if (errorCode && errorMessage) { + return `${errorCode} - ${errorType}: ${errorMessage}`.trim(); + } + } + + return `Unknown Yarn2 error${errorCode ? `: ${errorCode}` : ''}`; + } } diff --git a/code/lib/cli/src/utils.ts b/code/lib/cli/src/utils.ts index 4650fd055dcf..8a62c49c2f92 100644 --- a/code/lib/cli/src/utils.ts +++ b/code/lib/cli/src/utils.ts @@ -1,3 +1,8 @@ +import type { WriteStream } from 'fs-extra'; +import { move, remove, writeFile, readFile, createWriteStream } from 'fs-extra'; +import { join } from 'path'; +import tempy from 'tempy'; + export function parseList(str: string): string[] { return str .split(',') @@ -15,3 +20,65 @@ export function getEnvConfig(program: Record, configEnv: Record Promise; + removeLogFile: () => Promise; + clearLogFile: () => Promise; + readLogFile: () => Promise; + logStream: WriteStream; +}> => { + const finalLogPath = join(process.cwd(), logFileName); + const temporaryLogPath = tempy.file({ name: logFileName }); + + const logStream = createWriteStream(temporaryLogPath, { encoding: 'utf8' }); + + return new Promise((resolve, reject) => { + logStream.once('open', () => { + const moveLogFile = async () => move(temporaryLogPath, finalLogPath, { overwrite: true }); + const clearLogFile = async () => writeFile(temporaryLogPath, ''); + const removeLogFile = async () => remove(temporaryLogPath); + const readLogFile = async () => { + return readFile(temporaryLogPath, 'utf8'); // .replace(/[\x00-\x1F\x7F]/g, ''); + }; + resolve({ logStream, moveLogFile, clearLogFile, removeLogFile, readLogFile }); + }); + logStream.once('error', reject); + }); +};