[Deno/TypeScript] コマンドラインスクリプトのテンプレート(サブコマンド対応)

・コマンドライン スクリプトのテンプレート
・単一の.tsファイル
・サブコマンド対応
・環境
 deno 2.7.10 (stable, release, x86_64-pc-windows-msvc)
  v8 14.7.173.7-rusty
  typescript 5.9.2
・ダウンロード
#!/usr/bin/env -S deno run --allow-all
/**
 * 汎用コマンドラインツールのテンプレート - fuzzyutilsTS
 *
 * ### 提供コマンド:
 * - `renumber`: 指定したディレクトリ内の画像を連番で一括リネームする。
 *
 * ### 使用例:
 * ```bash
 * deno run -A fuzzyutilsTS.ts renumber --prefix "vacation_" --start 1
 * ```
 *
 * Deno 2.7.10 / TypeScript 準拠
 * @module
 */
import { parseArgs } from "jsr:@std/cli@^1.0.28/parse-args";
import * as path from "jsr:@std/path@^1.1.4";

const PROG_VERSION = "2026.04.06";


/**
 * ファイル操作を抽象化するインターフェース。
 *
 * @remark
 * テスト時にモックに差し替えるために export 指定。
 * @internal
 */
export interface FileSystem {
  readDir(path: string): AsyncIterable<Deno.DirEntry>;
  rename(oldPath: string, newPath: string): Promise<void>;
  exists(path: string): Promise<boolean>;
}

/**
 * 実際の OS ファイルシステムを使用する {@link FileSystem} の本番用実装。
 *
 * @remarks
 * `Deno.readDir` や `Deno.rename` などの標準 API のラップ。
 * 通常はデフォルトで使用し、テスト時はテスト用のモックを用意する。
 */
const RealFS: FileSystem = {
  readDir: Deno.readDir,
  rename: Deno.rename,

  // 標準ライブラリのexistsは使うまでもないので自前で実装
  exists: async (path) => {
    try {
      await Deno.stat(path);
      return true;
    }
    catch {
      return false;
    }
  },
};

/**
 * ファイル内部でのみ使用する処理結果を管理する型。
 * 成功、論理エラー(理由)、例外(Error)の3つの状態を厳格に区別する。
 * @internal
 */
export type InternalResult<T> =
  | { success: true;  value?: T;     reason?: never;  error?:never }
  | { success: false; value?: never; reason:  string; error?:never }
  | { success: false; value?: never; reason?: string; error: Error };

/**
 * コマンドの実行結果を表す型定義。
 *
 * `success` プロパティの値によって、成功時と失敗時の詳細な型が切り替わる
 * 判別共用体 (Discriminated Union) として定義。
 */
export type CommandResult =
  | { success: true; code: 0; message?: string }
  | { success: false; code: number; message: string; error?: Error };

/**
 * 全てのサブコマンドの基底となる抽象クラス。
 *
 * 引数の解析(Template Methodパターンの構成)と実行フローを制御する。
 * 継承先では、具体的なオプション型 `T` を定義し、解析と実行のロジックを
 * 実装すること。
 *
 * @template T コマンドが使用する解析済みオプションの型
 */
abstract class BaseCommand<T> {
  /**
   * コマンドの識別名(例: "renumber", "copy")。
   * コマンドラインでサブコマンドとして指定される文字列になる。
   */
  abstract readonly name: string;

  /**
   * ヘルプ画面に表示されるコマンドの簡潔な説明文。
   */
  abstract readonly description: string;

  /**
   * コンストラクター
   *
   * @param fs - 使用するファイルシステム操作の実装。
   * デフォルトは `RealFS`(Deno 標準 API を使用する実体)を使用。
   * 単体テスト時には、ここへモックオブジェクトを注入することで、
   * 実際のファイルシステムを汚さずにロジックを検証できる。
   * @example
   * // 本番用(通常はこちらを使用)
   * const cmd = new ReNumberCommand();
   * @example
   * // テスト用(ディレクトリ内のファイル一覧を偽装する)
   * const mockFS: FileSystem = {
   * readDir: async function* () { yield { name: "file1.txt", isFile: true, isDirectory: false, isSymlink: false }; },
   * exists: async () => false,
   * rename: async (s, d) => console.log(`Moved: ${s} -> ${d}`),
   * };
   * const cmd = new ReNumberCommand(mockFS);
   */
  constructor(protected fs: FileSystem = RealFS) {}

