Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: print a better error message on prepare pkg failure #5847

Merged
merged 10 commits into from Jan 1, 2023
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
11 changes: 11 additions & 0 deletions .changeset/four-colts-care.md
@@ -0,0 +1,11 @@
---
"@pnpm/plugin-commands-publishing": patch
"@pnpm/store-connection-manager": patch
"@pnpm/default-reporter": patch
"@pnpm/prepare-package": patch
"@pnpm/client": patch
"@pnpm/node.fetcher": patch
"pnpm": patch
---

Report to the console when a git-hosted dependency is built [#5847](https://github.com/pnpm/pnpm/pull/5847).
7 changes: 7 additions & 0 deletions .changeset/sweet-items-scream.md
@@ -0,0 +1,7 @@
---
"@pnpm/tarball-fetcher": patch
"@pnpm/git-fetcher": patch
"pnpm": patch
---

Print more contextual information when a git-hosted package fails to be prepared for installation [#5847](https://github.com/pnpm/pnpm/pull/5847).
Expand Up @@ -10,6 +10,7 @@ import { formatPrefix, formatPrefixNoTrim } from './utils/formatPrefix'
import { hlValue } from './outputConstants'

const NODE_MODULES = `${path.sep}node_modules${path.sep}`
const TMP_DIR_IN_STORE = `tmp${path.sep}_tmp_` // git-hosted dependencies are built in these temporary directories

// When streaming processes are spawned, use this color for prefix
const colorWheel = ['cyan', 'magenta', 'blue', 'yellow', 'green', 'red']
Expand Down Expand Up @@ -65,7 +66,7 @@ export function reportLifecycleScripts (
.forEach((log: LifecycleLog) => {
const key = `${log.stage}:${log.depPath}`
lifecycleMessages[key] = lifecycleMessages[key] || {
collapsed: log.wd.includes(NODE_MODULES),
collapsed: log.wd.includes(NODE_MODULES) || log.wd.includes(TMP_DIR_IN_STORE),
output: [],
startTime: process.hrtime(),
status: formatIndentedStatus(chalk.magentaBright('Running...')),
Expand Down Expand Up @@ -113,8 +114,13 @@ function renderCollapsedScriptOutput (
maxWidth: number
}
) {
messageCache.label = messageCache.label ??
`${highlightLastFolder(formatPrefixNoTrim(opts.cwd, log.wd))}: Running ${log.stage} script`
if (!messageCache.label) {
messageCache.label = highlightLastFolder(formatPrefixNoTrim(opts.cwd, log.wd))
if (log.wd.includes(TMP_DIR_IN_STORE)) {
messageCache.label += ` [${log.depPath}]`
}
messageCache.label += `: Running ${log.stage} script`
}
if (!opts.exit) {
updateMessageCache(log, messageCache, opts)
return `${messageCache.label}...`
Expand Down Expand Up @@ -191,7 +197,7 @@ function updateMessageCache (
if (log['exitCode'] === 0) {
messageCache.status = formatIndentedStatus(chalk.magentaBright(`Done in ${time}`))
} else {
messageCache.status = formatIndentedStatus(chalk.red(`Failed in ${time}`))
messageCache.status = formatIndentedStatus(chalk.red(`Failed in ${time} at ${log.wd}`))
}
} else {
messageCache.output.push(formatIndentedOutput(opts.maxWidth, log))
Expand Down
82 changes: 79 additions & 3 deletions cli/default-reporter/test/reportingLifecycleScripts.ts
Expand Up @@ -17,7 +17,6 @@ const OUTPUT_INDENTATION = chalk.magentaBright('│')
const STATUS_INDENTATION = chalk.magentaBright('└─')
const STATUS_RUNNING = chalk.magentaBright('Running...')
const STATUS_DONE = chalk.magentaBright('Done in 1s')
const STATUS_FAILED = chalk.red('Failed in 1s')
const EOL = '\n'

function replaceTimeWith1Sec (text: string) {
Expand Down Expand Up @@ -651,6 +650,79 @@ ${chalk.gray('node_modules/.registry.npmjs.org/qar/1.0.0/node_modules/')}qar: Ru
})
})

test('collapses lifecycle output from preparation of a git-hosted dependency', (done) => {
const output$ = toOutput$({
context: { argv: ['install'] },
reportingOptions: { outputMaxWidth: 79 },
streamParser: createStreamParser(),
})

const wdOfFoo = path.resolve(process.cwd(), 'tmp/_tmp_01243')

lifecycleLogger.debug({
depPath: 'registry.npmjs.org/foo/1.0.0',
optional: false,
script: 'node foo',
stage: 'preinstall',
wd: wdOfFoo,
})
lifecycleLogger.debug({
depPath: 'registry.npmjs.org/foo/1.0.0',
line: 'foo 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20',
stage: 'preinstall',
stdio: 'stdout',
wd: wdOfFoo,
})
lifecycleLogger.debug({
depPath: 'registry.npmjs.org/foo/1.0.0',
optional: false,
script: 'node foo',
stage: 'postinstall',
stdio: 'stdout',
wd: wdOfFoo,
})
lifecycleLogger.debug({
depPath: 'registry.npmjs.org/foo/1.0.0',
line: 'foo I',
stage: 'postinstall',
stdio: 'stdout',
wd: wdOfFoo,
})
lifecycleLogger.debug({
depPath: 'registry.npmjs.org/foo/1.0.0',
line: 'foo II',
stage: 'postinstall',
stdio: 'stdout',
wd: wdOfFoo,
})
lifecycleLogger.debug({
depPath: 'registry.npmjs.org/foo/1.0.0',
line: 'foo III',
stage: 'postinstall',
stdio: 'stdout',
wd: wdOfFoo,
})
lifecycleLogger.debug({
depPath: 'registry.npmjs.org/foo/1.0.0',
exitCode: 0,
optional: false,
stage: 'postinstall',
wd: wdOfFoo,
})

expect.assertions(1)

output$.pipe(skip(2), take(1), map(normalizeNewline)).subscribe({
complete: () => done(),
error: done,
next: (output: unknown) => {
expect(replaceTimeWith1Sec(output as string)).toBe(`\
${chalk.gray('tmp')}_tmp_01234: Running preinstall script...
${chalk.gray('tmp')}_tmp_01234: Running postinstall script, done in 1s`)
},
})
})

test('output of failed optional dependency is not shown', (done) => {
const output$ = toOutput$({
context: { argv: ['install'] },
Expand Down Expand Up @@ -734,7 +806,7 @@ test('output of failed non-optional dependency is printed', (done) => {
${chalk.gray('node_modules/.registry.npmjs.org/foo/1.0.0/node_modules/')}foo: Running install script, failed in 1s
.../foo/1.0.0/node_modules/foo ${INSTALL}$ node foo
${OUTPUT_INDENTATION} foo 0 1 2 3 4 5 6 7 8 9
${STATUS_INDENTATION} ${STATUS_FAILED}`)
${STATUS_INDENTATION} ${failedAt(wd)}`)
},
})
})
Expand Down Expand Up @@ -780,7 +852,7 @@ test('do not fail if the debug log has no output', (done) => {
${chalk.gray('node_modules/.registry.npmjs.org/foo/1.0.0/node_modules/')}foo: Running install script, failed in 1s
.../foo/1.0.0/node_modules/foo ${INSTALL}$ node foo
${OUTPUT_INDENTATION}
${STATUS_INDENTATION} ${STATUS_FAILED}`)
${STATUS_INDENTATION} ${failedAt(wd)}`)
},
})
})
Expand Down Expand Up @@ -844,3 +916,7 @@ Running ${POSTINSTALL} for ${hlPkgId('registry.npmjs.org/bar/1.0.0')}: ${childOu
},
})
})

function failedAt (wd: string) {
return chalk.red(`Failed in 1s at ${wd}`)
}
4 changes: 3 additions & 1 deletion env/node.fetcher/src/index.ts
Expand Up @@ -34,9 +34,11 @@ export async function fetchNode (fetch: FetchFromRegistry, version: string, targ
}
const getAuthHeader = () => undefined
const fetchers = createTarballFetcher(fetch, getAuthHeader, {
rawConfig: {}, // This is not needed for fetching Node.js
retry: opts.retry,
timeout: opts.fetchTimeout,
// These are not needed for fetching Node.js
rawConfig: {},
unsafePerm: false,
})
const cafs = createCafsStore(opts.cafsDir)
const fetchTarball = pickFetcher(fetchers, { tarball })
Expand Down
3 changes: 1 addition & 2 deletions exec/prepare-package/package.json
Expand Up @@ -29,8 +29,7 @@
},
"homepage": "https://github.com/pnpm/pnpm/blob/main/exec/prepare-package#readme",
"dependencies": {
"@pnpm/error": "workspace:*",
"@pnpm/npm-lifecycle": "^2.0.0",
"@pnpm/lifecycle": "workspace:*",
"@pnpm/read-package-json": "workspace:*",
"@zkochan/rimraf": "^2.1.2",
"execa": "npm:safe-execa@0.1.2",
Expand Down
34 changes: 22 additions & 12 deletions exec/prepare-package/src/index.ts
@@ -1,10 +1,8 @@
import path from 'path'
import { PnpmError } from '@pnpm/error'
import lifecycle from '@pnpm/npm-lifecycle'
import { runLifecycleHook, RunLifecycleHookOptions } from '@pnpm/lifecycle'
import { safeReadPackageJsonFromDir } from '@pnpm/read-package-json'
import { PackageScripts } from '@pnpm/types'
import rimraf from '@zkochan/rimraf'
import execa from 'execa'
import preferredPM from 'preferred-pm'
import omit from 'ramda/src/omit'

Expand All @@ -16,23 +14,35 @@ const PREPUBLISH_SCRIPTS = [
'postpublish',
]

export async function preparePackage (opts: { rawConfig: object }, pkgDir: string) {
export interface PreparePackageOptions {
rawConfig: object
unsafePerm?: boolean
}

export async function preparePackage (opts: PreparePackageOptions, pkgDir: string) {
const manifest = await safeReadPackageJsonFromDir(pkgDir)
if (manifest?.scripts == null || !packageShouldBeBuilt(manifest.scripts)) return
const pm = (await preferredPM(pkgDir))?.name ?? 'npm'
// We can't prepare a package without running its lifecycle scripts.
// An alternative solution could be to throw an exception.
const config = omit(['ignore-scripts'], opts.rawConfig)
const env = lifecycle.makeEnv(manifest, { config })
const execOpts = { cwd: pkgDir, env, extendEnv: true }
const execOpts: RunLifecycleHookOptions = {
depPath: `${manifest.name}@${manifest.version}`,
pkgRoot: pkgDir,
// We can't prepare a package without running its lifecycle scripts.
// An alternative solution could be to throw an exception.
rawConfig: omit(['ignore-scripts'], opts.rawConfig),
rootModulesDir: pkgDir, // We don't need this property but there is currently no way to not set it.
unsafePerm: Boolean(opts.unsafePerm),
}
try {
await execa(pm, ['install'], execOpts)
const installScriptName = `${pm}-install`
manifest.scripts[installScriptName] = `${pm} install`
await runLifecycleHook(installScriptName, manifest, execOpts)
for (const scriptName of PREPUBLISH_SCRIPTS) {
if (manifest.scripts[scriptName] == null || manifest.scripts[scriptName] === '') continue
await execa(pm, ['run', scriptName], execOpts)
await runLifecycleHook(scriptName, manifest, execOpts)
}
} catch (err: any) { // eslint-disable-line
throw new PnpmError('PREPARE_PKG_FAILURE', err.shortMessage ?? err.message)
err.code = 'ERR_PNPM_PREPARE_PACKAGE'
throw err
}
await rimraf(path.join(pkgDir, 'node_modules'))
}
Expand Down
6 changes: 3 additions & 3 deletions exec/prepare-package/tsconfig.json
Expand Up @@ -15,14 +15,14 @@
{
"path": "../../__utils__/test-fixtures"
},
{
"path": "../../packages/error"
},
{
"path": "../../packages/types"
},
{
"path": "../../pkg-manifest/read-package-json"
},
{
"path": "../lifecycle"
}
]
}
17 changes: 14 additions & 3 deletions fetching/git-fetcher/src/index.ts
Expand Up @@ -5,9 +5,15 @@ import rimraf from '@zkochan/rimraf'
import execa from 'execa'
import { URL } from 'url'

