create-projx 1.6.1 → 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 +17 -3
- package/dist/{baseline-5XAJJ457.js → baseline-RXPDDEDD.js} +2 -6
- package/dist/{chunk-FTHX7ILT.js → chunk-LYPPFXGK.js} +140 -34
- package/dist/{chunk-TNI4XBVS.js → chunk-OBYYB6PR.js} +293 -150
- package/dist/index.js +708 -172
- package/dist/{utils-OOY5OZDX.js → utils-BXHJP6HF.js} +3 -1
- package/package.json +8 -9
- package/src/templates/README.md.ejs +1 -1
- package/src/templates/ci.yml.ejs +83 -66
- package/src/templates/pre-commit.ejs +53 -52
- package/src/templates/setup.sh.ejs +16 -16
package/README.md
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
|
|
8
8
|
**Go from blank folder to production-ready project in 30 seconds.** Backend-only API, AI/ML app, mobile, full-stack, infra setup — pick what you need and get it wired with auth, database, Docker, CI/CD, hooks, and tests. All optional. All yours.
|
|
9
9
|
|
|
10
|
+

|
|
11
|
+
|
|
10
12
|
```bash
|
|
11
13
|
npx create-projx my-app
|
|
12
14
|
```
|
|
@@ -156,6 +158,16 @@ npx create-projx add frontend mobile
|
|
|
156
158
|
|
|
157
159
|
Copies the new component directories, regenerates shared files (docker-compose, CI, pre-commit hooks) to include them, and installs dependencies.
|
|
158
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
|
+
|
|
159
171
|
### Update Scaffolding
|
|
160
172
|
|
|
161
173
|
When templates improve, update your project:
|
|
@@ -211,6 +223,7 @@ To opt back in to updates for a skipped file, use `npx create-projx unpin <file>
|
|
|
211
223
|
npx create-projx <name> [options]
|
|
212
224
|
npx create-projx init
|
|
213
225
|
npx create-projx add <components...>
|
|
226
|
+
npx create-projx add <type> --name <dir>
|
|
214
227
|
npx create-projx update
|
|
215
228
|
npx create-projx diff
|
|
216
229
|
npx create-projx pin <patterns...>
|
|
@@ -221,6 +234,7 @@ npx create-projx gen entity <name> [--ai | --backend]
|
|
|
221
234
|
npx create-projx sync [--url <url>]
|
|
222
235
|
|
|
223
236
|
--components <list> Comma-separated: fastapi,fastify,frontend,mobile,e2e,infra
|
|
237
|
+
--name <dir> Custom directory for `add <type>` (multi-instance)
|
|
224
238
|
--ai Target fastapi (AI/ML) for gen entity
|
|
225
239
|
--backend Target fastify (API backend) for gen entity
|
|
226
240
|
--no-git Skip git init
|
|
@@ -289,7 +303,7 @@ Override with `--ai` (fastapi) or `--backend` (fastify).
|
|
|
289
303
|
| `frontend` | `src/types/<name>.ts` — TypeScript interface + Create/Update variants |
|
|
290
304
|
| `mobile` | `lib/entities/<name>/model.dart` — Dart class with fromJson/toJson/copyWith |
|
|
291
305
|
|
|
292
|
-
**Tests included**: every `gen entity` writes a working integration test file alongside the model — 11 tests for FastAPI (extending `BaseEntityApiTest`), 11 tests for Fastify (via `describeCrudEntity`). Both run against a real database (Postgres
|
|
306
|
+
**Tests included**: every `gen entity` writes a working integration test file alongside the model — 11 tests for FastAPI (extending `BaseEntityApiTest`), 11 tests for Fastify (via `describeCrudEntity`). Both run against a real database (Postgres). New entities ship green from day one — no scrambling to bolt on tests at go-live.
|
|
293
307
|
|
|
294
308
|
No migrations — run `alembic revision --autogenerate` or `prisma migrate dev` (via your package manager) when ready.
|
|
295
309
|
|
|
@@ -371,8 +385,8 @@ The CLI lives in `cli/`. Templates are the root-level component directories (`fa
|
|
|
371
385
|
|
|
372
386
|
```bash
|
|
373
387
|
cd cli
|
|
374
|
-
|
|
375
|
-
|
|
388
|
+
pnpm test # run tests
|
|
389
|
+
pnpm build # build CLI
|
|
376
390
|
```
|
|
377
391
|
|
|
378
392
|
## Try it now
|
|
@@ -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,33 @@ 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");
|
|
460
|
+
}
|
|
461
|
+
async function renderEjsInDir(dir, vars) {
|
|
462
|
+
if (!existsSync(dir)) return;
|
|
463
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
464
|
+
for (const entry of entries) {
|
|
465
|
+
const full = join(dir, entry.name);
|
|
466
|
+
if (entry.isDirectory()) {
|
|
467
|
+
await renderEjsInDir(full, vars);
|
|
468
|
+
} else if (entry.name.endsWith(".ejs")) {
|
|
469
|
+
const content = await readFile(full, "utf-8");
|
|
470
|
+
const rendered = render(content, vars);
|
|
471
|
+
const out = full.slice(0, -".ejs".length);
|
|
472
|
+
await writeFile(out, rendered);
|
|
473
|
+
await rm(full);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
371
476
|
}
|
|
372
477
|
function detectProjectName(cwd, components, componentPaths) {
|
|
373
478
|
for (const component of components) {
|
|
@@ -420,5 +525,6 @@ export {
|
|
|
420
525
|
discoverComponentPaths,
|
|
421
526
|
discoverComponentsFromMarkers,
|
|
422
527
|
render,
|
|
528
|
+
renderEjsInDir,
|
|
423
529
|
detectProjectName
|
|
424
530
|
};
|