evanw / esbuild

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

[Feature request] Specify sourceURL in build without stdin #3944

Open NotNite opened 5 days ago

NotNite commented 5 days ago

Hi! I noticed some weird issues with source maps and error stacktraces when using executeJavaScript in the Electron APIs. This is because this is effectively the same action as pasting the passed code into DevTools.

Take the following code (adapted from my own build scripts):

// build.js
import * as esbuild from "esbuild";

/** @type {import("esbuild").BuildOptions} */
const esbuildConfig = {
  entryPoints: ["./input.js"],
  outfile: "./output.js",
  sourcemap: "inline"
};

await esbuild.build(esbuildConfig);
// input.js
try {
  throw new Error("Test error");
} catch (e) {
  console.log(e);
}

This produces the input verbatim, plus a //# sourceMappingURL, which is expected. But, when running this code in my program with executeJavaScript, I get the following stacktrace:

Error: Test error
    at <anonymous>:2:9
    (my code that ran executeJavaScript follows, redacted for brevity)

Anonymous! Not great. The source map seems to not be loading.

Now, if I add the line //# sourceURL=blah.js to the end of my output script:

Error: Test error
    at input.js:2:9
    (my code that ran executeJavaScript follows, redacted for brevity)

There's some weird behavior going on here where specifying a sourceURL in DevTools causes it to load the source map, even if the provided URL is garbage. I don't entirely know what causes this, but TL;DR I want to be able to specify sourceURL in build without using stdin.

The following does not work:

// build.js
import * as esbuild from "esbuild";

/** @type {import("esbuild").BuildOptions} */
const esbuildConfig = {
  entryPoints: ["./input.js"],
  outfile: "./output.js",
  sourcemap: "inline",
  sourcefile: "blah.js"
};

await esbuild.build(esbuildConfig);

...because it expectedly errors with Invalid option in build() call: "sourcefile", because this isn't supported by esbuild.

hyrious commented 5 days ago

TIL, and it can be reproduced without electron because executeJavaScript() is effectively calling eval():

eval(`throw new Error("1");
//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsiaW5wdXQuanMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbInRocm93IG5ldyBFcnJvcignMScpIl0sCiAgIm1hcHBpbmdzIjogIkFBQUEsTUFBTSxJQUFJLE1BQU0sR0FBRzsiLAogICJuYW1lcyI6IFtdCn0K
//# sourceURL=blah.js`)

This behavior is also documented in the [sourcemap spec](https://sourcemaps.info/spec.html#:~:text=If%20the%20generated%20code%20is%20being%20evaluated%20as%20a%20string%20with%20the%20eval()%C2%A0function%20or%20via%20new%20Function()%2C%20then%20the%20source%20origin%20will%20be%20the%20page%E2%80%99s%20origin).

So it is not devtools "not loading sourcemap" in the first case, but it doesn't accept the "source" in the sourcemap url when it is passed into eval(). However, when you add another //# sourceURL=, the spec says it should respect that config. Although there're 2 possible sources, the inlined one seems more correct.

There's a webpack plugin EvalSourceMapDevToolPlugin demonstrating this usage. This is reasonable since webpack by default uses eval() in development build.

I don't think esbuild's sourcefile option is a right place to do this work, since it only means to let transform() know the file name and generates correct sourcemap. However it leaves to Evan to make the decision.

On the other hand, you may just append this line by yourself before feeding it to executeJavaScript().

NotNite commented 5 days ago

I'm currently adding it to the output file with footer, which seems to work for me. I won't close this just yet because this may be useful to someone else, but feel free to close yourself if the feature isn't wanted.

evanw commented 2 days ago

The footer option sounds like an appropriate way to do this from within esbuild. However, it doesn't even sound like you need esbuild for this. I assume you could also just do executeJavaScript(code + '\n//# sourceURL=code.js') if that's even more convenient. I'd expect either way to work fine.