-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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
Feature idea: copydir option #982
Comments
This seems to me like something that shouldn't be a part of esbuild. It's very straightforward to implement and is easier to debug and customize if it's not something that esbuild does itself. |
@mrgrain I sympathize with you -- I’ve needed to implement some kind of recursive directory logic for the projects in which I build on top of esbuild. That being said, esbuild can’t accommodate every quality of life feature because there would simply be too many. I agree w/ Evan that this feels out of scope. That being said, here’s the code I’ve used previously, which may or may not encourage you to use a similar approach. TypeScript / Node implementation: (w/ unit tests!)Code import * as fs from "fs"
import * as path from "path"
// readdirAll recursively reads a directory.
export async function readdirAll(entry: string, excludes: string[] = []): Promise<string[]> {
const ctx: string[] = []
async function recurse(current: string): Promise<void> {
let ls = await fs.promises.readdir(current)
ls = ls.map(item => path.join(current, item)) // Add entry
for (const li of ls) {
if (excludes.includes(li)) continue
const stats = await fs.promises.stat(li)
if (stats.isDirectory()) {
ctx.push(li)
await recurse(li)
continue
}
ctx.push(li)
}
}
await recurse(entry)
return ctx
}
// copyAll recursively copies files and directories.
export async function copyAll(src_dir: string, dst_dir: string, excludes: string[] = []): Promise<void> {
const dirs: string[] = []
const srcs: string[] = []
const ctx = await readdirAll(src_dir, excludes)
for (const item of ctx) {
const stats = await fs.promises.stat(item)
if (!stats.isDirectory()) {
srcs.push(item)
} else {
dirs.push(item)
}
}
for (const dir of dirs) {
const target = path.join(dst_dir, dir.slice(src_dir.length))
await fs.promises.mkdir(target, { recursive: true })
}
for (const src of srcs) {
const target = path.join(dst_dir, src.slice(src_dir.length))
await fs.promises.copyFile(src, target)
}
} Unit tests: import * as fs from "fs"
import * as path from "path"
import { copyAll, readdirAll } from "../fsAll"
test("copyAll", async () => {
await fs.promises.mkdir(path.join(__dirname, "foo"), { recursive: true })
await fs.promises.mkdir(path.join(__dirname, "foo", "bar"), { recursive: true })
await fs.promises.mkdir(path.join(__dirname, "foo", "bar", "baz"), { recursive: true })
await fs.promises.writeFile(path.join(__dirname, "foo/a"), "")
await fs.promises.writeFile(path.join(__dirname, "foo/bar/b"), "")
await fs.promises.writeFile(path.join(__dirname, "foo/bar/baz/c"), "")
await fs.promises.writeFile(path.join(__dirname, "foo/bar/baz/exclude"), "")
await copyAll(path.join(__dirname, "foo"), path.join(__dirname, "bar"), [path.join(__dirname, "foo/bar/baz/exclude")])
let foo = await readdirAll(path.join(__dirname, "foo"))
foo = foo.map(src => path.relative(__dirname, src))
let bar = await readdirAll(path.join(__dirname, "bar"))
bar = bar.map(src => path.relative(__dirname, src))
// prettier-ignore
expect(foo).toEqual([
"foo/a",
"foo/bar",
"foo/bar/b",
"foo/bar/baz",
"foo/bar/baz/c",
"foo/bar/baz/exclude",
])
// prettier-ignore
expect(bar).toEqual([
"bar/a",
"bar/bar",
"bar/bar/b",
"bar/bar/baz",
"bar/bar/baz/c",
])
await fs.promises.rmdir(path.join(__dirname, "foo"), { recursive: true })
await fs.promises.rmdir(path.join(__dirname, "bar"), { recursive: true })
})
test("readdirAll", async () => {
await fs.promises.mkdir(path.join(__dirname, "foo"), { recursive: true })
await fs.promises.mkdir(path.join(__dirname, "foo", "bar"), { recursive: true })
await fs.promises.mkdir(path.join(__dirname, "foo", "bar", "baz"), { recursive: true })
await fs.promises.writeFile(path.join(__dirname, "foo/a"), "")
await fs.promises.writeFile(path.join(__dirname, "foo/bar/b"), "")
await fs.promises.writeFile(path.join(__dirname, "foo/bar/baz/c"), "")
await fs.promises.writeFile(path.join(__dirname, "foo/bar/baz/exclude"), "")
let srcs = await readdirAll(path.join(__dirname, "foo"), [path.join(__dirname, "foo/bar/baz/exclude")])
srcs = srcs.map(src => path.relative(__dirname, src))
// prettier-ignore
expect(srcs).toEqual([
"foo/a",
"foo/bar",
"foo/bar/b",
"foo/bar/baz",
"foo/bar/baz/c",
])
await fs.promises.rmdir(path.join(__dirname, "foo"), { recursive: true })
}) Go implementationCode package main
import (
"io"
"io/fs"
"os"
"path/filepath"
)
type copyInfo struct {
source string
target string
}
func copyDir(src, dst string, excludes []string) error {
// Sweep for sources and targets
var infos []copyInfo
err := filepath.WalkDir(src, func(source string, entry fs.DirEntry, err error) error {
if err != nil {
return err
}
if entry.IsDir() {
return nil
}
for _, exclude := range excludes {
if source == exclude {
return nil
}
}
info := copyInfo{
source: source,
target: filepath.Join(dst, source),
}
infos = append(infos, info)
return nil
})
if err != nil {
return err
}
// Copy sources to targets
for _, info := range infos {
if dir := filepath.Dir(info.target); dir != "." {
if err := os.MkdirAll(dir, MODE_DIR); err != nil {
return err
}
}
source, err := os.Open(info.source)
if err != nil {
return err
}
target, err := os.Create(info.target)
if err != nil {
return err
}
if _, err := io.Copy(target, source); err != nil {
return err
}
source.Close()
target.Close()
}
return nil
} Of course you can use third-party packages -- I just like implementing these kinds of things from scratch so I can understand / debug them if needed. |
While working on my CDK integration of esbuild, I came across the need to copy other files to the output in addition to what esbuild produces. This would be similar to the
servedir
option on theserve
command, but for the build API.One basic use case was to copy an
index.html
file and some images into the right place, for a React app.I'm aware this might be possible already with loaders or superseded by an upcoming full-featured HTML integration. However I believe a simple interface like this, might still be useful as an alternative. It's also certainly not part of the core feature set, so I'd put in the quality-of-life category.
The text was updated successfully, but these errors were encountered: