secretlint 4.0.0をリリースしました。 secretlintはSSH private key, GCP Access token, AWS Access Token, Slack Token, npm auth tokenなどの機密情報のコミットを防いだり、ブラウザ拡張として動かしてサイト上に意図せず露出してしまっている情報を見つけるツールです。

ESLintのようにプラグイン構造を持っていて、ルールなどを自由に追加、実装できます。 ルールを追加したい場合は、次のドキュメントと、TypeScriptの型定義が@secretlint/typesで利用できるのでそれを参考にしてください。

今回のメジャーアップデートでは、このルールをNode.js 12+からネイティブでサポートされたECMAScript Modules(ESM)でかけるようになりました。 また、// secrelint-disableのようにコメントでLint結果を無視できるようになっています。

追加された機能

ESM rule support #187

SecretlintをESM(ECMAScript modules)として書いたものを読み込めるようになりました。 今までは、CommonJSに変換したものしか読み込めていませんでしたが、4.0.0からはexportをそのまま使ったモジュールも読み込めます。

ルールの作り方については次のドキュメントを参照してください。

今回の対応は、Secretlint本体をESMで書き直したのではなく、プラグインとなるルールを読み込むロジックを変更することで対応しています。

つまり、CommonJSからESMを読み込む方法を実装していて、具体的にはimport()を使うとCommonJSからESMを読み込めます。 しかし、現状のTypeScriptとNode.jsだと、このDynamic Importはいろいろな問題があります。

例えば、つぎのようにcjs(CommonJS)とmjs(ESM)のファイルを用意します。 export_transpiled.cjsは、BabelやTypeScriptなどでexport default 1;を変換したものです。

❯ cat export.cjs
module.exports = 1;

❯ cat export.mjs
export default 1;

❯ cat export_transpiled.cjs
Object.defineProperty(exports, '__esModule', { value: true })
module.exports.default = 1;

これを、index.cjs(CommonJS)からDynamic Importを使って読み込んでみると次のような結果になります。

import("./export.cjs").then(cjs => {
    console.log("cjs", cjs);
});
import("./export_transpiled.cjs").then(cjs => {
    console.log("cjs_transpiled", cjs);
});
import("./export.mjs").then(mjs => {
    console.log("mjs", mjs);
});

/*
mjs [Module: null prototype] { default: 1 }
cjs [Module: null prototype] { default: 1 }
cjs_transpiled [Module: null prototype] { __esModule: true, default: { default: 1 } }
*/

BabelやTypeScriptなどが使う__esModuleのinteropはNode.jsのネイティブESMでは機能していないことがわかります。 このexport default 1;のようにdefault exportされた値を取り出すには、次のような気持ち悪いコードが必要になります。

const mod = await import("./export_transpiled.cjs");
console.log(mod.default.default); // => export defaultされた値

Secretlintでは、読み込まれる側のモジュール = Secretlintルールをどう書くのかは、ユーザーによって異なるので制御できません。 このinteropの問題がずっと起きると大変なので、Secretlintルールではexport defaultではなく、export { creator } のようなnamed exportを使うように変更しました。

named exportなら、transpileされたコード/素のCommonJS/Native ESMでの扱い方にそこまで差がでないためです。

具体的には次のIssueにまとめてあります。

また、TypeScript 4.4では、module: CommonJSの場合にimport()が勝手にrequire()へと変換されてしまう問題があります。 これを回避するためには、eval的なコードを使ってimport()を評価する必要があります。

 const _importDynamic = new Function("modulePath", "return import(modulePath)"); 

これについては次期TypeScriptで改善される予定です。

TypeScriptでNative ESM対応はハマりどころが大変多いので、安定するまでにはも少し掛かりそうです。

Support secretlint-disable directive #195

@secretlint/secretlint-rule-filter-commentsというルールを追加して // secretlint-disable のようなコメントでのSecretlintを無効化/有効化できるようになりました。

このルールは@secretlint/secretlint-rule-preset-recommendに含まれているので、あまり意識せずに次のようにコメントで特定の行だけ、エラーを無視できるようになっています。

// secretlint-disable -- disable all rules

THIS IS SECRET A
THIS IS SECRET B
THIS IS SECRET C

// secretlint-enable -- enable again

// secretlint-disable-next-line @secretlint/secretlint-rule-secret-alphabet -- disable specific rule in next line
THIS IS SECRET D
THIS IS SECRET E // secretlint-disable-line -- disable current line

Secretlintでは//のようなコメントの記号かどうかは特に見ていないので、bashなら# secretlint-disableのように書いても機能します。

# secretlint-disable-next-line
echo "THIS IS SECRET, BUT IT WILL BE IGNORED"

詳しくはhttps://github.com/secretlint/secretlint/blob/master/docs/configuration.mdを参照してください。

📝 Secretlintではこのような無視コメントもルールとして実装しています。 具体的にはcontext.ignore()というAPIが用意されていて、これを扱うルールを書く形で実装しています。 特定のルールが特定のパターンを無視したいというケースも、ルール内に実装できる柔軟性を作っています。

コードは次の箇所にあります。

Breaking Changes

use export const creator instead of export default #190

先ほども書いていた、Dynamic ImportでTranspileされてCommonJSを読み込む際にexport defaultが大変扱いにくい問題です。 これをどうにかするために、Secretlintではルールでdefault exportを扱うのをやめて、named exportに変更しました。

幸いまだサードパーティルールはほぼないはずなので影響はないと思います。

もし、自分でSecretlintのルールを作っている場合は、exportする部分を次のように変更してください。

- export default creator;
+ export { creator }

Require Node.js 12 and update engines #193

Node.js 12が最小サポート環境となりました。 これは、Native ESMに対応するのがNode.js 12+からとなっているためです。

Secretlint自体はまだPure ESM packageではないですが、そのうち変換する気はします。

textlintでも同様のESMで書かれたルールを読み込めるサポートをしていきたいので、興味がある人はhttps://gitter.im/textlint-ja/textlint-jaあたりで話してたりするので、聞いてみてください。 (textlintはSecretlintより変更する必要がある箇所が多い予感がするので、色々手伝ってくれる人がほしい。まずは把握してIssue作るところから)

雑感

ESLintやtextlintなどプラグイン機構を持ったツールは多いと思いますが、こういった仕組みを持つツールは必ず非同期ロードをサポートしていないといけなくなっています。 現状では、CommonJS/ESMから動的にESMなプラグインをロードするにはDynamic Importを使うしかないため、非同期ロードを前提としたコードになってないと大変な感じがします。 プラグイン機構を作る予定がある人は、この辺を最初から考慮した設計にしておくとよさそうです。

Secretlintはtextlintの経験からモジュール化と非同期の対応が最初からほぼできていたので、@secretlint/config-loaderを変更するぐらいでできましたが、後からこれをやるのは結構大変そうです。

SecretlintでESMの対応が必要となったのは、pkgdeps/unverified-checksum-checkerというチェックサムをチェックしているかをチェックするツールを書いていて、Packemonを使ってESMなSecretlintルールを書いてみたらなんか動かなかったためです。

TypeScript + ESM + Node.jsはまだハマりどころがたくさんありますが、その辺の話はpkgdeps/unverified-checksum-checkerを公開するときにでも書きます。