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

can add support for __dirname and __filename? #859

Open
mulfyx opened this issue Feb 21, 2021 · 20 comments
Open

can add support for __dirname and __filename? #859

mulfyx opened this issue Feb 21, 2021 · 20 comments

Comments

@mulfyx
Copy link

mulfyx commented Feb 21, 2021

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});
@negezor
Copy link

negezor commented Feb 21, 2021

This also applies to import.meta.url.

@evanw
Copy link
Owner

evanw commented Feb 21, 2021

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 #208 (comment).

@mulfyx
Copy link
Author

mulfyx commented Feb 21, 2021

maybe add native support for this and add option?

@evanw
Copy link
Owner

evanw commented Feb 23, 2021

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.

@mulfyx
Copy link
Author

mulfyx commented Feb 23, 2021

but __dirname and __filename have a specification:

can implement it if target is a node?

@kzc
Copy link
Contributor

kzc commented Feb 26, 2021

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.

@zaydek
Copy link

zaydek commented Mar 4, 2021

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?

@negezor
Copy link

negezor commented Mar 5, 2021

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.

@richarddd
Copy link

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;

@dtruong0
Copy link

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/");

@SalvatorePreviti
Copy link
Contributor

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

@bschlenk
Copy link

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

@mulfyx
Copy link
Author

mulfyx commented Jun 16, 2021

import.media.url also doesn't work correctly

@capaj
Copy link

capaj commented Dec 16, 2021

Someone created a plugin just for this:
https://github.com/martonlederer/esbuild-plugin-fileloc

but it does not work for lambda.

@capaj
Copy link

capaj commented Dec 22, 2021

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.

@undefined-moe
Copy link

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.

@mishabruml
Copy link

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],
});

@hpohlmeyer
Copy link

@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,
        };
      }
    });
  },
};

@ulidtko
Copy link

ulidtko commented Mar 22, 2024

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.

@gsouf
Copy link

gsouf commented Mar 31, 2024

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

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

No branches or pull requests