Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/commonjs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
},
"dependencies": {
"@rollup/pluginutils": "^5.0.1",
"cjs-module-lexer": "^2.2.0",
"commondir": "^1.0.1",
"estree-walker": "^2.0.2",
"fdir": "^6.2.0",
Expand Down
80 changes: 80 additions & 0 deletions packages/commonjs/src/analyze-exports-lexer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { init, parse } from 'cjs-module-lexer';

let initialized = false;

/**
* Ensure cjs-module-lexer WASM is initialized.
* Safe to call multiple times — will only init once.
*/
export async function ensureInit() {
if (!initialized) {
await init();
initialized = true;
}
}

/**
* Analyze a CommonJS module source to detect named exports.
*
* @param {string} code — The raw CJS source code.
* @param {string} id — The module ID (for error reporting).
* @returns {Promise<{
* exports: string[]
* reexports: string[]
* hasDefaultExport: boolean
* }>}
*/
export async function analyzeExports(code, id) {
await ensureInit();
try {
const result = parse(code);
// Deduplicate and filter out "default" — handled separately
const namedExports = [...new Set(result.exports)].filter(e => e !== 'default');
const reexports = [...new Set(result.reexports)];
return {
exports: namedExports,
reexports,
hasDefaultExport: result.exports.includes('default'),
};
} catch (err) {
// If lexer fails (e.g. WASM issue), fall back gracefully
console.warn(
`[commonjs] cjs-module-lexer failed for ${id}: ${err.message}. ` +
'Falling back to no named exports.'
);

return { exports: [], reexports: [], hasDefaultExport: true };
}
}

/**
* Given a list of reexport sources, recursively resolve
* their named exports using the provided resolver.
*
* @param {string[]} reexportSources
* @param {(source: string) => Promise<string|null>} resolve
* @param {(id: string) => Promise<string>} loadCode
* @param {Set<string>} [seen]
* @returns {Promise<string[]>}
*/
export async function resolveReexports(reexportSources, resolve, loadCode, seen = new Set()) {
const allExports = [];
for (const source of reexportSources) {
const resolved = await resolve(source);
if (!resolved || seen.has(resolved)) continue;
seen.add(resolved);
try {
const code = await loadCode(resolved);
const { exports: childExports, reexports: childReexports } = await analyzeExports(code, resolved);
allExports.push(...childExports);
if (childReexports.length > 0) {
const nested = await resolveReexports(childReexports, resolve, loadCode, seen);
allExports.push(...nested);
}
} catch {
// skip unresolvable reexports
}
}

return [...new Set(allExports)];
}
24 changes: 19 additions & 5 deletions packages/commonjs/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { createFilter } from '@rollup/pluginutils';

import { peerDependencies, version } from '../package.json';

import { analyzeExports, ensureInit as ensureLexerInit } from './analyze-exports-lexer';
import analyzeTopLevelStatements from './analyze-top-level-statements';
import { getDynamicModuleRegistry, getDynamicRequireModules } from './dynamic-modules';

Expand Down Expand Up @@ -113,7 +114,7 @@ export default function commonjs(options = {}) {
// Initialized in buildStart
let requireResolver;

function transformAndCheckExports(code, id) {
async function transformAndCheckExports(code, id) {
const normalizedId = normalizePathSlashes(id);
const { isEsModule, hasDefaultExport, hasNamedExports, ast } = analyzeTopLevelStatements(
this.parse,
Expand All @@ -138,6 +139,16 @@ export default function commonjs(options = {}) {
return { meta: { commonjs: commonjsMeta } };
}

// Use cjs-module-lexer for named export detection on CJS modules
if (!isEsModule) {
const lexerResult = await analyzeExports(code, id);
commonjsMeta.lexerExports = lexerResult.exports;
commonjsMeta.lexerReexports = lexerResult.reexports;
if (lexerResult.hasDefaultExport && !commonjsMeta.hasDefaultExport) {
commonjsMeta.hasDefaultExport = true;
}
}

const needsRequireWrapper =
!isEsModule && (dynamicRequireModules.has(normalizedId) || strictRequiresFilter(id));

Expand Down Expand Up @@ -202,8 +213,9 @@ export default function commonjs(options = {}) {
return { ...rawOptions, plugins };
},

buildStart({ plugins }) {
async buildStart({ plugins }) {
validateVersion(this.meta.rollupVersion, peerDependencies.rollup, 'rollup');
await ensureLexerInit();
const nodeResolve = plugins.find(({ name }) => name === 'node-resolve');
if (nodeResolve) {
validateVersion(nodeResolve.version, '^13.0.6', '@rollup/plugin-node-resolve');
Expand Down Expand Up @@ -291,10 +303,12 @@ export default function commonjs(options = {}) {

if (isWrappedId(id, ES_IMPORT_SUFFIX)) {
const actualId = unwrapId(id, ES_IMPORT_SUFFIX);
const loadedModule = await this.load({ id: actualId });
return getEsImportProxy(
actualId,
getDefaultIsModuleExports(actualId),
(await this.load({ id: actualId })).moduleSideEffects
loadedModule.moduleSideEffects,
loadedModule.meta?.commonjs?.lexerExports || []
);
}

Expand All @@ -319,11 +333,11 @@ export default function commonjs(options = {}) {
return requireResolver.shouldTransformCachedModule.call(this, ...args);
},

transform(code, id) {
async transform(code, id) {
if (!isPossibleCjsId(id)) return null;

try {
return transformAndCheckExports.call(this, code, id);
return await transformAndCheckExports.call(this, code, id);
} catch (err) {
return this.error(err, err.pos);
}
Expand Down
16 changes: 14 additions & 2 deletions packages/commonjs/src/proxies.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,14 +57,15 @@ export function getEntryProxy(id, defaultIsModuleExports, getModuleInfo, shebang
}
return shebang + code;
}
const result = getEsImportProxy(id, defaultIsModuleExports, true);
const lexerExports = commonjsMeta?.lexerExports || [];
const result = getEsImportProxy(id, defaultIsModuleExports, true, lexerExports);
return {
...result,
code: shebang + result.code
};
}

export function getEsImportProxy(id, defaultIsModuleExports, moduleSideEffects) {
export function getEsImportProxy(id, defaultIsModuleExports, moduleSideEffects, lexerExports = []) {
const name = getName(id);
const exportsName = `${name}Exports`;
const requireModule = `require${capitalize(name)}`;
Expand All @@ -80,6 +81,17 @@ export function getEsImportProxy(id, defaultIsModuleExports, moduleSideEffects)
} else {
code += `\nexport default /*@__PURE__*/getDefaultExportFromCjs(${exportsName});`;
}

// Add explicit named re-exports detected by cjs-module-lexer
const namedExports = lexerExports.filter(
(e) => e !== 'default' && e !== '__esModule' && /^[$_a-zA-Z][$_a-zA-Z0-9]*$/.test(e)
);
if (namedExports.length > 0) {
for (const exportName of namedExports) {
code += `\nvar _cjsExport_${exportName} = ${exportsName}["${exportName}"];\nexport { _cjsExport_${exportName} as ${exportName} };`;
}
}

return {
code,
syntheticNamedExports: '__moduleExports'
Expand Down
25 changes: 24 additions & 1 deletion packages/commonjs/src/transform-commonjs.js
Original file line number Diff line number Diff line change
Expand Up @@ -531,7 +531,7 @@ export default async function transformCommonjs(
commonjsMeta
);
const usesRequireWrapper = commonjsMeta.isCommonJS === IS_WRAPPED_COMMONJS;
const exportBlock = isEsModule
let exportBlock = isEsModule
? ''
: rewriteExportsAndGetExportsBlock(
magicString,
Expand All @@ -553,6 +553,29 @@ export default async function transformCommonjs(
requireName
);

// Enhance export block with cjs-module-lexer detected exports
// that were not already found by the AST walk
if (!isEsModule && !usesRequireWrapper) {
const lexerExports = commonjsMeta.lexerExports || [];
const astDetectedExports = new Set(exportsAssignmentsByName.keys());
const additionalExports = lexerExports.filter(
(name) =>
!astDetectedExports.has(name) &&
name !== 'default' &&
name !== '__esModule' &&
/^[$_a-zA-Z][$_a-zA-Z0-9]*$/.test(name)
);
if (additionalExports.length > 0) {
const sourceObj = exportMode === 'module' ? exportedExportsName : exportsName;
for (const name of additionalExports) {
const deconflictedName = deconflict([scope], globals, name);
exportBlock += `\nvar ${deconflictedName} = ${sourceObj}["${name}"];\nexport { ${
deconflictedName === name ? name : `${deconflictedName} as ${name}`
} };`;
}
}
}

if (shouldWrap) {
wrapCode(magicString, uses, moduleName, exportsName, indentExclusionRanges);
}
Expand Down
8 changes: 8 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading