create-apollo-monorepo 0.9.1 → 0.9.3
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 +373 -108
- package/package.json +3 -3
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
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
import { execSync } from "node:child_process";
|
|
18
18
|
import { randomBytes } from "node:crypto";
|
|
19
|
-
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
19
|
+
import { existsSync, mkdirSync, writeFileSync, symlinkSync, rmSync } from "node:fs";
|
|
20
20
|
import { resolve, basename } from "node:path";
|
|
21
21
|
import { createInterface } from "node:readline";
|
|
22
22
|
|
|
@@ -27,7 +27,14 @@ const BACKEND_BRANCH = "main";
|
|
|
27
27
|
const BACKEND_PATH = "apps/backend";
|
|
28
28
|
const FRONTEND_PATH = "apps/frontend";
|
|
29
29
|
const PROXY_PATH = "apps/proxy";
|
|
30
|
-
const MIN_NODE_MAJOR =
|
|
30
|
+
const MIN_NODE_MAJOR = 22;
|
|
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;
|
|
31
38
|
|
|
32
39
|
const COLORS = {
|
|
33
40
|
reset: "\x1b[0m",
|
|
@@ -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
|
|
@@ -418,33 +435,7 @@ function writeRootPackageJson(targetDir, dirName) {
|
|
|
418
435
|
devDependencies: {
|
|
419
436
|
concurrently: "^9.0.0",
|
|
420
437
|
},
|
|
421
|
-
engines: { node: ">=
|
|
422
|
-
pnpm: {
|
|
423
|
-
// apollo-cms transitively pulls multiple esbuild versions (0.18, 0.25, 0.27).
|
|
424
|
-
// pnpm's binary symlink can pick the wrong platform binary for esbuild's
|
|
425
|
-
// postinstall version check, failing with "Expected X.Y.Z but got A.B.C".
|
|
426
|
-
// Allow listed packages to run their build/postinstall scripts; the rest
|
|
427
|
-
// are skipped (pnpm 10 default is empty allow-list).
|
|
428
|
-
onlyBuiltDependencies: [
|
|
429
|
-
"@parcel/watcher",
|
|
430
|
-
"@rolldown/binding-darwin-arm64",
|
|
431
|
-
"@rolldown/binding-linux-arm64-gnu",
|
|
432
|
-
"@rolldown/binding-linux-x64-gnu",
|
|
433
|
-
"@sentry/cli",
|
|
434
|
-
"@swc/core",
|
|
435
|
-
"@swc/core-darwin-arm64",
|
|
436
|
-
"@swc/core-darwin-x64",
|
|
437
|
-
"@swc/core-linux-arm64-gnu",
|
|
438
|
-
"@swc/core-linux-x64-gnu",
|
|
439
|
-
"better-sqlite3",
|
|
440
|
-
"core-js",
|
|
441
|
-
"core-js-pure",
|
|
442
|
-
"esbuild",
|
|
443
|
-
"msw",
|
|
444
|
-
"sharp",
|
|
445
|
-
"unrs-resolver",
|
|
446
|
-
],
|
|
447
|
-
},
|
|
438
|
+
engines: { node: ">=22", pnpm: ">=10" },
|
|
448
439
|
};
|
|
449
440
|
writeFileSync(resolve(targetDir, "package.json"), JSON.stringify(pkg, null, 2) + "\n");
|
|
450
441
|
|
|
@@ -458,17 +449,77 @@ function writeRootPackageJson(targetDir, dirName) {
|
|
|
458
449
|
"strict-peer-dependencies=false",
|
|
459
450
|
"public-hoist-pattern[]=*esbuild*",
|
|
460
451
|
"public-hoist-pattern[]=*types*",
|
|
452
|
+
// Next.js 16's instrumentation hook (used by Sentry / OpenTelemetry
|
|
453
|
+
// shipped via apollo-cms) loads require-in-the-middle / import-in-the-middle
|
|
454
|
+
// at runtime via a bare require. Under pnpm's strict node_modules these
|
|
455
|
+
// sit deep inside .pnpm/ and the resolver can't see them — Next then
|
|
456
|
+
// crashes on boot with "Failed to load external module require-in-the-middle".
|
|
457
|
+
// Hoisting them (plus their `shimmer` dep) to the root node_modules fixes it.
|
|
458
|
+
"public-hoist-pattern[]=*require-in-the-middle*",
|
|
459
|
+
"public-hoist-pattern[]=*import-in-the-middle*",
|
|
460
|
+
"public-hoist-pattern[]=*shimmer*",
|
|
461
461
|
"shell-emulator=true",
|
|
462
462
|
"",
|
|
463
463
|
].join("\n"),
|
|
464
464
|
);
|
|
465
465
|
}
|
|
466
466
|
|
|
467
|
+
// Link an app's .env.local to the monorepo root .env.local so the root file
|
|
468
|
+
// stays the single source of truth. Next.js only reads .env.local from its
|
|
469
|
+
// own CWD, so this symlink lets `cd apps/<app> && next dev|build` see the
|
|
470
|
+
// shared config. Falls back to a regular copy (with a warning) on platforms
|
|
471
|
+
// where symlink creation requires elevation (Windows non-admin).
|
|
472
|
+
function linkRootEnvLocal(targetDir, appRelPath, fallbackLines) {
|
|
473
|
+
const envPath = resolve(targetDir, appRelPath, ".env.local");
|
|
474
|
+
// appRelPath is `apps/<name>` (2 segments deep) → `../../.env.local`.
|
|
475
|
+
const depth = appRelPath.split("/").filter(Boolean).length;
|
|
476
|
+
const linkTarget = "../".repeat(depth) + ".env.local";
|
|
477
|
+
if (existsSync(envPath)) rmSync(envPath);
|
|
478
|
+
try {
|
|
479
|
+
symlinkSync(linkTarget, envPath);
|
|
480
|
+
success(`${appRelPath}/.env.local → ${linkTarget} (symlink)`);
|
|
481
|
+
} catch {
|
|
482
|
+
writeFileSync(envPath, [...fallbackLines, ""].join("\n"));
|
|
483
|
+
warn(
|
|
484
|
+
`symlink failed — wrote a copy at ${appRelPath}/.env.local (keep it in sync with the root file manually).`,
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
467
489
|
function writePnpmWorkspace(targetDir) {
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
490
|
+
// pnpm 10 reads `onlyBuiltDependencies` from pnpm-workspace.yaml (the
|
|
491
|
+
// package.json#pnpm location is deprecated for workspaces). apollo-cms
|
|
492
|
+
// transitively pulls multiple esbuild versions (0.18, 0.25, 0.27); pnpm's
|
|
493
|
+
// binary symlink can pick the wrong platform binary for esbuild's
|
|
494
|
+
// postinstall version check, failing with "Expected X.Y.Z but got A.B.C".
|
|
495
|
+
// Allow listed packages to run their build/postinstall scripts; the rest
|
|
496
|
+
// are skipped (pnpm 10 default is empty allow-list).
|
|
497
|
+
const yaml = [
|
|
498
|
+
"packages:",
|
|
499
|
+
" - 'apps/*'",
|
|
500
|
+
" - 'apps/cms-plugins/*'",
|
|
501
|
+
"",
|
|
502
|
+
"onlyBuiltDependencies:",
|
|
503
|
+
" - '@parcel/watcher'",
|
|
504
|
+
" - '@rolldown/binding-darwin-arm64'",
|
|
505
|
+
" - '@rolldown/binding-linux-arm64-gnu'",
|
|
506
|
+
" - '@rolldown/binding-linux-x64-gnu'",
|
|
507
|
+
" - '@sentry/cli'",
|
|
508
|
+
" - '@swc/core'",
|
|
509
|
+
" - '@swc/core-darwin-arm64'",
|
|
510
|
+
" - '@swc/core-darwin-x64'",
|
|
511
|
+
" - '@swc/core-linux-arm64-gnu'",
|
|
512
|
+
" - '@swc/core-linux-x64-gnu'",
|
|
513
|
+
" - 'better-sqlite3'",
|
|
514
|
+
" - 'core-js'",
|
|
515
|
+
" - 'core-js-pure'",
|
|
516
|
+
" - 'esbuild'",
|
|
517
|
+
" - 'msw'",
|
|
518
|
+
" - 'sharp'",
|
|
519
|
+
" - 'unrs-resolver'",
|
|
520
|
+
"",
|
|
521
|
+
].join("\n");
|
|
522
|
+
writeFileSync(resolve(targetDir, "pnpm-workspace.yaml"), yaml);
|
|
472
523
|
}
|
|
473
524
|
|
|
474
525
|
// Single-origin nginx reference. Only emitted when adminPrefix is set, since
|
|
@@ -545,10 +596,43 @@ function writeProxyApp(targetDir, dirName, adminPrefix) {
|
|
|
545
596
|
|
|
546
597
|
import http from "node:http";
|
|
547
598
|
import net from "node:net";
|
|
599
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
600
|
+
import { resolve, dirname } from "node:path";
|
|
601
|
+
import { fileURLToPath } from "node:url";
|
|
602
|
+
|
|
603
|
+
// Load root .env.local so the proxy honors PROXY_PORT/FRONTEND_PORT/BACKEND_PORT
|
|
604
|
+
// alongside the frontend & backend. Process env still wins (12-factor friendly).
|
|
605
|
+
(function loadRootEnv() {
|
|
606
|
+
try {
|
|
607
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
608
|
+
const envPath = resolve(here, "../../.env.local");
|
|
609
|
+
if (!existsSync(envPath)) return;
|
|
610
|
+
for (const raw of readFileSync(envPath, "utf8").split(/\\r?\\n/)) {
|
|
611
|
+
const line = raw.trim();
|
|
612
|
+
if (!line || line.startsWith("#")) continue;
|
|
613
|
+
const eq = line.indexOf("=");
|
|
614
|
+
if (eq < 0) continue;
|
|
615
|
+
const key = line.slice(0, eq).trim();
|
|
616
|
+
if (!/^[A-Z_][A-Z0-9_]*$/i.test(key)) continue;
|
|
617
|
+
if (process.env[key] !== undefined) continue;
|
|
618
|
+
let val = line.slice(eq + 1).trim();
|
|
619
|
+
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
|
|
620
|
+
val = val.slice(1, -1);
|
|
621
|
+
}
|
|
622
|
+
process.env[key] = val;
|
|
623
|
+
}
|
|
624
|
+
} catch {
|
|
625
|
+
// Best-effort. The proxy still runs on its hardcoded fallbacks.
|
|
626
|
+
}
|
|
627
|
+
})();
|
|
548
628
|
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
const
|
|
629
|
+
// Backward-compatible: PROXY_PORT/FRONTEND_PORT/BACKEND_PORT are the new names;
|
|
630
|
+
// PORT/FRONTEND/BACKEND are still honored if explicitly set in process.env.
|
|
631
|
+
const PORT = Number(process.env.PROXY_PORT ?? process.env.PORT ?? 3030);
|
|
632
|
+
const BACKEND_HOST = process.env.BACKEND ?? \`http://127.0.0.1:\${process.env.BACKEND_PORT ?? 3002}\`;
|
|
633
|
+
const FRONTEND_HOST = process.env.FRONTEND ?? \`http://127.0.0.1:\${process.env.FRONTEND_PORT ?? 3001}\`;
|
|
634
|
+
const BACKEND = parseTarget(BACKEND_HOST);
|
|
635
|
+
const FRONTEND = parseTarget(FRONTEND_HOST);
|
|
552
636
|
|
|
553
637
|
const ADMIN_PREFIX = process.env.ADMIN_PREFIX ?? "${prefix}";
|
|
554
638
|
// Pipe-separated list of /api/<segment> paths that route to the backend.
|
|
@@ -762,7 +846,7 @@ function parseTarget(input) {
|
|
|
762
846
|
scripts: {
|
|
763
847
|
start: "node server.mjs",
|
|
764
848
|
},
|
|
765
|
-
engines: { node: ">=
|
|
849
|
+
engines: { node: ">=22" },
|
|
766
850
|
};
|
|
767
851
|
writeFileSync(resolve(dir, "package.json"), JSON.stringify(pkg, null, 2) + "\n");
|
|
768
852
|
|
|
@@ -830,6 +914,10 @@ The Node proxy is intended for dev and small self-hosted deploys.
|
|
|
830
914
|
}
|
|
831
915
|
|
|
832
916
|
function writeRootGitignore(targetDir) {
|
|
917
|
+
// NOTE: .env.local is intentionally NOT ignored — it holds shared dev port
|
|
918
|
+
// defaults (PROXY_PORT/FRONTEND_PORT/BACKEND_PORT) and other non-secret
|
|
919
|
+
// workspace knobs that should travel with the repo. Keep real secrets in
|
|
920
|
+
// .env (ignored) or apps/backend/.env.local (ignored by the submodule).
|
|
833
921
|
const ignore = [
|
|
834
922
|
"node_modules",
|
|
835
923
|
".pnpm-store",
|
|
@@ -838,8 +926,6 @@ function writeRootGitignore(targetDir) {
|
|
|
838
926
|
"dist",
|
|
839
927
|
"build",
|
|
840
928
|
".env",
|
|
841
|
-
".env.local",
|
|
842
|
-
".env*.local",
|
|
843
929
|
"*.log",
|
|
844
930
|
".DS_Store",
|
|
845
931
|
"",
|
|
@@ -847,16 +933,16 @@ function writeRootGitignore(targetDir) {
|
|
|
847
933
|
writeFileSync(resolve(targetDir, ".gitignore"), ignore);
|
|
848
934
|
}
|
|
849
935
|
|
|
850
|
-
function writeRootEnv(targetDir, { dbUrl, siteUrl, locale, authSecret, cronSecret, adminPrefix, backendInternalUrl }) {
|
|
936
|
+
function writeRootEnv(targetDir, { dbUrl, siteUrl, locale, authSecret, cronSecret, adminPrefix, backendInternalUrl, pm2Namespace }) {
|
|
851
937
|
const header = adminPrefix
|
|
852
938
|
? [
|
|
853
939
|
"# Single-origin monorepo dev env",
|
|
854
|
-
|
|
855
|
-
|
|
940
|
+
`# Public origin = frontend (apps/frontend on :${DEFAULT_FRONTEND_PORT}). Frontend rewrites /admin/*,`,
|
|
941
|
+
`# /api/*, /uploads/*, and the asset prefix to the backend (apps/backend on :${DEFAULT_BACKEND_PORT}).`,
|
|
856
942
|
]
|
|
857
943
|
: [
|
|
858
944
|
"# Separate-origins monorepo dev env",
|
|
859
|
-
|
|
945
|
+
`# Backend runs at http://localhost:${DEFAULT_BACKEND_PORT}, frontend at http://localhost:${DEFAULT_FRONTEND_PORT}.`,
|
|
860
946
|
"# To switch to single-origin: set APOLLO_ASSET_PREFIX (default: /admin) and",
|
|
861
947
|
"# BACKEND_INTERNAL_URL, then add rewrites() to apps/frontend/next.config.ts.",
|
|
862
948
|
];
|
|
@@ -864,10 +950,25 @@ function writeRootEnv(targetDir, { dbUrl, siteUrl, locale, authSecret, cronSecre
|
|
|
864
950
|
const lines = [
|
|
865
951
|
...header,
|
|
866
952
|
"",
|
|
953
|
+
"# ── Dev server ports ─────────────────────────────────────────────",
|
|
954
|
+
"# Single source of truth for local ports. Consumed by:",
|
|
955
|
+
"# • apps/proxy/server.mjs (PROXY_PORT, FRONTEND_PORT, BACKEND_PORT)",
|
|
956
|
+
"# • scripts/with-env.mjs (maps FRONTEND_PORT/BACKEND_PORT → PORT",
|
|
957
|
+
"# when launching apps/frontend & apps/backend,",
|
|
958
|
+
"# and derives NEXT_PUBLIC_SITE_URL /",
|
|
959
|
+
"# BACKEND_INTERNAL_URL when those are unset)",
|
|
960
|
+
"# Override any of these; the proxy/frontend/backend will follow.",
|
|
961
|
+
`PROXY_PORT=${DEFAULT_PROXY_PORT}`,
|
|
962
|
+
`FRONTEND_PORT=${DEFAULT_FRONTEND_PORT}`,
|
|
963
|
+
`BACKEND_PORT=${DEFAULT_BACKEND_PORT}`,
|
|
964
|
+
"",
|
|
867
965
|
"# ── Shared (consumed by both apps) ───────────────────────────────",
|
|
868
966
|
`DATABASE_URL=${dbUrl}`,
|
|
869
967
|
`APOLLO_SECRET=${authSecret}`,
|
|
870
968
|
`CRON_SECRET=${cronSecret}`,
|
|
969
|
+
"# Public origin. In dev, leave blank to auto-derive from FRONTEND_PORT",
|
|
970
|
+
"# (single-origin) or BACKEND_PORT (separate-origins) via scripts/with-env.mjs.",
|
|
971
|
+
"# In prod set to the real domain (e.g. https://cms.example.com).",
|
|
871
972
|
`NEXT_PUBLIC_SITE_URL=${siteUrl}`,
|
|
872
973
|
`NEXT_PUBLIC_DEFAULT_LOCALE=${locale}`,
|
|
873
974
|
"",
|
|
@@ -882,8 +983,15 @@ function writeRootEnv(targetDir, { dbUrl, siteUrl, locale, authSecret, cronSecre
|
|
|
882
983
|
`APOLLO_ASSET_PREFIX=${adminPrefix}`,
|
|
883
984
|
"# Project-specific plugins — keeps the apollo-cms submodule clean.",
|
|
884
985
|
`APOLLO_EXTRA_PLUGINS_DIR=../cms-plugins`,
|
|
986
|
+
"# PM2 namespace — groups this project's processes so you can",
|
|
987
|
+
"# `pm2 stop <namespace>` / `pm2 restart <namespace>` independently of",
|
|
988
|
+
"# other projects on the same host. Consumed by ecosystem.config.cjs.",
|
|
989
|
+
`PM2_NAMESPACE=${pm2Namespace}`,
|
|
885
990
|
"",
|
|
886
991
|
"# ── Frontend (apps/frontend) ─────────────────────────────────────",
|
|
992
|
+
"# Where the frontend's rewrites point internally. In dev, leave blank",
|
|
993
|
+
"# to auto-derive http://127.0.0.1:${BACKEND_PORT} via scripts/with-env.mjs.",
|
|
994
|
+
"# In prod set to a private hostname (e.g. http://backend.internal:3000).",
|
|
887
995
|
`BACKEND_INTERNAL_URL=${backendInternalUrl}`,
|
|
888
996
|
"",
|
|
889
997
|
);
|
|
@@ -892,6 +1000,10 @@ function writeRootEnv(targetDir, { dbUrl, siteUrl, locale, authSecret, cronSecre
|
|
|
892
1000
|
"# ── Backend (apps/backend) ───────────────────────────────────────",
|
|
893
1001
|
"# Project-specific plugins — keeps the apollo-cms submodule clean.",
|
|
894
1002
|
`APOLLO_EXTRA_PLUGINS_DIR=../cms-plugins`,
|
|
1003
|
+
"# PM2 namespace — groups this project's processes so you can",
|
|
1004
|
+
"# `pm2 stop <namespace>` / `pm2 restart <namespace>` independently of",
|
|
1005
|
+
"# other projects on the same host. Consumed by ecosystem.config.cjs.",
|
|
1006
|
+
`PM2_NAMESPACE=${pm2Namespace}`,
|
|
895
1007
|
"",
|
|
896
1008
|
"# ── Frontend (apps/frontend) ─────────────────────────────────────",
|
|
897
1009
|
`NEXT_PUBLIC_BACKEND_URL=${siteUrl}`,
|
|
@@ -913,9 +1025,12 @@ function writeFrontendApp(targetDir, frontendName, siteUrl, adminPrefix, backend
|
|
|
913
1025
|
version: "0.0.0",
|
|
914
1026
|
private: true,
|
|
915
1027
|
scripts: {
|
|
916
|
-
|
|
1028
|
+
// No -p flag: Next.js reads PORT from env. The repo-root scripts use
|
|
1029
|
+
// scripts/with-env.mjs --port=FRONTEND_PORT to inject the right value
|
|
1030
|
+
// from .env.local (default 3001).
|
|
1031
|
+
dev: "next dev",
|
|
917
1032
|
build: "next build",
|
|
918
|
-
start: "next start
|
|
1033
|
+
start: "next start",
|
|
919
1034
|
lint: "next lint",
|
|
920
1035
|
typecheck: "tsc --noEmit",
|
|
921
1036
|
},
|
|
@@ -1045,7 +1160,8 @@ export default config;
|
|
|
1045
1160
|
|
|
1046
1161
|
writeFileSync(
|
|
1047
1162
|
resolve(dir, ".gitignore"),
|
|
1048
|
-
|
|
1163
|
+
// .env.local is intentionally committed — it only contains dev port hints.
|
|
1164
|
+
["node_modules", ".next", "dist", ""].join("\n"),
|
|
1049
1165
|
);
|
|
1050
1166
|
|
|
1051
1167
|
// Vercel project config for the frontend. Skip cron + region pinning is
|
|
@@ -1273,6 +1389,88 @@ console.log(\`Run: pnpm install && pnpm cms-plugins:build && pnpm dev:backend\`)
|
|
|
1273
1389
|
writeFileSync(resolve(scriptsDir, "new-cms-plugin.mjs"), script);
|
|
1274
1390
|
}
|
|
1275
1391
|
|
|
1392
|
+
// Small launcher that loads root .env.local, applies port defaults
|
|
1393
|
+
// (PROXY_PORT=3030 / FRONTEND_PORT=3001 / BACKEND_PORT=3002), optionally maps
|
|
1394
|
+
// one of them onto PORT (Next.js reads PORT), and execs the rest of argv.
|
|
1395
|
+
//
|
|
1396
|
+
// Usage:
|
|
1397
|
+
// node scripts/with-env.mjs [--port=VAR_NAME] <command> [args...]
|
|
1398
|
+
function writeWithEnvScript(targetDir) {
|
|
1399
|
+
const scriptsDir = resolve(targetDir, "scripts");
|
|
1400
|
+
mkdirSync(scriptsDir, { recursive: true });
|
|
1401
|
+
const script = `#!/usr/bin/env node
|
|
1402
|
+
// Loads <repo-root>/.env.local so frontend / backend / proxy all share one
|
|
1403
|
+
// source of truth for ports. Process env still wins.
|
|
1404
|
+
//
|
|
1405
|
+
// Usage:
|
|
1406
|
+
// node scripts/with-env.mjs [--port=VAR_NAME] <command> [args...]
|
|
1407
|
+
// --port=FRONTEND_PORT copies process.env.FRONTEND_PORT onto PORT before exec,
|
|
1408
|
+
// so \`next dev\` and \`next start\` pick up the right port without --port flags.
|
|
1409
|
+
|
|
1410
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
1411
|
+
import { spawn } from "node:child_process";
|
|
1412
|
+
import { dirname, resolve } from "node:path";
|
|
1413
|
+
import { fileURLToPath } from "node:url";
|
|
1414
|
+
|
|
1415
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
1416
|
+
const envPath = resolve(here, "../.env.local");
|
|
1417
|
+
|
|
1418
|
+
if (existsSync(envPath)) {
|
|
1419
|
+
for (const raw of readFileSync(envPath, "utf8").split(/\\r?\\n/)) {
|
|
1420
|
+
const line = raw.trim();
|
|
1421
|
+
if (!line || line.startsWith("#")) continue;
|
|
1422
|
+
const eq = line.indexOf("=");
|
|
1423
|
+
if (eq < 0) continue;
|
|
1424
|
+
const k = line.slice(0, eq).trim();
|
|
1425
|
+
if (!/^[A-Z_][A-Z0-9_]*$/i.test(k)) continue;
|
|
1426
|
+
if (process.env[k] !== undefined) continue;
|
|
1427
|
+
let v = line.slice(eq + 1).trim();
|
|
1428
|
+
if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) {
|
|
1429
|
+
v = v.slice(1, -1);
|
|
1430
|
+
}
|
|
1431
|
+
process.env[k] = v;
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
// Fallbacks — keep in sync with apps/proxy/server.mjs and PM2 config.
|
|
1436
|
+
process.env.PROXY_PORT ||= "3030";
|
|
1437
|
+
process.env.FRONTEND_PORT ||= "3001";
|
|
1438
|
+
process.env.BACKEND_PORT ||= "3002";
|
|
1439
|
+
|
|
1440
|
+
// Derive URL vars from PORTs when unset. Dev-friendly: edit a port and
|
|
1441
|
+
// everything follows. Prod-friendly: set these explicitly to real hostnames
|
|
1442
|
+
// (e.g. https://cms.example.com, http://backend.internal:3000) and the
|
|
1443
|
+
// derivation is bypassed.
|
|
1444
|
+
//
|
|
1445
|
+
// NEXT_PUBLIC_SITE_URL is the public origin — defaults to the frontend port
|
|
1446
|
+
// (frontend rewrites /admin, /api, /uploads to the backend). For \`pnpm dev:rp\`
|
|
1447
|
+
// set it to http://localhost:\${PROXY_PORT} so Better Auth / OAuth callbacks
|
|
1448
|
+
// land on the unified origin.
|
|
1449
|
+
process.env.NEXT_PUBLIC_SITE_URL ||= \`http://localhost:\${process.env.FRONTEND_PORT}\`;
|
|
1450
|
+
process.env.BACKEND_INTERNAL_URL ||= \`http://127.0.0.1:\${process.env.BACKEND_PORT}\`;
|
|
1451
|
+
|
|
1452
|
+
const args = process.argv.slice(2);
|
|
1453
|
+
if (args[0]?.startsWith("--port=")) {
|
|
1454
|
+
const name = args.shift().slice(7);
|
|
1455
|
+
if (process.env[name]) process.env.PORT = process.env[name];
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
if (!args.length) {
|
|
1459
|
+
console.error("Usage: node scripts/with-env.mjs [--port=VAR] <command> [args...]");
|
|
1460
|
+
process.exit(2);
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
const [cmd, ...rest] = args;
|
|
1464
|
+
const child = spawn(cmd, rest, {
|
|
1465
|
+
stdio: "inherit",
|
|
1466
|
+
env: process.env,
|
|
1467
|
+
shell: process.platform === "win32",
|
|
1468
|
+
});
|
|
1469
|
+
child.on("exit", (code, sig) => process.exit(sig ? 1 : code ?? 0));
|
|
1470
|
+
`;
|
|
1471
|
+
writeFileSync(resolve(scriptsDir, "with-env.mjs"), script);
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1276
1474
|
// Pre-build env check. Fails the build before \`next build\` runs if backend
|
|
1277
1475
|
// required vars are missing, so users don't sit through a 30s build only to
|
|
1278
1476
|
// hit a runtime error on first request.
|
|
@@ -1332,55 +1530,111 @@ console.log("✓ env check passed");
|
|
|
1332
1530
|
// since Next.js handles that internally and the proxy is single-threaded by design).
|
|
1333
1531
|
function writePm2Config(targetDir, dirName, adminPrefix) {
|
|
1334
1532
|
const singleOrigin = !!adminPrefix;
|
|
1533
|
+
// The proxy `env` block conditionally includes ADMIN_PREFIX in single-origin
|
|
1534
|
+
// mode. Built outside the template so we don't emit a stray comma or blank line.
|
|
1535
|
+
const proxyEnv = [
|
|
1536
|
+
"NODE_ENV: 'production',",
|
|
1537
|
+
"PROXY_PORT,",
|
|
1538
|
+
"FRONTEND_PORT,",
|
|
1539
|
+
"BACKEND_PORT,",
|
|
1540
|
+
...(singleOrigin ? [`ADMIN_PREFIX: '${adminPrefix}',`] : []),
|
|
1541
|
+
].map((l) => " " + l).join("\n");
|
|
1542
|
+
|
|
1335
1543
|
const config = `// PM2 ecosystem config for ${dirName}.
|
|
1544
|
+
//
|
|
1545
|
+
// Loads .env.local at the top so PM2 picks up PROXY_PORT / FRONTEND_PORT /
|
|
1546
|
+
// BACKEND_PORT / PM2_NAMESPACE without a shell wrapper. The .env.local file
|
|
1547
|
+
// is the single source of truth — edit ports there, not here.
|
|
1548
|
+
//
|
|
1336
1549
|
// Usage:
|
|
1337
1550
|
// pnpm install
|
|
1338
1551
|
// pnpm backend:upgrade
|
|
1339
1552
|
// pnpm build
|
|
1340
|
-
// pm2 start ecosystem.config.cjs # FE + BE
|
|
1341
|
-
// pm2 start ecosystem.config.cjs --only proxy #
|
|
1553
|
+
// pm2 start ecosystem.config.cjs # FE + BE + proxy + cron
|
|
1554
|
+
// pm2 start ecosystem.config.cjs --only proxy # only the reverse proxy
|
|
1342
1555
|
// pm2 start ecosystem.config.cjs --only cron # self-hosted cron fallback
|
|
1556
|
+
// pm2 stop \${PM2_NAMESPACE} # stop everything in this project
|
|
1343
1557
|
// pm2 save && pm2 startup # persist across reboots
|
|
1344
1558
|
|
|
1559
|
+
const fs = require('fs');
|
|
1560
|
+
const path = require('path');
|
|
1561
|
+
|
|
1562
|
+
// Inline .env.local parser. Kept tiny (no dotenv dep) so the ecosystem file
|
|
1563
|
+
// stays runnable before \`pnpm install\` finishes.
|
|
1564
|
+
const envPath = path.resolve(__dirname, '.env.local');
|
|
1565
|
+
if (fs.existsSync(envPath)) {
|
|
1566
|
+
for (const line of fs.readFileSync(envPath, 'utf8').split('\\n')) {
|
|
1567
|
+
const m = line.match(/^\\s*([A-Z0-9_]+)\\s*=\\s*(.*?)\\s*$/i);
|
|
1568
|
+
if (!m) continue;
|
|
1569
|
+
const k = m[1];
|
|
1570
|
+
let v = m[2];
|
|
1571
|
+
if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) v = v.slice(1, -1);
|
|
1572
|
+
if (process.env[k] === undefined) process.env[k] = v;
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
const NAMESPACE = process.env.PM2_NAMESPACE || '${dirName}';
|
|
1577
|
+
const FRONTEND_PORT = Number(process.env.FRONTEND_PORT) || 3001;
|
|
1578
|
+
const BACKEND_PORT = Number(process.env.BACKEND_PORT) || 3002;
|
|
1579
|
+
const PROXY_PORT = Number(process.env.PROXY_PORT) || 3030;
|
|
1580
|
+
|
|
1581
|
+
// Backend's internal URL (loopback). Cron MUST target this, never the proxy or
|
|
1582
|
+
// frontend — going through the FE just adds a failure mode (HTML error → JSON
|
|
1583
|
+
// parse blowup in dev-cron.ts).
|
|
1584
|
+
const BACKEND_INTERNAL_URL = process.env.BACKEND_INTERNAL_URL || \`http://127.0.0.1:\${BACKEND_PORT}\`;
|
|
1585
|
+
|
|
1345
1586
|
module.exports = {
|
|
1346
1587
|
apps: [
|
|
1347
1588
|
{
|
|
1348
|
-
name:
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1589
|
+
name: \`frontend:\${FRONTEND_PORT}\`,
|
|
1590
|
+
namespace: NAMESPACE,
|
|
1591
|
+
cwd: './apps/frontend',
|
|
1592
|
+
script: 'node_modules/next/dist/bin/next',
|
|
1593
|
+
args: 'start',
|
|
1594
|
+
env: {
|
|
1595
|
+
NODE_ENV: 'production',
|
|
1596
|
+
PORT: FRONTEND_PORT,
|
|
1597
|
+
// Next.js rewrites read this at request time. Without it the FE
|
|
1598
|
+
// falls back to http://localhost:3000 and every /admin, /api, and
|
|
1599
|
+
// /uploads request returns ECONNREFUSED.
|
|
1600
|
+
BACKEND_INTERNAL_URL,
|
|
1601
|
+
},
|
|
1602
|
+
max_memory_restart: '1G',
|
|
1354
1603
|
autorestart: true,
|
|
1355
1604
|
},
|
|
1356
1605
|
{
|
|
1357
|
-
name:
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1606
|
+
name: \`backend:\${BACKEND_PORT}\`,
|
|
1607
|
+
namespace: NAMESPACE,
|
|
1608
|
+
cwd: './apps/backend',
|
|
1609
|
+
script: 'node_modules/next/dist/bin/next',
|
|
1610
|
+
args: 'start',
|
|
1611
|
+
env: { NODE_ENV: 'production', PORT: BACKEND_PORT },
|
|
1612
|
+
max_memory_restart: '1G',
|
|
1363
1613
|
autorestart: true,
|
|
1364
1614
|
},
|
|
1365
1615
|
{
|
|
1366
|
-
name:
|
|
1367
|
-
|
|
1616
|
+
name: \`proxy:\${PROXY_PORT}\`,
|
|
1617
|
+
namespace: NAMESPACE,
|
|
1618
|
+
script: './apps/proxy/server.mjs',
|
|
1368
1619
|
env: {
|
|
1369
|
-
|
|
1370
|
-
PORT: 3030,
|
|
1371
|
-
BACKEND: "http://127.0.0.1:3000",
|
|
1372
|
-
FRONTEND: "http://127.0.0.1:3001",
|
|
1373
|
-
${singleOrigin ? `ADMIN_PREFIX: "${adminPrefix}",` : ""}
|
|
1620
|
+
${proxyEnv}
|
|
1374
1621
|
},
|
|
1375
|
-
max_memory_restart:
|
|
1622
|
+
max_memory_restart: '256M',
|
|
1376
1623
|
autorestart: true,
|
|
1377
1624
|
},
|
|
1378
1625
|
{
|
|
1379
|
-
name:
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1626
|
+
name: 'cron',
|
|
1627
|
+
namespace: NAMESPACE,
|
|
1628
|
+
cwd: './apps/backend',
|
|
1629
|
+
script: 'scripts/dev-cron.ts',
|
|
1630
|
+
interpreter: 'bun',
|
|
1631
|
+
env: {
|
|
1632
|
+
NODE_ENV: 'production',
|
|
1633
|
+
// dev-cron.ts derives its target from NEXT_PUBLIC_SITE_URL. Override
|
|
1634
|
+
// it to the backend loopback so cron skips the FE/proxy entirely.
|
|
1635
|
+
NEXT_PUBLIC_SITE_URL: BACKEND_INTERNAL_URL,
|
|
1636
|
+
CRON_SECRET: process.env.CRON_SECRET,
|
|
1637
|
+
},
|
|
1384
1638
|
autorestart: true,
|
|
1385
1639
|
},
|
|
1386
1640
|
],
|
|
@@ -1659,12 +1913,18 @@ The installer scaffolds \`ecosystem.config.cjs\`. To supervise everything:
|
|
|
1659
1913
|
|
|
1660
1914
|
\`\`\`bash
|
|
1661
1915
|
pm2 start ecosystem.config.cjs # FE + BE + proxy + cron (all four)
|
|
1662
|
-
pm2 start ecosystem.config.cjs --only
|
|
1916
|
+
pm2 start ecosystem.config.cjs --only proxy # only the reverse proxy
|
|
1663
1917
|
pm2 save && pm2 startup # persist across reboots
|
|
1664
|
-
pm2 logs
|
|
1665
|
-
pm2 reload
|
|
1918
|
+
pm2 logs \${PM2_NAMESPACE:-${dirName}} # tail just this project's logs
|
|
1919
|
+
pm2 reload \${PM2_NAMESPACE:-${dirName}} # zero-downtime restart (namespace-scoped)
|
|
1920
|
+
pm2 stop \${PM2_NAMESPACE:-${dirName}} # stop only this project
|
|
1666
1921
|
\`\`\`
|
|
1667
1922
|
|
|
1923
|
+
All four processes are grouped under the \`PM2_NAMESPACE\` set in \`.env.local\`
|
|
1924
|
+
(defaults to \`${dirName}\`), so namespace commands target only this project
|
|
1925
|
+
even when other PM2 apps share the host. Process names embed the bound port
|
|
1926
|
+
(\`frontend:3001\`, \`backend:3002\`, \`proxy:3030\`) for quick \`pm2 ls\` triage.
|
|
1927
|
+
|
|
1668
1928
|
The proxy and cron processes can be omitted with \`--only\` if you front the
|
|
1669
1929
|
apps with nginx/Caddy or use platform cron (system cron, k8s CronJob).
|
|
1670
1930
|
|
|
@@ -1758,7 +2018,7 @@ async function main() {
|
|
|
1758
2018
|
|
|
1759
2019
|
if (!flags.directory) {
|
|
1760
2020
|
log(HELP_TEXT);
|
|
1761
|
-
fatal("Please provide a directory name.\n Example: npx create-apollo-monorepo
|
|
2021
|
+
fatal("Please provide a directory name.\n Example: npx create-apollo-monorepo my-site");
|
|
1762
2022
|
}
|
|
1763
2023
|
|
|
1764
2024
|
log(`\n${COLORS.bold}${COLORS.cyan} Apollo CMS Monorepo Installer${COLORS.reset}\n`);
|
|
@@ -1777,7 +2037,7 @@ async function main() {
|
|
|
1777
2037
|
// Required by apollo-cms's /api/cron route and scripts/dev-cron.ts in dev.
|
|
1778
2038
|
// Without it the dev cron loop hits 403 "CRON_SECRET not configured".
|
|
1779
2039
|
const cronSecret = randomBytes(24).toString("hex");
|
|
1780
|
-
const backendInternalUrl =
|
|
2040
|
+
const backendInternalUrl = `http://localhost:${DEFAULT_BACKEND_PORT}`;
|
|
1781
2041
|
success(`Frontend pkg name: ${frontendName}`);
|
|
1782
2042
|
success(`Admin prefix: ${adminPrefix || "(disabled — separate origins)"}`);
|
|
1783
2043
|
|
|
@@ -1788,17 +2048,18 @@ async function main() {
|
|
|
1788
2048
|
writeRootPackageJson(targetDir, dirName);
|
|
1789
2049
|
writePnpmWorkspace(targetDir);
|
|
1790
2050
|
writeRootGitignore(targetDir);
|
|
1791
|
-
writeRootEnv(targetDir, { dbUrl, siteUrl, locale, authSecret, cronSecret, adminPrefix, backendInternalUrl });
|
|
2051
|
+
writeRootEnv(targetDir, { dbUrl, siteUrl, locale, authSecret, cronSecret, adminPrefix, backendInternalUrl, pm2Namespace: dirName });
|
|
1792
2052
|
writeReadme(targetDir, dirName, frontendName, adminPrefix);
|
|
1793
2053
|
writeClaudeMd(targetDir, adminPrefix);
|
|
1794
2054
|
if (adminPrefix) writeNginxSample(targetDir, adminPrefix);
|
|
1795
2055
|
writeProxyApp(targetDir, dirName, adminPrefix);
|
|
1796
2056
|
writeCheckEnvScript(targetDir);
|
|
2057
|
+
writeWithEnvScript(targetDir);
|
|
1797
2058
|
writePm2Config(targetDir, dirName, adminPrefix);
|
|
1798
2059
|
success(
|
|
1799
2060
|
`package.json, pnpm-workspace.yaml, .gitignore, .env.local, README.md, CLAUDE.md${
|
|
1800
2061
|
adminPrefix ? ", nginx.conf.sample" : ""
|
|
1801
|
-
}, apps/proxy, ecosystem.config.cjs, scripts/check-env.mjs`,
|
|
2062
|
+
}, apps/proxy, ecosystem.config.cjs, scripts/check-env.mjs, scripts/with-env.mjs`,
|
|
1802
2063
|
);
|
|
1803
2064
|
|
|
1804
2065
|
// ── Step 4: git init ──
|
|
@@ -1838,23 +2099,27 @@ async function main() {
|
|
|
1838
2099
|
);
|
|
1839
2100
|
}
|
|
1840
2101
|
|
|
1841
|
-
|
|
1842
|
-
const backendEnvLines = [
|
|
2102
|
+
const backendFallback = [
|
|
1843
2103
|
`DATABASE_URL=${dbUrl}`,
|
|
1844
2104
|
`APOLLO_SECRET=${authSecret}`,
|
|
1845
2105
|
`CRON_SECRET=${cronSecret}`,
|
|
1846
2106
|
`NEXT_PUBLIC_SITE_URL=${siteUrl}`,
|
|
1847
2107
|
`NEXT_PUBLIC_DEFAULT_LOCALE=${locale}`,
|
|
1848
2108
|
`APOLLO_EXTRA_PLUGINS_DIR=../cms-plugins`,
|
|
2109
|
+
`PM2_NAMESPACE=${dirName}`,
|
|
1849
2110
|
];
|
|
1850
|
-
if (adminPrefix) {
|
|
1851
|
-
|
|
1852
|
-
}
|
|
1853
|
-
backendEnvLines.push("");
|
|
1854
|
-
writeFileSync(resolve(targetDir, BACKEND_PATH, ".env.local"), backendEnvLines.join("\n"));
|
|
1855
|
-
success("apps/backend/.env.local");
|
|
2111
|
+
if (adminPrefix) backendFallback.push(`APOLLO_ASSET_PREFIX=${adminPrefix}`);
|
|
2112
|
+
linkRootEnvLocal(targetDir, BACKEND_PATH, backendFallback);
|
|
1856
2113
|
}
|
|
1857
2114
|
|
|
2115
|
+
// Frontend gets the same treatment — Next.js running in apps/frontend
|
|
2116
|
+
// can't see the root file otherwise, and next.config.ts references
|
|
2117
|
+
// BACKEND_INTERNAL_URL for the rewrites destination.
|
|
2118
|
+
const frontendFallback = adminPrefix
|
|
2119
|
+
? [`BACKEND_INTERNAL_URL=${backendInternalUrl}`]
|
|
2120
|
+
: [`NEXT_PUBLIC_BACKEND_URL=${siteUrl}`];
|
|
2121
|
+
linkRootEnvLocal(targetDir, FRONTEND_PATH, frontendFallback);
|
|
2122
|
+
|
|
1858
2123
|
// ── Step 7: Install ──
|
|
1859
2124
|
if (flags.skipInstall) {
|
|
1860
2125
|
step(7, "Skipping dependency installation (--skip-install)");
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-apollo-monorepo",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.3",
|
|
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"
|
|
7
7
|
},
|
|
8
8
|
"type": "module",
|
|
9
9
|
"engines": {
|
|
10
|
-
"node": ">=
|
|
10
|
+
"node": ">=22"
|
|
11
11
|
},
|
|
12
12
|
"keywords": [
|
|
13
13
|
"apollo",
|
|
@@ -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"
|