feat: add default-reporter

This commit is contained in:
Zoltan Kochan
2018-05-13 14:41:15 +03:00
22 changed files with 6231 additions and 0 deletions

View File

@@ -0,0 +1,11 @@
root = true
[*]
insert_final_newline = true
charset = utf-8
trim_trailing_whitespace = true
end_of_line = lf
[*.{ts,js,json}]
indent_style = space
indent_size = 2

39
packages/default-reporter/.gitignore vendored Normal file
View File

@@ -0,0 +1,39 @@
# Logs
logs
*.log
npm-debug.log*
# Runtime data
pids
*.pid
*.seed
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules
jspm_packages
# Optional npm cache directory
.npm
# Optional REPL history
.node_repl_history
lib

View File

@@ -0,0 +1,2 @@
tag-version-prefix = pnpm-default-reporter/
message = chore(release): %s

View File

@@ -0,0 +1,15 @@
language: node_js
node_js:
- 4
- 6
- 8
- 10
sudo: false
before_install:
- curl -L https://unpkg.com/@pnpm/self-installer | node
install:
- pnpm install
script:
- npm test
notifications:
email: false

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2017-2018 Zoltan Kochan <z@kochan.io>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,15 @@
# pnpm-default-reporter
[![Status](https://travis-ci.org/pnpm/pnpm-default-reporter.svg?branch=master)](https://travis-ci.org/pnpm/pnpm-default-reporter "See test builds")
> The default reporter of pnpm
## Install
```
npm install pnpm-default-reporter
```
## License
[MIT](LICENSE)

View File

@@ -0,0 +1,93 @@
{
"name": "pnpm-default-reporter",
"version": "0.16.4",
"description": "The default reporter of pnpm",
"main": "lib/index.js",
"typings": "lib/index.d.ts",
"files": [
"lib"
],
"bin": "lib/bin/pnpm-default-reporter.js",
"scripts": {
"lint": "tslint -c tslint.json --project .",
"pretty-test": "preview && ts-node test | tap-diff",
"test": "npm run lint && npm run just-test",
"just-test": "preview && ts-node test --type-check",
"tsc": "rimraf lib && tsc",
"prepublishOnly": "npm run tsc"
},
"repository": {
"type": "git",
"url": "git+https://github.com/pnpm/pnpm-reporter-default.git"
},
"keywords": [
"pnpm-reporter"
],
"author": {
"name": "Zoltan Kochan",
"email": "z@kochan.io",
"url": "https://www.kochan.io/",
"twitter": "ZoltanKochan"
},
"engines": {
"node": ">=4"
},
"license": "MIT",
"bugs": {
"url": "https://github.com/pnpm/pnpm-reporter-default/issues"
},
"homepage": "https://github.com/pnpm/pnpm-reporter-default#readme",
"peerDependencies": {
"supi": ">=0.16.0 <0.19.0"
},
"dependencies": {
"@types/common-tags": "^1.2.5",
"@types/node": "^9.3.0 || 10",
"@types/ramda": "^0.25.6",
"@types/semver": "^5.4.0",
"@types/strip-ansi": "^3.0.0",
"ansi-diff": "^1.0.10",
"chalk": "^2.2.0",
"cli-cursor": "^2.1.0",
"common-tags": "^1.4.0",
"most": "^1.7.2",
"most-last": "^1.0.0",
"ndjson": "^1.5.0",
"normalize-path": "^3.0.0",
"pretty-bytes": "^4.0.2",
"ramda": "^0.25.0",
"right-pad": "^1.0.1",
"semver": "^5.4.1",
"stacktracey": "^1.2.87",
"string-length": "^2.0.0",
"string.prototype.padstart": "^3.0.0",
"strip-ansi": "^4.0.0",
"zen-push": "^0.2.1"
},
"devDependencies": {
"@pnpm/logger": "^1.0.0",
"@types/delay": "^2.0.1",
"@types/tape": "^4.2.30",
"commitizen": "^2.9.5",
"delay": "^2.0.0",
"ghooks": "^2.0.0",
"mos-tap-diff": "^1.0.0",
"normalize-newline": "^3.0.0",
"package-preview": "^1.0.0",
"rimraf": "^2.5.4",
"supi": "^0.18.0",
"tape": "^4.8.0",
"ts-node": "^6.0.0",
"tslint": "^5.7.0",
"typescript": "^2.6.2",
"validate-commit-msg": "^2.8.2"
},
"config": {
"commitizen": {
"path": "./node_modules/cz-conventional-changelog"
},
"ghooks": {
"commit-msg": "node ./node_modules/validate-commit-msg/index.js"
}
}
}

View File

@@ -0,0 +1,6 @@
{
"extends": [
"config:base"
],
"pinVersions": false
}

View File

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,9 @@
#!/usr/bin/env node
import ndjson = require('ndjson')
import reporter from '..'
process.stdin.resume()
process.stdin.setEncoding('utf8')
const streamParser = process.stdin
.pipe(ndjson.parse())
reporter(streamParser, {cmd: 'install'}) // TODO: make it smarter

View File

@@ -0,0 +1,3 @@
// In terminals, always Unix line endings are used
// even on Windows
export const EOL = '\n'

View File

@@ -0,0 +1,158 @@
import createDiffer = require('ansi-diff')
import cliCursor = require('cli-cursor')
import most = require('most')
import R = require('ramda')
import * as supi from 'supi'
import PushStream = require('zen-push')
import {EOL} from './constants'
import mergeOutputs from './mergeOutputs'
import reporterForClient from './reporterForClient'
import reporterForServer from './reporterForServer'
export default function (
streamParser: object,
opts: {
cmd: string,
cwd?: string,
appendOnly?: boolean,
throttleProgress?: number,
width?: number,
},
) {
if (opts.cmd === 'server') {
const log$ = most.fromEvent<supi.Log>('data', streamParser)
reporterForServer(log$)
return
}
const width = opts.width || process.stdout.columns && process.stdout.columns - 2 || 80
const output$ = toOutput$(streamParser, {...opts, width})
if (opts.appendOnly) {
output$
.subscribe({
complete () {}, // tslint:disable-line:no-empty
error: (err) => console.error(err.message),
next: (line) => console.log(line),
})
return
}
cliCursor.hide()
const diff = createDiffer({
height: process.stdout.rows,
width,
})
output$
.subscribe({
complete () {}, // tslint:disable-line:no-empty
error: (err) => logUpdate(err.message),
next: logUpdate,
})
function logUpdate (view: string) {
process.stdout.write(diff.update(`${view}${EOL}`))
}
}
export function toOutput$ (
streamParser: object,
opts: {
cmd: string,
cwd?: string,
appendOnly?: boolean,
throttleProgress?: number,
width?: number,
},
): most.Stream<string> {
opts = opts || {}
const isRecursive = opts.cmd === 'recursive'
const progressPushStream = new PushStream()
const stagePushStream = new PushStream()
const deprecationPushStream = new PushStream()
const summaryPushStream = new PushStream()
const lifecyclePushStream = new PushStream()
const statsPushStream = new PushStream()
const installCheckPushStream = new PushStream()
const registryPushStream = new PushStream()
const rootPushStream = new PushStream()
const packageJsonPushStream = new PushStream()
const linkPushStream = new PushStream()
const cliPushStream = new PushStream()
const otherPushStream = new PushStream()
const hookPushStream = new PushStream()
const skippedOptionalDependencyPushStream = new PushStream()
setTimeout(() => { // setTimeout is a workaround for a strange bug in most https://github.com/cujojs/most/issues/491
streamParser['on']('data', (log: supi.Log) => {
switch (log.name) {
case 'pnpm:progress':
progressPushStream.next(log as supi.ProgressLog)
break
case 'pnpm:stage':
stagePushStream.next(log as supi.StageLog)
break
case 'pnpm:deprecation':
deprecationPushStream.next(log as supi.DeprecationLog)
break
case 'pnpm:summary':
summaryPushStream.next(log)
break
case 'pnpm:lifecycle':
lifecyclePushStream.next(log as supi.LifecycleLog)
break
case 'pnpm:stats':
statsPushStream.next(log as supi.StatsLog)
break
case 'pnpm:install-check':
installCheckPushStream.next(log as supi.InstallCheckLog)
break
case 'pnpm:registry':
registryPushStream.next(log as supi.RegistryLog)
break
case 'pnpm:root':
rootPushStream.next(log as supi.RootLog)
break
case 'pnpm:package-json':
packageJsonPushStream.next(log as supi.PackageJsonLog)
break
case 'pnpm:link' as any: // tslint:disable-line
linkPushStream.next(log)
break
case 'pnpm:cli' as any: // tslint:disable-line
cliPushStream.next(log)
break
case 'pnpm:hook' as any: // tslint:disable-line
hookPushStream.next(log)
break
case 'pnpm:skipped-optional-dependency':
skippedOptionalDependencyPushStream.next(log as supi.SkippedOptionalDependencyLog)
break
case 'pnpm' as any: // tslint:disable-line
otherPushStream.next(log)
break
}
})
}, 0)
const log$ = {
cli: most.from<supi.Log>(cliPushStream.observable),
deprecation: most.from<supi.DeprecationLog>(deprecationPushStream.observable),
hook: most.from<supi.Log>(hookPushStream.observable),
installCheck: most.from<supi.InstallCheckLog>(installCheckPushStream.observable),
lifecycle: most.from<supi.LifecycleLog>(lifecyclePushStream.observable),
link: most.from<supi.Log>(linkPushStream.observable),
other: most.from<supi.Log>(otherPushStream.observable),
packageJson: most.from<supi.PackageJsonLog>(packageJsonPushStream.observable),
progress: most.from<supi.ProgressLog>(progressPushStream.observable),
registry: most.from<supi.RegistryLog>(registryPushStream.observable),
root: most.from<supi.RootLog>(rootPushStream.observable),
skippedOptionalDependency: most.from<supi.SkippedOptionalDependencyLog>(skippedOptionalDependencyPushStream.observable),
stage: most.from<supi.StageLog>(stagePushStream.observable),
stats: most.from<supi.StatsLog>(statsPushStream.observable),
summary: most.from<supi.Log>(summaryPushStream.observable),
}
const outputs: Array<most.Stream<most.Stream<{msg: string}>>> = reporterForClient(log$, isRecursive, opts.cmd, opts.width, opts.appendOnly, opts.throttleProgress, opts.cwd)
if (opts.appendOnly) {
return most.join(
most.mergeArray(outputs)
.map((log: most.Stream<{msg: string}>) => log.map((msg) => msg.msg)),
)
}
return mergeOutputs(outputs).multicast()
}

View File

@@ -0,0 +1,67 @@
import most = require('most')
import {EOL} from './constants'
export default function mergeOutputs (outputs: Array<most.Stream<most.Stream<{msg: string}>>>): most.Stream<string> {
let blockNo = 0
let fixedBlockNo = 0
let started = false
return most.join(
most.mergeArray(outputs)
.map((log: most.Stream<{msg: string}>) => {
let currentBlockNo = -1
let currentFixedBlockNo = -1
return log
.map((msg) => {
if (msg['fixed']) {
if (currentFixedBlockNo === -1) {
currentFixedBlockNo = fixedBlockNo++
}
return {
blockNo: currentFixedBlockNo,
fixed: true,
msg: msg.msg,
}
}
if (currentBlockNo === -1) {
currentBlockNo = blockNo++
}
return {
blockNo: currentBlockNo,
fixed: false,
msg: typeof msg === 'string' ? msg : msg.msg,
prevFixedBlockNo: currentFixedBlockNo,
}
})
}),
)
.scan((acc, log) => {
if (log.fixed === true) {
acc.fixedBlocks[log.blockNo] = log.msg
} else {
delete acc.fixedBlocks[log['prevFixedBlockNo'] as number]
acc.blocks[log.blockNo] = log.msg
}
return acc
}, {fixedBlocks: [], blocks: []} as {fixedBlocks: string[], blocks: string[]})
.map((sections) => {
const fixedBlocks = sections.fixedBlocks.filter(Boolean)
const nonFixedPart = sections.blocks.filter(Boolean).join(EOL)
if (!fixedBlocks.length) {
return nonFixedPart
}
const fixedPart = fixedBlocks.join(EOL)
if (!nonFixedPart) {
return fixedPart
}
return `${nonFixedPart}${EOL}${fixedPart}`
})
.filter((msg) => {
if (started) {
return true
}
if (msg === '') return false
started = true
return true
})
.skipRepeats()
}

View File

@@ -0,0 +1,148 @@
import most = require('most')
import R = require('ramda')
import {
DeprecationLog,
Log,
} from 'supi'
import * as supi from 'supi'
export interface PackageDiff {
added: boolean,
from?: string,
name: string,
realName?: string,
version?: string,
deprecated?: boolean,
latest?: string,
linked?: true,
}
export interface Map<T> {
[index: string]: T,
}
export const propertyByDependencyType = {
dev: 'devDependencies',
optional: 'optionalDependencies',
prod: 'dependencies',
}
export default function (
log$: {
progress: most.Stream<supi.ProgressLog>,
stage: most.Stream<supi.StageLog>,
deprecation: most.Stream<supi.DeprecationLog>,
summary: most.Stream<supi.Log>,
lifecycle: most.Stream<supi.LifecycleLog>,
stats: most.Stream<supi.StatsLog>,
installCheck: most.Stream<supi.InstallCheckLog>,
registry: most.Stream<supi.RegistryLog>,
root: most.Stream<supi.RootLog>,
packageJson: most.Stream<supi.PackageJsonLog>,
link: most.Stream<supi.Log>,
other: most.Stream<supi.Log>,
},
) {
const deprecationSet$ = log$.deprecation
.scan((acc, log) => {
acc.add(log.pkgId)
return acc
}, new Set())
const pkgsDiff$ = most.combine(
(rootLog, deprecationSet) => [rootLog, deprecationSet],
log$.root,
deprecationSet$,
)
.scan((pkgsDiff, args) => {
const rootLog = args[0]
const deprecationSet = args[1] as Set<string>
if (rootLog['added']) {
pkgsDiff[rootLog['added'].dependencyType][`+${rootLog['added'].name}`] = {
added: true,
deprecated: deprecationSet.has(rootLog['added'].id),
latest: rootLog['added'].latest,
name: rootLog['added'].name,
realName: rootLog['added'].realName,
version: rootLog['added'].version,
}
return pkgsDiff
}
if (rootLog['removed']) {
pkgsDiff[rootLog['removed'].dependencyType][`-${rootLog['removed'].name}`] = {
added: false,
name: rootLog['removed'].name,
version: rootLog['removed'].version,
}
return pkgsDiff
}
if (rootLog['linked']) {
pkgsDiff[rootLog['linked'].dependencyType][`>${rootLog['linked'].name}`] = {
added: false,
from: rootLog['linked'].from,
linked: true,
name: rootLog['linked'].name,
}
return pkgsDiff
}
return pkgsDiff
}, {
dev: {},
optional: {},
prod: {},
} as {
dev: Map<PackageDiff>,
prod: Map<PackageDiff>,
optional: Map<PackageDiff>,
})
const packageJson$ = most.fromPromise(
most.merge(
log$.packageJson,
log$.summary.constant({}),
)
.take(2)
.reduce(R.merge, {}),
)
return most.combine(
(pkgsDiff, packageJsons) => {
const initialPackageJson = packageJsons['initial']
const updatedPackageJson = packageJsons['updated']
if (!initialPackageJson || !updatedPackageJson) return pkgsDiff
for (const depType of ['prod', 'optional', 'dev']) {
const prop = propertyByDependencyType[depType]
const initialDeps = R.keys(initialPackageJson[prop])
const updatedDeps = R.keys(updatedPackageJson[prop])
const removedDeps = R.difference(initialDeps, updatedDeps)
for (const removedDep of removedDeps) {
if (!pkgsDiff[depType][`-${removedDep}`]) {
pkgsDiff[depType][`-${removedDep}`] = {
added: false,
name: removedDep,
version: initialPackageJson[prop][removedDep],
}
}
}
const addedDeps = R.difference(updatedDeps, initialDeps)
for (const addedDep of addedDeps) {
if (!pkgsDiff[depType][`+${addedDep}`]) {
pkgsDiff[depType][`+${addedDep}`] = {
added: true,
name: addedDep,
version: updatedPackageJson[prop][addedDep],
}
}
}
}
return pkgsDiff
},
pkgsDiff$,
packageJson$,
)
}

View File

@@ -0,0 +1,133 @@
import chalk from 'chalk'
import commonTags = require('common-tags')
import StackTracey = require('stacktracey')
import {Log} from 'supi'
import {EOL} from './constants'
const stripIndent = commonTags.stripIndent
const stripIndents = commonTags.stripIndents
const highlight = chalk.yellow
const colorPath = chalk.gray
export default function reportError (logObj: Log) {
if (logObj['err']) {
const err = logObj['err'] as (Error & { code: string, stack: object })
switch (err.code) {
case 'UNEXPECTED_STORE':
return reportUnexpectedStore(err, logObj['message'])
case 'STORE_BREAKING_CHANGE':
return reportStoreBreakingChange(err, logObj['message'])
case 'MODULES_BREAKING_CHANGE':
return reportModulesBreakingChange(err, logObj['message'])
case 'MODIFIED_DEPENDENCY':
return reportModifiedDependency(err, logObj['message'])
case 'SHRINKWRAP_BREAKING_CHANGE':
return reportShrinkwrapBreakingChange(err, logObj['message'])
default:
return formatGenericError(err.message || logObj['message'], err.stack)
}
}
return formatErrorSummary(logObj['message'])
}
function reportUnexpectedStore (err: Error, msg: object) {
return stripIndent`
${formatErrorSummary(err.message)}
expected: ${highlight(msg['expectedStorePath'])}
actual: ${highlight(msg['actualStorePath'])}
If you want to use the new store, run the same command with the ${highlight('--force')} parameter.
`
}
function reportStoreBreakingChange (err: Error, msg: object) {
let output = stripIndent`
${formatErrorSummary(`The store used for the current node_modules is incomatible with the current version of pnpm`)}
Store path: ${colorPath(msg['storePath'])}
Try running the same command with the ${highlight('--force')} parameter.
`
if (msg['additionalInformation']) {
output += EOL + EOL + msg['additionalInformation']
}
output += formatRelatedSources(msg)
return output
}
function reportModulesBreakingChange (err: Error, msg: object) {
let output = stripIndent`
${formatErrorSummary(`The current version of pnpm is not compatible with the available node_modules structure`)}
node_modules path: ${colorPath(msg['modulesPath'])}
Run ${highlight('pnpm install --force')} to recreate node_modules.
`
if (msg['additionalInformation']) {
output += EOL + EOL + msg['additionalInformation']
}
output += formatRelatedSources(msg)
return output
}
function formatRelatedSources (msg: object) {
let output = ''
if (!msg['relatedIssue'] && !msg['relatedPR']) return output
output += EOL
if (msg['relatedIssue']) {
output += EOL + `Related issue: ${colorPath(`https://github.com/pnpm/pnpm/issues/${msg['relatedIssue']}`)}`
}
if (msg['relatedPR']) {
output += EOL + `Related PR: ${colorPath(`https://github.com/pnpm/pnpm/pull/${msg['relatedPR']}`)}`
}
return output
}
function formatGenericError (errorMessage: string, stack: object) {
if (stack) {
let prettyStack: string | undefined
try {
prettyStack = new StackTracey(stack).pretty
} catch (err) {
prettyStack = undefined
}
if (prettyStack) {
return stripIndents`
${formatErrorSummary(errorMessage)}
${prettyStack}
`
}
}
return formatErrorSummary(errorMessage)
}
function formatErrorSummary (message: string) {
return `${chalk.bgRed.black('\u2009ERROR\u2009')} ${chalk.red(message)}`
}
function reportModifiedDependency (err: Error, msg: object) {
return stripIndent`
${formatErrorSummary('Packages in the store have been mutated')}
These packages are modified:
${msg['modified'].map((pkgPath: string) => colorPath(pkgPath)).join(EOL)}
You can run ${highlight('pnpm install')} to refetch the modified packages
`
}
function reportShrinkwrapBreakingChange (err: Error, msg: object) {
return stripIndent`
${formatErrorSummary(err.message)}
Run with the ${highlight('--force')} parameter to recreate the shrinkwrap file.
`
}

View File

@@ -0,0 +1,533 @@
import chalk from 'chalk'
import most = require('most')
import {last as mostLast} from 'most-last'
import normalize = require('normalize-path')
import os = require('os')
import path = require('path')
import prettyBytes = require('pretty-bytes')
import R = require('ramda')
import rightPad = require('right-pad')
import semver = require('semver')
import stringLength = require('string-length')
import padStart = require('string.prototype.padstart')
import stripAnsi = require('strip-ansi')
import {
DeprecationLog,
InstallCheckLog,
LifecycleLog,
Log,
ProgressLog,
RegistryLog,
} from 'supi'
import * as supi from 'supi'
import PushStream = require('zen-push')
import {EOL} from './constants'
import getPkgsDiff, {
PackageDiff,
propertyByDependencyType,
} from './pkgsDiff'
import reportError from './reportError'
const BIG_TARBALL_SIZE = 1024 * 1024 * 5 // 5 MB
const ADDED_CHAR = chalk.green('+')
const REMOVED_CHAR = chalk.red('-')
const LINKED_CHAR = chalk.magentaBright('#')
const PREFIX_MAX_LENGTH = 40
const hlValue = chalk.blue
const hlPkgId = chalk['whiteBright']
export default function (
log$: {
progress: most.Stream<supi.ProgressLog>,
stage: most.Stream<supi.StageLog>,
deprecation: most.Stream<supi.DeprecationLog>,
summary: most.Stream<supi.Log>,
lifecycle: most.Stream<supi.LifecycleLog>,
stats: most.Stream<supi.StatsLog>,
installCheck: most.Stream<supi.InstallCheckLog>,
registry: most.Stream<supi.RegistryLog>,
root: most.Stream<supi.RootLog>,
packageJson: most.Stream<supi.PackageJsonLog>,
link: most.Stream<supi.Log>,
other: most.Stream<supi.Log>,
cli: most.Stream<supi.Log>,
hook: most.Stream<supi.Log>,
skippedOptionalDependency: most.Stream<supi.SkippedOptionalDependencyLog>,
},
isRecursive: boolean,
cmd: string,
widthArg?: number,
appendOnly?: boolean,
throttleProgress?: number,
cwdArg?: string,
): Array<most.Stream<most.Stream<{msg: string}>>> {
const width = widthArg || process.stdout.columns || 80
const outputs: Array<most.Stream<most.Stream<{msg: string}>>> = []
const cwd = cwdArg || process.cwd()
const resolutionDone$ = isRecursive
? most.never()
: log$.stage
.filter((log) => log.message === 'resolution_done')
const resolvingContentLog$ = log$.progress
.filter((log) => log.status === 'resolving_content')
.scan(R.inc, 0)
.skip(1)
.until(resolutionDone$)
const fedtchedLog$ = log$.progress
.filter((log) => log.status === 'fetched')
.scan(R.inc, 0)
const foundInStoreLog$ = log$.progress
.filter((log) => log.status === 'found_in_store')
.scan(R.inc, 0)
function createStatusMessage (resolving: number, fetched: number, foundInStore: number, importingDone: boolean) {
const msg = `Resolving: total ${hlValue(resolving.toString())}, reused ${hlValue(foundInStore.toString())}, downloaded ${hlValue(fetched.toString())}`
if (importingDone) {
return {
done: true,
fixed: false,
msg: `${msg}, done`,
}
}
return {
fixed: true,
msg,
}
}
const importingDone$ = log$.stage.filter((log) => log.message === 'importing_done')
.constant(true)
.take(1)
.startWith(false)
.multicast()
if (!isRecursive && typeof throttleProgress === 'number' && throttleProgress > 0) {
const resolutionStarted$ = log$.stage
.filter((log) => log.message === 'resolution_started' || log.message === 'importing_started').take(1)
const commandDone$ = log$.cli.filter((log) => log['message'] === 'command_done')
// Reporting is done every `throttleProgress` milliseconds
// and once all packages are fetched.
const sampler = most.merge(
most.periodic(throttleProgress).since(resolutionStarted$).until(most.merge<{}>(importingDone$.skip(1), commandDone$)),
importingDone$,
)
const progress = most.sample(
createStatusMessage,
sampler,
resolvingContentLog$,
fedtchedLog$,
foundInStoreLog$,
importingDone$,
)
// Avoid logs after all resolved packages were downloaded.
// Fixing issue: https://github.com/pnpm/pnpm/issues/1028#issuecomment-364782901
.skipAfter((msg) => msg.done === true)
outputs.push(most.of(progress))
} else {
const progress = most.combine(
createStatusMessage,
resolvingContentLog$,
fedtchedLog$,
foundInStoreLog$,
isRecursive ? most.of(false) : importingDone$,
)
outputs.push(most.of(progress))
}
if (!appendOnly) {
const tarballsProgressOutput$ = log$.progress
.filter((log) => log.status === 'fetching_started' &&
typeof log.size === 'number' && log.size >= BIG_TARBALL_SIZE &&
// When retrying the download, keep the existing progress line.
// Fixing issue: https://github.com/pnpm/pnpm/issues/1013
log.attempt === 1)
.map((startedLog) => {
const size = prettyBytes(startedLog['size'])
return log$.progress
.filter((log) => log.status === 'fetching_progress' && log.pkgId === startedLog['pkgId'])
.map((log) => log['downloaded'])
.startWith(0)
.map((downloadedRaw) => {
const done = startedLog['size'] === downloadedRaw
const downloaded = prettyBytes(downloadedRaw)
return {
fixed: !done,
msg: `Downloading ${hlPkgId(startedLog['pkgId'])}: ${hlValue(downloaded)}/${hlValue(size)}${done ? ', done' : ''}`,
}
})
})
outputs.push(tarballsProgressOutput$)
const lifecycleMessages: {
[depPath: string]: {
output: string[],
script: string,
},
} = {}
const lifecycleStreamByDepPath: {
[depPath: string]: {
observable: most.Observable<{msg: string}>,
complete (): void,
next (obj: object): void,
},
} = {}
const lifecyclePushStream = new PushStream()
outputs.push(most.from(lifecyclePushStream.observable))
log$.lifecycle
.forEach((log: LifecycleLog) => {
const key = `${log.stage}:${log.depPath}`
lifecycleMessages[key] = lifecycleMessages[key] || {output: []}
if (log['script']) {
lifecycleMessages[key].script = formatLifecycle(cwd, log)
} else {
if (!lifecycleMessages[key].output.length || log['exitCode'] !== 0) {
lifecycleMessages[key].output.push(formatLifecycle(cwd, log))
}
if (lifecycleMessages[key].output.length > 3) {
lifecycleMessages[key].output.shift()
}
}
if (!lifecycleStreamByDepPath[key]) {
lifecycleStreamByDepPath[key] = new PushStream()
lifecyclePushStream.next(most.from(lifecycleStreamByDepPath[key].observable))
}
lifecycleStreamByDepPath[key].next({
msg: EOL + [lifecycleMessages[key].script].concat(lifecycleMessages[key].output).join(EOL),
})
if (typeof log['exitCode'] === 'number') {
lifecycleStreamByDepPath[key].complete()
}
})
} else {
const lifecycleMessages: {[pkgId: string]: string} = {}
const lifecycleOutput$ = most.of(
log$.lifecycle
.map((log: LifecycleLog) => ({ msg: formatLifecycle(cwd, log) })),
)
outputs.push(lifecycleOutput$)
}
if (!isRecursive) {
const pkgsDiff$ = getPkgsDiff(log$)
const summaryLog$ = log$.summary
.take(1)
const summaryOutput$ = most.combine(
(pkgsDiff) => {
let msg = ''
for (const depType of ['prod', 'optional', 'dev']) {
const diffs = R.values(pkgsDiff[depType])
if (diffs.length) {
msg += EOL
msg += chalk.blue(`${propertyByDependencyType[depType]}:`)
msg += EOL
msg += printDiffs(diffs)
msg += EOL
}
}
return {msg}
},
pkgsDiff$,
summaryLog$,
)
.take(1)
.map(most.of)
outputs.push(summaryOutput$)
const deprecationOutput$ = log$.deprecation
// print warnings only about deprecated packages from the root
.filter((log) => log.depth === 0)
.map((log) => {
return {
msg: formatWarn(`${chalk.red('deprecated')} ${log.pkgName}@${log.pkgVersion}: ${log.deprecated}`),
}
})
.map(most.of)
outputs.push(deprecationOutput$)
}
if (!isRecursive) {
outputs.push(
most.fromPromise(
log$.stats
.take((cmd === 'install' || cmd === 'update') ? 2 : 1)
.reduce((acc, log) => {
if (typeof log['added'] === 'number') {
acc['added'] = log['added']
} else if (typeof log['removed'] === 'number') {
acc['removed'] = log['removed']
}
return acc
}, {}),
)
.map((stats) => {
if (!stats['removed'] && !stats['added']) {
return most.of({msg: 'Already up-to-date'})
}
let msg = 'Packages:'
if (stats['added']) {
msg += ' ' + chalk.green(`+${stats['added']}`)
}
if (stats['removed']) {
msg += ' ' + chalk.red(`-${stats['removed']}`)
}
msg += EOL + printPlusesAndMinuses(width, (stats['added'] || 0), (stats['removed'] || 0))
return most.of({msg})
}),
)
const installCheckOutput$ = log$.installCheck
.map(formatInstallCheck)
.filter(Boolean)
.map((msg) => ({msg}))
.map(most.of) as most.Stream<most.Stream<{msg: string}>>
outputs.push(installCheckOutput$)
const registryOutput$ = log$.registry
.filter((log) => log.level === 'warn')
.map((log: RegistryLog) => ({msg: formatWarn(log.message)}))
.map(most.of)
outputs.push(registryOutput$)
const miscOutput$ = most.merge(log$.link, log$.other)
.map((obj) => {
if (obj.level === 'debug') return
if (obj.level === 'warn') {
return formatWarn(obj['message'])
}
if (obj.level === 'error') {
return reportError(obj)
}
return obj['message']
})
.map((msg) => ({msg}))
.map(most.of)
outputs.push(miscOutput$)
outputs.push(
log$.skippedOptionalDependency
.filter((log) => Boolean(log.parents && log.parents.length === 0))
.map((log) => most.of({
msg: `info: ${
log.package['id'] || log.package.name && (`${log.package.name}@${log.package.version}`) || log.package['pref']
} is an optional dependency and failed compatibility check. Excluding it from installation.`,
})),
)
} else {
outputs.push(
log$.stats
.loop((stats, log) => {
if (stats[log.prefix]) {
const value = {...stats[log.prefix], ...log}
delete stats[log.prefix]
return {seed: stats, value}
}
stats[log.prefix] = log
return {seed: stats, value: null}
}, {})
.filter((stats) => stats !== null && (stats['removed'] || stats['added']))
.map((stats) => {
const prefix = formatPrefix(cwd, stats['prefix'])
let msg = `${rightPad(prefix, PREFIX_MAX_LENGTH)} |`
if (stats['added']) {
msg += ` ${padStep(chalk.green(`+${stats['added']}`), 4)}`
}
if (stats['removed']) {
msg += ` ${padStep(chalk.red(`-${stats['removed']}`), 4)}`
}
const rest = Math.max(0, width - 1 - stringLength(msg))
msg += ' ' + printPlusesAndMinuses(rest, roundStats(stats['added'] || 0), roundStats(stats['removed'] || 0))
return most.of({msg})
}),
)
const miscOutput$ = log$.other
.filter((obj) => obj.level === 'error')
.map((obj) => {
if (obj['message']['prefix']) {
return obj['message']['prefix'] + ':' + os.EOL + reportError(obj)
}
return reportError(obj)
})
.map((msg) => ({msg}))
.map(most.of)
outputs.push(miscOutput$)
}
if (!isRecursive) {
const hookOutput$ = log$.hook
.map((log) => ({msg: `${chalk.magentaBright(log['hook'])}: ${log['message']}`}))
.map(most.of)
outputs.push(hookOutput$)
} else {
const hookOutput$ = log$.hook
.map((log) => ({
msg: `${rightPad(formatPrefix(cwd, log['prefix']), PREFIX_MAX_LENGTH)} | ${chalk.magentaBright(log['hook'])}: ${log['message']}`,
}))
.map(most.of)
outputs.push(hookOutput$)
}
return outputs
}
function padStep (s: string, step: number) {
const sLength = stringLength(s)
const placeholderLength = Math.ceil(sLength / step) * step
if (sLength < placeholderLength) {
return R.repeat(' ', placeholderLength - sLength).join('') + s
}
return s
}
function roundStats (stat: number): number {
if (stat === 0) return 0
return Math.max(1, Math.round(stat / 10))
}
function formatPrefix (cwd: string, prefix: string) {
prefix = normalize(path.relative(cwd, prefix) || '.')
if (prefix.length <= PREFIX_MAX_LENGTH) {
return prefix
}
const shortPrefix = prefix.substr(-PREFIX_MAX_LENGTH + 3)
const separatorLocation = shortPrefix.indexOf('/')
if (separatorLocation <= 0) {
return `...${shortPrefix}`
}
return `...${shortPrefix.substr(separatorLocation)}`
}
function printPlusesAndMinuses (maxWidth: number, added: number, removed: number) {
if (maxWidth === 0) return ''
const changes = added + removed
let addedChars: number
let removedChars: number
if (changes > maxWidth) {
if (!added) {
addedChars = 0
removedChars = maxWidth
} else if (!removed) {
addedChars = maxWidth
removedChars = 0
} else {
const p = maxWidth / changes
addedChars = Math.min(Math.max(Math.floor(added * p), 1), maxWidth - 1)
removedChars = maxWidth - addedChars
}
} else {
addedChars = added
removedChars = removed
}
return `${R.repeat(ADDED_CHAR, addedChars).join('')}${R.repeat(REMOVED_CHAR, removedChars).join('')}`
}
function printDiffs (pkgsDiff: PackageDiff[]) {
// Sorts by alphabet then by removed/added
// + ava 0.10.0
// - chalk 1.0.0
// + chalk 2.0.0
pkgsDiff.sort((a, b) => (a.name.localeCompare(b.name) * 10 + (Number(!b.added) - Number(!a.added))))
const msg = pkgsDiff.map((pkg) => {
let result = pkg.added
? ADDED_CHAR
: pkg.linked
? LINKED_CHAR
: REMOVED_CHAR
if (!pkg.realName || pkg.name === pkg.realName) {
result += ` ${pkg.name}`
} else {
result += ` ${pkg.name} <- ${pkg.realName}`
}
if (pkg.version) {
result += ` ${chalk.grey(pkg.version)}`
if (pkg.latest && semver.lt(pkg.version, pkg.latest)) {
result += ` ${chalk.grey(`(${pkg.latest} is available)`)}`
}
}
if (pkg.deprecated) {
result += ` ${chalk.red('deprecated')}`
}
if (pkg.linked) {
result += ` ${chalk.magentaBright('linked from')} ${chalk.grey(pkg.from || '???')}`
}
return result
}).join(EOL)
return msg
}
function formatLifecycle (cwd: string, logObj: LifecycleLog) {
const prefix = `${
logObj.wd === logObj.depPath
? rightPad(formatPrefix(cwd, logObj.wd), PREFIX_MAX_LENGTH)
: rightPad(logObj.depPath, PREFIX_MAX_LENGTH)
} | ${hlValue(padStart(logObj.stage, 11))}`
if (logObj['script']) {
return `${prefix}$ ${logObj['script']}`
}
if (logObj['exitCode'] === 0) {
return `${prefix}: done`
}
const line = formatLine(logObj)
if (logObj.level === 'error') {
return `${prefix}: ${line}`
}
return `${prefix}: ${line}`
}
function formatLine (logObj: LifecycleLog) {
if (typeof logObj['exitCode'] === 'number') return chalk.red(`Exited with ${logObj['exitCode']}`)
// TODO: strip only the non-color/style ansi escape codes
if (logObj.level === 'error') {
return chalk.gray(stripAnsi(logObj['line']))
}
return stripAnsi(logObj['line'])
}
function formatInstallCheck (logObj: InstallCheckLog) {
switch (logObj.code) {
case 'EBADPLATFORM':
return formatWarn(`Unsupported system. Skipping dependency ${logObj.pkgId}`)
case 'ENOTSUP':
return logObj.toString()
default:
return
}
}
function formatWarn (message: string) {
// The \u2009 is the "thin space" unicode character
// It is used instead of ' ' because chalk (as of version 2.1.0)
// trims whitespace at the beginning
return `${chalk.bgYellow.black('\u2009WARN\u2009')} ${message}`
}

View File

@@ -0,0 +1,57 @@
import chalk from 'chalk'
import most = require('most')
import prettyBytes = require('pretty-bytes')
import R = require('ramda')
import semver = require('semver')
import {
DeprecationLog,
InstallCheckLog,
LifecycleLog,
Log,
ProgressLog,
RegistryLog,
} from 'supi'
import getPkgsDiff, {
PackageDiff,
propertyByDependencyType,
} from './pkgsDiff'
import reportError from './reportError'
export default function (
log$: most.Stream<Log>,
) {
log$.subscribe({
complete: () => undefined,
error: () => undefined,
next (log) {
if (log.name === 'pnpm:progress') {
switch (log.status) {
case 'fetched':
case 'fetching_started':
console.log(`${chalk.cyan(log.status)} ${log.pkgId}`)
}
return
}
switch (log.level) {
case 'warn':
console.log(formatWarn(log['message']))
return
case 'error':
console.log(reportError(log))
return
case 'debug':
return
default:
console.log(log['message'])
return
}
},
})
}
function formatWarn (message: string) {
// The \u2009 is the "thin space" unicode character
// It is used instead of ' ' because chalk (as of version 2.1.0)
// trims whitespace at the beginning
return `${chalk.bgYellow.black('\u2009WARN\u2009')} ${message}`
}

View File

@@ -0,0 +1,997 @@
import logger, {
createStreamParser,
} from '@pnpm/logger'
import delay = require('delay')
import test = require('tape')
import normalizeNewline = require('normalize-newline')
import {toOutput$} from 'pnpm-default-reporter'
import {stripIndents} from 'common-tags'
import chalk from 'chalk'
import most = require('most')
import StackTracey = require('stacktracey')
import R = require('ramda')
const WARN = chalk.bgYellow.black('\u2009WARN\u2009')
const ERROR = chalk.bgRed.black('\u2009ERROR\u2009')
const DEPRECATED = chalk.red('deprecated')
const versionColor = chalk.grey
const ADD = chalk.green('+')
const SUB = chalk.red('-')
const LINKED = chalk.magentaBright('#')
const h1 = chalk.blue
const hlValue = chalk.blue
const hlPkgId = chalk['whiteBright']
const POSTINSTALL = hlValue('postinstall')
const PREINSTALL = hlValue(' preinstall')
const INSTALL = hlValue(' install')
const progressLogger = logger<object>('progress')
const stageLogger = logger<string>('stage')
const rootLogger = logger<object>('root')
const deprecationLogger = logger<object>('deprecation')
const summaryLogger = logger<object>('summary')
const lifecycleLogger = logger<object>('lifecycle')
const packageJsonLogger = logger<object>('package-json')
const statsLogger = logger<object>('stats')
const hookLogger = logger<object>('hook')
const skippedOptionalDependencyLogger = logger<object>('skipped-optional-dependency')
const EOL = '\n'
test('prints progress beginning', t => {
const output$ = toOutput$(createStreamParser(), {cmd: 'install'})
const pkgId = 'registry.npmjs.org/foo/1.0.0'
progressLogger.debug({
status: 'resolving_content',
pkgId,
})
t.plan(1)
output$.take(1).subscribe({
next: output => {
t.equal(output, `Resolving: total ${hlValue('1')}, reused ${hlValue('0')}, downloaded ${hlValue('0')}`)
},
error: t.end,
complete: () => t.end(),
})
})
test('prints progress beginning when appendOnly is true', t => {
const output$ = toOutput$(createStreamParser(), {cmd: 'install', appendOnly: true})
const pkgId = 'registry.npmjs.org/foo/1.0.0'
progressLogger.debug({
status: 'resolving_content',
pkgId,
})
t.plan(1)
output$.take(1).subscribe({
next: output => {
t.equal(output, `Resolving: total ${hlValue('1')}, reused ${hlValue('0')}, downloaded ${hlValue('0')}`)
},
error: t.end,
complete: () => t.end(),
})
})
test('prints progress beginning during recursive install', t => {
const output$ = toOutput$(createStreamParser(), {cmd: 'recursive'})
const pkgId = 'registry.npmjs.org/foo/1.0.0'
progressLogger.debug({
status: 'resolving_content',
pkgId,
})
t.plan(1)
output$.take(1).subscribe({
next: output => {
t.equal(output, `Resolving: total ${hlValue('1')}, reused ${hlValue('0')}, downloaded ${hlValue('0')}`)
},
error: t.end,
complete: () => t.end(),
})
})
test('prints progress on first download', t => {
const output$ = toOutput$(createStreamParser(), {cmd: 'install', throttleProgress: 0})
const pkgId = 'registry.npmjs.org/foo/1.0.0'
progressLogger.debug({
status: 'resolving_content',
pkgId,
})
progressLogger.debug({
status: 'fetched',
pkgId,
})
t.plan(1)
output$.skip(1).take(1).subscribe({
next: output => {
t.equal(output, `Resolving: total ${hlValue('1')}, reused ${hlValue('0')}, downloaded ${hlValue('1')}`)
},
complete: () => t.end(),
error: t.end,
})
})
test('moves fixed line to the end', async t => {
const output$ = toOutput$(createStreamParser(), {cmd: 'install', throttleProgress: 0})
output$.skip(3).take(1).map(normalizeNewline).subscribe({
next: output => {
t.equal(output, stripIndents`
${WARN} foo
Resolving: total ${hlValue('1')}, reused ${hlValue('0')}, downloaded ${hlValue('1')}, done
`)
},
complete: v => t.end(),
error: t.end,
})
const pkgId = 'registry.npmjs.org/foo/1.0.0'
progressLogger.debug({
status: 'resolving_content',
pkgId,
})
progressLogger.debug({
status: 'fetched',
pkgId,
})
logger.warn('foo')
await delay(0) // w/o delay warning goes below for some reason. Started to happen after switch to most
stageLogger.debug('resolution_done')
stageLogger.debug('importing_done')
t.plan(1)
})
test('prints "Already up-to-date"', t => {
const output$ = toOutput$(createStreamParser(), {cmd: 'install'})
statsLogger.debug({ added: 0 })
statsLogger.debug({ removed: 0 })
t.plan(1)
output$.take(1).map(normalizeNewline).subscribe({
next: output => {
t.equal(output, stripIndents`
Already up-to-date
`)
},
complete: () => t.end(),
error: t.end,
})
})
test('prints summary', t => {
const output$ = toOutput$(createStreamParser(), {cmd: 'install'})
packageJsonLogger.debug({
initial: {
dependencies: {
'is-13': '^1.0.0',
},
devDependencies: {
'is-negative': '^1.0.0',
},
},
})
deprecationLogger.warn({
pkgName: 'bar',
pkgVersion: '2.0.0',
pkgId: 'registry.npmjs.org/bar/2.0.0',
deprecated: 'This package was deprecated because bla bla bla',
depth: 0,
})
rootLogger.info({
added: {
dependencyType: 'prod',
name: 'foo',
version: '1.0.0',
latest: '2.0.0',
id: 'registry.npmjs.org/foo/1.0.0',
},
})
rootLogger.info({
added: {
dependencyType: 'prod',
name: 'bar',
version: '2.0.0',
latest: '1.0.0', // this won't be printed in summary because latest is less than current version
id: 'registry.npmjs.org/bar/2.0.0',
},
})
rootLogger.info({
removed: {
dependencyType: 'prod',
name: 'foo',
version: '0.1.0',
},
})
rootLogger.info({
added: {
dependencyType: 'dev',
name: 'qar',
version: '2.0.0',
id: 'registry.npmjs.org/qar/2.0.0',
},
})
rootLogger.info({
added: {
dependencyType: 'optional',
name: 'lala',
version: '1.1.0',
id: 'registry.npmjs.org/lala/1.1.0',
},
})
rootLogger.info({
removed: {
dependencyType: 'optional',
name: 'is-positive',
},
})
rootLogger.debug({
linked: {
dependencyType: 'optional',
from: '/src/is-linked',
name: 'is-linked',
to: '/src/project/node_modules'
},
})
rootLogger.info({
added: {
dependencyType: 'prod',
name: 'winston',
realName: 'winst0n',
version: '1.0.0',
latest: '1.0.0',
id: 'registry.npmjs.org/winst0n/2.0.0',
},
})
packageJsonLogger.debug({
updated: {
dependencies: {
'is-negative': '^1.0.0',
},
devDependencies: {
'is-13': '^1.0.0',
},
}
})
summaryLogger.info()
t.plan(1)
output$.skip(1).take(1).map(normalizeNewline).subscribe({
next: output => {
t.equal(output, stripIndents`
${WARN} ${DEPRECATED} bar@2.0.0: This package was deprecated because bla bla bla
${h1('dependencies:')}
${ADD} bar ${versionColor('2.0.0')} ${DEPRECATED}
${SUB} foo ${versionColor('0.1.0')}
${ADD} foo ${versionColor('1.0.0')} ${versionColor('(2.0.0 is available)')}
${SUB} is-13 ${versionColor('^1.0.0')}
${ADD} is-negative ${versionColor('^1.0.0')}
${ADD} winston <- winst0n ${versionColor('1.0.0')}
${h1('optionalDependencies:')}
${LINKED} is-linked ${chalk.magentaBright('linked from')} ${chalk.grey('/src/is-linked')}
${SUB} is-positive
${ADD} lala ${versionColor('1.1.0')}
${h1('devDependencies:')}
${ADD} is-13 ${versionColor('^1.0.0')}
${SUB} is-negative ${versionColor('^1.0.0')}
${ADD} qar ${versionColor('2.0.0')}
` + '\n')
},
complete: () => t.end(),
error: t.end,
})
})
test('groups lifecycle output', t => {
const output$ = toOutput$(createStreamParser(), {cmd: 'install'})
const pkgId = 'registry.npmjs.org/foo/1.0.0'
lifecycleLogger.debug({
depPath: 'registry.npmjs.org/foo/1.0.0',
script: 'node foo',
stage: 'preinstall',
})
lifecycleLogger.debug({
depPath: 'registry.npmjs.org/foo/1.0.0',
line: 'foo',
stage: 'preinstall',
})
lifecycleLogger.debug({
depPath: 'registry.npmjs.org/foo/1.0.0',
script: 'node foo',
stage: 'postinstall',
})
lifecycleLogger.debug({
depPath: 'registry.npmjs.org/foo/1.0.0',
line: 'foo I',
stage: 'postinstall',
})
lifecycleLogger.debug({
depPath: 'registry.npmjs.org/bar/1.0.0',
script: 'node bar',
stage: 'postinstall',
})
lifecycleLogger.debug({
depPath: 'registry.npmjs.org/bar/1.0.0',
line: 'bar I',
stage: 'postinstall',
})
lifecycleLogger.debug({
depPath: 'registry.npmjs.org/foo/1.0.0',
line: 'foo II',
stage: 'postinstall',
})
lifecycleLogger.debug({
depPath: 'registry.npmjs.org/foo/1.0.0',
line: 'foo III',
stage: 'postinstall',
})
lifecycleLogger.debug({
depPath: 'registry.npmjs.org/qar/1.0.0',
script: 'node qar',
stage: 'install',
})
lifecycleLogger.debug({
depPath: 'registry.npmjs.org/qar/1.0.0',
exitCode: 0,
stage: 'install',
})
lifecycleLogger.debug({
depPath: 'registry.npmjs.org/foo/1.0.0',
exitCode: 0,
stage: 'postinstall',
})
t.plan(1)
output$.skip(9).take(1).map(normalizeNewline).subscribe({
next: output => {
t.equal(output, EOL + stripIndents`
registry.npmjs.org/foo/1.0.0 | ${PREINSTALL}$ node foo
registry.npmjs.org/foo/1.0.0 | ${PREINSTALL}: foo
registry.npmjs.org/foo/1.0.0 | ${POSTINSTALL}$ node foo
registry.npmjs.org/foo/1.0.0 | ${POSTINSTALL}: foo I
registry.npmjs.org/foo/1.0.0 | ${POSTINSTALL}: foo II
registry.npmjs.org/foo/1.0.0 | ${POSTINSTALL}: foo III
registry.npmjs.org/bar/1.0.0 | ${POSTINSTALL}$ node bar
registry.npmjs.org/bar/1.0.0 | ${POSTINSTALL}: bar I
registry.npmjs.org/qar/1.0.0 | ${INSTALL}$ node qar
registry.npmjs.org/qar/1.0.0 | ${INSTALL}: done
`)
},
complete: () => t.end(),
error: t.end,
})
})
// Many libs use stderr for logging, so showing all stderr adds not much value
test['skip']('prints lifecycle progress', t => {
const output$ = toOutput$(createStreamParser(), {cmd: 'install'})
const pkgId = 'registry.npmjs.org/foo/1.0.0'
lifecycleLogger.debug({
pkgId: 'registry.npmjs.org/foo/1.0.0',
line: 'foo I',
script: 'postinstall',
})
lifecycleLogger.debug({
pkgId: 'registry.npmjs.org/bar/1.0.0',
line: 'bar I',
script: 'postinstall',
})
lifecycleLogger.error({
pkgId: 'registry.npmjs.org/foo/1.0.0',
line: 'foo II',
script: 'postinstall',
})
lifecycleLogger.debug({
pkgId: 'registry.npmjs.org/foo/1.0.0',
line: 'foo III',
script: 'postinstall',
})
t.plan(1)
const childOutputColor = chalk.grey
const childOutputError = chalk.red
output$.skip(3).take(1).map(normalizeNewline).subscribe({
next: output => {
t.equal(output, stripIndents`
Running ${POSTINSTALL} for ${hlPkgId('registry.npmjs.org/foo/1.0.0')}: ${childOutputColor('foo I')}
Running ${POSTINSTALL} for ${hlPkgId('registry.npmjs.org/foo/1.0.0')}! ${childOutputError('foo II')}
Running ${POSTINSTALL} for ${hlPkgId('registry.npmjs.org/foo/1.0.0')}: ${childOutputColor('foo III')}
Running ${POSTINSTALL} for ${hlPkgId('registry.npmjs.org/bar/1.0.0')}: ${childOutputColor('bar I')}
`)
},
complete: () => t.end(),
error: t.end,
})
})
test('prints generic error', t => {
const output$ = toOutput$(createStreamParser(), {cmd: 'install'})
const err = new Error('some error')
logger.error(err)
t.plan(1)
output$.take(1).map(normalizeNewline).subscribe({
next: output => {
t.equal(output, stripIndents`
${ERROR} ${chalk.red('some error')}
${new StackTracey(err.stack).pretty}
`)
},
complete: () => t.end(),
error: t.end,
})
})
test('prints generic error when recursive install fails', t => {
const output$ = toOutput$(createStreamParser(), {cmd: 'recursive'})
const err = new Error('some error')
err['prefix'] = '/home/src/'
logger.error(err, err)
t.plan(1)
output$.take(1).map(normalizeNewline).subscribe({
next: output => {
t.equal(output, stripIndents`
/home/src/:
${ERROR} ${chalk.red('some error')}
${new StackTracey(err.stack).pretty}
`)
},
complete: () => t.end(),
error: t.end,
})
})
test('prints info', t => {
const output$ = toOutput$(createStreamParser(), {cmd: 'install'})
logger.info('info message')
t.plan(1)
output$.take(1).map(normalizeNewline).subscribe({
next: output => {
t.equal(output, 'info message')
},
complete: () => t.end(),
error: t.end,
})
})
test('prints progress of big files download', async t => {
t.plan(6)
let output$ = toOutput$(createStreamParser(), {cmd: 'install', throttleProgress: 0})
.map(normalizeNewline) as most.Stream<string>
const stream$: most.Stream<string>[] = []
const pkgId1 = 'registry.npmjs.org/foo/1.0.0'
const pkgId2 = 'registry.npmjs.org/bar/2.0.0'
const pkgId3 = 'registry.npmjs.org/qar/3.0.0'
stream$.push(
output$.take(1)
.tap(output => t.equal(output, `Resolving: total ${hlValue('1')}, reused ${hlValue('0')}, downloaded ${hlValue('0')}`))
)
output$ = output$.skip(1)
stream$.push(
output$.take(1)
.tap(output => t.equal(output, stripIndents`
Resolving: total ${hlValue('1')}, reused ${hlValue('0')}, downloaded ${hlValue('0')}
Downloading ${hlPkgId(pkgId1)}: ${hlValue('0 B')}/${hlValue('10.5 MB')}
`))
)
output$ = output$.skip(1)
stream$.push(
output$.take(1)
.tap(output => t.equal(output, stripIndents`
Resolving: total ${hlValue('1')}, reused ${hlValue('0')}, downloaded ${hlValue('0')}
Downloading ${hlPkgId(pkgId1)}: ${hlValue('5.77 MB')}/${hlValue('10.5 MB')}
`))
)
output$ = output$.skip(2)
stream$.push(
output$.take(1)
.tap(output => t.equal(output, stripIndents`
Resolving: total ${hlValue('2')}, reused ${hlValue('0')}, downloaded ${hlValue('0')}
Downloading ${hlPkgId(pkgId1)}: ${hlValue('7.34 MB')}/${hlValue('10.5 MB')}
`, 'downloading of small package not reported'))
)
output$ = output$.skip(3)
stream$.push(
output$.take(1)
.tap(output => t.equal(output, stripIndents`
Resolving: total ${hlValue('3')}, reused ${hlValue('0')}, downloaded ${hlValue('0')}
Downloading ${hlPkgId(pkgId1)}: ${hlValue('7.34 MB')}/${hlValue('10.5 MB')}
Downloading ${hlPkgId(pkgId3)}: ${hlValue('19.9 MB')}/${hlValue('21 MB')}
`))
)
output$ = output$.skip(1)
stream$.push(
output$.take(1)
.tap(output => t.equal(output, stripIndents`
Downloading ${hlPkgId(pkgId1)}: ${hlValue('10.5 MB')}/${hlValue('10.5 MB')}, done
Resolving: total ${hlValue('3')}, reused ${hlValue('0')}, downloaded ${hlValue('0')}
Downloading ${hlPkgId(pkgId3)}: ${hlValue('19.9 MB')}/${hlValue('21 MB')}
`))
)
most.mergeArray(stream$)
.subscribe({
next: () => undefined,
complete: () => t.end(),
error: t.end,
})
progressLogger.debug({
status: 'resolving_content',
pkgId: pkgId1,
})
progressLogger.debug({
status: 'fetching_started',
pkgId: pkgId1,
size: 1024 * 1024 * 10, // 10 MB
attempt: 1,
})
await delay(0)
progressLogger.debug({
status: 'fetching_progress',
pkgId: pkgId1,
downloaded: 1024 * 1024 * 5.5, // 5.5 MB
})
progressLogger.debug({
status: 'resolving_content',
pkgId: pkgId2,
})
progressLogger.debug({
status: 'fetching_started',
pkgId: pkgId1,
size: 10, // 10 B
attempt: 1,
})
progressLogger.debug({
status: 'fetching_progress',
pkgId: pkgId1,
downloaded: 1024 * 1024 * 7,
})
progressLogger.debug({
status: 'resolving_content',
pkgId: pkgId3,
})
progressLogger.debug({
status: 'fetching_started',
pkgId: pkgId3,
size: 1024 * 1024 * 20, // 20 MB
attempt: 1,
})
await delay(0)
progressLogger.debug({
status: 'fetching_progress',
pkgId: pkgId3,
downloaded: 1024 * 1024 * 19, // 19 MB
})
progressLogger.debug({
status: 'fetching_progress',
pkgId: pkgId1,
downloaded: 1024 * 1024 * 10, // 10 MB
})
})
test('prints added/removed stats during installation', t => {
const output$ = toOutput$(createStreamParser(), {cmd: 'install'})
statsLogger.debug({ added: 5 })
statsLogger.debug({ removed: 1 })
t.plan(1)
output$.take(1).map(normalizeNewline).subscribe({
next: output => {
t.equal(output, stripIndents`
Packages: ${chalk.green('+5')} ${chalk.red('-1')}
${ADD + ADD + ADD + ADD + ADD + SUB}`
)
},
complete: () => t.end(),
error: t.end,
})
})
test('prints added/removed stats during installation when 0 removed', t => {
const output$ = toOutput$(createStreamParser(), {cmd: 'install'})
statsLogger.debug({ added: 2 })
statsLogger.debug({ removed: 0 })
t.plan(1)
output$.take(1).map(normalizeNewline).subscribe({
next: output => {
t.equal(output, stripIndents`
Packages: ${chalk.green('+2')}
${ADD + ADD}`
)
},
complete: () => t.end(),
error: t.end,
})
})
test('prints only the added stats if nothing was removed', t => {
const output$ = toOutput$(createStreamParser(), {cmd: 'install'})
statsLogger.debug({ removed: 0 })
statsLogger.debug({ added: 1 })
t.plan(1)
output$.take(1).map(normalizeNewline).subscribe({
next: output => {
t.equal(output, stripIndents`
Packages: ${chalk.green('+1')}
${ADD}`
)
},
complete: () => t.end(),
error: t.end,
})
})
test('prints only the removed stats if nothing was added', t => {
const output$ = toOutput$(createStreamParser(), {cmd: 'install'})
statsLogger.debug({ removed: 1 })
statsLogger.debug({ added: 0 })
t.plan(1)
output$.take(1).map(normalizeNewline).subscribe({
next: output => {
t.equal(output, stripIndents`
Packages: ${chalk.red('-1')}
${SUB}`
)
},
complete: () => t.end(),
error: t.end,
})
})
test('prints only the added stats if nothing was removed and a lot added', t => {
const output$ = toOutput$(createStreamParser(), {cmd: 'install', width: 20})
statsLogger.debug({ removed: 0 })
statsLogger.debug({ added: 100 })
t.plan(1)
output$.take(1).map(normalizeNewline).subscribe({
next: output => {
t.equal(output, stripIndents`
Packages: ${chalk.green('+100')}
${R.repeat(ADD, 20).join('')}`
)
},
complete: () => t.end(),
error: t.end,
})
})
test('prints only the removed stats if nothing was added and a lot removed', t => {
const output$ = toOutput$(createStreamParser(), {cmd: 'install', width: 20})
statsLogger.debug({ removed: 100 })
statsLogger.debug({ added: 0 })
t.plan(1)
output$.take(1).map(normalizeNewline).subscribe({
next: output => {
t.equal(output, stripIndents`
Packages: ${chalk.red('-100')}
${R.repeat(SUB, 20).join('')}`
)
},
complete: () => t.end(),
error: t.end,
})
})
test('prints at least one remove sign when removed !== 0', t => {
const output$ = toOutput$(createStreamParser(), {cmd: 'install', width: 20})
statsLogger.debug({ removed: 1 })
statsLogger.debug({ added: 100 })
t.plan(1)
output$.take(1).map(normalizeNewline).subscribe({
next: output => {
t.equal(output, stripIndents`
Packages: ${chalk.green('+100')} ${chalk.red('-1')}
${R.repeat(ADD, 19).join('') + SUB}`
)
},
complete: () => t.end(),
error: t.end,
})
})
test('prints at least one add sign when added !== 0', t => {
const output$ = toOutput$(createStreamParser(), {cmd: 'install', width: 20})
statsLogger.debug({ removed: 100 })
statsLogger.debug({ added: 1 })
t.plan(1)
output$.take(1).map(normalizeNewline).subscribe({
next: output => {
t.equal(output, stripIndents`
Packages: ${chalk.green('+1')} ${chalk.red('-100')}
${ADD + R.repeat(SUB, 19).join('')}`
)
},
complete: () => t.end(),
error: t.end,
})
})
test('prints just removed during uninstallation', t => {
const output$ = toOutput$(createStreamParser(), {cmd: 'uninstall'})
statsLogger.debug({ removed: 4 })
t.plan(1)
output$.take(1).map(normalizeNewline).subscribe({
next: output => {
t.equal(output, stripIndents`
Packages: ${chalk.red('-4')}
${SUB + SUB + SUB + SUB}`
)
},
complete: () => t.end(),
error: t.end,
})
})
test('prints added/removed stats during recursive installation', t => {
const output$ = toOutput$(createStreamParser(), {cmd: 'recursive', cwd: '/home/jane/repo'})
statsLogger.debug({ removed: 1, prefix: '/home/jane/repo' })
statsLogger.debug({ added: 0, prefix: '/home/jane/repo' })
statsLogger.debug({ removed: 0, prefix: '/home/jane/repo/pkg-5' })
statsLogger.debug({ added: 0, prefix: '/home/jane/repo/pkg-5' })
statsLogger.debug({ added: 2, prefix: '/home/jane/repo/dir/pkg-2' })
statsLogger.debug({ added: 5, prefix: '/home/jane/repo/pkg-1' })
statsLogger.debug({ removed: 1, prefix: '/home/jane/repo/pkg-1' })
statsLogger.debug({ removed: 0, prefix: '/home/jane/repo/dir/pkg-2' })
statsLogger.debug({ removed: 0, prefix: '/home/jane/repo/loooooooooooooooooooooooooooooooooong/pkg-3' })
statsLogger.debug({ added: 1, prefix: '/home/jane/repo/loooooooooooooooooooooooooooooooooong/pkg-3' })
statsLogger.debug({ removed: 1, prefix: '/home/jane/repo/loooooooooooooooooooooooooooooooooong-pkg-4' })
statsLogger.debug({ added: 0, prefix: '/home/jane/repo/loooooooooooooooooooooooooooooooooong-pkg-4' })
t.plan(1)
output$.skip(4).take(1).map(normalizeNewline).subscribe({
next: output => {
t.equal(output, stripIndents`
. | ${chalk.red('-1')} ${SUB}
pkg-1 | ${chalk.green('+5')} ${chalk.red('-1')} ${ADD + SUB}
dir/pkg-2 | ${chalk.green('+2')} ${ADD}
.../pkg-3 | ${chalk.green('+1')} ${ADD}
...ooooooooooooooooooooooooooooong-pkg-4 | ${chalk.red('-1')} ${SUB}`
)
},
complete: () => t.end(),
error: t.end,
})
})
test('recursive installation: prints only the added stats if nothing was removed and a lot added', t => {
const output$ = toOutput$(createStreamParser(), {cmd: 'recursive', width: 60, cwd: '/home/jane/repo'})
statsLogger.debug({ removed: 0, prefix: '/home/jane/repo/pkg-1' })
statsLogger.debug({ added: 190, prefix: '/home/jane/repo/pkg-1' })
t.plan(1)
output$.take(1).map(normalizeNewline).subscribe({
next: output => {
t.equal(output, stripIndents`
pkg-1 | ${chalk.green('+190')} ${R.repeat(ADD, 12).join('')}`
)
},
complete: () => t.end(),
error: t.end,
})
})
test('recursive installation: prints only the removed stats if nothing was added and a lot removed', t => {
const output$ = toOutput$(createStreamParser(), {cmd: 'recursive', width: 60, cwd: '/home/jane/repo'})
statsLogger.debug({ removed: 190, prefix: '/home/jane/repo/pkg-1' })
statsLogger.debug({ added: 0, prefix: '/home/jane/repo/pkg-1' })
t.plan(1)
output$.take(1).map(normalizeNewline).subscribe({
next: output => {
t.equal(output, stripIndents`
pkg-1 | ${chalk.red('-190')} ${R.repeat(SUB, 12).join('')}`
)
},
complete: () => t.end(),
error: t.end,
})
})
test('recursive installation: prints at least one remove sign when removed !== 0', t => {
const output$ = toOutput$(createStreamParser(), {cmd: 'recursive', width: 62, cwd: '/home/jane/repo'})
statsLogger.debug({ removed: 1, prefix: '/home/jane/repo/pkg-1' })
statsLogger.debug({ added: 100, prefix: '/home/jane/repo/pkg-1' })
t.plan(1)
output$.take(1).map(normalizeNewline).subscribe({
next: output => {
t.equal(output, stripIndents`
pkg-1 | ${chalk.green('+100')} ${chalk.red('-1')} ${R.repeat(ADD, 8).join('') + SUB}`
)
},
complete: () => t.end(),
error: t.end,
})
})
test('recursive installation: prints at least one add sign when added !== 0', t => {
const output$ = toOutput$(createStreamParser(), {cmd: 'recursive', width: 62, cwd: '/home/jane/repo'})
statsLogger.debug({ removed: 100, prefix: '/home/jane/repo/pkg-1' })
statsLogger.debug({ added: 1, prefix: '/home/jane/repo/pkg-1' })
t.plan(1)
output$.take(1).map(normalizeNewline).subscribe({
next: output => {
t.equal(output, stripIndents`
pkg-1 | ${chalk.green('+1')} ${chalk.red('-100')} ${ADD + R.repeat(SUB, 8).join('')}`
)
},
complete: () => t.end(),
error: t.end,
})
})
test('install: print hook message', t => {
const output$ = toOutput$(createStreamParser(), {cmd: 'install', cwd: '/home/jane/repo'})
hookLogger.debug({
from: '/home/jane/repo/pnpmfile.js',
prefix: '/home/jane/repo',
hook: 'readPackage',
message: 'foo',
})
t.plan(1)
output$.take(1).map(normalizeNewline).subscribe({
next: output => {
t.equal(output, stripIndents`
${chalk.magentaBright('readPackage')}: foo`
)
},
complete: () => t.end(),
error: t.end,
})
})
test('recursive: print hook message', t => {
const output$ = toOutput$(createStreamParser(), {cmd: 'recursive', cwd: '/home/jane/repo'})
hookLogger.debug({
from: '/home/jane/repo/pnpmfile.js',
prefix: '/home/jane/repo/pkg-1',
hook: 'readPackage',
message: 'foo',
})
t.plan(1)
output$.take(1).map(normalizeNewline).subscribe({
next: output => {
t.equal(output, stripIndents`
pkg-1 | ${chalk.magentaBright('readPackage')}: foo`
)
},
complete: () => t.end(),
error: t.end,
})
})
test('prints skipped optional dependency info message', t => {
const output$ = toOutput$(createStreamParser(), {cmd: 'install'})
const pkgId = 'registry.npmjs.org/foo/1.0.0'
skippedOptionalDependencyLogger.debug({
package: {
id: pkgId,
name: 'foo',
version: '1.0.0',
},
parents: [],
reason: 'unsupported_platform',
})
t.plan(1)
output$.take(1).subscribe({
next: output => {
t.equal(output, `info: ${pkgId} is an optional dependency and failed compatibility check. Excluding it from installation.`)
},
error: t.end,
complete: () => t.end(),
})
})

View File

@@ -0,0 +1,24 @@
{
"compilerOptions": {
"removeComments": false,
"preserveConstEnums": true,
"sourceMap": true,
"declaration": true,
"noImplicitAny": true,
"noImplicitReturns": true,
"suppressImplicitAnyIndexErrors": true,
"allowSyntheticDefaultImports": true,
"strictNullChecks": true,
"target": "es6",
"outDir": "lib",
"module": "commonjs",
"moduleResolution": "node"
},
"include": [
"src/**/*.ts",
"typings/**/*.d.ts"
],
"atom": {
"rewriteTsconfig": true
}
}

View File

@@ -0,0 +1,45 @@
{
"extends": "tslint:recommended",
"rules": {
"curly": false,
"eofline": false,
"align": [true, "parameters"],
"class-name": true,
"indent": [true, "spaces"],
"max-line-length": false,
"no-any": true,
"no-consecutive-blank-lines": true,
"no-trailing-whitespace": true,
"no-duplicate-variable": true,
"no-var-keyword": true,
"no-unused-expression": true,
"no-use-before-declare": true,
"no-var-requires": true,
"no-require-imports": false,
"no-string-literal": false,
"space-before-function-paren": [true, "always"],
"interface-name": [true, "never-prefix"],
"no-console": false,
"one-line": [true,
"check-else",
"check-whitespace",
"check-open-brace"],
"quotemark": [true,
"single",
"avoid-escape"],
"semicolon": false,
"typedef-whitespace": [true, {
"call-signature": "nospace",
"index-signature": "nospace",
"parameter": "nospace",
"property-declaration": "nospace",
"variable-declaration": "nospace"
}],
"whitespace": [true,
"check-branch",
"check-decl",
"check-operator",
"check-separator",
"check-type"]
}
}

View File

@@ -0,0 +1 @@
/// <reference path="local.d.ts" />

View File

@@ -0,0 +1,54 @@
declare module 'ansi-diff' {
const anything: any;
export = anything;
}
declare module 'cli-cursor' {
const anything: any;
export = anything;
}
declare module 'ndjson' {
const anything: any;
export = anything;
}
declare module 'normalize-newline' {
const anything: any;
export = anything;
}
declare module 'pretty-bytes' {
const anything: any;
export = anything;
}
declare module 'stacktracey' {
const anything: any;
export = anything;
}
declare module 'zen-push' {
const anything: any;
export = anything;
}
declare module 'right-pad' {
const anything: any;
export = anything;
}
declare module 'string-length' {
function stringLength (s: string): number;
export = stringLength;
}
declare module 'normalize-path' {
function normalize (path: string): string;
export = normalize;
}
declare module 'string.prototype.padstart' {
function padStart (s: string, targetLength: number, padString?: string): string;
export = padStart;
}