Skip to content

dist/less.js (browser build) is UMD but since 4.6.0 treated as ESM due to "type": "module" – breaks bundlers #4439

@dominikschreiber

Description

@dominikschreiber

To reproduce:

This is not a Less language issue but a packaging issue with dist/less.js. Minimal reproduction:

mkdir repro && cd repro && npm init -y
npm install less@4.6.4 esbuild@0.27.7

src/index.js:

import less from 'less';
console.log(less.version);
npx esbuild src/index.js --bundle --platform=browser --format=esm --outfile=out.js

Current behavior:

Since 4.6.0 (#4411), the less package declares "type": "module" and has an exports map with a "browser" condition pointing to ./dist/less.js:

{
  "type": "module",
  "exports": {
    ".": {
      "browser": "./dist/less.js"
    }
  }
}

But dist/less.js is a UMD bundle that assigns module.exports:

(function (global, factory) {
    typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
    typeof define === 'function' && define.amd ? define(factory) :
    (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.less = factory());
})(this, (function () { 'use strict';
    // ...
}));

esbuild resolves dist/less.js via the "browser" export condition. Because the package has "type": "module", esbuild treats this file as ESM. Since the file has no export default, the default import yields undefined.

With less 4.5.x (no "type": "module", no exports map), esbuild treated dist/less.js as CJS and synthesized a default export automatically — the same build worked fine.

Our workaround is an esbuild plugin that wraps the UMD content in a CJS shim:

plugins: [{
  name: 'less-esm-compat',
  setup(build) {
    build.onResolve({filter: /^less$/}, () => ({
      path: lessDistPath,
      namespace: 'less-esm-compat',
    }));
    build.onLoad({filter: /.*/, namespace: 'less-esm-compat'}, async () => ({
      contents: `var module = {exports: {}}, exports = module.exports;\n${await readFile(lessDistPath, 'utf8')}\nexport default module.exports;`,
      resolveDir: '.',
    }));
  }
}]

Expected behavior:

The browser entry point should either:

  1. Be an actual ESM file with export default (preferred), or
  2. Use a .cjs extension so it is not subject to the "type": "module" interpretation, or
  3. Be explicitly marked as require in the exports map so bundlers apply CJS→ESM interop

Environment information:

  • less version: 4.6.0+ (tested with 4.6.4)
  • nodejs version: 22.x
  • operating system: macOS (not OS-specific)

This is a different issue from #4438 but both are regressions introduced with the 4.6 packaging changes. In our case the Node build (using --platform=node --packages=external) works fine — it's only the browser bundle path that is affected by the UMD/ESM mismatch.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions