diff --git a/.cursor/rules/comments.mdc b/.cursor/rules/comments.mdc new file mode 100644 index 0000000..d63fd30 --- /dev/null +++ b/.cursor/rules/comments.mdc @@ -0,0 +1,11 @@ +--- +description: Preserve existing comments when editing +alwaysApply: true +--- + +# Comments + +When editing any file in this workspace: + +- **Preserve all existing comments** in that file. Do not delete or shorten them unless you are **deleting or rewriting the exact lines they refer to**—then update those comments to match the new behavior. +- **Treat inline and block comments as part of the API** of the file: moving code should move comments with it; refactors must **re-home** comments, not drop them for brevity. diff --git a/.cursor/rules/resolvers.mdc b/.cursor/rules/resolvers.mdc new file mode 100644 index 0000000..50fbf06 --- /dev/null +++ b/.cursor/rules/resolvers.mdc @@ -0,0 +1,16 @@ +--- +description: Data resolvers and API design - createDataBridge, createResolver +globs: "**/resolvers/**/*.ts" +alwaysApply: false +--- + +# Data Resolvers + +When editing resolver files, reference [https://fullproduct.dev/docs/data-resolvers](https://fullproduct.dev/docs/data-resolvers) for: + +- `createDataBridge()` - Combines input/output schemas for type-safe API bridges +- `createResolver()` - Bind a bridge to business logic +- `createNextRouteHandler()` - Turn resolver into API route +- `createGraphResolver()` - Turn resolver into GraphQL mutation/query +- Use `.bridge.ts` suffix for bridge files +- RPC-style (GraphQL or API route) resolver (shape resolvers for UI screens, not traversible graph) diff --git a/.cursor/rules/routing.mdc b/.cursor/rules/routing.mdc new file mode 100644 index 0000000..0a92c69 --- /dev/null +++ b/.cursor/rules/routing.mdc @@ -0,0 +1,14 @@ +--- +description: Workspace-defined routes - define in features, not apps +globs: "**/routes/**/*" +alwaysApply: false +--- + +# Routing + +When editing route files, reference [https://fullproduct.dev/docs/universal-routing](https://fullproduct.dev/docs/universal-routing) for: + +- Routes live in `features/*/routes/`, not in `apps/expo/app/` or `apps/next/app/` +- Run `npm run link:routes` to re-export routes from features to the Expo/Next app routers +- Use `npm run add:route` generator to create new routes +- Re-export screens from route files; use Next.js-style routing conventions diff --git a/.cursor/rules/schemas.mdc b/.cursor/rules/schemas.mdc new file mode 100644 index 0000000..0c81264 --- /dev/null +++ b/.cursor/rules/schemas.mdc @@ -0,0 +1,15 @@ +--- +description: Zod schemas as single source of truth - schema() usage +globs: "**/schemas/**/*.ts" +alwaysApply: false +--- + +# Schemas + +When editing schema files, reference [https://fullproduct.dev/docs/single-sources-of-truth](https://fullproduct.dev/docs/single-sources-of-truth) for: + +- Use `schema()` from `@green-stack/schemas` (not raw `z.object()`) +- Schemas drive types, validation, GraphQL, API inputs/outputs, forms, docs and even DB models +- Use `.extendSchema()`, `.pickSchema()`, `.omitSchema()` for schema composition +- Add `.describe()`, `.example()`, `.default()` for docs and docgen +- Use `.sensitive()` for fields that shouldn't appear in GraphQL/introspection or on the front-end at all diff --git a/.cursor/rules/workspace-expo-dependencies.mdc b/.cursor/rules/workspace-expo-dependencies.mdc new file mode 100644 index 0000000..2c49ab5 --- /dev/null +++ b/.cursor/rules/workspace-expo-dependencies.mdc @@ -0,0 +1,25 @@ +--- +description: Portable workspaces — declare Expo/native deps on the package that imports them, not on @app/expo +globs: "**/package.json" +alwaysApply: true +--- + +# Expo & native dependencies (portable workspaces) + +**Goal:** Keep `apps/expo/package.json` lean. Anything required only by a feature or `packages/*` workspace should live in **that workspace’s `dependencies`** and `package.json`, not duplicated on the Expo app. + +## Adding or upgrading Expo SDK–aligned packages + +1. Use **`npm run add:dependencies`** (generator in `packages/@green-stack-core/generators/add-dependencies.ts`). It runs `expo install` in `@app/expo` to resolve **SDK-compatible** versions, then moves the new entries to the **target workspace** you choose. +2. Do **not** leave Expo-only deps on `@app/expo` unless the **Expo app** is the only consumer (shell, router, `expo-router`, `expo-constants`, etc.). + +## When to put a dep on `@app/expo` vs a workspace + +| Put on `@app/expo` | Put on the feature / `packages/*` workspace that imports it | +|--------------------|-------------------------------------------------------------| +| `expo`, `expo-router`, `expo-constants`, `nativewind`, app shell | `expo-auth-session`, `expo-secure-store`, `expo-web-browser` if only `@auth/clerk` (or similar) imports them | +| Core RN stack shared by the app entry | Driver-specific packages (`@clerk/*`, etc.) | + +## After edits + +Run `npm install` at the repo root and verify with **`npm run build:mobile`** (and any affected app build) when changing native/Expo deps. diff --git a/.gitignore b/.gitignore index ef95438..b7f2223 100644 --- a/.gitignore +++ b/.gitignore @@ -45,6 +45,9 @@ yarn-error.log* *.mobileprovision *.orig.* +# Expo Builds +apps/expo/dist + # Next.js /.next/* /out/ @@ -57,3 +60,13 @@ yarn-error.log* .pnp.js # -- @end @expo/next-adapter -- + +.turbo +.vscode/tmp + +# Cursor +.cursor/hooks/state +.cursor/worktrees.json + +# GraphQL +features/@app-core/graphql-env.d.ts.tmp diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..8b2185d --- /dev/null +++ b/.npmrc @@ -0,0 +1,5 @@ +# -i- React Native / Expo pin react@19.1.0 to match the bundled react-native-renderer. +# -i- Clerk packages declare peer ranges that omit 19.1.0 (e.g. ~19.1.4 is the next band), +# -i- so npm 7+ strict peer resolution prints ERESOLVE warnings even though root overrides intentionally align the tree. +# -i- legacy-peer-deps uses npm 6-style peer handling so installs stay quiet without changing pinned React. +legacy-peer-deps=true 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/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..794dcc1 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,96 @@ +# FullProduct.dev / GREEN Stack + +This project was kickstarted with FullProduct.dev's universal app starterkit: + +## Core conventions + +- **Zod schemas as single source of truth**: Use `schema()` from `@green-stack/schemas`. APIs, types, db models and docs derive from these to stay in sync. +- **Workspace-defined routes**: Define routes in `features/*/routes/`. Run `npm run link:routes` to re-export to Expo/Next. +- **Universal UI**: Use `View`, `Text`, `Image` from `@app/ui` with Nativewind compatible `className` instead of HTML primitives. +- **Expo + Next.js**: Most UI must work on **web (Next.js)** and **mobile (Expo)**. Routing, images, and some APIs use **React Portability Patterns** (shared types + `*.next` / `*.expo` implementations injected at app roots)—not only `.web.ts` / `.ios.ts` splits. See [React Portability Patterns](https://fullproduct.dev/docs/portability-patterns). +- **Feature-first colocation**: Routes, resolvers, schemas, screens live together in `features/*/` or `packages/*/` workspaces instead of being split by frontend/backend. +- **Portable workspaces and dependencies**: Expo/native packages used only by a `features/*/` or `packages/*/` workspace belong in **that workspace’s `package.json`**, not on `apps/expo`. Use **`npm run add:dependencies`** to install SDK-compatible versions via `expo install`, the script will then move the resolved versions to the target workspace (see `.cursor/rules/workspace-expo-dependencies.mdc`). + +## Detailed docs (reference when relevant) + +- [Project structure](https://fullproduct.dev/docs/project-structure) +- [Routing](https://fullproduct.dev/docs/universal-routing) +- [React Portability Patterns](https://fullproduct.dev/docs/portability-patterns) +- [Data resolvers & APIs](https://fullproduct.dev/docs/data-resolvers) +- [Data fetching](https://fullproduct.dev/docs/data-fetching) +- [Schemas & single sources of truth](https://fullproduct.dev/docs/single-sources-of-truth) +- [Universal styling](https://fullproduct.dev/docs/write-once-styles) +- [Form management](https://fullproduct.dev/docs/form-management) +- [Generators](https://fullproduct.dev/docs/generators) +- [Workspace drivers](https://fullproduct.dev/docs/workspace-drivers) +- [Env vars + App config](https://fullproduct.dev/docs/app-config) +- [Automatic docgen](https://fullproduct.dev/docs/automatic-docgen) + +--- +# Code Style +--- + +When updating or recreating files: +- always maintain the same code style and tab spacing of the existing files + +### Comments + +- **Preserve all existing comments** in any file you edit. Do not delete or shorten them unless you are **deleting or rewriting the exact lines they refer to**—then update those comments to match the new behavior. +- **Treat inline and block comments as part of the API** of the file: moving code should move comments with it; refactors must **re-home** comments, not drop them for brevity. + +### Type Safety + +- Try to never use `any` unless absolutely necessary. Attempt to use generics instead or inference instead. +- If we need a type and can extract a type from a Zod schema, we should. Check our single sources of truth docs for more info about that. + +### File layout (section dividers) + +- Use section dividers like `/* --- Section name ------------------------------------------------------------- */` padded to **~100 characters** (match surrounding files). +- Prefer this **order** when it fits: **constants** → **types** → **implementation** (add other sections such as styles or exports as needed). + +--- +# Plans +--- + +Wherever possible, think of a plan and good feeback loop for the code you'll be writing. + +Make the plan extremely concise. Sacrifice grammar for the sake of concision. + +At the end of each plan, give me a list of unresolved questions to answer, if any. + +A good feedback loop could be a test suite to run red green iterations with, or look at some of our scripts to run to help test everything still works as expected. + +--- +# Checks +--- + +### Testing and regression checks + +To see whether all apps are still working as expected, run the relevant commands to check: + +- `npm run test` runs all bun tests in `@green-stack/core`. +- `npm run typecheck` runs `typecheck` in every workspace that defines it (via Turbo, parallel + cached). +- `npm run typecheck:web` and `typecheck:mobile` scope to one app. +- `npm run build:web` runs the Next.js production build for `@app/next` (`apps/next`). +- `npm run build:mobile` runs `expo export` for `@app/expo` (`apps/expo`). +- `npm run build` runs the full Turbo `build` pipeline for the monorepo. + +--- +# Cursor Cloud specific instructions +--- + +### Prerequisites + +- **bun** is required for `npm run test` (the test runner is bun). Install via `curl -fsSL https://bun.sh/install | bash` if missing, then ensure `~/.bun/bin` is on `PATH`. + +### Running the app + +- `npm run dev:web` starts the Next.js app with Turbo; it first runs `build:schema` (which also triggers `collect:resolvers`, `collect:schemas`, `collect:drivers`, `collect:models`). +- **Expo / mobile**: Cloud agents usually run **Linux VMs** without **macOS/iOS Simulator** or a typical local **Android emulator**. Do not assume you can open a simulator here; rely on `dev:web` + tests/builds where possible and call out **manual Expo verification** for the user when changes are platform-sensitive. +- The DB layer has a built-in mock memory DB fallback if no `DB_URL`/`MONGODB_URI` is set, so the app runs without an external database. +- Clerk auth and Stripe require real secrets to function; without them the app still starts but auth/payment flows will fail. + +### Gotchas + +- `npm run dev:web` uses `next dev --webpack` (webpack mode, not Turbopack). +- The dev server first-compiles pages on demand; initial page loads may be slow. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..1e9c845 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,76 @@ +# FullProduct.dev / GREEN Stack + +This project was kickstarted with FullProduct.dev's universal app starterkit: + +## Core conventions + +- **Zod schemas as single source of truth**: Use `schema()` from `@green-stack/schemas`. APIs, types, db models and docs derive from these to stay in sync. +- **Workspace-defined routes**: Define routes in `features/*/routes/`. Run `npm run link:routes` to re-export to Expo/Next. +- **Universal UI**: Use `View`, `Text`, `Image` from `@app/ui` with Nativewind compatible `className` instead of HTML primitives. +- **Expo + Next.js**: Most UI must work on **web (Next.js)** and **mobile (Expo)**. Routing, images, and some APIs use **React Portability Patterns** (shared types + `*.next` / `*.expo` implementations injected at app roots)—not only `.web.ts` / `.ios.ts` splits. See [React Portability Patterns](https://fullproduct.dev/docs/portability-patterns). +- **Feature-first colocation**: Routes, resolvers, schemas, screens live together in `features/*/` or `packages/*/` workspaces instead of being split by frontend/backend. +- **Portable workspaces and dependencies**: Expo/native packages used only by a `features/*/` or `packages/*/` workspace belong in **that workspace’s `package.json`**, not on `apps/expo`. Use **`npm run add:dependencies`** to install SDK-compatible versions via `expo install`, the script will then move the resolved versions to the target workspace (see `.cursor/rules/workspace-expo-dependencies.mdc`). + +## Detailed docs (reference when relevant) + +- [Project structure](https://fullproduct.dev/docs/project-structure) +- [Routing](https://fullproduct.dev/docs/universal-routing) +- [React Portability Patterns](https://fullproduct.dev/docs/portability-patterns) +- [Data resolvers & APIs](https://fullproduct.dev/docs/data-resolvers) +- [Data fetching](https://fullproduct.dev/docs/data-fetching) +- [Schemas & single sources of truth](https://fullproduct.dev/docs/single-sources-of-truth) +- [Universal styling](https://fullproduct.dev/docs/write-once-styles) +- [Form management](https://fullproduct.dev/docs/form-management) +- [Generators](https://fullproduct.dev/docs/generators) +- [Workspace drivers](https://fullproduct.dev/docs/workspace-drivers) +- [Env vars + App config](https://fullproduct.dev/docs/app-config) +- [Automatic docgen](https://fullproduct.dev/docs/automatic-docgen) + +--- +# Code Style +--- + +When updating or recreating files: +- always maintain the same code style and tab spacing of the existing files + +### Comments + +- **Preserve all existing comments** in any file you edit. Do not delete or shorten them unless you are **deleting or rewriting the exact lines they refer to**—then update those comments to match the new behavior. +- **Treat inline and block comments as part of the API** of the file: moving code should move comments with it; refactors must **re-home** comments, not drop them for brevity. + +### Type Safety + +- Try to never use `any` unless absolutely necessary. Attempt to use generics instead or inference instead. +- If we need a type and can extract a type from a Zod schema, we should. Check our single sources of truth docs for more info about that. + +### File layout (section dividers) + +- Use section dividers like `/* --- Section name ------------------------------------------------------------- */` padded to **~100 characters** (match surrounding files). +- Prefer this **order** when it fits: **constants** → **types** → **implementation** (add other sections such as styles or exports as needed). + +--- +# Plans +--- + +Wherever possible, think of a plan and good feeback loop for the code you'll be writing. + +Make the plan extremely concise. Sacrifice grammar for the sake of concision. + +At the end of each plan, give me a list of unresolved questions to answer, if any. + +A good feedback loop could be a test suite to run red green iterations with, or look at some of our scripts to run to help test everything still works as expected. + +--- +# Checks +--- + +### Testing and regression checks + +To see whether all apps are still working as expected, run the relevant commands to check: + +- `npm run test` runs all bun tests in `@green-stack/core`. +- `npm run typecheck` runs `typecheck` in every workspace that defines it (via Turbo, parallel + cached). +- `npm run typecheck:web` and `typecheck:mobile` scope to one app. +- `npm run build:web` runs the Next.js production build for `@app/next` (`apps/next`). +- `npm run build:mobile` runs `expo export` for `@app/expo` (`apps/expo`). +- `npm run build` runs the full Turbo `build` pipeline for the monorepo. diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..47b53c9 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,163 @@ +# FullProduct.dev Demo Source-Available License (FDSAL) + +**Version 1.1 — 2026-01-27** + +Copyright (c) 2025–2026 Aetherspace Digital (FullProduct.dev). +All rights reserved. + +This license governs use of the FullProduct.dev “GREEN Stack Starter Demo” repository +(the “Software”). By using, copying, modifying, or distributing the Software, you agree +to this license. + +## Definitions + +- **End Product**: a specific web and/or mobile application you build using the Software. +- **Starter/Template**: a generalized or reusable codebase intended to be used as a + starting point for multiple different End Products by you or others. +- **Third party**: any person or entity other than you; however, for purposes of Sections + 5(c) and 5(d), a client and that client’s employees/contractors working on the same End + Product are permitted recipients of an End Product codebase. + +## Paid License Override (Order of Precedence) + +If you have purchased a valid paid FullProduct.dev license, then: + +- the restrictions in **Section 5** (No Private Redistribution of the Starter), and +- the attribution requirements in **Section 6** + +may be waived, but only to the extent permitted by your paid license/EULA. + +In case of conflict, the paid license/EULA controls for the paid components and for any +waivers it grants. + +## 1. License Grant (Demo) + +Subject to the terms below, we grant you a non-exclusive, worldwide, revocable license to: + +1. view and use the Software; +2. modify the Software to build End Products; +3. use the Software to perform paid development services for clients, including building + End Products for clients, provided you comply with this license; +4. distribute End Products you build with the Software; and +5. redistribute the Software only as permitted in **Section 4** (Redistribution). + +## 2. Commercial Use (Allowed Under Threshold) + +Commercial use is allowed provided that, in the prior 12 months, you have not generated +**USD $10,000 or more** in gross revenue that is attributable to: + +1. an End Product built with the Software, and/or +2. services substantially enabled by the Software + +(the **Revenue Threshold**). + +If you meet or exceed the Revenue Threshold, you must obtain a paid FullProduct.dev +license to continue using the Software for commercial purposes. + +## 3. No Premium Features; No Mixing Premium Code + +The demo Software does not include premium features (for example: CLI workflows, +installable PRs, auto-documentation, or other premium plugins). + +You may not copy, incorporate, or distribute any premium FullProduct.dev code, features, +or plugins unless you have a valid paid license for those premium components. If you are +licensed for premium components, you must comply with the applicable commercial +license/EULA for those components. + +## 4. Redistribution (Demo Only) — Allowed With Conditions + +You may redistribute the unmodified Software, or your modified version of the Software, +provided that: + +1. the redistributed code contains only demo-version code and does not contain any premium + code/features/plugins; +2. you include this `LICENSE.md` file in full with the redistributed code; +3. you do not represent the redistributed code as an official FullProduct.dev release; and +4. you comply with the attribution requirements in **Section 6** (unless waived under + **Section 0** by a paid license). + +## 5. No Private Redistribution of the Starter (Demo) + +You may not distribute the Software (including modified versions) as a reusable +Starter/Template, boilerplate, or codebase in any private manner to any third party +(including but not limited to: private GitHub forks, private mirrors, private repos, zipped +downloads, or direct sharing), except as permitted by a paid license under **Section 0**. + +### Clarifications + +1. This restriction does **not** prohibit you from performing paid client work using the + Software, as long as you do not provide the Software itself to the client (or any other + third party) as a reusable Starter/Template/codebase. +2. This restriction does **not** prohibit you from keeping your End Product source code + private, so long as you are not distributing the Software itself as a reusable + Starter/Template/codebase to third parties. +3. Delivering an End Product codebase to a client is permitted, provided that the delivered + codebase is a specific End Product (not marketed, packaged, or intended as a reusable + Starter/Template) and you continue to comply with this license (including **Section 3** + and **Section 6** unless waived under **Section 0**). +4. Sharing an End Product codebase with the client’s employees and contractors who are + working on that End Product is permitted, provided it is for the purpose of developing, + maintaining, or operating that End Product and not for creating or distributing a reusable + Starter/Template. +5. You must not provide the Software to any third party as a separate “clean starter” or + “base template” repository, even if you also deliver an End Product. + +## 6. Attribution Requirements (README + End Product) (Waivable With Paid License) + +Unless waived under **Section 0** by a paid license, if you redistribute the Software +(**Section 4**) or ship an End Product built with the Software, you must provide attribution +as follows: + +### (a) README attribution (for redistributed Software or source-available starters) + +Keep an attribution section in the README that states the project is based on FullProduct.dev +and links to: + +- https://fullproduct.dev +- the original demo repository URL (or your public redistribution URL) + +### (b) End Product attribution (for shipped apps/websites) + +Include a visible attribution in the End Product, such as in “About”, “Settings”, “Legal”, +an in-app credits screen, or a website footer, stating **“Built with FullProduct.dev”** +(or similar) and linking to https://fullproduct.dev where technically feasible. + +You may satisfy this for native mobile apps by including the attribution in a “Legal”, +“About”, or “Libraries” screen. + +## 7. Restrictions + +Except as expressly permitted by this license, you may not: + +1. sell, sublicense, or resell the Software as a starter kit, template, boilerplate, or codebase; +2. make the Software available to end users as a product whose primary purpose is to provide + access to the Software as a Starter/Template; or +3. remove or alter copyright, trademark, or licensing notices included with the Software. + +## 8. Trademarks + +“FullProduct.dev” and related marks/logos are trademarks of Aetherspace Digital. + +This license does not grant any rights to use our trademarks except for: + +1. the attribution required by **Section 6**, and +2. truthful, nominative use. + +## 9. Warranty Disclaimer + +THE SOFTWARE IS PROVIDED “AS IS” AND “AS AVAILABLE”, WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE, TITLE, AND NON-INFRINGEMENT. + +## 10. Limitation of Liability + +TO THE MAXIMUM EXTENT PERMITTED BY LAW, IN NO EVENT WILL THE COPYRIGHT HOLDER OR AUTHORS BE +LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, +OR OTHERWISE, ARISING FROM, OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. OUR TOTAL LIABILITY WILL NOT EXCEED THE AMOUNT YOU PAID FOR THE +SOFTWARE (IF ANY). + +## 11. Termination + +If you violate this license, your rights under this license terminate automatically. Upon +termination, you \ No newline at end of file diff --git a/README.md b/README.md index d22a3bc..2a4202c 100644 --- a/README.md +++ b/README.md @@ -1,82 +1,251 @@ -# 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 in 100 seconds on YouTube + -It's a good starting point if you want to: +> This project is built with [FullProduct.dev](https://fullproduct.dev?v=gh-demo-readme) ✦ -- ✅ 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 +> A universal starter kit for building apps with Expo (iOS / Android) and Next.js (Web + SSR) - Providing a familiar but optimized, write-once, app router experience. -> 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. +> [!NOTE] +> While the core of FullProduct.dev is pretty mature, it's still early when it comes to our available list of plugins and integrations. But this shouldn't be a huge problem for a good dev team as it's easy to add your own reusable features as git plugins. -## Getting Started +--- -```bash -npm install -``` +
+Why FullProduct.dev? ⚡️ -```bash -npm run dev -``` +--- + +[![FullProduct.dev Bento Slide](https://fullproduct.dev/full-product-dev-bento.jpg)](https://fullproduct.dev?v=gh-demo-readme) + +## 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. + +[![Rich Interactive docs example](https://fullproduct.dev/blog-assets/imgs/nextra-url-docs-example.jpg)](https://fullproduct.dev/docs/@app-ui/components/Button?showCode=true) + +- **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-ui/components/Button?showCode=true) + - Easy Onboardings / Handovers + *Great Context for LLMs* + +## ❇️ The GREEN stack: -Open [http://localhost:3000](http://localhost:3000) with your browser to see your **Next.js 14** app on web. +> 📗 **Docs** at [Fullproduct.dev/docs](https://fullproduct.dev/docs) -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. +![Combining Next.ts and Expo-Router app routers](https://fullproduct.dev/blog-assets/imgs/combining-app-routers.jpg) -## Documentation +The goal of any tech stack should be to stay **'Evergreen'** -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. +- ✅ **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 -## Alternative Universal App starters +These are proven and widely supported technologies. -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. +> 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. -**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 👇** +## 📦 What’s Included? - Demo -## Level up with [FullProduct.dev](https://fullproduct.dev) ⚡️ +![How portable feature workspaces combine into an Expo + Next.js app](https://fullproduct.dev/blog-assets/imgs/reusing-features-in-apps.jpg) -[![Screenshot of FullProduct.dev](https://github.com/user-attachments/assets/a2eecfd2-7889-4079-944b-1b5af6cf5ddf)](https://fullproduct.dev) +- 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 -

- - - -

+> **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. + +## 💡 Frequently Asked Questions + +![What about reusing web code?](https://fullproduct.dev/blog-assets/imgs/reusing-web-code.jpg) + +> 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) + +... + +- **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: + +```md +npm install +npm run dev +``` -### Git based Plugin Branches +- 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 -> "The best way to learn is through the Pull Requests" -> -- Theo / @t3dotgg +> **All set** 🚀 >> Continue from the **📗 [FullProduct.dev Docs](https://fullproduct.dev/docs?v=gh-demo-readme)** -[![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) +> ⚡️ [Quickstart](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) -**PR & branch based plugins will provide you with the ability to:** +... -✅ 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 +--- -*This universal base starter already has some git-based plugins in the form of mergeable pull-request.* +
+ FullProduct.dev - License -Needless to say, the FullProduct.dev Universal App starterkit will take this to a next level with plugin branches for: +--- -🔐 Universal Auth -💸 Payment systems like Stripe -✉️ Sending & building emails -📚 Automagic documentation -🔌 Various database integrations +## FullProduct.dev - License (Demo version) -On top of so many other options, you'll also be able to move *even faster* thanks to: +This free template repo is **source-available** under the **FullProduct.dev Demo Source-Available +License** -🚀 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 +- Commercial use is allowed below the revenue threshold. +- You may redistribute the demo code (demo-only) with attribution. +- You may not privately distribute this starter/template/codebase without a paid license. +- Attribution requirements may be waived with a paid license. -> Sound interesting? 👉 [FullProduct.dev](https://fullproduct.dev) +Please see the full license text in [`LICENSE.md`](./LICENSE.md) and the [EULA](https://fullproduct.dev/eula?v=gh-demo-readme-license) +for more details. -## 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..6c8df4e --- /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/demo/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..00b0619 --- /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/demo/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..09d561b --- /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/demo/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..7c99de4 --- /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/demo/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..d6e66aa 100644 --- a/apps/expo/app/ExpoRootLayout.tsx +++ b/apps/expo/app/ExpoRootLayout.tsx @@ -1,24 +1,67 @@ +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..fead72c 100644 --- a/apps/expo/babel.config.js +++ b/apps/expo/babel.config.js @@ -1,7 +1,27 @@ -// babel.config.js +const { hasModule } = require('babel-preset-expo/build/common') +const { expoRouterBabelPlugin } = require('babel-preset-expo/build/expo-router-plugin') + +/* --- Disclaimers ----------------------------------------------------------------------------) */ + +// -i- babel-preset-expo only adds expo-router's Babel plugin when `hasModule('expo-router')` is true; +// -i- that uses `require.resolve` from babel-preset-expo's package root. +// -i- If expo-router is nested (e.g. only under apps/expo/node_modules in a monorepo), +// -i- the preset skips the plugin and Metro fails on expo-router/_ctx.*.js + +const explicitExpoRouterPlugin = hasModule('expo-router') ? [] : [expoRouterBabelPlugin] + +/* --- Babel Config ----------------------------------------------------------------------------*/ + module.exports = function (api) { - api.cache(true) - return { - presets: ["babel-preset-expo"], - } + api.cache(true) + return { + presets: [ + // -i- babel-preset-expo manages the Reanimated Babel plugin for Reanimated v4 (Expo SDK 54+) + ['babel-preset-expo', { jsxImportSource: 'nativewind' }], + 'nativewind/babel', + ], + // -i- Reanimated 3.17 uses react-native-reanimated/plugin (worklets/plugin is for Reanimated 4.x) + // -i- react-native-reanimated/plugin MUST be last - required for valueUnpacker worklet transform + plugins: [...explicitExpoRouterPlugin, 'react-native-reanimated/plugin'], + } } diff --git a/apps/expo/index.js b/apps/expo/index.js index 131d230..16d92ba 100644 --- a/apps/expo/index.js +++ b/apps/expo/index.js @@ -1,2 +1,6 @@ +// Polyfills must load first +import './polyfills' +import 'react-native-get-random-values' + // registerRootComponent happens in "expo-router/entry" import 'expo-router/entry' diff --git a/apps/expo/metro.config.js b/apps/expo/metro.config.js index 873b97a..fa72ba6 100644 --- a/apps/expo/metro.config.js +++ b/apps/expo/metro.config.js @@ -1,23 +1,42 @@ // -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'), ] +// Singleton React because workspace packages (e.g. @auth/clerk → @clerk/clerk-react) can nest their own `node_modules/react`. +// A second copy could break hooks (e.g. if React Native uses the hoisted renderer + a different React version). +// By pinning the `react` + `react-dom` versions to the workspace root the entire expo app will always share 1 React instance. +const workspaceReact = path.resolve(workspaceRoot, 'node_modules/react') +const workspaceReactDom = path.resolve(workspaceRoot, 'node_modules/react-dom') +config.resolver.extraNodeModules = { + ...config.resolver.extraNodeModules, + react: workspaceReact, + 'react-dom': workspaceReactDom, +} + // 3. Force Metro to resolve (sub)dependencies only from the `nodeModulesPaths` // config.resolver.disableHierarchicalLookup = true +// 4. Use absolute path for NativeWind input so it resolves correctly (cwd can vary in monorepos) +const nativeWindInput = path.resolve(projectRoot, '../next/global.css') +const nativeWindConfigPath = path.resolve(projectRoot, 'tailwind.config.js') + // Export the modified config -module.exports = config +module.exports = withNativeWind(config, { + input: nativeWindInput, + configPath: nativeWindConfigPath, +}) 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..7d741c8 100644 --- a/apps/expo/package.json +++ b/apps/expo/package.json @@ -3,34 +3,48 @@ "version": "1.0.0", "private": true, "main": "index.js", + "expo": { + "install": { + "exclude": ["@types/react", "typescript"] + } + }, "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" + "@bacons/mdx": "~1.0.3", + "@expo/metro-runtime": "~6.1.2", + "expo": "~54.0.0", + "expo-constants": "~18.0.10", + "expo-image": "~3.0.10", + "expo-linking": "~8.0.8", + "expo-router": "~6.0.8", + "expo-status-bar": "~3.0.8", + "expo-system-ui": "~6.0.7", + "nativewind": "4.2.2", + "react": "19.1.0", + "react-dom": "19.1.0", + "react-native": "0.81.5", + "react-native-get-random-values": "^1.11.0", + "react-native-reanimated": "~4.1.1", + "react-native-safe-area-context": "~5.6.0", + "react-native-screens": "~4.16.0", + "react-native-web": "^0.21.0", + "react-native-webview": "13.15.0", + "react-native-worklets": "0.5.1" }, "devDependencies": { "@babel/core": "^7.19.3", "@expo/next-adapter": "^6.0.0", - "@types/react": "18.2.48", - "typescript": "5.3.3" + "@types/react": "~19.1.0" }, "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", + "typecheck": "tsc --noEmit --incremental false", + "dev": "npx expo start --clear", + "start": "npx expo start --clear", + "android": "npx expo start --clear --android", + "ios": "npx expo start --clear --ios", + "web": "npx expo start --clear --web", + "build": "npx expo export", + "doctor": "npx expo-doctor", + "add-dependencies": "npx expo install", "env:local": "cp .env.example .env.local" } } diff --git a/apps/expo/polyfills.js b/apps/expo/polyfills.js new file mode 100644 index 0000000..3909378 --- /dev/null +++ b/apps/expo/polyfills.js @@ -0,0 +1,13 @@ +// -i- Polyfills that must run before any other app code +// -i- This file is imported first in index.js + +/* --- Reanimated ------------------------------------------------------------------------------ */ +// -i- Reanimated v4 + react-native-worklets: the globalThis._toString polyfill that was used for +if (typeof globalThis._toString !== 'function') { + globalThis._toString = (value) => { + if (value === null) return 'null' + if (value === undefined) return 'undefined' + if (typeof value === 'object') return Object.prototype.toString.call(value) + return String(value) + } +} diff --git a/apps/expo/tailwind.config.js b/apps/expo/tailwind.config.js new file mode 100644 index 0000000..bc24d21 --- /dev/null +++ b/apps/expo/tailwind.config.js @@ -0,0 +1,17 @@ +const { universalTheme } = require('@app/ui/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}', + '!../../**/node_modules/**', + ], + 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..9b0d974 --- /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/demo/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..c9c92c7 --- /dev/null +++ b/apps/next/app/(generated)/demos/forms/page.tsx @@ -0,0 +1,2 @@ +"use client" +export { default, dynamic } from '@app/demo/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..3887b4e --- /dev/null +++ b/apps/next/app/(generated)/demos/images/page.tsx @@ -0,0 +1,2 @@ +"use client" +export { default } from '@app/demo/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..afae477 --- /dev/null +++ b/apps/next/app/(generated)/page.tsx @@ -0,0 +1,2 @@ +"use client" +export { default } from '@app/demo/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..d42dcfb --- /dev/null +++ b/apps/next/app/(generated)/subpages/[slug]/page.tsx @@ -0,0 +1,2 @@ +"use client" +export { default } from '@app/demo/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..22519d6 --- /dev/null +++ b/packages/@green-stack-core/components/WebView.web.tsx @@ -0,0 +1,11 @@ +import React from 'react' + +/* --- ------------------------------------------------------------------------------ */ + +export const WebView = (props: React.DetailedHTMLProps, HTMLIFrameElement>) => ( +