create-projx 1.3.0 → 1.3.2

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
@@ -185,28 +185,43 @@ async function readFileOrNull(path) {
185
185
  return null;
186
186
  }
187
187
  }
188
- async function writeComponentMarker(dir, component, origin = "scaffold") {
188
+ async function readComponentMarker(dir) {
189
+ const raw = await readFileOrNull(join(dir, COMPONENT_MARKER));
190
+ if (!raw) return null;
191
+ try {
192
+ const data = JSON.parse(raw);
193
+ return {
194
+ components: data.components ?? (data.component ? [data.component] : []),
195
+ origin: data.origin ?? "scaffold",
196
+ skip: data.skip
197
+ };
198
+ } catch {
199
+ return null;
200
+ }
201
+ }
202
+ async function writeComponentMarker(dir, component, origin = "scaffold", skip) {
189
203
  const markerPath = join(dir, COMPONENT_MARKER);
190
204
  let components = [component];
191
205
  let existingOrigin = origin;
206
+ let existingSkip = skip;
192
207
  const existing = await readFileOrNull(markerPath);
193
208
  if (existing) {
194
209
  try {
195
210
  const data = JSON.parse(existing);
196
211
  const prev = data.components ?? (data.component ? [data.component] : []);
197
- existingOrigin = data.origin ?? origin;
212
+ existingOrigin = origin ?? data.origin ?? "scaffold";
213
+ existingSkip = skip ?? data.skip;
198
214
  if (!prev.includes(component)) {
199
215
  components = [...prev, component];
200
216
  } else {
201
- return;
217
+ components = prev;
202
218
  }
203
219
  } catch {
204
220
  }
205
221
  }
206
- await writeFile(
207
- markerPath,
208
- JSON.stringify({ components, origin: existingOrigin }, null, 2) + "\n"
209
- );
222
+ const marker = { components, origin: existingOrigin };
223
+ if (existingSkip && existingSkip.length > 0) marker.skip = existingSkip;
224
+ await writeFile(markerPath, JSON.stringify(marker, null, 2) + "\n");
210
225
  }
211
226
  async function discoverComponentPaths(cwd, components) {
212
227
  const paths = {};
@@ -433,7 +448,43 @@ function removeWorktree(cwd, worktree) {
433
448
  }
434
449
  }
435
450
  }
436
- async function writeTemplateToDir(dest, repoDir, components, componentPaths, vars, version, origin) {
451
+ function matchesSkip(filePath, patterns) {
452
+ for (const pattern of patterns) {
453
+ if (pattern === "**") return true;
454
+ if (pattern.endsWith("/**")) {
455
+ const prefix = pattern.slice(0, -3);
456
+ if (filePath.startsWith(prefix + "/") || filePath === prefix) return true;
457
+ }
458
+ if (pattern.startsWith("**/")) {
459
+ const suffix = pattern.slice(3);
460
+ if (filePath.endsWith(suffix) || filePath.includes("/" + suffix)) return true;
461
+ }
462
+ if (pattern.startsWith("*.")) {
463
+ const ext = pattern.slice(1);
464
+ if (filePath.endsWith(ext)) return true;
465
+ }
466
+ if (filePath === pattern) return true;
467
+ }
468
+ return false;
469
+ }
470
+ async function removeSkippedFiles(dir, skipPatterns) {
471
+ if (skipPatterns.length === 0) return;
472
+ const { readdir: readdir3, unlink } = await import("fs/promises");
473
+ const walk = async (current, base) => {
474
+ const entries = await readdir3(current, { withFileTypes: true });
475
+ for (const entry of entries) {
476
+ const full = join3(current, entry.name);
477
+ const rel = full.slice(base.length + 1);
478
+ if (entry.isDirectory()) {
479
+ await walk(full, base);
480
+ } else if (entry.name !== ".projx-component" && matchesSkip(rel, skipPatterns)) {
481
+ await unlink(full);
482
+ }
483
+ }
484
+ };
485
+ await walk(dir, dir);
486
+ }
487
+ async function writeTemplateToDir(dest, repoDir, components, componentPaths, vars, version, origin, componentSkips) {
437
488
  const name = vars.projectName;
438
489
  const nameSnake = toSnake(name);
439
490
  for (const component of components) {
@@ -450,7 +501,11 @@ async function writeTemplateToDir(dest, repoDir, components, componentPaths, var
450
501
  }
451
502
  await rm2(join3(dest, "__tmp__"), { recursive: true, force: true });
452
503
  }
453
- await writeComponentMarker(join3(dest, targetDir), component, origin);
504
+ const skipPatterns = componentSkips?.[component] ?? [];
505
+ if (skipPatterns.length > 0) {
506
+ await removeSkippedFiles(join3(dest, targetDir), skipPatterns);
507
+ }
508
+ await writeComponentMarker(join3(dest, targetDir), component, origin, skipPatterns.length > 0 ? skipPatterns : void 0);
454
509
  }
455
510
  await substituteNames(dest, components, componentPaths, name, nameSnake);
456
511
  const hasBackend = components.includes("fastapi") || components.includes("fastify");
@@ -498,10 +553,10 @@ async function substituteNames(dest, components, paths, name, nameSnake) {
498
553
  await replaceInDir(join3(dest, `${paths.mobile}`), "package:projx_mobile/", `package:${nameSnake}_mobile/`, ".dart");
499
554
  }
500
555
  }
