esbuild icon indicating copy to clipboard operation
esbuild copied to clipboard

[Bug] Option `--public-path` doesn't work when using esbuild for building libraries

Open brillout opened this issue 2 years ago • 6 comments

As the author of a library, I'd like to use esbuild to build the following:

import iconCalendar from './icons/calendar.svg'
import timeUtils from './utils/time.js'

/* ... */

Into:

var iconCalendar = '/assets/icons/calendar-RT6VWYUT.svg';
import timeUtils from './utils/time-3GRCYC2A.js'; // Relative path

But, if I use --public-path=/assets/, I get this instead:

var iconCalendar = '/assets/icons/calendar-RT6VWYUT.svg';
import timeUtils from '/assets/utils/time-3GRCYC2A.js'; // Absolute path

Which breaks my library.

I understand the current behavior works for end-user apps, since utils/time-3GRCYC2A.js is indeed served at /assets/utils/time-3GRCYC2A.js. But that isn't the case for libraries, because the user of the library may use a tool like Next.js that won't be able to process /assets/utils/time-3GRCYC2A.js. My users's tools (Next.js, Vite, Parcel, ...) need the relative JavaScript paths to be preserved.

In a nutshell: if the target is the browser, then the current behavior makes sense. But, if the target is the user app (and therefore Next.js, Vite, Parcel, ...), then the current behavior is broken.

brillout avatar Mar 10 '23 12:03 brillout

+1

jeremysf avatar Jul 27 '23 00:07 jeremysf

Sorry, I'm confused. You didn't provide a way to reproduce your issue. How are you generating the code in your post? Marking this issue as unactionable because it doesn't have reproduction instructions.

evanw avatar Jun 27 '24 18:06 evanw

A reproduction: https://github.com/brillout/esbuild-reprod-2707. This is the same root cause: only static asset import paths should be transpiled to absolute paths, while JavaScript import paths should be kept relative.

Related feature request: https://github.com/evanw/esbuild/issues/2707. It's a feature request for addressing this issue.

brillout avatar Jun 27 '24 18:06 brillout

Thanks. I can observe the same behavior as you. I put the repro into the playground here, for my reference: (link).

I guess I still don't understand what you're doing. You're writing a library that is going to be bundled by users using a tool of their choice, which could all be wildly different and which understandably needs relative paths to work. But then it seems like you want to use --public-path for your assets instead of relative URLs, even though I'd think that same logic would apply to assets as well (the user's tools needing relative paths instead of absolute paths, since they aren't themselves hosting your stuff at specific absolute paths). Why are you using --public-path in this situation (for a library and not for an end-user app)?

evanw avatar Jun 28 '24 02:06 evanw

Yes, without SSR, it isn't an issue because the user's bundler (Vite/webpack/...) transpiles the library code (i.e. the code living at /node_modules/my-library/**/*).

But, with SSR and Vite, library code isn't transpiled: by default Vite externalizes dependencies on the server-side. This means that the library code (/node_modules/my-library/**/*) is directly executed by Node.js (without any Vite transpilation).

In that scenario, it's a good thing that asset imports are transpiled:

- import iconCalendar from './icons/calendar.svg'
+ var iconCalendar = '/assets/icons/calendar-RT6VWYUT.svg';

Because Node.js cannot import .svg files and would crash otherwise.

But for JavaScript imports, it's a bad thing as the following makes Node.js crash:

- import timeUtils from './utils/time.js'
+ import timeUtils from '/assets/utils/time-3GRCYC2A.js';

Node.js cannot import /assets/utils/time-3GRCYC2A.js and crashes.

Thus, the requirement of only setting the public base for asset imports while leaving JavaScript imports untouched.

brillout avatar Jun 28 '24 06:06 brillout

I understand that you're looking for a way to unify the code style to reference assets in js modules for both browser and Node.js to load. The problem here is that there's no such behavior specified in JavaScript language, nor the same API to use in every environment. import('/path/to/file.svg') is also not supported by browsers.

Your proposed solution was to only prepend absolute paths to assets. However that won't solve your initial problem of making your library work in Vite SSR build since the absolute path does not actually point to the file when resolved from the root of the Vite project:

// /node_modules/your-lib/index.js
var file = '/assets/file.svg'
export var Icon = h('img', { src: file }, null)

// /src/index.jsx
import {Icon} from 'your-lib'
render(<Icon />, document.body)

Note that the actuall file.svg is at /node_modules/your-lib/assets/file.svg. Result:

<img src="/assets/file.svg">  <!-- 404 -->

This is because when the file becomes a string in your js module, it loses the information that it links an external file. No bundler could recognize that fact and copy the file to the correct location.


There're some other actions you may take:

  • For SVG files, you can bundle them in your js modules with --loader:.svg=dataurl. So there will be no external asset files.
    • If you are working with JSX, you can embed you SVG elements in JSX directly.
  • If you're dealing with some large assets like wasm files, where it is better to load them separatedly. You can construct or require the URL to the asset. For example esbuild-wasm requires you to run esbuild.initialize({ wasmURL }) to help it find the file.

hyrious avatar Jun 29 '24 15:06 hyrious