'Isomorphic' __dirname in ESM and CommonJS
Asked Answered
P

2

6

I maintain a library, written in Typescript and I want to target both CommonJS and ESM. To do this I run the build twice. This requires me to write code (in Typescript) that's valid for both.

One issue is that CommonJS has a __dirname variable, which I use to load a file relatively. __dirname doesn't exist in ESM, so I tried to do something like this:

const _mydirname = typeof __dirname !== 'undefined'
  ? __dirname
  : path.dirname(url.fileURLToPath(import.meta.url));

// Example usage
const file =  fs.readFileSync(path.join(_mydirname, '../README.md'), 'utf-8')

Unfortunately when building this in Typescript, it emits the following error:

 Error: src/application.ts(22,36): error TS1343: The 'import.meta' meta-property is only allowed when the '--module' option is 'es2020', 'es2022', 'esnext', 'system', 'node16', or 'nodenext'.

Is there a way to do get a path relative to the current script that works in both scenarios?

Panslavism answered 5/2, 2023 at 19:2 Comment(0)
B
3

I have the same issue, actually I'm using this alternative.

// src.index.ts
import { dirname } from "node:path";
import { fileURLToPath } from "node:url";

let dir;
try {
  dir = __dirname;
} catch (e) {
  dir = dirname(fileURLToPath(import.meta.url));
}

console.log(dir);
// tsup.config.js
import { defineConfig } from "tsup";

export default defineConfig({
  entry: ["src/index.ts"],
  dts: true,
  format: ["esm", "cjs"]
});
// tsconfig.json
{
  "compilerOptions": {
    "target": "ESNext",
    "moduleResolution": "node",
    "module": "ESNext",
    "outDir": "dist",
    "esModuleInterop": true,
    "declaration": true,
    "sourceMap": true
  },
  "include": [
    "src"
  ],
}
// package.json
{
  "main": "dist/index.js",
  "exports": {
    "import": "./dist/index.mjs",
    "require": "./dist/index.js"
  },
  "types": "dist/index.d.ts",
  "scripts": {
    "dev": "tsup --watch",
    "build": "tsup"
  },
  "devDependencies": {
    "tsup": "^6.7.0",
    "typescript": "^4.9.4",
  }
}

That's seems to work with tsup. There is one warning but the build success. I'm not sure I used the best practice... Other ideas ?

EDIT: ok it seems that tsup has a clean solution.

https://tsup.egoist.dev/#inject-cjs-and-esm-shims

// tsup.config.js
import { defineConfig } from "tsup";

export default defineConfig({
  entry: ["src/index.ts"],
  dts: true,
  format: ["esm", "cjs"],
  shims: true
});
Brendin answered 28/3, 2023 at 15:48 Comment(0)
P
1

I don't think there is any uniform way to get the current script path for both CommonJS and ESM.

If you are building it twice anyway, you can add some conditional build time logic to generate the proper code for each module system, the actual implementation will depend on your typescript build system.

  • Bundlers like Webpack/Rollup have plugins for that. You can inject a virtual module to return the dirname based on the target or use a "replace" plugin to transform the code.
  • You can use Babel (after tsc build or with typescript plugin) to transform it. Babel plugin like babel-plugin-transform-import-meta can transform the import.meta.url to cjs.
  • If you're using plain tsc, you will probably need to implement it yourself.

UPDATED (as noted, this doesn't work in cjs):

If you want to keep it simple and rely on runtime assertion of __dirname/import.meta you can just tell typescript to ignore the error:
const _mydirname = typeof __dirname !== 'undefined'
  ? __dirname
    // @ts-ignore
  : path.dirname(fileURLToPath(import.meta.url));

As a side note, be careful when basing logic on source script location, it may make sense during development but have different structure when built (e.g if you're using a bundler).
Phillipp answered 6/2, 2023 at 15:51 Comment(1)
This won't work in CJS. You'd get the following runtime error: Cannot use 'import.meta' outside a moduleStertor

© 2022 - 2024 — McMap. All rights reserved.