create-projx 1.6.2 → 1.6.4
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 +18 -6
- package/dist/{baseline-KTCFW2FK.js → baseline-PZM4KJJW.js} +2 -6
- package/dist/{chunk-LTIJPVRZ.js → chunk-6YRBHJ2V.js} +151 -38
- package/dist/{chunk-D33FXCNT.js → chunk-XQ7FE4U3.js} +343 -153
- package/dist/index.js +749 -205
- package/dist/{utils-VY5BBJBQ.js → utils-AVKSTHIF.js} +1 -1
- package/package.json +1 -1
- package/src/templates/README.md.ejs +1 -1
- package/src/templates/ci.yml.ejs +63 -63
- package/src/templates/docker-compose.dev.yml.ejs +44 -29
- package/src/templates/docker-compose.yml.ejs +27 -25
- package/src/templates/pre-commit.ejs +52 -52
- package/src/templates/setup.sh.ejs +16 -16
package/README.md
CHANGED
|
@@ -39,7 +39,7 @@ Ask an LLM to "scaffold a full-stack app" and you get 50 files of plausible-look
|
|
|
39
39
|
```bash
|
|
40
40
|
npx create-projx my-app # interactive — pick exactly what you need
|
|
41
41
|
cd my-app
|
|
42
|
-
./setup.sh
|
|
42
|
+
./scripts/setup.sh # installs everything you picked
|
|
43
43
|
```
|
|
44
44
|
|
|
45
45
|
Pick any combination of components — they're all optional:
|
|
@@ -116,7 +116,7 @@ npx create-projx my-app -y
|
|
|
116
116
|
|
|
117
117
|
## Package Manager Support
|
|
118
118
|
|
|
119
|
-
Projx supports **npm**, **pnpm**, **yarn**, and **bun**. During `create`, you're prompted to pick one. The choice is stored in `.projx` and used everywhere — setup.sh
|
|
119
|
+
Projx supports **npm**, **pnpm**, **yarn**, and **bun**. During `create`, you're prompted to pick one. The choice is stored in `.projx` and used everywhere — `scripts/setup.sh`, Docker, CI, pre-commit hooks, and README.
|
|
120
120
|
|
|
121
121
|
```json
|
|
122
122
|
{ "packageManager": "pnpm" }
|
|
@@ -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 `scripts/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:
|
|
@@ -181,7 +191,7 @@ Common user-owned files are **default-skipped** automatically — template updat
|
|
|
181
191
|
|
|
182
192
|
| Scope | Default skips |
|
|
183
193
|
|-------|---------------|
|
|
184
|
-
| Root (`.projx`) | `docker-compose.yml`, `docker-compose.dev.yml`, `README.md`, `.githooks/pre-commit`, `.github/workflows/ci.yml`, `setup.sh` |
|
|
194
|
+
| Root (`.projx`) | `docker-compose.yml`, `docker-compose.dev.yml`, `README.md`, `.githooks/pre-commit`, `.github/workflows/ci.yml`, `scripts/setup.sh`, `scripts/setup-docker.sh`, `scripts/setup-ssl.sh` |
|
|
185
195
|
| fastapi | `pyproject.toml` |
|
|
186
196
|
| fastify / frontend / e2e | `package.json` |
|
|
187
197
|
| mobile | `pubspec.yaml` |
|
|
@@ -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
|
|
@@ -324,7 +336,7 @@ backend/.projx-component → { "components": ["fastapi"] }
|
|
|
324
336
|
web/.projx-component → { "components": ["frontend"] }
|
|
325
337
|
```
|
|
326
338
|
|
|
327
|
-
CI, setup.sh
|
|
339
|
+
CI, `scripts/setup.sh`, pre-commit hooks, and docker-compose are all regenerated with your custom directory names.
|
|
328
340
|
|
|
329
341
|
## What a Scaffolded Project Looks Like
|
|
330
342
|
|
|
@@ -341,7 +353,7 @@ my-app/
|
|
|
341
353
|
├── .github/workflows/ # CI per component (runs only on changes)
|
|
342
354
|
├── .githooks/pre-commit # Format + lint on commit
|
|
343
355
|
├── .vscode/ # Editor settings + recommended extensions
|
|
344
|
-
├── setup.sh
|
|
356
|
+
├── scripts/ # setup.sh, setup-docker.sh, setup-ssl.sh
|
|
345
357
|
└── .projx # Components list + version
|
|
346
358
|
```
|
|
347
359
|
|
|
@@ -366,7 +378,7 @@ Contributing to Projx itself:
|
|
|
366
378
|
```bash
|
|
367
379
|
git clone https://github.com/ukanhaupa/projx.git
|
|
368
380
|
cd projx
|
|
369
|
-
./setup.sh
|
|
381
|
+
./scripts/setup.sh
|
|
370
382
|
```
|
|
371
383
|
|
|
372
384
|
The CLI lives in `cli/`. Templates are the root-level component directories (`fastapi/`, `frontend/`, etc.).
|
|
@@ -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-XQ7FE4U3.js";
|
|
12
|
+
import "./chunk-6YRBHJ2V.js";
|
|
15
13
|
export {
|
|
16
14
|
BASELINE_REF,
|
|
17
15
|
applyTemplate,
|
|
18
|
-
buildDisplayNames,
|
|
19
|
-
buildPathsUpper,
|
|
20
16
|
collectAllFiles,
|
|
21
17
|
detectPackageNameOverrides,
|
|
22
18
|
getBaselineRef,
|
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
// src/utils.ts
|
|
2
2
|
import { execSync } from "child_process";
|
|
3
3
|
import { existsSync, readFileSync } from "fs";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
chmod,
|
|
6
|
+
cp,
|
|
7
|
+
mkdir,
|
|
8
|
+
readdir,
|
|
9
|
+
readFile,
|
|
10
|
+
rm,
|
|
11
|
+
writeFile
|
|
12
|
+
} from "fs/promises";
|
|
5
13
|
import { join, resolve } from "path";
|
|
6
14
|
import { tmpdir } from "os";
|
|
7
15
|
import { fileURLToPath } from "url";
|
|
@@ -19,13 +27,57 @@ var PACKAGE_MANAGERS = ["npm", "pnpm", "yarn", "bun"];
|
|
|
19
27
|
function pmCommands(pm) {
|
|
20
28
|
switch (pm) {
|
|
21
29
|
case "npm":
|
|
22
|
-
return {
|
|
30
|
+
return {
|
|
31
|
+
name: "npm",
|
|
32
|
+
install: "npm install",
|
|
33
|
+
ci: "npm ci",
|
|
34
|
+
run: "npm run",
|
|
35
|
+
exec: "npx",
|
|
36
|
+
dlx: "npx",
|
|
37
|
+
lockfile: "package-lock.json",
|
|
38
|
+
prismaExec: "npx prisma",
|
|
39
|
+
runDev: "npm run dev",
|
|
40
|
+
audit: "npm audit --omit=dev"
|
|
41
|
+
};
|
|
23
42
|
case "pnpm":
|
|
24
|
-
return {
|
|
43
|
+
return {
|
|
44
|
+
name: "pnpm",
|
|
45
|
+
install: "pnpm install",
|
|
46
|
+
ci: "pnpm install --frozen-lockfile",
|
|
47
|
+
run: "pnpm",
|
|
48
|
+
exec: "pnpm exec",
|
|
49
|
+
dlx: "pnpm dlx",
|
|
50
|
+
lockfile: "pnpm-lock.yaml",
|
|
51
|
+
prismaExec: "pnpm prisma",
|
|
52
|
+
runDev: "pnpm dev",
|
|
53
|
+
audit: "pnpm audit --prod"
|
|
54
|
+
};
|
|
25
55
|
case "yarn":
|
|
26
|
-
return {
|
|
56
|
+
return {
|
|
57
|
+
name: "yarn",
|
|
58
|
+
install: "yarn",
|
|
59
|
+
ci: "yarn --frozen-lockfile",
|
|
60
|
+
run: "yarn",
|
|
61
|
+
exec: "yarn",
|
|
62
|
+
dlx: "yarn dlx",
|
|
63
|
+
lockfile: "yarn.lock",
|
|
64
|
+
prismaExec: "yarn prisma",
|
|
65
|
+
runDev: "yarn dev",
|
|
66
|
+
audit: "yarn npm audit --environment production"
|
|
67
|
+
};
|
|
27
68
|
case "bun":
|
|
28
|
-
return {
|
|
69
|
+
return {
|
|
70
|
+
name: "bun",
|
|
71
|
+
install: "bun install",
|
|
72
|
+
ci: "bun install --frozen-lockfile",
|
|
73
|
+
run: "bun run",
|
|
74
|
+
exec: "bunx",
|
|
75
|
+
dlx: "bunx",
|
|
76
|
+
lockfile: "bun.lockb",
|
|
77
|
+
prismaExec: "bunx prisma",
|
|
78
|
+
runDev: "bun run dev",
|
|
79
|
+
audit: "bun audit --prod"
|
|
80
|
+
};
|
|
29
81
|
}
|
|
30
82
|
}
|
|
31
83
|
function detectPackageManager(cwd) {
|
|
@@ -78,17 +130,13 @@ async function downloadRepo(localPath) {
|
|
|
78
130
|
const dest = join(tmpdir(), `projx-${Date.now()}`);
|
|
79
131
|
await mkdir(dest, { recursive: true });
|
|
80
132
|
if (hasCommand("git")) {
|
|
81
|
-
execSync(
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
);
|
|
133
|
+
execSync(`git clone --depth 1 ${REPO_URL}.git "${dest}/repo"`, {
|
|
134
|
+
stdio: "pipe"
|
|
135
|
+
});
|
|
85
136
|
return join(dest, "repo");
|
|
86
137
|
}
|
|
87
138
|
const tarUrl = `${REPO_URL}/archive/refs/heads/main.tar.gz`;
|
|
88
|
-
execSync(
|
|
89
|
-
`curl -sL "${tarUrl}" | tar xz -C "${dest}"`,
|
|
90
|
-
{ stdio: "pipe" }
|
|
91
|
-
);
|
|
139
|
+
execSync(`curl -sL "${tarUrl}" | tar xz -C "${dest}"`, { stdio: "pipe" });
|
|
92
140
|
const entries = await readdir(dest);
|
|
93
141
|
const extracted = entries.find((e) => e.startsWith("projx-"));
|
|
94
142
|
if (!extracted) throw new Error("Failed to extract repo archive.");
|
|
@@ -165,10 +213,19 @@ async function copyStaticFiles(repoDir, dest) {
|
|
|
165
213
|
await cp(extensionsJson, join(dest, ".vscode/extensions.json"));
|
|
166
214
|
manifest.push(".vscode/extensions.json");
|
|
167
215
|
}
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
216
|
+
const staticScripts = ["setup-docker.sh", "setup-ssl.sh"];
|
|
217
|
+
const scriptsSrc = join(tpl, "scripts");
|
|
218
|
+
if (existsSync(scriptsSrc)) {
|
|
219
|
+
await mkdir(join(dest, "scripts"), { recursive: true });
|
|
220
|
+
for (const file of staticScripts) {
|
|
221
|
+
const src = join(scriptsSrc, file);
|
|
222
|
+
const dst = join(dest, "scripts", file);
|
|
223
|
+
if (existsSync(src) && !existsSync(dst)) {
|
|
224
|
+
await cp(src, dst);
|
|
225
|
+
await chmod(dst, 493);
|
|
226
|
+
manifest.push(`scripts/${file}`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
172
229
|
}
|
|
173
230
|
return manifest;
|
|
174
231
|
}
|
|
@@ -273,7 +330,9 @@ var DEFAULT_ROOT_SKIP_PATTERNS = [
|
|
|
273
330
|
"README.md",
|
|
274
331
|
".githooks/pre-commit",
|
|
275
332
|
".github/workflows/ci.yml",
|
|
276
|
-
"setup.sh"
|
|
333
|
+
"scripts/setup.sh",
|
|
334
|
+
"scripts/setup-docker.sh",
|
|
335
|
+
"scripts/setup-ssl.sh"
|
|
277
336
|
];
|
|
278
337
|
var DEFAULT_COMPONENT_SKIP_PATTERNS = {
|
|
279
338
|
fastapi: ["pyproject.toml"],
|
|
@@ -293,7 +352,9 @@ async function discoverComponentPaths(cwd, components) {
|
|
|
293
352
|
async function discoverComponentsFromMarkers(cwd) {
|
|
294
353
|
const components = [];
|
|
295
354
|
const paths = {};
|
|
296
|
-
|
|
355
|
+
const instances = [];
|
|
356
|
+
if (!existsSync(cwd))
|
|
357
|
+
return { components, paths, instances };
|
|
297
358
|
const entries = await readdir(cwd, { withFileTypes: true });
|
|
298
359
|
for (const entry of entries) {
|
|
299
360
|
if (!entry.isDirectory()) continue;
|
|
@@ -301,6 +362,7 @@ async function discoverComponentsFromMarkers(cwd) {
|
|
|
301
362
|
if (entry.name.startsWith(".")) continue;
|
|
302
363
|
const marker = await readComponentMarker(join(cwd, entry.name));
|
|
303
364
|
if (!marker) continue;
|
|
365
|
+
instances.push({ type: marker.component, path: entry.name });
|
|
304
366
|
if (!components.includes(marker.component)) {
|
|
305
367
|
components.push(marker.component);
|
|
306
368
|
paths[marker.component] = entry.name;
|
|
@@ -309,33 +371,82 @@ async function discoverComponentsFromMarkers(cwd) {
|
|
|
309
371
|
for (const c of components) {
|
|
310
372
|
if (!paths[c]) paths[c] = c;
|
|
311
373
|
}
|
|
312
|
-
return { components, paths };
|
|
374
|
+
return { components, paths, instances };
|
|
313
375
|
}
|
|
314
376
|
function render(template, vars) {
|
|
377
|
+
const lines = template.split("\n");
|
|
378
|
+
return renderLines(lines, vars).replace(/\n{3,}/g, "\n\n");
|
|
379
|
+
}
|
|
380
|
+
function evalExpr(expr, vars) {
|
|
315
381
|
const components = vars.components;
|
|
316
382
|
const projectName = vars.projectName;
|
|
317
|
-
const
|
|
383
|
+
const pmName = vars.pm?.name ?? "npm";
|
|
384
|
+
const argNames = ["components", "projectName", "pm"];
|
|
385
|
+
const argValues = [components, projectName, pmName];
|
|
386
|
+
for (const [k, v] of Object.entries(vars)) {
|
|
387
|
+
if (k === "components" || k === "projectName" || k === "pm") continue;
|
|
388
|
+
if (!/^[a-zA-Z_$][\w$]*$/.test(k)) continue;
|
|
389
|
+
argNames.push(k);
|
|
390
|
+
argValues.push(v);
|
|
391
|
+
}
|
|
392
|
+
const fn = new Function(...argNames, `return ${expr}`);
|
|
393
|
+
return fn(...argValues);
|
|
394
|
+
}
|
|
395
|
+
function findBlockEnd(lines, startIdx) {
|
|
396
|
+
let depth = 1;
|
|
397
|
+
for (let i = startIdx + 1; i < lines.length; i++) {
|
|
398
|
+
const line = lines[i];
|
|
399
|
+
if (/^<%\s*(if|for)\s*\(.+?\)\s*\{?\s*%>$/.test(line)) depth++;
|
|
400
|
+
else if (/^<%\s*\}\s*else\s+if\s*\((.+?)\)\s*\{?\s*%>$/.test(line)) {
|
|
401
|
+
} else if (/^<%\s*\}\s*else\s*\{?\s*%>$/.test(line)) {
|
|
402
|
+
} else if (/^<%\s*\}?\s*%>$/.test(line)) {
|
|
403
|
+
depth--;
|
|
404
|
+
if (depth === 0) return i;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
throw new Error("Unmatched template block");
|
|
408
|
+
}
|
|
409
|
+
function renderLines(lines, vars) {
|
|
318
410
|
const output = [];
|
|
319
411
|
const stack = [];
|
|
320
|
-
for (
|
|
412
|
+
for (let i = 0; i < lines.length; i++) {
|
|
413
|
+
const line = lines[i];
|
|
414
|
+
const forMatch = line.match(
|
|
415
|
+
/^<%\s*for\s*\(\s*(?:const|let)\s+(\w+)\s+of\s+(.+?)\s*\)\s*\{?\s*%>$/
|
|
416
|
+
);
|
|
417
|
+
if (forMatch) {
|
|
418
|
+
const varName = forMatch[1];
|
|
419
|
+
const iterExpr = forMatch[2];
|
|
420
|
+
const end = findBlockEnd(lines, i);
|
|
421
|
+
const bodyLines = lines.slice(i + 1, end);
|
|
422
|
+
if (stack.length === 0 || stack.every((v) => v.active)) {
|
|
423
|
+
const iterable = evalExpr(iterExpr, vars);
|
|
424
|
+
if (Array.isArray(iterable)) {
|
|
425
|
+
for (const item of iterable) {
|
|
426
|
+
const sub = renderLines(bodyLines, { ...vars, [varName]: item });
|
|
427
|
+
if (sub) output.push(sub);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
i = end;
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
321
434
|
const ifMatch = line.match(/^<%\s*if\s*\((.+?)\)\s*\{?\s*%>$/);
|
|
322
435
|
if (ifMatch) {
|
|
323
|
-
const
|
|
324
|
-
const fn = new Function("components", "projectName", "pm", `return ${ifMatch[1]}`);
|
|
325
|
-
const result = fn(components, projectName, pmName);
|
|
436
|
+
const result = Boolean(evalExpr(ifMatch[1], vars));
|
|
326
437
|
stack.push({ active: result, matched: result });
|
|
327
438
|
continue;
|
|
328
439
|
}
|
|
329
|
-
const elseIfMatch = line.match(
|
|
440
|
+
const elseIfMatch = line.match(
|
|
441
|
+
/^<%\s*\}\s*else\s+if\s*\((.+?)\)\s*\{?\s*%>$/
|
|
442
|
+
);
|
|
330
443
|
if (elseIfMatch) {
|
|
331
444
|
if (stack.length > 0) {
|
|
332
445
|
const top = stack[stack.length - 1];
|
|
333
446
|
if (top.matched) {
|
|
334
447
|
top.active = false;
|
|
335
448
|
} else {
|
|
336
|
-
const
|
|
337
|
-
const fn = new Function("components", "projectName", "pm", `return ${elseIfMatch[1]}`);
|
|
338
|
-
const result = fn(components, projectName, pmN);
|
|
449
|
+
const result = Boolean(evalExpr(elseIfMatch[1], vars));
|
|
339
450
|
top.active = result;
|
|
340
451
|
if (result) top.matched = true;
|
|
341
452
|
}
|
|
@@ -354,20 +465,22 @@ function render(template, vars) {
|
|
|
354
465
|
continue;
|
|
355
466
|
}
|
|
356
467
|
if (stack.length > 0 && stack.some((v) => !v.active)) continue;
|
|
357
|
-
const replaced = line.replace(
|
|
358
|
-
|
|
359
|
-
(
|
|
360
|
-
const parts =
|
|
361
|
-
let
|
|
468
|
+
const replaced = line.replace(/<%=\s*(.+?)\s*%>/g, (_, expr) => {
|
|
469
|
+
const trimmed = expr.trim();
|
|
470
|
+
if (/^[\w.]+$/.test(trimmed)) {
|
|
471
|
+
const parts = trimmed.split(".");
|
|
472
|
+
let val2 = vars;
|
|
362
473
|
for (const p of parts) {
|
|
363
|
-
|
|
474
|
+
val2 = val2?.[p];
|
|
364
475
|
}
|
|
365
|
-
return String(
|
|
476
|
+
return String(val2 ?? "");
|
|
366
477
|
}
|
|
367
|
-
|
|
478
|
+
const val = evalExpr(trimmed, vars);
|
|
479
|
+
return String(val ?? "");
|
|
480
|
+
});
|
|
368
481
|
output.push(replaced);
|
|
369
482
|
}
|
|
370
|
-
return output.join("\n")
|
|
483
|
+
return output.join("\n");
|
|
371
484
|
}
|
|
372
485
|
async function renderEjsInDir(dir, vars) {
|
|
373
486
|
if (!existsSync(dir)) return;
|