diff --git a/.gitignore b/.gitignore index ef95438..7d3e54e 100644 --- a/.gitignore +++ b/.gitignore @@ -57,3 +57,6 @@ yarn-error.log* .pnp.js # -- @end @expo/next-adapter -- + +.turbo +.vscode/tmp diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..2946048 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "typescript.tsdk": "node_modules/typescript/lib", + "typescript.enablePromptUseWorkspaceTsdk": true +} diff --git a/README.md b/README.md index d22a3bc..2801925 100644 --- a/README.md +++ b/README.md @@ -1,82 +1,221 @@ -# Universal Expo + Next.js App Router Starter +# Built with [FullProduct.dev](https://fullproduct.dev?v=gh-demo-readme) πŸš€ -A minimal starter for a universal Expo + Next.js app with their respective app routers. +[![FullProduct.dev Bento Slide](https://fullproduct.dev/full-product-dev-bento.jpg)](https://fullproduct.dev?v=gh-demo-readme) -It's a good starting point if you want to: +> This project with built with [FullProduct.dev](https://fullproduct.dev?v=gh-demo-readme) ❇️ A starterkit for building truly universal apps with Expo (iOS / Android) and Next.js (Web + SSR) - Providing a familiar but optimized, write-once, app router experience. -- βœ… make use of app-dir file based routing in expo and next.js -- βœ… have a minimal monorepo setup with Typescript but no monorepo tool yet -- βœ… leave all other tech choices for e.g. styling, dbs, component libs, etc. up to you +> [!NOTE] +> FullProduct.dev is still in beta and awaiting official release. Consider this an early preview of the project that will still be improved. -> This template repo is the result of a frequent exercise where I attempt to recreate the [FullProduct.dev](https://fullproduct.dev) Universal App Starterkit from scratch. I usually do this using the latest recommended expo + next.js starter from the Expo docs. This helps me see whether the setup and config for the Universal App Starter can be simplified. Also handy to notice where issues occur and how to fix them. +--- -## Getting Started +
+Why FullProduct.dev? ⚑️ -```bash -npm install -``` +--- -```bash -npm run dev -``` +## The [FullProduct.dev](https://fullproduct.dev?v=gh-demo-readme) πŸš€ Starterkit + +![It's a lot harder and costly to add a mobile app later than it is to start universally](https://fullproduct.dev/blog-assets/imgs/start-universally.jpg) + +- **Universal from the Start πŸ™Œ + Write-once UI:** + - Build for web, iOS, and Android with a single codebase. + - No more writing features at 2x / 3x the time, resources or cost. + - 90%+ of your UI and logic = shared across platforms. + - Use React Native primitives (`View`, `Text`, `Image`) + NativeWind for max portability + - ... while still styling your universal UI with Tailwind. + +![Write once + Universal UI](https://fullproduct.dev/blog-assets/imgs/write-once-universal-ui.jpg) + +- **The GREEN Stack βœ… for an *Evergreen* project setup:** + - **G**raphQL, **R**eact-Native, **E**xpo, **N**ext.js. + - Designed to be powerful, future-proof, flexible + - Easy to evolve as your project grows + +![Code colocation comparison, a vertical versus a horizontal split](https://fullproduct.dev/blog-assets/imgs/horizontal-vs-vertical-split.jpg) + +- **Copy-Pasteable πŸ“‚ - Monorepo Architecture:** + - Turborepo config already set up for you. + - Features are organized by domain, not by front-end/back-end split. + - This makes it easy to copy, reuse, and scale features between projects. + - Each feature workspace is self-contained: UI, API, models, schemas, utils, and more... + - All of it co-located in portable workspace packages. + +--- + +
+What does that look like? πŸ’‘ + +--- + +![Example Workspace Architecture](https://fullproduct.dev/blog-assets/imgs/colocate-by-feature-workspaces.jpg) + +The idea is that each feature is a self-contained workspace, that defines its own UI, APIs, schemas, models, etc. and has automation scripts re-export them to the right places. + +![](https://fullproduct.dev/blog-assets/imgs/feature-routes-to-universal-links.jpg) + +This allows you to copy-paste **"feature folders"** between projects, without the need for manual linking like you'd typically have to do without this architecture. + +
+ +--- + +![Matt Pocock - The right abstraction, found at the right time, can save you weeks of work. It's often worth putting the time in](https://fullproduct.dev/blog-assets/imgs/matt-pocock-right-abstractions.jpg) + +- **Single Sources of Truth πŸ’Ž - The Right Abstractions** + - Define your data shape once using Zod schema + - Derive or (auto-)generate types, validation, docs, db models, and more from them. + - Avoid bugs and wasted time by keeping your types, validation, and docs in sync automatically. + +![Universal Data Fetching](https://fullproduct.dev/blog-assets/imgs/universal-data-fetching.jpg) + +- **Universal Data Fetching πŸ”€ - For Expo and Next.js** + - GraphQL + React Query for type-safe, cross-platform data fetching. + - Fetch data the same way on server, browser, and mobile. + - Derive all GraphQL definitions and queries from Zod schemas. + - Use `react-query` to fetch serverside, in the browser and on mobile. + +![Generators vs AI Codegen](https://fullproduct.dev/blog-assets/imgs/generators-vs-ai.jpg) + +- **Modern DX & Codegen βš™οΈ - Beyond just the Setup** + - Built-in code generators for schemas, resolvers, forms, and more. + - Fast monorepo setup with Turborepo (or use standalone if you prefer). + - Includes a generator to quickly add new generators and scripts. -Open [http://localhost:3000](http://localhost:3000) with your browser to see your **Next.js 14** app on web. +[![Rich Interactive docs example](https://fullproduct.dev/blog-assets/imgs/nextra-url-docs-example.jpg)](https://fullproduct.dev/docs/@app-core/components/Button?showCode=true) -Install and/or open the [Expo Go](https://expo.io/client) app on your phone and scan the QR code to test your **Expo SDK 51** app on mobile. +- **Rich Interactive Docs πŸ“š - Automatically grow with your project** + - Full documentation at [fullproduct.dev/docs](https://fullproduct.dev/docs?v=gh-prfl) + - Best practices and guides included in the built-in docs + - Automatic UI, API and Types docs generation from Zod schemas [(e.g.)](https://fullproduct.dev/docs/@app-core/components/Button?showCode=true) + - Easy Onboardings / Handovers + *Great Context for LLMs* -## Documentation +## ❇️ The GREEN stack: -All docs for this basic Universal Starter can be found at [universal-base-starter-docs.vercel.app](https://universal-base-starter-docs.vercel.app/) and are built from the `with/mdx-docs-nextra` branch. +> πŸ“— **Docs** at [Fullproduct.dev/docs](https://fullproduct.dev/docs) -## Alternative Universal App starters +![Combining Next.ts and Expo-Router app routers](https://fullproduct.dev/blog-assets/imgs/combining-app-routers.jpg) -See [How to choose cross-platform tech](https://dev.to/codinsonn/why-use-react-native-over-flutter-a-recap-57b0) on dev.to for our more detailed list of alternatives. +The goal of any tech stack should be to stay **'Evergreen'** -**The main recommendation for a more opinionated, more automated and extensible Universal Expo + Next.js starter to [move fast and build things](https://dev.to/codinsonn/how-to-compete-with-elons-twitter-a-dev-perspective-4j64) will always be FullProduct.dev πŸ‘‡** +- βœ… **GraphQL** - Universal, type-safe data fetching +- βœ… **React-Native** - Write-once UI that feels native +- βœ… **Expo** - Cross-platform app dev (Web / iOS / Android) +- βœ… **EAS** - Effortless builds and deploys to App Stores +- βœ… **Next.js** - Web-vitals and best-in-class SSR / SEO optimization -## Level up with [FullProduct.dev](https://fullproduct.dev) ⚑️ +These are proven and widely supported technologies. -[![Screenshot of FullProduct.dev](https://github.com/user-attachments/assets/a2eecfd2-7889-4079-944b-1b5af6cf5ddf)](https://fullproduct.dev) +> Paired with TypeScript, Zod, and Tailwind (via Nativewind), this stack is designed to be robust, flexible, and here to stay. While still allowing you the freedom to choose your own Database and other core stack choices. -

- - - -

+## πŸ“¦ What’s Included? - Demo -### Git based Plugin Branches +![How portable feature workspaces combine into an Expo + Next.js app](https://fullproduct.dev/blog-assets/imgs/reusing-features-in-apps.jpg) -> "The best way to learn is through the Pull Requests" -> -- Theo / @t3dotgg +- Well-Rounded Universal App Setup (Expo + Next.js) +- Turborepo - Monorepo Workspace Structure +- Universal Routing, (Deep)Linking and Navigation +- Right Abstractions built around Zod as the Single Source of Truth +- GraphQL and API routes with Next.js +- Universal React Query setup - both for Expo and Next -[![Screenshot of list of Plugin Branches](https://github.com/user-attachments/assets/f2d4d836-c2ad-4249-bc53-de2ab7d5aac1)](https://github.com/Aetherspace/universal-app-router/pulls) +> **Note:** Git Based Plugins (for Auth, DB, Email, Payments, etc.) are coming soon! This base version is designed to be extended with plugins and your own features. -**PR & branch based plugins will provide you with the ability to:** +## πŸ’‘ Frequently Asked Questions -βœ… learn what code and files change together to add a feature -βœ… inspecting the diff that makes it possible -βœ… check-out, test and edit a plugin before merging +![What about reusing web code?](https://fullproduct.dev/blog-assets/imgs/reusing-web-code.jpg) -*This universal base starter already has some git-based plugins in the form of mergeable pull-request.* +> Just use Expo's new `"use dom"` directive [(here's how)](https://docs.expo.dev/guides/dom-components/?utm_source=fullproduct.dev&utm_medium=readme) -Needless to say, the FullProduct.dev Universal App starterkit will take this to a next level with plugin branches for: +... + +- **What is FullProduct.dev?** + - A universal app starterkit to help you launch cross-platform apps faster, with best-in-class DX and monorepo architecture set up and designed for copy-paste. +- **Why use this over other starters?** + - Most starters are either too opinionated or too barebones. This kit gives you a solid, flexible foundation and is designed for maximum code reuse across platforms, *and projects*. +- **I'm just starting out, should I use it?** + - If you know the basics of JS & React, this kit will teach you how to build universal apps that can be used in a browser / found in Google, but also be installable from the iOS / Android App Stores. + - Learning and knowing `react-native` and `expo` leads to a great skill potential employers *will* appreciate. + - Built-in markdown docs will help both you and AI coding assistants better understand your project and way of working. +- **I'm an experienced engineer, why should I use it?** + - Seniors like us know the right abstractions can save weeks / months of time. Start with a bunch of them already set up for you. + - Eases onboardings and handovers thanks to built-in docs that automatically grow as you build. + - Spend less time on boilerplate thanks to our generators and automation scripts. + - Architecture is designed for copy-paste, maximum reusability, across platforms, *and projects*. +- **How do I convince my boss to use this?** + - Show your non-technical lead the [FullProduct.dev](https://fullproduct.dev?v=gh-demo-readme) website. + - Direct your technical lead to the [docs](https://fullproduct.dev/docs?v=gh-demo-readme), specifically the [core-concepts](https://fullproduct.dev/docs/core-concepts?v=gh-demo-readme). + - Highlight the benefits of write-once universal apps: Bigger market share. More platforms = More trust = Higher margins. Maximum shareability with Universal Deeplinks > More viral potential. + - Emphasize flexibility to pick + choose your own stack while still having a solid foundation. (Mergeable ready-made `git` based plugins & PRs soon) +- **How is it licensed?** + - See `LICENSE.md` and the [eula](https://fullproduct.dev/eula?v=gh-demo-readme-license) for the details. + - Base / demo version is open source, but not full-on open contribution. + - Premium version and plugins are coming soon for [commercial licensing](https://fullproduct.dev/eula?v=gh-demo-readme-license). + +## Built with πŸ’š - by 🟒 [Thorr ⚑️ @codinsonn.dev](https://codinsonn.dev) + +![Timeline comparison to when I started experimenting with these universal app concepts vs. the releases Expo did, and the Web-Only boilerplate that have skyrocketed](https://fullproduct.dev/blog-assets/imgs/cross-platform-experimentation.jpg) + +> Hi πŸ‘‹ I'm Thorr, creator of the **❇️ [FullProduct.dev](https://fullproduct.dev)** - *Universal App Starter kit* + +This stack and kit are the result of years of experimentation building both web and mobile apps in startups, agencies, and as a freelancer + solopreneur. + +It's become a collection of best practices, patterns & tools I wish I had during **[my career ↗️](https://codinsonn.dev/resume?v=gh-demo-readme)** + +- Studies Design, Development, Motion Graphics +- Agencies B2B, MVPs, React SSR, Automatic Docgen, Expo EAS +- Startups Web, Mobile, Deeplinks, Drivers, Zod, AI +- Freelance Onboardings, Demos, Team Lead, Docs, Handovers +- SaaS + +Across a number of international projects, countries and industries: + +UK, Healthcare Europe, B2B, ECommerce US Retail, Incubator, MVP + +Now, I'm glad to share my learnings to help others build their own universal apps faster, with less manual boilerplate, and more code reusability than ever before. + +[![Timeline of my professional experience, contemplating why I have to rebuild the same feature for the 6th time](https://fullproduct.dev/blog-assets/imgs/why-are-features-not-reusable.jpg)](https://codinsonn.dev/resume?v=gh-demo-readme) + +> **Support the project** - *Please keep this entire collapsible section in your README* πŸ™ + +- [FullProduct.dev Docs πŸ“š](https://fullproduct.dev/docs?v=gh-demo-readme) - to learn / send to your lead architect +- [FullProduct.dev Landing Page](https://fullproduct.dev?v=gh-demo-readme) - upgrade / send to your boss +- [Read + Share the Blog](https://fullproduct.dev/blog?v=gh-demo-readme) or [Sponsor me](https://github.com/sponsors/codinsonn) πŸ’š + +[![Picture of me giving a talk on maximising efficiency by building universal apps](https://fullproduct.dev/blog-assets/imgs/maximise-efficiency-tech-talk-header.jpg)](https://fullproduct.dev/blog/maximize-efficiency-building-universal-apps?v=gh-demo-readme) + +> ⭐️ Follow me for updates, tips and tricks: + +- [codinsonn.dev](https://codinsonn.dev?v=gh-demo-readme) - Personal Website + social links +- Find me as [@codinsonn](https://twitter.com/codinsonn) - e.g. [GitHub](https://github.com/codinsonn) / [Twitter](https://twitter.com/codinsonn) / [LinkedIn](https://www.linkedin.com/in/thorr-stevens/) + +
+ +--- + +[![FullProduct.dev screenshot](https://github.com/user-attachments/assets/a2eecfd2-7889-4079-944b-1b5af6cf5ddf)](https://fullproduct.dev/demos?v=universal-app-router-pr-docs) + +## πŸ›  Getting Started + +Use **`git clone`**, or the GitHub UI to ❇️ **[generate a new project](https://github.com/new?template_name=green-stack-starter-demo&template_owner=FullProduct-dev&visibility=private&use_v2_form=true&description=🚧%20Make%20sure%20to%20run%20`npx%20@fullproduct/universal-app%20sync`%20to%20attach%20the%20starterkit%27s%20git%20history%20πŸ’‘%20Run%20`npx%20@fullproduct/universal-app%20install%20plugins`%20afterwards%20to%20expand%20your%20setup)** from our **[template repo](https://github.com/FullProduct-dev/green-stack-starter-demo)**, then run: + +```bash +npm install +npm run dev +``` -πŸ” Universal Auth -πŸ’Έ Payment systems like Stripe -βœ‰οΈ Sending & building emails -πŸ“š Automagic documentation -πŸ”Œ Various database integrations +- Open [http://localhost:3000](http://localhost:3000) for the Next.js app (web) +- Use [Expo Go](https://expo.io/client) or `npm run ios` / `npm run android` to test mobile -On top of so many other options, you'll also be able to move *even faster* thanks to: +--- -πŸš€ Codegen & automation so you can focus on business logic -πŸ“‹ Way of Working built for copy & pasting entire features across projects -πŸ’‘ Innovative way to use Zod as the Single Source of Truth for all data defs +**All set** πŸš€ >> Continue from the **πŸ“— [FullProduct.dev Docs](https://fullproduct.dev/docs?v=gh-demo-readme)** -> Sound interesting? πŸ‘‰ [FullProduct.dev](https://fullproduct.dev) +> ⚑️ [Quickstart Guide](https://fullproduct.dev/docs?v=gh-demo-readme) | +πŸ’‘ [Core Concepts](https://fullproduct.dev/docs/core-concepts?v=gh-demo-readme) | +πŸ“‚ [Project Structure](https://fullproduct.dev/docs/project-structure?v=gh-demo-readme) | +❇️ [Codegen](https://fullproduct.dev/docs/generators?v=gh-demo-readme) -## Next adapter & related docs +--- -- [Next Adapter repo](https://github.com/expo/expo-cli/tree/main/packages/next-adapter) -- [Expo](https://expo.io/) -- [Next.js](https://nextjs.org/) +... diff --git a/apps/expo/.env.example b/apps/expo/.env.example index 9646681..18ecaf5 100644 --- a/apps/expo/.env.example +++ b/apps/expo/.env.example @@ -1,4 +1,50 @@ -EXPO_PUBLIC_BASE_URL= -EXPO_PUBLIC_BACKEND_URL= -EXPO_PUBLIC_API_URL= -EXPO_PUBLIC_GRAPH_URL= +## --- Notes ----------------------------------------------------------------------------------- */ + +## -i- The `.env.example` file can be copied into `.env.local` using `npx turbo env:local` +## -i- For more info, development, staging & production environments, check the expo docs: +## -i- https://docs.expo.dev/guides/environment-variables/ + +## -i- Note that Expo will inline environment variables in your bundle during builds & deployments +## -i- This means dynamically retrieving environment variables from e.g. `process.env[someKey]` will not work +## -i- It also means that you should never include sensitive / private keys + +## -i- We suggest that for each environment variable you add here, you also add an entry in `appConfig.ts` +## -i- There, you can add logic like ```envValue: process.env.EXPO_PUBLIC_ENV_KEY || process.env.NEXT_PUBLIC_ENV_KEY``` +## -i- Where you would only define the EXPO_PUBLIC_ prefixed versions here in `.env.local` locally and using Expo UI for deployed envs + +EXPO_PUBLIC_APP_ENV=expo + +## --- General --------------------------------------------------------------------------------- */ +## -i- Env vars that should always be present & the same locally, independent of the simulated environment +## --------------------------------------------------------------------------------------------- */ + +# EXAMPLE= # ... + +## --- LOCAL ----------------------------------------------------------------------------------- */ +## -i- Defaults you might want to switch out for local development by commenting / uncommenting +## --------------------------------------------------------------------------------------------- */ + +EXPO_PUBLIC_BASE_URL= # Keep empty in `.env.local` for maximum local testability, `appConfig.ts` will figure out back-end URL from expo-config +EXPO_PUBLIC_BACKEND_URL= # Keep empty in `.env.local` for maximum local testability, `appConfig.ts` will figure out back-end URL from expo-config +EXPO_PUBLIC_API_URL= # Keep empty in `.env.local` for maximum local testability, `appConfig.ts` will figure out back-end URL from expo-config +EXPO_PUBLIC_GRAPH_URL= # Keep empty in `.env.local` for maximum local testability, `appConfig.ts` will figure out back-end URL from expo-config + +# EXAMPLE= # ... + +## --- DEV ------------------------------------------------------------------------------------- */ +# -i- Uncomment while on development branch to simulate the dev environment +## --------------------------------------------------------------------------------------------- */ + +# EXAMPLE= # ... + +## --- STAGE ----------------------------------------------------------------------------------- */ +# -i- Uncomment while on staging branch to simulate the stage environment +## --------------------------------------------------------------------------------------------- */ + +# EXAMPLE= # ... + +## --- PROD ------------------------------------------------------------------------------------ */ +# -i- Uncomment while on main branch to simulate the production environment +## --------------------------------------------------------------------------------------------- */ + +# EXAMPLE= # ... diff --git a/apps/expo/app.json b/apps/expo/app.json index 58344f5..91c6fcb 100644 --- a/apps/expo/app.json +++ b/apps/expo/app.json @@ -8,6 +8,8 @@ ], "web": { "bundler": "metro" - } + }, + "newArchEnabled": true, + "userInterfaceStyle": "automatic" } } diff --git a/apps/expo/app/(generated)/demos/forms/index.tsx b/apps/expo/app/(generated)/demos/forms/index.tsx new file mode 100644 index 0000000..388c5d6 --- /dev/null +++ b/apps/expo/app/(generated)/demos/forms/index.tsx @@ -0,0 +1,2 @@ +// -i- Automatically generated by 'npx turbo @green-stack/core#link:routes', do not modify manually, it will get overwritten +export { default } from '@app/core/routes/demos/forms/index' diff --git a/apps/expo/app/(generated)/demos/images/index.tsx b/apps/expo/app/(generated)/demos/images/index.tsx new file mode 100644 index 0000000..66c3b1f --- /dev/null +++ b/apps/expo/app/(generated)/demos/images/index.tsx @@ -0,0 +1,2 @@ +// -i- Automatically generated by 'npx turbo @green-stack/core#link:routes', do not modify manually, it will get overwritten +export { default } from '@app/core/routes/demos/images/index' diff --git a/apps/expo/app/(generated)/index.tsx b/apps/expo/app/(generated)/index.tsx new file mode 100644 index 0000000..0f5b0cc --- /dev/null +++ b/apps/expo/app/(generated)/index.tsx @@ -0,0 +1,2 @@ +// -i- Automatically generated by 'npx turbo @green-stack/core#link:routes', do not modify manually, it will get overwritten +export { default } from '@app/core/routes/index' diff --git a/apps/expo/app/(generated)/subpages/[slug]/index.tsx b/apps/expo/app/(generated)/subpages/[slug]/index.tsx new file mode 100644 index 0000000..7e3b4e8 --- /dev/null +++ b/apps/expo/app/(generated)/subpages/[slug]/index.tsx @@ -0,0 +1,2 @@ +// -i- Automatically generated by 'npx turbo @green-stack/core#link:routes', do not modify manually, it will get overwritten +export { default } from '@app/core/routes/subpages/[slug]/index' diff --git a/apps/expo/app/(main)/images/index.tsx b/apps/expo/app/(main)/images/index.tsx deleted file mode 100644 index bbf362b..0000000 --- a/apps/expo/app/(main)/images/index.tsx +++ /dev/null @@ -1,3 +0,0 @@ -import ImagesScreen from '@app/core/screens/ImagesScreen' - -export default ImagesScreen diff --git a/apps/expo/app/(main)/index.tsx b/apps/expo/app/(main)/index.tsx deleted file mode 100644 index 03d5802..0000000 --- a/apps/expo/app/(main)/index.tsx +++ /dev/null @@ -1,3 +0,0 @@ -import HomeScreen from '@app/core/screens/HomeScreen' - -export default HomeScreen diff --git a/apps/expo/app/(main)/subpages/[slug]/index.tsx b/apps/expo/app/(main)/subpages/[slug]/index.tsx deleted file mode 100644 index 03d778c..0000000 --- a/apps/expo/app/(main)/subpages/[slug]/index.tsx +++ /dev/null @@ -1,3 +0,0 @@ -import SlugScreen from '@app/core/screens/SlugScreen' - -export default SlugScreen diff --git a/apps/expo/app/ExpoRootLayout.tsx b/apps/expo/app/ExpoRootLayout.tsx index b8bd9f5..ff81d0f 100644 --- a/apps/expo/app/ExpoRootLayout.tsx +++ b/apps/expo/app/ExpoRootLayout.tsx @@ -1,24 +1,66 @@ +import { useEffect } from 'react' import { Stack } from 'expo-router' -import UniversalAppProviders from '@app/core/screens/UniversalAppProviders' -import UniversalRootLayout from '@app/core/screens/UniversalRootLayout' +import { configureReanimatedLogger, ReanimatedLogLevel } from 'react-native-reanimated' +import { isWeb } from '@app/config' +import UniversalAppProviders from '@app/screens/UniversalAppProviders' +import UniversalRootLayout from '@app/screens/UniversalRootLayout' +import { useColorScheme } from 'nativewind' +import { Image as ExpoContextImage } from '@green-stack/components/Image.expo' +import { Link as ExpoContextLink } from '@green-stack/navigation/Link.expo' +import { useRouter as useExpoContextRouter } from '@green-stack/navigation/useRouter.expo' +import { useRouteParams as useExpoRouteParams } from '@green-stack/navigation/useRouteParams.expo' // -i- Expo Router's layout setup is much simpler than Next.js's layout setup // -i- Since Expo doesn't require a custom document setup or server component root layout // -i- Use this file to apply your Expo specific layout setup: // -i- like rendering our Universal Layout and App Providers +/* --- Reanimated Setup ------------------------------------------------------------------------ */ + +configureReanimatedLogger({ + level: ReanimatedLogLevel.warn, + strict: false, +}) + +/* --- ------------------------------------------------------------------------ */ + export default function ExpoRootLayout() { - return ( - - - - - - ) + // Navigation + const expoContextRouter = useExpoContextRouter() + + // Theme + const scheme = useColorScheme() + + // -- Effects -- + + useEffect(() => { + // -i- Make nativewind dark mode work with Expo for Web + if (isWeb && typeof window !== 'undefined') { + const $html = document.querySelector('html') + const isDarkMode = scheme.colorScheme === 'dark' + $html?.classList.toggle('dark', isDarkMode) + } + }, [scheme.colorScheme]) + + // -- Render -- + + return ( + + + + + + ) } diff --git a/apps/expo/app/_layout.tsx b/apps/expo/app/_layout.tsx index ae07794..b3b0cf5 100644 --- a/apps/expo/app/_layout.tsx +++ b/apps/expo/app/_layout.tsx @@ -1,4 +1,5 @@ import ExpoRootLayout from './ExpoRootLayout' +import '../../next/global.css' // -i- Expo Router's layout setup is much simpler than Next.js's layout setup. // -i- Since Expo doesn't require a custom document setup or server component root layout. diff --git a/apps/expo/babel.config.js b/apps/expo/babel.config.js index 4f30874..39d2035 100644 --- a/apps/expo/babel.config.js +++ b/apps/expo/babel.config.js @@ -1,7 +1,9 @@ -// babel.config.js module.exports = function (api) { api.cache(true) return { - presets: ["babel-preset-expo"], + presets: [ + ['babel-preset-expo', { jsxImportSource: 'nativewind' }], + 'nativewind/babel', + ], } } diff --git a/apps/expo/metro.config.js b/apps/expo/metro.config.js index 873b97a..41873be 100644 --- a/apps/expo/metro.config.js +++ b/apps/expo/metro.config.js @@ -1,23 +1,24 @@ // -i- Copied from https://docs.expo.dev/guides/monorepos/#modify-the-metro-config const { getDefaultConfig } = require('expo/metro-config') +const { withNativeWind } = require('nativewind/metro') const path = require('path') // Find the project and workspace directories const projectRoot = __dirname const workspaceRoot = path.resolve(projectRoot, '../..') -const config = getDefaultConfig(projectRoot) +const config = getDefaultConfig(projectRoot, { isCSSEnabled: true }) // 1. Watch all files within the monorepo config.watchFolders = [workspaceRoot] // 2. Let Metro know where to resolve packages and in what order config.resolver.nodeModulesPaths = [ - path.resolve(projectRoot, 'node_modules'), - path.resolve(workspaceRoot, 'node_modules'), + path.resolve(projectRoot, 'node_modules'), + path.resolve(workspaceRoot, 'node_modules'), ] // 3. Force Metro to resolve (sub)dependencies only from the `nodeModulesPaths` // config.resolver.disableHierarchicalLookup = true // Export the modified config -module.exports = config +module.exports = withNativeWind(config, { input: '../next/global.css' }) diff --git a/apps/expo/nativewind-env.d.ts b/apps/expo/nativewind-env.d.ts new file mode 100644 index 0000000..c0d8380 --- /dev/null +++ b/apps/expo/nativewind-env.d.ts @@ -0,0 +1,3 @@ +/// + +// NOTE: This file should not be edited and should be committed with your source code. It is generated by NativeWind. \ No newline at end of file diff --git a/apps/expo/package.json b/apps/expo/package.json index ed38c73..e2257d4 100644 --- a/apps/expo/package.json +++ b/apps/expo/package.json @@ -4,33 +4,36 @@ "private": true, "main": "index.js", "dependencies": { - "@expo/metro-runtime": "^3.2.1", - "expo": "^51.0.8", - "expo-constants": "~16.0.1", - "expo-linking": "~6.3.1", - "expo-router": "~3.5.14", - "expo-status-bar": "~1.12.1", - "react": "18.2.0", - "react-dom": "18.2.0", - "react-native": "0.74.1", - "react-native-safe-area-context": "4.10.1", - "react-native-screens": "~3.31.1", - "react-native-web": "~0.19.11", - "expo-image": "~1.12.9" + "@expo/metro-runtime": "~4.0.0", + "expo": "~52.0.11", + "expo-constants": "~17.0.3", + "expo-image": "~2.0.2", + "expo-linking": "~7.0.3", + "expo-router": "~4.0.9", + "expo-status-bar": "~2.0.0", + "expo-system-ui": "~4.0.6", + "react": "18.3.1", + "react-dom": "18.3.1", + "react-native": "0.76.3", + "react-native-safe-area-context": "4.12.0", + "react-native-screens": "~4.1.0", + "react-native-web": "~0.19.13", + "react-native-webview": "^13.12.5" }, "devDependencies": { "@babel/core": "^7.19.3", "@expo/next-adapter": "^6.0.0", - "@types/react": "18.2.48", + "@types/react": "18.2.79", "typescript": "5.3.3" }, "scripts": { - "dev": "expo start --clear", - "start": "expo start --clear", - "android": "expo start --android", - "ios": "expo start --ios", - "web": "expo start --web", - "add-dependencies": "expo install", + "dev": "npx expo start --clear", + "start": "npx expo start --clear", + "android": "npx expo start --android", + "ios": "npx expo start --ios", + "web": "npx expo start --web", + "doctor": "npx expo-doctor", + "add-dependencies": "npx expo install", "env:local": "cp .env.example .env.local" } } diff --git a/apps/expo/tailwind.config.js b/apps/expo/tailwind.config.js new file mode 100644 index 0000000..2022d5e --- /dev/null +++ b/apps/expo/tailwind.config.js @@ -0,0 +1,16 @@ +const { universalTheme } = require('@app/core/tailwind.theme.js') + +/** @type {import('tailwindcss').Config} */ +module.exports = { + darkMode: 'class', + content: [ + '../../apps/**/*.{js,jsx,ts,tsx,md,mdx}', + '../../features/**/*.{js,jsx,ts,tsx,md,mdx}', + '../../packages/**/*.{js,jsx,ts,tsx,md,mdx}', + ], + presets: [require('nativewind/preset')], + theme: { + ...universalTheme, + }, + plugins: [require('tailwindcss-animate')], +} diff --git a/apps/expo/tsconfig.json b/apps/expo/tsconfig.json index 2ba3d6c..45a3288 100644 --- a/apps/expo/tsconfig.json +++ b/apps/expo/tsconfig.json @@ -1,12 +1,19 @@ { - "extends": "@app/core/tsconfig", - "include": [ - "**/*.ts", - "**/*.tsx", - "../../features/**/*.tsx", - "../../features/**/*.ts", - "../../packages/**/*.tsx", - "../../packages/**/*.ts" - ], - "exclude": ["node_modules"] -} + "extends": "@app/core/tsconfig", + "include": [ + "**/*.ts", + "**/*.tsx", + "../../apps/next/next-env.d.ts", + "../../packages/@green-stack-core/global.d.ts", + "../../features/@app-core/nativewind-env.d.ts", + "../../features/@app-core/appConfig.ts", + "../../features/**/*.tsx", + "../../features/**/*.ts", + "../../packages/**/*.tsx", + "../../packages/**/*.ts", + "nativewind-env.d.ts" + ], + "exclude": [ + "node_modules" + ] +} \ No newline at end of file diff --git a/apps/next/.env.example b/apps/next/.env.example index 14e33b4..846b3b0 100644 --- a/apps/next/.env.example +++ b/apps/next/.env.example @@ -1,4 +1,51 @@ +## --- Notes ----------------------------------------------------------------------------------- */ + +## -i- The `.env.example` file can be copied into `.env.local` using `npx turbo env:local` +## -i- For development, staging & production environments, check the next.js docs: +## -i- https://nextjs.org/docs/app/building-your-application/configuring/environment-variables + +## -i- Note that you should treat environment variables as if they could be inlined in your bundle during builds & deployments +## -i- This means dynamically retrieving environment variables from e.g. `process.env[someKey]` might not work +## -i- It also means that you should never prefix with `NEXT_PUBLIC_` for sensitive / private keys + +## -i- We suggest that for each environment variable you add here, you also add an entry in `appConfig.ts` +## -i- There, you can add logic like ```envValue: process.env.NEXT_PUBLIC_ENV_KEY || process.env.EXPO_PUBLIC_ENV_KEY``` +## -i- Where you would only define the NEXT_PUBLIC_ prefixed versions here in `.env.local` locally and using Next.js UI for deployed envs +## -i- For environment variables you only be available server-side, you can omit `NEXT_PUBLIC_` + +NEXT_PUBLIC_APP_ENV=next + +## --- General --------------------------------------------------------------------------------- */ +## -i- Env vars that should always be present & the same locally, independent of the simulated environment +## --------------------------------------------------------------------------------------------- */ + +APP_SECRET="your-secret-here" # used for signing header context, generate a random string + +## --- LOCAL ----------------------------------------------------------------------------------- */ +## -i- Defaults you might want to switch out for local development by commenting / uncommenting +## --------------------------------------------------------------------------------------------- */ + NEXT_PUBLIC_BASE_URL=http://localhost:3000 NEXT_PUBLIC_BACKEND_URL=http://localhost:3000 NEXT_PUBLIC_API_URL=http://localhost:3000/api NEXT_PUBLIC_GRAPH_URL=http://localhost:3000/api/graphql + +# DB_URL= # TODO: Add DB layer connection for full local dev... + +## --- DEV ------------------------------------------------------------------------------------- */ +# -i- Uncomment while on development branch to simulate the dev environment +## --------------------------------------------------------------------------------------------- */ + +# DB_URL= # TODO: Add DB layer connection for the dev environment... + +## --- STAGE ----------------------------------------------------------------------------------- */ +# -i- Uncomment while on staging branch to simulate the stage environment +## --------------------------------------------------------------------------------------------- */ + +# DB_URL= # TODO: Add DB layer connection for the stage environment... + +## --- PROD ------------------------------------------------------------------------------------ */ +# -i- Uncomment while on main branch to simulate the production environment +## --------------------------------------------------------------------------------------------- */ + +# DB_URL= # TODO: Add DB layer connection for the production environment... diff --git a/apps/next/app/(generated)/api/graphql/route.ts b/apps/next/app/(generated)/api/graphql/route.ts new file mode 100644 index 0000000..9685a2d --- /dev/null +++ b/apps/next/app/(generated)/api/graphql/route.ts @@ -0,0 +1,2 @@ +// -i- Automatically generated by 'npx turbo @green-stack/core#link:routes', do not modify manually, it will get overwritten +export { GET, POST } from '@app/core/routes/api/graphql/route' diff --git a/apps/next/app/(generated)/api/health/route.ts b/apps/next/app/(generated)/api/health/route.ts new file mode 100644 index 0000000..3c64cf9 --- /dev/null +++ b/apps/next/app/(generated)/api/health/route.ts @@ -0,0 +1,2 @@ +// -i- Automatically generated by 'npx turbo @green-stack/core#link:routes', do not modify manually, it will get overwritten +export { GET, POST } from '@app/core/routes/api/health/route' diff --git a/apps/next/app/(generated)/demos/forms/page.tsx b/apps/next/app/(generated)/demos/forms/page.tsx new file mode 100644 index 0000000..4bb7e14 --- /dev/null +++ b/apps/next/app/(generated)/demos/forms/page.tsx @@ -0,0 +1,2 @@ +"use client" +export { default, dynamic } from '@app/core/routes/demos/forms/index' diff --git a/apps/next/app/(generated)/demos/images/page.tsx b/apps/next/app/(generated)/demos/images/page.tsx new file mode 100644 index 0000000..dff79c9 --- /dev/null +++ b/apps/next/app/(generated)/demos/images/page.tsx @@ -0,0 +1,2 @@ +"use client" +export { default } from '@app/core/routes/demos/images/index' diff --git a/apps/next/app/(generated)/page.tsx b/apps/next/app/(generated)/page.tsx new file mode 100644 index 0000000..706bda0 --- /dev/null +++ b/apps/next/app/(generated)/page.tsx @@ -0,0 +1,2 @@ +"use client" +export { default } from '@app/core/routes/index' diff --git a/apps/next/app/(generated)/subpages/[slug]/page.tsx b/apps/next/app/(generated)/subpages/[slug]/page.tsx new file mode 100644 index 0000000..33a498f --- /dev/null +++ b/apps/next/app/(generated)/subpages/[slug]/page.tsx @@ -0,0 +1,2 @@ +"use client" +export { default } from '@app/core/routes/subpages/[slug]/index' diff --git a/apps/next/app/(main)/api/health/route.ts b/apps/next/app/(main)/api/health/route.ts deleted file mode 100644 index bf0ebcc..0000000 --- a/apps/next/app/(main)/api/health/route.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server' -import { healthCheck } from '@app/core/resolvers/healthCheck' -import { createRequestContext } from '@app/core/middleware/createRequestContext' - -/* --- Types ----------------------------------------------------------------------------------- */ - -type NextRequestContext> = { - params: T -} - -/* --- Handlers -------------------------------------------------------------------------------- */ - -const handler = async (req: NextRequest, nextRequestContext: NextRequestContext) => { - // Input - const searchParams = req.nextUrl.searchParams - const echo = searchParams.get('echo') || null - - // Build Context - const context = await createRequestContext({ req, ...nextRequestContext }) - - // Run Resolver - const serverHealth = await healthCheck({ args: { echo }, context }) - - // Respond - return NextResponse.json(serverHealth) -} - -/* --- Methods --------------------------------------------------------------------------------- */ - -export const GET = handler - diff --git a/apps/next/app/(main)/images/page.tsx b/apps/next/app/(main)/images/page.tsx deleted file mode 100644 index 2af31ea..0000000 --- a/apps/next/app/(main)/images/page.tsx +++ /dev/null @@ -1,4 +0,0 @@ -'use client' -import ImagesScreen from '@app/core/screens/ImagesScreen' - -export default ImagesScreen diff --git a/apps/next/app/(main)/page.tsx b/apps/next/app/(main)/page.tsx deleted file mode 100644 index ee45166..0000000 --- a/apps/next/app/(main)/page.tsx +++ /dev/null @@ -1,4 +0,0 @@ -'use client' -import HomeScreen from '@app/core/screens/HomeScreen' - -export default HomeScreen diff --git a/apps/next/app/(main)/subpages/[slug]/page.tsx b/apps/next/app/(main)/subpages/[slug]/page.tsx deleted file mode 100644 index 1ca1598..0000000 --- a/apps/next/app/(main)/subpages/[slug]/page.tsx +++ /dev/null @@ -1,4 +0,0 @@ -'use client' -import SlugScreen from '@app/core/screens/SlugScreen' - -export default SlugScreen diff --git a/apps/next/app/Document.tsx b/apps/next/app/Document.tsx index 34b9d3e..33835fb 100644 --- a/apps/next/app/Document.tsx +++ b/apps/next/app/Document.tsx @@ -1,4 +1,5 @@ -import React from 'react' +/* @jsxImportSource react */ +import type { ReactNode } from 'react' import UniversalRootLayout from '@app/screens/UniversalRootLayout' import ServerStylesProvider from './ServerStylesProvider' import '../global.css' @@ -9,29 +10,44 @@ import '../global.css' /* --- ------------------------------------------------------------------------------ */ -const Document = (props: { children: React.ReactNode }) => { - // Props - const { children } = props - - // -- Render -- - - return ( - - - {/* - Title & Keywords - */} - Universal App Router - {/* - Styling - */} - {children} - {/* - Other - */} - - - - - {children} - - - - ) +const Document = (props: { children: ReactNode }) => { + + // Props + const { children } = props + + // -- Render -- + + return ( + + + {/* - Title & Keywords - */} + Universal App Starter + + + + + {/* - Image Previews - */} + + + + + + + + + + {/* - Other - */} + + + + + +
{children}
+
+
+ + + ) } /* --- Exports --------------------------------------------------------------------------------- */ diff --git a/apps/next/app/NextClientRootLayout.tsx b/apps/next/app/NextClientRootLayout.tsx index 2c34c12..d231d18 100644 --- a/apps/next/app/NextClientRootLayout.tsx +++ b/apps/next/app/NextClientRootLayout.tsx @@ -1,6 +1,12 @@ 'use client' import React from 'react' +import { SafeAreaProvider } from 'react-native-safe-area-context' import UniversalAppProviders from '@app/screens/UniversalAppProviders' +import { Image as NextContextImage } from '@green-stack/core/components/Image.next' +import { Link as NextContextLink } from '@green-stack/core/navigation/Link.next' +import { useRouter as useNextContextRouter } from '@green-stack/navigation/useRouter.next' +import { useRouteParams as useNextRouteParams } from '@green-stack/navigation/useRouteParams.next' +import { isServer } from '@app/config' // -i- This is a regular react client component // -i- It's still rendered on the server during SSR, but it also hydrates on the client @@ -11,16 +17,39 @@ import UniversalAppProviders from '@app/screens/UniversalAppProviders' type NextClientRootLayoutProps = { children: React.ReactNode + requestContext?: Record } /* --- ---------------------------------------------------------------- */ -const NextClientRootLayout = ({ children }: NextClientRootLayoutProps) => ( - - {children} - -) +const NextClientRootLayout = ({ children, requestContext }: NextClientRootLayoutProps) => { + + // Navigation + const nextContextRouter = useNextContextRouter() + + // -- Render -- + + return ( + + + {children} + + + ) +} /* --- Exports --------------------------------------------------------------------------------- */ - + export default NextClientRootLayout diff --git a/apps/next/app/NextServerRootLayout.tsx b/apps/next/app/NextServerRootLayout.tsx index dd50e6d..cbaac8f 100644 --- a/apps/next/app/NextServerRootLayout.tsx +++ b/apps/next/app/NextServerRootLayout.tsx @@ -1,5 +1,9 @@ +/* @jsxImportSource react */ +import { ReactNode } from 'react' import Document from './Document' import NextClientRootLayout from './NextClientRootLayout' +import { headers } from 'next/headers' +import { parseIfJSON } from '@green-stack/utils/apiUtils' // -i- This is a react server component that serves as the root (server) layout for our app // -i- Use this file to do server-only things for web: @@ -9,18 +13,27 @@ import NextClientRootLayout from './NextClientRootLayout' /* --- Types ----------------------------------------------------------------------------------- */ type NextServerRootLayoutProps = { - children: React.ReactNode + children: ReactNode } /* --- ----------------------------------------------------------------- */ -const NextServerRootLayout = ({ children }: NextServerRootLayoutProps) => ( - - - {children} - - -) +const NextServerRootLayout = async ({ children }: NextServerRootLayoutProps) => { + + const headersContext = await headers() + const requestContextJSON = await headersContext.get('context') + const requestContext = parseIfJSON(requestContextJSON) + + // -- Render -- + + return ( + + + {children} + + + ) +} /* --- Exports --------------------------------------------------------------------------------- */ diff --git a/apps/next/app/ServerStylesProvider.tsx b/apps/next/app/ServerStylesProvider.tsx index 450fd2f..1d3a6d8 100644 --- a/apps/next/app/ServerStylesProvider.tsx +++ b/apps/next/app/ServerStylesProvider.tsx @@ -1,9 +1,8 @@ 'use client' /* eslint-disable @next/next/no-head-element */ import React from 'react' -import { AppRegistry } from 'react-native' +import { StyleSheet } from 'react-native' import { useServerInsertedHTML } from 'next/navigation' -import UniversalRootLayout from '@app/screens/UniversalRootLayout' // -i- This is a regular react client component // -i- However, it is rendered on the server during SSR @@ -14,30 +13,25 @@ import UniversalRootLayout from '@app/screens/UniversalRootLayout' const ServerStylesProvider = (props: { children: React.ReactNode }) => { // Props const { children } = props - + // -- Serverside Styles -- - + useServerInsertedHTML(() => { - // Get react-native-web styles - const Main = () => {children} - AppRegistry.registerComponent('Main', () => Main) // @ts-ignore - const mainApp = AppRegistry.getApplication('Main') - const reactNativeStyleElement = mainApp.getStyleElement() - // Inject styles - return ( - <> - {reactNativeStyleElement} - {/* OPTIONAL: Insert other SSR'd styles here? */} - - ) + // @ts-ignore + const sheet = StyleSheet.getSheet() + return ( + + ) +} + +/* --- Exports --------------------------------------------------------------------------------- */ + +export default Style diff --git a/packages/@green-stack-core/components/WebView.tsx b/packages/@green-stack-core/components/WebView.tsx new file mode 100644 index 0000000..02acbc0 --- /dev/null +++ b/packages/@green-stack-core/components/WebView.tsx @@ -0,0 +1,28 @@ +import { WebView as NativeWebView } from 'react-native-webview' +import { styled } from '../styles' + +/* --- Styles ---------------------------------------------------------------------------------- */ + +const StyledWebView = styled(NativeWebView) + +/* --- Props ----------------------------------------------------------------------------------- */ + +type WebViewProps = React.ComponentProps & { + className?: string + src?: string +} + +/* --- ------------------------------------------------------------------------------ */ + +export const WebView = (props: WebViewProps) => { + return ( + + ) +} + +/* --- Exports --------------------------------------------------------------------------------- */ + +export default WebView diff --git a/packages/@green-stack-core/components/WebView.web.tsx b/packages/@green-stack-core/components/WebView.web.tsx new file mode 100644 index 0000000..805488b --- /dev/null +++ b/packages/@green-stack-core/components/WebView.web.tsx @@ -0,0 +1,5 @@ +import React from 'react' + +export const WebView = (props: React.DetailedHTMLProps, HTMLIFrameElement>) => ( +