1const path = require('path') 2 3const getName = require('@npmcli/name-from-folder') 4const { minimatch } = require('minimatch') 5const rpj = require('read-package-json-fast') 6const { glob } = require('glob') 7 8function appendNegatedPatterns (patterns) { 9 const results = [] 10 for (let pattern of patterns) { 11 const excl = pattern.match(/^!+/) 12 if (excl) { 13 pattern = pattern.slice(excl[0].length) 14 } 15 16 // strip off any / from the start of the pattern. /foo => foo 17 pattern = pattern.replace(/^\/+/, '') 18 19 // an odd number of ! means a negated pattern. !!foo ==> foo 20 const negate = excl && excl[0].length % 2 === 1 21 results.push({ pattern, negate }) 22 } 23 24 return results 25} 26 27function getPatterns (workspaces) { 28 const workspacesDeclaration = 29 Array.isArray(workspaces.packages) 30 ? workspaces.packages 31 : workspaces 32 33 if (!Array.isArray(workspacesDeclaration)) { 34 throw getError({ 35 message: 'workspaces config expects an Array', 36 code: 'EWORKSPACESCONFIG', 37 }) 38 } 39 40 return appendNegatedPatterns(workspacesDeclaration) 41} 42 43function getPackageName (pkg, pathname) { 44 const { name } = pkg 45 return name || getName(pathname) 46} 47 48function pkgPathmame (opts) { 49 return (...args) => { 50 const cwd = opts.cwd ? opts.cwd : process.cwd() 51 return path.join.apply(null, [cwd, ...args]) 52 } 53} 54 55// make sure glob pattern only matches folders 56function getGlobPattern (pattern) { 57 pattern = pattern.replace(/\\/g, '/') 58 return pattern.endsWith('/') 59 ? pattern 60 : `${pattern}/` 61} 62 63function getError ({ Type = TypeError, message, code }) { 64 return Object.assign(new Type(message), { code }) 65} 66 67function reverseResultMap (map) { 68 return new Map(Array.from(map, item => item.reverse())) 69} 70 71async function mapWorkspaces (opts = {}) { 72 if (!opts || !opts.pkg) { 73 throw getError({ 74 message: 'mapWorkspaces missing pkg info', 75 code: 'EMAPWORKSPACESPKG', 76 }) 77 } 78 79 const { workspaces = [] } = opts.pkg 80 const patterns = getPatterns(workspaces) 81 const results = new Map() 82 const seen = new Map() 83 84 if (!patterns.length) { 85 return results 86 } 87 88 const getGlobOpts = () => ({ 89 ...opts, 90 ignore: [ 91 ...opts.ignore || [], 92 ...['**/node_modules/**'], 93 ], 94 }) 95 96 const getPackagePathname = pkgPathmame(opts) 97 98 for (const item of patterns) { 99 let matches = await glob(getGlobPattern(item.pattern), getGlobOpts()) 100 // preserves glob@8 behavior 101 matches = matches.sort((a, b) => a.localeCompare(b, 'en')) 102 103 for (const match of matches) { 104 let pkg 105 const packageJsonPathname = getPackagePathname(match, 'package.json') 106 const packagePathname = path.dirname(packageJsonPathname) 107 108 try { 109 pkg = await rpj(packageJsonPathname) 110 } catch (err) { 111 if (err.code === 'ENOENT') { 112 continue 113 } else { 114 throw err 115 } 116 } 117 118 const name = getPackageName(pkg, packagePathname) 119 120 let seenPackagePathnames = seen.get(name) 121 if (!seenPackagePathnames) { 122 seenPackagePathnames = new Set() 123 seen.set(name, seenPackagePathnames) 124 } 125 if (item.negate) { 126 seenPackagePathnames.delete(packagePathname) 127 } else { 128 seenPackagePathnames.add(packagePathname) 129 } 130 } 131 } 132 133 const errorMessageArray = ['must not have multiple workspaces with the same name'] 134 for (const [packageName, seenPackagePathnames] of seen) { 135 if (seenPackagePathnames.size === 0) { 136 continue 137 } 138 if (seenPackagePathnames.size > 1) { 139 addDuplicateErrorMessages(errorMessageArray, packageName, seenPackagePathnames) 140 } else { 141 results.set(packageName, seenPackagePathnames.values().next().value) 142 } 143 } 144 145 if (errorMessageArray.length > 1) { 146 throw getError({ 147 Type: Error, 148 message: errorMessageArray.join('\n'), 149 code: 'EDUPLICATEWORKSPACE', 150 }) 151 } 152 153 return results 154} 155 156function addDuplicateErrorMessages (messageArray, packageName, packagePathnames) { 157 messageArray.push( 158 `package '${packageName}' has conflicts in the following paths:` 159 ) 160 161 for (const packagePathname of packagePathnames) { 162 messageArray.push( 163 ' ' + packagePathname 164 ) 165 } 166} 167 168mapWorkspaces.virtual = function (opts = {}) { 169 if (!opts || !opts.lockfile) { 170 throw getError({ 171 message: 'mapWorkspaces.virtual missing lockfile info', 172 code: 'EMAPWORKSPACESLOCKFILE', 173 }) 174 } 175 176 const { packages = {} } = opts.lockfile 177 const { workspaces = [] } = packages[''] || {} 178 // uses a pathname-keyed map in order to negate the exact items 179 const results = new Map() 180 const patterns = getPatterns(workspaces) 181 if (!patterns.length) { 182 return results 183 } 184 patterns.push({ pattern: '**/node_modules/**', negate: true }) 185 186 const getPackagePathname = pkgPathmame(opts) 187 188 for (const packageKey of Object.keys(packages)) { 189 if (packageKey === '') { 190 continue 191 } 192 193 for (const item of patterns) { 194 if (minimatch(packageKey, item.pattern)) { 195 const packagePathname = getPackagePathname(packageKey) 196 const name = getPackageName(packages[packageKey], packagePathname) 197 198 if (item.negate) { 199 results.delete(packagePathname) 200 } else { 201 results.set(packagePathname, name) 202 } 203 } 204 } 205 } 206 207 // Invert pathname-keyed to a proper name-to-pathnames Map 208 return reverseResultMap(results) 209} 210 211module.exports = mapWorkspaces 212