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:
- Be an actual ESM file with
export default (preferred), or
- Use a
.cjs extension so it is not subject to the "type": "module" interpretation, or
- 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.
To reproduce:
This is not a Less language issue but a packaging issue with
dist/less.js. Minimal reproduction:src/index.js:Current behavior:
Since 4.6.0 (#4411), the
lesspackage 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.jsis a UMD bundle that assignsmodule.exports:esbuild resolves
dist/less.jsvia the"browser"export condition. Because the package has"type": "module", esbuild treats this file as ESM. Since the file has noexport default, the default import yieldsundefined.With less 4.5.x (no
"type": "module", no exports map), esbuild treateddist/less.jsas 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:
Expected behavior:
The browser entry point should either:
export default(preferred), or.cjsextension so it is not subject to the"type": "module"interpretation, orrequirein the exports map so bundlers apply CJS→ESM interopEnvironment information:
lessversion: 4.6.0+ (tested with 4.6.4)nodejsversion: 22.xoperating 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.