flow-typed
Version:
A repository of high quality flow type definitions
584 lines (535 loc) • 15.7 kB
Flow
/* @flow */
import colors from 'colors/safe';
import got from 'got';
import flowgen from 'flowgen';
import prettier from 'prettier';
import {determineFlowVersion} from './npm/npmProjectUtils';
import {findPackageJsonPath} from './npm/npmProjectUtils';
import {format} from 'util';
import {fs} from './node';
import globAsync from 'glob';
import {getPackageJsonData} from './npm/npmProjectUtils';
import {
getPackageJsonDependencies,
type PnpResolver,
type PkgJson,
} from './npm/npmProjectUtils';
import {mkdirp} from './fileUtils';
import {path} from './node';
import {signCode} from './codeSign';
import {verifySignedCode} from './codeSign';
import {versionToString} from './semver';
import {listItem} from './logger';
export function glob(pattern: string, options: Object): Promise<Array<string>> {
return new Promise((resolve, reject) =>
globAsync(pattern, options, (err, files) => {
if (err) {
reject(err);
} else {
resolve(files);
}
}),
);
}
async function resolvePkgDirPath(
pkgName: string,
pkgJsonDirPath: string,
pnpjs: PnpResolver | null,
): Promise<string> {
if (pnpjs != null) {
const pnpResolvedDirPath = pnpjs.resolveToUnqualified(
pkgName,
pkgJsonDirPath,
);
if (pnpResolvedDirPath != null) {
return pnpResolvedDirPath;
}
}
let prevNodeModulesDirPath;
let nodeModulesDirPath = path.resolve(pkgJsonDirPath, 'node_modules');
while (true) {
const pkgDirPath = path.resolve(nodeModulesDirPath, pkgName);
if (await fs.exists(pkgDirPath)) {
return pkgDirPath;
}
prevNodeModulesDirPath = nodeModulesDirPath;
nodeModulesDirPath = path.resolve(
nodeModulesDirPath,
'..',
'..',
'node_modules',
);
if (prevNodeModulesDirPath === nodeModulesDirPath) {
break;
}
}
throw new Error(
'Unable to find `' +
pkgName +
'/` install directory! ' +
'Did you forget to run `npm install` before running `flow-typed install`?',
);
}
const moduleStubTemplate = `
declare module '%s' {
declare module.exports: any;
}`.trim();
const aliasTemplate = `
declare module '%s%s' {
declare module.exports: $Exports<'%s'>;
}`.trim();
const guessedModuleStubTemplate = `
declare module '%s' {
declare module.exports: %s;
}`.trim();
function stubFor(moduleName: string, fileExt?: string): string {
const moduleStub = format(moduleStubTemplate, moduleName);
if (fileExt !== undefined) {
const aliasStub = format(aliasTemplate, moduleName, fileExt, moduleName);
return `${moduleStub}\n${aliasStub}`;
}
return moduleStub;
}
const functionTemplate = '(%s) => any';
const spaceByI = (i: number): string => ' '.repeat(i);
const keyTypeTemplate = ` %s: %s,\n`;
function objectToTypedTemplate(
obj: {...},
currentDepth: number,
maxDepth: number,
functionHeader?: string,
) {
const thisDepth = currentDepth + 1;
let formatedEntries = [];
if (functionHeader) {
formatedEntries.push(spaceByI(thisDepth + 1) + functionHeader);
}
for (let entrie of Object.entries(obj)) {
const toTypeResult = objectToType(
entrie[1],
maxDepth,
thisDepth,
thisDepth <= maxDepth,
);
formatedEntries.push(
format(
`${spaceByI(thisDepth)}${keyTypeTemplate}`,
entrie[0],
toTypeResult,
),
);
}
if (formatedEntries.length > 0) {
return `{\n${formatedEntries.join('')}${spaceByI(thisDepth)}}`;
} else {
return '{}';
}
}
function guessFunctionArguments(fun: Function): string {
let raw = fun.toString();
let args = raw.slice(raw.indexOf('(') + 1, raw.indexOf(')'));
args = args.replace(/ /g, '');
if (args.length > 0) {
args = args
.split(',')
.map((el: string) => `${el}: any`)
.join(', ');
}
return format(functionTemplate, args);
}
function functionToType(
fun: Function,
currentDepth: number,
maxDepth: number,
): string {
let output = guessFunctionArguments(fun);
let functionEntries = Object.entries(fun);
if (functionEntries.length > 0) {
return objectToTypedTemplate(
fun,
currentDepth,
maxDepth,
output.length > 0 ? output + ',\n' : undefined,
);
}
return output;
}
function objectToType(
obj: mixed,
maxDepth: number,
currentDepth: number = 0,
deep: boolean = true,
): string {
if (obj === null) return 'null';
// Every function that depends on objectToTypedTemplate need to check deep first
if (deep) {
if (typeof obj === 'object')
return objectToTypedTemplate(obj, currentDepth, maxDepth);
if (typeof obj === 'function')
return functionToType(obj, currentDepth, maxDepth);
}
if (typeof obj === 'object') return 'any';
if (typeof obj === 'function') return guessFunctionArguments(obj);
return typeof obj;
}
function guessedStubFor(
moduleName: string,
packagePath: string,
maxDepth: number = 1,
): string {
const module: {...} = (require: any)(packagePath);
const formattedTemplate: string = format(
guessedModuleStubTemplate,
moduleName,
objectToType(module, maxDepth),
);
return formattedTemplate;
}
async function writeStub(
projectRoot: string,
packageName: string,
packageVersion: string,
packageFolder: string | null,
overwrite: boolean,
files: Array<string>,
libdefDir: string,
maxDepth?: number,
typescriptTypingsPath?: string | null,
typescriptTypingsContent?: string | null,
): Promise<string> {
let flowgenOutput = null;
const flowgenTemplate = `
declare module '%s' {
%s
}
`.trim();
if (typescriptTypingsPath) {
const code = flowgen.compiler.compileDefinitionFile(typescriptTypingsPath);
try {
flowgenOutput = prettier.format(
format(flowgenTemplate, packageName, code),
{parser: 'babel-flow', singleQuote: true, semi: true},
);
} catch (e) {
if (
e.message.includes(
'`declare module` cannot be used inside another `declare module`',
)
) {
flowgenOutput = prettier.format(code, {
parser: 'babel-flow',
singleQuote: true,
semi: true,
});
} else {
flowgenOutput = format(flowgenTemplate, packageName, code);
}
}
} else if (typescriptTypingsContent) {
const code = flowgen.compiler.compileDefinitionString(
typescriptTypingsContent,
);
try {
flowgenOutput = prettier.format(
format(flowgenTemplate, packageName, code),
{parser: 'babel-flow', singleQuote: true, semi: true},
);
} catch (e) {
if (
e.message.includes(
'`declare module` cannot be used inside another `declare module`',
)
) {
flowgenOutput = prettier.format(code, {
parser: 'babel-flow',
singleQuote: true,
semi: true,
});
} else {
flowgenOutput = format(flowgenTemplate, packageName, code);
}
}
}
let output = [
'/**',
' * This is an autogenerated libdef stub for:',
' *',
` * '${packageName}'`,
' *',
' * Fill this stub out by replacing all the `any` types.',
' *',
' * Once filled out, we encourage you to share your work with the',
' * community by sending a pull request to:',
' * https://github.com/flowtype/flow-typed',
' */\n\n',
].join('\n');
if (packageFolder !== null && false) {
try {
output += guessedStubFor(packageName, packageFolder, maxDepth);
} catch (e) {
output += stubFor(packageName);
}
} else if (flowgenOutput) {
output += flowgenOutput;
} else {
output += stubFor(packageName);
}
if (files.length > 0) {
output += `
/**
* We include stubs for each file inside this npm package in case you need to
* require those files directly. Feel free to delete any files that aren't
* needed.
*/
`;
const [fileDecls, aliases] = files.reduce(
([fileDecls, aliases], file) => {
const ext = path.extname(file);
const name = file.substr(0, file.length - ext.length);
const moduleName = `${packageName}/${name}`;
if (name === 'index') {
aliases.push(format(aliasTemplate, moduleName, '', packageName));
aliases.push(format(aliasTemplate, moduleName, ext, packageName));
} else if (path.basename(name) === 'index') {
const dirModuleName = packageName + '/' + path.dirname(file);
fileDecls.push(format(moduleStubTemplate, dirModuleName));
aliases.push(format(aliasTemplate, moduleName, '', dirModuleName));
aliases.push(format(aliasTemplate, moduleName, ext, dirModuleName));
} else {
fileDecls.push(format(moduleStubTemplate, moduleName));
aliases.push(format(aliasTemplate, moduleName, ext, moduleName));
}
return [fileDecls, aliases];
},
[[], []],
);
output += fileDecls.join('\n\n');
output += '\n\n// Filename aliases\n';
output += aliases.join('\n');
}
output += '\n'; // File should end with a newline
const filename = path.join(
projectRoot,
libdefDir,
'npm',
format('%s_vx.x.x.js', packageName),
);
await mkdirp(path.dirname(filename));
if (!overwrite) {
const exists = await fs.exists(filename);
if (exists) {
const existingStub = await fs.readFile(filename, 'utf8');
if (!verifySignedCode(existingStub)) {
throw new Error(
'Stub already exists and has been modified. ' +
'Use --overwrite to overwrite',
);
}
}
}
const flowVersionRaw = await determineFlowVersion(projectRoot);
const flowVersion = flowVersionRaw
? `/flow_${versionToString(flowVersionRaw)}`
: '';
const stubVersion = `<<STUB>>/${packageName}_v${packageVersion}${flowVersion}`;
await fs.writeFile(filename, signCode(output, stubVersion));
return filename;
}
/**
* Look across a project root node_modules as well as
* each workspace's node_modules to find the dependency
* to determine if it is flow typed.
*/
export async function pkgHasFlowFiles(
projectRoot: string,
packageName: string,
pnpjs: PnpResolver | null,
workspacesPkgJsonData: Array<PkgJson>,
): Promise<{|
isFlowTyped: boolean,
pkgPath?: string,
|}> {
const findTypedFiles = async (path: string): Promise<void | string> => {
try {
let pathToPackage = await resolvePkgDirPath(packageName, path, pnpjs);
const typedFiles = await glob('**/*.flow', {
cwd: pathToPackage,
ignore: 'node_modules/**',
});
if (typedFiles.length > 0) {
return pathToPackage;
}
} catch (e) {
return undefined;
}
};
const rootTypedPath = await findTypedFiles(projectRoot);
if (rootTypedPath) {
return {
isFlowTyped: true,
pkgPath: rootTypedPath,
};
}
const typedWorkspacePaths = await Promise.all(
workspacesPkgJsonData.map(async pkgJson =>
findTypedFiles(path.dirname(pkgJson.pathStr)),
),
);
const workspacePath = typedWorkspacePaths.find(o => !!o);
if (workspacePath) {
return {
isFlowTyped: true,
pkgPath: workspacePath,
};
}
return {
isFlowTyped: false,
};
}
async function getDefinitelyTypedPackage(
packageName: string,
explicitVersion: string | null,
) {
const parts = packageName.split('/');
const typesPackageName = packageName.startsWith('@')
? parts[0].slice(1) + '__' + parts[1]
: packageName;
const version = explicitVersion ? `@${explicitVersion}` : '';
const typing = `https://unpkg.com/@types/${typesPackageName}${version}/index.d.ts`;
return got(typing, {method: 'GET'});
}
/**
* createStub("/path/to/root", "foo") will create a file
* /path/to/root/flow-typed/npm/foo.js that contains a stub for the module foo.
*
* If foo is installed, it will read the directory that require("foo") resolves
* to and include definitions for "foo/FILE", for every FILE in the foo package
*/
export async function createStub(
projectRoot: string,
packageName: string,
explicitVersion: string | null,
overwrite: boolean,
pnpjs: PnpResolver | null,
typescript: boolean,
libdefDir?: string,
maxDepth?: number,
): Promise<boolean> {
let files = [];
let resolutionError = null;
let pathToPackage = null;
let version = explicitVersion || null;
let typescriptTypingsPath = null;
let typescriptTypingsContent = null;
const typedefDir = libdefDir || 'flow-typed';
try {
pathToPackage = await resolvePkgDirPath(packageName, projectRoot, pnpjs);
files = await glob('**/*.{js,jsx}', {
cwd: pathToPackage,
ignore: 'node_modules/**',
});
} catch (e) {
resolutionError = e;
}
// Try to deduce a version if one isn't provided
if (version == null) {
// Look at the package.json for the installed module
if (pathToPackage != null) {
try {
version = (require: any)(path.join(pathToPackage, 'package.json'))
.version;
} catch (e) {}
}
}
if (typescript) {
if (typescriptTypingsPath == null) {
// Look at the package.json for the installed module
if (pathToPackage != null) {
try {
const pkg = (require: any)(path.join(pathToPackage, 'package.json'));
const typing = pkg.typings || pkg.types;
if (typing) typescriptTypingsPath = path.join(pathToPackage, typing);
} catch (e) {}
}
}
// If that failed, try looking for index.d.ts file
if (typescriptTypingsPath == null) {
// Look at the package.json for the installed module
if (pathToPackage != null) {
const typing = path.join(pathToPackage, 'index.d.ts');
if (await fs.exists(typing)) {
typescriptTypingsPath = typing;
}
}
}
if (typescriptTypingsPath == null) {
try {
const response = await getDefinitelyTypedPackage(
packageName,
explicitVersion,
);
typescriptTypingsContent = response.body;
} catch (e) {
console.log(
colors.red("❌\t%s%s': %s"),
packageName,
version ? '@' + version : '',
e.message,
);
return false;
}
}
}
// If that failed, try looking for a package.json in the root
if (version == null) {
try {
const pkgJsonPathStr = await findPackageJsonPath(projectRoot);
const pkgJsonData = await getPackageJsonData(pkgJsonPathStr);
const rootDependencies = await getPackageJsonDependencies(
pkgJsonData,
[],
[],
);
version = rootDependencies[packageName] || null;
} catch (e) {}
}
try {
if (version === null) {
throw new Error(
'Could not deduce version from node_modules or package.json. ' +
'Please provide an explicit version',
);
}
const filename = await writeStub(
projectRoot,
packageName,
version,
pathToPackage,
overwrite,
files,
typedefDir,
maxDepth,
typescriptTypingsPath,
typescriptTypingsContent,
);
const terseFilename = path.relative(projectRoot, filename);
listItem(
`${packageName}@${version}`,
colors.red(terseFilename),
resolutionError
? colors.yellow(
`Unable to stub all files in ${packageName}, so only created a stub for the main module (${resolutionError.message})`,
)
: undefined,
);
return true;
} catch (e) {
console.log(
colors.red("❌\t%s%s': %s"),
packageName,
version ? '@' + version : '',
e.message,
);
return false;
}
}