create-pilotprojects-app 0.1.0 → 0.2.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.
package/README.md
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# create-pilotprojects-app
|
|
2
|
+
|
|
3
|
+
Scaffold a production-ready monorepo in under a minute — Next.js 15 web app, Expo mobile app, or both, wired together with tRPC, Supabase, Drizzle ORM, and a shared design system.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# npm
|
|
9
|
+
npm create pilotprojects-app@latest
|
|
10
|
+
|
|
11
|
+
# pnpm
|
|
12
|
+
pnpm create pilotprojects-app
|
|
13
|
+
|
|
14
|
+
# yarn
|
|
15
|
+
yarn create pilotprojects-app
|
|
16
|
+
|
|
17
|
+
# With project name pre-filled
|
|
18
|
+
pnpm create pilotprojects-app my-project
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The CLI is interactive — it asks a few questions then scaffolds, installs dependencies, and runs `git init` automatically.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## What's included
|
|
26
|
+
|
|
27
|
+
### Core (always scaffolded)
|
|
28
|
+
|
|
29
|
+
| Package | Purpose |
|
|
30
|
+
| --------------------- | ------------------------------------------------------ |
|
|
31
|
+
| `@<scope>/api` | tRPC v11 routers with a feature-based service layer |
|
|
32
|
+
| `@<scope>/db` | Drizzle ORM + Supabase Postgres — migrations included |
|
|
33
|
+
| `@<scope>/auth` | Supabase Auth helpers for web (SSR cookies) and mobile |
|
|
34
|
+
| `@<scope>/validators` | Shared Zod schemas |
|
|
35
|
+
| `@<scope>/config` | Shared ESLint and TypeScript configs |
|
|
36
|
+
|
|
37
|
+
### Apps (your choice)
|
|
38
|
+
|
|
39
|
+
| App | Stack |
|
|
40
|
+
| ------------- | ----------------------------------------------------------------------------- |
|
|
41
|
+
| `apps/web` | Next.js 15 App Router · Tailwind CSS · shadcn/ui · tRPC client (RSC + client) |
|
|
42
|
+
| `apps/mobile` | Expo SDK 52 · React Native · NativeWind · tRPC HTTP client |
|
|
43
|
+
|
|
44
|
+
### Optional add-ons
|
|
45
|
+
|
|
46
|
+
| Feature | What it adds |
|
|
47
|
+
| -------------------- | -------------------------------------------------------------------------------------------- |
|
|
48
|
+
| **Design System** | `@<scope>/ui` — Tailwind preset, shadcn/ui components (web), react-native-reusables (mobile) |
|
|
49
|
+
| **Email** | `@<scope>/email` — Resend SDK + React Email templates |
|
|
50
|
+
| **Sentry** | Error monitoring configured for web and/or mobile |
|
|
51
|
+
| **PostHog** | Product analytics + feature flags |
|
|
52
|
+
| **Google Analytics** | `@next/third-parties` GA4 script in the web layout |
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Interactive prompts
|
|
57
|
+
|
|
58
|
+
| Prompt | Options | Default |
|
|
59
|
+
| ------------------------------ | ------------------- | ----------------- |
|
|
60
|
+
| Project name | Any lowercase slug | — |
|
|
61
|
+
| Package scope | `@<name>` | `@<project-name>` |
|
|
62
|
+
| Apps to scaffold | Web / Mobile / Both | Both |
|
|
63
|
+
| Environments | development / uat / production *(local always included)* | production |
|
|
64
|
+
| Include design system? | Yes / No | Yes |
|
|
65
|
+
| Include Sentry? | Yes / No | Yes |
|
|
66
|
+
| Include Resend (email)? | Yes / No | Yes |
|
|
67
|
+
| Include PostHog? | Yes / No | No |
|
|
68
|
+
| Google Analytics? _(web only)_ | Yes / No | No |
|
|
69
|
+
| Package manager | pnpm / npm / yarn | pnpm |
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## After scaffolding
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
cd my-project
|
|
77
|
+
|
|
78
|
+
# Copy and fill in environment files
|
|
79
|
+
cp apps/web/.env.development.example apps/web/.env.development
|
|
80
|
+
# Add: SUPABASE_URL, SUPABASE_ANON_KEY, SUPABASE_SERVICE_ROLE_KEY,
|
|
81
|
+
# DATABASE_URL, RESEND_API_KEY, SENTRY_DSN
|
|
82
|
+
|
|
83
|
+
# Start local Supabase (requires Supabase CLI)
|
|
84
|
+
supabase start
|
|
85
|
+
|
|
86
|
+
# Apply database migrations
|
|
87
|
+
pnpm --filter @repo/db db:migrate
|
|
88
|
+
|
|
89
|
+
# Start dev servers (web + mobile in parallel)
|
|
90
|
+
pnpm dev
|
|
91
|
+
# → web: http://localhost:3000
|
|
92
|
+
# → mobile: Expo DevTools
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Tech stack
|
|
98
|
+
|
|
99
|
+
| Layer | Choice |
|
|
100
|
+
| ----------- | ----------------------------------- |
|
|
101
|
+
| Monorepo | Turborepo + pnpm workspaces |
|
|
102
|
+
| Web | Next.js 15 (App Router) |
|
|
103
|
+
| Mobile | Expo SDK 52 / React Native 0.76 |
|
|
104
|
+
| API | tRPC v11 + React Query |
|
|
105
|
+
| Database | Drizzle ORM + Supabase Postgres |
|
|
106
|
+
| Auth | Supabase Auth |
|
|
107
|
+
| UI (web) | Tailwind CSS + shadcn/ui |
|
|
108
|
+
| UI (mobile) | NativeWind + react-native-reusables |
|
|
109
|
+
| Email | Resend + React Email |
|
|
110
|
+
| Validation | Zod |
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## License
|
|
115
|
+
|
|
116
|
+
MIT
|
package/dist/prompts.js
CHANGED
|
@@ -34,6 +34,16 @@ export async function runPrompts(projectNameArg) {
|
|
|
34
34
|
initialValues: ["web", "mobile"],
|
|
35
35
|
required: true,
|
|
36
36
|
}),
|
|
37
|
+
environments: () => p.multiselect({
|
|
38
|
+
message: "Which environments? (local is always included)",
|
|
39
|
+
options: [
|
|
40
|
+
{ value: "development", label: "Development", hint: "dev branch" },
|
|
41
|
+
{ value: "uat", label: "UAT", hint: "staging / QA" },
|
|
42
|
+
{ value: "production", label: "Production", hint: "live users" },
|
|
43
|
+
],
|
|
44
|
+
initialValues: ["production"],
|
|
45
|
+
required: true,
|
|
46
|
+
}),
|
|
37
47
|
designSystem: () => p.confirm({
|
|
38
48
|
message: "Include the default design system? (Tailwind + ShadCN web / NativeWind mobile)",
|
|
39
49
|
initialValue: true,
|
package/dist/scaffold.js
CHANGED
|
@@ -36,6 +36,7 @@ export async function scaffold(config, destDir) {
|
|
|
36
36
|
if (config.apps.includes("web")) {
|
|
37
37
|
spin.start("Scaffolding Next.js web app…");
|
|
38
38
|
await copyTemplate(path.join(TEMPLATES_DIR, "apps", "web"), path.join(destDir, "apps", "web"), vars);
|
|
39
|
+
await generateWebEnvFiles(path.join(destDir, "apps", "web"), config);
|
|
39
40
|
if (!config.sentry) {
|
|
40
41
|
await removeDeps(path.join(destDir, "apps", "web"), ["@sentry/nextjs"]);
|
|
41
42
|
await fs.remove(path.join(destDir, "apps", "web", "sentry.client.config.ts"));
|
|
@@ -54,6 +55,9 @@ export async function scaffold(config, destDir) {
|
|
|
54
55
|
if (config.apps.includes("mobile")) {
|
|
55
56
|
spin.start("Scaffolding Expo mobile app…");
|
|
56
57
|
await copyTemplate(path.join(TEMPLATES_DIR, "apps", "mobile"), path.join(destDir, "apps", "mobile"), vars);
|
|
58
|
+
await generateMobileEnvFiles(path.join(destDir, "apps", "mobile"), config);
|
|
59
|
+
await generateEasJson(path.join(destDir, "apps", "mobile"), config.environments);
|
|
60
|
+
await generateMobileAppConfig(path.join(destDir, "apps", "mobile"), config);
|
|
57
61
|
if (!config.sentry) {
|
|
58
62
|
await removeDeps(path.join(destDir, "apps", "mobile"), ["@sentry/react-native"]);
|
|
59
63
|
}
|
|
@@ -82,3 +86,186 @@ async function updateWorkspace(destDir, config) {
|
|
|
82
86
|
lines.push(' - "packages/*"');
|
|
83
87
|
await fs.outputFile(workspacePath, lines.join("\n") + "\n", "utf-8");
|
|
84
88
|
}
|
|
89
|
+
// ── Environment file generators ──────────────────────────────────────────────
|
|
90
|
+
const WEB_ENV_BASE = `# Supabase
|
|
91
|
+
DATABASE_URL=
|
|
92
|
+
SUPABASE_URL=
|
|
93
|
+
SUPABASE_ANON_KEY=
|
|
94
|
+
SUPABASE_SERVICE_ROLE_KEY=
|
|
95
|
+
NEXT_PUBLIC_SUPABASE_URL=
|
|
96
|
+
NEXT_PUBLIC_SUPABASE_ANON_KEY=
|
|
97
|
+
|
|
98
|
+
# Sentry (optional — leave empty to disable)
|
|
99
|
+
SENTRY_DSN=
|
|
100
|
+
SENTRY_AUTH_TOKEN=
|
|
101
|
+
NEXT_PUBLIC_SENTRY_DSN=
|
|
102
|
+
|
|
103
|
+
# PostHog (optional)
|
|
104
|
+
NEXT_PUBLIC_POSTHOG_KEY=
|
|
105
|
+
POSTHOG_KEY=
|
|
106
|
+
|
|
107
|
+
# Google Analytics (optional)
|
|
108
|
+
NEXT_PUBLIC_GA_MEASUREMENT_ID=
|
|
109
|
+
|
|
110
|
+
# App
|
|
111
|
+
APP_URL=`;
|
|
112
|
+
const MOBILE_ENV_BASE = `# Supabase
|
|
113
|
+
EXPO_PUBLIC_SUPABASE_URL=
|
|
114
|
+
EXPO_PUBLIC_SUPABASE_ANON_KEY=
|
|
115
|
+
|
|
116
|
+
# API (apps/web URL)
|
|
117
|
+
EXPO_PUBLIC_API_URL=
|
|
118
|
+
|
|
119
|
+
# Sentry (optional)
|
|
120
|
+
EXPO_PUBLIC_SENTRY_DSN=
|
|
121
|
+
|
|
122
|
+
# PostHog (optional)
|
|
123
|
+
EXPO_PUBLIC_POSTHOG_KEY=
|
|
124
|
+
`;
|
|
125
|
+
const WEB_APP_URL = {
|
|
126
|
+
development: "https://dev.yourapp.com",
|
|
127
|
+
uat: "https://uat.yourapp.com",
|
|
128
|
+
production: "https://yourapp.com",
|
|
129
|
+
};
|
|
130
|
+
const MOBILE_API_URL = {
|
|
131
|
+
development: "https://dev.yourapp.com",
|
|
132
|
+
uat: "https://uat.yourapp.com",
|
|
133
|
+
production: "https://yourapp.com",
|
|
134
|
+
};
|
|
135
|
+
async function generateWebEnvFiles(webDir, config) {
|
|
136
|
+
// .env.local.example is already copied from template (localhost values)
|
|
137
|
+
// generate one .env.{env}.example per selected non-local environment
|
|
138
|
+
for (const env of config.environments) {
|
|
139
|
+
const content = `${WEB_ENV_BASE}${WEB_APP_URL[env]}\n`;
|
|
140
|
+
await fs.outputFile(path.join(webDir, `.env.${env}.example`), content, "utf-8");
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
async function generateMobileEnvFiles(mobileDir, config) {
|
|
144
|
+
// .env.local.example is already copied from template (localhost values)
|
|
145
|
+
// generate one .env.{env}.example per selected non-local environment
|
|
146
|
+
for (const env of config.environments) {
|
|
147
|
+
const content = `APP_ENV=${env}\n\n${MOBILE_ENV_BASE}`;
|
|
148
|
+
const withUrl = content.replace("EXPO_PUBLIC_API_URL=", `EXPO_PUBLIC_API_URL=${MOBILE_API_URL[env]}`);
|
|
149
|
+
await fs.outputFile(path.join(mobileDir, `.env.${env}.example`), withUrl, "utf-8");
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
async function generateEasJson(mobileDir, environments) {
|
|
153
|
+
const build = {};
|
|
154
|
+
if (environments.includes("development")) {
|
|
155
|
+
build.development = {
|
|
156
|
+
developmentClient: true,
|
|
157
|
+
distribution: "internal",
|
|
158
|
+
channel: "development",
|
|
159
|
+
ios: { simulator: true },
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
if (environments.includes("uat")) {
|
|
163
|
+
build.uat = {
|
|
164
|
+
distribution: "internal",
|
|
165
|
+
channel: "uat",
|
|
166
|
+
ios: { buildConfiguration: "Release", credentialsSource: "remote" },
|
|
167
|
+
android: { buildType: "apk", credentialsSource: "remote" },
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
if (environments.includes("production")) {
|
|
171
|
+
build.production = {
|
|
172
|
+
distribution: "store",
|
|
173
|
+
channel: "production",
|
|
174
|
+
ios: { buildConfiguration: "Release", credentialsSource: "remote" },
|
|
175
|
+
android: { buildType: "app-bundle", credentialsSource: "remote" },
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
const submit = environments.includes("production")
|
|
179
|
+
? {
|
|
180
|
+
production: {
|
|
181
|
+
ios: { appleId: "", ascAppId: "", appleTeamId: "" },
|
|
182
|
+
android: { serviceAccountKeyPath: "./google-service-account.json", track: "internal" },
|
|
183
|
+
},
|
|
184
|
+
}
|
|
185
|
+
: {};
|
|
186
|
+
await fs.outputJSON(path.join(mobileDir, "eas.json"), { cli: { version: ">= 12.0.0" }, build, submit }, { spaces: 2 });
|
|
187
|
+
}
|
|
188
|
+
async function generateMobileAppConfig(mobileDir, config) {
|
|
189
|
+
const { projectName, packageScope, environments, sentry } = config;
|
|
190
|
+
const scopeSafe = packageScope.replace("@", "").replace("/", "-");
|
|
191
|
+
const allEnvs = ["local", ...environments];
|
|
192
|
+
const appEnvType = allEnvs.map((e) => `"${e}"`).join(" | ");
|
|
193
|
+
const defaultEnv = "local";
|
|
194
|
+
const envMeta = {
|
|
195
|
+
local: { label: " (Local)", bundleSuffix: ".local", apiUrl: "http://localhost:3000" },
|
|
196
|
+
development: { label: " (Dev)", bundleSuffix: ".dev", apiUrl: "https://dev.yourapp.com" },
|
|
197
|
+
uat: { label: " (UAT)", bundleSuffix: ".uat", apiUrl: "https://uat.yourapp.com" },
|
|
198
|
+
production: { label: "", bundleSuffix: "", apiUrl: "https://yourapp.com" },
|
|
199
|
+
};
|
|
200
|
+
const configEntries = allEnvs
|
|
201
|
+
.map((env) => {
|
|
202
|
+
const { label, bundleSuffix, apiUrl } = envMeta[env];
|
|
203
|
+
return ` ${env}: {
|
|
204
|
+
name: "${projectName}${label}",
|
|
205
|
+
bundleId: "com.yourcompany.${scopeSafe}${bundleSuffix}",
|
|
206
|
+
apiUrl: "${apiUrl}",
|
|
207
|
+
},`;
|
|
208
|
+
})
|
|
209
|
+
.join("\n");
|
|
210
|
+
const sentryPlugin = sentry ? `\n "@sentry/react-native/expo",` : "";
|
|
211
|
+
const content = `import type { ExpoConfig, ConfigContext } from "expo/config";
|
|
212
|
+
|
|
213
|
+
type AppEnv = ${appEnvType};
|
|
214
|
+
|
|
215
|
+
const ENV = (process.env.APP_ENV ?? "${defaultEnv}") as AppEnv;
|
|
216
|
+
|
|
217
|
+
const config: Record<AppEnv, { name: string; bundleId: string; apiUrl: string }> = {
|
|
218
|
+
${configEntries}
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const { name, bundleId, apiUrl } = config[ENV];
|
|
222
|
+
|
|
223
|
+
export default ({ config: base }: ConfigContext): ExpoConfig => ({
|
|
224
|
+
...base,
|
|
225
|
+
name,
|
|
226
|
+
slug: "${scopeSafe}",
|
|
227
|
+
version: "1.0.0",
|
|
228
|
+
orientation: "portrait",
|
|
229
|
+
scheme: "${scopeSafe}",
|
|
230
|
+
userInterfaceStyle: "automatic",
|
|
231
|
+
icon: "./assets/icon.png",
|
|
232
|
+
splash: {
|
|
233
|
+
image: "./assets/splash-icon.png",
|
|
234
|
+
resizeMode: "contain",
|
|
235
|
+
backgroundColor: "#ffffff",
|
|
236
|
+
},
|
|
237
|
+
ios: {
|
|
238
|
+
bundleIdentifier: bundleId,
|
|
239
|
+
supportsTablet: true,
|
|
240
|
+
},
|
|
241
|
+
android: {
|
|
242
|
+
package: bundleId,
|
|
243
|
+
adaptiveIcon: {
|
|
244
|
+
foregroundImage: "./assets/adaptive-icon.png",
|
|
245
|
+
backgroundColor: "#ffffff",
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
web: {
|
|
249
|
+
bundler: "metro",
|
|
250
|
+
output: "static",
|
|
251
|
+
favicon: "./assets/favicon.png",
|
|
252
|
+
},
|
|
253
|
+
plugins: [
|
|
254
|
+
"expo-router",${sentryPlugin}
|
|
255
|
+
],
|
|
256
|
+
newArchEnabled: true,
|
|
257
|
+
experiments: {
|
|
258
|
+
typedRoutes: true,
|
|
259
|
+
},
|
|
260
|
+
extra: {
|
|
261
|
+
apiUrl,
|
|
262
|
+
eas: { projectId: "YOUR_EAS_PROJECT_ID" },
|
|
263
|
+
},
|
|
264
|
+
updates: {
|
|
265
|
+
url: "https://u.expo.dev/YOUR_EAS_PROJECT_ID",
|
|
266
|
+
},
|
|
267
|
+
runtimeVersion: { policy: "sdkVersion" },
|
|
268
|
+
});
|
|
269
|
+
`;
|
|
270
|
+
await fs.outputFile(path.join(mobileDir, "app.config.ts"), content, "utf-8");
|
|
271
|
+
}
|
package/package.json
CHANGED
|
File without changes
|