create-projx 1.6.3 → 1.6.4

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
@@ -39,7 +39,7 @@ Ask an LLM to "scaffold a full-stack app" and you get 50 files of plausible-look
39
39
  ```bash
40
40
  npx create-projx my-app # interactive — pick exactly what you need
41
41
  cd my-app
42
- ./setup.sh # installs everything you picked
42
+ ./scripts/setup.sh # installs everything you picked
43
43
  ```
44
44
 
45
45
  Pick any combination of components — they're all optional:
@@ -116,7 +116,7 @@ npx create-projx my-app -y
116
116
 
117
117
  ## Package Manager Support
118
118
 
119
- Projx supports **npm**, **pnpm**, **yarn**, and **bun**. During `create`, you're prompted to pick one. The choice is stored in `.projx` and used everywhere — setup.sh, Docker, CI, pre-commit hooks, and README.
119
+ Projx supports **npm**, **pnpm**, **yarn**, and **bun**. During `create`, you're prompted to pick one. The choice is stored in `.projx` and used everywhere — `scripts/setup.sh`, Docker, CI, pre-commit hooks, and README.
120
120
 
121
121
  ```json
122
122
  { "packageManager": "pnpm" }
@@ -166,7 +166,7 @@ Need a second backend service alongside an existing one (e.g. an SMTP listener n
166
166
  npx create-projx add fastify --name email-ingestor
167
167
  ```
168
168
 
169
- Creates `email-ingestor/` with the fastify scaffold and a `.projx-component` marker. Each instance gets its own job in `.github/workflows/ci.yml`, its own section in `.githooks/pre-commit`, and its own install step in `setup.sh`. `update` keeps every instance refreshed on every run.
169
+ Creates `email-ingestor/` with the fastify scaffold and a `.projx-component` marker. Each instance gets its own job in `.github/workflows/ci.yml`, its own section in `.githooks/pre-commit`, and its own install step in `scripts/setup.sh`. `update` keeps every instance refreshed on every run.
170
170
 
171
171
  ### Update Scaffolding
172
172
 
@@ -191,7 +191,7 @@ Common user-owned files are **default-skipped** automatically — template updat
191
191
 
192
192
  | Scope | Default skips |
193
193
  |-------|---------------|
194
- | Root (`.projx`) | `docker-compose.yml`, `docker-compose.dev.yml`, `README.md`, `.githooks/pre-commit`, `.github/workflows/ci.yml`, `setup.sh` |
194
+ | Root (`.projx`) | `docker-compose.yml`, `docker-compose.dev.yml`, `README.md`, `.githooks/pre-commit`, `.github/workflows/ci.yml`, `scripts/setup.sh`, `scripts/setup-docker.sh`, `scripts/setup-ssl.sh` |
195
195
  | fastapi | `pyproject.toml` |
196
196
  | fastify / frontend / e2e | `package.json` |
197
197
  | mobile | `pubspec.yaml` |
@@ -336,7 +336,7 @@ backend/.projx-component → { "components": ["fastapi"] }
336
336
  web/.projx-component → { "components": ["frontend"] }
337
337
  ```
338
338
 
339
- CI, setup.sh, pre-commit hooks, and docker-compose are all regenerated with your custom directory names.
339
+ CI, `scripts/setup.sh`, pre-commit hooks, and docker-compose are all regenerated with your custom directory names.
340
340
 
341
341
  ## What a Scaffolded Project Looks Like
342
342
 
@@ -353,7 +353,7 @@ my-app/
353
353
  ├── .github/workflows/ # CI per component (runs only on changes)
354
354
  ├── .githooks/pre-commit # Format + lint on commit
355
355
  ├── .vscode/ # Editor settings + recommended extensions
356
- ├── setup.sh # Install all deps
356
+ ├── scripts/ # setup.sh, setup-docker.sh, setup-ssl.sh
357
357
  └── .projx # Components list + version
358
358
  ```
359
359
 
@@ -378,7 +378,7 @@ Contributing to Projx itself:
378
378
  ```bash
379
379
  git clone https://github.com/ukanhaupa/projx.git
380
380
  cd projx
381
- ./setup.sh
381
+ ./scripts/setup.sh
382
382
  ```
383
383
 
384
384
  The CLI lives in `cli/`. Templates are the root-level component directories (`fastapi/`, `frontend/`, etc.).
