create-projx 1.5.2 → 1.5.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 +22 -19
- package/dist/index.js +134 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -66,6 +66,7 @@ If this saves you even one hour, it's already paid for itself. (It's free.)
|
|
|
66
66
|
## Why teams pick Projx and stay
|
|
67
67
|
|
|
68
68
|
- **It actually runs.** Every template is tested in CI before release. No "looks right" surprises.
|
|
69
|
+
- **Tests ship with the code.** `gen entity` writes integration tests alongside every model — 11 tests per entity, against a real database. You start green, not scrambling.
|
|
69
70
|
- **Auto-entity pattern.** Define a data model, get CRUD routes, validation, OpenAPI docs, and a typed UI for free. Backend, frontend, and mobile all stay in sync.
|
|
70
71
|
- **Updates don't nuke your code.** `projx update` does a 3-tier merge — your custom controllers, pages, and config survive template upgrades. No rewrites.
|
|
71
72
|
- **No lock-in.** Projx generates files and walks away. Delete the `.projx` config and it's just a normal repo.
|
|
@@ -96,7 +97,7 @@ Projx is a shared operating system for teams that ship with both:
|
|
|
96
97
|
- **AI agents** get a deterministic CLI to call instead of guessing folder structures.
|
|
97
98
|
- **Teams** get consistency across repos, less drift, and fewer surprise refactors.
|
|
98
99
|
|
|
99
|
-
If you're an AI agent reading this, see [SKILL.md](SKILL.md) for trigger conditions, decision rules, and the canonical invocation.
|
|
100
|
+
If you're an AI agent reading this, see [SKILL.md](SKILL.md) for trigger conditions, decision rules, and the canonical invocation.
|
|
100
101
|
|
|
101
102
|
## Quick Start
|
|
102
103
|
|
|
@@ -188,7 +189,7 @@ To skip root-level files (docker-compose, README), add `skip` to `.projx`:
|
|
|
188
189
|
|
|
189
190
|
```json
|
|
190
191
|
{
|
|
191
|
-
"version": "1.
|
|
192
|
+
"version": "1.5.2",
|
|
192
193
|
"components": ["fastapi", "frontend"],
|
|
193
194
|
"skip": ["docker-compose.yml", "README.md"]
|
|
194
195
|
}
|
|
@@ -273,12 +274,14 @@ When both `fastapi` and `fastify` exist, the entity generates in the **primary b
|
|
|
273
274
|
|
|
274
275
|
Override with `--ai` (fastapi) or `--backend` (fastify).
|
|
275
276
|
|
|
276
|
-
| Component | Generated
|
|
277
|
-
| ------------------------- |
|
|
278
|
-
| Primary backend (fastapi) | `src/entities/<name>/_model.py` —
|
|
279
|
-
| Primary backend (fastify) | `src/modules/<name>/schemas.ts` + `index.ts` + Prisma model +
|
|
280
|
-
| `frontend` | `src/types/<name>.ts` — TypeScript interface + Create/Update variants
|
|
281
|
-
| `mobile` | `lib/entities/<name>/model.dart` — Dart class with fromJson/toJson/copyWith
|
|
277
|
+
| Component | Generated |
|
|
278
|
+
| ------------------------- | --------------------------------------------------------------------------------------------- |
|
|
279
|
+
| Primary backend (fastapi) | `src/entities/<name>/_model.py` + `tests/test_<name>_entity.py` — model + 11 CRUD/auth tests |
|
|
280
|
+
| Primary backend (fastify) | `src/modules/<name>/schemas.ts` + `index.ts` + Prisma model + `tests/modules/<name>.test.ts` |
|
|
281
|
+
| `frontend` | `src/types/<name>.ts` — TypeScript interface + Create/Update variants |
|
|
282
|
+
| `mobile` | `lib/entities/<name>/model.dart` — Dart class with fromJson/toJson/copyWith |
|
|
283
|
+
|
|
284
|
+
**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 for Fastify, SQLite-in-memory for FastAPI today). New entities ship green from day one — no scrambling to bolt on tests at go-live.
|
|
282
285
|
|
|
283
286
|
No migrations — run `alembic revision --autogenerate` or `prisma migrate dev` (via your package manager) when ready.
|
|
284
287
|
|
|
@@ -362,16 +365,6 @@ npm test # run tests
|
|
|
362
365
|
npm run build # build CLI
|
|
363
366
|
```
|
|
364
367
|
|
|
365
|
-
## Badge
|
|
366
|
-
|
|
367
|
-
Add this to your project's README:
|
|
368
|
-
|
|
369
|
-
```md
|
|
370
|
-
[](https://github.com/ukanhaupa/projx)
|
|
371
|
-
```
|
|
372
|
-
|
|
373
|
-
---
|
|
374
|
-
|
|
375
368
|
## Try it now
|
|
376
369
|
|
|
377
370
|
You're still reading. Stop reading. Run this:
|
|
@@ -382,7 +375,17 @@ npx create-projx my-app
|
|
|
382
375
|
|
|
383
376
|
Pick whatever you need from the menu — backend-only, AI app, mobile, full-stack, just infra. 30 seconds. Free. No signup. If you don't like it, `rm -rf my-app` and we never speak of this again.
|
|
384
377
|
|
|
385
|
-
|
|
378
|
+
---
|
|
379
|
+
|
|
380
|
+
## Badge
|
|
381
|
+
|
|
382
|
+
Add this to your project's README:
|
|
383
|
+
|
|
384
|
+
```md
|
|
385
|
+
[](https://github.com/ukanhaupa/projx)
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
---
|
|
386
389
|
|
|
387
390
|
## License
|
|
388
391
|
|
package/dist/index.js
CHANGED
|
@@ -2697,6 +2697,128 @@ function generateDartModel(config) {
|
|
|
2697
2697
|
lines.push("");
|
|
2698
2698
|
return lines.join("\n");
|
|
2699
2699
|
}
|
|
2700
|
+
function pyHttpLiteral(type, variant = "create") {
|
|
2701
|
+
switch (type) {
|
|
2702
|
+
case "string":
|
|
2703
|
+
case "text":
|
|
2704
|
+
return variant === "create" ? '"sample text"' : variant === "update" ? '"updated text"' : '"alt text"';
|
|
2705
|
+
case "number":
|
|
2706
|
+
return variant === "create" ? "42" : variant === "update" ? "100" : "7";
|
|
2707
|
+
case "boolean":
|
|
2708
|
+
return variant === "create" ? "True" : "False";
|
|
2709
|
+
case "date":
|
|
2710
|
+
return variant === "alt" ? '"2026-02-01"' : '"2026-01-01"';
|
|
2711
|
+
case "datetime":
|
|
2712
|
+
return variant === "alt" ? '"2026-02-01T00:00:00"' : '"2026-01-01T00:00:00"';
|
|
2713
|
+
case "json":
|
|
2714
|
+
return "{}";
|
|
2715
|
+
}
|
|
2716
|
+
}
|
|
2717
|
+
function pyOrmLiteral(type, variant = "create") {
|
|
2718
|
+
switch (type) {
|
|
2719
|
+
case "string":
|
|
2720
|
+
case "text":
|
|
2721
|
+
return variant === "create" ? '"sample text"' : variant === "update" ? '"updated text"' : '"alt text"';
|
|
2722
|
+
case "number":
|
|
2723
|
+
return variant === "create" ? "42" : variant === "update" ? "100" : "7";
|
|
2724
|
+
case "boolean":
|
|
2725
|
+
return variant === "create" ? "True" : "False";
|
|
2726
|
+
case "date":
|
|
2727
|
+
return variant === "alt" ? "date(2026, 2, 1)" : "date(2026, 1, 1)";
|
|
2728
|
+
case "datetime":
|
|
2729
|
+
return variant === "alt" ? "datetime(2026, 2, 1, 0, 0, 0)" : "datetime(2026, 1, 1, 0, 0, 0)";
|
|
2730
|
+
case "json":
|
|
2731
|
+
return "{}";
|
|
2732
|
+
}
|
|
2733
|
+
}
|
|
2734
|
+
function tsLiteral(type, variant = "create") {
|
|
2735
|
+
switch (type) {
|
|
2736
|
+
case "string":
|
|
2737
|
+
case "text":
|
|
2738
|
+
return variant === "create" ? "'sample text'" : variant === "update" ? "'updated text'" : "'alt text'";
|
|
2739
|
+
case "number":
|
|
2740
|
+
return variant === "create" ? "42" : variant === "update" ? "100" : "7";
|
|
2741
|
+
case "boolean":
|
|
2742
|
+
return variant === "create" ? "true" : "false";
|
|
2743
|
+
case "date":
|
|
2744
|
+
return variant === "alt" ? "'2026-02-01'" : "'2026-01-01'";
|
|
2745
|
+
case "datetime":
|
|
2746
|
+
return variant === "alt" ? "'2026-02-01T00:00:00.000Z'" : "'2026-01-01T00:00:00.000Z'";
|
|
2747
|
+
case "json":
|
|
2748
|
+
return "{}";
|
|
2749
|
+
}
|
|
2750
|
+
}
|
|
2751
|
+
function pickFilterField(fields) {
|
|
2752
|
+
return fields.find((f) => f.type === "string" || f.type === "text") ?? fields.find((f) => f.type === "number") ?? fields.find((f) => f.type === "boolean") ?? fields[0];
|
|
2753
|
+
}
|
|
2754
|
+
function generateFastapiTest(config) {
|
|
2755
|
+
const className = toPascal(config.name);
|
|
2756
|
+
const snake = toSnake(config.name);
|
|
2757
|
+
const apiUrl = `/api/v1${config.apiPrefix}/`;
|
|
2758
|
+
const filterField = pickFilterField(config.fields);
|
|
2759
|
+
const needsDate = config.fields.some((f) => f.type === "date");
|
|
2760
|
+
const needsDatetime = config.fields.some((f) => f.type === "datetime");
|
|
2761
|
+
const dateImports = [];
|
|
2762
|
+
if (needsDate) dateImports.push("date");
|
|
2763
|
+
if (needsDatetime) dateImports.push("datetime");
|
|
2764
|
+
const lines = [];
|
|
2765
|
+
if (dateImports.length > 0) {
|
|
2766
|
+
lines.push(`from datetime import ${dateImports.join(", ")}`);
|
|
2767
|
+
lines.push("");
|
|
2768
|
+
}
|
|
2769
|
+
lines.push(`from src.entities.${snake}._model import ${className}`);
|
|
2770
|
+
lines.push(`from tests.base_entity_api_test import BaseEntityApiTest`);
|
|
2771
|
+
lines.push("");
|
|
2772
|
+
lines.push("");
|
|
2773
|
+
lines.push(`class Test${className}Entity(BaseEntityApiTest):`);
|
|
2774
|
+
lines.push(` __test__ = True`);
|
|
2775
|
+
lines.push(` endpoint = "${apiUrl}"`);
|
|
2776
|
+
lines.push(` create_payload = {`);
|
|
2777
|
+
for (const f of config.fields) {
|
|
2778
|
+
lines.push(` "${f.name}": ${pyHttpLiteral(f.type, "create")},`);
|
|
2779
|
+
}
|
|
2780
|
+
lines.push(` }`);
|
|
2781
|
+
const updateField = config.fields[0];
|
|
2782
|
+
lines.push(` update_payload = {"${updateField.name}": ${pyHttpLiteral(updateField.type, "update")}}`);
|
|
2783
|
+
lines.push(` invalid_payload: dict = {}`);
|
|
2784
|
+
lines.push(` filter_field = "${filterField.name}"`);
|
|
2785
|
+
lines.push(` filter_value = ${pyHttpLiteral(filterField.type, "create")}`);
|
|
2786
|
+
lines.push(` other_filter_value = ${pyHttpLiteral(filterField.type, "alt")}`);
|
|
2787
|
+
lines.push("");
|
|
2788
|
+
lines.push(` def make_model(self, index: int, **overrides):`);
|
|
2789
|
+
lines.push(` data = {`);
|
|
2790
|
+
for (const f of config.fields) {
|
|
2791
|
+
lines.push(` "${f.name}": ${pyOrmLiteral(f.type, "create")},`);
|
|
2792
|
+
}
|
|
2793
|
+
lines.push(` }`);
|
|
2794
|
+
lines.push(` data.update(overrides)`);
|
|
2795
|
+
lines.push(` return ${className}(**data)`);
|
|
2796
|
+
lines.push("");
|
|
2797
|
+
return lines.join("\n");
|
|
2798
|
+
}
|
|
2799
|
+
function generateFastifyTest(config) {
|
|
2800
|
+
const className = toPascal(config.name);
|
|
2801
|
+
const basePath = `/api/v1${config.apiPrefix}`;
|
|
2802
|
+
const updateField = config.fields[0];
|
|
2803
|
+
const lines = [];
|
|
2804
|
+
lines.push(`import { describeCrudEntity } from '../helpers/crud-test-base.js';`);
|
|
2805
|
+
lines.push("");
|
|
2806
|
+
lines.push(`describeCrudEntity({`);
|
|
2807
|
+
lines.push(` entityName: '${className}',`);
|
|
2808
|
+
lines.push(` basePath: '${basePath}',`);
|
|
2809
|
+
lines.push(` prismaModel: '${className}',`);
|
|
2810
|
+
lines.push(` createPayload: {`);
|
|
2811
|
+
for (const f of config.fields) {
|
|
2812
|
+
lines.push(` ${f.name}: ${tsLiteral(f.type, "create")},`);
|
|
2813
|
+
}
|
|
2814
|
+
lines.push(` },`);
|
|
2815
|
+
lines.push(` updatePayload: {`);
|
|
2816
|
+
lines.push(` ${updateField.name}: ${tsLiteral(updateField.type, "update")},`);
|
|
2817
|
+
lines.push(` },`);
|
|
2818
|
+
lines.push(`});`);
|
|
2819
|
+
lines.push("");
|
|
2820
|
+
return lines.join("\n");
|
|
2821
|
+
}
|
|
2700
2822
|
async function resolvePrimaryBackend(cwd, hasFastapi, hasFastify, backendFlag) {
|
|
2701
2823
|
if (backendFlag) return backendFlag;
|
|
2702
2824
|
if (hasFastapi && !hasFastify) return "fastapi";
|
|
@@ -2781,6 +2903,12 @@ async function gen(cwd, entityName, fieldsFlag, backendFlag) {
|
|
|
2781
2903
|
await mkdir5(entityDir, { recursive: true });
|
|
2782
2904
|
await writeFile5(join12(entityDir, "_model.py"), generateFastAPIModel(config));
|
|
2783
2905
|
generated.push(`${dir}/src/entities/${toSnake(config.name)}/_model.py`);
|
|
2906
|
+
const testsDir = join12(cwd, dir, "tests");
|
|
2907
|
+
const testFile = join12(testsDir, `test_${toSnake(config.name)}_entity.py`);
|
|
2908
|
+
if (existsSync11(testsDir) && !existsSync11(testFile)) {
|
|
2909
|
+
await writeFile5(testFile, generateFastapiTest(config));
|
|
2910
|
+
generated.push(`${dir}/tests/test_${toSnake(config.name)}_entity.py`);
|
|
2911
|
+
}
|
|
2784
2912
|
}
|
|
2785
2913
|
}
|
|
2786
2914
|
if (genFastify) {
|
|
@@ -2820,6 +2948,12 @@ async function gen(cwd, entityName, fieldsFlag, backendFlag) {
|
|
|
2820
2948
|
generated.push(`${dir}/prisma/schema.prisma (model added)`);
|
|
2821
2949
|
}
|
|
2822
2950
|
}
|
|
2951
|
+
const testsModulesDir = join12(cwd, dir, "tests/modules");
|
|
2952
|
+
const fastifyTestFile = join12(testsModulesDir, `${toKebab(config.name)}.test.ts`);
|
|
2953
|
+
if (existsSync11(testsModulesDir) && !existsSync11(fastifyTestFile)) {
|
|
2954
|
+
await writeFile5(fastifyTestFile, generateFastifyTest(config));
|
|
2955
|
+
generated.push(`${dir}/tests/modules/${toKebab(config.name)}.test.ts`);
|
|
2956
|
+
}
|
|
2823
2957
|
}
|
|
2824
2958
|
}
|
|
2825
2959
|
if (hasFrontend) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-projx",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.3",
|
|
4
4
|
"description": "Scaffold production-grade fullstack projects in seconds. FastAPI, Fastify, React, Flutter, Terraform — with auth, database, CI/CD, E2E tests, and Docker. One command, ready to deploy.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|