diff --git a/docs/docs/quick-start.md b/docs/docs/quick-start.md index 340f4a9dfabe4..b64028af142d6 100644 --- a/docs/docs/quick-start.md +++ b/docs/docs/quick-start.md @@ -12,7 +12,7 @@ This quick start is intended for intermediate to advanced developers. For a gent npm init gatsby ``` -It'll ask for a site title and the name of the project's directory. Continue following the prompts to choose your preferred CMS, styling tools and additional features. +It'll ask for a site title and the name of the project's directory. Continue following the prompts to choose your preferred language (JavaScript or TypeScript), CMS, styling tools and additional features. 2. Once everything is downloaded you will see a message with instructions for navigating to your site and running it locally. @@ -38,6 +38,18 @@ Try editing the home page in `src/pages/index.js`. Saved changes will live reloa ## What's next? +### Use flags + +The CLI also supports two flags: + +- `-y` skips the questionnaire +- `-ts` initializes your project with the [minimal TypeScript starter](https://github.com/gatsbyjs/gatsby-starter-minimal-ts) instead of the [minimal JavaScript starter](https://github.com/gatsbyjs/gatsby-starter-minimal) + +Flags are not positional, so these commands are equivalent: + +- `npm init gatsby -y -ts my-site-name` +- `npm init gatsby my-site-name -y -ts` + ### Add more features [Follow our guides](/docs/how-to/) to add more functionality to your site or browse [our plugins](/plugins/) to quickly install additional features. diff --git a/packages/create-gatsby/src/__tests__/run.ts b/packages/create-gatsby/src/__tests__/run.ts new file mode 100644 index 0000000000000..48d17db23ec5b --- /dev/null +++ b/packages/create-gatsby/src/__tests__/run.ts @@ -0,0 +1,255 @@ +import { reporter } from "../utils/reporter" +import { initStarter } from "../init-starter" +import { trackCli } from "../tracking" +import { run, DEFAULT_STARTERS } from "../index" + +jest.mock(`../utils/parse-args`) +jest.mock(`enquirer`, () => { + const OriginalEnquirer = jest.requireActual(`enquirer`) + + class MockedEnquirer extends OriginalEnquirer { + constructor() { + super() + // Turns waiting for user input off and autofills with answers + this.options = { show: false, autofill: true } + + // Mock answers + this.answers = { + // First prompt answer + name: `hello-world`, + + // Main question set answers + project: `hello-world`, + language: `js`, + cms: `none`, + styling: `none`, + features: [], + + // Confirmation prompt answer + confirm: true, + } + } + } + return MockedEnquirer +}) +jest.mock(`../utils/reporter`) +jest.mock(`../tracking`, () => { + return { + trackCli: jest.fn(), + } +}) +jest.mock(`../init-starter`, () => { + return { + initStarter: jest.fn(), + getPackageManager: jest.fn(), + gitSetup: jest.fn(), + } +}) +jest.mock(`../install-plugins`, () => { + return { + installPlugins: jest.fn(), + } +}) +jest.mock(`../utils/site-metadata`, () => { + return { + setSiteMetadata: jest.fn(), + } +}) +jest.mock(`../utils/hash`, () => { + return { + sha256: jest.fn(args => args), + md5: jest.fn(args => args), + } +}) +jest.mock(`../utils/question-helpers`, () => { + const originalQuestionHelpers = jest.requireActual( + `../utils/question-helpers` + ) + return { + ...originalQuestionHelpers, + validateProjectName: jest.fn(() => true), + } +}) +jest.mock(`../components/utils`, () => { + return { + center: jest.fn(args => args), + wrap: jest.fn(args => args), + } +}) + +const dirName = `hello-world` +let parseArgsMock + +describe(`run`, () => { + beforeEach(() => { + jest.clearAllMocks() + parseArgsMock = require(`../utils/parse-args`).parseArgs + }) + + describe(`no skip flag`, () => { + beforeEach(() => { + parseArgsMock.mockReturnValueOnce({ + flags: { yes: false }, + dirName, + }) + }) + + it(`should welcome the user`, async () => { + await run() + expect(reporter.info).toHaveBeenCalledWith( + expect.stringContaining(`Welcome to Gatsby!`) + ) + }) + it(`should communicate setup questions will be asked`, async () => { + await run() + expect(reporter.info).toHaveBeenCalledWith( + expect.stringContaining( + `This command will generate a new Gatsby site for you` + ) + ) + }) + it(`should confirm actions`, async () => { + await run() + expect(reporter.info).toHaveBeenCalledWith( + expect.stringContaining(`Thanks! Here's what we'll now do`) + ) + }) + it(`should notify of successful site creation`, async () => { + await run() + expect(reporter.success).toHaveBeenCalledWith( + expect.stringContaining(`Created site`) + ) + }) + }) + + describe(`skip flag`, () => { + beforeEach(() => { + parseArgsMock.mockReturnValueOnce({ + flags: { yes: true }, + dirName, + }) + }) + + it(`should welcome the user`, async () => { + await run() + expect(reporter.info).toHaveBeenCalledWith( + expect.stringContaining(`Welcome to Gatsby!`) + ) + }) + it(`should not communicate setup questions`, async () => { + await run() + expect(reporter.info).not.toHaveBeenCalledWith( + expect.stringContaining( + `This command will generate a new Gatsby site for you` + ) + ) + }) + it(`should not confirm actions`, async () => { + await run() + expect(reporter.info).not.toHaveBeenCalledWith( + expect.stringContaining(`Thanks! Here's what we'll now do`) + ) + }) + it(`should notify of successful site creation`, async () => { + await run() + expect(reporter.success).toHaveBeenCalledWith( + expect.stringContaining(`Created site`) + ) + }) + it(`should use the JS starter by default`, async () => { + await run() + expect(initStarter).toHaveBeenCalledWith( + DEFAULT_STARTERS.js, + dirName, + [], + dirName + ) + }) + it(`should track JS was selected as language`, async () => { + await run() + expect(trackCli).toHaveBeenCalledWith(`CREATE_GATSBY_SELECT_OPTION`, { + name: `LANGUAGE`, + valueString: `js`, + }) + }) + }) + + describe(`no ts flag`, () => { + beforeEach(() => { + parseArgsMock.mockReturnValueOnce({ + flags: { ts: false }, + dirName, + }) + }) + + it(`should use the JS starter`, async () => { + await run() + expect(initStarter).toHaveBeenCalledWith( + DEFAULT_STARTERS.js, + dirName, + [], + dirName + ) + }) + it(`should track JS was selected as language`, async () => { + await run() + expect(trackCli).toHaveBeenCalledWith(`CREATE_GATSBY_SELECT_OPTION`, { + name: `LANGUAGE`, + valueString: `js`, + }) + }) + }) + + describe(`ts flag`, () => { + beforeEach(() => { + parseArgsMock.mockReturnValueOnce({ + flags: { ts: true }, + dirName, + }) + }) + + it(`should use the TS starter`, async () => { + await run() + expect(initStarter).toHaveBeenCalledWith( + DEFAULT_STARTERS.ts, + dirName, + [], + dirName + ) + }) + + it(`should track TS was selected as language`, async () => { + await run() + expect(trackCli).toHaveBeenCalledWith(`CREATE_GATSBY_SELECT_OPTION`, { + name: `LANGUAGE`, + valueString: `ts`, + }) + }) + }) +}) + +describe(`skip and ts flag`, () => { + beforeEach(() => { + parseArgsMock.mockReturnValueOnce({ + flags: { yes: true, ts: true }, + dirName, + }) + }) + + it(`should use the TS starter`, async () => { + await run() + expect(initStarter).toHaveBeenCalledWith( + DEFAULT_STARTERS.ts, + dirName, + [], + dirName + ) + }) + it(`should track TS was selected as language`, async () => { + await run() + expect(trackCli).toHaveBeenCalledWith(`CREATE_GATSBY_SELECT_OPTION`, { + name: `LANGUAGE`, + valueString: `ts`, + }) + }) +}) diff --git a/packages/create-gatsby/src/__tests__/utils/parse-args.ts b/packages/create-gatsby/src/__tests__/utils/parse-args.ts new file mode 100644 index 0000000000000..21f086b0c3d20 --- /dev/null +++ b/packages/create-gatsby/src/__tests__/utils/parse-args.ts @@ -0,0 +1,52 @@ +import { parseArgs } from "../../utils/parse-args" +import { reporter } from "../../utils/reporter" + +const dirNameArg = `hello-world` + +jest.mock(`../../utils/reporter`) + +describe(`parseArgs`, () => { + it(`should parse without flags and dir name`, () => { + const { flags, dirName } = parseArgs([]) + expect(flags.yes).toBeFalsy() + expect(flags.ts).toBeFalsy() + expect(dirName).toEqual(``) + }) + it(`should parse with dir name without flags`, () => { + const { flags, dirName } = parseArgs([dirNameArg]) + expect(flags.yes).toBeFalsy() + expect(flags.ts).toBeFalsy() + expect(dirName).toEqual(dirNameArg) + }) + it(`should parse with flags before dir name`, () => { + const { flags, dirName } = parseArgs([`-y`, `-ts`, dirNameArg]) + expect(flags.yes).toBeTruthy() + expect(flags.ts).toBeTruthy() + expect(dirName).toEqual(dirNameArg) + }) + it(`should parse with flags after dir name`, () => { + const { flags, dirName } = parseArgs([dirNameArg, `-y`, `-ts`]) + expect(flags.yes).toBeTruthy() + expect(flags.ts).toBeTruthy() + expect(dirName).toEqual(dirNameArg) + }) + it(`should parse with flags before and after dir name`, () => { + const { flags, dirName } = parseArgs([`-y`, dirNameArg, `-ts`]) + expect(flags.yes).toBeTruthy() + expect(flags.ts).toBeTruthy() + expect(dirName).toEqual(dirNameArg) + }) + it(`should warn if unknown flags are used`, () => { + const unknownFlag = `-unknown` + const { flags, dirName } = parseArgs([dirNameArg, unknownFlag]) + expect(reporter.warn).toBeCalledTimes(1) + expect(reporter.warn).toBeCalledWith( + expect.stringContaining( + `Found unknown argument "${unknownFlag}", ignoring. Known arguments are: -y, -ts` + ) + ) + expect(flags.yes).toBeFalsy() + expect(flags.ts).toBeFalsy() + expect(dirName).toEqual(dirNameArg) + }) +}) diff --git a/packages/create-gatsby/src/__tests__/utils/question-helpers.ts b/packages/create-gatsby/src/__tests__/utils/question-helpers.ts new file mode 100644 index 0000000000000..95911844b07c6 --- /dev/null +++ b/packages/create-gatsby/src/__tests__/utils/question-helpers.ts @@ -0,0 +1,143 @@ +import fs from "fs" +import { reporter } from "../../utils/reporter" +import { + makeChoices, + validateProjectName, + generateQuestions, +} from "../../utils/question-helpers" + +jest.mock(`fs`) +jest.mock(`../../utils/reporter`) + +describe(`question-helpers`, () => { + describe(`makeChoices`, () => { + it(`should return a select none option by default`, () => { + const options = { + init: { + message: `hello world`, + }, + } + const choices = makeChoices(options) + const [none] = choices + expect(none).toMatchObject({ + message: `No (or I'll add it later)`, + }) + }) + + it(`should return no select none option if must select indicated`, () => { + const name = `init` + const message = `hello world` + const options = { + [name]: { + message, + }, + } + const choices = makeChoices(options, true) + const [option] = choices + expect(option).toMatchObject({ + message, + name, + }) + }) + }) + + describe(`validateProjectName`, () => { + it(`should warn if no dir name`, () => { + const valid = validateProjectName(``) + expect(valid).toBeFalsy() + expect(reporter.warn).toBeCalledWith( + expect.stringContaining( + `You have not provided a directory name for your site. Please do so when running with the 'y' flag.` + ) + ) + }) + + it(`should warn if dir name has special character`, () => { + const name = ` { + jest.spyOn(fs, `existsSync`).mockReturnValueOnce(true) + const name = `hello-world` + const valid = validateProjectName(name) + expect(valid).toBeFalsy() + expect(reporter.warn).toBeCalledWith( + expect.stringContaining( + `The destination "${name}" already exists. Please choose a different name` + ) + ) + }) + + it(`should return true if the dir name meets all conditions`, () => { + const valid = validateProjectName(`hello-world`) + expect(valid).toBeTruthy() + }) + + describe(`windows`, () => { + const originalPlatform = process.platform + + beforeEach(() => { + Object.defineProperty(process, `platform`, { value: `win32` }) + }) + + afterEach(() => { + Object.defineProperty(process, `platform`, { value: originalPlatform }) + }) + + it(`should warn if dir name has invalid patterns`, () => { + const name = `aux` + const valid = validateProjectName(name) + expect(valid).toBeFalsy() + expect(reporter.warn).toBeCalledWith( + expect.stringContaining( + `The destination "${name}" is not a valid Windows filename. Please try another name` + ) + ) + }) + }) + }) + + describe(`generateQuestions`, () => { + it(`should return one question if the skip flag is passed`, () => { + const question = generateQuestions(`hello-world`, { + yes: true, + ts: false, + }) + expect(question.name).toEqual(`project`) + }) + + it(`should return all questions if no skip flag is passed`, () => { + const questions = generateQuestions(`hello-world`, { + yes: false, + ts: false, + }) + const [first, second, third, fourth, fifth] = questions + expect(questions).toHaveLength(5) + expect(first.name).toEqual(`project`) + expect(second.name).toEqual(`language`) + expect(third.name).toEqual(`cms`) + expect(fourth.name).toEqual(`styling`) + expect(fifth.name).toEqual(`features`) + }) + + it(`should return all questions except for language if ts flag is passed`, () => { + const questions = generateQuestions(`hello-world`, { + yes: false, + ts: true, + }) + const [first, second, third, fourth] = questions + expect(questions).toHaveLength(4) + expect(first.name).toEqual(`project`) + expect(second.name).toEqual(`cms`) + expect(third.name).toEqual(`styling`) + expect(fourth.name).toEqual(`features`) + }) + }) +}) diff --git a/packages/create-gatsby/src/index.ts b/packages/create-gatsby/src/index.ts index 3107ff68112ae..be25487a8bba5 100644 --- a/packages/create-gatsby/src/index.ts +++ b/packages/create-gatsby/src/index.ts @@ -2,6 +2,7 @@ import Enquirer from "enquirer" import cmses from "./questions/cmses.json" import styles from "./questions/styles.json" import features from "./questions/features.json" +import languages from "./questions/languages.json" import { initStarter, getPackageManager, gitSetup } from "./init-starter" import { installPlugins } from "./install-plugins" import colors from "ansi-colors" @@ -15,17 +16,22 @@ import { reporter } from "./utils/reporter" import { setSiteMetadata } from "./utils/site-metadata" import { makeNpmSafe } from "./utils/make-npm-safe" import { - validateProjectName, generateQuestions, + validateProjectName, } from "./utils/question-helpers" import { sha256, md5 } from "./utils/hash" import { maybeUseEmoji } from "./utils/emoji" +import { parseArgs } from "./utils/parse-args" -const DEFAULT_STARTER = `https://github.com/gatsbyjs/gatsby-starter-minimal.git` +export const DEFAULT_STARTERS: Record = { + js: `https://github.com/gatsbyjs/gatsby-starter-minimal.git`, + ts: `https://github.com/gatsbyjs/gatsby-starter-minimal-ts.git`, +} interface IAnswers { name: string project: string + language: keyof typeof languages styling?: keyof typeof styles cms?: keyof typeof cmses features?: Array @@ -59,12 +65,7 @@ export type PluginMap = Record export type PluginConfigMap = Record> export async function run(): Promise { - const [flag, siteDirectory] = process.argv.slice(2) // TODO - Refactor this to not be positional in upcoming TS PR since it's related - - let yesFlag = false - if (flag === `-y`) { - yesFlag = true - } + const { flags, dirName } = parseArgs(process.argv.slice(2)) trackCli(`CREATE_GATSBY_START`) @@ -72,6 +73,7 @@ export async function run(): Promise { reporter.info(colors.grey(`create-gatsby version ${version}`)) + // Wecome message reporter.info( ` @@ -82,7 +84,8 @@ ${center(colors.blueBright.bold.underline(`Welcome to Gatsby!`))} ` ) - if (!yesFlag) { + // If we aren't skipping prompts, communicate we'll ask setup questions + if (!flags.yes) { reporter.info( wrap( `This command will generate a new Gatsby site for you in ${colors.bold( @@ -96,108 +99,126 @@ ${center(colors.blueBright.bold.underline(`Welcome to Gatsby!`))} } const enquirer = new Enquirer() - enquirer.use(plugin) - let data - let siteName + // If we aren't skipping prompts, get a site name first to use as a default folder name + let npmSafeSiteName - if (!yesFlag) { - ;({ name: siteName } = await enquirer.prompt({ + if (!flags.yes) { + const { name } = await enquirer.prompt({ type: `textinput`, name: `name`, message: `What would you like to call your site?`, initial: `My Gatsby Site`, format: (value: string): string => colors.cyan(value), - } as any)) + } as any) - data = await enquirer.prompt( - generateQuestions(makeNpmSafe(siteName), yesFlag) - ) + npmSafeSiteName = makeNpmSafe(name) } else { - const warn = await validateProjectName(siteDirectory) - if (typeof warn === `string`) { - reporter.warn(warn) + const valid = validateProjectName(dirName) + + if (!valid) { return } - siteName = siteDirectory - data = await enquirer.prompt( - generateQuestions(makeNpmSafe(siteDirectory), yesFlag)[0] - ) + + npmSafeSiteName = makeNpmSafe(dirName) } - data.project = data.project.trim() + // Prompt user with questions and gather answers + const questions = generateQuestions(npmSafeSiteName, flags) + const answers = await enquirer.prompt(questions) + answers.project = answers.project.trim() + + // Language selection + if (flags.yes) { + answers.language = `js` + } + if (flags.ts) { + answers.language = `ts` + } + + // Telemetry trackCli(`CREATE_GATSBY_SELECT_OPTION`, { name: `project_name`, - valueString: sha256(data.project), + valueString: sha256(answers.project), + }) + trackCli(`CREATE_GATSBY_SELECT_OPTION`, { + name: `LANGUAGE`, + valueString: answers.language, }) trackCli(`CREATE_GATSBY_SELECT_OPTION`, { name: `CMS`, - valueString: data.cms || `none`, + valueString: answers.cms || `none`, }) trackCli(`CREATE_GATSBY_SELECT_OPTION`, { name: `CSS_TOOLS`, - valueString: data.styling || `none`, + valueString: answers.styling || `none`, }) trackCli(`CREATE_GATSBY_SELECT_OPTION`, { name: `PLUGIN`, - valueStringArray: data.features || [], + valueStringArray: answers.features || [], }) + // Collect a report of things we will do to present to the user once the questions are complete const messages: Array = [ `${maybeUseEmoji( `🛠 ` - )}Create a new Gatsby site in the folder ${colors.magenta(data.project)}`, + )}Create a new Gatsby site in the folder ${colors.magenta( + answers.project + )}`, ] const plugins: Array = [] const packages: Array = [] let pluginConfig: PluginConfigMap = {} - if (data.cms && data.cms !== `none`) { + // If a CMS is selected, ask CMS config questions after the main question set is complete + if (answers.cms && answers.cms !== `none`) { messages.push( `${maybeUseEmoji( `📚 ` )}Install and configure the plugin for ${colors.magenta( - cmses[data.cms].message + cmses[answers.cms].message )}` ) - const extraPlugins = cmses[data.cms].plugins || [] - plugins.push(data.cms, ...extraPlugins) + const extraPlugins = cmses[answers.cms].plugins || [] + plugins.push(answers.cms, ...extraPlugins) packages.push( - data.cms, - ...(cmses[data.cms].dependencies || []), + answers.cms, + ...(cmses[answers.cms].dependencies || []), ...extraPlugins ) - pluginConfig = { ...pluginConfig, ...cmses[data.cms].options } + pluginConfig = { ...pluginConfig, ...cmses[answers.cms].options } } - if (data.styling && data.styling !== `none`) { + // If a styling system is selected, ask styling config questions after the main question set is complete + if (answers.styling && answers.styling !== `none`) { messages.push( `${maybeUseEmoji(`🎨 `)}Get you set up to use ${colors.magenta( - styles[data.styling].message + styles[answers.styling].message )} for styling your site` ) - const extraPlugins = styles[data.styling].plugins || [] + const extraPlugins = styles[answers.styling].plugins || [] - plugins.push(data.styling, ...extraPlugins) + plugins.push(answers.styling, ...extraPlugins) packages.push( - data.styling, - ...(styles[data.styling].dependencies || []), + answers.styling, + ...(styles[answers.styling].dependencies || []), ...extraPlugins ) - pluginConfig = { ...pluginConfig, ...styles[data.styling].options } + pluginConfig = { ...pluginConfig, ...styles[answers.styling].options } } - if (data.features?.length) { + // If additional features are selected, install required dependencies in install step + if (answers.features?.length) { messages.push( - `${maybeUseEmoji(`🔌 `)}Install ${data.features + `${maybeUseEmoji(`🔌 `)}Install ${answers.features ?.map((feat: string) => colors.magenta(feat)) .join(`, `)}` ) - plugins.push(...data.features) - const featureDependencies = data.features?.map(featureKey => { + plugins.push(...answers.features) + const featureDependencies = answers.features?.map(featureKey => { const extraPlugins = features[featureKey].plugins || [] plugins.push(...extraPlugins) return [ @@ -212,14 +233,16 @@ ${center(colors.blueBright.bold.underline(`Welcome to Gatsby!`))} featureDependencies ) // here until we upgrade to node 11 and can use flatMap - packages.push(...data.features, ...flattenedDependencies) + packages.push(...answers.features, ...flattenedDependencies) // Merge plugin options - pluginConfig = data.features.reduce((prev, key) => { + pluginConfig = answers.features.reduce((prev, key) => { return { ...prev, ...features[key].options } }, pluginConfig) } + // Ask additional config questions if any const config = makePluginConfigQuestions(plugins) + if (config.length) { reporter.info( `\nGreat! A few of the selections you made need to be configured. Please fill in the options for each plugin now:\n` @@ -234,7 +257,9 @@ ${center(colors.blueBright.bold.underline(`Welcome to Gatsby!`))} trackCli(`CREATE_GATSBY_SET_PLUGINS_STOP`) } - if (!yesFlag) { + + // If we're not skipping prompts, give the user a report of what we're about to do + if (!flags.yes) { reporter.info(` ${colors.bold(`Thanks! Here's what we'll now do:`)} @@ -258,24 +283,28 @@ ${colors.bold(`Thanks! Here's what we'll now do:`)} } } + // Decide starter + const starter = DEFAULT_STARTERS[answers.language || `js`] + + // Do all the things await initStarter( - DEFAULT_STARTER, - data.project, + starter, + answers.project, packages.map((plugin: string) => plugin.split(`:`)[0]), - siteName + npmSafeSiteName ) - reporter.success(`Created site in ${colors.green(data.project)}`) + reporter.success(`Created site in ${colors.green(answers.project)}`) - const fullPath = path.resolve(data.project) + const fullPath = path.resolve(answers.project) if (plugins.length) { reporter.info(`${maybeUseEmoji(`🔌 `)}Setting-up plugins...`) await installPlugins(plugins, pluginConfig, fullPath, []) } - await setSiteMetadata(fullPath, `title`, siteName) + await setSiteMetadata(fullPath, `title`, dirName) - await gitSetup(data.project) + await gitSetup(answers.project) const pm = await getPackageManager() const runCommand = pm === `npm` ? `npm run` : `yarn` @@ -283,13 +312,13 @@ ${colors.bold(`Thanks! Here's what we'll now do:`)} reporter.info( stripIndent` ${maybeUseEmoji(`🎉 `)}Your new Gatsby site ${colors.bold( - siteName + dirName )} has been successfully created at ${colors.bold(fullPath)}. ` ) reporter.info(`Start by going to the directory with\n - ${colors.magenta(`cd ${data.project}`)} + ${colors.magenta(`cd ${answers.project}`)} `) reporter.info(`Start the local development server with\n diff --git a/packages/create-gatsby/src/init-starter.ts b/packages/create-gatsby/src/init-starter.ts index 39e8df3a5ad11..7b85684f0fe54 100644 --- a/packages/create-gatsby/src/init-starter.ts +++ b/packages/create-gatsby/src/init-starter.ts @@ -7,7 +7,6 @@ import { spin } from "tiny-spin" import { getConfigStore } from "./utils/get-config-store" type PackageManager = "yarn" | "npm" import colors from "ansi-colors" -import { makeNpmSafe } from "./utils/make-npm-safe" import { clearLine } from "./utils/clear-line" const packageManagerConfigKey = `cli.packageManager` @@ -82,12 +81,12 @@ const createInitialGitCommit = async (rootPath: string): Promise => { const setNameInPackage = async ( sitePath: string, - name: string + npmSafeSiteName: string ): Promise => { const packageJsonPath = path.join(sitePath, `package.json`) const packageJson = await fs.readJSON(packageJsonPath) - packageJson.name = makeNpmSafe(name) - packageJson.description = name + packageJson.name = npmSafeSiteName + packageJson.description = npmSafeSiteName delete packageJson.license try { const result = await execa(`git`, [`config`, `user.name`]) @@ -205,13 +204,13 @@ export async function initStarter( starter: string, rootPath: string, packages: Array, - siteName: string + npmSafeSiteName: string ): Promise { const sitePath = path.resolve(rootPath) await clone(starter, sitePath) - await setNameInPackage(sitePath, siteName) + await setNameInPackage(sitePath, npmSafeSiteName) await install(rootPath, packages) diff --git a/packages/create-gatsby/src/questions/languages.json b/packages/create-gatsby/src/questions/languages.json new file mode 100644 index 0000000000000..6d3734ae9e296 --- /dev/null +++ b/packages/create-gatsby/src/questions/languages.json @@ -0,0 +1,8 @@ +{ + "js": { + "message": "JavaScript" + }, + "ts": { + "message": "TypeScript" + } +} \ No newline at end of file diff --git a/packages/create-gatsby/src/questions/languages.json.d.ts b/packages/create-gatsby/src/questions/languages.json.d.ts new file mode 100644 index 0000000000000..92a15fe41b9e0 --- /dev/null +++ b/packages/create-gatsby/src/questions/languages.json.d.ts @@ -0,0 +1,5 @@ +import { PluginMap } from "../index" + +declare const language: PluginMap + +export default language diff --git a/packages/create-gatsby/src/questions/styles.json b/packages/create-gatsby/src/questions/styles.json index 16b01a0dc22ec..aeeb4fb4d0f80 100644 --- a/packages/create-gatsby/src/questions/styles.json +++ b/packages/create-gatsby/src/questions/styles.json @@ -15,5 +15,13 @@ "gatsby-plugin-theme-ui": { "message": "Theme UI", "dependencies": ["theme-ui"] + }, + "gatsby-plugin-vanilla-extract": { + "message": "vanilla-extract", + "dependencies": [ + "@vanilla-extract/webpack-plugin", + "@vanilla-extract/css", + "@vanilla-extract/babel-plugin" + ] } } diff --git a/packages/create-gatsby/src/utils/parse-args.ts b/packages/create-gatsby/src/utils/parse-args.ts new file mode 100644 index 0000000000000..8297c0ff1614a --- /dev/null +++ b/packages/create-gatsby/src/utils/parse-args.ts @@ -0,0 +1,61 @@ +import { reporter } from "./reporter" + +enum Flag { + yes = `-y`, // Skip prompts + ts = `-ts`, // Use TypeScript +} + +export interface IFlags { + yes: boolean + ts: boolean +} + +interface IArgs { + flags: IFlags + dirName: string +} + +/** + * Parse arguments without considering position. Both cases should work the same: + * + * - `npm init gatsby hello-world -y` + * - `npm init gatsby -y hello-world` + * + * We deliberately trade the edge case of a user attempting to create a directory name + * prepended with a dash (e.g. `-my-project`) for flags that work regardless of position. + */ +export function parseArgs(args: Array): IArgs { + const { flags, dirName } = args.reduce( + (sortedArgs, arg) => { + switch (arg) { + case Flag.yes: + sortedArgs.flags.yes = true + break + case Flag.ts: + sortedArgs.flags.ts = true + break + default: + if (arg.startsWith(`-`)) { + reporter.warn( + `Found unknown argument "${arg}", ignoring. Known arguments are: ${Flag.yes}, ${Flag.ts}` + ) + break + } + sortedArgs.dirName = arg + } + return sortedArgs + }, + { + flags: { + yes: false, + ts: false, + }, + dirName: ``, + } + ) + + return { + flags, + dirName, + } +} diff --git a/packages/create-gatsby/src/utils/question-helpers.ts b/packages/create-gatsby/src/utils/question-helpers.ts index 98bc4e1ee0b7e..fde5df7cbf010 100644 --- a/packages/create-gatsby/src/utils/question-helpers.ts +++ b/packages/create-gatsby/src/utils/question-helpers.ts @@ -1,9 +1,12 @@ import fs from "fs" import path from "path" +import languages from "../questions/languages.json" import cmses from "../questions/cmses.json" import styles from "../questions/styles.json" import features from "../questions/features.json" import colors from "ansi-colors" +import { reporter } from "./reporter" +import { IFlags } from "./parse-args" // eslint-disable-next-line no-control-regex const INVALID_FILENAMES = /[<>:"/\\|?*\u0000-\u001F]/g @@ -27,32 +30,42 @@ export const makeChoices = ( return [none, divider, ...entries] } -export const validateProjectName = async ( - value: string -): Promise => { +export function validateProjectName(value: string): boolean { if (!value) { - return `You have not provided a directory name for your site. Please do so when running with the 'y' flag.` + reporter.warn( + `You have not provided a directory name for your site. Please do so when running with the 'y' flag.` + ) + return false } value = value.trim() if (INVALID_FILENAMES.test(value)) { - return `The destination "${value}" is not a valid filename. Please try again, avoiding special characters.` + reporter.warn( + `The destination "${value}" is not a valid filename. Please try again, avoiding special characters.` + ) + return false } if (process.platform === `win32` && INVALID_WINDOWS.test(value)) { - return `The destination "${value}" is not a valid Windows filename. Please try another name` + reporter.warn( + `The destination "${value}" is not a valid Windows filename. Please try another name` + ) + return false } if (fs.existsSync(path.resolve(value))) { - return `The destination "${value}" already exists. Please choose a different name` + reporter.warn( + `The destination "${value}" already exists. Please choose a different name` + ) + return false } return true } -// The enquirer types are not accurate +// Enquirer types are not exported and are out of date // eslint-disable-next-line @typescript-eslint/no-explicit-any export const generateQuestions = ( initialFolderName: string, - skip: boolean -): any => [ - { + flags: IFlags +): any => { + const siteNameQuestion = { type: `textinput`, name: `project`, message: `What would you like to name the folder where your site will be created?`, @@ -61,27 +74,50 @@ export const generateQuestions = ( initial: initialFolderName, format: (value: string): string => colors.cyan(value), validate: validateProjectName, - skip, - }, - { - type: `selectinput`, - name: `cms`, - message: `Will you be using a CMS?`, - hint: `(Single choice) Arrow keys to move, enter to confirm`, - choices: makeChoices(cmses), - }, - { + skip: flags.yes, + } + + const languageQuestion = { type: `selectinput`, - name: `styling`, - message: `Would you like to install a styling system?`, + name: `language`, + message: `Will you be using JavaScript or TypeScript?`, 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, spacebar to select, and confirm with an enter on "Done"`, - choices: makeChoices(features, true), - }, -] + choices: makeChoices(languages, true), + } + + const otherQuestions = [ + { + 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, spacebar to select, and confirm with an enter on "Done"`, + choices: makeChoices(features, true), + }, + ] + + // Skip all questions + if (flags.yes) { + return siteNameQuestion + } + + // Skip language question + if (flags.ts) { + return [siteNameQuestion, ...otherQuestions] + } + + return [siteNameQuestion, languageQuestion, ...otherQuestions] +}