create-apollo-monorepo 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +65 -0
- package/index.mjs +583 -0
- package/package.json +26 -0
package/README.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# create-apollo-monorepo
|
|
2
|
+
|
|
3
|
+
Scaffold a pnpm monorepo where your custom frontend lives alongside Apollo CMS
|
|
4
|
+
mounted as a **git submodule** backend (read-only — pull updates only).
|
|
5
|
+
|
|
6
|
+
## Usage
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npx create-apollo-monorepo thamc-new
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
With flags:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npx create-apollo-monorepo thamc-new \
|
|
16
|
+
--frontend-name "@thamc/frontend" \
|
|
17
|
+
--db "postgresql://user:pass@localhost:5432/thamc" \
|
|
18
|
+
--url "http://localhost:3000" \
|
|
19
|
+
--locale th
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Result
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
thamc-new/
|
|
26
|
+
├── apps/
|
|
27
|
+
│ ├── frontend/ ← @thamc/frontend (Next.js skeleton)
|
|
28
|
+
│ └── backend/ ← git submodule → apollo-cms
|
|
29
|
+
├── package.json ← root workspace
|
|
30
|
+
├── pnpm-workspace.yaml
|
|
31
|
+
├── .env.local ← shared dev env
|
|
32
|
+
├── .gitmodules ← submodule config
|
|
33
|
+
└── .gitignore
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Flags
|
|
37
|
+
|
|
38
|
+
| Flag | Default | Description |
|
|
39
|
+
| -------------------------- | --------------------------------------- | ------------------------------------ |
|
|
40
|
+
| `--frontend-name <name>` | `@<dir>/frontend` | Frontend `package.json` name |
|
|
41
|
+
| `--backend-url <url>` | `https://github.com/5Lab-Group-Co-Ltd/apollo-cms.git` | Submodule git URL |
|
|
42
|
+
| `--backend-branch <name>` | `main` | Submodule branch to track |
|
|
43
|
+
| `-d, --db <url>` | _(prompted)_ | `DATABASE_URL` for backend |
|
|
44
|
+
| `-u, --url <url>` | `http://localhost:3000` | `NEXT_PUBLIC_SITE_URL` |
|
|
45
|
+
| `-l, --locale <code>` | `en` | `NEXT_PUBLIC_DEFAULT_LOCALE` |
|
|
46
|
+
| `--skip-install` | off | Don't run `pnpm install` |
|
|
47
|
+
| `--skip-submodule` | off | Don't add the git submodule |
|
|
48
|
+
| `-h, --help` | — | Show help |
|
|
49
|
+
|
|
50
|
+
## After install
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
cd thamc-new
|
|
54
|
+
pnpm backend:setup # push schema + seed apollo-cms
|
|
55
|
+
pnpm dev # frontend :3001 + backend :3000 in parallel
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Updating the backend
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
pnpm backend:update # git submodule update --remote --merge apps/backend
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Don't edit files inside `apps/backend` — open issues / PRs against the
|
|
65
|
+
`apollo-cms` repository upstream.
|
package/index.mjs
ADDED
|
@@ -0,0 +1,583 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// create-apollo-monorepo — Zero-dependency scaffolder for an Apollo CMS monorepo
|
|
4
|
+
// Usage: npx create-apollo-monorepo <directory> [flags]
|
|
5
|
+
//
|
|
6
|
+
// Produces:
|
|
7
|
+
// <directory>/
|
|
8
|
+
// ├── apps/
|
|
9
|
+
// │ ├── frontend/ ← your app (pnpm workspace member)
|
|
10
|
+
// │ └── backend/ ← git submodule → apollo-cms
|
|
11
|
+
// ├── package.json
|
|
12
|
+
// ├── pnpm-workspace.yaml
|
|
13
|
+
// ├── .env.local
|
|
14
|
+
// ├── .gitmodules
|
|
15
|
+
// └── .gitignore
|
|
16
|
+
|
|
17
|
+
import { execSync } from "node:child_process";
|
|
18
|
+
import { randomBytes } from "node:crypto";
|
|
19
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
20
|
+
import { resolve, basename } from "node:path";
|
|
21
|
+
import { createInterface } from "node:readline";
|
|
22
|
+
|
|
23
|
+
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
const BACKEND_REPO_URL = "https://github.com/5Lab-Group-Co-Ltd/apollo-cms.git";
|
|
26
|
+
const BACKEND_BRANCH = "main";
|
|
27
|
+
const BACKEND_PATH = "apps/backend";
|
|
28
|
+
const FRONTEND_PATH = "apps/frontend";
|
|
29
|
+
const MIN_NODE_MAJOR = 20;
|
|
30
|
+
|
|
31
|
+
const COLORS = {
|
|
32
|
+
reset: "\x1b[0m",
|
|
33
|
+
bold: "\x1b[1m",
|
|
34
|
+
dim: "\x1b[2m",
|
|
35
|
+
red: "\x1b[31m",
|
|
36
|
+
green: "\x1b[32m",
|
|
37
|
+
yellow: "\x1b[33m",
|
|
38
|
+
cyan: "\x1b[36m",
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const HELP_TEXT = `
|
|
42
|
+
${COLORS.bold}create-apollo-monorepo${COLORS.reset} — Scaffold a monorepo with Apollo CMS as a git submodule backend
|
|
43
|
+
|
|
44
|
+
${COLORS.bold}Usage:${COLORS.reset}
|
|
45
|
+
npx create-apollo-monorepo <directory> [flags]
|
|
46
|
+
|
|
47
|
+
${COLORS.bold}Examples:${COLORS.reset}
|
|
48
|
+
npx create-apollo-monorepo thamc-new
|
|
49
|
+
npx create-apollo-monorepo thamc-new --frontend-name "@thamc/frontend"
|
|
50
|
+
npx create-apollo-monorepo thamc-new --backend-branch develop --skip-install
|
|
51
|
+
|
|
52
|
+
${COLORS.bold}Flags:${COLORS.reset}
|
|
53
|
+
--frontend-name <name> Frontend package name (default: "@<dir>/frontend")
|
|
54
|
+
--backend-url <url> Backend git URL (default: ${BACKEND_REPO_URL})
|
|
55
|
+
--backend-branch <name> Backend branch to track (default: ${BACKEND_BRANCH})
|
|
56
|
+
-d, --db <url> DATABASE_URL for backend
|
|
57
|
+
-u, --url <url> NEXT_PUBLIC_SITE_URL (default: http://localhost:3000)
|
|
58
|
+
-l, --locale <code> NEXT_PUBLIC_DEFAULT_LOCALE (default: en)
|
|
59
|
+
--skip-install Skip dependency installation
|
|
60
|
+
--skip-submodule Skip git submodule add (you'll add it later)
|
|
61
|
+
-h, --help Show this help message
|
|
62
|
+
`;
|
|
63
|
+
|
|
64
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
function log(msg) {
|
|
67
|
+
console.log(msg);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function step(num, msg) {
|
|
71
|
+
log(`\n${COLORS.cyan}[${num}]${COLORS.reset} ${msg}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function success(msg) {
|
|
75
|
+
log(` ${COLORS.green}✓${COLORS.reset} ${msg}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function warn(msg) {
|
|
79
|
+
log(` ${COLORS.yellow}⚠${COLORS.reset} ${msg}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function fatal(msg) {
|
|
83
|
+
console.error(`\n${COLORS.red}Error:${COLORS.reset} ${msg}\n`);
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function run(cmd, opts = {}) {
|
|
88
|
+
return execSync(cmd, { stdio: "inherit", ...opts });
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function runSilent(cmd, opts = {}) {
|
|
92
|
+
try {
|
|
93
|
+
return execSync(cmd, { stdio: "pipe", ...opts }).toString().trim();
|
|
94
|
+
} catch {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function commandExists(cmd) {
|
|
100
|
+
return runSilent(process.platform === "win32" ? `where ${cmd}` : `which ${cmd}`) !== null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ─── Arg Parsing ─────────────────────────────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
function parseArgs(argv) {
|
|
106
|
+
const args = argv.slice(2);
|
|
107
|
+
const flags = {
|
|
108
|
+
directory: null,
|
|
109
|
+
frontendName: null,
|
|
110
|
+
backendUrl: BACKEND_REPO_URL,
|
|
111
|
+
backendBranch: BACKEND_BRANCH,
|
|
112
|
+
db: null,
|
|
113
|
+
url: null,
|
|
114
|
+
locale: null,
|
|
115
|
+
skipInstall: false,
|
|
116
|
+
skipSubmodule: false,
|
|
117
|
+
help: false,
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
let i = 0;
|
|
121
|
+
while (i < args.length) {
|
|
122
|
+
const arg = args[i];
|
|
123
|
+
switch (arg) {
|
|
124
|
+
case "-h":
|
|
125
|
+
case "--help":
|
|
126
|
+
flags.help = true;
|
|
127
|
+
break;
|
|
128
|
+
case "--frontend-name":
|
|
129
|
+
flags.frontendName = args[++i];
|
|
130
|
+
break;
|
|
131
|
+
case "--backend-url":
|
|
132
|
+
flags.backendUrl = args[++i];
|
|
133
|
+
break;
|
|
134
|
+
case "--backend-branch":
|
|
135
|
+
flags.backendBranch = args[++i];
|
|
136
|
+
break;
|
|
137
|
+
case "-d":
|
|
138
|
+
case "--db":
|
|
139
|
+
flags.db = args[++i];
|
|
140
|
+
break;
|
|
141
|
+
case "-u":
|
|
142
|
+
case "--url":
|
|
143
|
+
flags.url = args[++i];
|
|
144
|
+
break;
|
|
145
|
+
case "-l":
|
|
146
|
+
case "--locale":
|
|
147
|
+
flags.locale = args[++i];
|
|
148
|
+
break;
|
|
149
|
+
case "--skip-install":
|
|
150
|
+
flags.skipInstall = true;
|
|
151
|
+
break;
|
|
152
|
+
case "--skip-submodule":
|
|
153
|
+
flags.skipSubmodule = true;
|
|
154
|
+
break;
|
|
155
|
+
default:
|
|
156
|
+
if (arg.startsWith("-")) {
|
|
157
|
+
fatal(`Unknown flag: ${arg}\nRun with --help for usage.`);
|
|
158
|
+
}
|
|
159
|
+
if (flags.directory) {
|
|
160
|
+
fatal(`Unexpected argument: ${arg}\nOnly one directory name is allowed.`);
|
|
161
|
+
}
|
|
162
|
+
flags.directory = arg;
|
|
163
|
+
}
|
|
164
|
+
i++;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return flags;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ─── Validation ──────────────────────────────────────────────────────────────
|
|
171
|
+
|
|
172
|
+
function isValidDbUrl(url) {
|
|
173
|
+
return /^postgres(ql)?:\/\/.+/.test(url);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function isValidUrl(url) {
|
|
177
|
+
return /^https?:\/\/.+/.test(url);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function isValidLocale(code) {
|
|
181
|
+
return /^[a-z]{2,5}$/i.test(code);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ─── Pre-flight ──────────────────────────────────────────────────────────────
|
|
185
|
+
|
|
186
|
+
function preflight(flags) {
|
|
187
|
+
step(1, "Pre-flight checks");
|
|
188
|
+
|
|
189
|
+
const nodeMajor = parseInt(process.versions.node.split(".")[0], 10);
|
|
190
|
+
if (nodeMajor < MIN_NODE_MAJOR) {
|
|
191
|
+
fatal(
|
|
192
|
+
`Node.js ${MIN_NODE_MAJOR}+ is required (found ${process.versions.node}).\n Install: https://nodejs.org/`,
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
success(`Node.js ${process.versions.node}`);
|
|
196
|
+
|
|
197
|
+
if (!commandExists("git")) {
|
|
198
|
+
fatal("git is required but not found.\n Install: https://git-scm.com/");
|
|
199
|
+
}
|
|
200
|
+
success("git found");
|
|
201
|
+
|
|
202
|
+
if (!commandExists("pnpm")) {
|
|
203
|
+
warn("pnpm not found — install with: npm i -g pnpm (required for workspaces)");
|
|
204
|
+
} else {
|
|
205
|
+
success("pnpm found");
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const targetDir = resolve(flags.directory);
|
|
209
|
+
if (existsSync(targetDir)) {
|
|
210
|
+
fatal(`Directory already exists: ${targetDir}\n Choose a different name or remove it first.`);
|
|
211
|
+
}
|
|
212
|
+
success(`Target: ${targetDir}`);
|
|
213
|
+
|
|
214
|
+
return targetDir;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ─── Prompts ─────────────────────────────────────────────────────────────────
|
|
218
|
+
|
|
219
|
+
function createPrompt() {
|
|
220
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
221
|
+
const ask = (q) => new Promise((res) => rl.question(q, (a) => res(a.trim())));
|
|
222
|
+
const close = () => rl.close();
|
|
223
|
+
return { ask, close };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function gatherEnv(flags) {
|
|
227
|
+
let dbUrl = flags.db;
|
|
228
|
+
let siteUrl = flags.url ?? "http://localhost:3000";
|
|
229
|
+
let locale = flags.locale ?? "en";
|
|
230
|
+
|
|
231
|
+
if (flags.db && !isValidDbUrl(flags.db)) fatal("--db must start with postgresql:// or postgres://");
|
|
232
|
+
if (flags.url && !isValidUrl(flags.url)) fatal("--url must start with http:// or https://");
|
|
233
|
+
if (flags.locale && !isValidLocale(flags.locale)) fatal("--locale must be a 2-5 character code (e.g., en, th)");
|
|
234
|
+
|
|
235
|
+
const needsPrompt = !dbUrl || !flags.url || !flags.locale;
|
|
236
|
+
if (!needsPrompt) return { dbUrl, siteUrl, locale };
|
|
237
|
+
|
|
238
|
+
const { ask, close } = createPrompt();
|
|
239
|
+
|
|
240
|
+
if (!dbUrl) {
|
|
241
|
+
log("");
|
|
242
|
+
while (!dbUrl) {
|
|
243
|
+
const ans = await ask(
|
|
244
|
+
` ${COLORS.bold}DATABASE_URL${COLORS.reset} ${COLORS.dim}(postgresql://user:pass@host:5432/dbname)${COLORS.reset}\n > `,
|
|
245
|
+
);
|
|
246
|
+
if (isValidDbUrl(ans)) dbUrl = ans;
|
|
247
|
+
else warn("Must start with postgresql:// or postgres://");
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (!flags.url) {
|
|
252
|
+
const ans = await ask(
|
|
253
|
+
`\n ${COLORS.bold}NEXT_PUBLIC_SITE_URL${COLORS.reset} ${COLORS.dim}[${siteUrl}]${COLORS.reset}\n > `,
|
|
254
|
+
);
|
|
255
|
+
if (ans) {
|
|
256
|
+
if (isValidUrl(ans)) siteUrl = ans;
|
|
257
|
+
else warn(`Invalid URL, using default: ${siteUrl}`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (!flags.locale) {
|
|
262
|
+
const ans = await ask(
|
|
263
|
+
`\n ${COLORS.bold}Default locale${COLORS.reset} ${COLORS.dim}[${locale}]${COLORS.reset}\n > `,
|
|
264
|
+
);
|
|
265
|
+
if (ans) {
|
|
266
|
+
if (isValidLocale(ans)) locale = ans;
|
|
267
|
+
else warn(`Invalid locale code, using default: ${locale}`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
close();
|
|
272
|
+
return { dbUrl, siteUrl, locale };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// ─── Scaffolding ─────────────────────────────────────────────────────────────
|
|
276
|
+
|
|
277
|
+
function writeRootPackageJson(targetDir, dirName) {
|
|
278
|
+
const pkg = {
|
|
279
|
+
name: dirName,
|
|
280
|
+
version: "0.0.0",
|
|
281
|
+
private: true,
|
|
282
|
+
description: `${dirName} monorepo (frontend + apollo-cms backend submodule)`,
|
|
283
|
+
scripts: {
|
|
284
|
+
dev: "pnpm -r --parallel dev",
|
|
285
|
+
"dev:frontend": "pnpm --filter ./apps/frontend dev",
|
|
286
|
+
"dev:backend": "pnpm --filter ./apps/backend dev",
|
|
287
|
+
build: "pnpm -r build",
|
|
288
|
+
lint: "pnpm -r lint",
|
|
289
|
+
typecheck: "pnpm -r typecheck",
|
|
290
|
+
"backend:update": "git submodule update --remote --merge apps/backend",
|
|
291
|
+
"backend:setup": "pnpm --filter ./apps/backend setup",
|
|
292
|
+
},
|
|
293
|
+
engines: { node: ">=20", pnpm: ">=9" },
|
|
294
|
+
packageManager: "pnpm@9.0.0",
|
|
295
|
+
};
|
|
296
|
+
writeFileSync(resolve(targetDir, "package.json"), JSON.stringify(pkg, null, 2) + "\n");
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function writePnpmWorkspace(targetDir) {
|
|
300
|
+
writeFileSync(
|
|
301
|
+
resolve(targetDir, "pnpm-workspace.yaml"),
|
|
302
|
+
"packages:\n - 'apps/*'\n",
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function writeRootGitignore(targetDir) {
|
|
307
|
+
const ignore = [
|
|
308
|
+
"node_modules",
|
|
309
|
+
".pnpm-store",
|
|
310
|
+
".turbo",
|
|
311
|
+
".next",
|
|
312
|
+
"dist",
|
|
313
|
+
"build",
|
|
314
|
+
".env",
|
|
315
|
+
".env.local",
|
|
316
|
+
".env*.local",
|
|
317
|
+
"*.log",
|
|
318
|
+
".DS_Store",
|
|
319
|
+
"",
|
|
320
|
+
].join("\n");
|
|
321
|
+
writeFileSync(resolve(targetDir, ".gitignore"), ignore);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function writeRootEnv(targetDir, { dbUrl, siteUrl, locale, authSecret }) {
|
|
325
|
+
const lines = [
|
|
326
|
+
"# Shared dev env — backend reads these via apps/backend/.env.local symlink/copy",
|
|
327
|
+
`DATABASE_URL=${dbUrl}`,
|
|
328
|
+
`APOLLO_SECRET=${authSecret}`,
|
|
329
|
+
`BETTER_AUTH_SECRET=${authSecret}`,
|
|
330
|
+
`NEXT_PUBLIC_SITE_URL=${siteUrl}`,
|
|
331
|
+
`NEXT_PUBLIC_DEFAULT_LOCALE=${locale}`,
|
|
332
|
+
"",
|
|
333
|
+
"# Frontend",
|
|
334
|
+
`NEXT_PUBLIC_BACKEND_URL=${siteUrl}`,
|
|
335
|
+
"",
|
|
336
|
+
].join("\n");
|
|
337
|
+
writeFileSync(resolve(targetDir, ".env.local"), lines);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function writeFrontendApp(targetDir, frontendName, siteUrl) {
|
|
341
|
+
const dir = resolve(targetDir, FRONTEND_PATH);
|
|
342
|
+
mkdirSync(dir, { recursive: true });
|
|
343
|
+
mkdirSync(resolve(dir, "src/app"), { recursive: true });
|
|
344
|
+
mkdirSync(resolve(dir, "public"), { recursive: true });
|
|
345
|
+
|
|
346
|
+
const pkg = {
|
|
347
|
+
name: frontendName,
|
|
348
|
+
version: "0.0.0",
|
|
349
|
+
private: true,
|
|
350
|
+
scripts: {
|
|
351
|
+
dev: "next dev -p 3001",
|
|
352
|
+
build: "next build",
|
|
353
|
+
start: "next start -p 3001",
|
|
354
|
+
lint: "next lint",
|
|
355
|
+
typecheck: "tsc --noEmit",
|
|
356
|
+
},
|
|
357
|
+
dependencies: {
|
|
358
|
+
next: "^16.0.0",
|
|
359
|
+
react: "^19.0.0",
|
|
360
|
+
"react-dom": "^19.0.0",
|
|
361
|
+
},
|
|
362
|
+
devDependencies: {
|
|
363
|
+
"@types/node": "^22.0.0",
|
|
364
|
+
"@types/react": "^19.0.0",
|
|
365
|
+
"@types/react-dom": "^19.0.0",
|
|
366
|
+
typescript: "^5.6.0",
|
|
367
|
+
},
|
|
368
|
+
};
|
|
369
|
+
writeFileSync(resolve(dir, "package.json"), JSON.stringify(pkg, null, 2) + "\n");
|
|
370
|
+
|
|
371
|
+
writeFileSync(
|
|
372
|
+
resolve(dir, "tsconfig.json"),
|
|
373
|
+
JSON.stringify(
|
|
374
|
+
{
|
|
375
|
+
compilerOptions: {
|
|
376
|
+
target: "ES2022",
|
|
377
|
+
lib: ["dom", "dom.iterable", "esnext"],
|
|
378
|
+
allowJs: true,
|
|
379
|
+
skipLibCheck: true,
|
|
380
|
+
strict: true,
|
|
381
|
+
noEmit: true,
|
|
382
|
+
esModuleInterop: true,
|
|
383
|
+
module: "esnext",
|
|
384
|
+
moduleResolution: "bundler",
|
|
385
|
+
resolveJsonModule: true,
|
|
386
|
+
isolatedModules: true,
|
|
387
|
+
jsx: "preserve",
|
|
388
|
+
incremental: true,
|
|
389
|
+
plugins: [{ name: "next" }],
|
|
390
|
+
paths: { "@/*": ["./src/*"] },
|
|
391
|
+
},
|
|
392
|
+
include: ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
|
393
|
+
exclude: ["node_modules"],
|
|
394
|
+
},
|
|
395
|
+
null,
|
|
396
|
+
2,
|
|
397
|
+
) + "\n",
|
|
398
|
+
);
|
|
399
|
+
|
|
400
|
+
writeFileSync(
|
|
401
|
+
resolve(dir, "next.config.ts"),
|
|
402
|
+
`import type { NextConfig } from "next";\n\nconst config: NextConfig = {\n reactStrictMode: true,\n};\n\nexport default config;\n`,
|
|
403
|
+
);
|
|
404
|
+
|
|
405
|
+
writeFileSync(
|
|
406
|
+
resolve(dir, ".env.local.example"),
|
|
407
|
+
`NEXT_PUBLIC_BACKEND_URL=${siteUrl}\n`,
|
|
408
|
+
);
|
|
409
|
+
|
|
410
|
+
writeFileSync(
|
|
411
|
+
resolve(dir, "src/app/layout.tsx"),
|
|
412
|
+
`export const metadata = { title: "Frontend", description: "Apollo CMS frontend" };\n\nexport default function RootLayout({ children }: { children: React.ReactNode }) {\n return (\n <html lang="en">\n <body>{children}</body>\n </html>\n );\n}\n`,
|
|
413
|
+
);
|
|
414
|
+
|
|
415
|
+
writeFileSync(
|
|
416
|
+
resolve(dir, "src/app/page.tsx"),
|
|
417
|
+
`export default function Page() {\n return (\n <main style={{ padding: 40, fontFamily: "system-ui" }}>\n <h1>Frontend</h1>\n <p>Backend: <code>{process.env.NEXT_PUBLIC_BACKEND_URL}</code></p>\n </main>\n );\n}\n`,
|
|
418
|
+
);
|
|
419
|
+
|
|
420
|
+
writeFileSync(
|
|
421
|
+
resolve(dir, ".gitignore"),
|
|
422
|
+
["node_modules", ".next", "dist", ".env.local", ""].join("\n"),
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function writeReadme(targetDir, dirName, frontendName) {
|
|
427
|
+
const readme = `# ${dirName}
|
|
428
|
+
|
|
429
|
+
Monorepo with a custom frontend and Apollo CMS as a git submodule backend.
|
|
430
|
+
|
|
431
|
+
## Layout
|
|
432
|
+
|
|
433
|
+
\`\`\`
|
|
434
|
+
${dirName}/
|
|
435
|
+
├── apps/
|
|
436
|
+
│ ├── frontend/ ← ${frontendName}
|
|
437
|
+
│ └── backend/ ← git submodule → apollo-cms (read-only, pull updates)
|
|
438
|
+
├── package.json
|
|
439
|
+
└── pnpm-workspace.yaml
|
|
440
|
+
\`\`\`
|
|
441
|
+
|
|
442
|
+
## Quick start
|
|
443
|
+
|
|
444
|
+
\`\`\`bash
|
|
445
|
+
pnpm install
|
|
446
|
+
pnpm backend:setup # push schema + seed apollo-cms
|
|
447
|
+
pnpm dev # runs frontend (3001) + backend (3000) in parallel
|
|
448
|
+
\`\`\`
|
|
449
|
+
|
|
450
|
+
## Updating the backend
|
|
451
|
+
|
|
452
|
+
Apollo CMS is tracked as a git submodule. Pull the latest:
|
|
453
|
+
|
|
454
|
+
\`\`\`bash
|
|
455
|
+
pnpm backend:update
|
|
456
|
+
\`\`\`
|
|
457
|
+
|
|
458
|
+
Do **not** edit files inside \`apps/backend\`. Open issues / PRs in the
|
|
459
|
+
\`apollo-cms\` repository upstream.
|
|
460
|
+
|
|
461
|
+
## Environment variables
|
|
462
|
+
|
|
463
|
+
Shared dev env lives in the root \`.env.local\`. The backend reads its own
|
|
464
|
+
\`apps/backend/.env.local\` (already populated by the installer).
|
|
465
|
+
`;
|
|
466
|
+
writeFileSync(resolve(targetDir, "README.md"), readme);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// ─── Main ────────────────────────────────────────────────────────────────────
|
|
470
|
+
|
|
471
|
+
async function main() {
|
|
472
|
+
const flags = parseArgs(process.argv);
|
|
473
|
+
|
|
474
|
+
if (flags.help) {
|
|
475
|
+
log(HELP_TEXT);
|
|
476
|
+
process.exit(0);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
if (!flags.directory) {
|
|
480
|
+
log(HELP_TEXT);
|
|
481
|
+
fatal("Please provide a directory name.\n Example: npx create-apollo-monorepo thamc-new");
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
log(`\n${COLORS.bold}${COLORS.cyan} Apollo CMS Monorepo Installer${COLORS.reset}\n`);
|
|
485
|
+
|
|
486
|
+
// ── Step 1: Pre-flight ──
|
|
487
|
+
const targetDir = preflight(flags);
|
|
488
|
+
const dirName = basename(targetDir);
|
|
489
|
+
const frontendName = flags.frontendName ?? `@${dirName}/frontend`;
|
|
490
|
+
|
|
491
|
+
// ── Step 2: Gather env ──
|
|
492
|
+
step(2, "Configuring environment");
|
|
493
|
+
const { dbUrl, siteUrl, locale } = await gatherEnv(flags);
|
|
494
|
+
const authSecret = randomBytes(48).toString("base64");
|
|
495
|
+
success(`Frontend pkg name: ${frontendName}`);
|
|
496
|
+
|
|
497
|
+
// ── Step 3: Scaffold root ──
|
|
498
|
+
step(3, "Scaffolding monorepo root");
|
|
499
|
+
mkdirSync(targetDir, { recursive: true });
|
|
500
|
+
mkdirSync(resolve(targetDir, "apps"), { recursive: true });
|
|
501
|
+
writeRootPackageJson(targetDir, dirName);
|
|
502
|
+
writePnpmWorkspace(targetDir);
|
|
503
|
+
writeRootGitignore(targetDir);
|
|
504
|
+
writeRootEnv(targetDir, { dbUrl, siteUrl, locale, authSecret });
|
|
505
|
+
writeReadme(targetDir, dirName, frontendName);
|
|
506
|
+
success("package.json, pnpm-workspace.yaml, .gitignore, .env.local, README.md");
|
|
507
|
+
|
|
508
|
+
// ── Step 4: git init ──
|
|
509
|
+
step(4, "Initializing git");
|
|
510
|
+
run("git init -b main", { cwd: targetDir, stdio: "pipe" });
|
|
511
|
+
success("git repo initialized");
|
|
512
|
+
|
|
513
|
+
// ── Step 5: Frontend skeleton ──
|
|
514
|
+
step(5, "Creating frontend app");
|
|
515
|
+
writeFrontendApp(targetDir, frontendName, siteUrl);
|
|
516
|
+
success(`apps/frontend (${frontendName})`);
|
|
517
|
+
|
|
518
|
+
// ── Step 6: Backend submodule ──
|
|
519
|
+
if (flags.skipSubmodule) {
|
|
520
|
+
step(6, "Skipping git submodule (--skip-submodule)");
|
|
521
|
+
warn(
|
|
522
|
+
`Add it later:\n cd ${dirName}\n git submodule add -b ${flags.backendBranch} ${flags.backendUrl} ${BACKEND_PATH}`,
|
|
523
|
+
);
|
|
524
|
+
} else {
|
|
525
|
+
step(6, `Adding apollo-cms as git submodule (${BACKEND_PATH})`);
|
|
526
|
+
try {
|
|
527
|
+
run(
|
|
528
|
+
`git submodule add -b ${flags.backendBranch} ${flags.backendUrl} ${BACKEND_PATH}`,
|
|
529
|
+
{ cwd: targetDir },
|
|
530
|
+
);
|
|
531
|
+
run(`git submodule update --init --recursive`, { cwd: targetDir });
|
|
532
|
+
success("submodule added");
|
|
533
|
+
} catch {
|
|
534
|
+
fatal(
|
|
535
|
+
`Failed to add submodule.\n Check the URL and your network, then run:\n cd ${dirName} && git submodule add -b ${flags.backendBranch} ${flags.backendUrl} ${BACKEND_PATH}`,
|
|
536
|
+
);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// Mirror env to backend
|
|
540
|
+
const backendEnv = [
|
|
541
|
+
`DATABASE_URL=${dbUrl}`,
|
|
542
|
+
`APOLLO_SECRET=${authSecret}`,
|
|
543
|
+
`BETTER_AUTH_SECRET=${authSecret}`,
|
|
544
|
+
`NEXT_PUBLIC_SITE_URL=${siteUrl}`,
|
|
545
|
+
`NEXT_PUBLIC_DEFAULT_LOCALE=${locale}`,
|
|
546
|
+
"",
|
|
547
|
+
].join("\n");
|
|
548
|
+
writeFileSync(resolve(targetDir, BACKEND_PATH, ".env.local"), backendEnv);
|
|
549
|
+
success("apps/backend/.env.local");
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// ── Step 7: Install ──
|
|
553
|
+
if (flags.skipInstall) {
|
|
554
|
+
step(7, "Skipping dependency installation (--skip-install)");
|
|
555
|
+
warn(`Run later:\n cd ${dirName} && pnpm install`);
|
|
556
|
+
} else if (!commandExists("pnpm")) {
|
|
557
|
+
step(7, "Skipping dependency installation (pnpm not installed)");
|
|
558
|
+
warn(`Install pnpm and run:\n npm i -g pnpm && cd ${dirName} && pnpm install`);
|
|
559
|
+
} else {
|
|
560
|
+
step(7, "Installing dependencies (pnpm)");
|
|
561
|
+
try {
|
|
562
|
+
run("pnpm install", { cwd: targetDir });
|
|
563
|
+
success("dependencies installed");
|
|
564
|
+
} catch {
|
|
565
|
+
warn(`Install failed — run manually: cd ${dirName} && pnpm install`);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// ── Done ──
|
|
570
|
+
log(`
|
|
571
|
+
${COLORS.green}${COLORS.bold} Monorepo created!${COLORS.reset}
|
|
572
|
+
|
|
573
|
+
${COLORS.bold}cd${COLORS.reset} ${dirName}
|
|
574
|
+
${COLORS.bold}pnpm backend:setup${COLORS.reset} ${COLORS.dim}# push schema + seed apollo-cms${COLORS.reset}
|
|
575
|
+
${COLORS.bold}pnpm dev${COLORS.reset} ${COLORS.dim}# frontend :3001 + backend :3000${COLORS.reset}
|
|
576
|
+
|
|
577
|
+
${COLORS.dim}APOLLO_SECRET=${authSecret}${COLORS.reset}
|
|
578
|
+
`);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
main().catch((err) => {
|
|
582
|
+
fatal(err.message ?? String(err));
|
|
583
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-apollo-monorepo",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Scaffold a monorepo with a frontend app and Apollo CMS as a git submodule backend",
|
|
5
|
+
"bin": "./index.mjs",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=20"
|
|
9
|
+
},
|
|
10
|
+
"keywords": [
|
|
11
|
+
"apollo",
|
|
12
|
+
"cms",
|
|
13
|
+
"monorepo",
|
|
14
|
+
"submodule",
|
|
15
|
+
"pnpm",
|
|
16
|
+
"workspace",
|
|
17
|
+
"create",
|
|
18
|
+
"installer"
|
|
19
|
+
],
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "https://github.com/5Lab-Group-Co-Ltd/apollo-cms.git",
|
|
23
|
+
"directory": "installer-monorepo"
|
|
24
|
+
},
|
|
25
|
+
"license": "MIT"
|
|
26
|
+
}
|