flow-typed
Version:
A repository of high quality flow type definitions
638 lines (520 loc) • 24.5 kB
JavaScript
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;
}
}
;