Nuxt3でjavascript-obfuscationを使う方法

めちゃくちゃ沼った上に、解決方法がかなり複雑なので備忘録も兼ねて書いておきます。

# tl;dr
```bash
npm i git+https://github.com/ajiken4610/vite-plugin-javascript-obfuscator
```
```replacer.js
export default (matched) => {
  return {
    name: "vite-plugin-replace-resolve-component",
    transform(src, id) {
      let ret = src;
      ret = ret.replaceAll(
        /(?<!function )(_?resolveComponent)\(["']([^\)]+?)["']\)/g,
        (_, resolve, name) => {
          const ret = `__${resolve}${name}`;
          matched.indexOf(ret) === -1 && matched.push(ret);
          return `"${ret}"`;
        },
      );
      return {
        code: ret,
        map: null,
      };
    },
  };
};
```
```surrounder.js
export default () => {
  return {
    name: "vite-plugin-surround-resolve-component",
    transform(src) {
      let ret = src;
      ret = ret.replaceAll(
        /["']__(_?resolveComponent)([^"']+)["']/g,
        (_, resolve, name) => `(${resolve}("${name}"))`,
      );
      return {
        code: ret,
        map: null,
      };
    },
  };
};

```
```ts:nuxt.config.ts
const reservedStrings: string[] = []
...
vite: {
      plugins: [
        replacer(reservedStrings),
        obfuscator({
          options: {
            optionsPreset: "high-obfuscation",
            reservedStrings,
          },
          apply: "build",
        }),
        surrounder(),
      ],
```

注：`build.minify === false　&& selfDefending === true`だと、ブラウザがフリーズします。
# 何をしているか
やっていることは意外と単純で、

1. `resolveComponent("ComponentName")`を`"__resolveComponentComponentName"`という文字列に置換する
1. obfuscatorに、`"__resolveComponentComponentName"`以外をobfuscateしてもらう。
1. `"__resolveComponentComponentName"`を`(resolveComponent("ComponentName"))`に置換し戻す。

をしています。
# 経緯
ちょっと難読化してみたかっただけだけど、思ってた以上にここにたどり着くまでが長かった。
## この処理をしないとどうなるか
レンダリングすると、以下のようになります。
```html
<div id="__nuxt">
  <nuxtpage></nuxtpage>
</div>
```
私は、これを見ててっきり**NuxtPageがTree-shakeされてしまったんだ**と思っていました。

実際、この状態のソースコードにはNuxtPageのものが含まれません。

そこで、私は、明示的にNuxt関係のコンポーネントをビルドに含めてみたりいろいろしてたのですが、23時間パソコンとぶっ通しでにらめっこしてようやく発見しました。

https://github.com/nuxt/nuxt/blob/ba4361a0f05b1f101ea3740ae093a43e14ca39ab/packages/nuxt/src/components/loader.ts#L45

このソースを見てもらえばわかるのですが、Nuxt3は正規表現に一致したresolveComponent関数を見つけて、それに関するものをimportするという形式をとっているようです。

この正規表現がミソでした。

見るとわかりますが、`(resolveComponent("SomeComponent"))`というように、resolveComponent自体が`()`でくくられていないと機能しません。これはソース見ないとわからないよね。

ということで、原因は`resolveComponent()`が別の構文に変換されてしまうと、Nuxt3はそれを追跡できないから、でした。

では、tl;drに書いたことについてそれぞれ説明していきます。

# `git+https://github.com/ajiken4610/vite-plugin-javascript-obfuscator`
このレポは https://github.com/elmeet/vite-plugin-javascript-obfuscator からforkしたものです。
- javascript-obfuscatorのバージョンをlatestに
- vite plugin の `enforce:"post"`の削除

以上の2点を変更しています。

重要なのは2つ目で、`enforce:"post"`が指定されていると、その前後にほかのプラグインを挟むのが難しくなります。なので、削除する必要がありました。

# replacer.js
見やすいように、コメントを付けたものをもう一度貼っておきます。
```replacer.js
export default (matched) => {
  return {
    name: "vite-plugin-replace-resolve-component",
    transform(src, id) {
      // 引数をそのまま変更するのは嫌だったので、変数に移す。
      let ret = src;
      ret = ret.replaceAll(
        // この正規表現については後述
        /(?<!function )(_?resolveComponent)\(["']([^\)]+?)["']\)/g,
        (_, resolve, name) => {
          // ___?resolveComponentSomeComponentNameに置換。
          const ret = `__${resolve}${name}`;
          // obfuscatorに無視してもらうために、配列に追加
          matched.indexOf(ret) === -1 && matched.push(ret);
          // 文字列として置換するために、二重引用符を付ける。
          return `"${ret}"`;
        },
      );
      return {
        code: ret,
        map: null,
      };
    },
  };
};
```
## 引数`matched`
どうにかしてobfuscatorに見つかったコンポーネントを置換した文字列を無視してもらう必要があります。
なので、matchedという`string[]`を引数にとり、それをポインタとして使うことで、解決しました。
## 正規表現
前述のNuxt3のresolveComponentを見つける正規表現を見ると、`_resolveComponent`のように、前に`_`がついている場合があるようです。それはそのまま戻さなくてはならないので、
- `resolveComponent("SomeName")`なら`"__resolveComponentSomeName"`に、
- `_resolveComponent("SomeName")`なら`"___resolveComponentSomeName"`というように、

前につけるアンダーバーの数で区別するようにしています。

この説明の後にこれを見ると、`(?<!function )`がついているのがわかります。
```js
ret.replaceAll(/(?<!function )(_?resolveComponent)\(["']([^\)]+?)["']\)/g,...)
```
なぜこんなものがついているかというと、ソースコード中のresolveComponentの定義部分は置換する必要がない、というより置換してしまってはエラーになるからです。

どういうことかは以下を見てもらえばわかります。

### 正しい例
```js
_resolveComponent("NuxtLink")
```
が、以下に置換される
```
"___resolveComponentNuxtLink"
```
### おかしい例
```js
function _resolveComponent("NuxtLink"){ ... }
```
が、以下に置換される
```js
function "___resolveComponentNuxtLink"{ ... }
```

...?

あれれ、おかしい例はもともと文法的にあり得なかった。

関数定義時って引数を二重引用符でくくらないですよね。

`(?<!function )`無しでも問題ないかもしれませんが、試してません。動いてればそれでいいのだ。

何はともあれ、これで文字列に置換することができました。

# nuxt.config.ts
あとはこれをobfuscatorの引数に渡してやります。
```ts:nuxt.config.ts
const reservedStrings: string[] = []
...
vite: {
      plugins: [
        replacer(reservedStrings),
        obfuscator({
          options: {
            optionsPreset: "high-obfuscation",
            reservedStrings,
          },
          apply: "build",
        }),
        surrounder(),
      ],
```
これに関しては説明することは無いです。`reservedStrings`を定義して、それを`replacer`と`obfuscator`に渡しているだけですので。

# surrounder.js
コメントを付けたものを再掲。
```surrounder.js
export default () => {
  return {
    name: "vite-plugin-surround-resolve-component",
    transform(src) {
      // 引数に代入するのは以下略
      let ret = src;
      ret = ret.replaceAll(
        // 後述
        /["']__(_?resolveComponent)([^"']+)["']/g,
        // ___?resolveComponentSomeComponentNameを
        // (_?resolveComponent("SomeComponentname"))にしているだけです。
        // 先に書いた、Nuxt3の実装では、これがかっこでくくられていないと置換してくれません。
        (_, resolve, name) => `(${resolve}("${name}"))`,
      );
      return {
        code: ret,
        map: null,
      };
    },
  };
};

```
## 正規表現
```js
ret.replaceAll(/["']__(_?resolveComponent)([^"']+)["']/g,...)
```
ポイントは以下の通りです。
- `["']`: obfuscatorは二重引用符にするか一重にするか不明だったので、どちらにもマッチするようにしています。
- `_?`: アンダーバーの数を見ています。2個なら`resolveComponent`になるし、3個なら`_resolveComponent`になります。

`___resolveComponentComponentName`を`(_resolveComponent("ComponentName"))`に置換しているだけです。

# まとめ
resolveComponentが別の構文に変換されないように、以下の手順が必要
1. `_?resolveComponent(...)`を元に戻せる文字列に置換する
1. obfuscatorにその文字列以外をobfuscateしてもらう
1. 置換した文字列を、`(_?resolveComponent("..."))`に戻す

この手順でコンポーネントが無事resolveされました。

参考までに。

# 追記
javascript-obfuscatiorは`import.meta.XXX`を置換できないようなので、hot-module-replacementを使うと`import.meta.hot`にアクセスしようとしておかしくなります。dev時にはobfuscatorをオフにしてください。