diff --git a/package.json b/package.json index b59322ae..dd84eb3c 100644 --- a/package.json +++ b/package.json @@ -20,10 +20,10 @@ }, "scripts": { "format": "npm-run-all format:*", - "format:js": "prettier --write {.,**}/*.js", - "format:json": "prettier --write {.,**}/*.json", - "format:md": "prettier --write {.,**}/*.md", - "format:ts": "prettier --write {.,**}/*.{ts,tsx}", + "format:js": "prettier --write '{.,**}/*.js'", + "format:json": "prettier --write '{.,**}/*.json'", + "format:md": "prettier --write '{.,**}/*.md'", + "format:ts": "prettier --write '{.,**}/*.{ts,tsx}'", "lint": "eslint src test --ext .ts --fix", "test": "yarn build && jest --runInBand && yarn lint && yarn clean", "watch": "jest --runInBand --watch", diff --git a/readme.md b/readme.md index f8fdbb6d..0af0483d 100644 --- a/readme.md +++ b/readme.md @@ -50,6 +50,84 @@ You should see an app that looks like the screenshot above! Next step -- follow this tutorial to learn how to create a trivia app with Ignite Bowser: https://shift.infinite.red/creating-a-trivia-app-with-ignite-bowser-part-1-1987cc6e93a1 +## Generators + +The true gem of Ignite Bowser. Generators help you scaffold your app very quickly, be it for a proof-of-concept, a demo, or a production app. Generators are there to save you time, keep your code consistent, and help you with the basic structure of your app. + +``` +ignite generate +``` + +Will give you information of what generators are present. + +### Component generator + +This is the generator you will be using most often. There are 2 flavors: + +- React.FunctionComponent (i.e. "hooks enabled component") +- Stateless function (i.e. the "classic ignite-bowser component") + +``` +ignite generate component awesome-component +``` + +- Creates the component/function +- Creates a style file +- Creates a storybook test +- Will make the required additions to configuration files. + +You can also bypass the choice by providing which component type you want to create: + +``` +ignite generate component awesome-component --function-component +``` + +Or + +``` +ignite generate component awesome-component --stateless-function +``` + +### Screen generator + +Generates a "hooks enabled" screen. + +``` +ignite generate screen awesome-screen +``` + +- Creates the screen +- Will make the required additions to configuration files. + +### Navigator generator + +Helps you in a "wizard-style" create a new [react-navigation](https://reactnavigation.org/docs/en/getting-started.html) navigator. + +``` +ignite generate navigator awesome-navigator +``` + +- Creates the navigator +- Will make the required additions to configuration files. + +### Model generator + +Creates a Mobx-State-Tree model. + +``` +ignite generate model awesome-model +``` + +- Creates the model +- Creates a unit test file +- Will make the required additions to configuration files. + +### Advanced + +The built in generators aren't enough? Fret not, you can create your own generators that suit your project/company. These generators can live with the default ignite-bowser generators. + +Please refer to the [documentation on how to create your own generators.](https://github.com/infinitered/ignite/blob/master/docs/advanced-guides/creating-generators.md) + ## Explanation of folder structure The Ignite Bowser boilerplate project's structure will look similar to this: @@ -216,7 +294,7 @@ To keep your React Native app updated: To keep your Ignite Bowser based app updated: -- [ignite-bowser-difff-purge](https://github.com/nirre7/ignite-bowser-diff-purge) To help you see the diffs between versions +- [ignite-bowser-diff-purge](https://github.com/nirre7/ignite-bowser-diff-purge) To help you see the diffs between versions ## TypeScript diff --git a/src/commands/generate/component.ts b/src/commands/generate/component.ts index 33e3393f..6e8cf52e 100644 --- a/src/commands/generate/component.ts +++ b/src/commands/generate/component.ts @@ -3,8 +3,8 @@ import { GluegunToolbox } from "gluegun" export const description = "Generates a component, supporting files, and a storybook test." export const run = async function(toolbox: GluegunToolbox) { // grab some features - const { parameters, strings, print, ignite, patching, filesystem } = toolbox - const { pascalCase, isBlank } = strings + const { parameters, strings, print, ignite, patching, filesystem, prompt } = toolbox + const { pascalCase, camelCase, isBlank } = strings // validation if (isBlank(parameters.first)) { @@ -13,21 +13,62 @@ export const run = async function(toolbox: GluegunToolbox) { return } + let componentType + if (!parameters.options["function-component"] && !parameters.options["stateless-function"]) { + const componentTypes = [ + { + name: "functionComponent", + message: "React.FunctionComponent, aka \"hooks component\"", + }, + { + name: "statelessFunction", + message: "Stateless function, aka the \"classic\" ignite-bowser component", + }, + ] + + const { component } = await prompt.ask([ + { + name: "component", + message: "Which type of component do you want to generate?", + type: "select", + choices: componentTypes, + }, + ]) + componentType = component + } + const name = parameters.first const pascalName = pascalCase(name) + const camelCaseName = camelCase(name) + const props = { name, pascalName, camelCaseName } - const props = { name, pascalName } const jobs = [ - { - template: 'component.tsx.ejs', - target: `app/components/${name}/${name}.tsx` - }, { template: 'component.story.tsx.ejs', target: `app/components/${name}/${name}.story.tsx` + }, + { + template: 'styles.ts.ejs', + target: `app/components/${name}/${name}.styles.ts` } ] + if (componentType === "functionComponent" || parameters.options["function-component"]) { + jobs.push( + { + template: 'function-component.tsx.ejs', + target: `app/components/${name}/${name}.tsx` + } + ) + } else if (componentType === "statelessFunction" || parameters.options["stateless-function"]) { + jobs.push( + { + template: 'component.tsx.ejs', + target: `app/components/${name}/${name}.tsx` + } + ) + } + await ignite.copyBatch(toolbox, jobs, props) // patch the barrel export file diff --git a/templates/component.tsx.ejs b/templates/component.tsx.ejs index f8f5d9ac..1e8c5ca6 100644 --- a/templates/component.tsx.ejs +++ b/templates/component.tsx.ejs @@ -1,6 +1,7 @@ import * as React from "react" import { View, ViewStyle } from "react-native" import { Text } from "../" +import { <%= props.camelCaseName %>Styles as styles } from "./<%= props.name %>.styles" export interface <%= props.pascalName %>Props { /** diff --git a/templates/function-component.tsx.ejs b/templates/function-component.tsx.ejs new file mode 100644 index 00000000..a85823b3 --- /dev/null +++ b/templates/function-component.tsx.ejs @@ -0,0 +1,23 @@ +import * as React from "react" +import { View } from "react-native" +import { useObserver } from "mobx-react-lite" +import { Text } from "../" +import { useStores } from "../../models/root-store" +import { <%= props.camelCaseName %>Styles as styles } from "./<%= props.name %>.styles" + +export interface <%= props.pascalName %>Props {} + +/** + * React.FunctionComponent for your hook(s) needs + * + * Component description here for TypeScript tips. + */ +export const <%= props.pascalName %>: React.FunctionComponent<<%= props.pascalName %>Props> = props => { + // const { someStore } = useStores() + + return useObserver(() => ( + + Hi Func + + )) +} diff --git a/templates/styles.ts.ejs b/templates/styles.ts.ejs new file mode 100644 index 00000000..44ba3ced --- /dev/null +++ b/templates/styles.ts.ejs @@ -0,0 +1,7 @@ +import { ViewStyle } from "react-native" + +export const <%= props.camelCaseName %>Styles = { + WRAPPER: { + justifyContent: 'center' + } as ViewStyle +} diff --git a/test/generators-integration.test.ts b/test/generators-integration.test.ts index 4e408deb..19446718 100644 --- a/test/generators-integration.test.ts +++ b/test/generators-integration.test.ts @@ -58,13 +58,22 @@ describe("a generated app", () => { ) }) - test("generates a component", async () => { - const simpleComponent = "Simple" - await execa(IGNITE, ["g", "component", simpleComponent], { preferLocal: false }) - expect(jetpack.exists(`app/components/${simpleComponent}/${simpleComponent}.tsx`)).toBe("file") - expect(jetpack.exists(`app/components/${simpleComponent}/${simpleComponent}.story.tsx`)).toBe( - "file", - ) + test("generates a stateless function", async () => { + const statelessFunction = "Stateless" + await execa(IGNITE, ["g", "component", statelessFunction, "--stateless-function"], { preferLocal: false }) + expect(jetpack.exists(`app/components/${statelessFunction}/${statelessFunction}.tsx`)).toBe("file") + expect(jetpack.exists(`app/components/${statelessFunction}/${statelessFunction}.story.tsx`)).toBe("file") + expect(jetpack.exists(`app/components/${statelessFunction}/${statelessFunction}.styles.ts`)).toBe("file") + const lint = await execa("npm", ["-s", "run", "lint"]) + expect(lint.stderr).toBe("") + }) + + test("generates a function component", async () => { + const functionComponent = "FunctionComponent" + await execa(IGNITE, ["g", "component", functionComponent, "--function-component"], { preferLocal: false }) + expect(jetpack.exists(`app/components/${functionComponent}/${functionComponent}.tsx`)).toBe("file") + expect(jetpack.exists(`app/components/${functionComponent}/${functionComponent}.story.tsx`)).toBe("file") + expect(jetpack.exists(`app/components/${functionComponent}/${functionComponent}.styles.ts`)).toBe("file") const lint = await execa("npm", ["-s", "run", "lint"]) expect(lint.stderr).toBe("") })