pionxzh / wakaru

🔪📦 Javascript decompiler for modern frontend
https://wakaru.vercel.app/
MIT License
252 stars 13 forks source link

[smart-rename] improve `handleReactRename` #49

Open 0xdevalias opened 9 months ago

0xdevalias commented 9 months ago

Currently smart-rename's implementation has a handleReactRename function that appears to have renames for:

I figured I would use this as a bit of a meta-issue for capturing improvements that could be made to this smart-rename for React.

Context

If not otherwise specified, the webpack code I am looking at to derive my examples is the following (Ref), after using the CLI to unpack it to ./496-unpacked, and then unminify it to ./496-unminified:

⇒ cd ./unpacked/_next/static/chunks

⇒ npx @wakaru/unpacker 496.js -o ./496-unpacked
# ..snip..

⇒ npx @wakaru/unminify ./496-unpacked/* -o ./496-unminified
# ..snip..

TODO

See Also

0xdevalias commented 9 months ago

useState (but actually it ends up being more about @swc/helpers)

Looking at module-10604.js, we can see that it's a React component using useState:

module-10604.js (full source) Unpacked: ```js var r = require(39324), a = require(22830), i = require(4337), o = require(35250), s = require(19841), l = require(70079), u = require(34303), d = require(38317); function c() { var e = (0, i._)(["absolute right-0 top-1/2 -translate-y-1/2"]); return ( (c = function () { return e; }), e ); } exports.Z = l.forwardRef(function (e, t) { var n = e.name, i = e.placeholder, u = e.type, c = e.displayName, h = e.onChange, g = e.onBlur, m = e.value, p = e.saveOnBlur, v = e.icon, x = e.onInputIconClick, b = e.className, y = e.autoComplete, w = e.autoFocus, j = e.onPressEnter, _ = (0, a._)((0, l.useState)(m), 2), C = _[0], M = _[1], k = (0, l.useCallback)( function (e) { null == g || g(e), p && M(e.target.value); }, [g, p] ), T = (0, l.useCallback)( function (e) { null == h || h(e), p && M(e.target.value); }, [h, p] ), N = (0, l.useCallback)( function (e) { "Enter" === e.key && j && (e.preventDefault(), j()); }, [j] ); (0, l.useEffect)( function () { M(m); }, [m] ); var S = (0, r._)({}, p ? {} : { value: m }, p ? { value: C } : {}); return (0, o.jsxs)("div", { className: (0, s.Z)("rounded-md border border-gray-300 px-3 py-2 shadow-sm focus-within:border-indigo-600 focus-within:ring-1 focus-within:ring-indigo-600 dark:bg-gray-700", b), children: [(0, o.jsx)("label", { htmlFor: n, className: "block text-xs font-medium text-gray-900 dark:text-gray-100", children: c }), (0, o.jsxs)("div", { className: (0, s.Z)(c && "mt-1", "relative"), children: [(0, o.jsx)("input", (0, r._)({ ref: t, type: u, name: n, id: n, className: (0, s.Z)("block w-full border-0 p-0 text-gray-900 placeholder-gray-500 outline-none focus:ring-0 dark:bg-gray-700 dark:text-gray-100 sm:text-sm", v && "pr-6"), placeholder: i, onBlur: k, onChange: T, onKeyDown: N, autoComplete: y, autoFocus: w }, S)), v && (0, o.jsx)(f, { onClick: x, children: (0, o.jsx)(d.ZP, { icon: v }) })] })] }); }); var f = u.Z.button(c()); ``` Unminified: ```js const { _: _$1 } = require(39324); const { _: _$0 } = require(22830); const { _ } = require(4337); const { jsxs, jsx } = require(35250); const { Z: Z$0 } = require(19841); const l = require(70079); const { useState, useCallback, useEffect } = l; const u = require(34303); const d = require(38317); function c() { const e = _(["absolute right-0 top-1/2 -translate-y-1/2"]); c = () => e; return e; } export const Z = l.forwardRef((e, t) => { const { name, placeholder, type, displayName, onChange, onBlur, value, saveOnBlur, icon, onInputIconClick, className, autoComplete, autoFocus, onPressEnter, } = e; const [C, M] = _$0(useState(value), 2); const k = useCallback( (e) => { if (onBlur != null) { onBlur(e); } if (saveOnBlur) { M(e.target.value); } }, [onBlur, saveOnBlur] ); const T = useCallback( (e) => { if (onChange != null) { onChange(e); } if (saveOnBlur) { M(e.target.value); } }, [onChange, saveOnBlur] ); const N = useCallback( (e) => { if (e.key === "Enter" && onPressEnter) { e.preventDefault(); onPressEnter(); } }, [onPressEnter] ); useEffect(() => { M(value); }, [value]); const S = _$1( {}, saveOnBlur ? {} : { value: value }, saveOnBlur ? { value: C } : {} ); return (
{icon && {}}
); }); var F = u.Z.button(c()); ```

Unpacked:

var r = require(39324),
  a = require(22830),
  // ..snip
  l = require(70079),

// ..snip

exports.Z = l.forwardRef(function (e, t) {
  var n = e.name,
    // ..snip
    m = e.value,
    // ..snip
    _ = (0, a._)((0, l.useState)(m), 2),
    C = _[0],
    M = _[1],
    // ..snip
});

Unminified:

// ..snip

const { _: _$0 } = require(22830);

// ..snip

export const Z = l.forwardRef((e, t) => {
  const {
    // ..snip..
    value,
    // ..snip..
  } = e;

  const [C, M] = _$0(useState(value), 2);
  // ..snip
});

While there is a smart-rename for useState already (Ref), it appears it may not be getting applied due to the _$0 function that's wrapping const [C, M] = _$0(useState(value), 2);

Looking through the rest of the webpack bundle code (Ref) for the 22830 module, we find it in main.js; which after unpacking, becomes module-22830.js:

⇒ npx @wakaru/unpacker main.js -o ./main-unpacked/
# ..snip..

⇒ npx @wakaru/unminify ./main-unpacked/* -o ./main-unminified
# ..snip..
module-22830.js (full source) Unpacked: ```js "use strict";; ; var n = require(59378); function o(e, t) { return ( (function (e) { if (Array.isArray(e)) return e; })(e) || (function (e, t) { var r, n, o = null == e ? null : ("undefined" != typeof Symbol && e[Symbol.iterator]) || e["@@iterator"]; if (null != o) { var a = [], i = !0, u = !1; try { for ( o = o.call(e); !(i = (r = o.next()).done) && (a.push(r.value), !t || a.length !== t); i = !0 ); } catch (e) { (u = !0), (n = e); } finally { try { i || null == o.return || o.return(); } finally { if (u) throw n; } } return a; } })(e, t) || (0, n.N)(e, t) || (function () { throw TypeError( "Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method." ); })() ); } module.exports = { _: o, _sliced_to_array: o }; ``` Unminified: ``` const { N } = require(59378); function o(e, t) { return ( ((e) => { if (Array.isArray(e)) { return e; } })(e) || ((e, t) => { let r; let n; let o = e == null ? null : (typeof Symbol != "undefined" && e[Symbol.iterator]) || e["@@iterator"]; if (o != null) { const a = []; let i = true; let u = false; try { for ( o = o.call(e); !(i = (r = o.next()).done) && (a.push(r.value), !t || a.length !== t); i = true ) {} } catch (e) { u = true; n = e; } finally { try { if (!i && o.return != null) { o.return(); } } finally { if (u) { throw n; } } } return a; } })(e, t) || N(e, t) || (() => { throw TypeError( "Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method." ); })() ); } export default { _: o, _sliced_to_array: o, }; ```

Looking at module-22830.js, we find the following code:

// ..snip..
    N(e, t) ||
    (() => {
      throw TypeError(
        "Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."
      );
    })()
  );
}

export default {
  _: o,
  _sliced_to_array: o,
};

Which after using GitHub code search:

We find a relevant looking reference in a test for next/swc's hook_optimizer:

Which we can see appears to be testing the output of swc compiling some React useState code:

// ..snip..

describe('next/swc', () => {
  describe('hook_optimizer', () => {
    it('should leave alone array destructuring of hooks', async () => {
      const output = await swc(
        trim`
        import { useState } from 'react';
        const [count, setCount] = useState(0);
      `
      )

// ..snip..

And compares it to the compiled output, which includes helper functions like:

Within that output code, we see:

// ..snip..

function _non_iterable_rest() {
  throw new TypeError("Invalid attempt to destructure non-iterable instance.\\\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
}

// ..snip..

import { useState } from "react";
var _useState = _sliced_to_array(useState(0), 2),
count = _useState[0],
setCount = _useState[1];

Looking at the latter part of that, we can see how the _sliced_to_array(useState(0), 2) matches the format of our original unpacked useState code from module-10604.js:

Unpacked:

_ = (0, a._)((0, l.useState)(m), 2),
C = _[0],
M = _[1],

Unminified:

const [C, M] = _$0(useState(value), 2);

Which means that, based on the above, the _$0 in my webpacked code is likely the swc helper function _sliced_to_array:

function _sliced_to_array(arr, i) {
    return _array_with_holes(arr) || _iterable_to_array_limit(arr, i) || _unsupported_iterable_to_array(arr, i) || _non_iterable_rest();
}

We can also generate this output ourselves using the swc playground:

Searching the swc GitHub repo for _sliced_to_array, we see that it seems to be included in the @swc/helpers package:

Conclusion

It seems that the issue here is less about smart-rename's handleReactRename not handling useState properly; and more that wakaru needs to add support for swc's 'runtime helper' functions like _sliced_to_array / etc from @swc/helpers; probably in a similar way to how babel's are currently implemented:

This may in part be relevant to the following 'module detection' issue as well:


Edit: I spun the @swc/helpers part of this out into a more focussed issue here: