From 46166fb6400b9b4399b055fd8cdcfdecb4c1ef0c Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 6 Feb 2018 16:01:58 -0500 Subject: [PATCH] fix: handle vue invoke config merging for existing files close #788 --- .../@vue/cli-test-utils/createTestProject.js | 8 +- packages/@vue/cli/__tests__/Generator.spec.js | 43 ++--------- packages/@vue/cli/__tests__/invoke.spec.js | 73 +++++++++++++++++- packages/@vue/cli/acorn-test.js | 54 +++++++++++++ packages/@vue/cli/lib/Creator.js | 5 +- packages/@vue/cli/lib/Generator.js | 39 +++++++--- packages/@vue/cli/lib/GeneratorAPI.js | 7 +- packages/@vue/cli/lib/invoke.js | 14 ++-- .../lib/util/__tests__/extendJSConfig.spec.js | 59 +++++++++++++++ packages/@vue/cli/lib/util/configMap.js | 37 --------- .../@vue/cli/lib/util/configTransforms.js | 75 +++++++++++++++++++ packages/@vue/cli/lib/util/extendJSConfig.js | 60 +++++++++++++++ packages/@vue/cli/package.json | 2 + yarn.lock | 17 ++++- 14 files changed, 386 insertions(+), 107 deletions(-) create mode 100644 packages/@vue/cli/acorn-test.js create mode 100644 packages/@vue/cli/lib/util/__tests__/extendJSConfig.spec.js delete mode 100644 packages/@vue/cli/lib/util/configMap.js create mode 100644 packages/@vue/cli/lib/util/configTransforms.js create mode 100644 packages/@vue/cli/lib/util/extendJSConfig.js diff --git a/packages/@vue/cli-test-utils/createTestProject.js b/packages/@vue/cli-test-utils/createTestProject.js index 7a33c62ee9..74cb4c8fa3 100644 --- a/packages/@vue/cli-test-utils/createTestProject.js +++ b/packages/@vue/cli-test-utils/createTestProject.js @@ -4,6 +4,7 @@ const execa = require('execa') const { promisify } = require('util') const readFile = promisify(fs.readFile) const writeFile = promisify(fs.writeFile) +const rmFile = promisify(fs.unlink) const mkdirp = promisify(require('mkdirp')) module.exports = function createTestProject (name, preset, cwd) { @@ -25,6 +26,10 @@ module.exports = function createTestProject (name, preset, cwd) { return mkdirp(dir).then(() => writeFile(targetPath, content)) } + const rm = file => { + return rmFile(path.resolve(projectRoot, file)) + } + const run = (command, args) => { [command, ...args] = command.split(/\s+/) if (command === 'vue-cli-service') { @@ -54,6 +59,7 @@ module.exports = function createTestProject (name, preset, cwd) { has, read, write, - run + run, + rm })) } diff --git a/packages/@vue/cli/__tests__/Generator.spec.js b/packages/@vue/cli/__tests__/Generator.spec.js index 392152492f..c4551a87f0 100644 --- a/packages/@vue/cli/__tests__/Generator.spec.js +++ b/packages/@vue/cli/__tests__/Generator.spec.js @@ -67,41 +67,6 @@ test('api: extendPackage function', async () => { }) }) -test('api: extendPackage + { merge: false }', async () => { - const generator = new Generator('/', { - name: 'hello', - list: [1], - vue: { - foo: 1, - bar: 2 - } - }, [{ - id: 'test', - apply: api => { - api.extendPackage({ - name: 'hello2', - list: [2], - vue: { - foo: 2, - baz: 3 - } - }, { merge: false }) - } - }]) - - await generator.generate() - - const pkg = JSON.parse(fs.readFileSync('/package.json', 'utf-8')) - expect(pkg).toEqual({ - name: 'hello2', - list: [2], - vue: { - foo: 2, - baz: 3 - } - }) -}) - test('api: extendPackage merge dependencies', async () => { const generator = new Generator('/', {}, [ { @@ -292,7 +257,7 @@ test('api: onCreateComplete', () => { api.onCreateComplete(fn) } } - ], false, cbs) + ], cbs) expect(cbs).toContain(fn) }) @@ -333,9 +298,11 @@ test('extract config files', async () => { api.extendPackage(configs) } } - ], true) + ]) - await generator.generate() + await generator.generate({ + extractConfigFiles: true + }) const json = v => JSON.stringify(v, null, 2) expect(fs.readFileSync('/vue.config.js', 'utf-8')).toMatch('module.exports = {\n lintOnSave: true\n}') diff --git a/packages/@vue/cli/__tests__/invoke.spec.js b/packages/@vue/cli/__tests__/invoke.spec.js index 1b95447ff4..cf67caad47 100644 --- a/packages/@vue/cli/__tests__/invoke.spec.js +++ b/packages/@vue/cli/__tests__/invoke.spec.js @@ -1,4 +1,4 @@ -jest.setTimeout(10000) +jest.setTimeout(12000) jest.mock('inquirer') const invoke = require('../lib/invoke') @@ -22,13 +22,15 @@ async function assertUpdates (project) { const updatedPkg = JSON.parse(await project.read('package.json')) expect(updatedPkg.scripts.lint).toBe('vue-cli-service lint') expect(updatedPkg.devDependencies).toHaveProperty('lint-staged') - expect(updatedPkg.eslintConfig).toEqual({ - extends: ['plugin:vue/essential', '@vue/airbnb'] - }) expect(updatedPkg.gitHooks).toEqual({ 'pre-commit': 'lint-staged' }) + const eslintrc = JSON.parse(await project.read('.eslintrc')) + expect(eslintrc).toEqual({ + extends: ['plugin:vue/essential', '@vue/airbnb'] + }) + const lintedMain = await project.read('src/main.js') expect(lintedMain).toMatch(';') // should've been linted in post-generate hook } @@ -58,3 +60,66 @@ test('invoke with prompts', async () => { await invoke(`eslint`, {}, project.dir) await assertUpdates(project) }) + +test('invoke with existing files', async () => { + const project = await create(`invoke-existing`, { + useConfigFiles: true, + plugins: { + '@vue/cli-plugin-babel': {}, + '@vue/cli-plugin-eslint': { config: 'base' } + } + }) + // mock install + const pkg = JSON.parse(await project.read('package.json')) + pkg.devDependencies['@vue/cli-plugin-eslint'] = '*' + await project.write('package.json', JSON.stringify(pkg, null, 2)) + + // mock existing vue.config.js + await project.write('vue.config.js', `module.exports = { lintOnSave: false }`) + + const eslintrc = JSON.parse(await project.read('.eslintrc')) + expect(eslintrc).toEqual({ + extends: ['plugin:vue/essential', 'eslint:recommended'] + }) + + await project.run(`${require.resolve('../bin/vue')} invoke eslint --config airbnb --lintOn save,commit`) + + await assertUpdates(project) + const updatedVueConfig = await project.read('vue.config.js') + expect(updatedVueConfig).toMatch(`module.exports = { lintOnSave: true }`) +}) + +test('invoke with existing files (yaml)', async () => { + const project = await create(`invoke-existing`, { + useConfigFiles: true, + plugins: { + '@vue/cli-plugin-babel': {}, + '@vue/cli-plugin-eslint': { config: 'base' } + } + }) + // mock install + const pkg = JSON.parse(await project.read('package.json')) + pkg.devDependencies['@vue/cli-plugin-eslint'] = '*' + await project.write('package.json', JSON.stringify(pkg, null, 2)) + + const eslintrc = JSON.parse(await project.read('.eslintrc')) + expect(eslintrc).toEqual({ + extends: ['plugin:vue/essential', 'eslint:recommended'] + }) + + await project.rm(`.eslintrc`) + await project.write(`.eslintrc.yml`, ` +extends: + - 'plugin:vue/essential' + - 'eslint:recommended' + `.trim()) + + await project.run(`${require.resolve('../bin/vue')} invoke eslint --config airbnb`) + + const updated = await project.read('.eslintrc.yml') + expect(updated).toMatch(` +extends: + - 'plugin:vue/essential' + - '@vue/airbnb' +`.trim()) +}) diff --git a/packages/@vue/cli/acorn-test.js b/packages/@vue/cli/acorn-test.js new file mode 100644 index 0000000000..3e1b474543 --- /dev/null +++ b/packages/@vue/cli/acorn-test.js @@ -0,0 +1,54 @@ +const acorn = require('acorn') +const walk = require('acorn/dist/walk') + +const ast = acorn.parse(` +module.exports = { + lintOnSave: true, + css: { + loaderOptions: { + sass: { + data: 'foo' + } + } + }, + pluginsOptions: { + foo: 'bar' + } +} +`) + +let exportsIdentifier = null + +walk.simple(ast, { + AssignmentExpression (node) { + if ( + node.left.type === 'MemberExpression' && + node.left.object.name === 'module' && + node.left.property.name === 'exports' + ) { + if (node.right.type === 'ObjectExpression') { + augmentExports(node.right) + } else if (node.right.type === 'Identifier') { + // do a second pass + exportsIdentifier = node.right.name + } + } + } +}) + +if (exportsIdentifier) { + walk.simple(ast, { + VariableDeclarator (node) { + if ( + node.id.name === exportsIdentifier && + node.init.type === 'ObjectExpression' + ) { + augmentExports(node.init) + } + } + }) +} + +function augmentExports (node) { + console.log(node) +} diff --git a/packages/@vue/cli/lib/Creator.js b/packages/@vue/cli/lib/Creator.js index 6c3f75178a..870d541c06 100644 --- a/packages/@vue/cli/lib/Creator.js +++ b/packages/@vue/cli/lib/Creator.js @@ -136,10 +136,11 @@ module.exports = class Creator { context, pkg, plugins, - preset.useConfigFiles, createCompleteCbs ) - await generator.generate() + await generator.generate({ + extractConfigFiles: preset.useConfigFiles + }) // install additional deps (injected by generators) log(`📦 Installing additional dependencies...`) diff --git a/packages/@vue/cli/lib/Generator.js b/packages/@vue/cli/lib/Generator.js index a1f80ea97c..a9f35fbfe5 100644 --- a/packages/@vue/cli/lib/Generator.js +++ b/packages/@vue/cli/lib/Generator.js @@ -1,16 +1,17 @@ const ejs = require('ejs') const slash = require('slash') const debug = require('debug') -const configMap = require('./util/configMap') const GeneratorAPI = require('./GeneratorAPI') const sortObject = require('./util/sortObject') const writeFileTree = require('./util/writeFileTree') +const configTransforms = require('./util/configTransforms') module.exports = class Generator { - constructor (context, pkg, plugins, extractConfigFiles, completeCbs = []) { + constructor (context, pkg, plugins, completeCbs = []) { this.context = context this.plugins = plugins - this.pkg = pkg + this.originalPkg = pkg + this.pkg = Object.assign({}, pkg) this.completeCbs = completeCbs // for conflict resolution @@ -27,11 +28,14 @@ module.exports = class Generator { const api = new GeneratorAPI(id, this, options, rootOptions || {}) apply(api, options, rootOptions) }) - // extract configs from package.json into dedicated files. - this.extractConfigFiles(extractConfigFiles) } - async generate () { + async generate ({ + extractConfigFiles = false, + checkExisting = false + } = {}) { + // extract configs from package.json into dedicated files. + this.extractConfigFiles(extractConfigFiles, checkExisting) // wait for file resolve await this.resolveFiles() // set package.json @@ -41,21 +45,32 @@ module.exports = class Generator { await writeFileTree(this.context, this.files) } - extractConfigFiles (all) { + extractConfigFiles (extractAll, checkExisting) { const extract = key => { - if (configMap[key]) { + if ( + configTransforms[key] && + this.pkg[key] && + // do not extract if the field exists in original package.json + !this.originalPkg[key] + ) { const value = this.pkg[key] - const { transform, filename } = configMap[key] - this.files[filename] = transform(value) + const transform = configTransforms[key] + const res = transform( + value, + checkExisting, + this.context + ) + const { content, filename } = res + this.files[filename] = content delete this.pkg[key] } } - if (all) { + if (extractAll) { for (const key in this.pkg) { extract(key) } } else if (!process.env.VUE_CLI_TEST) { - // by default, extract vue.config.js + // by default, always extract vue.config.js extract('vue') } } diff --git a/packages/@vue/cli/lib/GeneratorAPI.js b/packages/@vue/cli/lib/GeneratorAPI.js index af3c86e3de..a825df262c 100644 --- a/packages/@vue/cli/lib/GeneratorAPI.js +++ b/packages/@vue/cli/lib/GeneratorAPI.js @@ -100,10 +100,9 @@ class GeneratorAPI { * Tool configuration fields may be extracted into standalone files before * files are written to disk. * - * @param {object} fields - Fields to merge. - * @param {object} [options] - pass { merge: false } to disable deep merging. + * @param {object | () => object} fields - Fields to merge. */ - extendPackage (fields, options = { merge: true }) { + extendPackage (fields) { const pkg = this.generator.pkg const toMerge = isFunction(fields) ? fields(pkg) : fields for (const key in toMerge) { @@ -117,7 +116,7 @@ class GeneratorAPI { value, this.generator.depSources ) - } else if (!options.merge || !(key in pkg)) { + } else if (!(key in pkg)) { pkg[key] = value } else if (Array.isArray(value) && Array.isArray(existing)) { pkg[key] = existing.concat(value) diff --git a/packages/@vue/cli/lib/invoke.js b/packages/@vue/cli/lib/invoke.js index 1b79cf0fa2..5a8f8a922f 100644 --- a/packages/@vue/cli/lib/invoke.js +++ b/packages/@vue/cli/lib/invoke.js @@ -81,13 +81,15 @@ async function invoke (pluginName, options = {}, context = process.cwd()) { context, pkg, [plugin], - isTestOrDebug ? false : loadOptions().useConfigFiles, createCompleteCbs ) log() logWithSpinner('🚀', `Invoking generator for ${id}...`) - await generator.generate() + await generator.generate({ + extractConfigFiles: true, + checkExisting: true + }) const newDeps = generator.pkg.dependencies const newDevDeps = generator.pkg.devDependencies @@ -115,9 +117,11 @@ async function invoke (pluginName, options = {}, context = process.cwd()) { log(` Successfully invoked generator for plugin: ${chalk.cyan(id)}`) if (!process.env.VUE_CLI_TEST && hasGit()) { const { stdout } = await execa('git', ['ls-files', '--exclude-standard', '--modified', '--others']) - log(` The following files have been updated / added:\n`) - log(chalk.red(stdout.split(/\r?\n/g).map(line => ` ${line}`).join('\n'))) - log() + if (stdout.trim()) { + log(` The following files have been updated / added:\n`) + log(chalk.red(stdout.split(/\r?\n/g).map(line => ` ${line}`).join('\n'))) + log() + } } log(` You should review and commit the changes.`) log() diff --git a/packages/@vue/cli/lib/util/__tests__/extendJSConfig.spec.js b/packages/@vue/cli/lib/util/__tests__/extendJSConfig.spec.js new file mode 100644 index 0000000000..34d6ffdff4 --- /dev/null +++ b/packages/@vue/cli/lib/util/__tests__/extendJSConfig.spec.js @@ -0,0 +1,59 @@ +const extend = require('../extendJSConfig') + +test(`basic`, () => { + const value = { + foo: true, + css: { + modules: true + } + } + const source = +`module.exports = { + foo: false, + css: { + modules: false + } +}` + expect(extend(value, source)).toMatch( + `module.exports = { + foo: true, + css: { + modules: true + } +}` + ) +}) + +test(`adding new property`, () => { + const value = { + foo: true + } + const source = +`module.exports = { + bar: 123 +}` + expect(extend(value, source)).toMatch( + `module.exports = { + bar: 123, + foo: true +}` + ) +}) + +test(`non direct assignment`, () => { + const value = { + foo: true + } + const source = +`const config = { + bar: 123 +} +module.exports = config` + expect(extend(value, source)).toMatch( + `const config = { + bar: 123, + foo: true +} +module.exports = config` + ) +}) diff --git a/packages/@vue/cli/lib/util/configMap.js b/packages/@vue/cli/lib/util/configMap.js deleted file mode 100644 index e9a1d1be04..0000000000 --- a/packages/@vue/cli/lib/util/configMap.js +++ /dev/null @@ -1,37 +0,0 @@ -const stringifyJS = require('javascript-stringify') -const json = value => JSON.stringify(value, null, 2) -const js = value => `module.exports = ${stringifyJS(value, null, 2)}` - -module.exports = { - vue: { - filename: 'vue.config.js', - transform: js - }, - babel: { - filename: '.babelrc', - transform: json - }, - postcss: { - filename: '.postcssrc', - transform: json - }, - eslintConfig: { - filename: '.eslintrc', - transform: json - }, - jest: { - filename: 'jest.config.js', - transform: js - } - - // these are less likely to be edited frequently - - // browserslist: { - // filename: '.browserslistrc', - // transform: value => value.join('\n') - // }, - // 'lint-staged': { - // filename: '.lintstagedrc', - // transform: json - // } -} diff --git a/packages/@vue/cli/lib/util/configTransforms.js b/packages/@vue/cli/lib/util/configTransforms.js new file mode 100644 index 0000000000..5b82e78fe8 --- /dev/null +++ b/packages/@vue/cli/lib/util/configTransforms.js @@ -0,0 +1,75 @@ +const fs = require('fs') +const path = require('path') +const extendJSConfig = require('./extendJSConfig') +const stringifyJS = require('javascript-stringify') + +function makeJSTransform (filename) { + return function transformToJS (value, checkExisting, context) { + const absolutePath = path.resolve(context, filename) + if (checkExisting && fs.existsSync(absolutePath)) { + return { + filename, + content: extendJSConfig(value, fs.readFileSync(absolutePath, 'utf-8')) + } + } else { + return { + filename, + content: `module.exports = ${stringifyJS(value, null, 2)}` + } + } + } +} + +function makeJSONTransform (filename) { + return function transformToJSON (value, checkExisting, context) { + let existing = {} + const absolutePath = path.resolve(context, filename) + if (checkExisting && fs.existsSync(absolutePath)) { + existing = JSON.parse(fs.readFileSync(absolutePath, 'utf-8')) + } + value = Object.assign(existing, value) + return { + filename, + content: JSON.stringify(value, null, 2) + } + } +} + +function makeMutliExtensionJSONTransform (filename) { + return function transformToMultiExtensions (value, checkExisting, context) { + if (!checkExisting) { + return makeJSONTransform(filename)(value, checkExisting, context) + } + const absolutePath = path.resolve(context, filename) + if (fs.existsSync(absolutePath)) { + return makeJSONTransform(filename)(value, checkExisting, context) + } else if (fs.existsSync(`${absolutePath}.json`)) { + return makeJSONTransform(`${filename}.json`)(value, checkExisting, context) + } else if (fs.existsSync(`${absolutePath}.js`)) { + return makeJSTransform(`${filename}.js`)(value, checkExisting, context) + } else if (fs.existsSync(`${absolutePath}.yaml`)) { + return transformYAML(value, `${filename}.yaml`, fs.readFileSync(`${absolutePath}.yaml`, 'utf-8')) + } else if (fs.existsSync(`${absolutePath}.yml`)) { + return transformYAML(value, `${filename}.yml`, fs.readFileSync(`${absolutePath}.yml`, 'utf-8')) + } else { + return makeJSONTransform(filename)(value, false, context) + } + } +} + +function transformYAML (value, filename, source) { + const yaml = require('js-yaml') + const existing = yaml.safeLoad(source) + return { + filename, + content: yaml.safeDump(Object.assign(existing, value)) + } +} + +module.exports = { + vue: makeJSTransform('vue.config.js'), + babel: makeJSONTransform('.babelrc'), + postcss: makeMutliExtensionJSONTransform('.postcssrc'), + eslintConfig: makeMutliExtensionJSONTransform('.eslintrc'), + jest: makeJSTransform('jest.config.js') +} diff --git a/packages/@vue/cli/lib/util/extendJSConfig.js b/packages/@vue/cli/lib/util/extendJSConfig.js new file mode 100644 index 0000000000..fbf77336ea --- /dev/null +++ b/packages/@vue/cli/lib/util/extendJSConfig.js @@ -0,0 +1,60 @@ +module.exports = function extendJSConfig (value, source) { + const recast = require('recast') + const stringifyJS = require('javascript-stringify') + + let exportsIdentifier = null + + const ast = recast.parse(source) + + recast.types.visit(ast, { + visitAssignmentExpression ({ node }) { + if ( + node.left.type === 'MemberExpression' && + node.left.object.name === 'module' && + node.left.property.name === 'exports' + ) { + if (node.right.type === 'ObjectExpression') { + augmentExports(node.right) + } else if (node.right.type === 'Identifier') { + // do a second pass + exportsIdentifier = node.right.name + } + return false + } + } + }) + + if (exportsIdentifier) { + recast.types.visit(ast, { + visitVariableDeclarator ({ node }) { + if ( + node.id.name === exportsIdentifier && + node.init.type === 'ObjectExpression' + ) { + augmentExports(node.init) + } + return false + } + }) + } + + function augmentExports (node) { + const valueAST = recast.parse(`(${stringifyJS(value, null, 2)})`) + const props = valueAST.program.body[0].expression.properties + const existingProps = node.properties + for (const prop of props) { + const existing = existingProps.findIndex(p => { + return !p.computed && p.key.name === prop.key.name + }) + if (existing > -1) { + // replace + existingProps[existing].value = prop.value + } else { + // append + existingProps.push(prop) + } + } + } + + return recast.print(ast).code +} diff --git a/packages/@vue/cli/package.json b/packages/@vue/cli/package.json index 00ac6160bb..0f33c102ae 100644 --- a/packages/@vue/cli/package.json +++ b/packages/@vue/cli/package.json @@ -38,10 +38,12 @@ "inquirer": "^4.0.1", "isbinaryfile": "^3.0.2", "javascript-stringify": "^1.6.0", + "js-yaml": "^3.10.0", "klaw-sync": "^3.0.2", "lodash.clonedeep": "^4.5.0", "minimist": "^1.2.0", "mkdirp": "^0.5.1", + "recast": "^0.13.0", "resolve": "^1.5.0", "rimraf": "^2.6.2", "semver": "^5.4.1", diff --git a/yarn.lock b/yarn.lock index f2b66e21da..f592e236e0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1046,7 +1046,7 @@ assign-symbols@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" -ast-types@0.x.x: +ast-types@0.10.1, ast-types@0.x.x: version "0.10.1" resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.10.1.tgz#f52fca9715579a14f841d67d7f8d25432ab6a3dd" @@ -3573,7 +3573,7 @@ esprima@^2.6.0: version "2.7.3" resolved "https://registry.yarnpkg.com/esprima/-/esprima-2.7.3.tgz#96e3b70d5779f6ad49cd032673d1c312767ba581" -esprima@^4.0.0: +esprima@^4.0.0, esprima@~4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.0.tgz#4499eddcd1110e0b218bacf2fa7f7f59f55ca804" @@ -5806,7 +5806,7 @@ js-tokens@^3.0.0, js-tokens@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" -js-yaml@^3.4.3, js-yaml@^3.7.0, js-yaml@^3.8.1, js-yaml@^3.9.0, js-yaml@^3.9.1: +js-yaml@^3.10.0, js-yaml@^3.4.3, js-yaml@^3.7.0, js-yaml@^3.8.1, js-yaml@^3.9.0, js-yaml@^3.9.1: version "3.10.0" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.10.0.tgz#2e78441646bd4682e963f22b6e92823c309c62dc" dependencies: @@ -7959,7 +7959,7 @@ pretty@2.0.0: extend-shallow "^2.0.1" js-beautify "^1.6.12" -private@^0.1.6, private@^0.1.7: +private@^0.1.6, private@^0.1.7, private@~0.1.5: version "0.1.8" resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff" @@ -8280,6 +8280,15 @@ realpath-native@^1.0.0: dependencies: util.promisify "^1.0.0" +recast@^0.13.0: + version "0.13.0" + resolved "https://registry.yarnpkg.com/recast/-/recast-0.13.0.tgz#a343a394a37d24668d700f88ed04b8d2c314d40d" + dependencies: + ast-types "0.10.1" + esprima "~4.0.0" + private "~0.1.5" + source-map "~0.6.1" + recursive-readdir@^2.1.0: version "2.2.1" resolved "https://registry.yarnpkg.com/recursive-readdir/-/recursive-readdir-2.2.1.tgz#90ef231d0778c5ce093c9a48d74e5c5422d13a99"