plugins icon indicating copy to clipboard operation
plugins copied to clipboard

[@rollup/plugin-commonjs] Bring back named exports in commonjs entry points

Open charlag opened this issue 5 months ago • 10 comments

  • Rollup Plugin Name: commonjs
  • Rollup Plugin Version: 27.0.0 - 28.0.6

Expected Behavior / Situation

When using commonjs file as an entry point I want to be able to use named exports

Actual Behavior / Situation

Only default export is generated

Modification Proposal

Bring back the behavior with named exports for entry point.

Motivation

We use rollup to vendor our dependencies: https://github.com/tutao/tutanota/blob/2218892625ed3b717ac987e531c4553989b9bf59/buildSrc/updateLibs.js

It makes much easier to review the changes and ensures that the build is less reliant on npm registry.

Unfortunately we run into an issue with undici where their changes to the entrypoint caused rollup to not generate named exports. We worked around it by normalizing the exports: https://github.com/nodejs/undici/pull/4324 but now when updating the plugin we run into the same issue.

It would make sense that we can use the library like the docs and types suggest and not import it differently just because it is vendored. When using node's native ES module interop the mapping would be different.

I would love if libraries provided native ESM entry point but right now it is not the case for some libraries that we use so we would kindly ask to reconsider the change. The alternative for us would be to maintain our own entry point/wrapper with named ESM exports which is not very appealing.

charlag avatar Jul 17 '25 14:07 charlag

@charlag can you pinpoint which version caused your fix to stop working?

shellscape avatar Oct 13 '25 01:10 shellscape

@shellscape 26.0.3 - old behavior, works, 27.0.0 - new behavior, does not work

26.0.3

var setGlobalDispatcher_1 = undici.setGlobalDispatcher = setGlobalDispatcher;
var getGlobalDispatcher_1 = undici.getGlobalDispatcher = getGlobalDispatcher;

const fetchImpl = requireFetch().fetch;
var fetch = undici.fetch = async function fetch (init, options = undefined) {
  try {
    return await fetchImpl(init, options)
  } catch (err) {
    if (err && typeof err === 'object') {
      Error.captureStackTrace(err);
    }

    throw err
  }
};
var Headers = undici.Headers = requireHeaders().Headers;
var Response = undici.Response = requireResponse().Response;
var Request = undici.Request = requireRequest().Request;
var FormData = undici.FormData = requireFormdata().FormData;

const { setGlobalOrigin, getGlobalOrigin } = requireGlobal();

var setGlobalOrigin_1 = undici.setGlobalOrigin = setGlobalOrigin;
var getGlobalOrigin_1 = undici.getGlobalOrigin = getGlobalOrigin;

const { CacheStorage } = requireCachestorage();
const { kConstruct } = requireSymbols();

// Cache & CacheStorage are tightly coupled with fetch. Even if it may run
// in an older version of Node, it doesn't have any use without fetch.
var caches = undici.caches = new CacheStorage(kConstruct);

const { deleteCookie, getCookies, getSetCookies, setCookie, parseCookie } = requireCookies();

var deleteCookie_1 = undici.deleteCookie = deleteCookie;
var getCookies_1 = undici.getCookies = getCookies;
var getSetCookies_1 = undici.getSetCookies = getSetCookies;
var setCookie_1 = undici.setCookie = setCookie;
var parseCookie_1 = undici.parseCookie = parseCookie;

const { parseMIMEType, serializeAMimeType } = requireDataUrl();

var parseMIMEType_1 = undici.parseMIMEType = parseMIMEType;
var serializeAMimeType_1 = undici.serializeAMimeType = serializeAMimeType;

const { CloseEvent, ErrorEvent, MessageEvent } = requireEvents();
var WebSocket = undici.WebSocket = requireWebsocket().WebSocket;
var CloseEvent_1 = undici.CloseEvent = CloseEvent;
var ErrorEvent_1 = undici.ErrorEvent = ErrorEvent;
var MessageEvent_1 = undici.MessageEvent = MessageEvent;

var WebSocketStream = undici.WebSocketStream = requireWebsocketstream().WebSocketStream;
var WebSocketError = undici.WebSocketError = requireWebsocketerror().WebSocketError;

