npm パッケージをインストールしながら動く単一 Node.js スクリプトファイルを作る

何を言っているのかというと。

要するに 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.jsonnode_modules/ は一瞬しか作られず、ゴミが残らず作業ができる。

いや、素直に Deno で書けよ、という時代ではある。w