create-expo-boilerplate 0.0.0-stage → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (234) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +142 -2
  3. package/dist/generate.js +47 -0
  4. package/dist/index.js +254 -0
  5. package/dist/options.js +66 -0
  6. package/dist/steps/format.js +19 -0
  7. package/dist/steps/identity.js +27 -0
  8. package/dist/steps/javascript.js +185 -0
  9. package/dist/steps/navigation.js +20 -0
  10. package/dist/steps/packageManager.js +103 -0
  11. package/dist/steps/readme.js +96 -0
  12. package/dist/steps/state.js +35 -0
  13. package/dist/steps/styling.js +71 -0
  14. package/dist/steps/tooling.js +44 -0
  15. package/dist/template.js +73 -0
  16. package/dist/utils/exec.js +31 -0
  17. package/dist/utils/files.js +100 -0
  18. package/dist/utils/packageJson.js +49 -0
  19. package/dist/versions.js +19 -0
  20. package/overlays/react-navigation/index.ts +12 -0
  21. package/overlays/react-navigation/src/App.tsx +21 -0
  22. package/overlays/react-navigation/src/navigation/AuthNavigator.tsx +17 -0
  23. package/overlays/react-navigation/src/navigation/MainTabNavigator.tsx +43 -0
  24. package/overlays/react-navigation/src/navigation/RootNavigator.tsx +30 -0
  25. package/overlays/react-navigation/src/navigation/index.ts +8 -0
  26. package/overlays/react-navigation/src/navigation/linking.ts +29 -0
  27. package/overlays/react-navigation/src/navigation/types.ts +32 -0
  28. package/overlays/react-navigation/src/shared/constants/navigation.ts +10 -0
  29. package/overlays/react-navigation/src/shared/hooks/useBlockBackNavigation.ts +51 -0
  30. package/overlays/redux/common/src/features/auth/session.ts +14 -0
  31. package/overlays/redux/common/src/store/StoreProvider.tsx +8 -0
  32. package/overlays/redux/common/src/store/hooks.ts +9 -0
  33. package/overlays/redux/common/src/store/index.ts +30 -0
  34. package/overlays/redux/common/src/store/persistStorage.ts +22 -0
  35. package/overlays/redux/saga/src/features/auth/hooks.ts +33 -0
  36. package/overlays/redux/saga/src/features/auth/store/__tests__/authSaga.test.ts +41 -0
  37. package/overlays/redux/saga/src/features/auth/store/__tests__/authSlice.test.ts +42 -0
  38. package/overlays/redux/saga/src/features/auth/store/authSaga.ts +26 -0
  39. package/overlays/redux/saga/src/features/auth/store/authSlice.ts +47 -0
  40. package/overlays/redux/saga/src/store/rootSaga.ts +12 -0
  41. package/overlays/redux/saga/src/store/store.ts +38 -0
  42. package/overlays/redux/thunk/src/features/auth/hooks.ts +41 -0
  43. package/overlays/redux/thunk/src/features/auth/store/__tests__/authSlice.test.ts +57 -0
  44. package/overlays/redux/thunk/src/features/auth/store/authSlice.ts +55 -0
  45. package/overlays/redux/thunk/src/store/store.ts +30 -0
  46. package/overlays/styling/stylesheet/common/src/features/auth/screens/LoginScreen.tsx +127 -0
  47. package/overlays/styling/stylesheet/common/src/features/home/screens/HomeScreen.tsx +233 -0
  48. package/overlays/styling/stylesheet/common/src/features/settings/screens/SettingsScreen.tsx +116 -0
  49. package/overlays/styling/stylesheet/common/src/shared/components/Button.tsx +115 -0
  50. package/overlays/styling/stylesheet/common/src/shared/components/ControlledTextField.tsx +81 -0
  51. package/overlays/styling/stylesheet/common/src/shared/components/FullScreenLoader.tsx +50 -0
  52. package/overlays/styling/stylesheet/common/src/shared/components/ScreenWrapper.tsx +99 -0
  53. package/overlays/styling/stylesheet/common/src/shared/components/TabBarIcon.tsx +34 -0
  54. package/overlays/styling/stylesheet/common/src/shared/components/TextField.tsx +125 -0
  55. package/overlays/styling/stylesheet/common/src/shared/utils/toast.tsx +172 -0
  56. package/overlays/styling/stylesheet/common/src/theme/TextStyles.ts +100 -0
  57. package/overlays/styling/stylesheet/common/src/theme/index.ts +4 -0
  58. package/overlays/styling/stylesheet/common/src/theme/makeStyles.ts +26 -0
  59. package/overlays/styling/stylesheet/common/src/theme/useAppTheme.ts +16 -0
  60. package/overlays/styling/stylesheet/docs/snippet-screen.jsonc +29 -0
  61. package/overlays/styling/stylesheet/docs/ui-avoid-style.md +3 -0
  62. package/overlays/styling/stylesheet/docs/ui-avoid-theme.md +2 -0
  63. package/overlays/styling/stylesheet/docs/ui-checklist.md +3 -0
  64. package/overlays/styling/stylesheet/docs/ui-intro.md +1 -0
  65. package/overlays/styling/stylesheet/docs/ui-styling.md +79 -0
  66. package/overlays/styling/stylesheet/docs/ui-tree.md +2 -0
  67. package/overlays/styling/stylesheet/docs/ui-wrapper-example.md +5 -0
  68. package/overlays/styling/stylesheet/docs/ui-wrapper-layout-rule.md +1 -0
  69. package/overlays/styling/stylesheet/docs/ui-wrapper-style-prop.md +1 -0
  70. package/overlays/styling/stylesheet/expo-router/src/app/+not-found.tsx +39 -0
  71. package/overlays/styling/tailwind/common/global.css +92 -0
  72. package/overlays/styling/tailwind/common/metro.config.js +11 -0
  73. package/overlays/styling/tailwind/common/src/css.d.ts +2 -0
  74. package/overlays/styling/tailwind/common/src/features/auth/screens/LoginScreen.tsx +97 -0
  75. package/overlays/styling/tailwind/common/src/features/home/screens/HomeScreen.tsx +179 -0
  76. package/overlays/styling/tailwind/common/src/features/settings/screens/SettingsScreen.tsx +69 -0
  77. package/overlays/styling/tailwind/common/src/shared/components/Button.tsx +75 -0
  78. package/overlays/styling/tailwind/common/src/shared/components/ControlledTextField.tsx +68 -0
  79. package/overlays/styling/tailwind/common/src/shared/components/FullScreenLoader.tsx +34 -0
  80. package/overlays/styling/tailwind/common/src/shared/components/ScreenWrapper.tsx +84 -0
  81. package/overlays/styling/tailwind/common/src/shared/components/TabBarIcon.tsx +27 -0
  82. package/overlays/styling/tailwind/common/src/shared/components/TabBarLabel.tsx +26 -0
  83. package/overlays/styling/tailwind/common/src/shared/components/TextField.tsx +97 -0
  84. package/overlays/styling/tailwind/common/src/shared/utils/toast.tsx +142 -0
  85. package/overlays/styling/tailwind/common/src/theme/index.ts +2 -0
  86. package/overlays/styling/tailwind/common/src/theme/useAppTheme.ts +17 -0
  87. package/overlays/styling/tailwind/common/src/uniwind-types.d.ts +10 -0
  88. package/overlays/styling/tailwind/docs/snippet-screen.jsonc +19 -0
  89. package/overlays/styling/tailwind/docs/ui-avoid-style.md +3 -0
  90. package/overlays/styling/tailwind/docs/ui-avoid-theme.md +2 -0
  91. package/overlays/styling/tailwind/docs/ui-checklist.md +3 -0
  92. package/overlays/styling/tailwind/docs/ui-intro.md +1 -0
  93. package/overlays/styling/tailwind/docs/ui-styling.md +50 -0
  94. package/overlays/styling/tailwind/docs/ui-tree.md +2 -0
  95. package/overlays/styling/tailwind/docs/ui-wrapper-example.md +5 -0
  96. package/overlays/styling/tailwind/docs/ui-wrapper-layout-rule.md +1 -0
  97. package/overlays/styling/tailwind/docs/ui-wrapper-style-prop.md +1 -0
  98. package/overlays/styling/tailwind/expo-router/src/app/+not-found.tsx +26 -0
  99. package/package.json +47 -4
  100. package/template/.eas/workflows/build-fast.yml +127 -0
  101. package/template/.eas/workflows/build.yml +58 -0
  102. package/template/.eas/workflows/release.yml +107 -0
  103. package/template/.easignore +89 -0
  104. package/template/.env.example +47 -0
  105. package/template/.husky/commit-msg +6 -0
  106. package/template/.husky/common.sh +35 -0
  107. package/template/.husky/post-merge +15 -0
  108. package/template/.husky/pre-commit +22 -0
  109. package/template/.lintstagedrc +5 -0
  110. package/template/.prettierrc +9 -0
  111. package/template/.skills/api-hook-creation.md +411 -0
  112. package/template/.skills/localization.md +436 -0
  113. package/template/.skills/ui-creation.md +257 -0
  114. package/template/.vscode/extensions.json +15 -0
  115. package/template/.vscode/project.code-snippets +153 -0
  116. package/template/.vscode/settings.json +30 -0
  117. package/template/AGENTS.md +3 -0
  118. package/template/__mocks__/@env.ts +12 -0
  119. package/template/__mocks__/empty-module.js +2 -0
  120. package/template/_gitignore +94 -0
  121. package/template/app.config.ts +94 -0
  122. package/template/assets/adaptive-icon.png +0 -0
  123. package/template/assets/favicon.png +0 -0
  124. package/template/assets/icon.png +0 -0
  125. package/template/assets/splash.png +0 -0
  126. package/template/babel.config.js +44 -0
  127. package/template/cliff.toml +49 -0
  128. package/template/commitlint.config.js +8 -0
  129. package/template/eas.json +60 -0
  130. package/template/env.ts +124 -0
  131. package/template/eslint.config.js +214 -0
  132. package/template/index.ts +8 -0
  133. package/template/jest.config.js +31 -0
  134. package/template/jest.pre-setup.js +48 -0
  135. package/template/jest.setup.ts +37 -0
  136. package/template/load-env.ts +61 -0
  137. package/template/package.json +168 -0
  138. package/template/scripts/eas-pre-install.sh +34 -0
  139. package/template/scripts/repack.mjs +351 -0
  140. package/template/scripts/setup-env.mjs +175 -0
  141. package/template/src/app/(auth)/_layout.tsx +6 -0
  142. package/template/src/app/(auth)/login.tsx +1 -0
  143. package/template/src/app/(tabs)/_layout.tsx +29 -0
  144. package/template/src/app/(tabs)/index.tsx +1 -0
  145. package/template/src/app/(tabs)/settings.tsx +1 -0
  146. package/template/src/app/+html.tsx +47 -0
  147. package/template/src/app/+native-intent.tsx +3 -0
  148. package/template/src/app/+not-found.tsx +39 -0
  149. package/template/src/app/_layout.tsx +53 -0
  150. package/template/src/assets/fonts/OpenSans-Bold.ttf +0 -0
  151. package/template/src/assets/fonts/OpenSans-BoldItalic.ttf +0 -0
  152. package/template/src/assets/fonts/OpenSans-Italic.ttf +0 -0
  153. package/template/src/assets/fonts/OpenSans-Regular.ttf +0 -0
  154. package/template/src/assets/fonts/OpenSans-Semibold.ttf +0 -0
  155. package/template/src/assets/fonts/OpenSans-SemiboldItalic.ttf +0 -0
  156. package/template/src/assets/ic_home/ic_home.png +0 -0
  157. package/template/src/assets/ic_home/ic_home@2x.png +0 -0
  158. package/template/src/assets/ic_home/ic_home@3x.png +0 -0
  159. package/template/src/assets/ic_settings/ic_settings.png +0 -0
  160. package/template/src/assets/ic_settings/ic_settings@2x.png +0 -0
  161. package/template/src/assets/ic_settings/ic_settings@3x.png +0 -0
  162. package/template/src/assets/index.ts +2 -0
  163. package/template/src/declarations.d.ts +4 -0
  164. package/template/src/features/auth/api/authenticate.ts +17 -0
  165. package/template/src/features/auth/hooks.ts +37 -0
  166. package/template/src/features/auth/index.ts +6 -0
  167. package/template/src/features/auth/screens/LoginScreen.tsx +127 -0
  168. package/template/src/features/auth/session.ts +9 -0
  169. package/template/src/features/auth/store/__tests__/useUserStore.test.ts +56 -0
  170. package/template/src/features/auth/store/useUserStore.ts +33 -0
  171. package/template/src/features/auth/types.ts +11 -0
  172. package/template/src/features/home/index.ts +1 -0
  173. package/template/src/features/home/screens/HomeScreen.tsx +233 -0
  174. package/template/src/features/posts/api/types.ts +14 -0
  175. package/template/src/features/posts/api/use-add-post.ts +18 -0
  176. package/template/src/features/posts/api/use-infinite-posts.ts +39 -0
  177. package/template/src/features/posts/api/use-post.ts +20 -0
  178. package/template/src/features/posts/api/use-posts.ts +20 -0
  179. package/template/src/features/posts/index.ts +5 -0
  180. package/template/src/features/settings/index.ts +1 -0
  181. package/template/src/features/settings/screens/SettingsScreen.tsx +116 -0
  182. package/template/src/localization/i18n.ts +46 -0
  183. package/template/src/localization/react-i18next.d.ts +9 -0
  184. package/template/src/localization/resources.ts +17 -0
  185. package/template/src/localization/translations/ar.json +62 -0
  186. package/template/src/localization/translations/en.json +62 -0
  187. package/template/src/localization/translations/es.json +62 -0
  188. package/template/src/localization/types.ts +17 -0
  189. package/template/src/localization/utils.tsx +92 -0
  190. package/template/src/providers/AppProviders.tsx +38 -0
  191. package/template/src/providers/index.ts +2 -0
  192. package/template/src/providers/useAppBootstrap.ts +39 -0
  193. package/template/src/services/__tests__/utils.test.ts +41 -0
  194. package/template/src/services/api-provider.tsx +39 -0
  195. package/template/src/services/client.ts +394 -0
  196. package/template/src/services/index.ts +3 -0
  197. package/template/src/services/types.ts +23 -0
  198. package/template/src/services/utils.ts +48 -0
  199. package/template/src/shared/components/Button.tsx +136 -0
  200. package/template/src/shared/components/ControlledTextField.tsx +79 -0
  201. package/template/src/shared/components/FullScreenLoader.tsx +50 -0
  202. package/template/src/shared/components/ScreenWrapper.tsx +95 -0
  203. package/template/src/shared/components/TabBarIcon.tsx +35 -0
  204. package/template/src/shared/components/TabBarLabel.tsx +23 -0
  205. package/template/src/shared/components/TextField.tsx +125 -0
  206. package/template/src/shared/components/index.ts +5 -0
  207. package/template/src/shared/constants/ApiUrls.ts +4 -0
  208. package/template/src/shared/constants/deviceInfo.ts +11 -0
  209. package/template/src/shared/constants/index.ts +3 -0
  210. package/template/src/shared/constants/navigation.ts +6 -0
  211. package/template/src/shared/constants/status.ts +6 -0
  212. package/template/src/shared/hooks/index.ts +3 -0
  213. package/template/src/shared/hooks/useAppState.ts +45 -0
  214. package/template/src/shared/hooks/useBlockBackNavigation.ts +45 -0
  215. package/template/src/shared/hooks/useDebounce.ts +29 -0
  216. package/template/src/shared/utils/__tests__/scale.test.ts +46 -0
  217. package/template/src/shared/utils/index.ts +1 -0
  218. package/template/src/shared/utils/scale.ts +20 -0
  219. package/template/src/shared/utils/toast.tsx +170 -0
  220. package/template/src/storage/index.ts +60 -0
  221. package/template/src/storage/token.ts +48 -0
  222. package/template/src/store/StoreProvider.tsx +9 -0
  223. package/template/src/store/createPersistedStore.ts +62 -0
  224. package/template/src/store/index.ts +18 -0
  225. package/template/src/styles/breakpoints.ts +17 -0
  226. package/template/src/styles/themes.ts +80 -0
  227. package/template/src/styles/unistyles.ts +33 -0
  228. package/template/src/theme/TextStyles.ts +100 -0
  229. package/template/src/theme/fonts.ts +40 -0
  230. package/template/src/theme/index.ts +3 -0
  231. package/template/src/theme/useAppTheme.ts +10 -0
  232. package/template/tsconfig.json +28 -0
  233. package/template/tsconfig.test.json +17 -0
  234. package/template/yarn.lock +10767 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 EndLess728
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,143 @@
1
- # Temporary Holding Version
1
+ # create-expo-boilerplate
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Scaffold a new Expo app from [React-Native-Expo-Boilterplate](https://github.com/EndLess728/React-Native-Expo-Boilterplate) — no cloning, no deleting example code by hand.
4
+
5
+ ```bash
6
+ npx create-expo-boilerplate my-app
7
+ # or
8
+ yarn create expo-boilerplate my-app
9
+ pnpm create expo-boilerplate my-app
10
+ bun create expo-boilerplate my-app
11
+ ```
12
+
13
+ It asks a few questions, writes the project, installs dependencies, sets up git (and Husky), and makes the first commit.
14
+
15
+ ## Options
16
+
17
+ | Prompt | Choices | Default | Flag |
18
+ |---|---|---|---|
19
+ | State management | Zustand · Redux Toolkit | Zustand | `--state zustand\|redux` |
20
+ | Redux side effects (Redux only) | Thunk · Saga | Thunk | `--redux-middleware thunk\|saga` |
21
+ | Navigation | Expo Router · React Navigation | Expo Router | `--navigation expo-router\|react-navigation` |
22
+ | Styling | Unistyles · StyleSheet · Tailwind (Uniwind) | Unistyles | `--styling unistyles\|stylesheet\|tailwind` |
23
+ | Language | TypeScript · JavaScript | TypeScript | `--language ts\|js` (or `--js`) |
24
+ | ESLint | yes · no | yes | `--eslint` / `--no-eslint` |
25
+ | Husky git hooks | yes · no | yes | `--husky` / `--no-husky` |
26
+ | App display name | — | from the folder name | `--name "My App"` |
27
+ | Bundle id / Android package | — | `com.<name>` | `--bundle-id com.company.app` |
28
+ | Package manager | yarn · npm · pnpm · bun | whatever ran the CLI | `--pm` |
29
+
30
+ Other flags: `--yes` (take defaults for anything not passed), `--no-install`, `--no-git`.
31
+
32
+ ```bash
33
+ # Fully non-interactive
34
+ npx create-expo-boilerplate my-app --yes \
35
+ --state redux --redux-middleware saga \
36
+ --navigation react-navigation --styling tailwind --js --no-husky
37
+ ```
38
+
39
+ ### Styling
40
+
41
+ | Option | Styles are written with | Extra packages |
42
+ |---|---|---|
43
+ | Unistyles (default) | `StyleSheet.create((theme, rt) => …)` from `react-native-unistyles` | `react-native-unistyles` |
44
+ | StyleSheet | React Native's `StyleSheet`; `makeStyles((theme) => …)` from `@/theme` for theme-aware styles | none |
45
+ | Tailwind | `className` via [Uniwind](https://uniwind.dev/) (Tailwind v4); theme colors, fonts and text roles in `global.css`, plus `metro.config.js` | `uniwind`, `tailwindcss` |
46
+
47
+ Every option has the same screens and the same light/dark theme tokens (`src/styles/themes.ts`), and exposes `useAppTheme()` from `@/theme` for colors that must be passed as props (icon colors, tab bar tints, `placeholderTextColor`). The project's `.skills/ui-creation.md` and the `rnfu` VS Code snippet are rewritten for the option you pick.
48
+
49
+ Every combination uses the same feature-based layout (`src/features/<feature>`, `src/shared`, …). Screens only talk to state through the auth feature's hooks (`useIsLoggedIn`, `useLogin`, …), which is why switching the state library or the navigator doesn't touch a single screen.
50
+
51
+ ## How it works
52
+
53
+ The repo root **is** the template: the Expo Router + Zustand + TypeScript app with ESLint and Husky. Nobody maintains a second copy.
54
+
55
+ ```
56
+ cli/
57
+ ├── src/
58
+ │ ├── index.ts # prompts + flags (commander, @clack/prompts)
59
+ │ ├── generate.ts # runs the steps below in order
60
+ │ ├── template.ts # copies the repo root (or the packaged template/)
61
+ │ └── steps/
62
+ │ ├── navigation.ts # React Navigation: drop src/app, add the overlay, edit app.config + deps
63
+ │ ├── styling.ts # StyleSheet / Tailwind: strip Unistyles, add the styling overlay, swap docs
64
+ │ ├── state.ts # Redux: drop the Zustand store, add redux/common + thunk|saga overlays
65
+ │ ├── tooling.ts # remove ESLint / Husky (+ lint-staged, Commitlint)
66
+ │ ├── identity.ts # name, slug, scheme, bundle id
67
+ │ ├── packageManager.ts # lockfile, `yarn …` mentions, eas.json, .npmrc
68
+ │ ├── javascript.ts # TS → JS with the TypeScript compiler, last
69
+ │ ├── readme.ts # README for the exact stack chosen
70
+ │ └── format.ts # Prettier with the project's .prettierrc
71
+ ├── overlays/ # files that differ per option, written in TypeScript
72
+ │ ├── react-navigation/
73
+ │ ├── redux/{common,thunk,saga}/
74
+ │ └── styling/{stylesheet,tailwind}/
75
+ │ ├── common/ # styled components, screens, theme hooks (+ global.css, metro.config.js)
76
+ │ ├── expo-router/ # route files that only exist with Expo Router
77
+ │ └── docs/ # replacement sections for .skills/ui-creation.md and the screen snippet
78
+ └── scripts/
79
+ ├── sync-template.mjs # copies the repo root into template/ when packing
80
+ └── test-matrix.mjs # generates + checks every combination
81
+ ```
82
+
83
+ - **Overlays are TypeScript only.** The JavaScript option transpiles the finished project with the TypeScript compiler (types erased, JSX untouched, blank lines kept), deletes files that only held types, and Prettier formats the result. There's no separate JS template to maintain.
84
+ - **Edits fail loudly.** Every string edit throws if its target text is missing, so if the boilerplate changes in a way a step depends on, generation (and CI) fails instead of shipping a half-converted project.
85
+ - **Region markers.** Option-specific blocks in the template are wrapped in `// @cli:<name>-start` … `// @cli:<name>-end` (or `<!-- @cli:… -->` in Markdown). The CLI either strips the markers and keeps the block, or replaces it:
86
+ - `zustand` — the Zustand-only lint rule in `eslint.config.js`, swapped for the typed-Redux-hooks rule.
87
+ - `unistyles` — Unistyles-only lines in `index.ts`, `babel.config.js`, `eslint.config.js` and `src/styles/themes.ts`, removed for StyleSheet / Tailwind.
88
+ - `ui-*` and `snippet-screen` — styling sections of `.skills/ui-creation.md` and `.vscode/project.code-snippets`, replaced from `overlays/styling/<option>/docs/`.
89
+
90
+ The test matrix fails if a marker survives into a generated project.
91
+
92
+ ## Developing
93
+
94
+ ```bash
95
+ cd cli
96
+ npm install
97
+ npm run build
98
+ node dist/index.js ../../my-test-app # generates from the current repo root
99
+ ```
100
+
101
+ Run from a checkout, the CLI copies the boilerplate straight from the repo root, so changes there show up in generated apps right away.
102
+
103
+ ### Test every combination
104
+
105
+ ```bash
106
+ npm run test:matrix # 26 core combinations
107
+ npm run test:matrix -- --all # all 144
108
+ npm run test:matrix -- --export # also bundle each with `expo export` (slow)
109
+ npm run test:matrix -- --only redux-saga.react-navigation.tailwind
110
+ ```
111
+
112
+ The script generates each project into your temp dir, installs **one** shared superset of dependencies, and checks each project for leftovers (stray markers, Zustand imports in Redux apps, Unistyles in StyleSheet / Tailwind apps, …), circular imports (madge), `tsc`, ESLint (or Prettier when ESLint is off), Jest, and `expo config`. CI runs the core set on every PR that touches the app or the CLI.
113
+
114
+ ### Adding an option
115
+
116
+ 1. Add the choice to `options.ts`, a prompt + flag in `index.ts`, and a step in `steps/` (or extend one).
117
+ 2. Put files that differ in `overlays/<option>/`, in TypeScript, mirroring project paths.
118
+ 3. Pin any new packages in `versions.ts`.
119
+ 4. Add the dimension to `scripts/test-matrix.mjs` and run it.
120
+
121
+ ### Upgrading the Expo SDK
122
+
123
+ Upgrade the boilerplate at the repo root as usual, then bump the overlay-only packages in `src/versions.ts` (React Navigation, Redux, Uniwind / Tailwind), and run `npm run test:matrix -- --all --export`.
124
+
125
+ ## Publishing
126
+
127
+ Publishing is automatic: `.github/workflows/publish-cli.yml` publishes to npm on every push to `main`. It needs an `NPM_TOKEN` repository secret (an npm automation token, or a granular token with publish rights to this package).
128
+
129
+ - **The version comes from the commits**, the same way the GitHub release is versioned: git-cliff reads the Conventional Commits since the last `v*` tag (`feat` → minor, `fix` / `perf` → patch, `!` or `BREAKING CHANGE` → major), and `v1.1.0` is published as `1.1.0`. The `version` in `package.json` is set on the CI runner only, so don't bump it by hand.
130
+ - **A push with nothing releasable** (only `chore` / `docs` / `ci` commits) resolves to a version that is already on npm, so the run skips the publish with a notice instead of failing.
131
+ - Packages are published with [npm provenance](https://docs.npmjs.com/generating-provenance-statements), linking each version to the workflow run that built it.
132
+
133
+ To publish by hand instead (for example, to test a release):
134
+
135
+ ```bash
136
+ cd cli
137
+ npm version 1.2.3 --no-git-tag-version
138
+ npm publish # prepack builds dist/ and syncs template/
139
+ ```
140
+
141
+ `prepack` compiles the CLI and copies the boilerplate into `template/`. npm drops `.gitignore` from tarballs, so it's stored as `_gitignore` and renamed back on generate. Check what ships with `npm pack --dry-run`.
142
+
143
+ To keep it private to the team, publish under a scope (`@your-org/create-expo-boilerplate`, run as `npm create @your-org/expo-boilerplate`) with restricted access.
@@ -0,0 +1,47 @@
1
+ import { STYLING_LABEL } from './options.js';
2
+ import { applyIdentity } from './steps/identity.js';
3
+ import { convertToJavaScript } from './steps/javascript.js';
4
+ import { applyNavigation } from './steps/navigation.js';
5
+ import { applyPackageManager } from './steps/packageManager.js';
6
+ import { writeReadme } from './steps/readme.js';
7
+ import { applyState } from './steps/state.js';
8
+ import { applyStyling } from './steps/styling.js';
9
+ import { applyTooling } from './steps/tooling.js';
10
+ import { formatProject } from './steps/format.js';
11
+ import { copyBaseTemplate } from './template.js';
12
+ import { ProjectFiles } from './utils/files.js';
13
+ import { PackageJsonEditor } from './utils/packageJson.js';
14
+ /**
15
+ * Writes the project to `options.targetDir`. Pure file generation — no
16
+ * installs, no git — so it is fast and easy to test.
17
+ *
18
+ * Order matters: every overlay is written in TypeScript, so the TS → JS pass
19
+ * runs after all of them, and formatting runs last.
20
+ */
21
+ export async function generateProject(options, onStep = () => { }) {
22
+ const files = new ProjectFiles(options.targetDir);
23
+ onStep('Copying the boilerplate');
24
+ await copyBaseTemplate(options.targetDir);
25
+ const pkg = await PackageJsonEditor.load(files);
26
+ onStep(`Setting up ${options.navigation === 'expo-router' ? 'Expo Router' : 'React Navigation'}`);
27
+ await applyNavigation(files, pkg, options);
28
+ onStep(`Setting up ${STYLING_LABEL[options.styling]}`);
29
+ await applyStyling(files, pkg, options);
30
+ onStep(options.state === 'zustand'
31
+ ? 'Setting up Zustand'
32
+ : `Setting up Redux Toolkit + ${options.reduxMiddleware === 'saga' ? 'Redux-Saga' : 'thunks'}`);
33
+ await applyState(files, pkg, options);
34
+ onStep('Configuring tooling');
35
+ await applyTooling(files, pkg, options);
36
+ onStep('Naming your app');
37
+ await applyIdentity(files, pkg, options);
38
+ await applyPackageManager(files, pkg, options);
39
+ if (options.language === 'js') {
40
+ onStep('Converting to JavaScript');
41
+ await convertToJavaScript(files, pkg, options);
42
+ }
43
+ await pkg.save();
44
+ await writeReadme(files, options);
45
+ onStep('Formatting');
46
+ await formatProject(files);
47
+ }
package/dist/index.js ADDED
@@ -0,0 +1,254 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync } from 'node:fs';
3
+ import { readdir } from 'node:fs/promises';
4
+ import { basename, join, relative, resolve } from 'node:path';
5
+ import * as p from '@clack/prompts';
6
+ import { Command, InvalidArgumentError } from 'commander';
7
+ import pc from 'picocolors';
8
+ import { generateProject } from './generate.js';
9
+ import { CHOICES, defaultBundleId, DEFAULTS, STYLING_LABEL, toDisplayName, toPackageName, validateBundleId, validateProjectName, } from './options.js';
10
+ import { detectPackageManager, PM } from './steps/packageManager.js';
11
+ import { PACKAGE_ROOT } from './template.js';
12
+ import { commandExists, run } from './utils/exec.js';
13
+ const { version } = JSON.parse(readFileSync(join(PACKAGE_ROOT, 'package.json'), 'utf8'));
14
+ const oneOf = (choices) => (value) => {
15
+ if (!choices.includes(value)) {
16
+ throw new InvalidArgumentError(`Expected one of: ${choices.join(', ')}.`);
17
+ }
18
+ return value;
19
+ };
20
+ const program = new Command()
21
+ .name('create-expo-boilerplate')
22
+ .description('Scaffold an Expo app from the React-Native-Expo-Boilterplate.')
23
+ .version(version)
24
+ .argument('[directory]', 'where to create the project')
25
+ .option('--state <library>', 'zustand | redux', oneOf(CHOICES.state))
26
+ .option('--redux-middleware <type>', 'thunk | saga (with --state redux)', oneOf(CHOICES.reduxMiddleware))
27
+ .option('--navigation <library>', 'expo-router | react-navigation', oneOf(CHOICES.navigation))
28
+ .option('--styling <library>', 'unistyles | stylesheet | tailwind', oneOf(CHOICES.styling))
29
+ .option('--language <lang>', 'ts | js', oneOf(CHOICES.language))
30
+ .option('--js', 'shorthand for --language js')
31
+ .option('--eslint', 'include ESLint')
32
+ .option('--no-eslint', 'skip ESLint')
33
+ .option('--husky', 'include Husky git hooks')
34
+ .option('--no-husky', 'skip Husky git hooks')
35
+ .option('--name <displayName>', 'app display name')
36
+ .option('--bundle-id <id>', 'iOS bundle identifier / Android package')
37
+ .option('--pm <manager>', 'yarn | npm | pnpm | bun', oneOf(CHOICES.packageManager))
38
+ .option('--no-install', 'skip installing dependencies')
39
+ .option('--no-git', 'skip git init and the initial commit')
40
+ .option('-y, --yes', 'accept defaults for every option not passed as a flag')
41
+ .addHelpText('after', `
42
+ Examples:
43
+ $ npx create-expo-boilerplate my-app
44
+ $ npx create-expo-boilerplate my-app --state redux --redux-middleware saga --navigation react-navigation
45
+ $ npx create-expo-boilerplate my-app --styling tailwind
46
+ $ npx create-expo-boilerplate my-app --yes --js --no-husky`);
47
+ function bail(value) {
48
+ if (p.isCancel(value)) {
49
+ p.cancel('Cancelled — nothing was created.');
50
+ process.exit(0);
51
+ }
52
+ }
53
+ async function ask(given, useDefault, fallback, prompt) {
54
+ if (given !== undefined)
55
+ return given;
56
+ if (useDefault)
57
+ return fallback;
58
+ const answer = await prompt();
59
+ bail(answer);
60
+ return answer;
61
+ }
62
+ async function isEmptyDir(dir) {
63
+ try {
64
+ const entries = await readdir(dir);
65
+ return entries.filter((e) => e !== '.DS_Store' && e !== '.git').length === 0;
66
+ }
67
+ catch {
68
+ return true; // doesn't exist yet
69
+ }
70
+ }
71
+ async function main() {
72
+ program.parse();
73
+ const [dirArg] = program.args;
74
+ const flags = program.opts();
75
+ const defaults = Boolean(flags.yes) || !process.stdout.isTTY;
76
+ console.log();
77
+ p.intro(pc.bgCyan(pc.black(' create-expo-boilerplate ')));
78
+ // ── Where ────────────────────────────────────────────────
79
+ const directory = await ask(dirArg, defaults, 'my-app', () => p.text({
80
+ message: 'Project name',
81
+ placeholder: 'my-app',
82
+ defaultValue: 'my-app',
83
+ validate: (v) => (v ? validateProjectName(v) : undefined),
84
+ }));
85
+ const targetDir = resolve(process.cwd(), directory);
86
+ const projectName = basename(targetDir);
87
+ if (validateProjectName(projectName)) {
88
+ p.cancel(validateProjectName(projectName));
89
+ process.exit(1);
90
+ }
91
+ if (!(await isEmptyDir(targetDir))) {
92
+ p.cancel(`${pc.bold(relative(process.cwd(), targetDir) || '.')} is not empty. Pick a new directory.`);
93
+ process.exit(1);
94
+ }
95
+ // ── Identity ─────────────────────────────────────────────
96
+ const displayName = await ask(flags.name, defaults, toDisplayName(projectName), () => p.text({
97
+ message: 'App display name',
98
+ initialValue: toDisplayName(projectName),
99
+ validate: (v) => (v?.trim() ? undefined : 'Display name is required.'),
100
+ }));
101
+ if (flags.bundleId && validateBundleId(flags.bundleId)) {
102
+ p.cancel(`--bundle-id: ${validateBundleId(flags.bundleId)}`);
103
+ process.exit(1);
104
+ }
105
+ const bundleId = await ask(flags.bundleId, defaults, defaultBundleId(projectName), () => p.text({
106
+ message: 'Bundle identifier / Android package',
107
+ initialValue: defaultBundleId(projectName),
108
+ validate: (v) => validateBundleId(v ?? ''),
109
+ }));
110
+ // ── Stack ────────────────────────────────────────────────
111
+ const state = await ask(flags.state, defaults, DEFAULTS.state, () => p.select({
112
+ message: 'State management',
113
+ initialValue: DEFAULTS.state,
114
+ options: [
115
+ { value: 'zustand', label: 'Zustand', hint: 'default — atomic stores, MMKV-persisted' },
116
+ { value: 'redux', label: 'Redux Toolkit', hint: 'slices + redux-persist on MMKV' },
117
+ ],
118
+ }));
119
+ let reduxMiddleware = null;
120
+ if (state === 'redux') {
121
+ reduxMiddleware = await ask(flags.reduxMiddleware, defaults, DEFAULTS.reduxMiddleware, () => p.select({
122
+ message: 'Redux side effects',
123
+ initialValue: DEFAULTS.reduxMiddleware,
124
+ options: [
125
+ { value: 'thunk', label: 'Thunk', hint: 'default — createAsyncThunk, built into RTK' },
126
+ { value: 'saga', label: 'Saga', hint: 'redux-saga generators' },
127
+ ],
128
+ }));
129
+ }
130
+ else if (flags.reduxMiddleware) {
131
+ p.log.warn('--redux-middleware is ignored without --state redux.');
132
+ }
133
+ const navigation = await ask(flags.navigation, defaults, DEFAULTS.navigation, () => p.select({
134
+ message: 'Navigation',
135
+ initialValue: DEFAULTS.navigation,
136
+ options: [
137
+ { value: 'expo-router', label: 'Expo Router', hint: 'default — file-based routing' },
138
+ { value: 'react-navigation', label: 'React Navigation', hint: 'navigators in code' },
139
+ ],
140
+ }));
141
+ const styling = await ask(flags.styling, defaults, DEFAULTS.styling, () => p.select({
142
+ message: 'Styling',
143
+ initialValue: DEFAULTS.styling,
144
+ options: [
145
+ { value: 'unistyles', label: 'Unistyles', hint: 'default — themed StyleSheet, light/dark' },
146
+ { value: 'stylesheet', label: 'StyleSheet', hint: 'plain React Native, no extra deps' },
147
+ { value: 'tailwind', label: 'Tailwind', hint: 'className via Uniwind (Tailwind v4)' },
148
+ ],
149
+ }));
150
+ const language = await ask(flags.js ? 'js' : flags.language, defaults, DEFAULTS.language, () => p.select({
151
+ message: 'Language',
152
+ initialValue: DEFAULTS.language,
153
+ options: [
154
+ { value: 'ts', label: 'TypeScript', hint: 'default' },
155
+ { value: 'js', label: 'JavaScript' },
156
+ ],
157
+ }));
158
+ const eslint = await ask(flags.eslint, defaults, DEFAULTS.eslint, () => p.confirm({ message: 'Include ESLint?', initialValue: DEFAULTS.eslint }));
159
+ const husky = await ask(flags.husky, defaults, DEFAULTS.husky, () => p.confirm({
160
+ message: 'Include Husky git hooks? (lint-staged + Commitlint)',
161
+ initialValue: DEFAULTS.husky,
162
+ }));
163
+ const detectedPm = detectPackageManager() ?? 'yarn';
164
+ const packageManager = await ask(flags.pm, defaults, detectedPm, () => p.select({
165
+ message: 'Package manager',
166
+ initialValue: detectedPm,
167
+ options: CHOICES.packageManager.map((pm) => ({ value: pm, label: pm })),
168
+ }));
169
+ const options = {
170
+ projectName,
171
+ targetDir,
172
+ displayName: displayName.trim(),
173
+ bundleId,
174
+ state,
175
+ reduxMiddleware,
176
+ navigation,
177
+ styling,
178
+ language,
179
+ eslint,
180
+ husky,
181
+ packageManager,
182
+ install: flags.install,
183
+ git: flags.git,
184
+ };
185
+ // ── Generate ─────────────────────────────────────────────
186
+ const s = p.spinner();
187
+ s.start('Generating project');
188
+ try {
189
+ await generateProject(options, (msg) => s.message(msg));
190
+ }
191
+ catch (error) {
192
+ s.error('Generation failed');
193
+ throw error;
194
+ }
195
+ s.stop(`Created ${pc.bold(toPackageName(projectName))}`);
196
+ const hasGit = options.git && (await commandExists('git'));
197
+ if (hasGit) {
198
+ // Before install, so Husky's `prepare` script can install the hooks.
199
+ await run('git', ['init', '-q', '-b', 'main'], targetDir).catch(() => run('git', ['init', '-q'], targetDir));
200
+ }
201
+ let installed = false;
202
+ if (options.install) {
203
+ if (await commandExists(packageManager)) {
204
+ s.start(`Installing dependencies with ${packageManager} — this takes a few minutes`);
205
+ try {
206
+ await run(packageManager, ['install'], targetDir);
207
+ installed = true;
208
+ s.stop('Dependencies installed');
209
+ }
210
+ catch (error) {
211
+ s.error(`${packageManager} install failed — run it yourself to see why`);
212
+ if (process.env.DEBUG)
213
+ console.error(error);
214
+ }
215
+ }
216
+ else {
217
+ p.log.warn(`${packageManager} isn't installed — skipping dependency install.`);
218
+ }
219
+ }
220
+ if (hasGit) {
221
+ try {
222
+ await run('git', ['add', '-A'], targetDir);
223
+ // --no-verify: the pre-commit hook blocks commits to main by design.
224
+ await run('git', ['commit', '-q', '--no-verify', '-m', 'chore: initial commit from create-expo-boilerplate'], targetDir);
225
+ }
226
+ catch {
227
+ p.log.warn('Skipped the initial commit (is git user.name / user.email set?).');
228
+ }
229
+ }
230
+ // ── Next steps ───────────────────────────────────────────
231
+ const cmd = PM[packageManager];
232
+ const rel = relative(process.cwd(), targetDir) || '.';
233
+ const steps = [
234
+ rel !== '.' ? `cd ${rel}` : null,
235
+ installed ? null : cmd.install,
236
+ '$EDITOR .env.development # add your API URLs',
237
+ `${cmd.run} android:development # or ios:development`,
238
+ ].filter(Boolean);
239
+ p.note(steps.join('\n'), 'Next steps');
240
+ p.outro(`${pc.dim('Stack:')} ${[
241
+ state === 'redux' ? `Redux Toolkit + ${reduxMiddleware}` : 'Zustand',
242
+ navigation === 'expo-router' ? 'Expo Router' : 'React Navigation',
243
+ STYLING_LABEL[styling],
244
+ language === 'ts' ? 'TypeScript' : 'JavaScript',
245
+ eslint ? 'ESLint' : null,
246
+ husky ? 'Husky' : null,
247
+ ]
248
+ .filter(Boolean)
249
+ .join(' · ')}`);
250
+ }
251
+ main().catch((error) => {
252
+ p.log.error(error instanceof Error ? error.message : String(error));
253
+ process.exit(1);
254
+ });
@@ -0,0 +1,66 @@
1
+ export const STYLING_LABEL = {
2
+ unistyles: 'Unistyles',
3
+ stylesheet: 'StyleSheet',
4
+ tailwind: 'Tailwind (Uniwind)',
5
+ };
6
+ export const DEFAULTS = {
7
+ state: 'zustand',
8
+ reduxMiddleware: 'thunk',
9
+ navigation: 'expo-router',
10
+ styling: 'unistyles',
11
+ language: 'ts',
12
+ eslint: true,
13
+ husky: true,
14
+ };
15
+ export const CHOICES = {
16
+ state: ['zustand', 'redux'],
17
+ reduxMiddleware: ['thunk', 'saga'],
18
+ navigation: ['expo-router', 'react-navigation'],
19
+ styling: ['unistyles', 'stylesheet', 'tailwind'],
20
+ language: ['ts', 'js'],
21
+ packageManager: ['yarn', 'npm', 'pnpm', 'bun'],
22
+ };
23
+ // ─────────────────────────────────────────────────────────────
24
+ // Name helpers
25
+ // ─────────────────────────────────────────────────────────────
26
+ /** `My Cool App` / `my_cool.app` → `my-cool-app` (valid npm name and Expo slug). */
27
+ export function toPackageName(input) {
28
+ return input
29
+ .trim()
30
+ .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
31
+ .toLowerCase()
32
+ .replace(/[^a-z0-9-]+/g, '-')
33
+ .replace(/^-+|-+$/g, '')
34
+ .replace(/-{2,}/g, '-');
35
+ }
36
+ /** `my-cool-app` → `My Cool App`. */
37
+ export function toDisplayName(input) {
38
+ return toPackageName(input)
39
+ .split('-')
40
+ .filter(Boolean)
41
+ .map((word) => word[0].toUpperCase() + word.slice(1))
42
+ .join(' ');
43
+ }
44
+ /** `my-cool-app` → `mycoolapp` (deep-link scheme, last bundle-id segment). */
45
+ export function toScheme(input) {
46
+ const scheme = toPackageName(input).replace(/-/g, '');
47
+ // A scheme / Java package segment must start with a letter.
48
+ return /^[a-z]/.test(scheme) ? scheme : `app${scheme}`;
49
+ }
50
+ export function defaultBundleId(projectName) {
51
+ return `com.${toScheme(projectName)}`;
52
+ }
53
+ const BUNDLE_ID_RE = /^[a-zA-Z][a-zA-Z0-9_]*(\.[a-zA-Z][a-zA-Z0-9_]*)+$/;
54
+ export function validateBundleId(value) {
55
+ if (!BUNDLE_ID_RE.test(value)) {
56
+ return 'Use reverse-DNS form with at least two segments, e.g. com.company.app (letters, digits, underscores).';
57
+ }
58
+ return undefined;
59
+ }
60
+ export function validateProjectName(value) {
61
+ if (!value.trim())
62
+ return 'Project name is required.';
63
+ if (!toPackageName(value))
64
+ return 'Project name needs at least one letter or digit.';
65
+ return undefined;
66
+ }
@@ -0,0 +1,19 @@
1
+ import * as prettier from 'prettier';
2
+ const CODE_RE = /\.(jsx?|tsx?|mjs|cjs)$/;
3
+ const JSON_FILES = ['package.json', 'eas.json', 'jsconfig.json', 'tsconfig.json'];
4
+ /**
5
+ * Runs Prettier with the project's own .prettierrc over code and the JSON
6
+ * files the CLI rewrote, so overlays, transpiled JS and edited configs match
7
+ * the house style. The base template is already formatted, so this is a no-op
8
+ * for files nobody touched.
9
+ */
10
+ export async function formatProject(files) {
11
+ const config = (await prettier.resolveConfig(files.abs('.prettierrc'))) ?? {};
12
+ const targets = (await files.list()).filter((rel) => (CODE_RE.test(rel) && !rel.startsWith('scripts/')) || JSON_FILES.includes(rel));
13
+ for (const rel of targets) {
14
+ const source = await files.read(rel);
15
+ const formatted = await prettier.format(source, { ...config, filepath: files.abs(rel) });
16
+ if (formatted !== source)
17
+ await files.write(rel, formatted);
18
+ }
19
+ }
@@ -0,0 +1,27 @@
1
+ import { toPackageName, toScheme } from '../options.js';
2
+ // Placeholder identity baked into the boilerplate.
3
+ const TEMPLATE = {
4
+ displayName: 'ExpoTemplate',
5
+ slug: 'ExpoTemplate',
6
+ scheme: 'expotemplate',
7
+ bundleId: 'com.expo.template',
8
+ };
9
+ /** Renames the app: package name, display names, slug, scheme and bundle id. */
10
+ export async function applyIdentity(files, pkg, options) {
11
+ const packageName = toPackageName(options.projectName);
12
+ const { displayName, bundleId } = options;
13
+ pkg.data.name = packageName;
14
+ await files.replace('app.config.ts', `slug: '${TEMPLATE.slug}'`, `slug: '${packageName}'`);
15
+ await files.replace('app.config.ts', `scheme: '${TEMPLATE.scheme}'`, `scheme: '${toScheme(options.projectName)}'`);
16
+ // env.ts — per-environment app names and the bundle id (incl. the
17
+ // commented multi-package example).
18
+ await files.replace('env.ts', `'${TEMPLATE.displayName} (Dev)'`, `'${displayName} (Dev)'`);
19
+ await files.replace('env.ts', `'${TEMPLATE.displayName} (Staging)'`, `'${displayName} (Staging)'`);
20
+ await files.replace('env.ts', `production: '${TEMPLATE.displayName}'`, `production: '${displayName}'`);
21
+ await files.replace('env.ts', TEMPLATE.bundleId, bundleId);
22
+ await files.replace('__mocks__/@env.ts', `'${TEMPLATE.displayName} (Dev)'`, `'${displayName} (Dev)'`);
23
+ await files.replace('__mocks__/@env.ts', `'${TEMPLATE.bundleId}'`, `'${bundleId}'`);
24
+ await files.replace('.env.example', '# reactnativetemplateapp env template', `# ${packageName} env template`, {
25
+ optional: true,
26
+ });
27
+ }