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 +12 -0
- package/dist/{baseline-KTCFW2FK.js → baseline-RXPDDEDD.js} +2 -6
- package/dist/{chunk-LTIJPVRZ.js → chunk-LYPPFXGK.js} +123 -34
- package/dist/{chunk-D33FXCNT.js → chunk-OBYYB6PR.js} +289 -150
- package/dist/index.js +708 -172
- package/dist/{utils-VY5BBJBQ.js → utils-BXHJP6HF.js} +1 -1
- package/package.json +1 -1
- package/src/templates/ci.yml.ejs +63 -63
- package/src/templates/pre-commit.ejs +52 -52
- package/src/templates/setup.sh.ejs +16 -16
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-
|
|
14
|
-
import "./chunk-
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
83
|
-
|
|
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
|
-
|
|
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
|
|
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 (
|
|
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
|
|
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(
|
|
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
|
|
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
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
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")
|
|
459
|
+
return output.join("\n");
|
|
371
460
|
}
|
|
372
461
|
async function renderEjsInDir(dir, vars) {
|
|
373
462
|
if (!existsSync(dir)) return;
|