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

Support outfile as a function #553

Open
silvenon opened this issue Nov 21, 2020 · 15 comments
Open

Support outfile as a function #553

silvenon opened this issue Nov 21, 2020 · 15 comments
Labels

Comments

@silvenon
Copy link

silvenon commented Nov 21, 2020

In my case I would like to write multiple entry points as revisioned paths with rev-path, but my understanding is that this currently isn't possible. My proposition is that the outfile can also be a function, which would then work for multiple entry points (where a string outfile only works for one). That function would receive file name and contents so I can output a revisioned file name based on its contents.

The only alternative I can see is renaming the path after already writing the file, but I'd like to do it all in one go if possible. 🤞

@evanw
Copy link
Owner

evanw commented Nov 24, 2020

In the past I have thought about adding a plugin callback for when a file is emitted, which is sort of like what you're asking for. It would run after the code generation has finished but before the file is written to the file system. If the file is a code splitting chunk, the code generation for other files which import that chunk would potentially be blocked until the plugin completes since the chunk's file path is baked into the import statements in the importing files. I was thinking of an API sort of like this:

let examplePlugin = {
  name: 'example',
  setup(build) {
    build.onEmit({ filter: /something/ }, args => {
      return { path: args.path, contents: args.contents }
    })
  },
}

Using a plugin for this instead of outfile would be more consistent with the other types of callbacks (load and resolve). It would also be more flexible because you could support multiple callbacks simultaneously with different purposes.

@lukeed
Copy link
Contributor

lukeed commented Nov 24, 2020

This could be solved by #518, or replace it – though ideally it's a built-in option.

@silvenon
Copy link
Author

silvenon commented Nov 24, 2020

Either way sounds good, having onEmit will surely be useful for other things, too, but having the content has feature built-in would be nice, as proposed in #518.

@remorses
Copy link
Contributor

I like the onEmit solution, another thing i would like to see is the ability to emit multiple files for a single entry file.

I am building an html plugin here and i need a way to emit the html code together with the js extracted from the html script tags

Something like this would be perfect

let htmlPlugin = {
    name: 'example',
    setup({ onEmit, onLoad }) {
        onEmit({ filter: /\.html/ }, (args) => {
            const htmlPath = args.path
            const jsPath = args.path + '.js'
            const scriptSrc = '/' + path.basename(jsPath)
            return [
                { path: jsPath, contents: args.contents },
                {
                    path: htmlPath,
                    content: `
                    <html>
                        <body>
                            <script src="${scriptSrc}" type="module"></script>
                        </body>
                    </html>
                    `,
                },
            ]
        })
        onLoad({ filter: /\.html$/ }, async (args) => {
            const html = await (
                await fs.promises.readFile(args.path, {
                    encoding: 'utf-8',
                })
            ).toString()

            const jsUrls = await getHtmlScriptsUrls(html)
            const contents = jsUrls
                .map((importPath) => `import '${importPath}'`)
                .join('\n')

            let resolveDir = path.dirname(args.path)

            return {
                loader: 'js',
                contents,
                resolveDir,
            }
        })
    },
}

@lahmatiy
Copy link

I believe we need more control over destination file paths, especially in complex cases. For instance, I've got an error Two output files share the same path but have different contents: out/foo.js when I do something like this:

require('esbuild').build({
    entryPoints: [
        'a/foo.js',
        'b/foo.js'
    ],
    outdir: 'out',
    plugins: [{
        name: 'example',
        setup({ onResolve, onLoad }) {
            onResolve({ filter: /.*/ }, args => ({
                namespace: 'example',
                path: args.path
            }));
            onLoad({ namespace: 'example', filter: /.*/ }, args => ({
                contents: 'console.log(' + JSON.stringify(args.path) + ')'
            }));
        }
    }]
})

Probably that's a bug (Should I fill a separate issue?). Anyway, in my case, I want to get a.js and b.js in out instead of a/foo.js and b/foo.js as per input. Current workaround is to use write:false with build() and then write result contents to files with desired paths. Alternative solution is to allow esbuild to write files and then move (rename) them. The second approach involves more FS ops, but faster in case of multiple entry points. Because writes to files happen as content is ready and with write:false we await until all contents are ready before start to write to files (not sure, but probably something else takes extra time).

Suggested solution with onEmit() looks good to me. However, it seems like two roles mixed up and it should be decouple into 2 methods which compliment to existing methods:

  • First method to expose the final path of a file, as opposite to onResolve() – let say it'll be onEmit(); it should be used before output contents serialising. For hashing purposes the method may take a lazy initiating dictionary from args with modules sources used to form that file (before the external import paths are substituted).
  • Another one to expose contents, as opposite to onLoad() – let say it'll be onWrite(); should be used before "write" to a file, that probably writes a resulting contents to a file as is. However, that's a controversial opportunity since not play well with source maps and banner/footer may be better choice instead of contents (the same for onLoad).

@evanw
Copy link
Owner

evanw commented Feb 23, 2021

For hashing purposes

Hashing is kind of complicated because of import cycles. Heads up that I am currently planning to use the algorithm described here: #518 (comment). Each output file is assigned a temporary identifier, then each output file is generated with the temporary identifiers of other modules used for import paths, then any onEmit plugins will run, then the final hashes will be calculated and the output files will be written to disk. If a plugin wants for the name to include a hash, I plan to support the [hash] placeholder in the file name. I'm not planning on including an onWrite API because you can just do write: false and get back all of the files anyway.

@mreinstein
Copy link

mreinstein commented Aug 6, 2021

I'm definitely missing some ability to change the output path. My workflow looks like this:

  1. find all entry point scripts: const entryPoints = glob.sync("**/*.main.js", { cwd: './src' })
  2. pass them to esbuild, putting the resulting bundles in build/
  3. strip .main out of the output files (build/sample.main.js -> build/sample.js)

currently there's no way to do (3) in the esbuild process, as far as I can tell. esbuild will emit something like build/myfile.main.js and I can only update the extension, not remove or change the rest of the filename.

@bgradin
Copy link

bgradin commented Aug 25, 2021

+1 for onEmit. Is this planned? What's the latest?

@bgradin
Copy link

bgradin commented Aug 30, 2021

PS - a note for anyone who finds this thread and like me, needs outputs relative to the entrypoint source files, there is a way to do this. For an arbitrary path like src/many/intervening/directories/module/src/foo.ts, to output the artifact in src/many/intervening/directories/module/js/foo.js:

esbuild
  .build({
    bundle: true,
    entryPoints,
    outdir: "src",
    outbase: "src",
    entryNames: "[dir]/../js/[name]",
  });

@bgradin
Copy link

bgradin commented Aug 30, 2021

PPS - onEmit would still be very nice, because currently my solution doesn't work if you need separate output directories for JS and CSS artifacts 🙃

@mreinstein
Copy link

It's an interesting workaround, but it doesn't help in my build pipeline because I need to modify part of the filename to strip out some characters.

onEmit or some similar handler is the last remaining thing I need before I can realistically use esbuild at work.

@mreinstein
Copy link

This issue is more important than yarn support. Just sayin' ;)

@bjesuiter
Copy link

I really like this idea of an onEmit function.

My problem:
I'm writing a single page application which has code for multiple clients inside it.
I must make sure that js files from one client cannot be loaded by another client, so I have an nginx rule which checks some hashes in the file names currently to validate if the current session id is allowed to load this js bundle.

=> Problem: if my files all have [name]-[hash] format, i cannot distinguish them enough.
I want to change the filename/output path based on input path.

So for example: all files from
'apps/my-app/src/protected/hash-of-client-name/' get into one /protected directory and the hash-of-client-name goes into each emitted file from below that folder.

Then i can have two nginx rules:

  1. Files from /protected can only be accessed with a valid session.
  2. Files with the customer name hash inside can only be accessed by this customer.

@magoniac
Copy link

magoniac commented Apr 6, 2024

As a total newbie and having only two weeks experience transferring to Esbuild, the onEmit() feature looks very promising still for my purposes. @evanw has been very clear in his vision concerning Esbuild future once it is stabilized. However onEmit has been on the radar for quite some time, so the question is if it is still to be implemented or not - would really appreciate some status update. Regards and thanks for your efforts.

@bjesuiter
Copy link

I would like to bring this feature back to attention since it really blocks us from moving to angular 18s app builder based on esbuild and vite, due to security issues.

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

No branches or pull requests

9 participants