Skip to content

Commit

Permalink
feat(commonjs): reconstruct real es module from __esModule marker
Browse files Browse the repository at this point in the history
  • Loading branch information
LarsDenBakker committed Aug 12, 2020
1 parent e4d21ba commit 1745141
Show file tree
Hide file tree
Showing 20 changed files with 258 additions and 339 deletions.
10 changes: 4 additions & 6 deletions packages/commonjs/src/index.js
Expand Up @@ -76,11 +76,8 @@ export default function commonjs(options = {}) {
const sourceMap = options.sourceMap !== false;

function transformAndCheckExports(code, id) {
const { isEsModule, hasDefaultExport, hasNamedExports, ast } = checkEsModule(
this.parse,
code,
id
);
const checkResult = checkEsModule(this.parse, code, id);
const { isEsModule, isCompiledEsModule, hasDefaultExport, hasNamedExports, ast } = checkResult;
if (hasDefaultExport) {
esModulesWithDefaultExport.add(id);
}
Expand All @@ -101,6 +98,7 @@ export default function commonjs(options = {}) {
code,
id,
isEsModule,
isCompiledEsModule,
ignoreGlobal || isEsModule,
ignoreRequire,
sourceMap,
Expand All @@ -110,7 +108,7 @@ export default function commonjs(options = {}) {
ast
);

setIsCjsPromise(id, isEsModule ? false : Boolean(transformed));
setIsCjsPromise(id, isEsModule || isCompiledEsModule ? false : Boolean(transformed));
return transformed;
}

Expand Down
162 changes: 129 additions & 33 deletions packages/commonjs/src/transform.js
Expand Up @@ -17,10 +17,11 @@ import {
} from './helpers';
import { getName } from './utils';

const KEY_COMPILED_ESM = '__esModule';
const reserved = 'process location abstract arguments boolean break byte case catch char class const continue debugger default delete do double else enum eval export extends false final finally float for from function goto if implements import in instanceof int interface let long native new null package private protected public return short static super switch synchronized this throw throws transient true try typeof var void volatile while with yield'.split(
' '
);
const blacklist = { __esModule: true };
const blacklist = { [KEY_COMPILED_ESM]: true };
reserved.forEach((word) => (blacklist[word] = true));

const exportsPattern = /^(?:module\.)?exports(?:\.([a-zA-Z_$][a-zA-Z_$0-9]*))?$/;
Expand Down Expand Up @@ -60,10 +61,51 @@ export function hasCjsKeywords(code, ignoreGlobal) {
return firstpass.test(code);
}

function isTrueNode(node) {
if (!node) return false;

switch (node.type) {
case 'Literal':
return node.value;
case 'UnaryExpression':
return node.operator === '!' && node.argument && node.argument.value === 0;
default:
return false;
}
}

function getAssignedMember(node) {
const { left, operator, right } = node.expression;
if (operator !== '=' || left.type !== 'MemberExpression') {
return null;
}
let assignedIdentifier;
if (left.object.type === 'Identifier') {
// exports.foo = ...
assignedIdentifier = left.object;
} else if (
left.object.type === 'MemberExpression' &&
left.object.property.type === 'Identifier'
) {
// module.exports.foo = ...
assignedIdentifier = left.object.property;
} else {
return null;
}

if (assignedIdentifier.name !== 'exports') {
return null;
}

const key = left.property ? left.property.name : null;
return { key, value: right };
}

export function checkEsModule(parse, code, id) {
const ast = tryParse(parse, code, id);

let isEsModule = false;
let isCompiledEsModule = false;
let hasDefaultExport = false;
let hasNamedExports = false;
for (const node of ast.body) {
Expand Down Expand Up @@ -93,9 +135,36 @@ export function checkEsModule(parse, code, id) {
} else if (node.type === 'ImportDeclaration') {
isEsModule = true;
}

if (node.type === 'ExpressionStatement' && node.expression) {
let compiledEsmValueNode;

if (node.expression.type === 'CallExpression') {
// detect Object.defineProperty(exports, '__esModule', { value: true });
const p = getDefinePropertyCallName(node.expression, 'exports');
if (p && p.key === KEY_COMPILED_ESM) {
compiledEsmValueNode = p.value;
}
} else if (node.expression.type === 'AssignmentExpression') {
// detect exports.__esModule = true;
const assignedMember = getAssignedMember(node);
if (assignedMember && assignedMember.key === KEY_COMPILED_ESM) {
compiledEsmValueNode = assignedMember.value;
}
}

if (compiledEsmValueNode) {
isCompiledEsModule = isTrueNode(compiledEsmValueNode);
}
}
}

return { isEsModule, hasDefaultExport, hasNamedExports, ast };
// don't treat mixed es modules as compiled es mdoules
if (isEsModule) {
isCompiledEsModule = false;
}

return { isEsModule, isCompiledEsModule, hasDefaultExport, hasNamedExports, ast };
}

function getDefinePropertyCallName(node, targetName) {
Expand All @@ -111,17 +180,23 @@ function getDefinePropertyCallName(node, targetName) {

if (node.arguments.length !== 3) return;

const [target, val] = node.arguments;
const [target, key, value] = node.arguments;
if (target.type !== 'Identifier' || target.name !== targetName) return;

if (value.type !== 'ObjectExpression' || !value.properties) return;
const valueProperty = value.properties.find((p) => p.key && p.key.name === 'value');
if (!valueProperty || !valueProperty.value) return;

// eslint-disable-next-line consistent-return
return val.value;
return { key: key.value, value: valueProperty.value };
}

export function transformCommonjs(
parse,
code,
id,
isEsModule,
isCompiledEsModule,
ignoreGlobal,
ignoreRequire,
sourceMap,
Expand Down Expand Up @@ -154,8 +229,7 @@ export function transformCommonjs(

const namedExports = {};

// TODO handle transpiled modules
let shouldWrap = /__esModule/.test(code);
let shouldWrap = false;
let usesCommonjsHelpers = false;

function isRequireStatement(node) {
Expand Down Expand Up @@ -457,8 +531,11 @@ export function transformCommonjs(
return;
}

const name = getDefinePropertyCallName(node, 'exports');
if (name && name === makeLegalIdentifier(name)) namedExports[name] = true;
if (node.type === 'ExpressionStatement' && node.expression) {
const p = getDefinePropertyCallName(node.expression, 'exports');
if (p && p.key === makeLegalIdentifier(p.key)) namedExports[p.key] = true;
if (p && p.key === KEY_COMPILED_ESM) node._shouldRemove = true;
}

// if this is `var x = require('x')`, we can do `import x from 'x'`
if (
Expand Down Expand Up @@ -541,6 +618,10 @@ export function transformCommonjs(
if (!keepDeclaration) {
magicString.remove(node.start, node.end);
}
} else if (node.type === 'ExpressionStatement') {
if (node._shouldRemove) {
magicString.remove(node.start, node.end);
}
}
}
});
Expand All @@ -556,9 +637,8 @@ export function transformCommonjs(
return null;
}

// If `isEsModule` is on, it means it has ES6 import/export statements,
// which just can't be wrapped in a function.
if (isEsModule) shouldWrap = false;
// if this is an es module or a comiled es module, we don't need to wrap it
if (isEsModule || isCompiledEsModule) shouldWrap = false;

usesCommonjsHelpers = usesCommonjsHelpers || shouldWrap;

Expand Down Expand Up @@ -589,7 +669,7 @@ export function transformCommonjs(
let wrapperEnd = '';

const moduleName = deconflict(scope, globals, getName(id));
if (!isEsModule) {
if (!isEsModule && !isCompiledEsModule) {
const exportModuleExports = {
str: `export { ${moduleName} as __moduleExports };`,
name: '__moduleExports'
Expand All @@ -600,6 +680,7 @@ export function transformCommonjs(

const defaultExportPropertyAssignments = [];
let hasDefaultExport = false;
let deconflictedDefaultExportName;

if (shouldWrap) {
const args = `module${uses.exports ? ', exports' : ''}`;
Expand Down Expand Up @@ -636,30 +717,37 @@ export function transformCommonjs(
magicString.overwrite(left.start, left.end, `var ${moduleName}`);
} else {
const [, name] = match;
const deconflicted = deconflict(scope, globals, name);

names.push({ name, deconflicted });
if (name !== KEY_COMPILED_ESM) {
const deconflicted = deconflict(scope, globals, name);

magicString.overwrite(node.start, left.end, `var ${deconflicted}`);
names.push({ name, deconflicted });

const declaration =
name === deconflicted
? `export { ${name} };`
: `export { ${deconflicted} as ${name} };`;
magicString.overwrite(node.start, left.end, `var ${deconflicted}`);

if (name !== 'default') {
namedExportDeclarations.push({
str: declaration,
name
});
}
const declaration =
name === deconflicted
? `export { ${name} };`
: `export { ${deconflicted} as ${name} };`;

if (name !== 'default') {
namedExportDeclarations.push({
str: declaration,
name
});
} else {
deconflictedDefaultExportName = deconflicted;
}

defaultExportPropertyAssignments.push(`${moduleName}.${name} = ${deconflicted};`);
defaultExportPropertyAssignments.push(`${moduleName}.${name} = ${deconflicted};`);
} else {
magicString.remove(node.start, node.end);
}
}
}
}

if (!(isEsModule || hasDefaultExport)) {
if (!(isEsModule || isCompiledEsModule || hasDefaultExport)) {
wrapperEnd = `\n\nvar ${moduleName} = {\n${names
.map(({ name, deconflicted }) => `\t${name}: ${deconflicted}`)
.join(',\n')}\n};`;
Expand All @@ -672,17 +760,21 @@ export function transformCommonjs(
.trim()
.append(wrapperEnd);

const defaultExport =
code.indexOf('__esModule') >= 0
? `export default /*@__PURE__*/${HELPERS_NAME}.getDefaultExportFromCjs(${moduleName});`
: `export default ${moduleName};`;
const defaultExport = [];
if (isCompiledEsModule) {
if (deconflictedDefaultExportName) {
defaultExport.push(`export default ${deconflictedDefaultExportName};`);
}
} else if (!isEsModule) {
defaultExport.push(`export default ${moduleName};`);
}

const named = namedExportDeclarations
.filter((x) => x.name !== 'default' || !hasDefaultExport)
.map((x) => x.str);

magicString.append(
`\n\n${(isEsModule ? [] : [defaultExport])
`\n\n${defaultExport
.concat(named)
.concat(hasDefaultExport ? defaultExportPropertyAssignments : [])
.join('\n')}`
Expand All @@ -691,5 +783,9 @@ export function transformCommonjs(
code = magicString.toString();
const map = sourceMap ? magicString.generateMap() : null;

return { code, map, syntheticNamedExports: isEsModule ? false : '__moduleExports' };
return {
code,
map,
syntheticNamedExports: isEsModule || isCompiledEsModule ? false : '__moduleExports'
};
}
4 changes: 3 additions & 1 deletion packages/commonjs/test/fixtures/.eslintrc
Expand Up @@ -10,6 +10,8 @@
"import/prefer-default-export": "off",
"import/extensions": "off",
"import/no-unresolved": "off",
"@typescript-eslint/no-unused-vars": "off"
"@typescript-eslint/no-unused-vars": "off",
"camelcase": "off",
"no-underscore-dangle": "off"
}
}
@@ -0,0 +1,3 @@
exports.__esModule = true;
exports.default = 'x';
exports.foo = 'foo';
@@ -0,0 +1,5 @@
var _default = 'x';
var foo = 'foo';

export default _default;
export { foo };
@@ -0,0 +1,3 @@
module.exports.__esModule = true;
module.exports.default = 'x';
module.exports.foo = 'foo';
@@ -0,0 +1,5 @@
var _default = 'x';
var foo = 'foo';

export default _default;
export { foo };
@@ -0,0 +1,4 @@
Object.defineProperty(exports, '__esModule', { value: true });
exports.foo = 'bar';

const foo = 'also bar';
@@ -0,0 +1,5 @@
const foo_1 = 'bar';

const foo = 'also bar';

export { foo_1 as foo };
@@ -0,0 +1,3 @@
Object.defineProperty(exports, '__esModule', { value: true });
exports.default = 'x';
exports.foo = 'foo';
@@ -0,0 +1,5 @@
var _default = 'x';
var foo = 'foo';

export default _default;
export { foo };
@@ -0,0 +1,2 @@
Object.defineProperty(exports, '__esModule', { value: !0 });
exports.foo = 'foo';
@@ -0,0 +1,3 @@
var foo = 'foo';

export { foo };
@@ -0,0 +1,3 @@
Object.defineProperty(exports, '__esModule', { value: true });
exports.foo = 'bar';
exports.bar = 'foo';
@@ -0,0 +1,5 @@
var foo = 'bar';
var bar = 'foo';

export { foo };
export { bar };
@@ -1,11 +1,5 @@
import * as entry from './entry.js';

t.deepEqual(entry, {
// Technically, this should ideally not exist, or if we cannot avoid it due
// to runtime default export detection, it should probably be undefined. We
// return the namespace instead as this will fix
// rollup/rollup-plugin-commonjs#224 until the remaining Rollup interop has
// been updated
default: { named: 'named' },
named: 'named'
});

0 comments on commit 1745141

Please sign in to comment.