@smi-digital/create-smi-app 2.7.4 → 2.7.6

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/dist/index.js CHANGED
@@ -156,6 +156,9 @@ var smiTheme = {
156
156
  cursor: primary12("\u276F")
157
157
  }
158
158
  };
159
+ function toSlug(value) {
160
+ return value.toLowerCase().replace(/[^a-z0-9]+/gv, "-").replace(/^-+|-+$/gv, "");
161
+ }
159
162
  async function askIntegrationInputs(inputConfig, index = 0, answers = {}) {
160
163
  const current = inputConfig[index];
161
164
  if (!current) {
@@ -178,9 +181,17 @@ async function askIntegrationInputs(inputConfig, index = 0, answers = {}) {
178
181
  const inputConfigData = {
179
182
  message: current.message,
180
183
  validate(value) {
181
- if (current.required && value.trim().length === 0) {
184
+ const trimmed = value.trim();
185
+ if (current.required && trimmed.length === 0) {
182
186
  return "This value is required.";
183
187
  }
188
+ if (current.validate === "slug" && trimmed.length > 0) {
189
+ const isSlug = /^[a-z0-9]+(?:-[a-z0-9]+)*$/v.test(trimmed);
190
+ if (!isSlug) {
191
+ const suggestion = toSlug(trimmed);
192
+ return `Must be a lowercase slug \u2014 letters, digits and single hyphens only (no dots, uppercase or spaces).${suggestion ? ` Try: ${suggestion}` : ""}`;
193
+ }
194
+ }
184
195
  return true;
185
196
  },
186
197
  theme: smiTheme
@@ -458,7 +469,7 @@ ${stderrTail}` : "";
458
469
 
459
470
  // src/modules/scaffoldActions/createApps.ts
460
471
  import { join as join4 } from "path";
461
- import { readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
472
+ import { writeFile as writeFile2 } from "fs/promises";
462
473
  async function createFrameworkApp(projectRoot, target) {
463
474
  const generator = FRAMEWORK_GENERATORS[target.framework];
464
475
  if (!generator) {
@@ -487,14 +498,20 @@ async function createApps(projectRoot, targets) {
487
498
  target.directory,
488
499
  "config/plugins.ts"
489
500
  );
490
- let pluginsContent = await readFile3(pluginsPath, "utf8");
491
- pluginsContent = pluginsContent.replace(
492
- "export default ({ env }) => ({",
493
- "export default () => ({"
501
+ await writeFile2(pluginsPath, "export default () => ({});\n");
502
+ } catch (error) {
503
+ console.warn(
504
+ `Could not normalize config/plugins.ts: ${error instanceof Error ? error.message : String(error)}`
494
505
  );
495
- await writeFile2(pluginsPath, pluginsContent);
496
- } catch {
497
506
  }
507
+ await runStep(
508
+ "Adding Postgres driver (pg) for production",
509
+ async () => runCommandQuiet(
510
+ "npm",
511
+ ["install", "pg"],
512
+ join4(projectRoot, target.directory)
513
+ )
514
+ );
498
515
  }
499
516
  await runSequentially(index + 1);
500
517
  };
@@ -502,7 +519,7 @@ async function createApps(projectRoot, targets) {
502
519
  }
503
520
 
504
521
  // src/modules/scaffoldActions/createIntegrations.ts
505
- import { mkdir, readFile as readFile4, writeFile as writeFile3 } from "fs/promises";
522
+ import { mkdir, readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
506
523
  import { dirname as dirname2, join as join5 } from "path";
507
524
  import { randomBytes } from "crypto";
508
525
  function toTemplateToken(key) {
@@ -517,7 +534,7 @@ function applyTemplateVariables(content, values) {
517
534
  return result;
518
535
  }
519
536
  async function copyTemplateFile(sourcePath, targetPath, values) {
520
- const rawContent = await readFile4(sourcePath, "utf8");
537
+ const rawContent = await readFile3(sourcePath, "utf8");
521
538
  const content = applyTemplateVariables(rawContent, values);
522
539
  await mkdir(dirname2(targetPath), { recursive: true });
523
540
  await writeFile3(targetPath, content, "utf8");
@@ -581,7 +598,14 @@ async function createIntegrations(options, projectRoot, templatesDir) {
581
598
  api_token_salt: generateSecret(),
582
599
  admin_jwt_secret: generateSecret(),
583
600
  transfer_token_salt: generateSecret(),
584
- jwt_secret: generateSecret()
601
+ jwt_secret: generateSecret(),
602
+ // Postgres identifier derived from app_name (hyphens -> underscores), so
603
+ // the role/database name needs no quoting.
604
+ db_name: String(options.integrationInputs?.app_name ?? "").replaceAll(
605
+ "-",
606
+ "_"
607
+ ),
608
+ db_password: generateSecret()
585
609
  /* eslint-enable camelcase */
586
610
  };
587
611
  return copyTemplateFile(
@@ -604,7 +628,7 @@ async function createIntegrations(options, projectRoot, templatesDir) {
604
628
  const frontendDir = join5(projectRoot, "frontend");
605
629
  await runCommandQuiet("npx", ["astro", "add", "node", "-y"], frontendDir);
606
630
  const astroConfigPath = join5(frontendDir, "astro.config.mjs");
607
- let astroConfig = await readFile4(astroConfigPath, "utf8");
631
+ let astroConfig = await readFile3(astroConfigPath, "utf8");
608
632
  astroConfig = astroConfig.replace(
609
633
  /export default defineConfig\s*\(\s*\{/v,
610
634
  "export default defineConfig({\n output: 'server',"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smi-digital/create-smi-app",
3
- "version": "2.7.4",
3
+ "version": "2.7.6",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -0,0 +1,15 @@
1
+ # Databases this project needs on the shared Postgres instance.
2
+ # The deploy provisions each one (login role + database) automatically — no
3
+ # manual step, no SSH. This file is the GitOps source of truth for the project's
4
+ # databases; passwords live (encrypted) in production.backend.env.
5
+ #
6
+ # - name: the database AND login-role name (Postgres-safe identifier)
7
+ # - password_env: the variable in production.backend.env holding its password
8
+ #
9
+ # To add an app-managed database (e.g. a separate dataset the site reads), add an
10
+ # entry here and its connection vars to production.backend.env:
11
+ # - name: my_dataset
12
+ # password_env: MY_DATASET_DATABASE_PASSWORD
13
+ databases:
14
+ - name: __DB_NAME__
15
+ password_env: DATABASE_PASSWORD
@@ -6,9 +6,9 @@ services:
6
6
  restart: unless-stopped
7
7
  env_file:
8
8
  - ./backend.env
9
- volumes:
9
+ volumes:
10
+ # Uploads stay on local disk for now; DB lives in Postgres (no sqlite volume).
10
11
  - ./data/uploads:/opt/app/public/uploads
11
- - ./data/database:/opt/app/.tmp
12
12
  networks:
13
13
  - agency_shared_network
14
14
  healthcheck:
@@ -27,7 +27,10 @@ services:
27
27
  - "traefik.http.services.__APP_NAME__-strapi.loadbalancer.server.port=1337"
28
28
  # Strapi requires larger body sizes for media uploads (25MB)
29
29
  - "traefik.http.middlewares.__APP_NAME__-strapi-limit.buffering.maxRequestBodyBytes=25000000"
30
- - "traefik.http.routers.__APP_NAME__-strapi.middlewares=__APP_NAME__-strapi-limit"
30
+ # Global middlewares (defined in webhosting-infra/traefik/dynamic) + the local
31
+ # body-size limit. rate-limit first so abusive traffic is dropped early; this
32
+ # matters most on the public Strapi admin/API.
33
+ - "traefik.http.routers.__APP_NAME__-strapi.middlewares=rate-limit@file,secure-headers@file,__APP_NAME__-strapi-limit"
31
34
 
32
35
  __APP_NAME__-astro:
33
36
  container_name: __APP_NAME__-astro
@@ -75,6 +78,8 @@ services:
75
78
  # Listen on both www and non-www
76
79
  - "traefik.http.routers.__APP_NAME__-astro.rule=Host(`__PROJECT_DOMAIN__`) || Host(`www.__PROJECT_DOMAIN__`)"
77
80
  - "traefik.http.routers.__APP_NAME__-astro.tls.certresolver=letsencrypt"
81
+ # Global middlewares (defined in webhosting-infra/traefik/dynamic)
82
+ - "traefik.http.routers.__APP_NAME__-astro.middlewares=secure-headers@file,rate-limit@file"
78
83
  # Route traffic to the Nginx Cache (Port 80) instead of Astro directly
79
84
  - "traefik.http.services.__APP_NAME__-astro.loadbalancer.server.port=80"
80
85
 
@@ -24,10 +24,11 @@
24
24
  },
25
25
  {
26
26
  "key": "app_name",
27
- "message": "What is the app name?",
27
+ "message": "What is the app name? (lowercase slug, e.g. my-new-client)",
28
28
  "type": "input",
29
29
  "default": "my-new-client",
30
- "required": true
30
+ "required": true,
31
+ "validate": "slug"
31
32
  },
32
33
  {
33
34
  "key": "include_www",
@@ -89,6 +90,10 @@
89
90
  {
90
91
  "template": "production.backend.env.template",
91
92
  "target": "production.backend.env"
93
+ },
94
+ {
95
+ "template": "databases.yml.template",
96
+ "target": "databases.yml"
92
97
  }
93
98
  ]
94
99
  }
@@ -17,6 +17,17 @@ ADMIN_JWT_SECRET=__ADMIN_JWT_SECRET__
17
17
  TRANSFER_TOKEN_SALT=__TRANSFER_TOKEN_SALT__
18
18
  JWT_SECRET=__JWT_SECRET__
19
19
 
20
+ # Database — production uses Postgres (local dev defaults to SQLite).
21
+ # A matching role + database must exist on the shared Postgres instance,
22
+ # reachable as DATABASE_HOST on the agency_shared_network.
23
+ DATABASE_CLIENT=postgres
24
+ DATABASE_HOST=postgres
25
+ DATABASE_PORT=5432
26
+ DATABASE_NAME=__DB_NAME__
27
+ DATABASE_USERNAME=__DB_NAME__
28
+ DATABASE_PASSWORD=__DB_PASSWORD__
29
+ DATABASE_SSL=false
30
+
20
31
  # Production Settings
21
32
  HOST=0.0.0.0
22
33
  PORT=1337