esbuild
esbuild copied to clipboard
can add support for __dirname and __filename?
example:
file1.js
import { basename } from 'path';
export const filename = basename(__filename);
file2.js
import { filename } from './file1';
console.log({ filename });
build:
esbuild --bundle --platform=node --outfile=build.js file2.js
and run:
node build.js
expect output:
{ filename: 'file1.js' }
actual output:
{ filename: 'build.js' }
the file 'build.js' looks like this:
// internal helpers...
// file1.js
var import_path = __toModule(require("path"));
var filename = import_path.basename(__filename);
// file2.js
console.log({filename});
why not replace __filename with the path to the file?
// internal helpers...
// file1.js
var import_path = __toModule(require("path"));
var filename = import_path.basename("/path/to/original/file1.js");
// file2.js
console.log({filename});
This also applies to import.meta.url.
It's possible to implement this with a plugin. You can write an on-load plugin that injects var __filename = "...some string..." at the top.
This also applies to
import.meta.url.
There are legitimate use cases for referencing the final location of the bundle instead of the location of the source code. For example, it's a common practice to use new URL(path, import.meta.url) to get a path relative to the final location of the bundle. Replacing import.meta.url with the location of the source code would break this common pattern. See also #795 and https://github.com/evanw/esbuild/issues/208#issuecomment-652691781.
maybe add native support for this and add option?
I think my preferred solution for this is going to be to use the --define feature in addition to adding the ability for plugins to add additional per-file configuration of input flags such as --define. This seems more general and appropriately minimal (not really adding more features, but just combining two existing features). This is not possible at the moment because plugins do not yet have the ability to configure these input flags per-file.
This is a highly custom feature request so using a plugin for this instead of having it be built in seems like the way to go. It may seem like this is a simple request. However, replacing import.meta.url with a string will likely break a lot of code because more and more code will be using the pattern in #795, so you will probably need the ability to only do this for certain files according to custom project-specific rules. Also different bundlers do this differently. Webpack replaces __filename with something like /index.js while Parcel replaces __filename with something like /home/user/dev/project/index.js and the decision between them seems arbitrary. Further, there is likely code that expects to be able to use __filename and/or import.meta.url to get at the path of the final bundle. Using a plugin gives the user control instead of having esbuild pick a side.
but __dirname and __filename have a specification:
- https://nodejs.org/api/modules.html#modules_dirname
- https://nodejs.org/api/modules.html#modules_filename
can implement it if target is a node?
I think this should be a plugin or an opt-in. I personally wouldn't want a bundler to replace __dirname and __filename with absolute paths by default even if the target was node. The directory in which one bundles and minifies something typically has no relation to where it is deployed.
I just ran into this myself. As someone who is experimenting in Node.js, it makes sense that __dirname, etc. should ‘just work’. But from a purely JS standpoint, this is an abstract global variable. In Chrome for example, __dirname doesn’t mean anything.
For my use case, this solved my problem: path.join(process.cwd(), src). From what I can tell this is simply the programmatic implementation of __dirname, but I could be wrong.
@mulfyx makes a good point but I agree with @kzc that this should probably be implemented in userland / as a plugin. The only problem about defining it as a side-effect of "node" is that suddenly users can weird builds where things stop working because they toggled the target field. Side-effects are great when you understand them and super confusing when you don’t. So I think this would probably just lead to confusion most of the time. And JS can already be pretty confusing.
Maybe esbuild should have a few ‘official, first-party’ plugins that fills this need so that the community doesn’t have to worry about this in the future?
For my use case, this solved my problem:
path.join(process.cwd(), src). From what I can tell this is simply the programmatic implementation of__dirname, but I could be wrong.
This only works as long as the application is launched at the root of the project.
This is what i did.
This plugin replaces __dirname and/or __filename with the correct values
const fs = require("fs");
const path = require("path");
const nodeModules = new RegExp(/^(?:.*[\\\/])?node_modules(?:[\\\/].*)?$/);
const dirnamePlugin = {
name: "dirname",
setup(build) {
build.onLoad({ filter: /.*/ }, ({ path: filePath }) => {
if (!filePath.match(nodeModules)) {
let contents = fs.readFileSync(filePath, "utf8");
const loader = path.extname(filePath).substring(1);
const dirname = path.dirname(filePath);
contents = contents
.replace("__dirname", `"${dirname}"`)
.replace("__filename", `"${filePath}"`);
return {
contents,
loader,
};
}
});
},
};
exports.default = dirnamePlugin;
Thanks @richarddd
Also if you are using ESbuild for Severless applications
You will have to update dirname to match the lambda execution environment
const dirname = path.dirname(filePath).replace(__dirname, "/var/task/");
The big problem with using an onLoad plugin for doing this or injecting other things is that it alters the source map output. There should be an option to add per-file banner or defines that is ignored generating the source map
FWIW, es modules in node don't support __dirname or __filename either, with import.meta.url as the recommended alternative. https://nodejs.org/api/esm.html#esm_no_filename_or_dirname
import.media.url also doesn't work correctly
Someone created a plugin just for this: https://github.com/martonlederer/esbuild-plugin-fileloc
but it does not work for lambda.
I will publish the little thing @richarddd posted as it's own NPM module today, will try to get it merged into https://github.com/floydspace/serverless-esbuild as well.
The directory in which one bundles and minifies something typically has no relation to where it is deployed.
maybe it is possible to transform it into paths related to actual file directory?
e.g.
esbuild --bundle foo.js --platform=node --outfile=foo.bundle.js
// foo.js
require('./bar/baz.js')
console.log(__dirname) // keep using __dirname as it's the same dir of entrypoint
// bar/baz.js
console.log(__dirname) // => transform __dirname to (__dirname+'/bar')
And I think that satisfies most situations. (Maybe you are using something like readFile(__dirname+'/../../../example.txt') ?)
I mean, at least this makes relative path correct and won't break original __dirname usage.
This is what i did.
This plugin replaces __dirname and/or __filename with the correct values
const fs = require("fs"); const path = require("path"); const nodeModules = new RegExp(/^(?:.*[\\\/])?node_modules(?:[\\\/].*)?$/); const dirnamePlugin = { name: "dirname", setup(build) { build.onLoad({ filter: /.*/ }, ({ path: filePath }) => { if (!filePath.match(nodeModules)) { let contents = fs.readFileSync(filePath, "utf8"); const loader = path.extname(filePath).substring(1); const dirname = path.dirname(filePath); contents = contents .replace("__dirname", `"${dirname}"`) .replace("__filename", `"${filePath}"`); return { contents, loader, }; } }); }, }; exports.default = dirnamePlugin;
Thanks a lot for this! Solved a problem I was having with postgres-migrations
My esbuild.ts now looks like:
import { build, Loader, PluginBuild } from 'esbuild';
import { readFileSync } from 'fs';
import { extname, dirname as _dirname } from 'path';
const nodeModules = new RegExp(
/^(?:.*[\\/])?node_modules(?:\/(?!postgres-migrations).*)?$/
);
const dirnamePlugin = {
name: 'dirname',
setup(build: PluginBuild) {
build.onLoad({ filter: /.*/ }, ({ path: filePath }) => {
if (!filePath.match(nodeModules)) {
let contents = readFileSync(filePath, 'utf8');
const loader = extname(filePath).substring(1) as Loader;
const dirname = _dirname(filePath);
contents = contents
.replace('__dirname', `"${dirname}"`)
.replace('__filename', `"${filePath}"`);
return {
contents,
loader,
};
}
});
},
};
void build({
entryPoints: ['src/index.ts'],
bundle: true,
minify: true,
sourcemap: true,
outfile: 'dist/index.js',
platform: 'node',
format: 'cjs',
external: ['pg-native'],
plugins: [dirnamePlugin],
});
@mishabruml Thanks for the snippet, replace() only replaces the first occurence though. replaceAll() fixes that:
const dirnamePlugin = {
name: "dirname",
setup(build) {
build.onLoad({ filter: /.*/ }, ({ path: filePath }) => {
if (!filePath.match(nodeModules)) {
let contents = fs.readFileSync(filePath, "utf8");
const loader = path.extname(filePath).substring(1);
const dirname = path.dirname(filePath);
contents = contents
.replaceAll("__dirname", `"${dirname}"`)
.replaceAll("__filename", `"${filePath}"`);
return {
contents,
loader,
};
}
});
},
};
The plugin approach did not work for me. __dirname remained in bundled output — and was getting quietly resolved by Node, ofcourse to a different value than the source was expecting.
In this particular case, I must also use thomaschaaf/esbuild-plugin-tsc — because this project uses decorators which esbuild refuses to support. Perhaps there was some interference between the 2 plugins.
I ended up ditching the dirnamePlugin idea; and instead reworked the source, so that it addresses assets by dist-relative paths, rather than source-relative.
This is of course very annoying, as esbuild seems to break a well-specced and widely (mis)used builtin feature of Node.
Note that the direnamePlugin solution proposed above will not work in synchronous mode, esbuild will not accept plugins in synchronous mode and will fail with the following error:
Cannot use plugins in synchronous API calls