From b09382376e92a24f705ce3bd149200db1ecad127 Mon Sep 17 00:00:00 2001 From: Zoltan Kochan Date: Mon, 20 Aug 2018 01:07:28 +0300 Subject: [PATCH] feat: link-workspace-packages close #1259 --- packages/config/src/PnpmConfigs.ts | 1 + packages/config/src/index.ts | 2 + packages/pnpm/src/cmd/help.ts | 15 ++-- packages/pnpm/src/cmd/install.ts | 42 +++++++-- packages/pnpm/src/cmd/recursive/filter.ts | 13 +++ packages/pnpm/src/cmd/recursive/index.ts | 19 +++- packages/pnpm/src/types.ts | 1 + packages/pnpm/test/monorepo/index.ts | 104 +++++++++++++++++++++- packages/supi/src/api/install.ts | 1 + 9 files changed, 182 insertions(+), 16 deletions(-) diff --git a/packages/config/src/PnpmConfigs.ts b/packages/config/src/PnpmConfigs.ts index df4ef30e21..f6c883da9f 100644 --- a/packages/config/src/PnpmConfigs.ts +++ b/packages/config/src/PnpmConfigs.ts @@ -68,4 +68,5 @@ export interface PnpmConfigs { workspaceConcurrency: number, workspacePrefix?: string, reporter?: string, + linkWorkspacePackages: boolean, } diff --git a/packages/config/src/index.ts b/packages/config/src/index.ts index a0593f7f85..99eb9baeda 100644 --- a/packages/config/src/index.ts +++ b/packages/config/src/index.ts @@ -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, diff --git a/packages/pnpm/src/cmd/help.ts b/packages/pnpm/src/cmd/help.ts index b6a5e1ac6e..ada86aa989 100644 --- a/packages/pnpm/src/cmd/help.ts +++ b/packages/pnpm/src/cmd/help.ts @@ -252,7 +252,8 @@ function getHelpText (command: string) { install update uninstall [<@scope>/]... 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>/]...] list dependencies in each package. outdated [[<@scope>/]...] check for outdated dependencies in every package. @@ -271,11 +272,13 @@ function getHelpText (command: string) { Options: - --filter restricts the scope to package names matching the given glob. - --filter ... includes all direct and indirect dependencies of the matched packages. - --filter ... 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 restricts the scope to package names matching the given glob. + --filter ... includes all direct and indirect dependencies of the matched packages. + --filter ... 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: diff --git a/packages/pnpm/src/cmd/install.ts b/packages/pnpm/src/cmd/install.ts index 97ab2da326..d7144fd156 100644 --- a/packages/pnpm/src/cmd/install.ts +++ b/packages/pnpm/src/cmd/install.ts @@ -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 + } } diff --git a/packages/pnpm/src/cmd/recursive/filter.ts b/packages/pnpm/src/cmd/recursive/filter.ts index b943385871..89a1006ed5 100644 --- a/packages/pnpm/src/cmd/recursive/filter.ts +++ b/packages/pnpm/src/cmd/recursive/filter.ts @@ -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() + const graph = pkgGraphToGraph(pkgGraph) + pickSubgraph(graph, [entryDirectory], walkedDependencies) + + return R.pick(Array.from(walkedDependencies), pkgGraph) +} + export function filterGraph ( pkgGraph: PackageGraph, filters: string[], diff --git a/packages/pnpm/src/cmd/recursive/index.ts b/packages/pnpm/src/cmd/recursive/index.ts index 012e79db50..4eacb898c2 100644 --- a/packages/pnpm/src/cmd/recursive/index.ts +++ b/packages/pnpm/src/cmd/recursive/index.ts @@ -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, diff --git a/packages/pnpm/src/types.ts b/packages/pnpm/src/types.ts index 36f5df6133..fedc4195b2 100644 --- a/packages/pnpm/src/types.ts +++ b/packages/pnpm/src/types.ts @@ -75,6 +75,7 @@ export interface PnpmOptions { useStoreServer?: boolean, workspaceConcurrency: number, workspacePrefix?: string, + linkWorkspacePackages: boolean, // cannot be specified via configs update?: boolean, diff --git a/packages/pnpm/test/monorepo/index.ts b/packages/pnpm/test/monorepo/index.ts index 372e09c7d9..084ca765ae 100644 --- a/packages/pnpm/test/monorepo/index.ts +++ b/packages/pnpm/test/monorepo/index.ts @@ -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') +}) diff --git a/packages/supi/src/api/install.ts b/packages/supi/src/api/install.ts index 02e0ccb6ad..22ae695094 100644 --- a/packages/supi/src/api/install.ts +++ b/packages/supi/src/api/install.ts @@ -749,6 +749,7 @@ async function installInContext ( } } + // TODO: link inside resolveDependencies.ts if (installCtx.localPackages.length) { const linkOpts = { ...opts,