wakaru icon indicating copy to clipboard operation
wakaru copied to clipboard

[smart-rename] improve `handleReactRename`

Open 0xdevalias opened this issue 1 year ago • 1 comments

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

  • const uContext = o.createContext(u);
  • const uRef = o.useRef(u);
  • const [e, SetE] = o.useState(0);

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

  • [ ] useState (but actually it ends up being more about @swc/helpers) (Ref)
    • See also: https://github.com/pionxzh/wakaru/issues/50

See Also

  • #48

0xdevalias avatar Nov 20 '23 09:11 0xdevalias

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:

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:

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 (
    <div
      className={Z$0(
        "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",
        className
      )}
    >
      <label
        htmlFor={name}
        className="block text-xs font-medium text-gray-900 dark:text-gray-100"
      >
        {displayName}
      </label>
      <div className={Z$0(displayName && "mt-1", "relative")}>
        <input
          {..._$1(
            {
              ref: t,
              type: type,
              name: name,
              id: name,
              className: Z$0(
                "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",
                icon && "pr-6"
              ),
              placeholder: placeholder,
              onBlur: k,
              onChange: T,
              onKeyDown: N,
              autoComplete: autoComplete,
              autoFocus: autoFocus,
            },
            S
          )}
        />
        {icon && <F onClick={onInputIconClick}>{<d.ZP icon={icon} />}</F>}
      </div>
    </div>
  );
});

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:

"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:

  • https://github.com/search?type=code&auto_enroll=true&q=%22Invalid+attempt+to+destructure+non-iterable+instance.%22+_sliced_to_array

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

  • https://github.com/vercel/next.js/blob/32c9ce6805ac66d3d1d91b982fac86c5b1f70134/test/unit/next-swc.test.ts#L11-L72

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:

  • function _array_like_to_array(arr, len) {
  • function _array_with_holes(arr) {
  • function _iterable_to_array_limit(arr, i) {
  • function _non_iterable_rest() {
  • function _sliced_to_array(arr, i) {
  • function _unsupported_iterable_to_array(o, minLen) {

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:

  • https://swc.rs/playground
    • Input
      • import { useState } from 'react';
        const [count, setCount] = useState(0);
        

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

  • https://github.com/search?q=repo%3Aswc-project%2Fswc%20_sliced_to_array&type=code
    • https://github.com/swc-project/swc/blob/main/packages/helpers/esm/_sliced_to_array.js

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:

  • https://github.com/pionxzh/wakaru/tree/main/packages/unminify/src/transformations/runtime-helpers
    • https://github.com/pionxzh/wakaru/blob/main/packages/unminify/src/transformations/runtime-helpers/index.ts#L11-L20
    • https://github.com/pionxzh/wakaru/tree/main/packages/unminify/src/transformations/runtime-helpers/babel

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

  • #41

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

  • https://github.com/pionxzh/wakaru/issues/50

0xdevalias avatar Nov 20 '23 10:11 0xdevalias