anhedral 0.1.4 → 0.1.5
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.
- package/README.md +19 -9
- package/dist/dependencies.js +2 -4
- package/dist/scaffold.js +208 -33
- package/dist/templates/backend.js +4 -49
- package/dist/templates/extension.js +7 -4
- package/dist/templates/frontend.js +59 -15
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -51,7 +51,7 @@ anhedral init
|
|
|
51
51
|
- RevenueCat + Stripe
|
|
52
52
|
- Vercel deploy
|
|
53
53
|
|
|
54
|
-
The scaffold is a pnpm workspace with `
|
|
54
|
+
The scaffold is a pnpm workspace with top-level `Frontend`, `Backend`, `Extension`, and `packages/*` folders. Shared code such as the Drizzle schema lives under `packages/db` so apps do not need separate database schemas.
|
|
55
55
|
|
|
56
56
|
Each scaffold writes a `stack.json` file with the selected architecture, generated outputs, and the verified dependency manifest used for that run.
|
|
57
57
|
|
|
@@ -59,26 +59,36 @@ Each scaffold writes a `stack.json` file with the selected architecture, generat
|
|
|
59
59
|
|
|
60
60
|
The generated workspace is one Git repository with two Vercel projects:
|
|
61
61
|
|
|
62
|
-
- `
|
|
63
|
-
- `
|
|
62
|
+
- `Frontend`: Expo web build. Build command: `pnpm build:web`. Output directory: `dist`.
|
|
63
|
+
- `Backend`: Fastify API. Build command: `pnpm build`. Vercel detects `src/index.ts` as the Fastify entrypoint.
|
|
64
64
|
|
|
65
|
-
The `
|
|
65
|
+
The `Frontend` source is also the EAS source for iOS and Android builds; Vercel only deploys its web export.
|
|
66
66
|
|
|
67
67
|
Import the same repository twice in Vercel and set each project Root Directory to the app path above. Enable source access outside the app root so `packages/*` workspace dependencies are available during builds.
|
|
68
68
|
|
|
69
69
|
Vercel deployments are Git-driven. After both projects are connected, pushes and pull requests deploy the Expo web app and Fastify API automatically from GitHub commits.
|
|
70
70
|
|
|
71
|
+
Generated projects also include `PRODUCTION.md` with the provider checklist and these root verification scripts:
|
|
72
|
+
|
|
73
|
+
```sh
|
|
74
|
+
pnpm verify
|
|
75
|
+
pnpm verify:frontend
|
|
76
|
+
pnpm verify:backend
|
|
77
|
+
pnpm verify:extension
|
|
78
|
+
```
|
|
79
|
+
|
|
71
80
|
## Native App Deployment
|
|
72
81
|
|
|
73
|
-
Use `
|
|
82
|
+
Use `Frontend` as the Expo project root for iOS and Android. Vercel only deploys the web export from this app; native builds go through EAS.
|
|
74
83
|
|
|
75
84
|
```sh
|
|
76
|
-
cd
|
|
85
|
+
cd Frontend
|
|
77
86
|
pnpm dlx eas-cli@latest login
|
|
78
87
|
pnpm dlx eas-cli@latest init
|
|
79
|
-
pnpm dlx eas-cli@latest build:configure
|
|
80
88
|
```
|
|
81
89
|
|
|
90
|
+
The scaffold generates `Frontend/eas.json` with `development`, `preview`, and `production` profiles.
|
|
91
|
+
|
|
82
92
|
Build commands:
|
|
83
93
|
|
|
84
94
|
```sh
|
|
@@ -123,10 +133,10 @@ pnpm dlx eas-cli@latest credentials
|
|
|
123
133
|
|
|
124
134
|
## Extension Release
|
|
125
135
|
|
|
126
|
-
The WXT extension is built from `
|
|
136
|
+
The WXT extension is built from `Extension`. It is not deployed by Vercel.
|
|
127
137
|
|
|
128
138
|
```sh
|
|
129
|
-
cd
|
|
139
|
+
cd Extension
|
|
130
140
|
pnpm build
|
|
131
141
|
pnpm zip
|
|
132
142
|
```
|
package/dist/dependencies.js
CHANGED
|
@@ -91,8 +91,6 @@ export const BACKEND_DEPENDENCIES = {
|
|
|
91
91
|
vitest: '4.0.16',
|
|
92
92
|
// renovate: datasource=npm depName=@vitest/coverage-v8
|
|
93
93
|
'@vitest/coverage-v8': '4.0.16',
|
|
94
|
-
// renovate: datasource=npm depName=@vercel/node
|
|
95
|
-
'@vercel/node': '5.5.13',
|
|
96
94
|
// renovate: datasource=npm depName=eslint
|
|
97
95
|
eslint: '9.39.2',
|
|
98
96
|
// renovate: datasource=npm depName=@eslint/js
|
|
@@ -144,8 +142,8 @@ export const EXTENSION_DEPENDENCIES = {
|
|
|
144
142
|
};
|
|
145
143
|
export const FRONTEND_ADDON_DEPENDENCIES = {
|
|
146
144
|
'@anhedral/api-client': 'workspace:*',
|
|
147
|
-
// renovate: datasource=npm depName=@clerk/
|
|
148
|
-
'@clerk/
|
|
145
|
+
// renovate: datasource=npm depName=@clerk/expo
|
|
146
|
+
'@clerk/expo': '3.2.11',
|
|
149
147
|
// renovate: datasource=npm depName=react-native-purchases
|
|
150
148
|
'react-native-purchases': '10.1.1',
|
|
151
149
|
// renovate: datasource=npm depName=react-native-purchases-ui
|
package/dist/scaffold.js
CHANGED
|
@@ -147,16 +147,17 @@ ${generatedPaths}
|
|
|
147
147
|
\`\`\`bash
|
|
148
148
|
pnpm install
|
|
149
149
|
cp .env.example .env
|
|
150
|
-
#
|
|
151
|
-
cp
|
|
150
|
+
# Frontend/.env is generated for local Expo development
|
|
151
|
+
cp Extension/.env.example Extension/.env
|
|
152
152
|
pnpm db:generate
|
|
153
153
|
pnpm db:migrate
|
|
154
|
-
pnpm
|
|
154
|
+
pnpm verify
|
|
155
|
+
pnpm dev:backend
|
|
155
156
|
pnpm dev:frontend
|
|
156
157
|
pnpm dev:extension
|
|
157
158
|
\`\`\`
|
|
158
159
|
|
|
159
|
-
For a provider-free smoke test, keep \`ANHEDRAL_DEMO=true\` in \`
|
|
160
|
+
For a provider-free smoke test, keep \`ANHEDRAL_DEMO=true\` in \`Backend/.env\`. Demo mode returns a signed-in sample user and active subscription responses without Clerk, RevenueCat, or Stripe credentials. It is for local development only.
|
|
160
161
|
|
|
161
162
|
## Provider Setup
|
|
162
163
|
|
|
@@ -164,8 +165,8 @@ For a provider-free smoke test, keep \`ANHEDRAL_DEMO=true\` in \`apps/api/.env\`
|
|
|
164
165
|
- Clerk: create an application, configure allowed origins, and set \`CLERK_PUBLISHABLE_KEY\`, \`CLERK_SECRET_KEY\`, plus the Expo and extension publishable keys.
|
|
165
166
|
- RevenueCat + Stripe: create an entitlement named \`pro\`, configure Stripe as a RevenueCat web billing source, configure app keys, then set \`RC_SECRET_API_KEY\`, \`RC_WEBHOOK_SECRET\`, and the \`EXPO_PUBLIC_RC_*\` values.
|
|
166
167
|
- Cloudflare R2: create a bucket and API token, then set \`R2_ACCOUNT_ID\`, \`R2_ACCESS_KEY_ID\`, \`R2_SECRET_ACCESS_KEY\`, and \`R2_BUCKET\`.
|
|
167
|
-
- Vercel: import this same Git repository twice. Use \`
|
|
168
|
-
- EAS: use \`
|
|
168
|
+
- Vercel: import this same Git repository twice. Use \`Frontend\` as the root directory for the Expo web project, and \`Backend\` as the root directory for the Fastify API project. Enable access to source files outside each root directory so workspace packages under \`packages/*\` are available during builds. The Backend project uses Vercel Fastify entrypoint detection from \`Backend/src/index.ts\`.
|
|
169
|
+
- EAS: use \`Frontend\` as the Expo project root for iOS and Android builds. Vercel deploys the web export from the same source.
|
|
169
170
|
|
|
170
171
|
## Setup Notes
|
|
171
172
|
|
|
@@ -173,19 +174,184 @@ For a provider-free smoke test, keep \`ANHEDRAL_DEMO=true\` in \`apps/api/.env\`
|
|
|
173
174
|
2. Review \`install-skills.sh\` and run the listed skill commands manually so you can choose scope and agent targets.
|
|
174
175
|
3. Add the real provider configuration for Clerk, RevenueCat, Stripe web billing, Neon, and R2 where the scaffolded helper files indicate.
|
|
175
176
|
4. Use \`stack.json\` when debugging generated projects; it records the verified dependency manifest used by this init run.
|
|
176
|
-
5.
|
|
177
|
+
5. Review \`PRODUCTION.md\` before connecting live provider projects.
|
|
178
|
+
6. Run database generation and migrations from the shared DB package:
|
|
177
179
|
|
|
178
180
|
\`\`\`bash
|
|
179
181
|
pnpm db:generate
|
|
180
182
|
pnpm db:migrate
|
|
181
183
|
\`\`\`
|
|
182
184
|
|
|
183
|
-
|
|
185
|
+
Run deployment readiness checks before pushing:
|
|
184
186
|
|
|
185
187
|
\`\`\`bash
|
|
188
|
+
pnpm verify
|
|
189
|
+
pnpm verify:frontend
|
|
190
|
+
pnpm verify:backend
|
|
191
|
+
pnpm verify:extension
|
|
192
|
+
\`\`\`
|
|
193
|
+
|
|
194
|
+
7. Build or run the extension separately. It is not deployed by Vercel:
|
|
195
|
+
|
|
196
|
+
\`\`\`bash
|
|
197
|
+
pnpm dev:extension
|
|
198
|
+
pnpm extension:zip
|
|
199
|
+
\`\`\`
|
|
200
|
+
`);
|
|
201
|
+
}
|
|
202
|
+
function writeProductionGuide(root, options) {
|
|
203
|
+
writeFile(path.join(root, 'PRODUCTION.md'), `# ${options.displayName} Production Guide
|
|
204
|
+
|
|
205
|
+
## Project Structure
|
|
206
|
+
|
|
207
|
+
The generated app is one pnpm monorepo:
|
|
208
|
+
|
|
209
|
+
\`\`\`txt
|
|
210
|
+
.
|
|
211
|
+
├─ Frontend/ Expo + React Native Reusables app
|
|
212
|
+
├─ Backend/ Fastify API
|
|
213
|
+
├─ Extension/ WXT Chrome extension
|
|
214
|
+
├─ packages/
|
|
215
|
+
│ ├─ api-client/ shared typed API client
|
|
216
|
+
│ ├─ config/ shared config constants/helpers
|
|
217
|
+
│ ├─ db/ Drizzle schema, Neon client, migrations
|
|
218
|
+
│ └─ types/ shared TypeScript types
|
|
219
|
+
├─ PRODUCTION.md development and deployment guide
|
|
220
|
+
├─ turbo.json workspace build graph
|
|
221
|
+
└─ package.json root scripts
|
|
222
|
+
\`\`\`
|
|
223
|
+
|
|
224
|
+
## Frontend
|
|
225
|
+
|
|
226
|
+
\`Frontend\` is the single Expo source for:
|
|
227
|
+
|
|
228
|
+
- Web on Vercel
|
|
229
|
+
- iOS through EAS
|
|
230
|
+
- Android through EAS
|
|
231
|
+
|
|
232
|
+
Important files:
|
|
233
|
+
|
|
234
|
+
- \`Frontend/package.json\`: Expo scripts
|
|
235
|
+
- \`Frontend/vercel.json\`: Vercel web export config
|
|
236
|
+
- \`Frontend/eas.json\`: native build profiles
|
|
237
|
+
- \`Frontend/app\`: Expo Router screens
|
|
238
|
+
|
|
239
|
+
Build output goes to \`Frontend/dist\`.
|
|
240
|
+
|
|
241
|
+
## Backend
|
|
242
|
+
|
|
243
|
+
\`Backend\` is the Fastify API.
|
|
244
|
+
|
|
245
|
+
Important files:
|
|
246
|
+
|
|
247
|
+
- \`Backend/src/index.ts\`: Vercel Fastify entrypoint
|
|
248
|
+
- \`Backend/src/app.ts\`: Fastify app construction
|
|
249
|
+
- \`Backend/vercel.json\`: minimal Vercel config
|
|
250
|
+
- \`Backend/src/routes\`: health/auth/subscription routes
|
|
251
|
+
|
|
252
|
+
## Extension
|
|
253
|
+
|
|
254
|
+
\`Extension\` is the WXT Chrome extension using the Side Panel API.
|
|
255
|
+
|
|
256
|
+
Important files:
|
|
257
|
+
|
|
258
|
+
- \`Extension/wxt.config.ts\`: manifest, side panel, permissions
|
|
259
|
+
- \`Extension/src/entrypoints/sidepanel\`: side panel UI
|
|
260
|
+
- \`Extension/src/entrypoints/background.ts\`: side panel behavior
|
|
261
|
+
|
|
262
|
+
ZIP output goes to \`Extension/.output\`.
|
|
263
|
+
|
|
264
|
+
## Develop
|
|
265
|
+
|
|
266
|
+
From the repository root:
|
|
267
|
+
|
|
268
|
+
\`\`\`sh
|
|
269
|
+
pnpm install
|
|
270
|
+
pnpm dev
|
|
271
|
+
\`\`\`
|
|
272
|
+
|
|
273
|
+
Or run one surface at a time:
|
|
274
|
+
|
|
275
|
+
\`\`\`sh
|
|
276
|
+
pnpm dev:frontend
|
|
277
|
+
pnpm dev:backend
|
|
186
278
|
pnpm dev:extension
|
|
279
|
+
\`\`\`
|
|
280
|
+
|
|
281
|
+
Use these before deploy:
|
|
282
|
+
|
|
283
|
+
\`\`\`sh
|
|
284
|
+
pnpm verify
|
|
285
|
+
pnpm verify:frontend
|
|
286
|
+
pnpm verify:backend
|
|
287
|
+
pnpm verify:extension
|
|
288
|
+
\`\`\`
|
|
289
|
+
|
|
290
|
+
Database workflow:
|
|
291
|
+
|
|
292
|
+
\`\`\`sh
|
|
293
|
+
pnpm db:generate
|
|
294
|
+
pnpm db:migrate
|
|
295
|
+
\`\`\`
|
|
296
|
+
|
|
297
|
+
Local demo mode is enabled through generated \`.env\` files. Real provider behavior needs Clerk, RevenueCat/Stripe, Neon, and R2 credentials.
|
|
298
|
+
|
|
299
|
+
## Environment Variables
|
|
300
|
+
|
|
301
|
+
- Root/local: \`DATABASE_URL\` for shared Drizzle commands.
|
|
302
|
+
- Frontend/Vercel Frontend: only \`EXPO_PUBLIC_*\` values.
|
|
303
|
+
- Backend/Vercel Backend: server secrets such as \`DATABASE_URL\`, \`CLERK_SECRET_KEY\`, \`RC_SECRET_API_KEY\`, \`RC_WEBHOOK_SECRET\`, and R2 credentials.
|
|
304
|
+
- Extension: \`VITE_API_URL\`, \`VITE_CLERK_PUBLISHABLE_KEY\`, and optionally \`VITE_CRX_PUBLIC_KEY\`.
|
|
305
|
+
- EAS: add native app public keys with \`eas secret:create\` or the EAS dashboard.
|
|
306
|
+
|
|
307
|
+
## Deploy
|
|
308
|
+
|
|
309
|
+
Vercel deployment is two Vercel projects from the same Git repo:
|
|
310
|
+
|
|
311
|
+
1. Frontend Vercel project
|
|
312
|
+
- Root Directory: \`Frontend\`
|
|
313
|
+
- Build command: \`pnpm build:web\`
|
|
314
|
+
- Output directory: \`dist\`
|
|
315
|
+
|
|
316
|
+
2. Backend Vercel project
|
|
317
|
+
- Root Directory: \`Backend\`
|
|
318
|
+
- Build command: \`pnpm build\`
|
|
319
|
+
- Fastify entrypoint: \`src/index.ts\`
|
|
320
|
+
|
|
321
|
+
Enable Vercel access to source files outside each project root so \`packages/*\` workspace packages resolve.
|
|
322
|
+
|
|
323
|
+
Native app deployment:
|
|
324
|
+
|
|
325
|
+
\`\`\`sh
|
|
326
|
+
cd Frontend
|
|
327
|
+
pnpm dlx eas-cli@latest login
|
|
328
|
+
pnpm dlx eas-cli@latest init
|
|
329
|
+
pnpm dlx eas-cli@latest build --platform all --profile production
|
|
330
|
+
pnpm dlx eas-cli@latest submit --platform all --latest --profile production
|
|
331
|
+
\`\`\`
|
|
332
|
+
|
|
333
|
+
Chrome extension deployment:
|
|
334
|
+
|
|
335
|
+
\`\`\`sh
|
|
187
336
|
pnpm extension:zip
|
|
188
337
|
\`\`\`
|
|
338
|
+
|
|
339
|
+
Upload \`Extension/.output/*-chrome.zip\` to the Chrome Web Store Developer Dashboard.
|
|
340
|
+
|
|
341
|
+
## Provider Checklist
|
|
342
|
+
|
|
343
|
+
- Clerk: configure allowed origins for the Expo web Vercel domain, native redirect/deep-link settings for the Expo scheme in \`Frontend/app.json\`, and the Chrome extension origin after publishing or after assigning a stable extension key.
|
|
344
|
+
- RevenueCat + Stripe: create a \`pro\` entitlement, configure iOS/Android/Web app API keys separately, connect Stripe as the web billing source, set the RevenueCat webhook URL to \`https://<backend-domain>/webhooks/revenuecat\`, and keep \`RC_WEBHOOK_SECRET\` only in Backend.
|
|
345
|
+
- Neon + Drizzle: create the Neon database, set \`DATABASE_URL\` locally and in the Backend Vercel project, then run \`pnpm db:generate\` and \`pnpm db:migrate\` before production traffic.
|
|
346
|
+
- Cloudflare R2: create an R2 bucket and least-privilege API token, then set \`R2_ACCOUNT_ID\`, \`R2_ACCESS_KEY_ID\`, \`R2_SECRET_ACCESS_KEY\`, and \`R2_BUCKET\` in Backend only.
|
|
347
|
+
|
|
348
|
+
## Relevant Docs
|
|
349
|
+
|
|
350
|
+
- [Vercel monorepos](https://vercel.com/docs/monorepos)
|
|
351
|
+
- [Expo web publishing](https://docs.expo.dev/guides/publishing-websites/)
|
|
352
|
+
- [Expo EAS](https://docs.expo.dev/eas/)
|
|
353
|
+
- [WXT publishing](https://wxt.dev/guide/essentials/publishing.html)
|
|
354
|
+
- [Chrome Web Store publishing](https://developer.chrome.com/docs/webstore/publish)
|
|
189
355
|
`);
|
|
190
356
|
}
|
|
191
357
|
function writeStackFile(root, stack) {
|
|
@@ -755,9 +921,9 @@ R2_BUCKET=
|
|
|
755
921
|
${paymentsEnv}`);
|
|
756
922
|
}
|
|
757
923
|
function writeRootVercelFiles(root) {
|
|
758
|
-
writeFile(path.join(root, '.vercelignore'), `
|
|
759
|
-
|
|
760
|
-
|
|
924
|
+
writeFile(path.join(root, '.vercelignore'), `Extension/.output
|
|
925
|
+
Extension/.wxt
|
|
926
|
+
Extension/dist
|
|
761
927
|
`);
|
|
762
928
|
}
|
|
763
929
|
function writeGeneratedCiWorkflow(root) {
|
|
@@ -799,22 +965,22 @@ jobs:
|
|
|
799
965
|
run: pnpm install --frozen-lockfile
|
|
800
966
|
|
|
801
967
|
- name: Check Expo dependency alignment
|
|
802
|
-
run: pnpm --filter ./
|
|
968
|
+
run: pnpm --filter ./Frontend exec expo install --check
|
|
803
969
|
|
|
804
970
|
- name: Build Expo web
|
|
805
|
-
run: pnpm --filter ./
|
|
971
|
+
run: pnpm --filter ./Frontend build:web
|
|
806
972
|
|
|
807
973
|
- name: Test API
|
|
808
|
-
run: pnpm --filter ./
|
|
974
|
+
run: pnpm --filter ./Backend test
|
|
809
975
|
|
|
810
976
|
- name: Build API
|
|
811
|
-
run: pnpm --filter ./
|
|
977
|
+
run: pnpm --filter ./Backend build
|
|
812
978
|
|
|
813
979
|
- name: Build extension
|
|
814
|
-
run: pnpm --filter ./
|
|
980
|
+
run: pnpm --filter ./Extension build
|
|
815
981
|
|
|
816
982
|
- name: Zip extension
|
|
817
|
-
run: pnpm --filter ./
|
|
983
|
+
run: pnpm --filter ./Extension zip
|
|
818
984
|
|
|
819
985
|
- name: Build workspace
|
|
820
986
|
run: pnpm build
|
|
@@ -822,32 +988,36 @@ jobs:
|
|
|
822
988
|
}
|
|
823
989
|
function writeFullstackRootFiles(root, options) {
|
|
824
990
|
const appFilters = [
|
|
825
|
-
'./
|
|
826
|
-
'./
|
|
827
|
-
'./
|
|
991
|
+
'./Frontend',
|
|
992
|
+
'./Backend',
|
|
993
|
+
'./Extension',
|
|
828
994
|
];
|
|
829
995
|
const parallelFilters = appFilters.map((entry) => `--filter=${entry}`).join(' ');
|
|
830
996
|
const scripts = {
|
|
831
997
|
dev: `turbo dev --parallel ${parallelFilters}`,
|
|
832
|
-
'dev:
|
|
998
|
+
'dev:backend': 'pnpm --filter ./Backend dev',
|
|
833
999
|
build: 'turbo build',
|
|
834
1000
|
typecheck: 'turbo typecheck',
|
|
1001
|
+
verify: 'pnpm verify:frontend && pnpm verify:backend && pnpm verify:extension',
|
|
1002
|
+
'verify:frontend': 'pnpm --filter ./Frontend exec expo install --check && pnpm --filter ./Frontend build:web',
|
|
1003
|
+
'verify:backend': 'pnpm --filter ./Backend test && pnpm --filter ./Backend build',
|
|
1004
|
+
'verify:extension': 'pnpm --filter ./Extension typecheck && pnpm --filter ./Extension zip',
|
|
835
1005
|
'db:generate': 'pnpm --filter @anhedral/db db:generate',
|
|
836
1006
|
'db:migrate': 'pnpm --filter @anhedral/db db:migrate',
|
|
837
1007
|
};
|
|
838
|
-
scripts['dev:frontend'] = 'pnpm --filter ./
|
|
839
|
-
scripts['dev:extension'] = 'pnpm --filter ./
|
|
840
|
-
scripts['extension:zip'] = 'pnpm --filter ./
|
|
841
|
-
writeRootPackageJson(root, options.projectName, scripts, ['
|
|
1008
|
+
scripts['dev:frontend'] = 'pnpm --filter ./Frontend dev';
|
|
1009
|
+
scripts['dev:extension'] = 'pnpm --filter ./Extension dev';
|
|
1010
|
+
scripts['extension:zip'] = 'pnpm --filter ./Extension zip';
|
|
1011
|
+
writeRootPackageJson(root, options.projectName, scripts, ['Frontend', 'Backend', 'Extension', 'packages/*'], {
|
|
842
1012
|
devDependencies: ROOT_DEPENDENCIES.devDependencies,
|
|
843
1013
|
});
|
|
844
|
-
writePnpmWorkspace(root, ['
|
|
1014
|
+
writePnpmWorkspace(root, ['Frontend', 'Backend', 'Extension', 'packages/*']);
|
|
845
1015
|
writeFile(path.join(root, 'turbo.json'), JSON.stringify({
|
|
846
1016
|
$schema: 'https://turborepo.dev/schema.json',
|
|
847
1017
|
tasks: {
|
|
848
1018
|
build: {
|
|
849
1019
|
dependsOn: ['^build'],
|
|
850
|
-
outputs: [
|
|
1020
|
+
outputs: [],
|
|
851
1021
|
},
|
|
852
1022
|
typecheck: {
|
|
853
1023
|
dependsOn: ['^build'],
|
|
@@ -861,7 +1031,9 @@ function writeFullstackRootFiles(root, options) {
|
|
|
861
1031
|
appendGitignore(root, [
|
|
862
1032
|
'node_modules',
|
|
863
1033
|
'.turbo',
|
|
864
|
-
'
|
|
1034
|
+
'Frontend/node_modules',
|
|
1035
|
+
'Backend/node_modules',
|
|
1036
|
+
'Extension/node_modules',
|
|
865
1037
|
'packages/*/node_modules',
|
|
866
1038
|
...SHARED_ENV_GITIGNORE_LINES,
|
|
867
1039
|
...SHARED_TYPESCRIPT_GITIGNORE_LINES,
|
|
@@ -903,9 +1075,10 @@ async function scaffoldFullstack(root, options) {
|
|
|
903
1075
|
});
|
|
904
1076
|
return [
|
|
905
1077
|
'.github/workflows/ci.yml',
|
|
906
|
-
'
|
|
907
|
-
'
|
|
908
|
-
'
|
|
1078
|
+
'PRODUCTION.md',
|
|
1079
|
+
'Frontend',
|
|
1080
|
+
'Backend',
|
|
1081
|
+
'Extension',
|
|
909
1082
|
'packages/db',
|
|
910
1083
|
'packages/types',
|
|
911
1084
|
'packages/config',
|
|
@@ -927,6 +1100,7 @@ export async function scaffoldProject(options) {
|
|
|
927
1100
|
writeSkillsGuide(root, skillCommands);
|
|
928
1101
|
const stack = normalizeStack(options, generatedPaths);
|
|
929
1102
|
writeRootDocs(root, options, stack);
|
|
1103
|
+
writeProductionGuide(root, options);
|
|
930
1104
|
writeStackFile(root, stack);
|
|
931
1105
|
anhedralPrint.done('Project metadata written');
|
|
932
1106
|
console.log('');
|
|
@@ -938,9 +1112,10 @@ export async function scaffoldProject(options) {
|
|
|
938
1112
|
anhedralPrint.info('Next commands:');
|
|
939
1113
|
console.log(' pnpm install');
|
|
940
1114
|
console.log(' cp .env.example .env');
|
|
941
|
-
console.log(' cp
|
|
1115
|
+
console.log(' cp Extension/.env.example Extension/.env');
|
|
942
1116
|
console.log(' pnpm db:generate && pnpm db:migrate');
|
|
943
|
-
console.log(' pnpm
|
|
1117
|
+
console.log(' pnpm verify');
|
|
1118
|
+
console.log(' pnpm dev:backend');
|
|
944
1119
|
console.log(' pnpm dev:frontend');
|
|
945
1120
|
console.log(' pnpm dev:extension');
|
|
946
1121
|
console.log('');
|
|
@@ -4,7 +4,7 @@ import { anhedralPrint } from '../print.js';
|
|
|
4
4
|
import { getBackendInstallCommands } from '../commands.js';
|
|
5
5
|
import { BACKEND_DEPENDENCIES } from '../dependencies.js';
|
|
6
6
|
export async function scaffoldBackend(root, { projectName, displayName, frontendUrl, skipInstall }) {
|
|
7
|
-
const dir = path.join(root, '
|
|
7
|
+
const dir = path.join(root, 'Backend');
|
|
8
8
|
anhedralPrint.section('Backend (Fastify)');
|
|
9
9
|
anhedralPrint.step('Writing backend source files');
|
|
10
10
|
writePackageJson(dir, projectName);
|
|
@@ -24,7 +24,6 @@ export async function scaffoldBackend(root, { projectName, displayName, frontend
|
|
|
24
24
|
writeRepositories(dir);
|
|
25
25
|
writeRoutes(dir, displayName);
|
|
26
26
|
writeAppAndIndex(dir, displayName);
|
|
27
|
-
writeVercelEntry(dir);
|
|
28
27
|
writeVercelConfig(dir);
|
|
29
28
|
writeTestFiles(dir);
|
|
30
29
|
anhedralPrint.done('Backend source files written');
|
|
@@ -2174,55 +2173,11 @@ main().catch((err) => {
|
|
|
2174
2173
|
});
|
|
2175
2174
|
`);
|
|
2176
2175
|
}
|
|
2177
|
-
function writeVercelEntry(dir) {
|
|
2178
|
-
writeFile(path.join(dir, 'api/index.ts'), `import type { VercelRequest, VercelResponse } from '@vercel/node';
|
|
2179
|
-
import type { FastifyInstance } from 'fastify';
|
|
2180
|
-
import { buildApp } from '../src/app.js';
|
|
2181
|
-
|
|
2182
|
-
let appReadyPromise: Promise<FastifyInstance> | undefined;
|
|
2183
|
-
|
|
2184
|
-
async function getApp(): Promise<FastifyInstance> {
|
|
2185
|
-
if (!appReadyPromise) {
|
|
2186
|
-
appReadyPromise = (async () => {
|
|
2187
|
-
const app = await buildApp();
|
|
2188
|
-
await app.ready();
|
|
2189
|
-
return app;
|
|
2190
|
-
})().catch((err) => {
|
|
2191
|
-
appReadyPromise = undefined;
|
|
2192
|
-
throw err;
|
|
2193
|
-
});
|
|
2194
|
-
}
|
|
2195
|
-
return appReadyPromise;
|
|
2196
|
-
}
|
|
2197
|
-
|
|
2198
|
-
export default async function handler(req: VercelRequest, res: VercelResponse) {
|
|
2199
|
-
const app = await getApp();
|
|
2200
|
-
if (req.url === '/api' || req.url === '/backend') {
|
|
2201
|
-
req.url = '/';
|
|
2202
|
-
} else if (req.url?.startsWith('/api/')) {
|
|
2203
|
-
req.url = req.url.slice('/api'.length);
|
|
2204
|
-
} else if (req.url?.startsWith('/backend/')) {
|
|
2205
|
-
req.url = req.url.slice('/backend'.length);
|
|
2206
|
-
}
|
|
2207
|
-
app.server.emit('request', req, res);
|
|
2208
|
-
}
|
|
2209
|
-
`);
|
|
2210
|
-
}
|
|
2211
2176
|
function writeVercelConfig(dir) {
|
|
2212
2177
|
writeFile(path.join(dir, 'vercel.json'), JSON.stringify({
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
src: 'api/index.ts',
|
|
2217
|
-
use: '@vercel/node',
|
|
2218
|
-
},
|
|
2219
|
-
],
|
|
2220
|
-
routes: [
|
|
2221
|
-
{
|
|
2222
|
-
src: '/(.*)',
|
|
2223
|
-
dest: 'api/index.ts',
|
|
2224
|
-
},
|
|
2225
|
-
],
|
|
2178
|
+
$schema: 'https://openapi.vercel.sh/vercel.json',
|
|
2179
|
+
buildCommand: 'pnpm build',
|
|
2180
|
+
devCommand: 'vercel dev',
|
|
2226
2181
|
}, null, 2) + '\n');
|
|
2227
2182
|
}
|
|
2228
2183
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -5,13 +5,11 @@ import { anhedralPrint } from '../print.js';
|
|
|
5
5
|
import { EXTENSION_DEPENDENCIES } from '../dependencies.js';
|
|
6
6
|
import { resolveToolchainChannel, resolveToolchain, toolPackageRef } from '../toolchain.js';
|
|
7
7
|
export async function scaffoldExtension(root, { projectName, displayName, skipInstall }) {
|
|
8
|
-
const
|
|
9
|
-
const dir = path.join(appsRoot, 'extension');
|
|
8
|
+
const dir = path.join(root, 'Extension');
|
|
10
9
|
const toolchain = resolveToolchain(resolveToolchainChannel(process.env.ANHEDRAL_TOOLCHAIN));
|
|
11
10
|
anhedralPrint.section('Chrome extension (WXT)');
|
|
12
11
|
anhedralPrint.step('Scaffolding WXT extension');
|
|
13
|
-
|
|
14
|
-
exec(`pnpm dlx ${toolPackageRef('wxt', toolchain.wxt)} init extension -t react --pm pnpm`, appsRoot);
|
|
12
|
+
exec(`pnpm dlx --allow-build=esbuild --allow-build=spawn-sync ${toolPackageRef('wxt', toolchain.wxt)} init Extension -t react --pm pnpm`, root);
|
|
15
13
|
writePackageJson(dir, projectName);
|
|
16
14
|
anhedralPrint.done('WXT extension scaffolded');
|
|
17
15
|
anhedralPrint.step('Installing Clerk + React + Tailwind dependencies');
|
|
@@ -98,6 +96,11 @@ export default defineConfig({
|
|
|
98
96
|
};
|
|
99
97
|
},
|
|
100
98
|
modules: ['@wxt-dev/module-react'],
|
|
99
|
+
vite: () => ({
|
|
100
|
+
build: {
|
|
101
|
+
chunkSizeWarningLimit: 3000,
|
|
102
|
+
},
|
|
103
|
+
}),
|
|
101
104
|
});
|
|
102
105
|
`);
|
|
103
106
|
}
|
|
@@ -6,7 +6,7 @@ import { appendGitignore, writeFile, exec, execExpect, liftNestedProject } from
|
|
|
6
6
|
import { FRONTEND_ADDON_DEPENDENCIES, withVersions } from '../dependencies.js';
|
|
7
7
|
import { resolveToolchainChannel, resolveToolchain, toolPackageRef } from '../toolchain.js';
|
|
8
8
|
export async function scaffoldFrontend(root, { projectName, displayName, skipInstall }) {
|
|
9
|
-
const dir = path.join(root, '
|
|
9
|
+
const dir = path.join(root, 'Frontend');
|
|
10
10
|
const toolchain = resolveToolchain(resolveToolchainChannel(process.env.ANHEDRAL_TOOLCHAIN));
|
|
11
11
|
anhedralPrint.section('Frontend (Expo)');
|
|
12
12
|
anhedralPrint.step('Scaffolding Expo app with react-native-reusables');
|
|
@@ -21,6 +21,7 @@ export async function scaffoldFrontend(root, { projectName, displayName, skipIns
|
|
|
21
21
|
anhedralPrint.step('Installing frontend dependencies');
|
|
22
22
|
patchFrontendPackageJson(dir);
|
|
23
23
|
patchExpoAppConfig(dir);
|
|
24
|
+
patchFrontendTsConfig(dir);
|
|
24
25
|
if (!skipInstall) {
|
|
25
26
|
exec('pnpm install --no-frozen-lockfile', dir);
|
|
26
27
|
exec('pnpm exec expo install --fix --pnpm', dir);
|
|
@@ -36,13 +37,14 @@ export async function scaffoldFrontend(root, { projectName, displayName, skipIns
|
|
|
36
37
|
writeEnvExample(dir);
|
|
37
38
|
writeEnvFile(dir);
|
|
38
39
|
writeVercelConfig(dir);
|
|
40
|
+
writeEasConfig(dir);
|
|
39
41
|
writeAnhedralLogo(dir);
|
|
40
42
|
writeSubscriptionProvider(dir);
|
|
41
43
|
writeUseSubscription(dir);
|
|
42
44
|
anhedralPrint.step('Installing Expo native packages');
|
|
43
45
|
if (skipInstall) {
|
|
44
46
|
anhedralPrint.info('Skipping Expo native package install (--skip-install)');
|
|
45
|
-
anhedralPrint.info('Run after init: pnpm --filter ./
|
|
47
|
+
anhedralPrint.info('Run after init: pnpm --filter ./Frontend exec expo install --pnpm expo-image-picker');
|
|
46
48
|
}
|
|
47
49
|
else {
|
|
48
50
|
exec('pnpm exec expo install --pnpm expo-image-picker', dir);
|
|
@@ -56,7 +58,7 @@ export async function scaffoldFrontend(root, { projectName, displayName, skipIns
|
|
|
56
58
|
'@revenuecat/purchases-js': FRONTEND_ADDON_DEPENDENCIES['@revenuecat/purchases-js'],
|
|
57
59
|
});
|
|
58
60
|
if (skipInstall) {
|
|
59
|
-
anhedralPrint.info(`Skipping RevenueCat SDK install (--skip-install). Run after init: pnpm --filter ./
|
|
61
|
+
anhedralPrint.info(`Skipping RevenueCat SDK install (--skip-install). Run after init: pnpm --filter ./Frontend add ${revenueCatDependencies.join(' ')}`);
|
|
60
62
|
}
|
|
61
63
|
else {
|
|
62
64
|
exec(`pnpm add ${revenueCatDependencies.join(' ')}`, dir);
|
|
@@ -69,7 +71,7 @@ function patchFrontendPackageJson(dir) {
|
|
|
69
71
|
packageJson.dependencies = {
|
|
70
72
|
...(packageJson.dependencies ?? {}),
|
|
71
73
|
'@anhedral/api-client': FRONTEND_ADDON_DEPENDENCIES['@anhedral/api-client'],
|
|
72
|
-
'@clerk/
|
|
74
|
+
'@clerk/expo': FRONTEND_ADDON_DEPENDENCIES['@clerk/expo'],
|
|
73
75
|
'react-native-purchases': FRONTEND_ADDON_DEPENDENCIES['react-native-purchases'],
|
|
74
76
|
'react-native-purchases-ui': FRONTEND_ADDON_DEPENDENCIES['react-native-purchases-ui'],
|
|
75
77
|
'@revenuecat/purchases-js': FRONTEND_ADDON_DEPENDENCIES['@revenuecat/purchases-js'],
|
|
@@ -82,6 +84,17 @@ function patchFrontendPackageJson(dir) {
|
|
|
82
84
|
};
|
|
83
85
|
writeFile(filePath, JSON.stringify(packageJson, null, 2) + '\n');
|
|
84
86
|
}
|
|
87
|
+
function patchFrontendTsConfig(dir) {
|
|
88
|
+
const filePath = path.join(dir, 'tsconfig.json');
|
|
89
|
+
if (!fs.existsSync(filePath)) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const tsConfig = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
93
|
+
if (tsConfig.extends === 'expo/tsconfig.base') {
|
|
94
|
+
tsConfig.extends = 'expo/tsconfig.base.json';
|
|
95
|
+
}
|
|
96
|
+
writeFile(filePath, JSON.stringify(tsConfig, null, 2) + '\n');
|
|
97
|
+
}
|
|
85
98
|
function patchExpoAppConfig(dir) {
|
|
86
99
|
const filePath = path.join(dir, 'app.json');
|
|
87
100
|
if (!fs.existsSync(filePath)) {
|
|
@@ -94,6 +107,10 @@ function patchExpoAppConfig(dir) {
|
|
|
94
107
|
bundler: 'metro',
|
|
95
108
|
output: 'static',
|
|
96
109
|
};
|
|
110
|
+
appJson.expo.plugins = Array.from(new Set([
|
|
111
|
+
...(Array.isArray(appJson.expo.plugins) ? appJson.expo.plugins : []),
|
|
112
|
+
'@clerk/expo',
|
|
113
|
+
]));
|
|
97
114
|
writeFile(filePath, JSON.stringify(appJson, null, 2) + '\n');
|
|
98
115
|
}
|
|
99
116
|
function writeApiClient(dir) {
|
|
@@ -169,7 +186,7 @@ export function getPlatformRevenueCatApiKey(): string {
|
|
|
169
186
|
`);
|
|
170
187
|
}
|
|
171
188
|
function writeApiHook(dir) {
|
|
172
|
-
writeFile(path.join(dir, 'hooks/useAPI.ts'), `import { useAuth } from '@clerk/
|
|
189
|
+
writeFile(path.join(dir, 'hooks/useAPI.ts'), `import { useAuth } from '@clerk/expo';
|
|
173
190
|
import { useMemo } from 'react';
|
|
174
191
|
import { APIClient } from '@/api/client';
|
|
175
192
|
import { apiBaseUrl } from '@/lib/config';
|
|
@@ -191,13 +208,39 @@ function writeVercelConfig(dir) {
|
|
|
191
208
|
rewrites: [
|
|
192
209
|
{
|
|
193
210
|
source: '/:path*',
|
|
194
|
-
destination: '/
|
|
211
|
+
destination: '/',
|
|
195
212
|
},
|
|
196
213
|
],
|
|
197
214
|
}, null, 2) + '\n');
|
|
198
215
|
}
|
|
216
|
+
function writeEasConfig(dir) {
|
|
217
|
+
writeFile(path.join(dir, 'eas.json'), JSON.stringify({
|
|
218
|
+
cli: {
|
|
219
|
+
version: '>= 16.24.0',
|
|
220
|
+
appVersionSource: 'remote',
|
|
221
|
+
},
|
|
222
|
+
build: {
|
|
223
|
+
development: {
|
|
224
|
+
developmentClient: true,
|
|
225
|
+
distribution: 'internal',
|
|
226
|
+
android: {
|
|
227
|
+
buildType: 'apk',
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
preview: {
|
|
231
|
+
distribution: 'internal',
|
|
232
|
+
},
|
|
233
|
+
production: {
|
|
234
|
+
autoIncrement: true,
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
submit: {
|
|
238
|
+
production: {},
|
|
239
|
+
},
|
|
240
|
+
}, null, 2) + '\n');
|
|
241
|
+
}
|
|
199
242
|
function writeAccountHook(dir) {
|
|
200
|
-
writeFile(path.join(dir, 'hooks/useAccount.ts'), `import { useAuth } from '@clerk/
|
|
243
|
+
writeFile(path.join(dir, 'hooks/useAccount.ts'), `import { useAuth } from '@clerk/expo';
|
|
201
244
|
import * as React from 'react';
|
|
202
245
|
import { useAPI } from '@/hooks/useAPI';
|
|
203
246
|
|
|
@@ -295,7 +338,7 @@ import Purchases, { type CustomerInfo, type PurchasesOfferings } from 'react-nat
|
|
|
295
338
|
import { Purchases as PurchasesWeb } from '@revenuecat/purchases-js';
|
|
296
339
|
import { AppState, Platform } from 'react-native';
|
|
297
340
|
import { subscriptionConfig, getPlatformRevenueCatApiKey } from '@/lib/config';
|
|
298
|
-
import { useAuth, useUser } from '@clerk/
|
|
341
|
+
import { useAuth, useUser } from '@clerk/expo';
|
|
299
342
|
import { useAPI } from '@/hooks/useAPI';
|
|
300
343
|
import RevenueCatUI, { PAYWALL_RESULT } from 'react-native-purchases-ui';
|
|
301
344
|
|
|
@@ -697,9 +740,10 @@ function writeAppShellFiles(dir, displayName) {
|
|
|
697
740
|
writeFile(path.join(dir, 'app/_layout.tsx'), `import '@/global.css';
|
|
698
741
|
|
|
699
742
|
import { SubscriptionProvider } from '@/contexts/SubscriptionProvider';
|
|
743
|
+
import { clerkPublishableKey } from '@/lib/config';
|
|
700
744
|
import { NAV_THEME } from '@/lib/theme';
|
|
701
|
-
import { ClerkProvider, useAuth } from '@clerk/
|
|
702
|
-
import { tokenCache } from '@clerk/
|
|
745
|
+
import { ClerkProvider, useAuth } from '@clerk/expo';
|
|
746
|
+
import { tokenCache } from '@clerk/expo/token-cache';
|
|
703
747
|
import { ThemeProvider } from '@react-navigation/native';
|
|
704
748
|
import { PortalHost } from '@rn-primitives/portal';
|
|
705
749
|
import { Stack } from 'expo-router';
|
|
@@ -714,7 +758,7 @@ export default function RootLayout() {
|
|
|
714
758
|
const { colorScheme } = useColorScheme();
|
|
715
759
|
|
|
716
760
|
return (
|
|
717
|
-
<ClerkProvider tokenCache={tokenCache}>
|
|
761
|
+
<ClerkProvider publishableKey={clerkPublishableKey} tokenCache={tokenCache}>
|
|
718
762
|
<ThemeProvider value={NAV_THEME[colorScheme ?? 'light']}>
|
|
719
763
|
<SubscriptionProvider>
|
|
720
764
|
<StatusBar style={colorScheme === 'dark' ? 'light' : 'dark'} />
|
|
@@ -763,7 +807,7 @@ function Routes() {
|
|
|
763
807
|
import { Button } from '@/components/ui/button';
|
|
764
808
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
|
765
809
|
import { Text } from '@/components/ui/text';
|
|
766
|
-
import { useAuth } from '@clerk/
|
|
810
|
+
import { useAuth } from '@clerk/expo';
|
|
767
811
|
import { Link } from 'expo-router';
|
|
768
812
|
import { CloudIcon, CreditCardIcon, ShieldCheckIcon, DatabaseIcon } from 'lucide-react-native';
|
|
769
813
|
import { ScrollView, View } from 'react-native';
|
|
@@ -1103,7 +1147,7 @@ import { Input } from '@/components/ui/input';
|
|
|
1103
1147
|
import { Label } from '@/components/ui/label';
|
|
1104
1148
|
import { Separator } from '@/components/ui/separator';
|
|
1105
1149
|
import { Text } from '@/components/ui/text';
|
|
1106
|
-
import { useSignIn } from '@clerk/
|
|
1150
|
+
import { useSignIn } from '@clerk/expo/legacy';
|
|
1107
1151
|
import { Link } from 'expo-router';
|
|
1108
1152
|
import * as React from 'react';
|
|
1109
1153
|
import { type TextInput, View } from 'react-native';
|
|
@@ -1217,7 +1261,7 @@ import { Input } from '@/components/ui/input';
|
|
|
1217
1261
|
import { Label } from '@/components/ui/label';
|
|
1218
1262
|
import { Separator } from '@/components/ui/separator';
|
|
1219
1263
|
import { Text } from '@/components/ui/text';
|
|
1220
|
-
import { useSignUp } from '@clerk/
|
|
1264
|
+
import { useSignUp } from '@clerk/expo/legacy';
|
|
1221
1265
|
import { Link, router } from 'expo-router';
|
|
1222
1266
|
import * as React from 'react';
|
|
1223
1267
|
import { TextInput, View } from 'react-native';
|
|
@@ -1321,7 +1365,7 @@ import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover
|
|
|
1321
1365
|
import { Text } from '@/components/ui/text';
|
|
1322
1366
|
import { useAccount } from '@/hooks/useAccount';
|
|
1323
1367
|
import { useSubscription } from '@/hooks/useSubscription';
|
|
1324
|
-
import { useAuth } from '@clerk/
|
|
1368
|
+
import { useAuth } from '@clerk/expo';
|
|
1325
1369
|
import type { TriggerRef } from '@rn-primitives/popover';
|
|
1326
1370
|
import { CameraIcon, CreditCardIcon, CoinsIcon, LoaderCircleIcon, LogOutIcon } from 'lucide-react-native';
|
|
1327
1371
|
import * as React from 'react';
|