create-projx 1.1.0 → 1.1.1

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
@@ -122,7 +122,7 @@ async function copyComponent(repoDir, component, dest) {
122
122
  async function copyStaticFiles(repoDir, dest) {
123
123
  const manifest = [];
124
124
  const tpl = repoDir;
125
- const statics = [".editorconfig", "LICENSE"];
125
+ const statics = [".editorconfig"];
126
126
  for (const file of statics) {
127
127
  const src = join(tpl, file);
128
128
  if (existsSync(src)) {
@@ -135,10 +135,11 @@ async function copyStaticFiles(repoDir, dest) {
135
135
  await cp(gitignore, join(dest, ".gitignore"));
136
136
  manifest.push(".gitignore");
137
137
  }
138
- const vscode = join(tpl, ".vscode");
139
- if (existsSync(vscode)) {
140
- await cp(vscode, join(dest, ".vscode"), { recursive: true });
141
- manifest.push(".vscode/settings.json", ".vscode/extensions.json");
138
+ const extensionsJson = join(tpl, ".vscode/extensions.json");
139
+ if (existsSync(extensionsJson)) {
140
+ await mkdir(join(dest, ".vscode"), { recursive: true });
141
+ await cp(extensionsJson, join(dest, ".vscode/extensions.json"));
142
+ manifest.push(".vscode/extensions.json");
142
143
  }
143
144
  const scripts = join(tpl, "scripts");
144
145
  if (existsSync(scripts)) {
@@ -185,9 +186,24 @@ async function readFileOrNull(path) {
185
186
  }
186
187
  }
187
188
  async function writeComponentMarker(dir, component) {
189
+ const markerPath = join(dir, COMPONENT_MARKER);
190
+ let components = [component];
191
+ const existing = await readFileOrNull(markerPath);
192
+ if (existing) {
193
+ try {
194
+ const data = JSON.parse(existing);
195
+ const prev = data.components ?? (data.component ? [data.component] : []);
196
+ if (!prev.includes(component)) {
197
+ components = [...prev, component];
198
+ } else {
199
+ return;
200
+ }
201
+ } catch {
202
+ }
203
+ }
188
204
  await writeFile(
189
- join(dir, COMPONENT_MARKER),
190
- JSON.stringify({ component }, null, 2) + "\n"
205
+ markerPath,
206
+ JSON.stringify({ components }, null, 2) + "\n"
191
207
  );
192
208
  }
193
209
  async function discoverComponentPaths(cwd, components) {
@@ -203,8 +219,11 @@ async function discoverComponentPaths(cwd, components) {
203
219
  if (existsSync(marker)) {
204
220
  try {
205
221
  const data = JSON.parse(await readFile(marker, "utf-8"));
206
- if (components.includes(data.component)) {
207
- paths[data.component] = entry.name;
222
+ const markerComponents = data.components ?? (data.component ? [data.component] : []);
223
+ for (const mc of markerComponents) {
224
+ if (components.includes(mc)) {
225
+ paths[mc] = entry.name;
226
+ }
208
227
  }
209
228
  } catch {
210
229
  }
@@ -331,6 +350,36 @@ async function generateCiYml(vars) {
331
350
  async function generateReadme(vars) {
332
351
  return renderShared("README.md.ejs", vars);
333
352
  }
353
+ function generateVscodeSettings(vars) {
354
+ const settings = {};
355
+ if (vars.components.includes("fastapi")) {
356
+ settings["[python]"] = {
357
+ "editor.defaultFormatter": "charliermarsh.ruff",
358
+ "editor.codeActionsOnSave": { "source.fixAll.ruff": "explicit" }
359
+ };
360
+ }
361
+ settings["[typescript]"] = { "editor.defaultFormatter": "esbenp.prettier-vscode" };
362
+ settings["[typescriptreact]"] = { "editor.defaultFormatter": "esbenp.prettier-vscode" };
363
+ settings["[javascript]"] = { "editor.defaultFormatter": "esbenp.prettier-vscode" };
364
+ settings["[json]"] = { "editor.defaultFormatter": "esbenp.prettier-vscode" };
365
+ settings["[css]"] = { "editor.defaultFormatter": "esbenp.prettier-vscode" };
366
+ settings["[yaml]"] = { "editor.defaultFormatter": "esbenp.prettier-vscode" };
367
+ settings["editor.formatOnSave"] = true;
368
+ settings["editor.codeActionsOnSave"] = { "source.fixAll.eslint": "explicit" };
369
+ settings["eslint.useFlatConfig"] = true;
370
+ const prettierComponent = ["frontend", "fastify", "e2e"].find(
371
+ (c) => vars.components.includes(c)
372
+ );
373
+ if (prettierComponent) {
374
+ settings["prettier.configPath"] = `${vars.paths[prettierComponent]}/.prettierrc`;
375
+ }
376
+ if (vars.components.includes("fastapi")) {
377
+ settings["ruff.lineLength"] = 120;
378
+ settings["python.analysis.extraPaths"] = [`${vars.paths.fastapi}/src`];
379
+ settings["python.analysis.importFormat"] = "absolute";
380
+ }
381
+ return JSON.stringify(settings, null, 2) + "\n";
382
+ }
334
383
 
335
384
  // src/scaffold.ts
336
385
  async function scaffold(opts, dest, localRepo) {
@@ -380,6 +429,8 @@ async function doScaffold(opts, dest, repoDir, name, nameSnake, vars) {
380
429
  await writeFile2(join3(dest, "setup.sh"), await generateSetupSh(vars));
381
430
  await chmod(join3(dest, "setup.sh"), 493);
382
431
  await copyStaticFiles(repoDir, dest);
432
+ await mkdir2(join3(dest, ".vscode"), { recursive: true });
433
+ await writeFile2(join3(dest, ".vscode/settings.json"), generateVscodeSettings(vars));
383
434
  const pkg = JSON.parse(
384
435
  await readFile3(join3(repoDir, "cli/package.json"), "utf-8")
385
436
  );
@@ -720,6 +771,8 @@ async function doUpdate(cwd, config, repoDir, version, componentPaths) {
720
771
  const setupSh = await generateSetupSh(vars);
721
772
  await writeFile3(join4(cwd, "setup.sh"), setupSh);
722
773
  await chmod2(join4(cwd, "setup.sh"), 493);
774
+ await mkdir3(join4(cwd, ".vscode"), { recursive: true });
775
+ await writeFile3(join4(cwd, ".vscode/settings.json"), generateVscodeSettings(vars));
723
776
  spinner5.stop("Shared files updated.");
724
777
  if (config.components.includes("mobile")) {
725
778
  const mobilePath = componentPaths.mobile ?? "mobile";
@@ -1279,7 +1332,7 @@ async function generateSharedFiles(cwd, repoDir, vars) {
1279
1332
  }
1280
1333
  }
1281
1334
  }
1282
- const statics = [".editorconfig", "LICENSE"];
1335
+ const statics = [".editorconfig"];
1283
1336
  for (const file of statics) {
1284
1337
  const src = join7(repoDir, file);
1285
1338
  const dest = join7(cwd, file);
@@ -1303,16 +1356,29 @@ async function generateSharedFiles(cwd, repoDir, vars) {
1303
1356
  }
1304
1357
  }
1305
1358
  }
1306
- const vscode = join7(repoDir, ".vscode");
1307
- if (existsSync6(vscode)) {
1308
- const vscodeDest = join7(cwd, ".vscode");
1309
- if (!existsSync6(vscodeDest)) {
1310
- await cp3(vscode, vscodeDest, { recursive: true });
1311
- p5.log.success(".vscode/");
1359
+ const vscodeDest = join7(cwd, ".vscode");
1360
+ await mkdir5(vscodeDest, { recursive: true });
1361
+ const settingsPath = join7(vscodeDest, "settings.json");
1362
+ const settingsContent = generateVscodeSettings(vars);
1363
+ const existingSettings = await readFileOrNull(settingsPath);
1364
+ if (existingSettings === null) {
1365
+ await writeFile5(settingsPath, settingsContent);
1366
+ p5.log.success(".vscode/settings.json");
1367
+ } else if (existingSettings !== settingsContent) {
1368
+ const action = await resolveConflict(".vscode/settings.json", existingSettings, settingsContent);
1369
+ if (action === "overwrite") {
1370
+ await writeFile5(settingsPath, settingsContent);
1371
+ p5.log.success(".vscode/settings.json \u2014 overwritten.");
1312
1372
  } else {
1313
- p5.log.info(".vscode/ \u2014 already exists, skipped.");
1373
+ p5.log.info(".vscode/settings.json \u2014 kept existing.");
1314
1374
  }
1315
1375
  }
1376
+ const extSrc = join7(repoDir, ".vscode/extensions.json");
1377
+ const extDest = join7(vscodeDest, "extensions.json");
1378
+ if (existsSync6(extSrc) && !existsSync6(extDest)) {
1379
+ await cp3(extSrc, extDest);
1380
+ p5.log.success(".vscode/extensions.json");
1381
+ }
1316
1382
  }
1317
1383
  async function resolveConflict(filePath, existing, template) {
1318
1384
  let action = await p5.select({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-projx",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "description": "Scaffold production-grade projects. Pick your stack (FastAPI, Fastify, React, Flutter), get a fully wired template with auth, database, CI/CD, and E2E tests.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -7,9 +7,66 @@ on:
7
7
  branches: [main]
8
8
 
9
9
  jobs:
10
+ changes:
11
+ name: Detect changes
12
+ runs-on: ubuntu-latest
13
+ permissions:
14
+ pull-requests: read
15
+ outputs:
16
+ <% if (components.includes('fastapi')) { %>
17
+ fastapi: ${{ steps.filter.outputs.<%= paths.fastapi %> }}
18
+ <% } %>
19
+ <% if (components.includes('fastify')) { %>
20
+ fastify: ${{ steps.filter.outputs.<%= paths.fastify %> }}
21
+ <% } %>
22
+ <% if (components.includes('frontend')) { %>
23
+ frontend: ${{ steps.filter.outputs.<%= paths.frontend %> }}
24
+ <% } %>
25
+ <% if (components.includes('mobile')) { %>
26
+ mobile: ${{ steps.filter.outputs.<%= paths.mobile %> }}
27
+ <% } %>
28
+ <% if (components.includes('e2e')) { %>
29
+ e2e: ${{ steps.filter.outputs.<%= paths.e2e %> }}
30
+ <% } %>
31
+ <% if (components.includes('infra')) { %>
32
+ infra: ${{ steps.filter.outputs.<%= paths.infra %> }}
33
+ <% } %>
34
+ steps:
35
+ - uses: actions/checkout@v5
36
+ - uses: dorny/paths-filter@v3
37
+ id: filter
38
+ with:
39
+ filters: |
40
+ <% if (components.includes('fastapi')) { %>
41
+ <%= paths.fastapi %>:
42
+ - '<%= paths.fastapi %>/**'
43
+ <% } %>
44
+ <% if (components.includes('fastify')) { %>
45
+ <%= paths.fastify %>:
46
+ - '<%= paths.fastify %>/**'
47
+ <% } %>
48
+ <% if (components.includes('frontend')) { %>
49
+ <%= paths.frontend %>:
50
+ - '<%= paths.frontend %>/**'
51
+ <% } %>
52
+ <% if (components.includes('mobile')) { %>
53
+ <%= paths.mobile %>:
54
+ - '<%= paths.mobile %>/**'
55
+ <% } %>
56
+ <% if (components.includes('e2e')) { %>
57
+ <%= paths.e2e %>:
58
+ - '<%= paths.e2e %>/**'
59
+ <% } %>
60
+ <% if (components.includes('infra')) { %>
61
+ <%= paths.infra %>:
62
+ - '<%= paths.infra %>/**'
63
+ <% } %>
10
64
  <% if (components.includes('fastapi')) { %>
65
+
11
66
  fastapi:
12
67
  name: FastAPI (format + lint)
68
+ needs: changes
69
+ if: needs.changes.outputs.fastapi == 'true'
13
70
  runs-on: ubuntu-latest
14
71
  defaults:
15
72
  run:
@@ -22,8 +79,11 @@ jobs:
22
79
  - run: uv run ruff check src tests
23
80
  <% } %>
24
81
  <% if (components.includes('fastify')) { %>
82
+
25
83
  fastify:
26
84
  name: Fastify (format + lint + typecheck)
85
+ needs: changes
86
+ if: needs.changes.outputs.fastify == 'true'
27
87
  runs-on: ubuntu-latest
28
88
  defaults:
29
89
  run:
@@ -45,8 +105,11 @@ jobs:
45
105
  - run: npx tsc --noEmit
46
106
  <% } %>
47
107
  <% if (components.includes('frontend')) { %>
108
+
48
109
  frontend:
49
110
  name: Frontend (format + lint + typecheck)
111
+ needs: changes
112
+ if: needs.changes.outputs.frontend == 'true'
50
113
  runs-on: ubuntu-latest
51
114
  defaults:
52
115
  run:
@@ -64,8 +127,11 @@ jobs:
64
127
  - run: npx tsc --noEmit
65
128
  <% } %>
66
129
  <% if (components.includes('mobile')) { %>
130
+
67
131
  mobile:
68
132
  name: Flutter (format + analyze)
133
+ needs: changes
134
+ if: needs.changes.outputs.mobile == 'true'
69
135
  runs-on: ubuntu-latest
70
136
  defaults:
71
137
  run:
@@ -81,8 +147,11 @@ jobs:
81
147
  - run: dart analyze --fatal-infos
82
148
  <% } %>
83
149
  <% if (components.includes('e2e')) { %>
150
+
84
151
  e2e:
85
152
  name: E2E (format + lint + typecheck)
153
+ needs: changes
154
+ if: needs.changes.outputs.e2e == 'true'
86
155
  runs-on: ubuntu-latest
87
156
  defaults:
88
157
  run:
@@ -100,8 +169,11 @@ jobs:
100
169
  - run: npx tsc --noEmit
101
170
  <% } %>
102
171
  <% if (components.includes('infra')) { %>
172
+
103
173
  infra:
104
174
  name: Terraform (fmt + validate)
175
+ needs: changes
176
+ if: needs.changes.outputs.infra == 'true'
105
177
  runs-on: ubuntu-latest
106
178
  defaults:
107
179
  run: