create-projx 1.1.2 → 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 +8 -81
- 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) {
|
|
@@ -592,10 +591,6 @@ var NEVER_OVERWRITE = [
|
|
|
592
591
|
/src\/migrations\/versions\//,
|
|
593
592
|
/\.projx-component$/
|
|
594
593
|
];
|
|
595
|
-
var MERGE_DEPS = [
|
|
596
|
-
/^[^/]+\/package\.json$/,
|
|
597
|
-
/^[^/]+\/pyproject\.toml$/
|
|
598
|
-
];
|
|
599
594
|
function isGitRepo(cwd) {
|
|
600
595
|
try {
|
|
601
596
|
execSync2("git rev-parse --is-inside-work-tree", { cwd, stdio: "pipe" });
|
|
@@ -700,8 +695,8 @@ async function update(cwd, localRepo) {
|
|
|
700
695
|
Review changes:
|
|
701
696
|
git diff ${originalBranch}...${branchName}
|
|
702
697
|
|
|
703
|
-
|
|
704
|
-
git checkout ${originalBranch} && git merge ${branchName}`
|
|
698
|
+
Merge (resolve conflicts for files you customized):
|
|
699
|
+
git checkout ${originalBranch} && git merge --no-ff ${branchName}`
|
|
705
700
|
);
|
|
706
701
|
} else {
|
|
707
702
|
const dlSpinner = p3.spinner();
|
|
@@ -752,16 +747,8 @@ async function doUpdate(cwd, config, repoDir, version, componentPaths) {
|
|
|
752
747
|
if (NEVER_OVERWRITE.some((re) => re.test(destRel))) continue;
|
|
753
748
|
const dir = dest.substring(0, dest.lastIndexOf("/"));
|
|
754
749
|
await mkdir3(dir, { recursive: true });
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
if (merged) {
|
|
758
|
-
await writeFile3(dest, merged);
|
|
759
|
-
touchedFiles.push(destRel);
|
|
760
|
-
}
|
|
761
|
-
} else {
|
|
762
|
-
await cp2(src, dest, { force: true });
|
|
763
|
-
touchedFiles.push(destRel);
|
|
764
|
-
}
|
|
750
|
+
await cp2(src, dest, { force: true });
|
|
751
|
+
touchedFiles.push(destRel);
|
|
765
752
|
}
|
|
766
753
|
await rm2(tmpDest, { recursive: true, force: true });
|
|
767
754
|
if (!existsSync3(join4(cwd, targetDir, ".projx-component"))) {
|
|
@@ -805,8 +792,7 @@ async function doUpdate(cwd, config, repoDir, version, componentPaths) {
|
|
|
805
792
|
const updatedConfig = {
|
|
806
793
|
version,
|
|
807
794
|
components: config.components,
|
|
808
|
-
createdAt: config.createdAt
|
|
809
|
-
paths: componentPaths
|
|
795
|
+
createdAt: config.createdAt
|
|
810
796
|
};
|
|
811
797
|
await writeFile3(join4(cwd, ".projx"), JSON.stringify(updatedConfig, null, 2));
|
|
812
798
|
touchedFiles.push(".projx");
|
|
@@ -831,63 +817,6 @@ function detectProjectName(cwd, components, componentPaths) {
|
|
|
831
817
|
}
|
|
832
818
|
return toKebab(cwd.split("/").pop());
|
|
833
819
|
}
|
|
834
|
-
async function mergeDeps(existingPath, templatePath) {
|
|
835
|
-
if (existingPath.endsWith("package.json")) {
|
|
836
|
-
return mergePackageJson(existingPath, templatePath);
|
|
837
|
-
}
|
|
838
|
-
if (existingPath.endsWith("pyproject.toml")) {
|
|
839
|
-
return mergePyprojectToml(existingPath, templatePath);
|
|
840
|
-
}
|
|
841
|
-
return null;
|
|
842
|
-
}
|
|
843
|
-
async function mergePackageJson(existingPath, templatePath) {
|
|
844
|
-
const existingRaw = await readFileOrNull(existingPath);
|
|
845
|
-
const templateRaw = await readFileOrNull(templatePath);
|
|
846
|
-
if (!existingRaw || !templateRaw) return null;
|
|
847
|
-
try {
|
|
848
|
-
const existing = JSON.parse(existingRaw);
|
|
849
|
-
const template = JSON.parse(templateRaw);
|
|
850
|
-
if (template.dependencies) {
|
|
851
|
-
existing.dependencies = { ...template.dependencies, ...existing.dependencies };
|
|
852
|
-
}
|
|
853
|
-
if (template.devDependencies) {
|
|
854
|
-
existing.devDependencies = { ...template.devDependencies, ...existing.devDependencies };
|
|
855
|
-
}
|
|
856
|
-
if (template.scripts) {
|
|
857
|
-
existing.scripts = { ...template.scripts, ...existing.scripts };
|
|
858
|
-
}
|
|
859
|
-
return JSON.stringify(existing, null, 2) + "\n";
|
|
860
|
-
} catch {
|
|
861
|
-
return null;
|
|
862
|
-
}
|
|
863
|
-
}
|
|
864
|
-
async function mergePyprojectToml(existingPath, templatePath) {
|
|
865
|
-
const existingRaw = await readFileOrNull(existingPath);
|
|
866
|
-
const templateRaw = await readFileOrNull(templatePath);
|
|
867
|
-
if (!existingRaw || !templateRaw) return null;
|
|
868
|
-
const templateDeps = extractTomlDeps(templateRaw);
|
|
869
|
-
if (templateDeps.length === 0) return null;
|
|
870
|
-
const existingDeps = extractTomlDeps(existingRaw);
|
|
871
|
-
const existingNames = new Set(existingDeps.map((d) => d.replace(/[><=!~[].*/, "").trim().toLowerCase()));
|
|
872
|
-
const newDeps = templateDeps.filter((d) => {
|
|
873
|
-
const name = d.replace(/[><=!~[].*/, "").trim().toLowerCase();
|
|
874
|
-
return !existingNames.has(name);
|
|
875
|
-
});
|
|
876
|
-
if (newDeps.length === 0) return null;
|
|
877
|
-
const depsMatch = existingRaw.match(/^dependencies\s*=\s*\[([^\]]*)\]/m);
|
|
878
|
-
if (!depsMatch) return null;
|
|
879
|
-
const closingBracket = existingRaw.indexOf("]", depsMatch.index);
|
|
880
|
-
const before = existingRaw.slice(0, closingBracket);
|
|
881
|
-
const after = existingRaw.slice(closingBracket);
|
|
882
|
-
const indent = " ";
|
|
883
|
-
const newLines = newDeps.map((d) => `${indent}"${d}",`).join("\n");
|
|
884
|
-
return before.trimEnd() + "\n" + newLines + "\n" + after;
|
|
885
|
-
}
|
|
886
|
-
function extractTomlDeps(toml) {
|
|
887
|
-
const match = toml.match(/^dependencies\s*=\s*\[([\s\S]*?)\]/m);
|
|
888
|
-
if (!match) return [];
|
|
889
|
-
return match[1].split("\n").map((l) => l.trim()).filter((l) => l.startsWith('"') || l.startsWith("'")).map((l) => l.replace(/^["']|["'],?$/g, "").trim()).filter(Boolean);
|
|
890
|
-
}
|
|
891
820
|
|
|
892
821
|
// src/add.ts
|
|
893
822
|
import { copyFileSync as copyFileSync2, existsSync as existsSync4, readFileSync as readFileSync2 } from "fs";
|
|
@@ -979,8 +908,7 @@ async function doAdd(cwd, config, toAdd, repoDir, skipInstall) {
|
|
|
979
908
|
const updatedConfig = {
|
|
980
909
|
version: pkg.version,
|
|
981
910
|
components: allComponents,
|
|
982
|
-
createdAt: config.createdAt
|
|
983
|
-
paths
|
|
911
|
+
createdAt: config.createdAt
|
|
984
912
|
};
|
|
985
913
|
await writeFile4(join5(cwd, ".projx"), JSON.stringify(updatedConfig, null, 2));
|
|
986
914
|
p4.outro(`Added ${toAdd.join(", ")}. Shared files updated for all ${allComponents.length} components.
|
|
@@ -1314,8 +1242,7 @@ async function init(cwd, localRepo) {
|
|
|
1314
1242
|
const projxConfig = {
|
|
1315
1243
|
version: pkg.version,
|
|
1316
1244
|
components,
|
|
1317
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString().split("T")[0]
|
|
1318
|
-
paths
|
|
1245
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString().split("T")[0]
|
|
1319
1246
|
};
|
|
1320
1247
|
await writeFile5(join7(cwd, ".projx"), JSON.stringify(projxConfig, null, 2));
|
|
1321
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": {
|