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 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 | Storage provider | AWS S3 (or S3-compatible) / Azure Blob Storage / Cloudinary |
42
- | 5 | Provider credentials | Endpoint/keys/bucket for S3, connection string/container for Azure, cloud name/API key/secret for Cloudinary |
43
- | 6 | Web app port / API port | Defaults `3000` / `4000` |
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
- `S3_ENDPOINT=${values.endpoint}`,
27
- `S3_REGION=${values.region}`,
28
- `S3_ACCESS_KEY_ID=${values.accessKeyId}`,
29
- `S3_SECRET_ACCESS_KEY=${values.secretAccessKey}`,
30
- `S3_BUCKET=${values.bucket}`,
31
- `S3_PUBLIC_URL=${values.publicUrl}`,
32
- `S3_FORCE_PATH_STYLE=${String(values.forcePathStyle)}`,
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
- `AZURE_STORAGE_CONNECTION_STRING=${values.connectionString}`,
38
- `AZURE_STORAGE_CONTAINER=${values.container}`,
39
- ...(values.publicUrl ? [`AZURE_STORAGE_PUBLIC_URL=${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
- `CLOUDINARY_CLOUD_NAME=${values.cloudName}`,
44
- `CLOUDINARY_API_KEY=${values.apiKey}`,
45
- `CLOUDINARY_API_SECRET=${values.apiSecret}`,
46
- `CLOUDINARY_FOLDER=${values.folder}`,
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
- `DATABASE_URL=${databaseUrl}`,
75
+ envLine("DATABASE_URL", databaseUrl),
53
76
  "",
54
77
  "# --- Auth ---",
55
- `JWT_SECRET=${jwtSecret}`,
56
- "JWT_EXPIRES_IN=7d",
57
- "COOKIE_NAME=pgcms_token",
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
- `SEED_ADMIN_EMAIL=${adminEmail}`,
61
- `SEED_ADMIN_PASSWORD=${adminPassword}`,
83
+ envLine("SEED_ADMIN_EMAIL", adminEmail),
84
+ envLine("SEED_ADMIN_PASSWORD", adminPassword),
62
85
  "",
63
86
  `# --- File storage (${storageProvider}) ---`,
64
- `STORAGE_PROVIDER=${storageProvider}`,
87
+ envLine("STORAGE_PROVIDER", storageProvider),
65
88
  ...storageEnvLines,
66
89
  "",
67
90
  "# --- API ---",
68
- `API_PORT=${apiPort}`,
69
- `WEB_ORIGIN=${siteUrl}`,
91
+ envLine("API_PORT", apiPort),
92
+ envLine("WEB_ORIGIN", siteUrl),
70
93
  "",
71
94
  "# --- Cache revalidation ---",
72
- `REVALIDATE_URL=${siteUrl}`,
73
- `REVALIDATE_SECRET=${revalidateSecret}`,
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 [`API_URL=${apiUrl}`, `SITE_URL=${siteUrl}`, `PORT=${webPort}`, ""].join("\n");
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
- `POSTGRES_USER=${db.user}`,
89
- `POSTGRES_PASSWORD=${db.password}`,
90
- `POSTGRES_DB=${db.name}`,
91
- `DATABASE_URL=${databaseUrl}`,
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
- // --- Ports / site / admin ---
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: "webPort", message: "Web app port", initial: "3000" },
150
- { type: "text", name: "apiPort", message: "API port", initial: "4000" },
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: site.apiPort,
176
+ apiPort: ports.apiPort,
169
177
  revalidateSecret: randomSecret(),
170
178
  });
171
- const webEnv = buildWebEnv({ apiUrl: site.apiUrl, siteUrl: site.siteUrl, webPort: site.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.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": {
@@ -2,8 +2,8 @@
2
2
  "name": "pg-cms",
3
3
  "private": true,
4
4
  "engines": {
5
- "node": "^24.0.0",
6
- "npm": "^11.0.0"
5
+ "node": ">=18.0.0",
6
+ "npm": ">=9.0.0"
7
7
  },
8
8
  "workspaces": [
9
9
  "packages/*",