create-projx 1.6.5 → 1.7.0

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.
Files changed (79) hide show
  1. package/README.md +92 -19
  2. package/dist/{baseline-PZM4KJJW.js → baseline-FHOZNS4D.js} +2 -2
  3. package/dist/{chunk-6YRBHJ2V.js → chunk-HAT7D4G2.js} +25 -8
  4. package/dist/{chunk-XQ7FE4U3.js → chunk-IMZKHDIL.js} +161 -19
  5. package/dist/index.js +1499 -276
  6. package/dist/{utils-AVKSTHIF.js → utils-BZGSJ7XZ.js} +5 -1
  7. package/package.json +13 -7
  8. package/src/addons/orms/drizzle/express/src/app.ts +81 -0
  9. package/src/addons/orms/drizzle/express/src/modules/_base/auto-routes.ts +278 -0
  10. package/src/addons/orms/drizzle/express/src/modules/_base/index.ts +20 -0
  11. package/src/addons/orms/drizzle/express/src/server.ts +32 -0
  12. package/src/addons/orms/drizzle/express/tests/app.test.ts +24 -0
  13. package/src/addons/orms/drizzle/express/vitest.config.ts +20 -0
  14. package/src/addons/orms/drizzle/fastify/src/app.ts +90 -0
  15. package/src/addons/orms/drizzle/fastify/src/modules/_base/auto-routes.ts +268 -0
  16. package/src/addons/orms/drizzle/fastify/src/modules/_base/index.ts +20 -0
  17. package/src/addons/orms/drizzle/fastify/tests/modules/app.test.ts +20 -0
  18. package/src/addons/orms/drizzle/fastify/vitest.config.ts +31 -0
  19. package/src/addons/orms/drizzle/gen-entity/express-router.ts +21 -0
  20. package/src/addons/orms/drizzle/gen-entity/express-test.ts +61 -0
  21. package/src/addons/orms/drizzle/gen-entity/fastify-router.ts +19 -0
  22. package/src/addons/orms/drizzle/gen-entity/fastify-test.ts +87 -0
  23. package/src/addons/orms/drizzle/manifest.json +52 -0
  24. package/src/addons/orms/drizzle/shared/drizzle.config.ts +12 -0
  25. package/src/addons/orms/drizzle/shared/src/db/client.ts +17 -0
  26. package/src/addons/orms/drizzle/shared/src/db/schema.ts +14 -0
  27. package/src/addons/orms/drizzle/shared/src/modules/_base/query-engine.ts +115 -0
  28. package/src/addons/orms/drizzle/shared/src/modules/_base/registry.ts +15 -0
  29. package/src/addons/orms/sequelize/express/src/app.ts +82 -0
  30. package/src/addons/orms/sequelize/express/src/modules/_base/auto-routes.ts +226 -0
  31. package/src/addons/orms/sequelize/express/src/modules/_base/index.ts +20 -0
  32. package/src/addons/orms/sequelize/express/src/server.ts +32 -0
  33. package/src/addons/orms/sequelize/express/tests/app.test.ts +24 -0
  34. package/src/addons/orms/sequelize/express/vitest.config.ts +20 -0
  35. package/src/addons/orms/sequelize/fastify/src/app.ts +83 -0
  36. package/src/addons/orms/sequelize/fastify/src/modules/_base/auto-routes.ts +216 -0
  37. package/src/addons/orms/sequelize/fastify/src/modules/_base/index.ts +20 -0
  38. package/src/addons/orms/sequelize/fastify/tests/modules/app.test.ts +20 -0
  39. package/src/addons/orms/sequelize/fastify/vitest.config.ts +31 -0
  40. package/src/addons/orms/sequelize/gen-entity/express-router.ts +17 -0
  41. package/src/addons/orms/sequelize/gen-entity/express-test.ts +65 -0
  42. package/src/addons/orms/sequelize/gen-entity/fastify-router.ts +19 -0
  43. package/src/addons/orms/sequelize/gen-entity/fastify-test.ts +89 -0
  44. package/src/addons/orms/sequelize/gen-entity/model.ts +21 -0
  45. package/src/addons/orms/sequelize/manifest.json +53 -0
  46. package/src/addons/orms/sequelize/shared/scripts/db-sync.ts +14 -0
  47. package/src/addons/orms/sequelize/shared/src/db/client.ts +19 -0
  48. package/src/addons/orms/sequelize/shared/src/models/index.ts +9 -0
  49. package/src/addons/orms/sequelize/shared/src/modules/_base/query-engine.ts +101 -0
  50. package/src/addons/orms/sequelize/shared/src/modules/_base/registry.ts +15 -0
  51. package/src/addons/orms/typeorm/express/src/app.ts +82 -0
  52. package/src/addons/orms/typeorm/express/src/modules/_base/auto-routes.ts +249 -0
  53. package/src/addons/orms/typeorm/express/src/modules/_base/index.ts +19 -0
  54. package/src/addons/orms/typeorm/express/src/server.ts +43 -0
  55. package/src/addons/orms/typeorm/express/tests/app.test.ts +24 -0
  56. package/src/addons/orms/typeorm/express/vitest.config.ts +20 -0
  57. package/src/addons/orms/typeorm/fastify/src/app.ts +86 -0
  58. package/src/addons/orms/typeorm/fastify/src/modules/_base/auto-routes.ts +239 -0
  59. package/src/addons/orms/typeorm/fastify/src/modules/_base/index.ts +19 -0
  60. package/src/addons/orms/typeorm/fastify/tests/modules/app.test.ts +20 -0
  61. package/src/addons/orms/typeorm/fastify/vitest.config.ts +31 -0
  62. package/src/addons/orms/typeorm/gen-entity/entity.ts +21 -0
  63. package/src/addons/orms/typeorm/gen-entity/express-router.ts +17 -0
  64. package/src/addons/orms/typeorm/gen-entity/express-test.ts +66 -0
  65. package/src/addons/orms/typeorm/gen-entity/fastify-router.ts +19 -0
  66. package/src/addons/orms/typeorm/gen-entity/fastify-test.ts +89 -0
  67. package/src/addons/orms/typeorm/manifest.json +53 -0
  68. package/src/addons/orms/typeorm/shared/scripts/db-sync.ts +14 -0
  69. package/src/addons/orms/typeorm/shared/src/db/data-source.ts +21 -0
  70. package/src/addons/orms/typeorm/shared/src/entities/index.ts +8 -0
  71. package/src/addons/orms/typeorm/shared/src/modules/_base/query-engine.ts +94 -0
  72. package/src/addons/orms/typeorm/shared/src/modules/_base/registry.ts +15 -0
  73. package/src/addons/orms/typeorm/shared/tsconfig.json +16 -0
  74. package/src/templates/README.md.ejs +21 -4
  75. package/src/templates/ci.yml.ejs +167 -37
  76. package/src/templates/docker-compose.yml.ejs +72 -5
  77. package/src/templates/pre-commit.ejs +28 -4
  78. package/src/templates/setup.sh.ejs +75 -1
  79. package/src/templates/docker-compose.dev.yml.ejs +0 -189
package/README.md CHANGED
@@ -51,6 +51,12 @@ npx create-projx vision-api --components fastapi -y
51
51
  # Node API + React frontend
52
52
  npx create-projx saas --components fastify,frontend -y
53
53
 
54
+ # Minimal Express API + React frontend
55
+ npx create-projx api-app --components express,frontend -y
56
+
57
+ # Drizzle-backed Node API + React frontend
58
+ npx create-projx ledger --components express,frontend --orm drizzle -y
59
+
54
60
  # Mobile app with backend
55
61
  npx create-projx field-app --components fastapi,mobile -y
56
62
 
@@ -74,18 +80,20 @@ If this saves you even one hour, it's already paid for itself. (It's free.)
74
80
  - **No lock-in.** Projx generates files and walks away. Delete the `.projx` config and it's just a normal repo.
75
81
  - **Adopt incrementally.** Already have a project? `projx init` adds CI, hooks, and Docker without touching your code.
76
82
  - **Pick your package manager.** npm, pnpm, yarn, or bun. The choice propagates everywhere — scripts, Docker, CI, docs.
83
+ - **Pick your Node ORM.** Prisma is the auto-CRUD default. Drizzle, Sequelize, and TypeORM ship as first-class addons with identical runtime surface (auto-routes, pagination, filtering, search, lifecycle hooks).
77
84
  - **AI-agent friendly.** Ships with [SKILL.md](SKILL.md) so Claude, Cursor, and other agents call Projx instead of hand-writing broken scaffolds.
78
85
 
79
86
  ## What you get
80
87
 
81
- | Component | Stack | What it gives you |
82
- | ---------- | --------------------------- | ------------------------------------------------------------ |
83
- | `fastapi` | Python, SQLAlchemy, Alembic | Auto-entity CRUD, JWT auth, migrations, OpenAPI docs |
84
- | `fastify` | Node.js, Prisma, TypeBox | Auto-entity CRUD, JWT auth, typed schemas, OpenAPI docs |
85
- | `frontend` | React 19, TypeScript, Vite | Auto-entity UI from `/_meta`, design tokens, light/dark mode |
86
- | `mobile` | Flutter, Riverpod, GoRouter | Auto-entity screens, offline-first with Isar, biometric auth |
87
- | `e2e` | Playwright | Page object model, auth fixtures, accessibility scans |
88
- | `infra` | Terraform, AWS | EKS, RDS, VPC, ALB, CodePipeline, multi-environment |
88
+ | Component | Stack | What it gives you |
89
+ | ---------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
90
+ | `fastapi` | Python, SQLAlchemy, Alembic | Auto-entity CRUD, JWT auth, migrations, OpenAPI docs |
91
+ | `fastify` | Node.js, Prisma / Drizzle / Sequelize / TypeORM, TypeBox | Auto-entity CRUD, JWT auth, typed schemas, OpenAPI docs (auth currently Prisma-only) |
92
+ | `express` | Express 5, TypeScript, Prisma / Drizzle / Sequelize / TypeORM | Auto-entity CRUD, validation, security middleware, health checks |
93
+ | `frontend` | React 19, TypeScript, Vite | Auto-entity UI from `/_meta`, design tokens, light/dark mode |
94
+ | `mobile` | Flutter, Riverpod, GoRouter | Auto-entity screens, offline-first with Isar, biometric auth |
95
+ | `e2e` | Playwright | Page object model, auth fixtures, accessibility scans |
96
+ | `infra` | Terraform, AWS | EKS, RDS, VPC, ALB, CodePipeline, multi-environment |
89
97
 
90
98
  Plus, in every project: Docker Compose for dev + prod, GitHub Actions CI per component (path-filtered), pre-commit hooks, secret detection, VS Code settings, and 80% test coverage enforced.
91
99
 
@@ -110,6 +118,9 @@ npx create-projx my-app
110
118
  # Non-interactive — specify components
111
119
  npx create-projx my-app --components fastify,frontend,e2e
112
120
 
121
+ # Use Drizzle for Node backends instead of Prisma
122
+ npx create-projx my-app --components express,frontend --orm drizzle
123
+
113
124
  # Accept defaults (Fastify + Frontend + E2E)
114
125
  npx create-projx my-app -y
115
126
  ```
@@ -189,12 +200,12 @@ Your custom files (controllers, pages, middleware) are never deleted. Files you
189
200
 
190
201
  Common user-owned files are **default-skipped** automatically — template updates won't touch them:
191
202
 
192
- | Scope | Default skips |
193
- |-------|---------------|
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` |
195
- | fastapi | `pyproject.toml` |
196
- | fastify / frontend / e2e | `package.json` |
197
- | mobile | `pubspec.yaml` |
203
+ | Scope | Default skips |
204
+ | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
205
+ | Root (`.projx`) | `docker-compose.yml`, `README.md`, `.githooks/pre-commit`, `.github/workflows/ci.yml`, `scripts/setup.sh`, `scripts/setup-docker.sh`, `scripts/setup-ssl.sh` |
206
+ | fastapi | `pyproject.toml` |
207
+ | fastify / frontend / e2e | `package.json` |
208
+ | mobile | `pubspec.yaml` |
198
209
 
199
210
  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):
200
211
 
@@ -233,7 +244,7 @@ npx create-projx doctor [--fix]
233
244
  npx create-projx gen entity <name> [--ai | --backend]
234
245
  npx create-projx sync [--url <url>]
235
246
 
236
- --components <list> Comma-separated: fastapi,fastify,frontend,mobile,e2e,infra
247
+ --components <list> Comma-separated: fastapi,fastify,express,frontend,mobile,e2e,infra
237
248
  --name <dir> Custom directory for `add <type>` (multi-instance)
238
249
  --ai Target fastapi (AI/ML) for gen entity
239
250
  --backend Target fastify (API backend) for gen entity
@@ -286,8 +297,11 @@ Scaffold a new entity in your primary backend + typed models for frontend/mobile
286
297
  npx create-projx gen entity invoice # interactive
287
298
  npx create-projx gen entity invoice --fields "name:string,amount:number" # non-interactive
288
299
  npx create-projx gen entity embedding --ai --fields "name:string,vector:json" # target AI backend
300
+ npx create-projx gen entity invite --fields "email:string,code:string:unique:generated" # server-fills `code`
289
301
  ```
290
302
 
303
+ **Field modifiers** (Fastify/Express only, after the type): `unique` adds a Prisma `@unique` constraint; `generated` (alias: `server`, `server-generated`) marks the field as server-populated — it's omitted from the create-schema and a `beforeCreate` hook stub is emitted that fills it before persist (override the body of `generateXxx()` with your slug/code/UUID logic). Use both together for server-issued unique identifiers (invite codes, slugs, short URLs).
304
+
291
305
  When both `fastapi` and `fastify` exist, the entity generates in the **primary backend** only (not both). First run prompts you to choose and saves to `.projx`:
292
306
 
293
307
  ```json
@@ -307,6 +321,21 @@ Override with `--ai` (fastapi) or `--backend` (fastify).
307
321
 
308
322
  No migrations — run `alembic revision --autogenerate` or `prisma migrate dev` (via your package manager) when ready.
309
323
 
324
+ ### ORM choice (Node backends)
325
+
326
+ Pick an ORM at create-time with `--orm <provider>`. Default: **prisma**. Supported: `prisma`, `drizzle`, `sequelize`, `typeorm`.
327
+
328
+ | ORM | Schema home | `gen entity` produces | Sync schema to DB |
329
+ | ----------- | ------------------------ | ----------------------------------------------------------------------------------- | ---------------------------------------------- |
330
+ | `prisma` | `prisma/schema.prisma` | Prisma model + Fastify/Express module (`schemas.ts`, `index.ts`) + integration test | `prisma migrate dev` |
331
+ | `drizzle` | `src/db/schema.ts` | Typed `pgTable` + Fastify/Express router wired via `_base/` + CRUD test | `drizzle-kit push --force` |
332
+ | `sequelize` | `src/models/<name>.ts` | Sequelize `Model` class + aggregator entry + Fastify/Express router + CRUD test | `pnpm db:sync` (uses `sequelize.sync`) |
333
+ | `typeorm` | `src/entities/<name>.ts` | Decorated `@Entity` class + aggregator entry + Fastify/Express router + CRUD test | `pnpm db:sync` (uses `dataSource.synchronize`) |
334
+
335
+ All four ORMs scaffold equivalent runtime behavior: `_base/auto-routes.ts` wires POST/GET/PATCH/DELETE/bulk routes with pagination, equality filters, `ILIKE` search, and the lifecycle hook contract (`beforeCreate`, `afterCreate`, `beforeUpdate`, `afterUpdate`, `beforeDelete`).
336
+
337
+ ORM-specific scaffolding lives in [cli/src/addons/orms/](cli/src/addons/orms/) — each ORM is a self-contained folder with a `manifest.json` (deps, file removals, scripts), `shared/` files, per-framework overlays, and `gen-entity/` templates. Adding a new ORM means adding a new folder there; no CLI core changes.
338
+
310
339
  ### Sync Types