export function createGitFetcher (createOpts: { gitShallowHosts?: string[], rawConfig: object }) {
export interface CreateGitFetcherOptions {
gitShallowHosts?: string[]
rawConfig: object
unsafePerm?: boolean
}

export function createGitFetcher (createOpts: CreateGitFetcherOptions) {
const allowedHosts = new Set(createOpts?.gitShallowHosts ?? [])
const preparePkg = preparePackage.bind(null, { rawConfig: createOpts.rawConfig })
const preparePkg = preparePackage.bind(null, { rawConfig: createOpts.rawConfig, unsafePerm: createOpts.unsafePerm })

const gitFetcher: GitFetcher = async (cafs, resolution, opts) => {
const tempLocation = await cafs.tempDir()
Expand All @@ -19,7 +25,12 @@ export function createGitFetcher (createOpts: { gitShallowHosts?: string[], rawC
await execGit(['clone', resolution.repo, tempLocation])
}
await execGit(['checkout', resolution.commit], { cwd: tempLocation })
await preparePkg(tempLocation)
try {
await preparePkg(tempLocation)
} catch (err: any) { // eslint-disable-line
err.message = `Failed to prepare git-hosted package fetched from "${resolution.repo}": ${err.message}` // eslint-disable-line
throw err
}
// removing /.git to make directory integrity calculation faster
await rimraf(path.join(tempLocation, '.git'))
const filesIndex = await cafs.addFilesFromDir(tempLocation, opts.manifest)
Expand Down
14 changes: 14 additions & 0 deletions fetching/git-fetcher/test/index.ts
Expand Up @@ -124,6 +124,20 @@ test('still able to shallow fetch for allowed hosts', async () => {
expect(name).toEqual('is-positive')
})

test('fail when preparing a git-hosted package', async () => {
const cafsDir = tempy.directory()
const fetch = createGitFetcher({ rawConfig: {} }).git
const manifest = pDefer<DependencyManifest>()
await expect(
fetch(createCafsStore(cafsDir),
{
commit: 'ba58874aae1210a777eb309dd01a9fdacc7e54e7',
repo: 'https://github.com/pnpm-e2e/prepare-script-fails.git',
type: 'git',
}, { manifest })
).rejects.toThrow('Failed to prepare git-hosted package fetched from "https://github.com/pnpm-e2e/prepare-script-fails.git": @pnpm.e2e/prepare-script-fails@1.0.0 npm-install: `npm install`')
})

function prefixGitArgs (): string[] {
return process.platform === 'win32' ? ['-c', 'core.longpaths=true'] : []
}
19 changes: 14 additions & 5 deletions fetching/tarball-fetcher/src/gitHostedTarballFetcher.ts
Expand Up @@ -10,17 +10,26 @@ interface Resolution {
tarball: string
}

export function createGitHostedTarballFetcher (fetchRemoteTarball: FetchFunction, rawConfig: object): FetchFunction {
export interface CreateGitHostedTarballFetcher {
rawConfig: object
unsafePerm?: boolean
}

export function createGitHostedTarballFetcher (fetchRemoteTarball: FetchFunction, fetcherOpts: CreateGitHostedTarballFetcher): FetchFunction {
const fetch = async (cafs: Cafs, resolution: Resolution, opts: FetchOptions) => {
const { filesIndex } = await fetchRemoteTarball(cafs, resolution, opts)

return { filesIndex: await prepareGitHostedPkg(filesIndex as FilesIndex, cafs, rawConfig) }
try {
return { filesIndex: await prepareGitHostedPkg(filesIndex as FilesIndex, cafs, fetcherOpts) }
} catch (err: any) { // eslint-disable-line
err.message = `Failed to prepare git-hosted package fetched from "${resolution.tarball}": ${err.message}` // eslint-disable-line
throw err
}
}

return fetch as FetchFunction
}

async function prepareGitHostedPkg (filesIndex: FilesIndex, cafs: Cafs, rawConfig: object) {
async function prepareGitHostedPkg (filesIndex: FilesIndex, cafs: Cafs, opts: CreateGitHostedTarballFetcher) {
const tempLocation = await cafs.tempDir()
await cafs.importPackage(tempLocation, {
filesResponse: {
Expand All @@ -29,7 +38,7 @@ async function prepareGitHostedPkg (filesIndex: FilesIndex, cafs: Cafs, rawConfi
},
force: true,
})
await preparePackage({ rawConfig }, tempLocation)
await preparePackage(opts, tempLocation)
const newFilesIndex = await cafs.addFilesFromDir(tempLocation)
// Important! We cannot remove the temp location at this stage.
// Even though we have the index of the package,
Expand Down
3 changes: 2 additions & 1 deletion fetching/tarball-fetcher/src/index.ts
Expand Up @@ -32,6 +32,7 @@ export function createTarballFetcher (
getAuthHeader: GetAuthHeader,
opts: {
rawConfig: object
unsafePerm?: boolean
timeout?: number
retry?: RetryTimeoutOptions
offline?: boolean
Expand All @@ -51,7 +52,7 @@ export function createTarballFetcher (
return {
localTarball: createLocalTarballFetcher(),
remoteTarball: remoteTarballFetcher,
gitHostedTarball: createGitHostedTarballFetcher(remoteTarballFetcher, opts.rawConfig),
gitHostedTarball: createGitHostedTarballFetcher(remoteTarballFetcher, opts),
}
}

Expand Down