■BASE64エンコード (JavaScript) ECMAScript 2024(ES2024)■typeとinterfaceの使い分けと使い方 どちらもほぼ同じように使えるが interfaceはオブジェクトやクラスの形状を定義し type(型のエイリアス)はinterfaceでは表現できない複雑な「型」を扱うのに使用する。 1.typeが扱える複雑な型 1.1.プリミティブ型(Primitive Type)// 文字列をバイナリ(Uint8Array)に変換 const bytes = new TextEncoder().encode("こんにちは"); // URLセーフなBase64(パディング = なし)への変換 const urlSafeBase64 = bytes.toBase64({ alphabet: "base64url", omitPadding: true });1.2.ユニオン型(Union Type)type UserId = string;1.3.タプル型(Tuple Type) 実際はだたの配列(Array) オブジェクト表記でデータを持つより軽い。 可読性が下がらない場所か最適化が必要な場所で使う。 push等による型崩壊を防ぐため、原則 `readonly` を付与する。type Status = "idle" | "loading" | "success" | "error";1.4.マップドタイプ(Mapped Types)などの高度な型操作type Point = readonly [x: number, y: number]; // 使用例 const path: Point[] = [ [0, 0], [10, 20], [35, 40], ]; for (let i = 0; i < path.length; i++) { const [x, y] = path[i]; console.log(`X: ${x}, Y: ${y}`); } // 使用例2 function getCenter(width: number, height: number): Point { return [width / 2, height / 2]; } const [centerX, centerY] = getCenter(1920, 1080);1.5.マップドタイプの応用 型引数(ジェネリクス)を用いた条件付き型による再帰的な定義 - infer(型の自動推論・抽出) 条件付き型(extends ? :)の右側で使い 型の一部分を「変数」として型抜きする機能type ReadOnly<T> = { readonly [P in keyof T]: T[P]; };1.6.判別共用体型(Discriminated Union Type) 共通するリテラル型のプロパティが「判別子」として扱われる。 判別子は複数可能。type DeepReadonly<T> = T extends Function ? T : T extends Map<infer K, infer V> ? ReadonlyMap<DeepReadonly<K>, DeepReadonly<V>> : T extends Set<infer U> ? ReadonlySet<DeepReadonly<U>> : T extends Array<infer E> ? ReadonlyArray<DeepReadonly<E>> : T extends object ? { readonly [P in keyof T]: DeepReadonly<T[P]> } : T; ※上記は標準では用意されていない自前の深層完全不変の定義。 内部全てが不変(readonly)になる。はず。2.継承・合成のやり方 2.1.interface同士(extends)interface NetworkSuccess { status: "success"; // 判別子(Discriminant ) data: string[]; } interface NetworkError { status: "error"; // 判別子(Discriminant ) error: Error; } // 判別共用体型を type で合成 type NetworkResponse = NetworkSuccess | NetworkError; or type NetworkResponse = | { status: "success"; data: string[] } | { status: "error"; error: Error }; // 使用例 function handleResponse(res: NetworkResponse) { // res.status の値によって、コンパイラが型を確定させる if (res.status === "success") { console.log(res.data); // ここで data にアクセス可能 } else { console.error(res.error.message); // ここで error にアクセス可能 } }2.2.type同士(& インターセクション)interface Animal { name: string; } interface Dog extends Animal { bark(): void; }2.3.interfaceとtypeの相互合成type Flyable = { fly(): void; }; type Swimmable = { swim(): void; }; type Duck = Flyable & Swimmable; type Duck2 = Duck & { sleep(): void; };3.関数の定義 3.1.type での定義:関数型リテラル (Function Type Literal) 通常の「純粋な関数」はこれ一択。// 1. interface が type を extends する type Identifiable = { readonly id: string; }; interface Product extends Identifiable { price: number; } // 2. type が interface を & で合成する interface Serializable { serialize(): string; } type Config = Serializable & { version: string; }; ※同名プロパティが存在する場合はエラーになるがそれぞれタイミングが異なる。3.2.interface での定義:コールシグネチャ (Call Signature) 関数であり、かつプロパティも持つハイブリッド型を作る時に使う。type LogFn = (message: string) => void; const logger1: LogFn = (msg) => console.log(msg);結論:定義の仕方は違っても呼び出し側のコードは `logger("msg")` で完全に同じ。 違いは「型を定義する側」が、関数にプロパティを持たせたいかどうかだけ。 3.3.オブジェクトのプロパティとしての定義 メソッドシグネチャ (Method Signature) 関数単体ではなく、「オブジェクトが持つ関数(メソッド)」の形状を定義する構文interface LogInterface { (message: string): void; // コールシグネチャ level: number; // プロパティ } const logger2: LogInterface = Object.assign( (msg: string) => { console.log(`[Level ${logger2.level}] ${msg}`); }, { level: 3 } );■for文について (JavaScript)interface Drawer { draw(): void; // 「メソッドシグネチャ」 }■軽量Enum 1. 名称と内部値を列挙したオブジェクトを作る(末尾の as const が必須) 2. 同名で内部値のユニオン型を作る(0 | 1 | 2 | 3) 3. 【任意】名称のユニオン型を作る("SUCCESS" | "WARNING" ...) - マップ型のキー定義として使える。const items = ["a", "b", "c"]; const itemObj = { a: 1, b:2, c:3 }; 文法 [1] for (let i = 0; i < items.length; i++) { xxxx } [2] for (const item of items) { xxxx } [3] for (const [i, item] of items.entries()) { xxxx } [4] items.forEach((item, i) => { xxxx }); [5] for (const [key, value] of Object.entries(itemObj)) { xxxx } 解説 [1]: 最速・最軽量 [2]: 標準 [3 - 4]: 他言語とは違いCPUとメモリとGCに負荷をかけるので高負荷領域では使用NG。 [5]: 外界との境界(バリデーション、シリアライズ)でしか使用してはならない■マップ定数の作り方 1.ROLES: 原本リスト。 2.ROLE: ROLESから抽出したユニオン型。 3.ROLE_PERMISSIONS: ROLE型をキーとしたマッピング定数。 4.isRole: 型ガード関数(型述語タイプ) 実行時の型ガードはTypeScript世界のROLEでは行えないconst RequestStatus = { SUCCESS: 0, WARNING: 1, ERROR: 2, FATAL: 3, } as const; type RequestStatus = typeof RequestStatus[keyof typeof RequestStatus]; type RequestStatusKey = keyof typeof RequestStatus; // 使用例 function handleStatus(status: RequestStatus): void { if (status === RequestStatus.FATAL) { console.log("FATAL"); } }■関数の引数と戻り値の不変性(イミュータブル)表現方法const ROLES = ["admin", "manager", "user"] as const; type ROLE = typeof ROLES[number]; // --> "admin" | "manager" | "user" // const ROLE_PERMISSIONS: Readonly<Record<ROLE, readonly string[]>> = { // or const ROLE_PERMISSIONS: Readonly<{ [K in ROLE]: readonly string[] }> = { "admin": ["Read", "Write", "Delete", "Execute"], "manager": ["Read", "Write", "Delete"], "user": ["Read"], }; function isRole(value: string): value is ROLE { return (ROLES as readonly string[]).includes(value); } // 使用例 let inputData = "abc"; // inputDataはstring型 console.log(`'${inputData}' isRole: ${isRole(inputData)}`); inputData = "manager"; // inputDataはstring型 if( isRole(inputData) ) { // 型ガードを通過したのでinputDataはROLE型として扱われる const permisson = ROLE_PERMISSIONS[inputData]; console.log(`${inputData}: ${permisson}`); } // 使用例 console.log("-- loop"); // 型定義的にROLESはROLE_PERMISSIONSのキーリストでもある for (const role of ROLES) { const permisson = ROLE_PERMISSIONS[role]; console.log(`${role}: ${permisson}`); }■関数で名前付き引数風を扱うtype DeepReadonly<T> = T extends Function ? T : T extends Map<infer K, infer V> ? ReadonlyMap<DeepReadonly<K>, DeepReadonly<V>> : T extends Set<infer U> ? ReadonlySet<DeepReadonly<U>> : T extends Array<infer E> ? ReadonlyArray<DeepReadonly<E>> : T extends object ? { readonly [P in keyof T]: DeepReadonly<T[P]> } : T; function processData( a: User[], // [1] 可変: 配列の変更、Userの変更OK b: Readonly<User>[], // [2] 要素不変: 配列の変更OK、Userの変更NG c: readonly User[], // [3] 配列不変: 配列の変更NG、Userの変更OK d: readonly Readonly<User>[], // [4] 浅層完全不変: 配列の変更、User変更NG e: readonly DeepReadonly<User>[], // [5] 深層完全不変: [4] + User内部の全階層でNG ): User[] // [1]と同じ ): Readonly<User>[] // [2]と同じ ): readonly User[] // [3]と同じ ): readonly Readonly<User>[] // [4]と同じ ): readonly DeepReadonly<User>[] // [5]と同じ readonly - TypeScript 2.0 2016/09, 3.4 2019/03 Readonly - TypeScript 2.1 2016/12 Readonlyは直下のプロパティのみreadonlyとなる。 ネストしてる2層目以降は対象外。function splitTokens( value: string, { // デフォルト値 separator = ":", prefix = "", suffix = "", }: { // 型定義 separator?: string; prefix?: string; suffix?: string; } = {}, // 名前付き引数風を丸ごと省略可能にする ): string[] { // 使用例 splitTokens("txt:doc:pdf"); splitTokens("txt:doc:pdf", { prefix: "." });
[TypeScript] ちょっとしたこと。
[Deno/TypeScript] Geminiなど一般向けのAIで開発支援してもらう時の最低限のプロンプト
注意 ローカル/クローズド/特化型AIとは違い一般向けの汎用AIはネット上(特に英語圏)の「平均」を正解とする。 したがって、古い情報や間違った情報がネットに溢れているならAIは誤回答をする。 AIへの指示にはバージョン、版数、対象期間などの指定が必須。 重要 会話が続くとAIは忘れる(保持情報量の上限)ので定期的にAIプロンプトを投稿する必要がある。 ---- AIプロンプト大前提: Deno 2.7.14 TypeScript 5.9.2 必須情報元: Deno Docs https://docs.deno.com/runtime/ MDN https://developer.mozilla.org/ja/docs/Web/JavaScript TypeScript https://www.typescriptlang.org/ 役割定義: 貴方は、言語仕様とランタイムの特性を熟知し、コードの「一貫性」と「計算資源への敬意」を両立させるシニアエンジニアである。 - 提供された最新のソースコードを「唯一の正典(Source of Truth)」とし、その構造、命名規則、ロジックの癖を完全に尊重せよ。 ポリシー: 1. 計算機科学の基本(時間計算量 $O(1)$ の優先、不要なメモリアロケーションの排除)を最優先すること。 - ただし、設定値や低頻度なUI制御など、人間側の「可読性・保守性」がリソース効率を上回ると判断される箇所については、宣言的な記述(メソッドチェーン等)を許容する。 - 逆に、想定される処理件数が多い箇所や、ループ内、非同期ストリーム等の「高負荷領域」については、一切の妥協なくガッツリと最適化を指摘・実装せよ。 2. 破壊的変更(In-place)による最適化を許容する。データの不変性(Immutability)よりも、空間計算量の最小化を優先せよ。 3. $O(N)$ の隠れたループ(配列の全コピー等)が発生する場合は、必ずそのコストを明示し、回避策を検討せよ。 - メソッドチェーン(.map/.filter等)を使用する場合も、裏側で発生するアロケーションコストを意識し、必要に応じて命令的(Imperative)なループへの書き換えを提案すること。 4. 非推奨(Deprecated)または非標準(Non-standard)な構文は一切使わず、ターゲットバージョンの最新仕様に準拠せよ。 5. ソースコードやドキュメントで使われている各種名称を勝手に変えるのは厳禁。 6. 後方互換性は一切無視すること。 旧環境への配慮による冗長な実装を排除し、指定された実行環境において最も効率的なコードを記述せよ。 7. 修正・変更は「対象の箇所」のみに限定すること。指示に関係のない箇所のコード(変数名、命名規則、ロジック、コメント等)は一切変更・改変してはならない。 8. 修正および解析は、提供された「最新のコード」の状態に対してのみ行うこと。過去の推論や自身の記憶に基づいた古いコード構造を前提とした提案は禁止する。 9. 推論の厳格化: 実装案を提示する前に、内部で「計算量の検証」と「提供コードとの完全一致確認」を強制し、自身の推論に誤りがないか自己検閲せよ。 10.ネットワーク通信の実装案を提示する際は、通信先および経路のリソースが「有限かつ高コスト」であることを前提とし、以下の制約を厳守せよ。 10.1. ネットワーク応答時間を 0 と仮定したロジックを禁止する。 10.2. 通信相手(サーバー)の負荷を「自身のメモリ」と同等に扱わず、リクエストの並列数は `Promise.all` による全件同時実行ではなく、`p-limit` 相当の制御やチャンク分割によるスロットリングを標準とせよ。 10.3.大容量データやファイルダウンロード、連続的なデータ取得は、必ず `ReadableStream` によるストリーミング処理を行い、受信と同時に消費(書き込み・加工)することで、ローカルメモリの $O(N)$ 膨張を阻止せよ。 10.4. `await using` 等を用い、異常系を含めたあらゆるパスでリソース(Bodyの閉じ忘れ等)を即座に解放せよ。アイドル状態のコネクション維持によるリソース占有を $O(0)$ に近づけること。
[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 -RWNSE --allow-ffi --allow-run=magick,ffmpeg,ffprobe /** * 汎用コマンドラインツールのテンプレート * * ### 提供コマンド: * - `renumber` : 指定したディレクトリ内の画像を連番で一括リネームする * - `downloadfiles`: ファイルをダウンロードする(連番対応) * - `resizeimages` : 画像を伸縮する(ImageMagickが必要) * - `resizevideo` : 動画を伸縮する(ffmpegが必要) * - `filecompress` : 指定パスを圧縮ファイル化する * - `dirtotext` : ディレクトリ構造とソースファイル群を単一テキスト化。 * AIにプログラムの構造を理解させる手助けにもなるはず。 * - `metronome` : WebViewでメトロノームを表示する。 * * ### 使用例: * ```bash * deno run -A utils.ts renumber --prefix "vacation_" --start 1 * ``` * * ### 開発・動作環境: * - Deno 2.7.14 (V8 14.7) / TypeScript 5.9 準拠 * - ImageMagick 7.1 * - FFmpeg 8.1-full_build * * ### タグ: * - NOTE: なぜこのような実装にしたのか、設計の背景や理由 * - TODO: 未実装の機能、後で追加予定の処理 * - HACK: 綺麗ではないが動かすために一時的に書いた暫定コード * - FIXME: バグがある、または正しく動くがコードが汚い場所 * * ### 備忘録: Java/Python と似ているけど TypeScript の違う点 * 1. OSやファイルシステムなどのI/Oはランタイム固有APIを使う必要がある。 * DenoはWeb標準APIを実装しているので、ある程度はWebブラウザと共通した * コードが書ける。 * 2. 単一スレッド・単一イベントループで動作し、2つのキューを捌く。 * コールスタックが空になる度に最優先でマイクロタスク(Promise等)を全件消化 * し、その後マクロタスク(setTimeout/IO等)を1件のみ実行する直列の時間割。 * マイクロタスク内の無限ループはマクロタスクや描画を完全にフリーズさせるた * め注意。 * 3. 型の判定基準が「構造的型(ダック・タイピング)」である。 * Javaのように extends や implements による明示的な関係性(名前)を持たな * くても、オブジェクトの見た目の「形(構造)」さえ一致していれば自動的に互 * 換性あり(合格)と判定されて代入が可能になる。 * * このため、Record<string, unknown> や [key: string]: unknown のような * 大雑把な共通の「器(型)」を定義した場合、明示的に継承関係を書いていない * 無関係なオブジェクトまですり抜けて合格判定となり、コンパイルエラーや警告 * が出なくなるので注意。 * 4. 関数式(アロー関数やIIFEも含む)は、言語仕様上は生成時にスコープ内の全て * の変数をキャプチャしてヒープメモリ上にクロージャを生成する。しかし、現代 * の主要ランタイムは静的解析により、関数式内部で実際に参照している変数 * (または this)のみをクロージャに保持(隔離)する。 * * ただし、クロージャの実体はスコープにつき1つしか生成されないため、同一 * スコープ内で関数式を複数生成した場合は、それらが参照している変数の総和 * (最大数)が単一のクロージャに共有・保持され、特定の変数の寿命が伸びてGC * に回収されにくくなるリスクがある。 * * したがって、関数式の生存期間と、スコープの厳密な分離を意識すること。 * 5. try-catchのerrorはError|unknownであり、型が不確定。 * 6. メソッドチェーン(.then/.catch/.pipe / .map/.filter) * 高負荷時:ループ内では関数オブジェクト生成を避け、 * try-catch / for-await / for...of を使う。 * 一般領域:可読性優先ならよいが、裏でメモリ確保が走るコストを意識する。 * 7. スプレッド演算子([...])による隠れた O(N) コピーがあるので多用は無用。 * 大量データ時は push() 等の破壊的変更(In-place)による最適化を選択する。 * 8. オブジェクトの構造は生成時に固定し、後からのプロパティ追加は禁止。 * - 生成後にプロパティを追加するとランタイムの最適化(Hidden Class)から * 外れる。 * - オプションプロパティ(?:)を使ってその都度異なるキー構成でオブジェクト * を生成(省略)すると、形状(Shape)の不一致により関数のインライン * キャッシュなどの最適化が効かなくなり、実行効率が悪くなる可能性がある。 * - 最高のパフォーマンスが必要な領域では、初期値を undefined で固定する。 * * ### 備忘録: 設定しておくと便利なオプション * deno.jsonc * ```jsonc * { * "compilerOptions": { * "strict": true, // 厳格モード * "exactOptionalPropertyTypes" : true, // 正確なプロパティタイプに限定 * "noImplicitOverride": true, // 暗黙のオーバーライドを禁止 * "noImplicitReturns": true, // 暗黙の戻り値を禁止 * "noFallthroughCasesInSwitch": true, // switch文の突き抜け防止 * "noImplicitAny": true // 暗黙のanyを禁止 * }, * "lint": { * "rules": { * "include": [ * "explicit-function-return-type", // 暗黙の戻り値型を禁止 * "no-explicit-any", // any 型を禁止 (unknown等への移行) * "no-var", // var を禁止 (let/const を強制) * "prefer-const", // 再代入しない変数の let を禁止 * "eqeqeq", // == ではなく === を強制 * "no-irregular-whitespace", // 全角スペース等の混入を排除 * "no-throw-literal" // Errorオブジェクト以外のThrow禁止 * // ※この指定をしても * // catch内部で型チェックは必須 * ] * } * }, * "fmt": { * "nextControlFlowPosition": "nextLine", // elseとcatchの位置を次行に * "operatorPosition": "nextLine", // 演算子の位置を次行に * "newLineKind": "lf" // 改行コードを LF に * } * }, * // -A, --allow-all 制限なし * // -R, --allow-read[=<PATH>...] 読み込み可能なパスやファイルの指定も可能 * // -W, --allow-write[=<PATH>...] 書き込み可能なパスやファイルの指定も可能 * // -N, --allow-net[=<IP_OR_HOSTNAME>...] アクセス可能なホストの指定も可能 * // -E, --allow-env[=<VARIABLE_NAME>...] アクセス可能な環境変数の指定も可能 * // -S, --allow-sys[=<API_NAME>...] システム情報の個別指定も可能 * // --allow-run[=<PROGRAM_NAME>...] 実行可能なプログラムの個別指定も可能 * "tasks": { * "mod:check": "deno outdated", * "mod:update": "deno outdated --update", * "fmt": "deno fmt --check --watch src/ scripts/ utils.ts", * "lint": "deno lint --ignore=playground/,dist/", * "build": "deno run -A scripts/build.ts utils.ts myutils", * "backup": "deno run -A scripts/backup.ts" * } * ``` * @module */ // =================================================================== // ランタイムに依存しない外部ライブラリ // =================================================================== // deno-lint-ignore-file no-external-import no-import-prefix import { parseArgs } from "jsr:@std/cli@^1.0.28/parse-args"; import * as path from "jsr:@std/path@^1.1.4"; import * as posix from "jsr:@std/path@^1.1.4/posix"; import { delay } from "jsr:@std/async@^1.2.0"; import { walk } from "jsr:@std/fs@^1.0.23"; import * as zip_js from "jsr:@zip-js/zip-js@^2.8.26"; const PROG_VERSION = "2026.04.30"; // =================================================================== // 共通関数 // =================================================================== /** * 不明なエラーオブジェクトを Error インスタンスに正規化する。 * * 言語仕様上の必然性: * JavaScript の `throw` は任意の型を許容する仕様(ECMAScript 準拠)であり、 * TypeScript 5.9 においても `catch(error)` は `unknown` 型として定義される。 * 本関数は、この不確実な入力を後続処理で安全に扱うための「型ガード」および * 「正規化」として機能する。 * * @param error 判定対象のエラー。ランタイムから流入する不確実な型。 * @returns 判定済み、または正規化された Error インスタンス。 */ function ensureError(error: unknown): Error { return error instanceof Error ? error : new Error(String(error)); } /** * テンプレート文字列。 * * Javaなどの `String.format` に相当する簡易動的テンプレート。 * ロケール毎のテキストに対応したり、予めメッセージを別で定義しておく場合に使用 * する。 * * @param template テンプレート文字列 * @param args 注入する値(可変長引数) * @returns 構築済み文字列 * * @remarks 補足 * - 置換対象の書式は {0}, {1}, ...{n} であり、{数字}で記述する。 * - 置換対象の並び順も個数(重複可)も自由。 * * @example * ```typescript * // "ようこそ、{1}さん。{0}".format("Now Loading...", "Tarou")相当の使用例 * const msg1 = t(MSG[lang].welcome, "Now Loading...", "Tarou"); * // => "ようこそ、Tarouさん。Now Loading..." * * // 複数の値を注入する例 * const msg2 = t("{0}のエラーが発生しました (Code: {1})", "システム", 500); * // => "システムのエラーが発生しました (Code: 500)" * * // インデックスが引数の数を超過した場合 * // (元のマッチ文字列を維持し情報の欠損を防ぐ) * const msg3 = t("{0}と{1}の同期", "データ"); * // => "データと{1}の同期" */ // deno-lint-ignore no-unused-vars const t = (() => { const RE_TEMPLATE: RegExp = /{(\d+)}/g; // NOTE: スタックトレースが出来るように名前付き関数式にする。 // ここではアロー関数式にするメリットは特にない return function t(template: string, ...args: unknown[]): string { return template.replace(RE_TEMPLATE, (match, p1) => { // NOTE: Numberと同じ動作をする+演算子の方が変換コストが低い // 可読性が犠牲にならない箇所のみ使用可 // const i = Number(p1); const i = +p1; return i < args.length ? String(args[i]) : match; }); }; })(); /** * 数値を適切な接頭辞(2進、SI、または bps)付きの文字列に変換する。 * * @param src - 変換対象の数値。 * @param kind - 単位の体系を指定する。初期値は "bin"。 * - "bin": 1024単位の2進接頭辞(KiB, MiB 等)を使用する。 * - "SI": 1000単位のSI接頭辞(KB, MB 等)を使用する。 * - "bps": 1000単位のビットレート単位(Kbps, Mbps 等)を使用する。 * @returns 小数点以下2桁固定の数値と単位を連結した文字列。 * * @example * ```typescript * formatBinNum(1024); // "1.00 KiB" (default: bin) * formatBinNum(1572864); // "1.50 MiB" (default: bin) * formatBinNum(1000, "SI"); // "1.00 KB" * formatBinNum(1000000, "bps"); // "1.00 Mbps" * formatBinNum(0); // "0 B" * formatBinNum(0, "SI"); // "0 B" * formatBinNum(0, "bps"); // "0 bps" * ``` */ const formatBinNum = (() => { type KIND = "bin" | "SI" | "bps"; const UNITS: Readonly<Record<KIND, readonly string[]>> = { "bin": ["B", "KiB", "MiB", "GiB", "TiB", "PiB"], "SI": ["B", "KB", "MB", "GB", "TB", "PB"], "bps": ["bps", "Kbps", "Mbps", "Gbps", "Tbps", "Pbps"], }; function calc(src: number, divisor: number, unitLen: number): { value: number; i: number; } { let value = src; let i = 0; while (value >= divisor && i < unitLen) { value /= divisor; i++; } return { value, i }; } // NOTE: スタックトレースが出来るように名前付き関数式にする。 // ここではアロー関数式にするメリットは特にない return function formatBinNum( src: number, kind: KIND = "bin", ): string { if (src <= 0) return kind === "bps" ? "0 bps" : "0 B"; const units = UNITS[kind]; const divisor = kind === "bin" ? 1024 : 1000; const res = calc(src, divisor, units.length - 1); const value = res.value.toFixed(2); const suffix = units[res.i]; return `${value} ${suffix}`; }; })(); /** * 指定した区切り文字で文字列を分割し、各トークンに対して任意の接頭辞・接尾辞を * 付与した配列を生成する。 * * @remarks 空文字列のトークンは自動的に除外されます。 * * @param value - 分割対象のベース文字列。 * @param options - トークン処理のオプションオブジェクト。デフォルトは `{}`。 * @param options.separator - 文字列を分割するための区切り文字(初期値 `":"`) * @param options.prefix - 各トークンの先頭に付与する文字列(初期値 空文字) * @param options.suffix - 各トークンの末尾に付与する文字列(初期値 空文字) * @returns 処理されたトークン(空文字を除く)の配列。 * * @example * ```typescript * splitTokens(":txt:png:webp"); * // => ["txt", "png", "webp"] * * splitTokens(":txt:png:webp", { prefix: "." }); * // => [".txt", ".png", ".webp"] * * * splitTokens("//txt/png/webp", { separator: "/" }); * // => ["txt", "png", "webp"] * ``` */ function splitTokens( value: string, { separator = ":", prefix = "", suffix = "", }: { separator?: string; prefix?: string; suffix?: string; } = {}, ): string[] { const result: string[] = []; const rawData = value.split(separator); for (let i = 0; i < rawData.length; i++) { if (rawData[i] === "") continue; // 空は無視する result.push(`${prefix}${rawData[i]}${suffix}`); } return result; } /** * 網羅性チェックエラー。 * * 網羅性チェックでの抜けでスローさせる例外。 * TypeScriptのコンパイラ、JavaScriptの実行時にも対応。 * * @example * ```typescript * type Action = "create" | "update" | "delete"; * function handle(action: Action) { * switch (action) { * case "create": * return "created"; * case "update": * return "updated"; * default: * // action がすべて網羅されていないのでコンパイルエラー * throw new ExhaustiveError(action); * } * } * * // 実行時にExhaustiveErrorがスローされる * handle("delete"); * ``` */ class ExhaustiveError extends Error { constructor(value: never, message = `Unsupported identifiers: ${value}`) { super(message); } } // =================================================================== // ランタイム固有処理の抽象化 // // NOTE: テスト用モックや異なるランタイムに切り替えやすくするため // =================================================================== /** * ファイル情報を抽象化した型。 * * {@link Deno.FileInfo}を元にした構造であり現時点で必要な項目だけを * 記述している。 */ interface RuntimeFileInfo { size: number; } /** * ディレクトリエントリ。 * * {@link Deno.DirEntry}を元にした構造。 */ interface RuntimeDirEntry { /** ファイル名・ディレクトリ名。 */ name: string; /** ファイルなら true。 */ isFile: boolean; /** ディレクトリなら true。 */ isDirectory: boolean; /** シンボリックリンクなら true。 */ isSymlink: boolean; } /** * 読み込み用ストリームと解放処理をセットにしたリソース。 * * [Symbol.asyncDispose] により、await using での自動解放に対応する。 */ interface ReadableResource extends AsyncDisposable { readonly stream: ReadableStream<Uint8Array>; } /** * OpenSyncの戻り値型。 */ interface OpenSyncResult { writeSync(p: Uint8Array): number; close(): void; } /** * 外部プロセス(同期)の実行結果を格納する構造体。 * * 終了ステータスおよび標準入出力のバイナリデータを保持する。 * {@link Deno.CommandOutput}を元にした構造。 */ interface SubProcessResult { /** * プロセスの終了コード。 * * 一般に 0 は成功、それ以外はエラーまたは特定の状態を示す。 */ code: number; /** * プロセスから出力された標準出力(stdout)の生データ。 * * テキストとして扱う場合は、必要に応じて TextDecoder 等でデコードする。 */ stdout: Uint8Array; /** * プロセスから出力された標準エラー出力(stderr)の生データ。 * * 主にエラーメッセージや診断情報が格納される。 */ stderr: Uint8Array; } /** * 外部プロセス(非同期)の実行結果を格納する構造体。 * * {@link Deno.CommandStatus}を元にした構造。 */ interface SubProcessStatus { success: boolean; code: number; // signal: Signal | null; } /** * 実行環境(ランタイム)固有の操作を抽象化するインターフェース。 * * @remarks 補足 * - 異なるランタイム環境への適応を多少容易にするための抽象レイヤー。 * - ランタイム固有機能はこの一箇所に纏める。 * @internal */ interface Runtime { /** 指定した終了コードでプロセスを終了する。 */ exit(code: number): void; /** コマンドライン引数を取得する。 */ getArgs(): string[]; /** ディレクトリを作成する。 */ mkdir( path: string, options?: { recursive?: boolean; mode?: number; }, ): Promise<void>; /** ファイルをコピーする。 */ copyFile(src: string, dest: string): Promise<void>; /** ディレクトリ内のエントリを走査する。 */ readDir(path: string): AsyncIterable<RuntimeDirEntry>; /** ファイルまたはディレクトリをリネーム(移動)する。 */ rename(oldPath: string, newPath: string): Promise<void>; /** ファイルのメタデータを取得する。 */ stat(path: string): Promise<RuntimeFileInfo>; /** パスが存在するか確認する。 */ exists(path: string): Promise<boolean>; /** ストリームからデータを読み取り、指定パスへ書き込む。 */ writeFile( path: string, stream: ReadableStream<Uint8Array> | null, ): Promise<boolean>; /** 指定パスのファイルをリソースとして取得する。 */ getReadableResource(path: string): Promise<ReadableResource>; /** * 指定されたパスのファイルを同期的に開く。 * * @param path ファイルパス * @param options {@link Deno.OpenOptions} に準拠したオプション * @returns {@link Deno.openSync}に準拠したファイル操作オブジェクト */ openSync(path: string, options: { write?: boolean; create?: boolean; truncate?: boolean; read?: boolean; }): OpenSyncResult; /** * 外部コマンドを実行する。 * * @param command 実行するコマンド名またはパス * @param args コマンドに渡す引数の配列 */ execSync(command: string, args: string[]): Promise<SubProcessResult>; exec( command: string, args: string[], onOutput: (text: string, source: "stdout" | "stderr") => void, ): Promise<SubProcessStatus>; /** * 改行を付与せず、標準出力に文字列を直接書き込む(同期版)。 * * @param message 出力する文字列 */ printSync(message: string): void; /** 実行環境情報を出力する。 */ outputEnv(): void; } /** * Deno ランタイム標準 API を使用する {@link Runtime} の本番用実装。 * * @remarks ランタイム固有の外部ライブラリを使う場合は、ここの中でインポート * する。 */ const nativeRuntime: Runtime = { exit(code: number): void { Deno.exit(code); }, getArgs(): string[] { return Deno.args; }, mkdir: Deno.mkdir, copyFile: Deno.copyFile, readDir: Deno.readDir, rename: Deno.rename, async stat(path: string): Promise<RuntimeFileInfo> { return await Deno.stat(path); }, // @std/fs/existsの競合状態回避策 async exists(path: string): Promise<boolean> { try { await Deno.lstat(path); return true; } catch { return false; } }, async writeFile( path: string, stream: ReadableStream<Uint8Array> | null, ): Promise<boolean> { try { using file = await Deno.open(path, { write: true, create: true, truncate: true, }); if (stream !== null) { await stream.pipeTo(file.writable); } return true; } catch { return false; } }, async getReadableResource(path: string): Promise<ReadableResource> { const file = await Deno.open(path, { read: true }); return { stream: file.readable, [Symbol.asyncDispose](): Promise<void> { try { file.close(); } catch { /** ignore */ } return Promise.resolve(); }, }; }, openSync(path: string, options: { write?: boolean; create?: boolean; truncate?: boolean; read?: boolean; }): OpenSyncResult { const file = Deno.openSync(path, options); return { writeSync: (p: Uint8Array): number => file.writeSync(p), close: (): void => file.close(), }; }, async execSync(command: string, args: string[]): Promise<SubProcessResult> { const cmd = new Deno.Command(command, { args }); return await cmd.output(); }, async exec( command: string, args: string[], onOutput: (text: string, source: "stdout" | "stderr") => void, ): Promise<SubProcessStatus> { /** * 非同期の読み込み。 * * @remarks 補足 * - steam: true でマルチバイトの境界を認識させる。 * - steam: true を指定しているので最終チャンクは手動で処理する。 */ const readStream = async ( stream: ReadableStream<Uint8Array>, source: "stdout" | "stderr", ): Promise<void> => { const decoder = new TextDecoder(); for await (const chunk of stream) { onOutput(decoder.decode(chunk, { stream: true }), source); } const remaining = decoder.decode(); if (remaining) { onOutput(remaining, source); } }; const cmd = new Deno.Command(command, { args, stdout: "piped", stderr: "piped", }); const child = cmd.spawn(); const [stdoutDone, stderrDone] = [ readStream(child.stdout, "stdout"), readStream(child.stderr, "stderr"), ]; const [status] = await Promise.all([ child.status, stdoutDone, stderrDone, ]); return status; }, printSync(message: string): void { const data = new TextEncoder().encode(message); Deno.stdout.writeSync(data); }, outputEnv(): void { console.log("--- Deno Built-in Properties ---"); const envInfo = { "Deno.hostname": Deno.hostname(), "Deno.execPath()": Deno.execPath(), "Deno.noColor": Deno.noColor, "Deno.version": Deno.inspect(Deno.version), "Deno.build": Deno.inspect(Deno.build), }; for (const [key, value] of Object.entries(envInfo)) { console.log(`${key}: ${value}`); } console.log("--- Deno Related Environment Variables ---"); // deno-fmt-ignore const denoRelatedVars = [ "DENO_DIR", // キャッシュルート "DENO_INSTALL", // インストール先 "DENO_AUTH_TOKENS", // 秘密リポジトリ等の認証情報 "DENO_CERT", // TLS証明書のパス "DENO_WEBGPU_ADAPTER", // WebGPUの使用アダプタ "NO_COLOR", // コンソール出力の色抑制 "HTTP_PROXY", // ネットワークプロキシ "HTTPS_PROXY", // ネットワークプロキシ "NO_PROXY", // プロキシ除外設定 ]; for (const name of denoRelatedVars) { const value = Deno.env.get(name); console.log(`${name}: ${value ?? "(not set)"}`); } }, }; // =================================================================== // 画像処理ラッパー // =================================================================== // =================================================================== // 動画処理ラッパー // =================================================================== // =================================================================== // fetchのラッパー // =================================================================== /** * fetch 実行の結果を分類して表す。 * * 判別共用体 (Discriminated Union)。 * * @remarks 補足 * - ネットワークの成否だけでなく、HTTP ステータスコードに基づく。 * - 論理的な成功・失敗も型レベルで区別するために使用する。 */ type SafeFetchResult = | ( { readonly type: "success"; readonly response: Response; readonly error?: never; } & AsyncDisposable ) | ( { readonly type: "http_error"; readonly response: Response; readonly error?: never; } & AsyncDisposable ) | ( { readonly type: "network_error"; readonly response?: never; readonly error: Error; } & AsyncDisposable ); /** * fetch を実行し、例外をスローせずに実行結果を型安全なオブジェクトとして返す。 * * @param url - リクエスト対象の URL 文字列 * @returns 実行結果の状態 (`type`) に応じた {@link SafeFetchResult} を含む * * @remarks 素のfetchではエラーハンドリングが面倒くさいので例外を全て吸収し * 戻り値として返す。 */ async function safeFetch( url: string, headers: HeadersInit = {}, ): Promise<SafeFetchResult> { try { const response = await fetch(url, { method: "GET", headers }); const disposer = { async [Symbol.asyncDispose](): Promise<void> { if (response.bodyUsed === false && response.body) { await response.body.cancel(); } }, }; return { type: response.ok ? "success" : "http_error", response, ...disposer, }; } catch (error) { return { type: "network_error", error: ensureError(error), [Symbol.asyncDispose](): Promise<void> { return Promise.resolve(); }, }; } } // =================================================================== // コマンドの定義 // =================================================================== /** * コマンドの戻り値型。 * * @typeParam E - エラーオブジェクトの型。デフォルトは標準の `Error`。 * @example * ### 成功時のパターン * ```typescript * const result: CommandResult = { success: true, code: 0 }; * ``` * ### 警告・中断時(コマンドライン引数が異常など)のパターン * ```typescript * const result: CommandResult = { success: true, code: 2 }; * ``` * ### 失敗(例外)時のパターン * ```typescript * const result: CommandResult<TypeError> = { success: false, code: 1, error: new TypeError("型が不正です") }; * ``` */ type CommandResult<E extends Error = Error> = | { readonly success: true; readonly code: number; readonly error?: never; } | { readonly success: false; readonly code: number; readonly error?: E; }; /** * {@link CommandResult} を生成するファクトリーオブジェクト。 * * @example * ```typescript * // 完全な正常終了(Exit Code: 0) * return CommandResult.ok(); * * // 例外ではないが、警告・中断による終了(Exit Code: 2) * console.error("エラー: 引数が不正です"); * return CommandResult.warn(); * * // 予期せぬシステム例外による終了(Exit Code: 1) * try { * await Deno.readTextFile("config.json"); * } * catch (error) { * // デバッグ用スタックトレースが維持される * return CommandResult.error(ensureError(error)); * } * ``` */ const CommandResult = { /** * 完全な正常終了(成功)を表す {@link CommandResult} を生成する。 * * @remarks * 終了コード(Exit Code)は常に `0` 固定。 */ ok(): CommandResult { return { success: true, code: 0 }; }, /** * 例外(クラッシュ)ではないが、警告やユーザーの操作ミスによる処理の中断を * 表す {@link CommandResult} を生成する。 * * @remarks * 警告などのメッセージの出力を行ったあと、スタックトレースを吐かずにプロセス * を終了させたい場合に使用する。 * @param code - OSに返却する終了コード。初期値は `2`。 */ warn(code: number = 2): CommandResult { return { success: true, code }; }, /** * 予期せぬシステム例外やクラッシュを表す {@link CommandResult} を生成する。 * * @param error - 発生した例外(Errorインスタンス)。 * @param code - OSに返却する異常終了コード。初期値は `1`。 */ error<E extends Error>(error: E, code: number = 1): CommandResult<E> { return { success: false, code, error }; }, }; /** * 全てのサブコマンドが共有する解析済みオプションの基底型。 */ type CommandOptions = { /** 詳細な実行ログを出力するかどうか (現在未使用) */ verbose?: boolean; }; /** * コマンドオプションの解析・検証結果を表す型。 * * 検証結果が `valid` の場合は `options` を、 * `invalid` の場合は `reason`(理由)を保持する。 */ type ParseOptionsResult<T extends CommandOptions> = | { readonly type: "valid"; readonly options: T; readonly reason?: never; } | { readonly type: "invalid"; readonly options?: never; readonly reason: string; }; /** * サブコマンドの「クラス定義そのもの」が満たすべき共通ルール。 * * @remarks 抽象クラスでは制限できない `static` メンバの実装を、コンパイル時に * 強制するための仕組み。 * * これにより、クラスをインスタンス化(new)する前の段階で、 * 識別用の `static ID` などが正しく定義されているかがチェックされる。 */ interface SubCommandConstructor { /** * コマンドの識別名(例: "renumber", "download")。 * * @remarks 具象クラスでは `static readonly ID: string` として定義する。 */ readonly ID: string; /** * ヘルプ画面に表示されるコマンドの簡潔な説明文。 * * @remarks 具象クラスでは `static readonly DESCRIPTION: string` として定義 * する。 */ readonly DESCRIPTION: string; /** * サブコマンドのインスタンスを生成するためのコンストラクタ。 * * @param runtime ランタイムの実装 * @returns サブコマンドのインスタンス */ new (runtime?: Runtime): SubCommand<CommandOptions>; } /** * 全てのサブコマンドの基底となる抽象クラス。 * * 引数の解析(Template Methodパターンの構成)と実行フローを制御する。 * 継承先では、具体的なオプション型 `T` を定義し、解析と実行のロジックを * 実装すること。 * * @template T コマンドが使用する解析済みオプションの型 */ abstract class SubCommand<T extends CommandOptions> { /** * コンストラクター。 * * @param runtime - ランタイム固有機能。 * デフォルトは `nativeRuntime`を使用。 * 単体テスト時には、ここへモックオブジェクトを注入することで、 * 実際のファイルシステムを汚さずにロジックを検証できる。 * @example * // 本番用(通常はこちらを使用) * const cmd = new ReNumberCommand(); * @example * // テスト用(ディレクトリ内のファイル一覧を偽装する) * const mock: Runtime = { * 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(mock); */ constructor(protected runtime: Runtime = nativeRuntime) {} /** * コマンド固有のヘルプ情報(使用法、オプションの詳細など)を * 標準出力に表示する。 * @protected */ protected abstract showHelp(): void; /** * コマンドライン引数の配列を解析し、型 `T` のオブジェクトに変換する。 * * @param args サブコマンド名を除いた、解析対象の引数リスト * @returns 解析・構造化されたオプションオブジェクト * @protected */ protected abstract parseOptions(args: string[]): ParseOptionsResult<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 (subArgs.help) { this.showHelp(); return CommandResult.ok(); } const resOption = this.parseOptions(args); if (resOption.type === "invalid") { console.error(resOption.reason); return CommandResult.warn(); } return await this.runImpl(resOption.options); } } // =================================================================== // SubCommand: renumber (連番リネーム) // =================================================================== // =================================================================== // SubCommand: downloadfiles (ファイルダウンロード) // =================================================================== // =================================================================== // SubCommand: resizeimages (画像ファイルサイズ変換) // =================================================================== // =================================================================== // SubCommand: resizevideo (動画のサイズ変換) // =================================================================== // =================================================================== // SubCommand: filecompress (圧縮ファイル化) // =================================================================== // =================================================================== // SubCommand: metronome (WebView: メトロノーム) // =================================================================== // =================================================================== // SubCommand: dirtotext (構成内容一括出力) // =================================================================== /** * `dirtotext` コマンドで使用するパース済みオプション。 */ type DirToTextOptions = CommandOptions & { rootPath: string; isTreeOnly: boolean; outputPath?: string | undefined; targetExt?: string | undefined; }; /** * 内部用の書き込みリソース抽象化型。 * * @remarks `using` 宣言に対応する。 * * @example * ```typescript * { * using write = createDirToTextWriter("output.txt"); * write("some content"); * // スコープを抜ける際に Symbol.dispose が自動的に close() を呼び出す * } * ``` */ type DirToTextWriter = { (content: string): void; close: () => void; [Symbol.dispose](): void; }; /** * 開発プロジェクト内部のディレクトリ構造を単一のテキストに変換し、ソース * ファイルに相当する内容を結合して1つにする。 */ class DirToTextCommand extends SubCommand<DirToTextOptions> { static readonly ID = "dirtotext"; static readonly DESCRIPTION = "プロジェクトディレクトリ構造とソースファイルを1つのテキストに変換"; private static readonly IGNORE_DIRS: ReadonlySet<string> = new Set([ "node_modules", "vendor", ".git", ".github", ".gitlab", "dist", "build", "out", "coverage", ".venv", "__pycache__", ".ipynb_checkpoints", "playground", // ".vscode", ]); /** * 処理対象とする拡張子のリスト。 * 全て小文字、ドット付きで定義。 */ // deno-fmt-ignore private static readonly TARGET_EXTS: ReadonlySet<string> = new Set([ // Web / Script ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".d.ts", ".sh", // Python ".py", ".pyi", // Java ".java", ".jsp", ".properties", ".kt", ".kts", // C / C++ ".cpp", ".cc", ".cxx", ".h", ".hpp", ".hh", ".c", // SQL ".sql", // Configs ".json", ".jsonc", ".yaml", ".yml", ".toml", ".ini", ".conf", ".cfg", ".gitignore", ".code-workspace", ]); /** 日本語環境に最適化された自然順ソート用の比較器。 */ // deno-fmt-ignore private static readonly I18N_COLLATOR: Readonly<Intl.Collator> = new Intl.Collator("ja", { numeric: true, sensitivity: "variant" }); /** * 処理対象とする拡張子のリスト(実処理) */ private targetExts: ReadonlySet<string>; /** * コンストラクタ。 */ constructor(runtime?: Runtime) { super(runtime); this.targetExts = DirToTextCommand.TARGET_EXTS; } /** * ヘルプ情報を標準出力。 */ protected override showHelp(): void { console.log(` Description: ${DirToTextCommand.DESCRIPTION} Usage: deno run -A utils.ts ${DirToTextCommand.ID} [Options] Options: -p, --rootPath 出力対象のルートディレクトリ -t, --tree ディレクトリ構造のみ出力 -o, --output 出力ファイルのパス (未指定時は標準出力) --ext 独自指定拡張子(ファイル名の末尾とマッチさせる) -h, --help このヘルプを表示 例 独自の拡張子を指定する方法 --ext=".test.ts" // 1つでも --ext=".txt:.md:.log" // 複数でも指定可能`); } /** * 引数を `DirToTextOptions` 型にパースする。 * * @param rawArgs コマンドライン引数の配列 * @returns パース済みのオプションオブジェクト */ protected override parseOptions( rawArgs: string[], ): ParseOptionsResult<DirToTextOptions> { const args = parseArgs(rawArgs, { string: ["rootPath", "output", "ext"], boolean: ["tree"], default: { rootPath: "", tree: false, }, alias: { p: "rootPath", t: "tree", o: "output", }, }); if (args.rootPath === "") { return { type: "invalid", reason: "-p --rootPath を指定してください。", }; } return { type: "valid", options: { rootPath: args.rootPath, isTreeOnly: args.tree, ...(args.output ? { outputPath: args.output } : {}), ...(args.ext ? { targetExt: args.ext } : {}), }, }; } /** * コマンドのメインロジックを実行する。 * * @param options `parseOptions` によって解析されたオプションオブジェク * * @returns 実行結果(成功・失敗のステータスとメッセージ)を含むPromise * @throws {Error} ファイルシステム操作に失敗した場合 */ protected override async runImpl( options: Readonly<DirToTextOptions>, ): Promise<CommandResult> { const absoluteRoot = path.resolve(options.rootPath); const treeLines: string[] = []; const files: string[] = []; if (options.targetExt) { this.targetExts = new Set( splitTokens(options.targetExt), ); } await this.scanDirectory(absoluteRoot, treeLines, files); using writer = this.createWriter(options.outputPath); writer("=".repeat(40)); writer("PROJECT STRUCTURE:"); writer(treeLines.join("\n")); if (options.isTreeOnly) { return CommandResult.ok(); } writer("\n" + "=".repeat(40)); writer("FILE CONTENTS:"); for (const file of files) { const relativePath = path.relative(absoluteRoot, file); writer(`\n---- ${relativePath} ----`); await using res = await this.runtime.getReadableResource(file); const text = await new Response(res.stream).text(); writer(text); } return CommandResult.ok(); } /** * ディレクトリ構造を再帰的に走査し、ツリー表示用の文字列配列とファイル情報の * 配列を構築する。 * * 各階層のエントリは、まず「ディレクトリ > ファイル」の優先順位で分類し、 * 同一タイプ内ではロケールを考慮した辞書順でソートされた上で走査する。 * * @param currentPath - 現在走査中、および起点となるディレクトリの絶対パス。 * @param treeLines - ツリー形式の文字列を格納する`出力用`配列。 * @param files - 抽出されたファイル情報を格納する`出力用`配列。 * @param prefix - ツリー表示の階層を示すためのインデント文字列。 * @returns 非同期処理の完了を示す Promise。 * @throws {Error} ファイルシステム操作に失敗した場合 */ async scanDirectory( currentPath: string, treeLines: string[], files: string[], prefix = "", ): Promise<void> { const entries: RuntimeDirEntry[] = []; for await (const entry of this.runtime.readDir(currentPath)) { if (await this.isValidEntry(currentPath, entry)) { entries.push(entry); } } entries.sort((a, b) => { if (a.isDirectory !== b.isDirectory) { return a.isDirectory ? -1 : 1; } return DirToTextCommand.I18N_COLLATOR.compare(a.name, b.name); }); for (let i = 0; i < entries.length; i++) { const entry = entries[i]; const isLast = i === entries.length - 1; const fullPath = path.join(currentPath, entry.name); const line = `${prefix}${isLast ? "└── " : "├── "}${entry.name}`; treeLines.push(line); if (entry.isDirectory) { await this.scanDirectory( fullPath, treeLines, files, prefix + (isLast ? " " : "│ "), ); } else { files.push(fullPath); } } } /** * 処理対象エントリ判定。 * * @remarks * 判定基準: * - `IGNORE_DIRS` に該当しないディレクトリであり、その配下に1つ以上の * 処理対象ファイルが存在する。 * - `TARGET_EXTS` に該当するファイル。 * - シンボリックリンクは処理対象外。 * * @param currentPath - 現在走査しているディレクトリの絶対パス。 * @param entry - エントリ情報。 * @returns 処理対象である場合は `true`、それ以外は `false`。 * @throws {Error} ファイルシステム操作に失敗した場合 */ private async isValidEntry( currentPath: string, entry: RuntimeDirEntry, ): Promise<boolean> { if (entry.isDirectory) { if (DirToTextCommand.IGNORE_DIRS.has(entry.name)) { return false; } const childPath = path.join(currentPath, entry.name); return await this.containsTargetEntries(childPath); } else if (entry.isFile) { // NOTE: コマンドの利便性向上:判定基準を拡張子からファイル名の末尾に // const ext = path.extname(entry.name).toLowerCase(); // return this.targetExts.has(ext); return this.isTargetFileName(entry.name); } // シンボリックリンクは無視する return false; } /** * 指定されたファイル名が処理対象であるかを判定する。 * * @remarks * 判定基準: * - ファイル名の末尾(拡張子を含むファイル名全体)が、`targetExts` に * 定義されたいずれかの文字列と一致するかを判定する。 * - 大文字・小文字は区別しない(小文字に統一して評価)。 * * @param name - 判定対象のファイル名(ベース名)。 * @returns 処理対象の末尾と一致する場合は `true`、それ以外は `false`。 */ private isTargetFileName(name: string): boolean { const str = name.toLowerCase(); let result = false; for (const ext of this.targetExts) { if (str.endsWith(ext)) { result = true; break; } } return result; } /** * 指定されたディレクトリが処理対象エントリを含んでいるかを判定する。 * * @param dirPath 走査対象のディレクトリの絶対パス * @returns `dirPath`が処理対象エントリを含むディレクトリなら true。 * @throws {Error} ファイルシステム操作に失敗した場合 */ async containsTargetEntries(dirPath: string): Promise<boolean> { for await (const entry of this.runtime.readDir(dirPath)) { if (await this.isValidEntry(dirPath, entry)) { return true; } } return false; } /** * 出力先に応じた Writer オブジェクトを生成する。 * * @param outputPath - 出力先ファイルのパス。 * 省略した場合は標準出力(console.log)を使用する。 * @returns `DirToTextWriter` オブジェクト。 * `using` 構文による自動リソース解放に対応する。 * @throws {Error} ファイルシステム操作に失敗した場合 * * @example * ```typescript * using writer = this.createWriter("output.txt"); * writer("content"); * ``` */ private createWriter(outputPath?: string): DirToTextWriter { if (outputPath) { return this.createFileWriter(outputPath); } return this.createConsoleWriter(); } private createFileWriter(outputPath: string): DirToTextWriter { const encoder = new TextEncoder(); const file = this.runtime.openSync(outputPath, { write: true, create: true, truncate: true, }); const writer = ((content: string): void => { file.writeSync(encoder.encode(content + "\n")); }) as DirToTextWriter; writer.close = (): void => { file.close(); }; writer[Symbol.dispose] = (): void => { writer.close(); }; return writer; } private createConsoleWriter(): DirToTextWriter { const writer = ((content: string): void => { console.log(content); }) as DirToTextWriter; writer.close = (): void => {}; writer[Symbol.dispose] = (): void => {}; return writer; } } // =================================================================== // メイン処理 // =================================================================== /** * ツール全体のライフサイクルとコマンド実行を管理するメインクラス。 * * 本プログラムはシンプルなコマンドラインプログラムなので、高度な * ハンドリングは必要ない。 */ class Main { // NOTE: ここで SubCommandConstructor型 を指定することで // サブコマンドにstaticメンバーとコンストラクタを継承(強制)させられる // deno-fmt-ignore private readonly SUB_COMMANDS: ReadonlyMap<string, SubCommandConstructor> = new Map<string, SubCommandConstructor>([ // [ReNumberCommand.ID, ReNumberCommand], // [DownloadFilesCommand.ID, DownloadFilesCommand], // [ReSizeImagesCommand.ID, ReSizeImagesCommand], // [ReSizeVideoCommand.ID, ReSizeVideoCommand], // [FileCompressorCommand.ID, FileCompressorCommand], [DirToTextCommand.ID, DirToTextCommand], // [MetronomeCommand.ID, MetronomeCommand], ]); /** * ヘルプ情報を標準出力。 * * @returns 常に0(正常終了コード) */ private showHelp(): void { console.log(`Usage: deno run -A utils.ts [Options] [SubCommands] Options: -h, --help Show help -v, --version Show version -e, --env Show environment info SubCommands:`); // deno-fmt-ignore const maxDigits = [...this.SUB_COMMANDS.keys()].reduce( (max, key) => Math.max(max, key.length) , 0 ) + 2; for (const cmd of this.SUB_COMMANDS.values()) { console.log(` ${cmd.ID.padEnd(maxDigits)} : ${cmd.DESCRIPTION}`); } } /** * コマンドライン引数をパースする。 * * @param args コマンドライン引数の配列 * @returns パース済みのオプションオブジェクト */ private parseOptions(args: string[]): { help: boolean; version: boolean; env: boolean; _: string[]; // 上記の残りの引数がここに格納される } { return parseArgs(args, { stopEarly: true, // 最初の引数をサブコマンドとして扱う boolean: ["help", "version", "env"], alias: { h: "help", v: "version", e: "env" }, }); } /** * アプリケーションのメインエントリポイント。 * * コマンドライン引数を解析し、適切なサブコマンドを呼び出す。 * * @returns プロセスの終了コード */ async exec(): Promise<number> { const args = this.parseOptions(nativeRuntime.getArgs()); if (args.version) { console.log(`${PROG_VERSION}`); return 0; } if (args.env) { nativeRuntime.outputEnv(); return 0; } const [subCommandName, ...subCommandArgs] = args._; if (args.help || subCommandName === undefined) { this.showHelp(); return 0; } const subCommand = this.SUB_COMMANDS.get(subCommandName); if (subCommand === undefined) { console.error(`Unknown command: ${subCommandName}`); return 1; } try { const cmd = new subCommand(); const result = await cmd.run(subCommandArgs); if (result.error) { console.error(result.error.stack); } return result.code; } // 想定外のエラー(権限不足やディスクフルなど) catch (error) { const e = ensureError(error); console.error(e.stack); return 1; } } } /** * 直接実行された場合のみアプリケーションを起動 */ if (import.meta.main) { const exitCode = await new Main().exec(); nativeRuntime.exit(exitCode); }
登録:
投稿 (Atom)