From 020fb00462f240ea0e8726e64e9a2c320f92d66a Mon Sep 17 00:00:00 2001 From: j4k0xb <55899582+j4k0xb@users.noreply.github.com> Date: Sat, 13 Jan 2024 03:20:26 +0100 Subject: [PATCH 01/81] wip --- packages/webcrack/src/ast-utils/matcher.ts | 21 +- packages/webcrack/src/ast-utils/rename.ts | 2 + packages/webcrack/src/unpack/bundle.ts | 1 + packages/webcrack/src/unpack/index.ts | 10 +- packages/webcrack/src/unpack/module.ts | 11 + .../src/unpack/test/define-exports.test.ts | 186 +++++++++++++++ .../unpack/test/samples/webpack-commonjs.js | 83 +++++++ .../test/samples/webpack-commonjs.js.snap | 30 +++ .../test/samples/webpack-jsonp-chunk.js.snap | 2 +- .../test/samples/webpack-var-injection.js | 67 ------ .../samples/webpack-var-injection.js.snap | 26 --- .../src/unpack/test/samples/webpack5-esm.js | 22 +- .../test/samples/webpack5-object.js.snap | 2 +- .../src/unpack/test/var-injections.test.ts | 16 ++ .../webcrack/src/unpack/webpack/bundle.ts | 61 +---- .../src/unpack/webpack/common-matchers.ts | 148 ++++++++++++ .../src/unpack/webpack/define-exports.ts | 197 ++++++++++++++++ packages/webcrack/src/unpack/webpack/esm.ts | 10 +- .../src/unpack/webpack/getDefaultExport.ts | 10 +- packages/webcrack/src/unpack/webpack/index.ts | 214 ------------------ .../webcrack/src/unpack/webpack/module.ts | 119 +++++++++- .../src/unpack/webpack/unpack-webpack-4.ts | 73 ++++++ .../src/unpack/webpack/unpack-webpack-5.ts | 73 ++++++ .../unpack/webpack/unpack-webpack-chunk.ts | 78 +++++++ .../src/unpack/webpack/var-injections.ts | 47 ++++ .../src/unpack/webpack/varInjection.ts | 53 ----- packages/webcrack/test/setup.ts | 6 + 27 files changed, 1127 insertions(+), 441 deletions(-) create mode 100644 packages/webcrack/src/unpack/test/define-exports.test.ts create mode 100644 packages/webcrack/src/unpack/test/samples/webpack-commonjs.js create mode 100644 packages/webcrack/src/unpack/test/samples/webpack-commonjs.js.snap delete mode 100644 packages/webcrack/src/unpack/test/samples/webpack-var-injection.js delete mode 100644 packages/webcrack/src/unpack/test/samples/webpack-var-injection.js.snap create mode 100644 packages/webcrack/src/unpack/test/var-injections.test.ts create mode 100644 packages/webcrack/src/unpack/webpack/common-matchers.ts create mode 100644 packages/webcrack/src/unpack/webpack/define-exports.ts delete mode 100644 packages/webcrack/src/unpack/webpack/index.ts create mode 100644 packages/webcrack/src/unpack/webpack/unpack-webpack-4.ts create mode 100644 packages/webcrack/src/unpack/webpack/unpack-webpack-5.ts create mode 100644 packages/webcrack/src/unpack/webpack/unpack-webpack-chunk.ts create mode 100644 packages/webcrack/src/unpack/webpack/var-injections.ts delete mode 100644 packages/webcrack/src/unpack/webpack/varInjection.ts 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/unpack/bundle.ts b/packages/webcrack/src/unpack/bundle.ts index 17ec8fe4..2f151faa 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; diff --git a/packages/webcrack/src/unpack/index.ts b/packages/webcrack/src/unpack/index.ts index 81ecdd27..dd6b15c4 100644 --- a/packages/webcrack/src/unpack/index.ts +++ b/packages/webcrack/src/unpack/index.ts @@ -2,10 +2,12 @@ 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'; @@ -27,7 +29,9 @@ export function unpackAST( ): 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 }); diff --git a/packages/webcrack/src/unpack/module.ts b/packages/webcrack/src/unpack/module.ts index 4d909449..c19d83af 100644 --- a/packages/webcrack/src/unpack/module.ts +++ b/packages/webcrack/src/unpack/module.ts @@ -1,6 +1,17 @@ +import type { NodePath } from '@babel/traverse'; import type * as t from '@babel/types'; import { generate } from '../ast-utils'; +export interface Import { + path: string; + id: string; + /** + * E.g. `require('./foo.js')` or `import './foo.js'` + * @internal + */ + nodePath: NodePath; +} + export class Module { id: string; isEntry: boolean; diff --git a/packages/webcrack/src/unpack/test/define-exports.test.ts b/packages/webcrack/src/unpack/test/define-exports.test.ts new file mode 100644 index 00000000..64304e6a --- /dev/null +++ b/packages/webcrack/src/unpack/test/define-exports.test.ts @@ -0,0 +1,186 @@ +import { test } from 'vitest'; +import { testTransform } from '../../../test'; +import defineExports from '../webpack/define-exports'; + +const expectJS = testTransform(defineExports); + +test('export named (webpack 4)', () => + expectJS(` + __webpack_require__.d(exports, "counter", function() { return foo; }); + var foo = 1; + `).toMatchInlineSnapshot(`export var counter = 1;`)); + +test('export named (webpack 5)', () => + expectJS(` + __webpack_require__.d(exports, { + counter: () => foo, + increment: () => increment, + }); + var foo = 1; + function increment() {} + `).toMatchInlineSnapshot(` + export var counter = 1; + export function increment() {} + `)); + +test.todo('export same variable with multiple names', () => + expectJS(` + __webpack_require__.d(exports, { + counter: () => foo, + increment: () => foo, + }); + var foo = 1; + `).toMatchInlineSnapshot(` + export var counter = 1; + export { counter as increment }; + `), +); + +test('export default variable', () => + expectJS(` + __webpack_require__.d(exports, "default", function() { return foo; }); + var foo = 1; + `).toMatchInlineSnapshot(`export default 1;`)); + +test('export default variable with multiple references', () => + expectJS(` + __webpack_require__.d(exports, "default", function() { return foo; }); + var foo = 1; + console.log(foo); + `).toMatchInlineSnapshot(` + var foo = 1; + export { foo as default }; + console.log(foo); + `)); + +test('export default function', () => + expectJS(` + __webpack_require__.d(exports, "default", function() { return foo; }); + function foo() {} + `).toMatchInlineSnapshot(`export default function foo() {}`)); + +test('export default class', () => + expectJS(` + __webpack_require__.d(exports, "default", function() { return foo; }); + class foo {} + `).toMatchInlineSnapshot(`export default class foo {}`)); + +test('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('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(exports, "...", function() { return foo; }); + var foo = 1; + `).toMatchInlineSnapshot(` + var foo = 1; + export { foo as "..." }; + `), +); + +test('re-export named', () => + expectJS(` + __webpack_require__.d(exports, "readFile", function() { return fs.readFile; }); + var fs = __webpack_require__("fs"); + `).toMatchInlineSnapshot(`export { readFile } from "fs";`)); + +test('re-export named, keep variable', () => + expectJS(` + __webpack_require__.d(exports, "readFile", function() { return fs.readFile; }); + var fs = __webpack_require__("fs"); + fs.writeFile(); + `).toMatchInlineSnapshot(` + export { readFile } from "fs"; + var fs = __webpack_require__("fs"); + fs.writeFile(); + `)); + +test('re-export multiple named', () => + expectJS(` + __webpack_require__.d(exports, { + readFile: () => fs.readFile, + writeFile: () => fs.writeFile, + }); + var fs = __webpack_require__("fs"); + `).toMatchInlineSnapshot(` + export { readFile } from "fs"; + export { writeFile } from "fs"; + var fs = __webpack_require__("fs"); + `)); + +test('re-export named as named', () => + expectJS(` + __webpack_require__.d(exports, "foo", function() { return fs.readFile; }); + var fs = __webpack_require__("fs"); + `).toMatchInlineSnapshot(`export { readFile as foo } from "fs";`)); + +test('re-export named as default', () => + expectJS(` + __webpack_require__.d(exports, "default", function() { return fs.readFile; }); + var fs = __webpack_require__("fs"); + `).toMatchInlineSnapshot(`export { readFile as default } from "fs";`)); + +test('re-export default as named', () => + expectJS(` + __webpack_require__.d(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(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 'fs'; + +test.todo('re-export all as named', () => + expectJS(` + __webpack_require__.d(exports, "fs", function() { return fs; }); + var fs = __webpack_require__("fs"); + `).toMatchInlineSnapshot(`export * as fs from "fs";`), +); + +test.todo('re-export all as default', () => + expectJS(` + __webpack_require__.d(exports, "default", function() { return fs; }); + var fs = __webpack_require__("fs"); + `).toMatchInlineSnapshot(`export * as default from "fs";`), +); diff --git a/packages/webcrack/src/unpack/test/samples/webpack-commonjs.js b/packages/webcrack/src/unpack/test/samples/webpack-commonjs.js new file mode 100644 index 00000000..33164b5b --- /dev/null +++ b/packages/webcrack/src/unpack/test/samples/webpack-commonjs.js @@ -0,0 +1,83 @@ +// https://github.com/webpack/webpack/tree/e518d0da94639476354f028cdf032de4be6813a0/examples/commonjs +/******/ (() => { // webpackBootstrap +/******/ var __webpack_modules__ = ([ +/* 0 */, +/* 1 */ +/*!**********************!*\ + !*** ./increment.js ***! + \**********************/ +/*! default exports */ +/*! export increment [provided] [no usage info] [missing usage info prevents renaming] */ +/*! other exports [not provided] [no usage info] */ +/*! runtime requirements: __webpack_require__, __webpack_exports__ */ +/***/ ((__unused_webpack_module, exports, __webpack_require__) => { + + const add = (__webpack_require__(/*! ./math */ 2).add); + exports.increment = function(val) { + return add(val, 1); + }; + + + /***/ }), + /* 2 */ + /*!*****************!*\ + !*** ./math.js ***! + \*****************/ + /*! default exports */ + /*! export add [provided] [no usage info] [missing usage info prevents renaming] */ + /*! other exports [not provided] [no usage info] */ + /*! runtime requirements: __webpack_exports__ */ + /***/ ((__unused_webpack_module, exports) => { + + exports.add = function() { + var sum = 0, i = 0, args = arguments, l = args.length; + while (i < l) { + sum += args[i++]; + } + return sum; + }; + + /***/ }) + /******/ ]); + /************************************************************************/ + /******/ // The module cache + /******/ var __webpack_module_cache__ = {}; + /******/ + /******/ // The require function + /******/ function __webpack_require__(moduleId) { + /******/ // Check if module is in cache + /******/ var cachedModule = __webpack_module_cache__[moduleId]; + /******/ if (cachedModule !== undefined) { + /******/ return cachedModule.exports; + /******/ } + /******/ // Create a new module (and put it into the cache) + /******/ var module = __webpack_module_cache__[moduleId] = { + /******/ // no module.id needed + /******/ // no module.loaded needed + /******/ exports: {} + /******/ }; + /******/ + /******/ // Execute the module function + /******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__); + /******/ + /******/ // Return the exports of the module + /******/ return module.exports; + /******/ } + /******/ + /************************************************************************/ + var __webpack_exports__ = {}; + // This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk. + (() => { + /*!********************!*\ + !*** ./example.js ***! + \********************/ + /*! unknown exports (runtime-defined) */ + /*! runtime requirements: __webpack_require__ */ + const inc = (__webpack_require__(/*! ./increment */ 1).increment); + const a = 1; + inc(a); // 2 + + })(); + + /******/ })() + ; \ No newline at end of file diff --git a/packages/webcrack/src/unpack/test/samples/webpack-commonjs.js.snap b/packages/webcrack/src/unpack/test/samples/webpack-commonjs.js.snap new file mode 100644 index 00000000..cd03c8d3 --- /dev/null +++ b/packages/webcrack/src/unpack/test/samples/webpack-commonjs.js.snap @@ -0,0 +1,30 @@ +WebpackBundle { + "entryId": "", + "modules": Map { + "1" => WebpackModule { + "ast": const add = require( /*! ./math */"./2.js").add; +exports.increment = function (val) { + return add(val, 1); +};, + "id": "1", + "isEntry": false, + "path": "./1.js", + }, + "2" => WebpackModule { + "ast": exports.add = function () { + var sum = 0, + i = 0, + args = arguments, + l = args.length; + while (i < l) { + sum += args[i++]; + } + return sum; +};, + "id": "2", + "isEntry": false, + "path": "./2.js", + }, + }, + "type": "webpack", +} \ No newline at end of file diff --git a/packages/webcrack/src/unpack/test/samples/webpack-jsonp-chunk.js.snap b/packages/webcrack/src/unpack/test/samples/webpack-jsonp-chunk.js.snap index 6387e659..bfd84e0e 100644 --- a/packages/webcrack/src/unpack/test/samples/webpack-jsonp-chunk.js.snap +++ b/packages/webcrack/src/unpack/test/samples/webpack-jsonp-chunk.js.snap @@ -3,7 +3,7 @@ WebpackBundle { "modules": Map { "2wwy" => WebpackModule { "ast": const a = require("./E8gZ.js"); -const b = require( /*webcrack:missing*/"./w6GO.js");, +const b = require( /*webcrack:missing*/"w6GO");, "id": "2wwy", "isEntry": false, "path": "./2wwy.js", diff --git a/packages/webcrack/src/unpack/test/samples/webpack-var-injection.js b/packages/webcrack/src/unpack/test/samples/webpack-var-injection.js deleted file mode 100644 index a6818592..00000000 --- a/packages/webcrack/src/unpack/test/samples/webpack-var-injection.js +++ /dev/null @@ -1,67 +0,0 @@ -!(function (e) { - var a = {}; - function n(t) { - if (a[t]) return a[t].exports; - var r = (a[t] = { i: t, l: !1, exports: {} }); - return e[t].call(r.exports, r, r.exports, n), (r.l = !0), 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.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; - var i = Object.create(null); - if ( - (n.r(i), - Object.defineProperty(i, 'default', { enumerable: !0, value: e }), - 2 & t && 'string' != typeof e) - ) - for (var a in e) - n.d( - i, - a, - function (t) { - return e[t]; - }.bind(null, a) - ); - return i; - }), - (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) { - return Object.prototype.hasOwnProperty.call(e, t); - }), - (n.p = ''), - n((n.s = 0)); -})([ - function (e, t, i) { - i.r(t); - (function (m, n) { - console.log(m, n); - }.call(this, i(1), i(2))); - }, - function (e, t, i) { - e.exports = 1; - }, - function (e, t, i) { - e.exports = 2; - }, -]); diff --git a/packages/webcrack/src/unpack/test/samples/webpack-var-injection.js.snap b/packages/webcrack/src/unpack/test/samples/webpack-var-injection.js.snap deleted file mode 100644 index 2c38e04a..00000000 --- a/packages/webcrack/src/unpack/test/samples/webpack-var-injection.js.snap +++ /dev/null @@ -1,26 +0,0 @@ -WebpackBundle { - "entryId": "0", - "modules": Map { - "0" => WebpackModule { - "ast": import * as m from "./1.js"; -import * as n from "./2.js"; -console.log(m, n);, - "id": "0", - "isEntry": true, - "path": "./index.js", - }, - "1" => WebpackModule { - "ast": module.exports = 1;, - "id": "1", - "isEntry": false, - "path": "./1.js", - }, - "2" => WebpackModule { - "ast": module.exports = 2;, - "id": "2", - "isEntry": false, - "path": "./2.js", - }, - }, - "type": "webpack", -} \ No newline at end of file diff --git a/packages/webcrack/src/unpack/test/samples/webpack5-esm.js b/packages/webcrack/src/unpack/test/samples/webpack5-esm.js index efbff966..5c664427 100644 --- a/packages/webcrack/src/unpack/test/samples/webpack5-esm.js +++ b/packages/webcrack/src/unpack/test/samples/webpack5-esm.js @@ -1,36 +1,36 @@ (() => { var modules = { - 2: (module, exports, require) => { + 2: (module, exports, __webpack_require__) => { 'use strict'; - const lib = require(3); + const lib = __webpack_require__(3); console.log(lib); }, - 3: (module, exports) => { + 3: (module, exports, __webpack_require__) => { 'use strict'; - require.r(exports); + __webpack_require__.r(exports); - const a = require(4); + const a = __webpack_require__(4); const obj = { version: '2.0.0', }; - require.d(exports, { + __webpack_require__.d(exports, { default: () => a.foo, version: () => obj.version, }); }, - 4: (module, exports) => { + 4: (module, exports, __webpack_require__) => { 'use strict'; - require.r(exports); + __webpack_require__.r(exports); const b = 2; - require.d(exports, { + __webpack_require__.d(exports, { foo: () => b, obj: () => x, }); var x = {}; - require.r(x); - require.d(x, { + __webpack_require__.r(x); + __webpack_require__.d(x, { Console: () => bar, }); function bar() {} diff --git a/packages/webcrack/src/unpack/test/samples/webpack5-object.js.snap b/packages/webcrack/src/unpack/test/samples/webpack5-object.js.snap index bae8cf96..acc27fd1 100644 --- a/packages/webcrack/src/unpack/test/samples/webpack5-object.js.snap +++ b/packages/webcrack/src/unpack/test/samples/webpack5-object.js.snap @@ -4,7 +4,7 @@ WebpackBundle { "2" => WebpackModule { "ast": const lib = require("./3.js"); console.log(lib); -const _0x8da276 = require(require.ab + "build/Release/spdlog.node");, +const _0x8da276 = __webpack_require__(__webpack_require__.ab + "build/Release/spdlog.node");, "id": "2", "isEntry": true, "path": "./index.js", diff --git a/packages/webcrack/src/unpack/test/var-injections.test.ts b/packages/webcrack/src/unpack/test/var-injections.test.ts new file mode 100644 index 00000000..6f0a6503 --- /dev/null +++ b/packages/webcrack/src/unpack/test/var-injections.test.ts @@ -0,0 +1,16 @@ +import { test } from 'vitest'; +import { testTransform } from '../../../test'; +import varInjections from '../webpack/var-injections'; + +const expectJS = testTransform(varInjections); + +test('replace', () => + expectJS(` + (function (m, n) { + console.log(m, n); + }.call(this, __webpack_require__(1), __webpack_require__(2))); + `).toMatchInlineSnapshot(` + var m = __webpack_require__(1); + var n = __webpack_require__(2); + console.log(m, n); + `)); diff --git a/packages/webcrack/src/unpack/webpack/bundle.ts b/packages/webcrack/src/unpack/webpack/bundle.ts index 8f73db61..1461d8f3 100644 --- a/packages/webcrack/src/unpack/webpack/bundle.ts +++ b/packages/webcrack/src/unpack/webpack/bundle.ts @@ -1,15 +1,12 @@ -import type { NodePath } from '@babel/traverse'; -import traverse from '@babel/traverse'; -import * as t from '@babel/types'; -import * as m from '@codemod/matchers'; import { Bundle } from '../bundle'; import { relativePath } from '../path'; import { convertESM } from './esm'; import { convertDefaultRequire } from './getDefaultExport'; import type { WebpackModule } from './module'; -import { inlineVarInjections } from './varInjection'; export class WebpackBundle extends Bundle { + declare modules: Map; + constructor(entryId: string, modules: Map) { super('webpack', entryId, modules); } @@ -18,55 +15,15 @@ export class WebpackBundle extends Bundle { * Undoes some of the transformations that Webpack injected into the modules. */ applyTransforms(): void { - this.modules.forEach(inlineVarInjections); - this.modules.forEach(convertESM); - convertDefaultRequire(this); - this.replaceRequirePaths(); - } - - /** - * Replaces `require(id)` calls with `require("./relative/path.js")` calls. - */ - private replaceRequirePaths() { - const requireId = m.capture(m.or(m.numericLiteral(), m.stringLiteral())); - const requireMatcher = m.or( - m.callExpression(m.identifier('require'), [requireId]), - ); - const importId = m.capture(m.stringLiteral()); - const importMatcher = m.importDeclaration(m.anything(), importId); - this.modules.forEach((module) => { - traverse(module.ast, { - 'CallExpression|ImportDeclaration': (path) => { - let moduleId: string; - let arg: NodePath; - - if (requireMatcher.match(path.node)) { - moduleId = requireId.current!.value.toString(); - [arg] = path.get('arguments') as NodePath[]; - } else if (importMatcher.match(path.node)) { - moduleId = importId.current!.value; - arg = path.get('source') as NodePath; - } else { - return; - } - - const requiredModule = this.modules.get(moduleId); - arg.replaceWith( - t.stringLiteral( - relativePath( - module.path, - requiredModule?.path ?? `./${moduleId}.js`, - ), - ), - ); - // For example if its stored in another chunk - if (!requiredModule) { - arg.addComment('leading', 'webcrack:missing'); - } - }, - noScope: true, + module.replaceRequireCalls((id) => { + const requiredModule = this.modules.get(id); + return requiredModule + ? { path: relativePath(module.path, requiredModule.path) } + : { path: id, external: true }; }); + convertESM(module); }); + convertDefaultRequire(this); } } diff --git a/packages/webcrack/src/unpack/webpack/common-matchers.ts b/packages/webcrack/src/unpack/webpack/common-matchers.ts new file mode 100644 index 00000000..01164ca4 --- /dev/null +++ b/packages/webcrack/src/unpack/webpack/common-matchers.ts @@ -0,0 +1,148 @@ +import { Binding, NodePath } from '@babel/traverse'; +import * as t from '@babel/types'; +import * as m from '@codemod/matchers'; +import { + anyFunctionExpression, + constMemberExpression, + getPropName, +} from '../../ast-utils'; + +export type FunctionPath = NodePath< + | t.FunctionExpression + | (t.ArrowFunctionExpression & { body: t.BlockStatement }) +>; + +/** + * @returns + * - `webpackRequire`: A Matcher for `function __webpack_require__(moduleId) { ... }` + * - `containerId`: A matcher for e.g. `__webpack_modules__` that has to be captured before `webpackRequire` is matched + */ +export function webpackRequireFunctionMatcher() { + // Example: __webpack_modules__ + const containerId = m.capture(m.identifier()); + const webpackRequire = m.capture( + m.functionDeclaration( + m.identifier(), // __webpack_require__ + [m.identifier()], // moduleId + m.containerOf( + m.callExpression( + m.or( + // Example (webpack 5): __webpack_modules__[moduleId](module, module.exports, __webpack_require__); + m.memberExpression( + m.fromCapture(containerId), + m.identifier(), + true, + ), + // Example (webpack 4): __webpack_modules__[moduleId].call(module.exports, module, module.exports, __webpack_require__); + // Example (webpack 0.11.x): __webpack_modules__[moduleId].call(null, module, module.exports, __webpack_require__); + constMemberExpression( + m.memberExpression( + m.fromCapture(containerId), + m.identifier(), + true, + ), + 'call', + ), + ), + ), + ), + ), + ); + + return { webpackRequire, containerId }; +} + +/** + * Matches + * - `[,,function (module, exports, require) {...}, ...]` where the index is the module ID + * - or `{0: function (module, exports, require) {...}, ...}` where the key is the module ID + */ +export function modulesContainerMatcher(): m.CapturedMatcher< + t.ArrayExpression | t.ObjectExpression +> { + return m.capture( + m.or( + m.arrayExpression(m.arrayOf(m.or(anyFunctionExpression(), null))), + m.objectExpression( + m.arrayOf( + m.or( + m.objectProperty( + m.or(m.numericLiteral(), m.stringLiteral(), m.identifier()), + anyFunctionExpression(), + ), + // Example (__webpack_public_path__): { c: "" } + m.objectProperty(m.identifier('c'), m.stringLiteral()), + ), + ), + ), + ), + ); +} + +/** + * @param container A node path to the modules container + * @returns A map of module IDs to their function node paths + */ +export function getModuleFunctions( + container: NodePath, +): Map { + const functions = new Map(); + + if (t.isArrayExpression(container.node)) { + container.node.elements.forEach((element, index) => { + if (element !== null) { + functions.set( + index.toString(), + container.get(`elements.${index}`) as FunctionPath, + ); + } + }); + } else { + (container.node.properties as t.ObjectProperty[]).forEach( + (property, index) => { + const key = getPropName(property.key)!; + if (anyFunctionExpression().match(property.value)) { + functions.set( + key, + container.get(`properties.${index}.value`) as FunctionPath, + ); + } + }, + ); + } + + return functions; +} + +/** + * Matches `__webpack_require__.s = ` + */ +export function findAssignedEntryId(webpackRequireBinding: Binding) { + const entryId = m.capture(m.or(m.numericLiteral(), m.stringLiteral())); + const assignment = m.assignmentExpression( + '=', + constMemberExpression(webpackRequireBinding.identifier.name, 's'), + entryId, + ); + for (const reference of webpackRequireBinding.referencePaths) { + if (assignment.match(reference.parentPath?.parent)) { + return String(entryId.current!.value); + } + } +} + +/** + * Matches `__webpack_require__()` + */ +export function findRequiredEntryId(webpackRequireBinding: Binding) { + const entryId = m.capture(m.or(m.numericLiteral(), m.stringLiteral())); + const call = m.callExpression( + m.identifier(webpackRequireBinding.identifier.name), + [entryId], + ); + for (const reference of webpackRequireBinding.referencePaths) { + if (call.match(reference.parent)) { + return String(entryId.current!.value); + } + } +} diff --git a/packages/webcrack/src/unpack/webpack/define-exports.ts b/packages/webcrack/src/unpack/webpack/define-exports.ts new file mode 100644 index 00000000..ad6b494b --- /dev/null +++ b/packages/webcrack/src/unpack/webpack/define-exports.ts @@ -0,0 +1,197 @@ +import { statement } from '@babel/template'; +import { Binding, NodePath } from '@babel/traverse'; +import * as t from '@babel/types'; +import * as m from '@codemod/matchers'; +import { + Transform, + constMemberExpression, + findPath, + renameFast, +} from '../../ast-utils'; + +// TODO: hoist re-exports to the top of the file (but retain order relative to imports) +// TODO: merge re-exports + +/** + * webpack/runtime/define property getters + * ```js + * __webpack_require__.d = (exports, definition) => { + * for (var key in definition) { + * if (__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) { + * Object.defineProperty(exports, key, { enumerable: true, get: definition[key] }); + * } + * } + * }; + * ``` + */ +export default { + name: 'define-exports', + tags: ['unsafe'], + scope: true, + visitor() { + const exportName = m.capture(m.anyString()); + const returnValue = m.capture(m.anyExpression()); + const getter = m.or( + m.functionExpression( + undefined, + [], + m.blockStatement([m.returnStatement(returnValue)]), + ), + m.arrowFunctionExpression([], returnValue), + ); + // Example (webpack v4): __webpack_require__.d(exports, 'counter', function () { return local }); + const singleExport = m.expressionStatement( + m.callExpression(constMemberExpression('__webpack_require__', 'd'), [ + m.identifier(), + m.stringLiteral(exportName), + getter, + ]), + ); + + const objectProperty = m.objectProperty(m.identifier(exportName), getter); + const properties = m.capture(m.arrayOf(objectProperty)); + // Example (webpack v5): __webpack_require__.d(exports, { a: () => b, c: () => d }); + const multiExport = m.expressionStatement( + m.callExpression(constMemberExpression('__webpack_require__', 'd'), [ + m.identifier(), + m.objectExpression(properties), + ]), + ); + + return { + ExpressionStatement(path) { + if (!path.parentPath.isProgram()) return; + + if (singleExport.match(path.node)) { + addExport(path, exportName.current!, returnValue.current!); + path.remove(); + this.changes++; + } else if (multiExport.match(path.node)) { + for (const property of properties.current!) { + objectProperty.match(property); // To easily get the captures per property + addExport(path, exportName.current!, returnValue.current!); + } + path.remove(); + this.changes++; + } + }, + }; + }, +} satisfies Transform; + +function addExport(path: NodePath, exportName: string, value: t.Expression) { + const object = m.capture(m.identifier()); + const property = m.capture(m.identifier()); + const memberValue = m.memberExpression(object, property); + + if (t.isIdentifier(value)) { + const binding = path.scope.getBinding(value.name); + if (!binding) return; + + if (exportName === 'default') { + exportDefault(binding); + } else { + exportNamed(binding, exportName); + } + } else if (memberValue.match(value)) { + const binding = path.scope.getBinding(object.current!.name); + if (!binding) return; + + const localName = property.current!.name; + reExportNamed(binding, exportName, localName); + } +} + +/** + * ```diff + * - __webpack_require__.d(exports, 'counter', () => local); + * - let local = 1; + * + export let counter = 1; + * ``` + */ +function exportNamed(binding: Binding, exportName: string): void { + const declaration = findPath( + binding.path, + m.or( + m.variableDeclaration(), + m.classDeclaration(), + m.functionDeclaration(), + ), + ); + if (!declaration) return; + + renameFast(binding, exportName); + declaration.replaceWith(t.exportNamedDeclaration(declaration.node)); +} + +/** + * ```diff + * - __webpack_require__.d(exports, 'default', () => local); + * - let local = 1; + * + export default 1; + * ``` + */ +function exportDefault(binding: Binding) { + const declaration = findPath( + binding.path, + m.or( + m.variableDeclaration(), + m.functionDeclaration(), + m.classDeclaration(), + ), + ); + if (!declaration) return; + + if (binding.references > 1) { + declaration.insertAfter( + statement`export { ${binding.identifier} as default };`(), + ); + } else { + declaration.replaceWith( + t.exportDefaultDeclaration( + t.isVariableDeclaration(declaration.node) + ? declaration.node.declarations[0].init! + : declaration.node, + ), + ); + } +} + +/** + * ```diff + * - __webpack_require__.d(exports, 'readFile', () => fs.readFile); + * - var fs = __webpack_require__('fs'); + * + export { readFile } from 'fs'; + * ``` + * alias: + * ```diff + * - __webpack_require__.d(exports, 'foo', () => fs.readFile); + * - var fs = __webpack_require__('fs'); + * + export { readFile as foo } from 'fs'; + * ``` + */ +function reExportNamed( + binding: Binding, + exportName: string, + localName: string, +) { + const moduleId = m.capture(m.or(m.numericLiteral(), m.stringLiteral())); + const variableMatcher = m.variableDeclaration(undefined, [ + m.variableDeclarator( + m.identifier(binding.identifier.name), + m.callExpression(m.identifier('__webpack_require__'), [moduleId]), + ), + ]); + + if (variableMatcher.match(binding.path.parent)) { + const modulePath = String(moduleId.current!.value); + // FIXME: does this mess up the imports references in webpack/module.ts? + binding.path.parentPath!.insertBefore( + statement`export { ${localName} as ${exportName} } from "${modulePath}";`(), + ); + // FIXME: only remove at the end to account for multiple re-exports/references + if (binding.references === 1) { + binding.path.parentPath!.remove(); + } + } +} diff --git a/packages/webcrack/src/unpack/webpack/esm.ts b/packages/webcrack/src/unpack/webpack/esm.ts index da20b19c..9d782a9b 100644 --- a/packages/webcrack/src/unpack/webpack/esm.ts +++ b/packages/webcrack/src/unpack/webpack/esm.ts @@ -67,12 +67,12 @@ export function convertESM(module: WebpackModule): void { // E.g. const lib = require("./lib.js"); const requireVariable = m.capture(m.identifier()); - const requiredModuleId = m.capture(m.anyNumber()); + const requiredModulePath = m.capture(m.anyString()); const requireMatcher = m.variableDeclaration(undefined, [ m.variableDeclarator( requireVariable, m.callExpression(m.identifier('require'), [ - m.numericLiteral(requiredModuleId), + m.stringLiteral(requiredModulePath), ]), ), ]); @@ -92,16 +92,16 @@ export function convertESM(module: WebpackModule): void { if (path.parentPath?.parentPath) return path.skip(); if (defineEsModuleMatcher.match(path.node)) { - module.ast.program.sourceType = 'module'; + module.ast.program.sourceType = 'esm'; path.remove(); } else if ( - module.ast.program.sourceType === 'module' && + module.ast.program.sourceType === 'esm' && requireMatcher.match(path.node) ) { path.replaceWith( buildNamespaceImport({ NAME: requireVariable.current, - PATH: String(requiredModuleId.current), + PATH: String(requiredModulePath.current), }), ); } else if (defineExportsMatcher.match(path.node)) { diff --git a/packages/webcrack/src/unpack/webpack/getDefaultExport.ts b/packages/webcrack/src/unpack/webpack/getDefaultExport.ts index 701c0704..a2d9f973 100644 --- a/packages/webcrack/src/unpack/webpack/getDefaultExport.ts +++ b/packages/webcrack/src/unpack/webpack/getDefaultExport.ts @@ -1,5 +1,5 @@ import { expression } from '@babel/template'; -import type { NodePath } from '@babel/traverse'; +import type { Scope } from '@babel/traverse'; import traverse from '@babel/traverse'; import * as m from '@codemod/matchers'; import { constMemberExpression } from '../../ast-utils'; @@ -33,9 +33,9 @@ import type { WebpackBundle } from './bundle'; * ``` */ export function convertDefaultRequire(bundle: WebpackBundle): void { - function getRequiredModule(path: NodePath) { + function getRequiredModule(scope: Scope) { // The variable that's passed to require.n - const binding = path.scope.getBinding(moduleArg.current!.name); + const binding = scope.getBinding(moduleArg.current!.name); const declarator = binding?.path.node; if (declaratorMatcher.match(declarator)) { return bundle.modules.get(requiredModuleId.current!.value.toString()); @@ -73,7 +73,7 @@ export function convertDefaultRequire(bundle: WebpackBundle): void { 'CallExpression|MemberExpression'(path) { if (defaultRequireMatcherAlternative.match(path.node)) { // Replace require.n(m).a or require.n(m)() with m or m.default - const requiredModule = getRequiredModule(path); + const requiredModule = getRequiredModule(path.scope); if (requiredModule?.ast.program.sourceType === 'module') { path.replaceWith( buildDefaultAccess({ OBJECT: moduleArg.current! }), @@ -86,7 +86,7 @@ export function convertDefaultRequire(bundle: WebpackBundle): void { VariableDeclarator(path) { if (defaultRequireMatcher.match(path.node)) { // Replace require.n(m); with m or m.default - const requiredModule = getRequiredModule(path); + const requiredModule = getRequiredModule(path.scope); const init = path.get('init'); if (requiredModule?.ast.program.sourceType === 'module') { init.replaceWith( diff --git a/packages/webcrack/src/unpack/webpack/index.ts b/packages/webcrack/src/unpack/webpack/index.ts deleted file mode 100644 index d1deeb14..00000000 --- a/packages/webcrack/src/unpack/webpack/index.ts +++ /dev/null @@ -1,214 +0,0 @@ -import type { NodePath } from '@babel/traverse'; -import * as t from '@babel/types'; -import * as m from '@codemod/matchers'; -import type { Transform } from '../../ast-utils'; -import { - constKey, - constMemberExpression, - getPropName, - renameParameters, -} from '../../ast-utils'; -import type { Bundle } from '../bundle'; -import { WebpackBundle } from './bundle'; -import { WebpackModule } from './module'; - -export const unpackWebpack = { - name: 'unpack-webpack', - tags: ['unsafe'], - scope: true, - visitor(options) { - const modules = new Map(); - - const entryIdMatcher = m.capture(m.numericLiteral()); - const moduleFunctionsMatcher = m.capture( - m.or( - // E.g. [,,function (e, t, i) {...}, ...], index is the module ID - m.arrayExpression( - m.arrayOf( - m.or(m.functionExpression(), m.arrowFunctionExpression(), null), - ), - ), - // E.g. {0: function (e, t, i) {...}, ...}, key is the module ID - m.objectExpression( - m.arrayOf( - m.or( - m.objectProperty( - m.or(m.numericLiteral(), m.stringLiteral(), m.identifier()), - m.or(m.functionExpression(), m.arrowFunctionExpression()), - ), - // __webpack_public_path__ (c: "") - m.objectProperty(constKey('c'), m.stringLiteral()), - ), - ), - ), - ), - ); - - const webpack4Matcher = m.callExpression( - m.functionExpression( - undefined, - undefined, - m.blockStatement( - m.anyList( - m.zeroOrMore(), - m.functionDeclaration(), - m.zeroOrMore(), - m.containerOf( - m.or( - // E.g. __webpack_require__.s = 2 - m.assignmentExpression( - '=', - constMemberExpression(m.identifier(), 's'), - entryIdMatcher, - ), - // E.g. return require(0); - m.callExpression(m.identifier(), [entryIdMatcher]), - ), - ), - ), - ), - ), - [moduleFunctionsMatcher], - ); - - const webpack5Matcher = m.callExpression( - m.arrowFunctionExpression( - undefined, - m.blockStatement( - m.anyList( - m.zeroOrMore(), - m.variableDeclaration(undefined, [ - m.variableDeclarator(undefined, moduleFunctionsMatcher), - ]), - // var installedModules = {}; - m.variableDeclaration(), - m.zeroOrMore(), - m.or( - // __webpack_require__.s = 2 - m.containerOf( - m.assignmentExpression( - '=', - constMemberExpression(m.identifier(), 's'), - entryIdMatcher, - ), - ), - m.expressionStatement( - m.assignmentExpression( - '=', - constMemberExpression( - m.identifier(), - m.or('e', 'd', 'j', 'm', 'r'), - ), - ), - ), - ), - m.zeroOrMore(), - // module.exports = entryModule - m.expressionStatement( - m.assignmentExpression( - '=', - constMemberExpression(m.identifier(), 'exports'), - m.identifier(), - ), - ), - ), - ), - ), - ); - - // Examples: self.webpackChunk_N_E, window.webpackJsonp, this.webpackJsonp - const jsonpGlobal = m.capture( - constMemberExpression( - m.or( - m.identifier(m.or('self', 'window', 'globalThis')), - m.thisExpression(), - ), - m.matcher((property) => property.startsWith('webpack')), - ), - ); - // (window.webpackJsonp = window.webpackJsonp || []).push([[0], {...}]) - const jsonpMatcher = m.callExpression( - constMemberExpression( - m.assignmentExpression( - '=', - jsonpGlobal, - m.logicalExpression( - '||', - m.fromCapture(jsonpGlobal), - m.arrayExpression([]), - ), - ), - 'push', - ), - [ - m.arrayExpression( - m.anyList( - m.arrayExpression( - m.arrayOf(m.or(m.numericLiteral(), m.stringLiteral())), - ), // chunkId - moduleFunctionsMatcher, - m.slice({ max: 1 }), // optional entry point like [["57iH",19,24,25]] or a function - ), - ), - ], - ); - - return { - CallExpression(path) { - if ( - !webpack4Matcher.match(path.node) && - !webpack5Matcher.match(path.node) && - !jsonpMatcher.match(path.node) - ) - return; - path.stop(); - - const modulesPath = path.get( - moduleFunctionsMatcher.currentKeys!.join('.'), - ) as NodePath; - - const moduleWrappers = modulesPath.isArrayExpression() - ? (modulesPath.get('elements') as NodePath[]) - : (modulesPath.get('properties') as NodePath[]); - - moduleWrappers.forEach((moduleWrapper, index) => { - let moduleId = index.toString(); - if (t.isObjectProperty(moduleWrapper.node)) { - moduleId = getPropName(moduleWrapper.node.key)!; - moduleWrapper = moduleWrapper.get('value') as NodePath; - } - - if ( - moduleWrapper.isFunction() && - moduleWrapper.node.body.type === 'BlockStatement' - ) { - renameParameters(moduleWrapper, ['module', 'exports', 'require']); - const file = t.file(t.program(moduleWrapper.node.body.body)); - - // Remove /***/ comments between modules (in webpack development builds) - const lastNode = file.program.body.at(-1); - if ( - lastNode?.trailingComments?.length === 1 && - lastNode.trailingComments[0].value === '*' - ) { - lastNode.trailingComments = null; - } - - const module = new WebpackModule( - moduleId, - file, - moduleId === entryIdMatcher.current?.value.toString(), - ); - - modules.set(moduleId, module); - } - }); - - if (modules.size > 0) { - const entryId = entryIdMatcher.current?.value.toString() ?? ''; - options!.bundle = new WebpackBundle(entryId, modules); - } - }, - }; - }, -} satisfies Transform<{ bundle: Bundle | undefined }>; diff --git a/packages/webcrack/src/unpack/webpack/module.ts b/packages/webcrack/src/unpack/webpack/module.ts index 474e8f29..06ec02a3 100644 --- a/packages/webcrack/src/unpack/webpack/module.ts +++ b/packages/webcrack/src/unpack/webpack/module.ts @@ -1,3 +1,118 @@ -import { Module } from '../module'; +import { Binding, NodePath } from '@babel/traverse'; +import * as t from '@babel/types'; +import * as m from '@codemod/matchers'; +import { + applyTransform, + constMemberExpression, + renameParameters, +} from '../../ast-utils'; +import { Import, Module } from '../module'; +import { FunctionPath } from './common-matchers'; +import defineExport from './define-exports'; +import varInjections from './var-injections'; -export class WebpackModule extends Module {} +export class WebpackModule extends Module { + #webpackRequireBinding: Binding | undefined; + // TODO: expose to public API + #imports: Import[] = []; + // TODO: expose to public API + #sourceType: 'commonjs' | 'esm' = 'commonjs'; + + constructor(id: string, ast: FunctionPath, isEntry: boolean) { + // TODO: refactor + const file = t.file(t.program(ast.node.body.body)); + super(id, file, isEntry); + + renameParameters(ast, ['module', 'exports', '__webpack_require__']); + this.#webpackRequireBinding = ast.scope.getBinding('__webpack_require__'); + applyTransform(file, varInjections); + this.removeTrailingComments(); + this.removeDefineESM(); + this.convertExportsToESM(); + } + + /** + * Remove /***\/ comments between modules (in webpack development builds) + */ + private removeTrailingComments(): void { + const lastNode = this.ast.program.body.at(-1); + if ( + lastNode?.trailingComments?.length === 1 && + lastNode.trailingComments[0].value === '*' + ) { + lastNode.trailingComments = null; + } + } + + /** + * Removes + * - `__webpack_require__.r(exports);` (webpack/runtime/make namespace object) + * - `Object.defineProperty(exports, "__esModule", { value: true });` + */ + private removeDefineESM(): void { + const matcher = m.expressionStatement( + m.or( + m.callExpression(constMemberExpression('__webpack_require__', 'r'), [ + m.identifier(), + ]), + m.callExpression(constMemberExpression('Object', 'defineProperty'), [ + m.identifier(), + m.stringLiteral('__esModule'), + m.objectExpression([ + m.objectProperty(m.identifier('value'), m.booleanLiteral(true)), + ]), + ]), + ), + ); + + for (let i = 0; i < this.ast.program.body.length; i++) { + const node = this.ast.program.body[i]; + if (matcher.match(node)) { + this.#sourceType = 'esm'; + this.ast.program.body.splice(i, 1); + i--; + } + } + } + + private convertExportsToESM(): void { + applyTransform(this.ast, defineExport); + } + + /** + * ```diff + * - __webpack_require__(id) + * + require("./relative/path.js") + * ``` + * @internal + */ + replaceRequireCalls( + onResolve: (id: string) => { path: string; external?: boolean }, + ): void { + if (!this.#webpackRequireBinding) return; + + const idArg = m.capture(m.or(m.numericLiteral(), m.stringLiteral())); + const requireCall = m.callExpression(m.identifier('__webpack_require__'), [ + idArg, + ]); + + this.#webpackRequireBinding.referencePaths.forEach((path) => { + m.matchPath(requireCall, { idArg }, path.parentPath!, ({ idArg }) => { + const id = idArg.node.value.toString(); + const result = onResolve(id); + + (path.node as t.Identifier).name = 'require'; + idArg.replaceWith(t.stringLiteral(result.path)); + if (result.external) { + idArg.addComment('leading', 'webcrack:missing'); + } + + this.#imports.push({ + id, + path: result.path, + nodePath: path.parentPath as NodePath, + }); + }); + }); + } +} diff --git a/packages/webcrack/src/unpack/webpack/unpack-webpack-4.ts b/packages/webcrack/src/unpack/webpack/unpack-webpack-4.ts new file mode 100644 index 00000000..264f5b4e --- /dev/null +++ b/packages/webcrack/src/unpack/webpack/unpack-webpack-4.ts @@ -0,0 +1,73 @@ +import { NodePath } from '@babel/traverse'; +import * as t from '@babel/types'; +import * as m from '@codemod/matchers'; +import { Bundle } from '..'; +import { Transform, renameFast } from '../../ast-utils'; +import { WebpackBundle } from './bundle'; +import { + findAssignedEntryId, + findRequiredEntryId, + getModuleFunctions, + modulesContainerMatcher, + webpackRequireFunctionMatcher, +} from './common-matchers'; +import { WebpackModule } from './module'; + +/** + * Format: + * ```js + * (function (__webpack_modules__) { + * function __webpack_require__(moduleId) { ... } + * })([...]); + * ``` + */ +export default { + name: 'unpack-webpack-4', + tags: ['unsafe'], + visitor(options = { bundle: undefined }) { + const { webpackRequire, containerId } = webpackRequireFunctionMatcher(); + const container = modulesContainerMatcher(); + + const matcher = m.callExpression( + m.functionExpression( + null, + [containerId], + m.blockStatement( + m.anyList( + m.zeroOrMore(), + webpackRequire, + m.zeroOrMore(), + ), + ), + ), + [container], + ); + + return { + CallExpression(path) { + if (!matcher.match(path.node)) return; + + const webpackRequireBinding = path + .get('callee') + .scope.getBinding(webpackRequire.current!.id!.name)!; + renameFast(webpackRequireBinding, '__webpack_require__'); + + const entryId = + findAssignedEntryId(webpackRequireBinding) || + findRequiredEntryId(webpackRequireBinding); + const containerPath = path.get( + container.currentKeys!.join('.'), + ) as NodePath; + + const modules = new Map(); + + for (const [id, func] of getModuleFunctions(containerPath)) { + const isEntry = id === entryId; + modules.set(id, new WebpackModule(id, func, isEntry)); + } + + options.bundle = new WebpackBundle(entryId ?? '', modules); + }, + }; + }, +} satisfies Transform<{ bundle: Bundle | undefined }>; diff --git a/packages/webcrack/src/unpack/webpack/unpack-webpack-5.ts b/packages/webcrack/src/unpack/webpack/unpack-webpack-5.ts new file mode 100644 index 00000000..1de76b30 --- /dev/null +++ b/packages/webcrack/src/unpack/webpack/unpack-webpack-5.ts @@ -0,0 +1,73 @@ +import { NodePath } from '@babel/traverse'; +import * as t from '@babel/types'; +import * as m from '@codemod/matchers'; +import { Bundle } from '..'; +import { Transform, renameFast } from '../../ast-utils'; +import { WebpackBundle } from './bundle'; +import { + findAssignedEntryId, + getModuleFunctions, + modulesContainerMatcher, + webpackRequireFunctionMatcher, +} from './common-matchers'; +import { WebpackModule } from './module'; + +// TODO: the entry module can be at the bottom in an iife + +/** + * Format: + * ```js + * (function () { + * var __webpack_modules__ = { ... }; + * function __webpack_require__(moduleId) { ... } + * })(); + * ``` + */ +export default { + name: 'unpack-webpack-5', + tags: ['unsafe'], + scope: true, + visitor(options = { bundle: undefined }) { + const { webpackRequire, containerId } = webpackRequireFunctionMatcher(); + const container = modulesContainerMatcher(); + + const matcher = m.blockStatement( + m.anyList( + m.zeroOrMore(), + // Example: var __webpack_modules__ = { ... }; + m.variableDeclaration(undefined, [ + m.variableDeclarator(containerId, container), + ]), + m.zeroOrMore(), + webpackRequire, + m.zeroOrMore(), + ), + ); + + return { + BlockStatement(path) { + if (!matcher.match(path.node)) return; + path.stop(); + + const webpackRequireBinding = path.scope.getBinding( + webpackRequire.current!.id!.name, + )!; + renameFast(webpackRequireBinding, '__webpack_require__'); + + const entryId = findAssignedEntryId(webpackRequireBinding); + const containerPath = path.get( + container.currentKeys!.join('.'), + ) as NodePath; + + const modules = new Map(); + + for (const [id, func] of getModuleFunctions(containerPath)) { + const isEntry = id === entryId; + modules.set(id, new WebpackModule(id, func, isEntry)); + } + + options.bundle = new WebpackBundle(entryId ?? '', modules); + }, + }; + }, +} satisfies Transform<{ bundle: Bundle | undefined }>; diff --git a/packages/webcrack/src/unpack/webpack/unpack-webpack-chunk.ts b/packages/webcrack/src/unpack/webpack/unpack-webpack-chunk.ts new file mode 100644 index 00000000..3b118499 --- /dev/null +++ b/packages/webcrack/src/unpack/webpack/unpack-webpack-chunk.ts @@ -0,0 +1,78 @@ +import { NodePath } from '@babel/traverse'; +import * as t from '@babel/types'; +import * as m from '@codemod/matchers'; +import { Bundle } from '..'; +import { Transform, constMemberExpression } from '../../ast-utils'; +import { WebpackBundle } from './bundle'; +import { getModuleFunctions, modulesContainerMatcher } from './common-matchers'; +import { WebpackModule } from './module'; + +/** + * Format: + * ```js + * (window.webpackJsonp = window.webpackJsonp || []).push([[0], {...}]) + * ``` + */ +export default { + name: 'unpack-webpack-chunk', + tags: ['unsafe'], + visitor(options = { bundle: undefined }) { + const container = modulesContainerMatcher(); + + // Examples: self.webpackChunk_N_E, window.webpackJsonp, this.webpackJsonp + const jsonpGlobal = m.capture( + constMemberExpression( + m.or(m.identifier(), m.thisExpression()), + m.matcher((property) => property.startsWith('webpack')), + ), + ); + + const matcher = m.callExpression( + constMemberExpression( + m.assignmentExpression( + '=', + jsonpGlobal, + m.logicalExpression( + '||', + m.fromCapture(jsonpGlobal), + m.arrayExpression([]), + ), + ), + 'push', + ), + [ + m.arrayExpression( + m.anyList( + // chunkIds + m.arrayExpression( + m.arrayOf(m.or(m.numericLiteral(), m.stringLiteral())), + ), + container, + // optional entry point like [["57iH",19,24,25]] or a function + m.zeroOrMore(), + ), + ), + ], + ); + + return { + CallExpression(path) { + if (!matcher.match(path.node)) return; + + // TODO: WebpackChunk class + const modules = new Map(); + + const containerPath = path.get( + container.currentKeys!.join('.'), + ) as NodePath; + + for (const [id, func] of getModuleFunctions(containerPath)) { + const isEntry = false; // FIXME: afaik after the modules there can be a function that specifies the entry point + modules.set(id, new WebpackModule(id, func, isEntry)); + } + + options.bundle = new WebpackBundle('', modules); + }, + }; + }, +} satisfies Transform<{ bundle: Bundle | undefined }>; diff --git a/packages/webcrack/src/unpack/webpack/var-injections.ts b/packages/webcrack/src/unpack/webpack/var-injections.ts new file mode 100644 index 00000000..0b9dee33 --- /dev/null +++ b/packages/webcrack/src/unpack/webpack/var-injections.ts @@ -0,0 +1,47 @@ +import * as t from '@babel/types'; +import * as m from '@codemod/matchers'; +import { Transform, constMemberExpression } from '../../ast-utils'; + +/** + * ```diff + * - (function(global) { ... }).call(exports, require(7)); + * + var global = require(7); + * ``` + */ +export default { + name: 'var-injections', + tags: ['unsafe'], // doesn't handle possible variable conflicts when merging with the parent scope + visitor() { + const statements = m.capture(m.arrayOf(m.anyStatement())); + const params = m.capture(m.arrayOf(m.identifier())); + const args = m.capture( + m.anyList( + m.or(m.thisExpression(), m.identifier('exports')), + m.oneOrMore(), + ), + ); + const matcher = m.expressionStatement( + m.callExpression( + constMemberExpression( + m.functionExpression(null, params, m.blockStatement(statements)), + 'call', + ), + args, + ), + ); + + return { + ExpressionStatement(path) { + if (!path.parentPath.isProgram() || !matcher.match(path.node)) return; + + const variables = params.current!.map((param, i) => + t.variableDeclaration('var', [ + t.variableDeclarator(param, args.current![i + 1]), + ]), + ); + path.replaceWithMultiple([...variables, ...statements.current!]); + this.changes++; + }, + }; + }, +} satisfies Transform; diff --git a/packages/webcrack/src/unpack/webpack/varInjection.ts b/packages/webcrack/src/unpack/webpack/varInjection.ts deleted file mode 100644 index 440da051..00000000 --- a/packages/webcrack/src/unpack/webpack/varInjection.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { statement } from '@babel/template'; -import type { Statement } from '@babel/types'; -import * as m from '@codemod/matchers'; -import { constMemberExpression } from '../../ast-utils'; -import type { WebpackModule } from './module'; - -const buildVar = statement`var NAME = INIT;`; - -/** - * ```js - * (function(global) { - * // ... - * }.call(exports, require(7))) - * ``` - * -> - * ```js - * var global = require(7); - * // ... - * ``` - */ -export function inlineVarInjections(module: WebpackModule): void { - const { program } = module.ast; - const newBody: Statement[] = []; - - const body = m.capture(m.blockStatement()); - const params = m.capture(m.arrayOf(m.identifier())); - const args = m.capture( - m.anyList(m.or(m.thisExpression(), m.identifier('exports')), m.oneOrMore()), - ); - const matcher = m.expressionStatement( - m.callExpression( - constMemberExpression( - m.functionExpression(undefined, params, body), - 'call', - ), - args, - ), - ); - - for (const node of program.body) { - if (matcher.match(node)) { - const vars = params.current!.map((param, i) => - buildVar({ NAME: param, INIT: args.current![i + 1] }), - ); - newBody.push(...vars); - newBody.push(...body.current!.body); - // We can skip replacing uses of `this` because it always refers to the exports - } else { - newBody.push(node); - } - } - program.body = newBody; -} diff --git a/packages/webcrack/test/setup.ts b/packages/webcrack/test/setup.ts index d819fe25..c1d384cc 100644 --- a/packages/webcrack/test/setup.ts +++ b/packages/webcrack/test/setup.ts @@ -1,3 +1,4 @@ +import { NodePath } from '@babel/traverse'; import * as t from '@babel/types'; import { expect } from 'vitest'; import { generate } from '../src/ast-utils'; @@ -6,3 +7,8 @@ expect.addSnapshotSerializer({ test: (val: unknown) => t.isNode(val) && !('parentPath' in val), serialize: (val: t.Node) => generate(val), }); + +expect.addSnapshotSerializer({ + test: (val: unknown) => t.isNode(val) && 'parentPath' in val, + serialize: (val: NodePath) => `NodePath<${val.type}>`, +}); From 821bf89469368dec7974205a842cd840afd839e9 Mon Sep 17 00:00:00 2001 From: j4k0xb <55899582+j4k0xb@users.noreply.github.com> Date: Sat, 13 Jan 2024 03:20:26 +0100 Subject: [PATCH 02/81] feat: webpack chunk class --- .../test/__snapshots__/unpack.test.ts.snap | 1 + .../test/samples/webpack-0.11.x.js.snap | 3 ++- .../test/samples/webpack-chunk-entries.js | 5 ++++ .../samples/webpack-chunk-entries.js.snap | 26 +++++++++++++++++++ .../test/samples/webpack-commonjs.js.snap | 3 ++- .../unpack/test/samples/webpack-esm.js.snap | 3 ++- .../test/samples/webpack-jsonp-chunk.js.snap | 21 +++++++++++++++ .../test/samples/webpack-object.js.snap | 3 ++- .../samples/webpack-path-traversal.js.snap | 3 ++- .../src/unpack/test/samples/webpack.js.snap | 3 ++- .../unpack/test/samples/webpack5-esm.js.snap | 3 ++- .../test/samples/webpack5-no-entry.js.snap | 3 ++- .../test/samples/webpack5-object.js.snap | 3 ++- .../webcrack/src/unpack/webpack/bundle.ts | 5 +++- packages/webcrack/src/unpack/webpack/chunk.ts | 17 ++++++++++++ .../unpack/webpack/unpack-webpack-chunk.ts | 14 ++++++---- 16 files changed, 101 insertions(+), 15 deletions(-) create mode 100644 packages/webcrack/src/unpack/test/samples/webpack-chunk-entries.js create mode 100644 packages/webcrack/src/unpack/test/samples/webpack-chunk-entries.js.snap create mode 100644 packages/webcrack/src/unpack/webpack/chunk.ts 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/samples/webpack-0.11.x.js.snap b/packages/webcrack/src/unpack/test/samples/webpack-0.11.x.js.snap index 7c86b612..1aa9ff0f 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,4 +1,5 @@ WebpackBundle { + "chunks": [], "entryId": "0", "modules": Map { "0" => WebpackModule { @@ -245,4 +246,4 @@ exports.readFileSync = function (filename) { }, }, "type": "webpack", -} \ No newline at end of file +} diff --git a/packages/webcrack/src/unpack/test/samples/webpack-chunk-entries.js b/packages/webcrack/src/unpack/test/samples/webpack-chunk-entries.js new file mode 100644 index 00000000..ddca77c8 --- /dev/null +++ b/packages/webcrack/src/unpack/test/samples/webpack-chunk-entries.js @@ -0,0 +1,5 @@ +(window['webpackJsonp'] = window['webpackJsonp'] || []).push([ + [1], + [function (module, exports, __webpack_require__) {}], + [[0, 0]], +]); diff --git a/packages/webcrack/src/unpack/test/samples/webpack-chunk-entries.js.snap b/packages/webcrack/src/unpack/test/samples/webpack-chunk-entries.js.snap new file mode 100644 index 00000000..3181dfc5 --- /dev/null +++ b/packages/webcrack/src/unpack/test/samples/webpack-chunk-entries.js.snap @@ -0,0 +1,26 @@ +WebpackBundle { + "chunks": [ + WebpackChunk { + "entryIds": [], + "id": "1", + "modules": Map { + "0" => WebpackModule { + "ast": , + "id": "0", + "isEntry": false, + "path": "./0.js", + }, + }, + }, + ], + "entryId": "", + "modules": Map { + "0" => WebpackModule { + "ast": , + "id": "0", + "isEntry": false, + "path": "./0.js", + }, + }, + "type": "webpack", +} \ No newline at end of file diff --git a/packages/webcrack/src/unpack/test/samples/webpack-commonjs.js.snap b/packages/webcrack/src/unpack/test/samples/webpack-commonjs.js.snap index cd03c8d3..6729e8ed 100644 --- a/packages/webcrack/src/unpack/test/samples/webpack-commonjs.js.snap +++ b/packages/webcrack/src/unpack/test/samples/webpack-commonjs.js.snap @@ -1,4 +1,5 @@ WebpackBundle { + "chunks": [], "entryId": "", "modules": Map { "1" => WebpackModule { @@ -27,4 +28,4 @@ exports.increment = function (val) { }, }, "type": "webpack", -} \ No newline at end of file +} diff --git a/packages/webcrack/src/unpack/test/samples/webpack-esm.js.snap b/packages/webcrack/src/unpack/test/samples/webpack-esm.js.snap index bf61ccc3..cd0fd694 100644 --- a/packages/webcrack/src/unpack/test/samples/webpack-esm.js.snap +++ b/packages/webcrack/src/unpack/test/samples/webpack-esm.js.snap @@ -1,4 +1,5 @@ WebpackBundle { + "chunks": [], "entryId": "0", "modules": Map { "0" => WebpackModule { @@ -47,4 +48,4 @@ export default 1;, }, }, "type": "webpack", -} \ No newline at end of file +} diff --git a/packages/webcrack/src/unpack/test/samples/webpack-jsonp-chunk.js.snap b/packages/webcrack/src/unpack/test/samples/webpack-jsonp-chunk.js.snap index bfd84e0e..7b08540a 100644 --- a/packages/webcrack/src/unpack/test/samples/webpack-jsonp-chunk.js.snap +++ b/packages/webcrack/src/unpack/test/samples/webpack-jsonp-chunk.js.snap @@ -1,4 +1,25 @@ WebpackBundle { + "chunks": [ + WebpackChunk { + "entryIds": [], + "id": "15", + "modules": Map { + "2wwy" => WebpackModule { + "ast": const a = require("./E8gZ.js"); +const b = require( /*webcrack:missing*/"w6GO");, + "id": "2wwy", + "isEntry": false, + "path": "./2wwy.js", + }, + "E8gZ" => WebpackModule { + "ast": module.exports = 4;, + "id": "E8gZ", + "isEntry": false, + "path": "./E8gZ.js", + }, + }, + }, + ], "entryId": "", "modules": Map { "2wwy" => WebpackModule { diff --git a/packages/webcrack/src/unpack/test/samples/webpack-object.js.snap b/packages/webcrack/src/unpack/test/samples/webpack-object.js.snap index 76a0c870..3c18bf17 100644 --- a/packages/webcrack/src/unpack/test/samples/webpack-object.js.snap +++ b/packages/webcrack/src/unpack/test/samples/webpack-object.js.snap @@ -1,4 +1,5 @@ WebpackBundle { + "chunks": [], "entryId": "386", "modules": Map { "386" => WebpackModule { @@ -15,4 +16,4 @@ WebpackBundle { }, }, "type": "webpack", -} \ No newline at end of file +} diff --git a/packages/webcrack/src/unpack/test/samples/webpack-path-traversal.js.snap b/packages/webcrack/src/unpack/test/samples/webpack-path-traversal.js.snap index 845dbeb9..2c519e4c 100644 --- a/packages/webcrack/src/unpack/test/samples/webpack-path-traversal.js.snap +++ b/packages/webcrack/src/unpack/test/samples/webpack-path-traversal.js.snap @@ -1,4 +1,5 @@ WebpackBundle { + "chunks": [], "entryId": "386", "modules": Map { "../tmp.js" => WebpackModule { @@ -9,4 +10,4 @@ WebpackBundle { }, }, "type": "webpack", -} \ No newline at end of file +} diff --git a/packages/webcrack/src/unpack/test/samples/webpack.js.snap b/packages/webcrack/src/unpack/test/samples/webpack.js.snap index d725b264..2264c2ca 100644 --- a/packages/webcrack/src/unpack/test/samples/webpack.js.snap +++ b/packages/webcrack/src/unpack/test/samples/webpack.js.snap @@ -1,4 +1,5 @@ WebpackBundle { + "chunks": [], "entryId": "2", "modules": Map { "1" => WebpackModule { @@ -29,4 +30,4 @@ exports.a = 3;, }, }, "type": "webpack", -} \ No newline at end of file +} diff --git a/packages/webcrack/src/unpack/test/samples/webpack5-esm.js.snap b/packages/webcrack/src/unpack/test/samples/webpack5-esm.js.snap index 55df33a4..87c7ba82 100644 --- a/packages/webcrack/src/unpack/test/samples/webpack5-esm.js.snap +++ b/packages/webcrack/src/unpack/test/samples/webpack5-esm.js.snap @@ -1,4 +1,5 @@ WebpackBundle { + "chunks": [], "entryId": "2", "modules": Map { "2" => WebpackModule { @@ -31,4 +32,4 @@ function bar() {}, }, }, "type": "webpack", -} \ No newline at end of file +} diff --git a/packages/webcrack/src/unpack/test/samples/webpack5-no-entry.js.snap b/packages/webcrack/src/unpack/test/samples/webpack5-no-entry.js.snap index 2f46591c..6a1343aa 100644 --- a/packages/webcrack/src/unpack/test/samples/webpack5-no-entry.js.snap +++ b/packages/webcrack/src/unpack/test/samples/webpack5-no-entry.js.snap @@ -1,4 +1,5 @@ WebpackBundle { + "chunks": [], "entryId": "", "modules": Map { "97418" => WebpackModule { @@ -9,4 +10,4 @@ WebpackBundle { }, }, "type": "webpack", -} \ No newline at end of file +} diff --git a/packages/webcrack/src/unpack/test/samples/webpack5-object.js.snap b/packages/webcrack/src/unpack/test/samples/webpack5-object.js.snap index acc27fd1..cd55226d 100644 --- a/packages/webcrack/src/unpack/test/samples/webpack5-object.js.snap +++ b/packages/webcrack/src/unpack/test/samples/webpack5-object.js.snap @@ -1,4 +1,5 @@ WebpackBundle { + "chunks": [], "entryId": "2", "modules": Map { "2" => WebpackModule { @@ -17,4 +18,4 @@ const _0x8da276 = __webpack_require__(__webpack_require__.ab + "build/Release/sp }, }, "type": "webpack", -} \ No newline at end of file +} diff --git a/packages/webcrack/src/unpack/webpack/bundle.ts b/packages/webcrack/src/unpack/webpack/bundle.ts index 1461d8f3..cd82e649 100644 --- a/packages/webcrack/src/unpack/webpack/bundle.ts +++ b/packages/webcrack/src/unpack/webpack/bundle.ts @@ -1,14 +1,17 @@ import { Bundle } from '../bundle'; import { relativePath } from '../path'; +import { WebpackChunk } from './chunk'; import { convertESM } from './esm'; import { convertDefaultRequire } from './getDefaultExport'; import type { WebpackModule } from './module'; export class WebpackBundle extends Bundle { declare modules: Map; + chunks: WebpackChunk[]; - constructor(entryId: string, modules: Map) { + constructor(entryId: string, modules: Map, chunks: WebpackChunk[] = []) { super('webpack', entryId, modules); + this.chunks = chunks; } /** diff --git a/packages/webcrack/src/unpack/webpack/chunk.ts b/packages/webcrack/src/unpack/webpack/chunk.ts new file mode 100644 index 00000000..4928b930 --- /dev/null +++ b/packages/webcrack/src/unpack/webpack/chunk.ts @@ -0,0 +1,17 @@ +import { WebpackModule } from './module'; + +export class WebpackChunk { + id: string; + entryIds: string[]; + modules: Map; + + constructor( + id: string, + entryIds: string[], + modules: Map, + ) { + this.id = id; + this.entryIds = entryIds; + this.modules = modules; + } +} diff --git a/packages/webcrack/src/unpack/webpack/unpack-webpack-chunk.ts b/packages/webcrack/src/unpack/webpack/unpack-webpack-chunk.ts index 3b118499..d84968ae 100644 --- a/packages/webcrack/src/unpack/webpack/unpack-webpack-chunk.ts +++ b/packages/webcrack/src/unpack/webpack/unpack-webpack-chunk.ts @@ -4,6 +4,7 @@ import * as m from '@codemod/matchers'; import { Bundle } from '..'; import { Transform, constMemberExpression } from '../../ast-utils'; import { WebpackBundle } from './bundle'; +import { WebpackChunk } from './chunk'; import { getModuleFunctions, modulesContainerMatcher } from './common-matchers'; import { WebpackModule } from './module'; @@ -27,6 +28,9 @@ export default { ), ); + const chunkIds = m.capture( + m.arrayOf(m.or(m.numericLiteral(), m.stringLiteral())), + ); const matcher = m.callExpression( constMemberExpression( m.assignmentExpression( @@ -43,10 +47,7 @@ export default { [ m.arrayExpression( m.anyList( - // chunkIds - m.arrayExpression( - m.arrayOf(m.or(m.numericLiteral(), m.stringLiteral())), - ), + m.arrayExpression(chunkIds), container, // optional entry point like [["57iH",19,24,25]] or a function m.zeroOrMore(), @@ -70,8 +71,11 @@ export default { const isEntry = false; // FIXME: afaik after the modules there can be a function that specifies the entry point modules.set(id, new WebpackModule(id, func, isEntry)); } + const chunks = chunkIds.current!.map( + (id) => new WebpackChunk(id.value.toString(), [], modules), + ); - options.bundle = new WebpackBundle('', modules); + options.bundle = new WebpackBundle('', modules, chunks); }, }; }, From 736926830a7b9e64b780cd2d574fc0bd96f6c2a4 Mon Sep 17 00:00:00 2001 From: j4k0xb <55899582+j4k0xb@users.noreply.github.com> Date: Sat, 13 Jan 2024 03:20:26 +0100 Subject: [PATCH 03/81] fix: remove trailing webpack comment --- packages/webcrack/src/unpack/webpack/module.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/webcrack/src/unpack/webpack/module.ts b/packages/webcrack/src/unpack/webpack/module.ts index 06ec02a3..a1691cf7 100644 --- a/packages/webcrack/src/unpack/webpack/module.ts +++ b/packages/webcrack/src/unpack/webpack/module.ts @@ -37,10 +37,11 @@ export class WebpackModule extends Module { private removeTrailingComments(): void { const lastNode = this.ast.program.body.at(-1); if ( - lastNode?.trailingComments?.length === 1 && - lastNode.trailingComments[0].value === '*' + lastNode?.trailingComments && + lastNode.trailingComments.length >= 1 && + lastNode.trailingComments.at(-1)!.value === '*' ) { - lastNode.trailingComments = null; + lastNode.trailingComments.pop(); } } From 95e519a703dfdae71d8562068fee245e1a1519b8 Mon Sep 17 00:00:00 2001 From: j4k0xb <55899582+j4k0xb@users.noreply.github.com> Date: Sat, 13 Jan 2024 03:20:26 +0100 Subject: [PATCH 04/81] feat: export default expression --- .../src/unpack/test/define-exports.test.ts | 22 +++++++++++++++++++ .../src/unpack/webpack/define-exports.ts | 13 ++++++++++- .../webcrack/src/unpack/webpack/module.ts | 4 +++- 3 files changed, 37 insertions(+), 2 deletions(-) diff --git a/packages/webcrack/src/unpack/test/define-exports.test.ts b/packages/webcrack/src/unpack/test/define-exports.test.ts index 64304e6a..a2e70ef3 100644 --- a/packages/webcrack/src/unpack/test/define-exports.test.ts +++ b/packages/webcrack/src/unpack/test/define-exports.test.ts @@ -65,6 +65,11 @@ test('export default class', () => class foo {} `).toMatchInlineSnapshot(`export default class foo {}`)); +test('export default expression (webpack 4)', () => + expectJS(` + exports.default = 1; + `).toMatchInlineSnapshot(`export default 1;`)); + test('export object destructuring', () => expectJS(` __webpack_require__.d(__webpack_exports__, { @@ -171,6 +176,23 @@ test('re-export default as default', () => // hard to detect this case test.todo('re-export all'); // export * from 'fs'; +test.todo('re-export all from commonjs (webpack 4)', () => + expectJS(` + var fs = __webpack_require__("fs"); + for (var importKey in fs) { + if (["default"].indexOf(importKey) < 0) { + (function (key) { + __webpack_require__.d(exports, key, function () { + return fs[key]; + }); + })(importKey); + } + } + `).toMatchInlineSnapshot(` + export * from "./fs"; + `), +); + test.todo('re-export all as named', () => expectJS(` __webpack_require__.d(exports, "fs", function() { return fs; }); diff --git a/packages/webcrack/src/unpack/webpack/define-exports.ts b/packages/webcrack/src/unpack/webpack/define-exports.ts index ad6b494b..25848e90 100644 --- a/packages/webcrack/src/unpack/webpack/define-exports.ts +++ b/packages/webcrack/src/unpack/webpack/define-exports.ts @@ -48,6 +48,14 @@ export default { ]), ); + const defaultExpressionExport = m.expressionStatement( + m.assignmentExpression( + '=', + constMemberExpression('exports', 'default'), + returnValue, + ), + ); + const objectProperty = m.objectProperty(m.identifier(exportName), getter); const properties = m.capture(m.arrayOf(objectProperty)); // Example (webpack v5): __webpack_require__.d(exports, { a: () => b, c: () => d }); @@ -60,12 +68,15 @@ export default { return { ExpressionStatement(path) { - if (!path.parentPath.isProgram()) return; + if (!path.parentPath.isProgram()) return path.skip(); if (singleExport.match(path.node)) { addExport(path, exportName.current!, returnValue.current!); path.remove(); this.changes++; + } else if (defaultExpressionExport.match(path.node)) { + path.replaceWith(t.exportDefaultDeclaration(returnValue.current!)); + this.changes++; } else if (multiExport.match(path.node)) { for (const property of properties.current!) { objectProperty.match(property); // To easily get the captures per property diff --git a/packages/webcrack/src/unpack/webpack/module.ts b/packages/webcrack/src/unpack/webpack/module.ts index a1691cf7..bfe876c7 100644 --- a/packages/webcrack/src/unpack/webpack/module.ts +++ b/packages/webcrack/src/unpack/webpack/module.ts @@ -28,7 +28,9 @@ export class WebpackModule extends Module { applyTransform(file, varInjections); this.removeTrailingComments(); this.removeDefineESM(); - this.convertExportsToESM(); + if (this.#sourceType === 'esm') { + this.convertExportsToESM(); + } } /** From c41576bbb9847cff270d44d33fafee93335f24d5 Mon Sep 17 00:00:00 2001 From: j4k0xb <55899582+j4k0xb@users.noreply.github.com> Date: Sat, 13 Jan 2024 03:20:26 +0100 Subject: [PATCH 05/81] refactor: store multiple chunk ids in a chunk --- .../unpack/test/samples/webpack-chunk-entries.js.snap | 4 +++- .../unpack/test/samples/webpack-jsonp-chunk.js.snap | 4 +++- packages/webcrack/src/unpack/webpack/chunk.ts | 6 +++--- .../src/unpack/webpack/unpack-webpack-chunk.ts | 11 ++++++----- 4 files changed, 15 insertions(+), 10 deletions(-) diff --git a/packages/webcrack/src/unpack/test/samples/webpack-chunk-entries.js.snap b/packages/webcrack/src/unpack/test/samples/webpack-chunk-entries.js.snap index 3181dfc5..2aeebdce 100644 --- a/packages/webcrack/src/unpack/test/samples/webpack-chunk-entries.js.snap +++ b/packages/webcrack/src/unpack/test/samples/webpack-chunk-entries.js.snap @@ -1,8 +1,10 @@ WebpackBundle { "chunks": [ WebpackChunk { + "chunkIds": [ + "1", + ], "entryIds": [], - "id": "1", "modules": Map { "0" => WebpackModule { "ast": , diff --git a/packages/webcrack/src/unpack/test/samples/webpack-jsonp-chunk.js.snap b/packages/webcrack/src/unpack/test/samples/webpack-jsonp-chunk.js.snap index 7b08540a..08d7dc1b 100644 --- a/packages/webcrack/src/unpack/test/samples/webpack-jsonp-chunk.js.snap +++ b/packages/webcrack/src/unpack/test/samples/webpack-jsonp-chunk.js.snap @@ -1,8 +1,10 @@ WebpackBundle { "chunks": [ WebpackChunk { + "chunkIds": [ + "15", + ], "entryIds": [], - "id": "15", "modules": Map { "2wwy" => WebpackModule { "ast": const a = require("./E8gZ.js"); diff --git a/packages/webcrack/src/unpack/webpack/chunk.ts b/packages/webcrack/src/unpack/webpack/chunk.ts index 4928b930..49659329 100644 --- a/packages/webcrack/src/unpack/webpack/chunk.ts +++ b/packages/webcrack/src/unpack/webpack/chunk.ts @@ -1,16 +1,16 @@ import { WebpackModule } from './module'; export class WebpackChunk { - id: string; + chunkIds: string[]; entryIds: string[]; modules: Map; constructor( - id: string, + id: string[], entryIds: string[], modules: Map, ) { - this.id = id; + this.chunkIds = id; this.entryIds = entryIds; this.modules = modules; } diff --git a/packages/webcrack/src/unpack/webpack/unpack-webpack-chunk.ts b/packages/webcrack/src/unpack/webpack/unpack-webpack-chunk.ts index d84968ae..9dfa8cfe 100644 --- a/packages/webcrack/src/unpack/webpack/unpack-webpack-chunk.ts +++ b/packages/webcrack/src/unpack/webpack/unpack-webpack-chunk.ts @@ -60,7 +60,6 @@ export default { CallExpression(path) { if (!matcher.match(path.node)) return; - // TODO: WebpackChunk class const modules = new Map(); const containerPath = path.get( @@ -71,11 +70,13 @@ export default { const isEntry = false; // FIXME: afaik after the modules there can be a function that specifies the entry point modules.set(id, new WebpackModule(id, func, isEntry)); } - const chunks = chunkIds.current!.map( - (id) => new WebpackChunk(id.value.toString(), [], modules), - ); - options.bundle = new WebpackBundle('', modules, chunks); + const chunk = new WebpackChunk( + chunkIds.current!.map((id) => id.value.toString()), + [], + modules, + ); + options.bundle = new WebpackBundle('', modules, [chunk]); }, }; }, From a3e126e78d63422c7485fa31964a95d9ba7ce107 Mon Sep 17 00:00:00 2001 From: j4k0xb <55899582+j4k0xb@users.noreply.github.com> Date: Sat, 13 Jan 2024 03:20:26 +0100 Subject: [PATCH 06/81] refactor: group export tests by webpack 4/5 --- .../src/unpack/test/define-exports.test.ts | 439 ++++++++++-------- .../webcrack/src/unpack/webpack/module.ts | 2 + 2 files changed, 239 insertions(+), 202 deletions(-) diff --git a/packages/webcrack/src/unpack/test/define-exports.test.ts b/packages/webcrack/src/unpack/test/define-exports.test.ts index a2e70ef3..3aba17bd 100644 --- a/packages/webcrack/src/unpack/test/define-exports.test.ts +++ b/packages/webcrack/src/unpack/test/define-exports.test.ts @@ -1,208 +1,243 @@ -import { test } from 'vitest'; +import { describe, test } from 'vitest'; import { testTransform } from '../../../test'; import defineExports from '../webpack/define-exports'; const expectJS = testTransform(defineExports); -test('export named (webpack 4)', () => - expectJS(` - __webpack_require__.d(exports, "counter", function() { return foo; }); - var foo = 1; - `).toMatchInlineSnapshot(`export var counter = 1;`)); - -test('export named (webpack 5)', () => - expectJS(` - __webpack_require__.d(exports, { - counter: () => foo, - increment: () => increment, - }); - var foo = 1; - function increment() {} - `).toMatchInlineSnapshot(` - export var counter = 1; - export function increment() {} - `)); - -test.todo('export same variable with multiple names', () => - expectJS(` - __webpack_require__.d(exports, { - counter: () => foo, - increment: () => foo, - }); - var foo = 1; - `).toMatchInlineSnapshot(` - export var counter = 1; - export { counter as increment }; - `), -); - -test('export default variable', () => - expectJS(` - __webpack_require__.d(exports, "default", function() { return foo; }); - var foo = 1; - `).toMatchInlineSnapshot(`export default 1;`)); - -test('export default variable with multiple references', () => - expectJS(` - __webpack_require__.d(exports, "default", function() { return foo; }); - var foo = 1; - console.log(foo); - `).toMatchInlineSnapshot(` - var foo = 1; - export { foo as default }; - console.log(foo); - `)); - -test('export default function', () => - expectJS(` - __webpack_require__.d(exports, "default", function() { return foo; }); - function foo() {} - `).toMatchInlineSnapshot(`export default function foo() {}`)); - -test('export default class', () => - expectJS(` - __webpack_require__.d(exports, "default", function() { return foo; }); - class foo {} - `).toMatchInlineSnapshot(`export default class foo {}`)); - -test('export default expression (webpack 4)', () => - expectJS(` - exports.default = 1; - `).toMatchInlineSnapshot(`export default 1;`)); - -test('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('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(exports, "...", function() { return foo; }); - var foo = 1; - `).toMatchInlineSnapshot(` - var foo = 1; - export { foo as "..." }; - `), -); - -test('re-export named', () => - expectJS(` - __webpack_require__.d(exports, "readFile", function() { return fs.readFile; }); - var fs = __webpack_require__("fs"); - `).toMatchInlineSnapshot(`export { readFile } from "fs";`)); - -test('re-export named, keep variable', () => - expectJS(` - __webpack_require__.d(exports, "readFile", function() { return fs.readFile; }); - var fs = __webpack_require__("fs"); - fs.writeFile(); - `).toMatchInlineSnapshot(` - export { readFile } from "fs"; - var fs = __webpack_require__("fs"); - fs.writeFile(); - `)); - -test('re-export multiple named', () => - expectJS(` - __webpack_require__.d(exports, { - readFile: () => fs.readFile, - writeFile: () => fs.writeFile, - }); - var fs = __webpack_require__("fs"); - `).toMatchInlineSnapshot(` - export { readFile } from "fs"; - export { writeFile } from "fs"; - var fs = __webpack_require__("fs"); - `)); - -test('re-export named as named', () => - expectJS(` - __webpack_require__.d(exports, "foo", function() { return fs.readFile; }); - var fs = __webpack_require__("fs"); - `).toMatchInlineSnapshot(`export { readFile as foo } from "fs";`)); - -test('re-export named as default', () => - expectJS(` - __webpack_require__.d(exports, "default", function() { return fs.readFile; }); - var fs = __webpack_require__("fs"); - `).toMatchInlineSnapshot(`export { readFile as default } from "fs";`)); - -test('re-export default as named', () => - expectJS(` - __webpack_require__.d(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(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 'fs'; - -test.todo('re-export all from commonjs (webpack 4)', () => - expectJS(` - var fs = __webpack_require__("fs"); - for (var importKey in fs) { - if (["default"].indexOf(importKey) < 0) { - (function (key) { - __webpack_require__.d(exports, key, function () { - return fs[key]; - }); - })(importKey); +describe('webpack 4', () => { + test('export default expression;', () => + expectJS(` + exports.default = 1; + `).toMatchInlineSnapshot(`export default 1;`)); + + test('export named', () => + expectJS(` + __webpack_require__.d(exports, "counter", function() { return foo; }); + var foo = 1; + `).toMatchInlineSnapshot(`export var counter = 1;`)); + + test('export default variable', () => + expectJS(` + __webpack_require__.d(exports, "default", function() { return foo; }); + var foo = 1; + `).toMatchInlineSnapshot(`export default 1;`)); + + test('export default variable with multiple references', () => + expectJS(` + __webpack_require__.d(exports, "default", function() { return foo; }); + var foo = 1; + console.log(foo); + `).toMatchInlineSnapshot(` + var foo = 1; + export { foo as default }; + console.log(foo); + `)); + + test('export default function', () => + expectJS(` + __webpack_require__.d(exports, "default", function() { return foo; }); + function foo() {} + `).toMatchInlineSnapshot(`export default function foo() {}`)); + + test('export default class', () => + expectJS(` + __webpack_require__.d(exports, "default", function() { return foo; }); + class foo {} + `).toMatchInlineSnapshot(`export default class foo {}`)); + + test('re-export named', () => + expectJS(` + __webpack_require__.d(exports, "readFile", function() { return lib.readFile; }); + var lib = __webpack_require__("lib"); + `).toMatchInlineSnapshot(` + export { readFile } from "lib"; + var lib = __webpack_require__("lib"); + `)); + + test('re-export named with multiple references', () => + expectJS(` + __webpack_require__.d(exports, "readFile", function() { return lib.readFile; }); + var lib = __webpack_require__("lib"); + lib.writeFile(); + `).toMatchInlineSnapshot(` + export { readFile } from "lib"; + var lib = __webpack_require__("lib"); + lib.writeFile(); + `)); + + test('re-export named as named', () => + expectJS(` + __webpack_require__.d(exports, "foo", function() { return lib.readFile; }); + var lib = __webpack_require__("lib"); + `).toMatchInlineSnapshot(` + export { readFile as foo } from "lib"; + var lib = __webpack_require__("lib"); + `)); + + test('re-export named as default', () => + expectJS(` + __webpack_require__.d(exports, "default", function() { return lib.readFile; }); + var lib = __webpack_require__("lib"); + `).toMatchInlineSnapshot(` + export { readFile as default } from "lib"; + var lib = __webpack_require__("lib"); + `)); + + test('re-export default as named', () => + expectJS(` + __webpack_require__.d(exports, "foo", function() { return lib.default; }); + var lib = __webpack_require__("lib"); + `).toMatchInlineSnapshot(` + export { default as foo } from "lib"; + var lib = __webpack_require__("lib"); + `)); + + test('re-export default as default', () => + expectJS(` + __webpack_require__.d(exports, "default", function() { return lib.default; }); + var lib = __webpack_require__("lib"); + `).toMatchInlineSnapshot(` + export { default } from "lib"; + var lib = __webpack_require__("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(exports, key, function () { + return lib[key]; + }); + })(importKey); + } } - } - `).toMatchInlineSnapshot(` - export * from "./fs"; - `), -); - -test.todo('re-export all as named', () => - expectJS(` - __webpack_require__.d(exports, "fs", function() { return fs; }); - var fs = __webpack_require__("fs"); - `).toMatchInlineSnapshot(`export * as fs from "fs";`), -); - -test.todo('re-export all as default', () => - expectJS(` - __webpack_require__.d(exports, "default", function() { return fs; }); - var fs = __webpack_require__("fs"); - `).toMatchInlineSnapshot(`export * as default from "fs";`), -); + `).toMatchInlineSnapshot(` + export * from "./lib"; + `), + ); + + test.todo('re-export all as named', () => + expectJS(` + __webpack_require__.d(exports, "lib", function() { return lib; }); + var lib = __webpack_require__("lib"); + `).toMatchInlineSnapshot(`export * as lib from "lib";`), + ); + + test.todo('re-export all as default', () => + expectJS(` + __webpack_require__.d(exports, "default", function() { return lib; }); + var lib = __webpack_require__("lib"); + `).toMatchInlineSnapshot(`export * as default from "lib";`), + ); +}); + +describe('webpack 5', () => { + test('export named', () => + expectJS(` + __webpack_require__.d(exports, { + counter: () => foo + }); + var foo = 1; + `).toMatchInlineSnapshot(` + export var counter = 1; + `)); + + test.todo('export same variable with multiple names', () => + expectJS(` + __webpack_require__.d(exports, { + counter: () => foo, + increment: () => foo, + }); + var foo = 1; + `).toMatchInlineSnapshot(` + export var counter = 1; + export { counter as increment }; + `), + ); + + test('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('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(exports, { + "...": () => foo + }); + var foo = 1; + `).toMatchInlineSnapshot(` + var foo = 1; + export { foo as "..." }; + `), + ); + + test.todo('re-export named merging', () => + expectJS(` + __webpack_require__.d(exports, { + readFile: () => lib.readFile, + writeFile: () => lib.writeFile, + }); + var lib = __webpack_require__("lib"); + `).toMatchInlineSnapshot(` + export { readFile, writeFile } from "lib"; + var lib = __webpack_require__("lib"); + `), + ); + + test.todo('re-export all from commonjs', () => + expectJS(` + var lib = 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(exports, reExportObject); + `).toMatchInlineSnapshot(` + export * from "./lib"; + `), + ); +}); diff --git a/packages/webcrack/src/unpack/webpack/module.ts b/packages/webcrack/src/unpack/webpack/module.ts index bfe876c7..e86eeeed 100644 --- a/packages/webcrack/src/unpack/webpack/module.ts +++ b/packages/webcrack/src/unpack/webpack/module.ts @@ -28,6 +28,8 @@ export class WebpackModule extends Module { applyTransform(file, varInjections); this.removeTrailingComments(); this.removeDefineESM(); + // FIXME: some bundles don't define __esModule but still declare esm exports + // https://github.com/0xdevalias/chatgpt-source-watch/blob/main/orig/_next/static/chunks/167-121de668c4456907.js if (this.#sourceType === 'esm') { this.convertExportsToESM(); } From a1698794f0ec47843673d341ca58906b38552f97 Mon Sep 17 00:00:00 2001 From: j4k0xb <55899582+j4k0xb@users.noreply.github.com> Date: Sat, 13 Jan 2024 03:20:26 +0100 Subject: [PATCH 07/81] perf: stop traversing once webpack chunk or module is found --- packages/webcrack/src/unpack/webpack/unpack-webpack-4.ts | 1 + packages/webcrack/src/unpack/webpack/unpack-webpack-chunk.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/webcrack/src/unpack/webpack/unpack-webpack-4.ts b/packages/webcrack/src/unpack/webpack/unpack-webpack-4.ts index 264f5b4e..b7516570 100644 --- a/packages/webcrack/src/unpack/webpack/unpack-webpack-4.ts +++ b/packages/webcrack/src/unpack/webpack/unpack-webpack-4.ts @@ -46,6 +46,7 @@ export default { return { CallExpression(path) { if (!matcher.match(path.node)) return; + path.stop(); const webpackRequireBinding = path .get('callee') diff --git a/packages/webcrack/src/unpack/webpack/unpack-webpack-chunk.ts b/packages/webcrack/src/unpack/webpack/unpack-webpack-chunk.ts index 9dfa8cfe..934e6bd5 100644 --- a/packages/webcrack/src/unpack/webpack/unpack-webpack-chunk.ts +++ b/packages/webcrack/src/unpack/webpack/unpack-webpack-chunk.ts @@ -59,6 +59,7 @@ export default { return { CallExpression(path) { if (!matcher.match(path.node)) return; + path.stop(); const modules = new Map(); From 463f47c5bfed1556db60ece4f810ca97b217257a Mon Sep 17 00:00:00 2001 From: j4k0xb <55899582+j4k0xb@users.noreply.github.com> Date: Sat, 13 Jan 2024 03:20:26 +0100 Subject: [PATCH 08/81] perf: skip traversing in var-injections --- .../webcrack/src/unpack/test/var-injections.test.ts | 11 +++++++++++ .../webcrack/src/unpack/webpack/var-injections.ts | 4 +++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/webcrack/src/unpack/test/var-injections.test.ts b/packages/webcrack/src/unpack/test/var-injections.test.ts index 6f0a6503..500800c3 100644 --- a/packages/webcrack/src/unpack/test/var-injections.test.ts +++ b/packages/webcrack/src/unpack/test/var-injections.test.ts @@ -14,3 +14,14 @@ test('replace', () => var n = __webpack_require__(2); console.log(m, n); `)); + +test('ignore different number of params and args', () => + expectJS(` + (function (m, n) { + console.log(m, n); + }.call(this, foo)); + `).toMatchInlineSnapshot(` + (function (m, n) { + console.log(m, n); + }).call(this, foo); + `)); diff --git a/packages/webcrack/src/unpack/webpack/var-injections.ts b/packages/webcrack/src/unpack/webpack/var-injections.ts index 0b9dee33..008f1750 100644 --- a/packages/webcrack/src/unpack/webpack/var-injections.ts +++ b/packages/webcrack/src/unpack/webpack/var-injections.ts @@ -32,7 +32,9 @@ export default { return { ExpressionStatement(path) { - if (!path.parentPath.isProgram() || !matcher.match(path.node)) return; + if (!path.parentPath.isProgram()) path.skip(); + if (!matcher.match(path.node)) return; + if (params.current!.length !== args.current!.length - 1) return; const variables = params.current!.map((param, i) => t.variableDeclaration('var', [ From 6d9cbe045050d9b38fb5537609b49156a9d26115 Mon Sep 17 00:00:00 2001 From: j4k0xb <55899582+j4k0xb@users.noreply.github.com> Date: Sat, 13 Jan 2024 03:20:26 +0100 Subject: [PATCH 09/81] feat: export analysis --- .../src/unpack/webpack/define-exports.ts | 138 +-------- .../unpack/webpack/import-export-manager.ts | 286 ++++++++++++++++++ .../webcrack/src/unpack/webpack/module.ts | 38 ++- 3 files changed, 318 insertions(+), 144 deletions(-) create mode 100644 packages/webcrack/src/unpack/webpack/import-export-manager.ts diff --git a/packages/webcrack/src/unpack/webpack/define-exports.ts b/packages/webcrack/src/unpack/webpack/define-exports.ts index 25848e90..34b0097a 100644 --- a/packages/webcrack/src/unpack/webpack/define-exports.ts +++ b/packages/webcrack/src/unpack/webpack/define-exports.ts @@ -1,13 +1,8 @@ -import { statement } from '@babel/template'; -import { Binding, NodePath } from '@babel/traverse'; import * as t from '@babel/types'; import * as m from '@codemod/matchers'; -import { - Transform, - constMemberExpression, - findPath, - renameFast, -} from '../../ast-utils'; +import assert from 'assert'; +import { Transform, constMemberExpression } from '../../ast-utils'; +import { ImportExportManager } from './import-export-manager'; // TODO: hoist re-exports to the top of the file (but retain order relative to imports) // TODO: merge re-exports @@ -28,7 +23,9 @@ export default { name: 'define-exports', tags: ['unsafe'], scope: true, - visitor() { + visitor(manager) { + assert(manager); + const exportName = m.capture(m.anyString()); const returnValue = m.capture(m.anyExpression()); const getter = m.or( @@ -71,7 +68,7 @@ export default { if (!path.parentPath.isProgram()) return path.skip(); if (singleExport.match(path.node)) { - addExport(path, exportName.current!, returnValue.current!); + // manager.addExport(path, exportName.current!, returnValue.current!); path.remove(); this.changes++; } else if (defaultExpressionExport.match(path.node)) { @@ -80,7 +77,7 @@ export default { } else if (multiExport.match(path.node)) { for (const property of properties.current!) { objectProperty.match(property); // To easily get the captures per property - addExport(path, exportName.current!, returnValue.current!); + // manager.addExport(path, exportName.current!, returnValue.current!); } path.remove(); this.changes++; @@ -88,121 +85,4 @@ export default { }, }; }, -} satisfies Transform; - -function addExport(path: NodePath, exportName: string, value: t.Expression) { - const object = m.capture(m.identifier()); - const property = m.capture(m.identifier()); - const memberValue = m.memberExpression(object, property); - - if (t.isIdentifier(value)) { - const binding = path.scope.getBinding(value.name); - if (!binding) return; - - if (exportName === 'default') { - exportDefault(binding); - } else { - exportNamed(binding, exportName); - } - } else if (memberValue.match(value)) { - const binding = path.scope.getBinding(object.current!.name); - if (!binding) return; - - const localName = property.current!.name; - reExportNamed(binding, exportName, localName); - } -} - -/** - * ```diff - * - __webpack_require__.d(exports, 'counter', () => local); - * - let local = 1; - * + export let counter = 1; - * ``` - */ -function exportNamed(binding: Binding, exportName: string): void { - const declaration = findPath( - binding.path, - m.or( - m.variableDeclaration(), - m.classDeclaration(), - m.functionDeclaration(), - ), - ); - if (!declaration) return; - - renameFast(binding, exportName); - declaration.replaceWith(t.exportNamedDeclaration(declaration.node)); -} - -/** - * ```diff - * - __webpack_require__.d(exports, 'default', () => local); - * - let local = 1; - * + export default 1; - * ``` - */ -function exportDefault(binding: Binding) { - const declaration = findPath( - binding.path, - m.or( - m.variableDeclaration(), - m.functionDeclaration(), - m.classDeclaration(), - ), - ); - if (!declaration) return; - - if (binding.references > 1) { - declaration.insertAfter( - statement`export { ${binding.identifier} as default };`(), - ); - } else { - declaration.replaceWith( - t.exportDefaultDeclaration( - t.isVariableDeclaration(declaration.node) - ? declaration.node.declarations[0].init! - : declaration.node, - ), - ); - } -} - -/** - * ```diff - * - __webpack_require__.d(exports, 'readFile', () => fs.readFile); - * - var fs = __webpack_require__('fs'); - * + export { readFile } from 'fs'; - * ``` - * alias: - * ```diff - * - __webpack_require__.d(exports, 'foo', () => fs.readFile); - * - var fs = __webpack_require__('fs'); - * + export { readFile as foo } from 'fs'; - * ``` - */ -function reExportNamed( - binding: Binding, - exportName: string, - localName: string, -) { - const moduleId = m.capture(m.or(m.numericLiteral(), m.stringLiteral())); - const variableMatcher = m.variableDeclaration(undefined, [ - m.variableDeclarator( - m.identifier(binding.identifier.name), - m.callExpression(m.identifier('__webpack_require__'), [moduleId]), - ), - ]); - - if (variableMatcher.match(binding.path.parent)) { - const modulePath = String(moduleId.current!.value); - // FIXME: does this mess up the imports references in webpack/module.ts? - binding.path.parentPath!.insertBefore( - statement`export { ${localName} as ${exportName} } from "${modulePath}";`(), - ); - // FIXME: only remove at the end to account for multiple re-exports/references - if (binding.references === 1) { - binding.path.parentPath!.remove(); - } - } -} +} satisfies Transform; diff --git a/packages/webcrack/src/unpack/webpack/import-export-manager.ts b/packages/webcrack/src/unpack/webpack/import-export-manager.ts new file mode 100644 index 00000000..51d45050 --- /dev/null +++ b/packages/webcrack/src/unpack/webpack/import-export-manager.ts @@ -0,0 +1,286 @@ +import traverse, { Binding, NodePath, Scope } from '@babel/traverse'; +import * as t from '@babel/types'; +import * as m from '@codemod/matchers'; +import assert from 'assert'; +import { constMemberExpression, findPath, renameFast } from '../../ast-utils'; + +/** + * Example: `__webpack_require__(id)` + */ +interface RequireCall { + moduleId: string; + path: NodePath; +} + +type RequireVarBinding = Binding & { path: NodePath }; + +/** + * Example: `var foo = __webpack_require__(id);` + */ +interface RequireVar { + name: string; + moduleId: string; + binding: RequireVarBinding; +} + +type ExportDefinition = + | { + /** + * Example: + * ```js + * __webpack_require__.d(exports, { counter: () => foo }); + * var foo = 1; + * ``` + */ + kind: 'local-var'; + exportName: string; + binding: Binding; + } + | { + /** + * Example: + * ```js + * __webpack_require__.d(exports, { readFile: () => lib.readFile }); + * var lib = __webpack_require__("lib"); + * ``` + */ + kind: 're-export-named'; + exportName: string; + localName: string; + moduleId: string; + binding: RequireVarBinding; + } + | { + /** + * Example: `export * as lib from 'lib';` + * ```js + * __webpack_require__.d(exports, { lib: () => lib }); + * var lib = __webpack_require__("lib"); + * ``` + */ + kind: 're-export-all-named'; + exportName: string; + moduleId: string; + binding: RequireVarBinding; + }; + +export class ImportExportManager { + private ast: t.File; + private requireBinding: Binding | undefined; + /** + * All `var foo = __webpack_require__(id);` statements + */ + private requireVars: RequireVar[] = []; + /** + * All `__webpack_require__(id)` calls + */ + private requireCalls: RequireCall[] = []; + private exportDefinitions: ExportDefinition[] = []; + + constructor(ast: t.File, webpackRequireBinding: Binding | undefined) { + this.ast = ast; + this.requireBinding = webpackRequireBinding; + this.collectRequireCalls(); + this.collectExportDefinitions(); + this.createExportStatements(); + this.removeUnusedVarRequires(); + } + + createExportStatements() { + const mergedReExports = new Map(); + + for (const definition of this.exportDefinitions) { + if (definition.kind === 'local-var') { + const declaration = findPath( + definition.binding.path, + m.or( + m.variableDeclaration(), + m.classDeclaration(), + m.functionDeclaration(), + ), + ); + if (!declaration) continue; + + renameFast(definition.binding, definition.exportName); + declaration.replaceWith(t.exportNamedDeclaration(declaration.node)); + } else if (definition.kind === 're-export-named') { + const existingExport = mergedReExports.get(definition.moduleId); + const specifier = t.exportSpecifier( + t.identifier(definition.localName), + t.identifier(definition.exportName), + ); + if (existingExport) { + existingExport.specifiers.push(specifier); + } else { + // TODO: resolve to file path + const exportDeclaration = t.exportNamedDeclaration( + undefined, + [specifier], + t.stringLiteral(definition.moduleId), + ); + definition.binding.path.parentPath.insertAfter(exportDeclaration); + mergedReExports.set(definition.moduleId, exportDeclaration); + } + } else if (definition.kind === 're-export-all-named') { + // TODO: resolve to file path + definition.binding.path.parentPath.insertAfter( + t.exportAllDeclaration(t.stringLiteral(definition.moduleId)), + ); + } + } + console.log(this.exportDefinitions); + } + + addExportDefinition(scope: Scope, exportName: string, value: t.Expression) { + const object = m.capture(m.identifier()); + const property = m.capture(m.identifier()); + const memberExpressionMatcher = m.memberExpression(object, property); + + const findRequireVar = (binding: Binding) => + this.requireVars.find((v) => v.binding.path.node === binding.path.node); + + if (t.isIdentifier(value)) { + const binding = scope.getBinding(value.name); + if (!binding) return; + const requireVar = findRequireVar(binding); + + if (requireVar) { + this.exportDefinitions.push({ + kind: 're-export-all-named', + exportName, + moduleId: requireVar.moduleId, + binding: requireVar.binding, + }); + } else { + this.exportDefinitions.push({ + kind: 'local-var', + exportName, + binding, + }); + } + } else if (memberExpressionMatcher.match(value)) { + const binding = scope.getBinding(object.current!.name); + if (!binding) return; + const requireVar = findRequireVar(binding); + if (!requireVar) return; + + this.exportDefinitions.push({ + kind: 're-export-named', + exportName, + localName: property.current!.name, + moduleId: requireVar.moduleId, + binding: requireVar.binding, + }); + } else { + // FIXME: implement, can this even happen? + assert(false, 'Unexpected export value'); + } + } + + removeUnusedVarRequires() { + // for (const v of this.requireVars) { + + // } + } + + /** + * Finds all `__webpack_require__(id)` and `var foo = __webpack_require__(id);` calls + */ + collectRequireCalls() { + const idArg = m.capture(m.or(m.numericLiteral(), m.stringLiteral())); + const requireCall = m.callExpression(m.identifier('__webpack_require__'), [ + idArg, + ]); + + const varName = m.capture(m.anyString()); + const requireVar = m.variableDeclaration(undefined, [ + m.variableDeclarator(m.identifier(varName), requireCall), + ]); + + this.requireBinding?.referencePaths.forEach((path) => { + m.matchPath(requireCall, { idArg }, path.parentPath!, ({ idArg }) => { + this.requireCalls.push({ + moduleId: idArg.node.value.toString(), + path: path.parentPath as NodePath, + }); + if (requireVar.match(path.parentPath!.parentPath?.parent)) { + const binding = path.scope.getBinding(varName.current!)!; + this.requireVars.push({ + moduleId: idArg.node.value.toString(), + name: varName.current!, + binding: binding as RequireVarBinding, + }); + } + }); + }); + } + + /** + * Extract the export information from all `__webpack_require__.d` calls + */ + collectExportDefinitions() { + const exportName = m.capture(m.anyString()); + const returnValue = m.capture(m.anyExpression()); + const getter = m.or( + m.functionExpression( + undefined, + [], + m.blockStatement([m.returnStatement(returnValue)]), + ), + m.arrowFunctionExpression([], returnValue), + ); + // Example (webpack v4): __webpack_require__.d(exports, 'counter', function () { return local }); + const singleExport = m.expressionStatement( + m.callExpression(constMemberExpression('__webpack_require__', 'd'), [ + m.identifier(), + m.stringLiteral(exportName), + getter, + ]), + ); + + const defaultExpressionExport = m.expressionStatement( + m.assignmentExpression( + '=', + constMemberExpression('exports', 'default'), + returnValue, + ), + ); + + const objectProperty = m.objectProperty(m.identifier(exportName), getter); + const properties = m.capture(m.arrayOf(objectProperty)); + // Example (webpack v5): __webpack_require__.d(exports, { a: () => b, c: () => d }); + const multiExport = m.expressionStatement( + m.callExpression(constMemberExpression('__webpack_require__', 'd'), [ + m.identifier(), + m.objectExpression(properties), + ]), + ); + + traverse(this.ast, { + ExpressionStatement: (path) => { + if (!path.parentPath.isProgram()) return path.skip(); + + if (singleExport.match(path.node)) { + this.addExportDefinition( + path.scope, + exportName.current!, + returnValue.current!, + ); + } else if (defaultExpressionExport.match(path.node)) { + // TODO: handle + // path.replaceWith(t.exportDefaultDeclaration(returnValue.current!)); + } else if (multiExport.match(path.node)) { + for (const property of properties.current!) { + objectProperty.match(property); // To easily get the captures per property + this.addExportDefinition( + path.scope, + exportName.current!, + returnValue.current!, + ); + } + } + path.remove(); + }, + }); + } +} diff --git a/packages/webcrack/src/unpack/webpack/module.ts b/packages/webcrack/src/unpack/webpack/module.ts index e86eeeed..74fec75b 100644 --- a/packages/webcrack/src/unpack/webpack/module.ts +++ b/packages/webcrack/src/unpack/webpack/module.ts @@ -6,15 +6,15 @@ import { constMemberExpression, renameParameters, } from '../../ast-utils'; -import { Import, Module } from '../module'; +import { Module } from '../module'; import { FunctionPath } from './common-matchers'; import defineExport from './define-exports'; +import { ImportExportManager } from './import-export-manager'; import varInjections from './var-injections'; export class WebpackModule extends Module { #webpackRequireBinding: Binding | undefined; - // TODO: expose to public API - #imports: Import[] = []; + #importExportManager: ImportExportManager; // TODO: expose to public API #sourceType: 'commonjs' | 'esm' = 'commonjs'; @@ -24,15 +24,21 @@ export class WebpackModule extends Module { super(id, file, isEntry); renameParameters(ast, ['module', 'exports', '__webpack_require__']); - this.#webpackRequireBinding = ast.scope.getBinding('__webpack_require__'); applyTransform(file, varInjections); this.removeTrailingComments(); - this.removeDefineESM(); - // FIXME: some bundles don't define __esModule but still declare esm exports - // https://github.com/0xdevalias/chatgpt-source-watch/blob/main/orig/_next/static/chunks/167-121de668c4456907.js - if (this.#sourceType === 'esm') { - this.convertExportsToESM(); - } + + this.#webpackRequireBinding = ast.scope.getBinding('__webpack_require__'); + this.#importExportManager = new ImportExportManager( + file, + this.#webpackRequireBinding, + ); + + // this.removeDefineESM(); + // // FIXME: some bundles don't define __esModule but still declare esm exports + // // https://github.com/0xdevalias/chatgpt-source-watch/blob/main/orig/_next/static/chunks/167-121de668c4456907.js + // if (this.#sourceType === 'esm') { + // this.convertExportsToESM(); + // } } /** @@ -85,6 +91,7 @@ export class WebpackModule extends Module { } /** + * // TODO: refactor to use ImportExportManager * ```diff * - __webpack_require__(id) * + require("./relative/path.js") @@ -95,6 +102,7 @@ export class WebpackModule extends Module { onResolve: (id: string) => { path: string; external?: boolean }, ): void { if (!this.#webpackRequireBinding) return; + return; const idArg = m.capture(m.or(m.numericLiteral(), m.stringLiteral())); const requireCall = m.callExpression(m.identifier('__webpack_require__'), [ @@ -112,11 +120,11 @@ export class WebpackModule extends Module { idArg.addComment('leading', 'webcrack:missing'); } - this.#imports.push({ - id, - path: result.path, - nodePath: path.parentPath as NodePath, - }); + // this.#imports.push({ + // id, + // path: result.path, + // nodePath: path.parentPath as NodePath, + // }); }); }); } From 9a773f986f38d283778741696c027498e0f753e4 Mon Sep 17 00:00:00 2001 From: j4k0xb <55899582+j4k0xb@users.noreply.github.com> Date: Sat, 13 Jan 2024 03:20:26 +0100 Subject: [PATCH 10/81] refactor: import export manager --- .../unpack/webpack/import-export-manager.ts | 245 ++++++++---------- 1 file changed, 110 insertions(+), 135 deletions(-) diff --git a/packages/webcrack/src/unpack/webpack/import-export-manager.ts b/packages/webcrack/src/unpack/webpack/import-export-manager.ts index 51d45050..5ffe6286 100644 --- a/packages/webcrack/src/unpack/webpack/import-export-manager.ts +++ b/packages/webcrack/src/unpack/webpack/import-export-manager.ts @@ -1,7 +1,6 @@ import traverse, { Binding, NodePath, Scope } from '@babel/traverse'; import * as t from '@babel/types'; import * as m from '@codemod/matchers'; -import assert from 'assert'; import { constMemberExpression, findPath, renameFast } from '../../ast-utils'; /** @@ -12,129 +11,45 @@ interface RequireCall { path: NodePath; } -type RequireVarBinding = Binding & { path: NodePath }; - /** * Example: `var foo = __webpack_require__(id);` */ interface RequireVar { name: string; moduleId: string; - binding: RequireVarBinding; + binding: Binding; } -type ExportDefinition = - | { - /** - * Example: - * ```js - * __webpack_require__.d(exports, { counter: () => foo }); - * var foo = 1; - * ``` - */ - kind: 'local-var'; - exportName: string; - binding: Binding; - } - | { - /** - * Example: - * ```js - * __webpack_require__.d(exports, { readFile: () => lib.readFile }); - * var lib = __webpack_require__("lib"); - * ``` - */ - kind: 're-export-named'; - exportName: string; - localName: string; - moduleId: string; - binding: RequireVarBinding; - } - | { - /** - * Example: `export * as lib from 'lib';` - * ```js - * __webpack_require__.d(exports, { lib: () => lib }); - * var lib = __webpack_require__("lib"); - * ``` - */ - kind: 're-export-all-named'; - exportName: string; - moduleId: string; - binding: RequireVarBinding; - }; - export class ImportExportManager { private ast: t.File; - private requireBinding: Binding | undefined; + private webpackRequire: Binding | undefined; /** - * All `var foo = __webpack_require__(id);` statements + * All `var foo = __webpack_require__(id);` variable declarations */ private requireVars: RequireVar[] = []; /** * All `__webpack_require__(id)` calls */ private requireCalls: RequireCall[] = []; - private exportDefinitions: ExportDefinition[] = []; + /** + * Used for merging multiple re-exports of a module + */ + private reExportCache = new Map(); constructor(ast: t.File, webpackRequireBinding: Binding | undefined) { this.ast = ast; - this.requireBinding = webpackRequireBinding; + this.webpackRequire = webpackRequireBinding; this.collectRequireCalls(); this.collectExportDefinitions(); - this.createExportStatements(); - this.removeUnusedVarRequires(); } - createExportStatements() { - const mergedReExports = new Map(); - - for (const definition of this.exportDefinitions) { - if (definition.kind === 'local-var') { - const declaration = findPath( - definition.binding.path, - m.or( - m.variableDeclaration(), - m.classDeclaration(), - m.functionDeclaration(), - ), - ); - if (!declaration) continue; - - renameFast(definition.binding, definition.exportName); - declaration.replaceWith(t.exportNamedDeclaration(declaration.node)); - } else if (definition.kind === 're-export-named') { - const existingExport = mergedReExports.get(definition.moduleId); - const specifier = t.exportSpecifier( - t.identifier(definition.localName), - t.identifier(definition.exportName), - ); - if (existingExport) { - existingExport.specifiers.push(specifier); - } else { - // TODO: resolve to file path - const exportDeclaration = t.exportNamedDeclaration( - undefined, - [specifier], - t.stringLiteral(definition.moduleId), - ); - definition.binding.path.parentPath.insertAfter(exportDeclaration); - mergedReExports.set(definition.moduleId, exportDeclaration); - } - } else if (definition.kind === 're-export-all-named') { - // TODO: resolve to file path - definition.binding.path.parentPath.insertAfter( - t.exportAllDeclaration(t.stringLiteral(definition.moduleId)), - ); - } - } - console.log(this.exportDefinitions); - } - - addExportDefinition(scope: Scope, exportName: string, value: t.Expression) { - const object = m.capture(m.identifier()); - const property = m.capture(m.identifier()); - const memberExpressionMatcher = m.memberExpression(object, property); + addExport(scope: Scope, exportName: string, value: t.Expression) { + const objectName = m.capture(m.anyString()); + const propertyName = m.capture(m.anyString()); + const memberExpressionMatcher = m.memberExpression( + m.identifier(objectName), + m.identifier(propertyName), + ); const findRequireVar = (binding: Binding) => this.requireVars.find((v) => v.binding.path.node === binding.path.node); @@ -145,48 +60,111 @@ export class ImportExportManager { const requireVar = findRequireVar(binding); if (requireVar) { - this.exportDefinitions.push({ - kind: 're-export-all-named', - exportName, - moduleId: requireVar.moduleId, - binding: requireVar.binding, - }); + this.reExportAll(binding, requireVar.moduleId); } else { - this.exportDefinitions.push({ - kind: 'local-var', - exportName, - binding, - }); + this.exportLocalVar(binding, exportName); } } else if (memberExpressionMatcher.match(value)) { - const binding = scope.getBinding(object.current!.name); + const binding = scope.getBinding(objectName.current!); if (!binding) return; const requireVar = findRequireVar(binding); if (!requireVar) return; - this.exportDefinitions.push({ - kind: 're-export-named', + this.reExportNamed( + requireVar.binding, + requireVar.moduleId, + propertyName.current!, exportName, - localName: property.current!.name, - moduleId: requireVar.moduleId, - binding: requireVar.binding, - }); + ); } else { - // FIXME: implement, can this even happen? - assert(false, 'Unexpected export value'); + throw new Error(`Unexpected export: ${value.type}`); } } - removeUnusedVarRequires() { - // for (const v of this.requireVars) { - - // } + /** + * Example: + * ```js + * __webpack_require__.d(exports, { foo: () => lib.bar }); + * var lib = __webpack_require__("lib"); + * ``` + * to + * ```js + * export { bar as foo } from 'lib'; + * ``` + */ + private reExportNamed( + binding: Binding, + moduleId: string, + localName: string, + exportName: string, + ) { + const existingExport = this.reExportCache.get(moduleId); + const specifier = t.exportSpecifier( + t.identifier(localName), + t.identifier(exportName), + ); + if (existingExport) { + existingExport.specifiers.push(specifier); + } else { + // TODO: resolve to file path + const exportDeclaration = t.exportNamedDeclaration( + undefined, + [specifier], + t.stringLiteral(moduleId), + ); + binding.path.parentPath?.insertAfter(exportDeclaration); + this.reExportCache.set(moduleId, exportDeclaration); + } + } + + /** + * Example: + * ```js + * __webpack_require__.d(exports, { counter: () => foo }); + * var foo = 1; + * ``` + * to + * ```js + * export var counter = 1; + * ``` + */ + private exportLocalVar(binding: Binding, exportName: string) { + const declaration = findPath( + binding.path, + m.or( + m.variableDeclaration(), + m.classDeclaration(), + m.functionDeclaration(), + ), + ); + if (!declaration) return; + // FIXME: check if its already exported and add `export { a as b }` instead. + renameFast(binding, exportName); + declaration.replaceWith(t.exportNamedDeclaration(declaration.node)); + } + + /** + * Example: + * ```js + * __webpack_require__.d(exports, { foo: () => lib }); + * var lib = __webpack_require__("lib"); + * ``` + * to + * ```js + * export * as foo from 'lib'; + * ``` + */ + private reExportAll(binding: Binding, moduleId: string) { + // TODO: resolve to file path + binding.path.parentPath?.insertAfter( + t.exportAllDeclaration(t.stringLiteral(moduleId)), + ); } /** * Finds all `__webpack_require__(id)` and `var foo = __webpack_require__(id);` calls */ - collectRequireCalls() { + private collectRequireCalls() { const idArg = m.capture(m.or(m.numericLiteral(), m.stringLiteral())); const requireCall = m.callExpression(m.identifier('__webpack_require__'), [ idArg, @@ -197,7 +175,7 @@ export class ImportExportManager { m.variableDeclarator(m.identifier(varName), requireCall), ]); - this.requireBinding?.referencePaths.forEach((path) => { + this.webpackRequire?.referencePaths.forEach((path) => { m.matchPath(requireCall, { idArg }, path.parentPath!, ({ idArg }) => { this.requireCalls.push({ moduleId: idArg.node.value.toString(), @@ -208,7 +186,7 @@ export class ImportExportManager { this.requireVars.push({ moduleId: idArg.node.value.toString(), name: varName.current!, - binding: binding as RequireVarBinding, + binding, }); } }); @@ -218,7 +196,7 @@ export class ImportExportManager { /** * Extract the export information from all `__webpack_require__.d` calls */ - collectExportDefinitions() { + private collectExportDefinitions() { const exportName = m.capture(m.anyString()); const returnValue = m.capture(m.anyExpression()); const getter = m.or( @@ -261,25 +239,22 @@ export class ImportExportManager { if (!path.parentPath.isProgram()) return path.skip(); if (singleExport.match(path.node)) { - this.addExportDefinition( - path.scope, - exportName.current!, - returnValue.current!, - ); + this.addExport(path.scope, exportName.current!, returnValue.current!); + path.remove(); } else if (defaultExpressionExport.match(path.node)) { // TODO: handle // path.replaceWith(t.exportDefaultDeclaration(returnValue.current!)); } else if (multiExport.match(path.node)) { for (const property of properties.current!) { objectProperty.match(property); // To easily get the captures per property - this.addExportDefinition( + this.addExport( path.scope, exportName.current!, returnValue.current!, ); } + path.remove(); } - path.remove(); }, }); } From ecc0a1f5f5e62cd0f6f93097317a66b5bdbf9918 Mon Sep 17 00:00:00 2001 From: j4k0xb <55899582+j4k0xb@users.noreply.github.com> Date: Sat, 13 Jan 2024 03:20:26 +0100 Subject: [PATCH 11/81] fix: export already exported variable --- .../unpack/webpack/import-export-manager.ts | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/packages/webcrack/src/unpack/webpack/import-export-manager.ts b/packages/webcrack/src/unpack/webpack/import-export-manager.ts index 5ffe6286..aeda008c 100644 --- a/packages/webcrack/src/unpack/webpack/import-export-manager.ts +++ b/packages/webcrack/src/unpack/webpack/import-export-manager.ts @@ -135,12 +135,24 @@ export class ImportExportManager { m.variableDeclaration(), m.classDeclaration(), m.functionDeclaration(), + m.exportNamedDeclaration(), ), ); if (!declaration) return; - // FIXME: check if its already exported and add `export { a as b }` instead. - renameFast(binding, exportName); - declaration.replaceWith(t.exportNamedDeclaration(declaration.node)); + + if (declaration.type === 'ExportNamedDeclaration') { + declaration.insertAfter( + t.exportNamedDeclaration(null, [ + t.exportSpecifier( + t.identifier(binding.identifier.name), + t.identifier(exportName), + ), + ]), + ); + } else { + renameFast(binding, exportName); + declaration.replaceWith(t.exportNamedDeclaration(declaration.node)); + } } /** From 639d6336f5c74a251a207766b93639f21a446f04 Mon Sep 17 00:00:00 2001 From: j4k0xb <55899582+j4k0xb@users.noreply.github.com> Date: Sat, 13 Jan 2024 03:20:26 +0100 Subject: [PATCH 12/81] feat: default export --- .../src/unpack/webpack/import-export-manager.ts | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/packages/webcrack/src/unpack/webpack/import-export-manager.ts b/packages/webcrack/src/unpack/webpack/import-export-manager.ts index aeda008c..7207ea25 100644 --- a/packages/webcrack/src/unpack/webpack/import-export-manager.ts +++ b/packages/webcrack/src/unpack/webpack/import-export-manager.ts @@ -40,7 +40,7 @@ export class ImportExportManager { this.ast = ast; this.webpackRequire = webpackRequireBinding; this.collectRequireCalls(); - this.collectExportDefinitions(); + this.transformExports(); } addExport(scope: Scope, exportName: string, value: t.Expression) { @@ -149,6 +149,11 @@ export class ImportExportManager { ), ]), ); + } else if (exportName === 'default') { + const value = t.isVariableDeclaration(declaration.node) + ? declaration.node.declarations[0].init! + : (declaration.node as t.ClassDeclaration | t.FunctionDeclaration); + declaration.replaceWith(t.exportDefaultDeclaration(value)); } else { renameFast(binding, exportName); declaration.replaceWith(t.exportNamedDeclaration(declaration.node)); @@ -208,7 +213,7 @@ export class ImportExportManager { /** * Extract the export information from all `__webpack_require__.d` calls */ - private collectExportDefinitions() { + private transformExports() { const exportName = m.capture(m.anyString()); const returnValue = m.capture(m.anyExpression()); const getter = m.or( @@ -228,7 +233,8 @@ export class ImportExportManager { ]), ); - const defaultExpressionExport = m.expressionStatement( + // Example (webpack v4): exports.default = 1; + const defaultExportAssignment = m.expressionStatement( m.assignmentExpression( '=', constMemberExpression('exports', 'default'), @@ -253,9 +259,8 @@ export class ImportExportManager { if (singleExport.match(path.node)) { this.addExport(path.scope, exportName.current!, returnValue.current!); path.remove(); - } else if (defaultExpressionExport.match(path.node)) { - // TODO: handle - // path.replaceWith(t.exportDefaultDeclaration(returnValue.current!)); + } else if (defaultExportAssignment.match(path.node)) { + path.replaceWith(t.exportDefaultDeclaration(returnValue.current!)); } else if (multiExport.match(path.node)) { for (const property of properties.current!) { objectProperty.match(property); // To easily get the captures per property From 18667e8723ffb8c40193a0a69dd583fabd8e97f2 Mon Sep 17 00:00:00 2001 From: j4k0xb <55899582+j4k0xb@users.noreply.github.com> Date: Sat, 13 Jan 2024 03:20:26 +0100 Subject: [PATCH 13/81] feat: track exports --- .../unpack/webpack/import-export-manager.ts | 45 +++++++++++-------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/packages/webcrack/src/unpack/webpack/import-export-manager.ts b/packages/webcrack/src/unpack/webpack/import-export-manager.ts index 7207ea25..23e99ae4 100644 --- a/packages/webcrack/src/unpack/webpack/import-export-manager.ts +++ b/packages/webcrack/src/unpack/webpack/import-export-manager.ts @@ -21,6 +21,8 @@ interface RequireVar { } export class ImportExportManager { + exports: t.ExportDeclaration[] = []; + private ast: t.File; private webpackRequire: Binding | undefined; /** @@ -60,9 +62,9 @@ export class ImportExportManager { const requireVar = findRequireVar(binding); if (requireVar) { - this.reExportAll(binding, requireVar.moduleId); + this.addExportAll(binding, requireVar.moduleId); } else { - this.exportLocalVar(binding, exportName); + this.addExportDeclaration(binding, exportName); } } else if (memberExpressionMatcher.match(value)) { const binding = scope.getBinding(objectName.current!); @@ -70,7 +72,7 @@ export class ImportExportManager { const requireVar = findRequireVar(binding); if (!requireVar) return; - this.reExportNamed( + this.addExportFrom( requireVar.binding, requireVar.moduleId, propertyName.current!, @@ -92,7 +94,7 @@ export class ImportExportManager { * export { bar as foo } from 'lib'; * ``` */ - private reExportNamed( + private addExportFrom( binding: Binding, moduleId: string, localName: string, @@ -112,6 +114,7 @@ export class ImportExportManager { [specifier], t.stringLiteral(moduleId), ); + this.exports.push(exportDeclaration); binding.path.parentPath?.insertAfter(exportDeclaration); this.reExportCache.set(moduleId, exportDeclaration); } @@ -128,7 +131,7 @@ export class ImportExportManager { * export var counter = 1; * ``` */ - private exportLocalVar(binding: Binding, exportName: string) { + private addExportDeclaration(binding: Binding, exportName: string) { const declaration = findPath( binding.path, m.or( @@ -141,22 +144,26 @@ export class ImportExportManager { if (!declaration) return; if (declaration.type === 'ExportNamedDeclaration') { - declaration.insertAfter( - t.exportNamedDeclaration(null, [ - t.exportSpecifier( - t.identifier(binding.identifier.name), - t.identifier(exportName), - ), - ]), - ); + const namedExport = t.exportNamedDeclaration(null, [ + t.exportSpecifier( + t.identifier(binding.identifier.name), + t.identifier(exportName), + ), + ]); + this.exports.push(namedExport); + declaration.insertAfter(namedExport); } else if (exportName === 'default') { const value = t.isVariableDeclaration(declaration.node) ? declaration.node.declarations[0].init! : (declaration.node as t.ClassDeclaration | t.FunctionDeclaration); - declaration.replaceWith(t.exportDefaultDeclaration(value)); + const defaultExport = t.exportDefaultDeclaration(value); + this.exports.push(defaultExport); + declaration.replaceWith(defaultExport); } else { renameFast(binding, exportName); - declaration.replaceWith(t.exportNamedDeclaration(declaration.node)); + const namedExport = t.exportNamedDeclaration(declaration.node); + this.exports.push(namedExport); + declaration.replaceWith(namedExport); } } @@ -171,11 +178,11 @@ export class ImportExportManager { * export * as foo from 'lib'; * ``` */ - private reExportAll(binding: Binding, moduleId: string) { + private addExportAll(binding: Binding, moduleId: string) { // TODO: resolve to file path - binding.path.parentPath?.insertAfter( - t.exportAllDeclaration(t.stringLiteral(moduleId)), - ); + const exportAll = t.exportAllDeclaration(t.stringLiteral(moduleId)); + this.exports.push(exportAll); + binding.path.parentPath?.insertAfter(exportAll); } /** From d79e94bef57e5bd616329fc5c5ee932350b0021e Mon Sep 17 00:00:00 2001 From: j4k0xb <55899582+j4k0xb@users.noreply.github.com> Date: Sat, 13 Jan 2024 03:20:26 +0100 Subject: [PATCH 14/81] fix: export variable named and default --- .../src/unpack/webpack/import-export-manager.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/webcrack/src/unpack/webpack/import-export-manager.ts b/packages/webcrack/src/unpack/webpack/import-export-manager.ts index 23e99ae4..2be7664c 100644 --- a/packages/webcrack/src/unpack/webpack/import-export-manager.ts +++ b/packages/webcrack/src/unpack/webpack/import-export-manager.ts @@ -143,8 +143,12 @@ export class ImportExportManager { ); if (!declaration) return; - if (declaration.type === 'ExportNamedDeclaration') { - const namedExport = t.exportNamedDeclaration(null, [ + if ( + declaration.type === 'ExportNamedDeclaration' || + (exportName === 'default' && binding.references > 1) + ) { + // Example: export { foo as bar }; + const namedExport = t.exportNamedDeclaration(undefined, [ t.exportSpecifier( t.identifier(binding.identifier.name), t.identifier(exportName), @@ -153,6 +157,7 @@ export class ImportExportManager { this.exports.push(namedExport); declaration.insertAfter(namedExport); } else if (exportName === 'default') { + // Example: export default 1; const value = t.isVariableDeclaration(declaration.node) ? declaration.node.declarations[0].init! : (declaration.node as t.ClassDeclaration | t.FunctionDeclaration); @@ -160,6 +165,7 @@ export class ImportExportManager { this.exports.push(defaultExport); declaration.replaceWith(defaultExport); } else { + // Example: export var counter = 1; renameFast(binding, exportName); const namedExport = t.exportNamedDeclaration(declaration.node); this.exports.push(namedExport); From 5251afdf841ec0b497920b0264d27799c0d5b8f7 Mon Sep 17 00:00:00 2001 From: j4k0xb <55899582+j4k0xb@users.noreply.github.com> Date: Sat, 13 Jan 2024 03:20:26 +0100 Subject: [PATCH 15/81] refactor: track export names only --- .../unpack/webpack/import-export-manager.ts | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/packages/webcrack/src/unpack/webpack/import-export-manager.ts b/packages/webcrack/src/unpack/webpack/import-export-manager.ts index 2be7664c..0c095cf0 100644 --- a/packages/webcrack/src/unpack/webpack/import-export-manager.ts +++ b/packages/webcrack/src/unpack/webpack/import-export-manager.ts @@ -21,7 +21,7 @@ interface RequireVar { } export class ImportExportManager { - exports: t.ExportDeclaration[] = []; + exports = new Set(); private ast: t.File; private webpackRequire: Binding | undefined; @@ -45,7 +45,13 @@ export class ImportExportManager { this.transformExports(); } - addExport(scope: Scope, exportName: string, value: t.Expression) { + private transformExport( + scope: Scope, + exportName: string, + value: t.Expression, + ) { + this.exports.add(exportName); + const objectName = m.capture(m.anyString()); const propertyName = m.capture(m.anyString()); const memberExpressionMatcher = m.memberExpression( @@ -114,7 +120,6 @@ export class ImportExportManager { [specifier], t.stringLiteral(moduleId), ); - this.exports.push(exportDeclaration); binding.path.parentPath?.insertAfter(exportDeclaration); this.reExportCache.set(moduleId, exportDeclaration); } @@ -154,7 +159,6 @@ export class ImportExportManager { t.identifier(exportName), ), ]); - this.exports.push(namedExport); declaration.insertAfter(namedExport); } else if (exportName === 'default') { // Example: export default 1; @@ -162,13 +166,11 @@ export class ImportExportManager { ? declaration.node.declarations[0].init! : (declaration.node as t.ClassDeclaration | t.FunctionDeclaration); const defaultExport = t.exportDefaultDeclaration(value); - this.exports.push(defaultExport); declaration.replaceWith(defaultExport); } else { // Example: export var counter = 1; renameFast(binding, exportName); const namedExport = t.exportNamedDeclaration(declaration.node); - this.exports.push(namedExport); declaration.replaceWith(namedExport); } } @@ -187,7 +189,6 @@ export class ImportExportManager { private addExportAll(binding: Binding, moduleId: string) { // TODO: resolve to file path const exportAll = t.exportAllDeclaration(t.stringLiteral(moduleId)); - this.exports.push(exportAll); binding.path.parentPath?.insertAfter(exportAll); } @@ -270,14 +271,19 @@ export class ImportExportManager { if (!path.parentPath.isProgram()) return path.skip(); if (singleExport.match(path.node)) { - this.addExport(path.scope, exportName.current!, returnValue.current!); + this.transformExport( + path.scope, + exportName.current!, + returnValue.current!, + ); path.remove(); } else if (defaultExportAssignment.match(path.node)) { + this.exports.add('default'); path.replaceWith(t.exportDefaultDeclaration(returnValue.current!)); } else if (multiExport.match(path.node)) { for (const property of properties.current!) { objectProperty.match(property); // To easily get the captures per property - this.addExport( + this.transformExport( path.scope, exportName.current!, returnValue.current!, From 336e9f13ee6907cae75ef4c617f33b5a76577c57 Mon Sep 17 00:00:00 2001 From: j4k0xb <55899582+j4k0xb@users.noreply.github.com> Date: Sat, 13 Jan 2024 03:20:26 +0100 Subject: [PATCH 16/81] refactor: export default --- .../unpack/webpack/import-export-manager.ts | 66 +++++++++++-------- 1 file changed, 40 insertions(+), 26 deletions(-) diff --git a/packages/webcrack/src/unpack/webpack/import-export-manager.ts b/packages/webcrack/src/unpack/webpack/import-export-manager.ts index 0c095cf0..ff336923 100644 --- a/packages/webcrack/src/unpack/webpack/import-export-manager.ts +++ b/packages/webcrack/src/unpack/webpack/import-export-manager.ts @@ -69,6 +69,8 @@ export class ImportExportManager { if (requireVar) { this.addExportAll(binding, requireVar.moduleId); + } else if (exportName === 'default' && binding.references === 1) { + this.addExportDefault(binding); } else { this.addExportDeclaration(binding, exportName); } @@ -137,21 +139,24 @@ export class ImportExportManager { * ``` */ private addExportDeclaration(binding: Binding, exportName: string) { - const declaration = findPath( - binding.path, - m.or( - m.variableDeclaration(), - m.classDeclaration(), - m.functionDeclaration(), - m.exportNamedDeclaration(), - ), + const statement = binding.path.getStatementParent()!; + const matcher = m.or( + m.variableDeclaration(), + m.classDeclaration(), + m.functionDeclaration(), + m.exportNamedDeclaration(), ); - if (!declaration) return; + if (!matcher.match(statement.node)) return; + + const isDeclarationExport = + exportName !== 'default' && statement.type !== 'ExportNamedDeclaration'; - if ( - declaration.type === 'ExportNamedDeclaration' || - (exportName === 'default' && binding.references > 1) - ) { + if (isDeclarationExport) { + // Example: export var counter = 1; + renameFast(binding, exportName); + const namedExport = t.exportNamedDeclaration(statement.node); + statement.replaceWith(namedExport); + } else { // Example: export { foo as bar }; const namedExport = t.exportNamedDeclaration(undefined, [ t.exportSpecifier( @@ -159,22 +164,31 @@ export class ImportExportManager { t.identifier(exportName), ), ]); - declaration.insertAfter(namedExport); - } else if (exportName === 'default') { - // Example: export default 1; - const value = t.isVariableDeclaration(declaration.node) - ? declaration.node.declarations[0].init! - : (declaration.node as t.ClassDeclaration | t.FunctionDeclaration); - const defaultExport = t.exportDefaultDeclaration(value); - declaration.replaceWith(defaultExport); - } else { - // Example: export var counter = 1; - renameFast(binding, exportName); - const namedExport = t.exportNamedDeclaration(declaration.node); - declaration.replaceWith(namedExport); + statement.insertAfter(namedExport); } } + /** + * Example: + * ```js + * __webpack_require__.d(exports, { default: () => foo }); + * var foo = 1; + * ``` + * to + * ```js + * export default 1; + * ``` + */ + private addExportDefault(binding: Binding) { + const node = binding.path.node; + const declaration = + node.type === 'VariableDeclarator' + ? node.init! + : (node as t.ClassDeclaration | t.FunctionDeclaration); + const defaultExport = t.exportDefaultDeclaration(declaration); + binding.path.getStatementParent()!.replaceWith(defaultExport); + } + /** * Example: * ```js From cf010375aeb669bfd4fc6e5a52249cf236a1d16a Mon Sep 17 00:00:00 2001 From: j4k0xb <55899582+j4k0xb@users.noreply.github.com> Date: Sat, 13 Jan 2024 03:20:26 +0100 Subject: [PATCH 17/81] refactor: remove RequireVar name --- .../webcrack/src/unpack/webpack/import-export-manager.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/webcrack/src/unpack/webpack/import-export-manager.ts b/packages/webcrack/src/unpack/webpack/import-export-manager.ts index ff336923..25962993 100644 --- a/packages/webcrack/src/unpack/webpack/import-export-manager.ts +++ b/packages/webcrack/src/unpack/webpack/import-export-manager.ts @@ -1,23 +1,22 @@ import traverse, { Binding, NodePath, Scope } from '@babel/traverse'; import * as t from '@babel/types'; import * as m from '@codemod/matchers'; -import { constMemberExpression, findPath, renameFast } from '../../ast-utils'; +import { constMemberExpression, renameFast } from '../../ast-utils'; /** * Example: `__webpack_require__(id)` */ interface RequireCall { - moduleId: string; path: NodePath; + moduleId: string; } /** * Example: `var foo = __webpack_require__(id);` */ interface RequireVar { - name: string; - moduleId: string; binding: Binding; + moduleId: string; } export class ImportExportManager { @@ -230,7 +229,6 @@ export class ImportExportManager { const binding = path.scope.getBinding(varName.current!)!; this.requireVars.push({ moduleId: idArg.node.value.toString(), - name: varName.current!, binding, }); } From fbf2be04c1f8f6c77b4f7cca6316a6cd919531e5 Mon Sep 17 00:00:00 2001 From: j4k0xb <55899582+j4k0xb@users.noreply.github.com> Date: Sat, 13 Jan 2024 03:20:26 +0100 Subject: [PATCH 18/81] fix: re-export all name --- .../src/unpack/webpack/import-export-manager.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/webcrack/src/unpack/webpack/import-export-manager.ts b/packages/webcrack/src/unpack/webpack/import-export-manager.ts index 25962993..8e6a28d9 100644 --- a/packages/webcrack/src/unpack/webpack/import-export-manager.ts +++ b/packages/webcrack/src/unpack/webpack/import-export-manager.ts @@ -67,7 +67,7 @@ export class ImportExportManager { const requireVar = findRequireVar(binding); if (requireVar) { - this.addExportAll(binding, requireVar.moduleId); + this.addExportAll(binding, requireVar.moduleId, exportName); } else if (exportName === 'default' && binding.references === 1) { this.addExportDefault(binding); } else { @@ -199,9 +199,13 @@ export class ImportExportManager { * export * as foo from 'lib'; * ``` */ - private addExportAll(binding: Binding, moduleId: string) { + private addExportAll(binding: Binding, moduleId: string, exportName: string) { // TODO: resolve to file path - const exportAll = t.exportAllDeclaration(t.stringLiteral(moduleId)); + const exportAll = t.exportNamedDeclaration( + undefined, + [t.exportNamespaceSpecifier(t.identifier(exportName))], + t.stringLiteral(moduleId), + ); binding.path.parentPath?.insertAfter(exportAll); } From 9913e500bfda39a386b927667e93efb5c7ac9681 Mon Sep 17 00:00:00 2001 From: j4k0xb <55899582+j4k0xb@users.noreply.github.com> Date: Sat, 13 Jan 2024 03:20:26 +0100 Subject: [PATCH 19/81] refactor: use babel template --- .../unpack/webpack/import-export-manager.ts | 56 ++++++++----------- 1 file changed, 24 insertions(+), 32 deletions(-) diff --git a/packages/webcrack/src/unpack/webpack/import-export-manager.ts b/packages/webcrack/src/unpack/webpack/import-export-manager.ts index 8e6a28d9..58fb3af1 100644 --- a/packages/webcrack/src/unpack/webpack/import-export-manager.ts +++ b/packages/webcrack/src/unpack/webpack/import-export-manager.ts @@ -1,3 +1,4 @@ +import { statement } from '@babel/template'; import traverse, { Binding, NodePath, Scope } from '@babel/traverse'; import * as t from '@babel/types'; import * as m from '@codemod/matchers'; @@ -108,20 +109,16 @@ export class ImportExportManager { exportName: string, ) { const existingExport = this.reExportCache.get(moduleId); - const specifier = t.exportSpecifier( - t.identifier(localName), - t.identifier(exportName), - ); if (existingExport) { - existingExport.specifiers.push(specifier); + existingExport.specifiers.push( + t.exportSpecifier(t.identifier(localName), t.identifier(exportName)), + ); } else { // TODO: resolve to file path - const exportDeclaration = t.exportNamedDeclaration( - undefined, - [specifier], - t.stringLiteral(moduleId), - ); - binding.path.parentPath?.insertAfter(exportDeclaration); + const exportDeclaration = + statement`export { ${localName} as ${exportName} } from '${moduleId}'`() as t.ExportNamedDeclaration; + const [path] = binding.path.parentPath!.insertAfter(exportDeclaration); + binding.reference(path.get('specifiers.0.local') as NodePath); this.reExportCache.set(moduleId, exportDeclaration); } } @@ -138,32 +135,29 @@ export class ImportExportManager { * ``` */ private addExportDeclaration(binding: Binding, exportName: string) { - const statement = binding.path.getStatementParent()!; + const statementPath = binding.path.getStatementParent()!; const matcher = m.or( m.variableDeclaration(), m.classDeclaration(), m.functionDeclaration(), m.exportNamedDeclaration(), ); - if (!matcher.match(statement.node)) return; + if (!matcher.match(statementPath.node)) return; const isDeclarationExport = - exportName !== 'default' && statement.type !== 'ExportNamedDeclaration'; + exportName !== 'default' && + statementPath.type !== 'ExportNamedDeclaration'; if (isDeclarationExport) { // Example: export var counter = 1; renameFast(binding, exportName); - const namedExport = t.exportNamedDeclaration(statement.node); - statement.replaceWith(namedExport); + statementPath.replaceWith(t.exportNamedDeclaration(statementPath.node)); } else { // Example: export { foo as bar }; - const namedExport = t.exportNamedDeclaration(undefined, [ - t.exportSpecifier( - t.identifier(binding.identifier.name), - t.identifier(exportName), - ), - ]); - statement.insertAfter(namedExport); + const [path] = statementPath.insertAfter( + statement`export { ${binding.identifier.name} as ${exportName} }`(), + ); + binding.reference(path.get('specifiers.0.local') as NodePath); } } @@ -180,12 +174,13 @@ export class ImportExportManager { */ private addExportDefault(binding: Binding) { const node = binding.path.node; - const declaration = + const value = node.type === 'VariableDeclarator' ? node.init! : (node as t.ClassDeclaration | t.FunctionDeclaration); - const defaultExport = t.exportDefaultDeclaration(declaration); - binding.path.getStatementParent()!.replaceWith(defaultExport); + binding.path + .getStatementParent()! + .replaceWith(statement`export default ${value}`()); } /** @@ -201,12 +196,9 @@ export class ImportExportManager { */ private addExportAll(binding: Binding, moduleId: string, exportName: string) { // TODO: resolve to file path - const exportAll = t.exportNamedDeclaration( - undefined, - [t.exportNamespaceSpecifier(t.identifier(exportName))], - t.stringLiteral(moduleId), + binding.path.parentPath!.insertAfter( + statement`export * as ${exportName} from '${moduleId}'`(), ); - binding.path.parentPath?.insertAfter(exportAll); } /** @@ -295,7 +287,7 @@ export class ImportExportManager { path.remove(); } else if (defaultExportAssignment.match(path.node)) { this.exports.add('default'); - path.replaceWith(t.exportDefaultDeclaration(returnValue.current!)); + path.replaceWith(statement`export default ${returnValue.current}`()); } else if (multiExport.match(path.node)) { for (const property of properties.current!) { objectProperty.match(property); // To easily get the captures per property From 8050dd169dee4ba93ecfed837064a606a3535643 Mon Sep 17 00:00:00 2001 From: j4k0xb <55899582+j4k0xb@users.noreply.github.com> Date: Sat, 13 Jan 2024 03:20:26 +0100 Subject: [PATCH 20/81] refactor: add unexpected export comment instead of throwing generate --- .../webcrack/src/unpack/webpack/import-export-manager.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/webcrack/src/unpack/webpack/import-export-manager.ts b/packages/webcrack/src/unpack/webpack/import-export-manager.ts index 58fb3af1..5225097b 100644 --- a/packages/webcrack/src/unpack/webpack/import-export-manager.ts +++ b/packages/webcrack/src/unpack/webpack/import-export-manager.ts @@ -2,7 +2,7 @@ import { statement } from '@babel/template'; import traverse, { Binding, NodePath, Scope } from '@babel/traverse'; import * as t from '@babel/types'; import * as m from '@codemod/matchers'; -import { constMemberExpression, renameFast } from '../../ast-utils'; +import { generate, renameFast } from '../../ast-utils'; /** * Example: `__webpack_require__(id)` @@ -87,7 +87,12 @@ export class ImportExportManager { exportName, ); } else { - throw new Error(`Unexpected export: ${value.type}`); + t.addComment( + this.ast.program, + 'inner', + `webcrack:unexpected-export: ${exportName} = ${generate(value)}`, + true, + ); } } From 4f5679fcf6ad746dddf111086f628440acaf3a8f Mon Sep 17 00:00:00 2001 From: j4k0xb <55899582+j4k0xb@users.noreply.github.com> Date: Sat, 13 Jan 2024 03:20:26 +0100 Subject: [PATCH 21/81] feat: track imports --- .../src/unpack/webpack/import-export-manager.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/webcrack/src/unpack/webpack/import-export-manager.ts b/packages/webcrack/src/unpack/webpack/import-export-manager.ts index 5225097b..7e37566c 100644 --- a/packages/webcrack/src/unpack/webpack/import-export-manager.ts +++ b/packages/webcrack/src/unpack/webpack/import-export-manager.ts @@ -21,6 +21,13 @@ interface RequireVar { } export class ImportExportManager { + /** + * All module ids that are imported + */ + imports = new Set(); + /** + * Names of all exports + */ exports = new Set(); private ast: t.File; @@ -222,14 +229,17 @@ export class ImportExportManager { this.webpackRequire?.referencePaths.forEach((path) => { m.matchPath(requireCall, { idArg }, path.parentPath!, ({ idArg }) => { + const moduleId = idArg.node.value.toString(); + this.imports.add(moduleId); this.requireCalls.push({ - moduleId: idArg.node.value.toString(), + moduleId, path: path.parentPath as NodePath, }); + if (requireVar.match(path.parentPath!.parentPath?.parent)) { const binding = path.scope.getBinding(varName.current!)!; this.requireVars.push({ - moduleId: idArg.node.value.toString(), + moduleId, binding, }); } From df71e783fada888d2577db37f5fd7521e18252f7 Mon Sep 17 00:00:00 2001 From: j4k0xb <55899582+j4k0xb@users.noreply.github.com> Date: Sat, 13 Jan 2024 03:20:27 +0100 Subject: [PATCH 22/81] feat: remove `__webpack_require__.hmd` and `__webpack_require__.nmd` --- .../src/unpack/test/module-decorator.test.ts | 15 ++++++++ .../webpack/runtime/module-decorator.ts | 38 +++++++++++++++++++ 2 files changed, 53 insertions(+) create mode 100644 packages/webcrack/src/unpack/test/module-decorator.test.ts create mode 100644 packages/webcrack/src/unpack/webpack/runtime/module-decorator.ts 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/webpack/runtime/module-decorator.ts b/packages/webcrack/src/unpack/webpack/runtime/module-decorator.ts new file mode 100644 index 00000000..c0926482 --- /dev/null +++ b/packages/webcrack/src/unpack/webpack/runtime/module-decorator.ts @@ -0,0 +1,38 @@ +import * as m from '@codemod/matchers'; +import { Transform, constMemberExpression } from '../../../ast-utils'; + +/** + * `webpack/runtime/harmony module decorator` and `webpack/runtime/node module decorator` + * + * The CommonJsPlugin injects this when a module accesses the global 'module' variable. + * + * - // TODO(webpack 4): `module = __webpack_require__('webpack/buildin/harmony-module.js');` + * or `module = __webpack_require__('webpack/buildin/module.js');` + * - webpack 5: `module = __webpack_require__.hmd(module);` or `module = __webpack_require__.nmd(module);` + */ +export default { + name: 'module-decorator', + tags: ['safe'], + visitor() { + const moduleVar = m.capture(m.identifier()); + const matcher = m.expressionStatement( + m.assignmentExpression( + '=', + moduleVar, + m.callExpression( + constMemberExpression('__webpack_require__', m.or('hmd', 'nmd')), + [m.fromCapture(moduleVar)], + ), + ), + ); + + return { + ExpressionStatement(path) { + if (!path.parentPath.isProgram()) return path.skip(); + if (!matcher.match(path.node)) return; + path.remove(); + this.changes++; + }, + }; + }, +} satisfies Transform; From 8d0faac5af3b4635c660ab4d102ebba896633ad4 Mon Sep 17 00:00:00 2001 From: j4k0xb <55899582+j4k0xb@users.noreply.github.com> Date: Sat, 13 Jan 2024 03:20:27 +0100 Subject: [PATCH 23/81] feat: remove `__webpack_require__.r` --- .../src/unpack/test/namespace-object.test.ts | 11 +++++++ .../webpack/runtime/namespace-object.ts | 29 +++++++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 packages/webcrack/src/unpack/test/namespace-object.test.ts create mode 100644 packages/webcrack/src/unpack/webpack/runtime/namespace-object.ts 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..104c83a5 --- /dev/null +++ b/packages/webcrack/src/unpack/test/namespace-object.test.ts @@ -0,0 +1,11 @@ +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(exports);`, result).toMatchInlineSnapshot(``); + expect(result.isESM).toBe(true); +}); diff --git a/packages/webcrack/src/unpack/webpack/runtime/namespace-object.ts b/packages/webcrack/src/unpack/webpack/runtime/namespace-object.ts new file mode 100644 index 00000000..71d51262 --- /dev/null +++ b/packages/webcrack/src/unpack/webpack/runtime/namespace-object.ts @@ -0,0 +1,29 @@ +import * as m from '@codemod/matchers'; +import { Transform, constMemberExpression } from '../../../ast-utils'; + +/** + * `webpack/runtime/make namespace object` + * + * `__webpack_require__.r(__webpack_exports__);` defines `__esModule` on exports. + */ +export default { + name: 'namespace-object', + tags: ['safe'], + visitor(options = { isESM: false }) { + const matcher = m.expressionStatement( + m.callExpression(constMemberExpression('__webpack_require__', 'r'), [ + m.identifier(), + ]), + ); + + return { + ExpressionStatement(path) { + if (!path.parentPath.isProgram()) return path.skip(); + if (!matcher.match(path.node)) return; + options.isESM = true; + path.remove(); + this.changes++; + }, + }; + }, +} satisfies Transform<{ isESM: boolean }>; From 21417e5e8d81172ed4b6dfe83b0f86d1ff45f388 Mon Sep 17 00:00:00 2001 From: j4k0xb <55899582+j4k0xb@users.noreply.github.com> Date: Sat, 13 Jan 2024 03:20:27 +0100 Subject: [PATCH 24/81] feat: replace `__webpack_require__.o` --- .../src/unpack/test/has-own-property.test.ts | 10 ++++++ .../webpack/runtime/has-own-property.ts | 31 +++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 packages/webcrack/src/unpack/test/has-own-property.test.ts create mode 100644 packages/webcrack/src/unpack/webpack/runtime/has-own-property.ts 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..0028e6b0 --- /dev/null +++ b/packages/webcrack/src/unpack/test/has-own-property.test.ts @@ -0,0 +1,10 @@ +import { test } from 'vitest'; +import { testTransform } from '../../../test'; +import hasOwnProperty from '../webpack/runtime/has-own-property'; + +const expectJS = testTransform(hasOwnProperty); + +test('replace hasOwnProperty', () => + expectJS(`__webpack_require__.o(obj, prop);`).toMatchInlineSnapshot(` + Object.hasOwn(obj, prop); + `)); diff --git a/packages/webcrack/src/unpack/webpack/runtime/has-own-property.ts b/packages/webcrack/src/unpack/webpack/runtime/has-own-property.ts new file mode 100644 index 00000000..520c4d9c --- /dev/null +++ b/packages/webcrack/src/unpack/webpack/runtime/has-own-property.ts @@ -0,0 +1,31 @@ +import { expression } from '@babel/template'; +import * as m from '@codemod/matchers'; +import { Transform, constMemberExpression } from '../../../ast-utils'; + +/** + * `webpack/runtime/hasOwnProperty shorthand` + * + * Used mostly in other runtime helpers but sometimes it also appears in user code. + */ +export default { + name: 'has-own-property', + tags: ['safe'], + visitor() { + const object = m.capture(m.anyExpression()); + const property = m.capture(m.anyExpression()); + const matcher = m.callExpression( + constMemberExpression('__webpack_require__', 'o'), + [object, property], + ); + + return { + CallExpression(path) { + if (!matcher.match(path.node)) return; + path.replaceWith( + expression`Object.hasOwn(${object.current}, ${property.current})`(), + ); + this.changes++; + }, + }; + }, +} satisfies Transform; From 16ac04d6dfcd0cfb6bb570fed405e5428b370427 Mon Sep 17 00:00:00 2001 From: j4k0xb <55899582+j4k0xb@users.noreply.github.com> Date: Sat, 13 Jan 2024 03:20:27 +0100 Subject: [PATCH 25/81] refactor: `__webpack_require__.d` --- ...est.ts => define-property-getters.test.ts} | 69 +++++++++------ .../unpack/webpack/import-export-manager.ts | 84 ++----------------- .../webcrack/src/unpack/webpack/module.ts | 4 +- .../define-property-getters.ts} | 47 +++++------ 4 files changed, 75 insertions(+), 129 deletions(-) rename packages/webcrack/src/unpack/test/{define-exports.test.ts => define-property-getters.test.ts} (84%) rename packages/webcrack/src/unpack/webpack/{define-exports.ts => runtime/define-property-getters.ts} (64%) diff --git a/packages/webcrack/src/unpack/test/define-exports.test.ts b/packages/webcrack/src/unpack/test/define-property-getters.test.ts similarity index 84% rename from packages/webcrack/src/unpack/test/define-exports.test.ts rename to packages/webcrack/src/unpack/test/define-property-getters.test.ts index 3aba17bd..8ca30d4e 100644 --- a/packages/webcrack/src/unpack/test/define-exports.test.ts +++ b/packages/webcrack/src/unpack/test/define-property-getters.test.ts @@ -1,8 +1,27 @@ -import { describe, test } from 'vitest'; -import { testTransform } from '../../../test'; -import defineExports from '../webpack/define-exports'; +import { parse } from '@babel/parser'; +import traverse from '@babel/traverse'; +import { describe, expect, test } from 'vitest'; +import { applyTransform } from '../../ast-utils'; +import { ImportExportManager } from '../webpack/import-export-manager'; +import definePropertyGetters from '../webpack/runtime/define-property-getters'; -const expectJS = testTransform(defineExports); +const expectJS = (input: string) => { + const ast = parse('var __webpack_require__; ' + input, { + sourceType: 'unambiguous', + allowReturnOutsideFunction: true, + }); + traverse(ast, { + Program(path) { + const webpackRequireBinding = path.scope.getBinding( + '__webpack_require__', + ); + const manager = new ImportExportManager(ast, webpackRequireBinding); + applyTransform(ast, definePropertyGetters, manager); + webpackRequireBinding!.path.remove(); + }, + }); + return expect(ast); +}; describe('webpack 4', () => { test('export default expression;', () => @@ -50,8 +69,8 @@ describe('webpack 4', () => { __webpack_require__.d(exports, "readFile", function() { return lib.readFile; }); var lib = __webpack_require__("lib"); `).toMatchInlineSnapshot(` - export { readFile } from "lib"; var lib = __webpack_require__("lib"); + export { readFile } from "lib"; `)); test('re-export named with multiple references', () => @@ -60,8 +79,8 @@ describe('webpack 4', () => { var lib = __webpack_require__("lib"); lib.writeFile(); `).toMatchInlineSnapshot(` - export { readFile } from "lib"; var lib = __webpack_require__("lib"); + export { readFile } from "lib"; lib.writeFile(); `)); @@ -70,8 +89,8 @@ describe('webpack 4', () => { __webpack_require__.d(exports, "foo", function() { return lib.readFile; }); var lib = __webpack_require__("lib"); `).toMatchInlineSnapshot(` - export { readFile as foo } from "lib"; var lib = __webpack_require__("lib"); + export { readFile as foo } from "lib"; `)); test('re-export named as default', () => @@ -79,8 +98,8 @@ describe('webpack 4', () => { __webpack_require__.d(exports, "default", function() { return lib.readFile; }); var lib = __webpack_require__("lib"); `).toMatchInlineSnapshot(` - export { readFile as default } from "lib"; var lib = __webpack_require__("lib"); + export { readFile as default } from "lib"; `)); test('re-export default as named', () => @@ -88,8 +107,8 @@ describe('webpack 4', () => { __webpack_require__.d(exports, "foo", function() { return lib.default; }); var lib = __webpack_require__("lib"); `).toMatchInlineSnapshot(` - export { default as foo } from "lib"; var lib = __webpack_require__("lib"); + export { default as foo } from "lib"; `)); test('re-export default as default', () => @@ -97,8 +116,8 @@ describe('webpack 4', () => { __webpack_require__.d(exports, "default", function() { return lib.default; }); var lib = __webpack_require__("lib"); `).toMatchInlineSnapshot(` - export { default } from "lib"; var lib = __webpack_require__("lib"); + export { default } from "lib"; `)); // webpack just declares all the exports individually @@ -123,12 +142,14 @@ describe('webpack 4', () => { `), ); - test.todo('re-export all as named', () => + test('re-export all as named', () => expectJS(` __webpack_require__.d(exports, "lib", function() { return lib; }); var lib = __webpack_require__("lib"); - `).toMatchInlineSnapshot(`export * as lib from "lib";`), - ); + `).toMatchInlineSnapshot(` + var lib = __webpack_require__("lib"); + export * as lib from "lib"; + `)); test.todo('re-export all as default', () => expectJS(` @@ -149,7 +170,7 @@ describe('webpack 5', () => { export var counter = 1; `)); - test.todo('export same variable with multiple names', () => + test('export same variable with multiple names', () => expectJS(` __webpack_require__.d(exports, { counter: () => foo, @@ -159,10 +180,9 @@ describe('webpack 5', () => { `).toMatchInlineSnapshot(` export var counter = 1; export { counter as increment }; - `), - ); + `)); - test('export object destructuring', () => + test.todo('export object destructuring', () => expectJS(` __webpack_require__.d(__webpack_exports__, { bar: () => bar, @@ -185,9 +205,10 @@ describe('webpack 5', () => { name1, name2: bar } = o; - `)); + `), + ); - test('export array destructuring', () => + test.todo('export array destructuring', () => expectJS(` __webpack_require__.d(__webpack_exports__, { bar: () => bar, @@ -198,7 +219,8 @@ describe('webpack 5', () => { `).toMatchInlineSnapshot(` const o = ["foo", "bar"]; export const [name1, bar] = o; - `)); + `), + ); test.todo('export as invalid identifier string name', () => expectJS(` @@ -212,7 +234,7 @@ describe('webpack 5', () => { `), ); - test.todo('re-export named merging', () => + test('re-export named merging', () => expectJS(` __webpack_require__.d(exports, { readFile: () => lib.readFile, @@ -220,10 +242,9 @@ describe('webpack 5', () => { }); var lib = __webpack_require__("lib"); `).toMatchInlineSnapshot(` - export { readFile, writeFile } from "lib"; var lib = __webpack_require__("lib"); - `), - ); + export { readFile, writeFile } from "lib"; + `)); test.todo('re-export all from commonjs', () => expectJS(` diff --git a/packages/webcrack/src/unpack/webpack/import-export-manager.ts b/packages/webcrack/src/unpack/webpack/import-export-manager.ts index 7e37566c..060518dd 100644 --- a/packages/webcrack/src/unpack/webpack/import-export-manager.ts +++ b/packages/webcrack/src/unpack/webpack/import-export-manager.ts @@ -1,8 +1,9 @@ import { statement } from '@babel/template'; -import traverse, { Binding, NodePath, Scope } from '@babel/traverse'; +import { Binding, NodePath, Scope } from '@babel/traverse'; import * as t from '@babel/types'; import * as m from '@codemod/matchers'; -import { generate, renameFast } from '../../ast-utils'; +import { applyTransform, generate, renameFast } from '../../ast-utils'; +import definePropertyGetters from './runtime/define-property-getters'; /** * Example: `__webpack_require__(id)` @@ -49,14 +50,10 @@ export class ImportExportManager { this.ast = ast; this.webpackRequire = webpackRequireBinding; this.collectRequireCalls(); - this.transformExports(); + applyTransform(ast, definePropertyGetters, this); } - private transformExport( - scope: Scope, - exportName: string, - value: t.Expression, - ) { + transformExport(scope: Scope, exportName: string, value: t.Expression) { this.exports.add(exportName); const objectName = m.capture(m.anyString()); @@ -246,75 +243,4 @@ export class ImportExportManager { }); }); } - - /** - * Extract the export information from all `__webpack_require__.d` calls - */ - private transformExports() { - const exportName = m.capture(m.anyString()); - const returnValue = m.capture(m.anyExpression()); - const getter = m.or( - m.functionExpression( - undefined, - [], - m.blockStatement([m.returnStatement(returnValue)]), - ), - m.arrowFunctionExpression([], returnValue), - ); - // Example (webpack v4): __webpack_require__.d(exports, 'counter', function () { return local }); - const singleExport = m.expressionStatement( - m.callExpression(constMemberExpression('__webpack_require__', 'd'), [ - m.identifier(), - m.stringLiteral(exportName), - getter, - ]), - ); - - // Example (webpack v4): exports.default = 1; - const defaultExportAssignment = m.expressionStatement( - m.assignmentExpression( - '=', - constMemberExpression('exports', 'default'), - returnValue, - ), - ); - - const objectProperty = m.objectProperty(m.identifier(exportName), getter); - const properties = m.capture(m.arrayOf(objectProperty)); - // Example (webpack v5): __webpack_require__.d(exports, { a: () => b, c: () => d }); - const multiExport = m.expressionStatement( - m.callExpression(constMemberExpression('__webpack_require__', 'd'), [ - m.identifier(), - m.objectExpression(properties), - ]), - ); - - traverse(this.ast, { - ExpressionStatement: (path) => { - if (!path.parentPath.isProgram()) return path.skip(); - - if (singleExport.match(path.node)) { - this.transformExport( - path.scope, - exportName.current!, - returnValue.current!, - ); - path.remove(); - } else if (defaultExportAssignment.match(path.node)) { - this.exports.add('default'); - path.replaceWith(statement`export default ${returnValue.current}`()); - } else if (multiExport.match(path.node)) { - for (const property of properties.current!) { - objectProperty.match(property); // To easily get the captures per property - this.transformExport( - path.scope, - exportName.current!, - returnValue.current!, - ); - } - path.remove(); - } - }, - }); - } } diff --git a/packages/webcrack/src/unpack/webpack/module.ts b/packages/webcrack/src/unpack/webpack/module.ts index 74fec75b..eaa53703 100644 --- a/packages/webcrack/src/unpack/webpack/module.ts +++ b/packages/webcrack/src/unpack/webpack/module.ts @@ -1,4 +1,4 @@ -import { Binding, NodePath } from '@babel/traverse'; +import { Binding } from '@babel/traverse'; import * as t from '@babel/types'; import * as m from '@codemod/matchers'; import { @@ -8,8 +8,8 @@ import { } from '../../ast-utils'; import { Module } from '../module'; import { FunctionPath } from './common-matchers'; -import defineExport from './define-exports'; import { ImportExportManager } from './import-export-manager'; +import defineExport from './runtime/define-property-getters'; import varInjections from './var-injections'; export class WebpackModule extends Module { diff --git a/packages/webcrack/src/unpack/webpack/define-exports.ts b/packages/webcrack/src/unpack/webpack/runtime/define-property-getters.ts similarity index 64% rename from packages/webcrack/src/unpack/webpack/define-exports.ts rename to packages/webcrack/src/unpack/webpack/runtime/define-property-getters.ts index 34b0097a..dfccc4ad 100644 --- a/packages/webcrack/src/unpack/webpack/define-exports.ts +++ b/packages/webcrack/src/unpack/webpack/runtime/define-property-getters.ts @@ -1,26 +1,18 @@ -import * as t from '@babel/types'; import * as m from '@codemod/matchers'; import assert from 'assert'; -import { Transform, constMemberExpression } from '../../ast-utils'; -import { ImportExportManager } from './import-export-manager'; +import { Transform, constMemberExpression } from '../../../ast-utils'; +import { ImportExportManager } from '../import-export-manager'; +import { statement } from '@babel/template'; // TODO: hoist re-exports to the top of the file (but retain order relative to imports) -// TODO: merge re-exports /** - * webpack/runtime/define property getters - * ```js - * __webpack_require__.d = (exports, definition) => { - * for (var key in definition) { - * if (__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) { - * Object.defineProperty(exports, key, { enumerable: true, get: definition[key] }); - * } - * } - * }; - * ``` + * `webpack/runtime/define property getters` + * + * Used to declare ESM exports. */ export default { - name: 'define-exports', + name: 'define-property-getters', tags: ['unsafe'], scope: true, visitor(manager) { @@ -45,7 +37,8 @@ export default { ]), ); - const defaultExpressionExport = m.expressionStatement( + // Example (webpack v4): exports.default = 1; + const defaultExportAssignment = m.expressionStatement( m.assignmentExpression( '=', constMemberExpression('exports', 'default'), @@ -64,23 +57,29 @@ export default { ); return { - ExpressionStatement(path) { + ExpressionStatement: (path) => { if (!path.parentPath.isProgram()) return path.skip(); if (singleExport.match(path.node)) { - // manager.addExport(path, exportName.current!, returnValue.current!); + manager.transformExport( + path.scope, + exportName.current!, + returnValue.current!, + ); path.remove(); - this.changes++; - } else if (defaultExpressionExport.match(path.node)) { - path.replaceWith(t.exportDefaultDeclaration(returnValue.current!)); - this.changes++; + } else if (defaultExportAssignment.match(path.node)) { + manager.exports.add('default'); + path.replaceWith(statement`export default ${returnValue.current}`()); } else if (multiExport.match(path.node)) { for (const property of properties.current!) { objectProperty.match(property); // To easily get the captures per property - // manager.addExport(path, exportName.current!, returnValue.current!); + manager.transformExport( + path.scope, + exportName.current!, + returnValue.current!, + ); } path.remove(); - this.changes++; } }, }; From 01315f99906f61f28b6321cd691f545aa1fb1fb5 Mon Sep 17 00:00:00 2001 From: j4k0xb <55899582+j4k0xb@users.noreply.github.com> Date: Sat, 13 Jan 2024 03:20:27 +0100 Subject: [PATCH 26/81] feat: replace `__webpack_require__.g` --- .../webcrack/src/unpack/test/global.test.ts | 10 +++++++++ .../src/unpack/webpack/runtime/global.ts | 22 +++++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 packages/webcrack/src/unpack/test/global.test.ts create mode 100644 packages/webcrack/src/unpack/webpack/runtime/global.ts 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..b5449b56 --- /dev/null +++ b/packages/webcrack/src/unpack/test/global.test.ts @@ -0,0 +1,10 @@ +import { test } from 'vitest'; +import { testTransform } from '../../../test'; +import global from '../webpack/runtime/global'; + +const expectJS = testTransform(global); + +test('replace __webpack_require__.g with global', () => + expectJS(` + __webpack_require__.g.setTimeout(() => {}); + `).toMatchInlineSnapshot(`global.setTimeout(() => {});`)); diff --git a/packages/webcrack/src/unpack/webpack/runtime/global.ts b/packages/webcrack/src/unpack/webpack/runtime/global.ts new file mode 100644 index 00000000..24d114c7 --- /dev/null +++ b/packages/webcrack/src/unpack/webpack/runtime/global.ts @@ -0,0 +1,22 @@ +import * as t from '@babel/types'; +import { Transform, constMemberExpression } from '../../../ast-utils'; + +/** + * `webpack/runtime/global`, `__webpack_require__.g` + * + * webpack injects this when a module accesses `global` + */ +export default { + name: 'global', + tags: ['safe'], + visitor() { + const matcher = constMemberExpression('__webpack_require__', 'g'); + return { + Expression(path) { + if (!matcher.match(path.node)) return; + path.replaceWith(t.identifier('global')); + this.changes++; + }, + }; + }, +} satisfies Transform; From bbe7574ed1a2f1add1fd30a3d99198fa08a25513 Mon Sep 17 00:00:00 2001 From: j4k0xb <55899582+j4k0xb@users.noreply.github.com> Date: Sat, 13 Jan 2024 03:20:27 +0100 Subject: [PATCH 27/81] refactor: use this instead of a state param in transform.run --- packages/webcrack/src/ast-utils/transform.ts | 10 +++++----- packages/webcrack/src/deobfuscate/index.ts | 12 ++++++------ .../src/deobfuscate/inline-decoded-strings.ts | 4 ++-- .../src/deobfuscate/inline-decoder-wrappers.ts | 6 +++--- 4 files changed, 16 insertions(+), 16 deletions(-) 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>; From 9efe754c548d0c88fa8409fca8802873fa8ecb43 Mon Sep 17 00:00:00 2001 From: j4k0xb <55899582+j4k0xb@users.noreply.github.com> Date: Sat, 13 Jan 2024 03:20:27 +0100 Subject: [PATCH 28/81] perf: use references for webpack transforms --- .../test/define-property-getters.test.ts | 28 ++++---------- .../webcrack/src/unpack/test/global.test.ts | 32 ++++++++++++++-- .../src/unpack/test/has-own-property.test.ts | 7 +++- packages/webcrack/src/unpack/test/index.ts | 37 +++++++++++++++++++ .../src/unpack/webpack/runtime/global.ts | 17 ++++----- .../webpack/runtime/has-own-property.ts | 21 +++++------ 6 files changed, 96 insertions(+), 46 deletions(-) create mode 100644 packages/webcrack/src/unpack/test/index.ts diff --git a/packages/webcrack/src/unpack/test/define-property-getters.test.ts b/packages/webcrack/src/unpack/test/define-property-getters.test.ts index 8ca30d4e..19007267 100644 --- a/packages/webcrack/src/unpack/test/define-property-getters.test.ts +++ b/packages/webcrack/src/unpack/test/define-property-getters.test.ts @@ -1,27 +1,13 @@ -import { parse } from '@babel/parser'; -import traverse from '@babel/traverse'; -import { describe, expect, test } from 'vitest'; -import { applyTransform } from '../../ast-utils'; +import { describe, test } from 'vitest'; +import { testWebpackModuleTransform } from '.'; import { ImportExportManager } from '../webpack/import-export-manager'; import definePropertyGetters from '../webpack/runtime/define-property-getters'; -const expectJS = (input: string) => { - const ast = parse('var __webpack_require__; ' + input, { - sourceType: 'unambiguous', - allowReturnOutsideFunction: true, - }); - traverse(ast, { - Program(path) { - const webpackRequireBinding = path.scope.getBinding( - '__webpack_require__', - ); - const manager = new ImportExportManager(ast, webpackRequireBinding); - applyTransform(ast, definePropertyGetters, manager); - webpackRequireBinding!.path.remove(); - }, - }); - return expect(ast); -}; +const expectJS = testWebpackModuleTransform( + definePropertyGetters, + ({ scope }, ast) => + new ImportExportManager(ast, scope.bindings.__webpack_require__), +); describe('webpack 4', () => { test('export default expression;', () => diff --git a/packages/webcrack/src/unpack/test/global.test.ts b/packages/webcrack/src/unpack/test/global.test.ts index b5449b56..81f78e8c 100644 --- a/packages/webcrack/src/unpack/test/global.test.ts +++ b/packages/webcrack/src/unpack/test/global.test.ts @@ -1,10 +1,36 @@ -import { test } from 'vitest'; -import { testTransform } from '../../../test'; +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 = testTransform(global); +const expectJS = testWebpackModuleTransform( + global, + ({ scope }) => scope.bindings.__webpack_require__, +); test('replace __webpack_require__.g with global', () => expectJS(` __webpack_require__.g.setTimeout(() => {}); `).toMatchInlineSnapshot(`global.setTimeout(() => {});`)); + +test('a', () => { + const ast = parse(` + (function(module, 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 (module, 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 index 0028e6b0..790e21c7 100644 --- a/packages/webcrack/src/unpack/test/has-own-property.test.ts +++ b/packages/webcrack/src/unpack/test/has-own-property.test.ts @@ -1,8 +1,11 @@ import { test } from 'vitest'; -import { testTransform } from '../../../test'; +import { testWebpackModuleTransform } from '.'; import hasOwnProperty from '../webpack/runtime/has-own-property'; -const expectJS = testTransform(hasOwnProperty); +const expectJS = testWebpackModuleTransform( + hasOwnProperty, + ({ scope }) => scope.bindings.__webpack_require__, +); test('replace hasOwnProperty', () => expectJS(`__webpack_require__.o(obj, prop);`).toMatchInlineSnapshot(` diff --git a/packages/webcrack/src/unpack/test/index.ts b/packages/webcrack/src/unpack/test/index.ts new file mode 100644 index 00000000..935468da --- /dev/null +++ b/packages/webcrack/src/unpack/test/index.ts @@ -0,0 +1,37 @@ +import { ParseResult, parse } from '@babel/parser'; +import traverse, { NodePath } from '@babel/traverse'; +import * as t from '@babel/types'; +import { Assertion, expect } from 'vitest'; +import { Transform, applyTransform } from '../../ast-utils'; + +/** + * Test a transform with the input being wrapped in `(function(module, exports, __webpack_require__) { ... })` + * @param transform the transform to apply + * @param cb specify the options that will be passed to the transform + */ +export function testWebpackModuleTransform( + transform: Transform, + cb?: (wrapperPath: NodePath, ast: t.File) => Options, +): (input: string) => Assertion> { + return (input) => { + const moduleCode = ` + (function(module, exports, __webpack_require__) { + ${input} + }); + `; + const ast = parse(moduleCode, { + sourceType: 'unambiguous', + allowReturnOutsideFunction: true, + }); + let innerAST: t.File; + traverse(ast, { + FunctionExpression(path) { + path.stop(); + innerAST = t.file(t.program(path.node.body.body)); + const options = cb?.(path, innerAST); + applyTransform(innerAST, transform, options); + }, + }); + return expect(innerAST!); + }; +} diff --git a/packages/webcrack/src/unpack/webpack/runtime/global.ts b/packages/webcrack/src/unpack/webpack/runtime/global.ts index 24d114c7..53fe5584 100644 --- a/packages/webcrack/src/unpack/webpack/runtime/global.ts +++ b/packages/webcrack/src/unpack/webpack/runtime/global.ts @@ -1,3 +1,4 @@ +import { Binding } from '@babel/traverse'; import * as t from '@babel/types'; import { Transform, constMemberExpression } from '../../../ast-utils'; @@ -9,14 +10,12 @@ import { Transform, constMemberExpression } from '../../../ast-utils'; export default { name: 'global', tags: ['safe'], - visitor() { + run(ast, binding) { const matcher = constMemberExpression('__webpack_require__', 'g'); - return { - Expression(path) { - if (!matcher.match(path.node)) return; - path.replaceWith(t.identifier('global')); - this.changes++; - }, - }; + binding?.referencePaths.forEach((path) => { + if (!matcher.match(path.parent)) return; + path.parentPath!.replaceWith(t.identifier('global')); + this.changes++; + }); }, -} satisfies Transform; +} satisfies Transform; diff --git a/packages/webcrack/src/unpack/webpack/runtime/has-own-property.ts b/packages/webcrack/src/unpack/webpack/runtime/has-own-property.ts index 520c4d9c..fdcdcfd5 100644 --- a/packages/webcrack/src/unpack/webpack/runtime/has-own-property.ts +++ b/packages/webcrack/src/unpack/webpack/runtime/has-own-property.ts @@ -1,4 +1,5 @@ import { expression } from '@babel/template'; +import { Binding } from '@babel/traverse'; import * as m from '@codemod/matchers'; import { Transform, constMemberExpression } from '../../../ast-utils'; @@ -10,7 +11,7 @@ import { Transform, constMemberExpression } from '../../../ast-utils'; export default { name: 'has-own-property', tags: ['safe'], - visitor() { + run(ast, binding) { const object = m.capture(m.anyExpression()); const property = m.capture(m.anyExpression()); const matcher = m.callExpression( @@ -18,14 +19,12 @@ export default { [object, property], ); - return { - CallExpression(path) { - if (!matcher.match(path.node)) return; - path.replaceWith( - expression`Object.hasOwn(${object.current}, ${property.current})`(), - ); - this.changes++; - }, - }; + binding?.referencePaths.forEach((path) => { + if (!matcher.match(path.parentPath?.parent)) return; + path.parentPath.parentPath!.replaceWith( + expression`Object.hasOwn(${object.current}, ${property.current})`(), + ); + this.changes++; + }); }, -} satisfies Transform; +} satisfies Transform; From 7b1c7e263f850eef10853da5fd41b3e54e2b8de4 Mon Sep 17 00:00:00 2001 From: j4k0xb <55899582+j4k0xb@users.noreply.github.com> Date: Sat, 13 Jan 2024 03:20:27 +0100 Subject: [PATCH 29/81] feat: apply webpack runtime transforms --- .../webcrack/src/unpack/webpack/bundle.ts | 31 +++++---- .../src/unpack/webpack/getDefaultExport.ts | 4 +- .../unpack/webpack/import-export-manager.ts | 4 +- .../webcrack/src/unpack/webpack/module.ts | 68 +++++++++++-------- 4 files changed, 57 insertions(+), 50 deletions(-) diff --git a/packages/webcrack/src/unpack/webpack/bundle.ts b/packages/webcrack/src/unpack/webpack/bundle.ts index cd82e649..068dc83c 100644 --- a/packages/webcrack/src/unpack/webpack/bundle.ts +++ b/packages/webcrack/src/unpack/webpack/bundle.ts @@ -1,15 +1,16 @@ import { Bundle } from '../bundle'; -import { relativePath } from '../path'; -import { WebpackChunk } from './chunk'; -import { convertESM } from './esm'; -import { convertDefaultRequire } from './getDefaultExport'; +import type { WebpackChunk } from './chunk'; import type { WebpackModule } from './module'; export class WebpackBundle extends Bundle { declare modules: Map; chunks: WebpackChunk[]; - constructor(entryId: string, modules: Map, chunks: WebpackChunk[] = []) { + constructor( + entryId: string, + modules: Map, + chunks: WebpackChunk[] = [], + ) { super('webpack', entryId, modules); this.chunks = chunks; } @@ -18,15 +19,15 @@ export class WebpackBundle extends Bundle { * Undoes some of the transformations that Webpack injected into the modules. */ applyTransforms(): void { - this.modules.forEach((module) => { - module.replaceRequireCalls((id) => { - const requiredModule = this.modules.get(id); - return requiredModule - ? { path: relativePath(module.path, requiredModule.path) } - : { path: id, external: true }; - }); - convertESM(module); - }); - convertDefaultRequire(this); + // this.modules.forEach((module) => { + // module.replaceRequireCalls((id) => { + // const requiredModule = this.modules.get(id); + // return requiredModule + // ? { path: relativePath(module.path, requiredModule.path) } + // : { path: id, external: true }; + // }); + // convertESM(module); + // }); + // convertDefaultRequire(this); } } diff --git a/packages/webcrack/src/unpack/webpack/getDefaultExport.ts b/packages/webcrack/src/unpack/webpack/getDefaultExport.ts index a2d9f973..2f112405 100644 --- a/packages/webcrack/src/unpack/webpack/getDefaultExport.ts +++ b/packages/webcrack/src/unpack/webpack/getDefaultExport.ts @@ -74,7 +74,7 @@ export function convertDefaultRequire(bundle: WebpackBundle): void { if (defaultRequireMatcherAlternative.match(path.node)) { // Replace require.n(m).a or require.n(m)() with m or m.default const requiredModule = getRequiredModule(path.scope); - if (requiredModule?.ast.program.sourceType === 'module') { + if (requiredModule?.ast.program.sourceType === 'esm') { path.replaceWith( buildDefaultAccess({ OBJECT: moduleArg.current! }), ); @@ -88,7 +88,7 @@ export function convertDefaultRequire(bundle: WebpackBundle): void { // Replace require.n(m); with m or m.default const requiredModule = getRequiredModule(path.scope); const init = path.get('init'); - if (requiredModule?.ast.program.sourceType === 'module') { + if (requiredModule?.ast.program.sourceType === 'esm') { init.replaceWith( buildDefaultAccess({ OBJECT: moduleArg.current! }), ); diff --git a/packages/webcrack/src/unpack/webpack/import-export-manager.ts b/packages/webcrack/src/unpack/webpack/import-export-manager.ts index 060518dd..347f212c 100644 --- a/packages/webcrack/src/unpack/webpack/import-export-manager.ts +++ b/packages/webcrack/src/unpack/webpack/import-export-manager.ts @@ -2,8 +2,7 @@ import { statement } from '@babel/template'; import { Binding, NodePath, Scope } from '@babel/traverse'; import * as t from '@babel/types'; import * as m from '@codemod/matchers'; -import { applyTransform, generate, renameFast } from '../../ast-utils'; -import definePropertyGetters from './runtime/define-property-getters'; +import { generate, renameFast } from '../../ast-utils'; /** * Example: `__webpack_require__(id)` @@ -50,7 +49,6 @@ export class ImportExportManager { this.ast = ast; this.webpackRequire = webpackRequireBinding; this.collectRequireCalls(); - applyTransform(ast, definePropertyGetters, this); } transformExport(scope: Scope, exportName: string, value: t.Expression) { diff --git a/packages/webcrack/src/unpack/webpack/module.ts b/packages/webcrack/src/unpack/webpack/module.ts index eaa53703..b118e8f0 100644 --- a/packages/webcrack/src/unpack/webpack/module.ts +++ b/packages/webcrack/src/unpack/webpack/module.ts @@ -9,7 +9,14 @@ import { import { Module } from '../module'; import { FunctionPath } from './common-matchers'; import { ImportExportManager } from './import-export-manager'; -import defineExport from './runtime/define-property-getters'; +import { + default as defineExport, + default as definePropertyGetters, +} from './runtime/define-property-getters'; +import global from './runtime/global'; +import hasOwnProperty from './runtime/has-own-property'; +import moduleDecorator from './runtime/module-decorator'; +import namespaceObject from './runtime/namespace-object'; import varInjections from './var-injections'; export class WebpackModule extends Module { @@ -33,6 +40,12 @@ export class WebpackModule extends Module { this.#webpackRequireBinding, ); + applyTransform(file, global, this.#webpackRequireBinding); + applyTransform(file, hasOwnProperty, this.#webpackRequireBinding); + applyTransform(file, moduleDecorator, this.#webpackRequireBinding); + applyTransform(file, namespaceObject); + applyTransform(file, definePropertyGetters, this.#importExportManager); + // this.removeDefineESM(); // // FIXME: some bundles don't define __esModule but still declare esm exports // // https://github.com/0xdevalias/chatgpt-source-watch/blob/main/orig/_next/static/chunks/167-121de668c4456907.js @@ -98,34 +111,29 @@ export class WebpackModule extends Module { * ``` * @internal */ - replaceRequireCalls( - onResolve: (id: string) => { path: string; external?: boolean }, - ): void { - if (!this.#webpackRequireBinding) return; - return; - - const idArg = m.capture(m.or(m.numericLiteral(), m.stringLiteral())); - const requireCall = m.callExpression(m.identifier('__webpack_require__'), [ - idArg, - ]); - - this.#webpackRequireBinding.referencePaths.forEach((path) => { - m.matchPath(requireCall, { idArg }, path.parentPath!, ({ idArg }) => { - const id = idArg.node.value.toString(); - const result = onResolve(id); - - (path.node as t.Identifier).name = 'require'; - idArg.replaceWith(t.stringLiteral(result.path)); - if (result.external) { - idArg.addComment('leading', 'webcrack:missing'); - } - - // this.#imports.push({ - // id, - // path: result.path, - // nodePath: path.parentPath as NodePath, - // }); - }); - }); + replaceRequireCalls() // onResolve: (id: string) => { path: string; external?: boolean }, + : void { + // if (!this.#webpackRequireBinding) return; + // return; + // const idArg = m.capture(m.or(m.numericLiteral(), m.stringLiteral())); + // const requireCall = m.callExpression(m.identifier('__webpack_require__'), [ + // idArg, + // ]); + // this.#webpackRequireBinding.referencePaths.forEach((path) => { + // m.matchPath(requireCall, { idArg }, path.parentPath!, ({ idArg }) => { + // const id = idArg.node.value.toString(); + // const result = onResolve(id); + // (path.node as t.Identifier).name = 'require'; + // idArg.replaceWith(t.stringLiteral(result.path)); + // if (result.external) { + // idArg.addComment('leading', 'webcrack:missing'); + // } + // // this.#imports.push({ + // // id, + // // path: result.path, + // // nodePath: path.parentPath as NodePath, + // // }); + // }); + // }); } } From dce09bd086d1b5d796a80f2e8eae35ed1340b8a4 Mon Sep 17 00:00:00 2001 From: j4k0xb <55899582+j4k0xb@users.noreply.github.com> Date: Sat, 13 Jan 2024 03:20:27 +0100 Subject: [PATCH 30/81] docs: webpack runtime comments --- .../webcrack/src/unpack/webpack/import-export-manager.ts | 2 ++ .../unpack/webpack/runtime/define-property-getters.ts | 8 ++------ packages/webcrack/src/unpack/webpack/runtime/global.ts | 2 +- .../src/unpack/webpack/runtime/has-own-property.ts | 2 +- .../src/unpack/webpack/runtime/module-decorator.ts | 9 ++++----- .../src/unpack/webpack/runtime/namespace-object.ts | 2 -- 6 files changed, 10 insertions(+), 15 deletions(-) diff --git a/packages/webcrack/src/unpack/webpack/import-export-manager.ts b/packages/webcrack/src/unpack/webpack/import-export-manager.ts index 347f212c..da4087ae 100644 --- a/packages/webcrack/src/unpack/webpack/import-export-manager.ts +++ b/packages/webcrack/src/unpack/webpack/import-export-manager.ts @@ -4,6 +4,8 @@ import * as t from '@babel/types'; import * as m from '@codemod/matchers'; import { generate, renameFast } from '../../ast-utils'; +// TODO: hoist re-exports to the top of the file (but retain order relative to imports) + /** * Example: `__webpack_require__(id)` */ diff --git a/packages/webcrack/src/unpack/webpack/runtime/define-property-getters.ts b/packages/webcrack/src/unpack/webpack/runtime/define-property-getters.ts index dfccc4ad..a44a61a4 100644 --- a/packages/webcrack/src/unpack/webpack/runtime/define-property-getters.ts +++ b/packages/webcrack/src/unpack/webpack/runtime/define-property-getters.ts @@ -1,15 +1,11 @@ +import { statement } from '@babel/template'; import * as m from '@codemod/matchers'; import assert from 'assert'; import { Transform, constMemberExpression } from '../../../ast-utils'; import { ImportExportManager } from '../import-export-manager'; -import { statement } from '@babel/template'; - -// TODO: hoist re-exports to the top of the file (but retain order relative to imports) /** - * `webpack/runtime/define property getters` - * - * Used to declare ESM exports. + * `__webpack_require__.d` defines getters on the exports object. Used in ESM. */ export default { name: 'define-property-getters', diff --git a/packages/webcrack/src/unpack/webpack/runtime/global.ts b/packages/webcrack/src/unpack/webpack/runtime/global.ts index 53fe5584..a15c2032 100644 --- a/packages/webcrack/src/unpack/webpack/runtime/global.ts +++ b/packages/webcrack/src/unpack/webpack/runtime/global.ts @@ -3,7 +3,7 @@ import * as t from '@babel/types'; import { Transform, constMemberExpression } from '../../../ast-utils'; /** - * `webpack/runtime/global`, `__webpack_require__.g` + * `__webpack_require__.g` * * webpack injects this when a module accesses `global` */ diff --git a/packages/webcrack/src/unpack/webpack/runtime/has-own-property.ts b/packages/webcrack/src/unpack/webpack/runtime/has-own-property.ts index fdcdcfd5..0ea9297c 100644 --- a/packages/webcrack/src/unpack/webpack/runtime/has-own-property.ts +++ b/packages/webcrack/src/unpack/webpack/runtime/has-own-property.ts @@ -4,7 +4,7 @@ import * as m from '@codemod/matchers'; import { Transform, constMemberExpression } from '../../../ast-utils'; /** - * `webpack/runtime/hasOwnProperty shorthand` + * `__webpack_require__.o` checks if an object has a property. * * Used mostly in other runtime helpers but sometimes it also appears in user code. */ diff --git a/packages/webcrack/src/unpack/webpack/runtime/module-decorator.ts b/packages/webcrack/src/unpack/webpack/runtime/module-decorator.ts index c0926482..2b85bf08 100644 --- a/packages/webcrack/src/unpack/webpack/runtime/module-decorator.ts +++ b/packages/webcrack/src/unpack/webpack/runtime/module-decorator.ts @@ -1,14 +1,13 @@ import * as m from '@codemod/matchers'; import { Transform, constMemberExpression } from '../../../ast-utils'; +// TODO(webpack 4): `module = __webpack_require__('webpack/buildin/harmony-module.js');` +// or `module = __webpack_require__('webpack/buildin/module.js');` + /** - * `webpack/runtime/harmony module decorator` and `webpack/runtime/node module decorator` + * `__webpack_require__.hmd` and `__webpack_require__.nmd` * * The CommonJsPlugin injects this when a module accesses the global 'module' variable. - * - * - // TODO(webpack 4): `module = __webpack_require__('webpack/buildin/harmony-module.js');` - * or `module = __webpack_require__('webpack/buildin/module.js');` - * - webpack 5: `module = __webpack_require__.hmd(module);` or `module = __webpack_require__.nmd(module);` */ export default { name: 'module-decorator', diff --git a/packages/webcrack/src/unpack/webpack/runtime/namespace-object.ts b/packages/webcrack/src/unpack/webpack/runtime/namespace-object.ts index 71d51262..58426457 100644 --- a/packages/webcrack/src/unpack/webpack/runtime/namespace-object.ts +++ b/packages/webcrack/src/unpack/webpack/runtime/namespace-object.ts @@ -2,8 +2,6 @@ import * as m from '@codemod/matchers'; import { Transform, constMemberExpression } from '../../../ast-utils'; /** - * `webpack/runtime/make namespace object` - * * `__webpack_require__.r(__webpack_exports__);` defines `__esModule` on exports. */ export default { From ea6d667e48072e0ec87b8ab6900ccab0f3951f11 Mon Sep 17 00:00:00 2001 From: j4k0xb <55899582+j4k0xb@users.noreply.github.com> Date: Sat, 13 Jan 2024 03:20:27 +0100 Subject: [PATCH 31/81] feat: imports --- .../unpack/webpack/import-export-manager.ts | 89 ++++++++++++++++++- .../webcrack/src/unpack/webpack/module.ts | 1 + 2 files changed, 89 insertions(+), 1 deletion(-) diff --git a/packages/webcrack/src/unpack/webpack/import-export-manager.ts b/packages/webcrack/src/unpack/webpack/import-export-manager.ts index da4087ae..309d41ad 100644 --- a/packages/webcrack/src/unpack/webpack/import-export-manager.ts +++ b/packages/webcrack/src/unpack/webpack/import-export-manager.ts @@ -1,4 +1,4 @@ -import { statement } from '@babel/template'; +import { expression, statement } from '@babel/template'; import { Binding, NodePath, Scope } from '@babel/traverse'; import * as t from '@babel/types'; import * as m from '@codemod/matchers'; @@ -46,6 +46,10 @@ export class ImportExportManager { * Used for merging multiple re-exports of a module */ private reExportCache = new Map(); + /** + * Used for merging multiple imports of a module + */ + private importCache = new Map(); constructor(ast: t.File, webpackRequireBinding: Binding | undefined) { this.ast = ast; @@ -53,6 +57,63 @@ export class ImportExportManager { this.collectRequireCalls(); } + transformImports() { + const property = m.capture(m.anyString()); + const memberExpressionMatcher = m.memberExpression( + m.identifier(), + m.identifier(property), + ); + const zeroSequenceMatcher = m.sequenceExpression([ + m.numericLiteral(0), + m.memberExpression(m.identifier(), m.identifier()), + ]); + + this.requireVars.forEach((requireVar) => { + const importedLocalNames = new Set(); + if ( + requireVar.binding.referencePaths.every((ref) => + memberExpressionMatcher.match(ref.parent), + ) + ) { + requireVar.binding.referencePaths.forEach((reference) => { + const importedName = property.current!; + const hasNameConflict = requireVar.binding.referencePaths.some( + (ref) => ref.scope.hasBinding(importedName), + ); + const localName = hasNameConflict + ? requireVar.binding.path.scope.generateUid(importedName) + : importedName; + + if (!importedLocalNames.has(localName)) { + importedLocalNames.add(localName); + this.addNamedImport(requireVar, localName, importedName); + } + + if (zeroSequenceMatcher.match(reference.parentPath?.parent)) { + reference.parentPath.parentPath!.replaceWith( + t.identifier(localName), + ); + } else { + reference.parentPath!.replaceWith(t.identifier(localName)); + } + if (!memberExpressionMatcher.match(reference.parent)) return; + }); + requireVar.binding.path.remove(); + } else { + requireVar.binding.referencePaths.forEach((reference) => { + reference.replaceWith( + t.identifier(requireVar.binding.identifier.name), + ); + }); + this.addImportAll(requireVar); + } + }); + + this.requireCalls.forEach(({ path, moduleId }) => { + path.replaceWith(expression`require('${moduleId}')`()); + }); + } + transformExport(scope: Scope, exportName: string, value: t.Expression) { this.exports.add(exportName); @@ -100,6 +161,32 @@ export class ImportExportManager { } } + private addNamedImport( + requireVar: RequireVar, + localName: string, + importedName: string, + ) { + const existingImport = this.importCache.get(requireVar.moduleId); + if (existingImport) { + existingImport.specifiers.push( + t.importSpecifier(t.identifier(localName), t.identifier(importedName)), + ); + } else { + // TODO: resolve to file path + const importDeclaration = + statement`import { ${importedName} as ${localName} } from '${requireVar.moduleId}'`() as t.ImportDeclaration; + requireVar.binding.path.parentPath!.insertAfter(importDeclaration); + this.importCache.set(requireVar.moduleId, importDeclaration); + } + } + + private addImportAll(requireVar: RequireVar) { + // TODO: resolve to file path + requireVar.binding.path.parentPath!.replaceWith( + statement`import * as ${requireVar.binding.identifier} from '${requireVar.moduleId}'`(), + ); + } + /** * Example: * ```js diff --git a/packages/webcrack/src/unpack/webpack/module.ts b/packages/webcrack/src/unpack/webpack/module.ts index b118e8f0..18ff89c0 100644 --- a/packages/webcrack/src/unpack/webpack/module.ts +++ b/packages/webcrack/src/unpack/webpack/module.ts @@ -45,6 +45,7 @@ export class WebpackModule extends Module { applyTransform(file, moduleDecorator, this.#webpackRequireBinding); applyTransform(file, namespaceObject); applyTransform(file, definePropertyGetters, this.#importExportManager); + this.#importExportManager.transformImports(); // this.removeDefineESM(); // // FIXME: some bundles don't define __esModule but still declare esm exports From 691794b71e78e322b84bac716d8e22b36dbbf6ae Mon Sep 17 00:00:00 2001 From: j4k0xb <55899582+j4k0xb@users.noreply.github.com> Date: Sat, 13 Jan 2024 03:20:27 +0100 Subject: [PATCH 32/81] docs: todo comments --- packages/webcrack/src/unpack/webpack/import-export-manager.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/webcrack/src/unpack/webpack/import-export-manager.ts b/packages/webcrack/src/unpack/webpack/import-export-manager.ts index 309d41ad..bd5b9039 100644 --- a/packages/webcrack/src/unpack/webpack/import-export-manager.ts +++ b/packages/webcrack/src/unpack/webpack/import-export-manager.ts @@ -5,6 +5,7 @@ import * as m from '@codemod/matchers'; import { generate, renameFast } from '../../ast-utils'; // TODO: hoist re-exports to the top of the file (but retain order relative to imports) +// TODO: when it accesses module.exports, dont convert to esm /** * Example: `__webpack_require__(id)` @@ -168,6 +169,7 @@ export class ImportExportManager { ) { const existingImport = this.importCache.get(requireVar.moduleId); if (existingImport) { + // FIXME: this can import the same name multiple times existingImport.specifiers.push( t.importSpecifier(t.identifier(localName), t.identifier(importedName)), ); @@ -176,6 +178,7 @@ export class ImportExportManager { const importDeclaration = statement`import { ${importedName} as ${localName} } from '${requireVar.moduleId}'`() as t.ImportDeclaration; requireVar.binding.path.parentPath!.insertAfter(importDeclaration); + // FIXME: register binding to avoid duplicate names this.importCache.set(requireVar.moduleId, importDeclaration); } } From 542b0149a72ddc85c51f7218a1bfeb87373b2f4c Mon Sep 17 00:00:00 2001 From: j4k0xb <55899582+j4k0xb@users.noreply.github.com> Date: Sat, 13 Jan 2024 03:20:27 +0100 Subject: [PATCH 33/81] fix: imports being mixed up --- .../src/unpack/webpack/import-export-manager.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/webcrack/src/unpack/webpack/import-export-manager.ts b/packages/webcrack/src/unpack/webpack/import-export-manager.ts index bd5b9039..ba8b945d 100644 --- a/packages/webcrack/src/unpack/webpack/import-export-manager.ts +++ b/packages/webcrack/src/unpack/webpack/import-export-manager.ts @@ -71,12 +71,13 @@ export class ImportExportManager { this.requireVars.forEach((requireVar) => { const importedLocalNames = new Set(); - if ( - requireVar.binding.referencePaths.every((ref) => - memberExpressionMatcher.match(ref.parent), - ) - ) { + const hasOnlyNamedImports = requireVar.binding.referencePaths.every( + (ref) => memberExpressionMatcher.match(ref.parent), + ); + + if (hasOnlyNamedImports) { requireVar.binding.referencePaths.forEach((reference) => { + memberExpressionMatcher.match(reference.parent); // to populate property.current const importedName = property.current!; const hasNameConflict = requireVar.binding.referencePaths.some( (ref) => ref.scope.hasBinding(importedName), @@ -87,6 +88,7 @@ export class ImportExportManager { if (!importedLocalNames.has(localName)) { importedLocalNames.add(localName); + this.addNamedImport(requireVar, localName, importedName); } From 77316346cfa5b6505ebc806b59c7c745c2c8e275 Mon Sep 17 00:00:00 2001 From: j4k0xb <55899582+j4k0xb@users.noreply.github.com> Date: Sat, 13 Jan 2024 03:20:27 +0100 Subject: [PATCH 34/81] fix: don't remove require var without references --- .../unpack/webpack/import-export-manager.ts | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/packages/webcrack/src/unpack/webpack/import-export-manager.ts b/packages/webcrack/src/unpack/webpack/import-export-manager.ts index ba8b945d..7aa17979 100644 --- a/packages/webcrack/src/unpack/webpack/import-export-manager.ts +++ b/packages/webcrack/src/unpack/webpack/import-export-manager.ts @@ -66,24 +66,27 @@ export class ImportExportManager { ); const zeroSequenceMatcher = m.sequenceExpression([ m.numericLiteral(0), - m.memberExpression(m.identifier(), m.identifier()), + m.memberExpression(m.identifier(), m.identifier(property)), ]); this.requireVars.forEach((requireVar) => { + const { binding } = requireVar; const importedLocalNames = new Set(); - const hasOnlyNamedImports = requireVar.binding.referencePaths.every( - (ref) => memberExpressionMatcher.match(ref.parent), - ); + const hasOnlyNamedImports = + binding.references > 0 && + binding.referencePaths.every((ref) => + memberExpressionMatcher.match(ref.parent), + ); if (hasOnlyNamedImports) { - requireVar.binding.referencePaths.forEach((reference) => { + binding.referencePaths.forEach((reference) => { memberExpressionMatcher.match(reference.parent); // to populate property.current const importedName = property.current!; - const hasNameConflict = requireVar.binding.referencePaths.some( - (ref) => ref.scope.hasBinding(importedName), + const hasNameConflict = binding.referencePaths.some((ref) => + ref.scope.hasBinding(importedName), ); const localName = hasNameConflict - ? requireVar.binding.path.scope.generateUid(importedName) + ? binding.path.scope.generateUid(importedName) : importedName; if (!importedLocalNames.has(localName)) { @@ -101,12 +104,10 @@ export class ImportExportManager { } if (!memberExpressionMatcher.match(reference.parent)) return; }); - requireVar.binding.path.remove(); + binding.path.remove(); } else { - requireVar.binding.referencePaths.forEach((reference) => { - reference.replaceWith( - t.identifier(requireVar.binding.identifier.name), - ); + binding.referencePaths.forEach((reference) => { + reference.replaceWith(t.identifier(binding.identifier.name)); }); this.addImportAll(requireVar); } From afbabb8a30f4487cd8e789b8bb7b59d03ccacd12 Mon Sep 17 00:00:00 2001 From: j4k0xb <55899582+j4k0xb@users.noreply.github.com> Date: Sat, 13 Jan 2024 03:20:27 +0100 Subject: [PATCH 35/81] fix: normalize module paths --- packages/webcrack/src/unpack/module.ts | 18 ++++++------------ packages/webcrack/src/unpack/path.ts | 2 +- packages/webcrack/src/unpack/test/path.test.ts | 1 + 3 files changed, 8 insertions(+), 13 deletions(-) diff --git a/packages/webcrack/src/unpack/module.ts b/packages/webcrack/src/unpack/module.ts index c19d83af..2d1bdde0 100644 --- a/packages/webcrack/src/unpack/module.ts +++ b/packages/webcrack/src/unpack/module.ts @@ -1,17 +1,7 @@ -import type { NodePath } from '@babel/traverse'; import type * as t from '@babel/types'; +import { normalize } from 'node:path'; import { generate } from '../ast-utils'; -export interface Import { - path: string; - id: string; - /** - * E.g. `require('./foo.js')` or `import './foo.js'` - * @internal - */ - nodePath: NodePath; -} - export class Module { id: string; isEntry: boolean; @@ -26,7 +16,11 @@ export class Module { this.id = id; this.ast = ast; this.isEntry = isEntry; - this.path = `./${isEntry ? 'index' : id}.js`; + this.path = this.normalizePath(isEntry ? 'index' : id); + } + + private normalizePath(path: string): string { + return normalize(path.endsWith('.js') ? path : `${path}.js`); } /** 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/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', () => { From 024df05bfe97a63393c288b90a830722d6bb4638 Mon Sep 17 00:00:00 2001 From: j4k0xb <55899582+j4k0xb@users.noreply.github.com> Date: Sat, 13 Jan 2024 03:20:27 +0100 Subject: [PATCH 36/81] feat: more specific webpack parameter names, handle custom namespace object --- .../test/define-property-getters.test.ts | 40 +++++++++---------- .../webcrack/src/unpack/test/global.test.ts | 4 +- packages/webcrack/src/unpack/test/index.ts | 10 ++++- .../src/unpack/test/namespace-object.test.ts | 17 +++++++- .../src/unpack/webpack/common-matchers.ts | 1 + .../webcrack/src/unpack/webpack/module.ts | 6 ++- .../runtime/define-property-getters.ts | 2 +- .../webpack/runtime/namespace-object.ts | 11 ++++- 8 files changed, 63 insertions(+), 28 deletions(-) diff --git a/packages/webcrack/src/unpack/test/define-property-getters.test.ts b/packages/webcrack/src/unpack/test/define-property-getters.test.ts index 19007267..30d40848 100644 --- a/packages/webcrack/src/unpack/test/define-property-getters.test.ts +++ b/packages/webcrack/src/unpack/test/define-property-getters.test.ts @@ -12,24 +12,24 @@ const expectJS = testWebpackModuleTransform( describe('webpack 4', () => { test('export default expression;', () => expectJS(` - exports.default = 1; + __webpack_exports__.default = 1; `).toMatchInlineSnapshot(`export default 1;`)); test('export named', () => expectJS(` - __webpack_require__.d(exports, "counter", function() { return foo; }); + __webpack_require__.d(__webpack_exports__, "counter", function() { return foo; }); var foo = 1; `).toMatchInlineSnapshot(`export var counter = 1;`)); test('export default variable', () => expectJS(` - __webpack_require__.d(exports, "default", function() { return foo; }); + __webpack_require__.d(__webpack_exports__, "default", function() { return foo; }); var foo = 1; `).toMatchInlineSnapshot(`export default 1;`)); test('export default variable with multiple references', () => expectJS(` - __webpack_require__.d(exports, "default", function() { return foo; }); + __webpack_require__.d(__webpack_exports__, "default", function() { return foo; }); var foo = 1; console.log(foo); `).toMatchInlineSnapshot(` @@ -40,19 +40,19 @@ describe('webpack 4', () => { test('export default function', () => expectJS(` - __webpack_require__.d(exports, "default", function() { return foo; }); + __webpack_require__.d(__webpack_exports__, "default", function() { return foo; }); function foo() {} `).toMatchInlineSnapshot(`export default function foo() {}`)); test('export default class', () => expectJS(` - __webpack_require__.d(exports, "default", function() { return foo; }); + __webpack_require__.d(__webpack_exports__, "default", function() { return foo; }); class foo {} `).toMatchInlineSnapshot(`export default class foo {}`)); test('re-export named', () => expectJS(` - __webpack_require__.d(exports, "readFile", function() { return lib.readFile; }); + __webpack_require__.d(__webpack_exports__, "readFile", function() { return lib.readFile; }); var lib = __webpack_require__("lib"); `).toMatchInlineSnapshot(` var lib = __webpack_require__("lib"); @@ -61,7 +61,7 @@ describe('webpack 4', () => { test('re-export named with multiple references', () => expectJS(` - __webpack_require__.d(exports, "readFile", function() { return lib.readFile; }); + __webpack_require__.d(__webpack_exports__, "readFile", function() { return lib.readFile; }); var lib = __webpack_require__("lib"); lib.writeFile(); `).toMatchInlineSnapshot(` @@ -72,7 +72,7 @@ describe('webpack 4', () => { test('re-export named as named', () => expectJS(` - __webpack_require__.d(exports, "foo", function() { return lib.readFile; }); + __webpack_require__.d(__webpack_exports__, "foo", function() { return lib.readFile; }); var lib = __webpack_require__("lib"); `).toMatchInlineSnapshot(` var lib = __webpack_require__("lib"); @@ -81,7 +81,7 @@ describe('webpack 4', () => { test('re-export named as default', () => expectJS(` - __webpack_require__.d(exports, "default", function() { return lib.readFile; }); + __webpack_require__.d(__webpack_exports__, "default", function() { return lib.readFile; }); var lib = __webpack_require__("lib"); `).toMatchInlineSnapshot(` var lib = __webpack_require__("lib"); @@ -90,7 +90,7 @@ describe('webpack 4', () => { test('re-export default as named', () => expectJS(` - __webpack_require__.d(exports, "foo", function() { return lib.default; }); + __webpack_require__.d(__webpack_exports__, "foo", function() { return lib.default; }); var lib = __webpack_require__("lib"); `).toMatchInlineSnapshot(` var lib = __webpack_require__("lib"); @@ -99,7 +99,7 @@ describe('webpack 4', () => { test('re-export default as default', () => expectJS(` - __webpack_require__.d(exports, "default", function() { return lib.default; }); + __webpack_require__.d(__webpack_exports__, "default", function() { return lib.default; }); var lib = __webpack_require__("lib"); `).toMatchInlineSnapshot(` var lib = __webpack_require__("lib"); @@ -117,7 +117,7 @@ describe('webpack 4', () => { for (var importKey in lib) { if (["default"].indexOf(importKey) < 0) { (function (key) { - __webpack_require__.d(exports, key, function () { + __webpack_require__.d(__webpack_exports__, key, function () { return lib[key]; }); })(importKey); @@ -130,7 +130,7 @@ describe('webpack 4', () => { test('re-export all as named', () => expectJS(` - __webpack_require__.d(exports, "lib", function() { return lib; }); + __webpack_require__.d(__webpack_exports__, "lib", function() { return lib; }); var lib = __webpack_require__("lib"); `).toMatchInlineSnapshot(` var lib = __webpack_require__("lib"); @@ -139,7 +139,7 @@ describe('webpack 4', () => { test.todo('re-export all as default', () => expectJS(` - __webpack_require__.d(exports, "default", function() { return lib; }); + __webpack_require__.d(__webpack_exports__, "default", function() { return lib; }); var lib = __webpack_require__("lib"); `).toMatchInlineSnapshot(`export * as default from "lib";`), ); @@ -148,7 +148,7 @@ describe('webpack 4', () => { describe('webpack 5', () => { test('export named', () => expectJS(` - __webpack_require__.d(exports, { + __webpack_require__.d(__webpack_exports__, { counter: () => foo }); var foo = 1; @@ -158,7 +158,7 @@ describe('webpack 5', () => { test('export same variable with multiple names', () => expectJS(` - __webpack_require__.d(exports, { + __webpack_require__.d(__webpack_exports__, { counter: () => foo, increment: () => foo, }); @@ -210,7 +210,7 @@ describe('webpack 5', () => { test.todo('export as invalid identifier string name', () => expectJS(` - __webpack_require__.d(exports, { + __webpack_require__.d(__webpack_exports__, { "...": () => foo }); var foo = 1; @@ -222,7 +222,7 @@ describe('webpack 5', () => { test('re-export named merging', () => expectJS(` - __webpack_require__.d(exports, { + __webpack_require__.d(__webpack_exports__, { readFile: () => lib.readFile, writeFile: () => lib.writeFile, }); @@ -242,7 +242,7 @@ describe('webpack 5', () => { reExportObject[importKey] = () => lib[importKey]; } } - __webpack_require__.d(exports, reExportObject); + __webpack_require__.d(__webpack_exports__, reExportObject); `).toMatchInlineSnapshot(` export * from "./lib"; `), diff --git a/packages/webcrack/src/unpack/test/global.test.ts b/packages/webcrack/src/unpack/test/global.test.ts index 81f78e8c..ae40697c 100644 --- a/packages/webcrack/src/unpack/test/global.test.ts +++ b/packages/webcrack/src/unpack/test/global.test.ts @@ -17,7 +17,7 @@ test('replace __webpack_require__.g with global', () => test('a', () => { const ast = parse(` - (function(module, exports, __webpack_require__) { + (function(__webpack_module__, __webpack_exports__, __webpack_require__) { __webpack_require__.g.setTimeout(() => {}); }); `); @@ -27,7 +27,7 @@ test('a', () => { const binding = path.scope.bindings.__webpack_require__; applyTransform(path.node, global, binding); expect(path.node).toMatchInlineSnapshot(` - function (module, exports, __webpack_require__) { + function (__webpack_module__, __webpack_exports__, __webpack_require__) { global.setTimeout(() => {}); } `); diff --git a/packages/webcrack/src/unpack/test/index.ts b/packages/webcrack/src/unpack/test/index.ts index 935468da..55711227 100644 --- a/packages/webcrack/src/unpack/test/index.ts +++ b/packages/webcrack/src/unpack/test/index.ts @@ -5,7 +5,13 @@ import { Assertion, expect } from 'vitest'; import { Transform, applyTransform } from '../../ast-utils'; /** - * Test a transform with the input being wrapped in `(function(module, exports, __webpack_require__) { ... })` + * Test a transform with the input being wrapped with + * ```js + * (function(__webpack_module__, __webpack_exports__, __webpack_require__) { + * // input + * }); + * ``` + * * @param transform the transform to apply * @param cb specify the options that will be passed to the transform */ @@ -15,7 +21,7 @@ export function testWebpackModuleTransform( ): (input: string) => Assertion> { return (input) => { const moduleCode = ` - (function(module, exports, __webpack_require__) { + (function(__webpack_module__, __webpack_exports__, __webpack_require__) { ${input} }); `; diff --git a/packages/webcrack/src/unpack/test/namespace-object.test.ts b/packages/webcrack/src/unpack/test/namespace-object.test.ts index 104c83a5..4878e1de 100644 --- a/packages/webcrack/src/unpack/test/namespace-object.test.ts +++ b/packages/webcrack/src/unpack/test/namespace-object.test.ts @@ -6,6 +6,21 @@ const expectJS = testTransform(namespaceObject); test('remove the __esModule property', () => { const result = { isESM: false }; - expectJS(`__webpack_require__.r(exports);`, result).toMatchInlineSnapshot(``); + 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(``); expect(result.isESM).toBe(true); }); diff --git a/packages/webcrack/src/unpack/webpack/common-matchers.ts b/packages/webcrack/src/unpack/webpack/common-matchers.ts index 01164ca4..95597d66 100644 --- a/packages/webcrack/src/unpack/webpack/common-matchers.ts +++ b/packages/webcrack/src/unpack/webpack/common-matchers.ts @@ -24,6 +24,7 @@ export function webpackRequireFunctionMatcher() { m.functionDeclaration( m.identifier(), // __webpack_require__ [m.identifier()], // moduleId + // TODO(perf): use m.anyList for statements m.containerOf( m.callExpression( m.or( diff --git a/packages/webcrack/src/unpack/webpack/module.ts b/packages/webcrack/src/unpack/webpack/module.ts index 18ff89c0..d7fd46cf 100644 --- a/packages/webcrack/src/unpack/webpack/module.ts +++ b/packages/webcrack/src/unpack/webpack/module.ts @@ -30,7 +30,11 @@ export class WebpackModule extends Module { const file = t.file(t.program(ast.node.body.body)); super(id, file, isEntry); - renameParameters(ast, ['module', 'exports', '__webpack_require__']); + renameParameters(ast, [ + '__webpack_module__', + '__webpack_exports__', + '__webpack_require__', + ]); applyTransform(file, varInjections); this.removeTrailingComments(); diff --git a/packages/webcrack/src/unpack/webpack/runtime/define-property-getters.ts b/packages/webcrack/src/unpack/webpack/runtime/define-property-getters.ts index a44a61a4..ca3aac49 100644 --- a/packages/webcrack/src/unpack/webpack/runtime/define-property-getters.ts +++ b/packages/webcrack/src/unpack/webpack/runtime/define-property-getters.ts @@ -37,7 +37,7 @@ export default { const defaultExportAssignment = m.expressionStatement( m.assignmentExpression( '=', - constMemberExpression('exports', 'default'), + constMemberExpression('__webpack_exports__', 'default'), returnValue, ), ); diff --git a/packages/webcrack/src/unpack/webpack/runtime/namespace-object.ts b/packages/webcrack/src/unpack/webpack/runtime/namespace-object.ts index 58426457..ae307aa3 100644 --- a/packages/webcrack/src/unpack/webpack/runtime/namespace-object.ts +++ b/packages/webcrack/src/unpack/webpack/runtime/namespace-object.ts @@ -3,14 +3,20 @@ import { Transform, constMemberExpression } from '../../../ast-utils'; /** * `__webpack_require__.r(__webpack_exports__);` defines `__esModule` on exports. + * + * When the [ModuleConcatenationPlugin](https://webpack.js.org/plugins/module-concatenation-plugin/) + * is enabled, it can also use a namespace object variable: + * https://github.com/webpack/webpack/blob/dfffd6a241bf1d593b3fd31b4b279f96f4a4aab1/lib/optimize/ConcatenatedModule.js#L58 */ export default { name: 'namespace-object', tags: ['safe'], + scope: true, visitor(options = { isESM: false }) { + const exportsName = m.capture(m.anyString()); const matcher = m.expressionStatement( m.callExpression(constMemberExpression('__webpack_require__', 'r'), [ - m.identifier(), + m.identifier(exportsName), ]), ); @@ -20,6 +26,9 @@ export default { if (!matcher.match(path.node)) return; options.isESM = true; path.remove(); + if (exportsName.current !== '__webpack_exports__') { + path.scope.getBinding(exportsName.current!)?.path.remove(); + } this.changes++; }, }; From 2075348c89b73dbedc5634cff4899aee198bc447 Mon Sep 17 00:00:00 2001 From: j4k0xb <55899582+j4k0xb@users.noreply.github.com> Date: Sat, 13 Jan 2024 03:20:27 +0100 Subject: [PATCH 37/81] fix: properly implement concatenated module exports --- .../test/define-property-getters.test.ts | 40 +++++++++++ .../src/unpack/test/namespace-object.test.ts | 8 ++- .../runtime/define-property-getters.ts | 66 ++++++++++++++----- .../webpack/runtime/namespace-object.ts | 32 +++++++-- 4 files changed, 123 insertions(+), 23 deletions(-) diff --git a/packages/webcrack/src/unpack/test/define-property-getters.test.ts b/packages/webcrack/src/unpack/test/define-property-getters.test.ts index 30d40848..1b7afb4b 100644 --- a/packages/webcrack/src/unpack/test/define-property-getters.test.ts +++ b/packages/webcrack/src/unpack/test/define-property-getters.test.ts @@ -143,6 +143,21 @@ describe('webpack 4', () => { 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', () => { @@ -247,4 +262,29 @@ describe('webpack 5', () => { 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() {} + `)); }); diff --git a/packages/webcrack/src/unpack/test/namespace-object.test.ts b/packages/webcrack/src/unpack/test/namespace-object.test.ts index 4878e1de..19ea73bf 100644 --- a/packages/webcrack/src/unpack/test/namespace-object.test.ts +++ b/packages/webcrack/src/unpack/test/namespace-object.test.ts @@ -21,6 +21,12 @@ test('remove the __esModule property from a namespace object', () => { __webpack_require__.r(lib_namespaceObject); `, result, - ).toMatchInlineSnapshot(``); + ).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/webpack/runtime/define-property-getters.ts b/packages/webcrack/src/unpack/webpack/runtime/define-property-getters.ts index ca3aac49..2433c4bb 100644 --- a/packages/webcrack/src/unpack/webpack/runtime/define-property-getters.ts +++ b/packages/webcrack/src/unpack/webpack/runtime/define-property-getters.ts @@ -1,4 +1,5 @@ import { statement } from '@babel/template'; +import { NodePath } from '@babel/traverse'; import * as m from '@codemod/matchers'; import assert from 'assert'; import { Transform, constMemberExpression } from '../../../ast-utils'; @@ -6,6 +7,12 @@ import { ImportExportManager } from '../import-export-manager'; /** * `__webpack_require__.d` defines getters on the exports object. Used in ESM. + * + * When the [ModuleConcatenationPlugin](https://webpack.js.org/plugins/module-concatenation-plugin/) + * is enabled, it can use a namespace object variable instead: + * https://github.com/webpack/webpack/blob/dfffd6a241bf1d593b3fd31b4b279f96f4a4aab1/lib/optimize/ConcatenatedModule.js#L58 + * + * It is very hard to separate concatenated modules again, so it will only transform the main module. */ export default { name: 'define-property-getters', @@ -14,6 +21,7 @@ export default { visitor(manager) { assert(manager); + const namespaceObject = m.capture(m.anyString()); const exportName = m.capture(m.anyString()); const returnValue = m.capture(m.anyExpression()); const getter = m.or( @@ -24,16 +32,16 @@ export default { ), m.arrowFunctionExpression([], returnValue), ); - // Example (webpack v4): __webpack_require__.d(exports, 'counter', function () { return local }); + // Example (webpack v4): __webpack_require__.d(__webpack_exports__, 'counter', function () { return local }); const singleExport = m.expressionStatement( m.callExpression(constMemberExpression('__webpack_require__', 'd'), [ - m.identifier(), + m.identifier(namespaceObject), m.stringLiteral(exportName), getter, ]), ); - // Example (webpack v4): exports.default = 1; + // Example (webpack v4): __webpack_exports__.default = 1; const defaultExportAssignment = m.expressionStatement( m.assignmentExpression( '=', @@ -44,24 +52,48 @@ export default { const objectProperty = m.objectProperty(m.identifier(exportName), getter); const properties = m.capture(m.arrayOf(objectProperty)); - // Example (webpack v5): __webpack_require__.d(exports, { a: () => b, c: () => d }); + // Example (webpack v5): __webpack_require__.d(__webpack_exports__, { a: () => b, c: () => d }); const multiExport = m.expressionStatement( m.callExpression(constMemberExpression('__webpack_require__', 'd'), [ - m.identifier(), + m.identifier(namespaceObject), m.objectExpression(properties), ]), ); + /** + * Used only for concatenated modules where we can't convert to ESM exports, so it still works at runtime. + */ + function definePropertyExport(path: NodePath) { + const [replacement] = path.insertAfter( + statement`Object.defineProperty(${namespaceObject.current!}, '${ + exportName.current + }', { + enumerable: true, + get: () => ${returnValue.current} + });`(), + ); + replacement.scope.crawl(); + replacement.addComment( + 'leading', + 'webcrack:concatenated-module-export', + true, + ); + } + return { ExpressionStatement: (path) => { if (!path.parentPath.isProgram()) return path.skip(); if (singleExport.match(path.node)) { - manager.transformExport( - path.scope, - exportName.current!, - returnValue.current!, - ); + if (namespaceObject.current === '__webpack_exports__') { + manager.transformExport( + path.scope, + exportName.current!, + returnValue.current!, + ); + } else { + definePropertyExport(path); + } path.remove(); } else if (defaultExportAssignment.match(path.node)) { manager.exports.add('default'); @@ -69,11 +101,15 @@ export default { } else if (multiExport.match(path.node)) { for (const property of properties.current!) { objectProperty.match(property); // To easily get the captures per property - manager.transformExport( - path.scope, - exportName.current!, - returnValue.current!, - ); + if (namespaceObject.current === '__webpack_exports__') { + manager.transformExport( + path.scope, + exportName.current!, + returnValue.current!, + ); + } else { + definePropertyExport(path); + } } path.remove(); } diff --git a/packages/webcrack/src/unpack/webpack/runtime/namespace-object.ts b/packages/webcrack/src/unpack/webpack/runtime/namespace-object.ts index ae307aa3..3964518c 100644 --- a/packages/webcrack/src/unpack/webpack/runtime/namespace-object.ts +++ b/packages/webcrack/src/unpack/webpack/runtime/namespace-object.ts @@ -1,3 +1,4 @@ +import { statement } from '@babel/template'; import * as m from '@codemod/matchers'; import { Transform, constMemberExpression } from '../../../ast-utils'; @@ -5,18 +6,20 @@ import { Transform, constMemberExpression } from '../../../ast-utils'; * `__webpack_require__.r(__webpack_exports__);` defines `__esModule` on exports. * * When the [ModuleConcatenationPlugin](https://webpack.js.org/plugins/module-concatenation-plugin/) - * is enabled, it can also use a namespace object variable: + * is enabled, it can use a namespace object variable instead: * https://github.com/webpack/webpack/blob/dfffd6a241bf1d593b3fd31b4b279f96f4a4aab1/lib/optimize/ConcatenatedModule.js#L58 + * + * It is very hard to separate concatenated modules again, so it will only transform the main module. */ export default { name: 'namespace-object', tags: ['safe'], scope: true, visitor(options = { isESM: false }) { - const exportsName = m.capture(m.anyString()); + const namespaceObject = m.capture(m.anyString()); const matcher = m.expressionStatement( m.callExpression(constMemberExpression('__webpack_require__', 'r'), [ - m.identifier(exportsName), + m.identifier(namespaceObject), ]), ); @@ -24,11 +27,26 @@ export default { ExpressionStatement(path) { if (!path.parentPath.isProgram()) return path.skip(); if (!matcher.match(path.node)) return; - options.isESM = true; - path.remove(); - if (exportsName.current !== '__webpack_exports__') { - path.scope.getBinding(exportsName.current!)?.path.remove(); + + if (namespaceObject.current === '__webpack_exports__') { + path.remove(); + } else { + const [replacement] = path.replaceWith( + statement`Object.defineProperty(${namespaceObject.current!}, "__esModule", { value: true });`(), + ); + replacement.scope.crawl(); + + const binding = path.scope.getBinding(namespaceObject.current!); + binding?.path + .getStatementParent() + ?.addComment( + 'leading', + 'webcrack:concatenated-module-namespace-object', + true, + ); } + + options.isESM = true; this.changes++; }, }; From 4faef28f81eeaf55d71a24b4f88851b9e4e149f4 Mon Sep 17 00:00:00 2001 From: j4k0xb <55899582+j4k0xb@users.noreply.github.com> Date: Sat, 13 Jan 2024 03:20:27 +0100 Subject: [PATCH 38/81] feat: default import --- .../unpack/test/get-default-export.test.ts | 35 ++++++++++ .../unpack/webpack/import-export-manager.ts | 54 ++++++++++++--- .../webcrack/src/unpack/webpack/module.ts | 2 + .../webpack/runtime/get-default-export.ts | 67 +++++++++++++++++++ 4 files changed, 149 insertions(+), 9 deletions(-) create mode 100644 packages/webcrack/src/unpack/test/get-default-export.test.ts create mode 100644 packages/webcrack/src/unpack/webpack/runtime/get-default-export.ts diff --git a/packages/webcrack/src/unpack/test/get-default-export.test.ts b/packages/webcrack/src/unpack/test/get-default-export.test.ts new file mode 100644 index 00000000..1ad49e4b --- /dev/null +++ b/packages/webcrack/src/unpack/test/get-default-export.test.ts @@ -0,0 +1,35 @@ +import { test } from 'vitest'; +import { testWebpackModuleTransform } from '.'; +import { ImportExportManager } from '../webpack/import-export-manager'; +import getDefaultExport from '../webpack/runtime/get-default-export'; + +const expectJS = testWebpackModuleTransform( + getDefaultExport, + ({ scope }, ast) => + new ImportExportManager(ast, scope.bindings.__webpack_require__), +); + +test('replace default import', () => + expectJS(` + var module = __webpack_require__(1); + var _tmp = __webpack_require__.n(module); + console.log(_tmp.a); + console.log(_tmp()); + `).toMatchInlineSnapshot(` + import _temp from "1"; + var module = __webpack_require__(1); + console.log(_temp); + console.log(_temp); + `)); + +test('replace inlined default import', () => + expectJS(` + var module = __webpack_require__(1); + console.log(__webpack_require__.n(module).a); + console.log(__webpack_require__.n(module)()); + `).toMatchInlineSnapshot(` + import _temp from "1"; + var module = __webpack_require__(1); + console.log(_temp); + console.log(_temp); + `)); diff --git a/packages/webcrack/src/unpack/webpack/import-export-manager.ts b/packages/webcrack/src/unpack/webpack/import-export-manager.ts index 7aa17979..e4f2f133 100644 --- a/packages/webcrack/src/unpack/webpack/import-export-manager.ts +++ b/packages/webcrack/src/unpack/webpack/import-export-manager.ts @@ -33,16 +33,18 @@ export class ImportExportManager { */ exports = new Set(); - private ast: t.File; - private webpackRequire: Binding | undefined; /** * All `var foo = __webpack_require__(id);` variable declarations */ - private requireVars: RequireVar[] = []; + requireVars: RequireVar[] = []; /** * All `__webpack_require__(id)` calls */ - private requireCalls: RequireCall[] = []; + requireCalls: RequireCall[] = []; + + webpackRequire: Binding | undefined; + + private ast: t.File; /** * Used for merging multiple re-exports of a module */ @@ -128,13 +130,10 @@ export class ImportExportManager { m.identifier(propertyName), ); - const findRequireVar = (binding: Binding) => - this.requireVars.find((v) => v.binding.path.node === binding.path.node); - if (t.isIdentifier(value)) { const binding = scope.getBinding(value.name); if (!binding) return; - const requireVar = findRequireVar(binding); + const requireVar = this.findRequireVar(binding.path.node); if (requireVar) { this.addExportAll(binding, requireVar.moduleId, exportName); @@ -146,7 +145,7 @@ export class ImportExportManager { } else if (memberExpressionMatcher.match(value)) { const binding = scope.getBinding(objectName.current!); if (!binding) return; - const requireVar = findRequireVar(binding); + const requireVar = this.findRequireVar(binding.path.node); if (!requireVar) return; this.addExportFrom( @@ -165,6 +164,43 @@ export class ImportExportManager { } } + /** + * Find the `var = __webpack_require__();` statement that belongs to a binding node + */ + findRequireVar(node: t.Node) { + return this.requireVars.find((v) => v.binding.path.node === node); + } + + /** + * @returns local name of the default import + */ + addDefaultImport(requireVar: RequireVar) { + const { moduleId } = requireVar; + const existingImport = this.importCache.get(moduleId); + const localName = requireVar.binding.scope.generateUid(); + + if (existingImport) { + const existingDefaultImport = existingImport.specifiers.find( + (specifier) => t.isImportDefaultSpecifier(specifier), + ); + if (existingDefaultImport) return existingDefaultImport.local.name; + + existingImport.specifiers.push( + t.importDefaultSpecifier(t.identifier(localName)), + ); + } else { + // TODO: resolve to file path + const importDeclaration = + statement`import ${localName} from '${moduleId}'`() as t.ImportDeclaration; + // TODO: find better place to insert + this.ast.program.body.unshift(importDeclaration); + // FIXME: register binding + this.importCache.set(moduleId, importDeclaration); + } + + return localName; + } + private addNamedImport( requireVar: RequireVar, localName: string, diff --git a/packages/webcrack/src/unpack/webpack/module.ts b/packages/webcrack/src/unpack/webpack/module.ts index d7fd46cf..6d06a9a4 100644 --- a/packages/webcrack/src/unpack/webpack/module.ts +++ b/packages/webcrack/src/unpack/webpack/module.ts @@ -18,6 +18,7 @@ import hasOwnProperty from './runtime/has-own-property'; import moduleDecorator from './runtime/module-decorator'; import namespaceObject from './runtime/namespace-object'; import varInjections from './var-injections'; +import getDefaultExport from './runtime/get-default-export'; export class WebpackModule extends Module { #webpackRequireBinding: Binding | undefined; @@ -48,6 +49,7 @@ export class WebpackModule extends Module { applyTransform(file, hasOwnProperty, this.#webpackRequireBinding); applyTransform(file, moduleDecorator, this.#webpackRequireBinding); applyTransform(file, namespaceObject); + applyTransform(file, getDefaultExport, this.#importExportManager); applyTransform(file, definePropertyGetters, this.#importExportManager); this.#importExportManager.transformImports(); diff --git a/packages/webcrack/src/unpack/webpack/runtime/get-default-export.ts b/packages/webcrack/src/unpack/webpack/runtime/get-default-export.ts new file mode 100644 index 00000000..ecef0123 --- /dev/null +++ b/packages/webcrack/src/unpack/webpack/runtime/get-default-export.ts @@ -0,0 +1,67 @@ +import * as t from '@babel/types'; +import * as m from '@codemod/matchers'; +import assert from 'assert'; +import { Transform, constMemberExpression } from '../../../ast-utils'; +import { ImportExportManager } from '../import-export-manager'; + +// var a = __webpack_require__(11); var i = __webpack_require__.n(a); let p = i.a; +// var c = __webpack_require__(11); let h = __webpack_require__.n(c).a; +// var i = __webpack_require__(11); __webpack_require__.n(i).a; +// const dDefault = __webpack_require__.n(b).a; +// const eDefault = __webpack_require__.n(c)(); + +/** + * `__webpack_require__.n` is used when declaring a default import from a commonjs module. + */ +export default { + name: 'get-default-export', + tags: ['safe'], + run(ast, manager) { + assert(manager); + + const moduleVar = m.capture(m.anyString()); + const getDefaultExportMatcher = m.callExpression( + constMemberExpression('__webpack_require__', 'n'), + [m.identifier(moduleVar)], + ); + const tmpVarName = m.capture(m.anyString()); + const getDefaultExportVarMatcher = m.variableDeclarator( + m.identifier(tmpVarName), + getDefaultExportMatcher, + ); + + manager.webpackRequire?.referencePaths.forEach((reference) => { + // `__webpack_require__.n(moduleVar)` + const callPath = reference.parentPath?.parentPath; + if (!getDefaultExportMatcher.match(callPath?.node)) return; + + // Example: `__webpack_require__.n(moduleVar).a` + const isInlined = !getDefaultExportVarMatcher.match(callPath.parent); + if (isInlined) { + const moduleBinding = reference.scope.getBinding(moduleVar.current!); + if (!moduleBinding) return; + const requireVar = manager.findRequireVar(moduleBinding.path.node); + if (!requireVar) return; + + const importName = manager.addDefaultImport(requireVar); + callPath.parentPath?.replaceWith(t.identifier(importName)); + this.changes++; + return; + } + + const tmpVarBinding = reference.scope.getBinding(tmpVarName.current!); + const moduleBinding = reference.scope.getBinding(moduleVar.current!); + if (!moduleBinding || !tmpVarBinding) return; + const requireVar = manager.findRequireVar(moduleBinding.path.node); + if (!requireVar) return; + + const importName = manager.addDefaultImport(requireVar); + // `_tmp.a` -> `importName` or `_tmp()` -> `importName()` + tmpVarBinding.referencePaths.forEach((refPath) => { + refPath.parentPath?.replaceWith(t.identifier(importName)); + }); + tmpVarBinding.path.remove(); + this.changes++; + }); + }, +} satisfies Transform; From 7a7c00cd92c432bc855c885ad75a4532fe936c7e Mon Sep 17 00:00:00 2001 From: j4k0xb <55899582+j4k0xb@users.noreply.github.com> Date: Sat, 13 Jan 2024 03:20:27 +0100 Subject: [PATCH 39/81] refactor: import/export management --- .../test/define-property-getters.test.ts | 18 +- .../unpack/test/get-default-export.test.ts | 22 +-- .../webcrack/src/unpack/test/global.test.ts | 5 +- .../src/unpack/test/has-own-property.test.ts | 6 +- packages/webcrack/src/unpack/test/index.ts | 25 +-- .../unpack/webpack/import-export-manager.ts | 183 ++++++++++-------- .../webcrack/src/unpack/webpack/module.ts | 4 +- .../runtime/define-property-getters.ts | 4 +- .../webpack/runtime/get-default-export.ts | 4 +- 9 files changed, 136 insertions(+), 135 deletions(-) diff --git a/packages/webcrack/src/unpack/test/define-property-getters.test.ts b/packages/webcrack/src/unpack/test/define-property-getters.test.ts index 1b7afb4b..7517033c 100644 --- a/packages/webcrack/src/unpack/test/define-property-getters.test.ts +++ b/packages/webcrack/src/unpack/test/define-property-getters.test.ts @@ -1,13 +1,7 @@ import { describe, test } from 'vitest'; import { testWebpackModuleTransform } from '.'; -import { ImportExportManager } from '../webpack/import-export-manager'; -import definePropertyGetters from '../webpack/runtime/define-property-getters'; -const expectJS = testWebpackModuleTransform( - definePropertyGetters, - ({ scope }, ast) => - new ImportExportManager(ast, scope.bindings.__webpack_require__), -); +const expectJS = testWebpackModuleTransform(); describe('webpack 4', () => { test('export default expression;', () => @@ -55,7 +49,6 @@ describe('webpack 4', () => { __webpack_require__.d(__webpack_exports__, "readFile", function() { return lib.readFile; }); var lib = __webpack_require__("lib"); `).toMatchInlineSnapshot(` - var lib = __webpack_require__("lib"); export { readFile } from "lib"; `)); @@ -65,7 +58,7 @@ describe('webpack 4', () => { var lib = __webpack_require__("lib"); lib.writeFile(); `).toMatchInlineSnapshot(` - var lib = __webpack_require__("lib"); + import * as lib from "lib"; export { readFile } from "lib"; lib.writeFile(); `)); @@ -75,7 +68,6 @@ describe('webpack 4', () => { __webpack_require__.d(__webpack_exports__, "foo", function() { return lib.readFile; }); var lib = __webpack_require__("lib"); `).toMatchInlineSnapshot(` - var lib = __webpack_require__("lib"); export { readFile as foo } from "lib"; `)); @@ -84,7 +76,6 @@ describe('webpack 4', () => { __webpack_require__.d(__webpack_exports__, "default", function() { return lib.readFile; }); var lib = __webpack_require__("lib"); `).toMatchInlineSnapshot(` - var lib = __webpack_require__("lib"); export { readFile as default } from "lib"; `)); @@ -93,7 +84,6 @@ describe('webpack 4', () => { __webpack_require__.d(__webpack_exports__, "foo", function() { return lib.default; }); var lib = __webpack_require__("lib"); `).toMatchInlineSnapshot(` - var lib = __webpack_require__("lib"); export { default as foo } from "lib"; `)); @@ -102,7 +92,6 @@ describe('webpack 4', () => { __webpack_require__.d(__webpack_exports__, "default", function() { return lib.default; }); var lib = __webpack_require__("lib"); `).toMatchInlineSnapshot(` - var lib = __webpack_require__("lib"); export { default } from "lib"; `)); @@ -133,7 +122,6 @@ describe('webpack 4', () => { __webpack_require__.d(__webpack_exports__, "lib", function() { return lib; }); var lib = __webpack_require__("lib"); `).toMatchInlineSnapshot(` - var lib = __webpack_require__("lib"); export * as lib from "lib"; `)); @@ -243,7 +231,7 @@ describe('webpack 5', () => { }); var lib = __webpack_require__("lib"); `).toMatchInlineSnapshot(` - var lib = __webpack_require__("lib"); + import * as lib from "lib"; export { readFile, writeFile } from "lib"; `)); diff --git a/packages/webcrack/src/unpack/test/get-default-export.test.ts b/packages/webcrack/src/unpack/test/get-default-export.test.ts index 1ad49e4b..3727a20b 100644 --- a/packages/webcrack/src/unpack/test/get-default-export.test.ts +++ b/packages/webcrack/src/unpack/test/get-default-export.test.ts @@ -1,13 +1,7 @@ import { test } from 'vitest'; import { testWebpackModuleTransform } from '.'; -import { ImportExportManager } from '../webpack/import-export-manager'; -import getDefaultExport from '../webpack/runtime/get-default-export'; -const expectJS = testWebpackModuleTransform( - getDefaultExport, - ({ scope }, ast) => - new ImportExportManager(ast, scope.bindings.__webpack_require__), -); +const expectJS = testWebpackModuleTransform(); test('replace default import', () => expectJS(` @@ -16,10 +10,9 @@ test('replace default import', () => console.log(_tmp.a); console.log(_tmp()); `).toMatchInlineSnapshot(` - import _temp from "1"; - var module = __webpack_require__(1); - console.log(_temp); - console.log(_temp); + import _default from "1"; + console.log(_default); + console.log(_default); `)); test('replace inlined default import', () => @@ -28,8 +21,7 @@ test('replace inlined default import', () => console.log(__webpack_require__.n(module).a); console.log(__webpack_require__.n(module)()); `).toMatchInlineSnapshot(` - import _temp from "1"; - var module = __webpack_require__(1); - console.log(_temp); - console.log(_temp); + import _default from "1"; + console.log(_default); + console.log(_default); `)); diff --git a/packages/webcrack/src/unpack/test/global.test.ts b/packages/webcrack/src/unpack/test/global.test.ts index ae40697c..25f72839 100644 --- a/packages/webcrack/src/unpack/test/global.test.ts +++ b/packages/webcrack/src/unpack/test/global.test.ts @@ -5,10 +5,7 @@ import { testWebpackModuleTransform } from '.'; import { applyTransform } from '../../ast-utils'; import global from '../webpack/runtime/global'; -const expectJS = testWebpackModuleTransform( - global, - ({ scope }) => scope.bindings.__webpack_require__, -); +const expectJS = testWebpackModuleTransform(); test('replace __webpack_require__.g with global', () => expectJS(` diff --git a/packages/webcrack/src/unpack/test/has-own-property.test.ts b/packages/webcrack/src/unpack/test/has-own-property.test.ts index 790e21c7..a03a3175 100644 --- a/packages/webcrack/src/unpack/test/has-own-property.test.ts +++ b/packages/webcrack/src/unpack/test/has-own-property.test.ts @@ -1,11 +1,7 @@ import { test } from 'vitest'; import { testWebpackModuleTransform } from '.'; -import hasOwnProperty from '../webpack/runtime/has-own-property'; -const expectJS = testWebpackModuleTransform( - hasOwnProperty, - ({ scope }) => scope.bindings.__webpack_require__, -); +const expectJS = testWebpackModuleTransform(); test('replace hasOwnProperty', () => expectJS(`__webpack_require__.o(obj, prop);`).toMatchInlineSnapshot(` diff --git a/packages/webcrack/src/unpack/test/index.ts b/packages/webcrack/src/unpack/test/index.ts index 55711227..03a00c99 100644 --- a/packages/webcrack/src/unpack/test/index.ts +++ b/packages/webcrack/src/unpack/test/index.ts @@ -1,24 +1,20 @@ import { ParseResult, parse } from '@babel/parser'; -import traverse, { NodePath } from '@babel/traverse'; +import traverse from '@babel/traverse'; import * as t from '@babel/types'; import { Assertion, expect } from 'vitest'; -import { Transform, applyTransform } from '../../ast-utils'; +import { WebpackModule } from '../webpack/module'; /** - * Test a transform with the input being wrapped with + * Test all transforms with the input being wrapped with * ```js * (function(__webpack_module__, __webpack_exports__, __webpack_require__) { * // input * }); * ``` - * - * @param transform the transform to apply - * @param cb specify the options that will be passed to the transform */ -export function testWebpackModuleTransform( - transform: Transform, - cb?: (wrapperPath: NodePath, ast: t.File) => Options, -): (input: string) => Assertion> { +export function testWebpackModuleTransform(): ( + input: string, +) => Assertion> { return (input) => { const moduleCode = ` (function(__webpack_module__, __webpack_exports__, __webpack_require__) { @@ -29,15 +25,14 @@ export function testWebpackModuleTransform( sourceType: 'unambiguous', allowReturnOutsideFunction: true, }); - let innerAST: t.File; + let file: t.File; traverse(ast, { FunctionExpression(path) { path.stop(); - innerAST = t.file(t.program(path.node.body.body)); - const options = cb?.(path, innerAST); - applyTransform(innerAST, transform, options); + file = t.file(t.program(path.node.body.body)); + new WebpackModule('test', path, true); }, }); - return expect(innerAST!); + return expect(file!); }; } diff --git a/packages/webcrack/src/unpack/webpack/import-export-manager.ts b/packages/webcrack/src/unpack/webpack/import-export-manager.ts index e4f2f133..f022d8c7 100644 --- a/packages/webcrack/src/unpack/webpack/import-export-manager.ts +++ b/packages/webcrack/src/unpack/webpack/import-export-manager.ts @@ -6,6 +6,8 @@ import { generate, renameFast } from '../../ast-utils'; // TODO: hoist re-exports to the top of the file (but retain order relative to imports) // TODO: when it accesses module.exports, dont convert to esm +// FIXME: remove unused require vars (when they were used for imports/exports) +// also store import/export metadata in the require var for easier management /** * Example: `__webpack_require__(id)` @@ -21,6 +23,12 @@ interface RequireCall { interface RequireVar { binding: Binding; moduleId: string; + imports: ( + | t.ImportSpecifier + | t.ImportNamespaceSpecifier + | t.ImportDefaultSpecifier + )[]; + exports: (t.ExportSpecifier | t.ExportNamespaceSpecifier)[]; } export class ImportExportManager { @@ -45,14 +53,6 @@ export class ImportExportManager { webpackRequire: Binding | undefined; private ast: t.File; - /** - * Used for merging multiple re-exports of a module - */ - private reExportCache = new Map(); - /** - * Used for merging multiple imports of a module - */ - private importCache = new Map(); constructor(ast: t.File, webpackRequireBinding: Binding | undefined) { this.ast = ast; @@ -60,7 +60,75 @@ export class ImportExportManager { this.collectRequireCalls(); } - transformImports() { + insertImportsAndExports() { + // const property = m.capture(m.anyString()); + // const memberExpressionMatcher = m.memberExpression( + // m.identifier(), + // m.identifier(property), + // ); + // const zeroSequenceMatcher = m.sequenceExpression([ + // m.numericLiteral(0), + // m.memberExpression(m.identifier(), m.identifier(property)), + // ]); + + this.requireVars.forEach((requireVar) => { + // TODO: resolve module id to path + const namedExports = t.exportNamedDeclaration( + undefined, + requireVar.exports.filter((node) => t.isExportSpecifier(node)), + t.stringLiteral(requireVar.moduleId), + ); + const namespaceExports = requireVar.exports + .filter((node) => t.isExportNamespaceSpecifier(node)) + .map((node) => + t.exportNamedDeclaration( + undefined, + [node], + t.stringLiteral(requireVar.moduleId), + ), + ); + if (namedExports.specifiers.length > 0) { + requireVar.binding.path.parentPath!.insertAfter(namedExports); + } + requireVar.binding.path.parentPath!.insertAfter(namespaceExports); + + // FIXME: collect this information earlier + if (requireVar.binding.references > 1) { + requireVar.binding.path.parentPath!.insertAfter( + statement`import * as ${requireVar.binding.identifier} from '${requireVar.moduleId}'`(), + ); + } + + const namedImports = t.importDeclaration( + [ + ...requireVar.imports.filter((node) => + t.isImportDefaultSpecifier(node), + ), + ...requireVar.imports.filter((node) => t.isImportSpecifier(node)), + ], + t.stringLiteral(requireVar.moduleId), + ); + const namespaceImports = requireVar.imports + .filter((node) => t.isImportNamespaceSpecifier(node)) + .map((node) => + t.importDeclaration([node], t.stringLiteral(requireVar.moduleId)), + ); + + if (namedImports.specifiers.length > 0) { + requireVar.binding.path.parentPath!.insertAfter(namedImports); + } + requireVar.binding.path.parentPath!.insertAfter(namespaceImports); + + requireVar.binding.path.remove(); + }); + + // TODO: hoist imports to the top of the file + this.requireCalls.forEach(({ path, moduleId }) => { + path.replaceWith(expression`require('${moduleId}')`()); + }); + } + + private collectImports() { const property = m.capture(m.anyString()); const memberExpressionMatcher = m.memberExpression( m.identifier(), @@ -111,7 +179,7 @@ export class ImportExportManager { binding.referencePaths.forEach((reference) => { reference.replaceWith(t.identifier(binding.identifier.name)); }); - this.addImportAll(requireVar); + this.addImportNamespace(requireVar); } }); @@ -120,7 +188,7 @@ export class ImportExportManager { }); } - transformExport(scope: Scope, exportName: string, value: t.Expression) { + addExport(scope: Scope, exportName: string, value: t.Expression) { this.exports.add(exportName); const objectName = m.capture(m.anyString()); @@ -136,7 +204,7 @@ export class ImportExportManager { const requireVar = this.findRequireVar(binding.path.node); if (requireVar) { - this.addExportAll(binding, requireVar.moduleId, exportName); + this.addExportNamespace(requireVar, exportName); } else if (exportName === 'default' && binding.references === 1) { this.addExportDefault(binding); } else { @@ -148,12 +216,7 @@ export class ImportExportManager { const requireVar = this.findRequireVar(binding.path.node); if (!requireVar) return; - this.addExportFrom( - requireVar.binding, - requireVar.moduleId, - propertyName.current!, - exportName, - ); + this.addExportFrom(requireVar, propertyName.current!, exportName); } else { t.addComment( this.ast.program, @@ -175,30 +238,18 @@ export class ImportExportManager { * @returns local name of the default import */ addDefaultImport(requireVar: RequireVar) { - const { moduleId } = requireVar; - const existingImport = this.importCache.get(moduleId); - const localName = requireVar.binding.scope.generateUid(); - - if (existingImport) { - const existingDefaultImport = existingImport.specifiers.find( - (specifier) => t.isImportDefaultSpecifier(specifier), - ); - if (existingDefaultImport) return existingDefaultImport.local.name; - - existingImport.specifiers.push( + const existingDefaultImport = requireVar.imports.find((node) => + t.isImportDefaultSpecifier(node), + ); + if (existingDefaultImport) { + return existingDefaultImport.local.name; + } else { + const localName = requireVar.binding.scope.generateUid('default'); + requireVar.imports.push( t.importDefaultSpecifier(t.identifier(localName)), ); - } else { - // TODO: resolve to file path - const importDeclaration = - statement`import ${localName} from '${moduleId}'`() as t.ImportDeclaration; - // TODO: find better place to insert - this.ast.program.body.unshift(importDeclaration); - // FIXME: register binding - this.importCache.set(moduleId, importDeclaration); + return localName; } - - return localName; } private addNamedImport( @@ -206,26 +257,16 @@ export class ImportExportManager { localName: string, importedName: string, ) { - const existingImport = this.importCache.get(requireVar.moduleId); - if (existingImport) { - // FIXME: this can import the same name multiple times - existingImport.specifiers.push( - t.importSpecifier(t.identifier(localName), t.identifier(importedName)), - ); - } else { - // TODO: resolve to file path - const importDeclaration = - statement`import { ${importedName} as ${localName} } from '${requireVar.moduleId}'`() as t.ImportDeclaration; - requireVar.binding.path.parentPath!.insertAfter(importDeclaration); - // FIXME: register binding to avoid duplicate names - this.importCache.set(requireVar.moduleId, importDeclaration); - } + requireVar.imports.push( + t.importSpecifier(t.identifier(localName), t.identifier(importedName)), + ); } - private addImportAll(requireVar: RequireVar) { - // TODO: resolve to file path - requireVar.binding.path.parentPath!.replaceWith( - statement`import * as ${requireVar.binding.identifier} from '${requireVar.moduleId}'`(), + private addImportNamespace(requireVar: RequireVar) { + requireVar.imports.push( + t.importNamespaceSpecifier( + t.identifier(requireVar.binding.identifier.name), + ), ); } @@ -241,24 +282,13 @@ export class ImportExportManager { * ``` */ private addExportFrom( - binding: Binding, - moduleId: string, + requireVar: RequireVar, localName: string, exportName: string, ) { - const existingExport = this.reExportCache.get(moduleId); - if (existingExport) { - existingExport.specifiers.push( - t.exportSpecifier(t.identifier(localName), t.identifier(exportName)), - ); - } else { - // TODO: resolve to file path - const exportDeclaration = - statement`export { ${localName} as ${exportName} } from '${moduleId}'`() as t.ExportNamedDeclaration; - const [path] = binding.path.parentPath!.insertAfter(exportDeclaration); - binding.reference(path.get('specifiers.0.local') as NodePath); - this.reExportCache.set(moduleId, exportDeclaration); - } + requireVar.exports.push( + t.exportSpecifier(t.identifier(localName), t.identifier(exportName)), + ); } /** @@ -332,10 +362,9 @@ export class ImportExportManager { * export * as foo from 'lib'; * ``` */ - private addExportAll(binding: Binding, moduleId: string, exportName: string) { - // TODO: resolve to file path - binding.path.parentPath!.insertAfter( - statement`export * as ${exportName} from '${moduleId}'`(), + private addExportNamespace(requireVar: RequireVar, exportName: string) { + requireVar.exports.push( + t.exportNamespaceSpecifier(t.identifier(exportName)), ); } @@ -367,6 +396,8 @@ export class ImportExportManager { this.requireVars.push({ moduleId, binding, + imports: [], + exports: [], }); } }); diff --git a/packages/webcrack/src/unpack/webpack/module.ts b/packages/webcrack/src/unpack/webpack/module.ts index 6d06a9a4..ebe9984c 100644 --- a/packages/webcrack/src/unpack/webpack/module.ts +++ b/packages/webcrack/src/unpack/webpack/module.ts @@ -13,12 +13,12 @@ import { default as defineExport, default as definePropertyGetters, } from './runtime/define-property-getters'; +import getDefaultExport from './runtime/get-default-export'; import global from './runtime/global'; import hasOwnProperty from './runtime/has-own-property'; import moduleDecorator from './runtime/module-decorator'; import namespaceObject from './runtime/namespace-object'; import varInjections from './var-injections'; -import getDefaultExport from './runtime/get-default-export'; export class WebpackModule extends Module { #webpackRequireBinding: Binding | undefined; @@ -51,7 +51,7 @@ export class WebpackModule extends Module { applyTransform(file, namespaceObject); applyTransform(file, getDefaultExport, this.#importExportManager); applyTransform(file, definePropertyGetters, this.#importExportManager); - this.#importExportManager.transformImports(); + this.#importExportManager.insertImportsAndExports(); // this.removeDefineESM(); // // FIXME: some bundles don't define __esModule but still declare esm exports diff --git a/packages/webcrack/src/unpack/webpack/runtime/define-property-getters.ts b/packages/webcrack/src/unpack/webpack/runtime/define-property-getters.ts index 2433c4bb..05e4cc75 100644 --- a/packages/webcrack/src/unpack/webpack/runtime/define-property-getters.ts +++ b/packages/webcrack/src/unpack/webpack/runtime/define-property-getters.ts @@ -86,7 +86,7 @@ export default { if (singleExport.match(path.node)) { if (namespaceObject.current === '__webpack_exports__') { - manager.transformExport( + manager.addExport( path.scope, exportName.current!, returnValue.current!, @@ -102,7 +102,7 @@ export default { for (const property of properties.current!) { objectProperty.match(property); // To easily get the captures per property if (namespaceObject.current === '__webpack_exports__') { - manager.transformExport( + manager.addExport( path.scope, exportName.current!, returnValue.current!, diff --git a/packages/webcrack/src/unpack/webpack/runtime/get-default-export.ts b/packages/webcrack/src/unpack/webpack/runtime/get-default-export.ts index ecef0123..782d12af 100644 --- a/packages/webcrack/src/unpack/webpack/runtime/get-default-export.ts +++ b/packages/webcrack/src/unpack/webpack/runtime/get-default-export.ts @@ -43,6 +43,7 @@ export default { const requireVar = manager.findRequireVar(moduleBinding.path.node); if (!requireVar) return; + requireVar.binding.dereference(); const importName = manager.addDefaultImport(requireVar); callPath.parentPath?.replaceWith(t.identifier(importName)); this.changes++; @@ -56,11 +57,12 @@ export default { if (!requireVar) return; const importName = manager.addDefaultImport(requireVar); - // `_tmp.a` -> `importName` or `_tmp()` -> `importName()` + // `_tmp.a` or `_tmp()` -> `importName` tmpVarBinding.referencePaths.forEach((refPath) => { refPath.parentPath?.replaceWith(t.identifier(importName)); }); tmpVarBinding.path.remove(); + requireVar.binding.dereference(); this.changes++; }); }, From 90d92cca46f1541eed88cf085cb5a80aa0c77ada Mon Sep 17 00:00:00 2001 From: j4k0xb <55899582+j4k0xb@users.noreply.github.com> Date: Sat, 13 Jan 2024 03:20:27 +0100 Subject: [PATCH 40/81] test: add more import tests --- ...operty-getters.test.ts => exports.test.ts} | 0 .../unpack/test/get-default-export.test.ts | 27 ---- .../webcrack/src/unpack/test/imports.test.ts | 127 ++++++++++++++++++ .../unpack/webpack/import-export-manager.ts | 1 + .../runtime/define-property-getters.ts | 1 + 5 files changed, 129 insertions(+), 27 deletions(-) rename packages/webcrack/src/unpack/test/{define-property-getters.test.ts => exports.test.ts} (100%) delete mode 100644 packages/webcrack/src/unpack/test/get-default-export.test.ts create mode 100644 packages/webcrack/src/unpack/test/imports.test.ts diff --git a/packages/webcrack/src/unpack/test/define-property-getters.test.ts b/packages/webcrack/src/unpack/test/exports.test.ts similarity index 100% rename from packages/webcrack/src/unpack/test/define-property-getters.test.ts rename to packages/webcrack/src/unpack/test/exports.test.ts diff --git a/packages/webcrack/src/unpack/test/get-default-export.test.ts b/packages/webcrack/src/unpack/test/get-default-export.test.ts deleted file mode 100644 index 3727a20b..00000000 --- a/packages/webcrack/src/unpack/test/get-default-export.test.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { test } from 'vitest'; -import { testWebpackModuleTransform } from '.'; - -const expectJS = testWebpackModuleTransform(); - -test('replace default import', () => - expectJS(` - var module = __webpack_require__(1); - var _tmp = __webpack_require__.n(module); - console.log(_tmp.a); - console.log(_tmp()); - `).toMatchInlineSnapshot(` - import _default from "1"; - console.log(_default); - console.log(_default); - `)); - -test('replace inlined default import', () => - expectJS(` - var module = __webpack_require__(1); - console.log(__webpack_require__.n(module).a); - console.log(__webpack_require__.n(module)()); - `).toMatchInlineSnapshot(` - import _default from "1"; - console.log(_default); - console.log(_default); - `)); 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..a1cb02dc --- /dev/null +++ b/packages/webcrack/src/unpack/test/imports.test.ts @@ -0,0 +1,127 @@ +import { describe, test } from 'vitest'; +import { testWebpackModuleTransform } from '.'; + +const expectJS = testWebpackModuleTransform(); + +describe('webpack 4', () => { + test('default import', () => + expectJS(` + __webpack_require__.r(__webpack_exports__); + const lib = __webpack_require__("lib"); + console.log(lib.default); + `).toMatchInlineSnapshot(` + import lib from "lib"; + console.log(lib); + `)); + + test('default import of commonjs module', () => + expectJS(` + var module = __webpack_require__(1); + var _tmp = __webpack_require__.n(module); + console.log(_tmp.a); + console.log(_tmp()); + `).toMatchInlineSnapshot(` + import _default from "1"; + console.log(_default); + console.log(_default); + `)); + + test('inlined default import of commonjs module', () => + expectJS(` + var module = __webpack_require__(1); + var _tmp = __webpack_require__.n(module); + console.log(_tmp.a); + console.log(_tmp()); + `).toMatchInlineSnapshot(` + import _default from "1"; + console.log(_default); + console.log(_default); + `)); + + test('named import', () => + expectJS(` + __webpack_require__.r(__webpack_exports__); + const lib = __webpack_require__("lib"); + console.log(lib.foo); + `).toMatchInlineSnapshot(` + import { foo } from "lib"; + console.log(foo); + `)); + + test('named import with indirect call', () => + expectJS(` + __webpack_require__.r(__webpack_exports__); + const lib = __webpack_require__("lib"); + console.log(Object(lib.foo)("bar")); + `).toMatchInlineSnapshot(` + import { foo } from "lib"; + console.log(foo("bar")); + `)); + + test('namespace import', () => + expectJS(` + __webpack_require__.r(__webpack_exports__); + const lib = __webpack_require__("lib"); + console.log(lib); + `).toMatchInlineSnapshot(` + import * as lib from "lib"; + console.log(lib); + `)); + + // TODO: maybe theres no var or it got inlined somewhere + test('side effect import', () => + expectJS(` + __webpack_require__.r(__webpack_exports__); + 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); + }); + `), + ); +}); + +describe('webpack 5', () => { + test('named import with indirect call', () => + expectJS(` + __webpack_require__.r(__webpack_exports__); + 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; + __webpack_require__.r(__webpack_exports__); + 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/webpack/import-export-manager.ts b/packages/webcrack/src/unpack/webpack/import-export-manager.ts index f022d8c7..f31fe34d 100644 --- a/packages/webcrack/src/unpack/webpack/import-export-manager.ts +++ b/packages/webcrack/src/unpack/webpack/import-export-manager.ts @@ -6,6 +6,7 @@ import { generate, renameFast } from '../../ast-utils'; // TODO: hoist re-exports to the top of the file (but retain order relative to imports) // TODO: when it accesses module.exports, dont convert to esm +// TODO: side-effect import // FIXME: remove unused require vars (when they were used for imports/exports) // also store import/export metadata in the require var for easier management diff --git a/packages/webcrack/src/unpack/webpack/runtime/define-property-getters.ts b/packages/webcrack/src/unpack/webpack/runtime/define-property-getters.ts index 05e4cc75..293e0b4a 100644 --- a/packages/webcrack/src/unpack/webpack/runtime/define-property-getters.ts +++ b/packages/webcrack/src/unpack/webpack/runtime/define-property-getters.ts @@ -60,6 +60,7 @@ export default { ]), ); + // TODO: use `var namespace = { get foo() { return foo; } };` instead of `Object.defineProperty` /** * Used only for concatenated modules where we can't convert to ESM exports, so it still works at runtime. */ From a2f7419dd87b5524a66508a64933b6c9e099cfc3 Mon Sep 17 00:00:00 2001 From: j4k0xb <55899582+j4k0xb@users.noreply.github.com> Date: Sat, 13 Jan 2024 03:20:27 +0100 Subject: [PATCH 41/81] refactor: remove unused files --- packages/webcrack/src/unpack/webpack/esm.ts | 179 ------------------ .../src/unpack/webpack/getDefaultExport.ts | 114 ----------- 2 files changed, 293 deletions(-) delete mode 100644 packages/webcrack/src/unpack/webpack/esm.ts delete mode 100644 packages/webcrack/src/unpack/webpack/getDefaultExport.ts diff --git a/packages/webcrack/src/unpack/webpack/esm.ts b/packages/webcrack/src/unpack/webpack/esm.ts deleted file mode 100644 index 9d782a9b..00000000 --- a/packages/webcrack/src/unpack/webpack/esm.ts +++ /dev/null @@ -1,179 +0,0 @@ -import { statement } from '@babel/template'; -import type { NodePath } from '@babel/traverse'; -import traverse from '@babel/traverse'; -import * as t from '@babel/types'; -import * as m from '@codemod/matchers'; -import { constMemberExpression, findPath, renameFast } from '../../ast-utils'; -import type { WebpackModule } from './module'; - -const buildNamespaceImport = statement`import * as NAME from "PATH";`; -const buildNamedExportLet = statement`export let NAME = VALUE;`; - -/** - * ```js - * require.r(exports); - * require.d(exports, 'counter', function () { - * return f; - * }); - * let f = 1; - * ``` - * -> - * ```js - * export let counter = 1; - * ``` - */ -export function convertESM(module: WebpackModule): void { - // E.g. require.r(exports); - const defineEsModuleMatcher = m.expressionStatement( - m.callExpression(constMemberExpression('require', 'r'), [m.identifier()]), - ); - - const exportsName = m.capture(m.identifier()); - const exportedName = m.capture(m.anyString()); - const returnedValue = m.capture(m.anyExpression()); - // E.g. require.d(exports, "counter", function () { return f }); - const defineExportMatcher = m.expressionStatement( - m.callExpression(constMemberExpression('require', 'd'), [ - exportsName, - m.stringLiteral(exportedName), - m.functionExpression( - undefined, - [], - m.blockStatement([m.returnStatement(returnedValue)]), - ), - ]), - ); - - const emptyObjectVarMatcher = m.variableDeclarator( - m.fromCapture(exportsName), - m.objectExpression([]), - ); - - const properties = m.capture( - m.arrayOf( - m.objectProperty( - m.identifier(), - m.arrowFunctionExpression([], m.anyExpression()), - ), - ), - ); - // E.g. require.d(exports, { foo: () => a, bar: () => b }); - const defineExportsMatcher = m.expressionStatement( - m.callExpression(constMemberExpression('require', 'd'), [ - exportsName, - m.objectExpression(properties), - ]), - ); - - // E.g. const lib = require("./lib.js"); - const requireVariable = m.capture(m.identifier()); - const requiredModulePath = m.capture(m.anyString()); - const requireMatcher = m.variableDeclaration(undefined, [ - m.variableDeclarator( - requireVariable, - m.callExpression(m.identifier('require'), [ - m.stringLiteral(requiredModulePath), - ]), - ), - ]); - - // module = require.hmd(module); - const hmdMatcher = m.expressionStatement( - m.assignmentExpression( - '=', - m.identifier('module'), - m.callExpression(constMemberExpression('require', 'hmd')), - ), - ); - - traverse(module.ast, { - enter(path) { - // Only traverse the top-level - if (path.parentPath?.parentPath) return path.skip(); - - if (defineEsModuleMatcher.match(path.node)) { - module.ast.program.sourceType = 'esm'; - path.remove(); - } else if ( - module.ast.program.sourceType === 'esm' && - requireMatcher.match(path.node) - ) { - path.replaceWith( - buildNamespaceImport({ - NAME: requireVariable.current, - PATH: String(requiredModulePath.current), - }), - ); - } else if (defineExportsMatcher.match(path.node)) { - const exportsBinding = path.scope.getBinding(exportsName.current!.name); - const emptyObject = emptyObjectVarMatcher.match( - exportsBinding?.path.node, - ) - ? (exportsBinding?.path.node.init as t.ObjectExpression) - : null; - - for (const property of properties.current!) { - const exportedKey = property.key as t.Identifier; - const returnedValue = (property.value as t.ArrowFunctionExpression) - .body as t.Expression; - if (emptyObject) { - emptyObject.properties.push( - t.objectProperty(exportedKey, returnedValue), - ); - } else { - exportVariable(path, returnedValue, exportedKey.name); - } - } - - path.remove(); - } else if (defineExportMatcher.match(path.node)) { - exportVariable(path, returnedValue.current!, exportedName.current!); - path.remove(); - } else if (hmdMatcher.match(path.node)) { - path.remove(); - } - }, - }); -} - -function exportVariable( - requireDPath: NodePath, - value: t.Expression, - exportName: string, -) { - if (value.type === 'Identifier') { - const binding = requireDPath.scope.getBinding(value.name); - if (!binding) return; - - const declaration = findPath( - binding.path, - m.or( - m.variableDeclaration(), - m.classDeclaration(), - m.functionDeclaration(), - ), - ); - if (!declaration) return; - - if (exportName === 'default') { - // `let f = 1;` -> `export default 1;` - declaration.replaceWith( - t.exportDefaultDeclaration( - t.isVariableDeclaration(declaration.node) - ? declaration.node.declarations[0].init! - : declaration.node, - ), - ); - } else { - // `let f = 1;` -> `export let counter = 1;` - renameFast(binding, exportName); - declaration.replaceWith(t.exportNamedDeclaration(declaration.node)); - } - } else if (exportName === 'default') { - requireDPath.insertAfter(t.exportDefaultDeclaration(value)); - } else { - requireDPath.insertAfter( - buildNamedExportLet({ NAME: t.identifier(exportName), VALUE: value }), - ); - } -} diff --git a/packages/webcrack/src/unpack/webpack/getDefaultExport.ts b/packages/webcrack/src/unpack/webpack/getDefaultExport.ts deleted file mode 100644 index 2f112405..00000000 --- a/packages/webcrack/src/unpack/webpack/getDefaultExport.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { expression } from '@babel/template'; -import type { Scope } from '@babel/traverse'; -import traverse from '@babel/traverse'; -import * as m from '@codemod/matchers'; -import { constMemberExpression } from '../../ast-utils'; -import type { WebpackBundle } from './bundle'; - -/* - * webpack/runtime/compat get default export - * getDefaultExport function for compatibility with non-harmony modules - * ```js - * __webpack_require__.n = (module) => { - * var getter = module && module.__esModule ? - * () => (module['default']) : - * () => (module); - * __webpack_require__.d(getter, { a: getter }); - * return getter; - * }; - * ``` - */ - -/** - * Convert require.n calls to require the default export depending on the target module type - * ```js - * const m = require(1); - * const getter = require.n(m); - * console.log(getter.a.prop, getter().prop); - * ``` - * -> - * ```js - * const m = require(1); - * console.log(m.prop, m.prop); - * ``` - */ -export function convertDefaultRequire(bundle: WebpackBundle): void { - function getRequiredModule(scope: Scope) { - // The variable that's passed to require.n - const binding = scope.getBinding(moduleArg.current!.name); - const declarator = binding?.path.node; - if (declaratorMatcher.match(declarator)) { - return bundle.modules.get(requiredModuleId.current!.value.toString()); - } - } - - const requiredModuleId = m.capture(m.numericLiteral()); - // E.g. const m = require(1); - const declaratorMatcher = m.variableDeclarator( - m.identifier(), - m.callExpression(m.identifier('require'), [requiredModuleId]), - ); - - // E.g. m - const moduleArg = m.capture(m.identifier()); - // E.g. getter - const getterVarName = m.capture(m.identifier()); - // E.g. require.n(m) - const requireN = m.callExpression(constMemberExpression('require', 'n'), [ - moduleArg, - ]); - // E.g. const getter = require.n(m) - const defaultRequireMatcher = m.variableDeclarator(getterVarName, requireN); - - // E.g. require.n(m).a or require.n(m)() - const defaultRequireMatcherAlternative = m.or( - constMemberExpression(requireN, 'a'), - m.callExpression(requireN, []), - ); - - const buildDefaultAccess = expression`OBJECT.default`; - - bundle.modules.forEach((module) => { - traverse(module.ast, { - 'CallExpression|MemberExpression'(path) { - if (defaultRequireMatcherAlternative.match(path.node)) { - // Replace require.n(m).a or require.n(m)() with m or m.default - const requiredModule = getRequiredModule(path.scope); - if (requiredModule?.ast.program.sourceType === 'esm') { - path.replaceWith( - buildDefaultAccess({ OBJECT: moduleArg.current! }), - ); - } else { - path.replaceWith(moduleArg.current!); - } - } - }, - VariableDeclarator(path) { - if (defaultRequireMatcher.match(path.node)) { - // Replace require.n(m); with m or m.default - const requiredModule = getRequiredModule(path.scope); - const init = path.get('init'); - if (requiredModule?.ast.program.sourceType === 'esm') { - init.replaceWith( - buildDefaultAccess({ OBJECT: moduleArg.current! }), - ); - } else { - init.replaceWith(moduleArg.current!); - } - - // Replace getter.a.prop and getter().prop with getter.prop - const binding = path.scope.getOwnBinding(getterVarName.current!.name); - binding?.referencePaths.forEach((refPath) => { - if ( - refPath.parentPath?.isCallExpression() || - refPath.parentPath?.isMemberExpression() - ) { - refPath.parentPath.replaceWith(refPath); - } - }); - } - }, - noScope: true, - }); - }); -} From 069cb494c725c293424536db1dd048c05c027eb0 Mon Sep 17 00:00:00 2001 From: j4k0xb <55899582+j4k0xb@users.noreply.github.com> Date: Sat, 13 Jan 2024 03:20:27 +0100 Subject: [PATCH 42/81] test: add nested var injections --- .../src/unpack/test/var-injections.test.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/packages/webcrack/src/unpack/test/var-injections.test.ts b/packages/webcrack/src/unpack/test/var-injections.test.ts index 500800c3..1d7702ae 100644 --- a/packages/webcrack/src/unpack/test/var-injections.test.ts +++ b/packages/webcrack/src/unpack/test/var-injections.test.ts @@ -15,6 +15,20 @@ test('replace', () => console.log(m, n); `)); +test('nested var injections', () => + expectJS(` + (function (m, n) { + (function (o) { + console.log(m, n, o); + }.call(this, __webpack_require__(3))); + }.call(this, __webpack_require__(1), __webpack_require__(2))); + `).toMatchInlineSnapshot(` + var m = __webpack_require__(1); + var n = __webpack_require__(2); + var o = __webpack_require__(3); + console.log(m, n, o); + `)); + test('ignore different number of params and args', () => expectJS(` (function (m, n) { From 94c8d1468e4475ff3e338a5a8e3015c08e7c0b82 Mon Sep 17 00:00:00 2001 From: j4k0xb <55899582+j4k0xb@users.noreply.github.com> Date: Sat, 13 Jan 2024 03:20:27 +0100 Subject: [PATCH 43/81] style: lint consistent-type-imports --- apps/playground/src/context/DeobfuscateContext.tsx | 2 +- apps/playground/src/webcrack.worker.ts | 2 +- packages/webcrack/src/unpack/test/index.ts | 6 ++++-- packages/webcrack/src/unpack/webpack/chunk.ts | 2 +- packages/webcrack/src/unpack/webpack/common-matchers.ts | 2 +- .../webcrack/src/unpack/webpack/import-export-manager.ts | 2 +- packages/webcrack/src/unpack/webpack/module.ts | 4 ++-- .../unpack/webpack/runtime/define-property-getters.ts | 7 ++++--- .../src/unpack/webpack/runtime/get-default-export.ts | 5 +++-- packages/webcrack/src/unpack/webpack/runtime/global.ts | 5 +++-- .../src/unpack/webpack/runtime/has-own-property.ts | 5 +++-- .../src/unpack/webpack/runtime/module-decorator.ts | 3 ++- .../src/unpack/webpack/runtime/namespace-object.ts | 3 ++- packages/webcrack/src/unpack/webpack/unpack-webpack-4.ts | 9 +++++---- packages/webcrack/src/unpack/webpack/unpack-webpack-5.ts | 9 +++++---- .../webcrack/src/unpack/webpack/unpack-webpack-chunk.ts | 9 +++++---- packages/webcrack/src/unpack/webpack/var-injections.ts | 3 ++- packages/webcrack/test/setup.ts | 2 +- 18 files changed, 46 insertions(+), 34 deletions(-) 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/unpack/test/index.ts b/packages/webcrack/src/unpack/test/index.ts index 03a00c99..296efe39 100644 --- a/packages/webcrack/src/unpack/test/index.ts +++ b/packages/webcrack/src/unpack/test/index.ts @@ -1,7 +1,9 @@ -import { ParseResult, parse } from '@babel/parser'; +import type { ParseResult } from '@babel/parser'; +import { parse } from '@babel/parser'; import traverse from '@babel/traverse'; import * as t from '@babel/types'; -import { Assertion, expect } from 'vitest'; +import type { Assertion } from 'vitest'; +import { expect } from 'vitest'; import { WebpackModule } from '../webpack/module'; /** diff --git a/packages/webcrack/src/unpack/webpack/chunk.ts b/packages/webcrack/src/unpack/webpack/chunk.ts index 49659329..57937903 100644 --- a/packages/webcrack/src/unpack/webpack/chunk.ts +++ b/packages/webcrack/src/unpack/webpack/chunk.ts @@ -1,4 +1,4 @@ -import { WebpackModule } from './module'; +import type { WebpackModule } from './module'; export class WebpackChunk { chunkIds: string[]; diff --git a/packages/webcrack/src/unpack/webpack/common-matchers.ts b/packages/webcrack/src/unpack/webpack/common-matchers.ts index 95597d66..3710b364 100644 --- a/packages/webcrack/src/unpack/webpack/common-matchers.ts +++ b/packages/webcrack/src/unpack/webpack/common-matchers.ts @@ -1,4 +1,4 @@ -import { Binding, NodePath } from '@babel/traverse'; +import type { Binding, NodePath } from '@babel/traverse'; import * as t from '@babel/types'; import * as m from '@codemod/matchers'; import { diff --git a/packages/webcrack/src/unpack/webpack/import-export-manager.ts b/packages/webcrack/src/unpack/webpack/import-export-manager.ts index f31fe34d..c255916d 100644 --- a/packages/webcrack/src/unpack/webpack/import-export-manager.ts +++ b/packages/webcrack/src/unpack/webpack/import-export-manager.ts @@ -1,5 +1,5 @@ import { expression, statement } from '@babel/template'; -import { Binding, NodePath, Scope } from '@babel/traverse'; +import type { Binding, NodePath, Scope } from '@babel/traverse'; import * as t from '@babel/types'; import * as m from '@codemod/matchers'; import { generate, renameFast } from '../../ast-utils'; diff --git a/packages/webcrack/src/unpack/webpack/module.ts b/packages/webcrack/src/unpack/webpack/module.ts index ebe9984c..a0967e3d 100644 --- a/packages/webcrack/src/unpack/webpack/module.ts +++ b/packages/webcrack/src/unpack/webpack/module.ts @@ -1,4 +1,4 @@ -import { Binding } from '@babel/traverse'; +import type { Binding } from '@babel/traverse'; import * as t from '@babel/types'; import * as m from '@codemod/matchers'; import { @@ -7,7 +7,7 @@ import { renameParameters, } from '../../ast-utils'; import { Module } from '../module'; -import { FunctionPath } from './common-matchers'; +import type { FunctionPath } from './common-matchers'; import { ImportExportManager } from './import-export-manager'; import { default as defineExport, diff --git a/packages/webcrack/src/unpack/webpack/runtime/define-property-getters.ts b/packages/webcrack/src/unpack/webpack/runtime/define-property-getters.ts index 293e0b4a..f3f25893 100644 --- a/packages/webcrack/src/unpack/webpack/runtime/define-property-getters.ts +++ b/packages/webcrack/src/unpack/webpack/runtime/define-property-getters.ts @@ -1,9 +1,10 @@ import { statement } from '@babel/template'; -import { NodePath } from '@babel/traverse'; +import type { NodePath } from '@babel/traverse'; import * as m from '@codemod/matchers'; import assert from 'assert'; -import { Transform, constMemberExpression } from '../../../ast-utils'; -import { ImportExportManager } from '../import-export-manager'; +import type { Transform } from '../../../ast-utils'; +import { constMemberExpression } from '../../../ast-utils'; +import type { ImportExportManager } from '../import-export-manager'; /** * `__webpack_require__.d` defines getters on the exports object. Used in ESM. diff --git a/packages/webcrack/src/unpack/webpack/runtime/get-default-export.ts b/packages/webcrack/src/unpack/webpack/runtime/get-default-export.ts index 782d12af..fd884892 100644 --- a/packages/webcrack/src/unpack/webpack/runtime/get-default-export.ts +++ b/packages/webcrack/src/unpack/webpack/runtime/get-default-export.ts @@ -1,8 +1,9 @@ import * as t from '@babel/types'; import * as m from '@codemod/matchers'; import assert from 'assert'; -import { Transform, constMemberExpression } from '../../../ast-utils'; -import { ImportExportManager } from '../import-export-manager'; +import type { Transform } from '../../../ast-utils'; +import { constMemberExpression } from '../../../ast-utils'; +import type { ImportExportManager } from '../import-export-manager'; // var a = __webpack_require__(11); var i = __webpack_require__.n(a); let p = i.a; // var c = __webpack_require__(11); let h = __webpack_require__.n(c).a; diff --git a/packages/webcrack/src/unpack/webpack/runtime/global.ts b/packages/webcrack/src/unpack/webpack/runtime/global.ts index a15c2032..1d18c759 100644 --- a/packages/webcrack/src/unpack/webpack/runtime/global.ts +++ b/packages/webcrack/src/unpack/webpack/runtime/global.ts @@ -1,6 +1,7 @@ -import { Binding } from '@babel/traverse'; +import type { Binding } from '@babel/traverse'; import * as t from '@babel/types'; -import { Transform, constMemberExpression } from '../../../ast-utils'; +import type { Transform } from '../../../ast-utils'; +import { constMemberExpression } from '../../../ast-utils'; /** * `__webpack_require__.g` diff --git a/packages/webcrack/src/unpack/webpack/runtime/has-own-property.ts b/packages/webcrack/src/unpack/webpack/runtime/has-own-property.ts index 0ea9297c..a6fba85c 100644 --- a/packages/webcrack/src/unpack/webpack/runtime/has-own-property.ts +++ b/packages/webcrack/src/unpack/webpack/runtime/has-own-property.ts @@ -1,7 +1,8 @@ import { expression } from '@babel/template'; -import { Binding } from '@babel/traverse'; +import type { Binding } from '@babel/traverse'; import * as m from '@codemod/matchers'; -import { Transform, constMemberExpression } from '../../../ast-utils'; +import type { Transform } from '../../../ast-utils'; +import { constMemberExpression } from '../../../ast-utils'; /** * `__webpack_require__.o` checks if an object has a property. diff --git a/packages/webcrack/src/unpack/webpack/runtime/module-decorator.ts b/packages/webcrack/src/unpack/webpack/runtime/module-decorator.ts index 2b85bf08..65fcf69a 100644 --- a/packages/webcrack/src/unpack/webpack/runtime/module-decorator.ts +++ b/packages/webcrack/src/unpack/webpack/runtime/module-decorator.ts @@ -1,5 +1,6 @@ import * as m from '@codemod/matchers'; -import { Transform, constMemberExpression } from '../../../ast-utils'; +import type { Transform } from '../../../ast-utils'; +import { constMemberExpression } from '../../../ast-utils'; // TODO(webpack 4): `module = __webpack_require__('webpack/buildin/harmony-module.js');` // or `module = __webpack_require__('webpack/buildin/module.js');` diff --git a/packages/webcrack/src/unpack/webpack/runtime/namespace-object.ts b/packages/webcrack/src/unpack/webpack/runtime/namespace-object.ts index 3964518c..c05e785d 100644 --- a/packages/webcrack/src/unpack/webpack/runtime/namespace-object.ts +++ b/packages/webcrack/src/unpack/webpack/runtime/namespace-object.ts @@ -1,6 +1,7 @@ import { statement } from '@babel/template'; import * as m from '@codemod/matchers'; -import { Transform, constMemberExpression } from '../../../ast-utils'; +import type { Transform } from '../../../ast-utils'; +import { constMemberExpression } from '../../../ast-utils'; /** * `__webpack_require__.r(__webpack_exports__);` defines `__esModule` on exports. diff --git a/packages/webcrack/src/unpack/webpack/unpack-webpack-4.ts b/packages/webcrack/src/unpack/webpack/unpack-webpack-4.ts index b7516570..a44f16e1 100644 --- a/packages/webcrack/src/unpack/webpack/unpack-webpack-4.ts +++ b/packages/webcrack/src/unpack/webpack/unpack-webpack-4.ts @@ -1,8 +1,9 @@ -import { NodePath } from '@babel/traverse'; -import * as t from '@babel/types'; +import type { NodePath } from '@babel/traverse'; +import type * as t from '@babel/types'; import * as m from '@codemod/matchers'; -import { Bundle } from '..'; -import { Transform, renameFast } from '../../ast-utils'; +import type { Bundle } from '..'; +import type { Transform } from '../../ast-utils'; +import { renameFast } from '../../ast-utils'; import { WebpackBundle } from './bundle'; import { findAssignedEntryId, diff --git a/packages/webcrack/src/unpack/webpack/unpack-webpack-5.ts b/packages/webcrack/src/unpack/webpack/unpack-webpack-5.ts index 1de76b30..ef71b8ea 100644 --- a/packages/webcrack/src/unpack/webpack/unpack-webpack-5.ts +++ b/packages/webcrack/src/unpack/webpack/unpack-webpack-5.ts @@ -1,8 +1,9 @@ -import { NodePath } from '@babel/traverse'; -import * as t from '@babel/types'; +import type { NodePath } from '@babel/traverse'; +import type * as t from '@babel/types'; import * as m from '@codemod/matchers'; -import { Bundle } from '..'; -import { Transform, renameFast } from '../../ast-utils'; +import type { Bundle } from '..'; +import type { Transform } from '../../ast-utils'; +import { renameFast } from '../../ast-utils'; import { WebpackBundle } from './bundle'; import { findAssignedEntryId, diff --git a/packages/webcrack/src/unpack/webpack/unpack-webpack-chunk.ts b/packages/webcrack/src/unpack/webpack/unpack-webpack-chunk.ts index 934e6bd5..d00b6e9a 100644 --- a/packages/webcrack/src/unpack/webpack/unpack-webpack-chunk.ts +++ b/packages/webcrack/src/unpack/webpack/unpack-webpack-chunk.ts @@ -1,8 +1,9 @@ -import { NodePath } from '@babel/traverse'; -import * as t from '@babel/types'; +import type { NodePath } from '@babel/traverse'; +import type * as t from '@babel/types'; import * as m from '@codemod/matchers'; -import { Bundle } from '..'; -import { Transform, constMemberExpression } from '../../ast-utils'; +import type { Bundle } from '..'; +import type { Transform } from '../../ast-utils'; +import { constMemberExpression } from '../../ast-utils'; import { WebpackBundle } from './bundle'; import { WebpackChunk } from './chunk'; import { getModuleFunctions, modulesContainerMatcher } from './common-matchers'; diff --git a/packages/webcrack/src/unpack/webpack/var-injections.ts b/packages/webcrack/src/unpack/webpack/var-injections.ts index 008f1750..6c427a70 100644 --- a/packages/webcrack/src/unpack/webpack/var-injections.ts +++ b/packages/webcrack/src/unpack/webpack/var-injections.ts @@ -1,6 +1,7 @@ import * as t from '@babel/types'; import * as m from '@codemod/matchers'; -import { Transform, constMemberExpression } from '../../ast-utils'; +import type { Transform } from '../../ast-utils'; +import { constMemberExpression } from '../../ast-utils'; /** * ```diff diff --git a/packages/webcrack/test/setup.ts b/packages/webcrack/test/setup.ts index c1d384cc..76dbf642 100644 --- a/packages/webcrack/test/setup.ts +++ b/packages/webcrack/test/setup.ts @@ -1,4 +1,4 @@ -import { NodePath } from '@babel/traverse'; +import type { NodePath } from '@babel/traverse'; import * as t from '@babel/types'; import { expect } from 'vitest'; import { generate } from '../src/ast-utils'; From ad3fed2eee95cb936c0e6452109e61665b71739f Mon Sep 17 00:00:00 2001 From: j4k0xb <55899582+j4k0xb@users.noreply.github.com> Date: Sat, 13 Jan 2024 03:20:27 +0100 Subject: [PATCH 44/81] test: add failing inlined variable export test case --- packages/webcrack/src/unpack/test/exports.test.ts | 9 +++++++++ .../webcrack/src/unpack/webpack/import-export-manager.ts | 1 + 2 files changed, 10 insertions(+) diff --git a/packages/webcrack/src/unpack/test/exports.test.ts b/packages/webcrack/src/unpack/test/exports.test.ts index 7517033c..fb51c8b7 100644 --- a/packages/webcrack/src/unpack/test/exports.test.ts +++ b/packages/webcrack/src/unpack/test/exports.test.ts @@ -15,6 +15,15 @@ describe('webpack 4', () => { var foo = 1; `).toMatchInlineSnapshot(`export var counter = 1;`)); + test.todo('export inlined variable', () => + expectJS(` + __webpack_require__.d(__webpack_exports__, "counter", function() { return foo; }); + for (var foo = 1, i = 0; i < 10; i++) {} + `).toMatchInlineSnapshot(` + export var foo = 1; + for (var i = 0; i < 10; i++) {} + `)); + test('export default variable', () => expectJS(` __webpack_require__.d(__webpack_exports__, "default", function() { return foo; }); diff --git a/packages/webcrack/src/unpack/webpack/import-export-manager.ts b/packages/webcrack/src/unpack/webpack/import-export-manager.ts index c255916d..6a26cfaa 100644 --- a/packages/webcrack/src/unpack/webpack/import-export-manager.ts +++ b/packages/webcrack/src/unpack/webpack/import-export-manager.ts @@ -311,6 +311,7 @@ export class ImportExportManager { m.functionDeclaration(), m.exportNamedDeclaration(), ); + // FIXME: most likely an inlined variable declaration if (!matcher.match(statementPath.node)) return; const isDeclarationExport = From e1d54b20b78e795339691e743538aaee6fdb9466 Mon Sep 17 00:00:00 2001 From: j4k0xb <55899582+j4k0xb@users.noreply.github.com> Date: Sat, 13 Jan 2024 03:20:27 +0100 Subject: [PATCH 45/81] wip --- .../unpack/webpack/import-export-manager.ts | 35 ++++++++++--------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/packages/webcrack/src/unpack/webpack/import-export-manager.ts b/packages/webcrack/src/unpack/webpack/import-export-manager.ts index 6a26cfaa..f2fcbe23 100644 --- a/packages/webcrack/src/unpack/webpack/import-export-manager.ts +++ b/packages/webcrack/src/unpack/webpack/import-export-manager.ts @@ -62,15 +62,7 @@ export class ImportExportManager { } insertImportsAndExports() { - // const property = m.capture(m.anyString()); - // const memberExpressionMatcher = m.memberExpression( - // m.identifier(), - // m.identifier(property), - // ); - // const zeroSequenceMatcher = m.sequenceExpression([ - // m.numericLiteral(0), - // m.memberExpression(m.identifier(), m.identifier(property)), - // ]); + this.collectImports(); this.requireVars.forEach((requireVar) => { // TODO: resolve module id to path @@ -94,11 +86,11 @@ export class ImportExportManager { requireVar.binding.path.parentPath!.insertAfter(namespaceExports); // FIXME: collect this information earlier - if (requireVar.binding.references > 1) { - requireVar.binding.path.parentPath!.insertAfter( - statement`import * as ${requireVar.binding.identifier} from '${requireVar.moduleId}'`(), - ); - } + // if (requireVar.binding.references > 1) { + // requireVar.binding.path.parentPath!.insertAfter( + // statement`import * as ${requireVar.binding.identifier} from '${requireVar.moduleId}'`(), + // ); + // } const namedImports = t.importDeclaration( [ @@ -120,7 +112,16 @@ export class ImportExportManager { } requireVar.binding.path.parentPath!.insertAfter(namespaceImports); - requireVar.binding.path.remove(); + // requireVar.binding.path.remove(); + }); + + this.requireVars.forEach((requireVar) => { + if (requireVar.binding.references === 0) { + // side-effect import + requireVar.binding.path.parentPath!.replaceWith( + t.importDeclaration([], t.stringLiteral(requireVar.moduleId)), + ); + } }); // TODO: hoist imports to the top of the file @@ -180,7 +181,9 @@ export class ImportExportManager { binding.referencePaths.forEach((reference) => { reference.replaceWith(t.identifier(binding.identifier.name)); }); - this.addImportNamespace(requireVar); + if (binding.references > 0) { + this.addImportNamespace(requireVar); + } } }); From 27f5813a60087349ac6cc9843fcba28d5bdc8881 Mon Sep 17 00:00:00 2001 From: j4k0xb <55899582+j4k0xb@users.noreply.github.com> Date: Sat, 13 Jan 2024 03:20:27 +0100 Subject: [PATCH 46/81] test: more exports --- .../webcrack/src/unpack/test/exports.test.ts | 50 ++++++++++++++++--- .../webcrack/src/unpack/test/imports.test.ts | 12 ++--- 2 files changed, 47 insertions(+), 15 deletions(-) diff --git a/packages/webcrack/src/unpack/test/exports.test.ts b/packages/webcrack/src/unpack/test/exports.test.ts index fb51c8b7..70b9459e 100644 --- a/packages/webcrack/src/unpack/test/exports.test.ts +++ b/packages/webcrack/src/unpack/test/exports.test.ts @@ -4,17 +4,17 @@ import { testWebpackModuleTransform } from '.'; const expectJS = testWebpackModuleTransform(); describe('webpack 4', () => { - test('export default expression;', () => - expectJS(` - __webpack_exports__.default = 1; - `).toMatchInlineSnapshot(`export default 1;`)); - 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.todo('export inlined variable', () => expectJS(` __webpack_require__.d(__webpack_exports__, "counter", function() { return foo; }); @@ -22,7 +22,8 @@ describe('webpack 4', () => { `).toMatchInlineSnapshot(` export var foo = 1; for (var i = 0; i < 10; i++) {} - `)); + `), + ); test('export default variable', () => expectJS(` @@ -30,6 +31,7 @@ describe('webpack 4', () => { var foo = 1; `).toMatchInlineSnapshot(`export default 1;`)); + // TODO: or `export default foo;` ? test('export default variable with multiple references', () => expectJS(` __webpack_require__.d(__webpack_exports__, "default", function() { return foo; }); @@ -180,6 +182,39 @@ describe('webpack 5', () => { 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 { foo as default }; + console.log(foo); + `)); + test.todo('export object destructuring', () => expectJS(` __webpack_require__.d(__webpack_exports__, { @@ -240,13 +275,12 @@ describe('webpack 5', () => { }); var lib = __webpack_require__("lib"); `).toMatchInlineSnapshot(` - import * as lib from "lib"; export { readFile, writeFile } from "lib"; `)); test.todo('re-export all from commonjs', () => expectJS(` - var lib = require("lib"); + var lib = __webpack_require__("lib"); var libDef = __webpack_require__.n(lib); var reExportObject = {}; for (const importKey in lib) { diff --git a/packages/webcrack/src/unpack/test/imports.test.ts b/packages/webcrack/src/unpack/test/imports.test.ts index a1cb02dc..c6607caa 100644 --- a/packages/webcrack/src/unpack/test/imports.test.ts +++ b/packages/webcrack/src/unpack/test/imports.test.ts @@ -16,8 +16,8 @@ describe('webpack 4', () => { test('default import of commonjs module', () => expectJS(` - var module = __webpack_require__(1); - var _tmp = __webpack_require__.n(module); + var lib = __webpack_require__(1); + var _tmp = __webpack_require__.n(lib); console.log(_tmp.a); console.log(_tmp()); `).toMatchInlineSnapshot(` @@ -28,14 +28,12 @@ describe('webpack 4', () => { test('inlined default import of commonjs module', () => expectJS(` - var module = __webpack_require__(1); - var _tmp = __webpack_require__.n(module); - console.log(_tmp.a); - console.log(_tmp()); + var lib = __webpack_require__(1); + var _tmp = __webpack_require__.n(lib).a; + console.log(_tmp); `).toMatchInlineSnapshot(` import _default from "1"; console.log(_default); - console.log(_default); `)); test('named import', () => From 4d8cf35ac64132ee0c80bb2e39e516d3bde0a4fe Mon Sep 17 00:00:00 2001 From: j4k0xb <55899582+j4k0xb@users.noreply.github.com> Date: Sat, 13 Jan 2024 03:20:27 +0100 Subject: [PATCH 47/81] refactor: remove redundant test --- packages/webcrack/src/unpack/test/exports.test.ts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/packages/webcrack/src/unpack/test/exports.test.ts b/packages/webcrack/src/unpack/test/exports.test.ts index 70b9459e..d8b50233 100644 --- a/packages/webcrack/src/unpack/test/exports.test.ts +++ b/packages/webcrack/src/unpack/test/exports.test.ts @@ -15,16 +15,6 @@ describe('webpack 4', () => { __webpack_exports__.default = 1; `).toMatchInlineSnapshot(`export default 1;`)); - test.todo('export inlined variable', () => - expectJS(` - __webpack_require__.d(__webpack_exports__, "counter", function() { return foo; }); - for (var foo = 1, i = 0; i < 10; i++) {} - `).toMatchInlineSnapshot(` - export var foo = 1; - for (var i = 0; i < 10; i++) {} - `), - ); - test('export default variable', () => expectJS(` __webpack_require__.d(__webpack_exports__, "default", function() { return foo; }); From 864125990607767ae1f8d431bc104c5a71cb182b Mon Sep 17 00:00:00 2001 From: j4k0xb <55899582+j4k0xb@users.noreply.github.com> Date: Sat, 13 Jan 2024 03:20:27 +0100 Subject: [PATCH 48/81] test: fix many import test cases --- .../webcrack/src/unpack/test/imports.test.ts | 15 +- .../unpack/webpack/import-export-manager.ts | 144 +++++++++--------- 2 files changed, 81 insertions(+), 78 deletions(-) diff --git a/packages/webcrack/src/unpack/test/imports.test.ts b/packages/webcrack/src/unpack/test/imports.test.ts index c6607caa..4d0b7742 100644 --- a/packages/webcrack/src/unpack/test/imports.test.ts +++ b/packages/webcrack/src/unpack/test/imports.test.ts @@ -10,8 +10,8 @@ describe('webpack 4', () => { const lib = __webpack_require__("lib"); console.log(lib.default); `).toMatchInlineSnapshot(` - import lib from "lib"; - console.log(lib); + import _lib_default from "lib"; + console.log(_lib_default); `)); test('default import of commonjs module', () => @@ -66,6 +66,17 @@ describe('webpack 4', () => { console.log(lib); `)); + test('combined namespace and default import', () => + expectJS(` + __webpack_require__.r(__webpack_exports__); + 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(` diff --git a/packages/webcrack/src/unpack/webpack/import-export-manager.ts b/packages/webcrack/src/unpack/webpack/import-export-manager.ts index f2fcbe23..fb078c98 100644 --- a/packages/webcrack/src/unpack/webpack/import-export-manager.ts +++ b/packages/webcrack/src/unpack/webpack/import-export-manager.ts @@ -1,4 +1,4 @@ -import { expression, statement } from '@babel/template'; +import { statement } from '@babel/template'; import type { Binding, NodePath, Scope } from '@babel/traverse'; import * as t from '@babel/types'; import * as m from '@codemod/matchers'; @@ -24,11 +24,9 @@ interface RequireCall { interface RequireVar { binding: Binding; moduleId: string; - imports: ( - | t.ImportSpecifier - | t.ImportNamespaceSpecifier - | t.ImportDefaultSpecifier - )[]; + defaultImport?: t.ImportDefaultSpecifier; + namespaceImport?: t.ImportNamespaceSpecifier; + namedImports: t.ImportSpecifier[]; exports: (t.ExportSpecifier | t.ExportNamespaceSpecifier)[]; } @@ -62,8 +60,6 @@ export class ImportExportManager { } insertImportsAndExports() { - this.collectImports(); - this.requireVars.forEach((requireVar) => { // TODO: resolve module id to path const namedExports = t.exportNamedDeclaration( @@ -92,42 +88,42 @@ export class ImportExportManager { // ); // } + // requireVar.binding.path.remove(); + }); + + this.collectImports(); + + this.requireVars.forEach((requireVar) => { const namedImports = t.importDeclaration( - [ - ...requireVar.imports.filter((node) => - t.isImportDefaultSpecifier(node), - ), - ...requireVar.imports.filter((node) => t.isImportSpecifier(node)), - ], + [requireVar.defaultImport ?? [], requireVar.namedImports].flat(), t.stringLiteral(requireVar.moduleId), ); - const namespaceImports = requireVar.imports - .filter((node) => t.isImportNamespaceSpecifier(node)) - .map((node) => - t.importDeclaration([node], t.stringLiteral(requireVar.moduleId)), - ); if (namedImports.specifiers.length > 0) { requireVar.binding.path.parentPath!.insertAfter(namedImports); } - requireVar.binding.path.parentPath!.insertAfter(namespaceImports); - - // requireVar.binding.path.remove(); - }); + if (requireVar.namespaceImport) { + const namespaceImport = t.importDeclaration( + [requireVar.namespaceImport], + t.stringLiteral(requireVar.moduleId), + ); + requireVar.binding.path.parentPath!.insertAfter(namespaceImport); + } - this.requireVars.forEach((requireVar) => { - if (requireVar.binding.references === 0) { + if (!requireVar.binding.referenced) { // side-effect import - requireVar.binding.path.parentPath!.replaceWith( + requireVar.binding.path.parentPath!.insertAfter( t.importDeclaration([], t.stringLiteral(requireVar.moduleId)), ); } + + requireVar.binding.path.parentPath!.remove(); }); // TODO: hoist imports to the top of the file - this.requireCalls.forEach(({ path, moduleId }) => { - path.replaceWith(expression`require('${moduleId}')`()); - }); + // this.requireCalls.forEach(({ path, moduleId }) => { + // path.replaceWith(expression`require('${moduleId}')`()); + // }); } private collectImports() { @@ -135,6 +131,7 @@ export class ImportExportManager { const memberExpressionMatcher = m.memberExpression( m.identifier(), m.identifier(property), + false, ); const zeroSequenceMatcher = m.sequenceExpression([ m.numericLiteral(0), @@ -144,16 +141,16 @@ export class ImportExportManager { this.requireVars.forEach((requireVar) => { const { binding } = requireVar; const importedLocalNames = new Set(); - const hasOnlyNamedImports = - binding.references > 0 && - binding.referencePaths.every((ref) => - memberExpressionMatcher.match(ref.parent), - ); - if (hasOnlyNamedImports) { - binding.referencePaths.forEach((reference) => { - memberExpressionMatcher.match(reference.parent); // to populate property.current + binding.referencePaths.forEach((reference) => { + if (memberExpressionMatcher.match(reference.parent)) { const importedName = property.current!; + if (importedName === 'default') { + const localName = this.addDefaultImport(requireVar); + reference.parentPath!.replaceWith(t.identifier(localName)); + return; + } + const hasNameConflict = binding.referencePaths.some((ref) => ref.scope.hasBinding(importedName), ); @@ -165,31 +162,32 @@ export class ImportExportManager { importedLocalNames.add(localName); this.addNamedImport(requireVar, localName, importedName); + if (zeroSequenceMatcher.match(reference.parentPath?.parent)) { + reference.parentPath.parentPath!.replaceWith( + t.identifier(localName), + ); + } else { + reference.parentPath!.replaceWith(t.identifier(localName)); + } } - - if (zeroSequenceMatcher.match(reference.parentPath?.parent)) { - reference.parentPath.parentPath!.replaceWith( - t.identifier(localName), - ); - } else { - reference.parentPath!.replaceWith(t.identifier(localName)); - } - if (!memberExpressionMatcher.match(reference.parent)) return; - }); - binding.path.remove(); - } else { - binding.referencePaths.forEach((reference) => { - reference.replaceWith(t.identifier(binding.identifier.name)); - }); - if (binding.references > 0) { - this.addImportNamespace(requireVar); + } else { + this.addNamespaceImport(requireVar); } - } - }); + }); - this.requireCalls.forEach(({ path, moduleId }) => { - path.replaceWith(expression`require('${moduleId}')`()); + // if (zeroSequenceMatcher.match(reference.parentPath?.parent)) { + // reference.parentPath.parentPath!.replaceWith( + // t.identifier(localName), + // ); + // } else { + // reference.parentPath!.replaceWith(t.identifier(localName)); + // } + // if (!memberExpressionMatcher.match(reference.parent)) return; }); + + // this.requireCalls.forEach(({ path, moduleId }) => { + // path.replaceWith(expression`require('${moduleId}')`()); + // }); } addExport(scope: Scope, exportName: string, value: t.Expression) { @@ -242,18 +240,12 @@ export class ImportExportManager { * @returns local name of the default import */ addDefaultImport(requireVar: RequireVar) { - const existingDefaultImport = requireVar.imports.find((node) => - t.isImportDefaultSpecifier(node), + requireVar.defaultImport ??= t.importDefaultSpecifier( + requireVar.binding.scope.generateUidIdentifier( + `${requireVar.binding.identifier.name}_default`, + ), ); - if (existingDefaultImport) { - return existingDefaultImport.local.name; - } else { - const localName = requireVar.binding.scope.generateUid('default'); - requireVar.imports.push( - t.importDefaultSpecifier(t.identifier(localName)), - ); - return localName; - } + return requireVar.defaultImport.local.name; } private addNamedImport( @@ -261,16 +253,14 @@ export class ImportExportManager { localName: string, importedName: string, ) { - requireVar.imports.push( + requireVar.namedImports.push( t.importSpecifier(t.identifier(localName), t.identifier(importedName)), ); } - private addImportNamespace(requireVar: RequireVar) { - requireVar.imports.push( - t.importNamespaceSpecifier( - t.identifier(requireVar.binding.identifier.name), - ), + private addNamespaceImport(requireVar: RequireVar) { + requireVar.namespaceImport ??= t.importNamespaceSpecifier( + t.identifier(requireVar.binding.identifier.name), ); } @@ -401,7 +391,9 @@ export class ImportExportManager { this.requireVars.push({ moduleId, binding, - imports: [], + defaultImport: undefined, + namespaceImport: undefined, + namedImports: [], exports: [], }); } From bbedc109a0b8c48a0fc2568a4f289b9359d8d2ba Mon Sep 17 00:00:00 2001 From: j4k0xb <55899582+j4k0xb@users.noreply.github.com> Date: Sat, 13 Jan 2024 03:20:27 +0100 Subject: [PATCH 49/81] fix: indirect call import --- .../webcrack/src/unpack/test/imports.test.ts | 4 +- .../unpack/webpack/import-export-manager.ts | 44 ++++++++----------- 2 files changed, 20 insertions(+), 28 deletions(-) diff --git a/packages/webcrack/src/unpack/test/imports.test.ts b/packages/webcrack/src/unpack/test/imports.test.ts index 4d0b7742..06d90a2c 100644 --- a/packages/webcrack/src/unpack/test/imports.test.ts +++ b/packages/webcrack/src/unpack/test/imports.test.ts @@ -32,8 +32,8 @@ describe('webpack 4', () => { var _tmp = __webpack_require__.n(lib).a; console.log(_tmp); `).toMatchInlineSnapshot(` - import _default from "1"; - console.log(_default); + import _lib_default from "1"; + console.log(_lib_default); `)); test('named import', () => diff --git a/packages/webcrack/src/unpack/webpack/import-export-manager.ts b/packages/webcrack/src/unpack/webpack/import-export-manager.ts index fb078c98..99f7dff8 100644 --- a/packages/webcrack/src/unpack/webpack/import-export-manager.ts +++ b/packages/webcrack/src/unpack/webpack/import-export-manager.ts @@ -80,15 +80,6 @@ export class ImportExportManager { requireVar.binding.path.parentPath!.insertAfter(namedExports); } requireVar.binding.path.parentPath!.insertAfter(namespaceExports); - - // FIXME: collect this information earlier - // if (requireVar.binding.references > 1) { - // requireVar.binding.path.parentPath!.insertAfter( - // statement`import * as ${requireVar.binding.identifier} from '${requireVar.moduleId}'`(), - // ); - // } - - // requireVar.binding.path.remove(); }); this.collectImports(); @@ -110,8 +101,13 @@ export class ImportExportManager { requireVar.binding.path.parentPath!.insertAfter(namespaceImport); } - if (!requireVar.binding.referenced) { - // side-effect import + const hasImports = + requireVar.defaultImport || + requireVar.namespaceImport || + requireVar.namedImports.length > 0; + + // side-effect import + if (!requireVar.binding.referenced && !hasImports) { requireVar.binding.path.parentPath!.insertAfter( t.importDeclaration([], t.stringLiteral(requireVar.moduleId)), ); @@ -133,10 +129,14 @@ export class ImportExportManager { m.identifier(property), false, ); - const zeroSequenceMatcher = m.sequenceExpression([ - m.numericLiteral(0), - m.memberExpression(m.identifier(), m.identifier(property)), - ]); + const indirectCallMatcher = m.callExpression( + m.or( + // webpack 4: Object(lib.foo)("bar") + m.callExpression(m.identifier('Object'), [memberExpressionMatcher]), + // webpack 5: (0, lib.foo)("bar") + m.sequenceExpression([m.numericLiteral(0), memberExpressionMatcher]), + ), + ); this.requireVars.forEach((requireVar) => { const { binding } = requireVar; @@ -162,8 +162,8 @@ export class ImportExportManager { importedLocalNames.add(localName); this.addNamedImport(requireVar, localName, importedName); - if (zeroSequenceMatcher.match(reference.parentPath?.parent)) { - reference.parentPath.parentPath!.replaceWith( + if (indirectCallMatcher.match(reference.parentPath?.parentPath?.parent)) { + reference.parentPath.parentPath.replaceWith( t.identifier(localName), ); } else { @@ -174,17 +174,9 @@ export class ImportExportManager { this.addNamespaceImport(requireVar); } }); - - // if (zeroSequenceMatcher.match(reference.parentPath?.parent)) { - // reference.parentPath.parentPath!.replaceWith( - // t.identifier(localName), - // ); - // } else { - // reference.parentPath!.replaceWith(t.identifier(localName)); - // } - // if (!memberExpressionMatcher.match(reference.parent)) return; }); + // this should never happen: // this.requireCalls.forEach(({ path, moduleId }) => { // path.replaceWith(expression`require('${moduleId}')`()); // }); From 949248e65e3648b3ae53b942fbc1828e29075211 Mon Sep 17 00:00:00 2001 From: j4k0xb <55899582+j4k0xb@users.noreply.github.com> Date: Sat, 13 Jan 2024 03:20:27 +0100 Subject: [PATCH 50/81] refactor --- .../webcrack/src/unpack/test/imports.test.ts | 6 +-- .../unpack/webpack/import-export-manager.ts | 43 ++++++++++--------- 2 files changed, 26 insertions(+), 23 deletions(-) diff --git a/packages/webcrack/src/unpack/test/imports.test.ts b/packages/webcrack/src/unpack/test/imports.test.ts index 06d90a2c..3fce1e95 100644 --- a/packages/webcrack/src/unpack/test/imports.test.ts +++ b/packages/webcrack/src/unpack/test/imports.test.ts @@ -21,9 +21,9 @@ describe('webpack 4', () => { console.log(_tmp.a); console.log(_tmp()); `).toMatchInlineSnapshot(` - import _default from "1"; - console.log(_default); - console.log(_default); + import _lib_default from "1"; + console.log(_lib_default); + console.log(_lib_default); `)); test('inlined default import of commonjs module', () => diff --git a/packages/webcrack/src/unpack/webpack/import-export-manager.ts b/packages/webcrack/src/unpack/webpack/import-export-manager.ts index 99f7dff8..fffb3ec3 100644 --- a/packages/webcrack/src/unpack/webpack/import-export-manager.ts +++ b/packages/webcrack/src/unpack/webpack/import-export-manager.ts @@ -2,13 +2,13 @@ import { statement } from '@babel/template'; import type { Binding, NodePath, Scope } from '@babel/traverse'; import * as t from '@babel/types'; import * as m from '@codemod/matchers'; +import assert from 'assert'; import { generate, renameFast } from '../../ast-utils'; // TODO: hoist re-exports to the top of the file (but retain order relative to imports) // TODO: when it accesses module.exports, dont convert to esm -// TODO: side-effect import +// TODO: sort named imports alphabetically // FIXME: remove unused require vars (when they were used for imports/exports) -// also store import/export metadata in the require var for easier management /** * Example: `__webpack_require__(id)` @@ -124,17 +124,17 @@ export class ImportExportManager { private collectImports() { const property = m.capture(m.anyString()); - const memberExpressionMatcher = m.memberExpression( + const memberExpression = m.memberExpression( m.identifier(), m.identifier(property), false, ); - const indirectCallMatcher = m.callExpression( + const indirectCall = m.callExpression( m.or( // webpack 4: Object(lib.foo)("bar") - m.callExpression(m.identifier('Object'), [memberExpressionMatcher]), + m.callExpression(m.identifier('Object'), [memberExpression]), // webpack 5: (0, lib.foo)("bar") - m.sequenceExpression([m.numericLiteral(0), memberExpressionMatcher]), + m.sequenceExpression([m.numericLiteral(0), memberExpression]), ), ); @@ -143,12 +143,15 @@ export class ImportExportManager { const importedLocalNames = new Set(); binding.referencePaths.forEach((reference) => { - if (memberExpressionMatcher.match(reference.parent)) { + if (memberExpression.match(reference.parent)) { const importedName = property.current!; + // lib.default -> _lib_default if (importedName === 'default') { const localName = this.addDefaultImport(requireVar); reference.parentPath!.replaceWith(t.identifier(localName)); return; + } else if (importedLocalNames.has(importedName)) { + return; } const hasNameConflict = binding.referencePaths.some((ref) => @@ -158,17 +161,15 @@ export class ImportExportManager { ? binding.path.scope.generateUid(importedName) : importedName; - if (!importedLocalNames.has(localName)) { - importedLocalNames.add(localName); - - this.addNamedImport(requireVar, localName, importedName); - if (indirectCallMatcher.match(reference.parentPath?.parentPath?.parent)) { - reference.parentPath.parentPath.replaceWith( - t.identifier(localName), - ); - } else { - reference.parentPath!.replaceWith(t.identifier(localName)); - } + importedLocalNames.add(localName); + this.addNamedImport(requireVar, localName, importedName); + + if (indirectCall.match(reference.parentPath?.parentPath?.parent)) { + reference.parentPath.parentPath.replaceWith( + t.identifier(localName), + ); + } else { + reference.parentPath!.replaceWith(t.identifier(localName)); } } else { this.addNamespaceImport(requireVar); @@ -296,8 +297,10 @@ export class ImportExportManager { m.functionDeclaration(), m.exportNamedDeclaration(), ); - // FIXME: most likely an inlined variable declaration - if (!matcher.match(statementPath.node)) return; + assert( + matcher.match(statementPath.node), + `unexpected export statement: ${statementPath.type}`, + ); const isDeclarationExport = exportName !== 'default' && From da538fd3594d3cc1d95dbbcee4118947a0e2c046 Mon Sep 17 00:00:00 2001 From: j4k0xb <55899582+j4k0xb@users.noreply.github.com> Date: Sat, 13 Jan 2024 03:20:27 +0100 Subject: [PATCH 51/81] fix: default import --- .../webcrack/src/unpack/test/imports.test.ts | 3 +- .../webpack/runtime/get-default-export.ts | 43 +++++++++---------- 2 files changed, 22 insertions(+), 24 deletions(-) diff --git a/packages/webcrack/src/unpack/test/imports.test.ts b/packages/webcrack/src/unpack/test/imports.test.ts index 3fce1e95..e90b396a 100644 --- a/packages/webcrack/src/unpack/test/imports.test.ts +++ b/packages/webcrack/src/unpack/test/imports.test.ts @@ -29,8 +29,7 @@ describe('webpack 4', () => { test('inlined default import of commonjs module', () => expectJS(` var lib = __webpack_require__(1); - var _tmp = __webpack_require__.n(lib).a; - console.log(_tmp); + console.log(__webpack_require__.n(lib).a); `).toMatchInlineSnapshot(` import _lib_default from "1"; console.log(_lib_default); diff --git a/packages/webcrack/src/unpack/webpack/runtime/get-default-export.ts b/packages/webcrack/src/unpack/webpack/runtime/get-default-export.ts index fd884892..0ca42ad9 100644 --- a/packages/webcrack/src/unpack/webpack/runtime/get-default-export.ts +++ b/packages/webcrack/src/unpack/webpack/runtime/get-default-export.ts @@ -37,33 +37,32 @@ export default { if (!getDefaultExportMatcher.match(callPath?.node)) return; // Example: `__webpack_require__.n(moduleVar).a` - const isInlined = !getDefaultExportVarMatcher.match(callPath.parent); - if (isInlined) { - const moduleBinding = reference.scope.getBinding(moduleVar.current!); - if (!moduleBinding) return; - const requireVar = manager.findRequireVar(moduleBinding.path.node); - if (!requireVar) return; - - requireVar.binding.dereference(); - const importName = manager.addDefaultImport(requireVar); - callPath.parentPath?.replaceWith(t.identifier(importName)); - this.changes++; - return; - } - - const tmpVarBinding = reference.scope.getBinding(tmpVarName.current!); const moduleBinding = reference.scope.getBinding(moduleVar.current!); - if (!moduleBinding || !tmpVarBinding) return; + if (!moduleBinding) return; const requireVar = manager.findRequireVar(moduleBinding.path.node); if (!requireVar) return; - const importName = manager.addDefaultImport(requireVar); - // `_tmp.a` or `_tmp()` -> `importName` - tmpVarBinding.referencePaths.forEach((refPath) => { - refPath.parentPath?.replaceWith(t.identifier(importName)); - }); - tmpVarBinding.path.remove(); + requireVar.binding.referencePaths = + requireVar.binding.referencePaths.filter( + (ref) => ref.parent !== callPath.node, + ); requireVar.binding.dereference(); + + const importName = manager.addDefaultImport(requireVar); + + const isInlined = !getDefaultExportVarMatcher.match(callPath.parent); + if (isInlined) { + callPath.parentPath?.replaceWith(t.identifier(importName)); + } else { + const tmpVarBinding = reference.scope.getBinding(tmpVarName.current!); + if (!tmpVarBinding) return; + + // `_tmp.a` or `_tmp()` -> `importName` + tmpVarBinding.referencePaths.forEach((refPath) => { + refPath.parentPath?.replaceWith(t.identifier(importName)); + }); + tmpVarBinding.path.remove(); + } this.changes++; }); }, From 5420156292179b740fdd985a86f6f0689eac9841 Mon Sep 17 00:00:00 2001 From: j4k0xb <55899582+j4k0xb@users.noreply.github.com> Date: Sat, 13 Jan 2024 03:20:27 +0100 Subject: [PATCH 52/81] fix: imports --- packages/webcrack/src/ast-utils/binding.ts | 15 ++++++++++++ .../webcrack/src/unpack/test/exports.test.ts | 4 ++-- .../unpack/webpack/import-export-manager.ts | 23 +++++++++---------- .../webpack/runtime/get-default-export.ts | 15 ++++++------ 4 files changed, 35 insertions(+), 22 deletions(-) create mode 100644 packages/webcrack/src/ast-utils/binding.ts 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/unpack/test/exports.test.ts b/packages/webcrack/src/unpack/test/exports.test.ts index d8b50233..efbbe767 100644 --- a/packages/webcrack/src/unpack/test/exports.test.ts +++ b/packages/webcrack/src/unpack/test/exports.test.ts @@ -57,11 +57,11 @@ describe('webpack 4', () => { expectJS(` __webpack_require__.d(__webpack_exports__, "readFile", function() { return lib.readFile; }); var lib = __webpack_require__("lib"); - lib.writeFile(); + console.log(lib); `).toMatchInlineSnapshot(` import * as lib from "lib"; export { readFile } from "lib"; - lib.writeFile(); + console.log(lib); `)); test('re-export named as named', () => diff --git a/packages/webcrack/src/unpack/webpack/import-export-manager.ts b/packages/webcrack/src/unpack/webpack/import-export-manager.ts index fffb3ec3..b42f9e71 100644 --- a/packages/webcrack/src/unpack/webpack/import-export-manager.ts +++ b/packages/webcrack/src/unpack/webpack/import-export-manager.ts @@ -4,9 +4,10 @@ import * as t from '@babel/types'; import * as m from '@codemod/matchers'; import assert from 'assert'; import { generate, renameFast } from '../../ast-utils'; +import { dereference } from '../../ast-utils/binding'; -// TODO: hoist re-exports to the top of the file (but retain order relative to imports) -// TODO: when it accesses module.exports, dont convert to esm +// TODO: hoist imports/re-exports to the top of the file (but retain order relative to imports) +// TODO: when it accesses module.exports, don't convert to esm // TODO: sort named imports alphabetically // FIXME: remove unused require vars (when they were used for imports/exports) @@ -105,9 +106,10 @@ export class ImportExportManager { requireVar.defaultImport || requireVar.namespaceImport || requireVar.namedImports.length > 0; + const hasExports = requireVar.exports.length > 0; // side-effect import - if (!requireVar.binding.referenced && !hasImports) { + if (!requireVar.binding.referenced && !hasImports && !hasExports) { requireVar.binding.path.parentPath!.insertAfter( t.importDeclaration([], t.stringLiteral(requireVar.moduleId)), ); @@ -116,7 +118,7 @@ export class ImportExportManager { requireVar.binding.path.parentPath!.remove(); }); - // TODO: hoist imports to the top of the file + // this should never happen: // this.requireCalls.forEach(({ path, moduleId }) => { // path.replaceWith(expression`require('${moduleId}')`()); // }); @@ -176,20 +178,15 @@ export class ImportExportManager { } }); }); - - // this should never happen: - // this.requireCalls.forEach(({ path, moduleId }) => { - // path.replaceWith(expression`require('${moduleId}')`()); - // }); } addExport(scope: Scope, exportName: string, value: t.Expression) { this.exports.add(exportName); - const objectName = m.capture(m.anyString()); + const objectId = m.capture(m.identifier()); const propertyName = m.capture(m.anyString()); const memberExpressionMatcher = m.memberExpression( - m.identifier(objectName), + objectId, m.identifier(propertyName), ); @@ -200,18 +197,20 @@ export class ImportExportManager { if (requireVar) { this.addExportNamespace(requireVar, exportName); + dereference(requireVar.binding, value); } else if (exportName === 'default' && binding.references === 1) { this.addExportDefault(binding); } else { this.addExportDeclaration(binding, exportName); } } else if (memberExpressionMatcher.match(value)) { - const binding = scope.getBinding(objectName.current!); + const binding = scope.getBinding(objectId.current!.name); if (!binding) return; const requireVar = this.findRequireVar(binding.path.node); if (!requireVar) return; this.addExportFrom(requireVar, propertyName.current!, exportName); + dereference(requireVar.binding, objectId.current!); } else { t.addComment( this.ast.program, diff --git a/packages/webcrack/src/unpack/webpack/runtime/get-default-export.ts b/packages/webcrack/src/unpack/webpack/runtime/get-default-export.ts index 0ca42ad9..82c98af0 100644 --- a/packages/webcrack/src/unpack/webpack/runtime/get-default-export.ts +++ b/packages/webcrack/src/unpack/webpack/runtime/get-default-export.ts @@ -3,6 +3,7 @@ import * as m from '@codemod/matchers'; import assert from 'assert'; import type { Transform } from '../../../ast-utils'; import { constMemberExpression } from '../../../ast-utils'; +import { dereference } from '../../../ast-utils/binding'; import type { ImportExportManager } from '../import-export-manager'; // var a = __webpack_require__(11); var i = __webpack_require__.n(a); let p = i.a; @@ -20,10 +21,10 @@ export default { run(ast, manager) { assert(manager); - const moduleVar = m.capture(m.anyString()); + const moduleVarId = m.capture(m.identifier()); const getDefaultExportMatcher = m.callExpression( constMemberExpression('__webpack_require__', 'n'), - [m.identifier(moduleVar)], + [moduleVarId], ); const tmpVarName = m.capture(m.anyString()); const getDefaultExportVarMatcher = m.variableDeclarator( @@ -37,16 +38,14 @@ export default { if (!getDefaultExportMatcher.match(callPath?.node)) return; // Example: `__webpack_require__.n(moduleVar).a` - const moduleBinding = reference.scope.getBinding(moduleVar.current!); + const moduleBinding = reference.scope.getBinding( + moduleVarId.current!.name, + ); if (!moduleBinding) return; const requireVar = manager.findRequireVar(moduleBinding.path.node); if (!requireVar) return; - requireVar.binding.referencePaths = - requireVar.binding.referencePaths.filter( - (ref) => ref.parent !== callPath.node, - ); - requireVar.binding.dereference(); + dereference(requireVar.binding, moduleVarId.current!); const importName = manager.addDefaultImport(requireVar); From cc203711e6a3f299950f5bd29b69c100919356e9 Mon Sep 17 00:00:00 2001 From: j4k0xb <55899582+j4k0xb@users.noreply.github.com> Date: Sat, 13 Jan 2024 03:20:27 +0100 Subject: [PATCH 53/81] test: enable remaining export tests --- packages/webcrack/src/unpack/test/exports.test.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/webcrack/src/unpack/test/exports.test.ts b/packages/webcrack/src/unpack/test/exports.test.ts index efbbe767..ebf9d16c 100644 --- a/packages/webcrack/src/unpack/test/exports.test.ts +++ b/packages/webcrack/src/unpack/test/exports.test.ts @@ -126,12 +126,11 @@ describe('webpack 4', () => { export * as lib from "lib"; `)); - test.todo('re-export all as default', () => + 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";`), - ); + `).toMatchInlineSnapshot(`export * as default from "lib";`)); test('namespace object', () => expectJS(` From 6975fb0c63bdb895afad3fe5b97233d574de18d8 Mon Sep 17 00:00:00 2001 From: j4k0xb <55899582+j4k0xb@users.noreply.github.com> Date: Sat, 13 Jan 2024 03:20:27 +0100 Subject: [PATCH 54/81] test: mixed import/require --- packages/webcrack/src/unpack/test/imports.test.ts | 10 ++++++++++ .../src/unpack/webpack/import-export-manager.ts | 11 ++++++----- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/packages/webcrack/src/unpack/test/imports.test.ts b/packages/webcrack/src/unpack/test/imports.test.ts index e90b396a..7b43d777 100644 --- a/packages/webcrack/src/unpack/test/imports.test.ts +++ b/packages/webcrack/src/unpack/test/imports.test.ts @@ -96,6 +96,16 @@ describe('webpack 4', () => { }); `), ); + + test('mixed import/require', () => + expectJS(` + __webpack_require__.r(__webpack_exports__); + 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', () => { diff --git a/packages/webcrack/src/unpack/webpack/import-export-manager.ts b/packages/webcrack/src/unpack/webpack/import-export-manager.ts index b42f9e71..0b86547f 100644 --- a/packages/webcrack/src/unpack/webpack/import-export-manager.ts +++ b/packages/webcrack/src/unpack/webpack/import-export-manager.ts @@ -1,4 +1,4 @@ -import { statement } from '@babel/template'; +import { expression, statement } from '@babel/template'; import type { Binding, NodePath, Scope } from '@babel/traverse'; import * as t from '@babel/types'; import * as m from '@codemod/matchers'; @@ -118,10 +118,11 @@ export class ImportExportManager { requireVar.binding.path.parentPath!.remove(); }); - // this should never happen: - // this.requireCalls.forEach(({ path, moduleId }) => { - // path.replaceWith(expression`require('${moduleId}')`()); - // }); + // this should never happen unless for mixed esm/commonjs: + this.requireCalls.forEach(({ path, moduleId }) => { + // TODO: resolve module id to path + path.replaceWith(expression`require('${moduleId}')`()); + }); } private collectImports() { From b7543c49f19387ca4cb7a8fad48c94e1ab8e0102 Mon Sep 17 00:00:00 2001 From: j4k0xb <55899582+j4k0xb@users.noreply.github.com> Date: Sat, 13 Jan 2024 03:20:27 +0100 Subject: [PATCH 55/81] feat: sort import specifiers alphabetically --- .../webcrack/src/unpack/test/imports.test.ts | 25 +++++++++++++++++++ .../unpack/webpack/import-export-manager.ts | 13 +++++++--- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/packages/webcrack/src/unpack/test/imports.test.ts b/packages/webcrack/src/unpack/test/imports.test.ts index 7b43d777..0ba5f6f5 100644 --- a/packages/webcrack/src/unpack/test/imports.test.ts +++ b/packages/webcrack/src/unpack/test/imports.test.ts @@ -97,6 +97,31 @@ describe('webpack 4', () => { `), ); + test('sort import specifiers alphabetically', () => + expectJS(` + __webpack_require__.r(__webpack_exports__); + 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; + __webpack_require__.r(__webpack_exports__); + 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); + `), + ); + test('mixed import/require', () => expectJS(` __webpack_require__.r(__webpack_exports__); diff --git a/packages/webcrack/src/unpack/webpack/import-export-manager.ts b/packages/webcrack/src/unpack/webpack/import-export-manager.ts index 0b86547f..96327683 100644 --- a/packages/webcrack/src/unpack/webpack/import-export-manager.ts +++ b/packages/webcrack/src/unpack/webpack/import-export-manager.ts @@ -6,10 +6,7 @@ import assert from 'assert'; import { generate, renameFast } from '../../ast-utils'; import { dereference } from '../../ast-utils/binding'; -// TODO: hoist imports/re-exports to the top of the file (but retain order relative to imports) // TODO: when it accesses module.exports, don't convert to esm -// TODO: sort named imports alphabetically -// FIXME: remove unused require vars (when they were used for imports/exports) /** * Example: `__webpack_require__(id)` @@ -86,6 +83,8 @@ export class ImportExportManager { this.collectImports(); this.requireVars.forEach((requireVar) => { + this.sortImportSpecifiers(requireVar.namedImports); + const namedImports = t.importDeclaration( [requireVar.defaultImport ?? [], requireVar.namedImports].flat(), t.stringLiteral(requireVar.moduleId), @@ -125,6 +124,14 @@ export class ImportExportManager { }); } + private sortImportSpecifiers(specifiers: t.ImportSpecifier[]) { + specifiers.sort((a, b) => + (a.imported as t.Identifier).name.localeCompare( + (b.imported as t.Identifier).name, + ), + ); + } + private collectImports() { const property = m.capture(m.anyString()); const memberExpression = m.memberExpression( From f1ac8399c4dbe61172a274859ddecaab52f36054 Mon Sep 17 00:00:00 2001 From: j4k0xb <55899582+j4k0xb@users.noreply.github.com> Date: Sat, 13 Jan 2024 03:20:27 +0100 Subject: [PATCH 56/81] refactor: separate namespace and named export properties --- .../unpack/webpack/import-export-manager.ts | 43 +++++++++++-------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/packages/webcrack/src/unpack/webpack/import-export-manager.ts b/packages/webcrack/src/unpack/webpack/import-export-manager.ts index 96327683..e7ec42fa 100644 --- a/packages/webcrack/src/unpack/webpack/import-export-manager.ts +++ b/packages/webcrack/src/unpack/webpack/import-export-manager.ts @@ -25,7 +25,8 @@ interface RequireVar { defaultImport?: t.ImportDefaultSpecifier; namespaceImport?: t.ImportNamespaceSpecifier; namedImports: t.ImportSpecifier[]; - exports: (t.ExportSpecifier | t.ExportNamespaceSpecifier)[]; + namespaceExport?: t.ExportNamespaceSpecifier; + namedExports: t.ExportSpecifier[]; } export class ImportExportManager { @@ -62,22 +63,21 @@ export class ImportExportManager { // TODO: resolve module id to path const namedExports = t.exportNamedDeclaration( undefined, - requireVar.exports.filter((node) => t.isExportSpecifier(node)), + requireVar.namedExports, t.stringLiteral(requireVar.moduleId), ); - const namespaceExports = requireVar.exports - .filter((node) => t.isExportNamespaceSpecifier(node)) - .map((node) => - t.exportNamedDeclaration( - undefined, - [node], - t.stringLiteral(requireVar.moduleId), - ), - ); if (namedExports.specifiers.length > 0) { requireVar.binding.path.parentPath!.insertAfter(namedExports); } - requireVar.binding.path.parentPath!.insertAfter(namespaceExports); + if (requireVar.namespaceExport) { + // TODO: resolve module id to path + const namespaceExports = t.exportNamedDeclaration( + undefined, + [requireVar.namespaceExport], + t.stringLiteral(requireVar.moduleId), + ); + requireVar.binding.path.parentPath!.insertAfter(namespaceExports); + } }); this.collectImports(); @@ -85,6 +85,7 @@ export class ImportExportManager { this.requireVars.forEach((requireVar) => { this.sortImportSpecifiers(requireVar.namedImports); + // TODO: resolve module id to path const namedImports = t.importDeclaration( [requireVar.defaultImport ?? [], requireVar.namedImports].flat(), t.stringLiteral(requireVar.moduleId), @@ -94,6 +95,7 @@ export class ImportExportManager { requireVar.binding.path.parentPath!.insertAfter(namedImports); } if (requireVar.namespaceImport) { + // TODO: resolve module id to path const namespaceImport = t.importDeclaration( [requireVar.namespaceImport], t.stringLiteral(requireVar.moduleId), @@ -102,13 +104,15 @@ export class ImportExportManager { } const hasImports = - requireVar.defaultImport || - requireVar.namespaceImport || + !!requireVar.defaultImport || + !!requireVar.namespaceImport || requireVar.namedImports.length > 0; - const hasExports = requireVar.exports.length > 0; + const hasExports = + !!requireVar.namespaceExport || requireVar.namedExports.length > 0; // side-effect import if (!requireVar.binding.referenced && !hasImports && !hasExports) { + // TODO: resolve module id to path requireVar.binding.path.parentPath!.insertAfter( t.importDeclaration([], t.stringLiteral(requireVar.moduleId)), ); @@ -280,7 +284,7 @@ export class ImportExportManager { localName: string, exportName: string, ) { - requireVar.exports.push( + requireVar.namedExports.push( t.exportSpecifier(t.identifier(localName), t.identifier(exportName)), ); } @@ -360,8 +364,8 @@ export class ImportExportManager { * ``` */ private addExportNamespace(requireVar: RequireVar, exportName: string) { - requireVar.exports.push( - t.exportNamespaceSpecifier(t.identifier(exportName)), + requireVar.namespaceExport ??= t.exportNamespaceSpecifier( + t.identifier(exportName), ); } @@ -396,7 +400,8 @@ export class ImportExportManager { defaultImport: undefined, namespaceImport: undefined, namedImports: [], - exports: [], + namespaceExport: undefined, + namedExports: [], }); } }); From 5f778fcd110ccc7399e97dcc9e3b76e7a060fbdb Mon Sep 17 00:00:00 2001 From: j4k0xb <55899582+j4k0xb@users.noreply.github.com> Date: Sat, 13 Jan 2024 03:20:27 +0100 Subject: [PATCH 57/81] feat: better default import --- .../webcrack/src/unpack/test/exports.test.ts | 14 +-------- .../unpack/webpack/import-export-manager.ts | 31 +++++++++++-------- 2 files changed, 19 insertions(+), 26 deletions(-) diff --git a/packages/webcrack/src/unpack/test/exports.test.ts b/packages/webcrack/src/unpack/test/exports.test.ts index ebf9d16c..ca40c96f 100644 --- a/packages/webcrack/src/unpack/test/exports.test.ts +++ b/packages/webcrack/src/unpack/test/exports.test.ts @@ -21,18 +21,6 @@ describe('webpack 4', () => { var foo = 1; `).toMatchInlineSnapshot(`export default 1;`)); - // TODO: or `export default foo;` ? - test('export default variable with multiple references', () => - expectJS(` - __webpack_require__.d(__webpack_exports__, "default", function() { return foo; }); - var foo = 1; - console.log(foo); - `).toMatchInlineSnapshot(` - var foo = 1; - export { foo as default }; - console.log(foo); - `)); - test('export default function', () => expectJS(` __webpack_require__.d(__webpack_exports__, "default", function() { return foo; }); @@ -200,7 +188,7 @@ describe('webpack 5', () => { console.log(foo); `).toMatchInlineSnapshot(` var foo = 1; - export { foo as default }; + export default foo; console.log(foo); `)); diff --git a/packages/webcrack/src/unpack/webpack/import-export-manager.ts b/packages/webcrack/src/unpack/webpack/import-export-manager.ts index e7ec42fa..f4c25bcc 100644 --- a/packages/webcrack/src/unpack/webpack/import-export-manager.ts +++ b/packages/webcrack/src/unpack/webpack/import-export-manager.ts @@ -208,11 +208,13 @@ export class ImportExportManager { const requireVar = this.findRequireVar(binding.path.node); if (requireVar) { - this.addExportNamespace(requireVar, exportName); dereference(requireVar.binding, value); - } else if (exportName === 'default' && binding.references === 1) { - this.addExportDefault(binding); + this.addExportNamespace(requireVar, exportName); + } else if (exportName === 'default') { + dereference(binding, value); + this.addExportDefault(binding, binding.referenced ? value : undefined); } else { + dereference(binding, value); this.addExportDeclaration(binding, exportName); } } else if (memberExpressionMatcher.match(value)) { @@ -221,8 +223,8 @@ export class ImportExportManager { const requireVar = this.findRequireVar(binding.path.node); if (!requireVar) return; - this.addExportFrom(requireVar, propertyName.current!, exportName); dereference(requireVar.binding, objectId.current!); + this.addExportFrom(requireVar, propertyName.current!, exportName); } else { t.addComment( this.ast.program, @@ -341,15 +343,18 @@ export class ImportExportManager { * export default 1; * ``` */ - private addExportDefault(binding: Binding) { - const node = binding.path.node; - const value = - node.type === 'VariableDeclarator' - ? node.init! - : (node as t.ClassDeclaration | t.FunctionDeclaration); - binding.path - .getStatementParent()! - .replaceWith(statement`export default ${value}`()); + private addExportDefault(binding: Binding, value?: t.Node) { + const statementParent = binding.path.getStatementParent()!; + if (value) { + statementParent.insertAfter(statement`export default ${value}`()); + } else { + const node = binding.path.node; + value = + node.type === 'VariableDeclarator' + ? node.init! + : (node as t.ClassDeclaration | t.FunctionDeclaration); + statementParent.replaceWith(statement`export default ${value}`()); + } } /** From ed1b93e796175bfe9c7ba073e8c133ee56d39dc8 Mon Sep 17 00:00:00 2001 From: j4k0xb <55899582+j4k0xb@users.noreply.github.com> Date: Sat, 13 Jan 2024 03:20:27 +0100 Subject: [PATCH 58/81] refactor(perf): avoid container matcher for webpack function --- .../src/unpack/webpack/common-matchers.ts | 41 +++++++++++-------- 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/packages/webcrack/src/unpack/webpack/common-matchers.ts b/packages/webcrack/src/unpack/webpack/common-matchers.ts index 3710b364..193be11b 100644 --- a/packages/webcrack/src/unpack/webpack/common-matchers.ts +++ b/packages/webcrack/src/unpack/webpack/common-matchers.ts @@ -24,27 +24,32 @@ export function webpackRequireFunctionMatcher() { m.functionDeclaration( m.identifier(), // __webpack_require__ [m.identifier()], // moduleId - // TODO(perf): use m.anyList for statements - m.containerOf( - m.callExpression( - m.or( - // Example (webpack 5): __webpack_modules__[moduleId](module, module.exports, __webpack_require__); - m.memberExpression( - m.fromCapture(containerId), - m.identifier(), - true, - ), - // Example (webpack 4): __webpack_modules__[moduleId].call(module.exports, module, module.exports, __webpack_require__); - // Example (webpack 0.11.x): __webpack_modules__[moduleId].call(null, module, module.exports, __webpack_require__); - constMemberExpression( - m.memberExpression( - m.fromCapture(containerId), - m.identifier(), - true, + m.blockStatement( + m.anyList( + m.zeroOrMore(), + m.expressionStatement( + m.callExpression( + m.or( + // Example (webpack 0.11.x): __webpack_modules__[moduleId].call(null, module, module.exports, __webpack_require__); + // Example (webpack 4): __webpack_modules__[moduleId].call(module.exports, module, module.exports, __webpack_require__); + constMemberExpression( + m.memberExpression( + m.fromCapture(containerId), + m.identifier(), + true, + ), + 'call', + ), + // Example (webpack 5): __webpack_modules__[moduleId](module, module.exports, __webpack_require__); + m.memberExpression( + m.fromCapture(containerId), + m.identifier(), + true, + ), ), - 'call', ), ), + m.zeroOrMore(), ), ), ), From 1d140331c618c73afd8909552f1b52e4e1dfb897 Mon Sep 17 00:00:00 2001 From: j4k0xb <55899582+j4k0xb@users.noreply.github.com> Date: Sat, 13 Jan 2024 03:20:27 +0100 Subject: [PATCH 59/81] refactor: separate webpack 4 and 5 matching --- .../src/unpack/webpack/common-matchers.ts | 50 +------------------ .../src/unpack/webpack/unpack-webpack-4.ts | 32 ++++++++++-- .../src/unpack/webpack/unpack-webpack-5.ts | 28 +++++++++-- 3 files changed, 56 insertions(+), 54 deletions(-) diff --git a/packages/webcrack/src/unpack/webpack/common-matchers.ts b/packages/webcrack/src/unpack/webpack/common-matchers.ts index 193be11b..9ce78e90 100644 --- a/packages/webcrack/src/unpack/webpack/common-matchers.ts +++ b/packages/webcrack/src/unpack/webpack/common-matchers.ts @@ -12,56 +12,10 @@ export type FunctionPath = NodePath< | (t.ArrowFunctionExpression & { body: t.BlockStatement }) >; -/** - * @returns - * - `webpackRequire`: A Matcher for `function __webpack_require__(moduleId) { ... }` - * - `containerId`: A matcher for e.g. `__webpack_modules__` that has to be captured before `webpackRequire` is matched - */ -export function webpackRequireFunctionMatcher() { - // Example: __webpack_modules__ - const containerId = m.capture(m.identifier()); - const webpackRequire = m.capture( - m.functionDeclaration( - m.identifier(), // __webpack_require__ - [m.identifier()], // moduleId - m.blockStatement( - m.anyList( - m.zeroOrMore(), - m.expressionStatement( - m.callExpression( - m.or( - // Example (webpack 0.11.x): __webpack_modules__[moduleId].call(null, module, module.exports, __webpack_require__); - // Example (webpack 4): __webpack_modules__[moduleId].call(module.exports, module, module.exports, __webpack_require__); - constMemberExpression( - m.memberExpression( - m.fromCapture(containerId), - m.identifier(), - true, - ), - 'call', - ), - // Example (webpack 5): __webpack_modules__[moduleId](module, module.exports, __webpack_require__); - m.memberExpression( - m.fromCapture(containerId), - m.identifier(), - true, - ), - ), - ), - ), - m.zeroOrMore(), - ), - ), - ), - ); - - return { webpackRequire, containerId }; -} - /** * Matches - * - `[,,function (module, exports, require) {...}, ...]` where the index is the module ID - * - or `{0: function (module, exports, require) {...}, ...}` where the key is the module ID + * - `[,,function (module, exports, require) {...}, ...]` where the indexes are the module ids + * - or `{0: function (module, exports, require) {...}, ...}` where the keys are the module ids */ export function modulesContainerMatcher(): m.CapturedMatcher< t.ArrayExpression | t.ObjectExpression diff --git a/packages/webcrack/src/unpack/webpack/unpack-webpack-4.ts b/packages/webcrack/src/unpack/webpack/unpack-webpack-4.ts index a44f16e1..f2aa829b 100644 --- a/packages/webcrack/src/unpack/webpack/unpack-webpack-4.ts +++ b/packages/webcrack/src/unpack/webpack/unpack-webpack-4.ts @@ -3,14 +3,13 @@ import type * as t from '@babel/types'; import * as m from '@codemod/matchers'; import type { Bundle } from '..'; import type { Transform } from '../../ast-utils'; -import { renameFast } from '../../ast-utils'; +import { constMemberExpression, renameFast } from '../../ast-utils'; import { WebpackBundle } from './bundle'; import { findAssignedEntryId, findRequiredEntryId, getModuleFunctions, modulesContainerMatcher, - webpackRequireFunctionMatcher, } from './common-matchers'; import { WebpackModule } from './module'; @@ -26,7 +25,34 @@ export default { name: 'unpack-webpack-4', tags: ['unsafe'], visitor(options = { bundle: undefined }) { - const { webpackRequire, containerId } = webpackRequireFunctionMatcher(); + // Example: __webpack_modules__ + const containerId = m.capture(m.identifier()); + const webpackRequire = m.capture( + m.functionDeclaration( + m.identifier(), // __webpack_require__ + [m.identifier()], // moduleId + m.blockStatement( + m.anyList( + m.zeroOrMore(), + // Example (webpack 0.11.x): __webpack_modules__[moduleId].call(null, module, module.exports, __webpack_require__); + // Example (webpack 4): __webpack_modules__[moduleId].call(module.exports, module, module.exports, __webpack_require__); + m.expressionStatement( + m.callExpression( + constMemberExpression( + m.memberExpression( + m.fromCapture(containerId), + m.identifier(), + true, + ), + 'call', + ), + ), + ), + m.zeroOrMore(), + ), + ), + ), + ); const container = modulesContainerMatcher(); const matcher = m.callExpression( diff --git a/packages/webcrack/src/unpack/webpack/unpack-webpack-5.ts b/packages/webcrack/src/unpack/webpack/unpack-webpack-5.ts index ef71b8ea..c51c0e73 100644 --- a/packages/webcrack/src/unpack/webpack/unpack-webpack-5.ts +++ b/packages/webcrack/src/unpack/webpack/unpack-webpack-5.ts @@ -9,7 +9,6 @@ import { findAssignedEntryId, getModuleFunctions, modulesContainerMatcher, - webpackRequireFunctionMatcher, } from './common-matchers'; import { WebpackModule } from './module'; @@ -29,7 +28,30 @@ export default { tags: ['unsafe'], scope: true, visitor(options = { bundle: undefined }) { - const { webpackRequire, containerId } = webpackRequireFunctionMatcher(); + // Example: __webpack_modules__ + const modulesId = m.capture(m.identifier()); + const webpackRequire = m.capture( + m.functionDeclaration( + m.identifier(), // __webpack_require__ + [m.identifier()], // moduleId + m.blockStatement( + m.anyList( + m.zeroOrMore(), + // Example: __webpack_modules__[moduleId](module, module.exports, __webpack_require__); + m.expressionStatement( + m.callExpression( + m.memberExpression( + m.fromCapture(modulesId), + m.identifier(), + true, + ), + ), + ), + m.zeroOrMore(), + ), + ), + ), + ); const container = modulesContainerMatcher(); const matcher = m.blockStatement( @@ -37,7 +59,7 @@ export default { m.zeroOrMore(), // Example: var __webpack_modules__ = { ... }; m.variableDeclaration(undefined, [ - m.variableDeclarator(containerId, container), + m.variableDeclarator(modulesId, container), ]), m.zeroOrMore(), webpackRequire, From 10074a78086881d25969c7b428e29118d8271df4 Mon Sep 17 00:00:00 2001 From: j4k0xb <55899582+j4k0xb@users.noreply.github.com> Date: Sat, 13 Jan 2024 03:20:27 +0100 Subject: [PATCH 60/81] refactor: remove unused webpack samples --- packages/webcrack/src/unpack/index.ts | 13 --- .../webcrack/src/unpack/test/samples.test.ts | 4 +- .../test/samples/{webpack.js => webpack-4.js} | 0 .../{webpack.js.snap => webpack-4.js.snap} | 0 ...ack5-no-entry.js => webpack-5-no-entry.js} | 2 +- ...try.js.snap => webpack-5-no-entry.js.snap} | 0 .../samples/{webpack5-esm.js => webpack-5.js} | 13 +-- ...webpack5-esm.js.snap => webpack-5.js.snap} | 0 .../unpack/test/samples/webpack-commonjs.js | 83 ---------------- .../test/samples/webpack-commonjs.js.snap | 31 ------ .../src/unpack/test/samples/webpack-esm.js | 97 ------------------- .../unpack/test/samples/webpack-esm.js.snap | 51 ---------- .../src/unpack/test/samples/webpack-object.js | 89 ----------------- .../test/samples/webpack-object.js.snap | 19 ---- .../unpack/test/samples/webpack5-object.js | 64 ------------ .../test/samples/webpack5-object.js.snap | 21 ---- .../webcrack/src/unpack/test/unpack.test.ts | 21 ++-- packages/webcrack/test/api.test.ts | 2 +- 18 files changed, 19 insertions(+), 491 deletions(-) rename packages/webcrack/src/unpack/test/samples/{webpack.js => webpack-4.js} (100%) rename packages/webcrack/src/unpack/test/samples/{webpack.js.snap => webpack-4.js.snap} (100%) rename packages/webcrack/src/unpack/test/samples/{webpack5-no-entry.js => webpack-5-no-entry.js} (97%) rename packages/webcrack/src/unpack/test/samples/{webpack5-no-entry.js.snap => webpack-5-no-entry.js.snap} (100%) rename packages/webcrack/src/unpack/test/samples/{webpack5-esm.js => webpack-5.js} (92%) rename packages/webcrack/src/unpack/test/samples/{webpack5-esm.js.snap => webpack-5.js.snap} (100%) delete mode 100644 packages/webcrack/src/unpack/test/samples/webpack-commonjs.js delete mode 100644 packages/webcrack/src/unpack/test/samples/webpack-commonjs.js.snap delete mode 100644 packages/webcrack/src/unpack/test/samples/webpack-esm.js delete mode 100644 packages/webcrack/src/unpack/test/samples/webpack-esm.js.snap delete mode 100644 packages/webcrack/src/unpack/test/samples/webpack-object.js delete mode 100644 packages/webcrack/src/unpack/test/samples/webpack-object.js.snap delete mode 100644 packages/webcrack/src/unpack/test/samples/webpack5-object.js delete mode 100644 packages/webcrack/src/unpack/test/samples/webpack5-object.js.snap diff --git a/packages/webcrack/src/unpack/index.ts b/packages/webcrack/src/unpack/index.ts index dd6b15c4..e29d8daf 100644 --- a/packages/webcrack/src/unpack/index.ts +++ b/packages/webcrack/src/unpack/index.ts @@ -1,4 +1,3 @@ -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'; @@ -11,18 +10,6 @@ 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> = {}, 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/webpack.js b/packages/webcrack/src/unpack/test/samples/webpack-4.js similarity index 100% rename from packages/webcrack/src/unpack/test/samples/webpack.js rename to packages/webcrack/src/unpack/test/samples/webpack-4.js diff --git a/packages/webcrack/src/unpack/test/samples/webpack.js.snap b/packages/webcrack/src/unpack/test/samples/webpack-4.js.snap similarity index 100% rename from packages/webcrack/src/unpack/test/samples/webpack.js.snap rename to packages/webcrack/src/unpack/test/samples/webpack-4.js.snap diff --git a/packages/webcrack/src/unpack/test/samples/webpack5-no-entry.js b/packages/webcrack/src/unpack/test/samples/webpack-5-no-entry.js similarity index 97% rename from packages/webcrack/src/unpack/test/samples/webpack5-no-entry.js rename to packages/webcrack/src/unpack/test/samples/webpack-5-no-entry.js index c9295fad..a1b00f2b 100644 --- a/packages/webcrack/src/unpack/test/samples/webpack5-no-entry.js +++ b/packages/webcrack/src/unpack/test/samples/webpack-5-no-entry.js @@ -17,7 +17,7 @@ loaded: false, exports: {} }; - __webpack_modules__[moduleId].call(module.exports, module, module.exports, __webpack_require__); + __webpack_modules__[moduleId](module, module.exports, __webpack_require__); module.loaded = true; return module.exports; } diff --git a/packages/webcrack/src/unpack/test/samples/webpack5-no-entry.js.snap b/packages/webcrack/src/unpack/test/samples/webpack-5-no-entry.js.snap similarity index 100% rename from packages/webcrack/src/unpack/test/samples/webpack5-no-entry.js.snap rename to packages/webcrack/src/unpack/test/samples/webpack-5-no-entry.js.snap diff --git a/packages/webcrack/src/unpack/test/samples/webpack5-esm.js b/packages/webcrack/src/unpack/test/samples/webpack-5.js similarity index 92% rename from packages/webcrack/src/unpack/test/samples/webpack5-esm.js rename to packages/webcrack/src/unpack/test/samples/webpack-5.js index 5c664427..f597e016 100644 --- a/packages/webcrack/src/unpack/test/samples/webpack5-esm.js +++ b/packages/webcrack/src/unpack/test/samples/webpack-5.js @@ -34,7 +34,7 @@ Console: () => bar, }); function bar() {} - } + }, }; var installedModules = {}; function __webpack_require__(moduleId) { @@ -47,17 +47,12 @@ loaded: false, exports: {}, }); - modules[moduleId].call( - module.exports, - module, - module.exports, - __webpack_require__ - ); + modules[moduleId](module, module.exports, __webpack_require__); module.loaded = true; return module.exports; } __webpack_require__.c = installedModules; - __webpack_require__.n = module => { + __webpack_require__.n = (module) => { var getter = module && module.__esModule ? () => module.default : () => module; __webpack_require__.d(getter, { @@ -80,7 +75,7 @@ }; __webpack_require__.o = (object, property) => Object.prototype.hasOwnProperty.call(object, property); - __webpack_require__.r = exports => { + __webpack_require__.r = (exports) => { if (typeof Symbol != 'undefined' && Symbol.toStringTag) { Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module', diff --git a/packages/webcrack/src/unpack/test/samples/webpack5-esm.js.snap b/packages/webcrack/src/unpack/test/samples/webpack-5.js.snap similarity index 100% rename from packages/webcrack/src/unpack/test/samples/webpack5-esm.js.snap rename to packages/webcrack/src/unpack/test/samples/webpack-5.js.snap diff --git a/packages/webcrack/src/unpack/test/samples/webpack-commonjs.js b/packages/webcrack/src/unpack/test/samples/webpack-commonjs.js deleted file mode 100644 index 33164b5b..00000000 --- a/packages/webcrack/src/unpack/test/samples/webpack-commonjs.js +++ /dev/null @@ -1,83 +0,0 @@ -// https://github.com/webpack/webpack/tree/e518d0da94639476354f028cdf032de4be6813a0/examples/commonjs -/******/ (() => { // webpackBootstrap -/******/ var __webpack_modules__ = ([ -/* 0 */, -/* 1 */ -/*!**********************!*\ - !*** ./increment.js ***! - \**********************/ -/*! default exports */ -/*! export increment [provided] [no usage info] [missing usage info prevents renaming] */ -/*! other exports [not provided] [no usage info] */ -/*! runtime requirements: __webpack_require__, __webpack_exports__ */ -/***/ ((__unused_webpack_module, exports, __webpack_require__) => { - - const add = (__webpack_require__(/*! ./math */ 2).add); - exports.increment = function(val) { - return add(val, 1); - }; - - - /***/ }), - /* 2 */ - /*!*****************!*\ - !*** ./math.js ***! - \*****************/ - /*! default exports */ - /*! export add [provided] [no usage info] [missing usage info prevents renaming] */ - /*! other exports [not provided] [no usage info] */ - /*! runtime requirements: __webpack_exports__ */ - /***/ ((__unused_webpack_module, exports) => { - - exports.add = function() { - var sum = 0, i = 0, args = arguments, l = args.length; - while (i < l) { - sum += args[i++]; - } - return sum; - }; - - /***/ }) - /******/ ]); - /************************************************************************/ - /******/ // The module cache - /******/ var __webpack_module_cache__ = {}; - /******/ - /******/ // The require function - /******/ function __webpack_require__(moduleId) { - /******/ // Check if module is in cache - /******/ var cachedModule = __webpack_module_cache__[moduleId]; - /******/ if (cachedModule !== undefined) { - /******/ return cachedModule.exports; - /******/ } - /******/ // Create a new module (and put it into the cache) - /******/ var module = __webpack_module_cache__[moduleId] = { - /******/ // no module.id needed - /******/ // no module.loaded needed - /******/ exports: {} - /******/ }; - /******/ - /******/ // Execute the module function - /******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__); - /******/ - /******/ // Return the exports of the module - /******/ return module.exports; - /******/ } - /******/ - /************************************************************************/ - var __webpack_exports__ = {}; - // This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk. - (() => { - /*!********************!*\ - !*** ./example.js ***! - \********************/ - /*! unknown exports (runtime-defined) */ - /*! runtime requirements: __webpack_require__ */ - const inc = (__webpack_require__(/*! ./increment */ 1).increment); - const a = 1; - inc(a); // 2 - - })(); - - /******/ })() - ; \ No newline at end of file diff --git a/packages/webcrack/src/unpack/test/samples/webpack-commonjs.js.snap b/packages/webcrack/src/unpack/test/samples/webpack-commonjs.js.snap deleted file mode 100644 index 6729e8ed..00000000 --- a/packages/webcrack/src/unpack/test/samples/webpack-commonjs.js.snap +++ /dev/null @@ -1,31 +0,0 @@ -WebpackBundle { - "chunks": [], - "entryId": "", - "modules": Map { - "1" => WebpackModule { - "ast": const add = require( /*! ./math */"./2.js").add; -exports.increment = function (val) { - return add(val, 1); -};, - "id": "1", - "isEntry": false, - "path": "./1.js", - }, - "2" => WebpackModule { - "ast": exports.add = function () { - var sum = 0, - i = 0, - args = arguments, - l = args.length; - while (i < l) { - sum += args[i++]; - } - return sum; -};, - "id": "2", - "isEntry": false, - "path": "./2.js", - }, - }, - "type": "webpack", -} diff --git a/packages/webcrack/src/unpack/test/samples/webpack-esm.js b/packages/webcrack/src/unpack/test/samples/webpack-esm.js deleted file mode 100644 index 09736e2d..00000000 --- a/packages/webcrack/src/unpack/test/samples/webpack-esm.js +++ /dev/null @@ -1,97 +0,0 @@ -!(function (e) { - var a = {}; - function n(t) { - if (a[t]) return a[t].exports; - var r = (a[t] = { i: t, l: !1, exports: {} }); - return e[t].call(r.exports, r, r.exports, n), (r.l = !0), 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.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; - var i = Object.create(null); - if ( - (n.r(i), - Object.defineProperty(i, 'default', { enumerable: !0, value: e }), - 2 & t && 'string' != typeof e) - ) - for (var a in e) - n.d( - i, - a, - function (t) { - return e[t]; - }.bind(null, a) - ); - return i; - }), - (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) { - return Object.prototype.hasOwnProperty.call(e, t); - }), - (n.p = ''), - n((n.s = 0)); -})([ - function (e, t, i) { - const a = i(1); - const b = i(2); - const c = i(3); - const bDefault = i.n(b); - const cDefault = i.n(c); - const dDefault = i.n(b).a; - const eDefault = i.n(c)(); - console.log(a.counter); - console.log(bDefault().VERSION); - console.log(cDefault.a.VERSION); - }, - function (e, t, i) { - e = require.hmd(e); - i.r(t); - i.d(t, 'counter', function () { - return f; - }); - class f {} - let counter = 1; - f = 2; - }, - function(e, t, i) { - const x = {}; - i.r(x); - i.d(x, 'default', function () { - return y; - }); - let y = 1; - }, - function(e, t, i) { - e.exports = { - VERSION: 1 - } - }, - function (e, t, i) { - i.r(t); - i.d(t, 'default', function () { - return f; - }); - class f {} - }, -]); diff --git a/packages/webcrack/src/unpack/test/samples/webpack-esm.js.snap b/packages/webcrack/src/unpack/test/samples/webpack-esm.js.snap deleted file mode 100644 index cd0fd694..00000000 --- a/packages/webcrack/src/unpack/test/samples/webpack-esm.js.snap +++ /dev/null @@ -1,51 +0,0 @@ -WebpackBundle { - "chunks": [], - "entryId": "0", - "modules": Map { - "0" => WebpackModule { - "ast": const a = require("./1.js"); -const b = require("./2.js"); -const c = require("./3.js"); -const bDefault = b.default; -const cDefault = c; -const dDefault = b.default; -const eDefault = c; -console.log(a.counter); -console.log(bDefault.VERSION); -console.log(cDefault.VERSION);, - "id": "0", - "isEntry": true, - "path": "./index.js", - }, - "1" => WebpackModule { - "ast": export class counter {} -let _counter = 1; -counter = 2;, - "id": "1", - "isEntry": false, - "path": "./1.js", - }, - "2" => WebpackModule { - "ast": const x = {}; -export default 1;, - "id": "2", - "isEntry": false, - "path": "./2.js", - }, - "3" => WebpackModule { - "ast": module.exports = { - VERSION: 1 -};, - "id": "3", - "isEntry": false, - "path": "./3.js", - }, - "4" => WebpackModule { - "ast": export default class f {}, - "id": "4", - "isEntry": false, - "path": "./4.js", - }, - }, - "type": "webpack", -} diff --git a/packages/webcrack/src/unpack/test/samples/webpack-object.js b/packages/webcrack/src/unpack/test/samples/webpack-object.js deleted file mode 100644 index eec54146..00000000 --- a/packages/webcrack/src/unpack/test/samples/webpack-object.js +++ /dev/null @@ -1,89 +0,0 @@ -(function (e) { - var n = {}; - function o(r) { - if (n[r]) { - return n[r].exports; - } - var a = (n[r] = { - i: r, - l: false, - exports: {}, - }); - e[r].call(a.exports, a, a.exports, o); - a.l = true; - return a.exports; - } - o.m = e; - o.c = n; - o.d = function (e, t, n) { - if (!o.o(e, t)) { - Object.defineProperty(e, t, { - enumerable: true, - get: n, - }); - } - }; - o.r = function (e) { - if ('undefined' != typeof Symbol && Symbol.toStringTag) { - Object.defineProperty(e, Symbol.toStringTag, { - value: 'Module', - }); - } - Object.defineProperty(e, '__esModule', { - value: true, - }); - }; - o.t = function (e, t) { - if (1 & t) { - e = o(e); - } - if (8 & t) { - return e; - } - if (4 & t && typeof e === 'object' && e && e.__esModule) { - return e; - } - var r = Object.create(null); - o.r(r); - Object.defineProperty(r, 'default', { - enumerable: true, - value: e, - }); - if (2 & t && typeof e != 'string') { - for (var i in e) { - o.d( - r, - i, - function (t) { - return e[t]; - }.bind(null, i) - ); - } - } - return r; - }; - o.n = function (e) { - var n = - e && e.__esModule - ? function () { - return e['default']; - } - : function () { - return e; - }; - o.d(n, 'a', n); - return n; - }; - o.o = function (e, t) { - return Object.prototype.hasOwnProperty.call(e, t); - }; - o.p = ''; - o((o.s = 386)); -})({ - 386: function (e, t, n) { - const r = n(387)['default']; - }, - 387: function (e, t, n) { - e.exports = 'test'; - }, -}); diff --git a/packages/webcrack/src/unpack/test/samples/webpack-object.js.snap b/packages/webcrack/src/unpack/test/samples/webpack-object.js.snap deleted file mode 100644 index 3c18bf17..00000000 --- a/packages/webcrack/src/unpack/test/samples/webpack-object.js.snap +++ /dev/null @@ -1,19 +0,0 @@ -WebpackBundle { - "chunks": [], - "entryId": "386", - "modules": Map { - "386" => WebpackModule { - "ast": const r = require("./387.js")['default'];, - "id": "386", - "isEntry": true, - "path": "./index.js", - }, - "387" => WebpackModule { - "ast": module.exports = 'test';, - "id": "387", - "isEntry": false, - "path": "./387.js", - }, - }, - "type": "webpack", -} diff --git a/packages/webcrack/src/unpack/test/samples/webpack5-object.js b/packages/webcrack/src/unpack/test/samples/webpack5-object.js deleted file mode 100644 index f0ae5353..00000000 --- a/packages/webcrack/src/unpack/test/samples/webpack5-object.js +++ /dev/null @@ -1,64 +0,0 @@ -(() => { - var modules = { - 2: (module, exports, require) => { - "use strict"; - const lib = require(3); - console.log(lib); - const _0x8da276 = require(require.ab + "build/Release/spdlog.node"); - }, - 3: module => { - "use strict"; - - module.exports = 'foo' - } - }; - var installedModules = {}; - function __webpack_require__(moduleId) { - var _0x1d30a7 = installedModules[moduleId]; - if (_0x1d30a7 !== undefined) { - return _0x1d30a7.exports; - } - var module = installedModules[moduleId] = { - id: moduleId, - loaded: false, - exports: {} - }; - modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); - module.loaded = true; - return module.exports; - } - __webpack_require__.c = installedModules; - __webpack_require__.n = module => { - var getter = module && module.__esModule ? () => module.default : () => module; - __webpack_require__.d(getter, { - a: getter - }); - return getter; - }; - __webpack_require__.d = (exports, definition) => { - for (var _0x43c485 in definition) { - if (__webpack_require__.o(definition, _0x43c485) && !__webpack_require__.o(exports, _0x43c485)) { - Object.defineProperty(exports, _0x43c485, { - enumerable: true, - get: definition[_0x43c485] - }); - } - } - }; - __webpack_require__.o = (object, property) => Object.prototype.hasOwnProperty.call(object, property); - __webpack_require__.r = exports => { - if (typeof Symbol != "undefined" && Symbol.toStringTag) { - Object.defineProperty(exports, Symbol.toStringTag, { - value: "Module" - }); - } - Object.defineProperty(exports, "__esModule", { - value: true - }); - }; - if (__webpack_require__ !== undefined) { - __webpack_require__.ab = __dirname + "/native_modules/"; - } - var entryModule = __webpack_require__(__webpack_require__.s = 2); - module.exports = entryModule; -})(); \ No newline at end of file diff --git a/packages/webcrack/src/unpack/test/samples/webpack5-object.js.snap b/packages/webcrack/src/unpack/test/samples/webpack5-object.js.snap deleted file mode 100644 index cd55226d..00000000 --- a/packages/webcrack/src/unpack/test/samples/webpack5-object.js.snap +++ /dev/null @@ -1,21 +0,0 @@ -WebpackBundle { - "chunks": [], - "entryId": "2", - "modules": Map { - "2" => WebpackModule { - "ast": const lib = require("./3.js"); -console.log(lib); -const _0x8da276 = __webpack_require__(__webpack_require__.ab + "build/Release/spdlog.node");, - "id": "2", - "isEntry": true, - "path": "./index.js", - }, - "3" => WebpackModule { - "ast": module.exports = 'foo';, - "id": "3", - "isEntry": false, - "path": "./3.js", - }, - }, - "type": "webpack", -} diff --git a/packages/webcrack/src/unpack/test/unpack.test.ts b/packages/webcrack/src/unpack/test/unpack.test.ts index 92f8b98c..68da6c59 100644 --- a/packages/webcrack/src/unpack/test/unpack.test.ts +++ b/packages/webcrack/src/unpack/test/unpack.test.ts @@ -3,27 +3,28 @@ import { readFile } from 'fs/promises'; import { tmpdir } from 'os'; import { join } from 'path'; import { expect, test } from 'vitest'; -import { unpack } from '../index'; +import { webcrack } from '../..'; const SAMPLES_DIR = join(__dirname, 'samples'); test('detect top-level bundle first', async () => { - const bundle = unpack( + const { bundle } = await webcrack( await readFile(join(SAMPLES_DIR, 'browserify-webpack-nested.js'), 'utf8'), ); - expect(bundle!.type).toBe('browserify'); + expect(bundle?.type).toBe('browserify'); }); test('path mapping', async () => { - const bundle = unpack( - await readFile(join(SAMPLES_DIR, 'webpack.js'), 'utf8'), + const { bundle } = await webcrack( + await readFile(join(SAMPLES_DIR, 'webpack-4.js'), 'utf8'), { - './utils/color.js': m.stringLiteral('#FBC02D'), - package: m.numericLiteral(4), + mappings: () => ({ + './utils/color.js': m.stringLiteral('#FBC02D'), + package: m.numericLiteral(4), + }), }, ); - expect(bundle).toBeDefined(); - expect(bundle!).toMatchSnapshot(); + expect(bundle).toMatchSnapshot(); }); test('prevent path traversal', async () => { @@ -31,7 +32,7 @@ test('prevent path traversal', async () => { join(SAMPLES_DIR, 'webpack-path-traversal.js'), 'utf8', ); - const bundle = unpack(code); + const { bundle } = await webcrack(code); expect(bundle).toBeDefined(); const dir = join(tmpdir(), 'path-traversal-test'); diff --git a/packages/webcrack/test/api.test.ts b/packages/webcrack/test/api.test.ts index 199ea5d2..e80e2b4a 100644 --- a/packages/webcrack/test/api.test.ts +++ b/packages/webcrack/test/api.test.ts @@ -8,7 +8,7 @@ const obfuscatedSrc = await readFile( 'utf8', ); const webpackSrc = await readFile( - join(__dirname, '../src/unpack/test/samples/webpack.js'), + join(__dirname, '../src/unpack/test/samples/webpack-4.js'), 'utf8', ); From 780b891aaf93c5485cc2481a312d28129a9bef28 Mon Sep 17 00:00:00 2001 From: j4k0xb <55899582+j4k0xb@users.noreply.github.com> Date: Sat, 13 Jan 2024 03:20:27 +0100 Subject: [PATCH 61/81] feat: rename module and exports in commonjs --- .../webcrack/src/unpack/test/commonjs.test.ts | 13 +++++++++++++ packages/webcrack/src/unpack/webpack/module.ts | 17 ++++++++++++++--- 2 files changed, 27 insertions(+), 3 deletions(-) create mode 100644 packages/webcrack/src/unpack/test/commonjs.test.ts 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/webpack/module.ts b/packages/webcrack/src/unpack/webpack/module.ts index a0967e3d..9a51fdda 100644 --- a/packages/webcrack/src/unpack/webpack/module.ts +++ b/packages/webcrack/src/unpack/webpack/module.ts @@ -4,6 +4,7 @@ import * as m from '@codemod/matchers'; import { applyTransform, constMemberExpression, + renameFast, renameParameters, } from '../../ast-utils'; import { Module } from '../module'; @@ -21,7 +22,9 @@ import namespaceObject from './runtime/namespace-object'; import varInjections from './var-injections'; export class WebpackModule extends Module { + #moduleBinding: Binding | undefined; #webpackRequireBinding: Binding | undefined; + #exportsBinding: Binding | undefined; #importExportManager: ImportExportManager; // TODO: expose to public API #sourceType: 'commonjs' | 'esm' = 'commonjs'; @@ -31,20 +34,24 @@ export class WebpackModule extends Module { const file = t.file(t.program(ast.node.body.body)); super(id, file, isEntry); + this.removeTrailingComments(); + // The params are temporarily renamed to these special names to avoid + // mixing them up with the global module/exports/require from Node.js renameParameters(ast, [ '__webpack_module__', '__webpack_exports__', '__webpack_require__', ]); + this.#moduleBinding = ast.scope.getBinding('__webpack_module__'); + this.#webpackRequireBinding = ast.scope.getBinding('__webpack_require__'); + this.#exportsBinding = ast.scope.getBinding('__webpack_exports__'); + applyTransform(file, varInjections); - this.removeTrailingComments(); - this.#webpackRequireBinding = ast.scope.getBinding('__webpack_require__'); this.#importExportManager = new ImportExportManager( file, this.#webpackRequireBinding, ); - applyTransform(file, global, this.#webpackRequireBinding); applyTransform(file, hasOwnProperty, this.#webpackRequireBinding); applyTransform(file, moduleDecorator, this.#webpackRequireBinding); @@ -53,6 +60,10 @@ export class WebpackModule extends Module { applyTransform(file, definePropertyGetters, this.#importExportManager); this.#importExportManager.insertImportsAndExports(); + // For CommonJS + if (this.#moduleBinding) renameFast(this.#moduleBinding, 'module'); + if (this.#exportsBinding) renameFast(this.#exportsBinding, 'exports'); + // this.removeDefineESM(); // // FIXME: some bundles don't define __esModule but still declare esm exports // // https://github.com/0xdevalias/chatgpt-source-watch/blob/main/orig/_next/static/chunks/167-121de668c4456907.js From 34c593881b3b39765e8b230c45d5fe198cb0bd9a Mon Sep 17 00:00:00 2001 From: j4k0xb <55899582+j4k0xb@users.noreply.github.com> Date: Sat, 13 Jan 2024 03:20:27 +0100 Subject: [PATCH 62/81] refactor: remove unused code --- .../webcrack/src/unpack/webpack/module.ts | 104 ++---------------- 1 file changed, 11 insertions(+), 93 deletions(-) diff --git a/packages/webcrack/src/unpack/webpack/module.ts b/packages/webcrack/src/unpack/webpack/module.ts index 9a51fdda..4958ad73 100644 --- a/packages/webcrack/src/unpack/webpack/module.ts +++ b/packages/webcrack/src/unpack/webpack/module.ts @@ -1,19 +1,9 @@ -import type { Binding } from '@babel/traverse'; import * as t from '@babel/types'; -import * as m from '@codemod/matchers'; -import { - applyTransform, - constMemberExpression, - renameFast, - renameParameters, -} from '../../ast-utils'; +import { applyTransform, renameFast, renameParameters } from '../../ast-utils'; import { Module } from '../module'; import type { FunctionPath } from './common-matchers'; import { ImportExportManager } from './import-export-manager'; -import { - default as defineExport, - default as definePropertyGetters, -} from './runtime/define-property-getters'; +import { default as definePropertyGetters } from './runtime/define-property-getters'; import getDefaultExport from './runtime/get-default-export'; import global from './runtime/global'; import hasOwnProperty from './runtime/has-own-property'; @@ -22,9 +12,6 @@ import namespaceObject from './runtime/namespace-object'; import varInjections from './var-injections'; export class WebpackModule extends Module { - #moduleBinding: Binding | undefined; - #webpackRequireBinding: Binding | undefined; - #exportsBinding: Binding | undefined; #importExportManager: ImportExportManager; // TODO: expose to public API #sourceType: 'commonjs' | 'esm' = 'commonjs'; @@ -42,27 +29,27 @@ export class WebpackModule extends Module { '__webpack_exports__', '__webpack_require__', ]); - this.#moduleBinding = ast.scope.getBinding('__webpack_module__'); - this.#webpackRequireBinding = ast.scope.getBinding('__webpack_require__'); - this.#exportsBinding = ast.scope.getBinding('__webpack_exports__'); + const moduleBinding = ast.scope.getBinding('__webpack_module__'); + const webpackRequireBinding = ast.scope.getBinding('__webpack_require__'); + const exportsBinding = ast.scope.getBinding('__webpack_exports__'); applyTransform(file, varInjections); this.#importExportManager = new ImportExportManager( file, - this.#webpackRequireBinding, + webpackRequireBinding, ); - applyTransform(file, global, this.#webpackRequireBinding); - applyTransform(file, hasOwnProperty, this.#webpackRequireBinding); - applyTransform(file, moduleDecorator, this.#webpackRequireBinding); + applyTransform(file, global, webpackRequireBinding); + applyTransform(file, hasOwnProperty, webpackRequireBinding); + applyTransform(file, moduleDecorator, webpackRequireBinding); applyTransform(file, namespaceObject); applyTransform(file, getDefaultExport, this.#importExportManager); applyTransform(file, definePropertyGetters, this.#importExportManager); this.#importExportManager.insertImportsAndExports(); // For CommonJS - if (this.#moduleBinding) renameFast(this.#moduleBinding, 'module'); - if (this.#exportsBinding) renameFast(this.#exportsBinding, 'exports'); + if (moduleBinding) renameFast(moduleBinding, 'module'); + if (exportsBinding) renameFast(exportsBinding, 'exports'); // this.removeDefineESM(); // // FIXME: some bundles don't define __esModule but still declare esm exports @@ -85,73 +72,4 @@ export class WebpackModule extends Module { lastNode.trailingComments.pop(); } } - - /** - * Removes - * - `__webpack_require__.r(exports);` (webpack/runtime/make namespace object) - * - `Object.defineProperty(exports, "__esModule", { value: true });` - */ - private removeDefineESM(): void { - const matcher = m.expressionStatement( - m.or( - m.callExpression(constMemberExpression('__webpack_require__', 'r'), [ - m.identifier(), - ]), - m.callExpression(constMemberExpression('Object', 'defineProperty'), [ - m.identifier(), - m.stringLiteral('__esModule'), - m.objectExpression([ - m.objectProperty(m.identifier('value'), m.booleanLiteral(true)), - ]), - ]), - ), - ); - - for (let i = 0; i < this.ast.program.body.length; i++) { - const node = this.ast.program.body[i]; - if (matcher.match(node)) { - this.#sourceType = 'esm'; - this.ast.program.body.splice(i, 1); - i--; - } - } - } - - private convertExportsToESM(): void { - applyTransform(this.ast, defineExport); - } - - /** - * // TODO: refactor to use ImportExportManager - * ```diff - * - __webpack_require__(id) - * + require("./relative/path.js") - * ``` - * @internal - */ - replaceRequireCalls() // onResolve: (id: string) => { path: string; external?: boolean }, - : void { - // if (!this.#webpackRequireBinding) return; - // return; - // const idArg = m.capture(m.or(m.numericLiteral(), m.stringLiteral())); - // const requireCall = m.callExpression(m.identifier('__webpack_require__'), [ - // idArg, - // ]); - // this.#webpackRequireBinding.referencePaths.forEach((path) => { - // m.matchPath(requireCall, { idArg }, path.parentPath!, ({ idArg }) => { - // const id = idArg.node.value.toString(); - // const result = onResolve(id); - // (path.node as t.Identifier).name = 'require'; - // idArg.replaceWith(t.stringLiteral(result.path)); - // if (result.external) { - // idArg.addComment('leading', 'webcrack:missing'); - // } - // // this.#imports.push({ - // // id, - // // path: result.path, - // // nodePath: path.parentPath as NodePath, - // // }); - // }); - // }); - } } From 1f137d6a308ef1fa88c1f20ea587ad9500c88df1 Mon Sep 17 00:00:00 2001 From: j4k0xb <55899582+j4k0xb@users.noreply.github.com> Date: Sat, 13 Jan 2024 03:38:30 +0100 Subject: [PATCH 63/81] fix: support multiple namespace imports --- .../webcrack/src/unpack/test/exports.test.ts | 10 +++++++ .../unpack/webpack/import-export-manager.ts | 27 ++++++++++--------- 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/packages/webcrack/src/unpack/test/exports.test.ts b/packages/webcrack/src/unpack/test/exports.test.ts index ca40c96f..6cec855c 100644 --- a/packages/webcrack/src/unpack/test/exports.test.ts +++ b/packages/webcrack/src/unpack/test/exports.test.ts @@ -114,6 +114,16 @@ describe('webpack 4', () => { 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; }); diff --git a/packages/webcrack/src/unpack/webpack/import-export-manager.ts b/packages/webcrack/src/unpack/webpack/import-export-manager.ts index f4c25bcc..8e8de41a 100644 --- a/packages/webcrack/src/unpack/webpack/import-export-manager.ts +++ b/packages/webcrack/src/unpack/webpack/import-export-manager.ts @@ -25,7 +25,7 @@ interface RequireVar { defaultImport?: t.ImportDefaultSpecifier; namespaceImport?: t.ImportNamespaceSpecifier; namedImports: t.ImportSpecifier[]; - namespaceExport?: t.ExportNamespaceSpecifier; + namespaceExports: t.ExportNamespaceSpecifier[]; namedExports: t.ExportSpecifier[]; } @@ -66,16 +66,18 @@ export class ImportExportManager { requireVar.namedExports, t.stringLiteral(requireVar.moduleId), ); + // TODO: resolve module id to path + const namespaceExports = requireVar.namespaceExports.map((specifier) => + t.exportNamedDeclaration( + undefined, + [specifier], + t.stringLiteral(requireVar.moduleId), + ), + ); if (namedExports.specifiers.length > 0) { requireVar.binding.path.parentPath!.insertAfter(namedExports); } - if (requireVar.namespaceExport) { - // TODO: resolve module id to path - const namespaceExports = t.exportNamedDeclaration( - undefined, - [requireVar.namespaceExport], - t.stringLiteral(requireVar.moduleId), - ); + if (namespaceExports.length > 0) { requireVar.binding.path.parentPath!.insertAfter(namespaceExports); } }); @@ -108,7 +110,8 @@ export class ImportExportManager { !!requireVar.namespaceImport || requireVar.namedImports.length > 0; const hasExports = - !!requireVar.namespaceExport || requireVar.namedExports.length > 0; + requireVar.namespaceExports.length > 0 || + requireVar.namedExports.length > 0; // side-effect import if (!requireVar.binding.referenced && !hasImports && !hasExports) { @@ -369,8 +372,8 @@ export class ImportExportManager { * ``` */ private addExportNamespace(requireVar: RequireVar, exportName: string) { - requireVar.namespaceExport ??= t.exportNamespaceSpecifier( - t.identifier(exportName), + requireVar.namespaceExports.push( + t.exportNamespaceSpecifier(t.identifier(exportName)), ); } @@ -405,7 +408,7 @@ export class ImportExportManager { defaultImport: undefined, namespaceImport: undefined, namedImports: [], - namespaceExport: undefined, + namespaceExports: [], namedExports: [], }); } From d8a23813676c2c02534670dc20377b8a52aa54fd Mon Sep 17 00:00:00 2001 From: j4k0xb <55899582+j4k0xb@users.noreply.github.com> Date: Sat, 13 Jan 2024 03:42:34 +0100 Subject: [PATCH 64/81] Revert "refactor: separate webpack 4 and 5 matching" This reverts commit 1d140331c618c73afd8909552f1b52e4e1dfb897. --- .../unpack/test/samples/webpack-5-no-entry.js | 2 +- .../src/unpack/webpack/common-matchers.ts | 50 ++++++++++++++++++- .../src/unpack/webpack/unpack-webpack-4.ts | 32 ++---------- .../src/unpack/webpack/unpack-webpack-5.ts | 28 ++--------- 4 files changed, 55 insertions(+), 57 deletions(-) diff --git a/packages/webcrack/src/unpack/test/samples/webpack-5-no-entry.js b/packages/webcrack/src/unpack/test/samples/webpack-5-no-entry.js index a1b00f2b..c9295fad 100644 --- a/packages/webcrack/src/unpack/test/samples/webpack-5-no-entry.js +++ b/packages/webcrack/src/unpack/test/samples/webpack-5-no-entry.js @@ -17,7 +17,7 @@ loaded: false, exports: {} }; - __webpack_modules__[moduleId](module, module.exports, __webpack_require__); + __webpack_modules__[moduleId].call(module.exports, module, module.exports, __webpack_require__); module.loaded = true; return module.exports; } diff --git a/packages/webcrack/src/unpack/webpack/common-matchers.ts b/packages/webcrack/src/unpack/webpack/common-matchers.ts index 9ce78e90..193be11b 100644 --- a/packages/webcrack/src/unpack/webpack/common-matchers.ts +++ b/packages/webcrack/src/unpack/webpack/common-matchers.ts @@ -12,10 +12,56 @@ export type FunctionPath = NodePath< | (t.ArrowFunctionExpression & { body: t.BlockStatement }) >; +/** + * @returns + * - `webpackRequire`: A Matcher for `function __webpack_require__(moduleId) { ... }` + * - `containerId`: A matcher for e.g. `__webpack_modules__` that has to be captured before `webpackRequire` is matched + */ +export function webpackRequireFunctionMatcher() { + // Example: __webpack_modules__ + const containerId = m.capture(m.identifier()); + const webpackRequire = m.capture( + m.functionDeclaration( + m.identifier(), // __webpack_require__ + [m.identifier()], // moduleId + m.blockStatement( + m.anyList( + m.zeroOrMore(), + m.expressionStatement( + m.callExpression( + m.or( + // Example (webpack 0.11.x): __webpack_modules__[moduleId].call(null, module, module.exports, __webpack_require__); + // Example (webpack 4): __webpack_modules__[moduleId].call(module.exports, module, module.exports, __webpack_require__); + constMemberExpression( + m.memberExpression( + m.fromCapture(containerId), + m.identifier(), + true, + ), + 'call', + ), + // Example (webpack 5): __webpack_modules__[moduleId](module, module.exports, __webpack_require__); + m.memberExpression( + m.fromCapture(containerId), + m.identifier(), + true, + ), + ), + ), + ), + m.zeroOrMore(), + ), + ), + ), + ); + + return { webpackRequire, containerId }; +} + /** * Matches - * - `[,,function (module, exports, require) {...}, ...]` where the indexes are the module ids - * - or `{0: function (module, exports, require) {...}, ...}` where the keys are the module ids + * - `[,,function (module, exports, require) {...}, ...]` where the index is the module ID + * - or `{0: function (module, exports, require) {...}, ...}` where the key is the module ID */ export function modulesContainerMatcher(): m.CapturedMatcher< t.ArrayExpression | t.ObjectExpression diff --git a/packages/webcrack/src/unpack/webpack/unpack-webpack-4.ts b/packages/webcrack/src/unpack/webpack/unpack-webpack-4.ts index f2aa829b..a44f16e1 100644 --- a/packages/webcrack/src/unpack/webpack/unpack-webpack-4.ts +++ b/packages/webcrack/src/unpack/webpack/unpack-webpack-4.ts @@ -3,13 +3,14 @@ import type * as t from '@babel/types'; import * as m from '@codemod/matchers'; import type { Bundle } from '..'; import type { Transform } from '../../ast-utils'; -import { constMemberExpression, renameFast } from '../../ast-utils'; +import { renameFast } from '../../ast-utils'; import { WebpackBundle } from './bundle'; import { findAssignedEntryId, findRequiredEntryId, getModuleFunctions, modulesContainerMatcher, + webpackRequireFunctionMatcher, } from './common-matchers'; import { WebpackModule } from './module'; @@ -25,34 +26,7 @@ export default { name: 'unpack-webpack-4', tags: ['unsafe'], visitor(options = { bundle: undefined }) { - // Example: __webpack_modules__ - const containerId = m.capture(m.identifier()); - const webpackRequire = m.capture( - m.functionDeclaration( - m.identifier(), // __webpack_require__ - [m.identifier()], // moduleId - m.blockStatement( - m.anyList( - m.zeroOrMore(), - // Example (webpack 0.11.x): __webpack_modules__[moduleId].call(null, module, module.exports, __webpack_require__); - // Example (webpack 4): __webpack_modules__[moduleId].call(module.exports, module, module.exports, __webpack_require__); - m.expressionStatement( - m.callExpression( - constMemberExpression( - m.memberExpression( - m.fromCapture(containerId), - m.identifier(), - true, - ), - 'call', - ), - ), - ), - m.zeroOrMore(), - ), - ), - ), - ); + const { webpackRequire, containerId } = webpackRequireFunctionMatcher(); const container = modulesContainerMatcher(); const matcher = m.callExpression( diff --git a/packages/webcrack/src/unpack/webpack/unpack-webpack-5.ts b/packages/webcrack/src/unpack/webpack/unpack-webpack-5.ts index c51c0e73..ef71b8ea 100644 --- a/packages/webcrack/src/unpack/webpack/unpack-webpack-5.ts +++ b/packages/webcrack/src/unpack/webpack/unpack-webpack-5.ts @@ -9,6 +9,7 @@ import { findAssignedEntryId, getModuleFunctions, modulesContainerMatcher, + webpackRequireFunctionMatcher, } from './common-matchers'; import { WebpackModule } from './module'; @@ -28,30 +29,7 @@ export default { tags: ['unsafe'], scope: true, visitor(options = { bundle: undefined }) { - // Example: __webpack_modules__ - const modulesId = m.capture(m.identifier()); - const webpackRequire = m.capture( - m.functionDeclaration( - m.identifier(), // __webpack_require__ - [m.identifier()], // moduleId - m.blockStatement( - m.anyList( - m.zeroOrMore(), - // Example: __webpack_modules__[moduleId](module, module.exports, __webpack_require__); - m.expressionStatement( - m.callExpression( - m.memberExpression( - m.fromCapture(modulesId), - m.identifier(), - true, - ), - ), - ), - m.zeroOrMore(), - ), - ), - ), - ); + const { webpackRequire, containerId } = webpackRequireFunctionMatcher(); const container = modulesContainerMatcher(); const matcher = m.blockStatement( @@ -59,7 +37,7 @@ export default { m.zeroOrMore(), // Example: var __webpack_modules__ = { ... }; m.variableDeclaration(undefined, [ - m.variableDeclarator(modulesId, container), + m.variableDeclarator(containerId, container), ]), m.zeroOrMore(), webpackRequire, From 39f9aafb8d6e740ee97d6127a92e19251607da78 Mon Sep 17 00:00:00 2001 From: j4k0xb <55899582+j4k0xb@users.noreply.github.com> Date: Sat, 13 Jan 2024 04:25:01 +0100 Subject: [PATCH 65/81] fix: multiple named imports it didn't replace the second `lib.foo` before --- .../webcrack/src/unpack/test/imports.test.ts | 10 +++++++ .../unpack/webpack/import-export-manager.ts | 27 ++++++++++--------- .../src/unpack/webpack/unpack-webpack-4.ts | 1 + .../unpack/webpack/unpack-webpack-chunk.ts | 1 + 4 files changed, 27 insertions(+), 12 deletions(-) diff --git a/packages/webcrack/src/unpack/test/imports.test.ts b/packages/webcrack/src/unpack/test/imports.test.ts index 0ba5f6f5..05a87fac 100644 --- a/packages/webcrack/src/unpack/test/imports.test.ts +++ b/packages/webcrack/src/unpack/test/imports.test.ts @@ -45,6 +45,16 @@ describe('webpack 4', () => { console.log(foo); `)); + test('multiple named imports', () => + expectJS(` + __webpack_require__.r(__webpack_exports__); + 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(` __webpack_require__.r(__webpack_exports__); diff --git a/packages/webcrack/src/unpack/webpack/import-export-manager.ts b/packages/webcrack/src/unpack/webpack/import-export-manager.ts index 8e8de41a..d7eaa913 100644 --- a/packages/webcrack/src/unpack/webpack/import-export-manager.ts +++ b/packages/webcrack/src/unpack/webpack/import-export-manager.ts @@ -167,21 +167,24 @@ export class ImportExportManager { const localName = this.addDefaultImport(requireVar); reference.parentPath!.replaceWith(t.identifier(localName)); return; - } else if (importedLocalNames.has(importedName)) { - return; } - const hasNameConflict = binding.referencePaths.some((ref) => - ref.scope.hasBinding(importedName), - ); - const localName = hasNameConflict - ? binding.path.scope.generateUid(importedName) - : importedName; - - importedLocalNames.add(localName); - this.addNamedImport(requireVar, localName, importedName); + let localName = importedName; + if (!importedLocalNames.has(importedName)) { + const hasNameConflict = binding.referencePaths.some((ref) => + ref.scope.hasBinding(importedName), + ); + localName = hasNameConflict + ? binding.path.scope.generateUid(importedName) + : importedName; + importedLocalNames.add(localName); + this.addNamedImport(requireVar, localName, importedName); + } - if (indirectCall.match(reference.parentPath?.parentPath?.parent)) { + if ( + indirectCall.match(reference.parentPath?.parentPath?.parent) && + reference.parentPath.parentPath?.key === 'callee' + ) { reference.parentPath.parentPath.replaceWith( t.identifier(localName), ); diff --git a/packages/webcrack/src/unpack/webpack/unpack-webpack-4.ts b/packages/webcrack/src/unpack/webpack/unpack-webpack-4.ts index a44f16e1..4162b0d1 100644 --- a/packages/webcrack/src/unpack/webpack/unpack-webpack-4.ts +++ b/packages/webcrack/src/unpack/webpack/unpack-webpack-4.ts @@ -25,6 +25,7 @@ import { WebpackModule } from './module'; export default { name: 'unpack-webpack-4', tags: ['unsafe'], + scope: true, visitor(options = { bundle: undefined }) { const { webpackRequire, containerId } = webpackRequireFunctionMatcher(); const container = modulesContainerMatcher(); diff --git a/packages/webcrack/src/unpack/webpack/unpack-webpack-chunk.ts b/packages/webcrack/src/unpack/webpack/unpack-webpack-chunk.ts index d00b6e9a..84466028 100644 --- a/packages/webcrack/src/unpack/webpack/unpack-webpack-chunk.ts +++ b/packages/webcrack/src/unpack/webpack/unpack-webpack-chunk.ts @@ -18,6 +18,7 @@ import { WebpackModule } from './module'; export default { name: 'unpack-webpack-chunk', tags: ['unsafe'], + scope: true, visitor(options = { bundle: undefined }) { const container = modulesContainerMatcher(); From de98b60f2a7266bd6d955857dda107b450494d0d Mon Sep 17 00:00:00 2001 From: j4k0xb <55899582+j4k0xb@users.noreply.github.com> Date: Sat, 13 Jan 2024 05:24:47 +0100 Subject: [PATCH 66/81] fix: indirect calls for default import --- .../webcrack/src/unpack/test/imports.test.ts | 12 ++++++++++ .../unpack/webpack/import-export-manager.ts | 24 ++++++++++--------- 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/packages/webcrack/src/unpack/test/imports.test.ts b/packages/webcrack/src/unpack/test/imports.test.ts index 05a87fac..42ca629a 100644 --- a/packages/webcrack/src/unpack/test/imports.test.ts +++ b/packages/webcrack/src/unpack/test/imports.test.ts @@ -107,6 +107,18 @@ describe('webpack 4', () => { `), ); + test('indirect calls', () => + expectJS(` + __webpack_require__.r(__webpack_exports__); + 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(` __webpack_require__.r(__webpack_exports__); diff --git a/packages/webcrack/src/unpack/webpack/import-export-manager.ts b/packages/webcrack/src/unpack/webpack/import-export-manager.ts index d7eaa913..d0ed7e6d 100644 --- a/packages/webcrack/src/unpack/webpack/import-export-manager.ts +++ b/packages/webcrack/src/unpack/webpack/import-export-manager.ts @@ -165,7 +165,7 @@ export class ImportExportManager { // lib.default -> _lib_default if (importedName === 'default') { const localName = this.addDefaultImport(requireVar); - reference.parentPath!.replaceWith(t.identifier(localName)); + replaceImportReference(reference, localName); return; } @@ -181,21 +181,23 @@ export class ImportExportManager { this.addNamedImport(requireVar, localName, importedName); } - if ( - indirectCall.match(reference.parentPath?.parentPath?.parent) && - reference.parentPath.parentPath?.key === 'callee' - ) { - reference.parentPath.parentPath.replaceWith( - t.identifier(localName), - ); - } else { - reference.parentPath!.replaceWith(t.identifier(localName)); - } + replaceImportReference(reference, localName); } else { this.addNamespaceImport(requireVar); } }); }); + + function replaceImportReference(path: NodePath, localName: string) { + if ( + indirectCall.match(path.parentPath?.parentPath?.parent) && + path.parentPath.parentPath?.key === 'callee' + ) { + path.parentPath.parentPath.replaceWith(t.identifier(localName)); + } else { + path.parentPath!.replaceWith(t.identifier(localName)); + } + } } addExport(scope: Scope, exportName: string, value: t.Expression) { From cd26c952db4d788feef10072482b08bfc7e51594 Mon Sep 17 00:00:00 2001 From: j4k0xb <55899582+j4k0xb@users.noreply.github.com> Date: Sat, 13 Jan 2024 06:08:07 +0100 Subject: [PATCH 67/81] fix: decompile jsx after unpacking the bundle --- packages/webcrack/src/index.ts | 22 ++++-------- .../src/unpack/test/samples/webpack-jsx.js | 10 ++++++ .../unpack/test/samples/webpack-jsx.js.snap | 34 +++++++++++++++++++ 3 files changed, 51 insertions(+), 15 deletions(-) create mode 100644 packages/webcrack/src/unpack/test/samples/webpack-jsx.js create mode 100644 packages/webcrack/src/unpack/test/samples/webpack-jsx.js.snap 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/test/samples/webpack-jsx.js b/packages/webcrack/src/unpack/test/samples/webpack-jsx.js new file mode 100644 index 00000000..4a9883d4 --- /dev/null +++ b/packages/webcrack/src/unpack/test/samples/webpack-jsx.js @@ -0,0 +1,10 @@ +(window['webpackJsonp'] = window['webpackJsonp'] || []).push([ + [1], + [function (module, exports, __webpack_require__) { + __webpack_require__.r(exports); + var r = __webpack_require__(35250); + function f() { + return (0, r.jsx)("p", {}); + } + }], +]); diff --git a/packages/webcrack/src/unpack/test/samples/webpack-jsx.js.snap b/packages/webcrack/src/unpack/test/samples/webpack-jsx.js.snap new file mode 100644 index 00000000..ff653bc7 --- /dev/null +++ b/packages/webcrack/src/unpack/test/samples/webpack-jsx.js.snap @@ -0,0 +1,34 @@ +WebpackBundle { + "chunks": [ + WebpackChunk { + "chunkIds": [ + "1", + ], + "entryIds": [], + "modules": Map { + "0" => WebpackModule { + "ast": import { jsx } from "35250"; +function f() { + return

; +}, + "id": "0", + "isEntry": false, + "path": "0.js", + }, + }, + }, + ], + "entryId": "", + "modules": Map { + "0" => WebpackModule { + "ast": import { jsx } from "35250"; +function f() { + return

; +}, + "id": "0", + "isEntry": false, + "path": "0.js", + }, + }, + "type": "webpack", +} \ No newline at end of file From 69c4f93581245b2b6eb8074571b39573acbafbd4 Mon Sep 17 00:00:00 2001 From: j4k0xb <55899582+j4k0xb@users.noreply.github.com> Date: Sat, 13 Jan 2024 07:23:58 +0100 Subject: [PATCH 68/81] test: remove `__webpack_require__.r` in every import test --- .../webcrack/src/unpack/test/exports.test.ts | 24 +++++++++---------- .../webcrack/src/unpack/test/imports.test.ts | 14 +---------- 2 files changed, 13 insertions(+), 25 deletions(-) diff --git a/packages/webcrack/src/unpack/test/exports.test.ts b/packages/webcrack/src/unpack/test/exports.test.ts index 6cec855c..78216a29 100644 --- a/packages/webcrack/src/unpack/test/exports.test.ts +++ b/packages/webcrack/src/unpack/test/exports.test.ts @@ -132,18 +132,18 @@ describe('webpack 4', () => { 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() {} - `)); + 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', () => { diff --git a/packages/webcrack/src/unpack/test/imports.test.ts b/packages/webcrack/src/unpack/test/imports.test.ts index 42ca629a..4baef7b5 100644 --- a/packages/webcrack/src/unpack/test/imports.test.ts +++ b/packages/webcrack/src/unpack/test/imports.test.ts @@ -6,7 +6,6 @@ const expectJS = testWebpackModuleTransform(); describe('webpack 4', () => { test('default import', () => expectJS(` - __webpack_require__.r(__webpack_exports__); const lib = __webpack_require__("lib"); console.log(lib.default); `).toMatchInlineSnapshot(` @@ -37,7 +36,6 @@ describe('webpack 4', () => { test('named import', () => expectJS(` - __webpack_require__.r(__webpack_exports__); const lib = __webpack_require__("lib"); console.log(lib.foo); `).toMatchInlineSnapshot(` @@ -47,7 +45,6 @@ describe('webpack 4', () => { test('multiple named imports', () => expectJS(` - __webpack_require__.r(__webpack_exports__); const lib = __webpack_require__("lib"); console.log(lib.foo, lib.foo, lib.bar); `).toMatchInlineSnapshot(` @@ -57,7 +54,6 @@ describe('webpack 4', () => { test('named import with indirect call', () => expectJS(` - __webpack_require__.r(__webpack_exports__); const lib = __webpack_require__("lib"); console.log(Object(lib.foo)("bar")); `).toMatchInlineSnapshot(` @@ -67,7 +63,6 @@ describe('webpack 4', () => { test('namespace import', () => expectJS(` - __webpack_require__.r(__webpack_exports__); const lib = __webpack_require__("lib"); console.log(lib); `).toMatchInlineSnapshot(` @@ -77,7 +72,6 @@ describe('webpack 4', () => { test('combined namespace and default import', () => expectJS(` - __webpack_require__.r(__webpack_exports__); const lib = __webpack_require__("lib"); console.log(lib, lib.default); `).toMatchInlineSnapshot(` @@ -89,7 +83,6 @@ describe('webpack 4', () => { // TODO: maybe theres no var or it got inlined somewhere test('side effect import', () => expectJS(` - __webpack_require__.r(__webpack_exports__); var lib = __webpack_require__("lib"); `).toMatchInlineSnapshot(` import "lib"; @@ -109,7 +102,6 @@ describe('webpack 4', () => { test('indirect calls', () => expectJS(` - __webpack_require__.r(__webpack_exports__); const lib = __webpack_require__("lib"); console.log(Object(lib.foo)("bar")); console.log(Object(lib.default)("bar")); @@ -121,7 +113,6 @@ describe('webpack 4', () => { test('sort import specifiers alphabetically', () => expectJS(` - __webpack_require__.r(__webpack_exports__); const lib = __webpack_require__("lib"); console.log(lib.xyz, lib.abc); `).toMatchInlineSnapshot(` @@ -132,7 +123,6 @@ describe('webpack 4', () => { test.todo('hoist imports', () => expectJS(` var _tmp; - __webpack_require__.r(__webpack_exports__); var lib = __webpack_require__("lib"); var lib2 = __webpack_require__("lib2"); console.log(lib, lib2); @@ -144,9 +134,9 @@ describe('webpack 4', () => { `), ); + // TODO: also create an import for the 2nd require call? test('mixed import/require', () => expectJS(` - __webpack_require__.r(__webpack_exports__); var lib = __webpack_require__("lib"); console.log(lib, __webpack_require__("lib2")); `).toMatchInlineSnapshot(` @@ -158,7 +148,6 @@ describe('webpack 4', () => { describe('webpack 5', () => { test('named import with indirect call', () => expectJS(` - __webpack_require__.r(__webpack_exports__); const lib = __webpack_require__("lib"); console.log((0, lib.foo)("bar")); `).toMatchInlineSnapshot(` @@ -169,7 +158,6 @@ describe('webpack 5', () => { test.todo('namespace import of commonjs module', () => expectJS(` var _cache; - __webpack_require__.r(__webpack_exports__); const lib = __webpack_require__("lib"); console.log(_cache ||= __webpack_require__.t(lib, 2)); `).toMatchInlineSnapshot(` From 379017eb8754088421550d7fa3267f658287bf03 Mon Sep 17 00:00:00 2001 From: j4k0xb <55899582+j4k0xb@users.noreply.github.com> Date: Sat, 13 Jan 2024 07:34:14 +0100 Subject: [PATCH 69/81] test: temporarily disable failing tests --- packages/webcrack/src/unpack/test/samples.test.ts | 2 +- packages/webcrack/src/unpack/test/unpack.test.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/webcrack/src/unpack/test/samples.test.ts b/packages/webcrack/src/unpack/test/samples.test.ts index 599d2d95..3590ac13 100644 --- a/packages/webcrack/src/unpack/test/samples.test.ts +++ b/packages/webcrack/src/unpack/test/samples.test.ts @@ -5,7 +5,7 @@ import { webcrack } from '../..'; const SAMPLES_DIR = join(__dirname, 'samples'); -describe('samples', async () => { +describe.skip('samples', async () => { const fileNames = (await readdir(SAMPLES_DIR)).filter((name) => name.endsWith('.js'), ); diff --git a/packages/webcrack/src/unpack/test/unpack.test.ts b/packages/webcrack/src/unpack/test/unpack.test.ts index 68da6c59..4b507618 100644 --- a/packages/webcrack/src/unpack/test/unpack.test.ts +++ b/packages/webcrack/src/unpack/test/unpack.test.ts @@ -14,7 +14,7 @@ test('detect top-level bundle first', async () => { expect(bundle?.type).toBe('browserify'); }); -test('path mapping', async () => { +test.skip('path mapping', async () => { const { bundle } = await webcrack( await readFile(join(SAMPLES_DIR, 'webpack-4.js'), 'utf8'), { @@ -27,7 +27,7 @@ test('path mapping', async () => { expect(bundle).toMatchSnapshot(); }); -test('prevent path traversal', async () => { +test.skip('prevent path traversal', async () => { const code = await readFile( join(SAMPLES_DIR, 'webpack-path-traversal.js'), 'utf8', From 31c2e70728011d434deab0bfe93e2d0633a17b97 Mon Sep 17 00:00:00 2001 From: j4k0xb <55899582+j4k0xb@users.noreply.github.com> Date: Sat, 13 Jan 2024 16:26:04 +0100 Subject: [PATCH 70/81] feat: remove `__esModule` property --- .../webcrack/src/unpack/test/exports.test.ts | 5 ++++ .../webcrack/src/unpack/webpack/module.ts | 28 +++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/packages/webcrack/src/unpack/test/exports.test.ts b/packages/webcrack/src/unpack/test/exports.test.ts index 78216a29..abd2ed90 100644 --- a/packages/webcrack/src/unpack/test/exports.test.ts +++ b/packages/webcrack/src/unpack/test/exports.test.ts @@ -306,3 +306,8 @@ describe('webpack 5', () => { function bar() {} `)); }); + +test('remove __esModule property', () => + expectJS(` + Object.defineProperty(exports, "__esModule", { value: true }); + `).toMatchInlineSnapshot(``)); diff --git a/packages/webcrack/src/unpack/webpack/module.ts b/packages/webcrack/src/unpack/webpack/module.ts index 4958ad73..2f44734a 100644 --- a/packages/webcrack/src/unpack/webpack/module.ts +++ b/packages/webcrack/src/unpack/webpack/module.ts @@ -1,5 +1,7 @@ import * as t from '@babel/types'; +import * as m from '@codemod/matchers'; import { applyTransform, renameFast, renameParameters } from '../../ast-utils'; +import { constMemberExpression } from '../../ast-utils/matcher'; import { Module } from '../module'; import type { FunctionPath } from './common-matchers'; import { ImportExportManager } from './import-export-manager'; @@ -46,6 +48,7 @@ export class WebpackModule extends Module { applyTransform(file, getDefaultExport, this.#importExportManager); applyTransform(file, definePropertyGetters, this.#importExportManager); this.#importExportManager.insertImportsAndExports(); + this.removeDefineESM(); // For CommonJS if (moduleBinding) renameFast(moduleBinding, 'module'); @@ -72,4 +75,29 @@ export class WebpackModule extends Module { lastNode.trailingComments.pop(); } } + + /** + * Remove `Object.defineProperty(exports, "__esModule", { value: trues });` + * This is most likely generated by TypeScript. + */ + private removeDefineESM(): void { + const matcher = m.expressionStatement( + m.callExpression(constMemberExpression('Object', 'defineProperty'), [ + m.identifier(), + m.stringLiteral('__esModule'), + m.objectExpression([ + m.objectProperty(m.identifier('value'), m.booleanLiteral(true)), + ]), + ]), + ); + + for (let i = 0; i < this.ast.program.body.length; i++) { + const node = this.ast.program.body[i]; + if (matcher.match(node)) { + this.#sourceType = 'esm'; + this.ast.program.body.splice(i, 1); + i--; + } + } + } } From 3a917965ef5d6266d88eef748455a5886e3b2a62 Mon Sep 17 00:00:00 2001 From: j4k0xb <55899582+j4k0xb@users.noreply.github.com> Date: Sat, 13 Jan 2024 17:18:27 +0100 Subject: [PATCH 71/81] feat: transform json modules --- apps/playground/src/App.tsx | 2 +- .../src/unpack/test/samples/webpack-4-json.js | 12 +++++ .../test/samples/webpack-4-json.js.snap | 44 +++++++++++++++++++ .../src/unpack/test/samples/webpack-5-json.js | 12 +++++ .../test/samples/webpack-5-json.js.snap | 44 +++++++++++++++++++ .../src/unpack/webpack/json-module.ts | 43 ++++++++++++++++++ .../webcrack/src/unpack/webpack/module.ts | 22 +++++++--- 7 files changed, 171 insertions(+), 8 deletions(-) create mode 100644 packages/webcrack/src/unpack/test/samples/webpack-4-json.js create mode 100644 packages/webcrack/src/unpack/test/samples/webpack-4-json.js.snap create mode 100644 packages/webcrack/src/unpack/test/samples/webpack-5-json.js create mode 100644 packages/webcrack/src/unpack/test/samples/webpack-5-json.js.snap create mode 100644 packages/webcrack/src/unpack/webpack/json-module.ts 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/packages/webcrack/src/unpack/test/samples/webpack-4-json.js b/packages/webcrack/src/unpack/test/samples/webpack-4-json.js new file mode 100644 index 00000000..22bdac2e --- /dev/null +++ b/packages/webcrack/src/unpack/test/samples/webpack-4-json.js @@ -0,0 +1,12 @@ +(window['webpackJsonp'] = window['webpackJsonp'] || []).push([ + [], + [ + function (module, exports, __webpack_require__) { + var json = __webpack_require__(1); + console.log(json); + }, + function (module, exports) { + module.exports = JSON.parse('{"foo":"bar"}'); + }, + ], +]); diff --git a/packages/webcrack/src/unpack/test/samples/webpack-4-json.js.snap b/packages/webcrack/src/unpack/test/samples/webpack-4-json.js.snap new file mode 100644 index 00000000..ee35a5b3 --- /dev/null +++ b/packages/webcrack/src/unpack/test/samples/webpack-4-json.js.snap @@ -0,0 +1,44 @@ +WebpackBundle { + "chunks": [ + WebpackChunk { + "chunkIds": [], + "entryIds": [], + "modules": Map { + "0" => WebpackModule { + "ast": import * as json from "1"; +console.log(json);, + "id": "0", + "isEntry": false, + "path": "0.js", + }, + "1" => WebpackModule { + "ast": module.exports = { + foo: "bar" +};, + "id": "1", + "isEntry": false, + "path": "1.json", + }, + }, + }, + ], + "entryId": "", + "modules": Map { + "0" => WebpackModule { + "ast": import * as json from "1"; +console.log(json);, + "id": "0", + "isEntry": false, + "path": "0.js", + }, + "1" => WebpackModule { + "ast": module.exports = { + foo: "bar" +};, + "id": "1", + "isEntry": false, + "path": "1.json", + }, + }, + "type": "webpack", +} \ No newline at end of file diff --git a/packages/webcrack/src/unpack/test/samples/webpack-5-json.js b/packages/webcrack/src/unpack/test/samples/webpack-5-json.js new file mode 100644 index 00000000..ca0c980d --- /dev/null +++ b/packages/webcrack/src/unpack/test/samples/webpack-5-json.js @@ -0,0 +1,12 @@ +(window['webpackJsonp'] = window['webpackJsonp'] || []).push([ + [], + [ + function (module, exports, __webpack_require__) { + var json = __webpack_require__(1); + console.log(json); + }, + function (module, exports) { + module.exports = { foo: 'bar' }; + }, + ], +]); diff --git a/packages/webcrack/src/unpack/test/samples/webpack-5-json.js.snap b/packages/webcrack/src/unpack/test/samples/webpack-5-json.js.snap new file mode 100644 index 00000000..ee35a5b3 --- /dev/null +++ b/packages/webcrack/src/unpack/test/samples/webpack-5-json.js.snap @@ -0,0 +1,44 @@ +WebpackBundle { + "chunks": [ + WebpackChunk { + "chunkIds": [], + "entryIds": [], + "modules": Map { + "0" => WebpackModule { + "ast": import * as json from "1"; +console.log(json);, + "id": "0", + "isEntry": false, + "path": "0.js", + }, + "1" => WebpackModule { + "ast": module.exports = { + foo: "bar" +};, + "id": "1", + "isEntry": false, + "path": "1.json", + }, + }, + }, + ], + "entryId": "", + "modules": Map { + "0" => WebpackModule { + "ast": import * as json from "1"; +console.log(json);, + "id": "0", + "isEntry": false, + "path": "0.js", + }, + "1" => WebpackModule { + "ast": module.exports = { + foo: "bar" +};, + "id": "1", + "isEntry": false, + "path": "1.json", + }, + }, + "type": "webpack", +} \ No newline at end of file diff --git a/packages/webcrack/src/unpack/webpack/json-module.ts b/packages/webcrack/src/unpack/webpack/json-module.ts new file mode 100644 index 00000000..bc66e83e --- /dev/null +++ b/packages/webcrack/src/unpack/webpack/json-module.ts @@ -0,0 +1,43 @@ +import traverse from '@babel/traverse'; +import type * as t from '@babel/types'; +import * as m from '@codemod/matchers'; +import { constKey, constMemberExpression } from '../../ast-utils'; + +/** + * @returns The parsed JSON value if the AST is a JSON module, otherwise undefined. + */ +export function transformJsonModule(ast: t.File): unknown { + const jsonValue = m.or( + m.stringLiteral(), + m.numericLiteral(), + m.unaryExpression('-', m.numericLiteral()), + m.booleanLiteral(), + m.nullLiteral(), + m.matcher( + (node) => jsonObject.match(node) || jsonArray.match(node), + ), + ); + const jsonObject = m.objectExpression( + m.arrayOf(m.objectProperty(constKey(), jsonValue)), + ); + const jsonArray = m.arrayExpression(m.arrayOf(jsonValue)); + const matcher = m.expressionStatement( + m.assignmentExpression( + '=', + constMemberExpression('module', 'exports'), + m.or(jsonObject, jsonArray), + ), + ); + + if (ast.program.body.length === 1 && matcher.match(ast.program.body[0])) { + let result: unknown; + traverse(ast, { + noScope: true, + 'ObjectExpression|ArrayExpression'(path) { + result = path.evaluate().value; + path.stop(); + }, + }); + return result; + } +} diff --git a/packages/webcrack/src/unpack/webpack/module.ts b/packages/webcrack/src/unpack/webpack/module.ts index 2f44734a..761810b0 100644 --- a/packages/webcrack/src/unpack/webpack/module.ts +++ b/packages/webcrack/src/unpack/webpack/module.ts @@ -5,6 +5,7 @@ import { constMemberExpression } from '../../ast-utils/matcher'; import { Module } from '../module'; import type { FunctionPath } from './common-matchers'; import { ImportExportManager } from './import-export-manager'; +import { transformJsonModule } from './json-module'; import { default as definePropertyGetters } from './runtime/define-property-getters'; import getDefaultExport from './runtime/get-default-export'; import global from './runtime/global'; @@ -16,7 +17,8 @@ import varInjections from './var-injections'; export class WebpackModule extends Module { #importExportManager: ImportExportManager; // TODO: expose to public API - #sourceType: 'commonjs' | 'esm' = 'commonjs'; + #sourceType: 'commonjs' | 'esm' | 'json' = 'commonjs'; + #json: unknown; constructor(id: string, ast: FunctionPath, isEntry: boolean) { // TODO: refactor @@ -54,12 +56,18 @@ export class WebpackModule extends Module { if (moduleBinding) renameFast(moduleBinding, 'module'); if (exportsBinding) renameFast(exportsBinding, 'exports'); - // this.removeDefineESM(); - // // FIXME: some bundles don't define __esModule but still declare esm exports - // // https://github.com/0xdevalias/chatgpt-source-watch/blob/main/orig/_next/static/chunks/167-121de668c4456907.js - // if (this.#sourceType === 'esm') { - // this.convertExportsToESM(); - // } + const json = transformJsonModule(this.ast); + if (json) { + this.#sourceType = 'json'; + this.#json = json; + this.path = this.path.replace(/\.js$/, '.json'); + } + } + + override get code() { + return this.#sourceType === 'json' + ? JSON.stringify(this.#json, null, 2) + : super.code; } /** From 6a0e470aee60ada1d21671a32a103f1cae811edc Mon Sep 17 00:00:00 2001 From: j4k0xb <55899582+j4k0xb@users.noreply.github.com> Date: Sat, 13 Jan 2024 17:40:40 +0100 Subject: [PATCH 72/81] feat(playground): add json file icons --- apps/playground/src/components/FileNode.tsx | 29 ++++++++++++++++----- 1 file changed, 23 insertions(+), 6 deletions(-) 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}
  • From 023800757eadd258fe5440be7bdb3d4c63968388 Mon Sep 17 00:00:00 2001 From: j4k0xb <55899582+j4k0xb@users.noreply.github.com> Date: Mon, 15 Jan 2024 01:46:56 +0100 Subject: [PATCH 73/81] feat: inline external modules --- packages/webcrack/src/unpack/test/index.ts | 3 +- .../test/samples/webpack-4-externals.js | 12 +++ .../test/samples/webpack-4-externals.js.snap | 52 ++++++++++++ .../webcrack/src/unpack/webpack/bundle.ts | 28 +++---- .../unpack/webpack/import-export-manager.ts | 29 ++++--- .../src/unpack/webpack/json-module.ts | 2 +- .../webcrack/src/unpack/webpack/module.ts | 81 +++++++++++++------ 7 files changed, 154 insertions(+), 53 deletions(-) create mode 100644 packages/webcrack/src/unpack/test/samples/webpack-4-externals.js create mode 100644 packages/webcrack/src/unpack/test/samples/webpack-4-externals.js.snap diff --git a/packages/webcrack/src/unpack/test/index.ts b/packages/webcrack/src/unpack/test/index.ts index 296efe39..12a5c65d 100644 --- a/packages/webcrack/src/unpack/test/index.ts +++ b/packages/webcrack/src/unpack/test/index.ts @@ -32,7 +32,8 @@ export function testWebpackModuleTransform(): ( FunctionExpression(path) { path.stop(); file = t.file(t.program(path.node.body.body)); - new WebpackModule('test', path, true); + const module = new WebpackModule('test', path, true); + module.applyTransforms((moduleId) => moduleId); }, }); return expect(file!); diff --git a/packages/webcrack/src/unpack/test/samples/webpack-4-externals.js b/packages/webcrack/src/unpack/test/samples/webpack-4-externals.js new file mode 100644 index 00000000..0b7dada8 --- /dev/null +++ b/packages/webcrack/src/unpack/test/samples/webpack-4-externals.js @@ -0,0 +1,12 @@ +(window['webpackJsonp'] = window['webpackJsonp'] || []).push([ + [], + [ + function (module, exports, __webpack_require__) { + var path = __webpack_require__(1); + console.log(path); + }, + function (module, exports) { + module.exports = require('path'); + }, + ], +]); diff --git a/packages/webcrack/src/unpack/test/samples/webpack-4-externals.js.snap b/packages/webcrack/src/unpack/test/samples/webpack-4-externals.js.snap new file mode 100644 index 00000000..f7fb8039 --- /dev/null +++ b/packages/webcrack/src/unpack/test/samples/webpack-4-externals.js.snap @@ -0,0 +1,52 @@ +WebpackBundle { + "chunks": [ + WebpackChunk { + "chunkIds": [], + "entryIds": [], + "modules": Map { + "0" => WebpackModule { + "ast": import * as path from "path"; +console.log(path);, + "externalModule": undefined, + "id": "0", + "imports": Set { + "1", + }, + "isEntry": false, + "path": "0.js", + }, + "1" => WebpackModule { + "ast": module.exports = require("path");, + "externalModule": "path", + "id": "1", + "imports": Set {}, + "isEntry": false, + "path": "1.js", + }, + }, + }, + ], + "entryId": "", + "modules": Map { + "0" => WebpackModule { + "ast": import * as path from "path"; +console.log(path);, + "externalModule": undefined, + "id": "0", + "imports": Set { + "1", + }, + "isEntry": false, + "path": "0.js", + }, + "1" => WebpackModule { + "ast": module.exports = require("path");, + "externalModule": "path", + "id": "1", + "imports": Set {}, + "isEntry": false, + "path": "1.js", + }, + }, + "type": "webpack", +} \ No newline at end of file diff --git a/packages/webcrack/src/unpack/webpack/bundle.ts b/packages/webcrack/src/unpack/webpack/bundle.ts index 068dc83c..b8a53eeb 100644 --- a/packages/webcrack/src/unpack/webpack/bundle.ts +++ b/packages/webcrack/src/unpack/webpack/bundle.ts @@ -1,4 +1,5 @@ import { Bundle } from '../bundle'; +import { relativePath } from '../path'; import type { WebpackChunk } from './chunk'; import type { WebpackModule } from './module'; @@ -13,21 +14,20 @@ export class WebpackBundle extends Bundle { ) { super('webpack', entryId, modules); this.chunks = chunks; + + this.modules.forEach((module) => { + module.applyTransforms((moduleId) => this.resolvePath(module, moduleId)); + }); } - /** - * Undoes some of the transformations that Webpack injected into the modules. - */ - applyTransforms(): void { - // this.modules.forEach((module) => { - // module.replaceRequireCalls((id) => { - // const requiredModule = this.modules.get(id); - // return requiredModule - // ? { path: relativePath(module.path, requiredModule.path) } - // : { path: id, external: true }; - // }); - // convertESM(module); - // }); - // convertDefaultRequire(this); + private resolvePath(module: WebpackModule, moduleId: string): string { + const importedModule = this.modules.get(moduleId); + // probably external or in an unknown chunk, keep as is + if (!importedModule) return moduleId; + + // inline external modules instead of requiring them + if (importedModule.externalModule) return importedModule.externalModule; + + return relativePath(module.path, importedModule.path); } } diff --git a/packages/webcrack/src/unpack/webpack/import-export-manager.ts b/packages/webcrack/src/unpack/webpack/import-export-manager.ts index d0ed7e6d..850ac734 100644 --- a/packages/webcrack/src/unpack/webpack/import-export-manager.ts +++ b/packages/webcrack/src/unpack/webpack/import-export-manager.ts @@ -1,4 +1,4 @@ -import { expression, statement } from '@babel/template'; +import { statement } from '@babel/template'; import type { Binding, NodePath, Scope } from '@babel/traverse'; import * as t from '@babel/types'; import * as m from '@codemod/matchers'; @@ -7,6 +7,7 @@ import { generate, renameFast } from '../../ast-utils'; import { dereference } from '../../ast-utils/binding'; // TODO: when it accesses module.exports, don't convert to esm +// FIXME: import name collisions: https://github.com/0xdevalias/chatgpt-source-watch/blob/main/orig/_next/static/chunks/167-121de668c4456907.js module 870 /** * Example: `__webpack_require__(id)` @@ -58,20 +59,18 @@ export class ImportExportManager { this.collectRequireCalls(); } - insertImportsAndExports() { + insertImportsAndExports(resolve: (moduleId: string) => string): void { this.requireVars.forEach((requireVar) => { - // TODO: resolve module id to path const namedExports = t.exportNamedDeclaration( undefined, requireVar.namedExports, - t.stringLiteral(requireVar.moduleId), + t.stringLiteral(resolve(requireVar.moduleId)), ); - // TODO: resolve module id to path const namespaceExports = requireVar.namespaceExports.map((specifier) => t.exportNamedDeclaration( undefined, [specifier], - t.stringLiteral(requireVar.moduleId), + t.stringLiteral(resolve(requireVar.moduleId)), ), ); if (namedExports.specifiers.length > 0) { @@ -87,10 +86,9 @@ export class ImportExportManager { this.requireVars.forEach((requireVar) => { this.sortImportSpecifiers(requireVar.namedImports); - // TODO: resolve module id to path const namedImports = t.importDeclaration( [requireVar.defaultImport ?? [], requireVar.namedImports].flat(), - t.stringLiteral(requireVar.moduleId), + t.stringLiteral(resolve(requireVar.moduleId)), ); if (namedImports.specifiers.length > 0) { @@ -100,7 +98,7 @@ export class ImportExportManager { // TODO: resolve module id to path const namespaceImport = t.importDeclaration( [requireVar.namespaceImport], - t.stringLiteral(requireVar.moduleId), + t.stringLiteral(resolve(requireVar.moduleId)), ); requireVar.binding.path.parentPath!.insertAfter(namespaceImport); } @@ -115,9 +113,11 @@ export class ImportExportManager { // side-effect import if (!requireVar.binding.referenced && !hasImports && !hasExports) { - // TODO: resolve module id to path requireVar.binding.path.parentPath!.insertAfter( - t.importDeclaration([], t.stringLiteral(requireVar.moduleId)), + t.importDeclaration( + [], + t.stringLiteral(resolve(requireVar.moduleId)), + ), ); } @@ -126,8 +126,11 @@ export class ImportExportManager { // this should never happen unless for mixed esm/commonjs: this.requireCalls.forEach(({ path, moduleId }) => { - // TODO: resolve module id to path - path.replaceWith(expression`require('${moduleId}')`()); + path.replaceWith( + t.callExpression(t.identifier('require'), [ + t.stringLiteral(resolve(moduleId)), + ]), + ); }); } diff --git a/packages/webcrack/src/unpack/webpack/json-module.ts b/packages/webcrack/src/unpack/webpack/json-module.ts index bc66e83e..f410f900 100644 --- a/packages/webcrack/src/unpack/webpack/json-module.ts +++ b/packages/webcrack/src/unpack/webpack/json-module.ts @@ -18,7 +18,7 @@ export function transformJsonModule(ast: t.File): unknown { ), ); const jsonObject = m.objectExpression( - m.arrayOf(m.objectProperty(constKey(), jsonValue)), + m.arrayOf(m.objectProperty(constKey(), jsonValue, false)), ); const jsonArray = m.arrayExpression(m.arrayOf(jsonValue)); const matcher = m.expressionStatement( diff --git a/packages/webcrack/src/unpack/webpack/module.ts b/packages/webcrack/src/unpack/webpack/module.ts index 761810b0..07481767 100644 --- a/packages/webcrack/src/unpack/webpack/module.ts +++ b/packages/webcrack/src/unpack/webpack/module.ts @@ -1,3 +1,4 @@ +import type { Binding } from '@babel/traverse'; import * as t from '@babel/types'; import * as m from '@codemod/matchers'; import { applyTransform, renameFast, renameParameters } from '../../ast-utils'; @@ -15,10 +16,23 @@ import namespaceObject from './runtime/namespace-object'; import varInjections from './var-injections'; export class WebpackModule extends Module { - #importExportManager: ImportExportManager; - // TODO: expose to public API + /** + * Module ids that are imported by this module. + */ + imports: Set; + + #manager: ImportExportManager; + #moduleBinding: Binding | undefined; + #exportsBinding: Binding | undefined; + #webpackRequireBinding: Binding | undefined; #sourceType: 'commonjs' | 'esm' | 'json' = 'commonjs'; #json: unknown; + /** + * The module name of a re-exported dependency. + * https://webpack.js.org/configuration/externals. + * @internal + */ + externalModule?: string; constructor(id: string, ast: FunctionPath, isEntry: boolean) { // TODO: refactor @@ -33,29 +47,15 @@ export class WebpackModule extends Module { '__webpack_exports__', '__webpack_require__', ]); - const moduleBinding = ast.scope.getBinding('__webpack_module__'); - const webpackRequireBinding = ast.scope.getBinding('__webpack_require__'); - const exportsBinding = ast.scope.getBinding('__webpack_exports__'); - - applyTransform(file, varInjections); - - this.#importExportManager = new ImportExportManager( - file, - webpackRequireBinding, - ); - applyTransform(file, global, webpackRequireBinding); - applyTransform(file, hasOwnProperty, webpackRequireBinding); - applyTransform(file, moduleDecorator, webpackRequireBinding); - applyTransform(file, namespaceObject); - applyTransform(file, getDefaultExport, this.#importExportManager); - applyTransform(file, definePropertyGetters, this.#importExportManager); - this.#importExportManager.insertImportsAndExports(); - this.removeDefineESM(); + this.#moduleBinding = ast.scope.getBinding('__webpack_module__'); + this.#webpackRequireBinding = ast.scope.getBinding('__webpack_require__'); + this.#exportsBinding = ast.scope.getBinding('__webpack_exports__'); - // For CommonJS - if (moduleBinding) renameFast(moduleBinding, 'module'); - if (exportsBinding) renameFast(exportsBinding, 'exports'); + const manager = new ImportExportManager(file, this.#webpackRequireBinding); + this.#manager = manager; + this.imports = new Set(manager.requireCalls.map((call) => call.moduleId)); + this.externalModule = this.detectExternalModule(); const json = transformJsonModule(this.ast); if (json) { this.#sourceType = 'json'; @@ -64,6 +64,21 @@ export class WebpackModule extends Module { } } + applyTransforms(resolve: (moduleId: string) => string): void { + applyTransform(this.ast, varInjections); + applyTransform(this.ast, global, this.#webpackRequireBinding); + applyTransform(this.ast, hasOwnProperty, this.#webpackRequireBinding); + applyTransform(this.ast, moduleDecorator, this.#webpackRequireBinding); + applyTransform(this.ast, namespaceObject); + applyTransform(this.ast, getDefaultExport, this.#manager); + applyTransform(this.ast, definePropertyGetters, this.#manager); + this.#manager.insertImportsAndExports(resolve); + this.removeDefineESM(); + + if (this.#moduleBinding) renameFast(this.#moduleBinding, 'module'); + if (this.#exportsBinding) renameFast(this.#exportsBinding, 'exports'); + } + override get code() { return this.#sourceType === 'json' ? JSON.stringify(this.#json, null, 2) @@ -84,8 +99,26 @@ export class WebpackModule extends Module { } } + private detectExternalModule(): string | undefined { + const moduleName = m.capture(m.anyString()); + const matcher = m.program([ + m.expressionStatement( + m.assignmentExpression( + '=', + constMemberExpression('__webpack_module__', 'exports'), + m.callExpression(m.identifier('require'), [ + m.stringLiteral(moduleName), + ]), + ), + ), + ]); + if (matcher.match(this.ast.program)) { + return moduleName.current!; + } + } + /** - * Remove `Object.defineProperty(exports, "__esModule", { value: trues });` + * Remove `Object.defineProperty(exports, "__esModule", { value: true });` * This is most likely generated by TypeScript. */ private removeDefineESM(): void { From 0c9a328753e08f8672dd9fc827a1160f856abdb5 Mon Sep 17 00:00:00 2001 From: j4k0xb <55899582+j4k0xb@users.noreply.github.com> Date: Mon, 15 Jan 2024 01:51:33 +0100 Subject: [PATCH 74/81] fix: use `__webpack_module__` for json module matcher --- packages/webcrack/src/unpack/webpack/json-module.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/webcrack/src/unpack/webpack/json-module.ts b/packages/webcrack/src/unpack/webpack/json-module.ts index f410f900..e153edf7 100644 --- a/packages/webcrack/src/unpack/webpack/json-module.ts +++ b/packages/webcrack/src/unpack/webpack/json-module.ts @@ -24,7 +24,7 @@ export function transformJsonModule(ast: t.File): unknown { const matcher = m.expressionStatement( m.assignmentExpression( '=', - constMemberExpression('module', 'exports'), + constMemberExpression('__webpack_module__', 'exports'), m.or(jsonObject, jsonArray), ), ); From b74ee884ba5d69a816b9c2f6028ca6e7abab1415 Mon Sep 17 00:00:00 2001 From: j4k0xb <55899582+j4k0xb@users.noreply.github.com> Date: Mon, 15 Jan 2024 02:06:17 +0100 Subject: [PATCH 75/81] test: re-enable unpack samples --- .../webcrack/src/unpack/test/samples.test.ts | 2 +- .../unpack/test/samples/browserify-2.js.snap | 4 +- .../samples/browserify-webpack-nested.js.snap | 91 ++++++++---- .../unpack/test/samples/browserify.js.snap | 8 +- .../test/samples/webpack-0.11.x.js.snap | 135 +++++++++++------- .../test/samples/webpack-4-json.js.snap | 16 ++- .../src/unpack/test/samples/webpack-4.js.snap | 24 +++- .../test/samples/webpack-5-json.js.snap | 16 ++- .../test/samples/webpack-5-no-entry.js.snap | 8 +- .../src/unpack/test/samples/webpack-5.js.snap | 38 +++-- .../samples/webpack-chunk-entries.js.snap | 8 +- .../test/samples/webpack-jsonp-chunk.js.snap | 30 ++-- .../unpack/test/samples/webpack-jsx.js.snap | 8 ++ .../samples/webpack-path-traversal.js.snap | 6 +- .../webcrack/src/unpack/test/unpack.test.ts | 2 +- .../webcrack/src/unpack/webpack/module.ts | 6 +- 16 files changed, 270 insertions(+), 132 deletions(-) diff --git a/packages/webcrack/src/unpack/test/samples.test.ts b/packages/webcrack/src/unpack/test/samples.test.ts index 3590ac13..599d2d95 100644 --- a/packages/webcrack/src/unpack/test/samples.test.ts +++ b/packages/webcrack/src/unpack/test/samples.test.ts @@ -5,7 +5,7 @@ import { webcrack } from '../..'; const SAMPLES_DIR = join(__dirname, 'samples'); -describe.skip('samples', async () => { +describe('samples', async () => { const fileNames = (await readdir(SAMPLES_DIR)).filter((name) => name.endsWith('.js'), ); 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 1aa9ff0f..3736faa9 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 @@ -4,26 +4,35 @@ WebpackBundle { "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!" }));, + "externalModule": undefined, "id": "0", + "imports": Set { + "1", + "4", + }, "isEntry": true, - "path": "./index.js", + "path": "index.js", }, "1" => WebpackModule { "ast": // style-loader: Adds some css to the DOM by adding a