311
340
 
312
341
  Regenerate all frontend/mobile types from a running backend:
@@ -321,10 +350,10 @@ Fetches `/_meta` from your backend, generates typed interfaces for every entity.
321
350
  The generic `api.ts` client accepts type parameters:
322
351
 
323
352
  ```tsx
324
- import type { Invoice } from '../types/invoice';
353
+ import type { Invoice } from "../types/invoice";
325
354
 
326
- const { data } = await api.list<Invoice>('/invoices'); // data: Invoice[]
327
- const item = await api.get<Invoice>('/invoices', id); // item: Invoice
355
+ const { data } = await api.list<Invoice>("/invoices"); // data: Invoice[]
356
+ const item = await api.get<Invoice>("/invoices", id); // item: Invoice
328
357
  ```
329
358
 
330
359
  ## Rename Component Directories
@@ -349,7 +378,6 @@ my-app/
349
378
  ├── e2e/ # Playwright E2E tests
350
379
  │ └── .projx-component
351
380
  ├── docker-compose.yml # Production (backend + frontend + SSL)
352
- ├── docker-compose.dev.yml # Development (PostgreSQL + hot reload)
353
381
  ├── .github/workflows/ # CI per component (runs only on changes)
354
382
  ├── .githooks/pre-commit # Format + lint on commit
355
383
  ├── .vscode/ # Editor settings + recommended extensions
@@ -371,6 +399,51 @@ The core idea: define a data model, get everything else for free.
371
399
 
372
400
  **Mobile** — Same metadata endpoint, generates list/detail/form screens. Offline-first with local DB and sync queue.
373
401
 
402
+ ## Encrypted Service Config
403
+
404
+ Both backends ship with a `service_configs` table for storing third-party credentials (SMTP, OAuth, S3, etc.) encrypted at rest with AES-256-GCM. The `purpose` column is a free-form string — pick whatever taxonomy fits your project.
405
+
406
+ ```ts
407
+ // Fastify
408
+ import { setServiceConfig, getServiceConfig } from "./lib/service-config.js";
409
+ await setServiceConfig(prisma, "smtp", { host, port, user, password });
410
+ const cfg = await getServiceConfig<{ host: string }>(prisma, "smtp");
411
+ ```
412
+
413
+ ```python
414
+ # FastAPI
415
+ from src.entities.service_config._repository import ServiceConfigRepository
416
+ repo = ServiceConfigRepository(session)
417
+ await repo.set_config('smtp', {'host': ..., 'port': 587})
418
+ cfg = await repo.get_config('smtp')
419
+ ```
420
+
421
+ Encryption key resolves from `CRED_ENCRYPTION_KEY` (32-byte base64). If absent, derived from `JWT_SECRET` for development. Set an explicit key in production. Reads are cached in-memory for 10 minutes; `invalidate(purpose)` clears.
422
+
423
+ ## Rate Limiting
424
+
425
+ The Fastify backend ships with `@fastify/rate-limit` registered globally. Defaults: **200 requests per minute per user** (or per IP for unauthenticated requests). Tune via `RATE_LIMIT_MAX` and `RATE_LIMIT_WINDOW` in `.env`.
426
+
427
+ Override per route for sensitive endpoints:
428
+
429
+ ```ts
430
+ fastify.post(
431
+ "/auth/resend-verification",
432
+ {
433
+ config: {
434
+ rateLimit: {
435
+ max: 5,
436
+ timeWindow: "1 hour",
437
+ keyGenerator: (req) => (req.body?.email ?? req.ip).toLowerCase(),
438
+ },
439
+ },
440
+ },
441
+ handler,
442
+ );
443
+ ```
444
+
445
+ For the FastAPI service, edge rate limits are enforced by the frontend nginx (`auth_limit` and `api_limit` zones in [frontend/nginx.conf](frontend/nginx.conf)) — no application-level limiter is wired by default since the service is internal.
446
+
374
447
  ## Development
375
448
 
376
449
  Contributing to Projx itself:
@@ -8,8 +8,8 @@ import {
8
8
  matchesSkip,
9
9
  saveBaselineRef,
10
10
  writeTemplateToDir
11
- } from "./chunk-XQ7FE4U3.js";
12
- import "./chunk-6YRBHJ2V.js";
11
+ } from "./chunk-IMZKHDIL.js";
12
+ import "./chunk-HAT7D4G2.js";
13
13
  export {
14
14
  BASELINE_REF,
15
15
  applyTemplate,
@@ -4,6 +4,7 @@ import { existsSync, readFileSync } from "fs";
4
4
  import {
5
5
  chmod,
6
6
  cp,
7
+ mkdtemp,
7
8
  mkdir,
8
9
  readdir,
9
10
  readFile,
@@ -18,12 +19,19 @@ var REPO_URL = `https://github.com/${REPO}`;
18
19
  var COMPONENTS = [
19
20
  "fastapi",
20
21
  "fastify",
22
+ "express",
21
23
  "frontend",
22
24
  "mobile",
23
25
  "e2e",
24
26
  "infra"
25
27
  ];
26
28
  var PACKAGE_MANAGERS = ["npm", "pnpm", "yarn", "bun"];
29
+ var ORM_PROVIDERS = [
30
+ "prisma",
31
+ "drizzle",
32
+ "sequelize",
33
+ "typeorm"
34
+ ];
27
35
  function pmCommands(pm) {
28
36
  switch (pm) {
29
37
  case "npm":
@@ -88,7 +96,7 @@ function detectPackageManager(cwd) {
88
96
  return null;
89
97
  }
90
98
  function detectPackageManagerFromComponents(cwd, componentPaths) {
91
- const jsComponents = ["fastify", "frontend", "e2e"];
99
+ const jsComponents = ["fastify", "express", "frontend", "e2e"];
92
100
  for (const component of jsComponents) {
93
101
  const dir = componentPaths[component];
94
102
  if (!dir) continue;
@@ -99,6 +107,7 @@ function detectPackageManagerFromComponents(cwd, componentPaths) {
99
107
  }
100
108
  return detectPackageManager(cwd);
101
109
  }
110
+ var KNOWN_FEATURES = ["auth"];
102
111
  function toKebab(s) {
103
112
  return s.replace(/([a-z])([A-Z])/g, "$1-$2").replace(/[\s_]+/g, "-").toLowerCase();
104
113
  }
@@ -127,8 +136,7 @@ async function downloadRepo(localPath) {
127
136
  if (localPath) {
128
137
  return localPath;
129
138
  }
130
- const dest = join(tmpdir(), `projx-${Date.now()}`);
131
- await mkdir(dest, { recursive: true });
139
+ const dest = await mkdtemp(join(tmpdir(), "projx-"));
132
140
  if (hasCommand("git")) {
133
141
  execSync(`git clone --depth 1 ${REPO_URL}.git "${dest}/repo"`, {
134
142
  stdio: "pipe"
@@ -213,7 +221,12 @@ async function copyStaticFiles(repoDir, dest) {
213
221
  await cp(extensionsJson, join(dest, ".vscode/extensions.json"));
214
222
  manifest.push(".vscode/extensions.json");
215
223
  }
216
- const staticScripts = ["setup-docker.sh", "setup-ssl.sh"];
224
+ const staticScripts = [
225
+ "ci-local.sh",
226
+ "setup-docker.sh",
227
+ "setup-ssl.sh",
228
+ "style-check.py"
229
+ ];
217
230
  const scriptsSrc = join(tpl, "scripts");
218
231
  if (existsSync(scriptsSrc)) {
219
232
  await mkdir(join(dest, "scripts"), { recursive: true });
@@ -326,10 +339,10 @@ async function writeProjxConfig(cwd, data) {
326
339
  }
327
340
  var DEFAULT_ROOT_SKIP_PATTERNS = [
328
341
  "docker-compose.yml",
329
- "docker-compose.dev.yml",
330
342
  "README.md",
331
343
  ".githooks/pre-commit",
332
344
  ".github/workflows/ci.yml",
345
+ "scripts/ci-local.sh",
333
346
  "scripts/setup.sh",
334
347
  "scripts/setup-docker.sh",
335
348
  "scripts/setup-ssl.sh"
@@ -337,6 +350,7 @@ var DEFAULT_ROOT_SKIP_PATTERNS = [
337
350
  var DEFAULT_COMPONENT_SKIP_PATTERNS = {
338
351
  fastapi: ["pyproject.toml"],
339
352
  fastify: ["package.json"],
353
+ express: ["package.json"],
340
354
  frontend: ["package.json"],
341
355
  e2e: ["package.json"],
342
356
  mobile: ["pubspec.yaml"]
@@ -381,10 +395,11 @@ function evalExpr(expr, vars) {
381
395
  const components = vars.components;
382
396
  const projectName = vars.projectName;
383
397
  const pmName = vars.pm?.name ?? "npm";
384
- const argNames = ["components", "projectName", "pm"];
385
- const argValues = [components, projectName, pmName];
398
+ const orm = vars.orm ?? "prisma";
399
+ const argNames = ["components", "projectName", "pm", "orm"];
400
+ const argValues = [components, projectName, pmName, orm];
386
401
  for (const [k, v] of Object.entries(vars)) {
387
- if (k === "components" || k === "projectName" || k === "pm") continue;
402
+ if (["components", "projectName", "pm", "orm"].includes(k)) continue;
388
403
  if (!/^[a-zA-Z_$][\w$]*$/.test(k)) continue;
389
404
  argNames.push(k);
390
405
  argValues.push(v);
@@ -521,9 +536,11 @@ export {
521
536
  REPO_URL,
522
537
  COMPONENTS,
523
538
  PACKAGE_MANAGERS,
539
+ ORM_PROVIDERS,
524
540
  pmCommands,
525
541
  detectPackageManager,
526
542
  detectPackageManagerFromComponents,
543
+ KNOWN_FEATURES,
527
544
  toKebab,
528
545
  toSnake,
529
546
  toTitle,
@@ -13,12 +13,14 @@ import {
13
13
  toSnake,
14
14
  upsertComponentMarker,
15
15
  writeProjxConfig
16
- } from "./chunk-6YRBHJ2V.js";
16
+ } from "./chunk-HAT7D4G2.js";
17
17
 
18
18
  // src/baseline.ts
19
19
  import { existsSync, writeFileSync, unlinkSync } from "fs";
20
20
  import {
21
21
  chmod,
22
+ cp,
23
+ mkdtemp,
22
24
  mkdir,
23
25
  writeFile,
24
26
  rm,
@@ -27,6 +29,7 @@ import {
27
29
  } from "fs/promises";
28
30
  import { execSync } from "child_process";
29
31
  import { join as join2, dirname } from "path";
32
+ import { fileURLToPath } from "url";
30
33
  import { tmpdir } from "os";
31
34
 
32
35
  // src/generators/index.ts
@@ -38,6 +41,7 @@ function shellSafeUpper(s) {
38
41
  var CANONICAL_DISPLAY = {
39
42
  fastapi: "FastAPI",
40
43
  fastify: "Fastify",
44
+ express: "Express",
41
45
  frontend: "Frontend",
42
46
  mobile: "Flutter",
43
47
  e2e: "E2E",
@@ -59,6 +63,7 @@ function withInstances(vars) {
59
63
  instances: enriched,
60
64
  fastapiInstances: byType("fastapi"),
61
65
  fastifyInstances: byType("fastify"),
66
+ expressInstances: byType("express"),
62
67
  frontendInstances: byType("frontend"),
63
68
  mobileInstances: byType("mobile"),
64
69
  e2eInstances: byType("e2e"),
@@ -72,9 +77,6 @@ async function renderShared(filename, vars) {
72
77
  async function generateDockerCompose(vars) {
73
78
  return renderShared("docker-compose.yml.ejs", withInstances(vars));
74
79
  }
75
- async function generateDockerComposeDev(vars) {
76
- return renderShared("docker-compose.dev.yml.ejs", withInstances(vars));
77
- }
78
80
  async function generatePreCommit(vars) {
79
81
  return renderShared("pre-commit.ejs", withInstances(vars));
80
82
  }
@@ -110,9 +112,7 @@ function generateVscodeSettings(vars) {
110
112
  settings["editor.formatOnSave"] = true;
111
113
  settings["editor.codeActionsOnSave"] = { "source.fixAll.eslint": "explicit" };
112
114
  settings["eslint.useFlatConfig"] = true;
113
- const prettierComponent = ["frontend", "fastify", "e2e"].find(
114
- (c) => vars.components.includes(c)
115
- );
115
+ const prettierComponent = ["frontend", "fastify", "express", "e2e"].find((c) => vars.components.includes(c));
116
116
  if (prettierComponent) {
117
117
  settings["prettier.configPath"] = `${vars.paths[prettierComponent]}/.prettierrc`;
118
118
  }
@@ -127,7 +127,7 @@ function generateVscodeSettings(vars) {
127
127
  // src/baseline.ts
128
128
  var BASELINE_REF = "refs/projx/baseline";
129
129
  async function migrateComponentMarkers(cwd, components, componentPaths, applyDefaults) {
130
- const { readComponentMarker: readComponentMarker2, writeComponentMarker } = await import("./utils-AVKSTHIF.js");
130
+ const { readComponentMarker: readComponentMarker2, writeComponentMarker } = await import("./utils-BZGSJ7XZ.js");
131
131
  for (const component of components) {
132
132
  const dir = componentPaths[component];
133
133
  const markerDir = join2(cwd, dir);
@@ -155,6 +155,9 @@ async function writeManagedProjx(cwd, version, vars, applyDefaults) {
155
155
  if (pmObj?.name && !merged.packageManager) {
156
156
  merged.packageManager = pmObj.name;
157
157
  }
158
+ if (typeof vars.orm === "string" && !merged.orm) {
159
+ merged.orm = vars.orm;
160
+ }
158
161
  if (applyDefaults && !merged.defaultsApplied) {
159
162
  const userSkip = Array.isArray(merged.skip) ? merged.skip : [];
160
163
  merged.skip = [.../* @__PURE__ */ new Set([...userSkip, ...DEFAULT_ROOT_SKIP_PATTERNS])];
@@ -413,7 +416,7 @@ async function writeTemplateToDir(dest, repoDir, components, componentPaths, var
413
416
  nameSnake
414
417
  });
415
418
  }
416
- const hasBackend = components.includes("fastapi") || components.includes("fastify");
419
+ const hasBackend = components.includes("fastapi") || components.includes("fastify") || components.includes("express");
417
420
  const userSkip = rootSkip ?? [];
418
421
  const defaultRootSkip = applyDefaults ? DEFAULT_ROOT_SKIP_PATTERNS : [];
419
422
  const effectiveSkip = [.../* @__PURE__ */ new Set([...userSkip, ...defaultRootSkip])];
@@ -427,11 +430,6 @@ async function writeTemplateToDir(dest, repoDir, components, componentPaths, var
427
430
  join2(dest, "docker-compose.yml"),
428
431
  await generateDockerCompose(vars)
429
432
  );
430
- if (shouldWrite("docker-compose.dev.yml"))
431
- await writeFile(
432
- join2(dest, "docker-compose.dev.yml"),
433
- await generateDockerComposeDev(vars)
434
- );
435
433
  }
436
434
  if (shouldWrite("README.md"))
437
435
  await writeFile(join2(dest, "README.md"), await generateReadme(vars));
@@ -497,9 +495,9 @@ async function writeOneInstance(inst, opts) {
497
495
  }
498
496
  const outDir = join2(dest, targetDir);
499
497
  await mkdir(outDir, { recursive: true });
500
- const { cp } = await import("fs/promises");
498
+ const { cp: cp2 } = await import("fs/promises");
501
499
  if (existsSync(srcDir)) {
502
- await cp(srcDir, outDir, { recursive: true, force: true });
500
+ await cp2(srcDir, outDir, { recursive: true, force: true });
503
501
  }
504
502
  await rm(tmpDir, { recursive: true, force: true });
505
503
  const instancePaths = {
@@ -507,6 +505,7 @@ async function writeOneInstance(inst, opts) {
507
505
  [type]: targetDir
508
506
  };
509
507
  await renderEjsInDir(outDir, { ...vars, paths: instancePaths });
508
+ await applyOrmProviderToInstance(outDir, type, vars);
510
509
  await upsertComponentMarker(
511
510
  join2(dest, targetDir),
512
511
  type,
@@ -520,6 +519,143 @@ async function writeOneInstance(inst, opts) {
520
519
  vars.nameOverrides
521
520
  );
522
521
  }
522
+ async function applyOrmProviderToInstance(dir, component, vars) {
523
+ const orm = typeof vars.orm === "string" ? vars.orm : void 0;
524
+ if (!orm || orm === "prisma") return;
525
+ if (component !== "fastify" && component !== "express") return;
526
+ await applyOrmAddon(orm, component, dir, vars);
527
+ }
528
+ function sharedAddonDir() {
529
+ const thisFile = fileURLToPath(import.meta.url);
530
+ return join2(thisFile, "../../src/addons");
531
+ }
532
+ async function loadOrmManifest(orm) {
533
+ const path = join2(sharedAddonDir(), "orms", orm, "manifest.json");
534
+ if (!existsSync(path)) {
535
+ throw new Error(
536
+ `ORM "${orm}" is not yet supported. No manifest found at ${path}.`
537
+ );
538
+ }
539
+ return JSON.parse(await readFile2(path, "utf-8"));
540
+ }
541
+ function applyPackageOverrides(pkg, overrides) {
542
+ if (overrides.descriptionReplace && typeof pkg.description === "string") {
543
+ pkg.description = pkg.description.replaceAll(
544
+ overrides.descriptionReplace.from,
545
+ overrides.descriptionReplace.to
546
+ );
547
+ }
548
+ const scripts = pkg.scripts ?? {};
549
+ for (const prefix of overrides.removeScriptPrefixes ?? []) {
550
+ for (const key of Object.keys(scripts)) {
551
+ if (key.startsWith(prefix)) delete scripts[key];
552
+ }
553
+ }
554
+ Object.assign(scripts, overrides.addScripts ?? {});
555
+ pkg.scripts = scripts;
556
+ const dependencies = pkg.dependencies ?? {};
557
+ for (const dep of overrides.removeDependencies ?? []) {
558
+ delete dependencies[dep];
559
+ }
560
+ Object.assign(dependencies, overrides.addDependencies ?? {});
561
+ pkg.dependencies = dependencies;
562
+ const devDependencies = pkg.devDependencies ?? {};
563
+ for (const dep of overrides.removeDevDependencies ?? []) {
564
+ delete devDependencies[dep];
565
+ }
566
+ Object.assign(devDependencies, overrides.addDevDependencies ?? {});
567
+ pkg.devDependencies = devDependencies;
568
+ }
569
+ async function applyOrmAddon(orm, framework, dir, vars) {
570
+ const manifest = await loadOrmManifest(orm);
571
+ if (!manifest.frameworks.includes(framework)) {
572
+ throw new Error(
573
+ `ORM "${orm}" does not support framework "${framework}". Supported: ${manifest.frameworks.join(", ")}`
574
+ );
575
+ }
576
+ for (const relPath of manifest.removeFromBase) {
577
+ await rm(join2(dir, relPath), { recursive: true, force: true });
578
+ }
579
+ const pkgPath = join2(dir, "package.json");
580
+ const pkg = await readJsonObject(pkgPath);
581
+ applyPackageOverrides(pkg, manifest.packageOverrides);
582
+ await writeJsonObject(pkgPath, pkg);
583
+ const addonRoot = join2(sharedAddonDir(), "orms", orm);
584
+ const sharedSrc = join2(addonRoot, "shared");
585
+ const frameworkSrc = join2(addonRoot, framework);
586
+ if (existsSync(sharedSrc)) {
587
+ await cp(sharedSrc, dir, { recursive: true, force: true });
588
+ }
589
+ if (existsSync(frameworkSrc)) {
590
+ await cp(frameworkSrc, dir, { recursive: true, force: true });
591
+ }
592
+ await writeFile(
593
+ join2(dir, "Dockerfile"),
594
+ ormNodeDockerfileSource(manifest, vars)
595
+ );
596
+ }
597
+ async function readJsonObject(path) {
598
+ return JSON.parse(await readFile2(path, "utf-8"));
599
+ }
600
+ async function writeJsonObject(path, data) {
601
+ await writeFile(path, JSON.stringify(data, null, 2) + "\n");
602
+ }
603
+ function ormNodeDockerfileSource(manifest, vars) {
604
+ const pm = vars.pm;
605
+ const pmName = pm.name ?? "npm";
606
+ const lockfile = pm.lockfile ?? "package-lock.json";
607
+ const install = pm.ci ?? "npm ci";
608
+ const exec = pm.exec ?? "npx";
609
+ const run = pm.run ?? "npm run";
610
+ const dockerCfg = manifest.dockerfile ?? {};
611
+ const extraConfigCopy = (dockerCfg.extraConfigFiles ?? []).join(" ");
612
+ const migrateCmd = dockerCfg.migrateCommand ?? "";
613
+ const setup = pmName === "pnpm" ? `ENV PNPM_HOME="/pnpm"
614
+ ENV PATH="$PNPM_HOME:$PATH"
615
+ RUN corepack enable && corepack prepare pnpm@10.33.0 --activate
616
+ ` : pmName === "yarn" ? "RUN corepack enable\n" : pmName === "bun" ? "RUN npm install -g bun\n" : "";
617
+ const buildCopy = extraConfigCopy ? `COPY package.json tsconfig.json ${extraConfigCopy} ./` : `COPY package.json tsconfig.json ./`;
618
+ const migrateStage = migrateCmd ? `FROM build AS migrate
619
+ CMD ["sh", "-c", "${exec} ${migrateCmd}"]
620
+
621
+ ` : "";
622
+ return `FROM node:22-bookworm-slim AS base
623
+
624
+ RUN apt-get update \\
625
+ && apt-get install -y --no-install-recommends ca-certificates \\
626
+ && rm -rf /var/lib/apt/lists/*
627
+
628
+ ${setup}WORKDIR /app
629
+
630
+ FROM base AS deps
631
+ COPY package.json ${lockfile} ./
632
+ RUN ${install}
633
+
634
+ FROM base AS build
635
+ ENV NODE_OPTIONS="--max-old-space-size=768"
636
+ COPY --from=deps /app/node_modules ./node_modules
637
+ ${buildCopy}
638
+ COPY src ./src
639
+ RUN ${run} build
640
+
641
+ ${migrateStage}FROM base AS runtime
642
+ ENV NODE_ENV=production
643
+ RUN npm install -g pm2@5.4.3
644
+ RUN chown -R node /app
645
+ USER node
646
+
647
+ COPY --from=build --chown=node:node /app/node_modules ./node_modules
648
+ COPY --from=build --chown=node:node /app/dist ./dist
649
+ COPY --chown=node:node package.json ecosystem.config.cjs* ./
650
+
651
+ EXPOSE 3000
652
+
653
+ HEALTHCHECK --interval=30s --timeout=10s --start-period=20s --retries=3 \\
654
+ CMD ["node", "-e", "require('http').get('http://localhost:3000/api/health', r => process.exit(r.statusCode === 200 ? 0 : 1)).on('error', () => process.exit(1))"]
655
+
656
+ CMD ["sh", "-c", "if [ -f ecosystem.config.cjs ]; then pm2-runtime ecosystem.config.cjs; else node dist/server.js; fi"]
657
+ `;
658
+ }
523
659
  async function substituteNamesForInstance(inst, dest, name, nameSnake, overrides) {
524
660
  const { type, path } = inst;
525
661
  const isCanonical = path === type;
@@ -537,6 +673,13 @@ async function substituteNamesForInstance(inst, dest, name, nameSnake, overrides
537
673
  "projx-fastify",
538
674
  target
539
675
  );
676
+ } else if (type === "express") {
677
+ const target = isCanonical ? overrides?.express ?? `${name}-express` : `${name}-${path}`;
678
+ await replaceInFile(
679
+ join2(dest, `${path}/package.json`),
680
+ "projx-express",
681
+ target
682
+ );
540
683
  } else if (type === "frontend") {
541
684
  const target = isCanonical ? overrides?.frontend ?? `${name}-frontend` : `${name}-${path}`;
542
685
  await replaceInFile(
@@ -573,7 +716,7 @@ async function detectPackageNameOverrides(cwd, components, componentPaths) {
573
716
  const name = await readTomlProjectName(file);
574
717
  if (name) overrides.fastapi = name;
575
718
  }
576
- for (const c of ["fastify", "frontend", "e2e"]) {
719
+ for (const c of ["fastify", "express", "frontend", "e2e"]) {
577
720
  if (!components.includes(c)) continue;
578
721
  const file = join2(cwd, componentPaths[c], "package.json");
579
722
  const name = await readJsonName(file);
@@ -715,8 +858,7 @@ async function applyTemplate(cwd, repoDir, components, componentPaths, vars, ver
715
858
  }
716
859
  const baselineRef = getBaselineRef(cwd);
717
860
  if (baselineRef) {
718
- const tmpTemplate = join2(tmpdir(), `projx-tpl-${Date.now()}`);
719
- await mkdir(tmpTemplate, { recursive: true });
861
+ const tmpTemplate = await mkdtemp(join2(tmpdir(), "projx-tpl-"));
720
862
  await writeTemplateToDir(
721
863
  tmpTemplate,
722
864
  repoDir,