diff --git a/packages/create-gatsby/.gitignore b/packages/create-gatsby/.gitignore new file mode 100644 index 0000000000000..9b26ed04f1c7a --- /dev/null +++ b/packages/create-gatsby/.gitignore @@ -0,0 +1,2 @@ +node_modules +lib \ No newline at end of file diff --git a/packages/create-gatsby/README.md b/packages/create-gatsby/README.md new file mode 100644 index 0000000000000..7a3276e130706 --- /dev/null +++ b/packages/create-gatsby/README.md @@ -0,0 +1,21 @@ +# create-gatsby (alpha) + +Create Gatsby apps in an interactive CLI experience that does the plumbing for you. + +## Quick Overview + +Create a new Gatsby app by running the following command: + +```shell +npm init gatsby +``` + +or + +```shell +yarn create gatsby +``` + +It will ask you questions about what you're building, and set up a Gatsby project for you. + +_Note: this package is different from the Gatsby CLI, it is intended solely to create new sites.._ diff --git a/packages/create-gatsby/package.json b/packages/create-gatsby/package.json new file mode 100644 index 0000000000000..e9d87b156699c --- /dev/null +++ b/packages/create-gatsby/package.json @@ -0,0 +1,44 @@ +{ + "name": "create-gatsby", + "version": "0.0.0-6", + "main": "lib/index.js", + "bin": "lib/cli.js", + "license": "MIT", + "scripts": { + "build": "tsc", + "watch": "tsc --watch", + "prepare": "yarn build", + "import-plugin-options": "node ./scripts/import-options-schema.js" + }, + "homepage": "https://github.com/gatsbyjs/gatsby/tree/master/packages/create-gatsby#readme", + "dependencies": { + "@babel/runtime": "^7.12.1", + "ansi-wordwrap": "^1.0.2", + "common-tags": "^1.8.0", + "enquirer": "^2.3.6", + "execa": "^4.0.3", + "fs-extra": "^9.0.1", + "gatsby-core-utils": "^1.4.0-next.0", + "stream-filter": "^2.1.0", + "string-length": "^4.0.1", + "terminal-link": "^2.1.1" + }, + "files": [ + "lib" + ], + "devDependencies": { + "@types/configstore": "^4.0.0", + "@types/fs-extra": "^9.0.2", + "@types/node": "^14.14.5", + "eslint": "^7.12.1", + "joi": "^17.2.1", + "prettier": "^2.1.2", + "typescript": "^4.0.5" + }, + "repository": { + "type": "git", + "url": "https://github.com/gatsbyjs/gatsby.git", + "directory": "packages/create-gatsby" + }, + "author": "Matt Kane " +} diff --git a/packages/create-gatsby/scripts/import-options-schema.js b/packages/create-gatsby/scripts/import-options-schema.js new file mode 100755 index 0000000000000..89c4a2a1d22dd --- /dev/null +++ b/packages/create-gatsby/scripts/import-options-schema.js @@ -0,0 +1,93 @@ +#!/usr/bin/env node + +const path = require("path") +const fs = require("fs-extra") +const pluginPath= process.argv[2] +const Joi = require("gatsby-plugin-utils") +async function run() { + if(!pluginPath) { + console.error("Please pass a path to the plugin directory") + return + } + + + const rootDir = path.resolve(pluginPath) + if(!fs.existsSync(rootDir)) { + console.error(`The plugin directory ${rootDir} does not exist`) + return + } + + const stat = await fs.stat(rootDir) + + if(!stat.isDirectory()) { + console.error(`The plugin path ${rootDir} is not a directory`) + return + } + + let pluginName + + try { + const { name } = require(path.resolve(rootDir, "package.json")) + if(!name) { + console.error("Plugin package.json does not have a name field") + return + } + pluginName = name + + } catch (e) { + console.error("Could not open package.json. Are you sure the plugin directory is correct?") + return + } + + const gatsbyNodePath = path.resolve(rootDir, "gatsby-node.js") + + if(!fs.existsSync(gatsbyNodePath)) { + console.error(`Could not find gatsby-node.js in ${gatsbyNodePath}. Are you sure this is a plugin directory?`) + return + } + + let pluginOptionsSchema + + try { + const gatsbyNode = require(gatsbyNodePath) + pluginOptionsSchema = gatsbyNode.pluginOptionsSchema + } catch(e) { + console.error(`Could not load gatsby-node.js. You may need to build the plugin first.`) + console.log("Error was:", e.message) + return + } + + if(!pluginOptionsSchema) { + console.error("The plugin does not include a pluginOptionsSchema") + return + } + + let optionsSchema + + try { + const schema = pluginOptionsSchema({ Joi }) + optionsSchema = schema.describe() + } catch (e) { + console.error("Failed to generate schema") + console.error(e.message) + return + } + + const schemataPath = path.resolve(__dirname, "..", "src", "plugin-schemas.json") + + if(!fs.existsSync(schemataPath)) { + console.error("Could not find output file") + return + } + + const json = await fs.readJSON(schemataPath) + + json[pluginName] = optionsSchema + + console.log(`Writing "${pluginName} to schemataPath`) + await fs.writeJSON(schemataPath, json) + + +} + +run() diff --git a/packages/create-gatsby/src/cli.ts b/packages/create-gatsby/src/cli.ts new file mode 100755 index 0000000000000..0c576018add71 --- /dev/null +++ b/packages/create-gatsby/src/cli.ts @@ -0,0 +1,6 @@ +#!/usr/bin/env node +import { run } from "." + +run().catch(e => { + console.warn(e) +}) diff --git a/packages/create-gatsby/src/cmses.json b/packages/create-gatsby/src/cmses.json new file mode 100644 index 0000000000000..a33ae347d70c2 --- /dev/null +++ b/packages/create-gatsby/src/cmses.json @@ -0,0 +1,7 @@ +{ + "gatsby-source-wordpress-experimental": { "message": "WordPress" }, + "gatsby-source-contentful": { "message": "Contentful" }, + "gatsby-source-sanity": { "message": "Sanity" }, + "gatsby-source-datocms": { "message": "DatoCMS" }, + "gatsby-source-shopify": { "message": "Shopify" } +} diff --git a/packages/create-gatsby/src/cmses.json.d.ts b/packages/create-gatsby/src/cmses.json.d.ts new file mode 100644 index 0000000000000..be0d2fb824369 --- /dev/null +++ b/packages/create-gatsby/src/cmses.json.d.ts @@ -0,0 +1,5 @@ +import {PluginMap} from "." + +declare const cmses: PluginMap + +export default cmses \ No newline at end of file diff --git a/packages/create-gatsby/src/components/form.js b/packages/create-gatsby/src/components/form.js new file mode 100644 index 0000000000000..01b4fd6dba937 --- /dev/null +++ b/packages/create-gatsby/src/components/form.js @@ -0,0 +1,72 @@ +import { Form } from "enquirer" +import placeholder from "./placeholder" +import colors from "ansi-colors" + +export class FormInput extends Form { + async renderChoice(choice, i) { + await this.onChoice(choice, i) + + let { state, styles } = this + let { cursor, initial = ``, name, input = `` } = choice + let { muted, submitted, primary, danger } = styles + + let focused = this.index === i + let validate = choice.validate || (() => true) + let sep = await this.choiceSeparator(choice, i) + let msg = choice.message + + if (this.align === `right`) msg = msg.padStart(this.longest + 1, ` `) + if (this.align === `left`) msg = msg.padEnd(this.longest + 1, ` `) + + // re-populate the form values (answers) object + let value = (this.values[name] = input || initial) + let color = input ? `success` : `dark` + + if ((await validate.call(choice, value, this.state)) !== true) { + color = `danger` + } + + let style = styles[color] + let indicator = style(await this.indicator(choice, i)) + (choice.pad || ``) + + let indent = this.indent(choice) + let line = () => + [indent, indicator, msg + sep, input].filter(Boolean).join(` `) + + if (state.submitted) { + msg = colors.unstyle(msg) + input = submitted(input) + return line() + } + + if (choice.format) { + input = await choice.format.call(this, input, choice, i) + } else { + let color = this.styles.muted + let options = { input, initial, pos: cursor, showCursor: focused, color } + input = placeholder(this, options) + } + + if (!this.isValue(input)) { + input = this.styles.muted(this.symbols.ellipsis) + } + + if (choice.result) { + this.values[name] = await choice.result.call(this, value, choice, i) + } + + if (focused) { + msg = primary(msg) + } + + if (choice.error) { + input += (input ? ` ` : ``) + danger(choice.error.trim()) + } else if (choice.hint && focused) { + input += + (input ? `\n${` `.repeat(this.longest + 6)}` : ``) + + muted(choice.hint.trim()) + } + + return line() + } +} diff --git a/packages/create-gatsby/src/components/placeholder.js b/packages/create-gatsby/src/components/placeholder.js new file mode 100644 index 0000000000000..04b8b3172ef35 --- /dev/null +++ b/packages/create-gatsby/src/components/placeholder.js @@ -0,0 +1,66 @@ +/** + * This file is taken almost unchanged from enquirer, because it's not exported from the module + */ + +const isPrimitive = val => + val != null && typeof val !== `object` && typeof val !== `function` + +/** + * Render a placeholder value with cursor and styling based on the + * position of the cursor. + * + * @param {Object} `prompt` Prompt instance. + * @param {String} `input` Input string. + * @param {String} `initial` The initial user-provided value. + * @param {Number} `pos` Current cursor position. + * @param {Boolean} `showCursor` Render a simulated cursor using the inverse primary style. + * @return {String} Returns the styled placeholder string. + * @api public + */ + +module.exports = (prompt, options = {}) => { + prompt.cursorHide() + + let { input = ``, initial = ``, pos, showCursor = true, color } = options + let style = color || prompt.styles.placeholder + let inverse = prompt.styles.primary.inverse + let blinker = str => inverse(str) + let output = input + let char = ` ` + let reverse = blinker(char) + + if (prompt.blink && prompt.blink.off === true) { + blinker = str => str + reverse = `` + } + + if (showCursor && pos === 0 && initial === `` && input === ``) { + return blinker(char) + } + + if (showCursor && pos === 0 && (input === initial || input === ``)) { + return blinker(initial[0]) + style(initial.slice(1)) + } + + initial = isPrimitive(initial) ? `${initial}` : `` + input = isPrimitive(input) ? `${input}` : `` + + let placeholder = initial && initial.startsWith(input) && initial !== input + let cursor = placeholder ? blinker(initial[input.length]) : reverse + + if (pos !== input.length && showCursor === true) { + output = input.slice(0, pos) + blinker(input[pos]) + input.slice(pos + 1) + cursor = `` + } + + if (showCursor === false) { + cursor = `` + } + + if (placeholder) { + let raw = prompt.styles.unstyle(output + cursor) + return output + cursor + style(initial.slice(raw.length)) + } + + return output + cursor +} diff --git a/packages/create-gatsby/src/components/plugin.js b/packages/create-gatsby/src/components/plugin.js new file mode 100644 index 0000000000000..8fb31068a7fb5 --- /dev/null +++ b/packages/create-gatsby/src/components/plugin.js @@ -0,0 +1,17 @@ +import { FormInput } from "./form" +import { TextInput } from "./text" +import { SelectInput, MultiSelectInput } from "./select" + +/** + * Enquirer plugin to add custom fields + * + * @param enquirer {import("enquirer")} + * @returns {import("enquirer")} + */ +export const plugin = enquirer => { + enquirer.register(`textinput`, TextInput) + enquirer.register(`selectinput`, SelectInput) + enquirer.register(`multiselectinput`, MultiSelectInput) + enquirer.register(`forminput`, FormInput) + return enquirer +} diff --git a/packages/create-gatsby/src/components/select.js b/packages/create-gatsby/src/components/select.js new file mode 100644 index 0000000000000..5efb6b6a8e97b --- /dev/null +++ b/packages/create-gatsby/src/components/select.js @@ -0,0 +1,121 @@ +import { Select } from "enquirer" + +export class SelectInput extends Select { + format() { + if (!this.state.submitted || this.state.cancelled) return `` + if (Array.isArray(this.selected)) { + return this.selected + .map(choice => + this.styles.primary(this.symbols.middot + ` ` + choice.message) + ) + .join(`\n`) + } + return this.styles.primary( + this.symbols.middot + ` ` + this.selected.message + ) + } + + async indicator(choice) { + if ( + !this.multiple || + choice.role === `separator` || + choice.name === `___done` + ) { + return `` + } + return this.symbols.radio[choice.enabled ? `on` : `off`] + } + + async pointer(choice, i) { + const val = await this.element(`pointer`, choice, i) + if (!val) { + return undefined + } + + let styles = this.styles + let focused = this.index === i + let style = focused ? styles.primary : val => val + let ele = await this.resolve(val[focused ? `on` : `off`] || val, this.state) + return focused ? style(ele) : ` `.repeat(ele.length) + } + + async render() { + let { submitted, size } = this.state + + let prompt = `` + let header = await this.header() + let prefix = await this.prefix() + let message = await this.message() + + if (this.options.promptLine !== false) { + prompt = [prefix, message].join(` `) + this.state.prompt = prompt + } + + let output = await this.format() + let help = (await this.error()) || (await this.hint()) + let body = await this.renderChoices() + let footer = await this.footer() + + if (output) prompt += `\n` + output + if (help && !prompt.includes(help)) prompt += `\n` + help + + if ( + submitted && + !output && + !body.trim() && + this.multiple && + this.emptyError != null + ) { + prompt += this.styles.danger(this.emptyError) + } + + this.clear(size) + this.write([header, prompt, body, footer].filter(Boolean).join(`\n`)) + this.write(this.margin[2]) + this.restore() + } +} + +export class MultiSelectInput extends SelectInput { + constructor(options) { + super({ ...options, multiple: true }) + } + + toggle(choice, enabled) { + if (choice.name === `___done`) { + super.submit() + } else { + super.toggle(choice, enabled) + } + } + + async toChoices(value, parent) { + const footer = [ + { + role: `separator`, + }, + { + message: this.styles.bold(`Done`), + name: `___done`, + }, + ] + + if (typeof value === `function`) { + value = await value.call(this) + } + if (value instanceof Promise) { + value = await value + } + + return super.toChoices([...value, ...footer], parent) + } + + submit() { + return this.space() + } + + next() { + return this.index === this.choices.length - 1 ? super.next() : this.end() + } +} diff --git a/packages/create-gatsby/src/components/spin.ts b/packages/create-gatsby/src/components/spin.ts new file mode 100644 index 0000000000000..4c3dfb742a7a0 --- /dev/null +++ b/packages/create-gatsby/src/components/spin.ts @@ -0,0 +1,21 @@ +// Tiny, zero dependency spinner +const dots = [`⠋`, `⠙`, `⠹`, `⠸`, `⠼`, `⠴`, `⠦`, `⠧`, `⠇`, `⠏`] +const out = process.stderr + +export function spin(message = ``, frames = dots, interval = 80): () => void { + let frame = 0 + // Hide cursor + out.write(`\x1b[?25l`) + const timer = setInterval(() => { + out.write(`${frames[frame]} ${message}`) + frame = (frame + 1) % frames.length + out.cursorTo(0) + }, interval) + + return function stop(): void { + clearInterval(timer) + out.cursorTo(0) + // Show cursor + out.write(`\x1b[?25h`) + } +} diff --git a/packages/create-gatsby/src/components/text.js b/packages/create-gatsby/src/components/text.js new file mode 100644 index 0000000000000..3de73c026b566 --- /dev/null +++ b/packages/create-gatsby/src/components/text.js @@ -0,0 +1,49 @@ +import { Input } from "enquirer" +export class TextInput extends Input { + constructor(options) { + super(options) + this.cursorShow() + } + async render() { + const size = this.state.size + + const prefix = await this.prefix() + const separator = await this.separator() + const message = await this.message() + + let prompt = [ + prefix, + ` `, + this.styles.muted(await this.element(`hint`)), + separator, + ] + .filter(Boolean) + .join(``) + this.state.prompt = prompt + + const header = await this.header() + let output = await this.format() + const unstyled = this.styles.unstyle(output) + + // Make a fake cursor if we're showing the placeholder + if (!this.input?.length && unstyled.length) { + this.cursorHide() + output = + this.styles.highlight(unstyled[0]) + + this.styles.placeholder(unstyled.slice(1)) + } else { + this.cursorShow() + } + const footer = await this.footer() + + prompt += ` ` + output + + this.clear(size) + this.write( + [header, message, prompt, await this.error(), footer] + .filter(Boolean) + .join(`\n`) + ) + this.restore() + } +} diff --git a/packages/create-gatsby/src/components/utils.ts b/packages/create-gatsby/src/components/utils.ts new file mode 100644 index 0000000000000..ee2b890e1c8ce --- /dev/null +++ b/packages/create-gatsby/src/components/utils.ts @@ -0,0 +1,21 @@ +import stringLength from "string-length" +// ansi and emoji-safe string length +import wordWrap from "ansi-wordwrap" + +const DEFAULT_WIDTH = process.stdout.columns + +export const wrap = (text: string, width = DEFAULT_WIDTH): string => + wordWrap(text, { width }) + +export function rule(char = `\u2501`, width = DEFAULT_WIDTH): string { + return char.repeat(width) +} + +export function center( + text: string, + padding = ` `, + width = DEFAULT_WIDTH +): string { + const pad = padding.repeat(Math.round((width - stringLength(text)) / 2)) + return pad + text +} diff --git a/packages/create-gatsby/src/features.json b/packages/create-gatsby/src/features.json new file mode 100644 index 0000000000000..7087fe6617531 --- /dev/null +++ b/packages/create-gatsby/src/features.json @@ -0,0 +1,47 @@ +{ + "gatsby-plugin-google-analytics": { + "message": "Add the Google Analytics tracking script" + }, + "gatsby-plugin-image": { + "message": "Add responsive images", + "plugins": [ + "gatsby-plugin-sharp", + "gatsby-transformer-sharp", + "gatsby-source-filesystem:images" + ], + "options": { + "gatsby-source-filesystem:images": { + "name": "images", + "path": "./src/images/" + } + } + }, + "gatsby-plugin-react-helmet": { + "message": "Add page meta tags with React Helmet", + "dependencies": ["react-helmet"] + }, + "gatsby-plugin-sitemap": { "message": "Add an automatic sitemap" }, + "gatsby-plugin-offline": { "message": "Enable offline functionality" }, + "gatsby-plugin-manifest": { "message": "Generate a manifest file" }, + "gatsby-transformer-remark": { + "message": "Add Markdown support (without MDX)", + "plugins": ["gatsby-source-filesystem:pages"], + "options": { + "gatsby-source-filesystem:pages": { + "name": "pages", + "path": "./src/pages/" + } + } + }, + "gatsby-plugin-mdx": { + "message": "Add Markdown and MDX support", + "plugins": ["gatsby-source-filesystem:pages"], + "dependencies": ["@mdx-js/react", "@mdx-js/mdx"], + "options": { + "gatsby-source-filesystem:pages": { + "name": "pages", + "path": "./src/pages/" + } + } + } +} diff --git a/packages/create-gatsby/src/features.json.d.ts b/packages/create-gatsby/src/features.json.d.ts new file mode 100644 index 0000000000000..97521924b34ad --- /dev/null +++ b/packages/create-gatsby/src/features.json.d.ts @@ -0,0 +1,5 @@ +import {PluginMap} from "." + +declare const features: PluginMap + +export default features \ No newline at end of file diff --git a/packages/create-gatsby/src/index.ts b/packages/create-gatsby/src/index.ts new file mode 100644 index 0000000000000..f2b2f62082d64 --- /dev/null +++ b/packages/create-gatsby/src/index.ts @@ -0,0 +1,291 @@ +import Enquirer from "enquirer" +import cmses from "./cmses.json" +import styles from "./styles.json" +import features from "./features.json" +import { initStarter, getPackageManager } from "./init-starter" +import { installPlugins } from "./install-plugins" +import c from "ansi-colors" +import path from "path" +import fs from "fs" +import { plugin } from "./components/plugin" +import { makePluginConfigQuestions } from "./plugin-options-form" +import { center, rule, wrap } from "./components/utils" +import { stripIndent } from "common-tags" + +// eslint-disable-next-line no-control-regex +const INVALID_FILENAMES = /[<>:"/\\|?*\u0000-\u001F]/g +const INVALID_WINDOWS = /^(con|prn|aux|nul|com\d|lpt\d)$/i + +// We're using a fork because it points to the canary version of gatsby +const DEFAULT_STARTER = `https://github.com/ascorbic/gatsby-starter-hello-world.git` + +const makeChoices = ( + options: Record }>, + multi = false +): Array<{ message: string; name: string; disabled?: boolean }> => { + const entries = Object.entries(options).map(([name, message]) => { + return { name, message: message.message } + }) + + if (multi) { + return entries + } + const none = { name: `none`, message: `No (or I'll add it later)` } + const divider = { name: `–`, role: `separator`, message: `–` } + + return [none, divider, ...entries] +} + +export const validateProjectName = async ( + value: string +): Promise => { + if (INVALID_FILENAMES.test(value)) { + return `The destination "${value}" is not a valid filename. Please try again, avoiding special characters.` + } + if (process.platform === `win32` && INVALID_WINDOWS.test(value)) { + return `The destination "${value}" is not a valid Windows filename. Please try another name` + } + if (fs.existsSync(path.resolve(value))) { + return `The destination "${value}" already exists. Please choose a different name` + } + return true +} + +// The enquirer types are not accurate +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const questions: any = [ + { + type: `textinput`, + name: `project`, + message: `What would you like to name the folder where your site will be created?`, + hint: path.basename(process.cwd()), + separator: `/`, + initial: `my-gatsby-site`, + format: (value: string): string => c.cyan(value), + validate: validateProjectName, + }, + { + type: `selectinput`, + name: `cms`, + message: `Will you be using a CMS?`, + hint: `(Single choice) Arrow keys to move, enter to confirm`, + choices: makeChoices(cmses), + }, + { + type: `selectinput`, + name: `styling`, + message: `Would you like to install a styling system?`, + hint: `(Single choice) Arrow keys to move, enter to confirm`, + choices: makeChoices(styles), + }, + { + type: `multiselectinput`, + name: `features`, + message: `Would you like to install additional features with other plugins?`, + hint: `(Multiple choice) Use arrow keys to move, enter to select, and choose "Done" to confirm your choices`, + choices: makeChoices(features, true), + }, +] +interface IAnswers { + project: string + styling?: keyof typeof styles + cms?: keyof typeof cmses + features?: Array +} + +interface IPluginEntry { + /** + * Message displayed in the menu when selecting the plugin + */ + message: string + /** + * Extra NPM packages to install + */ + dependencies?: Array + /** + * Items are either the plugin name, or the plugin name and key, separated by a colon (":") + * This allows duplicate entries for plugins such as gatsby-source-filesystem. + */ + plugins?: Array + /** + * Keys must match plugin names or name:key combinations from the plugins array + */ + options?: PluginConfigMap +} + +export type PluginMap = Record + +export type PluginConfigMap = Record> + +const removeKey = (plugin: string): string => plugin.split(`:`)[0] + +export async function run(): Promise { + const { version } = require(`../package.json`) + + console.log(c.grey(`create-gatsby version ${version}`)) + + console.log( + ` + + +${center(c.blueBright.bold.underline(`Welcome to Gatsby!`))} + + +` + ) + console.log(c.red(rule())) + console.log(center(c.red(`⚠️ This is currently for testing purposes only`))) + console.log(c.red(rule())) + + console.log( + wrap( + `This command will generate a new Gatsby site for you in ${c.bold( + process.cwd() + )} with the setup you select. ${c.white.bold( + `Let's answer some questions:\n` + )}`, + process.stdout.columns + ) + ) + console.log(``) + + const enquirer = new Enquirer() + + enquirer.use(plugin) + + const data = await enquirer.prompt(questions) + + const messages: Array = [ + `🛠 Create a new Gatsby site in the folder ${c.magenta(data.project)}`, + ] + + const plugins: Array = [] + const packages: Array = [] + let pluginConfig: PluginConfigMap = {} + + if (data.cms && data.cms !== `none`) { + messages.push( + `📚 Install and configure the plugin for ${c.magenta( + cmses[data.cms].message + )}` + ) + const extraPlugins = cmses[data.cms].plugins || [] + plugins.push(data.cms, ...extraPlugins) + packages.push( + data.cms, + ...(cmses[data.cms].dependencies || []), + ...extraPlugins + ) + pluginConfig = { ...pluginConfig, ...cmses[data.cms].options } + } + + if (data.styling && data.styling !== `none`) { + messages.push( + `🎨 Get you set up to use ${c.magenta( + styles[data.styling].message + )} for styling your site` + ) + const extraPlugins = styles[data.styling].plugins || [] + + plugins.push(data.styling, ...extraPlugins) + packages.push( + data.styling, + ...(styles[data.styling].dependencies || []), + ...extraPlugins + ) + pluginConfig = { ...pluginConfig, ...styles[data.styling].options } + } + + if (data.features?.length) { + messages.push( + `🔌 Install ${data.features + ?.map((feat: string) => c.magenta(feat)) + .join(`, `)}` + ) + plugins.push(...data.features) + const featureDependencies = data.features?.map(featureKey => { + const extraPlugins = features[featureKey].plugins || [] + plugins.push(...extraPlugins) + return [ + // Spread in extra dependencies + ...(features[featureKey].dependencies || []), + // Spread in plugins + ...extraPlugins, + ] + }) + const flattenedDependencies = ([] as Array).concat.apply( + [], + featureDependencies + ) // here until we upgrade to node 11 and can use flatMap + + packages.push(...data.features, ...flattenedDependencies) + // Merge plugin options + pluginConfig = data.features.reduce((prev, key) => { + return { ...prev, ...features[key].options } + }, pluginConfig) + } + + const config = makePluginConfigQuestions(plugins) + if (config.length) { + console.log( + `\nGreat! A few of the selections you made need to be configured. Please fill in the options for each plugin now:\n` + ) + + const enquirer = new Enquirer>() + enquirer.use(plugin) + pluginConfig = { ...pluginConfig, ...(await enquirer.prompt(config)) } + } + + console.log(` + +${c.bold(`Thanks! Here's what we'll now do:`)} + + ${messages.join(`\n `)} + `) + + const { confirm } = await new Enquirer<{ confirm: boolean }>().prompt({ + type: `confirm`, + name: `confirm`, + initial: `Yes`, + message: `Shall we do this?`, + format: value => (value ? c.greenBright(`Yes`) : c.red(`No`)), + }) + + if (!confirm) { + console.log(`OK, bye!`) + return + } + + await initStarter(DEFAULT_STARTER, data.project, packages.map(removeKey)) + + console.log(c.green(`✔ `) + `Created site in ` + c.green(data.project)) + console.log({ plugins, pluginConfig }) + if (plugins.length) { + console.log(c.bold(`🔌 Installing plugins...`)) + await installPlugins(plugins, pluginConfig, path.resolve(data.project), []) + } + + const pm = await getPackageManager() + + const runCommand = pm === `npm` ? `npm run` : `yarn` + + console.log( + stripIndent` + 🎉 Your new Gatsby site ${c.bold( + data.project + )} has been successfully bootstrapped + at ${c.bold(path.resolve(data.project))}. + ` + ) + console.log(`Start by going to the directory with\n + ${c.magenta(`cd ${data.project}`)} + `) + + console.log(`Start the local development server with\n + ${c.magenta(`${runCommand} develop`)} + `) + + console.log(`See all commands at\n + ${c.blueBright(`https://www.gatsbyjs.com/docs/gatsby-cli/`)} + `) +} diff --git a/packages/create-gatsby/src/init-starter.ts b/packages/create-gatsby/src/init-starter.ts new file mode 100644 index 0000000000000..94507ccc99b4c --- /dev/null +++ b/packages/create-gatsby/src/init-starter.ts @@ -0,0 +1,230 @@ +import { execSync } from "child_process" +import execa from "execa" +import fs from "fs-extra" +import path from "path" +import { updateSiteMetadata } from "gatsby-core-utils" +import { reporter } from "./reporter" +import { getConfigStore } from "gatsby-core-utils" +import filterStream from "stream-filter" +import { spin } from "./components/spin" +type PackageManager = "yarn" | "npm" + +const packageMangerConfigKey = `cli.packageManager` + +export const getPackageManager = (): PackageManager => + getConfigStore().get(packageMangerConfigKey) + +export const setPackageManager = (packageManager: PackageManager): void => { + getConfigStore().set(packageMangerConfigKey, packageManager) +} + +const spawnWithArgs = ( + file: string, + args: Array, + options?: execa.Options +): execa.ExecaChildProcess => + execa(file, args, { stdio: `inherit`, preferLocal: false, ...options }) + +const spawn = ( + cmd: string, + options?: execa.Options +): execa.ExecaChildProcess => { + const [file, ...args] = cmd.split(/\s+/) + return spawnWithArgs(file, args, options) +} +// Checks the existence of yarn package +// We use yarnpkg instead of yarn to avoid conflict with Hadoop yarn +// Refer to https://github.com/yarnpkg/yarn/issues/673 +const checkForYarn = (): boolean => { + try { + execSync(`yarnpkg --version`, { stdio: `ignore` }) + return true + } catch (e) { + return false + } +} + +// Initialize newly cloned directory as a git repo +const gitInit = async ( + rootPath: string +): Promise> => { + reporter.info(`Initialising git in ${rootPath}`) + + return await spawn(`git init`, { cwd: rootPath }) +} + +// Create a .gitignore file if it is missing in the new directory +const maybeCreateGitIgnore = async (rootPath: string): Promise => { + if (fs.existsSync(path.join(rootPath, `.gitignore`))) { + return + } + + reporter.info(`Creating minimal .gitignore in ${rootPath}`) + await fs.writeFile( + path.join(rootPath, `.gitignore`), + `.cache\nnode_modules\npublic\n` + ) +} + +// Create an initial git commit in the new directory +const createInitialGitCommit = async (rootPath: string): Promise => { + reporter.info(`Create initial git commit in ${rootPath}`) + + await spawn(`git add -A`, { cwd: rootPath }) + // use execSync instead of spawn to handle git clients using + // pgp signatures (with password) + try { + execSync(`git commit -m "Initial commit from gatsby"`, { + cwd: rootPath, + }) + } catch { + // Remove git support if initial commit fails + reporter.info(`Initial git commit failed - removing git support\n`) + fs.removeSync(path.join(rootPath, `.git`)) + } +} + +const filter = (pattern: string): NodeJS.ReadWriteStream => + filterStream((data: string): boolean => !data.toString().startsWith(pattern)) + +// Executes `npm install` or `yarn install` in rootPath. +const install = async ( + rootPath: string, + packages: Array +): Promise => { + const prevDir = process.cwd() + + let stop = spin(`Installing packages...`) + + process.chdir(rootPath) + + const npmConfigUserAgent = process.env.npm_config_user_agent + + const silent = `--silent` + + try { + if (!getPackageManager()) { + if (npmConfigUserAgent?.includes(`yarn`)) { + setPackageManager(`yarn`) + } else { + setPackageManager(`npm`) + } + } + if (getPackageManager() === `yarn` && checkForYarn()) { + if (await fs.pathExists(`package-lock.json`)) { + if (!(await fs.pathExists(`yarn.lock`))) { + await spawn(`yarnpkg import`) + } + await fs.remove(`package-lock.json`) + } + + const args = packages.length ? [`add`, silent, ...packages] : [silent] + + const childProcess = spawnWithArgs(`yarnpkg`, args, { + all: true, + stdio: `pipe`, + }) + // eslint-disable-next-line no-unused-expressions + childProcess.all?.pipe(filter(`warning`)).pipe(process.stderr) + + await childProcess + } else { + await fs.remove(`yarn.lock`) + + let childProcess = spawnWithArgs(`npm`, [`install`, silent], { + all: true, + stdio: `pipe`, + }) + // eslint-disable-next-line no-unused-expressions + childProcess.all?.pipe(filter(`npm WARN`)).pipe(process.stderr) + + await childProcess + + stop() + + stop = spin(`Installing plugins...`) + + childProcess = spawnWithArgs(`npm`, [`install`, silent, ...packages], { + all: true, + stdio: `pipe`, + }) + // eslint-disable-next-line no-unused-expressions + childProcess.all?.pipe(filter(`npm WARN`)).pipe(process.stderr) + + await childProcess + } + } catch (e) { + reporter.error(e) + } finally { + process.chdir(prevDir) + stop() + reporter.success(`Installed packages`) + } +} + +// Clones starter from URI. +const clone = async ( + url: string, + rootPath: string, + branch?: string +): Promise => { + const branchProps = branch ? [`-b`, branch] : [] + + const stop = spin(`Cloning site template`) + + const args = [ + `clone`, + ...branchProps, + url, + rootPath, + `--recursive`, + `--depth=1`, + `--quiet`, + ].filter(arg => Boolean(arg)) + + await spawnWithArgs(`git`, args) + stop() + reporter.success(`Created site from template`) + + await fs.remove(path.join(rootPath, `.git`)) +} + +async function gitSetup(rootPath: string): Promise { + await gitInit(rootPath) + await maybeCreateGitIgnore(rootPath) + await createInitialGitCommit(rootPath) +} + +/** + * Main function that clones or copies the starter. + */ +export async function initStarter( + starter: string, + rootPath: string, + packages: Array +): Promise { + const sitePath = path.resolve(rootPath) + + await clone(starter, sitePath) + + await install(rootPath, packages) + + const sitePackageJson = await fs + .readJSON(path.join(sitePath, `package.json`)) + .catch(() => { + reporter.verbose( + `Could not read "${path.join(sitePath, `package.json`)}"` + ) + }) + + await updateSiteMetadata( + { + name: sitePackageJson?.name || rootPath, + sitePath, + lastRun: Date.now(), + }, + false + ) + await gitSetup(rootPath) + // trackCli(`NEW_PROJECT_END`); +} diff --git a/packages/create-gatsby/src/install-plugins.ts b/packages/create-gatsby/src/install-plugins.ts new file mode 100644 index 0000000000000..f87a5d8d4fe64 --- /dev/null +++ b/packages/create-gatsby/src/install-plugins.ts @@ -0,0 +1,46 @@ +import { reporter } from "./reporter" +import path from "path" +import { PluginConfigMap } from "." +export async function installPlugins( + plugins: Array, + pluginOptions: PluginConfigMap = {}, + rootPath: string, + packages: Array +): Promise { + let installPluginCommand + let gatsbyPath + + try { + gatsbyPath = require.resolve(`gatsby/package.json`, { + paths: [rootPath], + }) + } catch (e) { + // Not found + console.warn(e) + } + + if (!gatsbyPath) { + reporter.error( + `Could not find "gatsby" in ${rootPath}. Perhaps it wasn't installed properly?` + ) + return + } + + try { + installPluginCommand = require.resolve(`gatsby-cli/lib/plugin-add`, { + // Try to find gatsby-cli in the site root, or in the site's gatsby dir + paths: [rootPath, path.dirname(gatsbyPath)], + }) + } catch (e) { + // The file is missing + } + + if (!installPluginCommand) { + reporter.error(`gatsby-cli not installed, or is too old`) + return + } + + const { addPlugins } = require(installPluginCommand) + + await addPlugins(plugins, pluginOptions, rootPath, packages) +} diff --git a/packages/create-gatsby/src/plugin-options-form.ts b/packages/create-gatsby/src/plugin-options-form.ts new file mode 100644 index 0000000000000..5081e51ce51a2 --- /dev/null +++ b/packages/create-gatsby/src/plugin-options-form.ts @@ -0,0 +1,111 @@ +import { stripIndent } from "common-tags" +import terminalLink from "terminal-link" +import Joi from "joi" +import pluginSchemas from "./plugin-schemas.json" +import cmses from "./cmses.json" +import styles from "./styles.json" +import c from "ansi-colors" + +const supportedOptionTypes = [`string`, `boolean`, `number`] + +type Schema = Joi.Description & { + // Limitation in Joi typings + // eslint-disable-next-line @typescript-eslint/no-explicit-any + flags?: Record +} + +type PluginName = keyof typeof pluginSchemas + +interface IFormPrompt { + type: string + name: string + multiple: boolean + message: string + choices: Array<{ + name: string + initial: unknown + message: string + hint?: string + }> +} + +function getName(key: string): string | undefined { + const plugins = [cmses, styles] // "features" doesn't map to names + for (const types of plugins) { + if (key in types) { + return types[key as keyof typeof types].message + } + } + return key +} + +function docsLink(pluginName: string): string { + return c.blueBright( + terminalLink( + `the plugin docs`, + `https://www.gatsbyjs.com/plugins/${pluginName}/`, + { fallback: (_, url) => url } + ) + ) +} + +export const makePluginConfigQuestions = ( + selectedPlugins: Array +): Array => { + const formPrompts: Array = [] + + selectedPlugins.forEach((pluginName: string): void => { + const schema = pluginSchemas[pluginName as PluginName] + if (!schema || typeof schema === `string` || !(`keys` in schema)) { + return + } + const options: Record | undefined = schema?.keys + const choices: Array<{ + name: string + initial: string + message: string + hint?: string + }> = [] + + if (!options) { + return + } + + Object.entries(options).forEach(([name, option]) => { + if (option?.flags?.presence !== `required`) { + return + } + choices.push({ + name, + initial: + option.flags?.default && + supportedOptionTypes.includes(typeof option.flags?.default) + ? option.flags?.default.toString() + : undefined, + message: name, + hint: option.flags?.description, + }) + }) + + if (choices.length) { + formPrompts.push({ + type: `forminput`, + name: pluginName, + multiple: true, + message: stripIndent` + Configure the ${getName(pluginName)} plugin. + See ${docsLink(pluginName)} for help. + ${ + choices.length > 1 + ? c.green( + `Use arrow keys to move between fields, and enter to finish` + ) + : `` + } + `, + choices, + }) + } + }) + return formPrompts +} diff --git a/packages/create-gatsby/src/plugin-schemas.json b/packages/create-gatsby/src/plugin-schemas.json new file mode 100644 index 0000000000000..26285c5a06597 --- /dev/null +++ b/packages/create-gatsby/src/plugin-schemas.json @@ -0,0 +1,308 @@ +{ + "gatsby-source-wordpress-experimental": { + "type": "object", + "keys": { + "url": { + "type": "string", + "flags": { + "description": "This should be the full url of your GraphQL endpoint set up by WP GraphQL", + "presence": "required" + } + } + } + }, + "gatsby-source-contentful": { + "type": "object", + "externals": [{}], + "keys": { + "accessToken": { + "type": "string", + "flags": { + "description": "Contentful delivery api key, when using the Preview API use your Preview API key", + "presence": "required" + } + }, + "spaceId": { + "type": "string", + "flags": { "description": "Contentful spaceId", "presence": "required" } + }, + "host": { + "type": "string", + "flags": { + "description": "The base host for all the API requests, by default it's 'cdn.contentful.com', if you want to use the Preview API set it to 'preview.contentful.com'. You can use your own host for debugging/testing purposes as long as you respect the same Contentful JSON structure.", + "default": "cdn.contentful.com" + } + }, + "environment": { + "type": "string", + "flags": { + "description": "The environment to pull the content from, for more info on environments check out this [Guide](https://www.contentful.com/developers/docs/concepts/multiple-environments/).", + "default": "master" + } + }, + "downloadLocal": { + "type": "boolean", + "flags": { + "description": "Downloads and caches ContentfulAsset's to the local filesystem. Allows you to query a ContentfulAsset's localFile field, which is not linked to Contentful's CDN. Useful for reducing data usage.\nYou can pass in any other options available in the [contentful.js SDK](https://github.com/contentful/contentful.js#configuration).", + "default": false + } + }, + "localeFilter": { + "type": "function", + "flags": { + "description": "Possibility to limit how many locales/nodes are created in GraphQL. This can limit the memory usage by reducing the amount of nodes created. Useful if you have a large space in contentful and only want to get the data from one selected locale.\nFor example, to filter locales on only germany `localeFilter: locale => locale.code === 'de-DE'`\n\nList of locales and their codes can be found in Contentful app -> Settings -> Locales" + } + }, + "forceFullSync": { + "type": "boolean", + "flags": { + "description": "Prevents the use of sync tokens when accessing the Contentful API.", + "default": false + } + }, + "pageLimit": { + "type": "number", + "flags": { + "description": "Number of entries to retrieve from Contentful at a time. Due to some technical limitations, the response payload should not be greater than 7MB when pulling content from Contentful. If you encounter this issue you can set this param to a lower number than 100, e.g 50.", + "default": 100 + }, + "rules": [{ "name": "integer" }] + }, + "assetDownloadWorkers": { + "type": "number", + "flags": { + "description": "Number of workers to use when downloading contentful assets. Due to technical limitations, opening too many concurrent requests can cause stalled downloads. If you encounter this issue you can set this param to a lower number than 50, e.g 25.", + "default": 50 + }, + "rules": [{ "name": "integer" }] + }, + "proxy": { + "type": "object", + "flags": { + "description": "Axios proxy configuration. See the [axios request config documentation](https://github.com/mzabriskie/axios#request-config) for further information about the supported values." + }, + "keys": { + "host": { "type": "string", "flags": { "presence": "required" } }, + "port": { "type": "number", "flags": { "presence": "required" } }, + "auth": { + "type": "object", + "keys": { + "username": { "type": "string" }, + "password": { "type": "string" } + } + } + } + }, + "useNameForId": { + "type": "boolean", + "flags": { + "description": "Use the content's `name` when generating the GraphQL schema e.g. a Content Type called `[Component] Navigation bar` will be named `contentfulComponentNavigationBar`.\n When set to `false`, the content's internal ID will be used instead e.g. a Content Type with the ID `navigationBar` will be called `contentfulNavigationBar`.\n\n Using the ID is a much more stable property to work with as it will change less often. However, in some scenarios, Content Types' IDs will be auto-generated (e.g. when creating a new Content Type without specifying an ID) which means the name in the GraphQL schema will be something like `contentfulC6XwpTaSiiI2Ak2Ww0oi6qa`. This won't change and will still function perfectly as a valid field name but it is obviously pretty ugly to work with.\n\n If you are confident your Content Types will have natural-language IDs (e.g. `blogPost`), then you should set this option to `false`. If you are unable to ensure this, then you should leave this option set to `true` (the default).", + "default": true + } + }, + "plugins": { "type": "array" }, + "richText": { + "type": "object", + "flags": { "default": {} }, + "keys": { + "resolveFieldLocales": { + "type": "boolean", + "flags": { + "description": "If you want to resolve the locales in fields of assets and entries that are referenced by rich text (e.g., via embedded entries or entry hyperlinks), set this to `true`. Otherwise, fields of referenced assets or entries will be objects keyed by locale.", + "default": false + } + } + } + } + } + }, + "gatsby-source-sanity": { + "type": "object", + "keys": { + "projectId": { + "type": "string", + "flags": { + "description": "Your Sanity project's ID", + "presence": "required" + } + }, + "dataset": { + "type": "string", + "flags": { + "description": "The dataset to fetch from", + "presence": "required" + } + } + } + }, + "gatsby-source-shopify": { + "type": "object", + "keys": { + "shopName": { + "type": "string", + "flags": { + "description": "The domain name of your Shopify shop", + "presence": "required" + } + }, + "accessToken": { + "type": "string", + "flags": { + "description": "An API access token to your Shopify shop", + "presence": "required" + } + } + } + }, + "gatsby-source-datocms": { + "type": "object", + "keys": { + "apiToken": { + "type": "string", + "flags": { + "description": "Your read-only API token under the Settings > API tokens section of your administrative area in DatoCMS", + "presence": "required" + } + } + } + }, + "gatsby-source-agility": { + "type": "object", + "keys": { + "guid": { + "type": "string", + "flags": { + "description": "your Agility Content Fetch API Guid", + "presence": "required" + } + } + } + }, + "gatsby-plugin-postcss": {}, + "gatsby-plugin-styled-components": {}, + "gatsby-plugin-emotion": {}, + "gatsby-plugin-sass": {}, + "gatsby-plugin-theme-ui": {}, + "gatsby-plugin-google-analytics": { + "type": "object", + "keys": { + "trackingId": { + "type": "string", + "flags": { + "description": "The property ID; the tracking code won't be generated without it", + "presence": "required" + } + }, + "head": { + "type": "boolean", + "flags": { + "default": false, + "description": "Defines where to place the tracking script - `true` in the head and `false` in the body" + } + }, + "anonymize": { "type": "boolean", "flags": { "default": false } }, + "respectDNT": { "type": "boolean", "flags": { "default": false } }, + "exclude": { + "type": "array", + "flags": { + "default": [], + "description": "Avoids sending pageview hits from custom paths" + }, + "items": [{ "type": "string" }] + }, + "pageTransitionDelay": { + "type": "number", + "flags": { + "default": 0, + "description": "Delays sending pageview hits on route update (in milliseconds)" + } + }, + "optimizeId": { + "type": "string", + "flags": { + "description": "Enables Google Optimize using your container Id" + } + }, + "experimentId": { + "type": "string", + "flags": { "description": "Enables Google Optimize Experiment ID" } + }, + "variationId": { + "type": "string", + "flags": { "description": "Set Variation ID. 0 for original 1,2,3...." } + }, + "defer": { + "type": "boolean", + "flags": { + "description": "Defers execution of google analytics script after page load" + } + }, + "sampleRate": { "type": "number" }, + "siteSpeedSampleRate": { "type": "number" }, + "cookieDomain": { "type": "string" } + } + }, + "gatsby-plugin-sitemap": {}, + "gatsby-plugin-mdx": {}, + "gatsby-plugin-offline": {}, + "gatsby-plugin-manifest": { + "type": "object", + "keys": { + "name": { "type": "string" }, + "short_name": { "type": "string" }, + "description": { "type": "string" }, + "lang": { "type": "string" }, + "localize": { + "type": "array", + "items": [ + { + "type": "object", + "keys": { + "start_url": { "type": "string" }, + "name": { "type": "string" }, + "short_name": { "type": "string" }, + "description": { "type": "string" }, + "lang": { "type": "string" } + } + } + ] + }, + "start_url": { "type": "string" }, + "background_color": { "type": "string" }, + "theme_color": { "type": "string" }, + "display": { "type": "string" }, + "legacy": { "type": "boolean" }, + "include_favicon": { "type": "boolean" }, + "icon": { "type": "string" }, + "theme_color_in_head": { "type": "boolean" }, + "crossOrigin": { + "type": "string", + "flags": { "only": true }, + "allow": ["use-credentials", "anonymous"] + }, + "cache_busting_mode": { + "type": "string", + "flags": { "only": true }, + "allow": ["query", "name", "none"] + }, + "icons": { + "type": "array", + "items": [ + { + "type": "object", + "keys": { + "src": { "type": "string" }, + "sizes": { "type": "string" }, + "type": { "type": "string" }, + "purpose": { "type": "string" } + } + } + ] + }, + "icon_options": { + "type": "object", + "keys": { "purpose": { "type": "string" } } + } + } + } +} diff --git a/packages/create-gatsby/src/reporter.ts b/packages/create-gatsby/src/reporter.ts new file mode 100644 index 0000000000000..e75fee312fa20 --- /dev/null +++ b/packages/create-gatsby/src/reporter.ts @@ -0,0 +1,14 @@ +import c from "ansi-colors" +// We don't want to depend on the whole of gatsby-cli, so we can't use reporter +export const reporter = { + info: (message: string): void => console.log(message), + verbose: (message: string): void => console.log(message), + log: (message: string): void => console.log(message), + success: (message: string): void => console.log(c.green(`✔ `) + message), + error: (message: string): void => console.error(c.red(`✘ `) + message), + panic: (message: string): void => { + console.error(message) + process.exit(1) + }, + warn: (message: string): void => console.warn(message), +} diff --git a/packages/create-gatsby/src/styles.json b/packages/create-gatsby/src/styles.json new file mode 100644 index 0000000000000..f6c42031c5e47 --- /dev/null +++ b/packages/create-gatsby/src/styles.json @@ -0,0 +1,10 @@ +{ + "gatsby-plugin-postcss": { "message": "CSS Modules/PostCSS", "dependencies": ["postcss"] }, + "gatsby-plugin-styled-components": { "message": "styled-components", "dependencies": ["styled-components", "babel-plugin-styled-components"] }, + "gatsby-plugin-emotion": { "message": "Emotion", "dependencies": ["@emotion/core", "@emotion/styled"] }, + "gatsby-plugin-sass": { "message": "Sass", "dependencies": ["node-sass"] }, + "gatsby-plugin-theme-ui": { + "message": "Theme UI", + "dependencies": ["theme-ui"] + } +} diff --git a/packages/create-gatsby/src/styles.json.d.ts b/packages/create-gatsby/src/styles.json.d.ts new file mode 100644 index 0000000000000..d723f32125043 --- /dev/null +++ b/packages/create-gatsby/src/styles.json.d.ts @@ -0,0 +1,5 @@ +import {PluginMap} from "." + +declare const styles: PluginMap + +export default styles \ No newline at end of file diff --git a/packages/create-gatsby/src/types.d.ts b/packages/create-gatsby/src/types.d.ts new file mode 100644 index 0000000000000..0acf19b243a69 --- /dev/null +++ b/packages/create-gatsby/src/types.d.ts @@ -0,0 +1,2 @@ +declare module "stream-filter" +declare module "ansi-wordwrap" diff --git a/packages/create-gatsby/tsconfig.json b/packages/create-gatsby/tsconfig.json new file mode 100644 index 0000000000000..1cdcc43b03cae --- /dev/null +++ b/packages/create-gatsby/tsconfig.json @@ -0,0 +1,59 @@ +{ + "compilerOptions": { + /* Basic Options */ + "target": "ES2015" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'. */, + "module": "commonjs" /* Specify module code generation: 'none', commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, + // "lib": [], /* Specify library files to be included in the compilation: */ + "allowJs": true, /* Allow javascript files to be compiled. */ + // "checkJs": true, /* Report errors in .js files. */ + // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ + // "declaration": true, /* Generates corresponding '.d.ts' file. */ + // "sourceMap": true, /* Generates corresponding '.map' file. */ + // "outFile": "./", /* Concatenate and emit output to single file. */ + "outDir": "./lib" /* Redirect output structure to the directory. */, + // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ + // "removeComments": true, /* Do not emit comments to output. */ + // "noEmit": true, /* Do not emit outputs. */ + // "importHelpers": true, /* Import emit helpers from 'tslib'. */ + // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ + // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ + /* Strict Type-Checking Options */ + "strict": true /* Enable all strict type-checking options. */, + // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* Enable strict null checks. */ + // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ + // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ + /* Additional Checks */ + // "noUnusedLocals": true, /* Report errors on unused locals. */ + // "noUnusedParameters": true, /* Report errors on unused parameters. */ + // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + /* Module Resolution Options */ + // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ + // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ + // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ + // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ + // "typeRoots": [], /* List of folders to include type definitions from. */ + // "types": [], /* Type declaration files to be included in compilation. */ + // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ + // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ + /* Source Map Options */ + // "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ + // "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ + // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ + /* Experimental Options */ + // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ + // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ + "esModuleInterop": true, + "resolveJsonModule": true + }, + "include": [ + "src/**/*.ts", + "src/**/*.json", + "src/**/*.js" + ], + "exclude": [ + "**/__tests__/**/*" + ] +} \ No newline at end of file diff --git a/packages/gatsby-cli/src/create-cli.ts b/packages/gatsby-cli/src/create-cli.ts index 16011f669570b..6d9938245d143 100644 --- a/packages/gatsby-cli/src/create-cli.ts +++ b/packages/gatsby-cli/src/create-cli.ts @@ -400,9 +400,7 @@ function buildLocalCommands(cli: yargs.Argv, isLocalSite: boolean): void { builder: yargs => yargs .positional(`cmd`, { - choices: process.env.GATSBY_EXPERIMENTAL_PLUGIN_COMMANDS - ? [`docs`, `add`, `configure`] - : [`docs`], + choices: [`docs`], describe: "Valid commands include `docs`.", type: `string`, }) diff --git a/packages/gatsby-cli/src/plugin-add.ts b/packages/gatsby-cli/src/plugin-add.ts new file mode 100644 index 0000000000000..3cd2c111650f8 --- /dev/null +++ b/packages/gatsby-cli/src/plugin-add.ts @@ -0,0 +1,83 @@ +import { NPMPackage, GatsbyPlugin } from "gatsby-recipes" +import reporter from "./reporter" +const normalizePluginName = (plugin: string): string => { + if (plugin.startsWith(`gatsby-`)) { + return plugin + } + if ( + plugin.startsWith(`source-`) || + plugin.startsWith(`transformer-`) || + plugin.startsWith(`plugin-`) + ) { + return `gatsby-${plugin}` + } + return `gatsby-plugin-${plugin}` +} + +async function installPluginPackage( + plugin: string, + root: string +): Promise { + const installTimer = reporter.activityTimer(`Installing ${plugin}`) + + installTimer.start() + reporter.info(`Installing ${plugin}`) + try { + const result = await NPMPackage.create({ root }, { name: plugin }) + reporter.info(result._message) + } catch (err) { + reporter.error(JSON.parse(err)?.message) + installTimer.setStatus(`FAILED`) + } + installTimer.end() +} + +async function installPluginConfig( + plugin: string, + options: Record | undefined, + root: string +): Promise { + // Plugins can optionally include a key, to allow duplicates + const [pluginName, pluginKey] = plugin.split(`:`) + + const installTimer = reporter.activityTimer( + `Adding ${plugin} to gatsby-config` + ) + + installTimer.start() + reporter.info(`Adding ${pluginName}`) + try { + const result = await GatsbyPlugin.create( + { root }, + { name: pluginName, options, key: pluginKey } + ) + reporter.info(result._message) + } catch (err) { + reporter.error(JSON.parse(err)?.message) + installTimer.setStatus(`FAILED`) + } + installTimer.end() +} + +export async function addPlugins( + plugins: Array, + pluginOptions: Record>, + directory: string, + packages: Array = [] +): Promise { + if (!plugins?.length) { + reporter.error(`Please specify a plugin to install`) + return + } + + const pluginList = plugins.map(normalizePluginName) + + await Promise.all( + packages.map(plugin => installPluginPackage(plugin, directory)) + ) + await Promise.all( + pluginList.map(plugin => + installPluginConfig(plugin, pluginOptions[plugin], directory) + ) + ) +} diff --git a/packages/gatsby-recipes/src/providers/gatsby/plugin.js b/packages/gatsby-recipes/src/providers/gatsby/plugin.js index 3ba8950dba685..192b9f6963301 100644 --- a/packages/gatsby-recipes/src/providers/gatsby/plugin.js +++ b/packages/gatsby-recipes/src/providers/gatsby/plugin.js @@ -131,12 +131,35 @@ const getDescriptionForPlugin = async (root, name) => { const readmeCache = new Map() -const getReadmeForPlugin = async name => { +const getPath = (module, file, root) => { + try { + return require.resolve(`${module}/${file}`, { paths: [root] }) + } catch (e) { + return undefined + } +} + +const getReadmeForPlugin = async (root, name) => { if (readmeCache.has(name)) { return readmeCache.get(name) } + let readmePath + + const readmes = [`readme.txt`, `readme`, `readme.md`, `README`, `README.md`] + while (!readmePath && readmes.length) { + readmePath = getPath(name, readmes.pop(), root) + } + try { + if (readmePath) { + const readme = await fs.readFile(readmePath, `utf8`) + if (readme) { + readmeCache.set(name, readme) + } + return readme + } + const readme = await fetch(`https://unpkg.com/${name}/README.md`) .then(res => res.text()) .catch(() => null) @@ -270,7 +293,7 @@ const read = async ({ root }, id) => { if (plugin?.name) { const [description, readme] = await Promise.all([ getDescriptionForPlugin(root, id), - getReadmeForPlugin(id), + getReadmeForPlugin(root, id), ]) const { shadowedFiles, shadowableFiles } = listShadowableFilesForTheme( root, diff --git a/packages/gatsby/src/commands/plugin-add.ts b/packages/gatsby/src/commands/plugin-add.ts deleted file mode 100644 index 78dd8f7c5e8d4..0000000000000 --- a/packages/gatsby/src/commands/plugin-add.ts +++ /dev/null @@ -1,6 +0,0 @@ -export default async function run({ plugins }: IProgram): Promise { - if (!plugins?.length) { - console.log(`Please specify a plugin to install`) - } - console.log(plugins) -} diff --git a/packages/gatsby/src/commands/plugin.ts b/packages/gatsby/src/commands/plugin.ts index 082e07af5d679..edde15d421dee 100644 --- a/packages/gatsby/src/commands/plugin.ts +++ b/packages/gatsby/src/commands/plugin.ts @@ -1,6 +1,6 @@ -import add from "./plugin-add" +import { IProgram } from "./types" -module.exports = async (args: IProgram): Promise => { +module.exports = async (args: IProgram & { cmd: string }): Promise => { const { report, cmd } = args switch (cmd) { case `docs`: @@ -24,13 +24,10 @@ module.exports = async (args: IProgram): Promise => { - Maintaining a Plugin (https://www.gatsbyjs.com/docs/maintaining-a-plugin/) - Join Discord #plugin-authoring channel to ask questions! (https://gatsby.dev/discord/) `) - return void 0 - - case `add`: - return add(args) + return default: report.error(`Unknown command ${cmd}`) } - return void 0 + return } diff --git a/yarn.lock b/yarn.lock index da7f091fe8da2..53133d8fef6a8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1193,7 +1193,7 @@ core-js-pure "^3.0.0" regenerator-runtime "^0.13.4" -"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.0", "@babel/runtime@^7.10.2", "@babel/runtime@^7.10.5", "@babel/runtime@^7.11.2", "@babel/runtime@^7.3.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": +"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.0", "@babel/runtime@^7.10.2", "@babel/runtime@^7.10.5", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.3.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": version "7.12.1" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.12.1.tgz#b4116a6b6711d010b2dad3b7b6e43bf1b9954740" integrity sha512-J5AIf3vPj3UwXaAzb5j1xM4WAQDX3EMgemF8rjCP3SoW09LfRKAXQKt6CoVYl230P6iWdRcBbnLDDdnqWxZSCA== @@ -1377,6 +1377,22 @@ resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz#8eed982e2ee6f7f4e44c253e12962980791efd46" integrity sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA== +"@eslint/eslintrc@^0.2.1": + version "0.2.1" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.2.1.tgz#f72069c330461a06684d119384435e12a5d76e3c" + integrity sha512-XRUeBZ5zBWLYgSANMpThFddrZZkEbGHgUdt5UJjZfnlN9BGCiUBrf+nvbRupSjMvqzwnQN0qwCmOxITt1cfywA== + dependencies: + ajv "^6.12.4" + debug "^4.1.1" + espree "^7.3.0" + globals "^12.1.0" + ignore "^4.0.6" + import-fresh "^3.2.1" + js-yaml "^3.13.1" + lodash "^4.17.19" + minimatch "^3.0.4" + strip-json-comments "^3.1.1" + "@evocateur/libnpmaccess@^3.1.2": version "3.1.2" resolved "https://registry.yarnpkg.com/@evocateur/libnpmaccess/-/libnpmaccess-3.1.2.tgz#ecf7f6ce6b004e9f942b098d92200be4a4b1c845" @@ -3682,6 +3698,11 @@ version "2.1.1" resolved "https://registry.yarnpkg.com/@types/configstore/-/configstore-2.1.1.tgz#cd1e8553633ad3185c3f2f239ecff5d2643e92b6" +"@types/configstore@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@types/configstore/-/configstore-4.0.0.tgz#cb718f9507e9ee73782f40d07aaca1cd747e36fa" + integrity sha512-SvCBBPzOIe/3Tu7jTl2Q8NjITjLmq9m7obzjSyb8PXWWZ31xVK6w4T6v8fOx+lrgQnqk3Yxc00LDolFsSakKCA== + "@types/connect@*": version "3.4.32" resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.32.tgz#aa0e9616b9435ccad02bc52b5b454ffc2c70ba28" @@ -3744,6 +3765,13 @@ dependencies: "@types/node" "*" +"@types/fs-extra@^9.0.2": + version "9.0.2" + resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-9.0.2.tgz#e1e1b578c48e8d08ae7fc36e552b94c6f4621609" + integrity sha512-jp0RI6xfZpi5JL8v7WQwpBEQTq63RqW2kxwTZt+m27LcJqQdPVU1yGnT1ZI4EtCDynQQJtIGyQahkiCGCS7e+A== + dependencies: + "@types/node" "*" + "@types/get-port@^3.2.0": version "3.2.0" resolved "https://registry.yarnpkg.com/@types/get-port/-/get-port-3.2.0.tgz#f9e0a11443cc21336470185eae3dfba4495d29bc" @@ -3942,6 +3970,11 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-12.19.3.tgz#a6e252973214079155f749e8bef99cc80af182fa" integrity sha512-8Jduo8wvvwDzEVJCOvS/G6sgilOLvvhn1eMmK3TW8/T217O7u1jdrK6ImKLv80tVryaPSVeKu6sjDEiFjd4/eg== +"@types/node@^14.14.5": + version "14.14.5" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.5.tgz#e92d3b8f76583efa26c1a63a21c9d3c1143daa29" + integrity sha512-H5Wn24s/ZOukBmDn03nnGTp18A60ny9AmCwnEcgJiTgSGsCO7k+NWP7zjCCbhlcnVCoI+co52dUAt9GMhOSULw== + "@types/node@^8.5.7": version "8.10.59" resolved "https://registry.yarnpkg.com/@types/node/-/node-8.10.59.tgz#9e34261f30183f9777017a13d185dfac6b899e04" @@ -4644,6 +4677,11 @@ acorn-jsx@^5.0.0, acorn-jsx@^5.0.1, acorn-jsx@^5.1.0: resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.2.0.tgz#4c66069173d6fdd68ed85239fc256226182b2ebe" integrity sha512-HiUX/+K2YpkpJ+SzBffkM/AQ2YE03S0U1kjTLVpoJdhZMOWy8qvXVN9JdLqv2QsaQ6MPYQIuNmwD8zOiYUofLQ== +acorn-jsx@^5.2.0: + version "5.3.1" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.1.tgz#fc8661e11b7ac1539c47dbfea2e72b3af34d267b" + integrity sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng== + acorn-walk@^6.0.1: version "6.1.1" resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-6.1.1.tgz#d363b66f5fac5f018ff9c3a1e7b6f8e310cc3913" @@ -4668,6 +4706,11 @@ acorn@^7.0.0, acorn@^7.1.0: resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.3.1.tgz#85010754db53c3fbaf3b9ea3e083aa5c5d147ffd" integrity sha512-tLc0wSnatxAQHVHUapaHdz72pi9KUyHjq5KyHjGg9Y8Ifdc79pTh2XvI6I1/chZbnM7QtNKzh66ooDogPZSleA== +acorn@^7.4.0: + version "7.4.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" + integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== + address@1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/address/-/address-1.0.3.tgz#b5f50631f8d6cec8bd20c963963afb55e06cbce9" @@ -4895,6 +4938,11 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0: "@types/color-name" "^1.1.1" color-convert "^2.0.1" +ansi-wordwrap@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/ansi-wordwrap/-/ansi-wordwrap-1.0.2.tgz#0195cc379ec07ce9bc263500efaa8a13dbb93fd7" + integrity sha512-kpF1wxavNq9oWFepGI+VDdA1h+NQW0gUfNZFBw6Ukujz6L5f0GGUqnTRgoY0//0D8cYIcpR93+deF/IX0ORcmA== + ansi-wrap@0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/ansi-wrap/-/ansi-wrap-0.1.0.tgz#a82250ddb0015e9a27ca82e82ea603bbfa45efaf" @@ -6747,6 +6795,11 @@ change-case@^3.0.1, change-case@^3.1.0: upper-case "^1.1.1" upper-case-first "^1.1.0" +char-regex@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf" + integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw== + character-entities-html4@^1.0.0: version "1.1.2" resolved "https://registry.yarnpkg.com/character-entities-html4/-/character-entities-html4-1.1.2.tgz#c44fdde3ce66b52e8d321d6c1bf46101f0150610" @@ -8486,7 +8539,7 @@ deep-freeze@^0.0.1: resolved "https://registry.yarnpkg.com/deep-freeze/-/deep-freeze-0.0.1.tgz#3a0b0005de18672819dfd38cd31f91179c893e84" integrity sha1-OgsABd4YZygZ39OM0x+RF5yJPoQ= -deep-is@~0.1.3: +deep-is@^0.1.3, deep-is@~0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" @@ -9339,7 +9392,7 @@ enhanced-resolve@^4.1.1, enhanced-resolve@^4.2.0, enhanced-resolve@^4.3.0: memory-fs "^0.5.0" tapable "^1.0.0" -enquirer@^2.3.6: +enquirer@^2.3.5, enquirer@^2.3.6: version "2.3.6" resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d" integrity sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg== @@ -9733,6 +9786,14 @@ eslint-scope@^5.0.0: esrecurse "^4.1.0" estraverse "^4.1.1" +eslint-scope@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" + integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== + dependencies: + esrecurse "^4.3.0" + estraverse "^4.1.1" + eslint-utils@^1.3.1, eslint-utils@^1.4.3: version "1.4.3" resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.4.3.tgz#74fec7c54d0776b6f67e0251040b5806564e981f" @@ -9747,11 +9808,28 @@ eslint-utils@^2.0.0: dependencies: eslint-visitor-keys "^1.1.0" +eslint-utils@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-2.1.0.tgz#d2de5e03424e707dc10c74068ddedae708741b27" + integrity sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg== + dependencies: + eslint-visitor-keys "^1.1.0" + eslint-visitor-keys@^1.0.0, eslint-visitor-keys@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz#e2a82cea84ff246ad6fb57f9bde5b46621459ec2" integrity sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A== +eslint-visitor-keys@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz#30ebd1ef7c2fdff01c3a4f151044af25fab0523e" + integrity sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ== + +eslint-visitor-keys@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz#21fdc8fbcd9c795cc0321f0563702095751511a8" + integrity sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ== + eslint@^4.19.1: version "4.19.1" resolved "https://registry.yarnpkg.com/eslint/-/eslint-4.19.1.tgz#32d1d653e1d90408854bfb296f076ec7e186a300" @@ -9880,6 +9958,49 @@ eslint@^6.8.0: text-table "^0.2.0" v8-compile-cache "^2.0.3" +eslint@^7.12.1: + version "7.12.1" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.12.1.tgz#bd9a81fa67a6cfd51656cdb88812ce49ccec5801" + integrity sha512-HlMTEdr/LicJfN08LB3nM1rRYliDXOmfoO4vj39xN6BLpFzF00hbwBoqHk8UcJ2M/3nlARZWy/mslvGEuZFvsg== + dependencies: + "@babel/code-frame" "^7.0.0" + "@eslint/eslintrc" "^0.2.1" + ajv "^6.10.0" + chalk "^4.0.0" + cross-spawn "^7.0.2" + debug "^4.0.1" + doctrine "^3.0.0" + enquirer "^2.3.5" + eslint-scope "^5.1.1" + eslint-utils "^2.1.0" + eslint-visitor-keys "^2.0.0" + espree "^7.3.0" + esquery "^1.2.0" + esutils "^2.0.2" + file-entry-cache "^5.0.1" + functional-red-black-tree "^1.0.1" + glob-parent "^5.0.0" + globals "^12.1.0" + ignore "^4.0.6" + import-fresh "^3.0.0" + imurmurhash "^0.1.4" + is-glob "^4.0.0" + js-yaml "^3.13.1" + json-stable-stringify-without-jsonify "^1.0.1" + levn "^0.4.1" + lodash "^4.17.19" + minimatch "^3.0.4" + natural-compare "^1.4.0" + optionator "^0.9.1" + progress "^2.0.0" + regexpp "^3.1.0" + semver "^7.2.1" + strip-ansi "^6.0.0" + strip-json-comments "^3.1.0" + table "^5.2.3" + text-table "^0.2.0" + v8-compile-cache "^2.0.3" + espree@^3.5.4: version "3.5.4" resolved "https://registry.yarnpkg.com/espree/-/espree-3.5.4.tgz#b0f447187c8a8bed944b815a660bddf5deb5d1a7" @@ -9905,6 +10026,15 @@ espree@^6.1.2: acorn-jsx "^5.1.0" eslint-visitor-keys "^1.1.0" +espree@^7.3.0: + version "7.3.0" + resolved "https://registry.yarnpkg.com/espree/-/espree-7.3.0.tgz#dc30437cf67947cf576121ebd780f15eeac72348" + integrity sha512-dksIWsvKCixn1yrEXO8UosNSxaDoSYpq9reEjZSbHLpT5hpaCAKTLBwq0RHtLrIr+c0ByiYzWT8KTMRzoRCNlw== + dependencies: + acorn "^7.4.0" + acorn-jsx "^5.2.0" + eslint-visitor-keys "^1.3.0" + esprima@^2.6.0: version "2.7.3" resolved "https://registry.yarnpkg.com/esprima/-/esprima-2.7.3.tgz#96e3b70d5779f6ad49cd032673d1c312767ba581" @@ -9932,12 +10062,26 @@ esquery@^1.0.0, esquery@^1.0.1: dependencies: estraverse "^4.0.0" +esquery@^1.2.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.3.1.tgz#b78b5828aa8e214e29fb74c4d5b752e1c033da57" + integrity sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ== + dependencies: + estraverse "^5.1.0" + esrecurse@^4.1.0: version "4.2.1" resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.2.1.tgz#007a3b9fdbc2b3bb87e4879ea19c92fdbd3942cf" dependencies: estraverse "^4.1.0" +esrecurse@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" + integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== + dependencies: + estraverse "^5.2.0" + estraverse-fb@^1.3.2: version "1.3.2" resolved "https://registry.yarnpkg.com/estraverse-fb/-/estraverse-fb-1.3.2.tgz#d323a4cb5e5ac331cea033413a9253e1643e07c4" @@ -9947,6 +10091,11 @@ estraverse@^4.0.0, estraverse@^4.1.0, estraverse@^4.1.1, estraverse@^4.2.0, estr resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== +estraverse@^5.1.0, estraverse@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.2.0.tgz#307df42547e6cc7324d3cf03c155d5cdb8c53880" + integrity sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ== + estraverse@~1.5.0: version "1.5.1" resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-1.5.1.tgz#867a3e8e58a9f84618afb6c2ddbcd916b7cbaf71" @@ -15192,6 +15341,14 @@ levn@^0.3.0, levn@~0.3.0: prelude-ls "~1.1.2" type-check "~0.3.2" +levn@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" + integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== + dependencies: + prelude-ls "^1.2.1" + type-check "~0.4.0" + li@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/li/-/li-1.3.0.tgz#22c59bcaefaa9a8ef359cf759784e4bf106aea1b" @@ -17755,6 +17912,18 @@ optionator@^0.8.1, optionator@^0.8.2, optionator@^0.8.3: type-check "~0.3.2" word-wrap "~1.2.3" +optionator@^0.9.1: + version "0.9.1" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499" + integrity sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw== + dependencies: + deep-is "^0.1.3" + fast-levenshtein "^2.0.6" + levn "^0.4.1" + prelude-ls "^1.2.1" + type-check "^0.4.0" + word-wrap "^1.2.3" + ordered-read-streams@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/ordered-read-streams/-/ordered-read-streams-1.0.1.tgz#77c0cb37c41525d64166d990ffad7ec6a0e1363e" @@ -19134,6 +19303,11 @@ prebuild-install@^5.3.4: tunnel-agent "^0.6.0" which-pm-runs "^1.0.0" +prelude-ls@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" + integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== + prelude-ls@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" @@ -19156,7 +19330,7 @@ prettier-linter-helpers@^1.0.0: dependencies: fast-diff "^1.1.2" -prettier@2.1.2, prettier@^2.0.5: +prettier@2.1.2, prettier@^2.0.5, prettier@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.1.2.tgz#3050700dae2e4c8b67c4c3f666cdb8af405e1ce5" integrity sha512-16c7K+x4qVlJg9rEbXl7HEGmQyZlG4R9AgP+oHKRMsMsuk8s+ATStlf1NpDqyBI1HpVyfjLOeMhH2LvuNvV5Vg== @@ -20271,6 +20445,11 @@ regexpp@^3.0.0: resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.0.0.tgz#dd63982ee3300e67b41c1956f850aa680d9d330e" integrity sha512-Z+hNr7RAVWxznLPuA7DIh8UNX1j9CDrUQxskw9IrBE1Dxue2lyXT+shqEIeLUjrokxIP8CMy1WkjgG3rTsd5/g== +regexpp@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.1.0.tgz#206d0ad0a5648cffbdb8ae46438f3dc51c9f78e2" + integrity sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q== + regexpu-core@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-1.0.0.tgz#86a763f58ee4d7c2f6b102e4764050de7ed90c6b" @@ -22170,7 +22349,7 @@ semver@7.0.0: resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e" integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A== -semver@^7.3.2: +semver@^7.2.1, semver@^7.3.2: version "7.3.2" resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.2.tgz#604962b052b81ed0786aae84389ffba70ffd3938" integrity sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ== @@ -22984,6 +23163,14 @@ stream-each@^1.1.0: end-of-stream "^1.1.0" stream-shift "^1.0.0" +stream-filter@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/stream-filter/-/stream-filter-2.1.0.tgz#bdff4eee3cde4c1525e02b78dde93d821fe529bd" + integrity sha1-vf9O7jzeTBUl4Ct43ek9gh/lKb0= + dependencies: + through2 "^2.0.1" + xtend "^4.0.1" + stream-http@^2.7.2: version "2.8.3" resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.8.3.tgz#b2d242469288a5a27ec4fe8933acf623de6514fc" @@ -23053,6 +23240,14 @@ string-length@^3.1.0: astral-regex "^1.0.0" strip-ansi "^5.2.0" +string-length@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.1.tgz#4a973bf31ef77c4edbceadd6af2611996985f8a1" + integrity sha512-PKyXUd0LK0ePjSOnWn34V2uD6acUWev9uy0Ft05k0E8xRW+SKcA0F7eMr7h5xlzfn+4O3N+55rduYyet3Jk+jw== + dependencies: + char-regex "^1.0.2" + strip-ansi "^6.0.0" + string-similarity@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/string-similarity/-/string-similarity-1.2.2.tgz#99b2c20a3c9bbb3903964eae1d89856db3d8db9b" @@ -23292,6 +23487,11 @@ strip-json-comments@^3.0.1: resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.0.1.tgz#85713975a91fb87bf1b305cca77395e40d2a64a7" integrity sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw== +strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" + integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== + strip-outer@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/strip-outer/-/strip-outer-1.0.1.tgz#b2fd2abf6604b9d1e6013057195df836b8a9d631" @@ -23737,7 +23937,7 @@ term-size@^2.1.0: resolved "https://registry.yarnpkg.com/term-size/-/term-size-2.1.0.tgz#3aec444c07a7cf936e157c1dc224b590c3c7eef2" integrity sha512-I42EWhJ+2aeNQawGx1VtpO0DFI9YcfuvAMNIdKyf/6sRbHJ4P+ZQ/zIT87tE+ln1ymAGcCJds4dolfSAS0AcNg== -terminal-link@^2.0.0: +terminal-link@^2.0.0, terminal-link@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/terminal-link/-/terminal-link-2.1.1.tgz#14a64a27ab3c0df933ea546fba55f2d078edc994" integrity sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ== @@ -24264,6 +24464,13 @@ tweetnacl@^0.14.3, tweetnacl@~0.14.0: version "0.14.5" resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" +type-check@^0.4.0, type-check@~0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" + integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== + dependencies: + prelude-ls "^1.2.1" + type-check@~0.3.2: version "0.3.2" resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" @@ -24352,6 +24559,11 @@ typescript@^3.9.5, typescript@^3.9.7: resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.7.tgz#98d600a5ebdc38f40cb277522f12dc800e9e25fa" integrity sha512-BLbiRkiBzAwsjut4x/dsibSTB6yWpwT5qWmC2OfuCg3GgVQCSgMs4vEctYPhsaGtd0AeuuHMkjZ2h2WG8MSzRw== +typescript@^4.0.5: + version "4.0.5" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.0.5.tgz#ae9dddfd1069f1cb5beb3ef3b2170dd7c1332389" + integrity sha512-ywmr/VrTVCmNTJ6iV2LwIrfG1P+lv6luD8sUJs+2eI9NLGigaN+nUQc13iHqisq7bra9lnmUSYqbJvegraBOPQ== + typography-normalize@^0.16.19: version "0.16.19" resolved "https://registry.yarnpkg.com/typography-normalize/-/typography-normalize-0.16.19.tgz#58e0cf12466870c5b27006daa051fe7307780660" @@ -25738,7 +25950,7 @@ wonka@^4.0.14: resolved "https://registry.yarnpkg.com/wonka/-/wonka-4.0.14.tgz#77d680a84e575ed15a9f975eb87d6c530488f3a4" integrity sha512-v9vmsTxpZjrA8CYfztbuoTQSHEsG3ZH+NCYfasHm0V3GqBupXrjuuz0RJyUaw2cRO7ouW2js0P6i853/qxlDcA== -word-wrap@~1.2.3: +word-wrap@^1.2.3, word-wrap@~1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==