diff --git a/README.md b/README.md index d22c63c..556292e 100644 --- a/README.md +++ b/README.md @@ -534,7 +534,6 @@ Available plugins are over at [Babel: Plugins List](https://babeljs.io/docs/plug #### Example: Remove all preconfigured defaults -```javascript parserPlugins: [] ``` @@ -571,7 +570,7 @@ parserPlugins: [ proposal: 'hack', }, ], -] +]; ``` ### `sortImports` diff --git a/lib/Configuration.js b/lib/Configuration.js index 892f6aa..03587ca 100644 --- a/lib/Configuration.js +++ b/lib/Configuration.js @@ -1,119 +1,23 @@ // @flow -import crypto from 'crypto'; import os from 'os'; import path from 'path'; import has from 'lodash/has'; -import globals from 'globals'; import semver from 'semver'; import FileUtils from './FileUtils'; import JsModule from './JsModule'; -import findPackageDependencies from './findPackageDependencies'; import meteorEnvironment from './environments/meteorEnvironment'; import nodeEnvironment from './environments/nodeEnvironment'; import normalizePath from './normalizePath'; import version from './version'; -import { DEFAULT_PARSER_PLUGINS } from './parse.js'; +import { validate, getDefaultConfig } from './configurationSchema.js'; const JSON_CONFIG_FILE = '.importjs.json'; const JS_CONFIG_FILES = ['.importjs.js', '.importjs.cjs', '.importjs.mjs']; -function findGlobalsFromEnvironments( - environments: Array, -): Array { - const result = Object.keys(globals.builtin); - - environments.forEach((environment: string) => { - const envGlobals = globals[environment]; - if (!envGlobals) { - return; - } - result.push(...Object.keys(envGlobals)); - }); - return result; -} - -const DEFAULT_CONFIG = { - aliases: {}, - declarationKeyword: 'import', - cacheLocation: ({ config }: Object): string => { - const hash = crypto - .createHash('md5') - .update(`${config.workingDirectory}-v4`) - .digest('hex'); - return path.join(os.tmpdir(), `import-js-${hash}.db`); - }, - coreModules: [], - namedExports: {}, - environments: [], - excludes: [], - globals: ({ config }: Object): Array => - findGlobalsFromEnvironments(config.get('environments')), - groupImports: true, - ignorePackagePrefixes: [], - importDevDependencies: false, - importFunction: 'require', - importStatementFormatter: ({ importStatement }: Object): string => - importStatement, - logLevel: 'info', - maxLineLength: 80, - minimumVersion: '0.0.0', - moduleNameFormatter: ({ moduleName }: Object): string => moduleName, - moduleSideEffectImports: (): Array => [], - sortImports: true, - emptyLineBetweenGroups: true, - stripFileExtensions: ['.js', '.jsx', '.ts', '.tsx'], - danglingCommas: true, - tab: ' ', - useRelativePaths: true, - packageDependencies: ({ config }: Object): Set => - findPackageDependencies( - config.workingDirectory, - config.get('importDevDependencies'), - ), - // Default configuration options, and options inherited from environment - // configuration are overridden if they appear in user config. Some options, - // however, get merged with the parent configuration. This list specifies which - // ones are merged. - mergableOptions: { - aliases: true, - coreModules: true, - namedExports: true, - globals: true, - }, - parserPlugins: DEFAULT_PARSER_PLUGINS, -}; - -const KNOWN_CONFIGURATION_OPTIONS = [ - 'aliases', - 'cacheLocation', - 'coreModules', - 'declarationKeyword', - 'environments', - 'excludes', - 'globals', - 'groupImports', - 'ignorePackagePrefixes', - 'importDevDependencies', - 'importFunction', - 'importStatementFormatter', - 'logLevel', - 'maxLineLength', - 'minimumVersion', - 'moduleNameFormatter', - 'moduleSideEffectImports', - 'namedExports', - 'sortImports', - 'stripFileExtensions', - 'tab', - 'useRelativePaths', - 'mergableOptions', - 'danglingCommas', - 'emptyLineBetweenGroups', - 'parserPlugins', -]; +const DEFAULT_CONFIG = getDefaultConfig(); const DEPRECATED_CONFIGURATION_OPTIONS = []; @@ -122,16 +26,12 @@ const ENVIRONMENTS = { meteor: meteorEnvironment, }; -function checkForUnknownConfiguration(config: Object): Array { +function checkConfiguration(config: Object): Array { const messages = []; - Object.keys(config).forEach((option: string) => { - if (KNOWN_CONFIGURATION_OPTIONS.indexOf(option) === -1) { - messages.push(`Unknown configuration: \`${option}\``); - } - }); + const result = validate(config); - return messages; + return result.messages; } function checkForDeprecatedConfiguration(config: Object): Array { @@ -242,7 +142,7 @@ export default class Configuration { if (userConfig) { this.configs.push(userConfig); - this.messages.push(...checkForUnknownConfiguration(userConfig)); + this.messages.push(...checkConfiguration(userConfig)); this.messages.push(...checkForDeprecatedConfiguration(userConfig)); // Add configurations for the environments specified in the user config diff --git a/lib/__tests__/ImportStatements-test.js b/lib/__tests__/ImportStatements-test.js index 9e6d9d5..057b3a6 100644 --- a/lib/__tests__/ImportStatements-test.js +++ b/lib/__tests__/ImportStatements-test.js @@ -119,7 +119,7 @@ describe('ImportStatements', () => { beforeEach(() => { FileUtils.__setFile(path.join(process.cwd(), '.importjs.js'), { environments: ['meteor'], - packageDependencies: new Set(['meteor/bar']), + packageDependencies: ['meteor/bar'], }); }); diff --git a/lib/__tests__/configurationSchema-test.js b/lib/__tests__/configurationSchema-test.js new file mode 100644 index 0000000..573bb76 --- /dev/null +++ b/lib/__tests__/configurationSchema-test.js @@ -0,0 +1,45 @@ +import { validate, getDefaultConfig } from '../configurationSchema.js'; + +it('should export defaults as an object', () => { + const config = getDefaultConfig(); + + expect(config.aliases).toEqual({}); +}); + +it('should validate successfully', () => { + const result = validate({ + aliases: { + _: 'third-party-libs/underscore', + }, + }); + expect(result.error).toEqual(false); + expect(result.messages).toEqual([]); +}); + +it('should notify about unknown identifiers, and remove them', () => { + const data = { + thisAintRight: 'better fail', + }; + const result = validate(data); + expect(result.error).toEqual(true); + expect(result.messages[0]).toEqual('Unknown configuration: `thisAintRight`'); + expect(data.hasOwnProperty('thisAintRight')).toBe(false); +}); + +it('should handle functions', () => { + const result = validate({ + aliases: () => ({ _: 'third-party-libs/underscore' }), + }); + expect(result.error).toEqual(false); +}); + +it('should notify about invalid identifiers, and remove them', () => { + const data = { + aliases: 123, + }; + const result = validate(data); + expect(result.error).toEqual(true); + expect(result.messages.length).toEqual(1); + expect(result.messages[0]).toEqual('Invalid configuration: `aliases`'); + expect(data.hasOwnProperty('aliases')).toBe(false); +}); diff --git a/lib/configurationSchema.js b/lib/configurationSchema.js new file mode 100644 index 0000000..3d12563 --- /dev/null +++ b/lib/configurationSchema.js @@ -0,0 +1,433 @@ +import Ajv from 'ajv'; +import ajvInstanceof from 'ajv-keywords/dist/keywords/instanceof'; +import globals from 'globals'; +import findPackageDependencies from './findPackageDependencies'; +import { DEFAULT_PARSER_PLUGINS } from './parse.js'; +import crypto from 'crypto'; +import path from 'path'; +import os from 'os'; + +const SCHEMA = { + type: 'object', + additionalProperties: false, + properties: { + aliases: { + default: {}, + anyOf: [ + { + instanceof: 'Function', + }, + { + type: 'object', + additionalProperties: { + type: 'string', + }, + }, + ], + }, + cacheLocation: { + instanceof: 'Function', + default: ({ config }) => { + const hash = crypto + .createHash('md5') + .update(`${config.workingDirectory}-v4`) + .digest('hex'); + return path.join(os.tmpdir(), `import-js-${hash}.db`); + }, + }, + coreModules: { + default: [], + anyOf: [ + { + instanceof: 'Function', + }, + { + type: 'array', + items: { + type: 'string', + }, + }, + ], + }, + danglingCommas: { + default: true, + anyOf: [ + { + instanceof: 'Function', + }, + { + type: 'boolean', + }, + ], + }, + declarationKeyword: { + default: 'import', + anyOf: [ + { + instanceof: 'Function', + }, + { + enum: ['var', 'const', 'import'], + }, + ], + }, + emptyLineBetweenGroups: { + default: true, + anyOf: [ + { + instanceof: 'Function', + }, + { + type: 'boolean', + }, + ], + }, + environments: { + default: [], + anyOf: [ + { + instanceof: 'Function', + }, + { + type: 'array', + items: { + enum: ['meteor', 'node', 'browser', 'jasmine', 'jest'], + }, + }, + ], + }, + excludes: { + default: [], + anyOf: [ + { + instanceof: 'Function', + }, + { + type: 'array', + items: { + type: 'string', + }, + }, + ], + }, + globals: { + default: ({ config }) => + findGlobalsFromEnvironments(config.get('environments')), + anyOf: [ + { + instanceof: 'Function', + }, + { + type: 'array', + items: { + type: 'string', + }, + }, + ], + }, + groupImports: { + default: true, + anyOf: [ + { + instanceof: 'Function', + }, + { + type: 'boolean', + }, + ], + }, + ignorePackagePrefixes: { + default: [], + anyOf: [ + { + instanceof: 'Function', + }, + { + type: 'array', + items: { + type: 'string', + }, + }, + ], + }, + importDevDependencies: { + default: false, + anyOf: [ + { + instanceof: 'Function', + }, + { + type: 'boolean', + }, + ], + }, + importFunction: { + default: 'require', + anyOf: [ + { + instanceof: 'Function', + }, + { + type: 'string', + }, + ], + }, + importStatementFormatter: { + instanceof: 'Function', + default: ({ importStatement }) => importStatement, + }, + logLevel: { + default: 'info', + anyOf: [ + { + instanceof: 'Function', + }, + { + default: 'info', + enum: ['debug', 'info', 'warn', 'error'], + }, + ], + }, + maxLineLength: { + default: 80, + anyOf: [ + { + instanceof: 'Function', + }, + { + type: 'integer', + minimum: 10, + }, + ], + }, + mergableOptions: { + type: 'object', + default: { + aliases: true, + coreModules: true, + namedExports: true, + globals: true, + }, + additionalProperties: false, + properties: { + aliases: { type: 'boolean' }, + coreModules: { type: 'boolean' }, + danglingCommas: { type: 'boolean' }, + declarationKeyword: { type: 'boolean' }, + environments: { type: 'boolean' }, + excludes: { type: 'boolean' }, + globals: { type: 'boolean' }, + groupImports: { type: 'boolean' }, + ignorePackagePrefixes: { type: 'boolean' }, + importDevDependencies: { type: 'boolean' }, + importFunction: { type: 'boolean' }, + importStatementFormatter: { type: 'boolean' }, + logLevel: { type: 'boolean' }, + maxLineLength: { type: 'boolean' }, + minimumVersion: { type: 'boolean' }, + moduleNameFormatter: { type: 'boolean' }, + namedExports: { type: 'boolean' }, + sortImports: { type: 'boolean' }, + stripFileExtensions: { type: 'boolean' }, + tab: { type: 'boolean' }, + useRelativePaths: { type: 'boolean' }, + }, + }, + minimumVersion: { + default: '0.0.0', + anyOf: [ + { + instanceof: 'Function', + }, + { + type: 'string', + }, + ], + }, + moduleNameFormatter: { + instanceof: 'Function', + default: ({ moduleName }) => moduleName, + }, + moduleSideEffectImports: { + instanceof: 'Function', + default: () => [], + }, + namedExports: { + default: {}, + anyOf: [ + { + instanceof: 'Function', + }, + { + type: 'object', + additionalProperties: { + type: 'array', + items: { + type: 'string', + }, + }, + }, + ], + }, + packageDependencies: { + default: ({ config }) => + findPackageDependencies( + config.workingDirectory, + config.get('importDevDependencies'), + ), + anyOf: [ + { + instanceof: 'Function', + }, + { + type: 'array', + items: { + type: `string`, + }, + }, + ], + }, + parserPlugins: { + default: DEFAULT_PARSER_PLUGINS, + type: 'array', + items: { + anyOf: [ + { + type: 'string', + }, + { + type: 'array', + minItems: 2, + additionalItems: false, + items: [ + { + type: 'string', + }, + { + type: 'object', + additionalProperties: { + type: 'string', + }, + }, + ], + }, + ], + }, + }, + sortImports: { + default: true, + anyOf: [ + { + instanceof: 'Function', + }, + { + type: 'boolean', + }, + ], + }, + stripFileExtensions: { + default: ['.js', '.jsx', '.ts', '.tsx'], + anyOf: [ + { + instanceof: 'Function', + }, + { + type: 'array', + items: { + type: 'string', + }, + }, + ], + }, + tab: { + default: ' ', + anyOf: [ + { + instanceof: 'Function', + }, + { + type: 'string', + }, + ], + }, + useRelativePaths: { + default: true, + anyOf: [ + { + instanceof: 'Function', + }, + { + type: 'boolean', + }, + ], + }, + }, +}; + +function findGlobalsFromEnvironments(environments) { + const result = Object.keys(globals.builtin); + + environments.forEach((environment) => { + const envGlobals = globals[environment]; + if (!envGlobals) { + return; + } + result.push(...Object.keys(envGlobals)); + }); + return result; +} + +export function getDefaultConfig() { + return Object.entries(SCHEMA.properties).reduce((acc, [k, v]) => { + if (typeof v !== `object` || v === null) { + throw new Error(`Expected schema key '${k}' to be an object`); + } + if (v.hasOwnProperty('default')) { + acc[k] = v.default; + } + return acc; + }, {}); +} + +export function validate(data) { + const ajv = new Ajv(); + ajvInstanceof(ajv); + + const validate = ajv.compile(SCHEMA); + + if (!validate(data)) { + return { + error: true, + messages: validate.errors + .map((err) => { + // report unknown identifiers the same as we have done in the past + // so we can showcase that the move to Ajv didn't change previous + // behaviors + if (err.message === 'must NOT have additional properties') { + // remove the extraneous identifier so we avoid later errors in code + // instancePath is empty if root key + const rootKey = + err.instancePath.split('/')[1] || err.params.additionalProperty; + delete data[rootKey]; + + return ( + 'Unknown configuration: `' + + (err.instancePath ? err.instancePath + `.` : ``) + + err.params.additionalProperty + + '`' + ); + } else { + // remove the failing identifier so we avoid later errors in code + const rootKey = err.instancePath.split('/')[1]; + delete data[rootKey]; + + return 'Invalid configuration: `' + err.instancePath.slice(1) + '`'; + } + }) + .filter((val, index, a) => a.indexOf(val) === index) + .filter((it) => !!it), + }; + } + + return { error: false, messages: [] }; +} diff --git a/package-lock.json b/package-lock.json index 578f108..4e9862a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,8 @@ "@babel/plugin-syntax-jsx": "^7.7.4", "@babel/plugin-syntax-typescript": "^7.7.4", "@babel/runtime": "^7.7.6", + "ajv": "^8.12.0", + "ajv-keywords": "^5.1.0", "commander": "^4.0.1", "fb-watchman": "^2.0.1", "glob": "^7.1.6", @@ -2075,6 +2077,23 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@eslint/eslintrc/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/@eslint/eslintrc/node_modules/globals": { "version": "13.24.0", "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", @@ -2091,6 +2110,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "peer": true + }, "node_modules/@eslint/eslintrc/node_modules/type-fest": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", @@ -4456,15 +4482,13 @@ } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "peer": true, + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", "dependencies": { "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", "uri-js": "^4.2.2" }, "funding": { @@ -4472,6 +4496,17 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, "node_modules/ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -6000,6 +6035,23 @@ "node": ">=10" } }, + "node_modules/eslint/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/eslint/node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -6152,6 +6204,13 @@ "node": ">=8" } }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "peer": true + }, "node_modules/eslint/node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -6800,9 +6859,7 @@ "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, - "peer": true + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", @@ -12307,11 +12364,9 @@ "dev": true }, "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "peer": true + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", @@ -13670,8 +13725,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "peer": true, "engines": { "node": ">=6" } @@ -13933,6 +13986,14 @@ "node": ">=0.10.0" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/require-main-filename": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", @@ -15408,8 +15469,6 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "peer": true, "dependencies": { "punycode": "^2.1.0" } diff --git a/package.json b/package.json index 2cdec6e..36cf392 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,8 @@ "@babel/plugin-syntax-jsx": "^7.7.4", "@babel/plugin-syntax-typescript": "^7.7.4", "@babel/runtime": "^7.7.6", + "ajv": "^8.12.0", + "ajv-keywords": "^5.1.0", "commander": "^4.0.1", "fb-watchman": "^2.0.1", "glob": "^7.1.6",