-
Notifications
You must be signed in to change notification settings - Fork 0
[codex] replace fs-extra and execa in scaffold #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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`. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<boolean> { | ||
| try { | ||
| await access(targetPath); | ||
| return true; | ||
| } catch { | ||
| return false; | ||
| } | ||
| } | ||
|
Comment on lines
+57
to
+64
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Duplicate This function is identical to the one in 🤖 Prompt for AI Agents |
||
|
|
||
| main().catch(console.error); | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,30 +1,28 @@ | ||||||||||||||
| 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<void> { | ||||||||||||||
| 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, { | ||||||||||||||
| name: config.name, | ||||||||||||||
| }); | ||||||||||||||
|
|
||||||||||||||
| // 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 }); | ||||||||||||||
|
Comment on lines
+52
to
+54
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial
Suggested simplification- 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 });
+ await rm(path.join(targetDir, "Dockerfile"), { force: true });
+ await rm(path.join(targetDir, "fly.toml"), { force: true });
+ await rm(path.join(targetDir, ".dockerignore"), { force: true });📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| // 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,31 +66,41 @@ async function copyTemplate( | |||||||||||||
| destDir: string, | ||||||||||||||
| vars: Record<string, string> | ||||||||||||||
| ): Promise<void> { | ||||||||||||||
| 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"]; | ||||||||||||||
| if (renderExtensions.some((ext) => entry.name.endsWith(ext))) { | ||||||||||||||
| content = renderTemplate(content, vars); | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| await fs.writeFile(destPath, content); | ||||||||||||||
| await writeFile(destPath, content); | ||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| async function pathExists(targetPath: string): Promise<boolean> { | ||||||||||||||
| try { | ||||||||||||||
| await access(targetPath); | ||||||||||||||
| return true; | ||||||||||||||
| } catch { | ||||||||||||||
| return false; | ||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
|
Comment on lines
+99
to
+106
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Duplicate As noted earlier, consider extracting this to 🤖 Prompt for AI Agents |
||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,8 @@ | ||||||||||||||||||||||||||||||||||||
| node_modules | ||||||||||||||||||||||||||||||||||||
| .git | ||||||||||||||||||||||||||||||||||||
| .turbo | ||||||||||||||||||||||||||||||||||||
| .next | ||||||||||||||||||||||||||||||||||||
| .output | ||||||||||||||||||||||||||||||||||||
| dist | ||||||||||||||||||||||||||||||||||||
| coverage | ||||||||||||||||||||||||||||||||||||
| .DS_Store | ||||||||||||||||||||||||||||||||||||
|
Comment on lines
+1
to
+8
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Consider adding The current patterns cover common build artifacts. However, Suggested addition node_modules
.git
.turbo
.next
.output
dist
coverage
.DS_Store
+.env*📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add blank lines around headings to fix markdown linting warnings.
Static analysis flagged MD022 violations on lines 5, 8, 11, and 14.
Suggested fix
🧰 Tools
🪛 markdownlint-cli2 (0.22.0)
[warning] 5-5: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
[warning] 8-8: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
[warning] 11-11: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
[warning] 14-14: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
🤖 Prompt for AI Agents