Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

improve esm conversion #31

Closed
wants to merge 2 commits into from
Closed
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
19 changes: 6 additions & 13 deletions packages/webcrack/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,23 +151,16 @@ export async function webcrack(
}),
options.mangle && (() => applyTransform(ast, mangle)),
// TODO: Also merge unminify visitor (breaks selfDefending/debugProtection atm)
(options.deobfuscate || options.jsx) &&
options.deobfuscate &&
(() => {
return applyTransforms(
ast,
[
// Have to run this after unminify to properly detect it
options.deobfuscate ? [selfDefending, debugProtection] : [],
options.jsx ? [jsx, jsxNew] : [],
].flat(),
{ noScope: true },
);
return applyTransforms(ast, [selfDefending, debugProtection], {
noScope: true,
});
}),
options.deobfuscate && (() => applyTransform(ast, mergeObjectAssignments)),
() => (outputCode = generate(ast)),
// Unpacking modifies the same AST and may result in imports not at top level
// so the code has to be generated before
options.unpack && (() => (bundle = unpackAST(ast, options.mappings(m)))),
options.jsx && (() => applyTransforms(ast, [jsx, jsxNew])),
() => (outputCode = generate(ast)),
].filter(Boolean) as (() => unknown)[];

for (let i = 0; i < stages.length; i++) {
Expand Down
4 changes: 2 additions & 2 deletions packages/webcrack/src/unpack/webpack/bundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ import * as t from '@babel/types';
import * as m from '@codemod/matchers';
import { Bundle } from '../bundle';
import { relativePath } from '../path';
import { convertDefaultRequire } from './default-export';
import { convertESM } from './esm';
import { convertDefaultRequire } from './getDefaultExport';
import { WebpackModule } from './module';
import { inlineVarInjections } from './varInjection';
import { inlineVarInjections } from './var-injection';

export class WebpackBundle extends Bundle {
constructor(entryId: string, modules: Map<string, WebpackModule>) {
Expand Down
129 changes: 108 additions & 21 deletions packages/webcrack/src/unpack/webpack/esm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ import { constMemberExpression, findPath, renameFast } from '../../ast-utils';
import { WebpackModule } from './module';

const buildNamespaceImport = statement`import * as NAME from "PATH";`;
const buildNamedImport = (locals: string[], imported: string[], path: string) =>
t.importDeclaration(
locals.map((local, i) =>
t.importSpecifier(t.identifier(local), t.identifier(imported[i])),
),
t.stringLiteral(path),
);
const buildNamedExportLet = statement`export let NAME = VALUE;`;

/**
Expand All @@ -27,46 +34,64 @@ export function convertESM(module: WebpackModule): void {
m.callExpression(constMemberExpression('require', 'r'), [m.identifier()]),
);

const exportsName = m.capture(m.identifier());
const exportsObjectName = m.capture(m.identifier());
const exportedName = m.capture(m.anyString());
const returnedValue = m.capture(m.anyExpression());
const exportedLocal = m.capture(m.anyExpression());
// E.g. require.d(exports, "counter", function () { return f });
const defineExportMatcher = m.expressionStatement(
m.callExpression(constMemberExpression('require', 'd'), [
exportsName,
exportsObjectName,
m.stringLiteral(exportedName),
m.functionExpression(
undefined,
[],
m.blockStatement([m.returnStatement(returnedValue)]),
m.blockStatement([m.returnStatement(exportedLocal)]),
),
]),
);
const exportAssignment = m.expressionStatement(
m.assignmentExpression(
'=',
m.memberExpression(
m.identifier('exports'),
m.identifier(exportedName),
false,
),
exportedLocal,
),
);

const emptyObjectVarMatcher = m.variableDeclarator(
m.fromCapture(exportsName),
m.fromCapture(exportsObjectName),
m.objectExpression([]),
);

const properties = m.capture(
m.arrayOf(
m.objectProperty(
m.identifier(),
m.arrowFunctionExpression([], m.anyExpression()),
m.or(
m.arrowFunctionExpression([], m.anyExpression()),
m.functionExpression(
null,
[],
m.blockStatement([m.returnStatement()]),
),
),
),
),
);
// E.g. require.d(exports, { foo: () => a, bar: () => b });
const defineExportsMatcher = m.expressionStatement(
m.callExpression(constMemberExpression('require', 'd'), [
exportsName,
exportsObjectName,
m.objectExpression(properties),
]),
);

// E.g. const lib = require("./lib.js");
const requireVariable = m.capture(m.identifier());
const requiredModuleId = m.capture(m.anyNumber());
// E.g. const lib = require(1);
const requireMatcher = m.variableDeclaration(undefined, [
m.variableDeclarator(
requireVariable,
Expand All @@ -76,6 +101,11 @@ export function convertESM(module: WebpackModule): void {
),
]);

const zeroSequenceMatcher = m.sequenceExpression([
m.numericLiteral(0),
m.identifier(),
]);

// module = require.hmd(module);
const hmdMatcher = m.expressionStatement(
m.assignmentExpression(
Expand All @@ -93,18 +123,64 @@ export function convertESM(module: WebpackModule): void {
if (defineEsModuleMatcher.match(path.node)) {
module.ast.program.sourceType = 'module';
path.remove();
} else if (
module.ast.program.sourceType === 'module' &&
requireMatcher.match(path.node)
) {
path.replaceWith(
buildNamespaceImport({
NAME: requireVariable.current,
PATH: String(requiredModuleId.current),
}),
} else if (requireMatcher.match(path.node)) {
const binding = path.scope.getBinding(requireVariable.current!.name)!;
const references = binding.referencePaths.map((p) => p.parentPath!);
const validateReferences = (
references: NodePath[],
): references is NodePath<
t.MemberExpression & { property: t.Identifier }
>[] =>
references.every((p) =>
m
.memberExpression(
m.fromCapture(requireVariable),
m.identifier(),
false,
)
.match(p.node),
);
if (!validateReferences(references)) {
path.replaceWith(
buildNamespaceImport({
NAME: requireVariable.current,
PATH: String(requiredModuleId.current),
}),
);
return;
}

const importNames = [
...new Set(references.map((p) => p.node.property.name)),
];
const localNames = importNames.map((name) => {
const hasNameConflict = binding.referencePaths.some((ref) =>
ref.scope.hasBinding(name),
);
return hasNameConflict ? path.scope.generateUid(name) : name;
});

const [importDeclaration] = path.replaceWith(
buildNamedImport(
localNames,
importNames,
String(requiredModuleId.current),
),
);
importDeclaration.scope.crawl();

[...references].forEach((ref) => {
const localName =
localNames[importNames.indexOf(ref.node.property.name)];
ref.replaceWith(t.identifier(localName));
if (zeroSequenceMatcher.match(ref.parent)) {
ref.parentPath.replaceWith(ref);
}
});
} else if (defineExportsMatcher.match(path.node)) {
const exportsBinding = path.scope.getBinding(exportsName.current!.name);
const exportsBinding = path.scope.getBinding(
exportsObjectName.current!.name,
);
const emptyObject = emptyObjectVarMatcher.match(
exportsBinding?.path.node,
)
Expand All @@ -113,8 +189,12 @@ export function convertESM(module: WebpackModule): void {

for (const property of properties.current!) {
const exportedKey = property.key as t.Identifier;
const returnedValue = (property.value as t.ArrowFunctionExpression)
.body as t.Expression;
const returnedValue = t.isArrowFunctionExpression(property.value)
? (property.value.body as t.Expression)
: ((
(property.value as t.FunctionExpression).body
.body[0] as t.ReturnStatement
).argument as t.Expression);
if (emptyObject) {
emptyObject.properties.push(
t.objectProperty(exportedKey, returnedValue),
Expand All @@ -125,8 +205,15 @@ export function convertESM(module: WebpackModule): void {
}

path.remove();
} else if (exportAssignment.match(path.node)) {
path.replaceWith(
buildNamedExportLet({
NAME: t.identifier(exportedName.current!),
VALUE: exportedLocal.current!,
}),
);
} else if (defineExportMatcher.match(path.node)) {
exportVariable(path, returnedValue.current!, exportedName.current!);
exportVariable(path, exportedLocal.current!, exportedName.current!);
path.remove();
} else if (hmdMatcher.match(path.node)) {
path.remove();
Expand Down
Loading