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