create-projx 1.6.4 → 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.
- package/README.md +92 -19
- package/dist/{baseline-PZM4KJJW.js → baseline-FHOZNS4D.js} +2 -2
- package/dist/{chunk-6YRBHJ2V.js → chunk-HAT7D4G2.js} +25 -8
- package/dist/{chunk-XQ7FE4U3.js → chunk-IMZKHDIL.js} +161 -19
- package/dist/index.js +1499 -276
- package/dist/{utils-AVKSTHIF.js → utils-BZGSJ7XZ.js} +5 -1
- package/package.json +13 -7
- package/src/addons/orms/drizzle/express/src/app.ts +81 -0
- package/src/addons/orms/drizzle/express/src/modules/_base/auto-routes.ts +278 -0
- package/src/addons/orms/drizzle/express/src/modules/_base/index.ts +20 -0
- package/src/addons/orms/drizzle/express/src/server.ts +32 -0
- package/src/addons/orms/drizzle/express/tests/app.test.ts +24 -0
- package/src/addons/orms/drizzle/express/vitest.config.ts +20 -0
- package/src/addons/orms/drizzle/fastify/src/app.ts +90 -0
- package/src/addons/orms/drizzle/fastify/src/modules/_base/auto-routes.ts +268 -0
- package/src/addons/orms/drizzle/fastify/src/modules/_base/index.ts +20 -0
- package/src/addons/orms/drizzle/fastify/tests/modules/app.test.ts +20 -0
- package/src/addons/orms/drizzle/fastify/vitest.config.ts +31 -0
- package/src/addons/orms/drizzle/gen-entity/express-router.ts +21 -0
- package/src/addons/orms/drizzle/gen-entity/express-test.ts +61 -0
- package/src/addons/orms/drizzle/gen-entity/fastify-router.ts +19 -0
- package/src/addons/orms/drizzle/gen-entity/fastify-test.ts +87 -0
- package/src/addons/orms/drizzle/manifest.json +52 -0
- package/src/addons/orms/drizzle/shared/drizzle.config.ts +12 -0
- package/src/addons/orms/drizzle/shared/src/db/client.ts +17 -0
- package/src/addons/orms/drizzle/shared/src/db/schema.ts +14 -0
- package/src/addons/orms/drizzle/shared/src/modules/_base/query-engine.ts +115 -0
- package/src/addons/orms/drizzle/shared/src/modules/_base/registry.ts +15 -0
- package/src/addons/orms/sequelize/express/src/app.ts +82 -0
- package/src/addons/orms/sequelize/express/src/modules/_base/auto-routes.ts +226 -0
- package/src/addons/orms/sequelize/express/src/modules/_base/index.ts +20 -0
- package/src/addons/orms/sequelize/express/src/server.ts +32 -0
- package/src/addons/orms/sequelize/express/tests/app.test.ts +24 -0
- package/src/addons/orms/sequelize/express/vitest.config.ts +20 -0
- package/src/addons/orms/sequelize/fastify/src/app.ts +83 -0
- package/src/addons/orms/sequelize/fastify/src/modules/_base/auto-routes.ts +216 -0
- package/src/addons/orms/sequelize/fastify/src/modules/_base/index.ts +20 -0
- package/src/addons/orms/sequelize/fastify/tests/modules/app.test.ts +20 -0
- package/src/addons/orms/sequelize/fastify/vitest.config.ts +31 -0
- package/src/addons/orms/sequelize/gen-entity/express-router.ts +17 -0
- package/src/addons/orms/sequelize/gen-entity/express-test.ts +65 -0
- package/src/addons/orms/sequelize/gen-entity/fastify-router.ts +19 -0
- package/src/addons/orms/sequelize/gen-entity/fastify-test.ts +89 -0
- package/src/addons/orms/sequelize/gen-entity/model.ts +21 -0
- package/src/addons/orms/sequelize/manifest.json +53 -0
- package/src/addons/orms/sequelize/shared/scripts/db-sync.ts +14 -0
- package/src/addons/orms/sequelize/shared/src/db/client.ts +19 -0
- package/src/addons/orms/sequelize/shared/src/models/index.ts +9 -0
- package/src/addons/orms/sequelize/shared/src/modules/_base/query-engine.ts +101 -0
- package/src/addons/orms/sequelize/shared/src/modules/_base/registry.ts +15 -0
- package/src/addons/orms/typeorm/express/src/app.ts +82 -0
- package/src/addons/orms/typeorm/express/src/modules/_base/auto-routes.ts +249 -0
- package/src/addons/orms/typeorm/express/src/modules/_base/index.ts +19 -0
- package/src/addons/orms/typeorm/express/src/server.ts +43 -0
- package/src/addons/orms/typeorm/express/tests/app.test.ts +24 -0
- package/src/addons/orms/typeorm/express/vitest.config.ts +20 -0
- package/src/addons/orms/typeorm/fastify/src/app.ts +86 -0
- package/src/addons/orms/typeorm/fastify/src/modules/_base/auto-routes.ts +239 -0
- package/src/addons/orms/typeorm/fastify/src/modules/_base/index.ts +19 -0
- package/src/addons/orms/typeorm/fastify/tests/modules/app.test.ts +20 -0
- package/src/addons/orms/typeorm/fastify/vitest.config.ts +31 -0
- package/src/addons/orms/typeorm/gen-entity/entity.ts +21 -0
- package/src/addons/orms/typeorm/gen-entity/express-router.ts +17 -0
- package/src/addons/orms/typeorm/gen-entity/express-test.ts +66 -0
- package/src/addons/orms/typeorm/gen-entity/fastify-router.ts +19 -0
- package/src/addons/orms/typeorm/gen-entity/fastify-test.ts +89 -0
- package/src/addons/orms/typeorm/manifest.json +53 -0
- package/src/addons/orms/typeorm/shared/scripts/db-sync.ts +14 -0
- package/src/addons/orms/typeorm/shared/src/db/data-source.ts +21 -0
- package/src/addons/orms/typeorm/shared/src/entities/index.ts +8 -0
- package/src/addons/orms/typeorm/shared/src/modules/_base/query-engine.ts +94 -0
- package/src/addons/orms/typeorm/shared/src/modules/_base/registry.ts +15 -0
- package/src/addons/orms/typeorm/shared/tsconfig.json +16 -0
- package/src/templates/README.md.ejs +21 -4
- package/src/templates/ci.yml.ejs +167 -37
- package/src/templates/docker-compose.yml.ejs +72 -5
- package/src/templates/pre-commit.ejs +28 -4
- package/src/templates/setup.sh.ejs +95 -6
- 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
|
|
82
|
-
| ---------- |
|
|
83
|
-
| `fastapi` | Python, SQLAlchemy, Alembic
|
|
84
|
-
| `fastify` | Node.js, Prisma, TypeBox
|
|
85
|
-
| `
|
|
86
|
-
| `
|
|
87
|
-
| `
|
|
88
|
-
| `
|
|
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
|
|
193
|
-
|
|
194
|
-
| Root (`.projx`)
|
|
195
|
-
| fastapi
|
|
196
|
-
| fastify / frontend / e2e | `package.json`
|
|
197
|
-
| mobile
|
|
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
|
|
353
|
+
import type { Invoice } from "../types/invoice";
|
|
325
354
|
|
|
326
|
-
const { data } = await api.list<Invoice>(
|
|
327
|
-
const item = await api.get<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:
|
|
@@ -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(),
|
|
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 = [
|
|
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
|
|
385
|
-
const
|
|
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 (
|
|
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-
|
|
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-
|
|
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
|
|
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(),
|
|
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,
|