flow-typed
Version:
A repository of high quality flow type definitions
319 lines (289 loc) • 8.85 kB
Flow
// @flow
import colors from 'colors/safe';
import glob from 'glob';
import semver, {intersects} from 'semver';
import {load} from 'js-yaml';
import {searchUpDirPath} from '../fileUtils';
import type {FlowSpecificVer} from '../flowVersion';
import type {FtConfig} from '../ftConfig';
import {fs, path} from '../node';
import {stringToVersion, type Version} from '../semver';
export type PkgJson = {|
pathStr: string,
content: {
name: string,
version: string,
private?: boolean,
workspaces?: string[] | {packages: string[], ...},
installConfig?: {pnp?: boolean},
bundledDependencies?: {[pkgName: string]: string},
dependencies?: {[pkgName: string]: string},
devDependencies?: {[pkgName: string]: string},
optionalDependencies?: {[pkgName: string]: string},
peerDependencies?: {[pkgName: string]: string},
},
|};
export type PnpResolver = {|
resolveToUnqualified: (string, string) => string | null,
|};
const PKG_JSON_DEP_FIELDS = [
'dependencies',
'devDependencies',
'peerDependencies',
'bundledDependencies',
];
export async function findPackageJsonDepVersionStr(
pkgJson: PkgJson,
depName: string,
): Promise<null | string> {
let matchedFields = [];
const deps = PKG_JSON_DEP_FIELDS.reduce((deps, section) => {
const contentSection = pkgJson.content[section];
if (contentSection && contentSection[depName]) {
matchedFields.push(section);
deps.push(contentSection[depName]);
}
return deps;
}, []);
if (deps.length === 0) {
return null;
} else if (deps.length === 1) {
return deps.pop();
} else {
throw new Error(
`Found ${depName} listed in ${String(deps.length)} places in ` +
`${pkgJson.pathStr}!`,
);
}
}
export async function findPackageJsonPath(pathStr: string): Promise<string> {
const pkgJsonPathStr = await searchUpDirPath(
pathStr,
async p => await fs.exists(path.join(p, 'package.json')),
);
if (pkgJsonPathStr === null) {
throw new Error(`Unable to find a package.json for ${pathStr}!`);
}
return path.join(pkgJsonPathStr, 'package.json');
}
function getWorkspacePatterns(cwd: string, pkgJson: PkgJson): string[] {
const pnpmWorkspacePath = path.join(cwd, 'pnpm-workspace.yaml');
const hasPnpmWorkspaces = fs.existsSync(pnpmWorkspacePath);
if (hasPnpmWorkspaces) {
const doc = load(fs.readFileSync(pnpmWorkspacePath, 'utf-8'));
if (doc && typeof doc === 'object' && doc.packages) {
return doc.packages;
}
}
if (Array.isArray(pkgJson.content.workspaces)) {
return pkgJson.content.workspaces;
}
if (
pkgJson.content.workspaces &&
Array.isArray(pkgJson.content.workspaces.packages)
) {
return pkgJson.content.workspaces.packages;
}
return [];
}
async function findWorkspacesPackagePaths(
pkgJson: PkgJson,
workspaces: Array<string>,
): Promise<string[]> {
const tasks = await Promise.all(
workspaces.map(pattern => {
return new Promise((resolve, reject) => {
glob(
`${path.dirname(pkgJson.pathStr)}/${pattern}/package.json`,
{absolute: true},
(err, files) => {
if (err) {
reject(err);
} else {
resolve(files);
}
},
);
});
}),
);
return tasks.flat();
}
export async function findWorkspacesPackages(
cwd: string,
pkgJson: PkgJson,
ftConfig: FtConfig,
): Promise<PkgJson[]> {
const paths = await findWorkspacesPackagePaths(
pkgJson,
getWorkspacePatterns(cwd, pkgJson),
);
const configPaths = ftConfig.workspaces
? await findWorkspacesPackagePaths(pkgJson, ftConfig.workspaces)
: [];
return Promise.all(
[...paths, ...configPaths].map(async pathStr => {
const pkgJsonContent = await fs.readJson(pathStr);
return {
pathStr,
content: pkgJsonContent,
};
}),
);
}
// TODO: Write tests for this
export function getPackageJsonDependencies(
pkgJson: PkgJson,
/**
* dependency groups to ignore
*
* eg: dev, optional, bundled, peer, etc
*/
ignoreDeps: Array<string>,
/**
* dependencies or scopes of dependencies to be ignored
*/
ignoreDefs: Array<string>,
): {[depName: string]: string} {
const depFields = PKG_JSON_DEP_FIELDS.filter(field => {
return ignoreDeps.indexOf(field.slice(0, -'Dependencies'.length)) === -1;
});
return depFields.reduce<{[key: string]: string}>((deps, section) => {
const contentSection = pkgJson.content[section];
if (contentSection) {
Object.keys(contentSection).forEach(pkgName => {
if (deps[pkgName]) {
console.warn(`Found ${pkgName} listed twice in package.json!`);
}
const pkgIgnored = ignoreDefs.some(cur => {
const ignoreDef = cur.trim();
if (ignoreDef === '') return false;
// if we are looking to ignore a scope dir
if (
ignoreDef.charAt(0) === '@' &&
(ignoreDef.indexOf('/') === -1 ||
ignoreDef.indexOf('/') === ignoreDef.length - 1)
) {
return pkgName.startsWith(ignoreDef);
}
return pkgName === ignoreDef;
});
if (pkgIgnored) return;
deps[pkgName] = contentSection[pkgName];
});
}
return deps;
}, {});
}
export function mergePackageJsonDependencies(
a: {[depName: string]: string},
b: {[depName: string]: string},
): {[depName: string]: string} {
const result = {...a};
for (const dep of Object.keys(b)) {
const version = b[dep];
let doesIntersect;
try {
doesIntersect = intersects(result[dep], version);
} catch (e) {
doesIntersect = result[dep] === version;
}
if (a[dep] != null && !doesIntersect) {
console.log(
colors.yellow(
"\t Conflicting versions for '%s' between '%s' and '%s'",
),
dep,
a[dep],
version,
);
} else {
result[dep] = version;
}
}
return result;
}
export async function getPackageJsonData(pathStr: string): Promise<PkgJson> {
const pkgJsonPath = await findPackageJsonPath(pathStr);
const pkgJsonContent = await fs.readJson(pkgJsonPath);
return {
pathStr: pkgJsonPath,
content: pkgJsonContent,
};
}
export async function determineFlowVersion(
pathStr: string,
): Promise<null | Version> {
const pkgJsonData = await getPackageJsonData(pathStr);
const flowBinVersionStr = await findPackageJsonDepVersionStr(
pkgJsonData,
'flow-bin',
);
if (flowBinVersionStr !== null) {
let flowVerStr;
if (semver.valid(flowBinVersionStr)) {
flowVerStr = flowBinVersionStr;
} else {
const flowVerRange = new semver.Range(flowBinVersionStr);
if (flowVerRange.set[0].length !== 2) {
const cliPkgJson = require('../../../package.json');
const cliFlowVer = cliPkgJson.devDependencies['flow-bin'];
throw new Error(
`Unable to extract flow-bin version from package.json!\n` +
`Never use a complex version range with flow-bin. Always use a ` +
`specific version (i.e. "${cliFlowVer}").`,
);
}
flowVerStr = flowVerRange.set[0][0].semver.version;
}
return stringToVersion('v' + flowVerStr);
}
return null;
}
export async function loadPnpResolver(
pkgJson: PkgJson,
): Promise<PnpResolver | null> {
if (!(pkgJson.content.installConfig && pkgJson.content.installConfig.pnp)) {
return null;
}
const pnpJsFile = path.resolve(pkgJson.pathStr, '..', '.pnp.js');
if (await fs.exists(pnpJsFile)) {
// $FlowFixMe[unsupported-syntax]
return require(pnpJsFile);
}
throw new Error(
'Unable to find Yarn PNP resolver lib: `.pnp.js`! ' +
'Did you forget to run `yarn install` before running `flow-typed install`?',
);
}
export async function findFlowSpecificVer(
startingPath: string,
): Promise<FlowSpecificVer> {
const flowSemver = await determineFlowVersion(startingPath);
if (flowSemver === null) {
throw new Error(
'Failed to find a flow-bin dependency in package.json.\n' +
'Please install flow-bin: `npm install --save-dev flow-bin`',
);
}
if (flowSemver.range !== undefined) {
throw new Error(
`Unable to extract flow-bin version from package.json!\n` +
`Never use a complex version range with flow-bin. Always use a ` +
`specific major/minor version (i.e. "^0.39").`,
);
}
const major = flowSemver.major;
if (major === 'x') {
throw new Error(
`Unable to extract flow-bin version from package.json!\n` +
`Never use a wildcard major version with flow-bin!`,
);
}
return {
major,
minor: flowSemver.minor,
patch: flowSemver.patch,
prerel: flowSemver.prerel == null ? null : flowSemver.prerel,
};
}