Permalink
Cannot retrieve contributors at this time
Join GitHub today
GitHub is home to over 50 million developers working together to host and review code, manage projects, and build software together.
Sign up
Fetching contributors…
| /* @flow */ | |
| /* eslint quotes: 0 */ | |
| import util from 'util'; | |
| import invariant from 'invariant'; | |
| import stripBOM from 'strip-bom'; | |
| import {LOCKFILE_VERSION} from '../constants.js'; | |
| import {MessageError} from '../errors.js'; | |
| import map from '../util/map.js'; | |
| const {safeLoad, FAILSAFE_SCHEMA} = require('js-yaml'); | |
| type Token = { | |
| line: number, | |
| col: number, | |
| type: string, | |
| value: boolean | number | string | void, | |
| }; | |
| export type ParseResultType = 'merge' | 'success' | 'conflict'; | |
| export type ParseResult = { | |
| type: ParseResultType, | |
| object: Object, | |
| }; | |
| const VERSION_REGEX = /^yarn lockfile v(\d+)$/; | |
| const TOKEN_TYPES = { | |
| boolean: 'BOOLEAN', | |
| string: 'STRING', | |
| identifier: 'IDENTIFIER', | |
| eof: 'EOF', | |
| colon: 'COLON', | |
| newline: 'NEWLINE', | |
| comment: 'COMMENT', | |
| indent: 'INDENT', | |
| invalid: 'INVALID', | |
| number: 'NUMBER', | |
| comma: 'COMMA', | |
| }; | |
| const VALID_PROP_VALUE_TOKENS = [TOKEN_TYPES.boolean, TOKEN_TYPES.string, TOKEN_TYPES.number]; | |
| function isValidPropValueToken(token): boolean { | |
| return VALID_PROP_VALUE_TOKENS.indexOf(token.type) >= 0; | |
| } | |
| function* tokenise(input: string): Iterator<Token> { | |
| let lastNewline = false; | |
| let line = 1; | |
| let col = 0; | |
| function buildToken(type, value): Token { | |
| return {line, col, type, value}; | |
| } | |
| while (input.length) { | |
| let chop = 0; | |
| if (input[0] === '\n' || input[0] === '\r') { | |
| chop++; | |
| // If this is a \r\n line, ignore both chars but only add one new line | |
| if (input[1] === '\n') { | |
| chop++; | |
| } | |
| line++; | |
| col = 0; | |
| yield buildToken(TOKEN_TYPES.newline); | |
| } else if (input[0] === '#') { | |
| chop++; | |
| let nextNewline = input.indexOf('\n', chop); | |
| if (nextNewline === -1) { | |
| nextNewline = input.length; | |
| } | |
| const val = input.substring(chop, nextNewline); | |
| chop = nextNewline; | |
| yield buildToken(TOKEN_TYPES.comment, val); | |
| } else if (input[0] === ' ') { | |
| if (lastNewline) { | |
| let indentSize = 1; | |
| for (let i = 1; input[i] === ' '; i++) { | |
| indentSize++; | |
| } | |
| if (indentSize % 2) { | |
| throw new TypeError('Invalid number of spaces'); | |
| } else { | |
| chop = indentSize; | |
| yield buildToken(TOKEN_TYPES.indent, indentSize / 2); | |
| } | |
| } else { | |
| chop++; | |
| } | |
| } else if (input[0] === '"') { | |
| let i = 1; | |
| for (; i < input.length; i++) { | |
| if (input[i] === '"') { | |
| const isEscaped = input[i - 1] === '\\' && input[i - 2] !== '\\'; | |
| if (!isEscaped) { | |
| i++; | |
| break; | |
| } | |
| } | |
| } | |
| const val = input.substring(0, i); | |
| chop = i; | |
| try { | |
| yield buildToken(TOKEN_TYPES.string, JSON.parse(val)); | |
| } catch (err) { | |
| if (err instanceof SyntaxError) { | |
| yield buildToken(TOKEN_TYPES.invalid); | |
| } else { | |
| throw err; | |
| } | |
| } | |
| } else if (/^[0-9]/.test(input)) { | |
| const val = /^[0-9]+/.exec(input)[0]; | |
| chop = val.length; | |
| yield buildToken(TOKEN_TYPES.number, +val); | |
| } else if (/^true/.test(input)) { | |
| yield buildToken(TOKEN_TYPES.boolean, true); | |
| chop = 4; | |
| } else if (/^false/.test(input)) { | |
| yield buildToken(TOKEN_TYPES.boolean, false); | |
| chop = 5; | |
| } else if (input[0] === ':') { | |
| yield buildToken(TOKEN_TYPES.colon); | |
| chop++; | |
| } else if (input[0] === ',') { | |
| yield buildToken(TOKEN_TYPES.comma); | |
| chop++; | |
| } else if (/^[a-zA-Z\/.-]/g.test(input)) { | |
| let i = 0; | |
| for (; i < input.length; i++) { | |
| const char = input[i]; | |
| if (char === ':' || char === ' ' || char === '\n' || char === '\r' || char === ',') { | |
| break; | |
| } | |
| } | |
| const name = input.substring(0, i); | |
| chop = i; | |
| yield buildToken(TOKEN_TYPES.string, name); | |
| } else { | |
| yield buildToken(TOKEN_TYPES.invalid); | |
| } | |
| if (!chop) { | |
| // will trigger infinite recursion | |
| yield buildToken(TOKEN_TYPES.invalid); | |
| } | |
| col += chop; | |
| lastNewline = input[0] === '\n' || (input[0] === '\r' && input[1] === '\n'); | |
| input = input.slice(chop); | |
| } | |
| yield buildToken(TOKEN_TYPES.eof); | |
| } | |
| class Parser { | |
| constructor(input: string, fileLoc: string = 'lockfile') { | |
| this.comments = []; | |
| this.tokens = tokenise(input); | |
| this.fileLoc = fileLoc; | |
| } | |
| fileLoc: string; | |
| token: Token; | |
| tokens: Iterator<Token>; | |
| comments: Array<string>; | |
| onComment(token: Token) { | |
| const value = token.value; | |
| invariant(typeof value === 'string', 'expected token value to be a string'); | |
| const comment = value.trim(); | |
| const versionMatch = comment.match(VERSION_REGEX); | |
| if (versionMatch) { | |
| const version = +versionMatch[1]; | |
| if (version > LOCKFILE_VERSION) { | |
| throw new MessageError( | |
| `Can't install from a lockfile of version ${version} as you're on an old yarn version that only supports ` + | |
| `versions up to ${LOCKFILE_VERSION}. Run \`$ yarn self-update\` to upgrade to the latest version.`, | |
| ); | |
| } | |
| } | |
| this.comments.push(comment); | |
| } | |
| next(): Token { | |
| const item = this.tokens.next(); | |
| invariant(item, 'expected a token'); | |
| const {done, value} = item; | |
| if (done || !value) { | |
| throw new Error('No more tokens'); | |
| } else if (value.type === TOKEN_TYPES.comment) { | |
| this.onComment(value); | |
| return this.next(); | |
| } else { | |
| return (this.token = value); | |
| } | |
| } | |
| unexpected(msg: string = 'Unexpected token') { | |
| throw new SyntaxError(`${msg} ${this.token.line}:${this.token.col} in ${this.fileLoc}`); | |
| } | |
| expect(tokType: string) { | |
| if (this.token.type === tokType) { | |
| this.next(); | |
| } else { | |
| this.unexpected(); | |
| } | |
| } | |
| eat(tokType: string): boolean { | |
| if (this.token.type === tokType) { | |
| this.next(); | |
| return true; | |
| } else { | |
| return false; | |
| } | |
| } | |
| parse(indent: number = 0): Object { | |
| const obj = map(); | |
| while (true) { | |
| const propToken = this.token; | |
| if (propToken.type === TOKEN_TYPES.newline) { | |
| const nextToken = this.next(); | |
| if (!indent) { | |
| // if we have 0 indentation then the next token doesn't matter | |
| continue; | |
| } | |
| if (nextToken.type !== TOKEN_TYPES.indent) { | |
| // if we have no indentation after a newline then we've gone down a level | |
| break; | |
| } | |
| if (nextToken.value === indent) { | |
| // all is good, the indent is on our level | |
| this.next(); | |
| } else { | |
| // the indentation is less than our level | |
| break; | |
| } | |
| } else if (propToken.type === TOKEN_TYPES.indent) { | |
| if (propToken.value === indent) { | |
| this.next(); | |
| } else { | |
| break; | |
| } | |
| } else if (propToken.type === TOKEN_TYPES.eof) { | |
| break; | |
| } else if (propToken.type === TOKEN_TYPES.string) { | |
| // property key | |
| const key = propToken.value; | |
| invariant(key, 'Expected a key'); | |
| const keys = [key]; | |
| this.next(); | |
| // support multiple keys | |
| while (this.token.type === TOKEN_TYPES.comma) { | |
| this.next(); // skip comma | |
| const keyToken = this.token; | |
| if (keyToken.type !== TOKEN_TYPES.string) { | |
| this.unexpected('Expected string'); | |
| } | |
| const key = keyToken.value; | |
| invariant(key, 'Expected a key'); | |
| keys.push(key); | |
| this.next(); | |
| } | |
| const wasColon = this.token.type === TOKEN_TYPES.colon; | |
| if (wasColon) { | |
| this.next(); | |
| } | |
| if (isValidPropValueToken(this.token)) { | |
| // plain value | |
| for (const key of keys) { | |
| obj[key] = this.token.value; | |
| } | |
| this.next(); | |
| } else if (wasColon) { | |
| // parse object | |
| const val = this.parse(indent + 1); | |
| for (const key of keys) { | |
| obj[key] = val; | |
| } | |
| if (indent && this.token.type !== TOKEN_TYPES.indent) { | |
| break; | |
| } | |
| } else { | |
| this.unexpected('Invalid value type'); | |
| } | |
| } else { | |
| this.unexpected(`Unknown token: ${util.inspect(propToken)}`); | |
| } | |
| } | |
| return obj; | |
| } | |
| } | |
| const MERGE_CONFLICT_ANCESTOR = '|||||||'; | |
| const MERGE_CONFLICT_END = '>>>>>>>'; | |
| const MERGE_CONFLICT_SEP = '======='; | |
| const MERGE_CONFLICT_START = '<<<<<<<'; | |
| /** | |
| * Extract the two versions of the lockfile from a merge conflict. | |
| */ | |
| function extractConflictVariants(str: string): [string, string] { | |
| const variants = [[], []]; | |
| const lines = str.split(/\r?\n/g); | |
| let skip = false; | |
| while (lines.length) { | |
| const line = lines.shift(); | |
| if (line.startsWith(MERGE_CONFLICT_START)) { | |
| // get the first variant | |
| while (lines.length) { | |
| const conflictLine = lines.shift(); | |
| if (conflictLine === MERGE_CONFLICT_SEP) { | |
| skip = false; | |
| break; | |
| } else if (skip || conflictLine.startsWith(MERGE_CONFLICT_ANCESTOR)) { | |
| skip = true; | |
| continue; | |
| } else { | |
| variants[0].push(conflictLine); | |
| } | |
| } | |
| // get the second variant | |
| while (lines.length) { | |
| const conflictLine = lines.shift(); | |
| if (conflictLine.startsWith(MERGE_CONFLICT_END)) { | |
| break; | |
| } else { | |
| variants[1].push(conflictLine); | |
| } | |
| } | |
| } else { | |
| variants[0].push(line); | |
| variants[1].push(line); | |
| } | |
| } | |
| return [variants[0].join('\n'), variants[1].join('\n')]; | |
| } | |
| /** | |
| * Check if a lockfile has merge conflicts. | |
| */ | |
| function hasMergeConflicts(str: string): boolean { | |
| return str.includes(MERGE_CONFLICT_START) && str.includes(MERGE_CONFLICT_SEP) && str.includes(MERGE_CONFLICT_END); | |
| } | |
| /** | |
| * Parse the lockfile. | |
| */ | |
| function parse(str: string, fileLoc: string): Object { | |
| const parser = new Parser(str, fileLoc); | |
| parser.next(); | |
| if (!fileLoc.endsWith(`.yml`)) { | |
| try { | |
| return parser.parse(); | |
| } catch (error1) { | |
| try { | |
| return safeLoad(str, { | |
| schema: FAILSAFE_SCHEMA, | |
| }); | |
| } catch (error2) { | |
| throw error1; | |
| } | |
| } | |
| } else { | |
| const result = safeLoad(str, { | |
| schema: FAILSAFE_SCHEMA, | |
| }); | |
| if (typeof result === 'object') { | |
| return result; | |
| } else { | |
| return {}; | |
| } | |
| } | |
| } | |
| /** | |
| * Parse and merge the two variants in a conflicted lockfile. | |
| */ | |
| function parseWithConflict(str: string, fileLoc: string): ParseResult { | |
| const variants = extractConflictVariants(str); | |
| try { | |
| return {type: 'merge', object: Object.assign({}, parse(variants[0], fileLoc), parse(variants[1], fileLoc))}; | |
| } 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); | |
| return hasMergeConflicts(str) ? parseWithConflict(str, fileLoc) : {type: 'success', object: parse(str, fileLoc)}; | |
| } |