create-projx 1.6.0 → 1.6.2

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
@@ -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
+ ![projx demo](.github/demo.gif)
11
+
10
12
  ```bash
11
13
  npx create-projx my-app
12
14
  ```
@@ -175,27 +177,35 @@ Your custom files (controllers, pages, middleware) are never deleted. Files you
175
177
 
176
178
  ### Skip Files
177
179
 
178
- To skip component source files, add `skip` to `.projx-component`:
180
+ Common user-owned files are **default-skipped** automatically template updates won't touch them:
181
+
182
+ | Scope | Default skips |
183
+ |-------|---------------|
184
+ | Root (`.projx`) | `docker-compose.yml`, `docker-compose.dev.yml`, `README.md`, `.githooks/pre-commit`, `.github/workflows/ci.yml`, `setup.sh` |
185
+ | fastapi | `pyproject.toml` |
186
+ | fastify / frontend / e2e | `package.json` |
187
+ | mobile | `pubspec.yaml` |
188
+
189
+ Defaults are applied once on first `update` and saved to the `skip` array. To skip additional files, add them to `skip` in `.projx` (root-level) or `.projx-component` (per-component):
179
190
 
180
191
  ```json
192
+ // .projx — root skip
181
193
  {
182
- "components": ["fastapi"],
183
- "origin": "init",
184
- "skip": ["src/**", "tests/**"]
194
+ "version": "x.y.z",
195
+ "skip": ["docker-compose.yml", "README.md", "my-custom-config.yml"]
185
196
  }
186
197
  ```
187
198
 
188
- To skip root-level files (docker-compose, README), add `skip` to `.projx`:
189
-
190
199
  ```json
200
+ // fastapi/.projx-component — component skip
191
201
  {
192
- "version": "1.5.2",
193
- "components": ["fastapi", "frontend"],
194
- "skip": ["docker-compose.yml", "README.md"]
202
+ "component": "fastapi",
203
+ "origin": "init",
204
+ "skip": ["pyproject.toml", "src/custom_middleware.py"]
195
205
  }
196
206
  ```
197
207
 
198
- Skipped files are excluded from template updates.
208
+ To opt back in to updates for a skipped file, use `npx create-projx unpin <file>`.
199
209
 
200
210
  ## Options
201
211
 
@@ -274,14 +284,14 @@ When both `fastapi` and `fastify` exist, the entity generates in the **primary b
274
284
 
275
285
  Override with `--ai` (fastapi) or `--backend` (fastify).
276
286
 
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 |
287
+ | Component | Generated |
288
+ | ------------------------- | -------------------------------------------------------------------------------------------- |
289
+ | Primary backend (fastapi) | `src/entities/<name>/_model.py` + `tests/test_<name>_entity.py` — model + 11 CRUD/auth tests |
290
+ | Primary backend (fastify) | `src/modules/<name>/schemas.ts` + `index.ts` + Prisma model + `tests/modules/<name>.test.ts` |
291
+ | `frontend` | `src/types/<name>.ts` — TypeScript interface + Create/Update variants |
292
+ | `mobile` | `lib/entities/<name>/model.dart` — Dart class with fromJson/toJson/copyWith |
283
293
 
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.
294
+ **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.
285
295
 
286
296
  No migrations — run `alembic revision --autogenerate` or `prisma migrate dev` (via your package manager) when ready.
287
297
 
@@ -343,6 +353,8 @@ The core idea: define a data model, get everything else for free.
343
353
 
344
354
  **Backend** — Drop a model file. The registry auto-discovers it and generates CRUD routes, schemas, pagination, filtering, sorting, search, FK expansion, and OpenAPI docs.
345
355
 
356
+ **Field privacy** — Sensitive columns (`password_hash`, `secret`, `api_key`, `mfa_secret`, etc.) are automatically stripped from API responses and `/_meta` via a built-in baseline. Add project-specific hidden fields per entity (`__hidden_fields__` in FastAPI, `hiddenFields` in Fastify). Mark entire entities as `__private__` / `private: true` to hide them from the API entirely — no routes registered, not listed in `/_meta`. The `/_meta` endpoint requires authentication on both backends.
357
+
346
358
  **Frontend** — Fetches metadata from `GET /api/v1/_meta`, renders table + form UI automatically. Customize with overrides.
