Skip to content

Commit

Permalink
feat(commonjs): add strictRequires option to wrap modules (#1038)
Browse files Browse the repository at this point in the history
  • Loading branch information
lukastaegert committed Apr 24, 2022
1 parent 493f753 commit e8dd3c3
Show file tree
Hide file tree
Showing 39 changed files with 711 additions and 328 deletions.
87 changes: 57 additions & 30 deletions packages/commonjs/src/generate-exports.js
Expand Up @@ -11,8 +11,9 @@ export function wrapCode(magicString, uses, moduleName, exportsName) {
}
magicString
.trim()
.indent('\t')
.prepend(`(function (${args.join(', ')}) {\n`)
.append(`\n}(${passedArgs.join(', ')}));`);
.append(`\n} (${passedArgs.join(', ')}));`);
}

export function rewriteExportsAndGetExportsBlock(
Expand All @@ -38,34 +39,18 @@ export function rewriteExportsAndGetExportsBlock(
const exportDeclarations = [];

if (usesRequireWrapper) {
// TODO Lukas Extract
if (exportMode === 'replace') {
for (const { left } of moduleExportsAssignments) {
magicString.overwrite(left.start, left.end, exportsName);
}
} else {
// Collect and rewrite module.exports assignments
for (const { left } of moduleExportsAssignments) {
magicString.overwrite(left.start, left.end, `${moduleName}.exports`);
}
// Collect and rewrite named exports
for (const [exportName, { nodes }] of exportsAssignmentsByName) {
for (const node of nodes) {
magicString.overwrite(node.start, node.left.end, `${exportsName}.${exportName}`);
}
}
// Collect and rewrite exports.__esModule assignments
for (const expression of defineCompiledEsmExpressions) {
const moduleExportsExpression =
expression.type === 'CallExpression' ? expression.arguments[0] : expression.left.object;
magicString.overwrite(
moduleExportsExpression.start,
moduleExportsExpression.end,
exportsName
);
}
}
exports.push(`${requireName} as __require`);
getExportsWhenUsingRequireWrapper(
magicString,
wrapped,
exportMode,
exports,
moduleExportsAssignments,
exportsAssignmentsByName,
moduleName,
exportsName,
requireName,
defineCompiledEsmExpressions
);
} else if (exportMode === 'replace') {
getExportsForReplacedModuleExports(
magicString,
Expand Down Expand Up @@ -109,6 +94,49 @@ export function rewriteExportsAndGetExportsBlock(
return `\n\n${exportDeclarations.join('\n')}`;
}

function getExportsWhenUsingRequireWrapper(
magicString,
wrapped,
exportMode,
exports,
moduleExportsAssignments,
exportsAssignmentsByName,
moduleName,
exportsName,
requireName,
defineCompiledEsmExpressions
) {
if (!wrapped) {
if (exportMode === 'replace') {
for (const { left } of moduleExportsAssignments) {
magicString.overwrite(left.start, left.end, exportsName);
}
} else {
// Collect and rewrite module.exports assignments
for (const { left } of moduleExportsAssignments) {
magicString.overwrite(left.start, left.end, `${moduleName}.exports`);
}
// Collect and rewrite named exports
for (const [exportName, { nodes }] of exportsAssignmentsByName) {
for (const node of nodes) {
magicString.overwrite(node.start, node.left.end, `${exportsName}.${exportName}`);
}
}
// Collect and rewrite exports.__esModule assignments
for (const expression of defineCompiledEsmExpressions) {
const moduleExportsExpression =
expression.type === 'CallExpression' ? expression.arguments[0] : expression.left.object;
magicString.overwrite(
moduleExportsExpression.start,
moduleExportsExpression.end,
exportsName
);
}
}
}
exports.push(`${requireName} as __require`);
}

function getExportsForReplacedModuleExports(
magicString,
exports,
Expand Down Expand Up @@ -196,7 +224,6 @@ function getExports(
}

if (!isRestorableCompiledEsm || defaultIsModuleExports === true) {
// TODO Lukas handle ESM importing CommonJS
exports.push(`${exportsName} as default`);
} else if (moduleExportsAssignments.length === 0 || defaultIsModuleExports === false) {
exports.push(`${deconflictedDefaultExportName || exportsName} as default`);
Expand Down
84 changes: 51 additions & 33 deletions packages/commonjs/src/generate-imports.js
Expand Up @@ -2,7 +2,14 @@ import { dirname, resolve } from 'path';

import { sync as nodeResolveSync } from 'resolve';

import { DYNAMIC_MODULES_ID, EXPORTS_SUFFIX, HELPERS_ID, MODULE_SUFFIX, wrapId } from './helpers';
import {
DYNAMIC_MODULES_ID,
EXPORTS_SUFFIX,
HELPERS_ID,
IS_WRAPPED_COMMONJS,
MODULE_SUFFIX,
wrapId
} from './helpers';
import { normalizePathSlashes } from './utils';

export function isRequireStatement(node, scope) {
Expand Down Expand Up @@ -106,6 +113,7 @@ export function getRequireHandlers() {
exportMode,
resolveRequireSourcesAndGetMeta,
usesRequireWrapper,
isEsModule,
usesRequire
) {
const imports = [];
Expand All @@ -127,40 +135,12 @@ export function getRequireHandlers() {
);
}
const requiresBySource = collectSources(requireExpressions);
// TODO Lukas consider extracting stuff
const result = await resolveRequireSourcesAndGetMeta(
usesRequireWrapper ? 'withRequireFunction' : true,
const requireTargets = await resolveRequireSourcesAndGetMeta(
id,
usesRequireWrapper ? IS_WRAPPED_COMMONJS : !isEsModule,
Object.keys(requiresBySource)
);
let uid = 0;
for (const { source, id: resolveId, isCommonJS } of result) {
const requires = requiresBySource[source];
let usesRequired = false;
let name;
const hasNameConflict = ({ scope }) => scope.contains(name);
do {
name = `require$$${uid}`;
uid += 1;
} while (requires.some(hasNameConflict));

// TODO Lukas extract constant
if (isCommonJS === 'withRequireFunction') {
for (const { node } of requires) {
magicString.overwrite(node.start, node.end, `${name}()`);
}
imports.push(`import { __require as ${name} } from ${JSON.stringify(resolveId)};`);
} else {
for (const { node, usesReturnValue, toBeRemoved } of requires) {
if (usesReturnValue) {
usesRequired = true;
magicString.overwrite(node.start, node.end, name);
} else {
magicString.remove(toBeRemoved.start, toBeRemoved.end);
}
}
imports.push(`import ${usesRequired ? `${name} from ` : ''}${JSON.stringify(resolveId)};`);
}
}
processRequireExpressions(imports, requireTargets, requiresBySource, magicString);
return imports.length ? `${imports.join('\n')}\n\n` : '';
}

Expand All @@ -181,3 +161,41 @@ function collectSources(requireExpressions) {
}
return requiresBySource;
}

function processRequireExpressions(imports, requireTargets, requiresBySource, magicString) {
const generateRequireName = getGenerateRequireName();
for (const { source, id: resolveId, isCommonJS } of requireTargets) {
const requires = requiresBySource[source];
const name = generateRequireName(requires);
if (isCommonJS === IS_WRAPPED_COMMONJS) {
for (const { node } of requires) {
magicString.overwrite(node.start, node.end, `${name}()`);
}
imports.push(`import { __require as ${name} } from ${JSON.stringify(resolveId)};`);
} else {
let usesRequired = false;
for (const { node, usesReturnValue, toBeRemoved } of requires) {
if (usesReturnValue) {
usesRequired = true;
magicString.overwrite(node.start, node.end, name);
} else {
magicString.remove(toBeRemoved.start, toBeRemoved.end);
}
}
imports.push(`import ${usesRequired ? `${name} from ` : ''}${JSON.stringify(resolveId)};`);
}
}
}

function getGenerateRequireName() {
let uid = 0;
return (requires) => {
let name;
const hasNameConflict = ({ scope }) => scope.contains(name);
do {
name = `require$$${uid}`;
uid += 1;
} while (requires.some(hasNameConflict));
return name;
};
}
2 changes: 2 additions & 0 deletions packages/commonjs/src/helpers.js
Expand Up @@ -11,6 +11,8 @@ export const ES_IMPORT_SUFFIX = '?es-import';
export const DYNAMIC_MODULES_ID = '\0commonjs-dynamic-modules';
export const HELPERS_ID = '\0commonjsHelpers.js';

export const IS_WRAPPED_COMMONJS = 'withRequireFunction';

// `x['default']` is used instead of `x.default` for backward compatibility with ES3 browsers.
// Minifiers like uglify will usually transpile it back if compatibility with ES3 is not enabled.
// This could be improved by inspecting Rollup's "generatedCode" option
Expand Down

0 comments on commit e8dd3c3

Please sign in to comment.