const fs = require('fs-extra'); const path = require('path'); const upath = require('upath'); const npm = require('./npm'); const lerna = require('./lerna'); const yarn = require('./yarn'); const pnpm = require('./pnpm'); module.exports = { hasPackageLock, hasYarnLock, hasShrinkwrapYaml, determineLockFileDirs, writeExistingFiles, writeUpdatedPackageFiles, getUpdatedLockFiles, }; function hasPackageLock(config, packageFile) { logger.trace( { packageFiles: config.packageFiles, packageFile }, 'hasPackageLock' ); for (const p of config.packageFiles) { if (p.packageFile === packageFile) { if (p.packageLock) { return true; } return false; } } throw new Error(`hasPackageLock cannot find ${packageFile}`); } function hasYarnLock(config, packageFile) { logger.trace( { packageFiles: config.packageFiles, packageFile }, 'hasYarnLock' ); for (const p of config.packageFiles) { if (p.packageFile === packageFile) { if (p.yarnLock) { return true; } return false; } } throw new Error(`hasYarnLock cannot find ${packageFile}`); } function hasShrinkwrapYaml(config, packageFile) { logger.trace( { packageFiles: config.packageFiles, packageFile }, 'hasShrinkwrapYaml' ); for (const p of config.packageFiles) { if (p.packageFile === packageFile) { if (p.shrinkwrapYaml) { return true; } return false; } } throw new Error(`hasShrinkwrapYaml cannot find ${packageFile}`); } function determineLockFileDirs(config) { const packageLockFileDirs = []; const yarnLockFileDirs = []; const shrinkwrapYamlDirs = []; const lernaDirs = []; for (const upgrade of config.upgrades) { if (upgrade.type === 'lockFileMaintenance') { // Return every direcotry that contains a lockfile for (const packageFile of config.packageFiles) { const dirname = path.dirname(packageFile.packageFile); if (packageFile.yarnLock) { yarnLockFileDirs.push(dirname); } if (packageFile.packageLock) { packageLockFileDirs.push(dirname); } if (packageFile.shrinkwrapYaml) { shrinkwrapYamlDirs.push(dirname); } } return { packageLockFileDirs, yarnLockFileDirs, shrinkwrapYamlDirs }; } } for (const packageFile of config.updatedPackageFiles) { if ( module.exports.hasYarnLock(config, packageFile.name) && !config.lernaLockFile ) { yarnLockFileDirs.push(path.dirname(packageFile.name)); } if ( module.exports.hasPackageLock(config, packageFile.name) && !config.lernaLockFile ) { packageLockFileDirs.push(path.dirname(packageFile.name)); } if (module.exports.hasShrinkwrapYaml(config, packageFile.name)) { shrinkwrapYamlDirs.push(path.dirname(packageFile.name)); } } if ( config.updatedPackageFiles && config.updatedPackageFiles.length && config.lernaLockFile ) { lernaDirs.push('.'); } // If yarn workspaces are in use, then we need to generate yarn.lock from the workspaces dir if ( config.updatedPackageFiles && config.updatedPackageFiles.length && config.workspaceDir ) { const updatedPackageFileNames = config.updatedPackageFiles.map(p => p.name); for (const packageFile of config.packageFiles) { if ( updatedPackageFileNames.includes(packageFile.packageFile) && packageFile.workspaceDir && !yarnLockFileDirs.includes(packageFile.workspaceDir) ) yarnLockFileDirs.push(packageFile.workspaceDir); } } return { yarnLockFileDirs, packageLockFileDirs, shrinkwrapYamlDirs, lernaDirs, }; } async function writeExistingFiles(config) { const lernaJson = await platform.getFile('lerna.json'); if (lernaJson) { logger.debug({ path: config.tmpDir.path }, 'Writing repo lerna.json'); await fs.outputFile( upath.join(config.tmpDir.path, 'lerna.json'), lernaJson ); } if (config.npmrc) { logger.debug({ path: config.tmpDir.path }, 'Writing repo .npmrc'); await fs.outputFile(upath.join(config.tmpDir.path, '.npmrc'), config.npmrc); } if (config.yarnrc) { logger.debug({ path: config.tmpDir.path }, 'Writing repo .yarnrc'); await fs.outputFile( upath.join(config.tmpDir.path, '.yarnrc'), config.yarnrc ); } if (!config.packageFiles) { return; } const npmFiles = config.packageFiles.filter(p => p.packageFile.endsWith('package.json') ); for (const packageFile of npmFiles) { const basedir = upath.join( config.tmpDir.path, path.dirname(packageFile.packageFile) ); logger.debug(`Writing package.json to ${basedir}`); // Massage the file to eliminate yarn errors const massagedFile = { ...packageFile.content }; if (massagedFile.name) { massagedFile.name = massagedFile.name.replace(/[{}]/g, ''); } delete massagedFile.engines; delete massagedFile.scripts; await fs.outputFile( upath.join(basedir, 'package.json'), JSON.stringify(massagedFile) ); if (config.copyLocalLibs) { const toCopy = listLocalLibs(massagedFile.dependencies); toCopy.push(...listLocalLibs(massagedFile.devDependencies)); if (toCopy.length !== 0) { logger.debug(`listOfNeededLocalFiles files to copy: ${toCopy}`); await Promise.all( toCopy.map(async localPath => { const resolvedLocalPath = upath.join( path.resolve(basedir, localPath) ); if (!resolvedLocalPath.startsWith(upath.join(config.tmpDir.path))) { logger.info( `local lib '${toCopy}' will not be copied because it's out of the repo.` ); } else { // at the root of local Lib we should find a package.json so that yarn/npm will use it to update *lock file const resolvedRepoPath = upath.join( resolvedLocalPath.substring(config.tmpDir.path.length + 1), 'package.json' ); const fileContent = await platform.getFile(resolvedRepoPath); if (fileContent !== null) { await fs.outputFile( upath.join(resolvedLocalPath, 'package.json'), fileContent ); } else { logger.info( `listOfNeededLocalFiles - file '${resolvedRepoPath}' not found.` ); } } }) ); } } if (packageFile.npmrc) { logger.debug(`Writing .npmrc to ${basedir}`); await fs.outputFile(upath.join(basedir, '.npmrc'), packageFile.npmrc); } else if ( config.npmrc && (packageFile.hasYarnLock || packageFile.hasPackageLock) ) { logger.debug('Writing repo .npmrc to package file dir'); await fs.outputFile(upath.join(basedir, '.npmrc'), config.npmrc); } if (packageFile.yarnrc) { logger.debug(`Writing .yarnrc to ${basedir}`); await fs.outputFile( upath.join(basedir, '.yarnrc'), packageFile.yarnrc.replace('--install.pure-lockfile true', '') ); } /* // TODO: restore this functionality when https://github.com/npm/npm/issues/19852 is fixed if (packageFile.packageLock && config.type !== 'lockFileMaintenance') { logger.debug(`Writing package-lock.json to ${basedir}`); const existingPackageLock = (await platform.branchExists(config.branchName)) && (await platform.getFile(packageFile.packageLock, config.branchName)); const packageLock = existingPackageLock || (await platform.getFile(packageFile.packageLock)); await fs.outputFile( upath.join(basedir, 'package-lock.json'), packageLock ); } else { logger.debug(`Removing ${basedir}/package-lock.json`); await fs.remove(upath.join(basedir, 'package-lock.json')); } */ if (packageFile.yarnLock && config.type !== 'lockFileMaintenance') { logger.debug(`Writing yarn.lock to ${basedir}`); const existingYarnLock = (await platform.branchExists(config.branchName)) && (await platform.getFile(packageFile.yarnLock, config.branchName)); const yarnLock = existingYarnLock || (await platform.getFile(packageFile.yarnLock)); await fs.outputFile(upath.join(basedir, 'yarn.lock'), yarnLock); } else { logger.debug(`Removing ${basedir}/yarn.lock`); await fs.remove(upath.join(basedir, 'yarn.lock')); } // TODO: Update the below with this once https://github.com/pnpm/pnpm/issues/992 is fixed const pnpmBug992 = true; // istanbul ignore next if ( packageFile.shrinkwrapYaml && config.type !== 'lockFileMaintenance' && !pnpmBug992 ) { logger.debug(`Writing shrinkwrap.yaml to ${basedir}`); const shrinkwrap = await platform.getFile(packageFile.shrinkwrapYaml); await fs.outputFile(upath.join(basedir, 'shrinkwrap.yaml'), shrinkwrap); } else { await fs.remove(upath.join(basedir, 'shrinkwrap.yaml')); } } } function listLocalLibs(dependencies) { logger.trace(`listLocalLibs (${dependencies})`); const toCopy = []; if (dependencies) { for (const [libName, libVersion] of Object.entries(dependencies)) { if (libVersion.startsWith('file:')) { if (libVersion.endsWith('.tgz')) { logger.info( `Link to local lib "${libName}": "${libVersion}" is not supported. Please do it like: 'file:/path/to/folder'` ); } else { toCopy.push(libVersion.substring('file:'.length)); } } } } return toCopy; } async function writeUpdatedPackageFiles(config) { logger.trace({ config }, 'writeUpdatedPackageFiles'); logger.debug('Writing any updated package files'); if (!config.updatedPackageFiles) { logger.debug('No files found'); return; } for (const packageFile of config.updatedPackageFiles) { if (!packageFile.name.endsWith('package.json')) { continue; // eslint-disable-line } logger.debug(`Writing ${packageFile.name}`); const massagedFile = JSON.parse(packageFile.contents); if (massagedFile.name) { massagedFile.name = massagedFile.name.replace(/[{}]/g, ''); } delete massagedFile.engines; delete massagedFile.scripts; await fs.outputFile( upath.join(config.tmpDir.path, packageFile.name), JSON.stringify(massagedFile) ); } } async function getUpdatedLockFiles(config) { logger.trace({ config }, 'getUpdatedLockFiles'); logger.debug('Getting updated lock files'); const lockFileErrors = []; const updatedLockFiles = []; if ( config.type === 'lockFileMaintenance' && (await platform.branchExists(config.branchName)) ) { return { lockFileErrors, updatedLockFiles }; } const dirs = module.exports.determineLockFileDirs(config); logger.debug({ dirs }, 'lock file dirs'); await module.exports.writeExistingFiles(config); await module.exports.writeUpdatedPackageFiles(config); const env = config.global && config.global.exposeEnv ? process.env : { HOME: process.env.HOME, PATH: process.env.PATH }; env.NODE_ENV = 'dev'; for (const lockFileDir of dirs.packageLockFileDirs) { logger.debug(`Generating package-lock.json for ${lockFileDir}`); const lockFileName = upath.join(lockFileDir, 'package-lock.json'); const res = await npm.generateLockFile( upath.join(config.tmpDir.path, lockFileDir), env ); if (res.error) { lockFileErrors.push({ lockFile: lockFileName, stderr: res.stderr, }); } else { const existingContent = await platform.getFile( lockFileName, config.parentBranch ); if (res.lockFile !== existingContent) { logger.debug('package-lock.json needs updating'); updatedLockFiles.push({ name: lockFileName, contents: res.lockFile, }); } else { logger.debug("package-lock.json hasn't changed"); } } } for (const lockFileDir of dirs.yarnLockFileDirs) { logger.debug(`Generating yarn.lock for ${lockFileDir}`); const lockFileName = upath.join(lockFileDir, 'yarn.lock'); const res = await yarn.generateLockFile( upath.join(config.tmpDir.path, lockFileDir), env ); if (res.error) { lockFileErrors.push({ lockFile: lockFileName, stderr: res.stderr, }); } else { const existingContent = await platform.getFile( lockFileName, config.parentBranch ); if (res.lockFile !== existingContent) { logger.debug('yarn.lock needs updating'); updatedLockFiles.push({ name: lockFileName, contents: res.lockFile, }); } else { logger.debug("yarn.lock hasn't changed"); } } } for (const lockFileDir of dirs.shrinkwrapYamlDirs) { logger.debug(`Generating shrinkwrap.yaml for ${lockFileDir}`); const lockFileName = upath.join(lockFileDir, 'shrinkwrap.yaml'); const res = await pnpm.generateLockFile( upath.join(config.tmpDir.path, lockFileDir), env ); if (res.error) { lockFileErrors.push({ lockFile: lockFileName, stderr: res.stderr, }); } else { const existingContent = await platform.getFile( lockFileName, config.parentBranch ); if (res.lockFile !== existingContent) { logger.debug('shrinkwrap.yaml needs updating'); updatedLockFiles.push({ name: lockFileName, contents: res.lockFile, }); } else { logger.debug("shrinkwrap.yaml hasn't changed"); } } } if (dirs.lernaDirs && dirs.lernaDirs.length) { let manager; let lockFile; if (config.lernaLockFile === 'npm') { manager = 'npm'; lockFile = 'package-lock.json'; } else { manager = 'yarn'; lockFile = 'yarn.lock'; } logger.debug({ manager, lockFile }, 'Generating lock files using lerna'); const res = await lerna.generateLockFiles(manager, config.tmpDir.path, env); // istanbul ignore else if (res.error) { lockFileErrors.push({ lockFile, stderr: res.stderr, }); } else { for (const packageFile of config.packageFiles) { const baseDir = path.dirname(packageFile.packageFile); const filename = upath.join(baseDir, lockFile); logger.debug('Checking for ' + filename); const existingContent = await platform.getFile( filename, config.parentBranch ); if (existingContent) { logger.debug('Found lock file'); const lockFilePath = upath.join(config.tmpDir.path, filename); logger.debug('Checking against ' + lockFilePath); const newContent = await fs.readFile(lockFilePath, 'utf8'); if (newContent !== existingContent) { logger.debug('File is updated'); updatedLockFiles.push({ name: filename, contents: newContent, }); } else { logger.debug('File is unchanged'); } } else { logger.debug('No lock file found'); } } } } return { lockFileErrors, updatedLockFiles }; }