Skip to content

Latest commit

 

History

History
281 lines (202 loc) · 12.4 KB

README.md

File metadata and controls

281 lines (202 loc) · 12.4 KB

GREEN stack quickstart ⚡

Welcome to Aetherspace! Set up your Web, iOS & Android app in minutes, and blow away the competition with write-once components that run on all platforms. Powered by GraphQL, React, Expo & Next.js

Kickstart for Web & Mobile with Aetherspace

Generate a new repo from the Aetherspace template and (optionally) include all branches.

GithubTemplateRepo.png

GithubTemplateRepoWithPlugins.png

Github will then generate a copy of the template repo for you to customize. It comes out of the box with setup for:

  • Next.js web app (File based app dir routing, Serverside rendering, Static site generation, ...)
  • Expo mobile app (Android & iOS with Expo-Router and react-native)
  • A REST & GraphQL API (with Apollo Server & Next.js API routes)
  • Generators and Automation scripts to automatically generate API & component documentation
  • Documentation for Aetherspace and its components (with docgen being a side effect of our recommended way of working)
  • Github actions for mobile deployments, linting your code & building your documentation

When you're ready to start developing, run yarn install to install all dependencies, followed by:

yarn dev:docs

Up and running in minutes

packages/@aetherspace houses a bunch of helpers that can be imported under the aetherspace namespace. These aim to make your cross-platform journey a breeze. All of it has been written in Typescript and is documented in Storybook and the repo README's. Feel free to edit these to your liking if you make changes, but here's what it you can do with it out of the box:

Write & style your components just once

💚 Aetherspace primitives are built with tailwind, iOS, Android, web, node and ssr (+ media queries) in mind.
We mention media queries specifically because react-native-web does not support them out of the box. But we've got you covered.

import { View, Text } from 'aetherspace/primitives'

export const MyComponent = () => (
  <View tw="px-2 max-w-[100px] items-center rounded-md">
    <Text tw="lg:text-xl font-primary-bold text-green">
        Hello World 👋
    </Text>
  </View>
)

Use semantic HTML & optimised primitives where necessary

✨ Thanks to a tiny wrapper around @expo/html-elements and primitives like Image using either the optimized Next.js or React-Native component under the hood, anything you build with these primitives will automatically be optimized for each platform as well 🎉

import { Image } from 'aetherspace/primitives'
import { Article, Section, H2, P } from 'aetherspace/html-elements'

// -i- Web: Renders article / section / h2 + next/image for better SEO & web vitals
// -i- Mobile: Renders react-native View / Text / ... 👉 Gets turned into actual native UI
export const MyBlogPost = (props: { paragraphs: string[] }) => (
  <Article tw="relative">
    <H2 tw="text-gray font-primary-black">My post title</H2>
    <Image tw="w-full" src="/img/article-header.png">
    <Section tw="px-4 mb-4">
      {/* render each paragraph as a <p> tag on web, or <Text> on mobile */}
      {props.paragraphs.map((paragraph) => <P tw="font-primary-regular">{paragraph}</P>)}
    </Section>
  </Article>
)

Test on Web, iOS, Android & Storybook

⏳ To start everything, but automatically wait for your Next.js server to start before starting up Expo & Storybook:

yarn dev:docs

This will run the dev command in each app workspace in parallell with Turborepo 👇

## Starts next.js web project + API routes on port 3000
next-app:dev: $ next
next-app:dev: ready - started server on 0.0.0.0:3000, url: http://localhost:3000
## Runs automations like docgen at next.js build time
next-app:dev: -i- Successfully created resolver registry at: 
next-app:dev: ✅ packages/@registries/resolvers.generated.ts
next-app:dev: -i- Auto documenting with 'yarn document-components' ...
next-app:dev: ✅ packages/@registries/docs/features/app-core/icons.stories.mdx
next-app:dev: ✅ packages/@registries/docs/features/app-core/screens.stories.mdx
## Checks whether backend is ready
aetherspace:dev-health-check: $ NODE_ENV=development node scripts/dev-health-check
aetherspace:dev-health-check: ✅ Health check 1 succeeded: Server, API routes & GraphQL up and running.
## Starts Expo for mobile dev in iOS / Android device or simulator
expo-app:start: $ npx expo start
expo-app:start: Your native app is running at exp://192.168.0.168:19000
## Starts up Storybook for developing & testing components in isolation
99% done plugins webpack-hot-middlewarewebpack built preview acfe5466784b8a1a2429 in 162ms
╭─────────────────────────────────────────────────────╮
│                                                     │
│   Storybook 6.5.10 for React started                │
│   3.12 s for manager and 8.9 s for preview          │
│                                                     │
│    Local:            http://localhost:6006/         │
│    On your network:  http://169.254.34.142:6006/    │
│                                                     │
╰─────────────────────────────────────────────────────╯

Define your data as actual single sources of truth

📐 Our aetherSchema() structure builder enables you to build for Typescript first (with Zod), but enables you to optionally generate documentation, validation logic, GraphQL typedefs and data resolvers from those schemas as well.

import { z, aetherSchema, AetherProps } from 'aetherspace/schemas'

/* --- Schematypes ------------- */

export const PropSchema = aetherSchema('MyComponentProps', {
  name: z.string().optional(), // string | undefined
  value: z.number().default(1), // number
})

/* --- <MyComponent/> ---------- */

// Infer types from props schema with 'AetherProps' type helper, or ...
export const MyComponent = (props: AetherProps<typeof PropSchema>) => {
    // Prop validation + apply defaults
    const { name, value } = PropSchema.parse(props)
    // ...
}

Hook into automatic docgen

📚 Documentation drives adoption... and Storybook is a great way to do it.
However, it can be a pain to set up and maintain the docs for every component manually. Luckily, we've already set it up for you. On top of that, all you need to do is assign your aetherSchema() & zod powered prop definition as a getDocumentationProps export and our scripts will automatically turn it into Storybook controls.

../../components/MyComponent.tsx

const PropSchema = aetherSchema('MyComponentProps', {
  name: z.string().optional().describe('Title of the component'),
  value: z.number().default(1).describe('Initial value of the component'),
})

/* --- <MyComponent/> ---------- */

// ... export your component with the same name as the file ...

/* --- Documenation ------------ */

export const getDocumentationProps = PropSchema.introspect()

// ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓

next-app:dev: -i- Auto documenting with 'yarn document-components' ...
next-app:dev: ✅ packages/@registries/docs/features/app-core/icons.stories.mdx
next-app:dev: ✅ packages/@registries/docs/features/app-core/screens.stories.mdx

example >>> icon docs

Build flexible data resolvers (API routes + GraphQL)

💪 Using aetherSchema() & zod to describe function arguments and responses opens it up to not just easier regular function use with async / await, but Next.js API routes and GraphQL resolvers as well.

features/app-core/routes/api/health/route.ts

// Schemas
import { z, aetherSchema } from 'aetherspace/schemas'
import { aetherResolver, makeNextApiHandler, makeGraphQLResolver } from 'aetherspace/utils/serverUtils'

/* --- Schemas ------------- */

export const HealthCheckArgs = aetherSchema('HealthCheckArgs', {
  echo: z.string().optional().describe('Echoes back the echo argument'),
})

// (You can reuse schema definitions with pick / omit / extend commands as well)
export const HealthCheckResponse = HealthCheckArgs.pickSchema('HealthCheckResponse', {
  echo: true, // <- Pick the echo argument from the args schema, since we're echoing it back
})

/* --- Config -------------- */

const resolverConfig = {
  argsSchema: HealthCheckArgs,
  responseSchema: HealthCheckResponse,
}

/* --- healthCheck() ------- */

// Our actual business logic
export const healthCheck = aetherResolver(async ({ args }) => ({
    echo: args.echo, // <- Echo back the echo argument 🤷‍♂️
}), resolverConfig)

/* --- Next.js API Routes -- */

export const GET = makeNextRouteHandler(healthCheck)

export const POST = makeNextRouteHandler(healthCheck)

/* --- GraphQL ------------- */

// Make resolver available to GraphQL (picked up by automation)
export const graphResolver = makeGraphQLResolver(healthCheck)

example >>> REST (e.g. at /api/health)

⚛️ Since we're exporting a graphResolver function using makeGraphQLResolver(), we can generate a GraphQL endpoint for our resolver function as well:

// ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓

next-app:dev: -i- Successfully created resolver registry at: 
next-app:dev: ✅ packages/@registries/resolvers.generated.ts

/api/graphql will then use the resolvers.generated.ts barrel file to build its graphql API from.

example >>> GraphQL (e.g. in /api/graphql)

Powerful results 💪

Performing these 6 steps has provided us with a bunch of value in little time:

  • Hybrid UI component that is styled with tailwind, but actually native on iOS and Android
  • Hybrid UI component that is optimized for SEO, media queries and Web-Vitals on Web
  • Storybook documentation without having to explicitly create it ourselves

  • 🤝 A single source of truth for all our props, args, responses, docs, types, defaults and validation

  • A Back-end resolver function we can call from other data resolvers or API routes
  • A GraphQL API powered by Apollo-Server, with automatically inferred type definitions
  • A Next.js powered REST API

Next steps

Optionally, extend your setup:

Learn more

Intro Article on DEV Intro Article on DEV