-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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
[RFE] unbundled / preserveModules support #708
Comments
Seconding this. Especially for library modules (e.g. component libraries) it seems unnecessary to bundle everything into one file. I wonder what the use case for the current implementation of |
With this libraries that need tree-shaking would be able to use esbuild directly, right now you have to run it through either rollup or webpack to not have one big bundle. |
Just commenting as this would be a great addition to esbuild |
Hi @evanw, is there anything we can do to help with this? Another use case is building design systems with Vanilla Extract. Currently, all my CSS is bundled into one file. I'd much prefer if each component would be retained as its own file so that the CSS can be separated more easily. Then when you import a tree-shaken part of the library, you don't import unneeded CSS. |
@graup Tree-shaking has little relation with code splitting. I even doubt whether it's possible to apply vanilla-extract on thrid-party libraries.
|
@hyrious Thanks for your reply. I don't want to apply vanilla-extract to third-party libraries. I want my library to build all its ".css.ts" files to corresponding js and css, but avoid merging all css into one file so that in the app I can import only the css that's needed for the components that are actually imported. I was under the impression that esbuild bundling is what combines all css into one, which is why I thought |
I tried some different configurations in our component library by checking what a typical app would do with the output. Bundling everything to a single file apparently prevents a default Next.js 12 app from tree shaking unused code. The following setup works fine though. Using it through tsup so can't paste a raw esbuild config but it should be the same.
|
I will explain our use case here, perhaps it helps maintainers.
Exposing module graph post-build would allow us to solve all of that by invoking the compiler and our checks but also having an unbundled build will help us with the first two points |
In April, Snowpack was deprecated, and now there is no such possibility almost anywhere. This is the only feature we're missing in esbuild in order to migrate. It seems that even the simple possibility of giving the tree of resulting chunks before merging would solve most cases. |
Could you clarify this? |
@Conaclos It's been a while, but I think I meant that even if one were to list every source file as an entrypoint, causing esbuild to create one output file per input file, it wouldn't produce usable output, since the import paths wouldn't match the output directory layout. |
This can now be done with package.json exports and self-referencing, e.g.
It's a reasonable practice anyway - do you really want to expose all your internal util files to the world? This keeps the public API explicit |
@akvadrako I am using esbuild to transpile my TypeScript projects. I basically run the following command to compile my source files:
If I use a tests directory, then all test files import the code to test via a self package import: // test file
import * as myPackage from "my-package" By the way, it could be nice to have some option to build all files of a project. Something like:
|
Unfortunately, if you simply pass all files as entrypoints, then in the case of TS, for example, aliases will not work. Another problem is that dependencies from node_modules will not be glued together (I would like them to be in separate chunks). |
You mean using tsconfig's paths?
I am not sure to follow you there. You want to compile individually every source file, except node_modules dependencies that are bundled together? |
Why I'm monitoring this thread:
Not sure this is possible in esbuild, but it is how our https://github.com/elmsln/HAXcms/tree/master/build/es6/node_modules |
Yes, this is exactly the result I want to get. I designed a solution with this result, but it works slowly and unstable (for example, tracking changes):
As a result, I get the original structure, both for my sources and for node_modules. Nevertheless, it seems that it is better to support such functionality in the collector than to pervert with your own queue. |
I solve this with a small plugin that uses tsc-alias to fix path aliases in my transpiled (from TS) code: import esbuild from 'esbuild'
/** @type (any) => esbuild.Plugin */
const tscAliasPlugin = () => {
return {
name: 'tsc-alias',
setup(build) {
build.onEnd(async (_result) => {
await replaceTscAliasPaths({ outDir: './dist', resolveFullPaths: true })
})
},
}
} |
This is a critical issue in my case for esnext. This makes js and css file versioning impossible, if you only use esbuild to force browser to invalidate js and css cache after changed js and css source files. |
This is keeping my team from migrating to the much-faster esbuild for building our UI library. We currently use rollup because of the |
@arvindell There is a rollup plugin to use esbuild for compilation under the hood, and then you can still use rollup's |
Thank you so much, it works like a charm @elramus |
@nikolay-govorov can you share your plugin/workaround? |
Here's my version of the import * as esbuild from 'esbuild'
import * as nodePath from 'node:path'
export interface BuildOptions
extends Omit<
esbuild.BuildOptions,
| 'metafile'
| 'mangleCache'
| 'entryPoints'
| 'stdin'
| 'bundle'
| 'outbase'
| 'outExtensions'
> {
entryPoints: string[]
outbase: string
}
export interface BuildResult<
ProvidedOptions extends BuildOptions = BuildOptions,
> extends Omit<
esbuild.BuildResult<ProvidedOptions>,
'metafile' | 'mangleCache' | 'outputFiles'
> {
outputFiles: esbuild.OutputFile[]
}
export async function build<T extends BuildOptions>(
options: esbuild.SameShape<BuildOptions, T>
): Promise<BuildResult<T>> {
const result: BuildResult<T> = {
errors: [],
warnings: [],
outputFiles: [],
}
const allEntryPoints = new Set(options.entryPoints)
let entryPoints = options.entryPoints.map(
(entryPoint) => ({
in: entryPoint,
out: nodePath.relative(options.outbase, entryPoint),
})
)
while (entryPoints.length) {
const newEntryPoints: { in: string; out: string }[] = []
const plugin: esbuild.Plugin = {
name: 'buildModules',
setup(build) {
build.onResolve({ filter: /.*/ }, async (args) => {
if (args.pluginData === true) {
return undefined
}
const resolveResult = await build.resolve(
args.path,
{
importer: args.importer,
namespace: args.namespace,
resolveDir: args.resolveDir,
kind: args.kind,
pluginData: true,
}
)
if (
!resolveResult.errors.length &&
!resolveResult.external &&
['import-statement', 'dynamic-import'].includes(
args.kind
)
) {
if (!allEntryPoints.has(resolveResult.path)) {
newEntryPoints.push({
in: resolveResult.path,
out: nodePath.relative(
options.outbase,
resolveResult.path
),
})
allEntryPoints.add(resolveResult.path)
}
const relativePath = `${nodePath.relative(
nodePath.dirname(args.importer),
resolveResult.path
)}.mjs`
return {
...resolveResult,
path: relativePath.startsWith('.')
? relativePath
: `./${relativePath}`,
namespace: 'buildModules',
external: true,
}
} else {
return resolveResult
}
})
},
}
const moduleResult = await esbuild.build({
...options,
bundle: true,
entryPoints,
outExtension: { ['.js']: '.mjs' },
plugins: [...(options.plugins ?? []), plugin],
})
result.errors.push(...moduleResult.errors)
result.warnings.push(...moduleResult.warnings)
result.outputFiles.push(
...(moduleResult.outputFiles ?? [])
)
entryPoints = newEntryPoints
}
return result
} |
Generate an entry point for each file and then set bundle:true would do what expected? example import glob from 'glob';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
esbuild.build({
entryPoints: Object.fromEntries(
glob.sync('src/**/*.ts').map(file => [
// This remove `src/` as well as the file extension from each
// file, so e.g. src/nested/foo.js becomes nested/foo
path.relative(
'src',
file.slice(0, file.length - path.extname(file).length)
),
// This expands the relative paths to absolute paths, so e.g.
// src/nested/foo becomes /project/src/nested/foo.js
fileURLToPath(new URL(file, import.meta.url))
])
),
format: 'es',
outDir: 'dist’,
loader:{
".svg":"dataurl"
}
}); Update: I managed to have something close of what I want(having a treeshakable output) with this configuration. esbuild.build({
entryPoints: ['src/main/**/*.ts'],
entryNames: '[dir]/[name]',
outbase: "src/main",
splitting: true,
bundle: true,
minify: false,
minifyIdentifiers: true,
minifySyntax: true,
minifyWhitespace: false, // this is important otherwise we're gonna loose PURE annotations
outdir: OUT_DIR_ESM,
}) Problem is that in a lot of files I have some bar imports like this. Since I have ` import "./chunks/chunk-FQDWJHHW.js"; export { |
thanks for the code @dcecile ! from my testing:
We are once again running into issues with this because dynamic imports get rolled up into the bundle that starts not as es module (a worker). We would like to once again bring it to the attention of the maintainers that without both unbundled output and splitting it is very hard to use esbuild |
I accomplish to maintain files/folder structure with a glob pattern on await build({
entryPoints: ["src/**/*.ts"],
bundle: false,
...
}); |
I quite like the design and speed of esbuild, but I found that when I use it with
bundle: false
it doesn't do what I would expect. The dependencies of entrypoints are not built unless they are also listed as entrypoints. Even if the deps are listed as entrypoints, the import paths are not adjusted so they won't work.So it would be nice if esbuild
bundle: false
worked like rollup's preserveModules.The text was updated successfully, but these errors were encountered: