create-projx 1.1.1 → 1.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +98 -20
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -592,6 +592,10 @@ var NEVER_OVERWRITE = [
592
592
  /src\/migrations\/versions\//,
593
593
  /\.projx-component$/
594
594
  ];
595
+ var MERGE_DEPS = [
596
+ /^[^/]+\/package\.json$/,
597
+ /^[^/]+\/pyproject\.toml$/
598
+ ];
595
599
  function isGitRepo(cwd) {
596
600
  try {
597
601
  execSync2("git rev-parse --is-inside-work-tree", { cwd, stdio: "pipe" });
@@ -680,13 +684,16 @@ async function update(cwd, localRepo) {
680
684
  }
681
685
  execSync2(`git checkout -b ${branchName}`, { cwd, stdio: "pipe" });
682
686
  p3.log.info(`Created branch: ${branchName}`);
687
+ let touchedFiles;
683
688
  try {
684
- await doUpdate(cwd, config, repoDir, pkg.version, componentPaths);
689
+ touchedFiles = await doUpdate(cwd, config, repoDir, pkg.version, componentPaths);
685
690
  } finally {
686
691
  await cleanupRepo(repoDir, isLocal);
687
692
  }
688
- execSync2("git add -A", { cwd, stdio: "pipe" });
689
- execSync2(`git commit -m "projx update to v${pkg.version}"`, { cwd, stdio: "pipe" });
693
+ for (const f of touchedFiles) {
694
+ execSync2(`git add "${f}"`, { cwd, stdio: "pipe" });
695
+ }
696
+ execSync2(`git commit --no-verify -m "projx update to v${pkg.version}"`, { cwd, stdio: "pipe" });
690
697
  p3.outro(
691
698
  `Updated on branch: ${branchName}
692
699
 
@@ -720,8 +727,15 @@ async function doUpdate(cwd, config, repoDir, version, componentPaths) {
720
727
  const name = detectProjectName(cwd, config.components, componentPaths);
721
728
  const nameSnake = toSnake(name);
722
729
  const vars = { projectName: name, components: config.components, paths: componentPaths };
730
+ const touchedFiles = [];
731
+ const usedPaths = /* @__PURE__ */ new Set();
723
732
  for (const component of config.components) {
724
733
  const targetDir = componentPaths[component];
734
+ if (usedPaths.has(targetDir)) {
735
+ p3.log.warn(`${component} shares directory ${targetDir}/ with another component \u2014 skipping overlay to avoid nesting.`);
736
+ continue;
737
+ }
738
+ usedPaths.add(targetDir);
725
739
  const spinner6 = p3.spinner();
726
740
  spinner6.start(`Updating ${targetDir}/ (${component})`);
727
741
  const componentSrc = join4(repoDir, component);
@@ -738,11 +752,21 @@ async function doUpdate(cwd, config, repoDir, version, componentPaths) {
738
752
  if (NEVER_OVERWRITE.some((re) => re.test(destRel))) continue;
739
753
  const dir = dest.substring(0, dest.lastIndexOf("/"));
740
754
  await mkdir3(dir, { recursive: true });
741
- await cp2(src, dest, { force: true });
755
+ if (MERGE_DEPS.some((re) => re.test(destRel)) && existsSync3(dest)) {
756
+ const merged = await mergeDeps(dest, src);
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
+ }
742
765
  }
743
766
  await rm2(tmpDest, { recursive: true, force: true });
744
767
  if (!existsSync3(join4(cwd, targetDir, ".projx-component"))) {
745
768
  await writeComponentMarker(join4(cwd, targetDir), component);
769
+ touchedFiles.push(`${targetDir}/.projx-component`);
746
770
  }
747
771
  spinner6.stop(`${targetDir}/ updated.`);
748
772
  }
@@ -750,29 +774,24 @@ async function doUpdate(cwd, config, repoDir, version, componentPaths) {
750
774
  spinner5.start("Updating shared files");
751
775
  const hasBackend = config.components.includes("fastapi") || config.components.includes("fastify");
752
776
  if (hasBackend || config.components.includes("frontend")) {
753
- await writeFile3(
754
- join4(cwd, "docker-compose.yml"),
755
- await generateDockerCompose(vars)
756
- );
757
- await writeFile3(
758
- join4(cwd, "docker-compose.dev.yml"),
759
- await generateDockerComposeDev(vars)
760
- );
777
+ await writeFile3(join4(cwd, "docker-compose.yml"), await generateDockerCompose(vars));
778
+ touchedFiles.push("docker-compose.yml");
779
+ await writeFile3(join4(cwd, "docker-compose.dev.yml"), await generateDockerComposeDev(vars));
780
+ touchedFiles.push("docker-compose.dev.yml");
761
781
  }
762
782
  await mkdir3(join4(cwd, ".githooks"), { recursive: true });
763
- const preCommit = await generatePreCommit(vars);
764
- await writeFile3(join4(cwd, ".githooks/pre-commit"), preCommit);
783
+ await writeFile3(join4(cwd, ".githooks/pre-commit"), await generatePreCommit(vars));
765
784
  await chmod2(join4(cwd, ".githooks/pre-commit"), 493);
785
+ touchedFiles.push(".githooks/pre-commit");
766
786
  await mkdir3(join4(cwd, ".github/workflows"), { recursive: true });
767
- await writeFile3(
768
- join4(cwd, ".github/workflows/ci.yml"),
769
- await generateCiYml(vars)
770
- );
771
- const setupSh = await generateSetupSh(vars);
772
- await writeFile3(join4(cwd, "setup.sh"), setupSh);
787
+ await writeFile3(join4(cwd, ".github/workflows/ci.yml"), await generateCiYml(vars));
788
+ touchedFiles.push(".github/workflows/ci.yml");
789
+ await writeFile3(join4(cwd, "setup.sh"), await generateSetupSh(vars));
773
790
  await chmod2(join4(cwd, "setup.sh"), 493);
791
+ touchedFiles.push("setup.sh");
774
792
  await mkdir3(join4(cwd, ".vscode"), { recursive: true });
775
793
  await writeFile3(join4(cwd, ".vscode/settings.json"), generateVscodeSettings(vars));
794
+ touchedFiles.push(".vscode/settings.json");
776
795
  spinner5.stop("Shared files updated.");
777
796
  if (config.components.includes("mobile")) {
778
797
  const mobilePath = componentPaths.mobile ?? "mobile";
@@ -790,6 +809,8 @@ async function doUpdate(cwd, config, repoDir, version, componentPaths) {
790
809
  paths: componentPaths
791
810
  };
792
811
  await writeFile3(join4(cwd, ".projx"), JSON.stringify(updatedConfig, null, 2));
812
+ touchedFiles.push(".projx");
813
+ return touchedFiles;
793
814
  }
794
815
  function detectProjectName(cwd, components, componentPaths) {
795
816
  for (const component of components) {
@@ -810,6 +831,63 @@ function detectProjectName(cwd, components, componentPaths) {
810
831
  }
811
832
  return toKebab(cwd.split("/").pop());
812
833
  }
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
+ }
813
891
 
814
892
  // src/add.ts
815
893
  import { copyFileSync as copyFileSync2, existsSync as existsSync4, readFileSync as readFileSync2 } from "fs";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-projx",
3
- "version": "1.1.1",
3
+ "version": "1.1.2",
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": {