diff --git a/__tests__/commands/add.js b/__tests__/commands/add.js index 9da59ffa4b..04727bd339 100644 --- a/__tests__/commands/add.js +++ b/__tests__/commands/add.js @@ -321,7 +321,7 @@ test.concurrent('add with offline mirror', (): Promise => { ).toBeGreaterThanOrEqual(0); const rawLockfile = await fs.readFile(path.join(config.cwd, constants.LOCKFILE_FILENAME)); - const lockfile = parse(rawLockfile); + const {object: lockfile} = parse(rawLockfile); expect(lockfile['is-array@^1.0.1']['resolved']).toEqual( 'https://registry.yarnpkg.com/is-array/-/is-array-1.0.1.tgz#e9850cc2cc860c3bc0977e84ccf0dd464584279a', @@ -384,7 +384,7 @@ test.concurrent('install with --save and without offline mirror', (): Promise { const rawLockfile = await fs.readFile(path.join(config.cwd, 'yarn.lock')); - const lockfile = parse(rawLockfile); + const {object: lockfile} = parse(rawLockfile); expect(lockfile['mime-types@2.1.14'].resolved).toEqual( 'https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.14.tgz#f7ef7d97583fcaf3b7d282b6f8b5679dab1e94ee', ); @@ -564,7 +564,7 @@ test.concurrent('offline mirror can be enabled from parent dir, with merging of }; return runInstall({}, fixture, async (config, reporter) => { const rawLockfile = await fs.readFile(path.join(config.cwd, 'yarn.lock')); - const lockfile = parse(rawLockfile); + const {object: lockfile} = parse(rawLockfile); expect(lockfile['mime-types@2.1.14'].resolved).toEqual( 'https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.14.tgz#f7ef7d97583fcaf3b7d282b6f8b5679dab1e94ee', ); @@ -579,7 +579,7 @@ test.concurrent('offline mirror can be disabled locally', (): Promise => { }; return runInstall({}, fixture, async (config, reporter) => { const rawLockfile = await fs.readFile(path.join(config.cwd, 'yarn.lock')); - const lockfile = parse(rawLockfile); + const {object: lockfile} = parse(rawLockfile); expect(lockfile['mime-types@2.1.14'].resolved).toEqual( 'https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.14.tgz#f7ef7d97583fcaf3b7d282b6f8b5679dab1e94ee', ); diff --git a/__tests__/lockfile.js b/__tests__/lockfile.js index 3f0ddad6f2..3d7a775523 100644 --- a/__tests__/lockfile.js +++ b/__tests__/lockfile.js @@ -11,19 +11,19 @@ const objs = [{foo: 'bar'}, {foo: {}}, {foo: 'foo', bar: 'bar'}, {foo: 5}]; let i = 0; for (const obj of objs) { test(`parse/stringify ${++i}`, () => { - expect(parse(stringify(obj))).toEqual(nullify(obj)); + expect(parse(stringify(obj)).object).toEqual(nullify(obj)); }); } test('parse', () => { - expect(parse('foo "bar"')).toEqual(nullify({foo: 'bar'})); - expect(parse('"foo" "bar"')).toEqual(nullify({foo: 'bar'})); - expect(parse('foo "bar"')).toEqual(nullify({foo: 'bar'})); - - expect(parse(`foo:\n bar "bar"`)).toEqual(nullify({foo: {bar: 'bar'}})); - expect(parse(`foo:\n bar:\n foo "bar"`)).toEqual(nullify({foo: {bar: {}, foo: 'bar'}})); - expect(parse(`foo:\n bar:\n foo "bar"`)).toEqual(nullify({foo: {bar: {foo: 'bar'}}})); - expect(parse('foo:\n bar:\n yes no\nbar:\n yes no')).toEqual( + expect(parse('foo "bar"').object).toEqual(nullify({foo: 'bar'})); + expect(parse('"foo" "bar"').object).toEqual(nullify({foo: 'bar'})); + expect(parse('foo "bar"').object).toEqual(nullify({foo: 'bar'})); + + expect(parse(`foo:\n bar "bar"`).object).toEqual(nullify({foo: {bar: 'bar'}})); + expect(parse(`foo:\n bar:\n foo "bar"`).object).toEqual(nullify({foo: {bar: {}, foo: 'bar'}})); + expect(parse(`foo:\n bar:\n foo "bar"`).object).toEqual(nullify({foo: {bar: {foo: 'bar'}}})); + expect(parse('foo:\n bar:\n yes no\nbar:\n yes no').object).toEqual( nullify({ foo: { bar: { @@ -193,3 +193,44 @@ test('Lockfile.getLockfile (sorting)', () => { expect(actual).toEqual(expected); }); + +test('parse merge conflicts', () => { + const file = ` +a: + no "yes" + +<<<<<<< HEAD +b: + foo "bar" +======= +c: + bar "foo" +>>>>>>> branch-a + +d: + yes "no" +`; + + const {type, object} = parse(file); + expect(type).toEqual('merge'); + expect(object.a.no).toEqual('yes'); + expect(object.b.foo).toEqual('bar'); + expect(object.c.bar).toEqual('foo'); + expect(object.d.yes).toEqual('no'); +}); + +test('parse merge conflict fail', () => { + const file = ` +<<<<<<< HEAD +b: + foo: "bar" +======= +c: + bar "foo" +>>>>>>> branch-a +`; + + const {type, object} = parse(file); + expect(type).toEqual('conflict'); + expect(Object.keys(object).length).toEqual(0); +}); diff --git a/src/lockfile/parse.js b/src/lockfile/parse.js index 9fe0a73826..9354f075e9 100644 --- a/src/lockfile/parse.js +++ b/src/lockfile/parse.js @@ -314,9 +314,101 @@ export class Parser { } } -export default function(str: string, fileLoc: string = 'lockfile'): Object { - str = stripBOM(str); +const MERGE_CONFLICT_START = '<<<<<<<'; +const MERGE_CONFLICT_SEP = '======='; +const MERGE_CONFLICT_END = '>>>>>>>'; + +/** + * Extract the two versions of the lockfile from a merge conflict. + */ + +export function extractConflictVariants(str: string): Array { + const variants: Array> = [[], []]; + const lines = str.split(/\n/g); + + while (lines.length) { + const line = lines.shift(); + if (line.startsWith(MERGE_CONFLICT_START)) { + // get the first variant + while (lines.length) { + const line = lines.shift(); + if (line === MERGE_CONFLICT_SEP) { + break; + } else { + variants[0].push(line); + } + } + + // get the second variant + while (lines.length) { + const line = lines.shift(); + if (line.startsWith(MERGE_CONFLICT_END)) { + break; + } else { + variants[1].push(line); + } + } + } else { + variants[0].push(line); + variants[1].push(line); + } + } + + return variants.map(lines => lines.join('\n')); +} + +/** + * Check if a lockfile has merge conflicts. + */ + +export function hasMergeConflicts(str: string): boolean { + return str.includes(MERGE_CONFLICT_START); +} + +/** + * Parse the lockfile. + */ + +function parse(str: string, fileLoc: string): Object { const parser = new Parser(str, fileLoc); parser.next(); return parser.parse(); } + +type ParseResult = { + type: 'merge' | 'none' | 'conflict', + object: Object, +}; + +/** + * Parse and merge the two variants in a conflicted lockfile. + */ + +function parseWithConflict(str: string, fileLoc: string): ParseResult { + const variants = extractConflictVariants(str); + + try { + const obj = Object.assign( + {}, + parse(variants[0], fileLoc), + parse(variants[1], fileLoc), + ); + return {type: 'merge', object: obj}; + } catch (err) { + if (err instanceof SyntaxError) { + return {type: 'conflict', object: {}}; + } else { + throw err; + } + } +} + +export default function(str: string, fileLoc: string = 'lockfile'): ParseResult { + str = stripBOM(str); + + if (hasMergeConflicts(str)) { + return parseWithConflict(str, fileLoc); + } else { + return {type: 'none', object: parse(str, fileLoc)}; + } +} diff --git a/src/lockfile/wrapper.js b/src/lockfile/wrapper.js index e328bc5cf2..338dc29683 100644 --- a/src/lockfile/wrapper.js +++ b/src/lockfile/wrapper.js @@ -95,7 +95,17 @@ export default class Lockfile { if (await fs.exists(lockfileLoc)) { rawLockfile = await fs.readFile(lockfileLoc); - lockfile = parse(rawLockfile, lockfileLoc); + const lockResult = parse(rawLockfile, lockfileLoc); + + if (reporter) { + if (lockResult.type === 'merge') { + reporter.info(reporter.lang('lockfileMerged')); + } else if (lockResult.type === 'conflict') { + reporter.warn(reporter.lang('lockfileConflict')); + } + } + + lockfile = lockResult.object; } else { if (reporter) { reporter.info(reporter.lang('noLockfileFound')); diff --git a/src/rc.js b/src/rc.js index e3894cab0b..3eeba66d7f 100644 --- a/src/rc.js +++ b/src/rc.js @@ -12,7 +12,7 @@ let rcArgsCache; const buildRcConf = () => rcUtil.findRc('yarn', (fileText, filePath) => { - const values = parse(fileText, 'yarnrc'); + const {object: values} = parse(fileText, 'yarnrc'); const keys = Object.keys(values); for (const key of keys) { diff --git a/src/registries/yarn-registry.js b/src/registries/yarn-registry.js index e4633ebd85..0f79fae2a2 100644 --- a/src/registries/yarn-registry.js +++ b/src/registries/yarn-registry.js @@ -72,7 +72,7 @@ export default class YarnRegistry extends NpmRegistry { async loadConfig(): Promise { for (const [isHome, loc, file] of await this.getPossibleConfigLocations('.yarnrc', this.reporter)) { - const config = parse(file, loc); + const {object: config} = parse(file, loc); if (isHome) { this.homeConfig = config; diff --git a/src/reporters/lang/en.js b/src/reporters/lang/en.js index 2c9a4c928f..b097889375 100644 --- a/src/reporters/lang/en.js +++ b/src/reporters/lang/en.js @@ -79,6 +79,8 @@ const messages = { couldntFindManifestIn: "Couldn't find manifest in $0.", shrinkwrapWarning: 'npm-shrinkwrap.json found. This will not be updated or respected. See https://yarnpkg.com/en/docs/migrating-from-npm for more information.', lockfileOutdated: 'Outdated lockfile. Please run `yarn install` and try again.', + lockfileMerged: 'Merge conflict detected in yarn.lock and successfully merged.', + lockfileConflict: 'A merge conflict was found in yarn.lock but it could not be successfully merged, regenerating yarn.lock from scratch.', ignoredScripts: 'Ignored scripts due to flag.', missingAddDependencies: 'Missing list of packages to add to your project.', yesWarning: 'The yes flag has been set. This will automatically answer yes to all questions which may have security implications.',