modern-cms 1.1.1 → 1.1.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 +3 -3
- package/bin/lib.js +53 -30
- package/bin/modern-cms.js +16 -8
- package/package.json +1 -1
- package/template/package.json +2 -2
package/README.md
CHANGED
|
@@ -38,9 +38,9 @@ npx modern-cms
|
|
|
38
38
|
| 1 | Project directory | Must be empty or not yet exist |
|
|
39
39
|
| 2 | Database engine | PostgreSQL or MySQL |
|
|
40
40
|
| 3 | Database host / port / name / username / password | Builds `DATABASE_URL` and rewrites the Prisma schema's datasource for you |
|
|
41
|
-
| 4 |
|
|
42
|
-
| 5 |
|
|
43
|
-
| 6 |
|
|
41
|
+
| 4 | Web app port / API port | Defaults `3000` / `4000` |
|
|
42
|
+
| 5 | Storage provider | AWS S3 (or S3-compatible) / Azure Blob Storage / Cloudinary |
|
|
43
|
+
| 6 | Provider credentials | Endpoint/keys/bucket for S3, connection string/container for Azure, cloud name/API key/secret for Cloudinary |
|
|
44
44
|
| 7 | Public site URL / API URL | Defaults follow whatever ports you picked |
|
|
45
45
|
| 8 | Admin email / password | Leave the password blank to auto-generate one (printed once, at the end) |
|
|
46
46
|
|
package/bin/lib.js
CHANGED
|
@@ -20,57 +20,80 @@ export function usesDbPush(engine) {
|
|
|
20
20
|
return engine === "mysql";
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
/** dotenv treats `#` as a start-of-comment marker on unquoted values (so a password
|
|
24
|
+
* like "Adm!n#2024" silently truncates to "Adm!n" — confirmed by testing dotenv.parse
|
|
25
|
+
* directly) and trims unquoted trailing whitespace. Wrapping every value in double
|
|
26
|
+
* quotes fixes both.
|
|
27
|
+
*
|
|
28
|
+
* Backslashes and embedded quotes need escaping too — dotenv's matcher stops at the
|
|
29
|
+
* first *unescaped* `"`, so a literal quote in the value would end the match early and
|
|
30
|
+
* corrupt everything after it on that line (confirmed against dotenv's own source:
|
|
31
|
+
* lib/main.js's LINE regex uses `(?:\\"|[^"])*` for double-quoted values). Note dotenv
|
|
32
|
+
* itself never un-escapes `\"`/`\\` back out on the read side (only `\n`/`\r` get
|
|
33
|
+
* expanded) — so a value containing a literal backslash or quote character can't be
|
|
34
|
+
* perfectly round-tripped by any quoting strategy; escaping still prevents the much
|
|
35
|
+
* worse failure (truncation/corrupted file), it just means those two specific
|
|
36
|
+
* characters aren't recommended in a password if byte-for-byte fidelity matters. */
|
|
37
|
+
function quoteEnvValue(value) {
|
|
38
|
+
const str = String(value ?? "");
|
|
39
|
+
return `"${str.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function envLine(key, value) {
|
|
43
|
+
return `${key}=${quoteEnvValue(value)}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
23
46
|
export function buildStorageEnvLines(provider, values) {
|
|
24
47
|
if (provider === "s3") {
|
|
25
48
|
return [
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
49
|
+
envLine("S3_ENDPOINT", values.endpoint),
|
|
50
|
+
envLine("S3_REGION", values.region),
|
|
51
|
+
envLine("S3_ACCESS_KEY_ID", values.accessKeyId),
|
|
52
|
+
envLine("S3_SECRET_ACCESS_KEY", values.secretAccessKey),
|
|
53
|
+
envLine("S3_BUCKET", values.bucket),
|
|
54
|
+
envLine("S3_PUBLIC_URL", values.publicUrl),
|
|
55
|
+
envLine("S3_FORCE_PATH_STYLE", String(values.forcePathStyle)),
|
|
33
56
|
];
|
|
34
57
|
}
|
|
35
58
|
if (provider === "azure") {
|
|
36
59
|
return [
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
...(values.publicUrl ? [
|
|
60
|
+
envLine("AZURE_STORAGE_CONNECTION_STRING", values.connectionString),
|
|
61
|
+
envLine("AZURE_STORAGE_CONTAINER", values.container),
|
|
62
|
+
...(values.publicUrl ? [envLine("AZURE_STORAGE_PUBLIC_URL", values.publicUrl)] : []),
|
|
40
63
|
];
|
|
41
64
|
}
|
|
42
65
|
return [
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
66
|
+
envLine("CLOUDINARY_CLOUD_NAME", values.cloudName),
|
|
67
|
+
envLine("CLOUDINARY_API_KEY", values.apiKey),
|
|
68
|
+
envLine("CLOUDINARY_API_SECRET", values.apiSecret),
|
|
69
|
+
envLine("CLOUDINARY_FOLDER", values.folder),
|
|
47
70
|
];
|
|
48
71
|
}
|
|
49
72
|
|
|
50
73
|
export function buildApiEnv({ databaseUrl, jwtSecret, adminEmail, adminPassword, storageProvider, storageEnvLines, siteUrl, apiPort, revalidateSecret }) {
|
|
51
74
|
return [
|
|
52
|
-
|
|
75
|
+
envLine("DATABASE_URL", databaseUrl),
|
|
53
76
|
"",
|
|
54
77
|
"# --- Auth ---",
|
|
55
|
-
|
|
56
|
-
"JWT_EXPIRES_IN
|
|
57
|
-
"COOKIE_NAME
|
|
78
|
+
envLine("JWT_SECRET", jwtSecret),
|
|
79
|
+
envLine("JWT_EXPIRES_IN", "7d"),
|
|
80
|
+
envLine("COOKIE_NAME", "pgcms_token"),
|
|
58
81
|
"",
|
|
59
82
|
"# --- Seed admin user ---",
|
|
60
|
-
|
|
61
|
-
|
|
83
|
+
envLine("SEED_ADMIN_EMAIL", adminEmail),
|
|
84
|
+
envLine("SEED_ADMIN_PASSWORD", adminPassword),
|
|
62
85
|
"",
|
|
63
86
|
`# --- File storage (${storageProvider}) ---`,
|
|
64
|
-
|
|
87
|
+
envLine("STORAGE_PROVIDER", storageProvider),
|
|
65
88
|
...storageEnvLines,
|
|
66
89
|
"",
|
|
67
90
|
"# --- API ---",
|
|
68
|
-
|
|
69
|
-
|
|
91
|
+
envLine("API_PORT", apiPort),
|
|
92
|
+
envLine("WEB_ORIGIN", siteUrl),
|
|
70
93
|
"",
|
|
71
94
|
"# --- Cache revalidation ---",
|
|
72
|
-
|
|
73
|
-
|
|
95
|
+
envLine("REVALIDATE_URL", siteUrl),
|
|
96
|
+
envLine("REVALIDATE_SECRET", revalidateSecret),
|
|
74
97
|
"",
|
|
75
98
|
"# --- Form integrations (optional) ---",
|
|
76
99
|
"FORMS_WEBHOOK_URL=",
|
|
@@ -79,16 +102,16 @@ export function buildApiEnv({ databaseUrl, jwtSecret, adminEmail, adminPassword,
|
|
|
79
102
|
}
|
|
80
103
|
|
|
81
104
|
export function buildWebEnv({ apiUrl, siteUrl, webPort }) {
|
|
82
|
-
return [
|
|
105
|
+
return [envLine("API_URL", apiUrl), envLine("SITE_URL", siteUrl), envLine("PORT", webPort), ""].join("\n");
|
|
83
106
|
}
|
|
84
107
|
|
|
85
108
|
export function buildRootEnv({ db, databaseUrl, storageProvider, storageEnvLines }) {
|
|
86
109
|
const s3Line = (prefix) => storageEnvLines.find((l) => l.startsWith(prefix));
|
|
87
110
|
return [
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
111
|
+
envLine("POSTGRES_USER", db.user),
|
|
112
|
+
envLine("POSTGRES_PASSWORD", db.password),
|
|
113
|
+
envLine("POSTGRES_DB", db.name),
|
|
114
|
+
envLine("DATABASE_URL", databaseUrl),
|
|
92
115
|
...(storageProvider === "s3"
|
|
93
116
|
? [s3Line("S3_ACCESS_KEY_ID="), s3Line("S3_SECRET_ACCESS_KEY="), s3Line("S3_BUCKET=")].filter(Boolean)
|
|
94
117
|
: []),
|
package/bin/modern-cms.js
CHANGED
|
@@ -85,12 +85,21 @@ async function main() {
|
|
|
85
85
|
if (fs.existsSync(migrationsDir)) fs.rmSync(migrationsDir, { recursive: true, force: true });
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
// --- Ports ---
|
|
89
|
+
const ports = await prompts(
|
|
90
|
+
[
|
|
91
|
+
{ type: "text", name: "webPort", message: "Web app port", initial: "3000" },
|
|
92
|
+
{ type: "text", name: "apiPort", message: "API port", initial: "4000" },
|
|
93
|
+
],
|
|
94
|
+
{ onCancel }
|
|
95
|
+
);
|
|
96
|
+
|
|
88
97
|
// --- Storage ---
|
|
89
98
|
const { provider } = await prompts(
|
|
90
99
|
{
|
|
91
100
|
type: "select",
|
|
92
101
|
name: "provider",
|
|
93
|
-
message: "File storage",
|
|
102
|
+
message: "File storage (AWS S3, Azure Blob Storage, or Cloudinary)",
|
|
94
103
|
choices: [
|
|
95
104
|
{ title: "AWS S3 (or any S3-compatible: MinIO, R2, Spaces...)", value: "s3" },
|
|
96
105
|
{ title: "Azure Blob Storage", value: "azure" },
|
|
@@ -143,13 +152,12 @@ async function main() {
|
|
|
143
152
|
}
|
|
144
153
|
const storageEnvLines = buildStorageEnvLines(provider, storageValues);
|
|
145
154
|
|
|
146
|
-
// ---
|
|
155
|
+
// --- Site / admin (necessary extras beyond what was asked for: the app needs to
|
|
156
|
+
// know its own public URLs, and the CMS needs an initial admin login) ---
|
|
147
157
|
const site = await prompts(
|
|
148
158
|
[
|
|
149
|
-
{ type: "text", name: "
|
|
150
|
-
{ type: "text", name: "
|
|
151
|
-
{ type: "text", name: "siteUrl", message: "Public site URL", initial: (_, values) => `http://localhost:${values.webPort}` },
|
|
152
|
-
{ type: "text", name: "apiUrl", message: "API URL", initial: (_, values) => `http://localhost:${values.apiPort}` },
|
|
159
|
+
{ type: "text", name: "siteUrl", message: "Public site URL", initial: `http://localhost:${ports.webPort}` },
|
|
160
|
+
{ type: "text", name: "apiUrl", message: "API URL", initial: `http://localhost:${ports.apiPort}` },
|
|
153
161
|
{ type: "text", name: "adminEmail", message: "Admin login email", initial: "admin@example.com" },
|
|
154
162
|
{ type: "password", name: "adminPassword", message: "Admin login password (leave blank to generate one)" },
|
|
155
163
|
],
|
|
@@ -165,10 +173,10 @@ async function main() {
|
|
|
165
173
|
storageProvider: provider,
|
|
166
174
|
storageEnvLines,
|
|
167
175
|
siteUrl: site.siteUrl,
|
|
168
|
-
apiPort:
|
|
176
|
+
apiPort: ports.apiPort,
|
|
169
177
|
revalidateSecret: randomSecret(),
|
|
170
178
|
});
|
|
171
|
-
const webEnv = buildWebEnv({ apiUrl: site.apiUrl, siteUrl: site.siteUrl, webPort:
|
|
179
|
+
const webEnv = buildWebEnv({ apiUrl: site.apiUrl, siteUrl: site.siteUrl, webPort: ports.webPort });
|
|
172
180
|
const rootEnv = buildRootEnv({ db, databaseUrl, storageProvider: provider, storageEnvLines });
|
|
173
181
|
|
|
174
182
|
fs.writeFileSync(path.join(dest, "apps/api/.env"), apiEnv);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "modern-cms",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.3",
|
|
4
4
|
"description": "Self-hosted, drag-and-drop website CMS (Next.js + Express + Prisma). Interactive installer scaffolds a full project with your choice of PostgreSQL/MySQL and AWS S3/Azure Blob/Cloudinary storage.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|