feat: link-workspace-packages

close #1259
This commit is contained in:
Zoltan Kochan
2018-08-20 01:07:28 +03:00
committed by Zoltan Kochan
parent 363d45ecf3
commit b09382376e
9 changed files with 182 additions and 16 deletions

View File

@@ -68,4 +68,5 @@ export interface PnpmConfigs {
workspaceConcurrency: number,
workspacePrefix?: string,
reporter?: string,
linkWorkspacePackages: boolean,
}

View File

@@ -30,6 +30,7 @@ export const types = Object.assign({
'ignore-stop-requests': Boolean,
'ignore-upload-requests': Boolean,
'independent-leaves': Boolean,
'link-workspace-packages': Boolean,
'lock': Boolean,
'lock-stale-duration': Number,
'network-concurrency': Number,
@@ -93,6 +94,7 @@ export default async (
'fetch-retry-maxtimeout': 60000,
'fetch-retry-mintimeout': 10000,
'globalconfig': npmDefaults.globalconfig,
'link-workspace-packages': false,
'lock': true,
'package-lock': npmDefaults['package-lock'],
'prefix': npmDefaults.prefix,

View File

@@ -252,7 +252,8 @@ function getHelpText (command: string) {
install
update
uninstall [<@scope>/]<pkg>... uninstall a dependency from each package
link runs installation in each package. If a package is available locally, the local version is linked.
link Deprecated. Use the install command with the link-workspace-packages config.
runs installation in each package. If a package is available locally, the local version is linked.
unlink removes links to local packages and reinstalls them from the registry.
list [[<@scope>/]<pkg>...] list dependencies in each package.
outdated [[<@scope>/]<pkg>...] check for outdated dependencies in every package.
@@ -271,11 +272,13 @@ function getHelpText (command: string) {
Options:
--filter <name> restricts the scope to package names matching the given glob.
--filter <name>... includes all direct and indirect dependencies of the matched packages.
--filter ...<name> includes all direct and indirect dependents of the matched packages.
--no-bail continues executing other tasks even if a task threw an error.
--workspace-concurrency set the maximum number of concurrency. Default is 4. For unlimited concurrency use Infinity.
--filter <name> restricts the scope to package names matching the given glob.
--filter <name>... includes all direct and indirect dependencies of the matched packages.
--filter ...<name> includes all direct and indirect dependents of the matched packages.
--no-bail continues executing other tasks even if a task threw an error.
--workspace-concurrency set the maximum number of concurrency. Default is 4. For unlimited concurrency use Infinity.
--link-workspace-packages locally available packages are linked to node_modules instead of being downloaded from the registry.
Convenient to use in a multi-package repository.
`
default:

View File

@@ -1,10 +1,13 @@
import {
install,
installPkgs,
rebuild,
} from 'supi'
import createStoreController from '../createStoreController'
import findWorkspacePackages, {arrayOfLocalPackagesToMap} from '../findWorkspacePackages'
import requireHooks from '../requireHooks'
import {PnpmOptions} from '../types'
import {recursive} from './recursive'
/**
* Perform installation.
@@ -19,18 +22,43 @@ export default async function installCmd (
input = input.filter(Boolean)
const prefix = opts.prefix || process.cwd()
const localPackages = opts.linkWorkspacePackages && opts.workspacePrefix
? arrayOfLocalPackagesToMap(await findWorkspacePackages(opts.workspacePrefix))
: undefined
if (!opts.ignorePnpmfile) {
opts.hooks = requireHooks(prefix, opts)
}
const store = await createStoreController(opts)
const installOpts = Object.assign(opts, {
const installOpts = {
...opts,
// In case installation is done in a multi-package repository
// The dependencies should be built first,
// so ignoring scripts for now
ignoreScripts: !!localPackages || opts.ignoreScripts,
localPackages,
store: store.path,
storeController: store.ctrl,
})
if (!input || !input.length) {
return install(installOpts)
}
return installPkgs(input, installOpts)
if (!input || !input.length) {
await install(installOpts)
} else {
await installPkgs(input, installOpts)
}
if (opts.linkWorkspacePackages && opts.workspacePrefix) {
// TODO: reuse somehow the previous read of packages
// this is not optimal
const allWorkspacePkgs = await findWorkspacePackages(opts.workspacePrefix)
await recursive(allWorkspacePkgs, [], {
...opts,
filterByEntryDirectory: prefix,
inputForEntryDirectory: input,
}, 'install', 'install')
if (opts.ignoreScripts) return
await rebuild({...opts, pending: true} as any) // tslint:disable-line:no-any
}
}

View File

@@ -10,6 +10,19 @@ interface Graph {
[nodeId: string]: string[],
}
export function filterGraphByEntryDirectory (
pkgGraph: PackageGraph,
entryDirectory: string,
): PackageGraph {
if (!pkgGraph[entryDirectory]) return {}
const walkedDependencies = new Set<string>()
const graph = pkgGraphToGraph(pkgGraph)
pickSubgraph(graph, [entryDirectory], walkedDependencies)
return R.pick(Array.from(walkedDependencies), pkgGraph)
}
export function filterGraph (
pkgGraph: PackageGraph,
filters: string[],

View File

@@ -26,6 +26,7 @@ import help from '../help'
import exec from './exec'
import {
filterGraph,
filterGraphByEntryDirectory,
filterGraphByScope,
} from './filter'
import list from './list'
@@ -78,7 +79,10 @@ export default async (
export async function recursive (
allPkgs: Array<{path: string, manifest: PackageJson}>,
input: string[],
opts: PnpmOptions,
opts: PnpmOptions & {
filterByEntryDirectory?: string,
inputForEntryDirectory?: string[],
},
cmdFullName: string,
cmd: string,
) {
@@ -90,6 +94,9 @@ export async function recursive (
} else if (opts.filter) {
pkgGraphResult.graph = filterGraph(pkgGraphResult.graph, opts.filter)
pkgs = allPkgs.filter((pkg: {path: string}) => pkgGraphResult.graph[pkg.path])
} else if (opts.filterByEntryDirectory) {
pkgGraphResult.graph = filterGraphByEntryDirectory(pkgGraphResult.graph, opts.filterByEntryDirectory)
pkgs = allPkgs.filter((pkg: {path: string}) => pkgGraphResult.graph[pkg.path])
} else {
pkgs = allPkgs
}
@@ -138,7 +145,12 @@ export async function recursive (
})
const chunks = graphSequencerResult.chunks
const localPackages = cmdFullName === 'link'
if (cmdFullName === 'link' && opts.linkWorkspacePackages) {
const err = new Error('"pnpm recursive link" is deprecated with link-workspace-packages = true. Please use "pnpm recursive install" instead')
err['code'] = 'ERR_PNPM_RECURSIVE_LINK_DEPRECATED' // tslint:disable-line:no-string-literal
throw err
}
const localPackages = cmdFullName === 'link' || opts.linkWorkspacePackages
? arrayOfLocalPackagesToMap(allPkgs)
: {}
const installOpts = Object.assign(opts, {
@@ -176,6 +188,9 @@ export async function recursive (
const hooks = opts.ignorePnpmfile ? {} : requireHooks(prefix, opts)
try {
const localConfigs = await readLocalConfigs(prefix)
if (opts.filterByEntryDirectory === prefix) {
return
}
await action({
...installOpts,
...localConfigs,

View File

@@ -75,6 +75,7 @@ export interface PnpmOptions {
useStoreServer?: boolean,
workspaceConcurrency: number,
workspacePrefix?: string,
linkWorkspacePackages: boolean,
// cannot be specified via configs
update?: boolean,

View File

@@ -1,3 +1,4 @@
import fs = require('mz/fs')
import tape = require('tape')
import promisifyTape from 'tape-promise'
import path = require('path')
@@ -29,7 +30,7 @@ test('linking a package inside a monorepo', async (t: tape.Test) => {
},
])
await writeYamlFile('pnpm-workspace.yaml', {packages: ['**']})
await writeYamlFile('pnpm-workspace.yaml', {packages: ['**', '!store/**']})
process.chdir('project-1')
@@ -49,3 +50,104 @@ test('linking a package inside a monorepo', async (t: tape.Test) => {
await projects['project-1'].has('project-3')
await projects['project-1'].has('project-4')
})
test('linking a package inside a monorepo with --link-workspace-packages when installing new dependencies', async (t: tape.Test) => {
const projects = preparePackages(t, [
{
name: 'project-1',
version: '1.0.0',
},
{
name: 'project-2',
version: '2.0.0',
},
{
name: 'project-3',
version: '3.0.0',
},
{
name: 'project-4',
version: '4.0.0',
},
])
await fs.writeFile('.npmrc', 'link-workspace-packages = true', 'utf8')
await writeYamlFile('pnpm-workspace.yaml', {packages: ['**', '!store/**']})
process.chdir('project-1')
await execPnpm('install', 'project-2')
await execPnpm('install', 'project-3', '--save-dev')
await execPnpm('install', 'project-4', '--save-optional')
const pkg = await import(path.resolve('package.json'))
t.deepEqual(pkg && pkg.dependencies, {'project-2': '^2.0.0'}, 'spec of linked package added to dependencies')
t.deepEqual(pkg && pkg.devDependencies, {'project-3': '^3.0.0'}, 'spec of linked package added to devDependencies')
t.deepEqual(pkg && pkg.optionalDependencies, {'project-4': '^4.0.0'}, 'spec of linked package added to optionalDependencies')
await projects['project-1'].has('project-2')
await projects['project-1'].has('project-3')
await projects['project-1'].has('project-4')
})
test('linking a package inside a monorepo with --link-workspace-packages', async (t: tape.Test) => {
const projects = preparePackages(t, [
{
name: 'project-1',
version: '1.0.0',
dependencies: {
'json-append': '1',
'project-2': '2.0.0',
},
devDependencies: {
'project-3': '3.0.0',
},
optionalDependencies: {
'project-4': '4.0.0',
},
scripts: {
install: `node -e "process.stdout.write('project-1')" | json-append ../output.json`,
},
},
{
name: 'project-2',
version: '2.0.0',
dependencies: {
'json-append': '1',
},
scripts: {
install: `node -e "process.stdout.write('project-2')" | json-append ../output.json`,
},
},
{
name: 'project-3',
version: '3.0.0',
},
{
name: 'project-4',
version: '4.0.0',
},
])
await fs.writeFile('.npmrc', 'link-workspace-packages = true', 'utf8')
await writeYamlFile('pnpm-workspace.yaml', {packages: ['**', '!store/**']})
process.chdir('project-1')
await execPnpm('install')
const outputs = await import(path.resolve('..', 'output.json')) as string[]
t.deepEqual(outputs, ['project-2', 'project-1'])
await projects['project-1'].has('project-2')
await projects['project-1'].has('project-3')
await projects['project-1'].has('project-4')
const shr = await projects['project-1'].loadShrinkwrap()
t.equal(shr.dependencies['project-2'], 'link:../project-2')
t.equal(shr.devDependencies['project-3'], 'link:../project-3')
t.equal(shr.optionalDependencies['project-4'], 'link:../project-4')
})

View File

@@ -749,6 +749,7 @@ async function installInContext (
}
}
// TODO: link inside resolveDependencies.ts
if (installCtx.localPackages.length) {
const linkOpts = {
...opts,