From 623bb0664d615cf12fbb22526ebe4e4574b59c5c Mon Sep 17 00:00:00 2001 From: Alexey Pyzhianov Date: Fri, 15 May 2020 17:06:14 +0300 Subject: [PATCH] Convert create-cli.js (gatsby-cli) to TypeScript (#23650) * chore(gatsby-cli) Add basic type annotations to create-cli.ts * Update resolve-cwd to 3.0.0 as it has TS definitions * Add @types/yargs * Add most obvious type annotations and checks to create-cli.ts * Make report.panic return "never" as it's supposed to NOTE: This still leaves a lot of any types in create-cli, especially when it comes to require('./some-local-file') expressions and parameters/options of yargs commands. Making everything type-safe might introduce a lot of complexity though, so one should think about benefits vs costs of doing this * chore(create-cli) Replace .fail with .recommendCommands * chore(gatsby-cli) Add types and type checks to command handlers NOTE: gatsby new arguments are now stringified before use. This makes the compiler happy and also it's now possible to init a gastsby project inside a folder with number as a name, in case you want to "gatsby new 42", which caused an error before * Address test failures Co-authored-by: Blaine Kasten --- .../gatsby-cli/__tests__/develop.js | 2 +- packages/gatsby-cli/package.json | 3 +- packages/gatsby-cli/src/__tests__/index.js | 12 +- .../src/{create-cli.js => create-cli.ts} | 151 ++++++++++-------- packages/gatsby-cli/src/did-you-mean.ts | 20 --- packages/gatsby-cli/src/index.ts | 2 +- packages/gatsby-cli/src/init-starter.ts | 14 +- .../gatsby-cli/src/reporter/patch-console.ts | 2 +- packages/gatsby-cli/src/reporter/reporter.ts | 4 +- yarn.lock | 9 +- 10 files changed, 109 insertions(+), 110 deletions(-) rename packages/gatsby-cli/src/{create-cli.js => create-cli.ts} (80%) delete mode 100644 packages/gatsby-cli/src/did-you-mean.ts diff --git a/integration-tests/gatsby-cli/__tests__/develop.js b/integration-tests/gatsby-cli/__tests__/develop.js index 2fadccac3a6d8..df87734a0935d 100644 --- a/integration-tests/gatsby-cli/__tests__/develop.js +++ b/integration-tests/gatsby-cli/__tests__/develop.js @@ -19,7 +19,7 @@ describe(`gatsby develop`, () => { const [childProcess, getLogs] = GatsbyCLI.from(cwd).invokeAsync(`develop`) // 2. Wait for the build process to finish - await timeout(10) + await timeout(15) // 3. kill the `gatsby develop` command so we can get logs childProcess.kill() diff --git a/packages/gatsby-cli/package.json b/packages/gatsby-cli/package.json index e0aa0e6076171..a686a85087c9c 100644 --- a/packages/gatsby-cli/package.json +++ b/packages/gatsby-cli/package.json @@ -40,7 +40,7 @@ "prompts": "^2.3.2", "react": "^16.8.0", "redux": "^4.0.5", - "resolve-cwd": "^2.0.0", + "resolve-cwd": "^3.0.0", "semver": "^6.3.0", "signal-exit": "^3.0.3", "source-map": "0.7.3", @@ -55,6 +55,7 @@ "@babel/cli": "^7.8.4", "@babel/core": "^7.9.6", "@types/hosted-git-info": "^3.0.0", + "@types/yargs": "^15.0.4", "babel-preset-gatsby-package": "^0.4.1", "cross-env": "^5.2.1" }, diff --git a/packages/gatsby-cli/src/__tests__/index.js b/packages/gatsby-cli/src/__tests__/index.js index 7280d3495e6b4..0391d33168e1b 100644 --- a/packages/gatsby-cli/src/__tests__/index.js +++ b/packages/gatsby-cli/src/__tests__/index.js @@ -12,13 +12,13 @@ const getCLI = () => { jest.resetModules() const reporter = require(`../reporter`) - const createCLI = require(`../create-cli`) + const { createCli } = require(`../create-cli`) require(`../`) return { reporter, - createCLI, + createCli, } } @@ -93,10 +93,10 @@ describe(`normal behavior`, () => { }) }) - it(`invokes createCLI`, () => { - const { createCLI } = setup() + it(`invokes createCli`, () => { + const { createCli } = setup() - expect(createCLI).toHaveBeenCalledTimes(1) - expect(createCLI).toHaveBeenCalledWith(process.argv) + expect(createCli).toHaveBeenCalledTimes(1) + expect(createCli).toHaveBeenCalledWith(process.argv) }) }) diff --git a/packages/gatsby-cli/src/create-cli.js b/packages/gatsby-cli/src/create-cli.ts similarity index 80% rename from packages/gatsby-cli/src/create-cli.js rename to packages/gatsby-cli/src/create-cli.ts index 17e749a8761db..dc2b26a82b479 100644 --- a/packages/gatsby-cli/src/create-cli.js +++ b/packages/gatsby-cli/src/create-cli.ts @@ -1,28 +1,24 @@ -const path = require(`path`) -const resolveCwd = require(`resolve-cwd`) -const yargs = require(`yargs`) -const report = require(`./reporter`) -const { setStore } = require(`./reporter/redux`) -const { didYouMean } = require(`./did-you-mean`) -const { getLocalGatsbyVersion } = require(`./util/version`) -const envinfo = require(`envinfo`) -const existsSync = require(`fs-exists-cached`).sync -const clipboardy = require(`clipboardy`) -const { - trackCli, - setDefaultTags, - setTelemetryEnabled, -} = require(`gatsby-telemetry`) -const { recipesHandler } = require(`./recipes`) - -const handlerP = fn => (...args) => { +import path from "path" +import resolveCwd from "resolve-cwd" +import yargs from "yargs" +import report from "./reporter" +import { setStore } from "./reporter/redux" +import { getLocalGatsbyVersion } from "./util/version" +import envinfo from "envinfo" +import { sync as existsSync } from "fs-exists-cached" +import clipboardy from "clipboardy" +import { trackCli, setDefaultTags, setTelemetryEnabled } from "gatsby-telemetry" +import { initStarter } from "./init-starter" +import { recipesHandler } from "./recipes" + +const handlerP = (fn: Function) => (...args: unknown[]): void => { Promise.resolve(fn(...args)).then( () => process.exit(0), err => report.panic(err) ) } -function buildLocalCommands(cli, isLocalSite) { +function buildLocalCommands(cli: yargs.Argv, isLocalSite: boolean): void { const defaultHost = `localhost` const defaultPort = `8000` const directory = path.resolve(`.`) @@ -33,7 +29,12 @@ function buildLocalCommands(cli, isLocalSite) { ? [`> 1%`, `last 2 versions`, `IE >= 9`] : [`>0.25%`, `not dead`] - const siteInfo = { directory, browserslist: DEFAULT_BROWSERS } + const siteInfo = { + directory, + browserslist: DEFAULT_BROWSERS, + sitePackageJson: undefined, + } + const useYarn = existsSync(path.join(directory, `yarn.lock`)) if (isLocalSite) { const json = require(path.join(directory, `package.json`)) @@ -41,17 +42,17 @@ function buildLocalCommands(cli, isLocalSite) { siteInfo.browserslist = json.browserslist || siteInfo.browserslist } - function getLocalGatsbyMajorVersion() { - let version = getLocalGatsbyVersion() + function getLocalGatsbyMajorVersion(): number | undefined { + const version = getLocalGatsbyVersion() if (version) { - version = Number(version.split(`.`)[0]) + return Number(version.split(`.`)[0]) } - return version + return undefined } - function resolveLocalCommand(command) { + function resolveLocalCommand(command: string): Function | never { if (!isLocalSite) { cli.showHelp() report.verbose(`current directory: ${directory}`) @@ -73,7 +74,15 @@ function buildLocalCommands(cli, isLocalSite) { ) report.verbose(`loading local command from: ${cmdPath}`) - return require(cmdPath) + + const cmd = require(cmdPath) + if (cmd instanceof Function) { + return cmd + } + + return report.panic( + `Handler for command "${command}" is not a function. Your Gatsby package might be corrupted, try reinstalling it and running the command again.` + ) } catch (err) { cli.showHelp() return report.panic( @@ -83,11 +92,14 @@ function buildLocalCommands(cli, isLocalSite) { } } - function getCommandHandler(command, handler) { - return argv => { + function getCommandHandler( + command: string, + handler?: (args: yargs.Arguments, cmd: Function) => void + ) { + return (argv: yargs.Arguments): void => { report.setVerbose(!!argv.verbose) - report.setNoColor(argv.noColor || process.env.NO_COLOR) + report.setNoColor(!!(argv.noColor || process.env.NO_COLOR)) process.env.gatsby_log_level = argv.verbose ? `verbose` : `normal` report.verbose(`set gatsby_log_level: "${process.env.gatsby_log_level}"`) @@ -105,7 +117,7 @@ function buildLocalCommands(cli, isLocalSite) { cli.command({ command: `develop`, - desc: + describe: `Start development server. Watches files, rebuilds, and hot reloads ` + `if something changes`, builder: _ => @@ -160,20 +172,20 @@ function buildLocalCommands(cli, isLocalSite) { describe: `Tracer configuration file (OpenTracing compatible). See https://gatsby.dev/tracing`, }), handler: handlerP( - getCommandHandler(`develop`, (args, cmd) => { + getCommandHandler(`develop`, (args: yargs.Arguments, cmd: Function) => { process.env.NODE_ENV = process.env.NODE_ENV || `development` cmd(args) // Return an empty promise to prevent handlerP from exiting early. // The development server shouldn't ever exit until the user directly // kills it so this is fine. - return new Promise(resolve => {}) + return new Promise(() => {}) }) ), }) cli.command({ command: `build`, - desc: `Build a Gatsby project.`, + describe: `Build a Gatsby project.`, builder: _ => _.option(`prefix-paths`, { type: `boolean`, @@ -216,7 +228,7 @@ function buildLocalCommands(cli, isLocalSite) { hidden: true, }), handler: handlerP( - getCommandHandler(`build`, (args, cmd) => { + getCommandHandler(`build`, (args: yargs.Arguments, cmd: Function) => { process.env.NODE_ENV = `production` return cmd(args) }) @@ -225,7 +237,7 @@ function buildLocalCommands(cli, isLocalSite) { cli.command({ command: `serve`, - desc: `Serve previously built Gatsby site.`, + describe: `Serve previously built Gatsby site.`, builder: _ => _.option(`H`, { alias: `host`, @@ -257,7 +269,7 @@ function buildLocalCommands(cli, isLocalSite) { cli.command({ command: `info`, - desc: `Get environment information for debugging and issue reporting`, + describe: `Get environment information for debugging and issue reporting`, builder: _ => _.option(`C`, { alias: `clipboard`, @@ -265,7 +277,7 @@ function buildLocalCommands(cli, isLocalSite) { default: false, describe: `Automagically copy environment information to clipboard`, }), - handler: args => { + handler: (args: yargs.Arguments) => { try { const copyToClipboard = // Clipboard is not accessible when on a linux tty @@ -311,27 +323,38 @@ function buildLocalCommands(cli, isLocalSite) { cli.command({ command: `clean`, - desc: `Wipe the local gatsby environment including built assets and cache`, + describe: `Wipe the local gatsby environment including built assets and cache`, handler: getCommandHandler(`clean`), }) cli.command({ command: `repl`, - desc: `Get a node repl with context of Gatsby environment, see (https://www.gatsbyjs.org/docs/gatsby-repl/)`, - handler: getCommandHandler(`repl`, (args, cmd) => { - process.env.NODE_ENV = process.env.NODE_ENV || `development` - return cmd(args) - }), + describe: `Get a node repl with context of Gatsby environment, see (https://www.gatsbyjs.org/docs/gatsby-repl/)`, + handler: getCommandHandler( + `repl`, + (args: yargs.Arguments, cmd: Function) => { + process.env.NODE_ENV = process.env.NODE_ENV || `development` + return cmd(args) + } + ), }) cli.command({ command: `recipes [recipe]`, - desc: `[EXPERIMENTAL] Run a recipe`, - handler: handlerP(({ recipe }) => recipesHandler(recipe)), + describe: `[EXPERIMENTAL] Run a recipe`, + handler: handlerP(({ recipe }: yargs.Arguments) => { + if (typeof recipe !== `string`) { + throw new Error( + `Error: gatsby recipes needs to be called with a specific recipe` + ) + } + + recipesHandler(recipe) + }), }) } -function isLocalGatsbySite() { +function isLocalGatsbySite(): boolean { let inGatsbySite = false try { const { dependencies, devDependencies } = require(path.resolve( @@ -346,7 +369,7 @@ function isLocalGatsbySite() { return !!inGatsbySite } -function getVersionInfo() { +function getVersionInfo(): string { const { version } = require(`../package.json`) const isGatsbySite = isLocalGatsbySite() if (isGatsbySite) { @@ -365,10 +388,11 @@ Gatsby version: ${gatsbyVersion} } } -module.exports = argv => { - const cli = yargs().parserConfiguration({ +export const createCli = (argv: string[]): yargs.Arguments => { + const cli = yargs(argv).parserConfiguration({ "boolean-negation": false, }) + const isLocalSite = isLocalGatsbySite() cli @@ -415,17 +439,19 @@ module.exports = argv => { return cli .command({ command: `new [rootPath] [starter]`, - desc: `Create new Gatsby project.`, - handler: handlerP(({ rootPath, starter }) => { - const { initStarter } = require(`./init-starter`) - return initStarter(starter, { rootPath }) + describe: `Create new Gatsby project.`, + handler: handlerP(async ({ rootPath, starter }) => { + const starterStr = starter ? String(starter) : undefined + const rootPathStr = rootPath ? String(rootPath) : undefined + + await initStarter(starterStr, rootPathStr) }), }) .command(`plugin`, `Useful commands relating to Gatsby plugins`, yargs => yargs .command({ command: `docs`, - desc: `Helpful info about using and creating plugins`, + describe: `Helpful info about using and creating plugins`, handler: handlerP(() => console.log(` Using a plugin: @@ -456,7 +482,7 @@ Creating a plugin: ) .command({ command: `telemetry`, - desc: `Enable or disable Gatsby anonymous analytics collection.`, + describe: `Enable or disable Gatsby anonymous analytics collection.`, builder: yargs => yargs .option(`enable`, { @@ -468,7 +494,7 @@ Creating a plugin: description: `Disable telemetry`, }), - handler: handlerP(({ enable, disable }) => { + handler: handlerP(({ enable, disable }: yargs.Arguments) => { const enabled = enable || !disable setTelemetryEnabled(enabled) report.log(`Telemetry collection ${enabled ? `enabled` : `disabled`}`) @@ -477,17 +503,6 @@ Creating a plugin: .wrap(cli.terminalWidth()) .demandCommand(1, `Pass --help to see all available commands and options.`) .strict() - .fail((msg, err, yargs) => { - const availableCommands = yargs.getCommands().map(commandDescription => { - const [command] = commandDescription - return command.split(` `)[0] - }) - const arg = argv.slice(2)[0] - const suggestion = arg ? didYouMean(arg, availableCommands) : `` - - cli.showHelp() - report.log(suggestion) - report.log(msg) - }) + .recommendCommands() .parse(argv.slice(2)) } diff --git a/packages/gatsby-cli/src/did-you-mean.ts b/packages/gatsby-cli/src/did-you-mean.ts deleted file mode 100644 index 1144f06c6fb8a..0000000000000 --- a/packages/gatsby-cli/src/did-you-mean.ts +++ /dev/null @@ -1,20 +0,0 @@ -import meant from "meant" - -export function didYouMean(scmd: string, commands: string[]): string { - const bestSimilarity = meant(scmd, commands).map(function ( - str: string - ): string { - return ` ${str}` - }) - - if (bestSimilarity.length === 0) return `` - if (bestSimilarity.length === 1) { - return `\nDid you mean this?\n ${bestSimilarity[0]}\n` - } else { - return ( - [`\nDid you mean one of these?`] - .concat(bestSimilarity.slice(0, 3)) - .join(`\n`) + `\n` - ) - } -} diff --git a/packages/gatsby-cli/src/index.ts b/packages/gatsby-cli/src/index.ts index d447b1a2b3cd9..7c131ae25ab6a 100755 --- a/packages/gatsby-cli/src/index.ts +++ b/packages/gatsby-cli/src/index.ts @@ -4,7 +4,7 @@ import "@babel/polyfill" import os from "os" import semver from "semver" import util from "util" -import createCli from "./create-cli" +import { createCli } from "./create-cli" import report from "./reporter" import pkg from "../package.json" import updateNotifier from "update-notifier" diff --git a/packages/gatsby-cli/src/init-starter.ts b/packages/gatsby-cli/src/init-starter.ts index 5e0765c7b4cf6..f36dd7f943358 100644 --- a/packages/gatsby-cli/src/init-starter.ts +++ b/packages/gatsby-cli/src/init-starter.ts @@ -211,8 +211,8 @@ interface IGetPaths { } const getPaths = async ( - starterPath: string, - rootPath: string + starterPath?: string, + rootPath?: string ): Promise => { let selectedOtherStarter = false @@ -260,10 +260,6 @@ const getPaths = async ( return { starterPath, rootPath, selectedOtherStarter } } -interface IInitOptions { - rootPath: string -} - const successMessage = (path: string): void => { report.info(` Your new Gatsby site has been successfully bootstrapped. Start developing it by running: @@ -277,12 +273,12 @@ Your new Gatsby site has been successfully bootstrapped. Start developing it by * Main function that clones or copies the starter. */ export async function initStarter( - starter: string, - options: IInitOptions + starter?: string, + root?: string ): Promise { const { starterPath, rootPath, selectedOtherStarter } = await getPaths( starter, - options.rootPath + root ) const urlObject = url.parse(rootPath) diff --git a/packages/gatsby-cli/src/reporter/patch-console.ts b/packages/gatsby-cli/src/reporter/patch-console.ts index f10d27e0e96c6..eb6552762c8d7 100644 --- a/packages/gatsby-cli/src/reporter/patch-console.ts +++ b/packages/gatsby-cli/src/reporter/patch-console.ts @@ -19,6 +19,6 @@ export const patchConsole = (reporter: typeof gatsbyReporter): void => { reporter.info(util.format(format === undefined ? `` : format, ...rest)) } console.error = (format: any, ...args: any[]): void => { - reporter.error(util.format(format, ...args)) + reporter.error(util.format(format === undefined ? `` : format, ...args)) } } diff --git a/packages/gatsby-cli/src/reporter/reporter.ts b/packages/gatsby-cli/src/reporter/reporter.ts index 54f3c73c3c3f4..ae25817236a6d 100644 --- a/packages/gatsby-cli/src/reporter/reporter.ts +++ b/packages/gatsby-cli/src/reporter/reporter.ts @@ -65,11 +65,11 @@ class Reporter { /** * Log arguments and exit process with status 1. */ - panic = (errorMeta: ErrorMeta, error?: Error | Error[]): void => { + panic = (errorMeta: ErrorMeta, error?: Error | Error[]): never => { const reporterError = this.error(errorMeta, error) trackError(`GENERAL_PANIC`, { error: reporterError }) prematureEnd() - process.exit(1) + return process.exit(1) } panicOnBuild = ( diff --git a/yarn.lock b/yarn.lock index 38b8dc6eec17c..650f704d4bf91 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4384,7 +4384,7 @@ dependencies: "@types/yargs-parser" "*" -"@types/yargs@^15.0.0": +"@types/yargs@^15.0.0", "@types/yargs@^15.0.4": version "15.0.4" resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-15.0.4.tgz#7e5d0f8ca25e9d5849f2ea443cf7c402decd8299" integrity sha512-9T1auFmbPZoxHz0enUFlUuKRy3it01R+hlggyVUMtnCTQRunsQYifnSGb8hET4Xo8yiC0o0r1paW3ud5+rbURg== @@ -21453,6 +21453,13 @@ resolve-cwd@^2.0.0: dependencies: resolve-from "^3.0.0" +resolve-cwd@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d" + integrity sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg== + dependencies: + resolve-from "^5.0.0" + resolve-dir@^1.0.0, resolve-dir@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/resolve-dir/-/resolve-dir-1.0.1.tgz#79a40644c362be82f26effe739c9bb5382046f43"