Skip to content
This repository has been archived by the owner on Jan 13, 2023. It is now read-only.

Commit

Permalink
feat(generators): Finish simplifying component definitions; component…
Browse files Browse the repository at this point in the history
… & screen generator tweaks (#363) [skip-ci]

* fix(test): update test for screen generator

* fix(screen): sync screen generator with template

* fix(warn): Fix a new-to-storybook-5.3.0 warning

* feat(types): Remove remaining components' & screens' FunctionComponent typing

* feat(generators): Update the component generator to generate observer-wrapped and plain components

Also, simplified the generated component content to require less initial work on new components,
and moved the style definitions into the component file (since our predefined components all have
integrated styles)

* feat(cli): Allow CamelCase or kebab-case when generating screens or components; tolerate either suffix

Consistenly use kebab-case for filenames

* fix(lint): `yarn format` fixes

Co-authored-by: Lasha Kava <lasha@Lashas-iMac.local>
Co-authored-by: Jamon Holmgren <jamonholmgren@gmail.com>
  • Loading branch information
3 people authored Aug 11, 2020
1 parent 4b1d8b5 commit 86b3223
Show file tree
Hide file tree
Showing 21 changed files with 114 additions and 184 deletions.
6 changes: 3 additions & 3 deletions boilerplate/app/app.tsx.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
*/
import "./i18n"
import "./utils/ignore-warnings"
import React, { useState, useEffect, useRef, FunctionComponent as Component } from "react"
import React, { useState, useEffect, useRef } from "react"
import { NavigationContainerRef } from "@react-navigation/native"
import { SafeAreaProvider, initialWindowSafeAreaInsets } from "react-native-safe-area-context"
<% if (props.useExpo) { -%>
Expand All @@ -38,7 +38,7 @@ export const NAVIGATION_PERSISTENCE_KEY = "NAVIGATION_STATE"
/**
* This is the root component of our app.
*/
const App: Component<{}> = () => {
function App() {
const navigationRef = useRef<NavigationContainerRef>()
const [rootStore, setRootStore] = useState<RootStore | undefined>(undefined)

Expand Down Expand Up @@ -79,4 +79,4 @@ const App: Component<{}> = () => {
)
}

export default App
export default App
4 changes: 2 additions & 2 deletions boilerplate/app/components/bullet-item/bullet-item.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as React from "react"
import { View, ViewStyle, ImageStyle, TextStyle } from "react-native"
import {Text} from "../text/text"
import {Icon} from "../icon/icon"
import { Text } from "../text/text"
import { Icon } from "../icon/icon"
import { spacing, typography } from "../../theme"

const BULLET_ITEM: ViewStyle = {
Expand Down
4 changes: 2 additions & 2 deletions boilerplate/app/components/header/header.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { FunctionComponent as Component } from "react"
import React from "react"
import { View, ViewStyle, TextStyle } from "react-native"
import { HeaderProps } from "./header.props"
import { Button } from "../button/button"
Expand All @@ -24,7 +24,7 @@ const RIGHT: ViewStyle = { width: 32 }
/**
* Header that appears on many screens. Will hold navigation buttons and screen title.
*/
export const Header: Component<HeaderProps> = props => {
export function Header(props: HeaderProps) {
const {
onLeftPress,
onRightPress,
Expand Down
4 changes: 2 additions & 2 deletions boilerplate/app/components/switch/switch.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { FunctionComponent as Component } from "react"
import React from "react"
import { ViewStyle, Animated, Easing, TouchableWithoutFeedback } from "react-native"
import { color } from "../../theme"
import { SwitchProps } from "./switch.props"
Expand Down Expand Up @@ -52,7 +52,7 @@ const enhance = (style, newStyles): any => {

const makeAnimatedValue = switchOn => new Animated.Value(switchOn ? 1 : 0)

export const Switch: Component<SwitchProps> = props => {
export function Switch(props: SwitchProps) {
const [timer] = React.useState<Animated.Value>(makeAnimatedValue(props.value))
const startAnimation = React.useMemo(
() => (newValue: boolean) => {
Expand Down
4 changes: 2 additions & 2 deletions boilerplate/app/components/text-field/text-field.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { FunctionComponent as Component } from "react"
import React from "react"
import { View, TextInput, TextStyle, ViewStyle } from "react-native"
import { color, spacing, typography } from "../../theme"
import { translate } from "../../i18n"
Expand Down Expand Up @@ -32,7 +32,7 @@ const enhance = (style, styleOverride) => {
/**
* A component which has a label and an input together.
*/
export const TextField: Component<TextFieldProps> = props => {
export function TextField(props: TextFieldProps) {
const {
placeholderTx,
placeholder,
Expand Down
4 changes: 2 additions & 2 deletions boilerplate/app/screens/demo-screen/demo-screen.tsx.ejs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { FunctionComponent as Component } from "react"
import React from "react"
import { Image, ImageStyle, Platform, TextStyle, View, ViewStyle } from "react-native"
import { useNavigation } from "@react-navigation/native"
import { observer } from "mobx-react-lite"
Expand Down Expand Up @@ -77,7 +77,7 @@ const HINT: TextStyle = {
marginVertical: spacing[2],
}

export const DemoScreen: Component = observer(function DemoScreen() {
export const DemoScreen = observer(function DemoScreen() {
const navigation = useNavigation()
const goBack = () => navigation.goBack()

Expand Down
6 changes: 3 additions & 3 deletions boilerplate/app/screens/welcome-screen/welcome-screen.tsx.ejs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { FunctionComponent as Component } from "react"
import React from "react"
import { View, Image, ViewStyle, TextStyle, ImageStyle, SafeAreaView } from "react-native"
import { useNavigation } from "@react-navigation/native"
import { observer } from "mobx-react-lite"
Expand Down Expand Up @@ -75,7 +75,7 @@ const FOOTER_CONTENT: ViewStyle = {
paddingHorizontal: spacing[4],
}

export const WelcomeScreen: Component = observer(function WelcomeScreen() {
export const WelcomeScreen = observer(function WelcomeScreen() {
const navigation = useNavigation()
const nextScreen = () => navigation.navigate("demo")

Expand Down Expand Up @@ -119,4 +119,4 @@ export const WelcomeScreen: Component = observer(function WelcomeScreen() {
</SafeAreaView>
</View>
)
})
})
2 changes: 1 addition & 1 deletion boilerplate/app/theme/typography.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export const typography = {
*/
secondary: Platform.select({ ios: "Arial", android: "sans-serif" }),

/**
/**
* Lets get fancy with a monospace font!
*/
code: Platform.select({ ios: "Courier", android: "monospace" }),
Expand Down
4 changes: 2 additions & 2 deletions boilerplate/assets/fonts/custom-fonts.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ ignote-bowser integrated `expo-fonts` according to [this guide](https://docs.exp
## For Non-Expo Projects

1. Find a font in `TTF` or `OTF` format that you like and put it in `./assets/fonts/`. I picked [this one](https://fonts.google.com/specimen/Shadows+Into+Light).
2. Now run `npx react-native link`. Even though we're past react native 0.60 we can still use this command to link the fonts. This is a one-way operation. You'll have to remove fonts yourself later if you want to remove them from your app. This will add items to your xcode project and `Info.plist` file as well as putting some files in place for android to bundle (specifically, it will move the font files to `./android/app/src/main/assets/).
2. Now run `npx react-native link`. Even though we're past react native 0.60 we can still use this command to link the fonts. This is a one-way operation. You'll have to remove fonts yourself later if you want to remove them from your app. This will add items to your xcode project and `Info.plist` file as well as putting some files in place for android to bundle (specifically, it will move the font files to `./android/app/src/main/assets/).
3. Both iOS and Android changes must be committed to source control, but sometimes other common libraries like `react-native-vector-icons` may mess with this process and add some fonts you don't intend to ship. Make sure to review these changes carefully before pushing any changes to source control.
4. The tricky part is next: knowing what font family to use. iOS uses the family name like `{fontFamily: 'ShadowsIntoLight'}` whereas on android, you must include all the different variations of the font you will use and reference them by their *filename*. So I downloaded `ShadowsIntoLight-Regular.ttf` that means android must use `{fontFamily: 'ShadowsIntoLight-Regular'}` (yes, it's case sensitive 🙄). [There are components to help you deal with this](https://github.com/lendup/react-native-cross-platform-text) or you can roll your own based on your needs and something like:
4. The tricky part is next: knowing what font family to use. iOS uses the family name like `{fontFamily: 'ShadowsIntoLight'}` whereas on android, you must include all the different variations of the font you will use and reference them by their _filename_. So I downloaded `ShadowsIntoLight-Regular.ttf` that means android must use `{fontFamily: 'ShadowsIntoLight-Regular'}` (yes, it's case sensitive 🙄). [There are components to help you deal with this](https://github.com/lendup/react-native-cross-platform-text) or you can roll your own based on your needs and something like:

```
const CUSTOM_FONT_TEXT: TextStyle = {
Expand Down
8 changes: 4 additions & 4 deletions boilerplate/storybook/storybook.tsx.ejs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useEffect, FunctionComponent } from "react"
import React, { useEffect } from "react"
import { getStorybookUI, configure } from "@storybook/react-native"
<% if (props.useExpo) { -%>
import { initFonts } from "../app/theme/fonts"
Expand All @@ -16,12 +16,12 @@ const StorybookUI = getStorybookUI({
onDeviceUI: true,
<% if (props.useExpo) { -%>
asyncStorage: require("react-native").AsyncStorage,
<% } else { -%>
asyncStorage: require("@react-native-community/async-storage").default
<% } else { -%>
asyncStorage: require("@react-native-community/async-storage").default || null,
<% } -%>
})

export const StorybookUIRoot: FunctionComponent = () => {
export function StorybookUIRoot() {
useEffect(() => {
(async () => {
<% if (props.useExpo) { -%>
Expand Down
7 changes: 5 additions & 2 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,11 @@ Will give you information of what generators are present.

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")
- Wrapped with mobx-react-lite's `observer` function - you need this if you
pass any mobx-state-tree objects as props to the component, and the component
will dereference properties of those objects.
- Plain, not wrapped with `observer`. If you're only passing plain values or
non-MST objects, this is fine.

```
ignite generate component awesome-component
Expand Down
4 changes: 1 addition & 3 deletions src/boilerplate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,7 @@ export const install = async (toolbox: IgniteToolbox) => {
)

const name = parameters.first
const spinner = print
.spin(`using the ${red("Infinite Red")} Bowser boilerplate`)
.succeed()
const spinner = print.spin(`using the ${red("Infinite Red")} Bowser boilerplate`).succeed()

let useExpo = parameters.options.expo
const askAboutExpo = useExpo === undefined
Expand Down
63 changes: 22 additions & 41 deletions src/commands/generate/component.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { GluegunToolbox } from "gluegun"

export const description = "Generates a component, supporting files, and a storybook test."
export const description = "Generates a component and a storybook test."
export const run = async function(toolbox: GluegunToolbox) {
// grab some features
const { parameters, strings, print, ignite, patching, filesystem, prompt } = toolbox
const { pascalCase, camelCase, isBlank } = strings
const { camelCase, isBlank, kebabCase, pascalCase } = strings

// validation
if (isBlank(parameters.first)) {
Expand All @@ -13,63 +13,44 @@ 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" (recommended)',
},
{
name: "statelessFunction",
message: 'Stateless function, aka the "classic" ignite-bowser component',
},
]
let observer = parameters.options.observer
if (parameters.options.observer === undefined) {
observer = await prompt.confirm(
`Should this component be _observed_ by Mobx?\n${print.colors.gray(`
const { component } = await prompt.ask([
{
name: "component",
message: "Which type of component do you want to generate?",
type: "select",
choices: componentTypes,
},
])
componentType = component
If you'll be passing any mobx-state-tree objects in this component's props
and dereferencing them within this component, you'll want the component wrapped
in \`observer\` so that the component rerenders when properties of the object change.
If all props will be simple values or objects that don't come from a mobx store,
you don't need the component to be wrapped in \`observer\`.
`)}`,
)
}

const name = parameters.first
const pascalName = pascalCase(name)
const camelCaseName = camelCase(name)
const props = { name, pascalName, camelCaseName }
const kebabCaseName = kebabCase(name)
const props = { camelCaseName, kebabCaseName, name, observer, pascalName }

const jobs = [
{
template: "component.story.tsx.ejs",
target: `app/components/${name}/${name}.story.tsx`,
target: `app/components/${kebabCaseName}/${kebabCaseName}.story.tsx`,
},
{
template: "styles.ts.ejs",
target: `app/components/${name}/${name}.styles.ts`,
template: "component.tsx.ejs",
target: `app/components/${kebabCaseName}/${kebabCaseName}.tsx`,
},
]

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
const barrelExportPath = `${process.cwd()}/app/components/index.ts`
const exportToAdd = `export * from "./${name}/${name}"\n`
const exportToAdd = `export * from "./${kebabCaseName}/${kebabCaseName}"\n`

if (!filesystem.exists(barrelExportPath)) {
const msg =
Expand All @@ -83,6 +64,6 @@ export const run = async function(toolbox: GluegunToolbox) {
// wire up example
await patching.prepend(
"./storybook/storybook-registry.ts",
`require("../app/components/${name}/${name}.story")\n`,
`require("../app/components/${kebabCaseName}/${kebabCaseName}.story")\n`,
)
}
4 changes: 1 addition & 3 deletions src/commands/generate/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,7 @@ export const run = async function(toolbox: GluegunToolbox) {
const exportToAdd = `export * from "./${name}/${name}"\n`

if (!filesystem.exists(barrelExportPath)) {
const msg =
`No '${barrelExportPath}' file found. Can't export model.` +
`Export your new model manually.`
const msg = `No '${barrelExportPath}' file found. Can't export model. Export your new model manually.`
print.warning(msg)
process.exit(1)
}
Expand Down
29 changes: 15 additions & 14 deletions src/commands/generate/screen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export const description = "Generates a React Native screen."
export const run = async function(toolbox: GluegunToolbox) {
// grab some features
const { parameters, print, strings, ignite, filesystem, patching } = toolbox
const { pascalCase, isBlank, camelCase } = strings
const { camelCase, isBlank, kebabCase, pascalCase } = strings

// validation
if (isBlank(parameters.first)) {
Expand All @@ -13,24 +13,25 @@ export const run = async function(toolbox: GluegunToolbox) {
return
}

const name = parameters.first
const screenName = name.endsWith("-screen") ? name : `${name}-screen`

// prettier-ignore
if (name.endsWith('-screen')) {
print.info(`Note: For future reference, the \`-screen\` suffix is automatically added for you.`)
let name = parameters.first
const matches = name.match(/(.*)((-s|S)creen)$/)
if (matches) {
name = matches[1] // grab the name without the suffix
// prettier-ignore
print.info(`Note: For future reference, the \`${matches[2]}\` suffix is automatically added for you.`)
print.info(`You're welcome to add it manually, but we wanted you to know you don't have to. :)`)
}

// get permutations of the given model name
const pascalName = pascalCase(screenName)
const camelName = camelCase(screenName)
// get permutations of the given name, suffixed
const pascalName = pascalCase(name) + "Screen"
const camelName = camelCase(name) + "Screen"
const kebabName = kebabCase(name) + "-screen"

const props = { name: screenName, pascalName, camelName }
const props = { pascalName, camelName }
const jobs = [
{
template: `screen.ejs`,
target: `app/screens/${screenName}.tsx`,
target: `app/screens/${kebabName}/${kebabName}.tsx`,
},
]

Expand All @@ -39,7 +40,7 @@ export const run = async function(toolbox: GluegunToolbox) {

// patch the barrel export file
const barrelExportPath = `${process.cwd()}/app/screens/index.ts`
const exportToAdd = `export * from "./${screenName}"\n`
const exportToAdd = `export * from "./${kebabName}/${kebabName}"\n`

if (!filesystem.exists(barrelExportPath)) {
const msg =
Expand All @@ -50,5 +51,5 @@ export const run = async function(toolbox: GluegunToolbox) {
}
await patching.append(barrelExportPath, exportToAdd)

print.info(`Screen ${screenName} created`)
print.info(`Screen ${pascalName} created`)
}
7 changes: 3 additions & 4 deletions templates/component.story.tsx.ejs
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
import * as React from "react"
import { storiesOf } from "@storybook/react-native"
import { StoryScreen, Story, UseCase } from "../../../storybook/views"
import { <%= props.pascalName %> } from "./<%= props.name %>"

declare var module
import { color } from "../../theme"
import { <%= props.pascalName %> } from "./<%= props.kebabCaseName %>"

storiesOf("<%= props.pascalName %>", module)
.addDecorator(fn => <StoryScreen>{fn()}</StoryScreen>)
.add("Style Presets", () => (
<Story>
<UseCase text="Primary" usage="The primary.">
<<%= props.pascalName %> text="<%= props.pascalName %>" />
<<%= props.pascalName %> style={{ backgroundColor: color.error }} />
</UseCase>
</Story>
))
Loading

0 comments on commit 86b3223

Please sign in to comment.