@@ -8,8 +8,8 @@ import {
8
8
  matchesSkip,
9
9
  saveBaselineRef,
10
10
  writeTemplateToDir
11
- } from "./chunk-OBYYB6PR.js";
12
- import "./chunk-LYPPFXGK.js";
11
+ } from "./chunk-XQ7FE4U3.js";
12
+ import "./chunk-6YRBHJ2V.js";
13
13
  export {
14
14
  BASELINE_REF,
15
15
  applyTemplate,
@@ -1,7 +1,15 @@
1
1
  // src/utils.ts
2
2
  import { execSync } from "child_process";
3
3
  import { existsSync, readFileSync } from "fs";
4
- import { cp, mkdir, readdir, readFile, rm, writeFile } from "fs/promises";
4
+ import {
5
+ chmod,
6
+ cp,
7
+ mkdir,
8
+ readdir,
9
+ readFile,
10
+ rm,
11
+ writeFile
12
+ } from "fs/promises";
5
13
  import { join, resolve } from "path";
6
14
  import { tmpdir } from "os";
7
15
  import { fileURLToPath } from "url";
@@ -205,10 +213,19 @@ async function copyStaticFiles(repoDir, dest) {
205
213
  await cp(extensionsJson, join(dest, ".vscode/extensions.json"));
206
214
  manifest.push(".vscode/extensions.json");
207
215
  }
208
- const scripts = join(tpl, "scripts");
209
- if (existsSync(scripts)) {
210
- await cp(scripts, join(dest, "scripts"), { recursive: true });
211
- manifest.push("scripts/setup-ssl.sh");
216
+ const staticScripts = ["setup-docker.sh", "setup-ssl.sh"];
217
+ const scriptsSrc = join(tpl, "scripts");
218
+ if (existsSync(scriptsSrc)) {
219
+ await mkdir(join(dest, "scripts"), { recursive: true });
220
+ for (const file of staticScripts) {
221
+ const src = join(scriptsSrc, file);
222
+ const dst = join(dest, "scripts", file);
223
+ if (existsSync(src) && !existsSync(dst)) {
224
+ await cp(src, dst);
225
+ await chmod(dst, 493);
226
+ manifest.push(`scripts/${file}`);
227
+ }
228
+ }
212
229
  }
213
230
  return manifest;
214
231
  }
@@ -313,7 +330,9 @@ var DEFAULT_ROOT_SKIP_PATTERNS = [
313
330
  "README.md",
314
331
  ".githooks/pre-commit",
315
332
  ".github/workflows/ci.yml",
316
- "setup.sh"
333
+ "scripts/setup.sh",
334
+ "scripts/setup-docker.sh",
335
+ "scripts/setup-ssl.sh"
317
336
  ];
318
337
  var DEFAULT_COMPONENT_SKIP_PATTERNS = {
319
338
  fastapi: ["pyproject.toml"],
@@ -446,12 +465,17 @@ function renderLines(lines, vars) {
446
465
  continue;
447
466
  }
448
467
  if (stack.length > 0 && stack.some((v) => !v.active)) continue;
449
- const replaced = line.replace(/<%=\s*([\w.]+)\s*%>/g, (_, expr) => {
450
- const parts = expr.split(".");
451
- let val = vars;
452
- for (const p of parts) {
453
- val = val?.[p];
468
+ const replaced = line.replace(/<%=\s*(.+?)\s*%>/g, (_, expr) => {
469
+ const trimmed = expr.trim();
470
+ if (/^[\w.]+$/.test(trimmed)) {
471
+ const parts = trimmed.split(".");
472
+ let val2 = vars;
473
+ for (const p of parts) {
474
+ val2 = val2?.[p];
475
+ }
476
+ return String(val2 ?? "");
454
477
  }
478
+ const val = evalExpr(trimmed, vars);
455
479
  return String(val ?? "");
456
480
  });
457
481
  output.push(replaced);
@@ -13,7 +13,7 @@ import {
13
13
  toSnake,
14
14
  upsertComponentMarker,
15
15
  writeProjxConfig
16
- } from "./chunk-LYPPFXGK.js";
16
+ } from "./chunk-6YRBHJ2V.js";
17
17
 
18
18
  // src/baseline.ts
19
19
  import { existsSync, writeFileSync, unlinkSync } from "fs";
@@ -44,7 +44,10 @@ var CANONICAL_DISPLAY = {
44
44
  infra: "Terraform"
45
45
  };
46
46
  function withInstances(vars) {
47
- const base = vars.instances && vars.instances.length > 0 ? vars.instances : vars.components.map((type) => ({ type, path: vars.paths[type] ?? type }));
47
+ const base = vars.instances && vars.instances.length > 0 ? vars.instances : vars.components.map((type) => ({
48
+ type,
49
+ path: vars.paths[type] ?? type
50
+ }));
48
51
  const enriched = base.map((inst) => ({
49
52
  ...inst,
50
53
  upper: shellSafeUpper(inst.path),
@@ -124,7 +127,7 @@ function generateVscodeSettings(vars) {
124
127
  // src/baseline.ts
125
128
  var BASELINE_REF = "refs/projx/baseline";
126
129
  async function migrateComponentMarkers(cwd, components, componentPaths, applyDefaults) {
127
- const { readComponentMarker: readComponentMarker2, writeComponentMarker } = await import("./utils-BXHJP6HF.js");
130
+ const { readComponentMarker: readComponentMarker2, writeComponentMarker } = await import("./utils-AVKSTHIF.js");
128
131
  for (const component of components) {
129
132
  const dir = componentPaths[component];
130
133
  const markerDir = join2(cwd, dir);
@@ -392,7 +395,10 @@ async function writeTemplateToDir(dest, repoDir, components, componentPaths, var
392
395
  type,
393
396
  path: componentPaths[type]
394
397
  }));
395
- const allInstances = [...primaryInstances, ...extraInstances];
398
+ const allInstances = [
399
+ ...primaryInstances,
400
+ ...extraInstances
401
+ ];
396
402
  const toScaffold = instancesToScaffold ?? allInstances;
397
403
  for (const inst of toScaffold) {
398
404
  await writeOneInstance(inst, {
@@ -444,9 +450,13 @@ async function writeTemplateToDir(dest, repoDir, components, componentPaths, var
444
450
  await generateCiYml(vars)
445
451
  );
446
452
  }
447
- if (shouldWrite("setup.sh")) {
448
- await writeFile(join2(dest, "setup.sh"), await generateSetupSh(vars));
449
- await chmod(join2(dest, "setup.sh"), 493);
453
+ if (shouldWrite("scripts/setup.sh")) {
454
+ await mkdir(join2(dest, "scripts"), { recursive: true });
455
+ await writeFile(
456
+ join2(dest, "scripts/setup.sh"),
457
+ await generateSetupSh(vars)
458
+ );
459
+ await chmod(join2(dest, "scripts/setup.sh"), 493);
450
460
  }
451
461
  await copyStaticFiles(repoDir, dest);
452
462
  if (shouldWrite(".vscode/settings.json")) {
@@ -459,14 +469,26 @@ async function writeTemplateToDir(dest, repoDir, components, componentPaths, var
459
469
  await writeManagedProjx(dest, version, vars, applyDefaults);
460
470
  }
461
471
  async function writeOneInstance(inst, opts) {
462
- const { dest, repoDir, vars, componentPaths, realCwd, applyDefaults, baseSkip, projectName, nameSnake } = opts;
472
+ const {
473
+ dest,
474
+ repoDir,
475
+ vars,
476
+ componentPaths,
477
+ realCwd,
478
+ applyDefaults,
479
+ baseSkip,
480
+ projectName,
481
+ nameSnake
482
+ } = opts;
463
483
  const { type, path: targetDir } = inst;
464
484
  const realMarker = await readComponentMarker(join2(realCwd, targetDir));
465
485
  const isNewMarker = !realMarker;
466
486
  const shouldApplyComponentDefault = isNewMarker || applyDefaults;
467
487
  const markerSkip = realMarker?.skip ?? [];
468
488
  const defaultSkip = shouldApplyComponentDefault ? DEFAULT_COMPONENT_SKIP_PATTERNS[type] ?? [] : [];
469
- const skipPatterns = [.../* @__PURE__ */ new Set([...baseSkip, ...markerSkip, ...defaultSkip])];
489
+ const skipPatterns = [
490
+ .../* @__PURE__ */ new Set([...baseSkip, ...markerSkip, ...defaultSkip])
491
+ ];
470
492
  const tmpDir = join2(dest, "__cptmp__");
471
493
  await copyComponent(repoDir, type, tmpDir);
472
494
  const srcDir = join2(tmpDir, type);
@@ -480,33 +502,62 @@ async function writeOneInstance(inst, opts) {
480
502
  await cp(srcDir, outDir, { recursive: true, force: true });
481
503
  }
482
504
  await rm(tmpDir, { recursive: true, force: true });
483
- const instancePaths = { ...componentPaths, [type]: targetDir };
505
+ const instancePaths = {
506
+ ...componentPaths,
507
+ [type]: targetDir
508
+ };
484
509
  await renderEjsInDir(outDir, { ...vars, paths: instancePaths });
485
510
  await upsertComponentMarker(
486
511
  join2(dest, targetDir),
487
512
  type,
488
513
  skipPatterns.length > 0 ? skipPatterns : void 0
489
514
  );
490
- await substituteNamesForInstance(inst, dest, projectName, nameSnake, vars.nameOverrides);
515
+ await substituteNamesForInstance(
516
+ inst,
517
+ dest,
518
+ projectName,
519
+ nameSnake,
520
+ vars.nameOverrides
521
+ );
491
522
  }
492
523
  async function substituteNamesForInstance(inst, dest, name, nameSnake, overrides) {
493
524
  const { type, path } = inst;
494
525
  const isCanonical = path === type;
495
526
  if (type === "fastapi") {
496
527
  const target = isCanonical ? overrides?.fastapi ?? `${name}-fastapi` : `${name}-${path}`;
497
- await replaceInFile(join2(dest, `${path}/pyproject.toml`), "projx-fastapi", target);
528
+ await replaceInFile(
529
+ join2(dest, `${path}/pyproject.toml`),
530
+ "projx-fastapi",
531
+ target
532
+ );
498
533
  } else if (type === "fastify") {
499
534
  const target = isCanonical ? overrides?.fastify ?? `${name}-fastify` : `${name}-${path}`;
500
- await replaceInFile(join2(dest, `${path}/package.json`), "projx-fastify", target);
535
+ await replaceInFile(
536
+ join2(dest, `${path}/package.json`),
537
+ "projx-fastify",
538
+ target
539
+ );
501
540
  } else if (type === "frontend") {
502
541
  const target = isCanonical ? overrides?.frontend ?? `${name}-frontend` : `${name}-${path}`;
503
- await replaceInFile(join2(dest, `${path}/package.json`), "projx-frontend", target);
542
+ await replaceInFile(
543
+ join2(dest, `${path}/package.json`),
544
+ "projx-frontend",
545
+ target
546
+ );
504
547
  } else if (type === "e2e") {
505
548
  const target = isCanonical ? overrides?.e2e ?? `${name}-e2e` : `${name}-${path}`;
506
- await replaceInFile(join2(dest, `${path}/package.json`), "projx-e2e", target);
549
+ await replaceInFile(
550
+ join2(dest, `${path}/package.json`),
551
+ "projx-e2e",
552
+ target
553
+ );
507
554
  } else if (type === "mobile") {
508
555
  const target = isCanonical ? overrides?.mobile ?? `${nameSnake}_mobile` : toSnake(`${nameSnake}_${path}`);
509
- await replaceInFile(join2(dest, `${path}/pubspec.yaml`), "projx_mobile", target);
556
+ await replaceInFile(
557
+ join2(dest, `${path}/pubspec.yaml`),
558
+ "projx_mobile",
559
+ target
560
+ );
510
561
  await replaceInDir(
511
562
  join2(dest, path),
512
563
  "package:projx_mobile/",
package/dist/index.js CHANGED
@@ -9,10 +9,11 @@ import {
9
9
  matchesSkip,
10
10
  saveBaselineRef,
11
11
  writeTemplateToDir
12
- } from "./chunk-OBYYB6PR.js";
12
+ } from "./chunk-XQ7FE4U3.js";
13
13
  import {
14
14
  COMPONENTS,
15
15
  COMPONENT_MARKER,
16
+ DEFAULT_ROOT_SKIP_PATTERNS,
16
17
  EXCLUDE,
17
18
  PACKAGE_MANAGERS,
18
19
  cleanupRepo,
@@ -33,7 +34,7 @@ import {
33
34
  toTitle,
34
35
  writeComponentMarker,
35
36
  writeProjxConfig
36
- } from "./chunk-LYPPFXGK.js";
37
+ } from "./chunk-6YRBHJ2V.js";
37
38
 
38
39
  // src/index.ts
39
40
  import { existsSync as existsSync11 } from "fs";
@@ -164,7 +165,7 @@ async function scaffold(opts, dest, localRepo) {
164
165
  `Done! Next steps:
165
166
 
166
167
  cd ${name}
167
- ./setup.sh
168
+ ./scripts/setup.sh
168
169
 
169
170
  Like projx? Star it: https://github.com/ukanhaupa/projx`
170
171
  );
@@ -474,7 +475,7 @@ function hasUncommittedChanges(cwd) {
474
475
  async function findPinnedFilesWithUpdates(cwd, repoDir, components, componentPaths, vars, version, componentSkips, rootSkip) {
475
476
  const { mkdir: mkdir5, rm: rm2, readFile: readFile7 } = await import("fs/promises");
476
477
  const { tmpdir: tmpdir2 } = await import("os");
477
- const { writeTemplateToDir: writeTemplateToDir2 } = await import("./baseline-RXPDDEDD.js");
478
+ const { writeTemplateToDir: writeTemplateToDir2 } = await import("./baseline-PZM4KJJW.js");
478
479
  const config = await readProjxConfig(cwd);
479
480
  const rootPinned = Array.isArray(config.skip) ? config.skip : [];
480
481
  const componentPinned = [];
@@ -806,7 +807,9 @@ async function addInstance(cwd, type, customName, config, existing, localRepo, s
806
807
  const INSTANCE_AWARE_ROOT = /* @__PURE__ */ new Set([
807
808
  ".github/workflows/ci.yml",
808
809
  ".githooks/pre-commit",
809
- "setup.sh"
810
+ "scripts/setup.sh",
811
+ "docker-compose.yml",
812
+ "docker-compose.dev.yml"
810
813
  ]);
811
814
  const rawSkip = Array.isArray(config.skip) ? config.skip : [];
812
815
  const rootSkip = rawSkip.filter((p11) => !INSTANCE_AWARE_ROOT.has(p11));
@@ -1056,12 +1059,14 @@ async function init(cwd, localRepo) {
1056
1059
  spinner7.stop(
1057
1060
  detected.length > 0 ? `Found ${detected.length} component(s).` : "No components detected."
1058
1061
  );
1059
- let confirmed;
1060
- if (detected.length > 0) {
1061
- confirmed = await confirmDetections(detected);
1062
- } else {
1063
- confirmed = await manualSelect(cwd);
1062
+ if (detected.length === 0) {
1063
+ await writeBareProjx(cwd, localRepo, isLocal, detectPackageManager(cwd));
1064
+ p5.outro(
1065
+ "Initialized empty .projx. Add components with 'npx create-projx add <component>'."
1066
+ );
1067
+ return;
1064
1068
  }
1069
+ const confirmed = await confirmDetections(detected);
1065
1070
  if (confirmed.length === 0) {
1066
1071
  p5.log.warn("No components selected. Nothing to do.");
1067
1072
  process.exit(0);
@@ -1161,6 +1166,36 @@ async function init(cwd, localRepo) {
1161
1166
  await cleanupRepo(repoDir, isLocal);
1162
1167
  }
1163
1168
  }
1169
+ async function writeBareProjx(cwd, localRepo, isLocal, pm) {
1170
+ const dlSpinner = p5.spinner();
1171
+ dlSpinner.start(
1172
+ isLocal ? "Using local templates" : "Downloading latest templates"
1173
+ );
1174
+ const repoDir = await downloadRepo(localRepo).catch((err) => {
1175
+ dlSpinner.stop("Failed.");
1176
+ p5.log.error(String(err));
1177
+ process.exit(1);
1178
+ });
1179
+ dlSpinner.stop(isLocal ? "Local templates loaded." : "Templates downloaded.");
1180
+ try {
1181
+ const pkg = JSON.parse(
1182
+ await readFile4(join5(repoDir, "cli/package.json"), "utf-8")
1183
+ );
1184
+ const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
1185
+ const config = {
1186
+ version: pkg.version,
1187
+ createdAt: today,
1188
+ updatedAt: today,
1189
+ skip: [...DEFAULT_ROOT_SKIP_PATTERNS],
1190
+ defaultsApplied: true
1191
+ };
1192
+ if (pm) config.packageManager = pm;
1193
+ await writeProjxConfig(cwd, config);
1194
+ saveBaselineRef(cwd);
1195
+ } finally {
1196
+ await cleanupRepo(repoDir, isLocal);
1197
+ }
1198
+ }
1164
1199
  async function confirmDetections(detected) {
1165
1200
  const confirmed = [];
1166
1201
  for (const d of detected) {
@@ -1175,33 +1210,6 @@ async function confirmDetections(detected) {
1175
1210
  }
1176
1211
  return confirmed;
1177
1212
  }
1178
- async function manualSelect(cwd) {
1179
- const selected = await p5.multiselect({
1180
- message: "No components detected. Select manually:",
1181
- options: COMPONENTS.map((c) => ({
1182
- value: c,
1183
- label: LABELS[c].label,
1184
- hint: LABELS[c].hint
1185
- })),
1186
- required: false
1187
- });
1188
- if (p5.isCancel(selected)) process.exit(0);
1189
- const result = [];
1190
- for (const component of selected) {
1191
- const dir = await p5.text({
1192
- message: `Directory for ${LABELS[component].label}?`,
1193
- placeholder: component,
1194
- defaultValue: component
1195
- });
1196
- if (p5.isCancel(dir)) process.exit(0);
1197
- if (!existsSync5(join5(cwd, dir))) {
1198
- p5.log.warn(`${dir}/ does not exist \u2014 skipping.`);
1199
- continue;
1200
- }
1201
- result.push({ component, directory: dir });
1202
- }
1203
- return result;
1204
- }
1205
1213
  function isGitRepo2(cwd) {
1206
1214
  try {
1207
1215
  execSync2("git rev-parse --is-inside-work-tree", { cwd, stdio: "pipe" });
@@ -33,7 +33,7 @@ import {
33
33
  upsertComponentMarker,
34
34
  writeComponentMarker,
35
35
  writeProjxConfig
36
- } from "./chunk-LYPPFXGK.js";
36
+ } from "./chunk-6YRBHJ2V.js";
37
37
  export {
38
38
  COMPONENTS,
39
39
  COMPONENT_MARKER,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-projx",
3
- "version": "1.6.3",
3
+ "version": "1.6.4",
4
4
  "description": "Scaffold production-grade fullstack projects in seconds. FastAPI, Fastify, React, Flutter, Terraform — with auth, database, CI/CD, E2E tests, and Docker. One command, ready to deploy.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -30,7 +30,7 @@ Scaffolded with [Projx](https://github.com/ukanhaupa/projx).
30
30
  ## Getting Started
31
31
 
32
32
  ```bash
33
- ./setup.sh # Install all dependencies
33
+ ./scripts/setup.sh # Install all dependencies
34
34
  docker compose -f docker-compose.dev.yml up # Start with Docker (dev mode)
35
35
  ```
36
36
  <% if (components.includes('fastapi')) { %>
@@ -1,5 +1,5 @@
1
1
  services:
2
- <% if (components.includes('fastapi') || components.includes('fastify')) { %>
2
+ <% if (fastapiInstances.length > 0 || fastifyInstances.length > 0) { %>
3
3
  db:
4
4
  image: postgres:16-alpine
5
5
  environment:
@@ -23,9 +23,9 @@ services:
23
23
  networks:
24
24
  - app-network
25
25
  <% } %>
26
- <% if (components.includes('fastapi')) { %>
27
- <%= paths.fastapi %>-migrate:
28
- build: ./<%= paths.fastapi %>
26
+ <% for (const inst of fastapiInstances) { %>
27
+ <%= inst.path %>-migrate:
28
+ build: ./<%= inst.path %>
29
29
  command: ['uv', 'run', 'migrate.py']
30
30
  environment:
31
31
  - SQLALCHEMY_DATABASE_URI=postgresql+asyncpg://dev:dev@db:5432/app
@@ -39,16 +39,21 @@ services:
39
39
  cpus: '0.5'
40
40
  networks:
41
41
  - app-network
42
- <%= paths.fastapi %>:
43
- build: ./<%= paths.fastapi %>
42
+ <%= inst.path %>:
43
+ build: ./<%= inst.path %>
44
44
  command:
45
45
  [
46
46
  'uv', 'run', 'uvicorn', 'src.app:app',
47
47
  '--host', '0.0.0.0', '--port', '7860',
48
48
  '--workers', '2', '--timeout-keep-alive', '120', '--reload',
49
49
  ]
50
+ <% if (inst.path === inst.type) { %>
50
51
  ports:
51
52
  - '7860:7860'
53
+ <% } else { %>
54
+ expose:
55
+ - '7860'
56
+ <% } %>
52
57
  environment:
53
58
  - SQLALCHEMY_DATABASE_URI=postgresql+asyncpg://dev:dev@db:5432/app
54
59
  - CORS_ALLOW_ORIGINS=http://localhost:3000,http://localhost
@@ -56,11 +61,11 @@ services:
56
61
  - JWT_SECRET=dev-secret-that-is-at-least-32-bytes-long
57
62
  - JWT_ALGORITHMS=HS256
58
63
  volumes:
59
- - ./<%= paths.fastapi %>/src:/app/src
60
- - ./<%= paths.fastapi %>/alembic.ini:/app/alembic.ini
61
- - ./<%= paths.fastapi %>/migrate.py:/app/migrate.py
64
+ - ./<%= inst.path %>/src:/app/src
65
+ - ./<%= inst.path %>/alembic.ini:/app/alembic.ini
66
+ - ./<%= inst.path %>/migrate.py:/app/migrate.py
62
67
  depends_on:
63
- <%= paths.fastapi %>-migrate:
68
+ <%= inst.path %>-migrate:
64
69
  condition: service_completed_successfully
65
70
  restart: unless-stopped
66
71
  healthcheck:
@@ -81,9 +86,9 @@ services:
81
86
  networks:
82
87
  - app-network
83
88
  <% } %>
84
- <% if (components.includes('fastify')) { %>
85
- <%= paths.fastify %>-migrate:
86
- build: ./<%= paths.fastify %>
89
+ <% for (const inst of fastifyInstances) { %>
90
+ <%= inst.path %>-migrate:
91
+ build: ./<%= inst.path %>
87
92
  command: ["sh", "-c", "<%= pm.prismaExec %> migrate deploy"]
88
93
  environment:
89
94
  - DATABASE_URL=postgresql://dev:dev@db:5432/app
@@ -97,20 +102,25 @@ services:
97
102
  cpus: '0.5'
98
103
  networks:
99
104
  - app-network
100
- <%= paths.fastify %>:
101
- build: ./<%= paths.fastify %>
105
+ <%= inst.path %>:
106
+ build: ./<%= inst.path %>
102
107
  command: ["sh", "-c", "<%= pm.runDev %>"]
108
+ <% if (inst.path === inst.type) { %>
103
109
  ports:
104
110
  - '3000:3000'
111
+ <% } else { %>
112
+ expose:
113
+ - '3000'
114
+ <% } %>
105
115
  environment:
106
116
  - DATABASE_URL=postgresql://dev:dev@db:5432/app
107
117
  - CORS_ALLOW_ORIGINS=http://localhost:5173,http://localhost
108
118
  - JWT_PROVIDER=shared_secret
109
119
  - JWT_SECRET=dev-secret-that-is-at-least-32-bytes-long
110
120
  volumes:
111
- - ./<%= paths.fastify %>/src:/app/src
121
+ - ./<%= inst.path %>/src:/app/src
112
122
  depends_on:
113
- <%= paths.fastify %>-migrate:
123
+ <%= inst.path %>-migrate:
114
124
  condition: service_completed_successfully
115
125
  restart: unless-stopped
116
126
  healthcheck:
@@ -127,25 +137,30 @@ services:
127
137
  networks:
128
138
  - app-network
129
139
  <% } %>
130
- <% if (components.includes('frontend')) { %>
131
- frontend:
140
+ <% for (const inst of frontendInstances) { %>
141
+ <%= inst.path %>:
132
142
  image: node:20-alpine
133
143
  working_dir: /app
134
144
  command: sh -c "<%= pm.install %> && <%= pm.run %> dev -- --host 0.0.0.0"
145
+ <% if (inst.path === inst.type) { %>
135
146
  ports:
136
147
  - '5173:5173'
148
+ <% } else { %>
149
+ expose:
150
+ - '5173'
151
+ <% } %>
137
152
  env_file:
138
- - ./<%= paths.frontend %>/.env
153
+ - ./<%= inst.path %>/.env
139
154
  volumes:
140
- - ./<%= paths.frontend %>:/app
141
- - frontend_node_modules:/app/node_modules
142
- <% if (components.includes('fastify')) { %>
155
+ - ./<%= inst.path %>:/app
156
+ - <%= inst.upper.toLowerCase() %>_node_modules:/app/node_modules
157
+ <% if (fastifyInstances.length > 0) { %>
143
158
  depends_on:
144
- <%= paths.fastify %>:
159
+ <%= fastifyInstances[0].path %>:
145
160
  condition: service_healthy
146
- <% } else if (components.includes('fastapi')) { %>
161
+ <% } else if (fastapiInstances.length > 0) { %>
147
162
  depends_on:
148
- <%= paths.fastapi %>:
163
+ <%= fastapiInstances[0].path %>:
149
164
  condition: service_healthy
150
165
  <% } %>
151
166
  healthcheck:
@@ -163,11 +178,11 @@ services:
163
178
  - app-network
164
179
  <% } %>
165
180
  volumes:
166
- <% if (components.includes('fastapi') || components.includes('fastify')) { %>
181
+ <% if (fastapiInstances.length > 0 || fastifyInstances.length > 0) { %>
167
182
  pgdata:
168
183
  <% } %>
169
- <% if (components.includes('frontend')) { %>
170
- frontend_node_modules:
184
+ <% for (const inst of frontendInstances) { %>
185
+ <%= inst.upper.toLowerCase() %>_node_modules:
171
186
  <% } %>
172
187
  networks:
173
188
  app-network:
@@ -1,21 +1,21 @@
1
1
  services:
2
- <% if (components.includes('fastapi')) { %>
3
- <%= paths.fastapi %>-migrate:
4
- build: ./<%= paths.fastapi %>
2
+ <% for (const inst of fastapiInstances) { %>
3
+ <%= inst.path %>-migrate:
4
+ build: ./<%= inst.path %>
5
5
  command: ["uv", "run", "migrate.py"]
6
6
  env_file:
7
- - ./<%= paths.fastapi %>/.env
7
+ - ./<%= inst.path %>/.env
8
8
  networks:
9
9
  - app-network
10
- <%= paths.fastapi %>:
11
- build: ./<%= paths.fastapi %>
10
+ <%= inst.path %>:
11
+ build: ./<%= inst.path %>
12
12
  expose:
13
13
  - "7860"
14
14
  env_file:
15
- - ./<%= paths.fastapi %>/.env
15
+ - ./<%= inst.path %>/.env
16
16
  restart: unless-stopped
17
17
  depends_on:
18
- <%= paths.fastapi %>-migrate:
18
+ <%= inst.path %>-migrate:
19
19
  condition: service_completed_successfully
20
20
  healthcheck:
21
21
  test:
@@ -32,23 +32,23 @@ services:
32
32
  networks:
33
33
  - app-network
34
34
  <% } %>
35
- <% if (components.includes('fastify')) { %>
36
- <%= paths.fastify %>-migrate:
37
- build: ./<%= paths.fastify %>
35
+ <% for (const inst of fastifyInstances) { %>
36
+ <%= inst.path %>-migrate:
37
+ build: ./<%= inst.path %>
38
38
  command: ["sh", "-c", "<%= pm.prismaExec %> migrate deploy"]
39
39
  env_file:
40
- - ./<%= paths.fastify %>/.env
40
+ - ./<%= inst.path %>/.env
41
41
  networks:
42
42
  - app-network
43
- <%= paths.fastify %>:
44
- build: ./<%= paths.fastify %>
43
+ <%= inst.path %>:
44
+ build: ./<%= inst.path %>
45
45
  expose:
46
46
  - "3000"
47
47
  env_file:
48
- - ./<%= paths.fastify %>/.env
48
+ - ./<%= inst.path %>/.env
49
49
  restart: unless-stopped
50
50
  depends_on:
51
- <%= paths.fastify %>-migrate:
51
+ <%= inst.path %>-migrate:
52
52
  condition: service_completed_successfully
53
53
  healthcheck:
54
54
  test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000/api/health"]
@@ -59,10 +59,10 @@ services:
59
59
  networks:
60
60
  - app-network
61
61
  <% } %>
62
- <% if (components.includes('frontend')) { %>
63
- frontend:
62
+ <% for (const inst of frontendInstances) { %>
63
+ <%= inst.path %>:
64
64
  build:
65
- context: ./<%= paths.frontend %>
65
+ context: ./<%= inst.path %>
66
66
  args:
67
67
  VITE_API_URL: ""
68
68
  ports:
@@ -71,13 +71,13 @@ services:
71
71
  volumes:
72
72
  - letsencrypt:/etc/letsencrypt
73
73
  - certbot-www:/var/www/certbot
74
- <% if (components.includes('fastify')) { %>
74
+ <% if (fastifyInstances.length > 0) { %>
75
75
  depends_on:
76
- <%= paths.fastify %>:
76
+ <%= fastifyInstances[0].path %>:
77
77
  condition: service_healthy
78
- <% } else if (components.includes('fastapi')) { %>
78
+ <% } else if (fastapiInstances.length > 0) { %>
79
79
  depends_on:
80
- <%= paths.fastapi %>:
80
+ <%= fastapiInstances[0].path %>:
81
81
  condition: service_healthy
82
82
  <% } %>
83
83
  restart: unless-stopped
@@ -89,6 +89,8 @@ services:
89
89
  start_period: 10s
90
90
  networks:
91
91
  - app-network
92
+ <% } %>
93
+ <% if (frontendInstances.length > 0) { %>
92
94
  certbot:
93
95
  image: certbot/certbot:latest
94
96
  volumes:
@@ -97,14 +99,14 @@ services:
97
99
  entrypoint: /bin/sh -c "trap exit TERM; while :; do certbot renew --quiet; sleep 12h & wait $${!}; done"
98
100
  restart: unless-stopped
99
101
  depends_on:
100
- frontend:
102
+ <%= frontendInstances[0].path %>:
101
103
  condition: service_healthy
102
104
  profiles:
103
105
  - ssl
104
106
  networks:
105
107
  - app-network
106
108
  <% } %>
107
- <% if (components.includes('frontend')) { %>
109
+ <% if (frontendInstances.length > 0) { %>
108
110
  volumes:
109
111
  letsencrypt:
110
112
  certbot-www: