diff --git a/apps/playground/src/App.tsx b/apps/playground/src/App.tsx
index 4619dfba..8bfc9579 100644
--- a/apps/playground/src/App.tsx
+++ b/apps/playground/src/App.tsx
@@ -119,7 +119,7 @@ function App() {
...result.files.map((file) =>
monaco.editor.createModel(
file.code,
- 'javascript',
+ file.path.endsWith('.json') ? 'json' : 'javascript',
monaco.Uri.file(file.path),
),
),
diff --git a/apps/playground/src/components/FileNode.tsx b/apps/playground/src/components/FileNode.tsx
index 56409a20..05438735 100644
--- a/apps/playground/src/components/FileNode.tsx
+++ b/apps/playground/src/components/FileNode.tsx
@@ -4,21 +4,38 @@ interface Props extends TreeNode {
onClick?: () => void;
}
+// Icons are from https://github.com/vscode-icons/vscode-icons
export default function FileNode(props: Props) {
function handleClick(event: Event) {
event.stopPropagation();
props.onClick?.();
}
+ const icon = () =>
+ props.name.endsWith('.json') ? (
+
+ ) : (
+
+ );
+
return (
-
+ {icon()}
{props.name}
diff --git a/apps/playground/src/context/DeobfuscateContext.tsx b/apps/playground/src/context/DeobfuscateContext.tsx
index f78be626..c64b407b 100644
--- a/apps/playground/src/context/DeobfuscateContext.tsx
+++ b/apps/playground/src/context/DeobfuscateContext.tsx
@@ -1,4 +1,4 @@
-import type { ParentProps} from 'solid-js';
+import type { ParentProps } from 'solid-js';
import { createContext, createSignal, useContext } from 'solid-js';
import type { Options } from 'webcrack';
import { evalCode } from '../sandbox';
diff --git a/apps/playground/src/webcrack.worker.ts b/apps/playground/src/webcrack.worker.ts
index 0c6e7814..9e4cd4ba 100644
--- a/apps/playground/src/webcrack.worker.ts
+++ b/apps/playground/src/webcrack.worker.ts
@@ -1,4 +1,4 @@
-import type { Options, Sandbox} from 'webcrack';
+import type { Options, Sandbox } from 'webcrack';
import { webcrack } from 'webcrack';
export type WorkerRequest =
diff --git a/packages/webcrack/src/ast-utils/binding.ts b/packages/webcrack/src/ast-utils/binding.ts
new file mode 100644
index 00000000..9f82bba0
--- /dev/null
+++ b/packages/webcrack/src/ast-utils/binding.ts
@@ -0,0 +1,15 @@
+import type { Binding } from '@babel/traverse';
+import type * as t from '@babel/types';
+
+/**
+ * Remove a referencePath from a binding and decrement the amount of references.
+ */
+export function dereference(binding: Binding, reference: t.Node) {
+ const index = binding.referencePaths.findIndex(
+ (ref) => ref.node === reference,
+ );
+ if (index !== -1) {
+ binding.referencePaths.splice(index, 1);
+ binding.dereference();
+ }
+}
diff --git a/packages/webcrack/src/ast-utils/matcher.ts b/packages/webcrack/src/ast-utils/matcher.ts
index 7e2eb2c6..577b4d57 100644
--- a/packages/webcrack/src/ast-utils/matcher.ts
+++ b/packages/webcrack/src/ast-utils/matcher.ts
@@ -49,7 +49,7 @@ export const iife = matchIife();
export const emptyIife = matchIife([]);
/**
- * Matches both identifier properties and string literal computed properties
+ * Matches either `object.property` or `object['property']`
*/
export function constMemberExpression(
object: string | m.Matcher,
@@ -107,6 +107,25 @@ export function findPath(
return path.find((path) => matcher.match(path.node)) as NodePath | null;
}
+/**
+ * Matches a function expression or arrow function expression with a block body.
+ */
+export function anyFunctionExpression(
+ params?:
+ | m.Matcher<(t.Identifier | t.Pattern | t.RestElement)[]>
+ | (
+ | m.Matcher
+ | m.Matcher
+ | m.Matcher
+ )[],
+ body?: m.Matcher,
+): m.Matcher {
+ return m.or(
+ m.functionExpression(undefined, params, body),
+ m.arrowFunctionExpression(params, body),
+ );
+}
+
/**
* Function expression matcher that captures the parameters
* and allows them to be referenced in the body.
diff --git a/packages/webcrack/src/ast-utils/rename.ts b/packages/webcrack/src/ast-utils/rename.ts
index b8cebeba..41344dfd 100644
--- a/packages/webcrack/src/ast-utils/rename.ts
+++ b/packages/webcrack/src/ast-utils/rename.ts
@@ -5,6 +5,8 @@ import * as m from '@codemod/matchers';
import { codePreview } from './generator';
export function renameFast(binding: Binding, newName: string): void {
+ if (binding.identifier.name === newName) return;
+
binding.referencePaths.forEach((ref) => {
if (!ref.isIdentifier()) {
throw new Error(
diff --git a/packages/webcrack/src/ast-utils/transform.ts b/packages/webcrack/src/ast-utils/transform.ts
index f723a337..3b99c208 100644
--- a/packages/webcrack/src/ast-utils/transform.ts
+++ b/packages/webcrack/src/ast-utils/transform.ts
@@ -12,7 +12,7 @@ export async function applyTransformAsync(
logger(`${transform.name}: started`);
const state: TransformState = { changes: 0 };
- await transform.run?.(ast, state, options);
+ await transform.run?.call(state, ast, options);
if (transform.visitor)
traverse(ast, transform.visitor(options), undefined, state);
@@ -28,7 +28,7 @@ export function applyTransform(
): TransformState {
logger(`${transform.name}: started`);
const state: TransformState = { changes: 0 };
- transform.run?.(ast, state, options);
+ transform.run?.call(state, ast, options);
if (transform.visitor) {
const visitor = transform.visitor(
@@ -53,7 +53,7 @@ export function applyTransforms(
const state: TransformState = { changes: 0 };
for (const transform of transforms) {
- transform.run?.(ast, state);
+ transform.run?.call(state, ast, state);
}
const traverseOptions = transforms.flatMap((t) => t.visitor?.() ?? []);
@@ -93,13 +93,13 @@ export interface Transform {
name: string;
tags: Tag[];
scope?: boolean;
- run?: (ast: Node, state: TransformState, options?: TOptions) => void;
+ run?: (this: TransformState, ast: Node, options?: TOptions) => void;
visitor?: (options?: TOptions) => Visitor;
}
export interface AsyncTransform
extends Transform {
- run?: (ast: Node, state: TransformState, options?: TOptions) => Promise;
+ run?: (this: TransformState, ast: Node, options?: TOptions) => Promise;
}
export type Tag = 'safe' | 'unsafe';
diff --git a/packages/webcrack/src/deobfuscate/index.ts b/packages/webcrack/src/deobfuscate/index.ts
index 4d17408e..9afd77ad 100644
--- a/packages/webcrack/src/deobfuscate/index.ts
+++ b/packages/webcrack/src/deobfuscate/index.ts
@@ -26,7 +26,7 @@ export default {
name: 'deobfuscate',
tags: ['unsafe'],
scope: true,
- async run(ast, state, sandbox) {
+ async run(ast, sandbox) {
if (!sandbox) return;
const logger = debug('webcrack:deobfuscate');
@@ -44,10 +44,10 @@ export default {
const decoders = findDecoders(stringArray);
logger(`String Array Encodings: ${decoders.length}`);
- state.changes += applyTransform(ast, inlineObjectProps).changes;
+ this.changes += applyTransform(ast, inlineObjectProps).changes;
for (const decoder of decoders) {
- state.changes += applyTransform(
+ this.changes += applyTransform(
ast,
inlineDecoderWrappers,
decoder.path,
@@ -55,7 +55,7 @@ export default {
}
const vm = new VMDecoder(sandbox, stringArray, decoders, rotator);
- state.changes += (
+ this.changes += (
await applyTransformAsync(ast, inlineDecodedStrings, { vm })
).changes;
@@ -63,10 +63,10 @@ export default {
stringArray.path.remove();
rotator?.remove();
decoders.forEach((decoder) => decoder.path.remove());
- state.changes += 2 + decoders.length;
+ this.changes += 2 + decoders.length;
}
- state.changes += applyTransforms(
+ this.changes += applyTransforms(
ast,
[mergeStrings, deadCode, controlFlowObject, controlFlowSwitch],
{ noScope: true },
diff --git a/packages/webcrack/src/deobfuscate/inline-decoded-strings.ts b/packages/webcrack/src/deobfuscate/inline-decoded-strings.ts
index f9515e1d..35ca8ec2 100644
--- a/packages/webcrack/src/deobfuscate/inline-decoded-strings.ts
+++ b/packages/webcrack/src/deobfuscate/inline-decoded-strings.ts
@@ -10,7 +10,7 @@ export default {
name: 'inline-decoded-strings',
tags: ['unsafe'],
scope: true,
- async run(ast, state, options) {
+ async run(ast, options) {
if (!options) return;
const calls = options.vm.decoders.flatMap((decoder) =>
@@ -27,6 +27,6 @@ export default {
call.addComment('leading', 'webcrack:decode_error');
}
- state.changes += calls.length;
+ this.changes += calls.length;
},
} satisfies AsyncTransform<{ vm: VMDecoder }>;
diff --git a/packages/webcrack/src/deobfuscate/inline-decoder-wrappers.ts b/packages/webcrack/src/deobfuscate/inline-decoder-wrappers.ts
index a534ee4a..4b54cbe9 100644
--- a/packages/webcrack/src/deobfuscate/inline-decoder-wrappers.ts
+++ b/packages/webcrack/src/deobfuscate/inline-decoder-wrappers.ts
@@ -10,14 +10,14 @@ export default {
name: 'inline-decoder-wrappers',
tags: ['unsafe'],
scope: true,
- run(ast, state, decoder) {
+ run(ast, decoder) {
if (!decoder?.node.id) return;
const decoderName = decoder.node.id.name;
const decoderBinding = decoder.parentPath.scope.getBinding(decoderName);
if (decoderBinding) {
- state.changes += inlineVariableAliases(decoderBinding).changes;
- state.changes += inlineFunctionAliases(decoderBinding).changes;
+ this.changes += inlineVariableAliases(decoderBinding).changes;
+ this.changes += inlineFunctionAliases(decoderBinding).changes;
}
},
} satisfies Transform>;
diff --git a/packages/webcrack/src/index.ts b/packages/webcrack/src/index.ts
index 5216eb42..73001bef 100644
--- a/packages/webcrack/src/index.ts
+++ b/packages/webcrack/src/index.ts
@@ -154,23 +154,15 @@ export async function webcrack(
}),
options.mangle && (() => applyTransform(ast, mangle)),
// TODO: Also merge unminify visitor (breaks selfDefending/debugProtection atm)
- (options.deobfuscate || options.jsx) &&
- (() => {
- return applyTransforms(
- ast,
- [
- // Have to run this after unminify to properly detect it
- options.deobfuscate ? [selfDefending, debugProtection] : [],
- options.jsx ? [jsx, jsxNew] : [],
- ].flat(),
- { noScope: true },
- );
- }),
+ options.deobfuscate &&
+ (() =>
+ 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++) {
diff --git a/packages/webcrack/src/unpack/bundle.ts b/packages/webcrack/src/unpack/bundle.ts
index 17ec8fe4..62b864e1 100644
--- a/packages/webcrack/src/unpack/bundle.ts
+++ b/packages/webcrack/src/unpack/bundle.ts
@@ -21,6 +21,7 @@ export class Bundle {
this.modules = modules;
}
+ // TODO: remove/deprecate (use module onResolve instead)
applyMappings(mappings: Record>): void {
const mappingPaths = Object.keys(mappings);
if (mappingPaths.length === 0) return;
@@ -85,6 +86,4 @@ export class Bundle {
}),
);
}
-
- applyTransforms(): void {}
}
diff --git a/packages/webcrack/src/unpack/index.ts b/packages/webcrack/src/unpack/index.ts
index 81ecdd27..899ad851 100644
--- a/packages/webcrack/src/unpack/index.ts
+++ b/packages/webcrack/src/unpack/index.ts
@@ -1,40 +1,30 @@
-import { parse } from '@babel/parser';
import traverse, { visitors } from '@babel/traverse';
import type * as t from '@babel/types';
import type * as m from '@codemod/matchers';
+import debug from 'debug';
import { unpackBrowserify } from './browserify';
import type { Bundle } from './bundle';
-import { unpackWebpack } from './webpack';
-import debug from 'debug';
+import unpackWebpack4 from './webpack/unpack-webpack-4';
+import unpackWebpack5 from './webpack/unpack-webpack-5';
+import unpackWebpackChunk from './webpack/unpack-webpack-chunk';
export { Bundle } from './bundle';
-export function unpack(
- code: string,
- mappings: Record> = {},
-): Bundle | undefined {
- const ast = parse(code, {
- sourceType: 'unambiguous',
- allowReturnOutsideFunction: true,
- plugins: ['jsx'],
- });
- return unpackAST(ast, mappings);
-}
-
export function unpackAST(
ast: t.Node,
mappings: Record> = {},
): Bundle | undefined {
const options: { bundle: Bundle | undefined } = { bundle: undefined };
const visitor = visitors.merge([
- unpackWebpack.visitor(options),
+ unpackWebpack4.visitor(options),
+ unpackWebpack5.visitor(options),
+ unpackWebpackChunk.visitor(options),
unpackBrowserify.visitor(options),
]);
traverse(ast, visitor, undefined, { changes: 0 });
// TODO: applyTransforms(ast, [unpackWebpack, unpackBrowserify]) instead
if (options.bundle) {
options.bundle.applyMappings(mappings);
- options.bundle.applyTransforms();
debug('webcrack:unpack')('Bundle:', options.bundle.type);
}
return options.bundle;
diff --git a/packages/webcrack/src/unpack/module.ts b/packages/webcrack/src/unpack/module.ts
index 4d909449..eacbab8a 100644
--- a/packages/webcrack/src/unpack/module.ts
+++ b/packages/webcrack/src/unpack/module.ts
@@ -1,6 +1,10 @@
import type * as t from '@babel/types';
+import { posix } from 'node:path';
import { generate } from '../ast-utils';
+// eslint-disable-next-line @typescript-eslint/unbound-method
+const { normalize, extname } = posix;
+
export class Module {
id: string;
isEntry: boolean;
@@ -15,7 +19,12 @@ export class Module {
this.id = id;
this.ast = ast;
this.isEntry = isEntry;
- this.path = `./${isEntry ? 'index' : id}.js`;
+ this.path =
+ extname(id) === '' && isEntry ? 'index.js' : this.normalizePath(id);
+ }
+
+ private normalizePath(path: string): string {
+ return normalize(extname(path) === '' ? `${path}.js` : path);
}
/**
diff --git a/packages/webcrack/src/unpack/path.ts b/packages/webcrack/src/unpack/path.ts
index 14cef65e..62c380c5 100644
--- a/packages/webcrack/src/unpack/path.ts
+++ b/packages/webcrack/src/unpack/path.ts
@@ -6,7 +6,7 @@ const { dirname, join, relative } = posix;
export function relativePath(from: string, to: string): string {
if (to.startsWith('node_modules/')) return to.replace('node_modules/', '');
const relativePath = relative(dirname(from), to);
- return relativePath.startsWith('.') ? relativePath : './' + relativePath;
+ return relativePath.startsWith('..') ? relativePath : './' + relativePath;
}
/**
diff --git a/packages/webcrack/src/unpack/test/__snapshots__/unpack.test.ts.snap b/packages/webcrack/src/unpack/test/__snapshots__/unpack.test.ts.snap
index 190449a5..22bb8bd5 100644
--- a/packages/webcrack/src/unpack/test/__snapshots__/unpack.test.ts.snap
+++ b/packages/webcrack/src/unpack/test/__snapshots__/unpack.test.ts.snap
@@ -2,6 +2,7 @@
exports[`path mapping 1`] = `
WebpackBundle {
+ "chunks": [],
"entryId": "2",
"modules": Map {
"1" => WebpackModule {
diff --git a/packages/webcrack/src/unpack/test/commonjs.test.ts b/packages/webcrack/src/unpack/test/commonjs.test.ts
new file mode 100644
index 00000000..d67bc349
--- /dev/null
+++ b/packages/webcrack/src/unpack/test/commonjs.test.ts
@@ -0,0 +1,13 @@
+import { test } from 'vitest';
+import { testWebpackModuleTransform } from '.';
+
+const expectJS = testWebpackModuleTransform();
+
+test('rename module, exports and require', () =>
+ expectJS(`
+ __webpack_module__.exports = __webpack_require__("foo");
+ __webpack_exports__.foo = 1;
+ `).toMatchInlineSnapshot(`
+ module.exports = require("foo");
+ exports.foo = 1;
+ `));
diff --git a/packages/webcrack/src/unpack/test/exports.test.ts b/packages/webcrack/src/unpack/test/exports.test.ts
new file mode 100644
index 00000000..e381c690
--- /dev/null
+++ b/packages/webcrack/src/unpack/test/exports.test.ts
@@ -0,0 +1,325 @@
+import { describe, test } from 'vitest';
+import { testWebpackModuleTransform } from '.';
+
+const expectJS = testWebpackModuleTransform();
+
+describe('webpack 4', () => {
+ test('export named', () =>
+ expectJS(`
+ __webpack_require__.d(__webpack_exports__, "counter", function() { return foo; });
+ var foo = 1;
+ `).toMatchInlineSnapshot(`export var counter = 1;`));
+
+ test('export default expression;', () =>
+ expectJS(`
+ __webpack_exports__.default = 1;
+ `).toMatchInlineSnapshot(`export default 1;`));
+
+ test('export default variable', () =>
+ expectJS(`
+ __webpack_require__.d(__webpack_exports__, "default", function() { return foo; });
+ var foo = 1;
+ `).toMatchInlineSnapshot(`export default 1;`));
+
+ test('export default function', () =>
+ expectJS(`
+ __webpack_require__.d(__webpack_exports__, "default", function() { return foo; });
+ function foo() {}
+ `).toMatchInlineSnapshot(`export default function foo() {}`));
+
+ test('export default class', () =>
+ expectJS(`
+ __webpack_require__.d(__webpack_exports__, "default", function() { return foo; });
+ class foo {}
+ `).toMatchInlineSnapshot(`export default class foo {}`));
+
+ test('re-export named', () =>
+ expectJS(`
+ __webpack_require__.d(__webpack_exports__, "readFile", function() { return lib.readFile; });
+ var lib = __webpack_require__("lib");
+ `).toMatchInlineSnapshot(`
+ export { readFile } from "lib";
+ `));
+
+ test('re-export named with multiple references', () =>
+ expectJS(`
+ __webpack_require__.d(__webpack_exports__, "readFile", function() { return lib.readFile; });
+ var lib = __webpack_require__("lib");
+ console.log(lib);
+ `).toMatchInlineSnapshot(`
+ import * as lib from "lib";
+ export { readFile } from "lib";
+ console.log(lib);
+ `));
+
+ test('re-export named as named', () =>
+ expectJS(`
+ __webpack_require__.d(__webpack_exports__, "foo", function() { return lib.readFile; });
+ var lib = __webpack_require__("lib");
+ `).toMatchInlineSnapshot(`
+ export { readFile as foo } from "lib";
+ `));
+
+ test('re-export named as default', () =>
+ expectJS(`
+ __webpack_require__.d(__webpack_exports__, "default", function() { return lib.readFile; });
+ var lib = __webpack_require__("lib");
+ `).toMatchInlineSnapshot(`
+ export { readFile as default } from "lib";
+ `));
+
+ test('re-export default as named', () =>
+ expectJS(`
+ __webpack_require__.d(__webpack_exports__, "foo", function() { return lib.default; });
+ var lib = __webpack_require__("lib");
+ `).toMatchInlineSnapshot(`
+ export { default as foo } from "lib";
+ `));
+
+ test('re-export default as default', () =>
+ expectJS(`
+ __webpack_require__.d(__webpack_exports__, "default", function() { return lib.default; });
+ var lib = __webpack_require__("lib");
+ `).toMatchInlineSnapshot(`
+ export { default } from "lib";
+ `));
+
+ // webpack just declares all the exports individually
+ // hard to detect this case
+ test.todo('re-export all'); // export * from 'lib';
+
+ test.todo('re-export all from commonjs', () =>
+ expectJS(`
+ var lib = __webpack_require__("lib");
+ var libDef = __webpack_require__.n(lib);
+ for (var importKey in lib) {
+ if (["default"].indexOf(importKey) < 0) {
+ (function (key) {
+ __webpack_require__.d(__webpack_exports__, key, function () {
+ return lib[key];
+ });
+ })(importKey);
+ }
+ }
+ `).toMatchInlineSnapshot(`
+ export * from "./lib";
+ `),
+ );
+
+ test('re-export all as named', () =>
+ expectJS(`
+ __webpack_require__.d(__webpack_exports__, "lib", function() { return lib; });
+ var lib = __webpack_require__("lib");
+ `).toMatchInlineSnapshot(`
+ export * as lib from "lib";
+ `));
+
+ test('multiple re-export all as named', () =>
+ expectJS(`
+ __webpack_require__.d(__webpack_exports__, "lib", function() { return lib; });
+ __webpack_require__.d(__webpack_exports__, "lib2", function() { return lib; });
+ var lib = __webpack_require__("lib");
+ `).toMatchInlineSnapshot(`
+ export * as lib from "lib";
+ export * as lib2 from "lib";
+ `));
+
+ test('re-export all as default', () =>
+ expectJS(`
+ __webpack_require__.d(__webpack_exports__, "default", function() { return lib; });
+ var lib = __webpack_require__("lib");
+ `).toMatchInlineSnapshot(`export * as default from "lib";`));
+
+ test('namespace object', () =>
+ expectJS(`
+ var lib_namespaceObject = {};
+ __webpack_require__.d(lib_namespaceObject, "foo", function() { return foo; });
+ function foo() {}
+ `).toMatchInlineSnapshot(`
+ var lib_namespaceObject = {};
+ //webcrack:concatenated-module-export
+ Object.defineProperty(lib_namespaceObject, "foo", {
+ enumerable: true,
+ get: () => foo
+ });
+ function foo() {}
+ `));
+});
+
+describe('webpack 5', () => {
+ test('export named', () =>
+ expectJS(`
+ __webpack_require__.d(__webpack_exports__, {
+ counter: () => foo
+ });
+ var foo = 1;
+ `).toMatchInlineSnapshot(`
+ export var counter = 1;
+ `));
+
+ test('export same variable with multiple names', () =>
+ expectJS(`
+ __webpack_require__.d(__webpack_exports__, {
+ counter: () => foo,
+ increment: () => foo,
+ });
+ var foo = 1;
+ `).toMatchInlineSnapshot(`
+ export var counter = 1;
+ export { counter as increment };
+ `));
+
+ test('export default expression;', () =>
+ expectJS(`
+ __webpack_require__.d(__webpack_exports__, {
+ default: () => foo
+ });
+ var foo = 1;
+ `).toMatchInlineSnapshot(`
+ export default 1;
+ `));
+
+ test('export default variable', () =>
+ expectJS(`
+ __webpack_require__.d(__webpack_exports__, {
+ default: () => foo
+ });
+ var foo = 1;
+ `).toMatchInlineSnapshot(`
+ export default 1;
+ `));
+
+ test('export default variable with multiple references', () =>
+ expectJS(`
+ __webpack_require__.d(__webpack_exports__, {
+ default: () => foo
+ });
+ var foo = 1;
+ console.log(foo);
+ `).toMatchInlineSnapshot(`
+ var foo = 1;
+ export default foo;
+ console.log(foo);
+ `));
+
+ test('export variable as default and named', () =>
+ expectJS(`
+ __webpack_require__.d(__webpack_exports__, {
+ foo: () => foo,
+ default: () => foo
+ });
+ var foo = 1;
+ `).toMatchInlineSnapshot(`
+ export var foo = 1;
+ export default foo;
+ `));
+
+ test.todo('export object destructuring', () =>
+ expectJS(`
+ __webpack_require__.d(__webpack_exports__, {
+ bar: () => bar,
+ name1: () => name1
+ });
+ const o = {
+ name1: "foo",
+ name2: "bar"
+ };
+ const {
+ name1,
+ name2: bar
+ } = o;
+ `).toMatchInlineSnapshot(`
+ const o = {
+ name1: "foo",
+ name2: "bar"
+ };
+ export const {
+ name1,
+ name2: bar
+ } = o;
+ `),
+ );
+
+ test.todo('export array destructuring', () =>
+ expectJS(`
+ __webpack_require__.d(__webpack_exports__, {
+ bar: () => bar,
+ name1: () => name1
+ });
+ const o = ["foo", "bar"];
+ const [name1, bar] = o;
+ `).toMatchInlineSnapshot(`
+ const o = ["foo", "bar"];
+ export const [name1, bar] = o;
+ `),
+ );
+
+ test.todo('export as invalid identifier string name', () =>
+ expectJS(`
+ __webpack_require__.d(__webpack_exports__, {
+ "...": () => foo
+ });
+ var foo = 1;
+ `).toMatchInlineSnapshot(`
+ var foo = 1;
+ export { foo as "..." };
+ `),
+ );
+
+ test('re-export named merging', () =>
+ expectJS(`
+ __webpack_require__.d(__webpack_exports__, {
+ readFile: () => lib.readFile,
+ writeFile: () => lib.writeFile,
+ });
+ var lib = __webpack_require__("lib");
+ `).toMatchInlineSnapshot(`
+ export { readFile, writeFile } from "lib";
+ `));
+
+ test.todo('re-export all from commonjs', () =>
+ expectJS(`
+ var lib = __webpack_require__("lib");
+ var libDef = __webpack_require__.n(lib);
+ var reExportObject = {};
+ for (const importKey in lib) {
+ if (importKey !== "default") {
+ reExportObject[importKey] = () => lib[importKey];
+ }
+ }
+ __webpack_require__.d(__webpack_exports__, reExportObject);
+ `).toMatchInlineSnapshot(`
+ export * from "./lib";
+ `),
+ );
+
+ test('namespace object', () =>
+ expectJS(`
+ var lib_namespaceObject = {};
+ __webpack_require__.d(lib_namespaceObject, {
+ foo: () => foo,
+ bar: () => bar,
+ });
+ function foo() {}
+ function bar() {}
+ `).toMatchInlineSnapshot(`
+ var lib_namespaceObject = {};
+ //webcrack:concatenated-module-export
+ Object.defineProperty(lib_namespaceObject, "bar", {
+ enumerable: true,
+ get: () => bar
+ });
+ //webcrack:concatenated-module-export
+ Object.defineProperty(lib_namespaceObject, "foo", {
+ enumerable: true,
+ get: () => foo
+ });
+ function foo() {}
+ function bar() {}
+ `));
+});
+
+test('remove __esModule property', () =>
+ expectJS(`
+ Object.defineProperty(exports, "__esModule", { value: true });
+ `).toMatchInlineSnapshot(``));
diff --git a/packages/webcrack/src/unpack/test/global.test.ts b/packages/webcrack/src/unpack/test/global.test.ts
new file mode 100644
index 00000000..25f72839
--- /dev/null
+++ b/packages/webcrack/src/unpack/test/global.test.ts
@@ -0,0 +1,33 @@
+import { parse } from '@babel/parser';
+import traverse from '@babel/traverse';
+import { expect, test } from 'vitest';
+import { testWebpackModuleTransform } from '.';
+import { applyTransform } from '../../ast-utils';
+import global from '../webpack/runtime/global';
+
+const expectJS = testWebpackModuleTransform();
+
+test('replace __webpack_require__.g with global', () =>
+ expectJS(`
+ __webpack_require__.g.setTimeout(() => {});
+ `).toMatchInlineSnapshot(`global.setTimeout(() => {});`));
+
+test('a', () => {
+ const ast = parse(`
+ (function(__webpack_module__, __webpack_exports__, __webpack_require__) {
+ __webpack_require__.g.setTimeout(() => {});
+ });
+ `);
+ traverse(ast, {
+ FunctionExpression(path) {
+ path.stop();
+ const binding = path.scope.bindings.__webpack_require__;
+ applyTransform(path.node, global, binding);
+ expect(path.node).toMatchInlineSnapshot(`
+ function (__webpack_module__, __webpack_exports__, __webpack_require__) {
+ global.setTimeout(() => {});
+ }
+ `);
+ },
+ });
+});
diff --git a/packages/webcrack/src/unpack/test/has-own-property.test.ts b/packages/webcrack/src/unpack/test/has-own-property.test.ts
new file mode 100644
index 00000000..a03a3175
--- /dev/null
+++ b/packages/webcrack/src/unpack/test/has-own-property.test.ts
@@ -0,0 +1,9 @@
+import { test } from 'vitest';
+import { testWebpackModuleTransform } from '.';
+
+const expectJS = testWebpackModuleTransform();
+
+test('replace hasOwnProperty', () =>
+ expectJS(`__webpack_require__.o(obj, prop);`).toMatchInlineSnapshot(`
+ Object.hasOwn(obj, prop);
+ `));
diff --git a/packages/webcrack/src/unpack/test/imports.test.ts b/packages/webcrack/src/unpack/test/imports.test.ts
new file mode 100644
index 00000000..4baef7b5
--- /dev/null
+++ b/packages/webcrack/src/unpack/test/imports.test.ts
@@ -0,0 +1,180 @@
+import { describe, test } from 'vitest';
+import { testWebpackModuleTransform } from '.';
+
+const expectJS = testWebpackModuleTransform();
+
+describe('webpack 4', () => {
+ test('default import', () =>
+ expectJS(`
+ const lib = __webpack_require__("lib");
+ console.log(lib.default);
+ `).toMatchInlineSnapshot(`
+ import _lib_default from "lib";
+ console.log(_lib_default);
+ `));
+
+ test('default import of commonjs module', () =>
+ expectJS(`
+ var lib = __webpack_require__(1);
+ var _tmp = __webpack_require__.n(lib);
+ console.log(_tmp.a);
+ console.log(_tmp());
+ `).toMatchInlineSnapshot(`
+ import _lib_default from "1";
+ console.log(_lib_default);
+ console.log(_lib_default);
+ `));
+
+ test('inlined default import of commonjs module', () =>
+ expectJS(`
+ var lib = __webpack_require__(1);
+ console.log(__webpack_require__.n(lib).a);
+ `).toMatchInlineSnapshot(`
+ import _lib_default from "1";
+ console.log(_lib_default);
+ `));
+
+ test('named import', () =>
+ expectJS(`
+ const lib = __webpack_require__("lib");
+ console.log(lib.foo);
+ `).toMatchInlineSnapshot(`
+ import { foo } from "lib";
+ console.log(foo);
+ `));
+
+ test('multiple named imports', () =>
+ expectJS(`
+ const lib = __webpack_require__("lib");
+ console.log(lib.foo, lib.foo, lib.bar);
+ `).toMatchInlineSnapshot(`
+ import { bar, foo } from "lib";
+ console.log(foo, foo, bar);
+ `));
+
+ test('named import with indirect call', () =>
+ expectJS(`
+ const lib = __webpack_require__("lib");
+ console.log(Object(lib.foo)("bar"));
+ `).toMatchInlineSnapshot(`
+ import { foo } from "lib";
+ console.log(foo("bar"));
+ `));
+
+ test('namespace import', () =>
+ expectJS(`
+ const lib = __webpack_require__("lib");
+ console.log(lib);
+ `).toMatchInlineSnapshot(`
+ import * as lib from "lib";
+ console.log(lib);
+ `));
+
+ test('combined namespace and default import', () =>
+ expectJS(`
+ const lib = __webpack_require__("lib");
+ console.log(lib, lib.default);
+ `).toMatchInlineSnapshot(`
+ import * as lib from "lib";
+ import _lib_default from "lib";
+ console.log(lib, _lib_default);
+ `));
+
+ // TODO: maybe theres no var or it got inlined somewhere
+ test('side effect import', () =>
+ expectJS(`
+ var lib = __webpack_require__("lib");
+ `).toMatchInlineSnapshot(`
+ import "lib";
+ `));
+
+ test.todo('dynamic import', () =>
+ expectJS(`
+ __webpack_require__.e("chunkId").then(__webpack_require__.bind(null, "lib")).then((lib) => {
+ console.log(lib);
+ });
+ `).toMatchInlineSnapshot(`
+ import("lib").then((lib) => {
+ console.log(lib);
+ });
+ `),
+ );
+
+ test('indirect calls', () =>
+ expectJS(`
+ const lib = __webpack_require__("lib");
+ console.log(Object(lib.foo)("bar"));
+ console.log(Object(lib.default)("bar"));
+ `).toMatchInlineSnapshot(`
+ import _lib_default, { foo } from "lib";
+ console.log(foo("bar"));
+ console.log(_lib_default("bar"));
+ `));
+
+ test('sort import specifiers alphabetically', () =>
+ expectJS(`
+ const lib = __webpack_require__("lib");
+ console.log(lib.xyz, lib.abc);
+ `).toMatchInlineSnapshot(`
+ import { abc, xyz } from "lib";
+ console.log(xyz, abc);
+ `));
+
+ test.todo('hoist imports', () =>
+ expectJS(`
+ var _tmp;
+ var lib = __webpack_require__("lib");
+ var lib2 = __webpack_require__("lib2");
+ console.log(lib, lib2);
+ `).toMatchInlineSnapshot(`
+ import * as lib from "lib";
+ import * as lib2 from "lib2";
+ var _tmp;
+ console.log(lib, lib2);
+ `),
+ );
+
+ // TODO: also create an import for the 2nd require call?
+ test('mixed import/require', () =>
+ expectJS(`
+ var lib = __webpack_require__("lib");
+ console.log(lib, __webpack_require__("lib2"));
+ `).toMatchInlineSnapshot(`
+ import * as lib from "lib";
+ console.log(lib, require("lib2"));
+ `));
+});
+
+describe('webpack 5', () => {
+ test('named import with indirect call', () =>
+ expectJS(`
+ const lib = __webpack_require__("lib");
+ console.log((0, lib.foo)("bar"));
+ `).toMatchInlineSnapshot(`
+ import { foo } from "lib";
+ console.log(foo("bar"));
+ `));
+
+ test.todo('namespace import of commonjs module', () =>
+ expectJS(`
+ var _cache;
+ const lib = __webpack_require__("lib");
+ console.log(_cache ||= __webpack_require__.t(lib, 2));
+ `).toMatchInlineSnapshot(`
+ import * as lib from "lib";
+ console.log(lib);
+ `),
+ );
+
+ test.todo('dynamic import', () =>
+ expectJS(`
+ __webpack_require__.e("chunkId").then(__webpack_require__.bind(__webpack_require__, "lib")).then((lib) => {
+ console.log(lib);
+ });
+ `).toMatchInlineSnapshot(`
+ import("lib").then((lib) => {
+ console.log(lib);
+ });
+ `),
+ );
+});
diff --git a/packages/webcrack/src/unpack/test/index.ts b/packages/webcrack/src/unpack/test/index.ts
new file mode 100644
index 00000000..12a5c65d
--- /dev/null
+++ b/packages/webcrack/src/unpack/test/index.ts
@@ -0,0 +1,41 @@
+import type { ParseResult } from '@babel/parser';
+import { parse } from '@babel/parser';
+import traverse from '@babel/traverse';
+import * as t from '@babel/types';
+import type { Assertion } from 'vitest';
+import { expect } from 'vitest';
+import { WebpackModule } from '../webpack/module';
+
+/**
+ * Test all transforms with the input being wrapped with
+ * ```js
+ * (function(__webpack_module__, __webpack_exports__, __webpack_require__) {
+ * // input
+ * });
+ * ```
+ */
+export function testWebpackModuleTransform(): (
+ input: string,
+) => Assertion> {
+ return (input) => {
+ const moduleCode = `
+ (function(__webpack_module__, __webpack_exports__, __webpack_require__) {
+ ${input}
+ });
+ `;
+ const ast = parse(moduleCode, {
+ sourceType: 'unambiguous',
+ allowReturnOutsideFunction: true,
+ });
+ let file: t.File;
+ traverse(ast, {
+ FunctionExpression(path) {
+ path.stop();
+ file = t.file(t.program(path.node.body.body));
+ const module = new WebpackModule('test', path, true);
+ module.applyTransforms((moduleId) => moduleId);
+ },
+ });
+ return expect(file!);
+ };
+}
diff --git a/packages/webcrack/src/unpack/test/module-decorator.test.ts b/packages/webcrack/src/unpack/test/module-decorator.test.ts
new file mode 100644
index 00000000..27f78901
--- /dev/null
+++ b/packages/webcrack/src/unpack/test/module-decorator.test.ts
@@ -0,0 +1,15 @@
+import { test } from 'vitest';
+import { testTransform } from '../../../test';
+import harmonyModuleDecorator from '../webpack/runtime/module-decorator';
+
+const expectJS = testTransform(harmonyModuleDecorator);
+
+test('remove harmony module decorator', () =>
+ expectJS(`module = __webpack_require__.hmd(module);`).toMatchInlineSnapshot(
+ ``,
+ ));
+
+test('remove node module decorator', () =>
+ expectJS(`module = __webpack_require__.nmd(module);`).toMatchInlineSnapshot(
+ ``,
+ ));
diff --git a/packages/webcrack/src/unpack/test/namespace-object.test.ts b/packages/webcrack/src/unpack/test/namespace-object.test.ts
new file mode 100644
index 00000000..19ea73bf
--- /dev/null
+++ b/packages/webcrack/src/unpack/test/namespace-object.test.ts
@@ -0,0 +1,32 @@
+import { expect, test } from 'vitest';
+import { testTransform } from '../../../test';
+import namespaceObject from '../webpack/runtime/namespace-object';
+
+const expectJS = testTransform(namespaceObject);
+
+test('remove the __esModule property', () => {
+ const result = { isESM: false };
+ expectJS(
+ `__webpack_require__.r(__webpack_exports__);`,
+ result,
+ ).toMatchInlineSnapshot(``);
+ expect(result.isESM).toBe(true);
+});
+
+test('remove the __esModule property from a namespace object', () => {
+ const result = { isESM: false };
+ expectJS(
+ `
+ var lib_namespaceObject = {};
+ __webpack_require__.r(lib_namespaceObject);
+ `,
+ result,
+ ).toMatchInlineSnapshot(`
+ //webcrack:concatenated-module-namespace-object
+ var lib_namespaceObject = {};
+ Object.defineProperty(lib_namespaceObject, "__esModule", {
+ value: true
+ });
+ `);
+ expect(result.isESM).toBe(true);
+});
diff --git a/packages/webcrack/src/unpack/test/path.test.ts b/packages/webcrack/src/unpack/test/path.test.ts
index a53f8cc8..55681f62 100644
--- a/packages/webcrack/src/unpack/test/path.test.ts
+++ b/packages/webcrack/src/unpack/test/path.test.ts
@@ -5,6 +5,7 @@ test('relative paths', () => {
expect(relativePath('./a.js', './x/y.js')).toBe('./x/y.js');
expect(relativePath('./x/y.js', './a.js')).toBe('../a.js');
expect(relativePath('./a.js', 'node_modules/lib')).toBe('lib');
+ expect(relativePath('./a.js', '.env')).toBe('./.env');
});
test('resolve browserify paths', () => {
diff --git a/packages/webcrack/src/unpack/test/samples.test.ts b/packages/webcrack/src/unpack/test/samples.test.ts
index 6d60bcea..599d2d95 100644
--- a/packages/webcrack/src/unpack/test/samples.test.ts
+++ b/packages/webcrack/src/unpack/test/samples.test.ts
@@ -1,7 +1,7 @@
import { readFile, readdir } from 'fs/promises';
import { join } from 'node:path';
import { describe, test } from 'vitest';
-import { unpack } from '../index';
+import { webcrack } from '../..';
const SAMPLES_DIR = join(__dirname, 'samples');
@@ -13,7 +13,7 @@ describe('samples', async () => {
fileNames.forEach((fileName) => {
test.concurrent(`unpack ${fileName}`, async ({ expect }) => {
const code = await readFile(join(SAMPLES_DIR, fileName), 'utf8');
- const bundle = unpack(code);
+ const { bundle } = await webcrack(code);
await expect(bundle).toMatchFileSnapshot(
join(SAMPLES_DIR, fileName + '.snap'),
diff --git a/packages/webcrack/src/unpack/test/samples/browserify-2.js.snap b/packages/webcrack/src/unpack/test/samples/browserify-2.js.snap
index 056c7e99..ca29ff55 100644
--- a/packages/webcrack/src/unpack/test/samples/browserify-2.js.snap
+++ b/packages/webcrack/src/unpack/test/samples/browserify-2.js.snap
@@ -9,8 +9,8 @@ BrowserifyBundle {
"path": "lib.js",
},
"2" => BrowserifyModule {
- "ast": const vscode = require('vscode');
-const lib = require('./lib');
+ "ast": const vscode = require("vscode");
+const lib = require("./lib");
console.log(lib);,
"dependencies": {
"1": "./lib",
diff --git a/packages/webcrack/src/unpack/test/samples/browserify-webpack-nested.js.snap b/packages/webcrack/src/unpack/test/samples/browserify-webpack-nested.js.snap
index c2bd0dbd..f1f87ca1 100644
--- a/packages/webcrack/src/unpack/test/samples/browserify-webpack-nested.js.snap
+++ b/packages/webcrack/src/unpack/test/samples/browserify-webpack-nested.js.snap
@@ -3,55 +3,86 @@ BrowserifyBundle {
"modules": Map {
"1" => BrowserifyModule {
"ast": module.exports = 1;
-!function (e) {
+(function (e) {
var a = {};
function n(t) {
- if (a[t]) return a[t].exports;
+ if (a[t]) {
+ return a[t].exports;
+ }
var r = a[t] = {
i: t,
- l: !1,
+ l: false,
exports: {}
};
- return e[t].call(r.exports, r, r.exports, n), r.l = !0, r.exports;
+ e[t].call(r.exports, r, r.exports, n);
+ r.l = true;
+ return r.exports;
}
- n.m = e, n.c = a, n.d = function (e, t, i) {
- n.o(e, t) || Object.defineProperty(e, t, {
- enumerable: !0,
- get: i
+ n.m = e;
+ n.c = a;
+ n.d = function (e, t, i) {
+ if (!n.o(e, t)) {
+ Object.defineProperty(e, t, {
+ enumerable: true,
+ get: i
+ });
+ }
+ };
+ n.r = function (e) {
+ if (typeof Symbol != "undefined" && Symbol.toStringTag) {
+ Object.defineProperty(e, Symbol.toStringTag, {
+ value: "Module"
+ });
+ }
+ Object.defineProperty(e, "__esModule", {
+ value: true
});
- }, n.r = function (e) {
- 'undefined' != typeof Symbol && Symbol.toStringTag && Object.defineProperty(e, Symbol.toStringTag, {
- value: 'Module'
- }), Object.defineProperty(e, '__esModule', {
- value: !0
- });
- }, n.t = function (e, t) {
- if (1 & t && (e = n(e)), 8 & t) return e;
- if (4 & t && 'object' == typeof e && e && e.__esModule) return e;
+ };
+ n.t = function (e, t) {
+ if (t & 1) {
+ e = n(e);
+ }
+ if (t & 8) {
+ return e;
+ }
+ if (t & 4 && typeof e == "object" && e && e.__esModule) {
+ return e;
+ }
var i = Object.create(null);
- if (n.r(i), Object.defineProperty(i, 'default', {
- enumerable: !0,
+ n.r(i);
+ Object.defineProperty(i, "default", {
+ enumerable: true,
value: e
- }), 2 & t && 'string' != typeof e) for (var a in e) n.d(i, a, function (t) {
- return e[t];
- }.bind(null, a));
+ });
+ if (t & 2 && typeof e != "string") {
+ for (var a in e) {
+ n.d(i, a, function (t) {
+ return e[t];
+ }.bind(null, a));
+ }
+ }
return i;
- }, n.n = function (e) {
+ };
+ n.n = function (e) {
var t = e && e.__esModule ? function () {
return e.default;
} : function () {
return e;
};
- return n.d(t, 'a', t), t;
- }, n.o = function (e, t) {
+ n.d(t, "a", t);
+ return t;
+ };
+ n.o = function (e, t) {
return Object.prototype.hasOwnProperty.call(e, t);
- }, n.p = '', n(n.s = 2);
-}([, function (e, t, i) {
+ };
+ n.p = "";
+ n(n.s = 2);
+})([, function (e, t, i) {
const a = i(3);
}, function (e, t, i) {
const a = i(1);
const module = 1;
- e.exports.color = '#FBC02D';
+ e.exports.color = "#FBC02D";
{
const module = 2;
console.log(module);
@@ -67,8 +98,8 @@ BrowserifyBundle {
"path": "lib.js",
},
"2" => BrowserifyModule {
- "ast": const vscode = require('vscode');
-const lib = require('./lib');
+ "ast": const vscode = require("vscode");
+const lib = require("./lib");
console.log(lib);,
"dependencies": {
"1": "./lib",
diff --git a/packages/webcrack/src/unpack/test/samples/browserify.js.snap b/packages/webcrack/src/unpack/test/samples/browserify.js.snap
index 1f11067b..13f6a386 100644
--- a/packages/webcrack/src/unpack/test/samples/browserify.js.snap
+++ b/packages/webcrack/src/unpack/test/samples/browserify.js.snap
@@ -12,10 +12,10 @@ module.exports = add;,
"path": "add.js",
},
"2" => BrowserifyModule {
- "ast": var sum = require('./sum');
+ "ast": var sum = require("./sum");
var numbers = [1, 2, 3];
var result = sum(numbers);
-var outputElement = document.getElementById('output');
+var outputElement = document.getElementById("output");
outputElement.innerHTML = result;,
"dependencies": {
"4": "./sum",
@@ -38,8 +38,8 @@ module.exports = reduce;,
"path": "reduce.js",
},
"4" => BrowserifyModule {
- "ast": var reduce = require('./reduce');
-var add = require('./add');
+ "ast": var reduce = require("./reduce");
+var add = require("./add");
function sum(list) {
return reduce(list, add, 0);
}
diff --git a/packages/webcrack/src/unpack/test/samples/webpack-0.11.x.js.snap b/packages/webcrack/src/unpack/test/samples/webpack-0.11.x.js.snap
index 7c86b612..157d6295 100644
--- a/packages/webcrack/src/unpack/test/samples/webpack-0.11.x.js.snap
+++ b/packages/webcrack/src/unpack/test/samples/webpack-0.11.x.js.snap
@@ -1,28 +1,40 @@
WebpackBundle {
+ "chunks": [],
"entryId": "0",
"modules": Map {
"0" => WebpackModule {
"ast": require("./1.js");
-var template = require("./4.js");
+import * as template from "./4.js";
document.write(template({
- hello: 'World!'
+ hello: "World!"
}));,
+ "dependencies": Set {
+ "1",
+ "4",
+ },
+ "exports": Set {},
+ "externalModule": undefined,
"id": "0",
"isEntry": true,
- "path": "./index.js",
+ "path": "index.js",
},
"1" => WebpackModule {
"ast": // style-loader: Adds some css to the DOM by adding a