Skip to content
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

Closed
mrgrain opened this issue Mar 16, 2021 · 3 comments
Closed

Feature idea: copydir option #982

mrgrain opened this issue Mar 16, 2021 · 3 comments

Comments

@mrgrain
Copy link

mrgrain commented Mar 16, 2021

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 the serve 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.

@evanw
Copy link
Owner

evanw commented Mar 18, 2021

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.

@zaydek
Copy link

zaydek commented Mar 19, 2021

@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 implementation

Code

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.

@mrgrain
Copy link
Author

mrgrain commented Mar 19, 2021

@evanw @zaydek Yes, I can totally understand that perspective. 😃

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants