/* @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> { 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 { 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, libdefDir: string, maxDepth?: number, typescriptTypingsPath?: string | null, typescriptTypingsContent?: string | null, ): Promise { 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 = `<>/${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, ): Promise<{| isFlowTyped: boolean, pkgPath?: string, |}> { const findTypedFiles = async (path: string): Promise => { 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 { 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; } }