create-apollo-monorepo 0.9.0 → 0.9.2
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 +7 -7
- package/index.mjs +301 -44
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -6,15 +6,15 @@ mounted as a **git submodule** backend (read-only — pull updates only).
|
|
|
6
6
|
## Usage
|
|
7
7
|
|
|
8
8
|
```bash
|
|
9
|
-
npx create-apollo-monorepo
|
|
9
|
+
npx create-apollo-monorepo my-site
|
|
10
10
|
```
|
|
11
11
|
|
|
12
12
|
With flags:
|
|
13
13
|
|
|
14
14
|
```bash
|
|
15
|
-
npx create-apollo-monorepo
|
|
16
|
-
--frontend-name "@
|
|
17
|
-
--db "postgresql://user:pass@localhost:5432/
|
|
15
|
+
npx create-apollo-monorepo my-site \
|
|
16
|
+
--frontend-name "@my-site/frontend" \
|
|
17
|
+
--db "postgresql://user:pass@localhost:5432/my-site" \
|
|
18
18
|
--url "http://localhost:3000" \
|
|
19
19
|
--locale th
|
|
20
20
|
```
|
|
@@ -22,9 +22,9 @@ npx create-apollo-monorepo thamc-new \
|
|
|
22
22
|
## Result
|
|
23
23
|
|
|
24
24
|
```
|
|
25
|
-
|
|
25
|
+
my-site/
|
|
26
26
|
├── apps/
|
|
27
|
-
│ ├── frontend/ ← @
|
|
27
|
+
│ ├── frontend/ ← @my-site/frontend (Next.js skeleton)
|
|
28
28
|
│ └── backend/ ← git submodule → apollo-cms
|
|
29
29
|
├── package.json ← root workspace
|
|
30
30
|
├── pnpm-workspace.yaml
|
|
@@ -89,7 +89,7 @@ subdomains (e.g. `cms.example.com` + `example.com`).
|
|
|
89
89
|
## After install
|
|
90
90
|
|
|
91
91
|
```bash
|
|
92
|
-
cd
|
|
92
|
+
cd my-site
|
|
93
93
|
pnpm backend:setup # push schema + seed apollo-cms
|
|
94
94
|
pnpm dev # frontend :3001 + backend :3000 in parallel
|
|
95
95
|
```
|
package/index.mjs
CHANGED
|
@@ -29,6 +29,13 @@ const FRONTEND_PATH = "apps/frontend";
|
|
|
29
29
|
const PROXY_PATH = "apps/proxy";
|
|
30
30
|
const MIN_NODE_MAJOR = 20;
|
|
31
31
|
|
|
32
|
+
// Default dev ports. Kept in sync with scripts/with-env.mjs and the proxy
|
|
33
|
+
// template. .env.local is the runtime source of truth — these are the
|
|
34
|
+
// installer-side fallbacks for computing initial URL defaults.
|
|
35
|
+
const DEFAULT_PROXY_PORT = 3030;
|
|
36
|
+
const DEFAULT_FRONTEND_PORT = 3001;
|
|
37
|
+
const DEFAULT_BACKEND_PORT = 3002;
|
|
38
|
+
|
|
32
39
|
const COLORS = {
|
|
33
40
|
reset: "\x1b[0m",
|
|
34
41
|
bold: "\x1b[1m",
|
|
@@ -46,9 +53,9 @@ ${COLORS.bold}Usage:${COLORS.reset}
|
|
|
46
53
|
npx create-apollo-monorepo <directory> [flags]
|
|
47
54
|
|
|
48
55
|
${COLORS.bold}Examples:${COLORS.reset}
|
|
49
|
-
npx create-apollo-monorepo
|
|
50
|
-
npx create-apollo-monorepo
|
|
51
|
-
npx create-apollo-monorepo
|
|
56
|
+
npx create-apollo-monorepo my-site
|
|
57
|
+
npx create-apollo-monorepo my-site --frontend-name "@my-site/frontend"
|
|
58
|
+
npx create-apollo-monorepo my-site --backend-branch develop --skip-install
|
|
52
59
|
|
|
53
60
|
${COLORS.bold}Flags:${COLORS.reset}
|
|
54
61
|
--frontend-name <name> Frontend package name (default: "@<dir>/frontend")
|
|
@@ -283,7 +290,14 @@ async function gatherEnv(flags, { singleOrigin }) {
|
|
|
283
290
|
let dbUrl = flags.db;
|
|
284
291
|
// In single-origin mode the frontend is the public entry → default :3001.
|
|
285
292
|
// In separate-origins mode the backend is the public CMS URL → default :3000.
|
|
286
|
-
|
|
293
|
+
// Public origin = frontend in single-origin mode (frontend rewrites /admin/*,
|
|
294
|
+
// /api/*, /uploads/* to the backend), backend in separate-origins mode.
|
|
295
|
+
// For `pnpm dev:rp` switch this to http://localhost:${PROXY_PORT} in .env.local.
|
|
296
|
+
// Derived from the PORT constants so changing a default updates both the
|
|
297
|
+
// .env.local seed and the prompt's suggested value.
|
|
298
|
+
const defaultSiteUrl = singleOrigin
|
|
299
|
+
? `http://localhost:${DEFAULT_FRONTEND_PORT}`
|
|
300
|
+
: `http://localhost:${DEFAULT_BACKEND_PORT}`;
|
|
287
301
|
let siteUrl = flags.url ?? defaultSiteUrl;
|
|
288
302
|
let locale = flags.locale ?? "en";
|
|
289
303
|
|
|
@@ -355,16 +369,17 @@ function writeRootPackageJson(targetDir, dirName) {
|
|
|
355
369
|
// rebuild and the backend's plugin loader picks up the fresh
|
|
356
370
|
// dist/server.mjs on next request.
|
|
357
371
|
dev:
|
|
358
|
-
"pnpm predev:setup && concurrently -k -n FE,BE,PL -c blue,magenta,yellow \"pnpm
|
|
372
|
+
"pnpm predev:setup && concurrently -k -n FE,BE,PL -c blue,magenta,yellow \"pnpm dev:frontend\" \"pnpm dev:backend\" \"pnpm cms-plugins:dev\"",
|
|
359
373
|
// Optional single-origin dev: same as `pnpm dev` but adds a Node.js
|
|
360
|
-
// reverse proxy on :3030 that fronts FE :3001
|
|
361
|
-
//
|
|
374
|
+
// reverse proxy on :3030 (PROXY_PORT) that fronts FE :3001 (FRONTEND_PORT)
|
|
375
|
+
// + BE :3002 (BACKEND_PORT). Ports come from .env.local; override there.
|
|
362
376
|
"dev:rp":
|
|
363
|
-
"pnpm predev:setup && concurrently -k -n FE,BE,PL,PX -c blue,magenta,yellow,cyan \"pnpm
|
|
364
|
-
"dev:proxy": "node apps/proxy/server.mjs",
|
|
365
|
-
"dev:frontend":
|
|
377
|
+
"pnpm predev:setup && concurrently -k -n FE,BE,PL,PX -c blue,magenta,yellow,cyan \"pnpm dev:frontend\" \"pnpm dev:backend\" \"pnpm cms-plugins:dev\" \"pnpm dev:proxy\"",
|
|
378
|
+
"dev:proxy": "node scripts/with-env.mjs node apps/proxy/server.mjs",
|
|
379
|
+
"dev:frontend":
|
|
380
|
+
"node scripts/with-env.mjs --port=FRONTEND_PORT pnpm --filter ./apps/frontend exec next dev",
|
|
366
381
|
"dev:backend":
|
|
367
|
-
"pnpm predev:setup && pnpm --filter ./apps/backend exec next dev",
|
|
382
|
+
"pnpm predev:setup && node scripts/with-env.mjs --port=BACKEND_PORT pnpm --filter ./apps/backend exec next dev",
|
|
368
383
|
"dev:cron": "pnpm --filter ./apps/backend exec bun scripts/dev-cron.ts",
|
|
369
384
|
// Build pipeline: cms-plugins → apollo-cms's own plugins → backend → frontend.
|
|
370
385
|
// `prebuild` runs first (npm/pnpm convention) and fails fast if the
|
|
@@ -375,17 +390,19 @@ function writeRootPackageJson(targetDir, dirName) {
|
|
|
375
390
|
"build:backend":
|
|
376
391
|
"pnpm cms-plugins:build && pnpm --filter ./apps/backend exec bun run plugins:build && pnpm --filter ./apps/backend build",
|
|
377
392
|
"build:frontend": "pnpm --filter ./apps/frontend build",
|
|
378
|
-
// Production start. Runs FE :
|
|
379
|
-
//
|
|
380
|
-
// NOT run migrations on boot (zero-downtime restart safety).
|
|
393
|
+
// Production start. Runs FE :FRONTEND_PORT + BE :BACKEND_PORT (defaults
|
|
394
|
+
// 3001 + 3002 from .env.local). Run \`pnpm backend:upgrade\` BEFORE this —
|
|
395
|
+
// start does NOT run migrations on boot (zero-downtime restart safety).
|
|
381
396
|
start:
|
|
382
|
-
"concurrently -k -n FE,BE -c blue,magenta \"pnpm
|
|
383
|
-
// Single-origin production: FE + BE + Node reverse proxy on :
|
|
397
|
+
"concurrently -k -n FE,BE -c blue,magenta \"pnpm start:frontend\" \"pnpm start:backend\"",
|
|
398
|
+
// Single-origin production: FE + BE + Node reverse proxy on :PROXY_PORT.
|
|
384
399
|
"start:rp":
|
|
385
|
-
"concurrently -k -n FE,BE,PX -c blue,magenta,cyan \"pnpm
|
|
386
|
-
"start:proxy": "NODE_ENV=production node apps/proxy/server.mjs",
|
|
387
|
-
"start:frontend":
|
|
388
|
-
|
|
400
|
+
"concurrently -k -n FE,BE,PX -c blue,magenta,cyan \"pnpm start:frontend\" \"pnpm start:backend\" \"pnpm start:proxy\"",
|
|
401
|
+
"start:proxy": "NODE_ENV=production node scripts/with-env.mjs node apps/proxy/server.mjs",
|
|
402
|
+
"start:frontend":
|
|
403
|
+
"node scripts/with-env.mjs --port=FRONTEND_PORT pnpm --filter ./apps/frontend exec next start",
|
|
404
|
+
"start:backend":
|
|
405
|
+
"node scripts/with-env.mjs --port=BACKEND_PORT pnpm --filter ./apps/backend exec next start",
|
|
389
406
|
// Self-hosted scheduler fallback. Vercel deploys use Vercel Cron via
|
|
390
407
|
// apps/backend/vercel.json — skip this on Vercel. For Docker / VPS
|
|
391
408
|
// you can either run \`pnpm start:cron\` alongside \`pnpm start\` (PM2
|
|
@@ -399,7 +416,13 @@ function writeRootPackageJson(targetDir, dirName) {
|
|
|
399
416
|
"cms-plugin:new": "node scripts/new-cms-plugin.mjs",
|
|
400
417
|
lint: "pnpm -r lint",
|
|
401
418
|
typecheck: "pnpm -r typecheck",
|
|
402
|
-
|
|
419
|
+
// Stash any local changes inside the submodule so the fast-forward
|
|
420
|
+
// merge can't conflict, then update, then run `pnpm install` at the
|
|
421
|
+
// root so workspace deps pick up any package.json changes pulled in
|
|
422
|
+
// from the new apollo-cms commit. The stash is restored if it was
|
|
423
|
+
// created; otherwise this is a no-op.
|
|
424
|
+
"backend:update":
|
|
425
|
+
"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');}}\"",
|
|
403
426
|
// `pnpm setup` is pnpm's CLI bootstrap built-in — must use `run` to
|
|
404
427
|
// forward to the workspace's setup script (apollo-cms's db:push + db:seed).
|
|
405
428
|
"backend:setup": "pnpm --filter ./apps/backend run setup",
|
|
@@ -539,10 +562,43 @@ function writeProxyApp(targetDir, dirName, adminPrefix) {
|
|
|
539
562
|
|
|
540
563
|
import http from "node:http";
|
|
541
564
|
import net from "node:net";
|
|
565
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
566
|
+
import { resolve, dirname } from "node:path";
|
|
567
|
+
import { fileURLToPath } from "node:url";
|
|
568
|
+
|
|
569
|
+
// Load root .env.local so the proxy honors PROXY_PORT/FRONTEND_PORT/BACKEND_PORT
|
|
570
|
+
// alongside the frontend & backend. Process env still wins (12-factor friendly).
|
|
571
|
+
(function loadRootEnv() {
|
|
572
|
+
try {
|
|
573
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
574
|
+
const envPath = resolve(here, "../../.env.local");
|
|
575
|
+
if (!existsSync(envPath)) return;
|
|
576
|
+
for (const raw of readFileSync(envPath, "utf8").split(/\\r?\\n/)) {
|
|
577
|
+
const line = raw.trim();
|
|
578
|
+
if (!line || line.startsWith("#")) continue;
|
|
579
|
+
const eq = line.indexOf("=");
|
|
580
|
+
if (eq < 0) continue;
|
|
581
|
+
const key = line.slice(0, eq).trim();
|
|
582
|
+
if (!/^[A-Z_][A-Z0-9_]*$/i.test(key)) continue;
|
|
583
|
+
if (process.env[key] !== undefined) continue;
|
|
584
|
+
let val = line.slice(eq + 1).trim();
|
|
585
|
+
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
|
|
586
|
+
val = val.slice(1, -1);
|
|
587
|
+
}
|
|
588
|
+
process.env[key] = val;
|
|
589
|
+
}
|
|
590
|
+
} catch {
|
|
591
|
+
// Best-effort. The proxy still runs on its hardcoded fallbacks.
|
|
592
|
+
}
|
|
593
|
+
})();
|
|
542
594
|
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
const
|
|
595
|
+
// Backward-compatible: PROXY_PORT/FRONTEND_PORT/BACKEND_PORT are the new names;
|
|
596
|
+
// PORT/FRONTEND/BACKEND are still honored if explicitly set in process.env.
|
|
597
|
+
const PORT = Number(process.env.PROXY_PORT ?? process.env.PORT ?? 3030);
|
|
598
|
+
const BACKEND_HOST = process.env.BACKEND ?? \`http://127.0.0.1:\${process.env.BACKEND_PORT ?? 3002}\`;
|
|
599
|
+
const FRONTEND_HOST = process.env.FRONTEND ?? \`http://127.0.0.1:\${process.env.FRONTEND_PORT ?? 3001}\`;
|
|
600
|
+
const BACKEND = parseTarget(BACKEND_HOST);
|
|
601
|
+
const FRONTEND = parseTarget(FRONTEND_HOST);
|
|
546
602
|
|
|
547
603
|
const ADMIN_PREFIX = process.env.ADMIN_PREFIX ?? "${prefix}";
|
|
548
604
|
// Pipe-separated list of /api/<segment> paths that route to the backend.
|
|
@@ -824,6 +880,10 @@ The Node proxy is intended for dev and small self-hosted deploys.
|
|
|
824
880
|
}
|
|
825
881
|
|
|
826
882
|
function writeRootGitignore(targetDir) {
|
|
883
|
+
// NOTE: .env.local is intentionally NOT ignored — it holds shared dev port
|
|
884
|
+
// defaults (PROXY_PORT/FRONTEND_PORT/BACKEND_PORT) and other non-secret
|
|
885
|
+
// workspace knobs that should travel with the repo. Keep real secrets in
|
|
886
|
+
// .env (ignored) or apps/backend/.env.local (ignored by the submodule).
|
|
827
887
|
const ignore = [
|
|
828
888
|
"node_modules",
|
|
829
889
|
".pnpm-store",
|
|
@@ -832,8 +892,6 @@ function writeRootGitignore(targetDir) {
|
|
|
832
892
|
"dist",
|
|
833
893
|
"build",
|
|
834
894
|
".env",
|
|
835
|
-
".env.local",
|
|
836
|
-
".env*.local",
|
|
837
895
|
"*.log",
|
|
838
896
|
".DS_Store",
|
|
839
897
|
"",
|
|
@@ -845,12 +903,12 @@ function writeRootEnv(targetDir, { dbUrl, siteUrl, locale, authSecret, cronSecre
|
|
|
845
903
|
const header = adminPrefix
|
|
846
904
|
? [
|
|
847
905
|
"# Single-origin monorepo dev env",
|
|
848
|
-
|
|
849
|
-
|
|
906
|
+
`# Public origin = frontend (apps/frontend on :${DEFAULT_FRONTEND_PORT}). Frontend rewrites /admin/*,`,
|
|
907
|
+
`# /api/*, /uploads/*, and the asset prefix to the backend (apps/backend on :${DEFAULT_BACKEND_PORT}).`,
|
|
850
908
|
]
|
|
851
909
|
: [
|
|
852
910
|
"# Separate-origins monorepo dev env",
|
|
853
|
-
|
|
911
|
+
`# Backend runs at http://localhost:${DEFAULT_BACKEND_PORT}, frontend at http://localhost:${DEFAULT_FRONTEND_PORT}.`,
|
|
854
912
|
"# To switch to single-origin: set APOLLO_ASSET_PREFIX (default: /admin) and",
|
|
855
913
|
"# BACKEND_INTERNAL_URL, then add rewrites() to apps/frontend/next.config.ts.",
|
|
856
914
|
];
|
|
@@ -858,10 +916,25 @@ function writeRootEnv(targetDir, { dbUrl, siteUrl, locale, authSecret, cronSecre
|
|
|
858
916
|
const lines = [
|
|
859
917
|
...header,
|
|
860
918
|
"",
|
|
919
|
+
"# ── Dev server ports ─────────────────────────────────────────────",
|
|
920
|
+
"# Single source of truth for local ports. Consumed by:",
|
|
921
|
+
"# • apps/proxy/server.mjs (PROXY_PORT, FRONTEND_PORT, BACKEND_PORT)",
|
|
922
|
+
"# • scripts/with-env.mjs (maps FRONTEND_PORT/BACKEND_PORT → PORT",
|
|
923
|
+
"# when launching apps/frontend & apps/backend,",
|
|
924
|
+
"# and derives NEXT_PUBLIC_SITE_URL /",
|
|
925
|
+
"# BACKEND_INTERNAL_URL when those are unset)",
|
|
926
|
+
"# Override any of these; the proxy/frontend/backend will follow.",
|
|
927
|
+
`PROXY_PORT=${DEFAULT_PROXY_PORT}`,
|
|
928
|
+
`FRONTEND_PORT=${DEFAULT_FRONTEND_PORT}`,
|
|
929
|
+
`BACKEND_PORT=${DEFAULT_BACKEND_PORT}`,
|
|
930
|
+
"",
|
|
861
931
|
"# ── Shared (consumed by both apps) ───────────────────────────────",
|
|
862
932
|
`DATABASE_URL=${dbUrl}`,
|
|
863
933
|
`APOLLO_SECRET=${authSecret}`,
|
|
864
934
|
`CRON_SECRET=${cronSecret}`,
|
|
935
|
+
"# Public origin. In dev, leave blank to auto-derive from FRONTEND_PORT",
|
|
936
|
+
"# (single-origin) or BACKEND_PORT (separate-origins) via scripts/with-env.mjs.",
|
|
937
|
+
"# In prod set to the real domain (e.g. https://cms.example.com).",
|
|
865
938
|
`NEXT_PUBLIC_SITE_URL=${siteUrl}`,
|
|
866
939
|
`NEXT_PUBLIC_DEFAULT_LOCALE=${locale}`,
|
|
867
940
|
"",
|
|
@@ -878,6 +951,9 @@ function writeRootEnv(targetDir, { dbUrl, siteUrl, locale, authSecret, cronSecre
|
|
|
878
951
|
`APOLLO_EXTRA_PLUGINS_DIR=../cms-plugins`,
|
|
879
952
|
"",
|
|
880
953
|
"# ── Frontend (apps/frontend) ─────────────────────────────────────",
|
|
954
|
+
"# Where the frontend's rewrites point internally. In dev, leave blank",
|
|
955
|
+
"# to auto-derive http://127.0.0.1:${BACKEND_PORT} via scripts/with-env.mjs.",
|
|
956
|
+
"# In prod set to a private hostname (e.g. http://backend.internal:3000).",
|
|
881
957
|
`BACKEND_INTERNAL_URL=${backendInternalUrl}`,
|
|
882
958
|
"",
|
|
883
959
|
);
|
|
@@ -907,9 +983,12 @@ function writeFrontendApp(targetDir, frontendName, siteUrl, adminPrefix, backend
|
|
|
907
983
|
version: "0.0.0",
|
|
908
984
|
private: true,
|
|
909
985
|
scripts: {
|
|
910
|
-
|
|
986
|
+
// No -p flag: Next.js reads PORT from env. The repo-root scripts use
|
|
987
|
+
// scripts/with-env.mjs --port=FRONTEND_PORT to inject the right value
|
|
988
|
+
// from .env.local (default 3001).
|
|
989
|
+
dev: "next dev",
|
|
911
990
|
build: "next build",
|
|
912
|
-
start: "next start
|
|
991
|
+
start: "next start",
|
|
913
992
|
lint: "next lint",
|
|
914
993
|
typecheck: "tsc --noEmit",
|
|
915
994
|
},
|
|
@@ -1039,7 +1118,8 @@ export default config;
|
|
|
1039
1118
|
|
|
1040
1119
|
writeFileSync(
|
|
1041
1120
|
resolve(dir, ".gitignore"),
|
|
1042
|
-
|
|
1121
|
+
// .env.local is intentionally committed — it only contains dev port hints.
|
|
1122
|
+
["node_modules", ".next", "dist", ""].join("\n"),
|
|
1043
1123
|
);
|
|
1044
1124
|
|
|
1045
1125
|
// Vercel project config for the frontend. Skip cron + region pinning is
|
|
@@ -1267,6 +1347,88 @@ console.log(\`Run: pnpm install && pnpm cms-plugins:build && pnpm dev:backend\`)
|
|
|
1267
1347
|
writeFileSync(resolve(scriptsDir, "new-cms-plugin.mjs"), script);
|
|
1268
1348
|
}
|
|
1269
1349
|
|
|
1350
|
+
// Small launcher that loads root .env.local, applies port defaults
|
|
1351
|
+
// (PROXY_PORT=3030 / FRONTEND_PORT=3001 / BACKEND_PORT=3002), optionally maps
|
|
1352
|
+
// one of them onto PORT (Next.js reads PORT), and execs the rest of argv.
|
|
1353
|
+
//
|
|
1354
|
+
// Usage:
|
|
1355
|
+
// node scripts/with-env.mjs [--port=VAR_NAME] <command> [args...]
|
|
1356
|
+
function writeWithEnvScript(targetDir) {
|
|
1357
|
+
const scriptsDir = resolve(targetDir, "scripts");
|
|
1358
|
+
mkdirSync(scriptsDir, { recursive: true });
|
|
1359
|
+
const script = `#!/usr/bin/env node
|
|
1360
|
+
// Loads <repo-root>/.env.local so frontend / backend / proxy all share one
|
|
1361
|
+
// source of truth for ports. Process env still wins.
|
|
1362
|
+
//
|
|
1363
|
+
// Usage:
|
|
1364
|
+
// node scripts/with-env.mjs [--port=VAR_NAME] <command> [args...]
|
|
1365
|
+
// --port=FRONTEND_PORT copies process.env.FRONTEND_PORT onto PORT before exec,
|
|
1366
|
+
// so \`next dev\` and \`next start\` pick up the right port without --port flags.
|
|
1367
|
+
|
|
1368
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
1369
|
+
import { spawn } from "node:child_process";
|
|
1370
|
+
import { dirname, resolve } from "node:path";
|
|
1371
|
+
import { fileURLToPath } from "node:url";
|
|
1372
|
+
|
|
1373
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
1374
|
+
const envPath = resolve(here, "../.env.local");
|
|
1375
|
+
|
|
1376
|
+
if (existsSync(envPath)) {
|
|
1377
|
+
for (const raw of readFileSync(envPath, "utf8").split(/\\r?\\n/)) {
|
|
1378
|
+
const line = raw.trim();
|
|
1379
|
+
if (!line || line.startsWith("#")) continue;
|
|
1380
|
+
const eq = line.indexOf("=");
|
|
1381
|
+
if (eq < 0) continue;
|
|
1382
|
+
const k = line.slice(0, eq).trim();
|
|
1383
|
+
if (!/^[A-Z_][A-Z0-9_]*$/i.test(k)) continue;
|
|
1384
|
+
if (process.env[k] !== undefined) continue;
|
|
1385
|
+
let v = line.slice(eq + 1).trim();
|
|
1386
|
+
if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) {
|
|
1387
|
+
v = v.slice(1, -1);
|
|
1388
|
+
}
|
|
1389
|
+
process.env[k] = v;
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
// Fallbacks — keep in sync with apps/proxy/server.mjs and PM2 config.
|
|
1394
|
+
process.env.PROXY_PORT ||= "3030";
|
|
1395
|
+
process.env.FRONTEND_PORT ||= "3001";
|
|
1396
|
+
process.env.BACKEND_PORT ||= "3002";
|
|
1397
|
+
|
|
1398
|
+
// Derive URL vars from PORTs when unset. Dev-friendly: edit a port and
|
|
1399
|
+
// everything follows. Prod-friendly: set these explicitly to real hostnames
|
|
1400
|
+
// (e.g. https://cms.example.com, http://backend.internal:3000) and the
|
|
1401
|
+
// derivation is bypassed.
|
|
1402
|
+
//
|
|
1403
|
+
// NEXT_PUBLIC_SITE_URL is the public origin — defaults to the frontend port
|
|
1404
|
+
// (frontend rewrites /admin, /api, /uploads to the backend). For \`pnpm dev:rp\`
|
|
1405
|
+
// set it to http://localhost:\${PROXY_PORT} so Better Auth / OAuth callbacks
|
|
1406
|
+
// land on the unified origin.
|
|
1407
|
+
process.env.NEXT_PUBLIC_SITE_URL ||= \`http://localhost:\${process.env.FRONTEND_PORT}\`;
|
|
1408
|
+
process.env.BACKEND_INTERNAL_URL ||= \`http://127.0.0.1:\${process.env.BACKEND_PORT}\`;
|
|
1409
|
+
|
|
1410
|
+
const args = process.argv.slice(2);
|
|
1411
|
+
if (args[0]?.startsWith("--port=")) {
|
|
1412
|
+
const name = args.shift().slice(7);
|
|
1413
|
+
if (process.env[name]) process.env.PORT = process.env[name];
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
if (!args.length) {
|
|
1417
|
+
console.error("Usage: node scripts/with-env.mjs [--port=VAR] <command> [args...]");
|
|
1418
|
+
process.exit(2);
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
const [cmd, ...rest] = args;
|
|
1422
|
+
const child = spawn(cmd, rest, {
|
|
1423
|
+
stdio: "inherit",
|
|
1424
|
+
env: process.env,
|
|
1425
|
+
shell: process.platform === "win32",
|
|
1426
|
+
});
|
|
1427
|
+
child.on("exit", (code, sig) => process.exit(sig ? 1 : code ?? 0));
|
|
1428
|
+
`;
|
|
1429
|
+
writeFileSync(resolve(scriptsDir, "with-env.mjs"), script);
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1270
1432
|
// Pre-build env check. Fails the build before \`next build\` runs if backend
|
|
1271
1433
|
// required vars are missing, so users don't sit through a 30s build only to
|
|
1272
1434
|
// hit a runtime error on first request.
|
|
@@ -1343,7 +1505,7 @@ module.exports = {
|
|
|
1343
1505
|
cwd: "./apps/frontend",
|
|
1344
1506
|
script: "node_modules/next/dist/bin/next",
|
|
1345
1507
|
args: "start",
|
|
1346
|
-
env: { NODE_ENV: "production", PORT: 3001 },
|
|
1508
|
+
env: { NODE_ENV: "production", PORT: Number(process.env.FRONTEND_PORT) || 3001 },
|
|
1347
1509
|
max_memory_restart: "1G",
|
|
1348
1510
|
autorestart: true,
|
|
1349
1511
|
},
|
|
@@ -1352,7 +1514,7 @@ module.exports = {
|
|
|
1352
1514
|
cwd: "./apps/backend",
|
|
1353
1515
|
script: "node_modules/next/dist/bin/next",
|
|
1354
1516
|
args: "start",
|
|
1355
|
-
env: { NODE_ENV: "production", PORT:
|
|
1517
|
+
env: { NODE_ENV: "production", PORT: Number(process.env.BACKEND_PORT) || 3002 },
|
|
1356
1518
|
max_memory_restart: "1G",
|
|
1357
1519
|
autorestart: true,
|
|
1358
1520
|
},
|
|
@@ -1361,9 +1523,9 @@ module.exports = {
|
|
|
1361
1523
|
script: "./apps/proxy/server.mjs",
|
|
1362
1524
|
env: {
|
|
1363
1525
|
NODE_ENV: "production",
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1526
|
+
PROXY_PORT: Number(process.env.PROXY_PORT) || 3030,
|
|
1527
|
+
FRONTEND_PORT: Number(process.env.FRONTEND_PORT) || 3001,
|
|
1528
|
+
BACKEND_PORT: Number(process.env.BACKEND_PORT) || 3002,
|
|
1367
1529
|
${singleOrigin ? `ADMIN_PREFIX: "${adminPrefix}",` : ""}
|
|
1368
1530
|
},
|
|
1369
1531
|
max_memory_restart: "256M",
|
|
@@ -1383,6 +1545,100 @@ module.exports = {
|
|
|
1383
1545
|
writeFileSync(resolve(targetDir, "ecosystem.config.cjs"), config);
|
|
1384
1546
|
}
|
|
1385
1547
|
|
|
1548
|
+
function writeClaudeMd(targetDir, adminPrefix) {
|
|
1549
|
+
const singleOrigin = !!adminPrefix;
|
|
1550
|
+
const prefix = adminPrefix || "/admin";
|
|
1551
|
+
const singleOriginSection = singleOrigin
|
|
1552
|
+
? `## Single-origin routing
|
|
1553
|
+
|
|
1554
|
+
Frontend is the public entry point. It rewrites these paths to the backend (do **not** create matching routes in the frontend):
|
|
1555
|
+
|
|
1556
|
+
- \`/admin/*\`, \`/uploads/*\`
|
|
1557
|
+
- \`/api/auth/*\`, \`/api/v1/*\`, \`/api/email/*\`, \`/api/cron\`, \`/api/health\`, \`/api/mcp\`, \`/api/admin/*\`, \`/api/editing-presence/*\`
|
|
1558
|
+
- \`/_next/static\` under \`APOLLO_ASSET_PREFIX=${prefix}\` (backend chunks)
|
|
1559
|
+
|
|
1560
|
+
Custom frontend APIs must be namespaced (e.g. \`/api/internal/*\`).
|
|
1561
|
+
|
|
1562
|
+
To disable single-origin: remove \`APOLLO_ASSET_PREFIX\` from \`apps/backend/.env.local\` and delete the \`rewrites()\` block in \`apps/frontend/next.config.ts\`.
|
|
1563
|
+
|
|
1564
|
+
Known gotcha: backend's \`/_next/image\` is not rewritten — custom admin code using \`<Image>\` on \`/uploads/*\` will 404. Use plain \`<img>\` or \`unoptimized\`.
|
|
1565
|
+
`
|
|
1566
|
+
: `## Routing model — separate origins
|
|
1567
|
+
|
|
1568
|
+
Frontend and backend run on independent origins. No rewrites; talk to the backend over its public URL.
|
|
1569
|
+
`;
|
|
1570
|
+
|
|
1571
|
+
const content = `# CLAUDE.md
|
|
1572
|
+
|
|
1573
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
1574
|
+
|
|
1575
|
+
## Repo shape
|
|
1576
|
+
|
|
1577
|
+
pnpm workspace monorepo. Three workspaces under \`apps/\`:
|
|
1578
|
+
|
|
1579
|
+
- \`apps/frontend\` — public Next.js site${singleOrigin ? " (the public origin in single-origin mode)" : ""}.
|
|
1580
|
+
- \`apps/backend\` — **git submodule** pointing at \`apollo-cms\`. Treat as read-only; do not edit files in here. Open PRs upstream.
|
|
1581
|
+
- \`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.
|
|
1582
|
+
- \`apps/proxy/server.mjs\` — zero-dep Node reverse proxy used for single-origin dev and self-hosted deploys.
|
|
1583
|
+
|
|
1584
|
+
Built-in plugins in \`apps/backend/plugins/\` always win on slug collisions with \`apps/cms-plugins/\`.
|
|
1585
|
+
|
|
1586
|
+
${singleOriginSection}
|
|
1587
|
+
## Common commands
|
|
1588
|
+
|
|
1589
|
+
\`\`\`bash
|
|
1590
|
+
pnpm install
|
|
1591
|
+
pnpm backend:setup # first-time: db:push + db:seed
|
|
1592
|
+
pnpm backend:upgrade # release-time: db:push + replay src/upgrades/0.1.*.ts + seed
|
|
1593
|
+
pnpm backend:update # fast-forward the apollo-cms submodule
|
|
1594
|
+
|
|
1595
|
+
pnpm dev # FE + BE + plugin watcher (parallel)
|
|
1596
|
+
pnpm dev:rp # same + reverse proxy on :3030 (single origin, shared cookies)
|
|
1597
|
+
pnpm dev:frontend # FE only
|
|
1598
|
+
pnpm dev:backend # BE only (runs predev:setup to build plugins first)
|
|
1599
|
+
|
|
1600
|
+
pnpm build # cms-plugins → backend plugins → backend → frontend
|
|
1601
|
+
pnpm build:backend
|
|
1602
|
+
pnpm build:frontend
|
|
1603
|
+
|
|
1604
|
+
pnpm start # FE + BE
|
|
1605
|
+
pnpm start:rp # FE + BE + proxy
|
|
1606
|
+
|
|
1607
|
+
pnpm lint # recursive
|
|
1608
|
+
pnpm typecheck # recursive
|
|
1609
|
+
|
|
1610
|
+
pnpm cms-plugin:new <slug> # scaffold a new plugin from example-plugin
|
|
1611
|
+
\`\`\`
|
|
1612
|
+
|
|
1613
|
+
\`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.
|
|
1614
|
+
|
|
1615
|
+
Release flow on a server: \`pnpm install && pnpm backend:upgrade && pnpm build && pm2 reload all\`. \`pnpm start\` does **not** run migrations.
|
|
1616
|
+
|
|
1617
|
+
## Ports
|
|
1618
|
+
|
|
1619
|
+
Configured in \`ecosystem.config.cjs\` (PM2) and consumed by \`apps/proxy/server.mjs\` via env:
|
|
1620
|
+
|
|
1621
|
+
| Service | Port |
|
|
1622
|
+
| -------- | ---- |
|
|
1623
|
+
| proxy | 3030 |
|
|
1624
|
+
| backend | 3000 |
|
|
1625
|
+
| frontend | 3001 |
|
|
1626
|
+
|
|
1627
|
+
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.
|
|
1628
|
+
|
|
1629
|
+
## Deploy
|
|
1630
|
+
|
|
1631
|
+
- 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.
|
|
1632
|
+
- 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.
|
|
1633
|
+
|
|
1634
|
+
## Submodule discipline
|
|
1635
|
+
|
|
1636
|
+
- Do not edit files under \`apps/backend\` — they belong to the upstream \`apollo-cms\` repo.
|
|
1637
|
+
- After \`pnpm backend:update\`, run \`pnpm install\` (in case package.json changed) then \`pnpm backend:upgrade\` (replays data migrations that \`backend:setup\` skips).
|
|
1638
|
+
`;
|
|
1639
|
+
writeFileSync(resolve(targetDir, "CLAUDE.md"), content);
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1386
1642
|
function writeReadme(targetDir, dirName, frontendName, adminPrefix) {
|
|
1387
1643
|
const singleOrigin = !!adminPrefix;
|
|
1388
1644
|
|
|
@@ -1511,14 +1767,13 @@ folder + \`plugin.json#name\`.
|
|
|
1511
1767
|
Apollo CMS is tracked as a git submodule. Full upgrade flow:
|
|
1512
1768
|
|
|
1513
1769
|
\`\`\`bash
|
|
1514
|
-
pnpm backend:update # fast-forward apps/backend
|
|
1515
|
-
pnpm install # in case the submodule changed package.json
|
|
1770
|
+
pnpm backend:update # auto-stash, fast-forward apps/backend, run pnpm install
|
|
1516
1771
|
pnpm backend:upgrade # drizzle push + replay version migrations + seed
|
|
1517
1772
|
\`\`\`
|
|
1518
1773
|
|
|
1519
1774
|
| Script | What it does |
|
|
1520
1775
|
| --- | --- |
|
|
1521
|
-
| \`pnpm backend:update\` |
|
|
1776
|
+
| \`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. |
|
|
1522
1777
|
| \`pnpm backend:setup\` | First-time bootstrap: \`db:push\` + \`db:seed\` only |
|
|
1523
1778
|
| \`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. |
|
|
1524
1779
|
|
|
@@ -1659,7 +1914,7 @@ async function main() {
|
|
|
1659
1914
|
|
|
1660
1915
|
if (!flags.directory) {
|
|
1661
1916
|
log(HELP_TEXT);
|
|
1662
|
-
fatal("Please provide a directory name.\n Example: npx create-apollo-monorepo
|
|
1917
|
+
fatal("Please provide a directory name.\n Example: npx create-apollo-monorepo my-site");
|
|
1663
1918
|
}
|
|
1664
1919
|
|
|
1665
1920
|
log(`\n${COLORS.bold}${COLORS.cyan} Apollo CMS Monorepo Installer${COLORS.reset}\n`);
|
|
@@ -1678,7 +1933,7 @@ async function main() {
|
|
|
1678
1933
|
// Required by apollo-cms's /api/cron route and scripts/dev-cron.ts in dev.
|
|
1679
1934
|
// Without it the dev cron loop hits 403 "CRON_SECRET not configured".
|
|
1680
1935
|
const cronSecret = randomBytes(24).toString("hex");
|
|
1681
|
-
const backendInternalUrl =
|
|
1936
|
+
const backendInternalUrl = `http://localhost:${DEFAULT_BACKEND_PORT}`;
|
|
1682
1937
|
success(`Frontend pkg name: ${frontendName}`);
|
|
1683
1938
|
success(`Admin prefix: ${adminPrefix || "(disabled — separate origins)"}`);
|
|
1684
1939
|
|
|
@@ -1691,14 +1946,16 @@ async function main() {
|
|
|
1691
1946
|
writeRootGitignore(targetDir);
|
|
1692
1947
|
writeRootEnv(targetDir, { dbUrl, siteUrl, locale, authSecret, cronSecret, adminPrefix, backendInternalUrl });
|
|
1693
1948
|
writeReadme(targetDir, dirName, frontendName, adminPrefix);
|
|
1949
|
+
writeClaudeMd(targetDir, adminPrefix);
|
|
1694
1950
|
if (adminPrefix) writeNginxSample(targetDir, adminPrefix);
|
|
1695
1951
|
writeProxyApp(targetDir, dirName, adminPrefix);
|
|
1696
1952
|
writeCheckEnvScript(targetDir);
|
|
1953
|
+
writeWithEnvScript(targetDir);
|
|
1697
1954
|
writePm2Config(targetDir, dirName, adminPrefix);
|
|
1698
1955
|
success(
|
|
1699
|
-
`package.json, pnpm-workspace.yaml, .gitignore, .env.local, README.md${
|
|
1956
|
+
`package.json, pnpm-workspace.yaml, .gitignore, .env.local, README.md, CLAUDE.md${
|
|
1700
1957
|
adminPrefix ? ", nginx.conf.sample" : ""
|
|
1701
|
-
}, apps/proxy, ecosystem.config.cjs, scripts/check-env.mjs`,
|
|
1958
|
+
}, apps/proxy, ecosystem.config.cjs, scripts/check-env.mjs, scripts/with-env.mjs`,
|
|
1702
1959
|
);
|
|
1703
1960
|
|
|
1704
1961
|
// ── Step 4: git init ──
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-apollo-monorepo",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.2",
|
|
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"
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
],
|
|
22
22
|
"repository": {
|
|
23
23
|
"type": "git",
|
|
24
|
-
"url": "https://github.com/5Lab-Group-Co-Ltd/apollo-cms.git",
|
|
24
|
+
"url": "git+https://github.com/5Lab-Group-Co-Ltd/apollo-cms.git",
|
|
25
25
|
"directory": "installer-monorepo"
|
|
26
26
|
},
|
|
27
27
|
"license": "MIT"
|