347
359
 
348
360
  **Mobile** — Same metadata endpoint, generates list/detail/form screens. Offline-first with local DB and sync queue.
@@ -361,8 +373,8 @@ The CLI lives in `cli/`. Templates are the root-level component directories (`fa
361
373
 
362
374
  ```bash
363
375
  cd cli
364
- npm test # run tests
365
- npm run build # build CLI
376
+ pnpm test # run tests
377
+ pnpm build # build CLI
366
378
  ```
367
379
 
368
380
  ## Try it now
@@ -10,8 +10,8 @@ import {
10
10
  matchesSkip,
11
11
  saveBaselineRef,
12
12
  writeTemplateToDir
13
- } from "./chunk-G74HYIE4.js";
14
- import "./chunk-FTHX7ILT.js";
13
+ } from "./chunk-D33FXCNT.js";
14
+ import "./chunk-LTIJPVRZ.js";
15
15
  export {
16
16
  BASELINE_REF,
17
17
  applyTemplate,
@@ -6,19 +6,20 @@ import {
6
6
  readComponentMarker,
7
7
  readProjxConfig,
8
8
  render,
9
+ renderEjsInDir,
9
10
  replaceInDir,
10
11
  replaceInFile,
11
12
  sharedTemplateDir,
12
13
  toSnake,
13
14
  upsertComponentMarker,
14
15
  writeProjxConfig
15
- } from "./chunk-FTHX7ILT.js";
16
+ } from "./chunk-LTIJPVRZ.js";
16
17
 
17
18
  // src/baseline.ts
18
19
  import { existsSync, writeFileSync, unlinkSync } from "fs";
19
- import { chmod, mkdir, writeFile, rm, readFile as readFile2 } from "fs/promises";
20
+ import { chmod, mkdir, writeFile, rm, readFile as readFile2, copyFile } from "fs/promises";
20
21
  import { execSync } from "child_process";
21
- import { join as join2 } from "path";
22
+ import { join as join2, dirname } from "path";
22
23
  import { tmpdir } from "os";
23
24
 
24
25
  // src/generators/index.ts
@@ -106,7 +107,7 @@ function buildDisplayNames(paths) {
106
107
  }
107
108
  var BASELINE_REF = "refs/projx/baseline";
108
109
  async function migrateComponentMarkers(cwd, components, componentPaths, applyDefaults) {
109
- const { readComponentMarker: readComponentMarker2, writeComponentMarker } = await import("./utils-OOY5OZDX.js");
110
+ const { readComponentMarker: readComponentMarker2, writeComponentMarker } = await import("./utils-VY5BBJBQ.js");
110
111
  for (const component of components) {
111
112
  const dir = componentPaths[component];
112
113
  const markerDir = join2(cwd, dir);
@@ -259,7 +260,12 @@ async function tryThreeWayMerge(cwd, templateDir, baselineRef, componentPaths) {
259
260
  if (file === ".projx") continue;
260
261
  if (file.endsWith("/.projx-component") || file === ".projx-component") continue;
261
262
  const oursPath = join2(cwd, file);
262
- if (!existsSync(oursPath)) continue;
263
+ if (!existsSync(oursPath)) {
264
+ await mkdir(dirname(oursPath), { recursive: true });
265
+ await copyFile(join2(templateDir, file), oursPath);
266
+ merged.push(file);
267
+ continue;
268
+ }
263
269
  const baseContent = lookupBaseContent(cwd, baselineRef, file, pathFallbacks);
264
270
  if (baseContent === null) continue;
265
271
  let theirsContent;
@@ -324,8 +330,10 @@ async function removeSkippedFiles(dir, skipPatterns, realDir) {
324
330
  const rel = full.slice(base.length + 1);
325
331
  if (entry.isDirectory()) {
326
332
  await walk(full, base);
327
- } else if (entry.name !== ".projx-component" && matchesSkip(rel, skipPatterns)) {
328
- if (realDir && !existsSync(join2(realDir, rel))) continue;
333
+ } else if (entry.name !== ".projx-component") {
334
+ const targetRel = rel.endsWith(".ejs") ? rel.slice(0, -".ejs".length) : rel;
335
+ if (!matchesSkip(targetRel, skipPatterns) && !matchesSkip(rel, skipPatterns)) continue;
336
+ if (realDir && !existsSync(join2(realDir, targetRel))) continue;
329
337
  await unlink(full);
330
338
  }
331
339
  }
@@ -358,6 +366,7 @@ async function writeTemplateToDir(dest, repoDir, components, componentPaths, var
358
366
  await cp(srcDir, outDir, { recursive: true, force: true });
359
367
  }
360
368
  await rm(tmpDir, { recursive: true, force: true });
369
+ await renderEjsInDir(outDir, vars);
361
370
  await upsertComponentMarker(join2(dest, targetDir), component, skipPatterns.length > 0 ? skipPatterns : void 0);
362
371
  }
363
372
  if (!vars.pathsUpper) {
@@ -19,13 +19,13 @@ 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" };
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" };
23
23
  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" };
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" };
25
25
  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" };
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" };
27
27
  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" };
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" };
29
29
  }
30
30
  }
31
31
  function detectPackageManager(cwd) {
@@ -369,6 +369,22 @@ function render(template, vars) {
369
369
  }
370
370
  return output.join("\n").replace(/\n{3,}/g, "\n\n");
371
371
  }
372
+ async function renderEjsInDir(dir, vars) {
373
+ if (!existsSync(dir)) return;
374
+ const entries = await readdir(dir, { withFileTypes: true });
375
+ for (const entry of entries) {
376
+ const full = join(dir, entry.name);
377
+ if (entry.isDirectory()) {
378
+ await renderEjsInDir(full, vars);
379
+ } else if (entry.name.endsWith(".ejs")) {
380
+ const content = await readFile(full, "utf-8");
381
+ const rendered = render(content, vars);
382
+ const out = full.slice(0, -".ejs".length);
383
+ await writeFile(out, rendered);
384
+ await rm(full);
385
+ }
386
+ }
387
+ }
372
388
  function detectProjectName(cwd, components, componentPaths) {
373
389
  for (const component of components) {
374
390
  const dir = componentPaths[component] ?? component;
@@ -420,5 +436,6 @@ export {
420
436
  discoverComponentPaths,
421
437
  discoverComponentsFromMarkers,
422
438
  render,
439
+ renderEjsInDir,
423
440
  detectProjectName
424
441
  };
package/dist/index.js CHANGED
@@ -9,7 +9,7 @@ import {
9
9
  matchesSkip,
10
10
  saveBaselineRef,
11
11
  writeTemplateToDir
12
- } from "./chunk-G74HYIE4.js";
12
+ } from "./chunk-D33FXCNT.js";
13
13
  import {
14
14
  COMPONENTS,
15
15
  COMPONENT_MARKER,
@@ -33,7 +33,7 @@ import {
33
33
  toTitle,
34
34
  writeComponentMarker,
35
35
  writeProjxConfig
36
- } from "./chunk-FTHX7ILT.js";
36
+ } from "./chunk-LTIJPVRZ.js";
37
37
 
38
38
  // src/index.ts
39
39
  import { existsSync as existsSync11 } from "fs";
@@ -371,7 +371,7 @@ function hasUncommittedChanges(cwd) {
371
371
  async function findPinnedFilesWithUpdates(cwd, repoDir, components, componentPaths, vars, version, componentSkips, rootSkip) {
372
372
  const { mkdir: mkdir5, rm: rm2, readFile: readFile7 } = await import("fs/promises");
373
373
  const { tmpdir: tmpdir2 } = await import("os");
374
- const { writeTemplateToDir: writeTemplateToDir2 } = await import("./baseline-72Z7TC2E.js");
374
+ const { writeTemplateToDir: writeTemplateToDir2 } = await import("./baseline-KTCFW2FK.js");
375
375
  const config = await readProjxConfig(cwd);
376
376
  const rootPinned = Array.isArray(config.skip) ? config.skip : [];
377
377
  const componentPinned = [];
@@ -23,6 +23,7 @@ import {
23
23
  readFileOrNull,
24
24
  readProjxConfig,
25
25
  render,
26
+ renderEjsInDir,
26
27
  replaceInDir,
27
28
  replaceInFile,
28
29
  sharedTemplateDir,
@@ -32,7 +33,7 @@ import {
32
33
  upsertComponentMarker,
33
34
  writeComponentMarker,
34
35
  writeProjxConfig
35
- } from "./chunk-FTHX7ILT.js";
36
+ } from "./chunk-LTIJPVRZ.js";
36
37
  export {
37
38
  COMPONENTS,
38
39
  COMPONENT_MARKER,
@@ -58,6 +59,7 @@ export {
58
59
  readFileOrNull,
59
60
  readProjxConfig,
60
61
  render,
62
+ renderEjsInDir,
61
63
  replaceInDir,
62
64
  replaceInFile,
63
65
  sharedTemplateDir,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-projx",
3
- "version": "1.6.0",
3
+ "version": "1.6.2",
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": {
@@ -10,13 +10,6 @@
10
10
  "dist",
11
11
  "src/templates"
12
12
  ],
13
- "scripts": {
14
- "build": "tsup src/index.ts --format esm --target node18 --clean",
15
- "dev": "tsup src/index.ts --format esm --target node18 --watch",
16
- "test": "vitest run",
17
- "test:watch": "vitest",
18
- "prepublishOnly": "npm run build"
19
- },
20
13
  "keywords": [
21
14
  "projx",
22
15
  "scaffold",
@@ -61,5 +54,11 @@
61
54
  "typescript": "^5",
62
55
  "typescript-eslint": "^8.58.0",
63
56
  "vitest": "^4.1.2"
57
+ },
58
+ "scripts": {
59
+ "build": "tsup src/index.ts --format esm --target node18 --clean",
60
+ "dev": "tsup src/index.ts --format esm --target node18 --watch",
61
+ "test": "vitest run",
62
+ "test:watch": "vitest"
64
63
  }
65
- }
64
+ }
@@ -24,7 +24,7 @@ Scaffolded with [Projx](https://github.com/ukanhaupa/projx).
24
24
  <% if (components.includes('infra')) { %>
25
25
  | **<%= paths.infra %>/** | Terraform, AWS (EKS, RDS, VPC, CodePipeline) |
26
26
  <% } %>
27
- | **Identity** | Keycloak (OIDC / JWT) |
27
+ | **Identity** | OIDC / JWT |
28
28
  | **Containers** | Docker, Docker Compose |
29
29
 
30
30
  ## Getting Started
@@ -61,10 +61,21 @@ jobs:
61
61
  <%= paths.infra %>:
62
62
  - '<%= paths.infra %>/**'
63
63
  <% } %>
64
+
65
+ secrets:
66
+ name: Secret scan
67
+ runs-on: ubuntu-latest
68
+ steps:
69
+ - uses: actions/checkout@v5
70
+ with:
71
+ fetch-depth: 0
72
+ - uses: gitleaks/gitleaks-action@v2
73
+ env:
74
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
64
75
  <% if (components.includes('fastapi')) { %>
65
76
 
66
77
  <%= paths.fastapi %>:
67
- name: <%= displayNames.fastapi %> (format + lint)
78
+ name: <%= displayNames.fastapi %> (format + lint + typecheck + test + audit)
68
79
  needs: changes
69
80
  if: needs.changes.outputs.<%= paths.fastapi %> == 'true'
70
81
  runs-on: ubuntu-latest
@@ -77,11 +88,14 @@ jobs:
77
88
  - run: uv sync --group dev
78
89
  - run: uv run ruff format --check src tests
79
90
  - run: uv run ruff check src tests
91
+ - run: uv run mypy
92
+ - run: uv run pytest
93
+ - run: uv run pip-audit
80
94
  <% } %>
81
95
  <% if (components.includes('fastify')) { %>
82
96
 
83
97
  <%= paths.fastify %>:
84
- name: <%= displayNames.fastify %> (format + lint + typecheck)
98
+ name: <%= displayNames.fastify %> (format + lint + typecheck + audit)
85
99
  needs: changes
86
100
  if: needs.changes.outputs.<%= paths.fastify %> == 'true'
87
101
  runs-on: ubuntu-latest
@@ -93,7 +107,7 @@ jobs:
93
107
  <% if (pm === 'pnpm') { %>
94
108
  - uses: pnpm/action-setup@v4
95
109
  with:
96
- version: 9
110
+ version: 10
97
111
  <% } %>
98
112
  <% if (pm === 'bun') { %>
99
113
  - uses: oven-sh/setup-bun@v2
@@ -110,11 +124,12 @@ jobs:
110
124
  - run: <%= pm.exec %> prettier --check .
111
125
  - run: <%= pm.exec %> eslint .
112
126
  - run: <%= pm.exec %> tsc --noEmit
127
+ - run: <%= pm.audit %>
113
128
  <% } %>
114
129
  <% if (components.includes('frontend')) { %>
115
130
 
116
131
  <%= paths.frontend %>:
117
- name: <%= displayNames.frontend %> (format + lint + typecheck)
132
+ name: <%= displayNames.frontend %> (format + lint + typecheck + audit)
118
133
  needs: changes
119
134
  if: needs.changes.outputs.<%= paths.frontend %> == 'true'
120
135
  runs-on: ubuntu-latest
@@ -126,7 +141,7 @@ jobs:
126
141
  <% if (pm === 'pnpm') { %>
127
142
  - uses: pnpm/action-setup@v4
128
143
  with:
129
- version: 9
144
+ version: 10
130
145
  <% } %>
131
146
  <% if (pm === 'bun') { %>
132
147
  - uses: oven-sh/setup-bun@v2
@@ -142,6 +157,7 @@ jobs:
142
157
  - run: <%= pm.exec %> prettier --check .
143
158
  - run: <%= pm.exec %> eslint 'src/**/*.{ts,tsx}'
144
159
  - run: <%= pm.exec %> tsc --noEmit
160
+ - run: <%= pm.audit %>
145
161
  <% } %>
146
162
  <% if (components.includes('mobile')) { %>
147
163
 
@@ -166,7 +182,7 @@ jobs:
166
182
  <% if (components.includes('e2e')) { %>
167
183
 
168
184
  <%= paths.e2e %>:
169
- name: <%= displayNames.e2e %> (format + lint + typecheck)
185
+ name: <%= displayNames.e2e %> (format + lint + typecheck + audit)
170
186
  needs: changes
171
187
  if: needs.changes.outputs.<%= paths.e2e %> == 'true'
172
188
  runs-on: ubuntu-latest
@@ -178,7 +194,7 @@ jobs:
178
194
  <% if (pm === 'pnpm') { %>
179
195
  - uses: pnpm/action-setup@v4
180
196
  with:
181
- version: 9
197
+ version: 10
182
198
  <% } %>
183
199
  <% if (pm === 'bun') { %>
184
200
  - uses: oven-sh/setup-bun@v2
@@ -194,6 +210,7 @@ jobs:
194
210
  - run: <%= pm.exec %> prettier --check .
195
211
  - run: <%= pm.exec %> eslint '**/*.ts'
196
212
  - run: <%= pm.exec %> tsc --noEmit
213
+ - run: <%= pm.audit %>
197
214
  <% } %>
198
215
  <% if (components.includes('infra')) { %>
199
216
 
@@ -31,10 +31,11 @@ STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACMR)
31
31
 
32
32
  <%= pathsUpper.fastapi %>_PY=$(echo "$STAGED_FILES" | grep '^<%= paths.fastapi %>/.*\.py$' || true)
33
33
  if [ -n "$<%= pathsUpper.fastapi %>_PY" ]; then
34
- echo "Formatting & linting <%= paths.fastapi %>..."
34
+ echo "Running quality gates for <%= paths.fastapi %>..."
35
35
  cd <%= paths.fastapi %>
36
36
  echo "$<%= pathsUpper.fastapi %>_PY" | sed 's|^<%= paths.fastapi %>/||' | xargs uv run ruff format
37
37
  echo "$<%= pathsUpper.fastapi %>_PY" | sed 's|^<%= paths.fastapi %>/||' | xargs uv run ruff check --fix
38
+ uv run mypy
38
39
  cd ..
39
40
  echo "$<%= pathsUpper.fastapi %>_PY" | xargs git add
40
41
  fi