create-projx 1.1.1 → 1.2.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 +54 -15
- package/dist/index.js +34 -29
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -54,32 +54,49 @@ Interactive prompt lets you pick components. Or specify them directly:
|
|
|
54
54
|
npx create-projx my-app --components fastapi,fastify,frontend,mobile,e2e,infra
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
-
###
|
|
57
|
+
### Adopt an Existing Project
|
|
58
|
+
|
|
59
|
+
Already have a project? Initialize projx to get the scaffolding (CI, hooks, docker-compose) without overwriting your code:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
cd my-existing-app
|
|
63
|
+
npx create-projx init
|
|
64
|
+
```
|
|
58
65
|
|
|
59
|
-
|
|
66
|
+
Auto-detects components by scanning for `fastapi` in pyproject.toml, `react`/`fastify` in package.json, `flutter` in pubspec.yaml, and `.tf` files. Confirms each mapping, writes `.projx-component` markers, and generates only missing shared files. Existing files get a diff/overwrite/skip prompt.
|
|
67
|
+
|
|
68
|
+
### Add Components Later
|
|
60
69
|
|
|
61
70
|
```bash
|
|
62
71
|
cd my-app
|
|
63
72
|
npx create-projx add frontend mobile
|
|
64
73
|
```
|
|
65
74
|
|
|
66
|
-
|
|
75
|
+
Copies the new component directories, regenerates shared files (docker-compose, CI, pre-commit hooks) to include them, and installs dependencies.
|
|
67
76
|
|
|
68
77
|
### Update Scaffolding
|
|
69
78
|
|
|
70
|
-
When
|
|
79
|
+
When templates improve, update your project:
|
|
71
80
|
|
|
72
81
|
```bash
|
|
73
82
|
cd my-app
|
|
74
83
|
npx create-projx@latest update
|
|
75
84
|
```
|
|
76
85
|
|
|
77
|
-
|
|
86
|
+
Creates a `projx/update-vX.X.X` branch with the latest template overlay. Your custom files are never deleted — only template files are added or replaced. Merge with conflict resolution:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
git diff main...projx/update-v1.1.2 # review changes
|
|
90
|
+
git checkout main && git merge --no-ff projx/update-v1.1.2 # merge with conflicts
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Git shows conflicts on files you customized (controllers, middleware, configs). You keep your code, accept template improvements.
|
|
78
94
|
|
|
79
95
|
## Options
|
|
80
96
|
|
|
81
97
|
```
|
|
82
98
|
npx create-projx <name> [options]
|
|
99
|
+
npx create-projx init
|
|
83
100
|
npx create-projx add <components...>
|
|
84
101
|
npx create-projx update
|
|
85
102
|
|
|
@@ -90,19 +107,34 @@ npx create-projx update
|
|
|
90
107
|
-h, --help Show help
|
|
91
108
|
```
|
|
92
109
|
|
|
110
|
+
## Rename Component Directories
|
|
111
|
+
|
|
112
|
+
Rename `fastapi/` to `backend/`? Just rename the folder — the `.projx-component` marker file moves with it. The `update` command auto-discovers where each component lives by scanning for these markers. No config changes needed.
|
|
113
|
+
|
|
114
|
+
```
|
|
115
|
+
backend/.projx-component → { "components": ["fastapi"] }
|
|
116
|
+
web/.projx-component → { "components": ["frontend"] }
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
CI, setup.sh, pre-commit hooks, and docker-compose are all regenerated with your custom directory names.
|
|
120
|
+
|
|
93
121
|
## What a Scaffolded Project Looks Like
|
|
94
122
|
|
|
95
123
|
```
|
|
96
124
|
my-app/
|
|
97
|
-
├── fastapi/
|
|
98
|
-
|
|
99
|
-
├──
|
|
100
|
-
|
|
101
|
-
├──
|
|
102
|
-
|
|
103
|
-
├── .
|
|
104
|
-
├──
|
|
105
|
-
|
|
125
|
+
├── fastapi/ # Auto-entity CRUD backend
|
|
126
|
+
│ └── .projx-component # Identifies this as the fastapi component
|
|
127
|
+
├── frontend/ # Auto-entity UI from /_meta
|
|
128
|
+
│ └── .projx-component
|
|
129
|
+
├── e2e/ # Playwright E2E tests
|
|
130
|
+
│ └── .projx-component
|
|
131
|
+
├── docker-compose.yml # Production (backend + frontend + SSL)
|
|
132
|
+
├── docker-compose.dev.yml # Development (PostgreSQL + hot reload)
|
|
133
|
+
├── .github/workflows/ # CI per component (runs only on changes)
|
|
134
|
+
├── .githooks/pre-commit # Format + lint on commit
|
|
135
|
+
├── .vscode/ # Editor settings + recommended extensions
|
|
136
|
+
├── setup.sh # Install all deps
|
|
137
|
+
└── .projx # Components list + version
|
|
106
138
|
```
|
|
107
139
|
|
|
108
140
|
Only the components you selected appear. Shared files (docker-compose, CI, hooks) are generated to match your selection.
|
|
@@ -121,9 +153,10 @@ The core idea: define a data model, get everything else for free.
|
|
|
121
153
|
|
|
122
154
|
- JWT auth with Keycloak (pluggable providers)
|
|
123
155
|
- Docker Compose for dev and prod
|
|
124
|
-
- GitHub Actions CI per component
|
|
156
|
+
- GitHub Actions CI per component (path-filtered — only runs when that component changes)
|
|
125
157
|
- Pre-commit hooks (format + lint + typecheck)
|
|
126
158
|
- Secret detection in pre-commit
|
|
159
|
+
- VS Code settings + recommended extensions
|
|
127
160
|
- 80% test coverage enforced
|
|
128
161
|
- Auto-entity discovery across all stacks
|
|
129
162
|
|
|
@@ -139,6 +172,12 @@ cd projx
|
|
|
139
172
|
|
|
140
173
|
The CLI lives in `cli/`. Templates are the root-level component directories (`fastapi/`, `frontend/`, etc.).
|
|
141
174
|
|
|
175
|
+
```bash
|
|
176
|
+
cd cli
|
|
177
|
+
npm test # run tests
|
|
178
|
+
npm run build # build CLI
|
|
179
|
+
```
|
|
180
|
+
|
|
142
181
|
## License
|
|
143
182
|
|
|
144
183
|
MIT
|
package/dist/index.js
CHANGED
|
@@ -437,8 +437,7 @@ async function doScaffold(opts, dest, repoDir, name, nameSnake, vars) {
|
|
|
437
437
|
const projxConfig = {
|
|
438
438
|
version: pkg.version,
|
|
439
439
|
components: opts.components,
|
|
440
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString().split("T")[0]
|
|
441
|
-
paths: vars.paths
|
|
440
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString().split("T")[0]
|
|
442
441
|
};
|
|
443
442
|
await writeFile2(join3(dest, ".projx"), JSON.stringify(projxConfig, null, 2));
|
|
444
443
|
if (opts.git) {
|
|
@@ -680,21 +679,24 @@ async function update(cwd, localRepo) {
|
|
|
680
679
|
}
|
|
681
680
|
execSync2(`git checkout -b ${branchName}`, { cwd, stdio: "pipe" });
|
|
682
681
|
p3.log.info(`Created branch: ${branchName}`);
|
|
682
|
+
let touchedFiles;
|
|
683
683
|
try {
|
|
684
|
-
await doUpdate(cwd, config, repoDir, pkg.version, componentPaths);
|
|
684
|
+
touchedFiles = await doUpdate(cwd, config, repoDir, pkg.version, componentPaths);
|
|
685
685
|
} finally {
|
|
686
686
|
await cleanupRepo(repoDir, isLocal);
|
|
687
687
|
}
|
|
688
|
-
|
|
689
|
-
|
|
688
|
+
for (const f of touchedFiles) {
|
|
689
|
+
execSync2(`git add "${f}"`, { cwd, stdio: "pipe" });
|
|
690
|
+
}
|
|
691
|
+
execSync2(`git commit --no-verify -m "projx update to v${pkg.version}"`, { cwd, stdio: "pipe" });
|
|
690
692
|
p3.outro(
|
|
691
693
|
`Updated on branch: ${branchName}
|
|
692
694
|
|
|
693
695
|
Review changes:
|
|
694
696
|
git diff ${originalBranch}...${branchName}
|
|
695
697
|
|
|
696
|
-
|
|
697
|
-
git checkout ${originalBranch} && git merge ${branchName}`
|
|
698
|
+
Merge (resolve conflicts for files you customized):
|
|
699
|
+
git checkout ${originalBranch} && git merge --no-ff ${branchName}`
|
|
698
700
|
);
|
|
699
701
|
} else {
|
|
700
702
|
const dlSpinner = p3.spinner();
|
|
@@ -720,8 +722,15 @@ async function doUpdate(cwd, config, repoDir, version, componentPaths) {
|
|
|
720
722
|
const name = detectProjectName(cwd, config.components, componentPaths);
|
|
721
723
|
const nameSnake = toSnake(name);
|
|
722
724
|
const vars = { projectName: name, components: config.components, paths: componentPaths };
|
|
725
|
+
const touchedFiles = [];
|
|
726
|
+
const usedPaths = /* @__PURE__ */ new Set();
|
|
723
727
|
for (const component of config.components) {
|
|
724
728
|
const targetDir = componentPaths[component];
|
|
729
|
+
if (usedPaths.has(targetDir)) {
|
|
730
|
+
p3.log.warn(`${component} shares directory ${targetDir}/ with another component \u2014 skipping overlay to avoid nesting.`);
|
|
731
|
+
continue;
|
|
732
|
+
}
|
|
733
|
+
usedPaths.add(targetDir);
|
|
725
734
|
const spinner6 = p3.spinner();
|
|
726
735
|
spinner6.start(`Updating ${targetDir}/ (${component})`);
|
|
727
736
|
const componentSrc = join4(repoDir, component);
|
|
@@ -739,10 +748,12 @@ async function doUpdate(cwd, config, repoDir, version, componentPaths) {
|
|
|
739
748
|
const dir = dest.substring(0, dest.lastIndexOf("/"));
|
|
740
749
|
await mkdir3(dir, { recursive: true });
|
|
741
750
|
await cp2(src, dest, { force: true });
|
|
751
|
+
touchedFiles.push(destRel);
|
|
742
752
|
}
|
|
743
753
|
await rm2(tmpDest, { recursive: true, force: true });
|
|
744
754
|
if (!existsSync3(join4(cwd, targetDir, ".projx-component"))) {
|
|
745
755
|
await writeComponentMarker(join4(cwd, targetDir), component);
|
|
756
|
+
touchedFiles.push(`${targetDir}/.projx-component`);
|
|
746
757
|
}
|
|
747
758
|
spinner6.stop(`${targetDir}/ updated.`);
|
|
748
759
|
}
|
|
@@ -750,29 +761,24 @@ async function doUpdate(cwd, config, repoDir, version, componentPaths) {
|
|
|
750
761
|
spinner5.start("Updating shared files");
|
|
751
762
|
const hasBackend = config.components.includes("fastapi") || config.components.includes("fastify");
|
|
752
763
|
if (hasBackend || config.components.includes("frontend")) {
|
|
753
|
-
await writeFile3(
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
);
|
|
757
|
-
await writeFile3(
|
|
758
|
-
join4(cwd, "docker-compose.dev.yml"),
|
|
759
|
-
await generateDockerComposeDev(vars)
|
|
760
|
-
);
|
|
764
|
+
await writeFile3(join4(cwd, "docker-compose.yml"), await generateDockerCompose(vars));
|
|
765
|
+
touchedFiles.push("docker-compose.yml");
|
|
766
|
+
await writeFile3(join4(cwd, "docker-compose.dev.yml"), await generateDockerComposeDev(vars));
|
|
767
|
+
touchedFiles.push("docker-compose.dev.yml");
|
|
761
768
|
}
|
|
762
769
|
await mkdir3(join4(cwd, ".githooks"), { recursive: true });
|
|
763
|
-
|
|
764
|
-
await writeFile3(join4(cwd, ".githooks/pre-commit"), preCommit);
|
|
770
|
+
await writeFile3(join4(cwd, ".githooks/pre-commit"), await generatePreCommit(vars));
|
|
765
771
|
await chmod2(join4(cwd, ".githooks/pre-commit"), 493);
|
|
772
|
+
touchedFiles.push(".githooks/pre-commit");
|
|
766
773
|
await mkdir3(join4(cwd, ".github/workflows"), { recursive: true });
|
|
767
|
-
await writeFile3(
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
);
|
|
771
|
-
const setupSh = await generateSetupSh(vars);
|
|
772
|
-
await writeFile3(join4(cwd, "setup.sh"), setupSh);
|
|
774
|
+
await writeFile3(join4(cwd, ".github/workflows/ci.yml"), await generateCiYml(vars));
|
|
775
|
+
touchedFiles.push(".github/workflows/ci.yml");
|
|
776
|
+
await writeFile3(join4(cwd, "setup.sh"), await generateSetupSh(vars));
|
|
773
777
|
await chmod2(join4(cwd, "setup.sh"), 493);
|
|
778
|
+
touchedFiles.push("setup.sh");
|
|
774
779
|
await mkdir3(join4(cwd, ".vscode"), { recursive: true });
|
|
775
780
|
await writeFile3(join4(cwd, ".vscode/settings.json"), generateVscodeSettings(vars));
|
|
781
|
+
touchedFiles.push(".vscode/settings.json");
|
|
776
782
|
spinner5.stop("Shared files updated.");
|
|
777
783
|
if (config.components.includes("mobile")) {
|
|
778
784
|
const mobilePath = componentPaths.mobile ?? "mobile";
|
|
@@ -786,10 +792,11 @@ async function doUpdate(cwd, config, repoDir, version, componentPaths) {
|
|
|
786
792
|
const updatedConfig = {
|
|
787
793
|
version,
|
|
788
794
|
components: config.components,
|
|
789
|
-
createdAt: config.createdAt
|
|
790
|
-
paths: componentPaths
|
|
795
|
+
createdAt: config.createdAt
|
|
791
796
|
};
|
|
792
797
|
await writeFile3(join4(cwd, ".projx"), JSON.stringify(updatedConfig, null, 2));
|
|
798
|
+
touchedFiles.push(".projx");
|
|
799
|
+
return touchedFiles;
|
|
793
800
|
}
|
|
794
801
|
function detectProjectName(cwd, components, componentPaths) {
|
|
795
802
|
for (const component of components) {
|
|
@@ -901,8 +908,7 @@ async function doAdd(cwd, config, toAdd, repoDir, skipInstall) {
|
|
|
901
908
|
const updatedConfig = {
|
|
902
909
|
version: pkg.version,
|
|
903
910
|
components: allComponents,
|
|
904
|
-
createdAt: config.createdAt
|
|
905
|
-
paths
|
|
911
|
+
createdAt: config.createdAt
|
|
906
912
|
};
|
|
907
913
|
await writeFile4(join5(cwd, ".projx"), JSON.stringify(updatedConfig, null, 2));
|
|
908
914
|
p4.outro(`Added ${toAdd.join(", ")}. Shared files updated for all ${allComponents.length} components.
|
|
@@ -1236,8 +1242,7 @@ async function init(cwd, localRepo) {
|
|
|
1236
1242
|
const projxConfig = {
|
|
1237
1243
|
version: pkg.version,
|
|
1238
1244
|
components,
|
|
1239
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString().split("T")[0]
|
|
1240
|
-
paths
|
|
1245
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString().split("T")[0]
|
|
1241
1246
|
};
|
|
1242
1247
|
await writeFile5(join7(cwd, ".projx"), JSON.stringify(projxConfig, null, 2));
|
|
1243
1248
|
p5.log.success(".projx");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-projx",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Scaffold production-grade projects. Pick your stack (FastAPI, Fastify, React, Flutter), get a fully wired template with auth, database, CI/CD, and E2E tests.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|