Skip to content

Commit

Permalink
feat(commonjs)!: reconstruct real es module from __esModule marker (#537
Browse files Browse the repository at this point in the history
)

BREAKING CHANGES: #537 (comment)

* feat(commonjs): reconstruct real es module from __esModule marker

* fix(commonjs): handle module.exports reassignment

* fix(commonjs): preserve namespace default export fallback

* fix(commonjs): mark redefined module.exports as CJS

* chore(commonjs): fix tests

* Make tests actually throw when there is an error in the code and skip broken test

* chore(commonjs): Improve how AST branche are skipped

* fix(commonjs): Fix Rollup peer dependency version check

* refactor(commonjs): Use a switch statement for top level analysis

* refactor(commonjs): Restructure transform slightly

* refactor(commonjs): Extract helpers

* refactor(commonjs): Extract helpers

* refactor(commonjs): Move __esModule detection logic entirely within CJS transformer

* refactor(commonjs): Add __moduleExports back to compiled modules

* refactor(commonjs): Move more parts into switch statement

* refactor(commonjs): Completely refactor require handling

Finally remove second AST walk

* fix(commonjs): Handle nested and multiple __esModule definitions

* fix(commonjs): Handle shadowed imports for multiple requires

* fix(commonjs): Handle double assignments to exports

* chore(commonjs): Further cleanup

* refactor(commonjs): extract logic to rewrite imports

* feat(commonjs): only add interop if necessary

* refactor(commonjs): Do not add require helper unless used

* refactor(commonjs): Inline dynamic require handling into loop

* refactor(commonjs): Extract import logic

* refactor(commonjs): Extract export generation

* refactor(commonjs): Avoid empty lines before wrapped code

* Do not remove leading comments in files

* refactor(commonjs): Remove unused code

* fix(commonjs): Improve error message for unsupported dynamic requires

Co-authored-by: Lukas Taegert-Atkinson <lukas.taegert-atkinson@tngtech.com>
  • Loading branch information
LarsDenBakker and lukastaegert authored Nov 28, 2020
1 parent 40eee93 commit 21c51e0
Show file tree
Hide file tree
Showing 92 changed files with 1,936 additions and 1,499 deletions.
48 changes: 48 additions & 0 deletions packages/commonjs/src/analyze-top-level-statements.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/* eslint-disable no-underscore-dangle */

import { tryParse } from './parse';

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

let isEsModule = false;
let hasDefaultExport = false;
let hasNamedExports = false;

for (const node of ast.body) {
switch (node.type) {
case 'ExportDefaultDeclaration':
isEsModule = true;
hasDefaultExport = true;
break;
case 'ExportNamedDeclaration':
isEsModule = true;
if (node.declaration) {
hasNamedExports = true;
} else {
for (const specifier of node.specifiers) {
if (specifier.exported.name === 'default') {
hasDefaultExport = true;
} else {
hasNamedExports = true;
}
}
}
break;
case 'ExportAllDeclaration':
isEsModule = true;
if (node.exported && node.exported.name === 'default') {
hasDefaultExport = true;
} else {
hasNamedExports = true;
}
break;
case 'ImportDeclaration':
isEsModule = true;
break;
default:
}
}

return { isEsModule, hasDefaultExport, hasNamedExports, ast };
}
125 changes: 72 additions & 53 deletions packages/commonjs/src/ast-utils.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
/* eslint-disable no-undefined */
export { default as isReference } from 'is-reference';

const operators = {
Expand All @@ -17,34 +16,30 @@ const operators = {
'||': (x) => isTruthy(x.left) || isTruthy(x.right)
};

const extractors = {
Identifier(names, node) {
names.push(node.name);
},

ObjectPattern(names, node) {
node.properties.forEach((prop) => {
getExtractor(prop.value.type)(names, prop.value);
});
},

ArrayPattern(names, node) {
node.elements.forEach((element) => {
if (!element) return;
getExtractor(element.type)(names, element);
});
},

RestElement(names, node) {
getExtractor(node.argument.type)(names, node.argument);
},

AssignmentPattern(names, node) {
getExtractor(node.left.type)(names, node.left);
}
};
function not(value) {
return value === null ? value : !value;
}

function equals(a, b, strict) {
if (a.type !== b.type) return null;
// eslint-disable-next-line eqeqeq
if (a.type === 'Literal') return strict ? a.value === b.value : a.value == b.value;
return null;
}

export function isTruthy(node) {
if (!node) return false;
if (node.type === 'Literal') return !!node.value;
if (node.type === 'ParenthesizedExpression') return isTruthy(node.expression);
if (node.operator in operators) return operators[node.operator](node);
return null;
}

export function flatten(node) {
export function isFalsy(node) {
return not(isTruthy(node));
}

export function getKeypath(node) {
const parts = [];

while (node.type === 'MemberExpression') {
Expand All @@ -63,36 +58,60 @@ export function flatten(node) {
return { name, keypath: parts.join('.') };
}

export function extractNames(node) {
const names = [];
extractors[node.type](names, node);
return names;
}
export const KEY_COMPILED_ESM = '__esModule';

function getExtractor(type) {
const extractor = extractors[type];
if (!extractor) throw new SyntaxError(`${type} pattern not supported.`);
return extractor;
export function isDefineCompiledEsm(node) {
const definedProperty =
getDefinePropertyCallName(node, 'exports') || getDefinePropertyCallName(node, 'module.exports');
if (definedProperty && definedProperty.key === KEY_COMPILED_ESM) {
return isTruthy(definedProperty.value);
}
return false;
}

export function isTruthy(node) {
if (node.type === 'Literal') return !!node.value;
if (node.type === 'ParenthesizedExpression') return isTruthy(node.expression);
if (node.operator in operators) return operators[node.operator](node);
return undefined;
}
function getDefinePropertyCallName(node, targetName) {
const targetNames = targetName.split('.');

const {
callee: { object, property }
} = node;
if (!object || object.type !== 'Identifier' || object.name !== 'Object') return;
if (!property || property.type !== 'Identifier' || property.name !== 'defineProperty') return;
if (node.arguments.length !== 3) return;

const [target, key, value] = node.arguments;
if (targetNames.length === 1) {
if (target.type !== 'Identifier' || target.name !== targetNames[0]) {
return;
}
}

export function isFalsy(node) {
return not(isTruthy(node));
}
if (targetNames.length === 2) {
if (
target.type !== 'MemberExpression' ||
target.object.name !== targetNames[0] ||
target.property.name !== targetNames[1]
) {
return;
}
}

function not(value) {
return value === undefined ? value : !value;
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 { key: key.value, value: valueProperty.value };
}

function equals(a, b, strict) {
if (a.type !== b.type) return undefined;
// eslint-disable-next-line eqeqeq
if (a.type === 'Literal') return strict ? a.value === b.value : a.value == b.value;
return undefined;
export function isLocallyShadowed(name, scope) {
while (scope.parent) {
if (scope.declarations[name]) {
return true;
}
// eslint-disable-next-line no-param-reassign
scope = scope.parent;
}
return false;
}
9 changes: 2 additions & 7 deletions packages/commonjs/src/dynamic-packages-manager.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
import { existsSync, readFileSync } from 'fs';
import { join } from 'path';

import {
DYNAMIC_PACKAGES_ID,
DYNAMIC_REGISTER_PREFIX,
getVirtualPathForDynamicRequirePath,
HELPERS_ID
} from './helpers';
import { normalizePathSlashes } from './transform';
import { DYNAMIC_PACKAGES_ID, DYNAMIC_REGISTER_PREFIX, HELPERS_ID } from './helpers';
import { getVirtualPathForDynamicRequirePath, normalizePathSlashes } from './utils';

export function getDynamicPackagesModule(dynamicRequireModuleDirPaths, commonDir) {
let code = `const commonjsRegister = require('${HELPERS_ID}?commonjsRegister');`;
Expand Down
2 changes: 1 addition & 1 deletion packages/commonjs/src/dynamic-require-paths.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { resolve } from 'path';

import glob from 'glob';

import { normalizePathSlashes } from './transform';
import { normalizePathSlashes } from './utils';

export default function getDynamicRequirePaths(patterns) {
const dynamicRequireModuleSet = new Set();
Expand Down
97 changes: 97 additions & 0 deletions packages/commonjs/src/generate-exports.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
export function wrapCode(magicString, uses, moduleName, HELPERS_NAME, virtualDynamicRequirePath) {
const args = `module${uses.exports ? ', exports' : ''}`;

magicString
.trim()
.prepend(`var ${moduleName} = ${HELPERS_NAME}.createCommonjsModule(function (${args}) {\n`)
.append(
`\n}${virtualDynamicRequirePath ? `, ${JSON.stringify(virtualDynamicRequirePath)}` : ''});`
);
}

export function rewriteExportsAndGetExportsBlock(
magicString,
moduleName,
wrapped,
topLevelModuleExportsAssignments,
topLevelExportsAssignmentsByName,
defineCompiledEsmExpressions,
deconflict,
isRestorableCompiledEsm,
code,
uses,
HELPERS_NAME
) {
const namedExportDeclarations = [`export { ${moduleName} as __moduleExports };`];
const moduleExportsPropertyAssignments = [];
let deconflictedDefaultExportName;

if (!wrapped) {
let hasModuleExportsAssignment = false;
const namedExportProperties = [];

// Collect and rewrite module.exports assignments
for (const { left } of topLevelModuleExportsAssignments) {
hasModuleExportsAssignment = true;
magicString.overwrite(left.start, left.end, `var ${moduleName}`);
}

// Collect and rewrite named exports
for (const [exportName, node] of topLevelExportsAssignmentsByName) {
const deconflicted = deconflict(exportName);
magicString.overwrite(node.start, node.left.end, `var ${deconflicted}`);

if (exportName === 'default') {
deconflictedDefaultExportName = deconflicted;
} else {
namedExportDeclarations.push(
exportName === deconflicted
? `export { ${exportName} };`
: `export { ${deconflicted} as ${exportName} };`
);
}

if (hasModuleExportsAssignment) {
moduleExportsPropertyAssignments.push(`${moduleName}.${exportName} = ${deconflicted};`);
} else {
namedExportProperties.push(`\t${exportName}: ${deconflicted}`);
}
}

// Regenerate CommonJS namespace
if (!hasModuleExportsAssignment) {
const moduleExports = `{\n${namedExportProperties.join(',\n')}\n}`;
magicString
.trim()
.append(
`\n\nvar ${moduleName} = ${
isRestorableCompiledEsm
? `/*#__PURE__*/Object.defineProperty(${moduleExports}, '__esModule', {value: true})`
: moduleExports
};`
);
}
}

// Generate default export
const defaultExport = [];
if (isRestorableCompiledEsm) {
defaultExport.push(`export default ${deconflictedDefaultExportName || moduleName};`);
} else if (
(wrapped || deconflictedDefaultExportName) &&
(defineCompiledEsmExpressions.length > 0 || code.indexOf('__esModule') >= 0)
) {
// eslint-disable-next-line no-param-reassign
uses.commonjsHelpers = true;
defaultExport.push(
`export default /*@__PURE__*/${HELPERS_NAME}.getDefaultExportFromCjs(${moduleName});`
);
} else {
defaultExport.push(`export default ${moduleName};`);
}

return `\n\n${defaultExport
.concat(namedExportDeclarations)
.concat(moduleExportsPropertyAssignments)
.join('\n')}`;
}
Loading

0 comments on commit 21c51e0

Please sign in to comment.