めちゃくちゃ沼った上に、解決方法がかなり複雑なので備忘録も兼ねて書いておきます。
tl;dr
npm i git+https://github.com/ajiken4610/vite-plugin-javascript-obfuscator
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,
};
},
};
};
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,
};
},
};
};
const reservedStrings: string[] = []
...
vite: {
plugins: [
replacer(reservedStrings),
obfuscator({
options: {
optionsPreset: "high-obfuscation",
reservedStrings,
},
apply: "build",
}),
surrounder(),
],
注:build.minify === false && selfDefending === true
だと、ブラウザがフリーズします。
何をしているか
やっていることは意外と単純で、
-
resolveComponent("ComponentName")
を"__resolveComponentComponentName"
という文字列に置換する - obfuscatorに、
"__resolveComponentComponentName"
以外をobfuscateしてもらう。 -
"__resolveComponentComponentName"
を(resolveComponent("ComponentName"))
に置換し戻す。
をしています。
経緯
ちょっと難読化してみたかっただけだけど、思ってた以上にここにたどり着くまでが長かった。
この処理をしないとどうなるか
レンダリングすると、以下のようになります。
<div id="__nuxt">
<nuxtpage></nuxtpage>
</div>
私は、これを見ててっきりNuxtPageがTree-shakeされてしまったんだと思っていました。
実際、この状態のソースコードにはNuxtPageのものが含まれません。
そこで、私は、明示的にNuxt関係のコンポーネントをビルドに含めてみたりいろいろしてたのですが、23時間パソコンとぶっ通しでにらめっこしてようやく発見しました。
このソースを見てもらえばわかるのですが、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
見やすいように、コメントを付けたものをもう一度貼っておきます。
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 )
がついているのがわかります。
ret.replaceAll(/(?<!function )(_?resolveComponent)\(["']([^\)]+?)["']\)/g,...)
なぜこんなものがついているかというと、ソースコード中のresolveComponentの定義部分は置換する必要がない、というより置換してしまってはエラーになるからです。
どういうことかは以下を見てもらえばわかります。
正しい例
_resolveComponent("NuxtLink")
が、以下に置換される
"___resolveComponentNuxtLink"
おかしい例
function _resolveComponent("NuxtLink"){ ... }
が、以下に置換される
function "___resolveComponentNuxtLink"{ ... }
...?
あれれ、おかしい例はもともと文法的にあり得なかった。
関数定義時って引数を二重引用符でくくらないですよね。
(?<!function )
無しでも問題ないかもしれませんが、試してません。動いてればそれでいいのだ。
何はともあれ、これで文字列に置換することができました。
nuxt.config.ts
あとはこれをobfuscatorの引数に渡してやります。
const reservedStrings: string[] = []
...
vite: {
plugins: [
replacer(reservedStrings),
obfuscator({
options: {
optionsPreset: "high-obfuscation",
reservedStrings,
},
apply: "build",
}),
surrounder(),
],
これに関しては説明することは無いです。reservedStrings
を定義して、それをreplacer
とobfuscator
に渡しているだけですので。
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,
};
},
};
};
正規表現
ret.replaceAll(/["']__(_?resolveComponent)([^"']+)["']/g,...)
ポイントは以下の通りです。
-
["']
: obfuscatorは二重引用符にするか一重にするか不明だったので、どちらにもマッチするようにしています。 -
_?
: アンダーバーの数を見ています。2個ならresolveComponent
になるし、3個なら_resolveComponent
になります。
___resolveComponentComponentName
を(_resolveComponent("ComponentName"))
に置換しているだけです。
まとめ
resolveComponentが別の構文に変換されないように、以下の手順が必要
-
_?resolveComponent(...)
を元に戻せる文字列に置換する - obfuscatorにその文字列以外をobfuscateしてもらう
- 置換した文字列を、
(_?resolveComponent("..."))
に戻す
この手順でコンポーネントが無事resolveされました。
参考までに。
追記
javascript-obfuscatiorはimport.meta.XXX
を置換できないようなので、hot-module-replacementを使うとimport.meta.hot
にアクセスしようとしておかしくなります。dev時にはobfuscatorをオフにしてください。