evanw / esbuild

An extremely fast bundler for the web
https://esbuild.github.io/
MIT License
38.17k stars 1.15k forks source link

feature: single banner/footer per compiled file #3517

Open dalechyn opened 11 months ago

dalechyn commented 11 months ago

As of now, esbuild inserts a banner before every built file even if it's bundled within a single chunk.

In my case, I insert the use client directive, and it's being inserted multiple times within a single chunk, which leads to the next error when my package is used in nextjs:

ReactServerComponentsError:

The "use client" directive must be placed before other expressions. Move it to the top of the file to resolve this issue.

My proposal is to extend the BannerOrFooter to accept a new option single: boolean which would default to false to ensure backwards compatibility, yet that would insert a banner or a footer only once in a compiled file.

...
banner: {
   js: '"use client";',
   single: true
},
...
dalechyn commented 11 months ago

This is the workaround I'm currently using:

    // banner: {
    //   js: '"use client";',
    // },
    // Hack to avoid multiple `use client` directives in single file instead of using banner.
    // Delete when implemented https://github.com/evanw/esbuild/issues/3517
    async onSuccess() {
      // recursively go through each js file in dist and add "use client" to the top
      const distDir = path.join(__dirname, 'dist')
      const files = await fs.readdir(distDir)

      async function processFilesRecursively(dir: string) {
        // Read dirents of the current folder
        const dirents = await fs.readdir(dir, { withFileTypes: true })
        const promises = dirents.map(async (dirent) => {
          const filePath = dirent.path + '/' + dirent.name
          if (dirent.isDirectory()) return processFilesRecursively(filePath)

          if (dirent.name.endsWith('.js')) {
            const fileContents = await fs.readFile(filePath, 'utf8')
            const newFileContents = `'use client'\n${fileContents
              .replaceAll('"use client"', '')
              .replaceAll("'use client'", '')}`
            await fs.writeFile(filePath, newFileContents)
          }
        })
        return Promise.all(promises.flat())
      }
      await processFilesRecursively(distDir)
    },