npm パッケージをインストールしながら動く単一 Node.js スクリプトファイルを作る
何を言っているのかというと。
npm installでパッケージをインストールして使いたいスクリプトを書いている- その環境で予め
npm installコマンドを叩いておくのではなく、必要に応じて自動的にパッケージをローカルインストールしてほしい package.json不要で動く仕組みにしたい- スクリプト実行後は
node_modules/ディレクトリなどのゴミが残らないようにしたい
要するに Deno でスクリプトを書いた時のように、あたかも node_modules/ などが存在しないかのように動作する仕組みを考えてみた。
以下がそのコード全量。// Main で区切ったところが任意の処理で、const requires の中身が require() したいパッケージ情報を書くところ。
#!/usr/bin/env node
// Requires
const requires = [
{ variableName: 'Downloader', packageName: 'nodejs-file-downloader', version: '4.10.6' }
];
(async () => {
let isNpmInstalled = true;
let isExistPackageJson = true;
const loadRequires = () => requires.forEach(requireInfo => globalThis[requireInfo.variableName] = require(requireInfo.packageName));
try {
loadRequires();
} catch(_cannotFindModuleError) {
isNpmInstalled = false;
try {
require('fs').statSync(require('path').resolve(__dirname, './package.json'));
} catch(_noEntryError) {
isExistPackageJson = false;
}
// npm Install
requires.forEach(requireInfo => require('child_process').execSync(`npm install --save ${requireInfo.packageName}@${requireInfo.version}`));
// Re-Require
loadRequires();
}
console.log('Start');
// Main --------------------------------------------------
const urls = [
'https://example.com/index.html'
];
for(const url of urls) {
try {
const downloader = new Downloader({ url, directory: '.' });
const { filePath, downloadStatus } = await downloader.download();
console.log(url, 'OK', { filePath, downloadStatus });
} catch(error) {
console.log(url, 'NG', error);
}
}
// Main ==================================================
// Remove Assets
try {
if(!isNpmInstalled ) require('fs').rmSync(require('path').resolve(__dirname, './node_modules'), { recursive: true });
if(!isExistPackageJson) require('fs').rmSync(require('path').resolve(__dirname, './package.json'));
} catch(error) {
console.log('Unknown Error', error);
}
console.log('Finished');
})();
やっていることは愚直で、require() しようとしてエラーが出たら、npm install コマンドを実行してから再度 require() する、というモノ。
npm install コマンドをいきなり実行すると、package.json がなければ自動的に生成される仕組みになっているので、処理が終わったら必要に応じて node_modules/ と package.json を削除している。
require() した内容は globalThis を利用して動的にグローバル変数として定義している。このやり方だと VSCode 内での型推論や入力補完が効かないのが難点か。
ちなみに内部で使っている nodejs-file-downloader というパッケージは、wget みたいなモノ。どうも wget だと User-Agent の関係かうまく DL できないファイルがあって、このパッケージを通せばうまくダウンロードできた次第。
この書き捨てのスクリプトのために npm init して npm install して…というのが面倒臭かったので、単一の JS ファイルで実行できる仕組みが作れないかなーと思って、こんなオマジナイを生み出してみたワケ。おかげさまでこのファイルを $ node index.js などと実行するだけで package.json と node_modules/ は一瞬しか作られず、ゴミが残らず作業ができる。
いや、素直に Deno で書けよ、という時代ではある。w