create-apollo-monorepo 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.mjs +331 -6
- package/package.json +1 -1
package/index.mjs
CHANGED
|
@@ -26,6 +26,7 @@ const BACKEND_REPO_URL = "https://github.com/5Lab-Group-Co-Ltd/apollo-cms.git";
|
|
|
26
26
|
const BACKEND_BRANCH = "main";
|
|
27
27
|
const BACKEND_PATH = "apps/backend";
|
|
28
28
|
const FRONTEND_PATH = "apps/frontend";
|
|
29
|
+
const PROXY_PATH = "apps/proxy";
|
|
29
30
|
const MIN_NODE_MAJOR = 20;
|
|
30
31
|
|
|
31
32
|
const COLORS = {
|
|
@@ -355,6 +356,12 @@ function writeRootPackageJson(targetDir, dirName) {
|
|
|
355
356
|
// dist/server.mjs on next request.
|
|
356
357
|
dev:
|
|
357
358
|
"pnpm predev:setup && concurrently -k -n FE,BE,PL -c blue,magenta,yellow \"pnpm --filter ./apps/frontend dev\" \"pnpm --filter ./apps/backend exec next dev\" \"pnpm cms-plugins:dev\"",
|
|
359
|
+
// Optional single-origin dev: same as `pnpm dev` but adds a Node.js
|
|
360
|
+
// reverse proxy on :3030 that fronts FE :3001 + BE :3000. Use it when
|
|
361
|
+
// you want one URL, shared cookies, and no CORS — without nginx.
|
|
362
|
+
"dev:rp":
|
|
363
|
+
"pnpm predev:setup && concurrently -k -n FE,BE,PL,PX -c blue,magenta,yellow,cyan \"pnpm --filter ./apps/frontend dev\" \"pnpm --filter ./apps/backend exec next dev\" \"pnpm cms-plugins:dev\" \"pnpm dev:proxy\"",
|
|
364
|
+
"dev:proxy": "node apps/proxy/server.mjs",
|
|
358
365
|
"dev:frontend": "pnpm --filter ./apps/frontend dev",
|
|
359
366
|
"dev:backend":
|
|
360
367
|
"pnpm predev:setup && pnpm --filter ./apps/backend exec next dev",
|
|
@@ -487,6 +494,312 @@ server {
|
|
|
487
494
|
writeFileSync(resolve(targetDir, "nginx.conf.sample"), conf);
|
|
488
495
|
}
|
|
489
496
|
|
|
497
|
+
// Optional Node.js reverse proxy. Mirrors nginx.conf.sample routing rules
|
|
498
|
+
// but runs in-process — opt in via `pnpm dev:rp`. Zero deps, ~80 LOC.
|
|
499
|
+
function writeProxyApp(targetDir, dirName, adminPrefix) {
|
|
500
|
+
const dir = resolve(targetDir, PROXY_PATH);
|
|
501
|
+
mkdirSync(dir, { recursive: true });
|
|
502
|
+
|
|
503
|
+
const prefix = adminPrefix || "/admin";
|
|
504
|
+
const server = `// ${dirName} — single-origin reverse proxy (zero deps).
|
|
505
|
+
// Mirrors ../../nginx.conf.sample. Run via \`pnpm dev:rp\` from the repo root.
|
|
506
|
+
//
|
|
507
|
+
// Routes (in order):
|
|
508
|
+
// ${prefix}, ${prefix}/*, /uploads/*, /monitoring → backend
|
|
509
|
+
// /api/editing-presence/* → backend (SSE)
|
|
510
|
+
// /api/{auth,v1,cron,email,health,mcp,admin,...}/* → backend
|
|
511
|
+
// /__proxy/health → 200 OK (proxy itself)
|
|
512
|
+
// everything else → frontend
|
|
513
|
+
//
|
|
514
|
+
// Production note: this proxy doesn't terminate TLS. For HTTPS, sit behind a
|
|
515
|
+
// load balancer (Caddy, Cloudflare, ALB) or use \`nginx.conf.sample\` instead.
|
|
516
|
+
|
|
517
|
+
import http from "node:http";
|
|
518
|
+
import net from "node:net";
|
|
519
|
+
|
|
520
|
+
const PORT = Number(process.env.PORT ?? 3030);
|
|
521
|
+
const BACKEND = parseTarget(process.env.BACKEND ?? "http://127.0.0.1:3000");
|
|
522
|
+
const FRONTEND = parseTarget(process.env.FRONTEND ?? "http://127.0.0.1:3001");
|
|
523
|
+
|
|
524
|
+
const ADMIN_PREFIX = process.env.ADMIN_PREFIX ?? "${prefix}";
|
|
525
|
+
// Pipe-separated list of /api/<segment> paths that route to the backend.
|
|
526
|
+
const BACKEND_API_SEGMENTS = (
|
|
527
|
+
process.env.BACKEND_API_PATHS ??
|
|
528
|
+
"auth|v1|cron|email|health|mcp|admin|editing-presence"
|
|
529
|
+
).trim();
|
|
530
|
+
const BACKEND_API = new RegExp(\`^/api/(\${BACKEND_API_SEGMENTS})(/|$)\`);
|
|
531
|
+
|
|
532
|
+
// 50MB matches nginx.conf.sample's client_max_body_size and Next.js's
|
|
533
|
+
// Server Actions body limit. Override via MAX_BODY_BYTES (0 = unlimited).
|
|
534
|
+
const MAX_BODY_BYTES = Number(process.env.MAX_BODY_BYTES ?? 50 * 1024 * 1024);
|
|
535
|
+
const UPSTREAM_TIMEOUT_MS = Number(process.env.UPSTREAM_TIMEOUT_MS ?? 60_000);
|
|
536
|
+
// When false (default), strip incoming X-Forwarded-* before adding our own —
|
|
537
|
+
// prevents header spoofing when the proxy faces the public internet directly.
|
|
538
|
+
// Set TRUST_PROXY=1 only when behind another trusted reverse proxy (CDN, LB).
|
|
539
|
+
const TRUST_PROXY = process.env.TRUST_PROXY === "1";
|
|
540
|
+
|
|
541
|
+
// Hop-by-hop headers (RFC 7230 §6.1) — must not be forwarded.
|
|
542
|
+
const HOP_BY_HOP = new Set([
|
|
543
|
+
"connection",
|
|
544
|
+
"keep-alive",
|
|
545
|
+
"proxy-authenticate",
|
|
546
|
+
"proxy-authorization",
|
|
547
|
+
"te",
|
|
548
|
+
"trailer",
|
|
549
|
+
"transfer-encoding",
|
|
550
|
+
"upgrade",
|
|
551
|
+
]);
|
|
552
|
+
|
|
553
|
+
// Reuse TCP connections to upstreams — eliminates ~1ms per request.
|
|
554
|
+
const backendAgent = new http.Agent({ keepAlive: true });
|
|
555
|
+
const frontendAgent = new http.Agent({ keepAlive: true });
|
|
556
|
+
|
|
557
|
+
function pickUpstream(url) {
|
|
558
|
+
if (url === ADMIN_PREFIX || url.startsWith(\`\${ADMIN_PREFIX}/\`))
|
|
559
|
+
return { ...BACKEND, agent: backendAgent };
|
|
560
|
+
if (url.startsWith("/uploads/")) return { ...BACKEND, agent: backendAgent };
|
|
561
|
+
if (url === "/monitoring") return { ...BACKEND, agent: backendAgent };
|
|
562
|
+
if (BACKEND_API.test(url)) return { ...BACKEND, agent: backendAgent };
|
|
563
|
+
return { ...FRONTEND, agent: frontendAgent };
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function sanitizeHeaders(req, clientIp) {
|
|
567
|
+
const out = {};
|
|
568
|
+
for (const [k, v] of Object.entries(req.headers)) {
|
|
569
|
+
const lower = k.toLowerCase();
|
|
570
|
+
if (HOP_BY_HOP.has(lower)) continue;
|
|
571
|
+
// Strip client-supplied X-Forwarded-* unless TRUST_PROXY is set.
|
|
572
|
+
if (!TRUST_PROXY && lower.startsWith("x-forwarded-")) continue;
|
|
573
|
+
out[k] = v;
|
|
574
|
+
}
|
|
575
|
+
// Always set X-Forwarded-* with our own values (append client IP).
|
|
576
|
+
const existingFor = TRUST_PROXY ? req.headers["x-forwarded-for"] : undefined;
|
|
577
|
+
out["x-forwarded-for"] = existingFor ? \`\${existingFor}, \${clientIp}\` : clientIp;
|
|
578
|
+
out["x-forwarded-proto"] = TRUST_PROXY
|
|
579
|
+
? req.headers["x-forwarded-proto"] ?? "http"
|
|
580
|
+
: "http";
|
|
581
|
+
out["x-forwarded-host"] = TRUST_PROXY
|
|
582
|
+
? req.headers["x-forwarded-host"] ?? req.headers.host ?? ""
|
|
583
|
+
: req.headers.host ?? "";
|
|
584
|
+
return out;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function logError(scope, err, extra) {
|
|
588
|
+
const ts = new Date().toISOString();
|
|
589
|
+
const detail = extra ? \` \${JSON.stringify(extra)}\` : "";
|
|
590
|
+
console.error(\`[\${ts}] proxy:\${scope} \${err.code ?? ""} \${err.message}\${detail}\`);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
const server = http.createServer((req, res) => {
|
|
594
|
+
// Health check (proxy itself, never forwarded).
|
|
595
|
+
if (req.url === "/__proxy/health") {
|
|
596
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
597
|
+
res.end(JSON.stringify({ status: "ok", uptime: process.uptime() }));
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// Enforce body size limit (DoS protection).
|
|
602
|
+
if (MAX_BODY_BYTES > 0) {
|
|
603
|
+
const declared = Number(req.headers["content-length"] ?? 0);
|
|
604
|
+
if (declared > MAX_BODY_BYTES) {
|
|
605
|
+
res.writeHead(413, { "content-type": "text/plain" });
|
|
606
|
+
res.end("Payload Too Large");
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
let received = 0;
|
|
610
|
+
req.on("data", (chunk) => {
|
|
611
|
+
received += chunk.length;
|
|
612
|
+
if (received > MAX_BODY_BYTES) {
|
|
613
|
+
req.destroy(new Error("Body exceeded MAX_BODY_BYTES"));
|
|
614
|
+
}
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
const upstream = pickUpstream(req.url);
|
|
619
|
+
const proxyReq = http.request(
|
|
620
|
+
{
|
|
621
|
+
host: upstream.host,
|
|
622
|
+
port: upstream.port,
|
|
623
|
+
method: req.method,
|
|
624
|
+
path: req.url,
|
|
625
|
+
headers: sanitizeHeaders(req, req.socket.remoteAddress ?? ""),
|
|
626
|
+
agent: upstream.agent,
|
|
627
|
+
timeout: UPSTREAM_TIMEOUT_MS,
|
|
628
|
+
},
|
|
629
|
+
(proxyRes) => {
|
|
630
|
+
// Strip hop-by-hop headers from upstream response too.
|
|
631
|
+
const safe = {};
|
|
632
|
+
for (const [k, v] of Object.entries(proxyRes.headers)) {
|
|
633
|
+
if (!HOP_BY_HOP.has(k.toLowerCase())) safe[k] = v;
|
|
634
|
+
}
|
|
635
|
+
res.writeHead(proxyRes.statusCode ?? 502, safe);
|
|
636
|
+
proxyRes.pipe(res);
|
|
637
|
+
},
|
|
638
|
+
);
|
|
639
|
+
proxyReq.on("timeout", () => {
|
|
640
|
+
logError("upstream-timeout", new Error("upstream timeout"), { url: req.url });
|
|
641
|
+
proxyReq.destroy(new Error("Upstream timeout"));
|
|
642
|
+
});
|
|
643
|
+
proxyReq.on("error", (err) => {
|
|
644
|
+
logError("upstream", err, { url: req.url });
|
|
645
|
+
if (!res.headersSent) {
|
|
646
|
+
res.writeHead(502, { "content-type": "text/plain" });
|
|
647
|
+
res.end("Bad Gateway");
|
|
648
|
+
}
|
|
649
|
+
if (!req.destroyed) req.destroy();
|
|
650
|
+
});
|
|
651
|
+
req.on("error", (err) => {
|
|
652
|
+
logError("client", err, { url: req.url });
|
|
653
|
+
if (!proxyReq.destroyed) proxyReq.destroy();
|
|
654
|
+
});
|
|
655
|
+
req.pipe(proxyReq);
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
// WebSocket / HTTP upgrade passthrough — required for Next.js HMR.
|
|
659
|
+
server.on("upgrade", (req, clientSocket, head) => {
|
|
660
|
+
const upstream = pickUpstream(req.url);
|
|
661
|
+
const upstreamSocket = net.connect(upstream.port, upstream.host, () => {
|
|
662
|
+
const headers = sanitizeHeaders(req, req.socket.remoteAddress ?? "");
|
|
663
|
+
// Re-add Connection: Upgrade & Upgrade headers stripped by sanitizeHeaders.
|
|
664
|
+
headers["connection"] = "Upgrade";
|
|
665
|
+
if (req.headers.upgrade) headers["upgrade"] = req.headers.upgrade;
|
|
666
|
+
const headerLines = [];
|
|
667
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
668
|
+
const values = Array.isArray(v) ? v : [v];
|
|
669
|
+
for (const single of values) {
|
|
670
|
+
const str = String(single);
|
|
671
|
+
// Defense-in-depth: reject CRLF in header values (header injection).
|
|
672
|
+
if (/[\\r\\n]/.test(str)) continue;
|
|
673
|
+
headerLines.push(\`\${k}: \${str}\`);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
upstreamSocket.write(
|
|
677
|
+
\`\${req.method} \${req.url} HTTP/\${req.httpVersion}\\r\\n\` +
|
|
678
|
+
headerLines.join("\\r\\n") +
|
|
679
|
+
"\\r\\n\\r\\n",
|
|
680
|
+
);
|
|
681
|
+
if (head?.length) upstreamSocket.write(head);
|
|
682
|
+
upstreamSocket.pipe(clientSocket);
|
|
683
|
+
clientSocket.pipe(upstreamSocket);
|
|
684
|
+
});
|
|
685
|
+
upstreamSocket.on("error", (err) => {
|
|
686
|
+
logError("ws-upstream", err, { url: req.url });
|
|
687
|
+
clientSocket.destroy();
|
|
688
|
+
});
|
|
689
|
+
clientSocket.on("error", (err) => {
|
|
690
|
+
logError("ws-client", err, { url: req.url });
|
|
691
|
+
upstreamSocket.destroy();
|
|
692
|
+
});
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
server.listen(PORT, () => {
|
|
696
|
+
console.log(\`apollo-proxy → http://localhost:\${PORT}\`);
|
|
697
|
+
console.log(\` \${ADMIN_PREFIX}/*, /uploads/*, /api/* → \${BACKEND.host}:\${BACKEND.port}\`);
|
|
698
|
+
console.log(\` /* → \${FRONTEND.host}:\${FRONTEND.port}\`);
|
|
699
|
+
console.log(\` TRUST_PROXY=\${TRUST_PROXY} MAX_BODY_BYTES=\${MAX_BODY_BYTES} UPSTREAM_TIMEOUT_MS=\${UPSTREAM_TIMEOUT_MS}\`);
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
// Graceful shutdown — let in-flight requests finish, then exit.
|
|
703
|
+
let shuttingDown = false;
|
|
704
|
+
function shutdown(signal) {
|
|
705
|
+
if (shuttingDown) return;
|
|
706
|
+
shuttingDown = true;
|
|
707
|
+
console.log(\`\\napollo-proxy: \${signal} received, draining...\`);
|
|
708
|
+
server.close(() => {
|
|
709
|
+
backendAgent.destroy();
|
|
710
|
+
frontendAgent.destroy();
|
|
711
|
+
process.exit(0);
|
|
712
|
+
});
|
|
713
|
+
// Hard exit after 10s if connections refuse to drain.
|
|
714
|
+
setTimeout(() => process.exit(1), 10_000).unref();
|
|
715
|
+
}
|
|
716
|
+
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
717
|
+
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
718
|
+
|
|
719
|
+
function parseTarget(input) {
|
|
720
|
+
const url = new URL(input.startsWith(":") ? \`http://127.0.0.1\${input}\` : input);
|
|
721
|
+
return { host: url.hostname, port: Number(url.port || 80) };
|
|
722
|
+
}
|
|
723
|
+
`;
|
|
724
|
+
writeFileSync(resolve(dir, "server.mjs"), server);
|
|
725
|
+
|
|
726
|
+
const pkg = {
|
|
727
|
+
name: `@${dirName}/proxy`,
|
|
728
|
+
private: true,
|
|
729
|
+
version: "0.0.0",
|
|
730
|
+
description: "Optional Node.js reverse proxy fronting frontend + backend on a single origin",
|
|
731
|
+
type: "module",
|
|
732
|
+
main: "server.mjs",
|
|
733
|
+
scripts: {
|
|
734
|
+
start: "node server.mjs",
|
|
735
|
+
},
|
|
736
|
+
engines: { node: ">=20" },
|
|
737
|
+
};
|
|
738
|
+
writeFileSync(resolve(dir, "package.json"), JSON.stringify(pkg, null, 2) + "\n");
|
|
739
|
+
|
|
740
|
+
const readme = `# @${dirName}/proxy
|
|
741
|
+
|
|
742
|
+
Optional single-origin reverse proxy. Routes \`${prefix}/*\`, \`/api/*\`, and
|
|
743
|
+
\`/uploads/*\` to the backend; everything else to the frontend. Zero deps.
|
|
744
|
+
|
|
745
|
+
## When to use
|
|
746
|
+
|
|
747
|
+
- You want **one URL** for FE + BE locally (shared cookies, no CORS).
|
|
748
|
+
- You don't want to install nginx for local dev.
|
|
749
|
+
|
|
750
|
+
## Usage
|
|
751
|
+
|
|
752
|
+
From the repo root:
|
|
753
|
+
|
|
754
|
+
\`\`\`bash
|
|
755
|
+
pnpm dev:rp # runs FE :3001 + BE :3000 + plugins watcher + proxy :3030
|
|
756
|
+
\`\`\`
|
|
757
|
+
|
|
758
|
+
Then open http://localhost:3030.
|
|
759
|
+
|
|
760
|
+
For best results, set \`NEXT_PUBLIC_SITE_URL=http://localhost:3030\` in your
|
|
761
|
+
\`.env.local\` so Better Auth, OAuth callbacks, and email links all use the
|
|
762
|
+
single-origin URL.
|
|
763
|
+
|
|
764
|
+
## Configuration
|
|
765
|
+
|
|
766
|
+
| Env var | Default | Notes |
|
|
767
|
+
| --------------------- | -------------------------------- | -------------------------------------------------------------- |
|
|
768
|
+
| \`PORT\` | \`3030\` | Public port |
|
|
769
|
+
| \`BACKEND\` | \`http://127.0.0.1:3000\` | apps/backend dev server |
|
|
770
|
+
| \`FRONTEND\` | \`http://127.0.0.1:3001\` | apps/frontend dev server |
|
|
771
|
+
| \`ADMIN_PREFIX\` | \`${prefix}\` | Backend admin path prefix |
|
|
772
|
+
| \`BACKEND_API_PATHS\` | \`auth\\|v1\\|cron\\|email\\|health\\|mcp\\|admin\\|editing-presence\` | Pipe-separated /api/* segments routed to backend |
|
|
773
|
+
| \`MAX_BODY_BYTES\` | \`52428800\` (50MB) | Reject larger bodies. \`0\` = unlimited |
|
|
774
|
+
| \`UPSTREAM_TIMEOUT_MS\` | \`60000\` | Request timeout to upstream |
|
|
775
|
+
| \`TRUST_PROXY\` | \`0\` | Set \`1\` only if behind another trusted proxy (CDN/LB) |
|
|
776
|
+
|
|
777
|
+
## Health check
|
|
778
|
+
|
|
779
|
+
\`GET /__proxy/health\` returns \`{"status":"ok","uptime":<seconds>}\`. Use it for
|
|
780
|
+
liveness probes; the path is reserved by the proxy and never forwarded.
|
|
781
|
+
|
|
782
|
+
## Better Auth interaction
|
|
783
|
+
|
|
784
|
+
Apollo CMS's Better Auth derives the request origin from \`x-forwarded-host\` /
|
|
785
|
+
\`x-forwarded-proto\`. This proxy sets both correctly, so \`trustedOrigins\` keeps
|
|
786
|
+
working through the proxy. The recent fix in \`src/lib/auth/server.ts\` ensures
|
|
787
|
+
the actual request origin (e.g. \`localhost:3030\`) is added to trusted origins
|
|
788
|
+
even when \`NEXT_PUBLIC_SITE_URL\` points elsewhere.
|
|
789
|
+
|
|
790
|
+
## Production
|
|
791
|
+
|
|
792
|
+
This proxy doesn't terminate TLS. For HTTPS, either:
|
|
793
|
+
|
|
794
|
+
- Sit it behind a TLS-terminating load balancer (Caddy, Cloudflare, ALB), **or**
|
|
795
|
+
- Use \`nginx.conf.sample\` at the repo root instead — nginx handles TLS, gzip,
|
|
796
|
+
and large-scale traffic better.
|
|
797
|
+
|
|
798
|
+
The Node proxy is intended for dev and small self-hosted deploys.
|
|
799
|
+
`;
|
|
800
|
+
writeFileSync(resolve(dir, "README.md"), readme);
|
|
801
|
+
}
|
|
802
|
+
|
|
490
803
|
function writeRootGitignore(targetDir) {
|
|
491
804
|
const ignore = [
|
|
492
805
|
"node_modules",
|
|
@@ -951,12 +1264,17 @@ paths to the backend so /_next/* doesn't collide:
|
|
|
951
1264
|
The frontend MUST NOT define routes at \`/admin\`, \`/api/auth\`, \`/api/v1\`, etc.
|
|
952
1265
|
If you need your own API, namespace it under \`/api/internal/*\` or similar.
|
|
953
1266
|
|
|
954
|
-
### Reverse proxy
|
|
1267
|
+
### Reverse proxy
|
|
955
1268
|
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
1269
|
+
Two options ship out of the box:
|
|
1270
|
+
|
|
1271
|
+
- **\`apps/proxy\`** (Node.js, zero deps) — opt in with \`pnpm dev:rp\`. Runs FE,
|
|
1272
|
+
BE, plugins watcher, and a reverse proxy on **:3030** so everything sits on
|
|
1273
|
+
one origin (shared cookies, no CORS). Best for local dev and small self-hosted
|
|
1274
|
+
deploys. Configure via \`PORT\`, \`BACKEND\`, \`FRONTEND\`, \`ADMIN_PREFIX\`.
|
|
1275
|
+
- **\`nginx.conf.sample\`** (production) — same routing baked in (frontend on
|
|
1276
|
+
:3001, backend on :3000). Use it when fronting both apps behind a single
|
|
1277
|
+
TLS-terminating proxy outside Vercel — drop in your domain and SSL certs.
|
|
960
1278
|
|
|
961
1279
|
To **disable** single-origin and run the backend on its own subdomain,
|
|
962
1280
|
delete \`APOLLO_ASSET_PREFIX\` from \`apps/backend/.env.local\` and remove the
|
|
@@ -1007,8 +1325,14 @@ ${dirName}/
|
|
|
1007
1325
|
pnpm install
|
|
1008
1326
|
pnpm backend:setup # push schema + seed apollo-cms
|
|
1009
1327
|
pnpm dev # runs frontend (3001) + backend (3000) in parallel
|
|
1328
|
+
# or:
|
|
1329
|
+
pnpm dev:rp # same + reverse proxy on :3030 (single-origin, shared cookies)
|
|
1010
1330
|
\`\`\`
|
|
1011
1331
|
|
|
1332
|
+
When using \`pnpm dev:rp\`, set \`NEXT_PUBLIC_SITE_URL=http://localhost:3030\`
|
|
1333
|
+
in your \`.env.local\` so Better Auth, OAuth callbacks, and email links use
|
|
1334
|
+
the unified origin.
|
|
1335
|
+
|
|
1012
1336
|
${originSection}
|
|
1013
1337
|
## Custom plugins
|
|
1014
1338
|
|
|
@@ -1182,10 +1506,11 @@ async function main() {
|
|
|
1182
1506
|
writeRootEnv(targetDir, { dbUrl, siteUrl, locale, authSecret, cronSecret, adminPrefix, backendInternalUrl });
|
|
1183
1507
|
writeReadme(targetDir, dirName, frontendName, adminPrefix);
|
|
1184
1508
|
if (adminPrefix) writeNginxSample(targetDir, adminPrefix);
|
|
1509
|
+
writeProxyApp(targetDir, dirName, adminPrefix);
|
|
1185
1510
|
success(
|
|
1186
1511
|
`package.json, pnpm-workspace.yaml, .gitignore, .env.local, README.md${
|
|
1187
1512
|
adminPrefix ? ", nginx.conf.sample" : ""
|
|
1188
|
-
}`,
|
|
1513
|
+
}, apps/proxy`,
|
|
1189
1514
|
);
|
|
1190
1515
|
|
|
1191
1516
|
// ── Step 4: git init ──
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-apollo-monorepo",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
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"
|