501
- async function createBaseline(cwd, repoDir, components, componentPaths, vars, version, origin = "scaffold") {
556
+ async function createBaseline(cwd, repoDir, components, componentPaths, vars, version, origin = "scaffold", componentSkips) {
502
557
  const worktree = createWorktree(cwd, BASELINE_BRANCH, true);
503
558
  try {
504
- await writeTemplateToDir(worktree, repoDir, components, componentPaths, vars, version, origin);
559
+ await writeTemplateToDir(worktree, repoDir, components, componentPaths, vars, version, origin, componentSkips);
505
560
  execSync2("git add -A", { cwd: worktree, stdio: "pipe" });
506
561
  execSync2(
507
562
  `git commit --no-verify -m "projx: baseline template v${version} [${components.join(", ")}]"`,
@@ -511,11 +566,11 @@ async function createBaseline(cwd, repoDir, components, componentPaths, vars, ve
511
566
  removeWorktree(cwd, worktree);
512
567
  }
513
568
  }
514
- async function updateBaseline(cwd, repoDir, components, componentPaths, vars, version) {
569
+ async function updateBaseline(cwd, repoDir, components, componentPaths, vars, version, componentSkips) {
515
570
  const worktree = createWorktree(cwd, BASELINE_BRANCH, false);
516
571
  try {
517
572
  execSync2("git rm -rf .", { cwd: worktree, stdio: "pipe" });
518
- await writeTemplateToDir(worktree, repoDir, components, componentPaths, vars, version, "scaffold");
573
+ await writeTemplateToDir(worktree, repoDir, components, componentPaths, vars, version, "scaffold", componentSkips);
519
574
  execSync2("git add -A", { cwd: worktree, stdio: "pipe" });
520
575
  const diff = execSync2("git diff --cached --stat", { cwd: worktree, stdio: "pipe" }).toString().trim();
521
576
  if (!diff) {
@@ -571,9 +626,9 @@ function mergeBaseline(cwd, message, allowUnrelated = false, oursOnConflict = fa
571
626
  };
572
627
  }
573
628
  }
574
- async function reconstructBaseline(cwd, repoDir, components, componentPaths, vars, version) {
629
+ async function reconstructBaseline(cwd, repoDir, components, componentPaths, vars, version, componentSkips) {
575
630
  p2.log.warn("projx/baseline branch not found. Reconstructing...");
576
- await createBaseline(cwd, repoDir, components, componentPaths, vars, version);
631
+ await createBaseline(cwd, repoDir, components, componentPaths, vars, version, "scaffold", componentSkips);
577
632
  mergeBaseline(
578
633
  cwd,
579
634
  `projx: reconstructed baseline for template v${version}`,
@@ -767,15 +822,25 @@ async function update(cwd, localRepo) {
767
822
  const version = pkg.version;
768
823
  const name = detectProjectName(cwd, config.components, componentPaths);
769
824
  const vars = { projectName: name, components: config.components, paths: componentPaths };
825
+ const componentSkips = {};
826
+ for (const component of config.components) {
827
+ const dir = componentPaths[component];
828
+ const marker = await readComponentMarker(join5(cwd, dir));
829
+ if (marker?.skip && marker.skip.length > 0) {
830
+ componentSkips[component] = marker.skip;
831
+ } else if (marker?.origin === "init") {
832
+ componentSkips[component] = ["**"];
833
+ }
834
+ }
770
835
  if (!hasBaseline(cwd)) {
771
836
  const rebuildSpinner = p4.spinner();
772
837
  rebuildSpinner.start("Establishing baseline (first-time migration)");
773
- await reconstructBaseline(cwd, repoDir, config.components, componentPaths, vars, config.version || version);
838
+ await reconstructBaseline(cwd, repoDir, config.components, componentPaths, vars, config.version || version, componentSkips);
774
839
  rebuildSpinner.stop("Baseline established.");
775
840
  }
776
841
  const updateSpinner = p4.spinner();
777
842
  updateSpinner.start("Updating baseline to latest template");
778
- const { changed } = await updateBaseline(cwd, repoDir, config.components, componentPaths, vars, version);
843
+ const { changed } = await updateBaseline(cwd, repoDir, config.components, componentPaths, vars, version, componentSkips);
779
844
  if (!changed) {
780
845
  updateSpinner.stop("Already up to date.");
781
846
  p4.outro("No template changes to apply.");
@@ -786,6 +851,16 @@ async function update(cwd, localRepo) {
786
851
  mergeSpinner.start("Merging template changes");
787
852
  const result = mergeBaseline(cwd, `projx: update to template v${version}`);
788
853
  mergeSpinner.stop("Merge complete.");
854
+ if (result.status === "clean") {
855
+ const updatedConfig = {
856
+ ...config,
857
+ version,
858
+ baseline: { branch: "projx/baseline", templateVersion: version }
859
+ };
860
+ const { writeFile: writeFile3 } = await import("fs/promises");
861
+ await writeFile3(join5(cwd, ".projx"), JSON.stringify(updatedConfig, null, 2) + "\n");
862
+ execSync3("git add .projx && git commit --no-verify --amend --no-edit", { cwd, stdio: "pipe" });
863
+ }
789
864
  if (result.status === "conflicts") {
790
865
  p4.log.warn(`Merge conflicts in ${result.conflictedFiles.length} file(s):`);
791
866
  for (const f of result.conflictedFiles) {
@@ -1140,9 +1215,13 @@ async function init(cwd, localRepo) {
1140
1215
  try {
1141
1216
  const pkg = JSON.parse(await readFile6(join8(repoDir, "cli/package.json"), "utf-8"));
1142
1217
  const version = pkg.version;
1218
+ const componentSkips = {};
1219
+ for (const c of components) {
1220
+ componentSkips[c] = ["**"];
1221
+ }
1143
1222
  const baselineSpinner = p6.spinner();
1144
1223
  baselineSpinner.start("Creating template baseline");
1145
- await createBaseline(cwd, repoDir, components, paths, vars, version, "init");
1224
+ await createBaseline(cwd, repoDir, components, paths, vars, version, "init", componentSkips);
1146
1225
  baselineSpinner.stop("Baseline created.");
1147
1226
  const mergeSpinner = p6.spinner();
1148
1227
  mergeSpinner.start("Merging baseline (preserving your code)");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-projx",
3
- "version": "1.3.0",
3
+ "version": "1.3.2",
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": {
@@ -2,21 +2,16 @@ services:
2
2
  <% if (components.includes('fastapi')) { %>
3
3
  migrate:
4
4
  build: ./<%= paths.fastapi %>
5
- command: ['uv', 'run', 'migrate.py']
5
+ command: ["uv", "run", "migrate.py"]
6
6
  env_file:
7
7
  - ./<%= paths.fastapi %>/.env
8
- deploy:
9
- resources:
10
- limits:
11
- memory: 256M
12
- cpus: '0.5'
13
8
  networks:
14
9
  - app-network
15
10
 
16
11
  backend:
17
12
  build: ./<%= paths.fastapi %>
18
13
  expose:
19
- - '7860'
14
+ - "7860"
20
15
  env_file:
21
16
  - ./<%= paths.fastapi %>/.env
22
17
  restart: unless-stopped
@@ -26,27 +21,15 @@ services:
26
21
  healthcheck:
27
22
  test:
28
23
  [
29
- 'CMD',
30
- 'python',
31
- '-c',
24
+ "CMD",
25
+ "python",
26
+ "-c",
32
27
  "import urllib.request; urllib.request.urlopen('http://localhost:7860/api/health')",
33
28
  ]
34
29
  interval: 30s
35
30
  timeout: 10s
36
31
  retries: 3
37
32
  start_period: 15s
38
- deploy:
39
- resources:
40
- limits:
41
- memory: 512M
42
- cpus: '1.0'
43
- reservations:
44
- memory: 256M
45
- security_opt:
46
- - no-new-privileges:true
47
- read_only: true
48
- tmpfs:
49
- - /tmp
50
33
  networks:
51
34
  - app-network
52
35
  <% } %>
@@ -54,23 +37,16 @@ services:
54
37
  backend-fastify:
55
38
  build: ./<%= paths.fastify %>
56
39
  expose:
57
- - '3000'
40
+ - "3000"
58
41
  env_file:
59
42
  - ./<%= paths.fastify %>/.env
60
43
  restart: unless-stopped
61
44
  healthcheck:
62
- test: ['CMD', 'wget', '--spider', '-q', 'http://localhost:3000/api/health']
45
+ test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000/api/health"]
63
46
  interval: 30s
64
47
  timeout: 10s
65
48
  retries: 3
66
49
  start_period: 15s
67
- deploy:
68
- resources:
69
- limits:
70
- memory: 512M
71
- cpus: '1.0'
72
- security_opt:
73
- - no-new-privileges:true
74
50
  networks:
75
51
  - app-network
76
52
  <% } %>
@@ -79,12 +55,10 @@ services:
79
55
  build:
80
56
  context: ./<%= paths.frontend %>
81
57
  args:
82
- VITE_API_URL: ''
58
+ VITE_API_URL: ""
83
59
  ports:
84
- - '80:80'
85
- - '443:443'
86
- environment:
87
- - DOMAIN=${DOMAIN:-localhost}
60
+ - "80:80"
61
+ - "443:443"
88
62
  volumes:
89
63
  - letsencrypt:/etc/letsencrypt
90
64
  - certbot-www:/var/www/certbot
@@ -100,18 +74,11 @@ services:
100
74
  <% } %>
101
75
  restart: unless-stopped
102
76
  healthcheck:
103
- test: ['CMD', 'wget', '--spider', '-q', 'http://localhost:80/']
77
+ test: ["CMD", "wget", "--spider", "-q", "http://localhost:80/"]
104
78
  interval: 30s
105
79
  timeout: 5s
106
80
  retries: 3
107
81
  start_period: 10s
108
- deploy:
109
- resources:
110
- limits:
111
- memory: 128M
112
- cpus: '0.5'
113
- security_opt:
114
- - no-new-privileges:true
115
82
  networks:
116
83
  - app-network
117
84
 
@@ -127,11 +94,6 @@ services:
127
94
  condition: service_healthy
128
95
  profiles:
129
96
  - ssl
130
- deploy:
131
- resources:
132
- limits:
133
- memory: 64M
134
- cpus: '0.25'
135
97
  networks:
136
98
  - app-network
137
99
  <% } %>