Skip to content

Commit

Permalink
feat: npm workspaces
Browse files Browse the repository at this point in the history
Introduces support to workspaces; adding ability to build an ideal tree
that links defined workspaces, storing and reading lockfiles that
contains workspaces info and also reifying installation trees properly
symlinking nested workspaces into place.

Handling of the config definitions is done via @npmcli/map-workspaces
module added.

refs:

- https://github.com/npm/rfcs/blob/ea2d3024e6e149cd8c6366ed18373c9a566b1124/accepted/0026-workspaces.md
- https://www.npmjs.com/package/@npmcli/map-workspaces
- npm/rfcs#103
  • Loading branch information
ruyadorno committed Apr 30, 2020
1 parent d367a70 commit ac9f4d8
Show file tree
Hide file tree
Showing 79 changed files with 3,029 additions and 43 deletions.
13 changes: 13 additions & 0 deletions lib/arborist/build-ideal-tree.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const npa = require('npm-package-arg')
const pacote = require('pacote')
const semver = require('semver')
const pickManifest = require('npm-pick-manifest')
const mapWorkspaces = require('@npmcli/map-workspaces')

const calcDepFlags = require('../calc-dep-flags.js')
const Shrinkwrap = require('../shrinkwrap.js')
Expand Down Expand Up @@ -46,6 +47,7 @@ const _nodeFromSpec = Symbol('nodeFromSpec')
const _fetchManifest = Symbol('fetchManifest')
const _problemEdges = Symbol('problemEdges')
const _manifests = Symbol('manifests')
const _mapWorkspaces = Symbol('mapWorkspaces')
const _linkFromSpec = Symbol('linkFromSpec')
const _loadPeerSet = Symbol('loadPeerSet')
// shared symbols so we can hit them with unit tests
Expand Down Expand Up @@ -203,6 +205,7 @@ module.exports = cls => class IdealTreeBuilder extends Tracker(Virtual(Actual(cl
.then(meta => Object.assign(root, {meta}))
: this.loadVirtual({ root }))

.then(tree => this[_mapWorkspaces](tree))
.then(tree => {
// null the virtual tree, because we're about to hack away at it
// if you want another one, load another copy.
Expand Down Expand Up @@ -234,9 +237,19 @@ module.exports = cls => class IdealTreeBuilder extends Tracker(Virtual(Actual(cl
optional: false,
global: this[_global],
legacyPeerDeps: this.legacyPeerDeps,
hasWorkspaces: !!pkg.workspaces,
})
}

[_mapWorkspaces] (node) {
return mapWorkspaces({ cwd: node.path, pkg: node.package })
.then(workspaces => {
if (workspaces.size)
node.workspaces = workspaces
return node
})
}

// process the add/rm requests by modifying the root node, and the
// update.names request by queueing nodes dependent on those named.
[_applyUserRequests] (options) {
Expand Down
17 changes: 16 additions & 1 deletion lib/arborist/load-virtual.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// mixin providing the loadVirtual method

const {resolve} = require('path')
const mapWorkspaces = require('@npmcli/map-workspaces')

const consistentResolve = require('../consistent-resolve.js')
const Shrinkwrap = require('../shrinkwrap.js')
Expand All @@ -15,6 +16,7 @@ const resolveLinks = Symbol('resolveLinks')
const assignParentage = Symbol('assignParentage')
const loadNode = Symbol('loadVirtualNode')
const loadLink = Symbol('loadVirtualLink')
const loadWorkspaces = Symbol('loadWorkspaces')

module.exports = cls => class VirtualLoader extends cls {
constructor (options) {
Expand All @@ -39,7 +41,10 @@ module.exports = cls => class VirtualLoader extends cls {
// when building the ideal tree, we pass in a root node to this function
// otherwise, load it from the root package in the lockfile
const {
root = this[loadNode]('', s.data.packages[''] || {})
root = this[loadWorkspaces](
this[loadNode]('', s.data.packages[''] || {}),
s
)
} = options

return this[loadFromShrinkwrap](s, root)
Expand Down Expand Up @@ -160,6 +165,16 @@ module.exports = cls => class VirtualLoader extends cls {
return node
}

[loadWorkspaces] (node, s) {
const workspaces = mapWorkspaces.virtual({
cwd: node.path,
lockfile: s.data
})
if (workspaces.size)
node.workspaces = workspaces
return node
}

[loadLink] (location, targetLoc, target, meta) {
const path = resolve(this.path, location)
const link = new Link({
Expand Down
6 changes: 6 additions & 0 deletions lib/edge.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// An edge in the dependency graph
// Represents a dependency relationship of some kind

const npa = require('npm-package-arg')
const depValid = require('./dep-valid.js')
const _from = Symbol('_from')
const _to = Symbol('_to')
Expand All @@ -18,6 +19,7 @@ const types = new Set([
'optional',
'peer',
'peerOptional',
'workspace'
])

class Edge {
Expand All @@ -26,6 +28,10 @@ class Edge {

if (typeof spec !== 'string')
throw new TypeError('must provide string spec')

if (type === 'workspace' && npa(spec).type !== 'directory')
throw new TypeError('workspace edges must be a symlink')

this[_spec] = spec

if (accept !== undefined) {
Expand Down
29 changes: 29 additions & 0 deletions lib/node.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ const _fsParent = Symbol('_fsParent')
const _reloadEdges = Symbol('_reloadEdges')
const _loadType = Symbol('_loadType')
const _loadDepType = Symbol('_loadDepType')
const _loadWorkspaces = Symbol('_loadWorkspaces')
const _reloadNamedEdges = Symbol('_reloadNamedEdges')
// overridden by Link class
const _loadDeps = Symbol.for('Arborist.Node._loadDeps')
Expand All @@ -55,6 +56,7 @@ const _refreshTopMeta = Symbol('_refreshTopMeta')
const _refreshPath = Symbol('_refreshPath')
const _delistFromMeta = Symbol('_delistFromMeta')
const _global = Symbol.for('global')
const _workspaces = Symbol('_workspaces')

const relpath = require('./relpath.js')
const consistentResolve = require('./consistent-resolve.js')
Expand Down Expand Up @@ -93,6 +95,8 @@ class Node {
// true if part of a global install
this[_global] = global

this[_workspaces] = null

this.errors = error ? [error] : []
const pkg = normalize(options.pkg || {})

Expand Down Expand Up @@ -209,6 +213,22 @@ class Node {
return this.global && this.parent.isRoot
}

get workspaces() {
return this[_workspaces]
}

set workspaces(workspaces) {
// deletes edges if they already exists
if (this[_workspaces])
for (const [name, path] of this[_workspaces].entries()) {
if (!workspaces.has(name)) this.edgesOut.get(name).detach()
}

this[_workspaces] = workspaces
this[_loadWorkspaces]()
this[_loadDeps]()
}

get binPaths () {
if (!this.parent)
return []
Expand Down Expand Up @@ -242,6 +262,7 @@ class Node {
}

this[_package] = pkg
this[_loadWorkspaces]()
this[_loadDeps]()
// do a hard reload, since the dependents may now be valid or invalid
// as a result of the package change.
Expand Down Expand Up @@ -334,6 +355,14 @@ class Node {
return this[_root] || this
}

[_loadWorkspaces] () {
if (!this[_workspaces]) return

for (const [name, path] of this[_workspaces].entries()) {
new Edge({ from: this, name, spec: `file:${path}`, type: 'workspace' })
}
}

[_loadDeps] () {
// Caveat! Order is relevant!
// packages in optionalDependencies and prod/peer/dev are
Expand Down
1 change: 1 addition & 0 deletions lib/shrinkwrap.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ const pkgMetaKeys = [
'hasInstallScript',
'bin',
'deprecated',
'workspaces',
]

const nodeMetaKeys = [
Expand Down
Loading

0 comments on commit ac9f4d8

Please sign in to comment.