create-projx 1.6.2 → 1.6.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/README.md CHANGED
@@ -158,6 +158,16 @@ npx create-projx add frontend mobile
158
158
 
159
159
  Copies the new component directories, regenerates shared files (docker-compose, CI, pre-commit hooks) to include them, and installs dependencies.
160
160
 
161
+ #### Multiple instances of the same type
162
+
163
+ Need a second backend service alongside an existing one (e.g. an SMTP listener next to your CRUD API)? Use `--name <dir>`:
164
+
165
+ ```bash
166
+ npx create-projx add fastify --name email-ingestor
167
+ ```
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.
170
+
161
171
  ### Update Scaffolding
162
172
 
163
173
  When templates improve, update your project:
@@ -213,6 +223,7 @@ To opt back in to updates for a skipped file, use `npx create-projx unpin <file>
213
223
  npx create-projx <name> [options]
214
224
  npx create-projx init
215
225
  npx create-projx add <components...>
226
+ npx create-projx add <type> --name <dir>
216
227
  npx create-projx update
217
228
  npx create-projx diff
218
229
  npx create-projx pin <patterns...>
@@ -223,6 +234,7 @@ npx create-projx gen entity <name> [--ai | --backend]
223
234
  npx create-projx sync [--url <url>]
224
235
 
225
236
  --components <list> Comma-separated: fastapi,fastify,frontend,mobile,e2e,infra
237
+ --name <dir> Custom directory for `add <type>` (multi-instance)
226
238
  --ai Target fastapi (AI/ML) for gen entity
227
239
  --backend Target fastify (API backend) for gen entity
228
240
  --no-git Skip git init
@@ -1,8 +1,6 @@
1
1
  import {
2
2
  BASELINE_REF,
3
3
  applyTemplate,
4
- buildDisplayNames,
5
- buildPathsUpper,
6
4
  collectAllFiles,
7
5
  detectPackageNameOverrides,
8
6
  getBaselineRef,
@@ -10,13 +8,11 @@ import {
10
8
  matchesSkip,
11
9
  saveBaselineRef,
12
10
  writeTemplateToDir
13
- } from "./chunk-D33FXCNT.js";
14
- import "./chunk-LTIJPVRZ.js";
11
+ } from "./chunk-OBYYB6PR.js";
12
+ import "./chunk-LYPPFXGK.js";
15
13
  export {
16
14
  BASELINE_REF,
17
15
  applyTemplate,
18
- buildDisplayNames,
19
- buildPathsUpper,
20
16
  collectAllFiles,
21
17
  detectPackageNameOverrides,
22
18
  getBaselineRef,
@@ -19,13 +19,57 @@ var PACKAGE_MANAGERS = ["npm", "pnpm", "yarn", "bun"];
19
19
  function pmCommands(pm) {
20
20
  switch (pm) {
21
21
  case "npm":
22
- return { name: "npm", install: "npm install", ci: "npm ci", run: "npm run", exec: "npx", dlx: "npx", lockfile: "package-lock.json", prismaExec: "npx prisma", runDev: "npm run dev", audit: "npm audit --omit=dev" };
22
+ return {
23
+ name: "npm",
24
+ install: "npm install",
25
+ ci: "npm ci",
26
+ run: "npm run",
27
+ exec: "npx",
28
+ dlx: "npx",
29
+ lockfile: "package-lock.json",
30
+ prismaExec: "npx prisma",
31
+ runDev: "npm run dev",
32
+ audit: "npm audit --omit=dev"
33
+ };
23
34
  case "pnpm":
24
- return { name: "pnpm", install: "pnpm install", ci: "pnpm install --frozen-lockfile", run: "pnpm", exec: "pnpm exec", dlx: "pnpm dlx", lockfile: "pnpm-lock.yaml", prismaExec: "pnpm prisma", runDev: "pnpm dev", audit: "pnpm audit --prod" };
35
+ return {
36
+ name: "pnpm",
37
+ install: "pnpm install",
38
+ ci: "pnpm install --frozen-lockfile",
39
+ run: "pnpm",
40
+ exec: "pnpm exec",
41
+ dlx: "pnpm dlx",
42
+ lockfile: "pnpm-lock.yaml",
43
+ prismaExec: "pnpm prisma",
44
+ runDev: "pnpm dev",
45
+ audit: "pnpm audit --prod"
46
+ };
25
47
  case "yarn":
26
- return { name: "yarn", install: "yarn", ci: "yarn --frozen-lockfile", run: "yarn", exec: "yarn", dlx: "yarn dlx", lockfile: "yarn.lock", prismaExec: "yarn prisma", runDev: "yarn dev", audit: "yarn npm audit --environment production" };
48
+ return {
49
+ name: "yarn",
50
+ install: "yarn",
51
+ ci: "yarn --frozen-lockfile",
52
+ run: "yarn",
53
+ exec: "yarn",
54
+ dlx: "yarn dlx",
55
+ lockfile: "yarn.lock",
56
+ prismaExec: "yarn prisma",
57
+ runDev: "yarn dev",
58
+ audit: "yarn npm audit --environment production"
59
+ };
27
60
  case "bun":
28
- return { name: "bun", install: "bun install", ci: "bun install --frozen-lockfile", run: "bun run", exec: "bunx", dlx: "bunx", lockfile: "bun.lockb", prismaExec: "bunx prisma", runDev: "bun run dev", audit: "bun audit --prod" };
61
+ return {
62
+ name: "bun",
63
+ install: "bun install",
64
+ ci: "bun install --frozen-lockfile",
65
+ run: "bun run",
66
+ exec: "bunx",
67
+ dlx: "bunx",
68
+ lockfile: "bun.lockb",
69
+ prismaExec: "bunx prisma",
70
+ runDev: "bun run dev",
71
+ audit: "bun audit --prod"
72
+ };
29
73
  }
30
74
  }
31
75
  function detectPackageManager(cwd) {
@@ -78,17 +122,13 @@ async function downloadRepo(localPath) {
78
122
  const dest = join(tmpdir(), `projx-${Date.now()}`);
79
123
  await mkdir(dest, { recursive: true });
80
124
  if (hasCommand("git")) {
81
- execSync(
82
- `git clone --depth 1 ${REPO_URL}.git "${dest}/repo"`,
83
- { stdio: "pipe" }
84
- );
125
+ execSync(`git clone --depth 1 ${REPO_URL}.git "${dest}/repo"`, {
126
+ stdio: "pipe"
127
+ });
85
128
  return join(dest, "repo");
86
129
  }
87
130
  const tarUrl = `${REPO_URL}/archive/refs/heads/main.tar.gz`;
88
- execSync(
89
- `curl -sL "${tarUrl}" | tar xz -C "${dest}"`,
90
- { stdio: "pipe" }
91
- );
131
+ execSync(`curl -sL "${tarUrl}" | tar xz -C "${dest}"`, { stdio: "pipe" });
92
132
  const entries = await readdir(dest);
93
133
  const extracted = entries.find((e) => e.startsWith("projx-"));
94
134
  if (!extracted) throw new Error("Failed to extract repo archive.");
@@ -293,7 +333,9 @@ async function discoverComponentPaths(cwd, components) {
293
333
  async function discoverComponentsFromMarkers(cwd) {
294
334
  const components = [];
295
335
  const paths = {};
296
- if (!existsSync(cwd)) return { components, paths };
336
+ const instances = [];
337
+ if (!existsSync(cwd))
338
+ return { components, paths, instances };
297
339
  const entries = await readdir(cwd, { withFileTypes: true });
298
340
  for (const entry of entries) {
299
341
  if (!entry.isDirectory()) continue;
@@ -301,6 +343,7 @@ async function discoverComponentsFromMarkers(cwd) {
301
343
  if (entry.name.startsWith(".")) continue;
302
344
  const marker = await readComponentMarker(join(cwd, entry.name));
303
345
  if (!marker) continue;
346
+ instances.push({ type: marker.component, path: entry.name });
304
347
  if (!components.includes(marker.component)) {
305
348
  components.push(marker.component);
306
349
  paths[marker.component] = entry.name;
@@ -309,33 +352,82 @@ async function discoverComponentsFromMarkers(cwd) {
309
352
  for (const c of components) {
310
353
  if (!paths[c]) paths[c] = c;
311
354
  }
312
- return { components, paths };
355
+ return { components, paths, instances };
313
356
  }
314
357
  function render(template, vars) {
358
+ const lines = template.split("\n");
359
+ return renderLines(lines, vars).replace(/\n{3,}/g, "\n\n");
360
+ }
361
+ function evalExpr(expr, vars) {
315
362
  const components = vars.components;
316
363
  const projectName = vars.projectName;
317
- const lines = template.split("\n");
364
+ const pmName = vars.pm?.name ?? "npm";
365
+ const argNames = ["components", "projectName", "pm"];
366
+ const argValues = [components, projectName, pmName];
367
+ for (const [k, v] of Object.entries(vars)) {
368
+ if (k === "components" || k === "projectName" || k === "pm") continue;
369
+ if (!/^[a-zA-Z_$][\w$]*$/.test(k)) continue;
370
+ argNames.push(k);
371
+ argValues.push(v);
372
+ }
373
+ const fn = new Function(...argNames, `return ${expr}`);
374
+ return fn(...argValues);
375
+ }
376
+ function findBlockEnd(lines, startIdx) {
377
+ let depth = 1;
378
+ for (let i = startIdx + 1; i < lines.length; i++) {
379
+ const line = lines[i];
380
+ if (/^<%\s*(if|for)\s*\(.+?\)\s*\{?\s*%>$/.test(line)) depth++;
381
+ else if (/^<%\s*\}\s*else\s+if\s*\((.+?)\)\s*\{?\s*%>$/.test(line)) {
382
+ } else if (/^<%\s*\}\s*else\s*\{?\s*%>$/.test(line)) {
383
+ } else if (/^<%\s*\}?\s*%>$/.test(line)) {
384
+ depth--;
385
+ if (depth === 0) return i;
386
+ }
387
+ }
388
+ throw new Error("Unmatched template block");
389
+ }
390
+ function renderLines(lines, vars) {
318
391
  const output = [];
319
392
  const stack = [];
320
- for (const line of lines) {
393
+ for (let i = 0; i < lines.length; i++) {
394
+ const line = lines[i];
395
+ const forMatch = line.match(
396
+ /^<%\s*for\s*\(\s*(?:const|let)\s+(\w+)\s+of\s+(.+?)\s*\)\s*\{?\s*%>$/
397
+ );
398
+ if (forMatch) {
399
+ const varName = forMatch[1];
400
+ const iterExpr = forMatch[2];
401
+ const end = findBlockEnd(lines, i);
402
+ const bodyLines = lines.slice(i + 1, end);
403
+ if (stack.length === 0 || stack.every((v) => v.active)) {
404
+ const iterable = evalExpr(iterExpr, vars);
405
+ if (Array.isArray(iterable)) {
406
+ for (const item of iterable) {
407
+ const sub = renderLines(bodyLines, { ...vars, [varName]: item });
408
+ if (sub) output.push(sub);
409
+ }
410
+ }
411
+ }
412
+ i = end;
413
+ continue;
414
+ }
321
415
  const ifMatch = line.match(/^<%\s*if\s*\((.+?)\)\s*\{?\s*%>$/);
322
416
  if (ifMatch) {
323
- const pmName = vars.pm?.name ?? "npm";
324
- const fn = new Function("components", "projectName", "pm", `return ${ifMatch[1]}`);
325
- const result = fn(components, projectName, pmName);
417
+ const result = Boolean(evalExpr(ifMatch[1], vars));
326
418
  stack.push({ active: result, matched: result });
327
419
  continue;
328
420
  }
329
- const elseIfMatch = line.match(/^<%\s*\}\s*else\s+if\s*\((.+?)\)\s*\{?\s*%>$/);
421
+ const elseIfMatch = line.match(
422
+ /^<%\s*\}\s*else\s+if\s*\((.+?)\)\s*\{?\s*%>$/
423
+ );
330
424
  if (elseIfMatch) {
331
425
  if (stack.length > 0) {
332
426
  const top = stack[stack.length - 1];
333
427
  if (top.matched) {
334
428
  top.active = false;
335
429
  } else {
336
- const pmN = vars.pm?.name ?? "npm";
337
- const fn = new Function("components", "projectName", "pm", `return ${elseIfMatch[1]}`);
338
- const result = fn(components, projectName, pmN);
430
+ const result = Boolean(evalExpr(elseIfMatch[1], vars));
339
431
  top.active = result;
340
432
  if (result) top.matched = true;
341
433
  }
@@ -354,20 +446,17 @@ function render(template, vars) {
354
446
  continue;
355
447
  }
356
448
  if (stack.length > 0 && stack.some((v) => !v.active)) continue;
357
- const replaced = line.replace(
358
- /<%=\s*([\w.]+)\s*%>/g,
359
- (_, expr) => {
360
- const parts = expr.split(".");
361
- let val = vars;
362
- for (const p of parts) {
363
- val = val?.[p];
364
- }
365
- return String(val ?? "");
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];
366
454
  }
367
- );
455
+ return String(val ?? "");
456
+ });
368
457
  output.push(replaced);
369
458
  }
370
- return output.join("\n").replace(/\n{3,}/g, "\n\n");
459
+ return output.join("\n");
371
460
  }
372
461
  async function renderEjsInDir(dir, vars) {
373
462
  if (!existsSync(dir)) return;