UNPKG

flow-typed

Version:

A repository of high quality flow type definitions

638 lines (520 loc) 24.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports._installNpmLibDef = installNpmLibDef; exports._installNpmLibDefs = installNpmLibDefs; exports.name = exports.description = void 0; exports.run = run; exports.setup = setup; var _safe = _interopRequireDefault(require("colors/safe")); var _semver = _interopRequireDefault(require("semver")); var _cacheRepoUtils = require("../lib/cacheRepoUtils"); var _codeSign = require("../lib/codeSign"); var _envDefs = require("../lib/envDefs"); var _fileUtils = require("../lib/fileUtils"); var _flowProjectUtils = require("../lib/flowProjectUtils"); var _flowVersion = require("../lib/flowVersion"); var _ftConfig = require("../lib/ftConfig"); var _node = require("../lib/node"); var _npmLibDefs = require("../lib/npm/npmLibDefs"); var _npmProjectUtils = require("../lib/npm/npmProjectUtils"); var _semver2 = require("../lib/semver"); var _stubUtils = require("../lib/stubUtils"); var _logger = require("../lib/logger"); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } const name = 'install [explicitLibDefs...]'; exports.name = name; const description = 'Installs libdefs into the ./flow-typed directory'; exports.description = description; function setup(yargs) { return yargs.usage(`$0 ${name} - ${description}`).positional('explicitLibDefs', { describe: 'Explicitly specify packages to install', default: [] }).options({ flowVersion: { alias: 'f', describe: 'The Flow version that fetched libdefs must be compatible with', type: 'string' }, verbose: { describe: 'Print additional, verbose info while installing libdefs', type: 'boolean', demandOption: false }, skip: { alias: 's', describe: 'Do not generate stubs for missing libdefs', type: 'boolean', demandOption: false }, skipCache: { describe: 'Do not update cache prior to installing libdefs', type: 'boolean', demandOption: false }, skipFlowRestart: { describe: 'Do not restart flow after installing libdefs', type: 'boolean', demandOption: false }, libdefDir: { alias: 'l', describe: 'Use a custom directory to install libdefs', type: 'string', demandOption: false }, cacheDir: { alias: 'c', describe: 'Directory (absolute or relative path, ~ is not supported) to store cache of libdefs', type: 'string', demandOption: false }, packageDir: { alias: 'p', describe: 'The relative path of package.json where flow-bin is installed', type: 'string' }, overwrite: { alias: 'o', describe: 'Overwrite an existing libdef', type: 'boolean', demandOption: false }, ignoreDeps: { alias: 'i', describe: 'Dependency categories to ignore when installing definitions', choices: ['dev', 'bundled', 'peer'], type: 'array' }, rootDir: { alias: 'r', describe: 'Directory of .flowconfig relative to node_modules', type: 'string' }, useCacheUntil: { alias: 'u', describe: 'Use cache until specified time in milliseconds', type: 'number' } }); } async function run(args) { const cwd = typeof args.rootDir === 'string' ? _node.path.resolve(args.rootDir) : process.cwd(); const packageDir = typeof args.packageDir === 'string' ? _node.path.resolve(args.packageDir) : cwd; const flowVersion = await (0, _flowVersion.determineFlowSpecificVersion)(packageDir, args.flowVersion); const libdefDir = typeof args.libdefDir === 'string' ? args.libdefDir : 'flow-typed'; if (args.ignoreDeps !== undefined && !Array.isArray(args.ignoreDeps)) { throw new Error('ignoreDeps is not array'); } const ignoreDeps = (args.ignoreDeps || []).map(dep => { if (typeof dep !== 'string') { throw new Error('ignoreDeps should be array of strings'); } return dep; }); if (args.explicitLibDefs !== undefined && !Array.isArray(args.explicitLibDefs)) { throw new Error('explicitLibDefs is not array'); } const explicitLibDefs = (args.explicitLibDefs || []).map(dep => { if (typeof dep !== 'string') { throw new Error('explicitLibDefs should be array of strings'); } return dep; }); const ftConfig = (0, _ftConfig.getFtConfig)(cwd); if (args.cacheDir) { const cacheDir = _node.path.resolve(String(args.cacheDir)); console.log('• Setting cache dir', cacheDir); (0, _cacheRepoUtils._setCustomCacheDir)(cacheDir); } const useCacheUntil = Number(args.useCacheUntil) || _cacheRepoUtils.CACHE_REPO_EXPIRY; const { status: npmLibDefResult, dependencyEnvs } = await installNpmLibDefs({ cwd, flowVersion, explicitLibDefs, libdefDir: libdefDir, verbose: Boolean(args.verbose), overwrite: Boolean(args.overwrite), skip: Boolean(args.skip), skipCache: Boolean(args.skipCache), ignoreDeps: ignoreDeps, useCacheUntil: Number(args.useCacheUntil) || _cacheRepoUtils.CACHE_REPO_EXPIRY, ftConfig }); if (npmLibDefResult !== 0) { return npmLibDefResult; } // Must be after `installNpmLibDefs` to ensure cache is updated first if (ftConfig !== null && ftConfig !== void 0 && ftConfig.env || dependencyEnvs.length > 0) { var _ftConfig$env; const envLibDefResult = await installEnvLibDefs([...new Set([...((_ftConfig$env = ftConfig === null || ftConfig === void 0 ? void 0 : ftConfig.env) !== null && _ftConfig$env !== void 0 ? _ftConfig$env : []), ...dependencyEnvs])], flowVersion, cwd, libdefDir, useCacheUntil, Boolean(args.overwrite)); if (envLibDefResult !== 0) { return envLibDefResult; } } // Once complete restart flow to solve flow issues when scanning large diffs if (!args.skipFlowRestart) { try { await _node.child_process.execP('npx flow stop'); await _node.child_process.execP('npx flow'); } catch (e) { console.log(_safe.default.red('!! Flow restarted with some errors')); } } return 0; } async function installEnvLibDefs(env, flowVersion, flowProjectRoot, libdefDir, useCacheUntil, overwrite) { if (env) { console.log(_safe.default.green('• `env` key found in `flow-typed.config.json`, attempting to install env definitions...\n')); if (!Array.isArray(env)) { console.log(_safe.default.yellow('Warning: `env` in `flow-typed.config.json` must be of type Array<string> - skipping')); return 0; } // Get a list of all environment defs const envDefs = await (0, _envDefs.getEnvDefs)(); const flowTypedDirPath = _node.path.join(flowProjectRoot, libdefDir, 'environments'); await (0, _fileUtils.mkdirp)(flowTypedDirPath); // Go through each env and try to install a libdef of the same name // for the given flow version, // if none is found throw a warning and continue. We shouldn't block the user. await Promise.all(env.map(async en => { if (typeof en === 'string') { const def = await (0, _envDefs.findEnvDef)(en, flowVersion, useCacheUntil, envDefs); if (def) { const fileName = `${en}.js`; const defLocalPath = _node.path.join(flowTypedDirPath, fileName); const envAlreadyInstalled = await _node.fs.exists(defLocalPath); if (envAlreadyInstalled) { const localFile = _node.fs.readFileSync(defLocalPath, 'utf-8'); if (!(0, _codeSign.verifySignedCode)(localFile) && !overwrite) { (0, _logger.listItem)(en, _safe.default.red(`${en} already exists and appears to have been manually written or changed!`), _safe.default.yellow(`Consider contributing your changes back to flow-typed repository :)`), _safe.default.yellow(`${en} already exists and appears to have been manually written or changed!`), _safe.default.yellow(`Read more at https://github.com/flow-typed/flow-typed/blob/main/CONTRIBUTING.md`), _safe.default.yellow(`Use --overwrite to overwrite the existing env defs.`)); return; } _node.fs.unlink(defLocalPath); } const repoVersion = await (0, _envDefs.getEnvDefVersionHash)((0, _cacheRepoUtils.getCacheRepoDir)(), def); const codeSignPreprocessor = (0, _codeSign.signCodeStream)(repoVersion); await (0, _fileUtils.copyFile)(def.path, defLocalPath, codeSignPreprocessor); (0, _logger.listItem)(en, _safe.default.green(`.${_node.path.sep}${_node.path.relative(flowProjectRoot, defLocalPath)}`)); } else { (0, _logger.listItem)(en, _safe.default.yellow(`Was unable to install ${en}. The env might not exist or there is not a version compatible with your version of flow`)); } } })); } return 0; } const FLOW_BUILT_IN_NPM_LIBS = ['react']; async function installNpmLibDefs({ cwd, flowVersion, explicitLibDefs, libdefDir, verbose, overwrite, skip, skipCache, ignoreDeps, useCacheUntil, ftConfig = {} }) { const flowProjectRoot = await (0, _flowProjectUtils.findFlowRoot)(cwd); if (flowProjectRoot === null) { console.error('Error: Unable to find a flow project in the current dir or any of ' + "it's parent dirs!\n" + 'Please run this command from within a Flow project.'); return { status: 1, dependencyEnvs: [] }; } const libdefsToSearchFor = new Map(); let ignoreDefs; if (Array.isArray(ftConfig.ignore)) { ignoreDefs = ftConfig.ignore; } else { try { ignoreDefs = _node.fs.readFileSync(_node.path.join(cwd, libdefDir, '.ignore'), 'utf-8').replace(/"/g, '').split('\n'); } catch (err) { // If the error is unrelated to file not existing we should continue throwing if (err.code !== 'ENOENT') { throw err; } ignoreDefs = []; } } let workspacesPkgJsonData; // If a specific pkg/version was specified, only add those packages. // Otherwise, extract dependencies from the package.json if (explicitLibDefs.length > 0) { for (var i = 0; i < explicitLibDefs.length; i++) { const term = explicitLibDefs[i]; const termMatches = term.match(/(@[^@\/]+\/)?([^@]+)@(.+)/); if (termMatches == null) { const pkgJsonData = await (0, _npmProjectUtils.getPackageJsonData)(cwd); workspacesPkgJsonData = await (0, _npmProjectUtils.findWorkspacesPackages)(cwd, pkgJsonData, ftConfig); const pkgJsonDeps = workspacesPkgJsonData.reduce((acc, pckData) => { return (0, _npmProjectUtils.mergePackageJsonDependencies)(acc, (0, _npmProjectUtils.getPackageJsonDependencies)(pckData, [], [])); }, (0, _npmProjectUtils.getPackageJsonDependencies)(pkgJsonData, [], [])); const packageVersion = pkgJsonDeps[term]; if (packageVersion) { libdefsToSearchFor.set(term, packageVersion); } else { console.error('ERROR: Package not found from package.json.\n' + 'Please specify version for the package in the format of `foo@1.2.3`'); return { status: 1, dependencyEnvs: [] }; } } else { const [_, npmScope, pkgName, pkgVerStr] = termMatches; const scopedPkgName = npmScope != null ? npmScope + pkgName : pkgName; libdefsToSearchFor.set(scopedPkgName, pkgVerStr); } } console.log(`• Searching for ${libdefsToSearchFor.size} libdefs...`); } else { const pkgJsonData = await (0, _npmProjectUtils.getPackageJsonData)(cwd); workspacesPkgJsonData = await (0, _npmProjectUtils.findWorkspacesPackages)(cwd, pkgJsonData, ftConfig); const pkgJsonDeps = workspacesPkgJsonData.reduce((acc, pckData) => { return (0, _npmProjectUtils.mergePackageJsonDependencies)(acc, (0, _npmProjectUtils.getPackageJsonDependencies)(pckData, ignoreDeps, ignoreDefs)); }, (0, _npmProjectUtils.getPackageJsonDependencies)(pkgJsonData, ignoreDeps, ignoreDefs)); for (const pkgName in pkgJsonDeps) { libdefsToSearchFor.set(pkgName, pkgJsonDeps[pkgName]); } if (libdefsToSearchFor.size === 0) { console.error("No dependencies were found in this project's package.json!"); return { status: 0, dependencyEnvs: [] }; } if (verbose) { libdefsToSearchFor.forEach((ver, name) => { console.log(`• Found package.json dependency: ${name}@${ver}`); }); } else { console.log(`• Found ${libdefsToSearchFor.size} dependencies in package.json to install libdefs for. Searching...`); } } const libDefsToSearchForEntries = [...libdefsToSearchFor.entries()]; // Search for the requested libdefs const libDefsToInstall = new Map(); const outdatedLibDefsToInstall = []; const unavailableLibDefs = []; const defDepsToInstall = {}; // This updates the cache for all definition types, npm/env/etc const libDefs = await (0, _npmLibDefs.getCacheNpmLibDefs)(useCacheUntil, skipCache); const getLibDefsToInstall = async entries => { await Promise.all(entries.map(async ([name, ver]) => { delete defDepsToInstall[name]; // To comment in json files a work around is to give a key value pair // of `"//": "comment"` we should exclude these so the install doesn't crash // Ref: https://stackoverflow.com/a/14221781/430128 if (name === '//') { return; } if (FLOW_BUILT_IN_NPM_LIBS.indexOf(name) !== -1) { return; } const libDef = await (0, _npmLibDefs.findNpmLibDef)(name, ver, flowVersion, useCacheUntil, true, libDefs); if (libDef === null) { unavailableLibDefs.push({ name, ver }); } else { libDefsToInstall.set(name, libDef); const libDefLower = (0, _semver2.getRangeLowerBound)(libDef.version); const depLower = (0, _semver2.getRangeLowerBound)(ver); if (_semver.default.lt(libDefLower, depLower)) { outdatedLibDefsToInstall.push([libDef, { name, ver }]); } // If this libdef has dependencies let's first add it to a giant list if (libDef.depVersions) { Object.keys(libDef.depVersions).forEach(dep => { if (libDef.depVersions) { // This may result in overriding other libDef dependencies // but we don't care because either way a project cannot have the // same module declared twice. defDepsToInstall[dep] = libDef.depVersions[dep]; } }); } } })); }; await getLibDefsToInstall(libDefsToSearchForEntries); const pnpResolver = await (0, _npmProjectUtils.loadPnpResolver)(await (0, _npmProjectUtils.getPackageJsonData)(cwd)); const dependencyEnvs = []; // If a package that's missing a flow-typed libdef has any .flow files, // we'll skip generating a stub for it. const untypedMissingLibDefs = []; const typedMissingLibDefs = []; await Promise.all(unavailableLibDefs.map(async ({ name: pkgName, ver: pkgVer }) => { const { isFlowTyped, pkgPath } = await (0, _stubUtils.pkgHasFlowFiles)(cwd, pkgName, pnpResolver, workspacesPkgJsonData); if (isFlowTyped && pkgPath) { typedMissingLibDefs.push([pkgName, pkgVer, pkgPath]); try { // If the flow typed dependency has a flow-typed config // check if they have env's defined and if so keep them in // a list so we can later install them along with project // defined envs. const pkgFlowTypedConfig = await _node.fs.readJson(_node.path.join(pkgPath, 'flow-typed.config.json')); if (pkgFlowTypedConfig.env) { dependencyEnvs.push(...pkgFlowTypedConfig.env); } } catch (e) {// Ignore if we cannot read flow-typed config } } else { untypedMissingLibDefs.push([pkgName, pkgVer]); } })); // If there are any typed packages we should try install any missing // lib defs for that package. // Scanning through all typed dependencies then checking their immediate deps // but do not overwrite the lib def that the project itself wants if (typedMissingLibDefs.length > 0) { const typedDepsLibDefsToSearchFor = []; await Promise.all(typedMissingLibDefs.map(async typedLibDef => { const pkgJsonData = await (0, _npmProjectUtils.getPackageJsonData)(`${typedLibDef[2]}/package.json`); const pkgJsonDeps = (0, _npmProjectUtils.getPackageJsonDependencies)(pkgJsonData, // Only get their production dependencies // as the rest aren't installed anyways ['dev', 'peer', ...ignoreDeps], ignoreDefs); for (const pkgName in pkgJsonDeps) { if (!libDefsToInstall.has(pkgName)) { typedDepsLibDefsToSearchFor.push([pkgName, pkgJsonDeps[pkgName]]); } } })); await getLibDefsToInstall(typedDepsLibDefsToSearchFor); } // Now that we've captured all package.json libdefs and typed package libdefs // We can compare libDefsToInstall with defDepsToInstall and override any that // are already needed but don't match the supported dependency versions. if (Object.keys(defDepsToInstall).length > 0) { (0, _logger.sectionHeader)('Running definition dependency check'); while (Object.keys(defDepsToInstall).length > 0) { await getLibDefsToInstall(Object.keys(defDepsToInstall).map(dep => { const libDef = libDefsToInstall.get(dep); const defVersions = defDepsToInstall[dep]; if (libDef) { if (!defVersions.includes(libDef.version)) { // If no supported version warn it'll be overridden and continue (0, _logger.listItem)(_safe.default.yellow(`One of your definitions has a dependency to ${dep} @ version(s) ${defVersions.join(', ')}`), `You have version ${_safe.default.yellow(libDef.version)} installed`, `We're overriding to a supported version to fix flow-typed ${_safe.default.red('but you may experience other type errors')}`); } else { // If a supported version already installed return dummy tuple // to get filtered out delete defDepsToInstall[dep]; return ['', '']; } } // This only hits if no supported version installed, // we will find the last version in dependency to install return [dep, defVersions[defVersions.length - 1]]; }).filter(o => !!o[0])); } (0, _logger.sectionHeader)('Check complete'); } // Scan libdefs that are already installed const libDefsToUninstall = new Map(); const alreadyInstalledLibDefs = await (0, _npmLibDefs.getInstalledNpmLibDefs)(_node.path.join(flowProjectRoot), libdefDir); [...alreadyInstalledLibDefs.entries()].forEach(([filePath, npmLibDef]) => { const fullFilePath = _node.path.join(flowProjectRoot, filePath); switch (npmLibDef.kind) { case 'LibDef': // If a libdef is already installed for some dependency, we need to // uninstall it before installing the new (potentially updated) ver const libDef = npmLibDef.libDef; const scopedPkgName = (0, _npmLibDefs.getScopedPackageName)(libDef); if (libDefsToInstall.has(scopedPkgName)) { libDefsToUninstall.set(scopedPkgName, fullFilePath); } break; case 'Stub': break; default: npmLibDef; } }); if (libDefsToInstall.size > 0) { (0, _logger.sectionHeader)(`Installing ${libDefsToInstall.size} libDefs...`); const flowTypedDirPath = _node.path.join(flowProjectRoot, libdefDir, 'npm'); await (0, _fileUtils.mkdirp)(flowTypedDirPath); const results = await Promise.all([...libDefsToInstall.values()].map(async libDef => { const toUninstall = libDefsToUninstall.get((0, _npmLibDefs.getScopedPackageName)(libDef)); if (toUninstall != null) { await _node.fs.unlink(toUninstall); } return installNpmLibDef(libDef, flowTypedDirPath, overwrite); })); if (results.some(res => !res)) { return { status: 1, dependencyEnvs: [] }; } } if ((verbose || unavailableLibDefs.length === 0) && outdatedLibDefsToInstall.length > 0) { console.log('• The following installed libdefs are compatible with your ' + 'dependencies, but may not include all minor and patch changes for ' + 'your specific dependency version:\n'); outdatedLibDefsToInstall.forEach(([libDef, { name: pkgName, ver: pkgVersion }]) => { console.log(' • libdef: %s (satisfies %s)', _safe.default.yellow(`${libDef.name}_${libDef.version}`), _safe.default.bold(`${pkgName}@${pkgVersion}`)); const libDefPlural = outdatedLibDefsToInstall.length > 1 ? ['versioned updates', 'these packages'] : ['a versioned update', 'this package']; console.log(`\n` + ` Consider submitting ${libDefPlural[0]} for ${libDefPlural[1]} to \n` + ` https://github.com/flowtype/flow-typed/\n`); }); } if (unavailableLibDefs.length > 0 && unavailableLibDefs.length === explicitLibDefs.length) { // If the user specified an explicit library to be installed, don't generate // a stub if no libdef exists -- just inform them that one doesn't exist console.log(_safe.default.red(`!! No flow@${(0, _flowVersion.toSemverString)(flowVersion)}-compatible libdefs ` + `found in flow-typed for the explicitly requested libdefs. !!`) + '\n' + '\n' + 'Consider using `%s` to generate an empty libdef that you can fill in.', _safe.default.bold(`flow-typed create-stub ${explicitLibDefs.join(' ')}`)); return { status: 1, dependencyEnvs: [] }; } else { if (untypedMissingLibDefs.length > 0 && !skip) { console.log('• Generating stubs for untyped dependencies...'); await Promise.all(untypedMissingLibDefs.map(async ([pkgName, pkgVerStr]) => { await (0, _stubUtils.createStub)(flowProjectRoot, pkgName, pkgVerStr, overwrite, pnpResolver, /* typescript */ false, libdefDir); })); console.log(_safe.default.red(`\n!! No flow@${(0, _flowVersion.toSemverString)(flowVersion)}-compatible libdefs ` + `found in flow-typed for the above untyped dependencies !!`)); const plural = unavailableLibDefs.length > 1 ? ['libdefs', 'these packages', 'them'] : ['a libdef', 'this package', 'it']; console.log(`\n` + `We've generated ${'`'}any${'`'}-typed stubs for ${plural[1]}, but ` + `consider submitting \n` + `${plural[0]} for ${plural[2]} to ` + `${_safe.default.bold('https://github.com/flowtype/flow-typed/')}\n`); } } return { status: 0, dependencyEnvs }; } async function installNpmLibDef(npmLibDef, npmDir, overwrite) { const scopedDir = npmLibDef.scope === null ? npmDir : _node.path.join(npmDir, npmLibDef.scope); (0, _fileUtils.mkdirp)(scopedDir); const fileName = `${npmLibDef.name}_${npmLibDef.version}.js`; const filePath = _node.path.join(scopedDir, fileName); // Find the libDef in the cached repo try { const terseFilePath = _node.path.relative(_node.path.resolve(npmDir, '..', '..'), filePath); if (!overwrite && (await _node.fs.exists(filePath))) { (0, _logger.listItem)(_safe.default.bold(_safe.default.red(`${terseFilePath} already exists and appears to have been manually ` + `written or changed!`)), _safe.default.green(`Consider contributing your changes back to flow-typed repository :)`), `Read more at https://github.com/flow-typed/flow-typed/blob/main/CONTRIBUTING.md`, 'Use --overwrite to overwrite the existing libdef.'); return true; } const repoVersion = await (0, _npmLibDefs.getNpmLibDefVersionHash)((0, _cacheRepoUtils.getCacheRepoDir)(), npmLibDef); const codeSignPreprocessor = (0, _codeSign.signCodeStream)(repoVersion); await (0, _fileUtils.copyFile)(npmLibDef.path, filePath, codeSignPreprocessor); (0, _logger.listItem)(fileName, _safe.default.green(`.${_node.path.sep}${terseFilePath}`)); // Remove any lingering stubs console.log(npmLibDef.name); console.log(scopedDir); const stubName = `${npmLibDef.name}_vx.x.x.js`; const stubPath = _node.path.join(scopedDir, stubName); if (overwrite && (await _node.fs.exists(stubPath))) { await _node.fs.unlink(stubPath); } return true; } catch (e) { console.error(` !! Failed to install ${npmLibDef.name} at ${filePath}`); console.error(` ERROR: ${e.message}`); return false; } }