From 9a99c233b2f8437005d79dc165e572cc4ee8dddf Mon Sep 17 00:00:00 2001 From: Jonas Rohde Date: Mon, 30 Mar 2026 16:10:02 +0200 Subject: [PATCH] replace fs-extra and execa in scaffold --- bun.lock | 61 ------------------------------------- docs/plans/decisions-log.md | 15 +++++++++ package.json | 3 -- src/index.ts | 16 +++++++--- src/scaffold.ts | 47 ++++++++++++++++------------ templates/base/dockerignore | 8 +++++ 6 files changed, 63 insertions(+), 87 deletions(-) create mode 100644 docs/plans/decisions-log.md create mode 100644 templates/base/dockerignore diff --git a/bun.lock b/bun.lock index 66c2f6f..9a3c1b9 100644 --- a/bun.lock +++ b/bun.lock @@ -6,12 +6,9 @@ "name": "create-edhor-stack", "dependencies": { "@clack/prompts": "^0.10.0", - "execa": "^9.5.0", - "fs-extra": "^11.3.0", "picocolors": "^1.1.1", }, "devDependencies": { - "@types/fs-extra": "^11.0.4", "@types/node": "^22.15.21", "typescript": "^5.8.3", }, @@ -22,72 +19,14 @@ "@clack/prompts": ["@clack/prompts@0.10.1", "", { "dependencies": { "@clack/core": "0.4.2", "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-Q0T02vx8ZM9XSv9/Yde0jTmmBQufZhPJfYAg2XrrrxWWaZgq1rr8nU8Hv710BQ1dhoP8rtY7YUdpGej2Qza/cw=="], - "@sec-ant/readable-stream": ["@sec-ant/readable-stream@0.4.1", "", {}, "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg=="], - - "@sindresorhus/merge-streams": ["@sindresorhus/merge-streams@4.0.0", "", {}, "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ=="], - - "@types/fs-extra": ["@types/fs-extra@11.0.4", "", { "dependencies": { "@types/jsonfile": "*", "@types/node": "*" } }, "sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ=="], - - "@types/jsonfile": ["@types/jsonfile@6.1.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ=="], - "@types/node": ["@types/node@22.19.7", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw=="], - "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], - - "execa": ["execa@9.6.1", "", { "dependencies": { "@sindresorhus/merge-streams": "^4.0.0", "cross-spawn": "^7.0.6", "figures": "^6.1.0", "get-stream": "^9.0.0", "human-signals": "^8.0.1", "is-plain-obj": "^4.1.0", "is-stream": "^4.0.1", "npm-run-path": "^6.0.0", "pretty-ms": "^9.2.0", "signal-exit": "^4.1.0", "strip-final-newline": "^4.0.0", "yoctocolors": "^2.1.1" } }, "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA=="], - - "figures": ["figures@6.1.0", "", { "dependencies": { "is-unicode-supported": "^2.0.0" } }, "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg=="], - - "fs-extra": ["fs-extra@11.3.3", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg=="], - - "get-stream": ["get-stream@9.0.1", "", { "dependencies": { "@sec-ant/readable-stream": "^0.4.1", "is-stream": "^4.0.1" } }, "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA=="], - - "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], - - "human-signals": ["human-signals@8.0.1", "", {}, "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ=="], - - "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], - - "is-stream": ["is-stream@4.0.1", "", {}, "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A=="], - - "is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="], - - "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], - - "jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="], - - "npm-run-path": ["npm-run-path@6.0.0", "", { "dependencies": { "path-key": "^4.0.0", "unicorn-magic": "^0.3.0" } }, "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA=="], - - "parse-ms": ["parse-ms@4.0.0", "", {}, "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw=="], - - "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], - "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], - "pretty-ms": ["pretty-ms@9.3.0", "", { "dependencies": { "parse-ms": "^4.0.0" } }, "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ=="], - - "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], - - "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], - - "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], - "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], - "strip-final-newline": ["strip-final-newline@4.0.0", "", {}, "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw=="], - "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - - "unicorn-magic": ["unicorn-magic@0.3.0", "", {}, "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA=="], - - "universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], - - "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], - - "yoctocolors": ["yoctocolors@2.1.2", "", {}, "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug=="], - - "npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], } } diff --git a/docs/plans/decisions-log.md b/docs/plans/decisions-log.md new file mode 100644 index 0000000..c4b3d25 --- /dev/null +++ b/docs/plans/decisions-log.md @@ -0,0 +1,15 @@ +# Decisions Log + +## 2026-03-30: Replace `fs-extra` and `execa` in the CLI scaffold + +### Context +The scaffold CLI depended on `fs-extra` for directory and file helpers and declared `execa` without using it. The package already runs on modern Node/Bun runtimes, so Node's built-in filesystem APIs cover the needed behavior. + +### Decision +Replace `fs-extra` usage with `node:fs/promises`, remove the unused `execa` dependency, and add template `.dockerignore` generation alongside the existing deployment files. + +### Rationale +This keeps the same scaffold behavior while reducing direct dependencies and aligns with the package replacement guidance from the JavaScript bloat review. + +### Consequences +The CLI depends on fewer packages, generated deployment templates now include `.dockerignore`, and deployment-disabled scaffolds remove that file together with `Dockerfile` and `fly.toml`. diff --git a/package.json b/package.json index 16ef053..6441195 100644 --- a/package.json +++ b/package.json @@ -32,12 +32,9 @@ }, "dependencies": { "@clack/prompts": "^0.10.0", - "execa": "^9.5.0", - "fs-extra": "^11.3.0", "picocolors": "^1.1.1" }, "devDependencies": { - "@types/fs-extra": "^11.0.4", "@types/node": "^22.15.21", "typescript": "^5.8.3" } diff --git a/src/index.ts b/src/index.ts index 034a2e2..7e1880d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,8 +1,8 @@ #!/usr/bin/env node import * as p from "@clack/prompts"; import pc from "picocolors"; +import { access, rm } from "node:fs/promises"; import path from "node:path"; -import fs from "fs-extra"; import { runPrompts } from "./prompts.js"; import { scaffoldProject } from "./scaffold.js"; @@ -15,8 +15,7 @@ async function main() { const targetDir = path.resolve(process.cwd(), config.name); - // Check if directory exists - if (await fs.pathExists(targetDir)) { + if (await pathExists(targetDir)) { const overwrite = await p.confirm({ message: `Directory ${pc.cyan(config.name)} already exists. Overwrite?`, initialValue: false, @@ -27,7 +26,7 @@ async function main() { return; } - await fs.remove(targetDir); + await rm(targetDir, { force: true, recursive: true }); } const s = p.spinner(); @@ -55,4 +54,13 @@ async function main() { } } +async function pathExists(targetPath: string): Promise { + try { + await access(targetPath); + return true; + } catch { + return false; + } +} + main().catch(console.error); diff --git a/src/scaffold.ts b/src/scaffold.ts index 353ffc9..4392d27 100644 --- a/src/scaffold.ts +++ b/src/scaffold.ts @@ -1,14 +1,12 @@ -import fs from "fs-extra"; +import { access, chmod, mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises"; import path from "node:path"; -import { execa } from "execa"; import type { ProjectConfig } from "./types.js"; import { getTemplatesDir, renderTemplate } from "./utils.js"; export async function scaffoldProject(config: ProjectConfig, targetDir: string): Promise { const templatesDir = getTemplatesDir(); - // Create target directory - await fs.ensureDir(targetDir); + await mkdir(targetDir, { recursive: true }); // Copy base template await copyTemplate(path.join(templatesDir, "base"), targetDir, { @@ -16,15 +14,15 @@ export async function scaffoldProject(config: ProjectConfig, targetDir: string): }); // Create apps and packages directories - await fs.ensureDir(path.join(targetDir, "apps")); - await fs.ensureDir(path.join(targetDir, "packages")); + await mkdir(path.join(targetDir, "apps"), { recursive: true }); + await mkdir(path.join(targetDir, "packages"), { recursive: true }); // Copy selected apps for (const app of config.apps) { const appSrc = path.join(templatesDir, "apps", app); const appDest = path.join(targetDir, "apps", app); - if (await fs.pathExists(appSrc)) { + if (await pathExists(appSrc)) { await copyTemplate(appSrc, appDest, { name: config.name }); } } @@ -34,7 +32,7 @@ export async function scaffoldProject(config: ProjectConfig, targetDir: string): const apiSrc = path.join(templatesDir, "apps", `api-${config.api}`); const apiDest = path.join(targetDir, "apps", "api"); - if (await fs.pathExists(apiSrc)) { + if (await pathExists(apiSrc)) { await copyTemplate(apiSrc, apiDest, { name: config.name }); } } @@ -44,21 +42,22 @@ export async function scaffoldProject(config: ProjectConfig, targetDir: string): const pkgSrc = path.join(templatesDir, "packages", pkg); const pkgDest = path.join(targetDir, "packages", pkg); - if (await fs.pathExists(pkgSrc)) { + if (await pathExists(pkgSrc)) { await copyTemplate(pkgSrc, pkgDest, { name: config.name }); } } // Remove deployment files if not enabled if (!config.deployment) { - await fs.remove(path.join(targetDir, "Dockerfile")); - await fs.remove(path.join(targetDir, "fly.toml")); + await rm(path.join(targetDir, "Dockerfile"), { force: true, recursive: true }); + await rm(path.join(targetDir, "fly.toml"), { force: true, recursive: true }); + await rm(path.join(targetDir, ".dockerignore"), { force: true, recursive: true }); } // Make husky pre-commit executable const preCommitPath = path.join(targetDir, ".husky", "pre-commit"); - if (await fs.pathExists(preCommitPath)) { - await fs.chmod(preCommitPath, 0o755); + if (await pathExists(preCommitPath)) { + await chmod(preCommitPath, 0o755); } } @@ -67,23 +66,24 @@ async function copyTemplate( destDir: string, vars: Record ): Promise { - await fs.ensureDir(destDir); - const entries = await fs.readdir(srcDir, { withFileTypes: true }); + await mkdir(destDir, { recursive: true }); + const entries = await readdir(srcDir, { withFileTypes: true }); for (const entry of entries) { const srcPath = path.join(srcDir, entry.name); let destName = entry.name.replace(/\.hbs$/, ""); - // Rename gitignore to .gitignore (npm excludes .gitignore files from packages) if (destName === "gitignore") { destName = ".gitignore"; + } else if (destName === "dockerignore") { + destName = ".dockerignore"; } const destPath = path.join(destDir, destName); if (entry.isDirectory()) { - await fs.ensureDir(destPath); + await mkdir(destPath, { recursive: true }); await copyTemplate(srcPath, destPath, vars); } else { - let content = await fs.readFile(srcPath, "utf-8"); + let content = await readFile(srcPath, "utf-8"); // Render handlebars-style variables in template files const renderExtensions = [".hbs", ".json", ".tsx", ".ts", ".md", ".toml"]; @@ -91,7 +91,16 @@ async function copyTemplate( content = renderTemplate(content, vars); } - await fs.writeFile(destPath, content); + await writeFile(destPath, content); } } } + +async function pathExists(targetPath: string): Promise { + try { + await access(targetPath); + return true; + } catch { + return false; + } +} diff --git a/templates/base/dockerignore b/templates/base/dockerignore new file mode 100644 index 0000000..d92b094 --- /dev/null +++ b/templates/base/dockerignore @@ -0,0 +1,8 @@ +node_modules +.git +.turbo +.next +.output +dist +coverage +.DS_Store