diff --git a/lib/packagers/yarn.js b/lib/packagers/yarn.js index 2c2b45f4cb..04374a231b 100644 --- a/lib/packagers/yarn.js +++ b/lib/packagers/yarn.js @@ -1,7 +1,7 @@ 'use strict'; /** * Yarn packager. - * + * * Yarn specific packagerOptions (default): * flat (false) - Use --flat with install * ignoreScripts (false) - Do not execute scripts during install @@ -12,7 +12,8 @@ const BbPromise = require('bluebird'); const Utils = require('../utils'); class Yarn { - static get lockfileName() { // eslint-disable-line lodash/prefer-constant + // eslint-disable-next-line lodash/prefer-constant + static get lockfileName() { return 'yarn.lock'; } @@ -20,18 +21,14 @@ class Yarn { return ['resolutions']; } - static get mustCopyModules() { // eslint-disable-line lodash/prefer-constant + // eslint-disable-next-line lodash/prefer-constant + static get mustCopyModules() { return false; } static getProdDependencies(cwd, depth) { const command = /^win/.test(process.platform) ? 'yarn.cmd' : 'yarn'; - const args = [ - 'list', - `--depth=${depth || 1}`, - '--json', - '--production' - ]; + const args = [ 'list', `--depth=${depth || 1}`, '--json', '--production' ]; // If we need to ignore some errors add them here const ignoredYarnErrors = []; @@ -39,55 +36,75 @@ class Yarn { return Utils.spawnProcess(command, args, { cwd: cwd }) - .catch(err => { - if (err instanceof Utils.SpawnError) { - // Only exit with an error if we have critical npm errors for 2nd level inside - const errors = _.split(err.stderr, '\n'); - const failed = _.reduce(errors, (failed, error) => { - if (failed) { - return true; + .catch((err) => { + if (err instanceof Utils.SpawnError) { + // Only exit with an error if we have critical npm errors for 2nd level inside + const errors = _.split(err.stderr, '\n'); + const failed = _.reduce( + errors, + (failed, error) => { + if (failed) { + return true; + } + return ( + !_.isEmpty(error) && + !_.some(ignoredYarnErrors, (ignoredError) => + _.startsWith(error, `npm ERR! ${ignoredError.npmError}`) + ) + ); + }, + false + ); + + if (!failed && !_.isEmpty(err.stdout)) { + return BbPromise.resolve({ stdout: err.stdout }); } - return !_.isEmpty(error) && !_.some(ignoredYarnErrors, ignoredError => _.startsWith(error, `npm ERR! ${ignoredError.npmError}`)); - }, false); - - if (!failed && !_.isEmpty(err.stdout)) { - return BbPromise.resolve({ stdout: err.stdout }); } - } - return BbPromise.reject(err); - }) - .then(processOutput => processOutput.stdout) - .then(depJson => BbPromise.try(() => JSON.parse(depJson))) - .then(parsedTree => { - const convertTrees = trees => _.reduce(trees, (__, tree) => { - const splitModule = _.split(tree.name, '@'); - // If we have a scoped module we have to re-add the @ - if (_.startsWith(tree.name, '@')) { - splitModule.splice(0, 1); - splitModule[0] = '@' + splitModule[0]; - } - __[_.first(splitModule)] = { - version: _.join(_.tail(splitModule), '@'), - dependencies: convertTrees(tree.children) + return BbPromise.reject(err); + }) + .then((processOutput) => processOutput.stdout) + .then((stdout) => + BbPromise.try(() => { + const lines = Utils.splitLines(stdout); + const parsedLines = _.map(lines, Utils.safeJsonParse); + return _.find(parsedLines, (line) => line && line.type === 'tree'); + }) + ) + .then((parsedTree) => { + const convertTrees = (trees) => + _.reduce( + trees, + (__, tree) => { + const splitModule = _.split(tree.name, '@'); + // If we have a scoped module we have to re-add the @ + if (_.startsWith(tree.name, '@')) { + splitModule.splice(0, 1); + splitModule[0] = '@' + splitModule[0]; + } + __[_.first(splitModule)] = { + version: _.join(_.tail(splitModule), '@'), + dependencies: convertTrees(tree.children) + }; + return __; + }, + {} + ); + + const trees = _.get(parsedTree, 'data.trees', []); + const result = { + problems: [], + dependencies: convertTrees(trees) }; - return __; - }, {}); - - const trees = _.get(parsedTree, 'data.trees', []); - const result = { - problems: [], - dependencies: convertTrees(trees) - }; - return result; - }); + return result; + }); } static rebaseLockfile(pathToPackageRoot, lockfile) { const fileVersionMatcher = /[^"/]@(?:file:)?((?:\.\/|\.\.\/).*?)[":,]/gm; const replacements = []; let match; - + // Detect all references and create replacement line strings while ((match = fileVersionMatcher.exec(lockfile)) !== null) { replacements.push({ @@ -97,26 +114,25 @@ class Yarn { } // Replace all lines in lockfile - return _.reduce(replacements, (__, replacement) => { - return _.replace(__, replacement.oldRef, replacement.newRef); - }, lockfile); + return _.reduce( + replacements, + (__, replacement) => { + return _.replace(__, replacement.oldRef, replacement.newRef); + }, + lockfile + ); } static install(cwd, packagerOptions) { const command = /^win/.test(process.platform) ? 'yarn.cmd' : 'yarn'; - const args = [ - 'install', - '--frozen-lockfile', - '--non-interactive' - ]; + const args = [ 'install', '--frozen-lockfile', '--non-interactive' ]; // Convert supported packagerOptions if (packagerOptions.ignoreScripts) { args.push('--ignore-scripts'); } - return Utils.spawnProcess(command, args, { cwd }) - .return(); + return Utils.spawnProcess(command, args, { cwd }).return(); } // "Yarn install" prunes automatically @@ -126,15 +142,11 @@ class Yarn { static runScripts(cwd, scriptNames) { const command = /^win/.test(process.platform) ? 'yarn.cmd' : 'yarn'; - return BbPromise.mapSeries(scriptNames, scriptName => { - const args = [ - 'run', - scriptName - ]; + return BbPromise.mapSeries(scriptNames, (scriptName) => { + const args = [ 'run', scriptName ]; return Utils.spawnProcess(command, args, { cwd }); - }) - .return(); + }).return(); } } diff --git a/lib/packagers/yarn.test.js b/lib/packagers/yarn.test.js index d77ac154df..ad9601fb08 100644 --- a/lib/packagers/yarn.test.js +++ b/lib/packagers/yarn.test.js @@ -48,8 +48,9 @@ describe('yarn', () => { describe('getProdDependencies', () => { it('should use yarn list', () => { Utils.spawnProcess.returns(BbPromise.resolve({ stdout: '{}', stderr: '' })); - return expect(yarnModule.getProdDependencies('myPath', 1)).to.be.fulfilled - .then(result => { + return expect( + yarnModule.getProdDependencies('myPath', 1) + ).to.be.fulfilled.then((result) => { expect(result).to.be.an('object'); expect(Utils.spawnProcess).to.have.been.calledOnce, expect(Utils.spawnProcess.firstCall).to.have.been.calledWith( @@ -62,14 +63,22 @@ describe('yarn', () => { }); it('should transform yarn trees to npm dependencies', () => { - const testYarnResult = `{"type":"tree","data":{"type":"list","trees":[ - {"name":"archiver@2.1.1","children":[],"hint":null,"color":"bold", - "depth":0},{"name":"bluebird@3.5.1","children":[],"hint":null,"color": - "bold","depth":0},{"name":"fs-extra@4.0.3","children":[],"hint":null, - "color":"bold","depth":0},{"name":"mkdirp@0.5.1","children":[{"name": - "minimist@0.0.8","children":[],"hint":null,"color":"bold","depth":0}], - "hint":null,"color":null,"depth":0},{"name":"@sls/webpack@1.0.0", - "children":[],"hint":null,"color":"bold","depth":0}]}}`; + const testYarnResult = + '{"type":"activityStart","data":{"id":0}}\n' + + '{"type":"activityTick","data":{"id":0,"name":"archiver@^2.1.1"}}\n' + + '{"type":"activityTick","data":{"id":0,"name":"bluebird@^3.5.1"}}\n' + + '{"type":"activityTick","data":{"id":0,"name":"fs-extra@^4.0.3"}}\n' + + '{"type":"activityTick","data":{"id":0,"name":"mkdirp@^0.5.1"}}\n' + + '{"type":"activityTick","data":{"id":0,"name":"minimist@^0.0.8"}}\n' + + '{"type":"activityTick","data":{"id":0,"name":"@sls/webpack@^1.0.0"}}\n' + + '{"type":"tree","data":{"type":"list","trees":[' + + '{"name":"archiver@2.1.1","children":[],"hint":null,"color":"bold",' + + '"depth":0},{"name":"bluebird@3.5.1","children":[],"hint":null,"color":' + + '"bold","depth":0},{"name":"fs-extra@4.0.3","children":[],"hint":null,' + + '"color":"bold","depth":0},{"name":"mkdirp@0.5.1","children":[{"name":' + + '"minimist@0.0.8","children":[],"hint":null,"color":"bold","depth":0}],' + + '"hint":null,"color":null,"depth":0},{"name":"@sls/webpack@1.0.0",' + + '"children":[],"hint":null,"color":"bold","depth":0}]}}\n'; const expectedResult = { problems: [], dependencies: { @@ -97,29 +106,43 @@ describe('yarn', () => { '@sls/webpack': { version: '1.0.0', dependencies: {} - }, + } } }; - Utils.spawnProcess.returns(BbPromise.resolve({ stdout: testYarnResult, stderr: '' })); - return expect(yarnModule.getProdDependencies('myPath', 1)).to.be.fulfilled - .then(result => { + Utils.spawnProcess.returns( + BbPromise.resolve({ stdout: testYarnResult, stderr: '' }) + ); + return expect( + yarnModule.getProdDependencies('myPath', 1) + ).to.be.fulfilled.then((result) => { expect(result).to.deep.equal(expectedResult); return null; }); }); it('should reject on critical yarn errors', () => { - Utils.spawnProcess.returns(BbPromise.reject(new Utils.SpawnError('Exited with code 1', '', 'Yarn failed.\nerror Could not find module.'))); - return expect(yarnModule.getProdDependencies('myPath', 1)).to.be.rejectedWith('Exited with code 1'); + Utils.spawnProcess.returns( + BbPromise.reject( + new Utils.SpawnError( + 'Exited with code 1', + '', + 'Yarn failed.\nerror Could not find module.' + ) + ) + ); + return expect( + yarnModule.getProdDependencies('myPath', 1) + ).to.be.rejectedWith('Exited with code 1'); }); - }); describe('rebaseLockfile', () => { it('should return the original lockfile', () => { const testContent = 'eugfogfoigqwoeifgoqwhhacvaisvciuviwefvc'; const testContent2 = 'eugfogfoigqwoeifgoqwhhacvaisvciuviwefvc'; - expect(yarnModule.rebaseLockfile('.', testContent)).to.equal(testContent2); + expect(yarnModule.rebaseLockfile('.', testContent)).to.equal( + testContent2 + ); }); it('should rebase file references', () => { @@ -198,38 +221,51 @@ describe('yarn', () => { version "5.5.3" resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.5.3.tgz#f473dd47e0277a08e28e9bec5aeeb04751f0b8c9" `; - - expect(yarnModule.rebaseLockfile('../../project', testContent)).to.equal(expectedContent); + + expect(yarnModule.rebaseLockfile('../../project', testContent)).to.equal( + expectedContent + ); }); }); describe('install', () => { it('should use yarn install', () => { - Utils.spawnProcess.returns(BbPromise.resolve({ stdout: 'installed successfully', stderr: '' })); - return expect(yarnModule.install('myPath', {})).to.be.fulfilled - .then(result => { - expect(result).to.be.undefined; - expect(Utils.spawnProcess).to.have.been.calledOnce; - expect(Utils.spawnProcess).to.have.been.calledWithExactly( - sinon.match(/^yarn/), - [ 'install', '--frozen-lockfile', '--non-interactive' ], - { - cwd: 'myPath' - } - ); - return null; - }); + Utils.spawnProcess.returns( + BbPromise.resolve({ stdout: 'installed successfully', stderr: '' }) + ); + return expect(yarnModule.install('myPath', {})).to.be.fulfilled.then( + (result) => { + expect(result).to.be.undefined; + expect(Utils.spawnProcess).to.have.been.calledOnce; + expect(Utils.spawnProcess).to.have.been.calledWithExactly( + sinon.match(/^yarn/), + [ 'install', '--frozen-lockfile', '--non-interactive' ], + { + cwd: 'myPath' + } + ); + return null; + } + ); }); it('should use ignoreScripts option', () => { - Utils.spawnProcess.returns(BbPromise.resolve({ stdout: 'installed successfully', stderr: '' })); - return expect(yarnModule.install('myPath', { ignoreScripts: true })).to.be.fulfilled - .then(result => { + Utils.spawnProcess.returns( + BbPromise.resolve({ stdout: 'installed successfully', stderr: '' }) + ); + return expect( + yarnModule.install('myPath', { ignoreScripts: true }) + ).to.be.fulfilled.then((result) => { expect(result).to.be.undefined; expect(Utils.spawnProcess).to.have.been.calledOnce; expect(Utils.spawnProcess).to.have.been.calledWithExactly( sinon.match(/^yarn/), - [ 'install', '--frozen-lockfile', '--non-interactive', '--ignore-scripts' ], + [ + 'install', + '--frozen-lockfile', + '--non-interactive', + '--ignore-scripts' + ], { cwd: 'myPath' } @@ -243,7 +279,9 @@ describe('yarn', () => { let installStub; before(() => { - installStub = sandbox.stub(yarnModule, 'install').returns(BbPromise.resolve()); + installStub = sandbox + .stub(yarnModule, 'install') + .returns(BbPromise.resolve()); }); after(() => { @@ -251,8 +289,7 @@ describe('yarn', () => { }); it('should call install', () => { - return expect(yarnModule.prune('myPath', {})).to.be.fulfilled - .then(() => { + return expect(yarnModule.prune('myPath', {})).to.be.fulfilled.then(() => { expect(installStub).to.have.been.calledOnce; expect(installStub).to.have.been.calledWithExactly('myPath', {}); return null; @@ -262,9 +299,12 @@ describe('yarn', () => { describe('runScripts', () => { it('should use yarn run for the given scripts', () => { - Utils.spawnProcess.returns(BbPromise.resolve({ stdout: 'success', stderr: '' })); - return expect(yarnModule.runScripts('myPath', [ 's1', 's2' ])).to.be.fulfilled - .then(result => { + Utils.spawnProcess.returns( + BbPromise.resolve({ stdout: 'success', stderr: '' }) + ); + return expect( + yarnModule.runScripts('myPath', [ 's1', 's2' ]) + ).to.be.fulfilled.then((result) => { expect(result).to.be.undefined; expect(Utils.spawnProcess).to.have.been.calledTwice; expect(Utils.spawnProcess.firstCall).to.have.been.calledWithExactly( @@ -285,5 +325,4 @@ describe('yarn', () => { }); }); }); - -}); \ No newline at end of file +}); diff --git a/lib/utils.js b/lib/utils.js index 1db482d3bc..1d00ec3aa0 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -10,20 +10,32 @@ function guid() { .toString(16) .substring(1); } - return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4(); + return ( + s4() + + s4() + + '-' + + s4() + + '-' + + s4() + + '-' + + s4() + + '-' + + s4() + + s4() + + s4() + ); } /** * Remove the specified module from the require cache. - * @param {string} moduleName + * @param {string} moduleName */ function purgeCache(moduleName) { - return searchAndProcessCache(moduleName, function (mod) { + return searchAndProcessCache(moduleName, function(mod) { delete require.cache[mod.id]; - }) - .then(() => { + }).then(() => { _.forEach(_.keys(module.constructor._pathCache), function(cacheKey) { - if (cacheKey.indexOf(moduleName)>0) { + if (cacheKey.indexOf(moduleName) > 0) { delete module.constructor._pathCache[cacheKey]; } }); @@ -34,7 +46,7 @@ function purgeCache(moduleName) { function searchAndProcessCache(moduleName, processor) { let mod_src = require.resolve(moduleName); const visitedModules = []; - if (mod_src && ((mod_src = require.cache[mod_src]) !== undefined)) { + if (mod_src && (mod_src = require.cache[mod_src]) !== undefined) { const modStack = [mod_src]; while (!_.isEmpty(modStack)) { @@ -78,18 +90,24 @@ function spawnProcess(command, args, options) { child.stdout.setEncoding('utf8'); child.stderr.setEncoding('utf8'); // Listen to stream events - child.stdout.on('data', data => { + child.stdout.on('data', (data) => { stdout += data; }); - child.stderr.on('data', data => { + child.stderr.on('data', (data) => { stderr += data; }); - child.on('error', err => { + child.on('error', (err) => { reject(err); }); - child.on('close', exitCode => { + child.on('close', (exitCode) => { if (exitCode !== 0) { - reject(new SpawnError(`${command} ${_.join(args, ' ')} failed with code ${exitCode}`, stdout, stderr)); + reject( + new SpawnError( + `${command} ${_.join(args, ' ')} failed with code ${exitCode}`, + stdout, + stderr, + ), + ); } else { resolve({ stdout, stderr }); } @@ -97,10 +115,24 @@ function spawnProcess(command, args, options) { }); } +function safeJsonParse(str) { + try { + return JSON.parse(str); + } catch (e) { + return null; + } +} + +function splitLines(str) { + return _.split(str, /\r?\n/); +} + module.exports = { guid, purgeCache, searchAndProcessCache, SpawnError, spawnProcess, + safeJsonParse, + splitLines, };