めちゃくちゃ沼った上に、解決方法がかなり複雑なので備忘録も兼ねて書いておきます。
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をオフにしてください。