Source: node-newer-files.js

#!/usr/bin/env node
"use strict";

import { statSync, accessSync, constants } from "fs";
import fs from "fs";
import { resolve } from "path";
import { sync as _globSync } from "glob";

/**
 * 対象ファイルリストを取得する
 * @private
 * @param extensions 拡張子の配列
 * @param cwd ソースディレクトリ
 * @returns {*}
 */
const getFileList = (extensions, cwd) => {
  const ext = "+(" + extensions.join("|") + ")";
  const pattern = `**/*.${ext}`;
  return _globSync(pattern, {
    cwd: cwd,
  });
};

/**
 * ファイルの更新が必要か否かを判定する
 * @private
 * @param filePath
 * @param srcDir
 * @param targetDir
 * @returns {boolean}
 */
const isNeedsUpdate = (filePath, srcDir, targetDir) => {
  const targetStats = getStats(filePath, targetDir);
  if (!targetStats) {
    return true;
  }

  //更新日比較 変更があったら更新対象
  const srcStats = getStats(filePath, srcDir);
  return srcStats.mtime > targetStats.mtime;
};

/**
 * fileの情報を取得する。fileが存在しない場合はnullを返す。
 * @private
 * @param filePath:string
 * @param targetDir:string
 * @returns {*}
 */
const getStats = (filePath, targetDir) => {
  try {
    const stats = statSync(targetDir + "/" + filePath);
    return stats;
  } catch (err) {
    return null;
  }
};

/**
 * 更新対象ファイルのリストを取得する。
 * @param extensions{string[]} 拡張子の配列
 * @param srcDir{string}
 * @param targetDir{string} 比較、保存対象ディレクトリ
 * @returns {Array} 更新対象ファイルリスト srcDirからの相対パス
 */
export function getFiles(extensions = [], srcDir, targetDir) {
  //ファイル拡張子が指定されていない場合は警告
  if (extensions.length === 0) {
    console.error(
      "node-newer-files.getFiles() : ファイル拡張子が指定されていません。空の配列を返します。",
    );
  }

  //srcディレクトリの存在確認
  if (!findSrcDir(srcDir, "getFiles")) {
    return [];
  }

  const fileList = getFileList(extensions, srcDir);
  const list = [];

  for (let file of fileList) {
    const update = isNeedsUpdate(file, srcDir, targetDir);

    if (!update) {
      continue;
    }
    list.push(file);
  }
  return list;
}

/**
 * ソースディレクトリの存在を確認する。
 * @private
 * @param srcDir
 * @param functionName
 * @returns {boolean}
 */
const findSrcDir = (srcDir, functionName) => {
  try {
    accessSync(srcDir, constants.R_OK);
    return true;
  } catch (err) {
    console.error(
      `node-newer-files.${functionName}() : srcディレクトリが存在しません。処理を中断します。`,
    );
    return false;
  }
};

/**
 * ファイルの同期を行う。
 * ソースディレクトリに存在しないファイルをターゲットディレクトリから削除する。
 * @param extensions{string[]}
 * @param srcDir{string}
 * @param targetDir{string}
 * @returns {Array} 削除されたファイルのパス配列
 */
export function sync(extensions, srcDir, targetDir) {
  //srcディレクトリの存在確認
  if (!findSrcDir(srcDir, "sync")) {
    return false;
  }

  if (!checkTargetDir(targetDir)) {
    return false;
  }

  const srcFiles = getFileList(extensions, srcDir);
  const targetFiles = getFileList(extensions, targetDir);
  const list = [];

  for (let target of targetFiles) {
    if (!srcFiles.includes(target)) {
      list.push(target);
      fs.unlink(resolve(targetDir, target), () => {});
    }
  }
  return list;
}

/**
 * ターゲットディレクトリの存在確認と、対象ディレクトリが不適切でないかの確認。
 * @private
 * @param targetDir
 * @returns {boolean}
 */
const checkTargetDir = (targetDir) => {
  //targetDirの存在確認
  try {
    accessSync(targetDir, constants.R_OK | constants.W_OK);
  } catch (err) {
    console.error(
      "node-newer-files.sync() : targetディレクトリが存在しないか、パーミッションがありません。処理を中断します。",
    );
    return false;
  }

  //targetDirはルートディレクトリを指定するのは禁止
  if (resolve() === resolve(targetDir) || targetDir === "/") {
    console.error(
      "node-newer-files.sync() : targetにプロジェクトルートが指定されています。ファイルの保護のため処理を中断します。",
    );
    return false;
  }
  return true;
};