var request = undici.request = makeDispatcher(api.request);
var stream = undici.stream = makeDispatcher(api.stream);
var pipeline = undici.pipeline = makeDispatcher(api.pipeline);
var connect = undici.connect = makeDispatcher(api.connect);
var upgrade = undici.upgrade = makeDispatcher(api.upgrade);

var MockClient_1 = undici.MockClient = MockClient;
var MockCallHistory_1 = undici.MockCallHistory = MockCallHistory;
var MockCallHistoryLog_1 = undici.MockCallHistoryLog = MockCallHistoryLog;
var MockPool_1 = undici.MockPool = MockPool;
var MockAgent_1 = undici.MockAgent = MockAgent;
var mockErrors_1 = undici.mockErrors = mockErrors;

const { EventSource } = requireEventsource();

var EventSource_1 = undici.EventSource = EventSource;

export { Agent_1 as Agent, BalancedPool_1 as BalancedPool, Client_1 as Client, CloseEvent_1 as CloseEvent, DecoratorHandler_1 as DecoratorHandler, Dispatcher_1 as Dispatcher, EnvHttpProxyAgent_1 as EnvHttpProxyAgent, ErrorEvent_1 as ErrorEvent, EventSource_1 as EventSource, FormData, H2CClient_1 as H2CClient, Headers, MessageEvent_1 as MessageEvent, MockAgent_1 as MockAgent, MockCallHistory_1 as MockCallHistory, MockCallHistoryLog_1 as MockCallHistoryLog, MockClient_1 as MockClient, MockPool_1 as MockPool, Pool_1 as Pool, ProxyAgent_1 as ProxyAgent, RedirectHandler_1 as RedirectHandler, Request, Response, RetryAgent_1 as RetryAgent, RetryHandler_1 as RetryHandler, WebSocket, WebSocketError, WebSocketStream, buildConnector_1 as buildConnector, cacheStores, caches, connect, undici as default, deleteCookie_1 as deleteCookie, errors_1 as errors, fetch, getCookies_1 as getCookies, getGlobalDispatcher_1 as getGlobalDispatcher, getGlobalOrigin_1 as getGlobalOrigin, getSetCookies_1 as getSetCookies, interceptors, mockErrors_1 as mockErrors, parseCookie_1 as parseCookie, parseMIMEType_1 as parseMIMEType, pipeline, request, serializeAMimeType_1 as serializeAMimeType, setCookie_1 as setCookie, setGlobalDispatcher_1 as setGlobalDispatcher, setGlobalOrigin_1 as setGlobalOrigin, stream, upgrade, util_1 as util };
27.0.0
var hasRequiredUndici;

function requireUndici () {
	if (hasRequiredUndici) return undici;
	hasRequiredUndici = 1;

        // ...

	undici.setGlobalDispatcher = setGlobalDispatcher;
	undici.getGlobalDispatcher = getGlobalDispatcher;

	const fetchImpl = requireFetch().fetch;
	undici.fetch = async function fetch (init, options = undefined) {
	  try {
	    return await fetchImpl(init, options)
	  } catch (err) {
	    if (err && typeof err === 'object') {
	      Error.captureStackTrace(err);
	    }

	    throw err
	  }
	};
	undici.Headers = requireHeaders().Headers;
	undici.Response = requireResponse().Response;
	undici.Request = requireRequest().Request;
	undici.FormData = requireFormdata().FormData;

	const { setGlobalOrigin, getGlobalOrigin } = requireGlobal$1();

	undici.setGlobalOrigin = setGlobalOrigin;
	undici.getGlobalOrigin = getGlobalOrigin;

	const { CacheStorage } = requireCachestorage();
	const { kConstruct } = requireSymbols();

	// Cache & CacheStorage are tightly coupled with fetch. Even if it may run
	// in an older version of Node, it doesn't have any use without fetch.
	undici.caches = new CacheStorage(kConstruct);

	const { deleteCookie, getCookies, getSetCookies, setCookie, parseCookie } = requireCookies();

	undici.deleteCookie = deleteCookie;
	undici.getCookies = getCookies;
	undici.getSetCookies = getSetCookies;
	undici.setCookie = setCookie;
	undici.parseCookie = parseCookie;

	const { parseMIMEType, serializeAMimeType } = requireDataUrl();

	undici.parseMIMEType = parseMIMEType;
	undici.serializeAMimeType = serializeAMimeType;

	const { CloseEvent, ErrorEvent, MessageEvent } = requireEvents();
	undici.WebSocket = requireWebsocket().WebSocket;
	undici.CloseEvent = CloseEvent;
	undici.ErrorEvent = ErrorEvent;
	undici.MessageEvent = MessageEvent;

	undici.WebSocketStream = requireWebsocketstream().WebSocketStream;
	undici.WebSocketError = requireWebsocketerror().WebSocketError;

	undici.request = makeDispatcher(api.request);
	undici.stream = makeDispatcher(api.stream);
	undici.pipeline = makeDispatcher(api.pipeline);
	undici.connect = makeDispatcher(api.connect);
	undici.upgrade = makeDispatcher(api.upgrade);

	undici.MockClient = MockClient;
	undici.MockCallHistory = MockCallHistory;
	undici.MockCallHistoryLog = MockCallHistoryLog;
	undici.MockPool = MockPool;
	undici.MockAgent = MockAgent;
	undici.mockErrors = mockErrors;

	const { EventSource } = requireEventsource();

	undici.EventSource = EventSource;
	return undici;
}

var undiciExports = requireUndici();
var index = /*@__PURE__*/getDefaultExportFromCjs(undiciExports);

export { index as default };

charlag avatar Oct 13 '25 08:10 charlag

@CharlieHelps please triage this issue

shellscape avatar Oct 20 '25 13:10 shellscape

Expand this to see my work.
  • Reviewed the new comment and the full history on #1885, noting missing environment details (Node, Rollup, plugin version) and any relevant rollup.config.js snippet.
  • Updated labels to triage and needs-info to reflect current status and request a minimal reproduction.
  • Searched for possible duplicates across recent issues; found no clear match to link as a duplicate.
  • Drafted a concise reply on #1885 requesting a minimal repro (small repo or failing rollup.config.js) and the exact error text to unblock verification.
  • Verified a local baseline by running rollup -c in a clean scaffold; build completes without errors, suggesting the report likely hinges on specific configuration or plugin usage.
  • Confirmed IS_WRAPPED_COMMONJS in packages/commonjs/src/helpers.js and traced how commonjsMeta.isCommonJS === IS_WRAPPED_COMMONJS toggles wrapper behavior across the plugin.
  • Reviewed packages/commonjs/src/resolve-id.js logic: wrapped CJS entries get ENTRY_SUFFIX with moduleSideEffects = true, while ES imports of wrapped CJS get ES_IMPORT_SUFFIX and metadata preserved.
  • Audited packages/commonjs/src/proxies.js: getEntryProxy() re-exports * (and default if present) for non-wrapped modules; wrapped CJS routes through getEsImportProxy() exposing __moduleExports with syntheticNamedExports: '__moduleExports'.
  • Checked packages/commonjs/src/transform-commonjs.js and index.js to align usesRequireWrapper/needsRequireWrapper behavior (dynamic/strict requires) with the observed entry/import proxying paths.

charliecreates[bot] avatar Oct 20 '25 13:10 charliecreates[bot]

Thanks for the ping, @shellscape.

Short triage: this is expected behavior on v27+ of @rollup/plugin-commonjs. When a CommonJS file is used as the entry and you output an ES module, the bundle exposes a default export only. Named exports are not materialized for the entry chunk. This matches the current docs (“Using CommonJS files as entry points” in packages/commonjs/README.md).

Why this changed (quick notes)
  • Since v27, CommonJS entries are proxied differently. For wrapped CJS entries we generate a small proxy that sets syntheticNamedExports: '__moduleExports' and a default export, but we do not emit explicit named export statements for the entry chunk itself. See packages/commonjs/src/proxies.js: getEntryProxy() and getEsImportProxy().

  • Your examples line up:

    • 26.0.3 emitted concrete named export statements in the entry output.
    • 27.0.0+ emits a default export for the entry; named members remain accessible via synthetic named exports when imported elsewhere, but the entry file itself won’t list them.

