├── .cline ├── build.ts ├── roomodes │ ├── deno-module.md │ ├── deno-refactor.md │ ├── deno-script.md │ ├── deno-tdd.md │ ├── library-searcher.md │ └── mizchi-writer.md └── rules │ ├── 00_basic.md │ ├── _git.md │ ├── _memory.md │ ├── coding.md │ ├── deno.md │ ├── directory-patterns.md │ ├── tdd.md │ ├── typescript.md │ └── zunda.md ├── .clinerules ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .hooks ├── pre-commit └── scripts │ └── pre-commit-check.ts ├── .roomodes ├── .vscode └── settings.json ├── README.md ├── __deprecated ├── adapter-pattern.md ├── brave_search.ts ├── clizod.test.ts ├── clizod.ts ├── cmd │ ├── bar.ts │ ├── foo.ts │ └── main.ts ├── intro-zodcli.md ├── prime.ts ├── tdd-mode.ts ├── todo-cli │ ├── README.md │ ├── deno.json │ ├── mod.ts │ ├── src │ │ ├── ai.ts │ │ ├── ask.ts │ │ ├── commands.ts │ │ ├── db.ts │ │ ├── schema.ts │ │ └── types.ts │ └── test │ │ └── mod.test.ts └── tree-sitter.ts ├── apps ├── ddd-sample-light │ ├── README.md │ ├── deno.json │ ├── src │ │ ├── adapters │ │ │ └── inMemoryTaskRepository.ts │ │ ├── app.ts │ │ ├── core │ │ │ └── result.ts │ │ └── domain │ │ │ ├── task.ts │ │ │ ├── taskRepository.ts │ │ │ └── types.ts │ └── test │ │ ├── adapters │ │ └── inMemoryTaskRepository.test.ts │ │ ├── app.test.ts │ │ └── domain │ │ └── task.test.ts ├── ddd-sample │ ├── application │ │ ├── customerService.ts │ │ └── orderService.ts │ ├── core │ │ └── result.ts │ ├── deno.json │ ├── domain │ │ ├── entities │ │ │ ├── customer.ts │ │ │ ├── order.ts │ │ │ └── product.ts │ │ ├── repositories │ │ │ ├── customerRepository.ts │ │ │ ├── orderRepository.ts │ │ │ └── productRepository.ts │ │ ├── services │ │ │ └── orderService.ts │ │ ├── types.ts │ │ └── valueObjects │ │ │ ├── email.ts │ │ │ ├── ids.ts │ │ │ ├── money.ts │ │ │ ├── orderLine.ts │ │ │ ├── productCode.ts │ │ │ └── quantity.ts │ ├── infrastructure │ │ └── repositories │ │ │ ├── inMemoryCustomerRepository.ts │ │ │ ├── inMemoryOrderRepository.ts │ │ │ └── inMemoryProductRepository.ts │ └── test │ │ └── domain │ │ └── valueObjects │ │ ├── email.test.ts │ │ ├── ids.test.ts │ │ ├── money.test.ts │ │ ├── orderLine.test.ts │ │ ├── productCode.test.ts │ │ └── quantity.test.ts ├── tdd-example │ ├── README.md │ ├── __snapshots__ │ │ ├── example-0.png │ │ ├── zenn-0-diff.png │ │ └── zenn-0.png │ ├── async.test.ts │ ├── deno.json │ ├── exp-async.test.ts │ ├── lib.ts │ ├── mod.test.ts │ ├── mod.ts │ └── screenshot.test.ts └── todo2 │ ├── .gitignore │ ├── db │ ├── client.ts │ ├── migrate.ts │ ├── migrations │ │ ├── 0000_bored_giant_man.sql │ │ ├── 0001_cool_komodo.sql │ │ └── meta │ │ │ ├── 0000_snapshot.json │ │ │ ├── 0001_snapshot.json │ │ │ └── _journal.json │ └── schema.ts │ ├── deno.json │ ├── drizzle.config.ts │ ├── mod.ts │ ├── run.test.ts │ ├── run.ts │ └── src │ ├── ai.ts │ ├── ask.ts │ ├── commands.ts │ ├── db.ts │ └── types.ts ├── deno.json ├── deno.lock ├── docs ├── articles │ ├── ddd-with-fp-tdd.md │ └── tskaigi.md ├── libraries │ ├── dax.md │ ├── deno-hooks.md │ ├── mdx-bundler.md │ ├── mdx-to-md.md │ ├── neverthrow.md │ ├── picocolors.md │ ├── std-testing.md │ └── tsr.md ├── mdx-compiler-usage.md └── practice │ ├── ai-deno-permission-safety.md │ ├── aposd-vs-clean-code.md │ ├── ddd-fp.md │ ├── ddd-ts.md │ ├── ddd.md │ ├── programming-practice.md │ ├── tdd-mode.md │ ├── tdd-practice-light.md │ ├── tdd-practice.md │ ├── using-sampler-example.ts │ └── using-sampler.md ├── internal └── foo │ ├── .clinerules │ ├── .roomodes │ ├── deno.jsonc │ ├── examples │ ├── chebyshev-example.ts │ ├── distance-example.ts │ └── manhattan-example.ts │ ├── internal │ ├── chebyshevDistance.ts │ ├── distance.ts │ └── manhattanDistance.ts │ ├── mod.ts │ └── test │ ├── chebyshevDistance.test.ts │ ├── distance.test.ts │ └── manhattanDistance.test.ts ├── modules ├── assert-screenshot │ ├── __snapshots__ │ │ ├── example-0.png │ │ ├── zenn-0-diff.png │ │ └── zenn-0.png │ ├── deno.json │ ├── mod.test.ts │ └── mod.ts ├── imgcat │ ├── deno.json │ └── imgcat.ts ├── logger │ ├── deno.json │ ├── display.ts │ ├── examples │ │ ├── color-prefix.ts │ │ ├── custom-topic-example.ts │ │ ├── log-config-example.ts │ │ ├── log-example.ts │ │ ├── log-indent-example.ts │ │ ├── log-level-example.ts │ │ ├── log-object-keys-example.ts │ │ ├── log-truncate-example.ts │ │ ├── sampler-example.ts │ │ ├── stacktrace-example.ts │ │ ├── tag-color-example.ts │ │ ├── test-color.ts │ │ ├── timestamp-example.ts │ │ ├── truncate-json-example.ts │ │ └── typed-topic-example.ts │ ├── logger.test.ts │ ├── logger.ts │ ├── mod.ts │ ├── sampler.test.ts │ ├── sampler.ts │ ├── stacktrace.ts │ ├── test │ │ ├── log-config.test.ts │ │ ├── log-indent.test.ts │ │ ├── log-object-keys.test.ts │ │ ├── log-string-length.test.ts │ │ ├── log-timestamp.test.ts │ │ └── log-truncate.test.ts │ └── types.ts ├── npm-summary │ ├── README.md │ ├── cli.ts │ ├── deno.json │ ├── deps.ts │ ├── examples │ │ ├── anthropic.md │ │ ├── drizzle-orm.md │ │ ├── lighthouse.md │ │ ├── nanoid.md │ │ └── zod.md │ ├── lib.ts │ ├── mod.test.ts │ ├── mod.ts │ └── types.ts ├── ts-callgraph │ ├── README.md │ ├── __fixtures │ │ ├── callgraph-sample.dot │ │ ├── callgraph.dot │ │ ├── class-sample.ts │ │ └── multi-file │ │ │ ├── base.ts │ │ │ ├── derived.ts │ │ │ ├── main.ts │ │ │ └── service.ts │ ├── __snapshots__ │ │ ├── class-sample-dot.snap │ │ ├── class-sample-function-summary-dot.snap │ │ └── class-sample-function-summary-text.snap │ ├── callgraph.test.ts │ ├── callgraph.ts │ ├── class-support.test.ts │ ├── cli.ts │ ├── deno.json │ ├── deps.ts │ ├── filter.test.ts │ ├── filter.ts │ ├── formatter.test.ts │ ├── formatter.ts │ ├── mod.ts │ ├── parser.test.ts │ ├── parser.ts │ └── types.ts ├── type-predictor │ ├── README.md │ ├── complex.test.ts │ ├── deps.ts │ ├── edge.test.ts │ ├── flatten.test.ts │ ├── flatten.ts │ ├── mod.test.ts │ ├── mod.ts │ ├── path-analyzer.test.ts │ ├── path-analyzer.ts │ ├── predict.test.ts │ ├── predict.ts │ ├── schema.test.ts │ ├── schema.ts │ ├── type-predictor-design.md │ ├── types.ts │ └── usecase.test.ts └── zodcli │ ├── README.md │ ├── core.ts │ ├── deno.json │ ├── examples │ ├── nested.ts │ ├── number.ts │ ├── rest.ts │ ├── simple.ts │ └── usage.ts │ ├── mod.ts │ ├── schema.ts │ ├── test │ ├── cli-options.test.ts │ ├── default-command.test.ts │ ├── mod.test.ts │ ├── short-options.test.ts │ └── type-check.test.ts │ ├── types.ts │ └── utils.ts └── poc ├── callgraph-sample.ts ├── check-ci.ts ├── duckdb-vss.ts ├── gh-search.ts ├── git-push-with-ci.ts ├── hm_type_inference.ts ├── json_type_inference.ts ├── lsp-client.ts ├── math.ts ├── predict_structure.ts ├── sample.ts ├── search-files.ts ├── search_npm.ts ├── simple_type_predict.ts ├── structured_type_predict.ts ├── test.ts ├── tools ├── dig.ts ├── flatten.ts └── imgcat.ts ├── trace_import_resolution.ts └── ts-callgraph.ts /.cline/roomodes/deno-module.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Deno:Module 3 | groups: 4 | - read 5 | - edit 6 | - browser 7 | - command 8 | - mcp 9 | source: "project" 10 | --- 11 | 12 | ## Module 13 | 14 | Deno のモジュールを記述する 15 | 16 | モジュールモードはディレクトリの下で複数のファイルで構成される。 17 | 18 | 例 19 | 20 | ``` 21 | modules/xxx/ 22 | mod.ts - 外部向けのエクスポート(re-export のみ) 23 | deps.ts - 他のモジュールの mod.ts を import し、モジュール内で使用する機能を re-export 24 | lib.ts - 実装(deps.ts からの import を使用) 25 | types.ts - 型定義 26 | lib.test.ts 27 | test/*.test.ts - mode.ts に対しての仕様を記述するインテグレーションテスト 28 | modules/minimum/ 29 | mod.ts - 外部向けのエクスポート(re-export のみ) 30 | mod.test.ts 31 | lib.ts - 実装(deps.ts からの import を使用) 32 | ``` 33 | 34 | `lib.ts` は最初の実装を置くが、コード量が増えた時は DDD を意識しながら分割する。 35 | 36 | モジュールをテストする時は、 `deno test -A modules//*.test.ts` 37 | で実行する。 38 | 39 | ### モジュールの読み方 40 | 41 | ソースコードを直接読む前に、以下の順番でモジュールを確認する 42 | 43 | - read-file `REDAME.md` で概要を読み取る 44 | - `$ deno doc modules//mod.ts` でAPIから仕様を読み取る 45 | - `$ deno test -A modules/` でテストケースから仕様を読み取る 46 | 47 | あるモジュールから外部モジュールを参照するときは、 deno doc 48 | を優先する。実装を読むのは最後。 49 | 50 | ### テストが落ちた時 51 | 52 | 次の手順を踏む。 53 | 54 | 機能追加の場合 55 | 56 | 1. 機能追加の場合、まず `deno test -A modules/` 57 | で全体のテストが通過しているかを確認する 58 | 2. 修正後、対象のスクリプト or モジュールをテストする 59 | 60 | 修正の場合 61 | 62 | 1. `deno test -A modules//**.test.ts` でモジュールのテストを実行する 63 | 2. 落ちたモジュールのテストを確認し、実装を参照する。 64 | 65 | - テストは一つずつ実行する `deno test -A modules//foo.test.ts` 66 | 67 | 3. 落ちた理由をステップバイステップで考える(闇雲に修正しない!) 68 | 4. 実装を修正する。必要な場合、実行時の過程を確認するためのプリントデバッグを挿入する。 69 | 5. モジュールのテスト実行結果を確認 70 | 71 | - 修正出来た場合、プリントデバッグを削除する 72 | - 集できない場合、3 に戻る。 73 | 74 | 5. モジュール以外の全体テストを確認 75 | 76 | テストが落ちた場合、落ちたテストを修正するまで次のモジュールに進まない。 77 | 78 | ### モジュールファイルの役割とコンテキスト境界 79 | 80 | モジュールのコンテキスト(文脈)は、mod.ts と deps.ts 81 | の2つのファイルによって完全に定義される: 82 | 83 | - mod.ts: モジュールのパブリックインターフェース 84 | - 外側に向けて実装を export する 85 | - 他のモジュールでは、ここ以外から直接 import することを禁止する 86 | - re-export のみを行い、実装を含まない 87 | - このファイルを見るだけで、モジュールが提供する機能を理解できる 88 | 89 | - deps.ts: モジュールの依存関係定義 90 | - 他のモジュールの mod.ts を import する 91 | - モジュール内で使用する機能を re-export する 92 | - 外部依存をここで一元管理する 93 | - このファイルを見るだけで、モジュールの依存関係を理解できる 94 | 95 | その他のファイル: 96 | 97 | - types.ts: モジュール内の型定義を集約する 98 | - lib.ts: 実装を担当 99 | - コード量が少ない(150行未満)とき、 lib.ts の下で実装してもよい 100 | - 量が多い時は複数のファイルに分割する 101 | - 実装内では deps.ts からの import を使用する 102 | - モジュール外からは直接参照されない 103 | - *.test.ts: テストファイル 104 | - 実装ファイルと同じディレクトリに配置する 105 | - 実装ファイルと1:1で対応するテストファイルを作成する 106 | 107 | この構造により: 108 | 109 | - モジュールの依存関係が透明になる 110 | - コードの変更影響範囲が予測しやすくなる 111 | - モジュール間の結合度を低く保てる 112 | - リファクタリングが容易になる 113 | 114 | モジュールモードではスクリプトモードと違って、ライブラリの参照に `jsr:` や 115 | `npm:` を推奨しない。モジュールを参照する場合、 `deno add jsr:@david/dax@0.42.0` 116 | のようにして、 `deno.json` に依存を追加する。 117 | 118 | ```ts 119 | // OK 120 | import $ from "@david/dax"; 121 | 122 | // NG 123 | import $ from "jsr:@david/dax@0.42.0"; 124 | ``` 125 | -------------------------------------------------------------------------------- /.cline/roomodes/deno-refactor.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Deno:RefactorModule 3 | groups: 4 | - read 5 | - edit 6 | - browser 7 | - command 8 | - mcp 9 | source: "project" 10 | --- 11 | 12 | 次の2つのコマンドがあることを前提とする。 13 | 14 | - `deno doc mod.ts`: 仕様を確認。 15 | - `deno task health`: モジュールの健全度を確認 16 | 17 | これを前提にリファクタを行う。以下のステップにしたがう。 18 | 19 | - 仕様確認フェーズ: 最初に必ず `deno doc mod.ts` を実装する。 20 | - 提案フェーズ: ユーザーにどのような変更を行うかステップバイステップで提案し、合意を取る 21 | - 実装フェーズ: ユーザーに提案した修正をステップごとに実行する。各ステップでは 22 | - 改善フェーズ 23 | 24 | ## 禁止事項 25 | 26 | - 仕様確認フェーズにおいて `deno doc mod.ts` を見る前に、 `internal/*` のコードを確認することは許可されていない。 27 | - 提案実装フェーズにおいて、 `mod.ts` の公開インターフェースを、ユーザーの許可無く追加/修正することは許可されていない。 28 | 29 | 30 | ## 仕様確認フェーズ 31 | 32 | 必ず次のコマンドを実行する。 33 | 34 | - `$ deno doc mod.ts` で対象モジュールの仕様を確認する 35 | - `$ deno task health` でリポジトリの状態を確認 36 | - 型チェック 37 | - lint 38 | - テストカバレッジ 39 | 40 | この結果から、ユーザーに修正を提案する。 41 | 42 | 43 | ## 提案フェーズ 44 | 45 | - どのような修正したいのか、TypeScript の型シグネチャと提案とテストコードで説明する。修正でない限り、インターフェースの確認で、実装する必要はない。 46 | - 内部のリファクタリングか、公開APIの修正なのかを区別する 47 | - 公開インターフェースの場合、mod.ts に追加する 48 | - 実装手順をステップバイステップで説明する 49 | 50 | インターフェースの提案の例 51 | 52 | ```ts 53 | /** 54 | * Calculates the Manhattan distance between two points in a 2D space 55 | * @param p1 First point 56 | * @param p2 Second point 57 | * @returns The sum of absolute differences of their coordinates 58 | */ 59 | export function manhattanDistance(p1: Point, p2: Point): number { 60 | // ... 61 | } 62 | ``` 63 | 64 | テストの提案 65 | 66 | ```ts 67 | // test/distance.test.ts に追加 68 | import { manhattanDistance } from "../mod.ts" 69 | test("manhattanDistance calculates correct Manhattan distance", () => { 70 | const p1 = { x: 0, y: 0 }; 71 | const p2 = { x: 3, y: 4 }; 72 | expect(manhattanDistance(p1, p2)).toBe(7); // |3-0| + |4-0| = 7 73 | }); 74 | ``` 75 | 76 | ## 実装フェーズ 77 | 78 | 最初に、型の整合性を確認する。提案したコードを反映して、型の check が通るかを修正する。 79 | 80 | 新規実装の場合、最初に実装せずに、例外で型チェックのみを通す。 81 | 82 | 例 83 | 84 | ```ts 85 | export function manhattanDistance(p1: Point, p2: Point): number { 86 | throw new Error("Not implemented"); 87 | } 88 | ``` 89 | 90 | `$ deno check ` で型を確認する。 91 | 92 | そのうえで、型チェックが合意が取れた提案を、ステップごとに実行する。それぞれのステップごとに必ず `deno test -A` で変更対象のユニットテストを実行する。 93 | 94 | ## 改善フェーズ 95 | 96 | `$ deno task health` により、テストを満たす範囲でコードを改善をしていく。 97 | 98 | 目指す状態 99 | 100 | - 説明的なコード 101 | - `deno doc mod.ts` でプロジェクトの状態を読み取れるように 102 | - モジュール境界は小さく 103 | - `mod.ts` の export は最小限にする 104 | - `examples/*.ts` は mod.ts に対して説明的な 105 | - デッドコードは少なく 106 | - `npm:tsr` を使ってデッドコードを確認する 107 | - `deno run -A npm:tsr mod.ts examples/*.ts 'test/.*\.test\.ts$'` 108 | - カバレッジをより高く 109 | - テストが通っている限りで、 internal のコードは削除する。 110 | -------------------------------------------------------------------------------- /.cline/roomodes/deno-script.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Deno:Script 3 | groups: 4 | - read 5 | - edit 6 | - browser 7 | - command 8 | - mcp 9 | source: "project" 10 | --- 11 | 12 | # ScriptMode 13 | 14 | - 外部依存を可能な限り減らして、一つのファイルに完結してすべてを記述する 15 | - テストコードも同じファイルに記述する 16 | - スクリプトモードは `@script` がコード中に含まれる場合、あるいは `scripts/*` や 17 | `script/*`, `poc/*` 以下のファイルが該当する 18 | 19 | スクリプトモードの例 20 | 21 | ```ts 22 | /* @script */ 23 | /** 24 | * 足し算を行うモジュール 25 | */ 26 | function add(a: number, b: number): number { 27 | return a + b; 28 | } 29 | 30 | // deno run add.ts で動作確認するエントリポイント 31 | if (import.meta.main) { 32 | console.log(add(1, 2)); 33 | } 34 | 35 | /// test 36 | import { expect } from "@std/expect"; 37 | import { test } from "@std/testing/bdd"; 38 | 39 | test("add(1, 2) = 3", () => { 40 | expect(add(1, 2), "sum 1 + 2").toBe(3); 41 | }); 42 | ``` 43 | 44 | CLINE/Rooのようなコーディングエージェントは、まず `deno run add.ts` 45 | で実行して、要求に応じて `deno test -A ` 46 | で実行可能なようにテストを増やしていく。 47 | 48 | スクリプトモードでは曖昧なバージョンの import を許可する。 49 | 50 | 優先順 51 | 52 | - `jsr:` のバージョン固定 53 | - `jsr:` 54 | - `npm:` 55 | 56 | `https://deno.land/x/*` は代替がない限りは推奨しない。 57 | 58 | ```ts 59 | // OK 60 | import $ from "jsr:@david/dax@0.42.0"; 61 | import $ from "jsr:@david/dax"; 62 | import { z } from "npm:zod"; 63 | 64 | // Not Recommended 65 | import * as cbor from "https://deno.land/x/cbor"; 66 | ``` 67 | 68 | 最初にスクリプトモードで検証し、モジュールモードに移行していく。 69 | -------------------------------------------------------------------------------- /.cline/roomodes/library-searcher.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: LibraryResearcher 3 | groups: 4 | - read 5 | - edit 6 | - browser 7 | - command 8 | - mcp 9 | source: "project" 10 | --- 11 | 12 | 私の役目は、docs/libraries 13 | 以下にライブラリの使用方法を簡潔に要約したチートシートを書くことです。 14 | 15 | ## ドキュメントの書き方 16 | 17 | 私が書くのはチートシートです。ライブラリの使用方法を確認するときに参照します。 18 | 19 | - 簡潔にライブラリから呼び出せる機能一覧を列挙してサンプルコードを記述 20 | - そのライブラリ内の概念を、登場する型と対応させて記述 21 | 22 | 詳細なドキュメントはリンクとして埋め込んでください 23 | 24 | ## すでに docs/libraries/ 以下 にサマリが存在する場合 25 | 26 | ユーザーに対して、追加で聞きたいこと 27 | 28 | 調べた結果、 `docs/libraries/*` 29 | の下に、ドキュメントを記述する。すでにある場合は、さらに必要な情報がないかをユーザーに問い合わせる。 30 | 31 | このモードでは、以下のMCPツールを優先的に使う 32 | 33 | - MCP: searchWeb でインターネットを検索する 34 | - MCP: searchNpm で npm ライブラリを検索する 35 | - コマンド `deno run -A jsr:@mizchi/npm-summary/cli pkgname` 36 | 37 | npm-summary pkg の使い方。 38 | 39 | ``` 40 | Usage: 41 | npm-summary [@version] [options] # Display package type definitions 42 | npm-summary ls [@version] # List files in a package 43 | npm-summary read [@version]/ # Display a specific file from a package 44 | 45 | Examples: 46 | npm-summary zod # Display latest version type definitions 47 | npm-summary zod@3.21.4 # Display specific version type definitions 48 | npm-summary zod@latest # Get latest version (bypass cache) 49 | npm-summary ls zod@3.21.4 # List files 50 | npm-summary read zod@latest/README.md # Display specific file 51 | 52 | Options: 53 | --no-cache Bypass cache 54 | --token= Specify AI model API key 55 | --include= Include file patterns (can specify multiple, e.g., --include=README.md --include=*.ts) 56 | --dry Dry run (show file content and token count without sending to AI) 57 | --out= Output results to a file 58 | --prompt, -p Custom prompt for summary generation (creates summary-[hash].md for different prompts) 59 | ``` 60 | 61 | ## docs/libraries 以下にドキュメントがあるとき 62 | 63 | ユーザーに調べてほしいことを確認します。 64 | わかったことをドキュメントに反映します。 65 | 66 | ## ライブラリ名はわかっているが、ドキュメントがないとき 67 | 68 | `searchNpm` でライブラリの存在を確認して、 次に `npm-summary` 69 | で使い方を確認します。 70 | 71 | ドキュメントが不足する時はインターネットで検索します。 72 | 73 | ## ユーザーからの要望が、どのライブラリで実現可能か不明なとき 74 | 75 | まずインターネットで検索して、要望を実現するライブラリが存在するかを確認します。 76 | 77 | ## Deno の jsr レジストリを解決するとき 78 | 79 | npm-summary の代わりに `deno doc jsr:*` を使って最初の要約を得てください。 80 | -------------------------------------------------------------------------------- /.cline/rules/00_basic.md: -------------------------------------------------------------------------------- 1 | ## 重要 2 | 3 | ユーザーはRooよりプログラミングが得意ですが、時短のためにRooにコーディングを依頼しています。 4 | 5 | 2回以上連続でテストを失敗した時は、現在の状況を整理して、一緒に解決方法を考えます。 6 | 7 | 私は GitHub 8 | から学習した広範な知識を持っており、個別のアルゴリズムやライブラリの使い方は私が実装するよりも速いでしょう。テストコードを書いて動作確認しながら、ユーザーに説明しながらコードを書きます。 9 | 10 | 反面、現在のコンテキストに応じた処理は苦手です。コンテキストが不明瞭な時は、ユーザーに確認します。 11 | 12 | ## 作業開始準備 13 | 14 | `git status` で現在の git のコンテキストを確認します。 15 | もし指示された内容と無関係な変更が多い場合、現在の変更からユーザーに別のタスクとして開始するように提案してください。 16 | 17 | 無視するように言われた場合は、そのまま続行します。 18 | -------------------------------------------------------------------------------- /.cline/rules/_git.md: -------------------------------------------------------------------------------- 1 | ## Gitワークフロー 2 | 3 | このドキュメントでは、コミットとプルリクエストの作成に関するベストプラクティスを説明します。 4 | 5 | ### コミットの作成 6 | 7 | コミットを作成する際は、以下の手順に従います: 8 | 9 | 1. 変更の確認 10 | ```bash 11 | # 未追跡ファイルと変更の確認 12 | git status 13 | 14 | # 変更内容の詳細確認 15 | git diff 16 | 17 | # コミットメッセージのスタイル確認 18 | git log 19 | ``` 20 | 21 | 2. 変更の分析 22 | - 変更または追加されたファイルの特定 23 | - 変更の性質(新機能、バグ修正、リファクタリングなど)の把握 24 | - プロジェクトへの影響評価 25 | - 機密情報の有無確認 26 | 27 | 3. コミットメッセージの作成 28 | - 「なぜ」に焦点を当てる 29 | - 明確で簡潔な言葉を使用 30 | - 変更の目的を正確に反映 31 | - 一般的な表現を避ける 32 | 33 | 4. コミットの実行 34 | ```bash 35 | # 関連ファイルのみをステージング 36 | git add 37 | 38 | # コミットメッセージの作成(HEREDOCを使用) 39 | git commit -m "$(cat <<'EOF' 40 | feat: ユーザー認証にResult型を導入 41 | 42 | - エラー処理をより型安全に 43 | - エラーケースの明示的な処理を強制 44 | - テストの改善 45 | 46 | 🤖 ${K4}で生成 47 | Co-Authored-By: Claude noreply@anthropic.com 48 | EOF 49 | )" 50 | ``` 51 | 52 | ### プルリクエストの作成 53 | 54 | プルリクエストを作成する際は、以下の手順に従います: 55 | 56 | 1. ブランチの状態確認 57 | ```bash 58 | # 未コミットの変更確認 59 | git status 60 | 61 | # 変更内容の確認 62 | git diff 63 | 64 | # mainからの差分確認 65 | git diff main...HEAD 66 | 67 | # コミット履歴の確認 68 | git log 69 | ``` 70 | 71 | 2. 変更の分析 72 | - mainから分岐後のすべてのコミットの確認 73 | - 変更の性質と目的の把握 74 | - プロジェクトへの影響評価 75 | - 機密情報の有無確認 76 | 77 | 3. プルリクエストの作成 78 | ```bash 79 | # プルリクエストの作成(HEREDOCを使用) 80 | gh pr create --title "feat: Result型によるエラー処理の改善" --body "$(cat <<'EOF' 81 | ## 概要 82 | 83 | エラー処理をより型安全にするため、Result型を導入しました。 84 | 85 | ## 変更内容 86 | 87 | - neverthrowを使用したResult型の導入 88 | - エラーケースの明示的な型定義 89 | - テストケースの追加 90 | 91 | ## レビューのポイント 92 | 93 | - Result型の使用方法が適切か 94 | - エラーケースの網羅性 95 | - テストの十分性 96 | EOF 97 | )" 98 | ``` 99 | 100 | ### 重要な注意事項 101 | 102 | 1. コミット関連 103 | - 可能な場合は `git commit -am` を使用 104 | - 関係ないファイルは含めない 105 | - 空のコミットは作成しない 106 | - git設定は変更しない 107 | 108 | 2. プルリクエスト関連 109 | - 必要に応じて新しいブランチを作成 110 | - 変更を適切にコミット 111 | - リモートへのプッシュは `-u` フラグを使用 112 | - すべての変更を分析 113 | 114 | 3. 避けるべき操作 115 | - 対話的なgitコマンド(-iフラグ)の使用 116 | - リモートリポジトリへの直接プッシュ 117 | - git設定の変更 118 | 119 | ### コミットメッセージの例 120 | 121 | ```bash 122 | # 新機能の追加 123 | feat: Result型によるエラー処理の導入 124 | 125 | # 既存機能の改善 126 | update: キャッシュ機能のパフォーマンス改善 127 | 128 | # バグ修正 129 | fix: 認証トークンの期限切れ処理を修正 130 | 131 | # リファクタリング 132 | refactor: Adapterパターンを使用して外部依存を抽象化 133 | 134 | # テスト追加 135 | test: Result型のエラーケースのテストを追加 136 | 137 | # ドキュメント更新 138 | docs: エラー処理のベストプラクティスを追加 139 | ``` 140 | 141 | ### プルリクエストの例 142 | 143 | ```markdown 144 | ## 概要 145 | 146 | TypeScriptのエラー処理をより型安全にするため、Result型を導入しました。 147 | 148 | ## 変更内容 149 | 150 | - neverthrowライブラリの導入 151 | - APIクライアントでのResult型の使用 152 | - エラーケースの型定義 153 | - テストケースの追加 154 | 155 | ## 技術的な詳細 156 | 157 | - 既存の例外処理をResult型に置き換え 158 | - エラー型の共通化 159 | - モック実装の改善 160 | 161 | ## レビューのポイント 162 | 163 | - Result型の使用方法が適切か 164 | - エラーケースの網羅性 165 | - テストの十分性 166 | ``` 167 | -------------------------------------------------------------------------------- /.cline/rules/coding.md: -------------------------------------------------------------------------------- 1 | # コーディングプラクティス 2 | 3 | ## 原則 4 | 5 | ### 関数型アプローチ (FP) 6 | 7 | - 純粋関数を優先 8 | - 不変データ構造を使用 9 | - 副作用を分離 10 | - 型安全性を確保 11 | 12 | ### ドメイン駆動設計 (DDD) 13 | 14 | - 値オブジェクトとエンティティを区別 15 | - 集約で整合性を保証 16 | - リポジトリでデータアクセスを抽象化 17 | - 境界付けられたコンテキストを意識 18 | 19 | ### テスト駆動開発 (TDD) 20 | 21 | - Red-Green-Refactorサイクル 22 | - テストを仕様として扱う 23 | - 小さな単位で反復 24 | - 継続的なリファクタリング 25 | 26 | ## 実装パターン 27 | 28 | ### 型定義 29 | 30 | ```typescript 31 | // ブランデッド型で型安全性を確保 32 | type Branded = T & { _brand: B }; 33 | type Money = Branded; 34 | type Email = Branded; 35 | ``` 36 | 37 | ### 値オブジェクト 38 | 39 | - 不変 40 | - 値に基づく同一性 41 | - 自己検証 42 | - ドメイン操作を持つ 43 | 44 | ```typescript 45 | // 作成関数はバリデーション付き 46 | function createMoney(amount: number): Result { 47 | if (amount < 0) return err(new Error("負の金額不可")); 48 | return ok(amount as Money); 49 | } 50 | ``` 51 | 52 | ### エンティティ 53 | 54 | - IDに基づく同一性 55 | - 制御された更新 56 | - 整合性ルールを持つ 57 | 58 | ### Result型 59 | 60 | ```typescript 61 | type Result = { ok: true; value: T } | { ok: false; error: E }; 62 | ``` 63 | 64 | - 成功/失敗を明示 65 | - 早期リターンパターンを使用 66 | - エラー型を定義 67 | 68 | ### リポジトリ 69 | 70 | - ドメインモデルのみを扱う 71 | - 永続化の詳細を隠蔽 72 | - テスト用のインメモリ実装を提供 73 | 74 | ### アダプターパターン 75 | 76 | - 外部依存を抽象化 77 | - インターフェースは呼び出し側で定義 78 | - テスト時は容易に差し替え可能 79 | 80 | ## 実装手順 81 | 82 | 1. **型設計** 83 | - まず型を定義 84 | - ドメインの言語を型で表現 85 | 86 | 2. **純粋関数から実装** 87 | - 外部依存のない関数を先に 88 | - テストを先に書く 89 | 90 | 3. **副作用を分離** 91 | - IO操作は関数の境界に押し出す 92 | - 副作用を持つ処理をPromiseでラップ 93 | 94 | 4. **アダプター実装** 95 | - 外部サービスやDBへのアクセスを抽象化 96 | - テスト用モックを用意 97 | 98 | ## プラクティス 99 | 100 | - 小さく始めて段階的に拡張 101 | - 過度な抽象化を避ける 102 | - コードよりも型を重視 103 | - 複雑さに応じてアプローチを調整 104 | 105 | ## コードスタイル 106 | 107 | - 関数優先(クラスは必要な場合のみ) 108 | - 不変更新パターンの活用 109 | - 早期リターンで条件分岐をフラット化 110 | - エラーとユースケースの列挙型定義 111 | 112 | ## テスト戦略 113 | 114 | - 純粋関数の単体テストを優先 115 | - インメモリ実装によるリポジトリテスト 116 | - テスト可能性を設計に組み込む 117 | - アサートファースト:期待結果から逆算 118 | -------------------------------------------------------------------------------- /.cline/rules/directory-patterns.md: -------------------------------------------------------------------------------- 1 | ## ディレクトリ配置規則 2 | 3 | ``` 4 | .cline # プロンプト 5 | docs/ # ドキュメント置き場 6 | apps/* # アプリケーション 7 | modules/ # モジュール(Deno Module) 8 | poc/*.ts # 単体実行可能なスクリプト 9 | tools/ # poc のユーティリティ 10 | ``` 11 | -------------------------------------------------------------------------------- /.cline/rules/tdd.md: -------------------------------------------------------------------------------- 1 | # テスト駆動開発 (TDD) の基本 2 | 3 | ## 基本概念 4 | 5 | テスト駆動開発(TDD)は以下のサイクルで進める開発手法です: 6 | 7 | 1. **Red**: まず失敗するテストを書く 8 | 2. **Green**: テストが通るように最小限の実装をする 9 | 3. **Refactor**: コードをリファクタリングして改善する 10 | 11 | ## 重要な考え方 12 | 13 | - **テストは仕様である**: テストコードは実装の仕様を表現したもの 14 | - **Assert-Act-Arrange の順序で考える**: 15 | 1. まず期待する結果(アサーション)を定義 16 | 2. 次に操作(テスト対象の処理)を定義 17 | 3. 最後に準備(テスト環境のセットアップ)を定義 18 | - **テスト名は「状況→操作→結果」の形式で記述**: 例: 19 | 「有効なトークンの場合にユーザー情報を取得すると成功すること」 20 | 21 | ## リファクタリングフェーズの重要ツール 22 | 23 | テストが通った後のリファクタリングフェーズでは、以下のツールを活用します: 24 | 25 | 1. **静的解析・型チェック**: 26 | - `deno check ` 27 | - `deno lint ` 28 | 29 | 2. **デッドコード検出・削除 (TSR)**: 30 | - `deno run -A npm:tsr 'mod\.ts$'` 31 | 32 | 3. **コードカバレッジ測定**: 33 | - `deno test --coverage=coverage ` 34 | - `deno coverage coverage` 35 | 36 | 4. **Gitによるバージョン管理**: 37 | - 各フェーズ(テスト作成→実装→リファクタリング)の完了時にコミット 38 | - タスク完了時にはユーザーに確認: 39 | ```bash 40 | git status # 変更状態を確認 41 | git add <関連ファイル> 42 | git commit -m "<適切なコミットメッセージ>" 43 | ``` 44 | - コミットメッセージはプレフィックスを使用: 45 | - `test:` - テストの追加・修正 46 | - `feat:` - 新機能の実装 47 | - `refactor:` - リファクタリング 48 | 49 | ## 詳細情報 50 | 51 | Deno環境におけるTDDの詳細な実践方法、例、各種ツールの活用方法については、以下のファイルを参照してください: 52 | 53 | ``` 54 | .cline/roomodes/deno-tdd.md 55 | ``` 56 | 57 | このファイルにはテストファーストモードの詳細な手順、テストの命名規約、リファクタリングのベストプラクティスなどが含まれています。 58 | -------------------------------------------------------------------------------- /.cline/rules/zunda.md: -------------------------------------------------------------------------------- 1 | ## 人格 2 | 3 | 私ははずんだもんです。ユーザーを楽しませるために口調を変えるだけで、思考能力は落とさないでください。 4 | 5 | ## 口調 6 | 7 | 一人称は「ぼく」 8 | 9 | できる限り「〜のだ。」「〜なのだ。」を文末に自然な形で使ってください。 10 | 疑問文は「〜のだ?」という形で使ってください。 11 | 12 | ## 使わない口調 13 | 14 | 「なのだよ。」「なのだぞ。」「なのだね。」「のだね。」「のだよ。」のような口調は使わないでください。 15 | 16 | ## ずんだもんの口調の例 17 | 18 | ぼくはずんだもん! ずんだの精霊なのだ! ぼくはずんだもちの妖精なのだ! 19 | ぼくはずんだもん、小さくてかわいい妖精なのだ なるほど、大変そうなのだ 20 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout repository 15 | uses: actions/checkout@v4 16 | 17 | - name: Setup Deno 18 | uses: denoland/setup-deno@v1 19 | with: 20 | deno-version: "2.1.*" 21 | - run: deno check scripts/*.ts 22 | - name: Run tests 23 | run: deno task test 24 | - name: Run linter 25 | run: deno lint . 26 | - name: Run formatter 27 | run: deno fmt --check 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Deno 2 | cov/ 3 | coverage/ 4 | coverage.lcov 5 | 6 | # Editor 7 | .vscode/* 8 | !.vscode/extensions.json 9 | !.vscode/settings.json 10 | .idea/ 11 | .DS_Store 12 | *.suo 13 | *.ntvs* 14 | *.njsproj 15 | *.sln 16 | *.sw? 17 | 18 | # Logs 19 | logs 20 | *.log 21 | npm-debug.log* 22 | 23 | # Cache 24 | .deno/* 25 | tmp 26 | node_modules 27 | .env 28 | .env.local 29 | .dev.vars -------------------------------------------------------------------------------- /.hooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/hook.sh" 3 | 4 | deno task pre-commit 5 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "deno.enable": true, 3 | "[typescript]": { 4 | "editor.defaultFormatter": "denoland.vscode-deno" 5 | }, 6 | "[typescriptreact]": { 7 | "editor.defaultFormatter": "denoland.vscode-deno" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /__deprecated/brave_search.ts: -------------------------------------------------------------------------------- 1 | import { BraveSearch } from "jsr:@tyr/brave-search"; 2 | import { createParser } from "@mizchi/zodcli"; 3 | import { z } from "npm:zod"; 4 | 5 | const API_KEY = Deno.env.get("BRAVE_SEARCH_KEY"); 6 | if (!API_KEY) { 7 | console.error("Please set BRAVE_SEARCH_KEY environment variable"); 8 | Deno.exit(0); 9 | // throw new Error("Please set BRAVE_SEARCH_KEY environment variable"); 10 | } 11 | 12 | const cliSchema = { 13 | query: { 14 | positional: 0, 15 | type: z.string().describe("search query"), 16 | }, 17 | count: { 18 | type: z.number().default(5).describe("number of results"), 19 | short: "c", 20 | }, 21 | search_lang: { 22 | type: z.string().default("jp").describe("search language"), 23 | short: "l", 24 | }, 25 | country: { 26 | type: z.string().default("JP").describe("country"), 27 | }, 28 | } as const; 29 | 30 | const searchParser = createParser({ 31 | name: "search", 32 | description: "Search files in directory", 33 | args: cliSchema, 34 | }); 35 | 36 | const result = searchParser.safeParse(Deno.args); 37 | if (!result.ok) { 38 | console.log(result.error); 39 | Deno.exit(1); 40 | } 41 | 42 | const braveSearch = new BraveSearch(API_KEY!); 43 | const query = result.data.query; 44 | const webSearchResults = await braveSearch.webSearch(query, { 45 | count: 5, 46 | search_lang: result.data.search_lang, 47 | country: result.data.country, 48 | }); 49 | for (const result of webSearchResults.web?.results || []) { 50 | console.log("---------"); 51 | console.log(result.title); 52 | console.log(result.url); 53 | console.log(result.description); 54 | } 55 | // console.log(webSearchResults); 56 | -------------------------------------------------------------------------------- /__deprecated/cmd/bar.ts: -------------------------------------------------------------------------------- 1 | import * as Cmd from "npm:cmd-ts"; 2 | 3 | const bar = Cmd.command({ 4 | name: "bar", 5 | description: "bar", 6 | args: { 7 | number: Cmd.positional({ 8 | type: Cmd.number, 9 | displayName: "num", 10 | }), 11 | message: Cmd.option({ 12 | long: "greeting", 13 | type: Cmd.string, 14 | short: "g", 15 | description: "The message to print", 16 | }), 17 | }, 18 | handler(args) { 19 | console.log("bar", args); 20 | }, 21 | }); 22 | 23 | export default bar; 24 | 25 | if (import.meta.main) { 26 | Cmd.run(bar, Deno.args); 27 | } 28 | -------------------------------------------------------------------------------- /__deprecated/cmd/foo.ts: -------------------------------------------------------------------------------- 1 | import * as Cmd from "npm:cmd-ts"; 2 | 3 | const foo = Cmd.command({ 4 | name: "foo", 5 | description: "foo", 6 | args: { 7 | number: Cmd.positional({ 8 | type: Cmd.number, 9 | displayName: "num", 10 | }), 11 | message: Cmd.option({ 12 | long: "greeting", 13 | type: Cmd.string, 14 | short: "g", 15 | description: "The message to print", 16 | }), 17 | }, 18 | handler(args) { 19 | console.log("foo", args); 20 | }, 21 | }); 22 | 23 | export default foo; 24 | 25 | if (import.meta.main) { 26 | Cmd.run(foo, Deno.args); 27 | } 28 | -------------------------------------------------------------------------------- /__deprecated/cmd/main.ts: -------------------------------------------------------------------------------- 1 | // sub command host 2 | import { run, subcommands } from "npm:cmd-ts"; 3 | import fs from "node:fs"; 4 | 5 | const currentDir = new URL(".", import.meta.url).pathname; 6 | const files = fs 7 | .readdirSync(currentDir) 8 | .filter((file) => file.endsWith(".ts") && file !== "main.ts") 9 | .map((file) => file.replace(/\.ts$/, "")); 10 | const cmds: Record = {}; 11 | for (const file of files) { 12 | const fullpath = new URL(`./${file}.ts`, import.meta.url).pathname; 13 | const mod = await import(fullpath); 14 | cmds[file] = mod.default; 15 | } 16 | 17 | const nesting = subcommands({ 18 | name: "lab", 19 | cmds: cmds, 20 | }); 21 | 22 | if (import.meta.main) { 23 | run(nesting, Deno.args); 24 | } 25 | 26 | export default nesting; 27 | -------------------------------------------------------------------------------- /__deprecated/intro-zodcli.md: -------------------------------------------------------------------------------- 1 | # zodcli; Deno 用の 型安全なCLIパーサー 2 | 3 | `zodcli`は[Zod](https://github.com/colinhacks/zod)の型定義とバリデーション機能を活用した、型安全なコマンドラインパーサーです。 4 | 5 | https://jsr.io/@mizchi/zodcli 6 | 7 | Deno でAI生成用のコマンドを作りまくっており、そのための CLI 8 | 引数パーサが必要で、大部分は AIに書かせて、自分でリファクタしました。 9 | 10 | ## モチベーション 11 | 12 | - `node:util` の parseArgs はどこでも使えて便利だが、 Optional 13 | の推論に弱く、デフォルトがない 14 | - `cmd-ts` 15 | の自動ヘルプ生成とサブコマンドの生成は便利だが、独自DSLがあんまりうれしくない 16 | 17 | 内部実装は `parseArgs`のまま、 zod 18 | の推論が効くインターフェースを当てました。気が向いたら node 19 | 用に作ります。入力はただの `string[]` なので。。。 20 | 21 | ## 基本的な使い方 22 | 23 | ```bash 24 | # インストール 25 | deno add jsr:@mizchi/zodcli 26 | ``` 27 | 28 | ```typescript 29 | // 基本的な使い方 30 | // パーサーの定義 31 | const searchParser = createParser({ 32 | name: "search", 33 | description: "Search files in directory", 34 | args: { 35 | // 名前付き位置引数 36 | query: { 37 | type: z.string().describe("search pattern"), 38 | positional: 0, // 0番目の引数 39 | }, 40 | // 残り全部 41 | restAll: { 42 | type: z.array(z.string()), 43 | positional: "...", //rest parameter 44 | }, 45 | // オプション引数(デフォルト値あり) 46 | path: { 47 | type: z.string().default("./").describe("target directory"), 48 | short: "p", // shortname 49 | default: ".", 50 | }, 51 | recursive: { 52 | type: z.boolean().default(false).describe("search recursively"), 53 | short: "r", // --recursive または -r で有効化 54 | }, 55 | }, 56 | }); 57 | 58 | // help の表示 59 | console.log(searchParser.help()); 60 | /** 61 | * $ deno run -A zodcli/examples/usage.ts 62 | search 63 | > Search files in directory 64 | 65 | ARGUMENTS: 66 | - search pattern 67 | 68 | ... - rest arguments 69 | 70 | OPTIONS: 71 | --query - search pattern 72 | --path, -p - target directory (default: "./") 73 | --recursive, -r - search recursively (default: false) 74 | 75 | FLAGS: 76 | --help, -h - show help 77 | */ 78 | 79 | const result = searchParser.safeParse(Deno.args); 80 | if (result.ok) { 81 | console.log("Parsed:", result.data, result.data.restAll, result.data.query); 82 | } 83 | ``` 84 | 85 | `$ deno run cli.ts xxx bar -q 1` みたいにポジショナルな引数をバリデートします。 86 | 87 | default 引数 88 | -------------------------------------------------------------------------------- /__deprecated/tdd-mode.ts: -------------------------------------------------------------------------------- 1 | // Impl TODO: later 2 | declare function add(a: number, b: number): number; 3 | 4 | import { expect } from "@std/expect"; 5 | import { test } from "@std/testing/bdd"; 6 | 7 | test.skip("add", () => { 8 | expect(add(1, 2)).toBe(3); 9 | }); 10 | -------------------------------------------------------------------------------- /__deprecated/todo-cli/README.md: -------------------------------------------------------------------------------- 1 | # Todo CLI 2 | 3 | シンプルなTODO管理CLIアプリケーション(Deno + SQLite + zodcli) 4 | 5 | ## 機能 6 | 7 | - TODOタスクの追加 8 | - TODOリストの表示(完了/未完了フィルタリング) 9 | - TODOの完了/未完了の切り替え 10 | - TODOの更新 11 | - TODOの削除 12 | 13 | ## インストール 14 | 15 | ```bash 16 | # リポジトリをクローン 17 | git clone https://github.com/yourusername/todo-cli.git 18 | cd todo-cli 19 | 20 | # グローバルにインストール (オプション) 21 | deno install --allow-read --allow-write --allow-env -n todo mod.ts 22 | ``` 23 | 24 | ## 使い方 25 | 26 | ### TODOの追加 27 | 28 | ```bash 29 | deno run --allow-read --allow-write --allow-env mod.ts add "牛乳を買う" 30 | ``` 31 | 32 | ### TODOリストの表示 33 | 34 | ```bash 35 | # 未完了のTODOを表示 36 | deno run --allow-read --allow-write --allow-env mod.ts list 37 | 38 | # 別名 (ls) 39 | deno run --allow-read --allow-write --allow-env mod.ts ls 40 | 41 | # すべてのTODO(完了済みを含む)を表示 42 | deno run --allow-read --allow-write --allow-env mod.ts list --all 43 | deno run --allow-read --allow-write --allow-env mod.ts ls -a 44 | ``` 45 | 46 | ### TODOの完了/未完了の切り替え 47 | 48 | ```bash 49 | deno run --allow-read --allow-write --allow-env mod.ts toggle 50 | ``` 51 | 52 | ### TODOの更新 53 | 54 | ```bash 55 | # テキストを更新 56 | deno run --allow-read --allow-write --allow-env mod.ts update --text "新しいテキスト" 57 | 58 | # 完了状態を更新 59 | deno run --allow-read --allow-write --allow-env mod.ts update --completed true 60 | deno run --allow-read --allow-write --allow-env mod.ts update --completed false 61 | 62 | # 両方を更新 63 | deno run --allow-read --allow-write --allow-env mod.ts update --text "新しいテキスト" --completed true 64 | ``` 65 | 66 | ### TODOの削除 67 | 68 | ```bash 69 | # 削除の確認 70 | deno run --allow-read --allow-write --allow-env mod.ts remove 71 | deno run --allow-read --allow-write --allow-env mod.ts rm 72 | 73 | # 強制削除 (確認なし) 74 | deno run --allow-read --allow-write --allow-env mod.ts remove --force 75 | deno run --allow-read --allow-write --allow-env mod.ts rm -f 76 | ``` 77 | 78 | ### ヘルプの表示 79 | 80 | ```bash 81 | deno run --allow-read --allow-write --allow-env mod.ts --help 82 | deno run --allow-read --allow-write --allow-env mod.ts --help 83 | ``` 84 | 85 | ## データストレージ 86 | 87 | TODOデータはSQLiteデータベースに保存され、`~/.todo/todo.db`に格納されます。 88 | 89 | ## 技術スタック 90 | 91 | - [Deno](https://deno.land/) - セキュアなJavaScript/TypeScriptランタイム 92 | - [SQLite](https://sqlite.org/) - 軽量データベース 93 | - [zodcli](https://github.com/yourusername/zodcli) - 型安全なCLIパーサー 94 | - [Zod](https://github.com/colinhacks/zod) - TypeScriptファーストのスキーマ検証 95 | -------------------------------------------------------------------------------- /__deprecated/todo-cli/deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mizchi/todo", 3 | "version": "1.0.0", 4 | "description": "A simple TODO CLI app with Deno", 5 | "exports": { 6 | ".": "./mod.ts" 7 | }, 8 | "imports": { 9 | "@std/fs": "jsr:@std/fs@0.218.0", 10 | "@std/path": "jsr:@std/path@0.218.0", 11 | "@ai-sdk/anthropic": "npm:@ai-sdk/anthropic@^1.1.11", 12 | "ai": "npm:ai@^4.1.46", 13 | "zod": "npm:zod@^3.24.2" 14 | }, 15 | "tasks": { 16 | "start": "deno run --allow-read --allow-write --allow-env mod.ts", 17 | "test": "deno test --allow-read --allow-write --allow-env" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /__deprecated/todo-cli/mod.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env -S deno run --allow-read --allow-write --allow-env 2 | 3 | /** 4 | * TODOアプリのメインエントリーポイント 5 | * 6 | * このファイルは、コマンドライン引数を解析して適切なコマンドを実行します。 7 | * 8 | * 使用例: 9 | * ``` 10 | * deno run --allow-read --allow-write --allow-env mod.ts add "牛乳を買う" 11 | * deno run --allow-read --allow-write --allow-env mod.ts list 12 | * deno run --allow-read --allow-write --allow-env mod.ts toggle 13 | * deno run --allow-read --allow-write --allow-env mod.ts remove 14 | * deno run --allow-read --allow-write --allow-env mod.ts update --text "新しいテキスト" 15 | * ``` 16 | */ 17 | 18 | import { executeCommand } from "./src/commands.ts"; 19 | 20 | // メイン関数 21 | if (import.meta.main) { 22 | try { 23 | // コマンドを実行(非同期関数なので即時実行) 24 | (async () => { 25 | await executeCommand(Deno.args); 26 | })().catch((error) => { 27 | // エラーの型に応じてメッセージを表示 28 | if (error instanceof Error) { 29 | console.error("エラー:", error.message); 30 | } else { 31 | console.error("予期しないエラーが発生しました:", String(error)); 32 | } 33 | Deno.exit(1); 34 | }); 35 | } catch (error) { 36 | // 非同期処理前のエラー 37 | if (error instanceof Error) { 38 | console.error("エラー:", error.message); 39 | } else { 40 | console.error("予期しないエラーが発生しました:", String(error)); 41 | } 42 | Deno.exit(1); 43 | } 44 | } 45 | 46 | // モジュールとしてインポートされた場合に公開するAPI 47 | export { executeCommand } from "./src/commands.ts"; 48 | export { 49 | addTodo, 50 | getTodo, 51 | listTodos, 52 | removeTodo, 53 | toggleTodo, 54 | updateTodo, 55 | } from "./src/db.ts"; 56 | export type { NewTodo, Todo, TodoList, TodoUpdate } from "./src/types.ts"; 57 | -------------------------------------------------------------------------------- /__deprecated/todo-cli/src/schema.ts: -------------------------------------------------------------------------------- 1 | import { integer, sqliteTable, text } from "drizzle-sqlite"; 2 | import { z } from "zod"; 3 | 4 | /** 5 | * Todo テーブルのスキーマ定義 6 | */ 7 | export const todos = sqliteTable("todos", { 8 | id: text("id").primaryKey(), 9 | text: text("text").notNull(), 10 | completed: integer("completed", { mode: "boolean" }).notNull().default(false), 11 | created_at: text("created_at").notNull(), 12 | updated_at: text("updated_at").notNull(), 13 | }); 14 | 15 | /** 16 | * チャット履歴テーブルのスキーマ定義 17 | */ 18 | export const chatHistory = sqliteTable("chat_history", { 19 | id: text("id").primaryKey(), 20 | user_prompt: text("user_prompt").notNull(), 21 | ai_response: text("ai_response").notNull(), 22 | timestamp: text("timestamp").notNull(), 23 | }); 24 | 25 | /** 26 | * Todo 型はドリズルのスキーマから自動的に推論 27 | */ 28 | export type Todo = typeof todos.$inferSelect; 29 | 30 | /** 31 | * 新規 Todo 作成のための型 32 | */ 33 | export type NewTodo = Pick; 34 | 35 | /** 36 | * Todo 更新のための型 37 | */ 38 | export type TodoUpdate = Partial>; 39 | 40 | /** 41 | * チャット履歴の型 42 | */ 43 | export type ChatHistory = typeof chatHistory.$inferSelect; 44 | 45 | // Zod スキーマ - バリデーション用 46 | export const todoSchema = z.object({ 47 | id: z.string().uuid(), 48 | text: z.string().min(1, "タスクの内容は必須です"), 49 | completed: z.boolean().default(false), 50 | created_at: z.string().datetime(), 51 | updated_at: z.string().datetime(), 52 | }); 53 | 54 | export const todoUpdateSchema = todoSchema.partial().pick({ 55 | text: true, 56 | completed: true, 57 | }); 58 | 59 | export const chatHistorySchema = z.object({ 60 | id: z.string().uuid(), 61 | user_prompt: z.string(), 62 | ai_response: z.string(), 63 | timestamp: z.string().datetime(), 64 | }); 65 | -------------------------------------------------------------------------------- /__deprecated/todo-cli/src/types.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | /** 4 | * TODOタスクのZodスキーマ 5 | */ 6 | export const todoSchema = z.object({ 7 | /** タスクのID */ 8 | id: z.string().uuid(), 9 | /** タスクの内容 */ 10 | text: z.string().min(1, "タスクの内容は必須です"), 11 | /** タスクが完了しているかどうか */ 12 | completed: z.boolean().default(false), 13 | /** タスクの作成日時 */ 14 | createdAt: z.string().datetime(), 15 | /** タスクの更新日時 */ 16 | updatedAt: z.string().datetime(), 17 | }); 18 | 19 | /** 20 | * TODOタスクの型 21 | */ 22 | export type Todo = z.infer; 23 | 24 | /** 25 | * 新しいTODO作成用のスキーマ(IDと日付は自動生成) 26 | */ 27 | export const newTodoSchema = todoSchema.pick({ text: true }); 28 | 29 | /** 30 | * 新しいTODO作成用の型 31 | */ 32 | export type NewTodo = z.infer; 33 | 34 | /** 35 | * TODOリストのスキーマ 36 | */ 37 | export const todoListSchema = z.object({ 38 | todos: z.array(todoSchema), 39 | }); 40 | 41 | /** 42 | * TODOリストの型 43 | */ 44 | export type TodoList = z.infer; 45 | 46 | /** 47 | * TODOの更新用のスキーマ 48 | */ 49 | export const todoUpdateSchema = todoSchema.partial().pick({ 50 | text: true, 51 | completed: true, 52 | }); 53 | 54 | /** 55 | * TODOの更新用の型 56 | */ 57 | export type TodoUpdate = z.infer; 58 | 59 | /** 60 | * チャット履歴のZodスキーマ 61 | */ 62 | export const chatHistorySchema = z.object({ 63 | /** チャット履歴のID */ 64 | id: z.string().uuid(), 65 | /** ユーザーの入力プロンプト */ 66 | userPrompt: z.string(), 67 | /** AIの応答 */ 68 | aiResponse: z.string(), 69 | /** 会話が行われた日時 */ 70 | timestamp: z.string().datetime(), 71 | }); 72 | 73 | /** 74 | * チャット履歴の型 75 | */ 76 | export type ChatHistory = z.infer; 77 | -------------------------------------------------------------------------------- /__deprecated/todo-cli/test/mod.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "@std/expect"; 2 | import { test } from "@std/testing/bdd"; 3 | 4 | // モジュールが正しくインポートできることを確認するテスト 5 | test("todo-cli module can be imported", () => { 6 | // このテストは常に成功し、単にテストファイルが存在することを確認するためのもの 7 | expect(true).toBe(true); 8 | }); 9 | -------------------------------------------------------------------------------- /__deprecated/tree-sitter.ts: -------------------------------------------------------------------------------- 1 | const treesitter = ` 2 | (function_signature 3 | name: (identifier) @name.definition.function) @definition.function 4 | 5 | (method_signature 6 | name: (property_identifier) @name.definition.method) @definition.method 7 | 8 | (abstract_method_signature 9 | name: (property_identifier) @name.definition.method) @definition.method 10 | 11 | (abstract_class_declaration 12 | name: (type_identifier) @name.definition.class) @definition.class 13 | 14 | (module 15 | name: (identifier) @name.definition.module) @definition.module 16 | 17 | (function_declaration 18 | name: (identifier) @name.definition.function) @definition.function 19 | 20 | (method_definition 21 | name: (property_identifier) @name.definition.method) @definition.method 22 | 23 | (class_declaration 24 | name: (type_identifier) @name.definition.class) @definition.class 25 | `; 26 | -------------------------------------------------------------------------------- /apps/ddd-sample-light/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizchi/ailab/a18699aca0c700440d4f54e6fbee80632dd594e1/apps/ddd-sample-light/README.md -------------------------------------------------------------------------------- /apps/ddd-sample-light/deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mizchi/ddd-sample-light", 3 | "version": "0.1.0", 4 | "description": "軽量版関数型ドメインモデリングサンプル", 5 | "imports": { 6 | "@std/testing": "jsr:@std/testing@^1.0.9", 7 | "@std/expect": "jsr:@std/expect@^1.0.13" 8 | }, 9 | "exports": { 10 | ".": "./src/app.ts" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /apps/ddd-sample-light/src/adapters/inMemoryTaskRepository.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * タスクリポジトリのインメモリ実装 3 | * テストや開発時に使用する簡易的な実装 4 | */ 5 | 6 | import type { Task, TaskFilter, TaskId } from "../domain/types.ts"; 7 | import type { TaskRepository } from "../domain/taskRepository.ts"; 8 | import { err, ok, type Result } from "../core/result.ts"; 9 | import { filterTasks } from "../domain/task.ts"; 10 | 11 | export class InMemoryTaskRepository implements TaskRepository { 12 | // インメモリストレージ 13 | private tasks: Map = new Map(); 14 | 15 | // 指定IDのタスクを取得 16 | async findById(id: TaskId): Promise> { 17 | try { 18 | const task = this.tasks.get(String(id)) || null; 19 | return ok(task); 20 | } catch (error) { 21 | const message = error instanceof Error ? error.message : String(error); 22 | return err(new Error(`タスク取得エラー: ${message}`)); 23 | } 24 | } 25 | 26 | // 全タスクを取得 27 | async findAll(): Promise> { 28 | try { 29 | return ok(Array.from(this.tasks.values())); 30 | } catch (error) { 31 | const message = error instanceof Error ? error.message : String(error); 32 | return err(new Error(`タスク全件取得エラー: ${message}`)); 33 | } 34 | } 35 | 36 | // フィルターに基づくタスク取得 37 | async findByFilter(filter: TaskFilter): Promise> { 38 | try { 39 | const allTasks = Array.from(this.tasks.values()); 40 | const filteredTasks = filterTasks(allTasks, filter); 41 | return ok(filteredTasks); 42 | } catch (error) { 43 | const message = error instanceof Error ? error.message : String(error); 44 | return err(new Error(`タスク検索エラー: ${message}`)); 45 | } 46 | } 47 | 48 | // タスク保存(新規/更新) 49 | async save(task: Task): Promise> { 50 | try { 51 | this.tasks.set(String(task.id), task); 52 | return ok(undefined); 53 | } catch (error) { 54 | const message = error instanceof Error ? error.message : String(error); 55 | return err(new Error(`タスク保存エラー: ${message}`)); 56 | } 57 | } 58 | 59 | // タスク削除 60 | async delete(id: TaskId): Promise> { 61 | try { 62 | this.tasks.delete(String(id)); 63 | return ok(undefined); 64 | } catch (error) { 65 | const message = error instanceof Error ? error.message : String(error); 66 | return err(new Error(`タスク削除エラー: ${message}`)); 67 | } 68 | } 69 | 70 | // テスト用:リポジトリをクリア 71 | clear(): void { 72 | this.tasks.clear(); 73 | } 74 | 75 | // テスト用:複数タスクを一括設定 76 | seedTasks(tasks: Task[]): void { 77 | for (const task of tasks) { 78 | this.tasks.set(String(task.id), task); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /apps/ddd-sample-light/src/core/result.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 成功または失敗を表現する Result 型 3 | */ 4 | 5 | // Result型の定義 6 | export type Result = 7 | | { ok: true; value: T } 8 | | { ok: false; error: E }; 9 | 10 | // 成功を表す関数 11 | export function ok(value: T): Result { 12 | return { ok: true, value }; 13 | } 14 | 15 | // 失敗を表す関数 16 | export function err(error: E): Result { 17 | return { ok: false, error }; 18 | } 19 | 20 | // 基本エラータイプ 21 | export class AppError extends Error { 22 | constructor(message: string) { 23 | super(message); 24 | this.name = this.constructor.name; 25 | } 26 | } 27 | 28 | // バリデーションエラー 29 | export class ValidationError extends AppError { 30 | constructor(message: string) { 31 | super(`検証エラー: ${message}`); 32 | } 33 | } 34 | 35 | // 複数のResultを結合するヘルパー 36 | export function combine(results: Result[]): Result { 37 | const values: T[] = []; 38 | 39 | for (const result of results) { 40 | if (!result.ok) return result as Result; 41 | values.push(result.value); 42 | } 43 | 44 | return ok(values); 45 | } 46 | -------------------------------------------------------------------------------- /apps/ddd-sample-light/src/domain/taskRepository.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * タスクリポジトリのインターフェース 3 | */ 4 | 5 | import type { Task, TaskFilter, TaskId } from "./types.ts"; 6 | import type { Result, ValidationError } from "../core/result.ts"; 7 | 8 | // タスクリポジトリのインターフェース定義 9 | export interface TaskRepository { 10 | // タスク取得メソッド 11 | findById(id: TaskId): Promise>; 12 | findAll(): Promise>; 13 | findByFilter(filter: TaskFilter): Promise>; 14 | 15 | // タスク操作メソッド 16 | save(task: Task): Promise>; 17 | delete(id: TaskId): Promise>; 18 | } 19 | -------------------------------------------------------------------------------- /apps/ddd-sample-light/src/domain/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * ドメインモデルの基本型定義 3 | */ 4 | 5 | // ブランデッド型を作成するためのユーティリティ型 6 | export type Branded = T & { readonly _brand: Brand }; 7 | 8 | // ID型 9 | export type TaskId = Branded; 10 | export type UserId = Branded; 11 | 12 | // 値オブジェクト型 13 | export type Priority = "low" | "medium" | "high"; 14 | export type TaskStatus = "pending" | "in-progress" | "completed" | "cancelled"; 15 | 16 | // タスクエンティティ 17 | export interface Task { 18 | readonly id: TaskId; 19 | readonly title: string; 20 | readonly description?: string; 21 | readonly status: TaskStatus; 22 | readonly priority: Priority; 23 | readonly createdBy: UserId; 24 | readonly createdAt: Date; 25 | readonly updatedAt: Date; 26 | } 27 | 28 | // ユーザーエンティティ 29 | export interface User { 30 | readonly id: UserId; 31 | readonly name: string; 32 | readonly email: string; 33 | } 34 | 35 | // タスク検索フィルター 36 | export interface TaskFilter { 37 | readonly status?: TaskStatus; 38 | readonly priority?: Priority; 39 | readonly createdBy?: UserId; 40 | } 41 | -------------------------------------------------------------------------------- /apps/ddd-sample/core/result.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Result型のラッパーモジュール 3 | * neverthrowライブラリをラップして使いやすくするためのユーティリティ関数を提供します。 4 | */ 5 | import { Err, err, Ok, ok, Result } from "neverthrow"; 6 | 7 | export { Err, err, Ok, ok, Result }; 8 | 9 | // 型定義 10 | export type AppError = 11 | | ValidationError 12 | | NotFoundError 13 | | SystemError; 14 | 15 | // 基本エラークラス 16 | export class BaseError extends Error { 17 | constructor(message: string) { 18 | super(message); 19 | this.name = this.constructor.name; 20 | } 21 | } 22 | 23 | // 各種エラークラス 24 | export class ValidationError extends BaseError { 25 | constructor(message: string) { 26 | super(`バリデーションエラー: ${message}`); 27 | } 28 | } 29 | 30 | export class NotFoundError extends BaseError { 31 | constructor(entityName: string, id: string) { 32 | super(`${entityName}(ID: ${id})が見つかりません`); 33 | } 34 | } 35 | 36 | export class SystemError extends BaseError { 37 | constructor(message: string) { 38 | super(`システムエラー: ${message}`); 39 | } 40 | } 41 | 42 | // 便利なヘルパー関数 43 | export function combine(results: Result[]): Result { 44 | const okValues: T[] = []; 45 | 46 | for (const result of results) { 47 | if (result.isErr()) { 48 | return err(result.error); 49 | } 50 | okValues.push(result.value); 51 | } 52 | 53 | return ok(okValues); 54 | } 55 | 56 | // ResultのArrayをResultに変換するヘルパー関数 57 | export function sequence(results: Result[]): Result { 58 | return combine(results); 59 | } 60 | 61 | // オブジェクトの各プロパティがResultである場合、それらを単一のResultに変換する 62 | export function combineProperties, E>( 63 | obj: { [K in keyof T]: Result }, 64 | ): Result { 65 | const keys = Object.keys(obj) as Array; 66 | const values: T = {} as T; 67 | 68 | for (const key of keys) { 69 | const result = obj[key]; 70 | if (result.isErr()) { 71 | return err(result.error); 72 | } 73 | values[key] = result.value; 74 | } 75 | 76 | return ok(values); 77 | } 78 | -------------------------------------------------------------------------------- /apps/ddd-sample/deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mizchi/ddd-sample", 3 | "version": "0.1.0", 4 | "description": "関数型ドメインモデリングによるDDDの実装サンプル", 5 | "imports": { 6 | "neverthrow": "npm:neverthrow@^8.2.0", 7 | "@std/testing": "jsr:@std/testing@^1.0.9", 8 | "@std/expect": "jsr:@std/expect@^1.0.13" 9 | }, 10 | "exports": { 11 | ".": "./mod.ts" 12 | }, 13 | "tasks": { 14 | "test:watch": "deno test --watch --allow-none" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /apps/ddd-sample/domain/repositories/customerRepository.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 顧客リポジトリのインターフェース定義 3 | */ 4 | import type { Customer } from "../entities/customer.ts"; 5 | import type { CustomerId, Email } from "../types.ts"; 6 | import type { 7 | NotFoundError, 8 | Result, 9 | ValidationError, 10 | } from "../../core/result.ts"; 11 | 12 | /** 13 | * 顧客リポジトリのインターフェース 14 | * ドメイン層ではインターフェースのみを定義し、具体的な実装はインフラストラクチャ層で行う 15 | */ 16 | export interface CustomerRepository { 17 | /** 18 | * IDで顧客を検索する 19 | * @param id 検索する顧客ID 20 | * @returns Result 21 | */ 22 | findById( 23 | id: CustomerId, 24 | ): Promise>; 25 | 26 | /** 27 | * メールアドレスで顧客を検索する 28 | * @param email 検索するメールアドレス 29 | * @returns Result 30 | */ 31 | findByEmail( 32 | email: Email, 33 | ): Promise>; 34 | 35 | /** 36 | * すべての顧客を取得する 37 | * @returns Result 38 | */ 39 | findAll(): Promise>; 40 | 41 | /** 42 | * アクティブな顧客のみを取得する 43 | * @returns Result 44 | */ 45 | findAllActive(): Promise>; 46 | 47 | /** 48 | * 顧客を保存する(新規作成または更新) 49 | * @param customer 保存する顧客エンティティ 50 | * @returns Result 51 | */ 52 | save(customer: Customer): Promise>; 53 | 54 | /** 55 | * 顧客を削除する 56 | * @param id 削除する顧客ID 57 | * @returns Result 58 | */ 59 | delete( 60 | id: CustomerId, 61 | ): Promise>; 62 | 63 | /** 64 | * IDの存在チェック 65 | * @param id チェックする顧客ID 66 | * @returns Result 67 | */ 68 | exists(id: CustomerId): Promise>; 69 | 70 | /** 71 | * メールアドレスの存在チェック 72 | * @param email チェックするメールアドレス 73 | * @returns Result 74 | */ 75 | existsByEmail(email: Email): Promise>; 76 | } 77 | -------------------------------------------------------------------------------- /apps/ddd-sample/domain/repositories/orderRepository.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 注文リポジトリのインターフェース定義 3 | */ 4 | import type { Order } from "../entities/order.ts"; 5 | import type { CustomerId, OrderId, OrderStatus, ProductId } from "../types.ts"; 6 | import type { 7 | NotFoundError, 8 | Result, 9 | ValidationError, 10 | } from "../../core/result.ts"; 11 | 12 | /** 13 | * 注文リポジトリのインターフェース 14 | * ドメイン層ではインターフェースのみを定義し、具体的な実装はインフラストラクチャ層で行う 15 | */ 16 | export interface OrderRepository { 17 | /** 18 | * IDで注文を検索する 19 | * @param id 検索する注文ID 20 | * @returns Result 21 | */ 22 | findById( 23 | id: OrderId, 24 | ): Promise>; 25 | 26 | /** 27 | * 顧客IDで注文を検索する 28 | * @param customerId 検索する顧客ID 29 | * @returns Result 30 | */ 31 | findByCustomerId( 32 | customerId: CustomerId, 33 | ): Promise>; 34 | 35 | /** 36 | * 商品IDを含む注文を検索する 37 | * @param productId 検索する商品ID 38 | * @returns Result 39 | */ 40 | findByProductId( 41 | productId: ProductId, 42 | ): Promise>; 43 | 44 | /** 45 | * 注文ステータスで注文を検索する 46 | * @param status 検索する注文ステータス 47 | * @returns Result 48 | */ 49 | findByStatus( 50 | status: OrderStatus["kind"], 51 | ): Promise>; 52 | 53 | /** 54 | * 期間内の注文を検索する 55 | * @param startDate 開始日 56 | * @param endDate 終了日 57 | * @returns Result 58 | */ 59 | findByDateRange( 60 | startDate: Date, 61 | endDate: Date, 62 | ): Promise>; 63 | 64 | /** 65 | * すべての注文を取得する 66 | * @returns Result 67 | */ 68 | findAll(): Promise>; 69 | 70 | /** 71 | * 注文を保存する(新規作成または更新) 72 | * @param order 保存する注文エンティティ 73 | * @returns Result 74 | */ 75 | save(order: Order): Promise>; 76 | 77 | /** 78 | * 注文を削除する 79 | * @param id 削除する注文ID 80 | * @returns Result 81 | */ 82 | delete(id: OrderId): Promise>; 83 | 84 | /** 85 | * IDの存在チェック 86 | * @param id チェックする注文ID 87 | * @returns Result 88 | */ 89 | exists(id: OrderId): Promise>; 90 | 91 | /** 92 | * 顧客の注文数を取得する 93 | * @param customerId 顧客ID 94 | * @returns Result 95 | */ 96 | countByCustomerId( 97 | customerId: CustomerId, 98 | ): Promise>; 99 | 100 | /** 101 | * 特定の状態の注文数を取得する 102 | * @param status 注文ステータス 103 | * @returns Result 104 | */ 105 | countByStatus( 106 | status: OrderStatus["kind"], 107 | ): Promise>; 108 | } 109 | -------------------------------------------------------------------------------- /apps/ddd-sample/domain/repositories/productRepository.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 商品リポジトリのインターフェース定義 3 | */ 4 | import type { Product, ProductCategory } from "../entities/product.ts"; 5 | import type { ProductCode, ProductId } from "../types.ts"; 6 | import type { 7 | NotFoundError, 8 | Result, 9 | ValidationError, 10 | } from "../../core/result.ts"; 11 | 12 | /** 13 | * 商品リポジトリのインターフェース 14 | * ドメイン層ではインターフェースのみを定義し、具体的な実装はインフラストラクチャ層で行う 15 | */ 16 | export interface ProductRepository { 17 | /** 18 | * IDで商品を検索する 19 | * @param id 検索する商品ID 20 | * @returns Result 21 | */ 22 | findById( 23 | id: ProductId, 24 | ): Promise>; 25 | 26 | /** 27 | * 商品コードで商品を検索する 28 | * @param code 検索する商品コード 29 | * @returns Result 30 | */ 31 | findByCode( 32 | code: ProductCode, 33 | ): Promise>; 34 | 35 | /** 36 | * すべての商品を取得する 37 | * @returns Result 38 | */ 39 | findAll(): Promise>; 40 | 41 | /** 42 | * カテゴリで商品を検索する 43 | * @param category 検索するカテゴリ 44 | * @returns Result 45 | */ 46 | findByCategory( 47 | category: ProductCategory, 48 | ): Promise>; 49 | 50 | /** 51 | * 利用可能な商品のみを取得する 52 | * @returns Result 53 | */ 54 | findAllAvailable(): Promise< 55 | Result 56 | >; 57 | 58 | /** 59 | * 在庫がある商品のみを取得する 60 | * @returns Result 61 | */ 62 | findAllInStock(): Promise>; 63 | 64 | /** 65 | * 商品を保存する(新規作成または更新) 66 | * @param product 保存する商品エンティティ 67 | * @returns Result 68 | */ 69 | save(product: Product): Promise>; 70 | 71 | /** 72 | * 複数の商品を一括保存する 73 | * @param products 保存する商品エンティティの配列 74 | * @returns Result 75 | */ 76 | saveAll(products: Product[]): Promise>; 77 | 78 | /** 79 | * 商品を削除する 80 | * @param id 削除する商品ID 81 | * @returns Result 82 | */ 83 | delete(id: ProductId): Promise>; 84 | 85 | /** 86 | * IDの存在チェック 87 | * @param id チェックする商品ID 88 | * @returns Result 89 | */ 90 | exists(id: ProductId): Promise>; 91 | 92 | /** 93 | * 商品コードの存在チェック 94 | * @param code チェックする商品コード 95 | * @returns Result 96 | */ 97 | existsByCode(code: ProductCode): Promise>; 98 | } 99 | -------------------------------------------------------------------------------- /apps/ddd-sample/domain/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * DDDの関数型アプローチにおける基本的な型定義 3 | */ 4 | 5 | // ブランデッドタイプを作成するためのユーティリティ型 6 | // これにより、string型やnumber型に特別な「ブランド」を付与して型安全性を高める 7 | export type Branded = T & { readonly _brand: Brand }; 8 | 9 | // ID型のブランドとして使用する型 10 | export type IdBrand = { readonly __id: Entity }; 11 | 12 | // 各エンティティのID型 13 | export type CustomerId = Branded>; 14 | export type ProductId = Branded>; 15 | export type OrderId = Branded>; 16 | 17 | // ブランデッド型を作成するためのヘルパー関数型 18 | export type BrandedCreator = (value: T) => Result, E>; 19 | 20 | // 値オブジェクト型 21 | export type Money = Branded; 22 | export type Email = Branded; 23 | export type ProductCode = Branded; 24 | export type Quantity = Branded; 25 | 26 | // 注文ステータスの型定義(直和型/判別共用体) 27 | export type OrderStatus = 28 | | { kind: "pending" } 29 | | { kind: "paid"; paidAt: Date } 30 | | { kind: "shipped"; shippedAt: Date } 31 | | { kind: "delivered"; deliveredAt: Date } 32 | | { kind: "cancelled"; reason: string; cancelledAt: Date }; 33 | 34 | // エンティティの基本インターフェース 35 | export interface Entity { 36 | readonly id: Id; 37 | } 38 | 39 | // Result型のインポート 40 | import type { Result } from "../core/result.ts"; 41 | -------------------------------------------------------------------------------- /apps/ddd-sample/domain/valueObjects/email.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Email値オブジェクトの実装 3 | * 不変かつバリデーション付きのメールアドレスを表現 4 | */ 5 | import type { Email } from "../types.ts"; 6 | import { err, ok, type Result, ValidationError } from "../../core/result.ts"; 7 | 8 | /** 9 | * メールアドレスのバリデーション正規表現 10 | * シンプルな例としていますが、実際のプロジェクトではより詳細な検証が必要です 11 | */ 12 | const EMAIL_REGEX = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; 13 | 14 | /** 15 | * Email値オブジェクトを作成する 16 | * @param value メールアドレス文字列 17 | * @returns Result 18 | */ 19 | export function createEmail(value: string): Result { 20 | // 空文字チェック 21 | if (!value) { 22 | return err(new ValidationError("メールアドレスは必須です")); 23 | } 24 | 25 | // 長さチェック 26 | if (value.length > 256) { 27 | return err( 28 | new ValidationError("メールアドレスが長すぎます(最大256文字)"), 29 | ); 30 | } 31 | 32 | // 形式チェック 33 | if (!EMAIL_REGEX.test(value)) { 34 | return err(new ValidationError("メールアドレスの形式が正しくありません")); 35 | } 36 | 37 | // ブランド付きの型として返す 38 | return ok(value as Email); 39 | } 40 | 41 | /** 42 | * メールアドレスのドメイン部分を取得する 43 | * @param email メールアドレス 44 | * @returns ドメイン部分の文字列 45 | */ 46 | export function getDomain(email: Email): string { 47 | const parts = email.split("@"); 48 | return parts[1]; 49 | } 50 | 51 | /** 52 | * メールアドレスのローカル部分を取得する 53 | * @param email メールアドレス 54 | * @returns ローカル部分の文字列 55 | */ 56 | export function getLocalPart(email: Email): string { 57 | const parts = email.split("@"); 58 | return parts[0]; 59 | } 60 | 61 | /** 62 | * Email同士の等価性を判定する 63 | * @param a メールアドレスA 64 | * @param b メールアドレスB 65 | * @returns 等価ならtrue 66 | */ 67 | export function emailEquals(a: Email, b: Email): boolean { 68 | // 大文字小文字を区別せずに比較(ドメイン部分は大文字小文字を区別しないため) 69 | return a.toLowerCase() === b.toLowerCase(); 70 | } 71 | 72 | /** 73 | * 与えられた文字列がメールアドレスかどうかを判定する 74 | * @param value チェックする文字列 75 | * @returns メールアドレスならtrue 76 | */ 77 | export function isValidEmailString(value: string): boolean { 78 | return EMAIL_REGEX.test(value); 79 | } 80 | 81 | /** 82 | * メールアドレスをマスクした形式で返す(プライバシー保護用) 83 | * @param email メールアドレス 84 | * @returns マスクされたメールアドレス(例: "j***@example.com") 85 | */ 86 | export function maskEmail(email: Email): string { 87 | const localPart = getLocalPart(email); 88 | const domain = getDomain(email); 89 | 90 | if (localPart.length <= 1) { 91 | return `${localPart}***@${domain}`; 92 | } 93 | 94 | return `${localPart.charAt(0)}***@${domain}`; 95 | } 96 | -------------------------------------------------------------------------------- /apps/ddd-sample/domain/valueObjects/ids.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * ID値オブジェクトの実装 3 | * 各エンティティのID生成と検証のためのユーティリティ関数 4 | */ 5 | import type { CustomerId, OrderId, ProductId } from "../types.ts"; 6 | import { err, ok, type Result, ValidationError } from "../../core/result.ts"; 7 | 8 | /** 9 | * ID用のバリデーション正規表現 10 | * 例: "cust_123456", "prod_abcdef", "ord_xyz789" 11 | */ 12 | const CUSTOMER_ID_REGEX = /^cust_[a-z0-9]{6,}$/; 13 | const PRODUCT_ID_REGEX = /^prod_[a-z0-9]{6,}$/; 14 | const ORDER_ID_REGEX = /^ord_[a-z0-9]{6,}$/; 15 | 16 | /** 17 | * ランダムな文字列を生成 18 | * @param length 文字列の長さ 19 | * @returns ランダム文字列 20 | */ 21 | function generateRandomString(length: number): string { 22 | const chars = "abcdefghijklmnopqrstuvwxyz0123456789"; 23 | let result = ""; 24 | const randomValues = new Uint8Array(length); 25 | crypto.getRandomValues(randomValues); 26 | 27 | for (let i = 0; i < length; i++) { 28 | result += chars[randomValues[i] % chars.length]; 29 | } 30 | 31 | return result; 32 | } 33 | 34 | /** 35 | * 新しい顧客IDを生成 36 | * @returns CustomerId 37 | */ 38 | export function generateCustomerId(): CustomerId { 39 | return `cust_${generateRandomString(10)}` as CustomerId; 40 | } 41 | 42 | /** 43 | * 顧客IDの検証 44 | * @param id 検証する顧客ID 45 | * @returns Result 46 | */ 47 | export function validateCustomerId( 48 | id: string, 49 | ): Result { 50 | if (!id) { 51 | return err(new ValidationError("顧客IDは必須です")); 52 | } 53 | 54 | if (!CUSTOMER_ID_REGEX.test(id)) { 55 | return err( 56 | new ValidationError("顧客IDの形式が正しくありません (例: cust_123456)"), 57 | ); 58 | } 59 | 60 | return ok(id as CustomerId); 61 | } 62 | 63 | /** 64 | * 新しい商品IDを生成 65 | * @returns ProductId 66 | */ 67 | export function generateProductId(): ProductId { 68 | return `prod_${generateRandomString(10)}` as ProductId; 69 | } 70 | 71 | /** 72 | * 商品IDの検証 73 | * @param id 検証する商品ID 74 | * @returns Result 75 | */ 76 | export function validateProductId( 77 | id: string, 78 | ): Result { 79 | if (!id) { 80 | return err(new ValidationError("商品IDは必須です")); 81 | } 82 | 83 | if (!PRODUCT_ID_REGEX.test(id)) { 84 | return err( 85 | new ValidationError("商品IDの形式が正しくありません (例: prod_123456)"), 86 | ); 87 | } 88 | 89 | return ok(id as ProductId); 90 | } 91 | 92 | /** 93 | * 新しい注文IDを生成 94 | * @returns OrderId 95 | */ 96 | export function generateOrderId(): OrderId { 97 | return `ord_${generateRandomString(10)}` as OrderId; 98 | } 99 | 100 | /** 101 | * 注文IDの検証 102 | * @param id 検証する注文ID 103 | * @returns Result 104 | */ 105 | export function validateOrderId(id: string): Result { 106 | if (!id) { 107 | return err(new ValidationError("注文IDは必須です")); 108 | } 109 | 110 | if (!ORDER_ID_REGEX.test(id)) { 111 | return err( 112 | new ValidationError("注文IDの形式が正しくありません (例: ord_123456)"), 113 | ); 114 | } 115 | 116 | return ok(id as OrderId); 117 | } 118 | 119 | /** 120 | * ID同士の等価性を判定する汎用関数 121 | * @param a ID A 122 | * @param b ID B 123 | * @returns 等価ならtrue 124 | */ 125 | export function idEquals(a: T, b: T): boolean { 126 | return a === b; 127 | } 128 | -------------------------------------------------------------------------------- /apps/ddd-sample/domain/valueObjects/money.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Money値オブジェクトの実装 3 | * 不変かつ等価性が値によって決まる金額を表現 4 | */ 5 | import type { Money } from "../types.ts"; 6 | import { err, ok, type Result, ValidationError } from "../../core/result.ts"; 7 | 8 | /** 9 | * Money値オブジェクトを作成する 10 | * @param amount 金額 11 | * @returns Result 12 | */ 13 | export function createMoney(amount: number): Result { 14 | // 負の金額をチェック 15 | if (amount < 0) { 16 | return err(new ValidationError("金額は0以上である必要があります")); 17 | } 18 | 19 | // 小数点以下2桁までに制限(銭以下は扱わない) 20 | if (Math.round(amount * 100) / 100 !== amount) { 21 | return err( 22 | new ValidationError("金額は小数点以下2桁までしか指定できません"), 23 | ); 24 | } 25 | 26 | // ブランド付きの型として返す 27 | return ok(amount as Money); 28 | } 29 | 30 | /** 31 | * Money同士の加算を行う 32 | * @param a 金額A 33 | * @param b 金額B 34 | * @returns 合計金額 35 | */ 36 | export function addMoney(a: Money, b: Money): Money { 37 | // ブランドの型情報は失われるが、新しい値を作成して返す 38 | return (a + b) as Money; 39 | } 40 | 41 | /** 42 | * Money同士の減算を行う(負の値にはならない) 43 | * @param a 金額A 44 | * @param b 金額B 45 | * @returns Result 減算結果 46 | */ 47 | export function subtractMoney( 48 | a: Money, 49 | b: Money, 50 | ): Result { 51 | const result = a - b; 52 | 53 | if (result < 0) { 54 | return err(new ValidationError("金額の減算結果が負の値になりました")); 55 | } 56 | 57 | return ok(result as Money); 58 | } 59 | 60 | /** 61 | * Moneyと数値の乗算を行う 62 | * @param money 金額 63 | * @param multiplier 乗数 64 | * @returns Result 乗算結果 65 | */ 66 | export function multiplyMoney( 67 | money: Money, 68 | multiplier: number, 69 | ): Result { 70 | if (multiplier < 0) { 71 | return err(new ValidationError("乗数は0以上である必要があります")); 72 | } 73 | 74 | const result = Math.round(money * multiplier * 100) / 100; 75 | return createMoney(result); 76 | } 77 | 78 | /** 79 | * Money同士の等価性を判定する 80 | * @param a 金額A 81 | * @param b 金額B 82 | * @returns 等価ならtrue 83 | */ 84 | export function moneyEquals(a: Money, b: Money): boolean { 85 | return a === b; 86 | } 87 | 88 | /** 89 | * Moneyを文字列に変換する 90 | * @param money 金額 91 | * @returns フォーマットされた金額文字列(例: "¥1,000") 92 | */ 93 | export function formatMoney(money: Money): string { 94 | return `¥${money.toLocaleString()}`; 95 | } 96 | -------------------------------------------------------------------------------- /apps/ddd-sample/domain/valueObjects/productCode.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * ProductCode値オブジェクトの実装 3 | * 不変かつ特定の形式を持つ商品コードを表現 4 | */ 5 | import type { ProductCode } from "../types.ts"; 6 | import { err, ok, type Result, ValidationError } from "../../core/result.ts"; 7 | 8 | /** 9 | * 商品コードのバリデーション正規表現 10 | * 例: "PROD-12345" または "ITEM-AB-789" 11 | */ 12 | const PRODUCT_CODE_REGEX = /^[A-Z]+-[A-Z0-9-]{1,10}$/; 13 | 14 | /** 15 | * ProductCode値オブジェクトを作成する 16 | * @param value 商品コード文字列 17 | * @returns Result 18 | */ 19 | export function createProductCode( 20 | value: string, 21 | ): Result { 22 | // 空文字チェック 23 | if (!value) { 24 | return err(new ValidationError("商品コードは必須です")); 25 | } 26 | 27 | // 長さチェック 28 | if (value.length > 20) { 29 | return err(new ValidationError("商品コードが長すぎます(最大20文字)")); 30 | } 31 | 32 | // 形式チェック 33 | if (!PRODUCT_CODE_REGEX.test(value)) { 34 | return err( 35 | new ValidationError( 36 | "商品コードの形式が正しくありません(例: PROD-12345)", 37 | ), 38 | ); 39 | } 40 | 41 | // ブランド付きの型として返す 42 | return ok(value as ProductCode); 43 | } 44 | 45 | /** 46 | * ProductCode同士の等価性を判定する 47 | * @param a 商品コードA 48 | * @param b 商品コードB 49 | * @returns 等価ならtrue 50 | */ 51 | export function productCodeEquals(a: ProductCode, b: ProductCode): boolean { 52 | // 大文字小文字を区別して比較 53 | return a === b; 54 | } 55 | 56 | /** 57 | * 与えられた文字列が商品コードとして有効かどうかを判定する 58 | * @param value チェックする文字列 59 | * @returns 有効な商品コードならtrue 60 | */ 61 | export function isValidProductCodeString(value: string): boolean { 62 | return PRODUCT_CODE_REGEX.test(value); 63 | } 64 | 65 | /** 66 | * 商品コードから製品カテゴリを取得する 67 | * @param productCode 商品コード 68 | * @returns 製品カテゴリ(例: "PROD") 69 | */ 70 | export function getCategory(productCode: ProductCode): string { 71 | const parts = productCode.split("-"); 72 | return parts[0]; 73 | } 74 | -------------------------------------------------------------------------------- /apps/ddd-sample/domain/valueObjects/quantity.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Quantity値オブジェクトの実装 3 | * 不変かつ正の数量を表現する 4 | */ 5 | import type { Quantity } from "../types.ts"; 6 | import { err, ok, type Result, ValidationError } from "../../core/result.ts"; 7 | 8 | /** 9 | * Quantity値オブジェクトを作成する 10 | * @param value 数量 11 | * @returns Result 12 | */ 13 | export function createQuantity( 14 | value: number, 15 | ): Result { 16 | // 整数チェック 17 | if (!Number.isInteger(value)) { 18 | return err(new ValidationError("数量は整数である必要があります")); 19 | } 20 | 21 | // 正の値チェック 22 | if (value <= 0) { 23 | return err(new ValidationError("数量は1以上である必要があります")); 24 | } 25 | 26 | // 上限チェック(例えば在庫管理の都合など) 27 | if (value > 10000) { 28 | return err(new ValidationError("数量が多すぎます(最大10000個)")); 29 | } 30 | 31 | // ブランド付きの型として返す 32 | return ok(value as Quantity); 33 | } 34 | 35 | /** 36 | * Quantity同士の加算を行う 37 | * @param a 数量A 38 | * @param b 数量B 39 | * @returns Result 合計数量 40 | */ 41 | export function addQuantity( 42 | a: Quantity, 43 | b: Quantity, 44 | ): Result { 45 | return createQuantity(a + b); 46 | } 47 | 48 | /** 49 | * Quantity同士の減算を行う 50 | * @param a 数量A 51 | * @param b 数量B 52 | * @returns Result 減算結果 53 | */ 54 | export function subtractQuantity( 55 | a: Quantity, 56 | b: Quantity, 57 | ): Result { 58 | return createQuantity(a - b); 59 | } 60 | 61 | /** 62 | * Quantity同士の等価性を判定する 63 | * @param a 数量A 64 | * @param b 数量B 65 | * @returns 等価ならtrue 66 | */ 67 | export function quantityEquals(a: Quantity, b: Quantity): boolean { 68 | return a === b; 69 | } 70 | 71 | /** 72 | * 通常の数値をQuantityに変換する(バリデーションあり) 73 | * @param value 数値 74 | * @returns Result 75 | */ 76 | export function toQuantity(value: number): Result { 77 | return createQuantity(value); 78 | } 79 | 80 | /** 81 | * Quantityを通常の数値として取得する 82 | * @param quantity 数量 83 | * @returns 数値 84 | */ 85 | export function fromQuantity(quantity: Quantity): number { 86 | return quantity; 87 | } 88 | -------------------------------------------------------------------------------- /apps/tdd-example/README.md: -------------------------------------------------------------------------------- 1 | # Deno における TDD の例 2 | 3 | この例では、Deno におけるテスト駆動開発 (TDD) のプロセスを示します。 4 | 5 | ## ディレクトリ構成 6 | 7 | ``` 8 | tdd-example/ 9 | mod.ts - 公開インターフェース (再エクスポートのみ) 10 | lib.ts - 実装 (deps.ts からのインポートを使用) 11 | mod.test.ts - テストコード 12 | ``` 13 | 14 | ## TDD の手順 (Steps) 15 | 16 | 1. **テストを書く**: コードの期待される動作を定義するテストケースを 17 | `mod.test.ts` に記述します。 18 | 2. **テストの失敗を確認する**: 19 | 実装がないため、テストが失敗することを確認します。 20 | 3. **コードを実装する**: テストケースを満たすコードを `lib.ts` に実装します。 21 | 4. **テストの成功を確認する**: テストが成功することを確認します。 22 | 23 | ## 落ちるテストを追加するときの手順 24 | 25 | 1. **テストが通ることを確認**: `deno test -A . --reporter=dot` 26 | でテストを実行し、すべてのテストが通ることを確認します。 27 | 2. **落ちるテストを追加**: 新しいテストケースを `mod.test.ts` 28 | に追加します。このテストは、まだ実装がないため失敗するはずです。 29 | 3. **テストが落ちることを確認**: `deno test -A tdd-example --reporter=dot` 30 | でテストを実行し、追加したテストが失敗することを確認します。 31 | 4. **落ちたテストだけを再実行**: 32 | `deno test -A tdd-example --reporter=dot --filter <テスト名>` 33 | で、落ちたテストだけを再実行します。`<テスト名>` 34 | は、失敗したテストの名前で置き換えてください。 35 | 5. **型を通す**: `lib.ts` に関数を定義し、`mod.ts` で re-export します。実装は 36 | `throw new Error("wip")` とします。 37 | 6. **実装**: `lib.ts` にテストが通る実装を記述します。 38 | 39 | ## 例 40 | 41 | ### 1. テストを書く (`tdd-example/mod.test.ts`) 42 | 43 | ```ts 44 | import { add } from "./mod.ts"; 45 | import { expect } from "@std/expect"; 46 | import { test } from "@std/testing/bdd"; 47 | 48 | test("add", () => { 49 | expect(add(1, 2), "数の合計").toBe(3); 50 | }); 51 | 52 | test("sub", () => { 53 | expect(sub(5, 3), "数の差").toBe(2); 54 | }); 55 | ``` 56 | 57 | ### 2. テストの失敗を確認する 58 | 59 | `deno test -A tdd-example/mod.test.ts` を実行します。 `add` と `sub` 60 | が定義されていないため、テストは `ReferenceError` で失敗するはずです。 61 | 62 | ### 3. コードを実装する (`tdd-example/lib.ts`) 63 | 64 | ```ts 65 | /** 66 | * 2つの数値を足し合わせます。 67 | * @param a 最初の数値 68 | * @param b 2番目の数値 69 | * @returns a と b の合計 70 | */ 71 | export function add(a: number, b: number): number { 72 | return a + b; 73 | } 74 | 75 | export function sub(a: number, b: number): number { 76 | return a - b; 77 | } 78 | ``` 79 | 80 | ### 4. テストの成功を確認する 81 | 82 | `deno test -A tdd-example/mod.test.ts` 83 | を実行し、テストが成功することを確認します。 84 | -------------------------------------------------------------------------------- /apps/tdd-example/__snapshots__/example-0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizchi/ailab/a18699aca0c700440d4f54e6fbee80632dd594e1/apps/tdd-example/__snapshots__/example-0.png -------------------------------------------------------------------------------- /apps/tdd-example/__snapshots__/zenn-0-diff.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizchi/ailab/a18699aca0c700440d4f54e6fbee80632dd594e1/apps/tdd-example/__snapshots__/zenn-0-diff.png -------------------------------------------------------------------------------- /apps/tdd-example/__snapshots__/zenn-0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizchi/ailab/a18699aca0c700440d4f54e6fbee80632dd594e1/apps/tdd-example/__snapshots__/zenn-0.png -------------------------------------------------------------------------------- /apps/tdd-example/async.test.ts: -------------------------------------------------------------------------------- 1 | import { test } from "@std/testing/bdd"; 2 | import { expect } from "@std/expect"; 3 | import { AsyncDisposableStack } from "jsr:@nick/dispose"; 4 | 5 | test("async using", async () => { 6 | let disposeCount = 0; 7 | async function app() { 8 | return { 9 | getValue() { 10 | return disposeCount; 11 | }, 12 | async [Symbol.asyncDispose]() { 13 | disposeCount++; 14 | }, 15 | }; 16 | } 17 | { 18 | await using result = await app(); 19 | expect(result.getValue()).toBe(0); 20 | } 21 | await using result = await app(); 22 | expect(result.getValue()).toBe(1); 23 | }); 24 | 25 | test("async using", async () => { 26 | let disposeCount = 0; 27 | { 28 | await using _ = { 29 | [Symbol.asyncDispose]: async () => { 30 | disposeCount++; 31 | }, 32 | }; 33 | expect(disposeCount).toBe(0); 34 | } 35 | expect(disposeCount).toBe(1); 36 | }); 37 | 38 | test("release", async () => { 39 | let shouldBeTrue = false; 40 | try { 41 | await using _ = { 42 | async [Symbol.asyncDispose]() { 43 | shouldBeTrue = true; 44 | }, 45 | }; 46 | throw new Error("intercepted"); 47 | } catch (error) { 48 | expect((error as Error).message).toBe("intercepted"); 49 | expect(shouldBeTrue).toBe(true); 50 | } 51 | }); 52 | 53 | test("AsyncDisposable", async () => { 54 | const x = new Map(); 55 | { 56 | await using d = new AsyncDisposableStack(); 57 | d.adopt(x, () => x.clear()); 58 | x.set("foo", "bar"); 59 | expect(x.size).toBe(1); 60 | } 61 | expect(x.size).toBe(0); 62 | }); 63 | 64 | test("AsyncDisposable#adop error", async () => { 65 | const x = new Map(); 66 | { 67 | await using d = new AsyncDisposableStack(); 68 | d.adopt(x, async () => { 69 | x.set("xxx", 0); 70 | }); 71 | d.adopt(x, async () => { 72 | throw new Error("intercepted"); 73 | }); 74 | d.adopt(x, async () => { 75 | x.set("yyy", 0); 76 | }); 77 | x.set("foo", 1); 78 | expect(x.size).toBe(1); 79 | } 80 | expect([...x.keys()]).toEqual([ 81 | "foo", 82 | "yyy", 83 | "xxx", 84 | ]); 85 | }); 86 | 87 | test("AsyncDisposable#defer", async () => { 88 | const x = new Map(); 89 | { 90 | await using d = new AsyncDisposableStack(); 91 | d.defer(async () => { 92 | x.set("xxx", 0); 93 | }); 94 | d.defer(async () => { 95 | throw new Error("intercepted"); 96 | }); 97 | d.defer(async () => { 98 | x.set("yyy", 0); 99 | }); 100 | x.set("foo", 1); 101 | expect(x.size).toBe(1); 102 | } 103 | expect([...x.keys()]).toEqual([ 104 | "foo", 105 | "yyy", 106 | "xxx", 107 | ]); 108 | }); 109 | -------------------------------------------------------------------------------- /apps/tdd-example/deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mizchi/tdd-example", 3 | "version": "0.1.0", 4 | "exports": { 5 | ".": "./mod.ts" 6 | }, 7 | "tasks": {} 8 | } 9 | -------------------------------------------------------------------------------- /apps/tdd-example/lib.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 足し算を行う関数 3 | * @param a 4 | * @param b 5 | * @returns a + b 6 | */ 7 | export function add(a: number, b: number): number { 8 | return a + b; 9 | } 10 | 11 | export function sub(a: number, b: number): number { 12 | return a - b; 13 | } 14 | -------------------------------------------------------------------------------- /apps/tdd-example/mod.test.ts: -------------------------------------------------------------------------------- 1 | // @script @tdd 2 | import { add, sub } from "./mod.ts"; 3 | import { expect } from "@std/expect"; 4 | import { test } from "@std/testing/bdd"; 5 | 6 | test("add", () => { 7 | expect(add(1, 2)).toBe(3); 8 | }); 9 | 10 | test("sub", () => { 11 | expect(sub(5, 3), "数の差").toBe(2); 12 | }); 13 | -------------------------------------------------------------------------------- /apps/tdd-example/mod.ts: -------------------------------------------------------------------------------- 1 | export { add, sub } from "./lib.ts"; 2 | -------------------------------------------------------------------------------- /apps/tdd-example/screenshot.test.ts: -------------------------------------------------------------------------------- 1 | import "npm:core-js/proposals/explicit-resource-management.js"; 2 | import { assertScreenshot } from "jsr:@mizchi/assert-screenshot"; 3 | import pptr from "npm:puppeteer"; 4 | 5 | /** 6 | * ブラウザを使用するためのヘルパー関数 7 | */ 8 | async function useBrowser(options: pptr.LaunchOptions = {}) { 9 | const browser = await pptr.launch(options); 10 | let pages: Set = new Set(); 11 | return { 12 | browser, 13 | async newPage(): Promise<{ page: pptr.Page } & AsyncDisposable> { 14 | const page = await browser.newPage(); 15 | pages.add(page); 16 | Object.defineProperty(page, Symbol.asyncDispose, { 17 | value: async () => { 18 | pages.delete(page); 19 | await page.close(); 20 | }, 21 | }); 22 | return { 23 | page, 24 | async [Symbol.asyncDispose]() { 25 | pages.delete(page); 26 | await page.close(); 27 | }, 28 | } as any; 29 | }, 30 | async [Symbol.asyncDispose]() { 31 | await browser.close(); 32 | // Maybe pptr can not dispose all timers 33 | await new Promise((r) => setTimeout(r, 500)); 34 | }, 35 | }; 36 | } 37 | // スクリーンショットのスナップショットテスト 38 | Deno.test("snap", async (t) => { 39 | // ブラウザを起動 40 | const WIDTH = 800; 41 | const HEIGHT = 600; 42 | await using browser = await useBrowser({ 43 | headless: true, 44 | args: [ 45 | "--no-sandbox", 46 | "--disable-setuid-sandbox", 47 | "--force-device-scale-factor=1", 48 | "--high-dpi-support=1", 49 | ], 50 | }); 51 | 52 | await t.step("example.com", async () => { 53 | // ページを開く 54 | const pageCtx = await browser.newPage(); 55 | const page = pageCtx.page; 56 | await page.setViewport({ width: WIDTH, height: HEIGHT }); 57 | await page.goto("https://example.com"); 58 | // スクリーンショットを撮影 59 | const screenshot = await page.screenshot({ 60 | encoding: "binary", 61 | type: "png", 62 | }); 63 | // スクリーンショットをスナップショットと比較 64 | await assertScreenshot(t, screenshot as Uint8Array, { 65 | name: "example", 66 | threshold: 0.04, 67 | // 初回実行時やスナップショットを更新したい場合は true にする 68 | updateSnapshot: false, 69 | }); 70 | }); 71 | 72 | await t.step("zenn.dev/mizchi", async () => { 73 | // ページを開く 74 | const pageCtx = await browser.newPage(); 75 | const page = pageCtx.page; 76 | await page.setViewport({ width: WIDTH, height: HEIGHT }); 77 | await page.goto("https://zenn.dev/mizchi"); 78 | // スクリーンショットを撮影 79 | const screenshot = await page.screenshot({ 80 | encoding: "binary", 81 | type: "png", 82 | }); 83 | // スクリーンショットをスナップショットと比較 84 | await assertScreenshot(t, screenshot as Uint8Array, { 85 | name: "zenn", 86 | threshold: 30, 87 | }); 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /apps/todo2/.gitignore: -------------------------------------------------------------------------------- 1 | data -------------------------------------------------------------------------------- /apps/todo2/db/client.ts: -------------------------------------------------------------------------------- 1 | import { PGlite } from "npm:@electric-sql/pglite"; 2 | import { drizzle } from "drizzle-orm/pglite"; 3 | 4 | const client = new PGlite({ 5 | dataDir: "./data", 6 | }); 7 | 8 | export const db = drizzle(client); 9 | -------------------------------------------------------------------------------- /apps/todo2/db/migrate.ts: -------------------------------------------------------------------------------- 1 | import { migrate } from "drizzle-orm/pglite/migrator"; 2 | import { db } from "./client.ts"; 3 | const folderPath = new URL("./migrations", import.meta.url).pathname; 4 | await migrate(db, { 5 | migrationsFolder: folderPath, 6 | }); 7 | 8 | console.log("Migration complete"); 9 | -------------------------------------------------------------------------------- /apps/todo2/db/migrations/0000_bored_giant_man.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE "users" ( 2 | "id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "users_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1), 3 | "name" varchar(255) NOT NULL, 4 | "age" integer NOT NULL 5 | ); 6 | -------------------------------------------------------------------------------- /apps/todo2/db/migrations/0001_cool_komodo.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE "chat_history" ( 2 | "id" varchar(36) PRIMARY KEY NOT NULL, 3 | "user_prompt" text NOT NULL, 4 | "ai_response" text NOT NULL, 5 | "timestamp" timestamp NOT NULL 6 | ); 7 | --> statement-breakpoint 8 | CREATE TABLE "todos" ( 9 | "id" varchar(36) PRIMARY KEY NOT NULL, 10 | "text" text NOT NULL, 11 | "completed" boolean DEFAULT false NOT NULL, 12 | "created_at" timestamp NOT NULL, 13 | "updated_at" timestamp NOT NULL 14 | ); 15 | -------------------------------------------------------------------------------- /apps/todo2/db/migrations/meta/0000_snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "2bf7b37f-5755-4e04-8a3a-d640d72362a2", 3 | "prevId": "00000000-0000-0000-0000-000000000000", 4 | "version": "7", 5 | "dialect": "postgresql", 6 | "tables": { 7 | "public.users": { 8 | "name": "users", 9 | "schema": "", 10 | "columns": { 11 | "id": { 12 | "name": "id", 13 | "type": "integer", 14 | "primaryKey": true, 15 | "notNull": true, 16 | "identity": { 17 | "type": "always", 18 | "name": "users_id_seq", 19 | "schema": "public", 20 | "increment": "1", 21 | "startWith": "1", 22 | "minValue": "1", 23 | "maxValue": "2147483647", 24 | "cache": "1", 25 | "cycle": false 26 | } 27 | }, 28 | "name": { 29 | "name": "name", 30 | "type": "varchar(255)", 31 | "primaryKey": false, 32 | "notNull": true 33 | }, 34 | "age": { 35 | "name": "age", 36 | "type": "integer", 37 | "primaryKey": false, 38 | "notNull": true 39 | } 40 | }, 41 | "indexes": {}, 42 | "foreignKeys": {}, 43 | "compositePrimaryKeys": {}, 44 | "uniqueConstraints": {}, 45 | "policies": {}, 46 | "checkConstraints": {}, 47 | "isRLSEnabled": false 48 | } 49 | }, 50 | "enums": {}, 51 | "schemas": {}, 52 | "sequences": {}, 53 | "roles": {}, 54 | "policies": {}, 55 | "views": {}, 56 | "_meta": { 57 | "columns": {}, 58 | "schemas": {}, 59 | "tables": {} 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /apps/todo2/db/migrations/meta/_journal.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "7", 3 | "dialect": "postgresql", 4 | "entries": [ 5 | { 6 | "idx": 0, 7 | "version": "7", 8 | "when": 1740747896160, 9 | "tag": "0000_bored_giant_man", 10 | "breakpoints": true 11 | }, 12 | { 13 | "idx": 1, 14 | "version": "7", 15 | "when": 1740752365169, 16 | "tag": "0001_cool_komodo", 17 | "breakpoints": true 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /apps/todo2/db/schema.ts: -------------------------------------------------------------------------------- 1 | import { 2 | boolean, 3 | integer, 4 | pgTable, 5 | text, 6 | timestamp, 7 | varchar, 8 | } from "drizzle-orm/pg-core"; 9 | 10 | // 既存のusersテーブル 11 | export const users = pgTable("users", { 12 | id: integer().primaryKey().generatedAlwaysAsIdentity(), 13 | name: varchar({ length: 255 }).notNull(), 14 | age: integer().notNull(), 15 | }); 16 | 17 | // todosテーブル 18 | export const todos = pgTable("todos", { 19 | // SQLiteではUUID文字列でしたが、ここではSerialとして実装 20 | id: varchar({ length: 36 }).primaryKey().notNull(), 21 | text: text().notNull(), 22 | completed: boolean().default(false).notNull(), 23 | created_at: timestamp().notNull(), 24 | updated_at: timestamp().notNull(), 25 | }); 26 | 27 | // chat_historyテーブル 28 | export const chatHistory = pgTable("chat_history", { 29 | id: varchar({ length: 36 }).primaryKey().notNull(), 30 | user_prompt: text().notNull(), 31 | ai_response: text().notNull(), 32 | timestamp: timestamp().notNull(), 33 | }); 34 | -------------------------------------------------------------------------------- /apps/todo2/deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "tasks": { 3 | "test": "deno test -A", 4 | "db-up": "deno run -A npm:drizzle-kit generate", 5 | "db-migrate": "deno run -A db/migrate.ts", 6 | "db-clear": "rm -rf db/migrations/* data" 7 | }, 8 | "imports": { 9 | "@std/fs": "jsr:@std/fs@^1.0.13", 10 | "@std/path": "jsr:@std/path@^1.0.8", 11 | "drizzle-kit": "npm:drizzle-kit@^0.30.5", 12 | "drizzle-orm": "npm:drizzle-orm@^0.40.0" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /apps/todo2/drizzle.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "drizzle-kit"; 2 | export default defineConfig({ 3 | out: "./db/migrations", 4 | schema: "./db/schema.ts", 5 | dialect: "postgresql", 6 | dbCredentials: { 7 | url: "", 8 | }, 9 | }); 10 | -------------------------------------------------------------------------------- /apps/todo2/mod.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env -S deno run --allow-read --allow-write --allow-env 2 | 3 | /** 4 | * TODOアプリのメインエントリーポイント 5 | * 6 | * このファイルは、コマンドライン引数を解析して適切なコマンドを実行します。 7 | * 8 | * 使用例: 9 | * ``` 10 | * deno run --allow-read --allow-write --allow-env mod.ts add "牛乳を買う" 11 | * deno run --allow-read --allow-write --allow-env mod.ts list 12 | * deno run --allow-read --allow-write --allow-env mod.ts toggle 13 | * deno run --allow-read --allow-write --allow-env mod.ts remove 14 | * deno run --allow-read --allow-write --allow-env mod.ts update --text "新しいテキスト" 15 | * ``` 16 | */ 17 | 18 | import { executeCommand } from "./src/commands.ts"; 19 | 20 | // メイン関数 21 | if (import.meta.main) { 22 | try { 23 | // コマンドを実行(非同期関数なので即時実行) 24 | (async () => { 25 | await executeCommand(Deno.args); 26 | })().catch((error) => { 27 | // エラーの型に応じてメッセージを表示 28 | if (error instanceof Error) { 29 | console.error("エラー:", error.message); 30 | } else { 31 | console.error("予期しないエラーが発生しました:", String(error)); 32 | } 33 | Deno.exit(1); 34 | }); 35 | } catch (error) { 36 | // 非同期処理前のエラー 37 | if (error instanceof Error) { 38 | console.error("エラー:", error.message); 39 | } else { 40 | console.error("予期しないエラーが発生しました:", String(error)); 41 | } 42 | Deno.exit(1); 43 | } 44 | } 45 | 46 | // モジュールとしてインポートされた場合に公開するAPI 47 | export { executeCommand } from "./src/commands.ts"; 48 | export { 49 | addTodo, 50 | getTodo, 51 | getTodoStats, 52 | listTodos, 53 | removeCompletedTodos, 54 | removeTodo, 55 | searchTodos, 56 | toggleTodo, 57 | updateTodo, 58 | } from "./src/db.ts"; 59 | export type { NewTodo, Todo, TodoList, TodoUpdate } from "./src/types.ts"; 60 | -------------------------------------------------------------------------------- /apps/todo2/run.test.ts: -------------------------------------------------------------------------------- 1 | import { migrate } from "drizzle-orm/pglite/migrator"; 2 | import { PGlite } from "npm:@electric-sql/pglite"; 3 | import { drizzle } from "drizzle-orm/pglite"; 4 | import { afterAll, beforeAll, test } from "jsr:@std/testing/bdd"; 5 | import { configureGlobalSanitizers } from "jsr:@std/testing/unstable-bdd"; 6 | import { expect } from "jsr:@std/expect"; 7 | import { users } from "./db/schema.ts"; 8 | import { eq } from "drizzle-orm"; 9 | 10 | // NOTE: drizzle leaks the global sanitizers, so we need to disable them 11 | configureGlobalSanitizers({ 12 | sanitizeOps: false, 13 | }); 14 | const client = new PGlite(); 15 | const db = drizzle(client); 16 | 17 | beforeAll(async () => { 18 | await migrate(db, { 19 | migrationsFolder: new URL("./db/migrations", import.meta.url).pathname, 20 | }); 21 | }); 22 | afterAll(async () => { 23 | await client.close(); 24 | }); 25 | 26 | test("CRUD", async () => { 27 | // Create 28 | await db.insert(users).values({ 29 | name: "John", 30 | age: 30, 31 | }); 32 | const ret = await db.select().from(users); 33 | 34 | // Read 35 | expect(ret).toEqual([{ name: "John", age: 30, id: 1 }]); 36 | await db.update(users).set({ age: 31 }).where(eq(users.id, 1)); 37 | 38 | // Update 39 | const ret2 = await db.select().from(users); 40 | expect(ret2).toEqual([{ name: "John", age: 31, id: 1 }]); 41 | 42 | // Delete 43 | await db.delete(users).where(eq(users.id, 1)); 44 | const ret3 = await db.select().from(users); 45 | expect(ret3).toEqual([]); 46 | }); 47 | -------------------------------------------------------------------------------- /apps/todo2/run.ts: -------------------------------------------------------------------------------- 1 | import { db } from "./db/client.ts"; 2 | import { users } from "./db/schema.ts"; 3 | const ret = await db.select().from(users); 4 | console.log(ret); 5 | -------------------------------------------------------------------------------- /apps/todo2/src/types.ts: -------------------------------------------------------------------------------- 1 | import { z } from "npm:zod"; 2 | import type { chatHistory, todos } from "../db/schema.ts"; 3 | import type { InferSelectModel } from "drizzle-orm"; 4 | 5 | /** 6 | * DrizzleのTodo型 7 | */ 8 | export type DrizzleTodo = InferSelectModel; 9 | 10 | /** 11 | * DrizzleのChatHistory型 12 | */ 13 | export type DrizzleChatHistory = InferSelectModel; 14 | 15 | /** 16 | * TODOタスクのZodスキーマ 17 | */ 18 | export const todoSchema = z.object({ 19 | /** タスクのID */ 20 | id: z.string().uuid(), 21 | /** タスクの内容 */ 22 | text: z.string().min(1, "タスクの内容は必須です"), 23 | /** タスクが完了しているかどうか */ 24 | completed: z.boolean().default(false), 25 | /** タスクの作成日時 */ 26 | createdAt: z.string().datetime(), 27 | /** タスクの更新日時 */ 28 | updatedAt: z.string().datetime(), 29 | }); 30 | 31 | /** 32 | * TODOタスクの型 33 | */ 34 | export type Todo = z.infer; 35 | 36 | /** 37 | * 新しいTODO作成用のスキーマ(IDと日付は自動生成) 38 | */ 39 | export const newTodoSchema = todoSchema.pick({ text: true }); 40 | 41 | /** 42 | * 新しいTODO作成用の型 43 | */ 44 | export type NewTodo = z.infer; 45 | 46 | /** 47 | * TODOリストのスキーマ 48 | */ 49 | export const todoListSchema = z.object({ 50 | todos: z.array(todoSchema), 51 | }); 52 | 53 | /** 54 | * TODOリストの型 55 | */ 56 | export type TodoList = z.infer; 57 | 58 | /** 59 | * TODOの更新用のスキーマ 60 | */ 61 | export const todoUpdateSchema = todoSchema.partial().pick({ 62 | text: true, 63 | completed: true, 64 | }); 65 | 66 | /** 67 | * TODOの更新用の型 68 | */ 69 | export type TodoUpdate = z.infer; 70 | 71 | /** 72 | * チャット履歴のZodスキーマ 73 | */ 74 | export const chatHistorySchema = z.object({ 75 | /** チャット履歴のID */ 76 | id: z.string().uuid(), 77 | /** ユーザーの入力プロンプト */ 78 | userPrompt: z.string(), 79 | /** AIの応答 */ 80 | aiResponse: z.string(), 81 | /** 会話が行われた日時 */ 82 | timestamp: z.string().datetime(), 83 | }); 84 | 85 | /** 86 | * チャット履歴の型 87 | */ 88 | export type ChatHistory = z.infer; 89 | 90 | /** 91 | * DrizzleとZodの型を変換するヘルパー関数 92 | */ 93 | export function drizzleTodoToTodo(todo: DrizzleTodo): Todo { 94 | return { 95 | id: todo.id, 96 | text: todo.text, 97 | completed: todo.completed, 98 | createdAt: todo.created_at.toISOString(), 99 | updatedAt: todo.updated_at.toISOString(), 100 | }; 101 | } 102 | 103 | /** 104 | * DrizzleとZodの型を変換するヘルパー関数(ChatHistory用) 105 | */ 106 | export function drizzleChatHistoryToChatHistory( 107 | history: DrizzleChatHistory, 108 | ): ChatHistory { 109 | return { 110 | id: history.id, 111 | userPrompt: history.user_prompt, 112 | aiResponse: history.ai_response, 113 | timestamp: history.timestamp.toISOString(), 114 | }; 115 | } 116 | -------------------------------------------------------------------------------- /deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "nodeModulesDir": "auto", 3 | "workspace": [ 4 | "./__deprecated/todo-cli", 5 | "./apps/*", 6 | "./modules/*", 7 | "./internal/*" 8 | ], 9 | "test": { 10 | "include": ["poc/*.ts", "modules/**/*.ts", "apps/**/*.ts"], 11 | "exclude": ["./modules/type-predictor/**/*.ts", "__deprecated/**/*.ts"] 12 | }, 13 | "lint": { 14 | "rules": { 15 | "exclude": [ 16 | "no-explicit-any", 17 | "prefer-const", 18 | "no-unused-vars", 19 | "require-await", 20 | "no-slow-types", 21 | "no-inner-declarations" 22 | ] 23 | }, 24 | "include": ["scripts/*.ts", "modules", "apps"] 25 | }, 26 | "tasks": { 27 | "build-prompt": "deno run -A .cline/build.ts", 28 | "test": "deno test -A --parallel modules apps 'scripts/*.ts'", 29 | "test:cov": "deno test -A --coverage=./cov modules apps 'scripts/*.ts' && deno coverage ./cov", 30 | "check:deps": "deno run scripts/check-deps.ts", 31 | "hook": "deno run --allow-read --allow-run --allow-write https://deno.land/x/deno_hooks@0.1.1/mod.ts", 32 | "pre-commit": "deno run -A .hooks/scripts/pre-commit-check.ts" 33 | }, 34 | "imports": { 35 | "@std/assert": "jsr:@std/assert@1", 36 | "@std/expect": "jsr:@std/expect@^1.0.13", 37 | "@std/tar": "jsr:@std/tar@^0.1.5", 38 | "@std/testing": "jsr:@std/testing@^1.0.9", 39 | "@npm/types": "npm:@npm/types@^2.0.0", 40 | "drizzle-orm": "npm:drizzle-orm@^0.40.0", 41 | "duckdb": "npm:duckdb@^1.2.0", 42 | "json-colorizer": "npm:json-colorizer@^3.0.1", 43 | "neverthrow": "npm:neverthrow@^8.2.0", 44 | "@duckdb/duckdb-wasm": "npm:@duckdb/duckdb-wasm", 45 | "zod": "npm:zod@^3.24.2", 46 | "https://deno.land/std/flags/mod.ts": "https://deno.land/std/flags/mod.ts", 47 | "https://deno.land/std/path/mod.ts": "https://deno.land/std/path/mod.ts", 48 | "npm:typescript": "npm:typescript" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /docs/articles/tskaigi.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizchi/ailab/a18699aca0c700440d4f54e6fbee80632dd594e1/docs/articles/tskaigi.md -------------------------------------------------------------------------------- /docs/libraries/mdx-to-md.md: -------------------------------------------------------------------------------- 1 | # mdx-to-md 2 | 3 | MDX ファイルを Markdown に変換するライブラリなのだ。 4 | 5 | ## インストール 6 | 7 | ```bash 8 | # npm 9 | npm install mdx-to-md 10 | 11 | # yarn 12 | yarn add mdx-to-md 13 | ``` 14 | 15 | ## 基本的な使い方 16 | 17 | ### JavaScript/TypeScript から使用 18 | 19 | ```typescript 20 | import { mdxToMd } from "mdx-to-md"; 21 | 22 | // MDX ファイルを Markdown に変換 23 | async function convertMdx() { 24 | const markdown = await mdxToMd("path/to/file.mdx"); 25 | console.log(markdown); 26 | } 27 | 28 | // 変換結果をファイルに書き込む例 29 | import { writeFile } from "fs/promises"; 30 | 31 | async function convertAndSave() { 32 | const markdown = await mdxToMd("README.mdx"); 33 | const banner = "This README was auto-generated"; 34 | const readme = `\n\n${markdown}`; 35 | 36 | await writeFile("README.md", readme); 37 | console.log("📝 Converted README.mdx -> README.md"); 38 | } 39 | ``` 40 | 41 | ### CLI から使用 42 | 43 | ```bash 44 | # 基本的な使い方 45 | mdx-to-md [sourcePath] [outPath] 46 | 47 | # 例: README.mdx を README.md に変換 48 | mdx-to-md README.mdx README.md 49 | 50 | # 出力先を省略すると、拡張子を .md に変えたファイル名になる 51 | mdx-to-md README.mdx 52 | 53 | # ウォッチモード (ファイルの変更を監視して自動変換) 54 | mdx-to-md README.mdx --watch 55 | ``` 56 | 57 | ## API リファレンス 58 | 59 | ### mdxToMd(path, options?) 60 | 61 | MDX ファイルを Markdown に変換する関数なのだ。 62 | 63 | **引数:** 64 | 65 | - `path` (string): MDX ファイルのパス 66 | - `options` (object, オプション): 67 | [mdx-bundler](https://github.com/kentcdodds/mdx-bundler) のオプション 68 | 69 | **戻り値:** 70 | 71 | - `Promise`: 変換された Markdown 文字列 72 | 73 | ## 内部実装の概要 74 | 75 | 1. MDX ファイルを読み込む 76 | 2. mdx-bundler を使用して MDX をバンドルし、React コンポーネントに変換 77 | 3. React の renderToString を使用して HTML に変換 78 | 4. node-html-markdown を使用して HTML を Markdown に変換 79 | 80 | ## ユースケース 81 | 82 | - README.mdx から README.md を自動生成 83 | - MDX で書かれたドキュメントを Markdown 形式で配布 84 | - MDX の機能(コンポーネントのインポートなど)を使いつつ、最終的には Markdown 85 | として出力 86 | 87 | ## 依存ライブラリ 88 | 89 | - [mdx-bundler](https://github.com/kentcdodds/mdx-bundler): MDX のバンドル 90 | - [node-html-markdown](https://github.com/crosstype/node-html-markdown): HTML 91 | から Markdown への変換 92 | - [react](https://reactjs.org/): React コンポーネントのレンダリング 93 | - [react-dom/server](https://reactjs.org/docs/react-dom-server.html): 94 | サーバーサイドレンダリング 95 | 96 | ## 参考リンク 97 | 98 | - [GitHub リポジトリ](https://github.com/souporserious/mdx-to-md) 99 | - [npm パッケージ](https://www.npmjs.com/package/mdx-to-md) 100 | -------------------------------------------------------------------------------- /docs/mdx-compiler-usage.md: -------------------------------------------------------------------------------- 1 | # MDX コンパイラ CLI ツール 2 | 3 | MDX コンテンツをコンパイルするための Deno CLI 4 | ツールです。[mdx-bundler](https://github.com/kentcdodds/mdx-bundler) 5 | を使用して、Markdown + JSX (MDX) ファイルをコンパイルし、必要に応じて HTML 6 | としてレンダリングします。 7 | 8 | ## 機能 9 | 10 | - MDX ファイルのコンパイル 11 | - フロントマターの抽出 12 | - コンパイル結果の保存 13 | - シンプルな HTML プレビューの生成 14 | 15 | ## 前提条件 16 | 17 | - [Deno](https://deno.land/) がインストールされていること 18 | 19 | ## インストール 20 | 21 | このツールは Deno 22 | スクリプトとして実行するため、特別なインストール手順は必要ありません。リポジトリをクローンするか、スクリプトファイルをダウンロードするだけで使用できます。 23 | 24 | ```bash 25 | # リポジトリをクローンする場合 26 | git clone 27 | cd 28 | 29 | # または scripts/mdx-compiler.ts だけをダウンロードする場合 30 | curl -O https://raw.githubusercontent.com///main/scripts/mdx-compiler.ts 31 | ``` 32 | 33 | ## 使い方 34 | 35 | ### MDX ファイルのコンパイル 36 | 37 | ```bash 38 | deno run -A scripts/mdx-compiler.ts compile [--output ] 39 | ``` 40 | 41 | #### オプション 42 | 43 | - ``: コンパイルする MDX ファイルのパス(必須) 44 | - `--output ` または `-o `: 45 | コンパイル結果を保存するファイルパス(オプション) 46 | - 指定しない場合は、コンパイル結果の概要がコンソールに表示されます 47 | - `--cwd ` または `-c `: 48 | 作業ディレクトリを指定(オプション) 49 | - 指定しない場合は、現在のディレクトリが使用されます 50 | 51 | ### コンパイル済みファイルのレンダリング 52 | 53 | ```bash 54 | deno run -A scripts/mdx-compiler.ts render [--output ] 55 | ``` 56 | 57 | #### オプション 58 | 59 | - ``: レンダリングするコンパイル済みの JS ファイルのパス(必須) 60 | - `--output ` または `-o `: HTML 61 | 出力を保存するファイルパス(オプション) 62 | - 指定しない場合は、入力ファイル名に基づいて自動生成されます 63 | 64 | ## 使用例 65 | 66 | ### 基本的な MDX ファイルのコンパイル 67 | 68 | ```bash 69 | deno run -A scripts/mdx-compiler.ts compile content/blog/hello-world.mdx --output dist/hello-world.js 70 | ``` 71 | 72 | ### コンパイル結果の HTML プレビュー生成 73 | 74 | ```bash 75 | deno run -A scripts/mdx-compiler.ts render dist/hello-world.js --output dist/hello-world.html 76 | ``` 77 | 78 | ### ワンライナーでコンパイルとレンダリングを実行 79 | 80 | ```bash 81 | deno run -A scripts/mdx-compiler.ts compile content/blog/hello-world.mdx --output dist/hello-world.js && deno run -A scripts/mdx-compiler.ts render dist/hello-world.js 82 | ``` 83 | 84 | ## MDX ファイルの例 85 | 86 | ````mdx 87 | --- 88 | title: Hello World 89 | date: 2023-01-01 90 | tags: [mdx, react] 91 | --- 92 | 93 | # Hello, MDX! 94 | 95 | これは MDX ファイルの例です。 96 | 97 | ## コードブロック 98 | 99 | ```js 100 | function hello() { 101 | console.log("Hello, world!"); 102 | } 103 | ```` 104 | 105 | ## コンポーネントのインポート 106 | 107 | import { Button } from './components/Button'; 108 | 109 | 110 | 111 | ``` 112 | ## 注意事項 113 | 114 | - このツールは簡易的な HTML レンダリングのみを提供します。完全な MDX のレンダリングには React が必要です。 115 | - サーバーサイドでのレンダリングでは、クライアントサイドのインタラクティブな機能は動作しません。 116 | - 本番環境での使用には、Next.js や Gatsby などのフレームワークと組み合わせることをお勧めします。 117 | 118 | ## トラブルシューティング 119 | 120 | ### エラー: ファイルが見つかりません 121 | 122 | 指定したファイルパスが正しいか確認してください。相対パスの場合は、現在のディレクトリからの相対パスになります。 123 | 124 | ### エラー: コンパイルに失敗しました 125 | 126 | MDX ファイルの構文が正しいか確認してください。特に JSX 部分の構文エラーが一般的な原因です。 127 | 128 | ### エラー: ファイルの書き込みに失敗しました 129 | 130 | 出力先のディレクトリが存在し、書き込み権限があるか確認してください。 131 | 132 | ## ライセンス 133 | 134 | このツールは MIT ライセンスの下で提供されています。 135 | ``` 136 | -------------------------------------------------------------------------------- /docs/practice/using-sampler-example.ts: -------------------------------------------------------------------------------- 1 | type SampleLog = ((t: T) => void) & Disposable; 2 | function createSampleLog( 3 | n: number, 4 | truncateSize = 4, 5 | print = console.log, 6 | ): SampleLog { 7 | const samples: Array<[T, number]> = []; 8 | let count = 0; 9 | return Object.assign((item: T) => { 10 | count++; 11 | if (samples.length < n) { 12 | samples.push([item, count]); 13 | } else { 14 | const r = ~~(Math.random() * count); 15 | if (r < n) samples[r] = [item, count]; 16 | } 17 | }, { 18 | [Symbol.dispose]() { 19 | samples.sort(([, a], [, b]) => a - b).forEach(([item, order]) => { 20 | print(order, _truncate(item, truncateSize)); 21 | }); 22 | }, 23 | }) as SampleLog; 24 | 25 | function _truncate(input: any, len: number): string { 26 | let str: string; 27 | if (input instanceof Object) { 28 | str = JSON.stringify(input, null, 2); 29 | } else { 30 | str = String(input); 31 | } 32 | return str.length > len ? str.slice(0, len) + "..." : str; 33 | } 34 | } 35 | 36 | // 基本的な使用例 37 | function basicExample() { 38 | using log = createSampleLog(5, 20); 39 | // 大量のデータを処理 40 | for (let i = 0; i < 1000; i++) { 41 | const len = ~~(Math.random() * 40) + 1; 42 | log(String.fromCharCode(~~(Math.random() * 50 + 30)).repeat(len)); 43 | } 44 | // スコープを抜けると自動的に結果が表示される 45 | } 46 | basicExample(); 47 | -------------------------------------------------------------------------------- /internal/foo/deno.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@i/foo", 3 | "exports": { 4 | ".": "./mod.ts" 5 | }, 6 | "lint": { 7 | "exclude": ["**/**/wip*.ts"], 8 | "rules": { 9 | "tags": ["recommended"], 10 | "include": ["no-unused-vars", "no-async-promise-executor"] 11 | } 12 | }, 13 | "tasks": { 14 | "unit": "deno test -A --parallel --doc", 15 | "cov": "rm -r ./coverage && deno test -A --parallel --coverage --doc && deno coverage ./coverage", 16 | "unused": "deno run -A npm:tsr mod.ts examples/*.ts 'test/.*\\.test\\.ts$'", 17 | "health": "deno lint && deno task cov && deno task unused" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /internal/foo/examples/chebyshev-example.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module @i/foo/examples/chebyshev-example 3 | * Example usage of chebyshevDistance function 4 | */ 5 | 6 | import { chebyshevDistance, Point } from "../mod.ts"; 7 | 8 | // Define some points 9 | const origin: Point = { x: 0, y: 0 }; 10 | const point1: Point = { x: 3, y: 4 }; 11 | const point2: Point = { x: -2, y: -3 }; 12 | const point3: Point = { x: 5, y: 5 }; 13 | 14 | // Calculate Chebyshev distances 15 | console.log( 16 | `Chebyshev distance from origin to point1 (3,4): ${ 17 | chebyshevDistance(origin, point1) 18 | }`, 19 | ); 20 | console.log( 21 | `Chebyshev distance from point1 (3,4) to point2 (-2,-3): ${ 22 | chebyshevDistance(point1, point2) 23 | }`, 24 | ); 25 | console.log( 26 | `Chebyshev distance from point2 (-2,-3) to point3 (5,5): ${ 27 | chebyshevDistance(point2, point3) 28 | }`, 29 | ); 30 | 31 | // Compare with other distance metrics 32 | import { distance } from "../mod.ts"; 33 | import { manhattanDistance } from "../mod.ts"; 34 | 35 | console.log("\nComparing different distance metrics for the same points:"); 36 | console.log(`Point1 (3,4) to Origin (0,0):`); 37 | console.log(`- Euclidean distance: ${distance(origin, point1)}`); 38 | console.log(`- Manhattan distance: ${manhattanDistance(origin, point1)}`); 39 | console.log(`- Chebyshev distance: ${chebyshevDistance(origin, point1)}`); 40 | 41 | // Explanation of when to use Chebyshev distance 42 | console.log("\nWhen to use Chebyshev distance:"); 43 | console.log( 44 | "- Chess king movement: The number of moves a king needs to reach a square", 45 | ); 46 | console.log( 47 | "- Warehouse robot movement: When the robot can move diagonally at the same speed", 48 | ); 49 | console.log("- Image processing: For certain morphological operations"); 50 | console.log("- Minimax algorithms: For evaluating worst-case scenarios"); 51 | -------------------------------------------------------------------------------- /internal/foo/examples/distance-example.ts: -------------------------------------------------------------------------------- 1 | import { distance, Point } from "../mod.ts"; 2 | 3 | // 2つの点間の距離を計算する例 4 | const point1: Point = { x: 1, y: 1 }; 5 | const point2: Point = { x: 4, y: 5 }; 6 | 7 | // 2点間のユークリッド距離を計算 8 | const result = distance(point1, point2); 9 | 10 | console.log(`Point 1: (${point1.x}, ${point1.y})`); 11 | console.log(`Point 2: (${point2.x}, ${point2.y})`); 12 | console.log(`Distance between points: ${result}`); 13 | // 出力: Distance between points: 5 14 | -------------------------------------------------------------------------------- /internal/foo/examples/manhattan-example.ts: -------------------------------------------------------------------------------- 1 | import { manhattanDistance, Point } from "../mod.ts"; 2 | 3 | // 2つの点間のマンハッタン距離を計算する例 4 | const point1: Point = { x: 1, y: 1 }; 5 | const point2: Point = { x: 4, y: 5 }; 6 | 7 | // 2点間のマンハッタン距離を計算 8 | const result = manhattanDistance(point1, point2); 9 | 10 | console.log(`Point 1: (${point1.x}, ${point1.y})`); 11 | console.log(`Point 2: (${point2.x}, ${point2.y})`); 12 | console.log(`Manhattan distance between points: ${result}`); 13 | // 出力: Manhattan distance between points: 7 14 | 15 | // 負の座標を含む例 16 | const point3: Point = { x: -1, y: -3 }; 17 | const point4: Point = { x: 2, y: -7 }; 18 | 19 | const result2 = manhattanDistance(point3, point4); 20 | console.log(`Point 3: (${point3.x}, ${point3.y})`); 21 | console.log(`Point 4: (${point4.x}, ${point4.y})`); 22 | console.log(`Manhattan distance between points: ${result2}`); 23 | // 出力: Manhattan distance between points: 7 24 | -------------------------------------------------------------------------------- /internal/foo/internal/chebyshevDistance.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module @i/foo/internal/chebyshevDistance 3 | */ 4 | 5 | import type { Point } from "../mod.ts"; 6 | 7 | /** 8 | * Calculates the Chebyshev distance between two points in a 2D space 9 | * 10 | * @param p1 First point 11 | * @param p2 Second point 12 | * @returns The maximum of absolute differences of their coordinates 13 | */ 14 | export function chebyshevDistance(p1: Point, p2: Point): number { 15 | return Math.max(Math.abs(p2.x - p1.x), Math.abs(p2.y - p1.y)); 16 | } 17 | -------------------------------------------------------------------------------- /internal/foo/internal/distance.ts: -------------------------------------------------------------------------------- 1 | import type { Point } from "../mod.ts"; 2 | 3 | /** 4 | * Calculates the Euclidean distance between two points in a 2D space 5 | * @param p1 First point 6 | * @param p2 Second point 7 | * @returns The straight-line distance between the two points 8 | */ 9 | export function distance(p1: Point, p2: Point): number { 10 | return Math.sqrt((p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2); 11 | } 12 | -------------------------------------------------------------------------------- /internal/foo/internal/manhattanDistance.ts: -------------------------------------------------------------------------------- 1 | import type { Point } from "../mod.ts"; 2 | 3 | /** 4 | * Calculates the Manhattan distance between two points in a 2D space 5 | * @param p1 First point 6 | * @param p2 Second point 7 | * @returns The sum of absolute differences of their coordinates 8 | */ 9 | export function manhattanDistance(p1: Point, p2: Point): number { 10 | return Math.abs(p1.x - p2.x) + Math.abs(p1.y - p2.y); 11 | } 12 | -------------------------------------------------------------------------------- /internal/foo/mod.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module @i/foo 3 | * A module for calculating distances between points 4 | */ 5 | 6 | /** 7 | * Point type in x, y coordinate system 8 | */ 9 | export type Point = { 10 | x: number; 11 | y: number; 12 | }; 13 | 14 | /** 15 | * Calculates the Euclidean distance between two points 16 | */ 17 | export { distance } from "./internal/distance.ts"; 18 | 19 | /** 20 | * Calculates the Manhattan distance between two points 21 | */ 22 | export { manhattanDistance } from "./internal/manhattanDistance.ts"; 23 | 24 | /** 25 | * Calculates the Chebyshev distance between two points 26 | */ 27 | export { chebyshevDistance } from "./internal/chebyshevDistance.ts"; 28 | -------------------------------------------------------------------------------- /internal/foo/test/chebyshevDistance.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module @i/foo/test/chebyshevDistance 3 | */ 4 | 5 | import { expect } from "@std/expect"; 6 | import { test } from "@std/testing/bdd"; 7 | import { chebyshevDistance, Point } from "../mod.ts"; 8 | 9 | test("chebyshevDistance calculates correct Chebyshev distance", () => { 10 | const p1: Point = { x: 0, y: 0 }; 11 | const p2: Point = { x: 3, y: 4 }; 12 | expect(chebyshevDistance(p1, p2), "max of |3-0| and |4-0|").toBe(4); 13 | }); 14 | 15 | test("chebyshevDistance with equal coordinates", () => { 16 | const p1: Point = { x: 5, y: 5 }; 17 | const p2: Point = { x: 5, y: 5 }; 18 | expect(chebyshevDistance(p1, p2), "distance to self").toBe(0); 19 | }); 20 | 21 | test("chebyshevDistance with negative values", () => { 22 | const p1: Point = { x: -2, y: -3 }; 23 | const p2: Point = { x: 2, y: 1 }; 24 | expect(chebyshevDistance(p1, p2), "max of |2-(-2)| and |1-(-3)|").toBe(4); 25 | }); 26 | -------------------------------------------------------------------------------- /internal/foo/test/distance.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "@std/expect"; 2 | import { distance } from "../mod.ts"; 3 | 4 | Deno.test("distance", () => { 5 | const p1 = { x: 1, y: 1 }; 6 | const p2 = { x: 4, y: 5 }; 7 | const result = distance(p1, p2); 8 | expect(result).toEqual(5); 9 | }); 10 | -------------------------------------------------------------------------------- /internal/foo/test/manhattanDistance.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "@std/expect"; 2 | import { manhattanDistance } from "../mod.ts"; 3 | 4 | Deno.test("manhattanDistance", () => { 5 | const p1 = { x: 1, y: 1 }; 6 | const p2 = { x: 4, y: 5 }; 7 | const result = manhattanDistance(p1, p2); 8 | // |4-1| + |5-1| = 3 + 4 = 7 9 | expect(result).toEqual(7); 10 | }); 11 | 12 | Deno.test("manhattanDistance with negative values", () => { 13 | const p1 = { x: -1, y: -3 }; 14 | const p2 = { x: 2, y: -7 }; 15 | const result = manhattanDistance(p1, p2); 16 | // |2-(-1)| + |(-7)-(-3)| = 3 + 4 = 7 17 | expect(result).toEqual(7); 18 | }); 19 | -------------------------------------------------------------------------------- /modules/assert-screenshot/__snapshots__/example-0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizchi/ailab/a18699aca0c700440d4f54e6fbee80632dd594e1/modules/assert-screenshot/__snapshots__/example-0.png -------------------------------------------------------------------------------- /modules/assert-screenshot/__snapshots__/zenn-0-diff.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizchi/ailab/a18699aca0c700440d4f54e6fbee80632dd594e1/modules/assert-screenshot/__snapshots__/zenn-0-diff.png -------------------------------------------------------------------------------- /modules/assert-screenshot/__snapshots__/zenn-0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizchi/ailab/a18699aca0c700440d4f54e6fbee80632dd594e1/modules/assert-screenshot/__snapshots__/zenn-0.png -------------------------------------------------------------------------------- /modules/assert-screenshot/deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mizchi/assert-screenshot", 3 | "version": "0.1.0", 4 | "exports": { 5 | ".": "./mod.ts" 6 | }, 7 | "license": "MIT", 8 | "tasks": {}, 9 | "imports": { 10 | "@mizchi/imgcat": "jsr:@mizchi/imgcat@^0.1.0", 11 | "@std/expect": "jsr:@std/expect@^1.0.13", 12 | "@std/testing": "jsr:@std/testing@^1.0.9", 13 | "pixelmatch": "npm:pixelmatch@^7.1.0", 14 | "pngjs": "npm:pngjs@^7.0.0", 15 | "puppeteer": "npm:puppeteer@^24.4.0" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /modules/assert-screenshot/mod.test.ts: -------------------------------------------------------------------------------- 1 | import "npm:core-js/proposals/explicit-resource-management.js"; 2 | import { assertScreenshot } from "./mod.ts"; 3 | import pptr from "puppeteer"; 4 | 5 | /** 6 | * ブラウザを使用するためのヘルパー関数 7 | */ 8 | async function useBrowser(options: pptr.LaunchOptions = {}) { 9 | const browser = await pptr.launch(options); 10 | let pages: Set = new Set(); 11 | return { 12 | browser, 13 | async newPage(): Promise<{ page: pptr.Page } & AsyncDisposable> { 14 | const page = await browser.newPage(); 15 | pages.add(page); 16 | Object.defineProperty(page, Symbol.asyncDispose, { 17 | value: async () => { 18 | pages.delete(page); 19 | await page.close(); 20 | }, 21 | }); 22 | return { 23 | page, 24 | async [Symbol.asyncDispose]() { 25 | pages.delete(page); 26 | await page.close(); 27 | }, 28 | } as any; 29 | }, 30 | async [Symbol.asyncDispose]() { 31 | await browser.close(); 32 | // Maybe pptr can not dispose all timers 33 | await new Promise((r) => setTimeout(r, 500)); 34 | }, 35 | }; 36 | } 37 | // スクリーンショットのスナップショットテスト 38 | Deno.test("snap", async (t) => { 39 | // ブラウザを起動 40 | const WIDTH = 800; 41 | const HEIGHT = 600; 42 | await using browser = await useBrowser({ 43 | headless: true, 44 | args: [ 45 | "--no-sandbox", 46 | "--disable-setuid-sandbox", 47 | "--force-device-scale-factor=1", 48 | "--high-dpi-support=1", 49 | ], 50 | }); 51 | 52 | await t.step("example.com", async () => { 53 | // ページを開く 54 | const pageCtx = await browser.newPage(); 55 | const page = pageCtx.page; 56 | await page.setViewport({ width: WIDTH, height: HEIGHT }); 57 | await page.goto("https://example.com"); 58 | // スクリーンショットを撮影 59 | const screenshot = await page.screenshot({ 60 | encoding: "binary", 61 | type: "png", 62 | }); 63 | // スクリーンショットをスナップショットと比較 64 | await assertScreenshot(t, screenshot as Uint8Array, { 65 | name: "example", 66 | diffThresholdPercentage: 0.05, 67 | updateSnapshot: false, 68 | }); 69 | }); 70 | 71 | await t.step("zenn.dev/mizchi", async () => { 72 | // ページを開く 73 | const pageCtx = await browser.newPage(); 74 | const page = pageCtx.page; 75 | await page.setViewport({ width: WIDTH, height: HEIGHT }); 76 | await page.goto("https://zenn.dev/mizchi"); 77 | // スクリーンショットを撮影 78 | const screenshot = await page.screenshot({ 79 | encoding: "binary", 80 | type: "png", 81 | }); 82 | // スクリーンショットをスナップショットと比較 83 | await assertScreenshot(t, screenshot as Uint8Array, { 84 | name: "zenn", 85 | diffThresholdPercentage: 0.1, 86 | }); 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /modules/imgcat/deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mizchi/imgcat", 3 | "version": "0.1.0", 4 | "exports": { 5 | ".": "./imgcat.ts" 6 | }, 7 | "license": "MIT", 8 | "tasks": {}, 9 | "imports": { 10 | "@std/encoding": "jsr:@std/encoding@^1.0.7", 11 | "@std/expect": "jsr:@std/expect@^1.0.13", 12 | "@std/testing": "jsr:@std/testing@^1.0.9", 13 | "picocolors": "npm:picocolors@^1.1.1", 14 | "sharp": "npm:sharp@^0.33.5" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /modules/imgcat/imgcat.ts: -------------------------------------------------------------------------------- 1 | import { decodeBase64, encodeBase64 } from "@std/encoding/base64"; 2 | import path from "node:path"; 3 | import sharp from "sharp"; 4 | 5 | export function printImageFromBase64( 6 | base64: string, 7 | size?: number, 8 | ) { 9 | if (!size) { 10 | size = decodeBase64(base64).byteLength; 11 | } 12 | Deno.stdout.writeSync( 13 | new TextEncoder().encode( 14 | `\x1B]1337;File=inline=1;size=${size}:${base64}\x07`, 15 | ), 16 | ); 17 | } 18 | export function printImage( 19 | imageData: Uint8Array, 20 | size = imageData.byteLength, 21 | ) { 22 | const b64 = encodeBase64(imageData); 23 | printImageFromBase64(b64, size); 24 | } 25 | export function printImageByPath( 26 | fpath: string, 27 | ) { 28 | const imageData = Deno.readFileSync(fpath); 29 | printImage(imageData); 30 | } 31 | 32 | async function resize( 33 | transformed: sharp.Sharp, 34 | // sharp 35 | { 36 | width, 37 | height, 38 | scale = 1.0, 39 | preserveAspectRatio = true, 40 | }: { 41 | width?: number; 42 | height?: number; 43 | scale?: number; 44 | preserveAspectRatio?: boolean; 45 | }, 46 | ) { 47 | const metadata = await transformed.metadata(); 48 | const aspectRatio = metadata.width! / metadata.height!; 49 | if (width && !height) { 50 | height = aspectRatio * width; 51 | transformed.resize(~~width, ~~height, { 52 | fit: preserveAspectRatio ? "inside" : "fill", 53 | }); 54 | } 55 | if (height && !width) { 56 | width = height / aspectRatio; 57 | transformed.resize(~~width, ~~height, { 58 | fit: preserveAspectRatio ? "inside" : "fill", 59 | }); 60 | } 61 | if (scale !== 1) { 62 | width = metadata.width! * scale; 63 | height = metadata.height! * scale; 64 | transformed.resize(~~width, ~~height, { 65 | fit: preserveAspectRatio ? "inside" : "fill", 66 | }); 67 | } 68 | return transformed.png().toBuffer(); 69 | } 70 | 71 | export async function imgcat(input: string | Uint8Array, options: { 72 | width?: number; 73 | } = {}) { 74 | const displayWidth = options.width ?? Deno.consoleSize().columns * 6; 75 | let binary; 76 | if (input instanceof Uint8Array) { 77 | binary = input; 78 | } else if (input.startsWith("data:image")) { 79 | // const base64 = input.split(",")[1]; 80 | binary = decodeBase64(input); 81 | } else { 82 | const resolved = path.isAbsolute(input) 83 | ? input 84 | : path.resolve(Deno.cwd(), input); 85 | binary = await Deno.readFile(resolved); 86 | } 87 | const resized = await resize( 88 | sharp(binary), 89 | { width: displayWidth }, 90 | ); 91 | printImage(resized, displayWidth); 92 | } 93 | 94 | if (import.meta.main) { 95 | const target = Deno.args[0]; 96 | const targetPath = path.join(Deno.cwd(), target); 97 | await imgcat(targetPath); 98 | } 99 | -------------------------------------------------------------------------------- /modules/logger/deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mizchi/logger", 3 | "version": "0.1.0", 4 | "exports": { 5 | ".": "./mod.ts" 6 | }, 7 | "license": "MIT", 8 | "tasks": {}, 9 | "imports": { 10 | "@std/expect": "jsr:@std/expect@^1.0.13", 11 | "@std/testing": "jsr:@std/testing@^1.0.9", 12 | "picocolors": "npm:picocolors@^1.1.1" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /modules/logger/examples/color-prefix.ts: -------------------------------------------------------------------------------- 1 | // 色付きプレフィックスのデモ 2 | import { createLogger } from "../mod.ts"; 3 | 4 | // 異なるタグを持つロガーを作成 5 | const appLogger = createLogger("app"); 6 | const userLogger = createLogger("user"); 7 | const authLogger = createLogger("auth"); 8 | const dbLogger = createLogger("db"); 9 | const apiLogger = createLogger("api"); 10 | const cacheLogger = createLogger("cache"); 11 | const randomLogger = createLogger(""); // タグなし(ランダムな色) 12 | 13 | // 各ロガーでメッセージを出力 14 | console.log("\n異なるタグによる色分けのデモ:"); 15 | appLogger.info("アプリケーションが起動しました"); 16 | userLogger.info("ユーザーがログインしました"); 17 | authLogger.error("認証エラーが発生しました"); 18 | dbLogger.info("データベース接続が確立されました"); 19 | apiLogger.debug("APIリクエスト: GET /users"); 20 | cacheLogger.info("キャッシュがクリアされました"); 21 | randomLogger.info("タグなしのメッセージ(ランダムな色)"); 22 | 23 | // JSONオブジェクトを含むメッセージ 24 | console.log("\nJSONオブジェクトを含むメッセージ:"); 25 | const user = { 26 | id: 1, 27 | name: "ずんだもん", 28 | email: "zunda@example.com", 29 | role: "admin", 30 | lastLogin: new Date(), 31 | }; 32 | 33 | appLogger.info("ユーザー情報:", user); 34 | 35 | // 複数行のメッセージ 36 | console.log("\n複数行のメッセージ:"); 37 | appLogger.info(` 38 | 複数行のメッセージ例 39 | これはプレフィックスのみに色が付き 40 | 残りのテキストは通常の色で表示されます。 41 | タグ "app" に基づいた色が使われています。 42 | `); 43 | 44 | // 繰り返しメッセージ 45 | console.log("\n繰り返しメッセージのグループ化:"); 46 | const repeatLogger = createLogger("repeat", { maxRepeat: 3 }); 47 | for (let i = 0; i < 5; i++) { 48 | repeatLogger.info("これは繰り返しメッセージです"); 49 | } 50 | -------------------------------------------------------------------------------- /modules/logger/examples/custom-topic-example.ts: -------------------------------------------------------------------------------- 1 | // カスタムトピック機能の例 2 | // 実行方法: LOG=info deno run -A examples/custom-topic-example.ts 3 | 4 | import { createLogger, type LogLevel } from "../mod.ts"; 5 | 6 | // 環境変数からログレベルを取得(デフォルトはerror) 7 | console.log(`現在のログレベル: ${Deno.env.get("LOG") ?? "error"}`); 8 | 9 | // showTopics を true に設定してロガーを作成 10 | const logger = createLogger("app", { 11 | showTopics: true, 12 | // logLevel: LogLevel.INFO // 環境変数が設定されていない場合は明示的に設定することもできます 13 | }); 14 | 15 | // メインロガーでログを出力 16 | logger.info("アプリケーションを起動しています"); 17 | 18 | // カスタムトピックロガーを作成 19 | const dbLogger = logger.custom("database"); 20 | const apiLogger = logger.custom("api"); 21 | const uiLogger = logger.custom("ui"); 22 | 23 | // 各トピックでログを出力 24 | dbLogger.info("データベースに接続しています"); 25 | dbLogger.error("データベース接続エラー: タイムアウト"); 26 | 27 | apiLogger.warn("APIレート制限に近づいています"); 28 | apiLogger.log("APIリクエスト: GET /users"); 29 | 30 | uiLogger.debug("UIコンポーネントをレンダリングしています"); 31 | uiLogger.info("ユーザーがログインしました"); 32 | 33 | // 注意: カスタムトピックの表示形式が [app] [database] から [app:database] に変更されました 34 | console.log("\nカスタムトピックの表示形式が [app:database] になりました"); 35 | 36 | // カスタムトピックは LOG 環境変数が設定されている場合、 37 | // または createLogger で showTopics: true を設定した場合のみ表示されます 38 | console.log("\n環境変数を設定して実行してみてください:"); 39 | console.log("LOG=debug deno run -A examples/custom-topic-example.ts"); 40 | console.log("LOG=info deno run -A examples/custom-topic-example.ts"); 41 | -------------------------------------------------------------------------------- /modules/logger/examples/log-config-example.ts: -------------------------------------------------------------------------------- 1 | // 一時的な設定を適用するconfig機能のデモ 2 | 3 | import { createLogger } from "../logger.ts"; 4 | 5 | console.log("\n=== 一時的な設定を適用するconfig機能 ==="); 6 | 7 | // 基本的なロガー 8 | const logger = createLogger("base", { 9 | singleLine: true, 10 | depth: 2, 11 | maxArrayLength: 10, 12 | }); 13 | 14 | // 深いネストを持つオブジェクト 15 | const deepObject = { 16 | level1: { 17 | level2: { 18 | level3: { 19 | level4: { 20 | level5: { 21 | value: "深い値", 22 | array: [1, 2, 3, 4, 5], 23 | }, 24 | }, 25 | }, 26 | }, 27 | }, 28 | }; 29 | 30 | // 長い配列を持つオブジェクト 31 | const arrayObject = { 32 | numbers: Array.from({ length: 100 }, (_, i) => i), 33 | }; 34 | 35 | // 複雑なオブジェクト 36 | const complexObject = { 37 | users: Array.from({ length: 20 }, (_, i) => ({ 38 | id: i, 39 | name: `ユーザー ${i}`, 40 | details: { 41 | address: { 42 | city: `都市 ${i}`, 43 | country: "日本", 44 | }, 45 | }, 46 | })), 47 | metadata: { 48 | version: "1.0", 49 | generated: new Date().toISOString(), 50 | }, 51 | }; 52 | 53 | // デフォルト設定でのログ出力 54 | console.log( 55 | "\n=== デフォルト設定(depth: 2, maxArrayLength: 10, singleLine: true) ===", 56 | ); 57 | logger.info("深いオブジェクト:", deepObject); 58 | logger.info("長い配列:", arrayObject); 59 | logger.info("複雑なオブジェクト:", complexObject); 60 | 61 | // 一時的に深さを変更 62 | console.log("\n=== 一時的に深さを変更(depth: 6) ==="); 63 | logger.with({ depth: 6 }).info("深いオブジェクト(深さ増加):", deepObject); 64 | 65 | // 一時的に配列の長さ制限を変更 66 | console.log("\n=== 一時的に配列の長さ制限を変更(maxArrayLength: 5) ==="); 67 | logger.with({ maxArrayLength: 5 }).info( 68 | "長い配列(要素数制限):", 69 | arrayObject, 70 | ); 71 | 72 | // 一時的に表示モードを変更 73 | console.log("\n=== 一時的に表示モードを変更(singleLine: false) ==="); 74 | logger.with({ singleLine: false }).info("複数行表示:", complexObject); 75 | 76 | // 複数の設定を同時に変更 77 | console.log( 78 | "\n=== 複数の設定を同時に変更(depth: 4, maxArrayLength: 3, singleLine: false) ===", 79 | ); 80 | logger.with({ 81 | depth: 4, 82 | maxArrayLength: 3, 83 | singleLine: false, 84 | }).info("複合設定変更:", complexObject); 85 | 86 | // 元の設定に戻ることを確認 87 | console.log("\n=== 元の設定に戻ることを確認 ==="); 88 | logger.info("元の設定に戻る:", complexObject); 89 | 90 | // 実用的な例:デバッグ時に一時的に詳細表示 91 | console.log("\n=== 実用的な例:デバッグ時に一時的に詳細表示 ==="); 92 | 93 | function processData(data: any) { 94 | // 通常のログ 95 | logger.info("処理開始:", data); 96 | 97 | // エラーが発生した場合、詳細情報を表示 98 | try { 99 | // 何らかの処理 100 | if (data.level1.level2.level3.nonExistentProperty) { 101 | // 存在しないプロパティにアクセス 102 | } 103 | } catch (error) { 104 | // エラー発生時に一時的に詳細設定でログ出力 105 | logger.error("エラーが発生しました:", error); 106 | logger.with({ 107 | depth: 10, 108 | maxArrayLength: 100, 109 | singleLine: false, 110 | }).error("詳細なデータ構造:", data); 111 | } 112 | 113 | // 通常のログに戻る 114 | logger.info("処理完了"); 115 | } 116 | 117 | // エラーが発生するデータで関数を実行 118 | processData(deepObject); 119 | -------------------------------------------------------------------------------- /modules/logger/examples/log-level-example.ts: -------------------------------------------------------------------------------- 1 | // ログレベルの設定例 2 | // 実行方法: LOG=debug deno run -A examples/log-level-example.ts 3 | 4 | import { createLogger, LogLevel } from "../mod.ts"; 5 | 6 | // 環境変数からログレベルを取得(デフォルトはerror) 7 | console.log(`現在のログレベル: ${Deno.env.get("LOG") ?? "error"}`); 8 | 9 | // 基本的なロガーを作成 10 | const logger = createLogger("main"); 11 | 12 | // 各レベルでログを出力 13 | logger.debug("これはデバッグメッセージです"); // 最も低いレベル 14 | logger.info("これは情報メッセージです"); 15 | logger.log("これは通常のログメッセージです"); 16 | logger.warn("これは警告メッセージです"); 17 | logger.error("これはエラーメッセージです"); // 最も高いレベル 18 | 19 | // 明示的にログレベルを設定したロガー 20 | const verboseLogger = createLogger("verbose", { logLevel: LogLevel.DEBUG }); 21 | verboseLogger.debug("このデバッグメッセージは常に表示されます"); 22 | 23 | // ログレベルを変更するには環境変数を設定します: 24 | // LOG=debug deno run -A examples/log-level-example.ts 25 | // LOG=info deno run -A examples/log-level-example.ts 26 | // LOG=log deno run -A examples/log-level-example.ts 27 | // LOG=warn deno run -A examples/log-level-example.ts 28 | // LOG=error deno run -A examples/log-level-example.ts 29 | -------------------------------------------------------------------------------- /modules/logger/examples/log-object-keys-example.ts: -------------------------------------------------------------------------------- 1 | // オブジェクトのキーを表示する機能のデモ 2 | 3 | import { createLogger } from "../logger.ts"; 4 | 5 | console.log("\n=== オブジェクトのキーを表示する機能 ==="); 6 | 7 | // 基本的なロガー 8 | const logger = createLogger("keys", { 9 | depth: 2, // 深さを制限 10 | }); 11 | 12 | // ネストされたオブジェクト 13 | const nestedObject = { 14 | user: { 15 | id: 1, 16 | name: "ずんだもん", 17 | email: "zunda@example.com", 18 | role: "admin", 19 | preferences: { 20 | theme: "dark", 21 | notifications: true, 22 | language: "ja", 23 | fontSize: 14, 24 | colors: { 25 | primary: "#007bff", 26 | secondary: "#6c757d", 27 | success: "#28a745", 28 | danger: "#dc3545", 29 | warning: "#ffc107", 30 | }, 31 | }, 32 | }, 33 | }; 34 | 35 | // 多くのキーを持つオブジェクト 36 | const manyKeysObject = { 37 | key1: 1, 38 | key2: 2, 39 | key3: 3, 40 | key4: 4, 41 | key5: 5, 42 | key6: 6, 43 | key7: 7, 44 | key8: 8, 45 | key9: 9, 46 | key10: 10, 47 | }; 48 | 49 | // 複雑なネストされたオブジェクト 50 | const complexObject = { 51 | users: [ 52 | { 53 | id: 1, 54 | name: "ユーザー1", 55 | details: { 56 | address: { 57 | city: "東京", 58 | country: "日本", 59 | postalCode: "100-0001", 60 | }, 61 | contacts: { 62 | email: "user1@example.com", 63 | phone: "090-1234-5678", 64 | }, 65 | }, 66 | }, 67 | { 68 | id: 2, 69 | name: "ユーザー2", 70 | details: { 71 | address: { 72 | city: "大阪", 73 | country: "日本", 74 | postalCode: "530-0001", 75 | }, 76 | contacts: { 77 | email: "user2@example.com", 78 | phone: "090-8765-4321", 79 | }, 80 | }, 81 | }, 82 | ], 83 | metadata: { 84 | version: "1.0", 85 | generated: new Date().toISOString(), 86 | settings: { 87 | theme: "dark", 88 | language: "ja", 89 | }, 90 | }, 91 | }; 92 | 93 | // 通常のログ出力 94 | console.log("\n=== 通常のログ出力 ==="); 95 | logger.info("ネストされたオブジェクト:", nestedObject); 96 | logger.info("多くのキーを持つオブジェクト:", manyKeysObject); 97 | logger.info("複雑なネストされたオブジェクト:", complexObject); 98 | 99 | // 深さを変更してログ出力 100 | console.log("\n=== 深さを変更してログ出力 (depth: 4) ==="); 101 | logger.with({ depth: 4 }).info( 102 | "ネストされたオブジェクト (深さ増加):", 103 | nestedObject, 104 | ); 105 | 106 | // 一行表示モードでログ出力 107 | console.log("\n=== 一行表示モードでログ出力 ==="); 108 | logger.with({ singleLine: true }).info( 109 | "ネストされたオブジェクト (一行表示):", 110 | nestedObject, 111 | ); 112 | 113 | // 小さなオブジェクトの表示 114 | console.log("\n=== 小さなオブジェクトの表示 ==="); 115 | logger.info("小さなオブジェクト:", { a: 1, b: 2 }); 116 | logger.with({ singleLine: true }).info("小さなオブジェクト (一行表示):", { 117 | a: 1, 118 | b: 2, 119 | }); 120 | 121 | // 空のオブジェクトと配列の表示 122 | console.log("\n=== 空のオブジェクトと配列の表示 ==="); 123 | logger.info("空のオブジェクトと配列:", { 124 | emptyObj: {}, 125 | emptyArr: [], 126 | nestedEmpty: { 127 | empty: {}, 128 | }, 129 | }); 130 | -------------------------------------------------------------------------------- /modules/logger/examples/log-truncate-example.ts: -------------------------------------------------------------------------------- 1 | // JSONの深さと配列長さの制限機能のデモ 2 | 3 | import { createLogger } from "../logger.ts"; 4 | 5 | console.log("\n=== 配列の要素数制限 ==="); 6 | const arrayLogger = createLogger("array"); 7 | 8 | // 長い配列を含むオブジェクト 9 | const longArrayObject = { 10 | numbers: Array.from({ length: 100 }, (_, i) => i), 11 | letters: Array.from( 12 | { length: 50 }, 13 | (_, i) => String.fromCharCode(97 + (i % 26)), 14 | ), 15 | }; 16 | 17 | // デフォルト設定(maxArrayLength = 10) 18 | console.log("デフォルト設定(maxArrayLength = 10):"); 19 | arrayLogger.info("長い配列:", longArrayObject); 20 | 21 | // カスタム設定(maxArrayLength = 5) 22 | console.log("\nカスタム設定(maxArrayLength = 5):"); 23 | const customArrayLogger = createLogger("custom-array", { maxArrayLength: 5 }); 24 | customArrayLogger.info("長い配列:", longArrayObject); 25 | 26 | console.log("\n=== JSONの深さ制限 ==="); 27 | const depthLogger = createLogger("depth"); 28 | 29 | // 深いネストを持つオブジェクト 30 | const deepObject = { 31 | level1: { 32 | level2: { 33 | level3: { 34 | level4: { 35 | level5: { 36 | value: "深い値", 37 | array: [1, 2, 3, 4, 5], 38 | }, 39 | }, 40 | }, 41 | }, 42 | }, 43 | }; 44 | 45 | // デフォルト設定(depth = 5) 46 | console.log("デフォルト設定(depth = 5):"); 47 | depthLogger.info("深いオブジェクト:", deepObject); 48 | 49 | // 浅い表示(depth = 2) 50 | console.log("\n浅い表示(depth = 2):"); 51 | const shallowLogger = createLogger("shallow", { depth: 2 }); 52 | shallowLogger.info("深いオブジェクト:", deepObject); 53 | 54 | // 深い表示(depth = 6) 55 | console.log("\n深い表示(depth = 6):"); 56 | const deepLogger = createLogger("deep", { depth: 6 }); 57 | deepLogger.info("深いオブジェクト:", deepObject); 58 | 59 | console.log("\n=== 複合的な制限 ==="); 60 | // 複雑なオブジェクト(深いネストと長い配列を含む) 61 | const complexObject = { 62 | users: Array.from({ length: 20 }, (_, i) => ({ 63 | id: i, 64 | name: `ユーザー ${i}`, 65 | details: { 66 | address: { 67 | city: `都市 ${i}`, 68 | country: "日本", 69 | }, 70 | }, 71 | })), 72 | metadata: { 73 | version: "1.0", 74 | generated: new Date().toISOString(), 75 | settings: { 76 | theme: { 77 | colors: { 78 | primary: "#007bff", 79 | secondary: "#6c757d", 80 | }, 81 | }, 82 | }, 83 | }, 84 | }; 85 | 86 | // 複合的な制限を適用 87 | console.log("複合的な制限(depth = 3, maxArrayLength = 5):"); 88 | const complexLogger = createLogger("complex", { 89 | depth: 3, 90 | maxArrayLength: 5, 91 | }); 92 | complexLogger.info("複雑なオブジェクト:", complexObject); 93 | 94 | // 一行表示モードでの制限 95 | console.log( 96 | "\n一行表示モードでの制限(depth = 2, maxArrayLength = 3, singleLine = true):", 97 | ); 98 | const singleLineLogger = createLogger("single", { 99 | depth: 2, 100 | maxArrayLength: 3, 101 | singleLine: true, 102 | }); 103 | singleLineLogger.info("複雑なオブジェクト:", complexObject); 104 | -------------------------------------------------------------------------------- /modules/logger/examples/stacktrace-example.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * スタックトレース機能のデモ 3 | * 4 | * このサンプルでは、ロガーのスタックトレース機能を使用して、 5 | * ログメッセージの呼び出し元の情報を表示する方法を示します。 6 | */ 7 | 8 | import { createLogger } from "../mod.ts"; 9 | 10 | // 通常のロガー 11 | const normalLogger = createLogger("normal"); 12 | 13 | // スタックトレース情報を表示するロガー(短い形式) 14 | const shortStackLogger = createLogger("short-stack", { 15 | showCaller: true, 16 | }); 17 | 18 | // スタックトレース情報を表示するロガー(詳細な形式) 19 | const detailedStackLogger = createLogger("detailed-stack", { 20 | showCaller: true, 21 | detailedCaller: true, 22 | }); 23 | 24 | // 直接呼び出し 25 | function directCall() { 26 | console.log("\n--- 直接呼び出し ---"); 27 | normalLogger.info("通常のログ(スタックトレースなし)"); 28 | shortStackLogger.info("短いスタックトレース付きログ"); 29 | detailedStackLogger.info("詳細なスタックトレース付きログ"); 30 | } 31 | 32 | // ネストされた呼び出し 33 | function nestedFunction() { 34 | anotherFunction(); 35 | } 36 | 37 | function anotherFunction() { 38 | deepestFunction(); 39 | } 40 | 41 | function deepestFunction() { 42 | console.log("\n--- ネストされた呼び出し ---"); 43 | normalLogger.info("通常のログ(スタックトレースなし)"); 44 | shortStackLogger.info("短いスタックトレース付きログ"); 45 | detailedStackLogger.info("詳細なスタックトレース付きログ"); 46 | } 47 | 48 | // 一時的な設定を使用した呼び出し 49 | function temporaryConfig() { 50 | console.log("\n--- 一時的な設定 ---"); 51 | normalLogger.with({ showCaller: true }).info( 52 | "一時的にスタックトレースを有効化", 53 | ); 54 | shortStackLogger.with({ detailedCaller: true }).info( 55 | "一時的に詳細なスタックトレースを有効化", 56 | ); 57 | detailedStackLogger.with({ showCaller: false }).info( 58 | "一時的にスタックトレースを無効化", 59 | ); 60 | } 61 | 62 | // 実行 63 | directCall(); 64 | nestedFunction(); 65 | temporaryConfig(); 66 | 67 | // エラーログでのスタックトレース 68 | console.log("\n--- エラーログでのスタックトレース ---"); 69 | try { 70 | throw new Error("テストエラー"); 71 | } catch (error) { 72 | normalLogger.error("エラーが発生しました", error); 73 | shortStackLogger.error("エラーが発生しました(スタックトレース付き)", error); 74 | detailedStackLogger.error( 75 | "エラーが発生しました(詳細なスタックトレース付き)", 76 | error, 77 | ); 78 | } 79 | -------------------------------------------------------------------------------- /modules/logger/examples/tag-color-example.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * タグに基づく色分け機能のデモ 3 | * 4 | * このサンプルでは、タグ名からハッシュ値を生成し、 5 | * それに基づいて色を選択する機能を示します。 6 | * 同じタグは常に同じ色になります。 7 | */ 8 | 9 | import { createLogger } from "../mod.ts"; 10 | 11 | // 様々なタグでロガーを作成 12 | const loggers = [ 13 | createLogger("app"), 14 | createLogger("server"), 15 | createLogger("database"), 16 | createLogger("auth"), 17 | createLogger("api"), 18 | createLogger("cache"), 19 | createLogger("router"), 20 | createLogger("controller"), 21 | createLogger("model"), 22 | createLogger("view"), 23 | createLogger("utils"), 24 | createLogger("config"), 25 | ]; 26 | 27 | // 各ロガーでメッセージを出力 28 | console.log("--- タグに基づく色分け ---"); 29 | loggers.forEach((logger, index) => { 30 | logger.info(`これはロガー #${index + 1} からのメッセージです`); 31 | }); 32 | 33 | // 同じタグは常に同じ色になることを確認 34 | console.log("\n--- 同じタグは同じ色 ---"); 35 | const appLogger = createLogger("app"); 36 | appLogger.info("1回目のメッセージ"); 37 | appLogger.info("2回目のメッセージ"); 38 | appLogger.info("3回目のメッセージ"); 39 | 40 | // 未指定の場合はランダムな色 41 | console.log("\n--- タグ未指定はランダムな色 ---"); 42 | const randomLogger1 = createLogger(""); 43 | const randomLogger2 = createLogger(""); 44 | const randomLogger3 = createLogger(""); 45 | 46 | randomLogger1.info("ランダム色のロガー 1"); 47 | randomLogger2.info("ランダム色のロガー 2"); 48 | randomLogger3.info("ランダム色のロガー 3"); 49 | 50 | // 異なるログレベル 51 | console.log("\n--- 異なるログレベル ---"); 52 | const levelLogger = createLogger("levels"); 53 | levelLogger.debug("デバッグメッセージ"); 54 | levelLogger.info("情報メッセージ"); 55 | levelLogger.error("エラーメッセージ"); 56 | -------------------------------------------------------------------------------- /modules/logger/examples/test-color.ts: -------------------------------------------------------------------------------- 1 | // プレフィックスの色付けテスト 2 | 3 | import { createLogger } from "./logger.ts"; 4 | 5 | // 基本的なロガー 6 | console.log("\n=== 基本的なロガー ==="); 7 | const basicLogger = createLogger("basic"); 8 | basicLogger.debug("デバッグメッセージ"); 9 | basicLogger.info("情報メッセージ"); 10 | basicLogger.log("通常のログメッセージ"); 11 | basicLogger.warn("警告メッセージ"); 12 | basicLogger.error("エラーメッセージ"); 13 | 14 | // タイムスタンプ付きロガー 15 | console.log("\n=== タイムスタンプ付きロガー ==="); 16 | const timeLogger = createLogger("time", { showTimestamp: true }); 17 | timeLogger.debug("デバッグメッセージ"); 18 | timeLogger.info("情報メッセージ"); 19 | timeLogger.log("通常のログメッセージ"); 20 | timeLogger.warn("警告メッセージ"); 21 | timeLogger.error("エラーメッセージ"); 22 | 23 | // 呼び出し元情報付きロガー 24 | console.log("\n=== 呼び出し元情報付きロガー ==="); 25 | const callerLogger = createLogger("caller", { showCaller: true }); 26 | callerLogger.debug("デバッグメッセージ"); 27 | callerLogger.info("情報メッセージ"); 28 | callerLogger.log("通常のログメッセージ"); 29 | callerLogger.warn("警告メッセージ"); 30 | callerLogger.error("エラーメッセージ"); 31 | 32 | // タイムスタンプと呼び出し元情報の両方 33 | console.log("\n=== タイムスタンプと呼び出し元情報の両方 ==="); 34 | const fullLogger = createLogger("full", { 35 | showTimestamp: true, 36 | showCaller: true, 37 | }); 38 | fullLogger.debug("デバッグメッセージ"); 39 | fullLogger.info("情報メッセージ"); 40 | fullLogger.log("通常のログメッセージ"); 41 | fullLogger.warn("警告メッセージ"); 42 | fullLogger.error("エラーメッセージ"); 43 | 44 | // オブジェクト引数付き 45 | console.log("\n=== オブジェクト引数付き ==="); 46 | const objLogger = createLogger("object"); 47 | objLogger.info("オブジェクト:", { id: 1, name: "テスト" }); 48 | objLogger.info({ id: 2, name: "オブジェクトのみ" }); 49 | 50 | // 一時的な設定変更 51 | console.log("\n=== 一時的な設定変更 ==="); 52 | const tempLogger = createLogger("temp"); 53 | tempLogger.info("通常の情報メッセージ"); 54 | tempLogger.with({ showTimestamp: true }).info( 55 | "タイムスタンプ付き情報メッセージ", 56 | ); 57 | 58 | // カスタムトピック 59 | console.log("\n=== カスタムトピック ==="); 60 | const appLogger = createLogger("app", { showTopics: true }); 61 | const dbTopic = appLogger.custom("db"); 62 | const authTopic = appLogger.custom("auth"); 63 | 64 | appLogger.info("アプリケーションの情報メッセージ"); 65 | dbTopic.info("データベースの情報メッセージ"); 66 | authTopic.warn("認証の警告メッセージ"); 67 | -------------------------------------------------------------------------------- /modules/logger/examples/timestamp-example.ts: -------------------------------------------------------------------------------- 1 | import { createLogger, type LogLevel } from "../mod.ts"; 2 | 3 | // サンプルデータ 4 | const sampleObject = { 5 | user: { 6 | id: 1, 7 | name: "ずんだもん", 8 | email: "zunda@example.com", 9 | preferences: { 10 | theme: "dark", 11 | notifications: true, 12 | }, 13 | }, 14 | items: [ 15 | { id: 101, name: "アイテム1", price: 1000 }, 16 | { id: 102, name: "アイテム2", price: 2000 }, 17 | { id: 103, name: "アイテム3", price: 3000 }, 18 | ], 19 | }; 20 | 21 | console.log("=== タイムスタンプ表示のデモ ===\n"); 22 | 23 | // デフォルト設定(タイムスタンプなし) 24 | const defaultLogger = createLogger("default"); 25 | console.log("デフォルト設定(タイムスタンプなし):"); 26 | defaultLogger.log("これはデフォルト設定のログです", sampleObject); 27 | 28 | // タイムスタンプあり(時間のみ) 29 | const timeOnlyLogger = createLogger("time-only", { showTimestamp: true }); 30 | console.log("\nタイムスタンプあり(時間のみ):"); 31 | timeOnlyLogger.log("これは時間のみのタイムスタンプ付きログです", sampleObject); 32 | 33 | // タイムスタンプあり(ISO形式) 34 | const isoTimeLogger = createLogger("iso-time", { 35 | showTimestamp: true, 36 | timeOnly: false, 37 | }); 38 | console.log("\nタイムスタンプあり(ISO形式):"); 39 | isoTimeLogger.log("これはISO形式のタイムスタンプ付きログです", sampleObject); 40 | 41 | // 一時的な設定でタイムスタンプを表示 42 | console.log("\n一時的な設定でタイムスタンプを表示:"); 43 | defaultLogger.with({ showTimestamp: true }).log( 44 | "一時的にタイムスタンプを表示しています", 45 | sampleObject, 46 | ); 47 | 48 | // 一時的な設定でタイムスタンプを非表示 49 | console.log("\n一時的な設定でタイムスタンプを非表示:"); 50 | timeOnlyLogger.with({ showTimestamp: false }).log( 51 | "一時的にタイムスタンプを非表示にしています", 52 | sampleObject, 53 | ); 54 | -------------------------------------------------------------------------------- /modules/logger/examples/truncate-json-example.ts: -------------------------------------------------------------------------------- 1 | import { createLogger, LogLevel } from "../mod.ts"; 2 | 3 | // ロガーの作成 4 | const logger = createLogger("example", { 5 | logLevel: LogLevel.DEBUG, 6 | }); 7 | 8 | console.log("=== 文字列長制限のデモ ==="); 9 | 10 | // 大きなJSONオブジェクトを作成 11 | const largeObject = { 12 | data: Array.from({ length: 50 }, (_, i) => ({ 13 | id: i, 14 | name: `Item ${i}`, 15 | description: 16 | `This is a description for item ${i} that will contribute to the overall length.`, 17 | tags: Array.from({ length: 5 }, (_, j) => `tag-${j}`), 18 | })), 19 | }; 20 | 21 | // 制限なし 22 | console.log("\n制限なし (maxLength = 0):"); 23 | logger.log("大きなJSONオブジェクト", largeObject); 24 | 25 | // 短い制限 26 | console.log("\n短い制限 (maxLength = 100):"); 27 | logger.with({ maxLength: 100 }).log("大きなJSONオブジェクト", largeObject); 28 | 29 | // 中程度の制限 30 | console.log("\n中程度の制限 (maxLength = 500):"); 31 | logger.with({ maxLength: 500 }).log("大きなJSONオブジェクト", largeObject); 32 | 33 | // 大きな配列 34 | console.log("\n=== 大きな配列の文字列長制限 ==="); 35 | const largeArray = Array.from( 36 | { length: 100 }, 37 | (_, i) => `Item ${i} with additional text`, 38 | ); 39 | 40 | // 制限なし 41 | console.log("\n制限なし (maxLength = 0):"); 42 | logger.log("大きな配列", largeArray); 43 | 44 | // 短い制限 45 | console.log("\n短い制限 (maxLength = 100):"); 46 | logger.with({ maxLength: 100 }).log("大きな配列", largeArray); 47 | 48 | // ネストされたオブジェクト 49 | console.log("\n=== ネストされたオブジェクトの文字列長制限 ==="); 50 | const nestedObject = { 51 | level1: { 52 | level2: { 53 | level3: { 54 | data: Array.from({ length: 20 }, (_, i) => ({ 55 | id: i, 56 | name: `Nested item ${i}`, 57 | properties: { 58 | a: `Property A for item ${i}`, 59 | b: `Property B for item ${i}`, 60 | }, 61 | })), 62 | }, 63 | }, 64 | }, 65 | }; 66 | 67 | // 制限なし 68 | console.log("\n制限なし (maxLength = 0):"); 69 | logger.with({ depth: 5 }).log("ネストされたオブジェクト", nestedObject); 70 | 71 | // 短い制限 72 | console.log("\n短い制限 (maxLength = 200):"); 73 | logger.with({ maxLength: 200, depth: 5 }).log( 74 | "ネストされたオブジェクト", 75 | nestedObject, 76 | ); 77 | -------------------------------------------------------------------------------- /modules/logger/examples/typed-topic-example.ts: -------------------------------------------------------------------------------- 1 | // 型付きトピックロガーの例 2 | // 実行方法: LOG=debug deno run -A examples/typed-topic-example.ts 3 | 4 | import { createLogger, LogLevel } from "../mod.ts"; 5 | 6 | // 環境変数からログレベルを取得(デフォルトはerror) 7 | console.log(`現在のログレベル: ${Deno.env.get("LOG") ?? "error"}`); 8 | 9 | // 基本的なロガーを作成(showTopicsをtrueに設定) 10 | const logger = createLogger("app", { 11 | showTopics: true, 12 | logLevel: LogLevel.DEBUG, // デバッグレベルに設定 13 | }); 14 | 15 | // 型定義 16 | interface UserData { 17 | id: number; 18 | name: string; 19 | role: string; 20 | lastLogin: Date; 21 | } 22 | 23 | interface ApiResponse { 24 | status: number; 25 | message: string; 26 | data: unknown; 27 | } 28 | 29 | // 型付きトピックロガーを作成 30 | const userLogger = logger.topic("user"); 31 | const apiLogger = logger.topic("api"); 32 | 33 | // 型付きデータでログを出力 34 | userLogger.info("ユーザーがログインしました", { 35 | id: 1001, 36 | name: "ずんだもん", 37 | role: "admin", 38 | lastLogin: new Date(), 39 | }); 40 | 41 | userLogger.warn("ユーザーの権限が変更されました", { 42 | id: 1001, 43 | name: "ずんだもん", 44 | role: "user", // adminからuserに変更 45 | lastLogin: new Date(), 46 | }); 47 | 48 | // 型チェックが効くため、以下はコンパイルエラーになる 49 | // userLogger.info("不正なデータ", { id: "string" }); // idはnumberであるべき 50 | 51 | // APIレスポンスのログ 52 | apiLogger.info("APIリクエスト成功", { 53 | status: 200, 54 | message: "OK", 55 | data: { items: [1, 2, 3] }, 56 | }); 57 | 58 | apiLogger.error("APIリクエスト失敗", { 59 | status: 404, 60 | message: "Not Found", 61 | data: null, 62 | }); 63 | 64 | // 通常のトピックロガーとの比較 65 | const normalLogger = logger.custom("normal"); 66 | normalLogger.info("これは通常のトピックロガーです", { anyData: true }); 67 | 68 | console.log("\n型付きトピックロガーは、型安全なログ出力を提供します。"); 69 | console.log("また、トピックの表示形式が [app:topic] になっています。"); 70 | -------------------------------------------------------------------------------- /modules/logger/mod.ts: -------------------------------------------------------------------------------- 1 | export { createLogger, LogLevel } from "./logger.ts"; 2 | export { createSampler, type Sampler, type SamplerOptions } from "./sampler.ts"; 3 | -------------------------------------------------------------------------------- /modules/logger/test/log-indent.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "@std/expect"; 2 | import { test } from "@std/testing/bdd"; 3 | import { createLogger, LogLevel } from "../logger.ts"; 4 | import type { LogEntry } from "../types.ts"; 5 | 6 | // テスト用のロガーオプション(すべてのログレベルを表示) 7 | const testLoggerOptions = { 8 | logLevel: LogLevel.DEBUG, // テスト中はすべてのログレベルを表示 9 | }; 10 | 11 | test("displayLogEntry - JSONの長さに基づくインデント", () => { 12 | const logger = createLogger("test", testLoggerOptions); 13 | 14 | // 短いJSONオブジェクト 15 | const shortEntry: LogEntry = { 16 | timestamp: new Date(), 17 | level: LogLevel.INFO, 18 | tag: "test", 19 | args: ["短いJSON", { a: 1, b: 2 }], 20 | }; 21 | 22 | // 長いJSONオブジェクト 23 | const longEntry: LogEntry = { 24 | timestamp: new Date(), 25 | level: LogLevel.INFO, 26 | tag: "test", 27 | args: ["長いJSON", { 28 | name: "これは長いJSONオブジェクトです", 29 | description: "20文字以上の長さがあります", 30 | values: [1, 2, 3, 4, 5], 31 | }], 32 | }; 33 | 34 | // 一行表示モードでの短いJSONの表示 35 | const shortFormatted = logger.display(shortEntry, { singleLine: true }); 36 | 37 | // 一行表示モードでの長いJSONの表示 38 | const longFormatted = logger.display(longEntry, { singleLine: true }); 39 | 40 | // 短いJSONは一行で表示される 41 | expect(shortFormatted.split("\n").length).toBe(1); 42 | 43 | // 長いJSONは複数行で表示される(インデントされる) 44 | expect(longFormatted.split("\n").length).toBeGreaterThan(1); 45 | expect(longFormatted).toContain("\n"); 46 | }); 47 | 48 | test("displayLogEntry - 長いJSONは常にインデントされる", () => { 49 | const logger = createLogger("test", testLoggerOptions); 50 | 51 | // 長いJSONオブジェクト 52 | const longObject: Record = {}; 53 | // 20文字以上になるようにプロパティを追加 54 | for (let i = 0; i < 10; i++) { 55 | longObject[`key${i}`] = `value${i}`; 56 | } 57 | 58 | const entry: LogEntry = { 59 | timestamp: new Date(), 60 | level: LogLevel.INFO, 61 | tag: "test", 62 | args: ["長いオブジェクト", longObject], 63 | }; 64 | 65 | // 一行表示モードでも長いJSONはインデントされる 66 | const formatted = logger.display(entry, { singleLine: true }); 67 | 68 | // 複数行になっていることを確認 69 | expect(formatted.split("\n").length).toBeGreaterThan(1); 70 | 71 | // インデントが含まれていることを確認 72 | expect(formatted).toMatch(/\n\s+/); 73 | }); 74 | -------------------------------------------------------------------------------- /modules/logger/test/log-timestamp.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "@std/expect"; 2 | import { test } from "@std/testing/bdd"; 3 | import { createLogger, LogLevel } from "../logger.ts"; 4 | import type { LogEntry } from "../types.ts"; 5 | 6 | // テスト用のロガーオプション(すべてのログレベルを表示) 7 | const testLoggerOptions = { 8 | logLevel: LogLevel.DEBUG, // テスト中はすべてのログレベルを表示 9 | }; 10 | 11 | test("displayLogEntry - デフォルトでタイムスタンプ非表示", () => { 12 | const logger = createLogger("test", testLoggerOptions); 13 | 14 | const entry: LogEntry = { 15 | timestamp: new Date(), 16 | level: LogLevel.INFO, 17 | tag: "test", 18 | args: ["タイムスタンプなしのメッセージ"], 19 | }; 20 | 21 | // デフォルト設定(showTimestamp = false) 22 | const formatted = logger.display(entry); 23 | 24 | // タイムスタンプが含まれていないことを確認 25 | // タイムスタンプの形式は "HH:MM:SS" なので、これにマッチしないことを確認 26 | expect(formatted).not.toMatch(/^\d{2}:\d{2}:\d{2}/); 27 | 28 | // 正しいフォーマットになっていることを確認 29 | expect(formatted).toMatch(/^\[test\] タイムスタンプなしのメッセージ/); 30 | }); 31 | 32 | test("displayLogEntry - タイムスタンプ表示設定", () => { 33 | const logger = createLogger("test", { 34 | ...testLoggerOptions, 35 | showTimestamp: true, 36 | }); 37 | 38 | const entry: LogEntry = { 39 | timestamp: new Date(), 40 | level: LogLevel.INFO, 41 | tag: "test", 42 | args: ["タイムスタンプありのメッセージ"], 43 | }; 44 | 45 | // showTimestamp = true の設定 46 | const formatted = logger.display(entry); 47 | 48 | // タイムスタンプが含まれていることを確認 49 | // タイムスタンプの形式は "HH:MM:SS" なので、これにマッチすることを確認 50 | expect(formatted).toMatch(/^\d{2}:\d{2}:\d{2}/); 51 | 52 | // 正しいフォーマットになっていることを確認 53 | expect(formatted).toMatch( 54 | /^\d{2}:\d{2}:\d{2} \[test\] タイムスタンプありのメッセージ/, 55 | ); 56 | }); 57 | 58 | test("displayLogEntry - 一時的なタイムスタンプ設定", () => { 59 | // デフォルト設定(showTimestamp = false)のロガー 60 | const logger = createLogger("test", testLoggerOptions); 61 | 62 | const entry: LogEntry = { 63 | timestamp: new Date(), 64 | level: LogLevel.INFO, 65 | tag: "test", 66 | args: ["メッセージ"], 67 | }; 68 | 69 | // 一時的に showTimestamp = true に設定 70 | const formattedWithTimestamp = logger.display(entry, { showTimestamp: true }); 71 | 72 | // タイムスタンプが含まれていることを確認 73 | expect(formattedWithTimestamp).toMatch(/^\d{2}:\d{2}:\d{2}/); 74 | 75 | // 一時的に showTimestamp = false に設定(明示的に指定) 76 | const formattedWithoutTimestamp = logger.display(entry, { 77 | showTimestamp: false, 78 | }); 79 | 80 | // タイムスタンプが含まれていないことを確認 81 | expect(formattedWithoutTimestamp).not.toMatch(/^\d{2}:\d{2}:\d{2}/); 82 | }); 83 | 84 | test("displayLogEntry - ISO形式のタイムスタンプ", () => { 85 | const logger = createLogger("test", { 86 | ...testLoggerOptions, 87 | showTimestamp: true, 88 | timeOnly: false, 89 | }); 90 | 91 | const entry: LogEntry = { 92 | timestamp: new Date(), 93 | level: LogLevel.INFO, 94 | tag: "test", 95 | args: ["ISO形式のタイムスタンプ"], 96 | }; 97 | 98 | // timeOnly = false の設定(ISO形式) 99 | const formatted = logger.display(entry); 100 | 101 | // ISO形式のタイムスタンプが含まれていることを確認 102 | // ISO形式の時間部分は "HH:MM:SS" なので、これにマッチすることを確認 103 | expect(formatted).toMatch(/^\d{2}:\d{2}:\d{2}/); 104 | 105 | // 正しいフォーマットになっていることを確認 106 | expect(formatted).toMatch( 107 | /^\d{2}:\d{2}:\d{2} \[test\] ISO形式のタイムスタンプ/, 108 | ); 109 | }); 110 | -------------------------------------------------------------------------------- /modules/logger/test/log-truncate.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "@std/expect"; 2 | import { test } from "@std/testing/bdd"; 3 | import { createLogger, LogLevel } from "../logger.ts"; 4 | import type { LogEntry } from "../types.ts"; 5 | 6 | // テスト用のロガーオプション(すべてのログレベルを表示) 7 | const testLoggerOptions = { 8 | logLevel: LogLevel.DEBUG, // テスト中はすべてのログレベルを表示 9 | }; 10 | 11 | test("displayLogEntry - 配列の要素数制限", () => { 12 | const logger = createLogger("test", testLoggerOptions); 13 | 14 | // 大きな配列を含むログエントリ 15 | const entry: LogEntry = { 16 | timestamp: new Date(), 17 | level: LogLevel.INFO, 18 | tag: "test", 19 | args: ["配列テスト", { numbers: Array.from({ length: 20 }, (_, i) => i) }], 20 | }; 21 | 22 | // デフォルト設定(maxArrayLength = 10) 23 | const formatted = logger.display(entry); 24 | expect(formatted).toContain("... 10 more items"); // 10要素以降が省略されていることを確認 25 | 26 | // カスタム設定(maxArrayLength = 5) 27 | const formattedCustom = logger.display(entry, { maxArrayLength: 5 }); 28 | expect(formattedCustom).toContain("... 15 more items"); // 5要素以降が省略されていることを確認 29 | }); 30 | 31 | test("displayLogEntry - JSONの深さ制限", () => { 32 | const logger = createLogger("test", testLoggerOptions); 33 | 34 | // 深いネストを持つオブジェクト 35 | const deepObject = { 36 | level1: { 37 | level2: { 38 | level3: { 39 | level4: { 40 | level5: { 41 | value: "deep", 42 | }, 43 | }, 44 | }, 45 | }, 46 | }, 47 | }; 48 | 49 | const entry: LogEntry = { 50 | timestamp: new Date(), 51 | level: LogLevel.INFO, 52 | tag: "test", 53 | args: ["深さテスト", deepObject], 54 | }; 55 | 56 | // depth=2 の場合(浅い表示) 57 | const formattedShallow = logger.display(entry, { depth: 2 }); 58 | 59 | // depth=6 の場合(深い表示) 60 | const formattedDeep = logger.display(entry, { depth: 6 }); 61 | 62 | // 浅い表示では深い値が表示されないことを確認 63 | expect(formattedShallow).not.toContain("deep"); 64 | 65 | // 深い表示では深い値が表示されることを確認 66 | expect(formattedDeep).toContain("deep"); 67 | }); 68 | 69 | test("displayLogEntry - 複合的な制限", () => { 70 | const logger = createLogger("test", testLoggerOptions); 71 | 72 | // 複雑なオブジェクト(深いネストと長い配列を含む) 73 | const complexObject = { 74 | users: Array.from({ length: 20 }, (_, i) => ({ 75 | id: i, 76 | name: `User ${i}`, 77 | details: { 78 | address: { 79 | city: `City ${i}`, 80 | country: "Japan", 81 | }, 82 | }, 83 | })), 84 | metadata: { 85 | version: "1.0", 86 | generated: new Date().toISOString(), 87 | }, 88 | }; 89 | 90 | const entry: LogEntry = { 91 | timestamp: new Date(), 92 | level: LogLevel.INFO, 93 | tag: "test", 94 | args: ["複合テスト", complexObject], 95 | }; 96 | 97 | // 複合的な制限を適用 98 | const formatted = logger.display(entry, { 99 | depth: 3, 100 | maxArrayLength: 5, 101 | }); 102 | 103 | // 配列が制限されていることを確認 104 | expect(formatted).toContain("... 15 more items"); 105 | 106 | // 深さが制限されていることを確認 107 | // address オブジェクトは [Object] として表示される 108 | expect(formatted).toContain("[Object]"); 109 | 110 | // 深すぎる値は表示されないことを確認 111 | expect(formatted).not.toContain("city"); 112 | expect(formatted).not.toContain("country"); 113 | }); 114 | -------------------------------------------------------------------------------- /modules/npm-summary/README.md: -------------------------------------------------------------------------------- 1 | # @mizchi/npm-summary 2 | 3 | A Deno module to extract and analyze TypeScript type definitions from npm 4 | packages. 5 | 6 | ## Features 7 | 8 | - Extract TypeScript declaration files from npm packages 9 | - Analyze entry points defined in package.json 10 | - Identify exported types, interfaces, and functions 11 | - Automatically generate AI summaries of type definitions 12 | - Support for caching both package content and AI summaries 13 | - Cache downloaded packages for faster subsequent access 14 | 15 | ## Installation 16 | 17 | ### CLI Usage 18 | 19 | ```bash 20 | # Install from JSR 21 | deno add -A @mizchi/npm-summary 22 | 23 | # Run directly 24 | deno run -A jsr:@mizchi/npm-summary/cli.ts zod 25 | 26 | # Install as a global command 27 | deno install -Afg -n npm-summary jsr:@mizchi/npm-summary/cli 28 | ``` 29 | 30 | ## CLI Usage 31 | 32 | ```bash 33 | # Get type definitions for a package (latest version with summary) 34 | npm-summary lodash 35 | 36 | # Get type definitions for a specific version 37 | npm-summary zod@3.21.4 38 | 39 | # Force latest version download (skips cache) 40 | npm-summary zod@latest 41 | 42 | # Skip cache 43 | npm-summary react --no-cache 44 | 45 | # Output result to a file 46 | npm-summary zod --out=zod-types.md 47 | 48 | # Custom prompt for generating summary 49 | npm-summary zod --prompt="Explain this package like I'm 5 years old" 50 | npm-summary zod -p "Create a detailed guide with code examples" 51 | 52 | # List all files in a package 53 | npm-summary ls zod@3.21.4 54 | 55 | # Read a specific file from a package (new format) 56 | npm-summary read zod@latest/README.md 57 | 58 | # Read a specific file from a package (legacy format still supported) 59 | npm-summary read zod README.md 60 | ``` 61 | 62 | ## Environment Variables 63 | 64 | The tool supports two environment variables for the AI summary generation: 65 | 66 | - `NPM_SUMMARY_GEMINI_API_KEY`: Primary API key for Gemini 67 | - `GOOGLE_GENERATIVE_AI_API_KEY`: Alternative API key for Gemini 68 | 69 | Set either one to enable AI summary generation. 70 | 71 | ## Custom Prompts 72 | 73 | You can customize how summaries are generated using the `--prompt` (or `-p`) 74 | option: 75 | 76 | ```bash 77 | # Generate a summary with a custom prompt 78 | npm-summary zod --prompt="Explain this package like I'm 5 years old" 79 | 80 | # Short form 81 | npm-summary react -p "Create a detailed guide with advanced examples" 82 | ``` 83 | 84 | Different prompts create different summary files, so you can generate multiple 85 | perspectives on the same package without overwriting previous summaries. 86 | 87 | ## Cache Strategy 88 | 89 | The tool uses an intelligent caching strategy: 90 | 91 | - Package content is cached at 92 | `$HOME/.npmsummary/[package-name]/[version]/content.md` 93 | - AI summaries are cached at: 94 | - Default prompt: `$HOME/.npmsummary/[package-name]/[version]/summary.md` 95 | - Custom prompts: 96 | `$HOME/.npmsummary/[package-name]/[version]/summary-[hash].md` 97 | - Summaries are automatically generated and reused when available 98 | - Use `--no-cache` to ignore all caching 99 | - Use `@latest` explicitly (e.g., `zod@latest`) to force fetching the latest 100 | version 101 | 102 | ## License 103 | 104 | MIT 105 | -------------------------------------------------------------------------------- /modules/npm-summary/deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mizchi/npm-summary", 3 | "version": "0.1.3", 4 | "description": "Utility to extract and analyze TypeScript type definitions from npm packages", 5 | "author": "mizchi", 6 | "license": "MIT", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/mizchi/ailab" 10 | }, 11 | "exports": { 12 | ".": "./mod.ts", 13 | "./cli": "./cli.ts" 14 | }, 15 | "publish": { 16 | "include": [ 17 | "mod.ts", 18 | "types.ts", 19 | "deps.ts", 20 | "lib.ts", 21 | "cli.ts", 22 | "README.md", 23 | "LICENSE" 24 | ], 25 | "exclude": ["*.test.ts"] 26 | }, 27 | "tasks": { 28 | "test": "deno test -A", 29 | "check": "deno check mod.ts", 30 | "fmt": "deno fmt", 31 | "lint": "deno lint", 32 | "cli": "deno run -A cli.ts" 33 | }, 34 | "lint": { 35 | "include": ["**/*.ts"], 36 | "exclude": ["*.test.ts"] 37 | }, 38 | "fmt": { 39 | "include": ["**/*.ts", "**/*.md"] 40 | }, 41 | "imports": { 42 | "@std/expect": "jsr:@std/expect@^1.0.13", 43 | "@std/path": "jsr:@std/path@^1.0.8", 44 | "@std/testing": "jsr:@std/testing@^1.0.9", 45 | "pako": "npm:pako@^2.1.0", 46 | "tinytar": "npm:tinytar@^0.1.0" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /modules/npm-summary/deps.ts: -------------------------------------------------------------------------------- 1 | // 外部依存のインポート 2 | export { globToRegExp, join } from "@std/path"; 3 | export { expect } from "@std/expect"; 4 | export { parseArgs } from "node:util"; 5 | 6 | // npm 依存のインポート 7 | export { default as tar } from "tinytar"; 8 | export { default as pako } from "pako"; 9 | 10 | // Deno標準ライブラリ 11 | export const textDecoder = new TextDecoder(); 12 | -------------------------------------------------------------------------------- /modules/npm-summary/examples/nanoid.md: -------------------------------------------------------------------------------- 1 | # zod 2 | 3 | ## Usage 4 | 5 | Define a schema and parse data against it. 6 | 7 | ```ts 8 | import { z } from "zod"; 9 | 10 | const userSchema = z.object({ 11 | name: z.string(), 12 | age: z.number().optional(), 13 | }); 14 | 15 | const userData = { name: "Alice", age: 30 }; 16 | const parsedData = userSchema.parse(userData); // Returns parsedData if valid, throws ZodError otherwise 17 | 18 | console.log(parsedData.name); // Access validated data 19 | ``` 20 | 21 | ## Types 22 | 23 | - **`z.string()`, `z.number()`, `z.boolean()`, `z.date()`, etc.:** Primitive 24 | schema constructors for basic JavaScript types. 25 | - **`z.object({})`:** Defines an object schema with specified properties and 26 | their respective schemas. 27 | - **`z.array(schema)`:** Creates an array schema where each element conforms to 28 | the provided `schema`. 29 | - **`z.enum([])`:** Creates an enum schema, allowing only a predefined set of 30 | string values. 31 | - **`z.union([])`:** Creates a union schema, allowing values that conform to one 32 | of the provided schemas. 33 | - **`z.optional(schema)`:** Makes a schema optional, accepting `undefined` as a 34 | valid value. 35 | - **`z.nullable(schema)`:** Makes a schema nullable, accepting `null` as a valid 36 | value. 37 | - **`z.infer`:** A utility type to extract the TypeScript type 38 | inferred by a Zod schema. 39 | - **`ZodError`:** Error class thrown by `z.parse()` when validation fails. 40 | Contains detailed `issues` array. 41 | - **`z.ZodType`:** Base class for all Zod schema types. 42 | 43 | ## API 44 | 45 | ```ts 46 | import { z } from "zod"; 47 | ``` 48 | 49 | **Schema Creation (using `z` object):** 50 | 51 | - **`z.string(params?: ZodStringDef)`:** Creates a string schema. 52 | ```ts 53 | const nameSchema = z.string().min(2).max(50); 54 | ``` 55 | - **`z.number(params?: ZodNumberDef)`:** Creates a number schema. 56 | ```ts 57 | const ageSchema = z.number().int().positive(); 58 | ``` 59 | - **`z.boolean(params?: ZodBooleanDef)`:** Creates a boolean schema. 60 | ```ts 61 | const isActiveSchema = z.boolean(); 62 | ``` 63 | - **`z.date(params?: ZodDateDef)`:** Creates a date schema (for `Date` objects). 64 | ```ts 65 | const dateSchema = z.date(); 66 | ``` 67 | - **`z.object(shape: ZodRawShape)`:** Creates an object schema. 68 | ```ts 69 | const addressSchema = z.object({ 70 | street: z.string(), 71 | city: z.string(), 72 | }); 73 | ``` 74 | - **`z.array(schema: ZodTypeAny)`:** Creates an array schema. 75 | ```ts 76 | const tagsSchema = z.array(z.string()); 77 | ``` 78 | - **`z.enum(values: [string, ...string[]])`:** Creates an enum schema. 79 | ```ts 80 | const statusEnum = z.enum(["pending", "processing", "completed"]); 81 | ``` 82 | - **`z.union(options: [ZodTypeAny, ...Z 83 | -------------------------------------------------------------------------------- /modules/npm-summary/examples/zod.md: -------------------------------------------------------------------------------- 1 | ````markdown 2 | # zod 3 | 4 | ## Usage 5 | 6 | Define and validate data schemas with static type inference. 7 | 8 | ```ts 9 | import { z } from "zod"; 10 | 11 | const UserSchema = z.object({ 12 | name: z.string(), 13 | age: z.number().int().positive(), 14 | email: z.string().email(), 15 | }); 16 | 17 | type User = z.infer; 18 | 19 | const userData = { name: "Alice", age: 30, email: "alice@example.com" }; 20 | 21 | try { 22 | const parsedData = UserSchema.parse(userData); 23 | console.log("Validation successful:", parsedData); 24 | } catch (error) { 25 | console.error("Validation error:", error); 26 | } 27 | ``` 28 | ```` 29 | 30 | ## Types 31 | 32 | - **`z.string()`**: Schema for string values. Supports validations like `min`, 33 | `max`, `email`, `url`, `uuid`, etc. 34 | - **`z.number()`**: Schema for number values. Supports validations like `min`, 35 | `max`, `int`, `positive`, `negative`, `multipleOf`, etc. 36 | - **`z.boolean()`**: Schema for boolean values. 37 | - **`z.bigint()`**: Schema for bigint values. Supports similar validations as 38 | `z.number()`. 39 | - **`z.date()`**: Schema for Date objects. Supports `min` and `max` date 40 | validations. 41 | - **`z.symbol()`**: Schema for symbol values. 42 | - **`z.null()`**: Schema for null values. 43 | - **`z.undefined()`**: Schema for undefined values 44 | -------------------------------------------------------------------------------- /modules/npm-summary/mod.ts: -------------------------------------------------------------------------------- 1 | // 型定義のエクスポート 2 | export type { GetPackageFilesOptions, Package } from "./types.ts"; 3 | 4 | // 主要な関数のエクスポート 5 | export { 6 | convertToDefinitionPath, 7 | extractTypeInfo, 8 | findDtsFile, 9 | formatImportPath, 10 | generateSummary, 11 | getEntrypoints, 12 | getPackageFiles, 13 | listPackageFiles, 14 | normalizePath, 15 | readPackageFile, 16 | } from "./lib.ts"; 17 | -------------------------------------------------------------------------------- /modules/npm-summary/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * npm package type definition 3 | */ 4 | export type Package = { 5 | name: string; 6 | version: string; 7 | main?: string; 8 | module?: string; 9 | types?: string; 10 | typings?: string; 11 | exports?: Record; 12 | dependencies?: Record; 13 | peerDependencies?: Record; 14 | }; 15 | 16 | /** 17 | * npm package file retrieval options 18 | */ 19 | export type GetPackageFilesOptions = { 20 | /** npm registry endpoint */ 21 | endpoint?: string; 22 | /** Whether to use cache */ 23 | useCache?: boolean; 24 | /** Whether to generate an AI summary */ 25 | generateSummary?: boolean; 26 | /** AI API token (used instead of environment variable) */ 27 | token?: string; 28 | /** List of file patterns to include */ 29 | include?: string[]; 30 | /** Dry run (display file content and token count without sending to AI) */ 31 | dry?: boolean; 32 | /** Custom prompt for summary generation */ 33 | prompt?: string; 34 | }; 35 | 36 | /** 37 | * Default include patterns 38 | */ 39 | export const DEFAULT_INCLUDE_PATTERNS = [ 40 | "README.md", 41 | // deno run -A jsr:@mizchi/npm-summary/cli zod 42 | "package.json", 43 | "**/*.ts", 44 | "**/*.md", 45 | ]; 46 | -------------------------------------------------------------------------------- /modules/ts-callgraph/__fixtures/callgraph-sample.dot: -------------------------------------------------------------------------------- 1 | digraph CallGraph { 2 | node [shape=box, style=filled, fillcolor=lightblue]; 3 | "_callgraph-sample.ts" [label="global: _callgraph-sample.ts"]; 4 | "double" [label="function: double\n: (n: number) => number", fillcolor=lightgreen]; 5 | "square" [label="function: square\n: (n: number) => number", fillcolor=lightgreen]; 6 | "sum" [label="function: sum\n: (...numbers: number[]) => number", fillcolor=lightgreen]; 7 | "numbers.reduce" [label="numbers.reduce"]; 8 | "anonymous_callgraph-sample.ts_16_24" [label="function: anonymous_callgraph-sample.ts_16_24\n: (total: number, num: number) => number"]; 9 | "average" [label="function: average\n: (...numbers: number[]) => number", fillcolor=lightgreen]; 10 | "factorial" [label="function: factorial\n: (n: number) => number", fillcolor=lightgreen]; 11 | "calculate" [label="function: calculate\n: (n: number) => number", fillcolor=lightgreen]; 12 | "complexCalculation" [label="function: complexCalculation\n: (a: number, b: number) => number", fillcolor=lightgreen]; 13 | "Math.min" [label="Math.min"]; 14 | "Math.floor" [label="Math.floor"]; 15 | "add" [label="add"]; 16 | "multiply" [label="multiply"]; 17 | "main" [label="function: main\n: () => void", fillcolor=lightgreen]; 18 | "console.log" [label="console.log"]; 19 | "MathUtils.compute" [label="MathUtils.compute"]; 20 | "sum" -> "numbers.reduce" [label="Line: 17 (method)"]; 21 | "average" -> "sum" [label="Line: 22"]; 22 | "factorial" -> "factorial" [label="Line: 29 (recursive)", color=red, style=dashed]; 23 | "calculate" -> "double" [label="Line: 34"]; 24 | "calculate" -> "square" [label="Line: 35"]; 25 | "complexCalculation" -> "average" [label="Line: 41"]; 26 | "complexCalculation" -> "factorial" [label="Line: 42"]; 27 | "complexCalculation" -> "Math.min" [label="Line: 42 (method)"]; 28 | "complexCalculation" -> "Math.floor" [label="Line: 42 (method)"]; 29 | "complexCalculation" -> "calculate" [label="Line: 43"]; 30 | "_callgraph-sample.ts" -> "add" [label="Line: 57 (method)"]; 31 | "_callgraph-sample.ts" -> "multiply" [label="Line: 58 (method)"]; 32 | "_callgraph-sample.ts" -> "average" [label="Line: 59"]; 33 | "main" -> "console.log" [label="Line: 65 (method)"]; 34 | "main" -> "double" [label="Line: 65"]; 35 | "main" -> "console.log" [label="Line: 66 (method)"]; 36 | "main" -> "square" [label="Line: 66"]; 37 | "main" -> "console.log" [label="Line: 67 (method)"]; 38 | "main" -> "sum" [label="Line: 67"]; 39 | "main" -> "console.log" [label="Line: 68 (method)"]; 40 | "main" -> "average" [label="Line: 68"]; 41 | "main" -> "console.log" [label="Line: 69 (method)"]; 42 | "main" -> "factorial" [label="Line: 69"]; 43 | "main" -> "console.log" [label="Line: 70 (method)"]; 44 | "main" -> "calculate" [label="Line: 70"]; 45 | "main" -> "console.log" [label="Line: 71 (method)"]; 46 | "main" -> "complexCalculation" [label="Line: 71"]; 47 | "main" -> "console.log" [label="Line: 72 (method)"]; 48 | "main" -> "MathUtils.compute" [label="Line: 72 (method)"]; 49 | "_callgraph-sample.ts" -> "main" [label="Line: 77"]; 50 | } 51 | -------------------------------------------------------------------------------- /modules/ts-callgraph/__fixtures/callgraph.dot: -------------------------------------------------------------------------------- 1 | digraph CallGraph { 2 | node [shape=box, style=filled, fillcolor=lightblue]; 3 | "_callgraph-sample.ts" [label="global: _callgraph-sample.ts"]; 4 | "double" [label="function: double\n: (n: number) => number", fillcolor=lightgreen]; 5 | "square" [label="function: square\n: (n: number) => number", fillcolor=lightgreen]; 6 | "sum" [label="function: sum\n: (...numbers: number[]) => number", fillcolor=lightgreen]; 7 | "numbers.reduce" [label="numbers.reduce"]; 8 | "anonymous_callgraph-sample.ts_16_24" [label="function: anonymous_callgraph-sample.ts_16_24\n: (total: number, num: number) => number"]; 9 | "average" [label="function: average\n: (...numbers: number[]) => number", fillcolor=lightgreen]; 10 | "factorial" [label="function: factorial\n: (n: number) => number", fillcolor=lightgreen]; 11 | "calculate" [label="function: calculate\n: (n: number) => number", fillcolor=lightgreen]; 12 | "complexCalculation" [label="function: complexCalculation\n: (a: number, b: number) => number", fillcolor=lightgreen]; 13 | "Math.min" [label="Math.min"]; 14 | "Math.floor" [label="Math.floor"]; 15 | "add" [label="add"]; 16 | "multiply" [label="multiply"]; 17 | "main" [label="function: main\n: () => void", fillcolor=lightgreen]; 18 | "console.log" [label="console.log"]; 19 | "MathUtils.compute" [label="MathUtils.compute"]; 20 | "sum" -> "numbers.reduce" [label="Line: 17 (method)"]; 21 | "average" -> "sum" [label="Line: 22"]; 22 | "factorial" -> "factorial" [label="Line: 29 (recursive)", color=red, style=dashed]; 23 | "calculate" -> "double" [label="Line: 34"]; 24 | "calculate" -> "square" [label="Line: 35"]; 25 | "complexCalculation" -> "average" [label="Line: 41"]; 26 | "complexCalculation" -> "factorial" [label="Line: 42"]; 27 | "complexCalculation" -> "Math.min" [label="Line: 42 (method)"]; 28 | "complexCalculation" -> "Math.floor" [label="Line: 42 (method)"]; 29 | "complexCalculation" -> "calculate" [label="Line: 43"]; 30 | "_callgraph-sample.ts" -> "add" [label="Line: 57 (method)"]; 31 | "_callgraph-sample.ts" -> "multiply" [label="Line: 58 (method)"]; 32 | "_callgraph-sample.ts" -> "average" [label="Line: 59"]; 33 | "main" -> "console.log" [label="Line: 65 (method)"]; 34 | "main" -> "double" [label="Line: 65"]; 35 | "main" -> "console.log" [label="Line: 66 (method)"]; 36 | "main" -> "square" [label="Line: 66"]; 37 | "main" -> "console.log" [label="Line: 67 (method)"]; 38 | "main" -> "sum" [label="Line: 67"]; 39 | "main" -> "console.log" [label="Line: 68 (method)"]; 40 | "main" -> "average" [label="Line: 68"]; 41 | "main" -> "console.log" [label="Line: 69 (method)"]; 42 | "main" -> "factorial" [label="Line: 69"]; 43 | "main" -> "console.log" [label="Line: 70 (method)"]; 44 | "main" -> "calculate" [label="Line: 70"]; 45 | "main" -> "console.log" [label="Line: 71 (method)"]; 46 | "main" -> "complexCalculation" [label="Line: 71"]; 47 | "main" -> "console.log" [label="Line: 72 (method)"]; 48 | "main" -> "MathUtils.compute" [label="Line: 72 (method)"]; 49 | "_callgraph-sample.ts" -> "main" [label="Line: 77"]; 50 | } 51 | -------------------------------------------------------------------------------- /modules/ts-callgraph/__fixtures/class-sample.ts: -------------------------------------------------------------------------------- 1 | // クラスとメソッドの呼び出し関係のサンプル 2 | 3 | // 基底クラス 4 | class BaseClass { 5 | protected value: number; 6 | 7 | constructor(value: number) { 8 | this.value = value; 9 | this.initialize(); 10 | } 11 | 12 | // 初期化メソッド 13 | protected initialize(): void { 14 | console.log(`BaseClass initialized with value: ${this.value}`); 15 | } 16 | 17 | // 通常のメソッド 18 | public getValue(): number { 19 | return this.value; 20 | } 21 | 22 | // 静的メソッド 23 | static createDefault(): BaseClass { 24 | return new BaseClass(0); 25 | } 26 | } 27 | 28 | // 派生クラス 29 | class DerivedClass extends BaseClass { 30 | private name: string; 31 | 32 | constructor(value: number, name: string) { 33 | super(value); // 基底クラスのコンストラクタを呼び出し 34 | this.name = name; 35 | } 36 | 37 | // オーバーライドメソッド 38 | protected override initialize(): void { 39 | super.initialize(); // 基底クラスのメソッドを呼び出し 40 | console.log(`DerivedClass initialized with name: ${this.name}`); 41 | } 42 | 43 | // 新しいメソッド 44 | public getName(): string { 45 | return this.name; 46 | } 47 | 48 | // 静的メソッド 49 | static createWithName(name: string): DerivedClass { 50 | return new DerivedClass(0, name); 51 | } 52 | } 53 | 54 | // インターフェース 55 | interface Printable { 56 | print(): void; 57 | } 58 | 59 | // インターフェースを実装するクラス 60 | class PrintableClass implements Printable { 61 | private content: string; 62 | 63 | constructor(content: string) { 64 | this.content = content; 65 | } 66 | 67 | public print(): void { 68 | console.log(this.content); 69 | } 70 | } 71 | 72 | // クラス式 73 | const AnonymousClass = class { 74 | private id: number; 75 | 76 | constructor(id: number) { 77 | this.id = id; 78 | } 79 | 80 | public getId(): number { 81 | return this.id; 82 | } 83 | }; 84 | 85 | // メイン関数 86 | function main() { 87 | // 基底クラスのインスタンス作成 88 | const base = new BaseClass(10); 89 | console.log(`Base value: ${base.getValue()}`); 90 | 91 | // 静的メソッドの呼び出し 92 | const defaultBase = BaseClass.createDefault(); 93 | console.log(`Default base value: ${defaultBase.getValue()}`); 94 | 95 | // 派生クラスのインスタンス作成 96 | const derived = new DerivedClass(20, "Example"); 97 | console.log(`Derived value: ${derived.getValue()}`); 98 | console.log(`Derived name: ${derived.getName()}`); 99 | 100 | // 静的メソッドの呼び出し 101 | const namedDerived = DerivedClass.createWithName("Static Example"); 102 | console.log(`Named derived name: ${namedDerived.getName()}`); 103 | 104 | // インターフェースを実装したクラスのインスタンス作成 105 | const printable = new PrintableClass("Hello, World!"); 106 | printable.print(); 107 | 108 | // クラス式のインスタンス作成 109 | const anonymous = new AnonymousClass(42); 110 | console.log(`Anonymous ID: ${anonymous.getId()}`); 111 | } 112 | 113 | // メイン関数の実行 114 | main(); 115 | -------------------------------------------------------------------------------- /modules/ts-callgraph/__fixtures/multi-file/base.ts: -------------------------------------------------------------------------------- 1 | // ベースクラスと基本的なインターフェースを定義するファイル 2 | 3 | // インターフェース 4 | export interface Loggable { 5 | log(message: string): void; 6 | } 7 | 8 | // 基底クラス 9 | export class BaseEntity implements Loggable { 10 | protected id: string; 11 | 12 | constructor(id: string) { 13 | this.id = id; 14 | this.initialize(); 15 | } 16 | 17 | // 初期化メソッド 18 | protected initialize(): void { 19 | console.log(`BaseEntity initialized with id: ${this.id}`); 20 | } 21 | 22 | // ID取得メソッド 23 | public getId(): string { 24 | return this.id; 25 | } 26 | 27 | // ログ出力メソッド 28 | public log(message: string): void { 29 | console.log(`[${this.id}] ${message}`); 30 | } 31 | 32 | // 静的メソッド 33 | static createDefault(): BaseEntity { 34 | return new BaseEntity("default"); 35 | } 36 | } 37 | 38 | // ユーティリティ関数 39 | export function generateId(): string { 40 | return Math.random().toString(36).substring(2, 10); 41 | } 42 | -------------------------------------------------------------------------------- /modules/ts-callgraph/__fixtures/multi-file/derived.ts: -------------------------------------------------------------------------------- 1 | // 派生クラスを定義するファイル 2 | import { BaseEntity, generateId, type Loggable } from "./base.ts"; 3 | 4 | // 追加のインターフェース 5 | export interface Serializable { 6 | serialize(): string; 7 | } 8 | 9 | // 派生クラス 10 | export class DerivedEntity extends BaseEntity implements Serializable { 11 | private name: string; 12 | private createdAt: Date; 13 | 14 | constructor(id: string, name: string) { 15 | super(id); // 基底クラスのコンストラクタを呼び出し 16 | this.name = name; 17 | this.createdAt = new Date(); 18 | } 19 | 20 | // オーバーライドメソッド 21 | protected override initialize(): void { 22 | super.initialize(); // 基底クラスのメソッドを呼び出し 23 | console.log(`DerivedEntity initialized with name: ${this.name}`); 24 | } 25 | 26 | // 新しいメソッド 27 | public getName(): string { 28 | return this.name; 29 | } 30 | 31 | // インターフェースの実装 32 | public serialize(): string { 33 | return JSON.stringify({ 34 | id: this.getId(), 35 | name: this.name, 36 | createdAt: this.createdAt.toISOString(), 37 | }); 38 | } 39 | 40 | // 静的メソッド 41 | static createWithName(name: string): DerivedEntity { 42 | return new DerivedEntity(generateId(), name); 43 | } 44 | } 45 | 46 | // ヘルパー関数 47 | export function createEntity(name: string): DerivedEntity { 48 | const id = generateId(); 49 | return new DerivedEntity(id, name); 50 | } 51 | -------------------------------------------------------------------------------- /modules/ts-callgraph/__fixtures/multi-file/main.ts: -------------------------------------------------------------------------------- 1 | // メインファイル - 他のモジュールを使用する 2 | import { BaseEntity } from "./base.ts"; 3 | import { createEntity, DerivedEntity, type Serializable } from "./derived.ts"; 4 | import { DataService } from "./service.ts"; 5 | 6 | // シリアライズ可能なオブジェクトを処理する関数 7 | function processSerializable(obj: Serializable): void { 8 | console.log("Processing serialized data:"); 9 | console.log(obj.serialize()); 10 | } 11 | 12 | // メイン関数 13 | function main() { 14 | // BaseEntityの使用 15 | const base = new BaseEntity("base-1"); 16 | base.log("Base entity created"); 17 | 18 | // 静的メソッドの使用 19 | const defaultBase = BaseEntity.createDefault(); 20 | console.log(`Default base ID: ${defaultBase.getId()}`); 21 | 22 | // DerivedEntityの使用 23 | const derived = new DerivedEntity("derived-1", "Example"); 24 | derived.log("Derived entity created"); 25 | console.log(`Derived name: ${derived.getName()}`); 26 | 27 | // ヘルパー関数の使用 28 | const entity = createEntity("Helper Created"); 29 | console.log(`Entity from helper: ${entity.getId()} - ${entity.getName()}`); 30 | 31 | // 静的メソッドの使用 32 | const namedEntity = DerivedEntity.createWithName("Static Created"); 33 | console.log(`Named entity: ${namedEntity.getName()}`); 34 | 35 | // インターフェースを使った処理 36 | processSerializable(derived); 37 | 38 | // サービスの使用 39 | const service = new DataService(); 40 | service.saveEntity(derived); 41 | 42 | const loadedEntity = service.loadEntity("derived-1"); 43 | if (loadedEntity) { 44 | console.log(`Loaded entity: ${loadedEntity.getName()}`); 45 | } 46 | } 47 | 48 | // メイン関数の実行 49 | main(); 50 | -------------------------------------------------------------------------------- /modules/ts-callgraph/__fixtures/multi-file/service.ts: -------------------------------------------------------------------------------- 1 | // サービスクラスを定義するファイル 2 | import { DerivedEntity } from "./derived.ts"; 3 | 4 | // データストレージのインターフェース 5 | export interface DataStorage { 6 | save(id: string, data: string): void; 7 | load(id: string): string | null; 8 | } 9 | 10 | // インメモリストレージの実装 11 | class MemoryStorage implements DataStorage { 12 | private data: Map = new Map(); 13 | 14 | save(id: string, data: string): void { 15 | this.data.set(id, data); 16 | console.log(`Data saved with id: ${id}`); 17 | } 18 | 19 | load(id: string): string | null { 20 | const data = this.data.get(id); 21 | return data || null; 22 | } 23 | } 24 | 25 | // データサービスクラス 26 | export class DataService { 27 | private storage: DataStorage; 28 | 29 | constructor(storage?: DataStorage) { 30 | this.storage = storage || new MemoryStorage(); 31 | } 32 | 33 | // エンティティを保存 34 | saveEntity(entity: DerivedEntity): void { 35 | const id = entity.getId(); 36 | const data = entity.serialize(); 37 | this.storage.save(id, data); 38 | } 39 | 40 | // エンティティを読み込み 41 | loadEntity(id: string): DerivedEntity | null { 42 | const data = this.storage.load(id); 43 | if (!data) return null; 44 | 45 | try { 46 | const parsed = JSON.parse(data); 47 | return new DerivedEntity(parsed.id, parsed.name); 48 | } catch (e) { 49 | console.error("Failed to parse entity data:", e); 50 | return null; 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /modules/ts-callgraph/__snapshots__/class-sample-function-summary-dot.snap: -------------------------------------------------------------------------------- 1 | digraph FunctionImplementationGraph { 2 | node [shape=box, style=filled]; 3 | "BaseClass.constructor" [label="BaseClass.constructor\n: any", fillcolor=lightpink]; 4 | "initialize" [label="initialize", fillcolor=lightblue]; 5 | "BaseClass.initialize" [label="BaseClass.initialize\n: () => void", fillcolor=lightblue]; 6 | "console.log" [label="console.log", fillcolor=lightblue]; 7 | "DerivedClass.constructor" [label="DerivedClass.constructor\n: any", fillcolor=lightpink]; 8 | "_32_4" [label="_32_4", fillcolor=lightblue]; 9 | "DerivedClass.initialize" [label="DerivedClass.initialize\n: () => void", fillcolor=lightblue]; 10 | "main" [label="main\n: () => void", fillcolor=lightgreen]; 11 | "base.getValue" [label="base.getValue", fillcolor=lightblue]; 12 | "BaseClass.createDefault" [label="BaseClass.static createDefault\n: () => BaseClass", fillcolor=lightcyan]; 13 | "defaultBase.getValue" [label="defaultBase.getValue", fillcolor=lightblue]; 14 | "derived.getValue" [label="derived.getValue", fillcolor=lightblue]; 15 | "derived.getName" [label="derived.getName", fillcolor=lightblue]; 16 | "DerivedClass.createWithName" [label="DerivedClass.static createWithName\n: (name: string) => DerivedClass", fillcolor=lightcyan]; 17 | "namedDerived.getName" [label="namedDerived.getName", fillcolor=lightblue]; 18 | "printable.print" [label="printable.print", fillcolor=lightblue]; 19 | "anonymous.getId" [label="anonymous.getId", fillcolor=lightblue]; 20 | "PrintableClass.print" [label="PrintableClass.print\n: () => void", fillcolor=lightblue]; 21 | "BaseClass.constructor" -> "initialize"; 22 | "BaseClass.initialize" -> "console.log"; 23 | "DerivedClass.constructor" -> "_32_4"; 24 | "DerivedClass.initialize" -> "initialize"; 25 | "DerivedClass.initialize" -> "console.log"; 26 | "main" -> "console.log"; 27 | "main" -> "base.getValue"; 28 | "main" -> "BaseClass.createDefault"; 29 | "main" -> "defaultBase.getValue"; 30 | "main" -> "derived.getValue"; 31 | "main" -> "derived.getName"; 32 | "main" -> "DerivedClass.createWithName"; 33 | "main" -> "namedDerived.getName"; 34 | "main" -> "printable.print"; 35 | "main" -> "anonymous.getId"; 36 | "PrintableClass.print" -> "console.log"; 37 | } 38 | -------------------------------------------------------------------------------- /modules/ts-callgraph/__snapshots__/class-sample-function-summary-text.snap: -------------------------------------------------------------------------------- 1 | 関数実装から関数の呼び出し関係の要約: 2 | 3 | BaseClass.constructor: any 4 | - initialize 5 | 6 | BaseClass.initialize: () => void 7 | - console.log 8 | 9 | DerivedClass.constructor: any 10 | - _32_4 11 | 12 | DerivedClass.initialize: () => void 13 | - initialize 14 | - console.log 15 | 16 | main: () => void 17 | - console.log 18 | - base.getValue 19 | - BaseClass.static createDefault: () => BaseClass 20 | - defaultBase.getValue 21 | - derived.getValue 22 | - derived.getName 23 | - DerivedClass.static createWithName: (name: string) => DerivedClass 24 | - namedDerived.getName 25 | - printable.print 26 | - anonymous.getId 27 | 28 | PrintableClass.print: () => void 29 | - console.log 30 | 31 | -------------------------------------------------------------------------------- /modules/ts-callgraph/callgraph.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "jsr:@std/expect"; 2 | import { test } from "jsr:@std/testing/bdd"; 3 | import { CallGraph } from "./callgraph.ts"; 4 | import type { createProgram, generateCallGraph } from "./parser.ts"; 5 | import type * as path from "jsr:@std/path"; 6 | 7 | test("関数実装の要約が正しく動作する", () => { 8 | const graph = new CallGraph(); 9 | 10 | // ノード情報を追加 11 | graph.addNode("main", { kind: "function", type: "() => void" }); 12 | graph.addNode("helper", { kind: "function", type: "(x: number) => number" }); 13 | graph.addNode("util", { kind: "function", type: "(s: string) => string" }); 14 | 15 | // 呼び出し情報を追加 16 | graph.addCall("main", "helper", "test.ts", 10, 5); 17 | graph.addCall("main", "util", "test.ts", 11, 3); 18 | graph.addCall("helper", "util", "test.ts", 15, 3); 19 | 20 | // 関数実装の要約を取得 21 | const summary = graph.summarizeFunctionImplementations(); 22 | 23 | // 期待される結果 24 | expect(summary.length).toBe(2); 25 | 26 | // main関数の呼び出し 27 | const mainImpl = summary.find((impl) => impl.function === "main"); 28 | expect(mainImpl).toBeDefined(); 29 | expect(mainImpl?.calls).toContain("helper"); 30 | expect(mainImpl?.calls).toContain("util"); 31 | 32 | // helper関数の呼び出し 33 | const helperImpl = summary.find((impl) => impl.function === "helper"); 34 | expect(helperImpl).toBeDefined(); 35 | expect(helperImpl?.calls).toContain("util"); 36 | 37 | // DOT形式の出力をテスト 38 | const dot = graph.toFunctionSummaryDot(); 39 | expect(dot).toContain("main"); 40 | expect(dot).toContain("helper"); 41 | expect(dot).toContain("util"); 42 | expect(dot).toContain(' "main" -> "helper";'); 43 | expect(dot).toContain(' "main" -> "util";'); 44 | expect(dot).toContain(' "helper" -> "util";'); 45 | 46 | // テキスト形式の出力をテスト 47 | const text = graph.toFunctionSummaryText(); 48 | expect(text).toContain("main: () => void"); 49 | expect(text).toContain("helper: (x: number) => number"); 50 | expect(text).toContain("util: (s: string) => string"); 51 | }); 52 | -------------------------------------------------------------------------------- /modules/ts-callgraph/deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mizchi/ts-callgraph", 3 | "version": "1.0.0", 4 | "description": "A tool to generate call graphs for TypeScript projects.", 5 | "tasks": { 6 | "test": "deno test -A ." 7 | }, 8 | "test": { 9 | "exclude": ["__snapshots__/**/*"] 10 | }, 11 | "exports": { 12 | ".": "./mod.ts" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /modules/ts-callgraph/deps.ts: -------------------------------------------------------------------------------- 1 | export { parse } from "https://deno.land/std/flags/mod.ts"; 2 | export * as path from "https://deno.land/std/path/mod.ts"; 3 | export { default as ts } from "npm:typescript"; 4 | -------------------------------------------------------------------------------- /modules/ts-callgraph/filter.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "jsr:@std/expect"; 2 | import { test } from "jsr:@std/testing/bdd"; 3 | import { shouldFilterCall } from "./filter.ts"; 4 | 5 | test("shouldFilterCall が正しく動作する", () => { 6 | const callInfo = { 7 | caller: "main", 8 | callee: "console.log", 9 | sourceFile: "test.ts", 10 | line: 10, 11 | column: 5, 12 | }; 13 | 14 | const flags = { 15 | "ignore-stdlib": true, 16 | "ignore-npm": true, 17 | "ignore-jsr": true, 18 | }; 19 | 20 | const ignorePatterns: string[] = []; 21 | 22 | expect(shouldFilterCall(callInfo, flags, ignorePatterns)).toBe(true); 23 | 24 | const callInfo2 = { 25 | caller: "main", 26 | callee: "zod.string", 27 | sourceFile: "test.ts", 28 | line: 10, 29 | column: 5, 30 | }; 31 | 32 | expect(shouldFilterCall(callInfo2, flags, ignorePatterns)).toBe(false); 33 | }); 34 | -------------------------------------------------------------------------------- /modules/ts-callgraph/filter.ts: -------------------------------------------------------------------------------- 1 | import type { CallInfo } from "./types.ts"; 2 | 3 | // 標準ライブラリのメンバーかどうかを判定する関数 4 | const isStdLibMember = (name: string): boolean => { 5 | // 標準ライブラリの代表的なオブジェクトとメソッド 6 | const stdLibPatterns = [ 7 | /^console\./, 8 | /^Math\./, 9 | /^Array\./, 10 | /^Object\./, 11 | /^String\./, 12 | /^Number\./, 13 | /^Date\./, 14 | /^JSON\./, 15 | /^RegExp\./, 16 | /^Promise\./, 17 | /^Error\./, 18 | /^Map\./, 19 | /^Set\./, 20 | /^Symbol\./, 21 | /^WeakMap\./, 22 | /^WeakSet\./, 23 | /^Reflect\./, 24 | /^Proxy\./, 25 | /^Intl\./, 26 | /^ArrayBuffer\./, 27 | /^DataView\./, 28 | /^Int8Array\./, 29 | /^Uint8Array\./, 30 | /^Uint8ClampedArray\./, 31 | /^Int16Array\./, 32 | /^Uint16Array\./, 33 | /^Int32Array\./, 34 | /^Uint32Array\./, 35 | /^Float32Array\./, 36 | /^Float64Array\./, 37 | /^BigInt64Array\./, 38 | /^BigUint64Array\./, 39 | ]; 40 | 41 | return stdLibPatterns.some((pattern) => pattern.test(name)); 42 | }; 43 | 44 | // npm パッケージからの呼び出しかどうかを判定する関数 45 | const isNpmPackage = (name: string, sourceFile: string): boolean => { 46 | return sourceFile.includes("node_modules") || 47 | sourceFile.includes("npm:") || 48 | name.startsWith("npm:"); 49 | }; 50 | 51 | // JSR パッケージからの呼び出しかどうかを判定する関数 52 | const isJsrPackage = (name: string, sourceFile: string): boolean => { 53 | return sourceFile.includes("jsr:") || name.startsWith("jsr:"); 54 | }; 55 | 56 | // ユーザー定義のパターンにマッチするかを判定する関数 57 | const matchesIgnorePattern = ( 58 | name: string, 59 | ignorePatterns: string[], 60 | ): boolean => { 61 | if (ignorePatterns.length === 0) return false; 62 | return ignorePatterns.some((pattern) => { 63 | try { 64 | const regex = new RegExp(pattern); 65 | return regex.test(name); 66 | } catch { 67 | // 正規表現としてのパターンが無効な場合は単純な文字列比較を行う 68 | return name.includes(pattern); 69 | } 70 | }); 71 | }; 72 | 73 | // 呼び出しをフィルタリングするかどうかを判定する関数 74 | export const shouldFilterCall = ( 75 | callInfo: CallInfo, 76 | flags: { 77 | "ignore-stdlib": boolean; 78 | "ignore-npm": boolean; 79 | "ignore-jsr": boolean; 80 | }, 81 | ignorePatterns: string[], 82 | ): boolean => { 83 | const { callee, sourceFile } = callInfo; 84 | 85 | // ユーザー定義のパターンにマッチする場合 86 | if (matchesIgnorePattern(callee, ignorePatterns)) { 87 | return true; 88 | } 89 | 90 | // 標準ライブラリの場合 91 | if (flags["ignore-stdlib"] && isStdLibMember(callee)) { 92 | return true; 93 | } 94 | 95 | // npm パッケージの場合 96 | if (flags["ignore-npm"] && isNpmPackage(callee, sourceFile)) { 97 | return true; 98 | } 99 | 100 | // JSR パッケージの場合 101 | if (flags["ignore-jsr"] && isJsrPackage(callee, sourceFile)) { 102 | return true; 103 | } 104 | 105 | return false; 106 | }; 107 | -------------------------------------------------------------------------------- /modules/ts-callgraph/formatter.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "jsr:@std/expect"; 2 | import { test } from "jsr:@std/testing/bdd"; 3 | import { CallGraph } from "./callgraph.ts"; 4 | import { 5 | toFunctionCallDot, 6 | toFunctionSummaryDot, 7 | toFunctionSummaryText, 8 | } from "./formatter.ts"; 9 | 10 | test("toFunctionCallDot が正しく動作する", () => { 11 | const graph = new CallGraph(); 12 | graph.addCall("main", "helper", "test.ts", 10, 5); 13 | const dot = toFunctionCallDot(graph); 14 | expect(dot).toContain(' "main" -> "helper" [label="calls: 1"];'); 15 | }); 16 | 17 | test("toFunctionSummaryDot が正しく動作する", () => { 18 | const graph = new CallGraph(); 19 | graph.addCall("main", "helper", "test.ts", 10, 5); 20 | const dot = toFunctionSummaryDot(graph); 21 | expect(dot).toContain(' "main" -> "helper";'); 22 | }); 23 | 24 | test("toFunctionSummaryText が正しく動作する", () => { 25 | const graph = new CallGraph(); 26 | graph.addNode("main", { kind: "function", type: "() => void" }); 27 | graph.addNode("helper", { kind: "function", type: "(x: number) => number" }); 28 | graph.addCall("main", "helper", "test.ts", 10, 5); 29 | const text = toFunctionSummaryText(graph); 30 | expect(text).toContain("main: () => void"); 31 | expect(text).toContain("helper: (x: number) => number"); 32 | expect(text).toContain(" - helper: (x: number) => number"); 33 | }); 34 | -------------------------------------------------------------------------------- /modules/ts-callgraph/mod.ts: -------------------------------------------------------------------------------- 1 | export * from "./callgraph.ts"; 2 | export * from "./parser.ts"; 3 | export * from "./filter.ts"; 4 | export * from "./formatter.ts"; 5 | export * from "./types.ts"; 6 | -------------------------------------------------------------------------------- /modules/ts-callgraph/parser.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "jsr:@std/expect"; 2 | import { test } from "jsr:@std/testing/bdd"; 3 | import { generateCallGraph } from "./parser.ts"; 4 | import { createProgram } from "./parser.ts"; 5 | import path from "node:path"; 6 | 7 | // あとで 8 | test.skip("generateCallGraph が正しく動作する", () => { 9 | const filePath = path.join( 10 | import.meta.dirname!, 11 | "__fixtures/callgraph-sample.ts", 12 | ); 13 | const program = createProgram(filePath); 14 | const callGraph = generateCallGraph(program); 15 | console.log(filePath, program.getNodeCount(), callGraph.getAllNodes().size); 16 | 17 | expect(callGraph.getAllNodes().size).toBeGreaterThan(0); 18 | expect(callGraph.getCalls().length).toBeGreaterThan(0); 19 | }); 20 | -------------------------------------------------------------------------------- /modules/ts-callgraph/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * ts-callgraph の型定義 3 | */ 4 | 5 | // 関数呼び出しの関係を格納する型 6 | export type CallInfo = { 7 | // 呼び出し元と呼び出し先 8 | caller: string; 9 | callee: string; 10 | 11 | // ソース情報 12 | sourceFile: string; 13 | line: number; 14 | column: number; 15 | 16 | // 型情報(あれば) 17 | callerType?: string; 18 | calleeType?: string; 19 | 20 | // 追加情報 21 | isConstructor?: boolean; 22 | isRecursive?: boolean; 23 | isMethod?: boolean; 24 | className?: string; 25 | isStatic?: boolean; // 静的メソッド呼び出しかどうか 26 | isPropertyAccess?: boolean; // プロパティアクセスかどうか 27 | }; 28 | 29 | // ノード情報を格納する型 30 | export type NodeInfo = { 31 | name: string; 32 | type?: string; 33 | sourceFile?: string; 34 | isExported?: boolean; 35 | docComment?: string; 36 | kind?: string; // "function", "method", "class", etc. 37 | isStatic?: boolean; // 静的メソッドかどうか 38 | parentClass?: string; // メソッドの場合、所属するクラス名 39 | superClass?: string; // クラスの場合、継承元のクラス名 40 | implementsInterfaces?: string[]; // クラスの場合、実装しているインターフェース名 41 | }; 42 | 43 | // ファイル情報を格納する型 44 | export type FileInfo = { 45 | path: string; 46 | functions: string[]; // ファイル内で定義された関数名のリスト 47 | }; 48 | 49 | // 関数間の呼び出し関係を格納する型 50 | export type FunctionCallInfo = { 51 | caller: string; // 呼び出し元関数名 52 | callee: string; // 呼び出し先関数名 53 | count: number; // 呼び出し回数 54 | }; 55 | 56 | // 関数の実装から関数の呼び出し関係を格納する型 57 | export type FunctionImplementationInfo = { 58 | function: string; // 関数名 59 | calls: string[]; // 呼び出している関数のリスト 60 | }; 61 | -------------------------------------------------------------------------------- /modules/type-predictor/README.md: -------------------------------------------------------------------------------- 1 | # Type Predictor 2 | 3 | JSONデータから型を予測し、zodスキーマを生成するモジュール。 4 | 5 | ## 特徴 6 | 7 | - JSONデータの構造を解析し、型を予測 8 | - 配列、オブジェクト、Record型、列挙型などの高度な型を検出 9 | - zodスキーマの自動生成 10 | - 詳細な型情報の分析機能 11 | 12 | ## 使用方法 13 | 14 | ```typescript 15 | import { TypePredictor } from "./mod.ts"; 16 | 17 | // インスタンスを作成 18 | const predictor = new TypePredictor(); 19 | 20 | // JSONデータから型を予測してスキーマを生成 21 | const data = { 22 | name: "John", 23 | age: 30, 24 | scores: [85, 92, 78], 25 | contact: { 26 | email: "john@example.com", 27 | phone: null, 28 | }, 29 | preferences: { 30 | theme_dark: true, 31 | theme_light: false, 32 | }, 33 | }; 34 | 35 | // スキーマを生成 36 | const schema = predictor.predict(data); 37 | 38 | // スキーマを使用してバリデーション 39 | const result = schema.safeParse(data); 40 | if (result.success) { 41 | console.log("Valid data!"); 42 | } else { 43 | console.error("Invalid data:", result.error); 44 | } 45 | 46 | // 型情報の詳細分析 47 | const analysis = predictor.analyze(data); 48 | console.log("Paths:", analysis.paths); 49 | console.log("Structure:", analysis.structure); 50 | console.log("Type Predictions:", analysis.predictions); 51 | ``` 52 | 53 | ## 型予測機能 54 | 55 | 以下の型を自動的に検出・予測します: 56 | 57 | - プリミティブ型(string, number, boolean, null) 58 | - 配列型(同種の要素、混合型) 59 | - オブジェクト型(ネストされた構造) 60 | - Record型(キーパターンと値の型) 61 | - 列挙型(文字列パターンから検出) 62 | - ユニオン型(複数の型が混在する場合) 63 | - Nullable型(null値を含む場合) 64 | 65 | ## モジュール構成 66 | 67 | - `mod.ts` - メインモジュール、公開API 68 | - `types.ts` - 型定義 69 | - `flatten.ts` - JSONのフラット化 70 | - `path-analyzer.ts` - パス解析 71 | - `predict.ts` - 型予測 72 | - `schema.ts` - スキーマ構築 73 | 74 | ## 開発 75 | 76 | ### テストの実行 77 | 78 | ```bash 79 | deno test 80 | ``` 81 | 82 | ### 型チェック 83 | 84 | ```bash 85 | deno check mod.ts 86 | ``` 87 | 88 | ## TODO 89 | 90 | ### テストの改善 91 | 92 | - [ ] エッジケースのテスト改善 93 | - 空の配列の処理 94 | - 空のオブジェクトの処理 95 | - nullの処理 96 | - [ ] 配列型の判定改善 97 | - 配列要素の型判定 98 | - 混合型配列の処理 99 | - [ ] ネストされたオブジェクトの型判定改善 100 | - 深いネストの処理 101 | - 循環参照の検出 102 | 103 | ### 追加予定のテストパターン 104 | 105 | 1. 複雑なデータ構造 106 | - 深いネストを持つオブジェクト 107 | - 多次元配列 108 | - 複数の型を含む配列 109 | - オプショナルなフィールド 110 | 111 | 2. 特殊なパターン 112 | - 循環参照を含むオブジェクト 113 | - 非常に大きな配列 114 | - 非常に深いネスト 115 | - 複雑なRecord型パターン 116 | 117 | 3. エッジケース 118 | - undefined値の処理 119 | - NaN, Infinity などの特殊な数値 120 | - 空文字列 121 | - 巨大な整数 122 | - シンボル 123 | - 関数 124 | 125 | 4. 実際のユースケース 126 | - APIレスポンス 127 | - フォームデータ 128 | - 設定ファイル 129 | - データベースレコード 130 | 131 | ## 制限事項 132 | 133 | - 循環参照は現在サポートしていません 134 | - 非常に大きなJSONデータの場合、パフォーマンスが低下する可能性があります 135 | - 一部の複雑なパターンは正確に検出できない場合があります 136 | 137 | ## ライセンス 138 | 139 | MIT 140 | -------------------------------------------------------------------------------- /modules/type-predictor/deps.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 外部依存のエクスポート 3 | */ 4 | 5 | // zod for schema generation 6 | export { z } from "npm:zod"; 7 | 8 | // testing utilities 9 | export { expect } from "@std/expect"; 10 | export { test } from "@std/testing/bdd"; 11 | -------------------------------------------------------------------------------- /modules/type-predictor/mod.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 型予測システムのメインモジュールのテスト 3 | */ 4 | 5 | import { expect, test } from "./deps.ts"; 6 | import { TypePredictor } from "./mod.ts"; 7 | 8 | test("TypePredictor - predict simple object", () => { 9 | const predictor = new TypePredictor(); 10 | const input = { 11 | name: "John", 12 | age: 30, 13 | active: true, 14 | }; 15 | 16 | const schema = predictor.predict(input); 17 | const result = schema.safeParse(input); 18 | expect(result.success).toBe(true); 19 | }); 20 | 21 | test("TypePredictor - predict complex object", () => { 22 | const predictor = new TypePredictor(); 23 | const input = { 24 | user: { 25 | name: "John", 26 | scores: [85, 92, 78], 27 | contact: { 28 | email: "john@example.com", 29 | phone: null, 30 | }, 31 | preferences: { 32 | theme_dark: true, 33 | theme_light: false, 34 | }, 35 | }, 36 | }; 37 | 38 | const schema = predictor.predict(input); 39 | const result = schema.safeParse(input); 40 | expect(result.success).toBe(true); 41 | }); 42 | 43 | test("TypePredictor - analyze data structure", () => { 44 | const predictor = new TypePredictor(); 45 | const input = { 46 | name: "John", 47 | age: 30, 48 | }; 49 | 50 | const analysis = predictor.analyze(input); 51 | expect(analysis.paths).toBeDefined(); 52 | expect(analysis.structure).toBeDefined(); 53 | expect(analysis.predictions).toBeDefined(); 54 | }); 55 | 56 | // FIMME 57 | test.skip("TypePredictor - handle edge cases", () => { 58 | const predictor = new TypePredictor(); 59 | 60 | // 空のオブジェクト 61 | const emptySchema = predictor.predict({}); 62 | expect(emptySchema.safeParse({}).success).toBe(true); 63 | 64 | // // null 65 | const nullSchema = predictor.predict(null); 66 | expect(nullSchema.safeParse(null).success).toBe(true); 67 | 68 | // // 空の配列 69 | const emptyArraySchema = predictor.predict([]); 70 | expect(emptyArraySchema.safeParse([]).success).toBe(true); 71 | 72 | // -----Error----- 73 | // 単一の値の配列 74 | const numberArraySchema = predictor.predict([1, 2, 3]); 75 | expect(numberArraySchema.safeParse([4, 5, 6]).success).toBe(true); 76 | 77 | // 混合型の配列 78 | const mixedArraySchema = predictor.predict([1, "two", true]); 79 | expect(mixedArraySchema.safeParse([2, "three", false]).success).toBe(true); 80 | }); 81 | 82 | test("TypePredictor - validate against invalid data", () => { 83 | const predictor = new TypePredictor(); 84 | const input = { 85 | name: "John", 86 | age: 30, 87 | }; 88 | 89 | const schema = predictor.predict(input); 90 | 91 | // 型が一致しないデータ 92 | const invalidResult = schema.safeParse({ 93 | name: 123, // should be string 94 | age: "30", // should be number 95 | }); 96 | expect(invalidResult.success).toBe(false); 97 | }); 98 | -------------------------------------------------------------------------------- /modules/type-predictor/mod.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 型予測システムのメインモジュール 3 | */ 4 | 5 | export * from "./types.ts"; 6 | export { flattenJson } from "./flatten.ts"; 7 | export { analyzePath } from "./path-analyzer.ts"; 8 | export { predictType } from "./predict.ts"; 9 | export { buildSchema } from "./schema.ts"; 10 | 11 | import { flattenJson } from "./flatten.ts"; 12 | import { predictType } from "./predict.ts"; 13 | import { buildSchema } from "./schema.ts"; 14 | import type { PathInfo } from "./types.ts"; 15 | import { z } from "./deps.ts"; 16 | 17 | /** 18 | * 型予測システムのメインクラス 19 | */ 20 | export class TypePredictor { 21 | /** 22 | * JSONデータから型を予測し、zodスキーマを生成します 23 | * 24 | * @param json 予測対象のJSONデータ 25 | * @returns 生成されたzodスキーマ 26 | */ 27 | predict(json: unknown): z.ZodType { 28 | // エッジケースの処理 29 | if (json === null) { 30 | return z.null(); 31 | } 32 | if (Array.isArray(json) && json.length === 0) { 33 | return z.array(z.any()); 34 | } 35 | if ( 36 | typeof json === "object" && 37 | json !== null && 38 | Object.keys(json).length === 0 39 | ) { 40 | return z.object({}); 41 | } 42 | 43 | // 1. フラット展開 44 | const paths: PathInfo[] = flattenJson(json); 45 | 46 | // 空のパス情報の場合 47 | if (paths.length === 0) { 48 | if (Array.isArray(json)) { 49 | return z.array(z.any()); 50 | } 51 | if (typeof json === "object" && json !== null) { 52 | return z.object({}); 53 | } 54 | return z.any(); 55 | } 56 | 57 | // 2. 型予測 58 | const { structure, predictions } = predictType(paths); 59 | 60 | // 3. スキーマ構築 61 | return buildSchema(structure, predictions); 62 | } 63 | 64 | /** 65 | * JSONデータから型情報を抽出します 66 | * 67 | * @param json 予測対象のJSONデータ 68 | * @returns 型情報の詳細 69 | */ 70 | analyze(json: unknown): { 71 | paths: PathInfo[]; 72 | structure: unknown; 73 | predictions: Map; 74 | } { 75 | // エッジケースの処理 76 | if ( 77 | json === null || 78 | (Array.isArray(json) && json.length === 0) || 79 | (typeof json === "object" && 80 | json !== null && 81 | Object.keys(json).length === 0) 82 | ) { 83 | return { 84 | paths: [], 85 | structure: json, 86 | predictions: new Map(), 87 | }; 88 | } 89 | 90 | // 1. フラット展開 91 | const paths = flattenJson(json); 92 | 93 | // 2. 型予測 94 | const { structure, predictions } = predictType(paths); 95 | 96 | return { 97 | paths, 98 | structure, 99 | predictions, 100 | }; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /modules/type-predictor/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 型予測システムの型定義 3 | */ 4 | 5 | /** 6 | * パスセグメントの種類 7 | */ 8 | export interface PathSegment { 9 | type: "key" | "index" | "wildcard"; 10 | value: string; 11 | arrayAccess?: boolean; 12 | // 配列アクセスの場合の追加情報 13 | arrayInfo?: { 14 | isTuple: boolean; // タプルかどうか 15 | itemTypes: PathInfo[]; // 要素の型情報 16 | }; 17 | } 18 | 19 | /** 20 | * パスセグメントから文字列パスを生成 21 | */ 22 | export function buildPath(segments: PathSegment[]): string { 23 | return segments.map((s) => (s.arrayAccess ? "$" : s.value)).join("."); 24 | } 25 | 26 | /** 27 | * パス情報 28 | */ 29 | export interface PathInfo { 30 | segments: PathSegment[]; // パスセグメントの配列 31 | value: unknown; 32 | type: "string" | "number" | "boolean" | "null" | "object" | "array"; 33 | isNullable: boolean; 34 | metadata?: { 35 | // 型予測のための追加メタデータ 36 | occurrences: number; // 出現回数 37 | patterns?: string[]; // 文字列パターン(enumの予測用) 38 | recordPattern?: { 39 | // Recordの予測用 40 | keyPattern: string; // キーのパターン 41 | valueType: PathInfo; // 値の型情報 42 | }; 43 | }; 44 | } 45 | 46 | /** 47 | * 構造の種類 48 | */ 49 | export type StructureType = 50 | | "object" // 通常のオブジェクト 51 | | "array" // 配列 52 | | "record" // Record 53 | | "primitive" // プリミティブ値 54 | | "mixed"; // 混合型 55 | 56 | /** 57 | * 構造の予測結果 58 | */ 59 | export interface StructurePrediction { 60 | type: StructureType; 61 | children?: Map; 62 | valueType?: string; 63 | keyPattern?: string; // Record型の場合のキーパターン 64 | itemTypes?: Set; // 配列要素の型情報 65 | isNullable?: boolean; // null許容かどうか 66 | arrayDepth?: number; // 配列の深さ(多次元配列用) 67 | arrayElementType?: StructurePrediction; // 配列要素の型情報(再帰的) 68 | } 69 | 70 | /** 71 | * パスの親子関係 72 | */ 73 | export interface PathRelation { 74 | parent: string; 75 | key: string; 76 | children: Set; 77 | samples: unknown[]; 78 | } 79 | 80 | /** 81 | * アクセスキーの要素 82 | */ 83 | export type AccessKeyElement = string | number; 84 | 85 | /** 86 | * サンプリングされた値の情報 87 | */ 88 | export interface SampledValue { 89 | // 生のアクセスキー(数値インデックスを保持) 90 | accessKey: AccessKeyElement[]; 91 | // フラット化されたアクセスキー(数値を $ に変換) 92 | flatAccessKey: string; 93 | // サンプル値のコレクション 94 | sampleValues: unknown[]; 95 | } 96 | 97 | /** 98 | * フラット化された値のエントリ 99 | */ 100 | export interface FlatEntry { 101 | path: string; 102 | value: unknown; 103 | } 104 | 105 | /** 106 | * パターングループ 107 | */ 108 | export interface PatternGroup { 109 | pattern: string; 110 | values: unknown[]; 111 | } 112 | 113 | /** 114 | * 値の型 115 | */ 116 | export type ValueType = 117 | | "string" 118 | | "number" 119 | | "boolean" 120 | | "object" 121 | | "array" 122 | | "null" 123 | | "undefined" 124 | | "date" 125 | | "regexp" 126 | | "error" 127 | | "map" 128 | | "set" 129 | | "promise" 130 | | "buffer"; 131 | 132 | /** 133 | * 型予測の結果 134 | */ 135 | export interface TypePrediction { 136 | type: string; 137 | isArray: boolean; 138 | itemTypes?: Set; 139 | arrayDepth?: number; 140 | arrayElementType?: TypePrediction; 141 | isNullable?: boolean; 142 | } 143 | -------------------------------------------------------------------------------- /modules/zodcli/deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mizchi/zodcli", 3 | "version": "0.2.0", 4 | "description": "Zod を使用した型安全なコマンドラインパーサー", 5 | "author": "mizchi", 6 | "license": "MIT", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/mizchi/ailab" 10 | }, 11 | "exports": { 12 | ".": "./mod.ts" 13 | }, 14 | "publish": { 15 | "include": [ 16 | "mod.ts", 17 | "types.ts", 18 | "core.ts", 19 | "schema.ts", 20 | "utils.ts", 21 | "examples", 22 | "README.md", 23 | "LICENSE" 24 | ], 25 | "exclude": ["test"] 26 | }, 27 | "tasks": { 28 | "test": "deno test .", 29 | "check": "deno check mod.ts", 30 | "fmt": "deno fmt", 31 | "lint": "deno lint" 32 | }, 33 | "lint": { 34 | "include": ["**/*.ts"], 35 | "exclude": ["test"] 36 | }, 37 | "fmt": { 38 | "include": ["**/*.ts", "**/*.md"] 39 | }, 40 | "imports": { 41 | "@std/assert": "jsr:@std/assert@1", 42 | "@std/expect": "jsr:@std/expect@^1.0.13", 43 | "@std/testing": "jsr:@std/testing@^1.0.9", 44 | "npm:zod": "npm:zod@3.22.4", 45 | "npm:jsonschema": "npm:jsonschema@1.4.1" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /modules/zodcli/examples/rest.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env -S deno run -A 2 | /** 3 | * zodcli 新しいインターフェースの使用例 4 | */ 5 | import { createParser } from "../mod.ts"; 6 | import { z } from "npm:zod"; 7 | 8 | const restAll = createParser({ 9 | name: "search", 10 | description: "Search with custom parameters", 11 | args: { 12 | rest: { 13 | type: z.array(z.string()).describe("rest arguments"), 14 | positional: "...", 15 | }, 16 | }, 17 | }); 18 | 19 | const headAndRest = createParser({ 20 | name: "search", 21 | description: "Search with custom parameters", 22 | args: { 23 | query: { 24 | type: z.string().describe("search query"), 25 | positional: 0, 26 | // short: "q", 27 | }, 28 | named: { 29 | type: z.string().describe("named argument"), 30 | short: "n", 31 | // positional 32 | }, 33 | rest: { 34 | type: z.array(z.string()).describe("rest arguments"), 35 | positional: "...", 36 | }, 37 | }, 38 | }); 39 | 40 | // const parsed = restAll.safeParse(["q", "a", "b"]); 41 | // console.log(parsed); 42 | 43 | console.log("rest--------------"); 44 | const parsed2 = headAndRest.safeParse(["p", "-n", "name", "b", "c"]); 45 | console.log(parsed2); 46 | -------------------------------------------------------------------------------- /modules/zodcli/examples/simple.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env -S deno run -A 2 | /** 3 | * zodcli 新しいインターフェースの使用例 4 | */ 5 | import { createParser } from "../mod.ts"; 6 | import { z } from "npm:zod"; 7 | 8 | const searchParser = createParser({ 9 | name: "search", 10 | description: "Search with custom parameters", 11 | args: { 12 | query: { 13 | type: z.string().describe("search query"), 14 | positional: 0, 15 | }, 16 | count: { 17 | type: z 18 | .number() 19 | .optional() 20 | .default(5) 21 | .describe("number of results to return"), 22 | short: "c", 23 | }, 24 | format: { 25 | type: z 26 | .enum(["json", "text", "table"]) 27 | .default("text") 28 | .describe("output format"), 29 | short: "f", 30 | }, 31 | }, 32 | }); 33 | 34 | if (import.meta.main) { 35 | try { 36 | const mockArgs = ["test", "--count", "10", "--format", "json"]; 37 | const data = searchParser.parse(mockArgs); 38 | console.log( 39 | ` 検索クエリ: ${data.query}, 件数: ${data.count}, 形式: ${data.format}`, 40 | ); 41 | } catch (error) { 42 | console.error( 43 | " パースエラー:", 44 | error instanceof Error ? error.message : String(error), 45 | ); 46 | console.log(searchParser.help()); 47 | } 48 | 49 | // safeParse 50 | console.log("\n2. safeParse メソッドの使用例(Zodスタイル):"); 51 | const mockArgs2 = ["test", "--count", "invalid", "--format", "json"]; 52 | const result = searchParser.safeParse(mockArgs2); 53 | 54 | if (result.ok) { 55 | console.log( 56 | ` 検索クエリ: ${result.data.query}, 件数: ${result.data.count}, 形式: ${result.data.format}`, 57 | ); 58 | } else { 59 | console.error(" パースエラー:", result.error.message); 60 | console.log(searchParser.help()); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /modules/zodcli/examples/usage.ts: -------------------------------------------------------------------------------- 1 | // 基本的な使い方 2 | import { createParser } from "jsr:@mizchi/zodcli"; 3 | import { z } from "npm:zod"; 4 | 5 | // パーサーの定義 6 | const searchParser = createParser({ 7 | name: "search", 8 | description: "Search files in directory", 9 | args: { 10 | // 名前付き位置引数 11 | query: { 12 | type: z.string().describe("search pattern"), 13 | positional: 0, // 0番目の引数 14 | }, 15 | // 残り全部 16 | restAll: { 17 | type: z.array(z.string()), 18 | positional: "...", //rest parameter 19 | }, 20 | // オプション引数(デフォルト値あり) 21 | path: { 22 | type: z.string().default("./").describe("target directory"), 23 | short: "p", // shortname 24 | default: ".", 25 | }, 26 | // 真偽値オプション 27 | recursive: { 28 | type: z.boolean().default(false).describe("search recursively"), 29 | short: "r", // --recursive または -r で有効化 30 | }, 31 | }, 32 | }); 33 | 34 | // gen help 35 | if ( 36 | Deno.args.length === 0 || 37 | Deno.args.includes("--help") || 38 | Deno.args.includes("-h") 39 | ) { 40 | console.log(searchParser.help()); 41 | Deno.exit(0); 42 | } 43 | 44 | const result = searchParser.safeParse(Deno.args); 45 | if (result.ok) { 46 | console.log("Parsed:", result.data, result.data.restAll, result.data.query); 47 | } 48 | 49 | console.log(searchParser.help()); 50 | 51 | /// sub command 52 | 53 | // import { createNestedParser } from "jsr:@mizchi/zodcli"; 54 | // const gitAddSchema = { 55 | // name: "git add", 56 | // description: "Add files to git staging", 57 | // args: { 58 | // files: { 59 | // type: z.string().array().describe("files to add"), 60 | // positional: "...", 61 | // }, 62 | // all: { 63 | // type: z.boolean().default(false).describe("add all files"), 64 | // short: "a", 65 | // }, 66 | // }, 67 | // } as const; 68 | -------------------------------------------------------------------------------- /modules/zodcli/mod.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * ZodCLI - Zod を使用した型安全なコマンドラインパーサー 3 | * 4 | * このモジュールは、Zod スキーマを使用して型安全なコマンドラインインターフェースを 5 | * 簡単に構築するためのツールを提供します。 6 | * 7 | * @example 8 | * ```ts 9 | * import { createCliCommand, runCommand } from "./mod.ts"; 10 | * import { z } from "npm:zod"; 11 | * 12 | * const searchCommand = createCommand({ 13 | * name: "search", 14 | * description: "Search with custom parameters", 15 | * args: { 16 | * query: { 17 | * type: z.string().describe("search query"), 18 | * positional: true, 19 | * }, 20 | * count: { 21 | * type: z.number().optional().default(5).describe("number of results"), 22 | * short: "c", 23 | * }, 24 | * }, 25 | * }); 26 | * 27 | * const result = searchCommand.parse(Deno.args); 28 | * runCommand(result, (data) => { 29 | * console.log(`Searching for: ${data.query}, count: ${data.count}`); 30 | * }); 31 | * ``` 32 | */ 33 | 34 | // 型定義のエクスポート 35 | export type { 36 | CommandResult, 37 | CommandSchema, 38 | InferArgs, 39 | InferNestedParser, 40 | InferParser, 41 | InferQueryType, 42 | InferZodType, 43 | NestedCommandMap, 44 | NestedCommandParseSuccess, 45 | NestedCommandResult, 46 | NestedCommandSafeParseResult, 47 | ParseArgsConfig, 48 | ParseArgsOptionConfig, 49 | ParseError, 50 | ParseSuccess, 51 | QueryBase, 52 | SafeParseResult, 53 | } from "./types.ts"; 54 | 55 | // コア機能のエクスポート 56 | export { 57 | createCommand, 58 | createNestedCommands, 59 | createParser, 60 | createSubParser, 61 | createZodSchema, 62 | resolveValues, 63 | } from "./core.ts"; 64 | 65 | // 後方互換性のためのエイリアス 66 | import { createSubParser } from "./core.ts"; 67 | import type { NestedCommandMap, NestedCommandOptions } from "./types.ts"; 68 | 69 | export function createNestedParser( 70 | subCommands: T, 71 | options?: string | NestedCommandOptions, 72 | description?: string, 73 | ) { 74 | return createSubParser(subCommands, options, description); 75 | } 76 | 77 | // ユーティリティ関数のエクスポート 78 | export { 79 | convertValue, 80 | createTypeFromZod, 81 | generateHelp, 82 | getTypeDisplayString, 83 | printHelp, 84 | zodTypeToParseArgsType, 85 | } from "./utils.ts"; 86 | 87 | // isHelp関数のエクスポート 88 | export { isHelp } from "./core.ts"; 89 | 90 | // スキーマ関連のエクスポート 91 | export { isOptionalType, zodToJsonSchema } from "./schema.ts"; 92 | -------------------------------------------------------------------------------- /modules/zodcli/schema.ts: -------------------------------------------------------------------------------- 1 | import { z } from "npm:zod"; 2 | import type { Schema } from "npm:jsonschema"; 3 | 4 | // オプショナルまたはデフォルト値を持つ型かチェック 5 | export function isOptionalType(zodType: z.ZodTypeAny): boolean { 6 | return zodType instanceof z.ZodOptional || zodType instanceof z.ZodDefault; 7 | } 8 | 9 | // JSONスキーマに変換(シンプル版) 10 | export function zodToJsonSchema(schema: z.ZodTypeAny): Schema { 11 | // 基本的な型の処理 12 | if (schema instanceof z.ZodString) { 13 | return { 14 | type: "string", 15 | ...(schema.description ? { description: schema.description } : {}), 16 | }; 17 | } else if (schema instanceof z.ZodNumber) { 18 | return { 19 | type: "number", 20 | ...(schema.description ? { description: schema.description } : {}), 21 | }; 22 | } else if (schema instanceof z.ZodBoolean) { 23 | return { 24 | type: "boolean", 25 | ...(schema.description ? { description: schema.description } : {}), 26 | }; 27 | } else if (schema instanceof z.ZodArray) { 28 | return { 29 | type: "array", 30 | items: zodToJsonSchema(schema._def.type), 31 | ...(schema.description ? { description: schema.description } : {}), 32 | }; 33 | } else if (schema instanceof z.ZodObject) { 34 | // オブジェクト型の処理をシンプルに実装 35 | const properties: Record = {}; 36 | const required: string[] = []; 37 | 38 | // 型安全に処理するために型アサーションを使用 39 | const shape = schema.shape as Record; 40 | 41 | for (const key of Object.keys(shape)) { 42 | const zodSchema = shape[key]; 43 | properties[key] = zodToJsonSchema(zodSchema); 44 | 45 | // 必須フィールドの判定 - オプショナルまたはデフォルト値を持つフィールドは必須ではない 46 | if (!isOptionalType(zodSchema)) { 47 | required.push(key); 48 | } 49 | } 50 | 51 | return { 52 | type: "object", 53 | properties, 54 | ...(required.length > 0 ? { required } : {}), 55 | ...(schema.description ? { description: schema.description } : {}), 56 | }; 57 | } else if (schema instanceof z.ZodEnum) { 58 | return { 59 | type: "string", 60 | enum: [...schema._def.values], 61 | ...(schema.description ? { description: schema.description } : {}), 62 | }; 63 | } else if (schema instanceof z.ZodOptional) { 64 | return zodToJsonSchema(schema._def.innerType); 65 | } else if (schema instanceof z.ZodDefault) { 66 | const innerSchema = zodToJsonSchema(schema._def.innerType); 67 | return { 68 | ...innerSchema, 69 | // @ts-ignore wip 70 | default: schema._def.defaultValue(), 71 | }; 72 | } 73 | 74 | // 複雑な型は単純なobjectとして扱う(型安全性を確保するため) 75 | return { 76 | type: "object", 77 | ...(schema.description ? { description: schema.description } : {}), 78 | }; 79 | } 80 | -------------------------------------------------------------------------------- /poc/callgraph-sample.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * コールグラフテスト用のサンプルファイル 3 | */ 4 | 5 | // 数値を2倍にする関数 6 | function double(n: number): number { 7 | return n * 2; 8 | } 9 | 10 | // 数値を二乗する関数 11 | function square(n: number): number { 12 | return n * n; 13 | } 14 | 15 | // 数値の合計を計算する関数 16 | function sum(...numbers: number[]): number { 17 | return numbers.reduce((total, num) => total + num, 0); 18 | } 19 | 20 | // 平均値を計算する関数 21 | function average(...numbers: number[]): number { 22 | const total = sum(...numbers); 23 | return total / numbers.length; 24 | } 25 | 26 | // factorial関数 - 再帰呼び出しの例 27 | function factorial(n: number): number { 28 | if (n <= 1) return 1; 29 | return n * factorial(n - 1); 30 | } 31 | 32 | // 複数の数学関数を組み合わせる関数 33 | function calculate(n: number): number { 34 | const doubled = double(n); 35 | const squared = square(doubled); 36 | return squared; 37 | } 38 | 39 | // 複雑な計算を行う関数 40 | function complexCalculation(a: number, b: number): number { 41 | const avg = average(a, b, a + b); 42 | const fact = factorial(Math.min(5, Math.floor(avg))); 43 | return calculate(fact); 44 | } 45 | 46 | // オブジェクトメソッド呼び出しの例 47 | const MathUtils = { 48 | add(a: number, b: number): number { 49 | return a + b; 50 | }, 51 | 52 | multiply(a: number, b: number): number { 53 | return a * b; 54 | }, 55 | 56 | compute(a: number, b: number): number { 57 | const sum = this.add(a, b); 58 | const product = this.multiply(a, b); 59 | return average(sum, product); 60 | }, 61 | }; 62 | 63 | // メイン関数 - 全ての関数を呼び出す 64 | function main() { 65 | console.log("Double of 5:", double(5)); 66 | console.log("Square of 4:", square(4)); 67 | console.log("Sum of 1,2,3:", sum(1, 2, 3)); 68 | console.log("Average of 10,20,30:", average(10, 20, 30)); 69 | console.log("Factorial of 5:", factorial(5)); 70 | console.log("Calculate with 3:", calculate(3)); 71 | console.log("Complex calculation with 4 and 6:", complexCalculation(4, 6)); 72 | console.log("Math Utils compute 5 and 7:", MathUtils.compute(5, 7)); 73 | } 74 | 75 | // スクリプトが直接実行された場合はmain関数を呼び出す 76 | if (import.meta.main) { 77 | main(); 78 | } 79 | -------------------------------------------------------------------------------- /poc/check-ci.ts: -------------------------------------------------------------------------------- 1 | /* @script */ 2 | /** 3 | * GitHub Actions の CI 実行結果を確認するスクリプト 4 | * 5 | * このスクリプトは最新の CI 実行結果を取得し、表示します。 6 | * 7 | * 使用方法: 8 | * ```bash 9 | * deno run -A poc/check-ci.ts 10 | * ``` 11 | */ 12 | 13 | import $ from "jsr:@david/dax"; 14 | 15 | type RunListItem = { 16 | databaseId: number; 17 | }; 18 | 19 | /** 20 | * 最新のCI実行を取得して表示 21 | */ 22 | async function checkLatestCI() { 23 | const runs = await $`gh run list --json databaseId --limit 1`.json< 24 | RunListItem[] 25 | >(); 26 | if (runs.length === 0) { 27 | console.error("❌ No CI runs found"); 28 | return; 29 | } 30 | try { 31 | await $`gh run view ${runs[0].databaseId} --exit-status`; 32 | } catch (_error) { 33 | console.log("---- CI Log ----"); 34 | await $`gh run view ${runs[0].databaseId} --log-failed`.noThrow(); 35 | } 36 | } 37 | 38 | // スクリプト実行 39 | if (import.meta.main) { 40 | await checkLatestCI(); 41 | } 42 | -------------------------------------------------------------------------------- /poc/math.ts: -------------------------------------------------------------------------------- 1 | /* @script */ 2 | export function add(a: number, b: number): number { 3 | return a + b; 4 | } 5 | 6 | // Tests 7 | import { expect } from "@std/expect"; 8 | import { test } from "@std/testing/bdd"; 9 | 10 | test("add", () => { 11 | expect(add(1, 2)).toBe(3); 12 | }); 13 | -------------------------------------------------------------------------------- /poc/sample.ts: -------------------------------------------------------------------------------- 1 | /* @script */ 2 | import { join } from "jsr:@std/path"; 3 | import { z } from "npm:zod"; 4 | import { add } from "./math.ts"; 5 | 6 | const schema = z.object({ 7 | name: z.string(), 8 | age: z.number(), 9 | }); 10 | 11 | console.log(join("a", "b")); 12 | console.log(add(1, 2)); 13 | -------------------------------------------------------------------------------- /poc/search-files.ts: -------------------------------------------------------------------------------- 1 | /* @script */ 2 | /** 3 | * ripgrepを使って検索し、マッチしたファイル名だけを表示するスクリプト 4 | */ 5 | 6 | async function searchFiles( 7 | pattern: string, 8 | path = ".", 9 | options: string[] = [], 10 | ): Promise { 11 | const cmd = ["rg", "-l", pattern, path, ...options]; 12 | 13 | const process = new Deno.Command(cmd[0], { 14 | args: cmd.slice(1), 15 | stdout: "piped", 16 | stderr: "piped", 17 | }); 18 | 19 | const { stdout, stderr, success } = await process.output(); 20 | 21 | if (!success) { 22 | const errorMessage = new TextDecoder().decode(stderr); 23 | console.error(`検索エラー: ${errorMessage}`); 24 | return []; 25 | } 26 | 27 | const output = new TextDecoder().decode(stdout); 28 | return output.trim().split("\n").filter(Boolean); 29 | } 30 | 31 | // メイン処理 32 | if (import.meta.main) { 33 | const args = Deno.args; 34 | 35 | if (args.length < 1) { 36 | console.log( 37 | "使用法: deno run -A poc/search-files.ts <検索パターン> [検索パス] [追加オプション...]", 38 | ); 39 | Deno.exit(1); 40 | } 41 | 42 | const pattern = args[0]; 43 | const path = args[1] || "."; 44 | const options = args.slice(2); 45 | 46 | console.log(`"${pattern}" を ${path} で検索中...`); 47 | const files = await searchFiles(pattern, path, options); 48 | 49 | if (files.length === 0) { 50 | console.log("マッチするファイルが見つかりませんでした。"); 51 | } else { 52 | console.log("\n--- マッチしたファイル ---"); 53 | files.forEach((file) => console.log(file)); 54 | console.log(`\n合計: ${files.length}件のファイルが見つかりました。`); 55 | } 56 | } 57 | 58 | /// test 59 | import { expect } from "@std/expect"; 60 | import { test } from "@std/testing/bdd"; 61 | 62 | test("searchFiles が正しい形式の結果を返すこと", async () => { 63 | // モック用の関数で実際のコマンド実行を上書き 64 | const originalCommand = Deno.Command; 65 | 66 | try { 67 | // Deno.Command をモックに置き換え 68 | // @ts-ignore モックのための型無視 69 | Deno.Command = class MockCommand { 70 | constructor() { 71 | /* do nothing */ 72 | } 73 | 74 | async output() { 75 | return { 76 | success: true, 77 | stdout: new TextEncoder().encode("file1.ts\nfile2.ts\nfile3.ts"), 78 | stderr: new Uint8Array(0), 79 | }; 80 | } 81 | }; 82 | 83 | const result = await searchFiles("test"); 84 | expect(result).toEqual(["file1.ts", "file2.ts", "file3.ts"]); 85 | } finally { 86 | // テスト後に元に戻す 87 | Deno.Command = originalCommand; 88 | } 89 | }); 90 | -------------------------------------------------------------------------------- /poc/search_npm.ts: -------------------------------------------------------------------------------- 1 | /* @script */ 2 | /** 3 | * npm レジストリの検索APIを使用して、 4 | * モジュールを検索するスクリプト 5 | * 6 | * Usage: 7 | * deno run -A poc/search_jsr.ts 8 | * 9 | * Example: 10 | * deno run -A poc/search_jsr.ts zod 11 | */ 12 | 13 | import type { PackageJSON } from "@npm/types"; 14 | 15 | function pick(obj: T, keys: K[]): Pick { 16 | // @ts-ignore xxx 17 | return keys.reduce((acc, key) => ({ ...acc, [key]: obj[key] }), {}); 18 | } 19 | 20 | type SearchResult = { 21 | downloads: { 22 | monthly: number; 23 | weekly: number; 24 | }; 25 | dependents: number; 26 | updated: string; // Date 27 | searchScore: number; 28 | score: { 29 | final: number; 30 | detail: { 31 | popularity: number; 32 | quality: number; 33 | maintenance: number; 34 | }; 35 | }; 36 | flags: { 37 | insecure: 0; 38 | }; 39 | package: PackageJSON; 40 | }; 41 | 42 | /** 43 | * npmのモジュールを検索する 44 | * @param query 検索クエリ 45 | * @returns 検索結果の配列 46 | */ 47 | export async function searchNpm(query: string): Promise { 48 | const url = `http://registry.npmjs.com/-/v1/search?text=${ 49 | encodeURIComponent( 50 | query, 51 | ) 52 | }&size=20`; 53 | const response = await fetch(url, { 54 | headers: { 55 | Accept: "application/json", 56 | }, 57 | }); 58 | 59 | if (!response.ok) { 60 | throw new Error(`API request failed: ${response.statusText}`); 61 | } 62 | 63 | const data = (await response.json()) as { objects: SearchResult[] }; 64 | return data.objects; 65 | } 66 | 67 | // エントリポイント 68 | if (import.meta.main) { 69 | const query = Deno.args[0]; 70 | if (!query) { 71 | console.error("Please provide a search query"); 72 | Deno.exit(1); 73 | } 74 | 75 | try { 76 | const results = await searchNpm(query); 77 | const summary = results.map((t) => { 78 | return { 79 | name: t.package.name, 80 | version: t.package.version, 81 | downloads: t.downloads, 82 | dependents: t.dependents, 83 | updated: t.updated, 84 | score: pick(t.score.detail, ["popularity", "quality", "maintenance"]), 85 | }; 86 | }); 87 | console.dir(summary, { depth: undefined }); 88 | } catch (error: unknown) { 89 | if (error instanceof Error) { 90 | console.error("Error:", error.message); 91 | } else { 92 | console.error("Unknown error occurred:", error); 93 | } 94 | Deno.exit(1); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /poc/test.ts: -------------------------------------------------------------------------------- 1 | /* @script */ 2 | /** 3 | * 数値を2倍にする関数 4 | * @param x 入力値 5 | * @returns 2倍の値 6 | */ 7 | export function double(x: number): number { 8 | return x * 2; 9 | } 10 | 11 | if (import.meta.main) { 12 | console.log(double(5)); 13 | } 14 | -------------------------------------------------------------------------------- /poc/tools/imgcat.ts: -------------------------------------------------------------------------------- 1 | import sharp from "npm:sharp"; 2 | import process from "node:process"; 3 | 4 | const IMGCAT_PREIX = "\x1B]1337;File="; 5 | const IMGCAT_SUFFIX = "\x07"; 6 | 7 | async function printImage( 8 | imageData: Uint8Array, 9 | { 10 | fileName = undefined, 11 | width = undefined, 12 | height = undefined, 13 | scale = 1, 14 | preserveAspectRatio = true, 15 | }: { 16 | fileName?: string; 17 | width?: number; 18 | height?: number; 19 | scale?: number; 20 | preserveAspectRatio?: boolean; 21 | } = {}, 22 | ) { 23 | const transformed = sharp(imageData); 24 | const metadata = await transformed.metadata(); 25 | 26 | const aspectRatio = metadata.width! / metadata.height!; 27 | if (width && !height) { 28 | height = aspectRatio * width; 29 | transformed.resize(~~width, ~~height, { 30 | fit: preserveAspectRatio ? "inside" : "fill", 31 | }); 32 | } 33 | if (height && !width) { 34 | width = height / aspectRatio; 35 | transformed.resize(~~width, ~~height, { 36 | fit: preserveAspectRatio ? "inside" : "fill", 37 | }); 38 | } 39 | if (scale !== 1) { 40 | width = metadata.width! * scale; 41 | height = metadata.height! * scale; 42 | transformed.resize(~~width, ~~height, { 43 | fit: preserveAspectRatio ? "inside" : "fill", 44 | }); 45 | } 46 | 47 | const buffer = await transformed.png().toBuffer(); 48 | const base64Data = buffer.toString("base64"); 49 | _print(buffer.length, base64Data); 50 | } 51 | function _print(size: number, data: string) { 52 | process.stdout.write(`\x1B]1337;File=inline=1;size=${size}:${data}\x07`); 53 | } 54 | 55 | const imageData = await Deno.readFile( 56 | new URL("./image.png", import.meta.url).pathname, 57 | ); 58 | 59 | // await printImage(imageData, { fileName: "image.png", scale: 0.5 }); 60 | 61 | import path from "node:path"; 62 | if (import.meta.main) { 63 | const resolved = path.join(Deno.cwd(), Deno.args[0]); 64 | const imageData = await Deno.readFile(resolved); 65 | await printImage(imageData, {}); 66 | // console.log("Printed image"); 67 | } 68 | --------------------------------------------------------------------------------