Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[smart-rename] improve handleReactRename #49

Open
1 task
0xdevalias opened this issue Nov 20, 2023 · 1 comment
Open
1 task

[smart-rename] improve handleReactRename #49

0xdevalias opened this issue Nov 20, 2023 · 1 comment
Labels
enhancement New feature or request

Comments

@0xdevalias
Copy link

0xdevalias commented Nov 20, 2023

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

See Also

@0xdevalias
Copy link
Author

0xdevalias commented Nov 20, 2023

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:

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:

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

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:

@pionxzh pionxzh added the enhancement New feature or request label Nov 21, 2023
@0xdevalias 0xdevalias changed the title improve smart-rename's handleReactRename [smart-rename] improve handleReactRename Dec 20, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

2 participants