  /**
   * コマンド固有のヘルプ情報(使用法、オプションの詳細など)を
   * 標準出力に表示する。
   */
  abstract showHelp(): void;

  /**
   * コマンドライン引数の配列を解析し、型 `T` のオブジェクトに変換する。
   *
   * @param args サブコマンド名を除いた、解析対象の引数リスト
   * @returns 解析・構造化されたオプションオブジェクト
   * @protected
   */
  protected abstract parseOptions(args: string[]): T;

  /**
   * コマンドの具体的な実行ロジックを実装する。
   *
   * @param options `parseOptions` によって解析されたオプションオブジェクト
   * @returns 実行結果(成功・失敗のステータスとメッセージ)を含むPromise
   * @protected
   */
  protected abstract runImpl(options: T): Promise<CommandResult>;

  /**
   * コマンドの実行エントリポイント。
   *
   * 引数の解析 (`parseOptions`) を行い、その結果を元に実行 (`runImpl`) を
   * 呼び出す。
   *
   * @param args コマンドラインから渡された未解析の引数リスト
   * @returns コマンドの実行結果を保持するオブジェクト
   */
  async run(args: string[]): Promise<CommandResult> {
    const subArgs = parseArgs(args, {
      boolean: ["help"],
      alias: { h: "help" },
    });
    if (args.length === 0 || subArgs.help) {
      this.showHelp();

      return { success: true, code: 0 };
    }

    const options = this.parseOptions(args);
    return await this.runImpl(options);
  }
}


// ==================================================================
//  SubCommand: renumber (連番リネーム)
// ==================================================================

/**
 * `renumber` コマンドで使用するパース済みオプション
 */
export interface ReNumberOptions {
  /** 出力ファイル名の先頭に付与する文字列 */
  prefix: string;

  /** 連番の開始番号 */
  startNo: number;

  /** リネーム実施フラグ(trueで実際にリネームをする) */
  isFix: boolean;

  /** 最低桁数 */
  minDigits: number;
}

interface FileInfo {
  fullPath: string;
  baseName: string;
  ext: string;
}

interface RenamePlan {
  file: FileInfo;
  newName: string;
  isNew: boolean;
}

/**
 * ディレクトリ内の画像ファイルを連番で一括リネームするコマンド
 */
export class ReNumberCommand extends BaseCommand<ReNumberOptions> {
  readonly name = "renumber";
  readonly description = "画像ファイルを連番でリネームします。";

  /**
   * ヘルプ情報を標準出力。
   */
  showHelp(): void {
    console.log(`
      Description: ${this.description}
      Command: ${this.name}

Usage:
  deno run -A fuzzyutilsTS.ts ${this.name} [options]

Options:
  -e, --prefix <string>  出力ファイル名の接頭辞 (デフォルト: "")
  -n, --start <number>   開始番号 (デフォルト: 1)
  -d, --digits <number>  最低桁数 (デフォルト: 自動計算)
  --fix                  シミュレーションではなく実際にリネームを実行
  -h, --help             このヘルプを表示

FEATURES:
  - 自然順ソート: "file2.txt" が "file10.txt" より前に並びます。

CAUTION:
  - リネーム先に「対象外の同名ファイル」が存在する場合は
    上書き防止のため処理を中断します。`,
    );
  }

  /**
   * 引数を `ReNumberOptions` 型にパースする。
   *
   * @param rawArgs コマンドライン引数の配列
   * @returns パース済みのオプションオブジェクト
   */
  protected parseOptions(rawArgs: string[]): ReNumberOptions {
    const args = parseArgs(rawArgs, {
      string: ["e"],
      boolean: ["fix"],
      alias: {
        e: "prefix",
        n: "start",
        d: "digits",
      },
      default: {
        e: "",
        n: 1,
        d: 0, // 0は自動計算を意味する
        fix: false,
      },
    });

    return {
      prefix: String(args.e),
      startNo: Number(args.start ?? 1),
      minDigits: Number(args.d) || 0,
      isFix: Boolean(args.fix),
    };
  }

  /**
   * コマンドのメインロジックを実行する。
   *
   * @param rawArgs 引数配列
   * @returns 終了コード (0: 成功, 1: 衝突による中止)
   * @throws {Error} ファイルシステム操作に失敗した場合
   */
  async runImpl(options: ReNumberOptions): Promise<CommandResult> {
    const targetFiles = await this.collectFiles();
    if (targetFiles.length === 0) {
      return {
        success: true,
        code: 0,
        message: "対象となる画像ファイルが見つかりませんでした。",
      };
    }

    console.log(`対象ファイル: ${targetFiles.length} 件`);

    const autoDigits = String(targetFiles.length + options.startNo - 1).length;
    const digits = Math.max(autoDigits, options.minDigits);

    const renamePlan = this.createRenamePlan(
      targetFiles,
      options.prefix,
      options.startNo,
      digits,
    );

    let res = await this.validatePlan(renamePlan, targetFiles);
    if (!res.success) {
      return {
        success: false,
        code: 1,
        message: res.reason || "安全のため処理を中断しました。",
        error: res.error,
      };
    }

    res = await this.executeRename(renamePlan, options.isFix);
    if (!res.success) {
      if (res.reason) console.error(res.reason);

      return {
        success: false,
        code: 1,
        message: res.reason || "実行エラーが発生しました。",
        error: res.error,
      };
    }

    if (!options.isFix) {
      console.log(
        "\n【シミュレーション】" +
        " 実際に変更するには --fix を指定してください",
      );
    }

    return { success: true, code: 0 };
  }

  /**
   * 処理対象とする拡張子のリスト。
   * 全て小文字、ドット付きで定義。
   */
  private static readonly TARGET_EXTS: readonly string[] = [
    ".jpg",
    ".png",
    ".webp",
    ".bmp",
    ".jpeg",
    ".gif",
  ];

  /**
   * カレントディレクトリから対象ファイルを収集し、自然順でソートし取得する。
   *
   * @returns ソート済みのファイル情報(FileInfo)の配列
   *
   * @remarks
   * - 拡張子の判定は大文字小文字を区別しない。
   * - ソートは「自然順(数値順)」かつ
   *   「大文字小文字・全角半角を区別する」設定で行う。
   */
  private async collectFiles(): Promise<FileInfo[]> {
    const files: FileInfo[] = [];

    for await (const entry of Deno.readDir(".")) {
      if (!entry.isFile) continue;

      const { name: basename, ext } = path.parse(entry.name);
      if (ReNumberCommand.TARGET_EXTS.includes(ext.toLowerCase())) {
        files.push({
          fullPath: entry.name,
          ext: ext,
          baseName: basename,
        });
      }
    }

    const collator = new Intl.Collator("ja", {
      numeric: true,
      sensitivity: "variant",
    });
    files.sort((a, b) => collator.compare(a.baseName, b.baseName));

    return files;
  }

  /**
   * 対象ファイル群に対して、新しいファイル名を含むリネーム計画を作成する。
   *
   * @param targetFiles - リネーム対象となるファイルの情報の配列
   * @param prefix - ファイル名の先頭に付与する文字列
   * @param startNo - 連番の開始番号
   * @param digits - 連番部分の最小桁数(足りない場合は `0` で埋められる)
   * @returns 各ファイルの新旧名と変更の有無を含む計画オブジェクトの配列
   *
   * @remarks
   * - 実際のファイル名は元の拡張子の大小文字を維持する。
   * - 変更の有無判定においては、ベース名は厳密に比較するが、
   *   拡張子のみ大文字小文字の違いを無視して比較を行う。
   * - 例:`Photo.JPG` から `Photo.jpg` への変更は「変更なし」と判定する。
   */
  private createRenamePlan(
    targetFiles: FileInfo[],
    prefix: string,
    startNo: number,
    digits: number,
  ) {
    return targetFiles.map((file, index) => {
      const numPart = String(startNo + index).padStart(digits, "0");
      const newName = `${prefix}${numPart}${file.ext}`;
      const isNew = file.fullPath !== newName;

      return {
        file,
        newName,
        isNew,
      };
    });
  }

  /**
   * 作成されたリネーム計画に衝突や重複がないか、実行前に一括検証する。
   *
   * @param renamePlan - 検証対象となるリネーム計画の配列
   * @param allTargets - 今回のリネーム処理の全対象ファイル情報の配列
   * @returns 計画が安全で実行可能な場合は true、衝突が検知された場合は false
   *
   * @remarks
   * 以下の 2 点をチェックし、データの安全性を担保する:
   *
   * 1. **リネーム予定名同士の重複**:
   *    計画内で複数のファイルが同じ名前になろうとしていないかを確認する。
   *    判定は大文字小文字を区別せずに行う。
   *
   * 2. **外部ファイルとの衝突**:
   *    リネーム先のパスに既にファイルが存在する場合、それが「今回移動して場所を
   *    空ける予定のファイル」であるかを確認する。
   *    今回の対象外である(居座り続ける)ファイルと衝突する場合は、上書き事故を
   *    防ぐため不許可とする。
   */
  private async validatePlan(
    renamePlan: RenamePlan[],
    allTargets: FileInfo[],
  ): Promise<InternalResult<number>> {
    const originalCount = allTargets.length;
    const newNamesSet = new Set<string>();

    for (const plan of renamePlan) {
      newNamesSet.add(plan.newName.toLowerCase());
    }

    if (newNamesSet.size !== originalCount) {
      const lostCount = originalCount - newNamesSet.size;

      return {
        success: false,
        reason: "中止: ファイル名が重複しています。" +
          "\nこのまま実行すると、上書きにより " +
          `${lostCount} 個のファイルが消失します。`,
      };
    }

    for (const plan of renamePlan) {
      if (!plan.isNew) continue;

      if (await this.fs.exists(plan.newName)) {
        const isWillBeMoved = allTargets.some((f) =>
          f.fullPath.toLowerCase() === plan.newName.toLowerCase()
        );
        if (!isWillBeMoved) {
          return {
            success: false,
            reason: "中止: 既存の別ファイルと衝突します: " +
              `${plan.newName}`,
          };
        }
      }
    }

    return { success: true };
  }

  /**
   * リネーム計画に基づき、実際のファイル移動処理を実行する。
   *
   * @param renamePlan - 作成されたリネーム計画の配列
   * @param isFix - true の場合は実際にリネームを実行し、false の場合は表示のみ行う
   * @returns 全てのリネームが正常に完了した場合は true、途中で失敗した場合は false
   *
   * @remarks
   * - **玉突き衝突の動的回避**:
   *   リネームによる移動の向き(前方または後方)を自動判定し、処理順序を最適化
   *   することで「移動先にまだ古いファイルが存在する」ことによる上書き消失や
   *   エラーを防ぐ。
   *
   * - **後方移動 (例: 1→2)**:
   *   配列を逆順(後ろ)から処理し、末尾から空き地を作る。
   *
   * - **前方移動 (例: 2→1)**:
   *   配列を正順(前)から処理し、先頭から空き地を作る。
   *
   * - **二段構えの安全性**:
   *   1. `validatePlan` による事前の全数・重複チェック。
   *   2. `Deno.rename` 直前の `exists` による最終衝突確認。
   *
   * - **エラーハンドリング**:
   *   いずれかのリネームに失敗した場合、それ以上の被害(ファイル消失や不整合)
   *   を防ぐため即座に処理を中断し `false` を返す。
   */
  private async executeRename(
    renamePlan: RenamePlan[],
    isFix: boolean,
  ): Promise<InternalResult<number>> {
    if (renamePlan.length === 0) return { success: true };


    // 最初のファイルが「手前」に行くか「後ろ」に行くかを比較
    // localeCompare で 新名 > 旧名 なら「後ろへ移動(逆順が必要)」と判断
    const first = renamePlan[0];
    const isMovingBackward = first.newName.localeCompare(
      first.file.fullPath,
      undefined,
      { numeric: true },
    ) > 0;

    // 向きに合わせてインデックスの並びを作成
    const indices = renamePlan.map((_, i) => i);
    if (isMovingBackward) {
      indices.reverse(); // 番号が増えるリネームなら後ろから(逆順)
    }

    for (const i of indices) {
      const { file, newName, isNew } = renamePlan[i];

      const status = isNew ? "" : " (変更なし)";
      const logPrefix = isFix ? "" : "Simulation:";
      console.log(`${logPrefix} ${file.fullPath} -> ${newName}${status}`);

      if (!isNew || !isFix) continue;

      try {
        // 最終確認
        if (await this.fs.exists(newName)) {
          return {
            success: false,
            reason: `移動先 "${newName}" が既に存在します。`,
          };
        }

        await this.fs.rename(file.fullPath, newName);
      }
      catch (error) {
        return {
          success: false,
          reason: `${file.fullPath} のリネームに失敗しました。`,
          error: error instanceof Error ? error : new Error(String(error)),
        };
      }
    }

    return { success: true };
  }
}


// ==================================================================
//  メイン処理
// ==================================================================

/**
 * ツール全体のライフサイクルとコマンド実行を管理するメインクラス
 */
class Main {
  private commands: Map<string, BaseCommand<unknown>> = new Map();

  constructor() {
    const subCommands: BaseCommand<unknown>[] = [
      new ReNumberCommand(),
      // -----------------------------------------------------------
      // TODO: **** ここにサブコマンドを追記していく ****
      // -----------------------------------------------------------
    ];
    for (const cmd of subCommands) {
      this.registerCommand(cmd);
    }
  }

  /**
   * コマンドを内部レジストリに登録する。
   *
   * @param cmd 登録するコマンドインスタンス
   */
  private registerCommand(cmd: BaseCommand<unknown>): void {
    if (this.commands.has(cmd.name)) {
      console.warn(`Warning: Command "${cmd.name}" is already registered.`);
    }

    this.commands.set(cmd.name, cmd);
  }

  /**
   * ヘルプ情報を標準出力。
   *
   * @returns 常に0(正常終了コード)
   */
  private showHelp(): number {
    console.log(
      "Usage: deno run -A fuzzyutils.ts [command] [options]" +
      "\nCommands:",
    );

    for (const cmd of this.commands.values()) {
      console.log(`  ${cmd.name.padEnd(15)} : ${cmd.description}`);
    }

    return 0;
  }

  /**
   * アプリケーションのメインエントリーポイント。
   *
   * 引数を解析し、適切なサブコマンドを呼び出す。
   *
   * @returns プロセスの終了コード
   */
  async exec(): Promise<number> {
    const args = parseArgs(Deno.args, {
      stopEarly: true, // 最初の引数をサブコマンドとして扱う
      boolean: ["help", "version"],
      alias: { h: "help", v: "version" },
    });

    if (args.version) {
      console.log(`${PROG_VERSION}`);
      return 0;
    }

    const [subCommandName, ...remainingArgs] = args._.map(String);
    if (args.help || !subCommandName) {
      return this.showHelp();
    }

    const command = this.commands.get(subCommandName);
    if (!command) {
      console.error(`Unknown command: ${subCommandName}`);
      return 1;
    }

    try {
      const result = await command.run(remainingArgs);
      if (result.success) {
        if (result.message) {
          console.log(`${result.message}`);
        }
      }
      else {
        console.error(`[Error] ${result.message}`);

        if (result.error) console.debug(result.error.stack);
      }

      return result.code;
    }
    // 想定外のエラー(権限不足やディスクフルなど)
    catch (error) {
      const msg = error instanceof Error ? error.message : String(error);
      console.error(`[Fatal Error] ${command.name}: ${msg}`);

      return 1;
    }
  }
}

/**
 * 直接実行された場合のみアプリケーションを起動
 */
if (import.meta.main) {
  const exitCode = await new Main().exec();
  Deno.exit(exitCode);
}

[Deno] 開発環境構築 例 VSCode

1.インストール
・公式サイト https://deno.com/
・Deno自体は単一ファイル 約128MB
   deno 2.7.9 (stable, release, x86_64-pc-windows-msvc)
   v8 14.7.173.7-rusty
   typescript 5.9.2
・TypeScript/JavaScriptの開発ツール+実行環境+配布ツールというオールインワン
・Node.jsを作った人がNode.jsの設計ミスを悔やんで新しく作った

# Denoのインストール(システムグローバル)
## Linux / macOS
$ curl -fsSL https://deno.com/install.sh | sh

## Windows PowerShell
$ iwr https://deno.com/install.ps1 -useb | iex

# バージョン確認
$ deno --version

# Deno自身にアップデートがあるか確認
$ deno upgrade --dry-run

# deno自体を最新版にアップグレード
$ deno upgrade

# 指定したバージョンのDenoを取得する
$ mkdir bin
$ deno upgrade --version 2.5.6 --output .\bin\deno-2.5.6.exe

※Deno自体は単一の実行可能ファイルなので余計なパスを生成しないzipファイルをDLしてもよい。
 https://github.com/denoland/deno/releases
※複数バージョンを利用したい場合は環境切り替えツールを使うか、実行可能ファイルをリネームして使う(deno2.1.exe deno2.2.exeなど)
※キャッシュ(外部ライブラリ)自体はバージョン毎に保持されている。
※2024年以降、外部ライブラリは https://deno.land/x/ ではなく JSR (jsr:) を使うのが標準。

2.実行
deno.json のタスクを使う(推奨)
deno.json に以下のように記述しプロジェクト内のDenoを意識せず実行する。
{
    // プロジェクト内にライブラリを実体化し、オフライン/持ち運びを可能にする
    "nodeModulesDir": "auto",
    "vendor": true,

    "tasks": {
        "check-update": "./bin/deno.exe outdated",
        "update": "./bin/deno.exe outdated --update",
        "backup": "./bin/deno.exe run --allow-read --allow-write --allow-run scripts/backup.ts",

        "backup": "./bin/deno.exe run --allow-all scripts/backup.ts",
            "build": "deno run --allow-all scripts/build.ts",

        "dev": "./bin/deno.exe run --allow-all --watch main.ts",
        "release": "./bin/deno.exe run --allow-write --allow-run scripts/build.ts ./main.ts myprog",

        "dev-2_5_6": "./bin/deno-2.5.6.exe run --allow-all --watch main.ts",
        "release-2_5_6": "./bin/deno-2.5.6.exe run --allow-write --allow-run scripts/build.ts ./main.ts myprog2.5"
        }
}
$ deno task dev # システムグローバルのDenoから起動する $ ./bin/deno task dev # プロジェクト内のDenoから起動する ※tasksではプロジェクト内のDenoを使う記述なのでどちらで起動しても同じ ※deno task (引数なし) を実行すると deno.json に登録されているタスク名と実行内容の一覧が表示される。 3.VSCodeで使う方法 ・プロジェクトディレクト内で完結する持ち運び可能な構成 ・Deno自体を格納する(./bin/deno.exe) ・プロジェクトで使う実行時に必要なDeno関連の追加モジュール(./vendor or ./node_modules) ☆必須拡張機能 Deno (公式)   拡張機能 ID: denoland.vscode-deno   提供元: denoland (Deno公式チーム)   役割: 型チェック、補完、フォーマット(整形)、リント(構文チェック)、デバッグ、テスト実行など、Deno開発に必要なすべての機能が含まれている。  初期化  SHIFT+CTRL+P で Deno: Initialize Workspace Configuration 3.1.設定ファイルの書き方 プロジェクトディレクトリ配下 .vscode/settings.json
{
    "deno.enable": true,
    "deno.lint": true,
    "deno.cacheOnSave": true,

    "deno.path": "./bin/deno.exe",
    "terminal.integrated.env.windows": {
        "PATH": "${workspaceFolder}\\bin;${env:PATH}"
    }
}
・settings.json の "deno.path" は単一のパスしか保持できない。 ・エディタの補完(IntelliSense)に使うDeno本体を切り替えたい場合は deno.path を手動で書き換える必要がある。 3.2.推奨されるディレクトリ構成の例 my-project/ ├── .vscode/ │ ├── settings.json VS Code拡張機能・ターミナルの設定 │ └── launch.json F5デバッグの設定 ├── vendor/ 【自動生成】ここにライブラリの実体が展開される ├── node_modules/ 【自動生成】ここにライブラリの実体が展開される ├── bin/ │ └── deno.exe 特定バージョンのDeno本体 ├── scripts/ │ ├── build.ts リリース物生成 │ └── backup.ts 全体バックアップ ├── dist/ 【自動生成】リリース用生成物 ├── src/ ソースコード階層 │ ├── auth/ │ │ ├── mod.ts パッケージの玄関口 │ │ └── login.ts 内部ロジック │ └── utils/ 共通ユーティリティ │ ├── mod.ts パッケージの玄関口 │ └── logger.ts 内部ロジック ├── .gitignore git非対象パスの指定 ├── deno.json プロジェクト設定・タスク定義 ├── deno.lock 【自動生成】依存関係のバージョン固定ファイル └── main.ts エントリーポイント 3.3.インポートは相対パスで main.ts から auth パッケージを呼び出す場合は以下のように書く。
import { login } from "./src/auth/mod.ts";
3.4.mod.ts という慣習 Deno(およびRustなど)の世界では、ディレクトリ内の機能を外部に公開するファイル名を mod.ts とするのが一般的。Pythonの __init__.py と同じ役割で、そのディレクトリ内の「玄関口」になる。 3.5.deno.json でのパス解決(インポートマップ) 階層が深くなって ./../../ と書くのが面倒な場合は deno.json にエイリアス(別名)を設定する。
{
    "tasks": { ... },   // ここの詳細は上記を参照
    "imports": {
        // 外部ライブラリ
        "@deno-library/compress": "jsr:@deno-library/compress@^0.5.6",
        "@std/datetime": "jsr:@std/datetime@^0.225.7",
        "@std/path": "jsr:@std/path@^1.1.4",

        // 内部パッケージ
        "@auth": "./src/auth/mod.ts"
    } 
}
こうすると、どこからでも import { authenticate } from "@auth"; で呼び出せるようになり、Pythonのパッケージインポートに近い感覚になる。 ※外部モジュールは手動書き換えではなく、ターミナルで $ deno add jsr:@std/path と打つ。  これにより、最新の安定版が deno.json に自動登録されスペルミスやバージョン不整合を防げる。 3.6.gitを使う場合 この構成をGitで管理する場合は vendor/ と node_modules/ は絶対に .gitignore に入れる。 理由: キャッシュにはダウンロードされた膨大な外部ライブラリが含まれるため、Gitのリポジトリが巨大化する。 推奨される .gitignore
vendor/
node_modules/
dist/
4.実装のイメージ 4.1.内部ロジック (src/auth/login.ts)
export function authenticate(user: string): string {
    return `${user} は認証されました`;
}
4.2.玄関口 (src/auth/mod.ts) Pythonの from .login import authenticate に相当する
// 内部の必要な機能だけを選んで再エクスポート(Re-export)する
export { authenticate } from "./login.ts";
4.3.利用側(main.ts) インポートマップで定義したエイリアス、または mod.ts を直接指定して機能を呼び出す。
import { authenticate } from "@auth";

