jiti icon indicating copy to clipboard operation
jiti copied to clipboard

Make `babel` transform lazy loading optional

Open iivvaannxx opened this issue 1 month ago • 5 comments

Context

Jiti's lazy-loading approach for the Babel transform is the best option for most use cases. Since the Babel transform requires loading a substantial dependency, deferring its load until needed provides meaningful performance benefits when the transform isn't required. However, I think I ran into a use case where it might make sense to eagerly load it.

We work with a runtime environment that only supports CommonJS execution. The workflow involves:

  1. Writing source code in ESM for better developer experience
  2. Using jiti to dynamically load configuration files at runtime
  3. Bundling everything with Webpack into a single CJS file for deployment
import { createJiti } from "jiti";

async function loadConfig(name) {
  const jiti = createJiti(__filename);
  const path = resolve(__dirname, `./config/config.${name}`);

  const config = await jiti.import(path, { default: true });
  return config;
}

Problem

When bundling with Webpack, the ESM version of jiti (dist/lib/jiti.mjs) contains this code:

let _transform;
function lazyTransform(...args) {
  if (!_transform) {
    _transform = createRequire(import.meta.url)("../dist/babel.cjs");
  }
  return _transform(...args);
}

And Webpack's default behavior is to replace import.meta.url with a static file path at build time:

// After Webpack processes it
_transform = createRequire("file:///Users/username/project/node_modules/jiti/lib/jiti.mjs")("../dist/babel.cjs");

This hardcoded absolute path doesn't exist in the production runtime, causing the lazy load to fail:

Error: Cannot find module '../dist/babel.cjs'

Workaround

I managed to workaround it by using the CJS distributed version via require('jiti') as it does not use import.meta.url under the hood:

const { createJiti } = require("jiti");  // Uses lib/jiti.cjs

Webpack can properly handle and bundle this require() call. However, the CommonJS entry point is marked as deprecated, and I'm concerned about future compatibility.

Proposed solutions

Of course, if someone has a better solution please let me know. From my point of view I think either of these two options below could work:

Option 1: Configuration Flag

const jiti = createJiti(__filename, {
  eagerTransform: true  // Loads babel immediately
});

Option 2: Separate Entry Point

import { createJiti } from "jiti/eager";

Additional information

  • [x] Would you be willing to help implement this feature?

iivvaannxx avatar Nov 07 '25 13:11 iivvaannxx

Thanks for explaining your situation.

Have you tried force resolving to jiti.cjs? If you do it, webpack can transform predictable commonjs requires it only cannot determine ESM lazy import.

pi0 avatar Nov 07 '25 18:11 pi0

Have you tried force resolving to jiti.cjs?

First of all, thanks for such a quick answer!

Do you have an example on how to do this? I tried some configuration options but didn't work. Never heard of a way to force resolve to CJS 🤔. For now, I was able to patch jiti and successfully implement option 2, so it's doable.

I actually don't think option 1 is doable as I don't know of a way to conditionally and statically import a module in ESM.

iivvaannxx avatar Nov 07 '25 21:11 iivvaannxx

Although to be fair, I believe I don't need this anymore (I know it has been less than a day 😆 but the "complexity" of what we were trying to do made us reconsider the whole workflow). We have a way to hook into the build system, so we can actually use jiti there (no runtime constraints), parse the config (which is available at build time) and rewrite it into a format that's easier to work with at runtime (e.g. JSON), then deploy that.

This way we also avoid having to bundle jiti and babel into the final code, which causes a large increase in bundle size.

So, feel free to close this issue if you think this is a not worthy feature (understandable as it's a pretty niche situation), or let me know if you want me to open a PR with solution 2 ^^

iivvaannxx avatar Nov 07 '25 21:11 iivvaannxx

I guess you might try a webpack resolve alias like { jiti: require.resolve("jiti") } in webpack.config (when using require.resolve it will be resolve to node_modules/jiti/lib/jiti.cjs)

Benefit is, even without lazy import, webpack should make a lazy evaluated function (saves eval time of bundle)

pi0 avatar Nov 07 '25 21:11 pi0

The lazy loading feature caused bug in nitropack build too.

Image

When i use [email protected], dis/babel.cjs won't be packaged in, this will caused runtime error;

Listening on http://[::]:30410
[unhandledRejection] Error: Cannot find module '../dist/babel.cjs'
Require stack:
- /app/.output/server/node_modules/jiti/lib/jiti.mjs
    at Function._resolveFilename (node:internal/modules/cjs/loader:1401:15)
    at defaultResolveImpl (node:internal/modules/cjs/loader:1057:19)
    at resolveForCJSWithHooks (node:internal/modules/cjs/loader:1062:22)
    at Function._load (node:internal/modules/cjs/loader:1211:37)
    at TracingChannel.traceSync (node:diagnostics_channel:322:14)
    at wrapModuleLoad (node:internal/modules/cjs/loader:235:24)
    at Module.require (node:internal/modules/cjs/loader:1487:12)
    at require (node:internal/modules/helpers:135:16)
    at Object.lazyTransform [as transform] (file:///app/.output/server/node_modules/jiti/lib/jiti.mjs:13:48)
    at /app/.output/server/node_modules/jiti/dist/jiti.cjs:1:152918 {
  code: 'MODULE_NOT_FOUND',
  requireStack: [ '/app/.output/server/node_modules/jiti/lib/jiti.mjs' ]

Then, i override jiti to 2.4.2, it works ok.

BluesYoung-web avatar Nov 13 '25 01:11 BluesYoung-web