All of these edge cases come into play when the shims end up in a different chunk than they're used in.
When outputting a ESM bundle with shims enabled:
__filename
will never be correct as the import.meta.url
will be the chunk it's exported from, not the one it's used in.__dirname
will be incorrect if the chunk containing the shims is in a different directory than the chunk importing the shims.dist
βββ a
β βββ index.js // <- __filename will be /dist/chunk-with-shims.js, not /dist/a/index.js
βββ b
β βββ index.js // <- if the __dirname shims are imported in this chunk, it will be /dist, not /dist/b
βββ c
β βββ index.js
βββ chunk-with-shims.js
βββ ...
For the ESM shims, import.meta.url
will never be correct if the shims are in a chunk.
dist
βββ a
β βββ index.js
βββ b
β βββ index.js
βββ c
β βββ index.js
βββ chunk-with-shims.js <- the importMetaUrl export uses __filename, which will always be dist/chunk-with-shims.js
βββ ...
var _shimChunk = require('../chunk-with-shims.js');
var _fs = require('fs');
var _fs2 = _interopRequireDefault(_fs);
// This is adapted from https://nodejs.org/api/esm.html#importmetaurl
// _shimChunk.importMetaUrl will be the __filename of `../chunk-with-shims.js`, not this file
const buffer = _fs2.default.readFileSync(new URL('./data.proto', _shimChunk.importMetaUrl));
One impact of this, is that it breaks checking whether a module is main
. https://exploringjs.com/nodejs-shell-scripting/ch_nodejs-path.html#detecting-if-module-is-main
import * as url from 'node:url';
if (import.meta.url.startsWith('file:')) { // (A)
// import.meta.url will be wrong if the shim ends up in a chunk, and this
// will never run when called from a shell.
const modulePath = url.fileURLToPath(import.meta.url);
if (process.argv[1] === modulePath) { // (B)
// Main ESM module
}
}
The ESM/CJS shims should be injected into each chunk that requires them, rather than using the inject
feature of esbuild
as it's currently done. https://github.com/egoist/tsup/blob/v7.1.0/src/esbuild/index.ts#L220
This could be done with an ESBuild plugin, which modifies the output chunks after the build, or as a tsup plugin.
This is a pretty hacky plugin I whipped up to replace the tsup
shims when building ESM bundles. Having poked around the code base, (and seeing how tsup plugins work), this likely breaks sourcemaps, but they aren't turned on in our project.
import { Options } from "tsup";
import * as path from "path";
import * as fs from "fs";
// https://stackoverflow.com/a/51399781
type ArrayElement<ArrayType extends readonly unknown[]> =
ArrayType extends readonly (infer ElementType)[] ? ElementType : never;
type EsbuildPlugin = ArrayElement<NonNullable<Options["esbuildPlugins"]>>;
const CjsShimPlugin: EsbuildPlugin = {
name: "cjs-shim-plugin",
setup(build) {
build.onEnd((result) => {
const filesNeedingShims =
result.outputFiles?.filter(
(file) => file.text.match(/__dirname|__filename/) !== null
) || [];
filesNeedingShims.map((file) => {
const dirnameShim = `
import { fileURLToPath as fileURLToPathShimImport } from "node:url";
import { dirname as dirnamePathShimImport } from "node:path";
const __filename = fileURLToPathShimImport(import.meta.url);
const __dirname = dirnamePathShimImport(__filename);
`;
const shimBuffer = Buffer.from(dirnameShim);
file.contents = Buffer.concat([shimBuffer, file.contents]);
});
});
},
};
export default CjsShimPlugin;
Pay now to fund the work behind this issue.
Get updates on progress being made.
Maintainer is rewarded once the issue is completed.
You're funding impactful open source efforts
You want to contribute to this effort
You want to get funding like this too