create-apollo-monorepo 0.8.0 → 0.9.1

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 (2) hide show
  1. package/index.mjs +294 -6
  2. package/package.json +1 -1
package/index.mjs CHANGED
@@ -367,8 +367,31 @@ function writeRootPackageJson(targetDir, dirName) {
367
367
  "pnpm predev:setup && pnpm --filter ./apps/backend exec next dev",
368
368
  "dev:cron": "pnpm --filter ./apps/backend exec bun scripts/dev-cron.ts",
369
369
  // Build pipeline: cms-plugins → apollo-cms's own plugins → backend → frontend.
370
+ // `prebuild` runs first (npm/pnpm convention) and fails fast if the
371
+ // backend's required env vars are missing.
372
+ prebuild: "node scripts/check-env.mjs",
370
373
  build:
371
374
  "pnpm cms-plugins:build && pnpm --filter ./apps/backend exec bun run plugins:build && pnpm --filter ./apps/backend build && pnpm --filter ./apps/frontend build",
375
+ "build:backend":
376
+ "pnpm cms-plugins:build && pnpm --filter ./apps/backend exec bun run plugins:build && pnpm --filter ./apps/backend build",
377
+ "build:frontend": "pnpm --filter ./apps/frontend build",
378
+ // Production start. Runs FE :3001 + BE :3000 in parallel via
379
+ // \`next start\`. Run \`pnpm backend:upgrade\` BEFORE this — start does
380
+ // NOT run migrations on boot (zero-downtime restart safety).
381
+ start:
382
+ "concurrently -k -n FE,BE -c blue,magenta \"pnpm --filter ./apps/frontend start\" \"pnpm --filter ./apps/backend exec next start\"",
383
+ // Single-origin production: FE + BE + Node reverse proxy on :3030.
384
+ "start:rp":
385
+ "concurrently -k -n FE,BE,PX -c blue,magenta,cyan \"pnpm --filter ./apps/frontend start\" \"pnpm --filter ./apps/backend exec next start\" \"pnpm start:proxy\"",
386
+ "start:proxy": "NODE_ENV=production node apps/proxy/server.mjs",
387
+ "start:frontend": "pnpm --filter ./apps/frontend start",
388
+ "start:backend": "pnpm --filter ./apps/backend exec next start",
389
+ // Self-hosted scheduler fallback. Vercel deploys use Vercel Cron via
390
+ // apps/backend/vercel.json — skip this on Vercel. For Docker / VPS
391
+ // you can either run \`pnpm start:cron\` alongside \`pnpm start\` (PM2
392
+ // handles this via ecosystem.config.cjs), or use system cron / k8s
393
+ // CronJob to hit /api/cron with CRON_SECRET.
394
+ "start:cron": "pnpm --filter ./apps/backend exec bun scripts/dev-cron.ts",
372
395
  "cms-plugins:build":
373
396
  "pnpm --filter './apps/cms-plugins/*' --parallel --if-present build",
374
397
  "cms-plugins:dev":
@@ -376,7 +399,13 @@ function writeRootPackageJson(targetDir, dirName) {
376
399
  "cms-plugin:new": "node scripts/new-cms-plugin.mjs",
377
400
  lint: "pnpm -r lint",
378
401
  typecheck: "pnpm -r typecheck",
379
- "backend:update": "git submodule update --remote --merge apps/backend",
402
+ // Stash any local changes inside the submodule so the fast-forward
403
+ // merge can't conflict, then update, then run `pnpm install` at the
404
+ // root so workspace deps pick up any package.json changes pulled in
405
+ // from the new apollo-cms commit. The stash is restored if it was
406
+ // created; otherwise this is a no-op.
407
+ "backend:update":
408
+ "node -e \"const {execSync:e}=require('child_process');const r=s=>e(s,{stdio:'inherit'});const has=e('git -C apps/backend status --porcelain',{encoding:'utf8'}).trim().length>0;if(has)r('git -C apps/backend stash push -u -m backend-update-auto');r('git submodule update --remote --merge apps/backend');r('pnpm install');if(has){try{r('git -C apps/backend stash pop');}catch(_){console.error('[backend:update] stash pop had conflicts — resolve manually with: git -C apps/backend stash list');}}\"",
380
409
  // `pnpm setup` is pnpm's CLI bootstrap built-in — must use `run` to
381
410
  // forward to the workspace's setup script (apollo-cms's db:push + db:seed).
382
411
  "backend:setup": "pnpm --filter ./apps/backend run setup",
@@ -1244,6 +1273,216 @@ console.log(\`Run: pnpm install && pnpm cms-plugins:build && pnpm dev:backend\`)
1244
1273
  writeFileSync(resolve(scriptsDir, "new-cms-plugin.mjs"), script);
1245
1274
  }
1246
1275
 
1276
+ // Pre-build env check. Fails the build before \`next build\` runs if backend
1277
+ // required vars are missing, so users don't sit through a 30s build only to
1278
+ // hit a runtime error on first request.
1279
+ function writeCheckEnvScript(targetDir) {
1280
+ const scriptsDir = resolve(targetDir, "scripts");
1281
+ mkdirSync(scriptsDir, { recursive: true });
1282
+ const script = `#!/usr/bin/env node
1283
+ // Pre-build sanity check. Reads apps/backend/.env.local and verifies that
1284
+ // required vars are present and non-empty. Skipped when SKIP_ENV_CHECK=1.
1285
+ //
1286
+ // In CI/Vercel where envs come from the platform (not files), set
1287
+ // SKIP_ENV_CHECK=1 and rely on the platform's own validation.
1288
+
1289
+ import { existsSync, readFileSync } from "node:fs";
1290
+ import { dirname, resolve } from "node:path";
1291
+ import { fileURLToPath } from "node:url";
1292
+
1293
+ if (process.env.SKIP_ENV_CHECK === "1") process.exit(0);
1294
+
1295
+ const REQUIRED = ["DATABASE_URL", "APOLLO_SECRET", "NEXT_PUBLIC_DEFAULT_LOCALE"];
1296
+
1297
+ const __dirname = dirname(fileURLToPath(import.meta.url));
1298
+ const envPath = resolve(__dirname, "../apps/backend/.env.local");
1299
+
1300
+ if (!existsSync(envPath)) {
1301
+ // Allow build to proceed if env vars are coming from process.env directly.
1302
+ const fromProcess = REQUIRED.every((k) => process.env[k]);
1303
+ if (fromProcess) process.exit(0);
1304
+ console.error(
1305
+ \`✗ Missing apps/backend/.env.local and required vars not in process.env.\\n Required: \${REQUIRED.join(", ")}\\n Hint: re-run the installer or copy apps/backend/.env.local.example\`,
1306
+ );
1307
+ process.exit(1);
1308
+ }
1309
+
1310
+ const env = Object.fromEntries(
1311
+ readFileSync(envPath, "utf-8")
1312
+ .split("\\n")
1313
+ .filter((line) => line && !line.startsWith("#"))
1314
+ .map((line) => {
1315
+ const i = line.indexOf("=");
1316
+ return i === -1 ? [line, ""] : [line.slice(0, i).trim(), line.slice(i + 1).trim()];
1317
+ }),
1318
+ );
1319
+
1320
+ const missing = REQUIRED.filter((k) => !env[k] && !process.env[k]);
1321
+ if (missing.length) {
1322
+ console.error(\`✗ Missing required env vars: \${missing.join(", ")}\\n Edit apps/backend/.env.local or set them in process.env.\`);
1323
+ process.exit(1);
1324
+ }
1325
+ console.log("✓ env check passed");
1326
+ `;
1327
+ writeFileSync(resolve(scriptsDir, "check-env.mjs"), script);
1328
+ }
1329
+
1330
+ // PM2 ecosystem config — production process supervision for self-hosted deploys.
1331
+ // Includes FE, BE, optional proxy, and optional cron in fork mode (no clustering
1332
+ // since Next.js handles that internally and the proxy is single-threaded by design).
1333
+ function writePm2Config(targetDir, dirName, adminPrefix) {
1334
+ const singleOrigin = !!adminPrefix;
1335
+ const config = `// PM2 ecosystem config for ${dirName}.
1336
+ // Usage:
1337
+ // pnpm install
1338
+ // pnpm backend:upgrade
1339
+ // pnpm build
1340
+ // pm2 start ecosystem.config.cjs # FE + BE
1341
+ // pm2 start ecosystem.config.cjs --only proxy # add reverse proxy
1342
+ // pm2 start ecosystem.config.cjs --only cron # self-hosted cron fallback
1343
+ // pm2 save && pm2 startup # persist across reboots
1344
+
1345
+ module.exports = {
1346
+ apps: [
1347
+ {
1348
+ name: "frontend",
1349
+ cwd: "./apps/frontend",
1350
+ script: "node_modules/next/dist/bin/next",
1351
+ args: "start",
1352
+ env: { NODE_ENV: "production", PORT: 3001 },
1353
+ max_memory_restart: "1G",
1354
+ autorestart: true,
1355
+ },
1356
+ {
1357
+ name: "backend",
1358
+ cwd: "./apps/backend",
1359
+ script: "node_modules/next/dist/bin/next",
1360
+ args: "start",
1361
+ env: { NODE_ENV: "production", PORT: 3000 },
1362
+ max_memory_restart: "1G",
1363
+ autorestart: true,
1364
+ },
1365
+ {
1366
+ name: "proxy",
1367
+ script: "./apps/proxy/server.mjs",
1368
+ env: {
1369
+ NODE_ENV: "production",
1370
+ PORT: 3030,
1371
+ BACKEND: "http://127.0.0.1:3000",
1372
+ FRONTEND: "http://127.0.0.1:3001",
1373
+ ${singleOrigin ? `ADMIN_PREFIX: "${adminPrefix}",` : ""}
1374
+ },
1375
+ max_memory_restart: "256M",
1376
+ autorestart: true,
1377
+ },
1378
+ {
1379
+ name: "cron",
1380
+ cwd: "./apps/backend",
1381
+ script: "scripts/dev-cron.ts",
1382
+ interpreter: "bun",
1383
+ env: { NODE_ENV: "production" },
1384
+ autorestart: true,
1385
+ },
1386
+ ],
1387
+ };
1388
+ `;
1389
+ writeFileSync(resolve(targetDir, "ecosystem.config.cjs"), config);
1390
+ }
1391
+
1392
+ function writeClaudeMd(targetDir, adminPrefix) {
1393
+ const singleOrigin = !!adminPrefix;
1394
+ const prefix = adminPrefix || "/admin";
1395
+ const singleOriginSection = singleOrigin
1396
+ ? `## Single-origin routing
1397
+
1398
+ Frontend is the public entry point. It rewrites these paths to the backend (do **not** create matching routes in the frontend):
1399
+
1400
+ - \`/admin/*\`, \`/uploads/*\`
1401
+ - \`/api/auth/*\`, \`/api/v1/*\`, \`/api/email/*\`, \`/api/cron\`, \`/api/health\`, \`/api/mcp\`, \`/api/admin/*\`, \`/api/editing-presence/*\`
1402
+ - \`/_next/static\` under \`APOLLO_ASSET_PREFIX=${prefix}\` (backend chunks)
1403
+
1404
+ Custom frontend APIs must be namespaced (e.g. \`/api/internal/*\`).
1405
+
1406
+ To disable single-origin: remove \`APOLLO_ASSET_PREFIX\` from \`apps/backend/.env.local\` and delete the \`rewrites()\` block in \`apps/frontend/next.config.ts\`.
1407
+
1408
+ Known gotcha: backend's \`/_next/image\` is not rewritten — custom admin code using \`<Image>\` on \`/uploads/*\` will 404. Use plain \`<img>\` or \`unoptimized\`.
1409
+ `
1410
+ : `## Routing model — separate origins
1411
+
1412
+ Frontend and backend run on independent origins. No rewrites; talk to the backend over its public URL.
1413
+ `;
1414
+
1415
+ const content = `# CLAUDE.md
1416
+
1417
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
1418
+
1419
+ ## Repo shape
1420
+
1421
+ pnpm workspace monorepo. Three workspaces under \`apps/\`:
1422
+
1423
+ - \`apps/frontend\` — public Next.js site${singleOrigin ? " (the public origin in single-origin mode)" : ""}.
1424
+ - \`apps/backend\` — **git submodule** pointing at \`apollo-cms\`. Treat as read-only; do not edit files in here. Open PRs upstream.
1425
+ - \`apps/cms-plugins/<slug>\` — project-specific Apollo CMS plugins. Loaded by the backend via \`APOLLO_EXTRA_PLUGINS_DIR=../cms-plugins\`. Each plugin is built with esbuild to \`dist/server.mjs\` before the backend builds.
1426
+ - \`apps/proxy/server.mjs\` — zero-dep Node reverse proxy used for single-origin dev and self-hosted deploys.
1427
+
1428
+ Built-in plugins in \`apps/backend/plugins/\` always win on slug collisions with \`apps/cms-plugins/\`.
1429
+
1430
+ ${singleOriginSection}
1431
+ ## Common commands
1432
+
1433
+ \`\`\`bash
1434
+ pnpm install
1435
+ pnpm backend:setup # first-time: db:push + db:seed
1436
+ pnpm backend:upgrade # release-time: db:push + replay src/upgrades/0.1.*.ts + seed
1437
+ pnpm backend:update # fast-forward the apollo-cms submodule
1438
+
1439
+ pnpm dev # FE + BE + plugin watcher (parallel)
1440
+ pnpm dev:rp # same + reverse proxy on :3030 (single origin, shared cookies)
1441
+ pnpm dev:frontend # FE only
1442
+ pnpm dev:backend # BE only (runs predev:setup to build plugins first)
1443
+
1444
+ pnpm build # cms-plugins → backend plugins → backend → frontend
1445
+ pnpm build:backend
1446
+ pnpm build:frontend
1447
+
1448
+ pnpm start # FE + BE
1449
+ pnpm start:rp # FE + BE + proxy
1450
+
1451
+ pnpm lint # recursive
1452
+ pnpm typecheck # recursive
1453
+
1454
+ pnpm cms-plugin:new <slug> # scaffold a new plugin from example-plugin
1455
+ \`\`\`
1456
+
1457
+ \`pnpm dev\` and \`pnpm build\` both run \`cms-plugins:build\` first (via \`predev:setup\` / build script chain) — plugins must compile to \`dist/server.mjs\` before the backend resolves them in production. In dev under Bun the loader also accepts \`index.ts\` directly.
1458
+
1459
+ Release flow on a server: \`pnpm install && pnpm backend:upgrade && pnpm build && pm2 reload all\`. \`pnpm start\` does **not** run migrations.
1460
+
1461
+ ## Ports
1462
+
1463
+ Configured in \`ecosystem.config.cjs\` (PM2) and consumed by \`apps/proxy/server.mjs\` via env:
1464
+
1465
+ | Service | Port |
1466
+ | -------- | ---- |
1467
+ | proxy | 3030 |
1468
+ | backend | 3000 |
1469
+ | frontend | 3001 |
1470
+
1471
+ Proxy reads \`PORT\`, \`BACKEND\`, \`FRONTEND\`, \`ADMIN_PREFIX\` from env. When using the proxy locally, set \`NEXT_PUBLIC_SITE_URL=http://localhost:3030\` so Better Auth / OAuth callbacks land on the unified origin.
1472
+
1473
+ ## Deploy
1474
+
1475
+ - Self-hosted: build on a runner, rsync to the server, then \`pm2 startOrReload ecosystem.config.cjs --update-env\`. If the backend submodule is private, the deploy runner needs a token with read access.
1476
+ - Vercel: two projects per repo (\`apps/backend\` and \`apps/frontend\` root dirs). Backend's Build Command must copy \`apps/cms-plugins/*/dist\` into \`apps/backend/plugins/\` before \`next build\` because Turbopack rejects \`outputFileTracingIncludes\` globs above the project root. "Include all submodules" must be ON in both projects.
1477
+
1478
+ ## Submodule discipline
1479
+
1480
+ - Do not edit files under \`apps/backend\` — they belong to the upstream \`apollo-cms\` repo.
1481
+ - After \`pnpm backend:update\`, run \`pnpm install\` (in case package.json changed) then \`pnpm backend:upgrade\` (replays data migrations that \`backend:setup\` skips).
1482
+ `;
1483
+ writeFileSync(resolve(targetDir, "CLAUDE.md"), content);
1484
+ }
1485
+
1247
1486
  function writeReadme(targetDir, dirName, frontendName, adminPrefix) {
1248
1487
  const singleOrigin = !!adminPrefix;
1249
1488
 
@@ -1372,14 +1611,13 @@ folder + \`plugin.json#name\`.
1372
1611
  Apollo CMS is tracked as a git submodule. Full upgrade flow:
1373
1612
 
1374
1613
  \`\`\`bash
1375
- pnpm backend:update # fast-forward apps/backend to the latest apollo-cms commit
1376
- pnpm install # in case the submodule changed package.json
1614
+ pnpm backend:update # auto-stash, fast-forward apps/backend, run pnpm install
1377
1615
  pnpm backend:upgrade # drizzle push + replay version migrations + seed
1378
1616
  \`\`\`
1379
1617
 
1380
1618
  | Script | What it does |
1381
1619
  | --- | --- |
1382
- | \`pnpm backend:update\` | \`git submodule update --remote --merge apps/backend\` code only |
1620
+ | \`pnpm backend:update\` | Auto-stashes local submodule changes, fast-forwards \`apps/backend\`, runs \`pnpm install\` so new backend deps are picked up, then restores the stash. |
1383
1621
  | \`pnpm backend:setup\` | First-time bootstrap: \`db:push\` + \`db:seed\` only |
1384
1622
  | \`pnpm backend:upgrade\` | Full pipeline: \`db:push\` + replay \`src/upgrades/0.1.*.ts\` data migrations + \`db:seed\`. **Use this** after \`backend:update\` — \`backend:setup\` skips the data-migration phase. |
1385
1623
 
@@ -1391,6 +1629,53 @@ Do **not** edit files inside \`apps/backend\`. Open issues / PRs in the
1391
1629
  Shared dev env lives in the root \`.env.local\`. The backend reads its own
1392
1630
  \`apps/backend/.env.local\` (already populated by the installer).
1393
1631
 
1632
+ ## Deploy (self-hosted)
1633
+
1634
+ Standard sequence on a VPS / Docker host / bare metal:
1635
+
1636
+ \`\`\`bash
1637
+ pnpm install
1638
+ pnpm backend:upgrade # db push + replay data migrations + seed (run ONCE per release)
1639
+ pnpm build # builds plugins → backend → frontend
1640
+ pnpm start # FE :3001 + BE :3000 — or:
1641
+ pnpm start:rp # adds Node reverse proxy on :3030 (single origin)
1642
+ \`\`\`
1643
+
1644
+ \`pnpm start\` does **not** run migrations on boot — that lets you restart
1645
+ processes without re-running \`drizzle-kit push\` every time. Always run
1646
+ \`pnpm backend:upgrade\` once per release before \`pnpm start\`.
1647
+
1648
+ | Script | What it does |
1649
+ | ------------------- | ------------------------------------------------------------------- |
1650
+ | \`pnpm build\` | Full pipeline: cms-plugins → backend plugins → backend → frontend |
1651
+ | \`pnpm start\` | FE + BE (parallel \`next start\`) |
1652
+ | \`pnpm start:rp\` | FE + BE + reverse proxy on :3030 |
1653
+ | \`pnpm start:proxy\` | Reverse proxy alone (already running FE/BE separately) |
1654
+ | \`pnpm start:cron\` | Self-hosted cron fallback — only if not using Vercel Cron / k8s |
1655
+
1656
+ ### PM2 (recommended for VPS)
1657
+
1658
+ The installer scaffolds \`ecosystem.config.cjs\`. To supervise everything:
1659
+
1660
+ \`\`\`bash
1661
+ pm2 start ecosystem.config.cjs # FE + BE + proxy + cron (all four)
1662
+ pm2 start ecosystem.config.cjs --only frontend,backend # just the two apps
1663
+ pm2 save && pm2 startup # persist across reboots
1664
+ pm2 logs # tail all processes
1665
+ pm2 reload all # zero-downtime restart
1666
+ \`\`\`
1667
+
1668
+ The proxy and cron processes can be omitted with \`--only\` if you front the
1669
+ apps with nginx/Caddy or use platform cron (system cron, k8s CronJob).
1670
+
1671
+ ### Docker / k8s
1672
+
1673
+ For containerized deploys:
1674
+ - Bake the build into the image (\`pnpm install && pnpm build\`)
1675
+ - Set entrypoint to \`pnpm start\` or \`pnpm start:rp\`
1676
+ - Run \`pnpm backend:upgrade\` as a separate init container / Job before app pods start
1677
+ - Use platform-native cron (k8s CronJob hitting \`/api/cron\` with \`CRON_SECRET\`) instead of \`pnpm start:cron\`
1678
+
1394
1679
  ## Deploy on Vercel
1395
1680
 
1396
1681
  Two Vercel projects, one repo. Each project picks up its own Root Directory.
@@ -1505,12 +1790,15 @@ async function main() {
1505
1790
  writeRootGitignore(targetDir);
1506
1791
  writeRootEnv(targetDir, { dbUrl, siteUrl, locale, authSecret, cronSecret, adminPrefix, backendInternalUrl });
1507
1792
  writeReadme(targetDir, dirName, frontendName, adminPrefix);
1793
+ writeClaudeMd(targetDir, adminPrefix);
1508
1794
  if (adminPrefix) writeNginxSample(targetDir, adminPrefix);
1509
1795
  writeProxyApp(targetDir, dirName, adminPrefix);
1796
+ writeCheckEnvScript(targetDir);
1797
+ writePm2Config(targetDir, dirName, adminPrefix);
1510
1798
  success(
1511
- `package.json, pnpm-workspace.yaml, .gitignore, .env.local, README.md${
1799
+ `package.json, pnpm-workspace.yaml, .gitignore, .env.local, README.md, CLAUDE.md${
1512
1800
  adminPrefix ? ", nginx.conf.sample" : ""
1513
- }, apps/proxy`,
1801
+ }, apps/proxy, ecosystem.config.cjs, scripts/check-env.mjs`,
1514
1802
  );
1515
1803
 
1516
1804
  // ── Step 4: git init ──
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-apollo-monorepo",
3
- "version": "0.8.0",
3
+ "version": "0.9.1",
4
4
  "description": "Scaffold a monorepo with a frontend app and Apollo CMS as a git submodule backend (single-origin via Next.js rewrites + assetPrefix)",
5
5
  "bin": {
6
6
  "create-apollo-monorepo": "index.mjs"