console.log(authenticate("Deno太郎"));
4.4.Denoならではのポイント 明示的な指定: Denoでは ./src/auth のようにディレクトリ名で止めることはできず、必ず mod.ts などのファイル名と .ts 拡張子まで書くのが基本ルール。 カプセル化: login.ts に関数が多数あっても、mod.ts で export しない限り外部からは見えない。これによりパッケージの独立性を保てる。 5.デバッグ 5.1.F5でデバックするための設定 プロジェクトディレクトリ配下 .vscode/launch.json
{
    "version": "0.2.0",
    "configurations": [
        {
            "request": "launch",
            "name": "Launch Program",
            "type": "node",
            "program": "${workspaceFolder}/main.ts",
            "cwd": "${workspaceFolder}",
            "env": {},
            "runtimeExecutable": "${workspaceFolder}/bin/deno.exe",
            "runtimeArgs": [
                "run",
                "--unstable",
                "--inspect-wait",
                "--allow-all"
            ],
            "attachSimplePort": 9229
        }
    ]
}
設定のポイント 1.runtimeExecutable: ここにプロジェクト内の deno.exe をフルパス(変数を活用)で指定することで、システムのDenoではなく「そのプロジェクト専用のDeno」でデバッグが走る。 2.--inspect-wait: デバッガーが接続されるまでプログラムの実行を待機させるフラグ。これにより、最初の1行目からステップ実行が可能になる。 6.リリース 単一.tsファイルならそのままでもOK。 Windows以外の環境ならShebang(#!/usr/bin/env deno)が有効なので .ts ファイルで直接実行可能。 複数ファイル構造ならWindows環境を含めた配布を考えるなら deno compile によるバイナリ化が最適。 6.1.scripts/build.ts 実行可能モジュールとしてリリースする場合はコンパル用のバッチプログラムを作っておく。 ※Deno 2.x の compile は、npm: インポートを含むプロジェクトも単一バイナリに固めることができる。
import * as datetime from "@std/datetime";
import * as path from "@std/path";

// 1. 保存先ディレクトリの作成
const distDir = "./dist";
await Deno.mkdir(distDir, { recursive: true });

// 2. タイムスタンプ生成 (YYYYMMDD_hhmmss)
const now = new Date();
const timestamp = datetime.format(now, "yyyyMMdd_HHmmss");

// 3. 出力パスの組み立て
const extension = Deno.build.os === "windows" ? ".exe" : "";
const outputName = `myapp_v${timestamp}${extension}`;
const outputPath = path.join(distDir, outputName);

console.log(`Building to: ${outputPath}...`);

// 4. コンパイル実行
const command = new Deno.Command(Deno.execPath(), {
    args: [
        "compile",
        "--allow-all",
        "--output", outputPath,
        "main.ts"
    ],
});

const { success, stderr } = await command.output();

if(success) {
    console.log(`Successfully created: ${outputPath}`);
}
else {
    console.error("Compile failed:");
    console.error(new TextDecoder().decode(stderr));
}
7.プロジェクトのバックアップ (scripts/backup.ts) プロジェクト全体をZIPにまとめ、親ディレクトリへ退避させるスクリプト。 ※実行には最新の std/archive モジュールを利用する想定。 7.1.scripts/backup.ts の実装
/**
 * 使い方:
 * deno task build  
 * 例: deno task build src/main.ts my-app
 */
import * as datetime from "@std/datetime";
import * as path from "@std/path";

const [entryPoint, binName] = Deno.args;

if(!entryPoint || !binName) {
  console.error("Usage: deno task build  ");
  console.error("example: deno task build src/main.ts my-app");
  Deno.exit(1);
}

// 1. 保存先ディレクトリの作成
const distDir = "./dist";
await Deno.mkdir(distDir, { recursive: true });

// 2. タイムスタンプ生成 (YYYYMMDD_hhmmss)
const now = new Date();
const timestamp = datetime.format(now, "yyyyMMdd_HHmmss");

// 3. 出力パスの組み立て
const extension = Deno.build.os === "windows" ? ".exe" : "";
const outputName = `${binName}_v${timestamp}${extension}`;
const outputPath = path.join(distDir, outputName);

console.log(`Building to: ${outputPath}...`);

// 4. コンパイル実行
const command = new Deno.Command(Deno.execPath(), {
    args: [
        "compile",
        "--allow-all",
        "--output", outputPath,
        entryPoint
    ],
});

const { success, stderr } = await command.output();

if(success) {
    console.log(`Successfully created: ${outputPath}`);
}
else {
    console.error("Compile failed:");
    console.error(new TextDecoder().decode(stderr));
}