UNPKG

flow-typed

Version:

A repository of high quality flow type definitions

866 lines (799 loc) 27.1 kB
// @flow import colors from 'colors/safe'; import semver from 'semver'; import typeof Yargs from 'yargs'; import { getCacheRepoDir, _setCustomCacheDir as setCustomCacheDir, CACHE_REPO_EXPIRY, } from '../lib/cacheRepoUtils'; import {signCodeStream, verifySignedCode} from '../lib/codeSign'; import {getEnvDefs, findEnvDef, getEnvDefVersionHash} from '../lib/envDefs'; import {copyFile, mkdirp} from '../lib/fileUtils'; import {findFlowRoot} from '../lib/flowProjectUtils'; import { toSemverString as flowVersionToSemver, determineFlowSpecificVersion, type FlowVersion, } from '../lib/flowVersion'; import {getFtConfig, type FtConfig} from '../lib/ftConfig'; import {fs, path, child_process} from '../lib/node'; import { findNpmLibDef, getCacheNpmLibDefs, getInstalledNpmLibDefs, getNpmLibDefVersionHash, getScopedPackageName, type NpmLibDef, } from '../lib/npm/npmLibDefs'; import { findWorkspacesPackages, getPackageJsonData, getPackageJsonDependencies, loadPnpResolver, mergePackageJsonDependencies, } from '../lib/npm/npmProjectUtils'; import {getRangeLowerBound} from '../lib/semver'; import {createStub, pkgHasFlowFiles} from '../lib/stubUtils'; import {listItem, sectionHeader} from '../lib/logger'; export const name = 'install [explicitLibDefs...]'; export const description = 'Installs libdefs into the ./flow-typed directory'; export type Args = { flowVersion?: mixed, // string overwrite: mixed, // boolean skip: mixed, // boolean skipCache?: mixed, // boolean skipFlowRestart?: mixed, // boolean verbose: mixed, // boolean libdefDir?: mixed, // string cacheDir?: mixed, // string packageDir?: mixed, // string ignoreDeps?: mixed, // Array<string> rootDir?: mixed, // string, useCacheUntil?: mixed, // number (milliseconds) explicitLibDefs: mixed, // Array<string> ... }; export function setup(yargs: Yargs): 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', }, }); } export async function run(args: Args): Promise<number> { const cwd = typeof args.rootDir === 'string' ? path.resolve(args.rootDir) : process.cwd(); const packageDir = typeof args.packageDir === 'string' ? path.resolve(args.packageDir) : cwd; const flowVersion = await 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 = getFtConfig(cwd); if (args.cacheDir) { const cacheDir = path.resolve(String(args.cacheDir)); console.log('• Setting cache dir', cacheDir); setCustomCacheDir(cacheDir); } const useCacheUntil = Number(args.useCacheUntil) || 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) || CACHE_REPO_EXPIRY, ftConfig, }); if (npmLibDefResult !== 0) { return npmLibDefResult; } // Must be after `installNpmLibDefs` to ensure cache is updated first if (ftConfig?.env || dependencyEnvs.length > 0) { const envLibDefResult = await installEnvLibDefs( [...new Set([...(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 child_process.execP('npx flow stop'); await child_process.execP('npx flow'); } catch (e) { console.log(colors.red('!! Flow restarted with some errors')); } } return 0; } async function installEnvLibDefs( env: Array<string>, flowVersion: FlowVersion, flowProjectRoot: string, libdefDir: string, useCacheUntil: number, overwrite: boolean, ): Promise<number> { if (env) { console.log( colors.green( '• `env` key found in `flow-typed.config.json`, attempting to install env definitions...\n', ), ); if (!Array.isArray(env)) { console.log( colors.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 getEnvDefs(); const flowTypedDirPath = path.join( flowProjectRoot, libdefDir, 'environments', ); await 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 findEnvDef(en, flowVersion, useCacheUntil, envDefs); if (def) { const fileName = `${en}.js`; const defLocalPath = path.join(flowTypedDirPath, fileName); const envAlreadyInstalled = await fs.exists(defLocalPath); if (envAlreadyInstalled) { const localFile = fs.readFileSync(defLocalPath, 'utf-8'); if (!verifySignedCode(localFile) && !overwrite) { listItem( en, colors.red( `${en} already exists and appears to have been manually written or changed!`, ), colors.yellow( `Consider contributing your changes back to flow-typed repository :)`, ), colors.yellow( `${en} already exists and appears to have been manually written or changed!`, ), colors.yellow( `Read more at https://github.com/flow-typed/flow-typed/blob/main/CONTRIBUTING.md`, ), colors.yellow( `Use --overwrite to overwrite the existing env defs.`, ), ); return; } fs.unlink(defLocalPath); } const repoVersion = await getEnvDefVersionHash( getCacheRepoDir(), def, ); const codeSignPreprocessor = signCodeStream(repoVersion); await copyFile(def.path, defLocalPath, codeSignPreprocessor); listItem( en, colors.green( `.${path.sep}${path.relative(flowProjectRoot, defLocalPath)}`, ), ); } else { listItem( en, colors.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']; type installNpmLibDefsArgs = {| cwd: string, flowVersion: FlowVersion, explicitLibDefs: Array<string>, libdefDir: string, verbose: boolean, overwrite: boolean, skip: boolean, skipCache: boolean, ignoreDeps: Array<string>, useCacheUntil: number, ftConfig?: FtConfig, |}; async function installNpmLibDefs({ cwd, flowVersion, explicitLibDefs, libdefDir, verbose, overwrite, skip, skipCache, ignoreDeps, useCacheUntil, ftConfig = {}, }: installNpmLibDefsArgs): Promise<{| status: number, dependencyEnvs: Array<string>, |}> { const flowProjectRoot = await 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: Map<string, string> = new Map(); let ignoreDefs: Array<string>; if (Array.isArray(ftConfig.ignore)) { ignoreDefs = ftConfig.ignore; } else { try { ignoreDefs = fs .readFileSync(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 getPackageJsonData(cwd); workspacesPkgJsonData = await findWorkspacesPackages( cwd, pkgJsonData, ftConfig, ); const pkgJsonDeps = workspacesPkgJsonData.reduce((acc, pckData) => { return mergePackageJsonDependencies( acc, getPackageJsonDependencies(pckData, [], []), ); }, 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 getPackageJsonData(cwd); workspacesPkgJsonData = await findWorkspacesPackages( cwd, pkgJsonData, ftConfig, ); const pkgJsonDeps = workspacesPkgJsonData.reduce((acc, pckData) => { return mergePackageJsonDependencies( acc, getPackageJsonDependencies(pckData, ignoreDeps, ignoreDefs), ); }, 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: Map<string, NpmLibDef> = new Map(); const outdatedLibDefsToInstall: [ NpmLibDef, {name: string, ver: string}, ][] = []; const unavailableLibDefs = []; const defDepsToInstall: { [deps: string]: Array<string>, } = {}; // This updates the cache for all definition types, npm/env/etc const libDefs = await getCacheNpmLibDefs(useCacheUntil, skipCache); const getLibDefsToInstall = async (entries: Array<[string, string]>) => { 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 findNpmLibDef( name, ver, flowVersion, useCacheUntil, true, libDefs, ); if (libDef === null) { unavailableLibDefs.push({name, ver}); } else { libDefsToInstall.set(name, libDef); const libDefLower = getRangeLowerBound(libDef.version); const depLower = getRangeLowerBound(ver); if (semver.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 loadPnpResolver(await getPackageJsonData(cwd)); const dependencyEnvs: Array<string> = []; // If a package that's missing a flow-typed libdef has any .flow files, // we'll skip generating a stub for it. const untypedMissingLibDefs: Array<[string, string]> = []; const typedMissingLibDefs: Array<[string, string, string]> = []; await Promise.all( unavailableLibDefs.map(async ({name: pkgName, ver: pkgVer}) => { const {isFlowTyped, pkgPath} = await 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: FtConfig = await fs.readJson( 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: Array<[string, string]> = []; await Promise.all( typedMissingLibDefs.map(async typedLibDef => { const pkgJsonData = await getPackageJsonData( `${typedLibDef[2]}/package.json`, ); const pkgJsonDeps = 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) { sectionHeader('Running definition dependency check'); while (Object.keys(defDepsToInstall).length > 0) { await getLibDefsToInstall( Object.keys(defDepsToInstall) .map((dep): [string, string] => { 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 listItem( colors.yellow( `One of your definitions has a dependency to ${dep} @ version(s) ${defVersions.join( ', ', )}`, ), `You have version ${colors.yellow(libDef.version)} installed`, `We're overriding to a supported version to fix flow-typed ${colors.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]), ); } sectionHeader('Check complete'); } // Scan libdefs that are already installed const libDefsToUninstall = new Map<string, string>(); const alreadyInstalledLibDefs = await getInstalledNpmLibDefs( path.join(flowProjectRoot), libdefDir, ); [...alreadyInstalledLibDefs.entries()].forEach(([filePath, npmLibDef]) => { const fullFilePath = 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 = getScopedPackageName(libDef); if (libDefsToInstall.has(scopedPkgName)) { libDefsToUninstall.set(scopedPkgName, fullFilePath); } break; case 'Stub': break; default: (npmLibDef: empty); } }); if (libDefsToInstall.size > 0) { sectionHeader(`Installing ${libDefsToInstall.size} libDefs...`); const flowTypedDirPath = path.join(flowProjectRoot, libdefDir, 'npm'); await mkdirp(flowTypedDirPath); const results = await Promise.all( [...libDefsToInstall.values()].map(async (libDef: NpmLibDef) => { const toUninstall = libDefsToUninstall.get( getScopedPackageName(libDef), ); if (toUninstall != null) { await 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)', colors.yellow(`${libDef.name}_${libDef.version}`), colors.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( colors.red( `!! No flow@${flowVersionToSemver(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.', colors.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 createStub( flowProjectRoot, pkgName, pkgVerStr, overwrite, pnpResolver, /* typescript */ false, libdefDir, ); }), ); console.log( colors.red( `\n!! No flow@${flowVersionToSemver( 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 ` + `${colors.bold('https://github.com/flowtype/flow-typed/')}\n`, ); } } return { status: 0, dependencyEnvs, }; } async function installNpmLibDef( npmLibDef: NpmLibDef, npmDir: string, overwrite: boolean, ): Promise<boolean> { const scopedDir = npmLibDef.scope === null ? npmDir : path.join(npmDir, npmLibDef.scope); mkdirp(scopedDir); const fileName = `${npmLibDef.name}_${npmLibDef.version}.js`; const filePath = path.join(scopedDir, fileName); // Find the libDef in the cached repo try { const terseFilePath = path.relative( path.resolve(npmDir, '..', '..'), filePath, ); if (!overwrite && (await fs.exists(filePath))) { listItem( colors.bold( colors.red( `${terseFilePath} already exists and appears to have been manually ` + `written or changed!`, ), ), colors.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 getNpmLibDefVersionHash( getCacheRepoDir(), npmLibDef, ); const codeSignPreprocessor = signCodeStream(repoVersion); await copyFile(npmLibDef.path, filePath, codeSignPreprocessor); listItem(fileName, colors.green(`.${path.sep}${terseFilePath}`)); // Remove any lingering stubs console.log(npmLibDef.name); console.log(scopedDir); const stubName = `${npmLibDef.name}_vx.x.x.js`; const stubPath = path.join(scopedDir, stubName); if (overwrite && (await fs.exists(stubPath))) { await fs.unlink(stubPath); } return true; } catch (e) { console.error(` !! Failed to install ${npmLibDef.name} at ${filePath}`); console.error(` ERROR: ${e.message}`); return false; } } export { installNpmLibDefs as _installNpmLibDefs, installNpmLibDef as _installNpmLibDef, };