What you can do now

  • If you need named exports in the final ES bundle, use a thin ESM entry that re-exports from the CJS file:
    // main.cjs
    exports.foo = 'foo';
    exports.bar = 'bar';
    
    // main.mjs (use as Rollup input)
    export { foo, bar } from './main.cjs';
    
  • If you’re outputting CommonJS/IIFE, keep output.exports at its default ('auto') or 'default' to align with the generated default export for CJS entries.
  • If you can’t change your entry yet, you can pin @rollup/plugin-commonjs to 26.x while you evaluate the wrapper approach.

Open question for maintainers

  • We can discuss an opt-in to “re-materialize” named exports for CJS entry proxies, but it’s non-trivial (we’d need to surface analyzed export names to the entry proxy and it reintroduces behavior we previously moved away from). If there’s broader demand, we can scope an option and its edge cases.

Next steps

  1. @charlag could you share your Rollup config (especially output.format) and confirm whether an ESM shim entry would work for your setup?
  2. If you’re targeting ES output and want to pursue an opt-in for named entry exports, let’s capture concrete requirements here and we’ll propose an approach.

charliecreates[bot] avatar Oct 20 '25 13:10 charliecreates[bot]

I think this is the exact issue I'm running into when trying to use @slate-serializers/[email protected]

When calling htmlToSlate, in the built javascript output, it attempts to do new DomHandler.DomHandler(...) however it should've been new DomHandler(...).

I was able to get this working by forking and exporting an esm version, and this does the expected new DomHandler(...)

flamerohr avatar Oct 30 '25 02:10 flamerohr

the bot said pretty much the same things I said except with some technical details which I'm not sure I can trust (which makes me annoyed that I've tried to understand but maybe in vain). I think I have provided everything including links to the code examples and linked the issue with at least one library.

here's where we write the bundle for undici:

https://github.com/tutao/tutanota/blob/2218892625ed3b717ac987e531c4553989b9bf59/buildSrc/updateLibs.js#L168

i feel like the change is a bit self-defeating. We are using this plugin to use commonjs libraries. yet now it is almost impossible to do it out of the box. yes, we can maintain a shim. maybe we can even upstream it. it will be a lot of work, continuously. maybe it makes sense to use another bundler to pre-processs things. no I don't think it's a rare case, most people will just not bother trying to figure out why it doesn't work.

charlag avatar Oct 30 '25 05:10 charlag

@charlag the primary reason I brought Charlie (bot) into the repo is because we have a pervasive lack of community contribution, and maintainers come and go. There's no one active to work on that plugin right now, and Charlie has helped get a lot of older issues closed, and pending work over the line.

I think I have provided everything including links to the code examples and linked the issue with at least one library.

In our Bug issue template, we ask users for minimal reproductions on stackblitz et al. I simply don't have the time to comb through the complexities of how you're using Rollup in your repo. If you'd like to distill that down a minimal reproduction, that would help the bot take a look at what the internals are doing. I'd also recommend a PR with a failing test, which will help as well.

Or if you'd like to spearhead the fix/mod for this, by all means please do. If you decide to take this on, I would suggest using an option to change the behavior, rather than trying to fundamentally change the behavior.

shellscape avatar Oct 31 '25 13:10 shellscape

@shellscape thank you for the reply. The lack of resources is very sympathetic and understandable. If it would be a new feature that we would be requesting it would be very unreasonable of us to ask you to implement it. This case is slightly different as this was a deliberate change so simply sending a PR to undo it probably will not work and we need some maintainer involvement on understanding why and also on how to proceed with it. We will have to see if we can dedicate some resources on fixing this as we are obviously interested in it. Can you confirm that the response from the bot is valid?

I apologize, I thought the undici issue I have posted has a stackblitz link but it seems like it's not valid. I've made another repro: https://stackblitz.com/edit/github-zdltp46a-nggeyx8z?file=package.json

it's really as simple as

  1. Run Rollup on a commonjs module as an entry point (with plugin-commonjs of course)
  2. Try to import the result of step 1 either at runtime or during bundling
  3. Step 2 will fail as commonjs module from step 1 does not have named exports

There is no specific configuration involved. It is also impossible to work around this issue by changing configuration. The only workaround is to write esm entry point for every library involved.

I hope we can find a solution, thank you for your time.

charlag avatar Nov 03 '25 10:11 charlag

I see now thank you. Given this specific use-case, we'll need a feature to re-enable the behavior that was "fixed." While we're totally open to this, it will require community contribution.

shellscape avatar Nov 03 '25 14:11 shellscape