modern-cms 1.1.2 → 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/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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "modern-cms",
3
- "version": "1.1.2",
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/*",