diff --git a/README.md b/README.md index a31b58cc..fb0dd1a7 100644 --- a/README.md +++ b/README.md @@ -126,6 +126,15 @@ module.exports = function() { dependencies will be restored to their prior state. */ useYarn: true, + + /* + buildManagerOptions allows you to opt-out of the default options such as `--ignore-engines --no-lockfile`. + The buildManagerOptions function is aware of each scenario so you can customize your options. + */ + buildManagerOptions(scenario) { + return ['--ignore-engines']; + } + scenarios: [ { name: 'Ember 1.10 with ember-data', diff --git a/lib/dependency-manager-adapters/npm.js b/lib/dependency-manager-adapters/npm.js index af30f86e..12fdca24 100644 --- a/lib/dependency-manager-adapters/npm.js +++ b/lib/dependency-manager-adapters/npm.js @@ -16,11 +16,16 @@ module.exports = CoreObject.extend({ }, useYarnCommand: false, yarnLock: 'yarn.lock', + yarnLockBackupFileName: 'yarn.lock.ember-try', configKey: 'npm', packageJSON: 'package.json', packageJSONBackupFileName: 'package.json.ember-try', nodeModules: 'node_modules', nodeModulesBackupLocation: '.node_modules.ember-try', + npmShrinkWrap: 'npm-shrinkwrap.json', + npmShrinkWrapBackupFileName: 'npm-shrinkwrap.json.ember-try', + packageLock: 'package-lock.json', + packageLockBackupFileName: 'package-lock.json.ember-try', setup(options) { if (!options) { options = {}; @@ -33,7 +38,7 @@ module.exports = CoreObject.extend({ adapter.applyDependencySet(depSet); - return adapter._install().then(() => { + return adapter._install(depSet).then(() => { let deps = extend({}, depSet.dependencies || {}, depSet.devDependencies || {}); let currentDeps = Object.keys(deps).map((dep) => { return { @@ -58,6 +63,18 @@ module.exports = CoreObject.extend({ let cleanupTasks = [rimraf(path.join(adapter.cwd, adapter.packageJSONBackupFileName)), rimraf(path.join(adapter.cwd, adapter.nodeModulesBackupLocation))]; + if (fs.existsSync(path.join(this.cwd, this.yarnLockBackupFileName))) { + cleanupTasks.push(rimraf(path.join(adapter.cwd, adapter.yarnLockBackupFileName))); + } + + if (fs.existsSync(path.join(this.cwd, this.npmShrinkWrapBackupFileName))) { + cleanupTasks.push(rimraf(path.join(adapter.cwd, adapter.npmShrinkWrapBackupFileName))); + } + + if (fs.existsSync(path.join(this.cwd, this.packageLockBackupFileName))) { + cleanupTasks.push(rimraf(path.join(adapter.cwd, adapter.packageLockBackupFileName))); + } + return RSVP.all(cleanupTasks); }).catch((e) => { console.log('Error cleaning up npm scenario:', e); // eslint-disable-line no-console @@ -82,26 +99,35 @@ module.exports = CoreObject.extend({ return null; } }, - _install() { + _install(depSet) { let adapter = this; let mgrOptions = this.managerOptions || []; + let cmd = this.useYarnCommand ? 'yarn' : 'npm'; - debug('Run npm install with options %s', mgrOptions); + // buildManagerOptions overrides all default + if (typeof this.buildManagerOptions === 'function') { + mgrOptions = this.buildManagerOptions(depSet); - let cmd = this.useYarnCommand ? 'yarn' : 'npm'; - if (this.useYarnCommand) { - if (mgrOptions.indexOf('--no-lockfile') === -1) { - mgrOptions = mgrOptions.concat(['--no-lockfile']); + if (!Array.isArray(mgrOptions)) { + throw new Error('buildManagerOptions must return an array of options'); } - // npm warns on incompatible engines - // yarn errors, not a good experience - if (mgrOptions.indexOf('--ignore-engines') === -1) { - mgrOptions = mgrOptions.concat(['--ignore-engines']); + } else { + if (this.useYarnCommand) { + if (mgrOptions.indexOf('--no-lockfile') === -1) { + mgrOptions = mgrOptions.concat(['--no-lockfile']); + } + // npm warns on incompatible engines + // yarn errors, not a good experience + if (mgrOptions.indexOf('--ignore-engines') === -1) { + mgrOptions = mgrOptions.concat(['--ignore-engines']); + } + } else if (mgrOptions.indexOf('--no-shrinkwrap') === -1) { + mgrOptions = mgrOptions.concat(['--no-shrinkwrap']); } - } else if (mgrOptions.indexOf('--no-shrinkwrap') === -1) { - mgrOptions = mgrOptions.concat(['--no-shrinkwrap']); } + debug('Run npm/yarn install with options %s', mgrOptions); + return this.run(cmd, [].concat(['install'], mgrOptions), { cwd: this.cwd }).then(() => { if (!adapter.useYarnCommand) { return adapter.run('npm', ['--version'], { cwd: this.cwd, stdio: 'pipe' }).then((res) => { @@ -171,6 +197,21 @@ module.exports = CoreObject.extend({ path.join(this.cwd, this.nodeModules), { clobber: true }), ]; + if (fs.existsSync(path.join(this.cwd, this.yarnLockBackupFileName))) { + restoreTasks.push(copy(path.join(this.cwd, this.yarnLockBackupFileName), + path.join(this.cwd, this.yarnLock))); + } + + if (fs.existsSync(path.join(this.cwd, this.npmShrinkWrapBackupFileName))) { + restoreTasks.push(copy(path.join(this.cwd, this.npmShrinkWrapBackupFileName), + path.join(this.cwd, this.npmShrinkWrap))); + } + + if (fs.existsSync(path.join(this.cwd, this.packageLockBackupFileName))) { + restoreTasks.push(copy(path.join(this.cwd, this.packageLockBackupFileName), + path.join(this.cwd, this.packageLock))); + } + return RSVP.all(restoreTasks); }, _backupOriginalDependencies() { @@ -184,6 +225,21 @@ module.exports = CoreObject.extend({ copy(path.join(this.cwd, this.nodeModules), path.join(this.cwd, this.nodeModulesBackupLocation), { clobber: true })]; + if (fs.existsSync(path.join(this.cwd, this.yarnLock))) { + backupTasks.push(copy(path.join(this.cwd, this.yarnLock), + path.join(this.cwd, this.yarnLockBackupFileName))); + } + + if (fs.existsSync(path.join(this.cwd, this.npmShrinkWrap))) { + backupTasks.push(copy(path.join(this.cwd, this.npmShrinkWrap), + path.join(this.cwd, this.npmShrinkWrapBackupFileName))); + } + + if (fs.existsSync(path.join(this.cwd, this.packageLock))) { + backupTasks.push(copy(path.join(this.cwd, this.packageLock), + path.join(this.cwd, this.packageLockBackupFileName))); + } + return RSVP.all(backupTasks); }, }); diff --git a/lib/dependency-manager-adapters/workspace.js b/lib/dependency-manager-adapters/workspace.js index baa60566..b5eb79fd 100644 --- a/lib/dependency-manager-adapters/workspace.js +++ b/lib/dependency-manager-adapters/workspace.js @@ -47,6 +47,7 @@ module.exports = CoreObject.extend({ run: this.run, managerOptions: this.managerOptions, useYarnCommand: true, + buildManagerOptions: this.buildManagerOptions, }); }); @@ -59,7 +60,7 @@ module.exports = CoreObject.extend({ adapter.applyDependencySet(depSet); }); - return this._install().then(() => { + return this._install(depSet).then(() => { let deps = extend({}, depSet.dependencies || {}, depSet.devDependencies || {}); let currentDeps = Object.keys(deps).map((dep) => { return { @@ -80,20 +81,29 @@ module.exports = CoreObject.extend({ return RSVP.all(this._packageAdapters.map(adapter => adapter.cleanup())); }, - _install() { + _install(depSet) { let mgrOptions = this.managerOptions || []; - debug('Run yarn install with options %s', mgrOptions); - - if (mgrOptions.indexOf('--no-lockfile') === -1) { - mgrOptions = mgrOptions.concat(['--no-lockfile']); - } - // npm warns on incompatible engines - // yarn errors, not a good experience - if (mgrOptions.indexOf('--ignore-engines') === -1) { - mgrOptions = mgrOptions.concat(['--ignore-engines']); + // buildManagerOptions overrides all default + if (typeof this.buildManagerOptions === 'function') { + mgrOptions = this.buildManagerOptions(depSet); + + if (!Array.isArray(mgrOptions)) { + throw new Error('buildManagerOptions must return an array of options'); + } + } else { + if (mgrOptions.indexOf('--no-lockfile') === -1) { + mgrOptions = mgrOptions.concat(['--no-lockfile']); + } + // npm warns on incompatible engines + // yarn errors, not a good experience + if (mgrOptions.indexOf('--ignore-engines') === -1) { + mgrOptions = mgrOptions.concat(['--ignore-engines']); + } } + debug('Run yarn install with options %s', mgrOptions); + return this.run('yarn', [].concat(['install'], mgrOptions), { cwd: this.cwd }); }, diff --git a/lib/utils/dependency-manager-adapter-factory.js b/lib/utils/dependency-manager-adapter-factory.js index f2c159c8..b524dd44 100644 --- a/lib/utils/dependency-manager-adapter-factory.js +++ b/lib/utils/dependency-manager-adapter-factory.js @@ -31,11 +31,12 @@ module.exports = { new WorkspaceAdapter({ cwd: root, managerOptions: config.npmOptions, - useYarnCommand: config.useYarn + useYarnCommand: config.useYarn, + buildManagerOptions: config.buildManagerOptions }) ); } else if (hasNpm || hasBower) { - adapters.push(new NpmAdapter({ cwd: root, managerOptions: config.npmOptions, useYarnCommand: config.useYarn })); + adapters.push(new NpmAdapter({ cwd: root, managerOptions: config.npmOptions, useYarnCommand: config.useYarn, buildManagerOptions: config.buildManagerOptions })); adapters.push(new BowerAdapter({ cwd: root, managerOptions: config.bowerOptions })); } diff --git a/test/dependency-manager-adapters/npm-adapter-test.js b/test/dependency-manager-adapters/npm-adapter-test.js index 8879bbb9..239a7473 100644 --- a/test/dependency-manager-adapters/npm-adapter-test.js +++ b/test/dependency-manager-adapters/npm-adapter-test.js @@ -39,6 +39,24 @@ describe('npmAdapter', () => { assertFileContainsJSON(path.join(tmpdir, '.node_modules.ember-try/prove-it.json'), { originalNodeModules: true }); }); }); + + it('backs up the yarn.lock file, npm-shrinkwrap.json and package-lock.json if they exist', () => { + fs.mkdirSync('node_modules'); + writeJSONFile('node_modules/prove-it.json', { originalNodeModules: true }); + writeJSONFile('package.json', { originalPackageJSON: true }); + writeJSONFile('yarn.lock', { originalYarnLock: true }); + writeJSONFile('npm-shrinkwrap.json', { originalNpmShrinkWrap: true }); + writeJSONFile('package-lock.json', { originalPackageLock: true }); + return new NpmAdapter({ + cwd: tmpdir, + }).setup().then(() => { + assertFileContainsJSON(path.join(tmpdir, 'package.json.ember-try'), { originalPackageJSON: true }); + assertFileContainsJSON(path.join(tmpdir, '.node_modules.ember-try/prove-it.json'), { originalNodeModules: true }); + assertFileContainsJSON(path.join(tmpdir, 'yarn.lock.ember-try'), { originalYarnLock: true }); + assertFileContainsJSON(path.join(tmpdir, 'npm-shrinkwrap.json.ember-try'), { originalNpmShrinkWrap: true }); + assertFileContainsJSON(path.join(tmpdir, 'package-lock.json.ember-try'), { originalPackageLock: true }); + }); + }); }); describe('#_install', () => { @@ -127,6 +145,46 @@ describe('npmAdapter', () => { expect(runCount).to.equal(2); }); }); + + it('uses buildManagerOptions for npm commands', () => { + writeJSONFile('package.json', fixturePackage); + let runCount = 0; + let stubbedRun = generateMockRun([{ + command: 'npm install --flat', + callback() { + runCount++; + return RSVP.resolve(); + }, + }, { + command: 'npm --version', + callback() { + runCount++; + return RSVP.resolve({stdout: '5.7.1'}); + }, + }], { allowPassthrough: false }); + + return new NpmAdapter({ + cwd: tmpdir, + run: stubbedRun, + buildManagerOptions: function() { + return ['--flat']; + }, + })._install().then(() => { + expect(runCount).to.equal(2, 'npm install should run with buildManagerOptions'); + }); + }); + + it('throws an error if buildManagerOptions does not return an array', () => { + expect(() => { + new NpmAdapter({ + cwd: tmpdir, + run: () => {}, + buildManagerOptions: function() { + return 'string'; + }, + })._install() + }).to.throw(/buildManagerOptions must return an array of options/); + }); }); describe('with yarn', () => { @@ -171,6 +229,42 @@ describe('npmAdapter', () => { expect(runCount).to.equal(1, 'Only yarn install should run with manager options'); }); }); + + it('uses buildManagerOptions for yarn commands', () => { + writeJSONFile('package.json', fixturePackage); + let runCount = 0; + let stubbedRun = generateMockRun([{ + command: 'yarn install --flat', + callback() { + runCount++; + return RSVP.resolve(); + }, + }], { allowPassthrough: false }); + + return new NpmAdapter({ + cwd: tmpdir, + run: stubbedRun, + useYarnCommand: true, + buildManagerOptions: function() { + return ['--flat']; + }, + })._install().then(() => { + expect(runCount).to.equal(1, 'Only yarn install should run with buildManagerOptions'); + }); + }); + + it('throws an error if buildManagerOptions does not return an array', () => { + expect(() => { + new NpmAdapter({ + cwd: tmpdir, + run: () => {}, + useYarnCommand: true, + buildManagerOptions: function() { + return 'string'; + }, + })._install() + }).to.throw(/buildManagerOptions must return an array of options/); + }); }); }); @@ -185,6 +279,26 @@ describe('npmAdapter', () => { assertFileContainsJSON(path.join(tmpdir, 'node_modules/prove-it.json'), { originalNodeModules: true }); }); }); + + it('replaces the yarn.lock, npm-shrinkwrap.json and package-lock.json with the backed up version if they exist', () => { + writeJSONFile('package.json.ember-try', { originalPackageJSON: true }); + writeJSONFile('package.json', { originalPackageJSON: false }); + fs.mkdirSync('.node_modules.ember-try'); + writeJSONFile('.node_modules.ember-try/prove-it.json', { originalNodeModules: true }); + writeJSONFile('yarn.lock.ember-try', { originalYarnLock: true }); + writeJSONFile('yarn.lock', { originalYarnLock: false }); + writeJSONFile('npm-shrinkwrap.json.ember-try', { originalNpmShrinkWrap: true }); + writeJSONFile('npm-shrinkwrap.json', { originalNpmShrinkWrap: false }); + writeJSONFile('package-lock.json.ember-try', { originalPackageLock: true }); + writeJSONFile('package-lock.json', { originalPackageLock: false }); + return new NpmAdapter({ cwd: tmpdir })._restoreOriginalDependencies().then(() => { + assertFileContainsJSON(path.join(tmpdir, 'package.json'), { originalPackageJSON: true }); + assertFileContainsJSON(path.join(tmpdir, 'node_modules/prove-it.json'), { originalNodeModules: true }); + assertFileContainsJSON(path.join(tmpdir, 'yarn.lock'), { originalYarnLock: true }); + assertFileContainsJSON(path.join(tmpdir, 'npm-shrinkwrap.json'), { originalNpmShrinkWrap: true }); + assertFileContainsJSON(path.join(tmpdir, 'package-lock.json'), { originalPackageLock: true }); + }); + }); }); describe('#_packageJSONForDependencySet', () => { diff --git a/test/dependency-manager-adapters/workspace-adapter-test.js b/test/dependency-manager-adapters/workspace-adapter-test.js index e145aa33..090c487e 100644 --- a/test/dependency-manager-adapters/workspace-adapter-test.js +++ b/test/dependency-manager-adapters/workspace-adapter-test.js @@ -44,6 +44,26 @@ describe('workspaceAdapter', () => { }); }); + it('backs up the yarn.lock file, npm-shrinkwrap.json and package-lock.json if they exist', () => { + fs.ensureDirSync('packages/test/node_modules'); + + writeJSONFile('packages/test/package.json', { originalPackageJSON: true }); + writeJSONFile('packages/test/node_modules/prove-it.json', { originalNodeModules: true }); + writeJSONFile('packages/test/yarn.lock', { originalYarnLock: true }); + writeJSONFile('packages/test/npm-shrinkwrap.json', { originalNpmShrinkWrap: true }); + writeJSONFile('packages/test/package-lock.json', { originalPackageLock: true }); + return new WorkspaceAdapter({ + cwd: tmpdir, + useYarnCommand: true, + }).setup().then(() => { + assertFileContainsJSON(path.join(tmpdir, 'packages/test/package.json.ember-try'), { originalPackageJSON: true }); + assertFileContainsJSON(path.join(tmpdir, 'packages/test/.node_modules.ember-try/prove-it.json'), { originalNodeModules: true }); + assertFileContainsJSON(path.join(tmpdir, 'packages/test/yarn.lock.ember-try'), { originalYarnLock: true }); + assertFileContainsJSON(path.join(tmpdir, 'packages/test/npm-shrinkwrap.json.ember-try'), { originalNpmShrinkWrap: true }); + assertFileContainsJSON(path.join(tmpdir, 'packages/test/package-lock.json.ember-try'), { originalPackageLock: true }); + }); + }); + it('throws an error if workspaces are not present', () => { writeJSONFile('package.json', { name: 'a-test-project-with-workspaces' @@ -109,6 +129,41 @@ describe('workspaceAdapter', () => { expect(runCount).to.equal(1, 'Only yarn install should run with manager options'); }); }); + + it('uses buildManagerOptions to override defaults for yarn commands', () => { + let runCount = 0; + let stubbedRun = generateMockRun([{ + command: 'yarn install --flat', + callback() { + runCount++; + return RSVP.resolve(); + }, + }], { allowPassthrough: false }); + + return new WorkspaceAdapter({ + cwd: tmpdir, + run: stubbedRun, + useYarnCommand: true, + buildManagerOptions: function() { + return ['--flat']; + }, + })._install().then(() => { + expect(runCount).to.equal(1, 'Only yarn install should run with buildManagerOptions'); + }); + }); + + it('throws an error if buildManagerOptions does not return an array', () => { + expect(() => { + new WorkspaceAdapter({ + cwd: tmpdir, + run: () => {}, + useYarnCommand: true, + buildManagerOptions: function() { + return 'string'; + }, + })._install() + }).to.throw(/buildManagerOptions must return an array of options/); + }); }); }); @@ -135,6 +190,38 @@ describe('workspaceAdapter', () => { assertFileContainsJSON(path.join(tmpdir, 'packages/test/node_modules/prove-it.json'), { originalNodeModules: true }); }); }); + + it('replaces the yarn.lock, npm-shrinkwrap.json and package-lock.json with the backed up version if they exist', () => { + fs.ensureDirSync('packages/test/node_modules'); + + writeJSONFile('packages/test/package.json', { originalPackageJSON: true }); + writeJSONFile('packages/test/node_modules/prove-it.json', { originalNodeModules: true }); + writeJSONFile('packages/test/yarn.lock', { originalYarnLock: true }); + writeJSONFile('packages/test/npm-shrinkwrap.json', { originalNpmShrinkWrap: true }); + writeJSONFile('packages/test/package-lock.json', { originalPackageLock: true }); + + let workspaceAdapter = new WorkspaceAdapter({ + cwd: tmpdir, + useYarnCommand: true, + run: () => Promise.resolve(), + }); + + return workspaceAdapter.setup().then(() => { + writeJSONFile('packages/test/package.json', { originalPackageJSON: false }); + writeJSONFile('packages/test/node_modules/prove-it.json', { originalNodeModules: true }); + writeJSONFile('packages/test/yarn.lock', { originalYarnLock: false }); + writeJSONFile('packages/test/npm-shrinkwrap.json', { originalNpmShrinkWrap: false }); + writeJSONFile('packages/test/package-lock.json', { originalPackageLock: false }); + + return workspaceAdapter.cleanup(); + }).then(() => { + assertFileContainsJSON(path.join(tmpdir, 'packages/test/package.json'), { originalPackageJSON: true }); + assertFileContainsJSON(path.join(tmpdir, 'packages/test/node_modules/prove-it.json'), { originalNodeModules: true }); + assertFileContainsJSON(path.join(tmpdir, 'packages/test/yarn.lock'), { originalYarnLock: true }); + assertFileContainsJSON(path.join(tmpdir, 'packages/test/npm-shrinkwrap.json'), { originalNpmShrinkWrap: true }); + assertFileContainsJSON(path.join(tmpdir, 'packages/test/package-lock.json'), { originalPackageLock: true }); + }); + }); }); describe('#changeToDependencySet', () => { @@ -222,5 +309,30 @@ describe('workspaceAdapter', () => { }); }); }); + + it('passes the scenario into buildManagerOptions to run with the correct options', () => { + let runCount = 0; + workspaceAdapter.run = generateMockRun([{ + command: 'yarn install --flat', + callback(command, args, opts) { + runCount++; + expect(opts).to.have.property('cwd', tmpdir); + return RSVP.resolve(); + }, + }], { allowPassthrough: false }); + workspaceAdapter.buildManagerOptions = function(scenario) { + if (scenario.name === 'scenario1') { + return ['--flat'] + } + + return []; + }; + + return workspaceAdapter.changeToDependencySet({ + name: 'scenario1' + }).then(() => { + expect(runCount).to.equal(1, 'yarn install should run with correct options from buildManagerOptions'); + }); + }); }); });