create-projx 1.4.2 → 1.5.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 +27 -7
- package/dist/index.js +171 -48
- package/package.json +1 -1
- package/src/templates/README.md.ejs +5 -5
- package/src/templates/ci.yml.ejs +46 -19
- package/src/templates/docker-compose.dev.yml.ejs +3 -3
- package/src/templates/docker-compose.yml.ejs +1 -1
- package/src/templates/pre-commit.ejs +9 -9
- package/src/templates/setup.sh.ejs +3 -3
package/README.md
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/create-projx)
|
|
4
4
|
[](https://github.com/ukanhaupa/projx/actions/workflows/ci.yml)
|
|
5
|
+
[](https://github.com/ukanhaupa/projx)
|
|
5
6
|
[](https://opensource.org/licenses/MIT)
|
|
6
7
|
|
|
7
8
|
Production-grade project scaffolder. Pick your stack, get a fully wired project with auth, database, CI/CD, and E2E tests — ready to deploy.
|
|
@@ -31,6 +32,16 @@ npx create-projx my-app --components fastify,frontend,e2e
|
|
|
31
32
|
npx create-projx my-app -y
|
|
32
33
|
```
|
|
33
34
|
|
|
35
|
+
## Package Manager Support
|
|
36
|
+
|
|
37
|
+
Projx supports **npm**, **pnpm**, **yarn**, and **bun**. During `create`, you're prompted to pick one. The choice is stored in `.projx` and used everywhere — setup.sh, Docker, CI, pre-commit hooks, and README.
|
|
38
|
+
|
|
39
|
+
```json
|
|
40
|
+
{ "packageManager": "pnpm" }
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
For `init`, the package manager is auto-detected from lockfiles (`pnpm-lock.yaml` → pnpm, `yarn.lock` → yarn, `bun.lockb` → bun). Falls back to a prompt if no lockfile is found.
|
|
44
|
+
|
|
34
45
|
## Components
|
|
35
46
|
|
|
36
47
|
| Component | Stack | What You Get |
|
|
@@ -111,7 +122,7 @@ To skip root-level files (docker-compose, README), add `skip` to `.projx`:
|
|
|
111
122
|
|
|
112
123
|
```json
|
|
113
124
|
{
|
|
114
|
-
"version": "1.
|
|
125
|
+
"version": "1.4.2",
|
|
115
126
|
"components": ["fastapi", "frontend"],
|
|
116
127
|
"skip": ["docker-compose.yml", "README.md"]
|
|
117
128
|
}
|
|
@@ -131,10 +142,12 @@ npx create-projx pin <patterns...>
|
|
|
131
142
|
npx create-projx unpin <patterns...>
|
|
132
143
|
npx create-projx pin --list
|
|
133
144
|
npx create-projx doctor [--fix]
|
|
134
|
-
npx create-projx gen entity <name>
|
|
145
|
+
npx create-projx gen entity <name> [--ai | --backend]
|
|
135
146
|
npx create-projx sync [--url <url>]
|
|
136
147
|
|
|
137
148
|
--components <list> Comma-separated: fastapi,fastify,frontend,mobile,e2e,infra
|
|
149
|
+
--ai Target fastapi (AI/ML) for gen entity
|
|
150
|
+
--backend Target fastify (API backend) for gen entity
|
|
138
151
|
--no-git Skip git init
|
|
139
152
|
--no-install Skip dependency installation
|
|
140
153
|
-y, --yes Accept defaults (fastify + frontend + e2e)
|
|
@@ -178,23 +191,30 @@ Checks: config validity, component markers, baseline ref, stale worktrees, skip
|
|
|
178
191
|
|
|
179
192
|
### Generate Entities
|
|
180
193
|
|
|
181
|
-
Scaffold a new entity
|
|
194
|
+
Scaffold a new entity in your primary backend + typed models for frontend/mobile:
|
|
182
195
|
|
|
183
196
|
```bash
|
|
184
197
|
npx create-projx gen entity invoice # interactive
|
|
185
198
|
npx create-projx gen entity invoice --fields "name:string,amount:number" # non-interactive
|
|
199
|
+
npx create-projx gen entity embedding --ai --fields "name:string,vector:json" # target AI backend
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
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`:
|
|
203
|
+
|
|
204
|
+
```json
|
|
205
|
+
{ "primaryBackend": "fastify" }
|
|
186
206
|
```
|
|
187
207
|
|
|
188
|
-
|
|
208
|
+
Override with `--ai` (fastapi) or `--backend` (fastify).
|
|
189
209
|
|
|
190
210
|
| Component | Generated |
|
|
191
211
|
| --------- | --------- |
|
|
192
|
-
|
|
|
193
|
-
|
|
|
212
|
+
| Primary backend (fastapi) | `src/entities/<name>/_model.py` — auto-discovered by registry |
|
|
213
|
+
| Primary backend (fastify) | `src/modules/<name>/schemas.ts` + `index.ts` + Prisma model + app.ts import |
|
|
194
214
|
| `frontend` | `src/types/<name>.ts` — TypeScript interface + Create/Update variants |
|
|
195
215
|
| `mobile` | `lib/entities/<name>/model.dart` — Dart class with fromJson/toJson/copyWith |
|
|
196
216
|
|
|
197
|
-
No migrations — run `alembic revision --autogenerate` or `
|
|
217
|
+
No migrations — run `alembic revision --autogenerate` or `prisma migrate dev` (via your package manager) when ready.
|
|
198
218
|
|
|
199
219
|
### Sync Types
|
|
200
220
|
|
package/dist/index.js
CHANGED
|
@@ -21,6 +21,26 @@ var COMPONENTS = [
|
|
|
21
21
|
"e2e",
|
|
22
22
|
"infra"
|
|
23
23
|
];
|
|
24
|
+
var PACKAGE_MANAGERS = ["npm", "pnpm", "yarn", "bun"];
|
|
25
|
+
function pmCommands(pm) {
|
|
26
|
+
switch (pm) {
|
|
27
|
+
case "npm":
|
|
28
|
+
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" };
|
|
29
|
+
case "pnpm":
|
|
30
|
+
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" };
|
|
31
|
+
case "yarn":
|
|
32
|
+
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" };
|
|
33
|
+
case "bun":
|
|
34
|
+
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" };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function detectPackageManager(cwd) {
|
|
38
|
+
if (existsSync(join(cwd, "bun.lockb"))) return "bun";
|
|
39
|
+
if (existsSync(join(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
40
|
+
if (existsSync(join(cwd, "yarn.lock"))) return "yarn";
|
|
41
|
+
if (existsSync(join(cwd, "package-lock.json"))) return "npm";
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
24
44
|
function toKebab(s) {
|
|
25
45
|
return s.replace(/([a-z])([A-Z])/g, "$1-$2").replace(/[\s_]+/g, "-").toLowerCase();
|
|
26
46
|
}
|
|
@@ -289,8 +309,9 @@ function render(template, vars) {
|
|
|
289
309
|
for (const line of lines) {
|
|
290
310
|
const ifMatch = line.match(/^<%\s*if\s*\((.+?)\)\s*\{?\s*%>$/);
|
|
291
311
|
if (ifMatch) {
|
|
292
|
-
const
|
|
293
|
-
const
|
|
312
|
+
const pmName = vars.pm?.name ?? "npm";
|
|
313
|
+
const fn = new Function("components", "projectName", "pm", `return ${ifMatch[1]}`);
|
|
314
|
+
const result = fn(components, projectName, pmName);
|
|
294
315
|
stack.push({ active: result, matched: result });
|
|
295
316
|
continue;
|
|
296
317
|
}
|
|
@@ -301,8 +322,9 @@ function render(template, vars) {
|
|
|
301
322
|
if (top.matched) {
|
|
302
323
|
top.active = false;
|
|
303
324
|
} else {
|
|
304
|
-
const
|
|
305
|
-
const
|
|
325
|
+
const pmN = vars.pm?.name ?? "npm";
|
|
326
|
+
const fn = new Function("components", "projectName", "pm", `return ${elseIfMatch[1]}`);
|
|
327
|
+
const result = fn(components, projectName, pmN);
|
|
306
328
|
top.active = result;
|
|
307
329
|
if (result) top.matched = true;
|
|
308
330
|
}
|
|
@@ -391,7 +413,18 @@ async function runPrompts(nameArg) {
|
|
|
391
413
|
if (components.length === 0) {
|
|
392
414
|
p.log.warn("No components selected. Creating an empty project.");
|
|
393
415
|
}
|
|
394
|
-
|
|
416
|
+
const hasJs = components.some((c) => ["fastify", "frontend", "e2e"].includes(c));
|
|
417
|
+
let packageManager = "npm";
|
|
418
|
+
if (hasJs) {
|
|
419
|
+
const pm = await p.select({
|
|
420
|
+
message: "Package manager",
|
|
421
|
+
options: PACKAGE_MANAGERS.map((pm2) => ({ value: pm2, label: pm2 })),
|
|
422
|
+
initialValue: "npm"
|
|
423
|
+
});
|
|
424
|
+
if (p.isCancel(pm)) process.exit(0);
|
|
425
|
+
packageManager = pm;
|
|
426
|
+
}
|
|
427
|
+
return { name, components, git: true, install: true, packageManager };
|
|
395
428
|
}
|
|
396
429
|
|
|
397
430
|
// src/scaffold.ts
|
|
@@ -686,6 +719,8 @@ async function writeTemplateToDir(dest, repoDir, components, componentPaths, var
|
|
|
686
719
|
components,
|
|
687
720
|
createdAt: (/* @__PURE__ */ new Date()).toISOString().split("T")[0]
|
|
688
721
|
};
|
|
722
|
+
const pmObj = vars.pm;
|
|
723
|
+
if (pmObj?.name) projxConfig.packageManager = pmObj.name;
|
|
689
724
|
await writeFile2(join3(dest, ".projx"), JSON.stringify(projxConfig, null, 2) + "\n");
|
|
690
725
|
}
|
|
691
726
|
async function substituteNames(dest, components, paths, name, nameSnake) {
|
|
@@ -774,6 +809,8 @@ async function applyTemplate(cwd, repoDir, components, componentPaths, vars, ver
|
|
|
774
809
|
components,
|
|
775
810
|
createdAt: (/* @__PURE__ */ new Date()).toISOString().split("T")[0]
|
|
776
811
|
};
|
|
812
|
+
const pmObj = vars.pm;
|
|
813
|
+
if (pmObj?.name) projxConfig.packageManager = pmObj.name;
|
|
777
814
|
await writeFile2(join3(cwd, ".projx"), JSON.stringify(projxConfig, null, 2) + "\n");
|
|
778
815
|
if (result.conflicted.length === 0) {
|
|
779
816
|
execSync2("git add -A", { cwd, stdio: "pipe" });
|
|
@@ -817,10 +854,11 @@ async function applyTemplate(cwd, repoDir, components, componentPaths, vars, ver
|
|
|
817
854
|
// src/scaffold.ts
|
|
818
855
|
async function scaffold(opts, dest, localRepo) {
|
|
819
856
|
const name = toKebab(opts.name);
|
|
857
|
+
const pm = opts.packageManager ?? "npm";
|
|
820
858
|
const paths = Object.fromEntries(
|
|
821
859
|
opts.components.map((c) => [c, c])
|
|
822
860
|
);
|
|
823
|
-
const vars = { projectName: name, components: opts.components, paths };
|
|
861
|
+
const vars = { projectName: name, components: opts.components, paths, pm: pmCommands(pm) };
|
|
824
862
|
const isLocal = !!localRepo;
|
|
825
863
|
await mkdir3(dest, { recursive: true });
|
|
826
864
|
const dlSpinner = p2.spinner();
|
|
@@ -844,7 +882,7 @@ async function scaffold(opts, dest, localRepo) {
|
|
|
844
882
|
await applyTemplate(dest, repoDir, opts.components, paths, vars, version);
|
|
845
883
|
spinner7.stop("Scaffold complete.");
|
|
846
884
|
if (opts.install) {
|
|
847
|
-
await installDeps(dest, opts.components);
|
|
885
|
+
await installDeps(dest, opts.components, pm);
|
|
848
886
|
}
|
|
849
887
|
copyEnvExamples(dest, opts.components);
|
|
850
888
|
if (opts.git) {
|
|
@@ -865,7 +903,9 @@ async function scaffold(opts, dest, localRepo) {
|
|
|
865
903
|
|
|
866
904
|
Like projx? Star it: https://github.com/ukanhaupa/projx`);
|
|
867
905
|
}
|
|
868
|
-
async function installDeps(dest, components) {
|
|
906
|
+
async function installDeps(dest, components, pm) {
|
|
907
|
+
const cmds = pmCommands(pm);
|
|
908
|
+
const pmBin = pm === "bun" ? "bun" : pm;
|
|
869
909
|
for (const component of components) {
|
|
870
910
|
const spinner7 = p2.spinner();
|
|
871
911
|
try {
|
|
@@ -880,25 +920,31 @@ async function installDeps(dest, components) {
|
|
|
880
920
|
}
|
|
881
921
|
break;
|
|
882
922
|
case "fastify":
|
|
883
|
-
if (hasCommand(
|
|
884
|
-
spinner7.start(
|
|
885
|
-
exec(
|
|
923
|
+
if (hasCommand(pmBin)) {
|
|
924
|
+
spinner7.start(`Installing Fastify dependencies (${cmds.install})`);
|
|
925
|
+
exec(cmds.install, join4(dest, "fastify"));
|
|
886
926
|
spinner7.stop("Fastify dependencies installed.");
|
|
887
927
|
} else {
|
|
888
|
-
|
|
889
|
-
exec("npm install", join4(dest, "fastify"));
|
|
890
|
-
spinner7.stop("Fastify dependencies installed.");
|
|
928
|
+
p2.log.warn(`${pm} not found \u2014 run 'cd fastify && ${cmds.install}' manually.`);
|
|
891
929
|
}
|
|
892
930
|
break;
|
|
893
931
|
case "frontend":
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
932
|
+
if (hasCommand(pmBin)) {
|
|
933
|
+
spinner7.start(`Installing Frontend dependencies (${cmds.install})`);
|
|
934
|
+
exec(cmds.install, join4(dest, "frontend"));
|
|
935
|
+
spinner7.stop("Frontend dependencies installed.");
|
|
936
|
+
} else {
|
|
937
|
+
p2.log.warn(`${pm} not found \u2014 run 'cd frontend && ${cmds.install}' manually.`);
|
|
938
|
+
}
|
|
897
939
|
break;
|
|
898
940
|
case "e2e":
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
941
|
+
if (hasCommand(pmBin)) {
|
|
942
|
+
spinner7.start(`Installing E2E dependencies (${cmds.install})`);
|
|
943
|
+
exec(cmds.install, join4(dest, "e2e"));
|
|
944
|
+
spinner7.stop("E2E dependencies installed.");
|
|
945
|
+
} else {
|
|
946
|
+
p2.log.warn(`${pm} not found \u2014 run 'cd e2e && ${cmds.install}' manually.`);
|
|
947
|
+
}
|
|
902
948
|
break;
|
|
903
949
|
case "mobile":
|
|
904
950
|
if (hasCommand("flutter")) {
|
|
@@ -993,7 +1039,9 @@ async function update(cwd, localRepo) {
|
|
|
993
1039
|
const pkg = JSON.parse(await readFile5(join5(repoDir, "cli/package.json"), "utf-8"));
|
|
994
1040
|
const version = pkg.version;
|
|
995
1041
|
const name = detectProjectName(cwd, config.components, componentPaths);
|
|
996
|
-
const
|
|
1042
|
+
const raw = existsSync4(configPath) ? JSON.parse(await readFile5(configPath, "utf-8")) : {};
|
|
1043
|
+
const pm = raw.packageManager ?? "npm";
|
|
1044
|
+
const vars = { projectName: name, components: config.components, paths: componentPaths, pm: pmCommands(pm) };
|
|
997
1045
|
const spinner7 = p3.spinner();
|
|
998
1046
|
spinner7.start("Applying template update");
|
|
999
1047
|
const rootSkip = config.skip ?? [];
|
|
@@ -1184,8 +1232,9 @@ async function add(cwd, newComponents, localRepo, skipInstall = false) {
|
|
|
1184
1232
|
const existingPaths = await discoverComponentPaths(cwd, existing);
|
|
1185
1233
|
const paths = { ...existingPaths };
|
|
1186
1234
|
for (const c of toAdd) paths[c] = c;
|
|
1235
|
+
const pm = config.packageManager ?? "npm";
|
|
1187
1236
|
const name = detectProjectName(cwd, existing, paths);
|
|
1188
|
-
const vars = { projectName: name, components: allComponents, paths };
|
|
1237
|
+
const vars = { projectName: name, components: allComponents, paths, pm: pmCommands(pm) };
|
|
1189
1238
|
const pkg = JSON.parse(await readFile6(join6(repoDir, "cli/package.json"), "utf-8"));
|
|
1190
1239
|
const version = pkg.version;
|
|
1191
1240
|
const spinner7 = p4.spinner();
|
|
@@ -1193,7 +1242,7 @@ async function add(cwd, newComponents, localRepo, skipInstall = false) {
|
|
|
1193
1242
|
await writeTemplateToDir(cwd, repoDir, allComponents, paths, vars, version, "scaffold");
|
|
1194
1243
|
spinner7.stop("Components added.");
|
|
1195
1244
|
if (!skipInstall) {
|
|
1196
|
-
await installDeps2(cwd, toAdd);
|
|
1245
|
+
await installDeps2(cwd, toAdd, pm);
|
|
1197
1246
|
}
|
|
1198
1247
|
for (const component of toAdd) {
|
|
1199
1248
|
const example = join6(cwd, component, ".env.example");
|
|
@@ -1212,7 +1261,9 @@ async function add(cwd, newComponents, localRepo, skipInstall = false) {
|
|
|
1212
1261
|
await cleanupRepo(repoDir, isLocal);
|
|
1213
1262
|
}
|
|
1214
1263
|
}
|
|
1215
|
-
async function installDeps2(dest, components) {
|
|
1264
|
+
async function installDeps2(dest, components, pm) {
|
|
1265
|
+
const cmds = pmCommands(pm);
|
|
1266
|
+
const pmBin = pm === "bun" ? "bun" : pm;
|
|
1216
1267
|
for (const component of components) {
|
|
1217
1268
|
const spinner7 = p4.spinner();
|
|
1218
1269
|
try {
|
|
@@ -1227,25 +1278,31 @@ async function installDeps2(dest, components) {
|
|
|
1227
1278
|
}
|
|
1228
1279
|
break;
|
|
1229
1280
|
case "fastify":
|
|
1230
|
-
if (hasCommand(
|
|
1231
|
-
spinner7.start(
|
|
1232
|
-
exec(
|
|
1281
|
+
if (hasCommand(pmBin)) {
|
|
1282
|
+
spinner7.start(`Installing Fastify dependencies (${cmds.install})`);
|
|
1283
|
+
exec(cmds.install, join6(dest, "fastify"));
|
|
1233
1284
|
spinner7.stop("Fastify dependencies installed.");
|
|
1234
1285
|
} else {
|
|
1235
|
-
|
|
1236
|
-
exec("npm install", join6(dest, "fastify"));
|
|
1237
|
-
spinner7.stop("Fastify dependencies installed.");
|
|
1286
|
+
p4.log.warn(`${pm} not found \u2014 run 'cd fastify && ${cmds.install}' manually.`);
|
|
1238
1287
|
}
|
|
1239
1288
|
break;
|
|
1240
1289
|
case "frontend":
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1290
|
+
if (hasCommand(pmBin)) {
|
|
1291
|
+
spinner7.start(`Installing Frontend dependencies (${cmds.install})`);
|
|
1292
|
+
exec(cmds.install, join6(dest, "frontend"));
|
|
1293
|
+
spinner7.stop("Frontend dependencies installed.");
|
|
1294
|
+
} else {
|
|
1295
|
+
p4.log.warn(`${pm} not found \u2014 run 'cd frontend && ${cmds.install}' manually.`);
|
|
1296
|
+
}
|
|
1244
1297
|
break;
|
|
1245
1298
|
case "e2e":
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1299
|
+
if (hasCommand(pmBin)) {
|
|
1300
|
+
spinner7.start(`Installing E2E dependencies (${cmds.install})`);
|
|
1301
|
+
exec(cmds.install, join6(dest, "e2e"));
|
|
1302
|
+
spinner7.stop("E2E dependencies installed.");
|
|
1303
|
+
} else {
|
|
1304
|
+
p4.log.warn(`${pm} not found \u2014 run 'cd e2e && ${cmds.install}' manually.`);
|
|
1305
|
+
}
|
|
1249
1306
|
break;
|
|
1250
1307
|
case "mobile":
|
|
1251
1308
|
if (hasCommand("flutter")) {
|
|
@@ -1392,8 +1449,25 @@ async function init(cwd, localRepo) {
|
|
|
1392
1449
|
const paths = Object.fromEntries(
|
|
1393
1450
|
confirmed.map((c) => [c.component, c.directory])
|
|
1394
1451
|
);
|
|
1452
|
+
const hasJs = components.some((c) => ["fastify", "frontend", "e2e"].includes(c));
|
|
1453
|
+
let pm = "npm";
|
|
1454
|
+
if (hasJs) {
|
|
1455
|
+
const detected2 = detectPackageManager(cwd);
|
|
1456
|
+
if (detected2) {
|
|
1457
|
+
pm = detected2;
|
|
1458
|
+
p5.log.info(`Detected package manager: ${pm}`);
|
|
1459
|
+
} else if (process.stdin.isTTY) {
|
|
1460
|
+
const choice = await p5.select({
|
|
1461
|
+
message: "Package manager",
|
|
1462
|
+
options: PACKAGE_MANAGERS.map((v) => ({ value: v, label: v })),
|
|
1463
|
+
initialValue: "npm"
|
|
1464
|
+
});
|
|
1465
|
+
if (p5.isCancel(choice)) process.exit(0);
|
|
1466
|
+
pm = choice;
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1395
1469
|
const projectName = toKebab(cwd.split("/").pop());
|
|
1396
|
-
const vars = { projectName, components, paths };
|
|
1470
|
+
const vars = { projectName, components, paths, pm: pmCommands(pm) };
|
|
1397
1471
|
const dlSpinner = p5.spinner();
|
|
1398
1472
|
dlSpinner.start(isLocal ? "Using local templates" : "Downloading latest templates");
|
|
1399
1473
|
const repoDir = await downloadRepo(localRepo).catch((err) => {
|
|
@@ -1992,7 +2066,7 @@ async function diff(cwd, localRepo) {
|
|
|
1992
2066
|
const version = pkg.version;
|
|
1993
2067
|
p8.log.info(`Current: v${config.version} \u2192 Template: v${version}`);
|
|
1994
2068
|
const name = detectProjectName(cwd, config.components, componentPaths);
|
|
1995
|
-
const vars = { projectName: name, components: config.components, paths: componentPaths };
|
|
2069
|
+
const vars = { projectName: name, components: config.components, paths: componentPaths, pm: pmCommands(raw.packageManager ?? "npm") };
|
|
1996
2070
|
const spinner7 = p8.spinner();
|
|
1997
2071
|
spinner7.start("Analyzing changes");
|
|
1998
2072
|
const tmpTemplate = join11(tmpdir3(), `projx-diff-${Date.now()}`);
|
|
@@ -2551,9 +2625,10 @@ function dartFromJson(fieldName, type, required) {
|
|
|
2551
2625
|
})();
|
|
2552
2626
|
return required ? `${key} as ${dartT}` : `${key} as ${dartT}?`;
|
|
2553
2627
|
}
|
|
2554
|
-
function dartToJson(fieldName, camelName, type) {
|
|
2628
|
+
function dartToJson(fieldName, camelName, type, required) {
|
|
2555
2629
|
const isDate = type === "date" || type === "datetime";
|
|
2556
|
-
if (isDate) return `'${fieldName}': ${camelName}
|
|
2630
|
+
if (isDate && required) return `'${fieldName}': ${camelName}.toIso8601String()`;
|
|
2631
|
+
if (isDate && !required) return `'${fieldName}': ${camelName}?.toIso8601String()`;
|
|
2557
2632
|
return `'${fieldName}': ${camelName}`;
|
|
2558
2633
|
}
|
|
2559
2634
|
function generateDartModel(config) {
|
|
@@ -2602,7 +2677,7 @@ function generateDartModel(config) {
|
|
|
2602
2677
|
lines.push(` Map<String, dynamic> toJson() {`);
|
|
2603
2678
|
lines.push(` return {`);
|
|
2604
2679
|
for (const f of allFields) {
|
|
2605
|
-
lines.push(` ${dartToJson(f.snake, f.camel, f.fieldType)},`);
|
|
2680
|
+
lines.push(` ${dartToJson(f.snake, f.camel, f.fieldType, f.required)},`);
|
|
2606
2681
|
}
|
|
2607
2682
|
lines.push(` };`);
|
|
2608
2683
|
lines.push(` }`);
|
|
@@ -2622,13 +2697,49 @@ function generateDartModel(config) {
|
|
|
2622
2697
|
lines.push("");
|
|
2623
2698
|
return lines.join("\n");
|
|
2624
2699
|
}
|
|
2625
|
-
async function
|
|
2700
|
+
async function resolvePrimaryBackend(cwd, hasFastapi, hasFastify, backendFlag) {
|
|
2701
|
+
if (backendFlag) return backendFlag;
|
|
2702
|
+
if (hasFastapi && !hasFastify) return "fastapi";
|
|
2703
|
+
if (hasFastify && !hasFastapi) return "fastify";
|
|
2704
|
+
const configPath = join12(cwd, ".projx");
|
|
2705
|
+
if (existsSync11(configPath)) {
|
|
2706
|
+
try {
|
|
2707
|
+
const data = JSON.parse(await readFile11(configPath, "utf-8"));
|
|
2708
|
+
if (data.primaryBackend === "fastapi" || data.primaryBackend === "fastify") {
|
|
2709
|
+
return data.primaryBackend;
|
|
2710
|
+
}
|
|
2711
|
+
} catch {
|
|
2712
|
+
}
|
|
2713
|
+
}
|
|
2714
|
+
if (!process.stdin.isTTY) return "fastify";
|
|
2715
|
+
const choice = await p9.select({
|
|
2716
|
+
message: "Both backends detected. Which is your primary?",
|
|
2717
|
+
options: [
|
|
2718
|
+
{ value: "fastify", label: "fastify (API backend)" },
|
|
2719
|
+
{ value: "fastapi", label: "fastapi (AI/ML engine)" }
|
|
2720
|
+
],
|
|
2721
|
+
initialValue: "fastify"
|
|
2722
|
+
});
|
|
2723
|
+
if (p9.isCancel(choice)) process.exit(0);
|
|
2724
|
+
try {
|
|
2725
|
+
const data = JSON.parse(await readFile11(configPath, "utf-8"));
|
|
2726
|
+
data.primaryBackend = choice;
|
|
2727
|
+
await writeFile5(configPath, JSON.stringify(data, null, 2) + "\n");
|
|
2728
|
+
p9.log.success(`Saved primaryBackend: ${choice} to .projx`);
|
|
2729
|
+
} catch {
|
|
2730
|
+
}
|
|
2731
|
+
return choice;
|
|
2732
|
+
}
|
|
2733
|
+
async function gen(cwd, entityName, fieldsFlag, backendFlag) {
|
|
2626
2734
|
p9.intro(`projx gen entity ${entityName}`);
|
|
2627
2735
|
const configPath = join12(cwd, ".projx");
|
|
2628
2736
|
if (!existsSync11(configPath)) {
|
|
2629
2737
|
p9.log.error("No .projx file found. Run 'npx create-projx init' first.");
|
|
2630
2738
|
process.exit(1);
|
|
2631
2739
|
}
|
|
2740
|
+
const projxData = JSON.parse(await readFile11(configPath, "utf-8"));
|
|
2741
|
+
const pmName = projxData.packageManager ?? "npm";
|
|
2742
|
+
const pm = pmCommands(pmName);
|
|
2632
2743
|
const { components: discovered, paths: componentPaths } = await discoverComponentsFromMarkers(cwd);
|
|
2633
2744
|
const hasFastapi = discovered.includes("fastapi");
|
|
2634
2745
|
const hasFastify = discovered.includes("fastify");
|
|
@@ -2638,6 +2749,9 @@ async function gen(cwd, entityName, fieldsFlag) {
|
|
|
2638
2749
|
p9.log.error("No backend component found. Need fastapi or fastify.");
|
|
2639
2750
|
process.exit(1);
|
|
2640
2751
|
}
|
|
2752
|
+
const targetBackend = await resolvePrimaryBackend(cwd, hasFastapi, hasFastify, backendFlag);
|
|
2753
|
+
const genFastapi = targetBackend === "fastapi" && hasFastapi;
|
|
2754
|
+
const genFastify = targetBackend === "fastify" && hasFastify;
|
|
2641
2755
|
let config;
|
|
2642
2756
|
if (fieldsFlag) {
|
|
2643
2757
|
const fields = parseFieldsFlag(fieldsFlag);
|
|
@@ -2658,7 +2772,7 @@ async function gen(cwd, entityName, fieldsFlag) {
|
|
|
2658
2772
|
config = await promptEntityConfig(entityName);
|
|
2659
2773
|
}
|
|
2660
2774
|
const generated = [];
|
|
2661
|
-
if (
|
|
2775
|
+
if (genFastapi) {
|
|
2662
2776
|
const dir = componentPaths.fastapi;
|
|
2663
2777
|
const entityDir = join12(cwd, dir, "src/entities", toSnake(config.name));
|
|
2664
2778
|
if (existsSync11(entityDir)) {
|
|
@@ -2669,7 +2783,7 @@ async function gen(cwd, entityName, fieldsFlag) {
|
|
|
2669
2783
|
generated.push(`${dir}/src/entities/${toSnake(config.name)}/_model.py`);
|
|
2670
2784
|
}
|
|
2671
2785
|
}
|
|
2672
|
-
if (
|
|
2786
|
+
if (genFastify) {
|
|
2673
2787
|
const dir = componentPaths.fastify;
|
|
2674
2788
|
const moduleDir = join12(cwd, dir, "src/modules", toKebab(config.name));
|
|
2675
2789
|
if (existsSync11(moduleDir)) {
|
|
@@ -2754,16 +2868,16 @@ async function gen(cwd, entityName, fieldsFlag) {
|
|
|
2754
2868
|
p9.log.info(` ${f}`);
|
|
2755
2869
|
}
|
|
2756
2870
|
const className = toPascal(config.name);
|
|
2757
|
-
if (
|
|
2871
|
+
if (genFastapi) {
|
|
2758
2872
|
p9.log.info("");
|
|
2759
2873
|
p9.log.info("FastAPI next steps:");
|
|
2760
2874
|
p9.log.info(` alembic revision --autogenerate -m "add ${config.tableName}"`);
|
|
2761
2875
|
p9.log.info(" alembic upgrade head");
|
|
2762
2876
|
}
|
|
2763
|
-
if (
|
|
2877
|
+
if (genFastify) {
|
|
2764
2878
|
p9.log.info("");
|
|
2765
2879
|
p9.log.info("Fastify next steps:");
|
|
2766
|
-
p9.log.info(`
|
|
2880
|
+
p9.log.info(` ${pm.prismaExec} migrate dev --name add_${toSnake(config.name)}`);
|
|
2767
2881
|
}
|
|
2768
2882
|
if (hasFrontend) {
|
|
2769
2883
|
p9.log.info("");
|
|
@@ -3146,6 +3260,14 @@ function parseArgs() {
|
|
|
3146
3260
|
flags.fix = true;
|
|
3147
3261
|
continue;
|
|
3148
3262
|
}
|
|
3263
|
+
if (arg === "--ai") {
|
|
3264
|
+
flags.ai = true;
|
|
3265
|
+
continue;
|
|
3266
|
+
}
|
|
3267
|
+
if (arg === "--backend") {
|
|
3268
|
+
flags.backend = true;
|
|
3269
|
+
continue;
|
|
3270
|
+
}
|
|
3149
3271
|
if (arg === "--url") {
|
|
3150
3272
|
const val = args[++i];
|
|
3151
3273
|
if (val) extraArgs.push(`--url=${val}`);
|
|
@@ -3266,7 +3388,8 @@ async function main() {
|
|
|
3266
3388
|
const entityName = extraArgs[1];
|
|
3267
3389
|
const fieldsArg = extraArgs.find((a) => a.startsWith("--fields="));
|
|
3268
3390
|
const fieldsFlag = fieldsArg ? fieldsArg.split("=").slice(1).join("=") : void 0;
|
|
3269
|
-
|
|
3391
|
+
const backendFlag = flags.ai ? "fastapi" : flags.backend ? "fastify" : void 0;
|
|
3392
|
+
await gen(process.cwd(), entityName, fieldsFlag, backendFlag);
|
|
3270
3393
|
return;
|
|
3271
3394
|
}
|
|
3272
3395
|
let opts;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-projx",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
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": {
|
|
@@ -48,7 +48,7 @@ API docs at `http://localhost:7860/docs`.
|
|
|
48
48
|
### Fastify
|
|
49
49
|
|
|
50
50
|
```bash
|
|
51
|
-
cd <%= paths.fastify %> && cp .env.example .env &&
|
|
51
|
+
cd <%= paths.fastify %> && cp .env.example .env && <%= pm.install %> && <%= pm.exec %> prisma migrate dev && <%= pm.run %> dev
|
|
52
52
|
```
|
|
53
53
|
|
|
54
54
|
API docs at `http://localhost:3000/docs`.
|
|
@@ -58,7 +58,7 @@ API docs at `http://localhost:3000/docs`.
|
|
|
58
58
|
### Frontend
|
|
59
59
|
|
|
60
60
|
```bash
|
|
61
|
-
cd <%= paths.frontend %> && cp .env.example .env &&
|
|
61
|
+
cd <%= paths.frontend %> && cp .env.example .env && <%= pm.install %> && <%= pm.run %> dev
|
|
62
62
|
```
|
|
63
63
|
<% } %>
|
|
64
64
|
<% if (components.includes('mobile')) { %>
|
|
@@ -77,16 +77,16 @@ cd <%= paths.mobile %> && cp .env.example .env && flutter pub get && flutter run
|
|
|
77
77
|
cd <%= paths.fastapi %> && uv run pytest
|
|
78
78
|
<% } %>
|
|
79
79
|
<% if (components.includes('fastify')) { %>
|
|
80
|
-
cd <%= paths.fastify %> &&
|
|
80
|
+
cd <%= paths.fastify %> && <%= pm.run %> test
|
|
81
81
|
<% } %>
|
|
82
82
|
<% if (components.includes('frontend')) { %>
|
|
83
|
-
cd <%= paths.frontend %> &&
|
|
83
|
+
cd <%= paths.frontend %> && <%= pm.exec %> vitest run
|
|
84
84
|
<% } %>
|
|
85
85
|
<% if (components.includes('mobile')) { %>
|
|
86
86
|
cd <%= paths.mobile %> && flutter test
|
|
87
87
|
<% } %>
|
|
88
88
|
<% if (components.includes('e2e')) { %>
|
|
89
|
-
cd <%= paths.e2e %> &&
|
|
89
|
+
cd <%= paths.e2e %> && <%= pm.exec %> playwright test
|
|
90
90
|
<% } %>
|
|
91
91
|
```
|
|
92
92
|
|
package/src/templates/ci.yml.ejs
CHANGED
|
@@ -90,19 +90,26 @@ jobs:
|
|
|
90
90
|
working-directory: <%= paths.fastify %>
|
|
91
91
|
steps:
|
|
92
92
|
- uses: actions/checkout@v5
|
|
93
|
+
<% if (pm === 'pnpm') { %>
|
|
93
94
|
- uses: pnpm/action-setup@v4
|
|
94
95
|
with:
|
|
95
96
|
version: 9
|
|
97
|
+
<% } %>
|
|
98
|
+
<% if (pm === 'bun') { %>
|
|
99
|
+
- uses: oven-sh/setup-bun@v2
|
|
100
|
+
<% } %>
|
|
101
|
+
<% if (pm !== 'bun') { %>
|
|
96
102
|
- uses: actions/setup-node@v5
|
|
97
103
|
with:
|
|
98
104
|
node-version: 20
|
|
99
|
-
cache:
|
|
100
|
-
cache-dependency-path: <%= paths.fastify
|
|
101
|
-
|
|
102
|
-
- run:
|
|
103
|
-
- run:
|
|
104
|
-
- run:
|
|
105
|
-
- run:
|
|
105
|
+
cache: <%= pm.name %>
|
|
106
|
+
cache-dependency-path: <%= paths.fastify %>/<%= pm.lockfile %>
|
|
107
|
+
<% } %>
|
|
108
|
+
- run: <%= pm.ci %>
|
|
109
|
+
- run: <%= pm.prismaExec %> generate
|
|
110
|
+
- run: <%= pm.exec %> prettier --check .
|
|
111
|
+
- run: <%= pm.exec %> eslint .
|
|
112
|
+
- run: <%= pm.exec %> tsc --noEmit
|
|
106
113
|
<% } %>
|
|
107
114
|
<% if (components.includes('frontend')) { %>
|
|
108
115
|
|
|
@@ -116,15 +123,25 @@ jobs:
|
|
|
116
123
|
working-directory: <%= paths.frontend %>
|
|
117
124
|
steps:
|
|
118
125
|
- uses: actions/checkout@v5
|
|
126
|
+
<% if (pm === 'pnpm') { %>
|
|
127
|
+
- uses: pnpm/action-setup@v4
|
|
128
|
+
with:
|
|
129
|
+
version: 9
|
|
130
|
+
<% } %>
|
|
131
|
+
<% if (pm === 'bun') { %>
|
|
132
|
+
- uses: oven-sh/setup-bun@v2
|
|
133
|
+
<% } %>
|
|
134
|
+
<% if (pm !== 'bun') { %>
|
|
119
135
|
- uses: actions/setup-node@v5
|
|
120
136
|
with:
|
|
121
137
|
node-version: 22
|
|
122
|
-
cache:
|
|
123
|
-
cache-dependency-path: <%= paths.frontend
|
|
124
|
-
|
|
125
|
-
- run:
|
|
126
|
-
- run:
|
|
127
|
-
- run:
|
|
138
|
+
cache: <%= pm.name %>
|
|
139
|
+
cache-dependency-path: <%= paths.frontend %>/<%= pm.lockfile %>
|
|
140
|
+
<% } %>
|
|
141
|
+
- run: <%= pm.ci %>
|
|
142
|
+
- run: <%= pm.exec %> prettier --check .
|
|
143
|
+
- run: <%= pm.exec %> eslint 'src/**/*.{ts,tsx}'
|
|
144
|
+
- run: <%= pm.exec %> tsc --noEmit
|
|
128
145
|
<% } %>
|
|
129
146
|
<% if (components.includes('mobile')) { %>
|
|
130
147
|
|
|
@@ -158,15 +175,25 @@ jobs:
|
|
|
158
175
|
working-directory: <%= paths.e2e %>
|
|
159
176
|
steps:
|
|
160
177
|
- uses: actions/checkout@v5
|
|
178
|
+
<% if (pm === 'pnpm') { %>
|
|
179
|
+
- uses: pnpm/action-setup@v4
|
|
180
|
+
with:
|
|
181
|
+
version: 9
|
|
182
|
+
<% } %>
|
|
183
|
+
<% if (pm === 'bun') { %>
|
|
184
|
+
- uses: oven-sh/setup-bun@v2
|
|
185
|
+
<% } %>
|
|
186
|
+
<% if (pm !== 'bun') { %>
|
|
161
187
|
- uses: actions/setup-node@v5
|
|
162
188
|
with:
|
|
163
189
|
node-version: 22
|
|
164
|
-
cache:
|
|
165
|
-
cache-dependency-path: <%= paths.e2e
|
|
166
|
-
|
|
167
|
-
- run:
|
|
168
|
-
- run:
|
|
169
|
-
- run:
|
|
190
|
+
cache: <%= pm.name %>
|
|
191
|
+
cache-dependency-path: <%= paths.e2e %>/<%= pm.lockfile %>
|
|
192
|
+
<% } %>
|
|
193
|
+
- run: <%= pm.ci %>
|
|
194
|
+
- run: <%= pm.exec %> prettier --check .
|
|
195
|
+
- run: <%= pm.exec %> eslint '**/*.ts'
|
|
196
|
+
- run: <%= pm.exec %> tsc --noEmit
|
|
170
197
|
<% } %>
|
|
171
198
|
<% if (components.includes('infra')) { %>
|
|
172
199
|
|
|
@@ -84,7 +84,7 @@ services:
|
|
|
84
84
|
<% if (components.includes('fastify')) { %>
|
|
85
85
|
<%= paths.fastify %>-migrate:
|
|
86
86
|
build: ./<%= paths.fastify %>
|
|
87
|
-
command: [
|
|
87
|
+
command: ["sh", "-c", "<%= pm.prismaExec %> migrate deploy"]
|
|
88
88
|
environment:
|
|
89
89
|
- DATABASE_URL=postgresql://dev:dev@db:5432/app
|
|
90
90
|
depends_on:
|
|
@@ -99,7 +99,7 @@ services:
|
|
|
99
99
|
- app-network
|
|
100
100
|
<%= paths.fastify %>:
|
|
101
101
|
build: ./<%= paths.fastify %>
|
|
102
|
-
command: [
|
|
102
|
+
command: ["sh", "-c", "<%= pm.runDev %>"]
|
|
103
103
|
ports:
|
|
104
104
|
- '3000:3000'
|
|
105
105
|
environment:
|
|
@@ -131,7 +131,7 @@ services:
|
|
|
131
131
|
frontend:
|
|
132
132
|
image: node:20-alpine
|
|
133
133
|
working_dir: /app
|
|
134
|
-
command: sh -c "
|
|
134
|
+
command: sh -c "<%= pm.install %> && <%= pm.run %> dev -- --host 0.0.0.0"
|
|
135
135
|
ports:
|
|
136
136
|
- '5173:5173'
|
|
137
137
|
environment:
|
|
@@ -35,7 +35,7 @@ services:
|
|
|
35
35
|
<% if (components.includes('fastify')) { %>
|
|
36
36
|
<%= paths.fastify %>-migrate:
|
|
37
37
|
build: ./<%= paths.fastify %>
|
|
38
|
-
command: ["
|
|
38
|
+
command: ["sh", "-c", "<%= pm.prismaExec %> migrate deploy"]
|
|
39
39
|
env_file:
|
|
40
40
|
- ./<%= paths.fastify %>/.env
|
|
41
41
|
networks:
|
|
@@ -46,10 +46,10 @@ FASTIFY_ALL=$(echo "$STAGED_FILES" | grep '^<%= paths.fastify %>/' || true)
|
|
|
46
46
|
if [ -n "$FASTIFY_ALL" ]; then
|
|
47
47
|
echo "Formatting <%= paths.fastify %>..."
|
|
48
48
|
cd <%= paths.fastify %>
|
|
49
|
-
echo "$FASTIFY_ALL" | sed 's|^<%= paths.fastify %>/||' | xargs
|
|
49
|
+
echo "$FASTIFY_ALL" | sed 's|^<%= paths.fastify %>/||' | xargs <%= pm.exec %> prettier --write --ignore-unknown
|
|
50
50
|
if [ -n "$FASTIFY_TS" ]; then
|
|
51
|
-
echo "$FASTIFY_TS" | sed 's|^<%= paths.fastify %>/||' | xargs
|
|
52
|
-
|
|
51
|
+
echo "$FASTIFY_TS" | sed 's|^<%= paths.fastify %>/||' | xargs <%= pm.exec %> eslint --fix
|
|
52
|
+
<%= pm.exec %> tsc --noEmit
|
|
53
53
|
fi
|
|
54
54
|
cd ..
|
|
55
55
|
echo "$FASTIFY_ALL" | xargs git add
|
|
@@ -62,10 +62,10 @@ FRONTEND_ALL=$(echo "$STAGED_FILES" | grep '^<%= paths.frontend %>/' || true)
|
|
|
62
62
|
if [ -n "$FRONTEND_ALL" ]; then
|
|
63
63
|
echo "Formatting <%= paths.frontend %>..."
|
|
64
64
|
cd <%= paths.frontend %>
|
|
65
|
-
echo "$FRONTEND_ALL" | sed 's|^<%= paths.frontend %>/||' | xargs
|
|
65
|
+
echo "$FRONTEND_ALL" | sed 's|^<%= paths.frontend %>/||' | xargs <%= pm.exec %> prettier --write --ignore-unknown
|
|
66
66
|
if [ -n "$FRONTEND_TS" ]; then
|
|
67
|
-
echo "$FRONTEND_TS" | sed 's|^<%= paths.frontend %>/||' | xargs
|
|
68
|
-
|
|
67
|
+
echo "$FRONTEND_TS" | sed 's|^<%= paths.frontend %>/||' | xargs <%= pm.exec %> eslint --fix
|
|
68
|
+
<%= pm.exec %> tsc --noEmit
|
|
69
69
|
fi
|
|
70
70
|
cd ..
|
|
71
71
|
echo "$FRONTEND_ALL" | xargs git add
|
|
@@ -78,10 +78,10 @@ E2E_ALL=$(echo "$STAGED_FILES" | grep '^<%= paths.e2e %>/' || true)
|
|
|
78
78
|
if [ -n "$E2E_ALL" ]; then
|
|
79
79
|
echo "Formatting <%= paths.e2e %>..."
|
|
80
80
|
cd <%= paths.e2e %>
|
|
81
|
-
echo "$E2E_ALL" | sed 's|^<%= paths.e2e %>/||' | xargs
|
|
81
|
+
echo "$E2E_ALL" | sed 's|^<%= paths.e2e %>/||' | xargs <%= pm.exec %> prettier --write --ignore-unknown
|
|
82
82
|
if [ -n "$E2E_TS" ]; then
|
|
83
|
-
echo "$E2E_TS" | sed 's|^<%= paths.e2e %>/||' | xargs
|
|
84
|
-
|
|
83
|
+
echo "$E2E_TS" | sed 's|^<%= paths.e2e %>/||' | xargs <%= pm.exec %> eslint --fix
|
|
84
|
+
<%= pm.exec %> tsc --noEmit
|
|
85
85
|
fi
|
|
86
86
|
cd ..
|
|
87
87
|
echo "$E2E_ALL" | xargs git add
|
|
@@ -10,17 +10,17 @@ echo "FastAPI dependencies installed."
|
|
|
10
10
|
<% } %>
|
|
11
11
|
<% if (components.includes('fastify')) { %>
|
|
12
12
|
|
|
13
|
-
cd <%= paths.fastify %> &&
|
|
13
|
+
cd <%= paths.fastify %> && <%= pm.ci %> && cd ..
|
|
14
14
|
echo "Fastify dependencies installed."
|
|
15
15
|
<% } %>
|
|
16
16
|
<% if (components.includes('frontend')) { %>
|
|
17
17
|
|
|
18
|
-
cd <%= paths.frontend %> &&
|
|
18
|
+
cd <%= paths.frontend %> && <%= pm.ci %> && cd ..
|
|
19
19
|
echo "Frontend dependencies installed."
|
|
20
20
|
<% } %>
|
|
21
21
|
<% if (components.includes('e2e')) { %>
|
|
22
22
|
|
|
23
|
-
cd <%= paths.e2e %> &&
|
|
23
|
+
cd <%= paths.e2e %> && <%= pm.ci %> && cd ..
|
|
24
24
|
echo "E2E dependencies installed."
|
|
25
25
|
<% } %>
|
|
26
26
|
<% if (components.includes('mobile')) { %>
|