360 Views
March 17, 25
スライド概要
DenoでCLIアプリを作る
~ 虎の穴ラボでの利用例を添えて ~
Deno でCLIを作る利点について述べ、虎の穴ラボで運用しているCLIアプリを導入例として紹介する。
虎の穴ラボ株式会社は、主にとらのあな関連サービスのシステム開発を専門に担う、エンジニアの会社です。
DenoでCLIアプリを作る ~ 虎の穴ラボでの利用例を添えて ~ toranona.deno #20 2025/3/14 虎の穴ラボ株式会社 奥谷 一陽 Copyright (C) 2025 Toranoana Lab Inc. All Rights Reserved.
自己紹介 奥谷 一陽 所属:虎の穴ラボ株式会社 興味:Deno、TypeScript 最近届いたもの:1/144 HG デスルター X:@okutann88 github:Octo8080X toranoana.deno 運営の1人 Copyright (C) 2025 Toranoana Lab Inc. All Rights Reserved.
アジェンダ - 「Deno でCLIアプリを作る」での使ってもらう側の利点と 感じること - deno compile でよりポータブルに - 標準ライブラリでのサポート状況 - 虎の穴ラボでの導入事例 - - 経緯 - 実装 まとめ Copyright (C) 2025 Toranoana Lab Inc. All Rights Reserved.
「Deno でCLIアプリを作る」での使ってもらう側の利点と感じること - CLIアプリとWebアプリの大きな違い - 利用者に比較的低レイヤーの環境の構築相当を要求するか? - Webであればブラウザとネットワークで概ね同じように動く - CLIは、ランタイムの準備が必要な場合もある - 注: バイナリを配布するのではない場合 =>「ランタイムの導入が楽」は大きな利点! と感じています Copyright (C) 2025 Toranoana Lab Inc. All Rights Reserved.
「Deno でCLIアプリを作る」での利点と感じること - - インストールが楽 - macOS/Linux: curl -fsSL https://deno.land/install.sh | sh - Windows : irm https://deno.land/install.ps1 | iex さらに Deno 本体でバージョン上げ下げできること - 最新 :deno upgrade - 指定バージョン:deno upgrade v1.46.0 サードパーティのバージョン管理ツールの導入不要 => 環境構築を案内しやすい! Copyright (C) 2025 Toranoana Lab Inc. All Rights Reserved.
deno compile でよりポータブルに - CLIアプリとWebアプリの大きな違い - 利用者に比較的マシンに低レイヤーの環境の構築相当を要求するか? - Webであればブラウザとネットワークで概ね同じように動く - CLIは、ランタイムの準備が必要な場合もある - 注: バイナリを配布するのではない場合 Copyright (C) 2025 Toranoana Lab Inc. All Rights Reserved.
deno compile でよりポータブルに deno compile を使うことで、スタンドアロン実行ファイルを作成できる。 > deno init > deno compile -o compiled-main main.ts > ./compiled-main コンパイル結果だけ渡すこともできる。 Add 2 + 3 = 5 => 非開発エンジニアにも提供しやすいはず。 Copyright (C) 2025 Toranoana Lab Inc. All Rights Reserved.
標準ライブラリでの支援状況 jsr.ioでは、標準ライブラリとして41パッケージ公開(2025/3/7時点) Copyright (C) 2025 Toranoana Lab Inc. All Rights Reserved.
標準ライブラリでの支援状況 cliの名を冠するモジュールがある。 Copyright (C) 2025 Toranoana Lab Inc. All Rights Reserved.
標準ライブラリでの支援状況
READMEの記載では、引数のパースだけ?に見えるが、
コマンドライン上でのselectや、スピナーなど機能豊富(unstable)
> import { promptMultipleSelect } from "jsr:@std/cli/unstable-prompt-multiple-select";
undefined
> const browsers = promptMultipleSelect("Please select browsers:", ["safari", "chrome", "firefox"],
{ clear: true });
Please select browsers:
> import { Spinner } from "jsr:@std/cli/unstable-spinner";
◯ safari
const spinner = new Spinner({ message: "Loading...", color: "yellow" });
❯ ◯ chrome
spinner.start();
◯ firefox
undefined
⠧ Loading...
もちろん、サードパーティモジュールも使える
Copyright (C) 2025 Toranoana Lab Inc. All Rights Reserved.
虎の穴ラボでの導入事例 虎の穴ラボでも Deno を使ったcliがある。 - ラフにデータの加工に使っている自分用のもの - リポジトリ管理されているもの 紹介 Copyright (C) 2025 Toranoana Lab Inc. All Rights Reserved.
虎の穴ラボでの導入事例 - 経緯 こんな業務がある。 - 社内では、マークダウンで管理されているAPIのドキュメントを、 社外にPDFにして提供する。 - PDFを引渡しする初回は、VSCodeの拡張で変換し出力したが... 不安 - 個人のローカルのVSCodeの設定に依存していいとは思えない - 拡張機能がアップデートされて同じ体裁で出力してくれないかも... =>ツール化して、個人の依存と外部的なアップデートから強くしよう! Copyright (C) 2025 Toranoana Lab Inc. All Rights Reserved.
虎の穴ラボでの導入事例 - 経緯 - 業務に取り込む前に、個人的に Deno でPDFを取り扱う方法を調べていた https://www.ccbaxy.xyz/blog/2024/05/19/js92 この事前調査を踏まえて実装 Copyright (C) 2025 Toranoana Lab Inc. All Rights Reserved.
虎の穴ラボでの導入事例 - 実装 - 導入が簡単なので、 READMEの記述もシンプル Copyright (C) 2025 Toranoana Lab Inc. All Rights Reserved.
虎の穴ラボでの導入事例 - 実装 - ソースコード
import puppeteer from "https://deno.land/x/puppeteer@16.2.0/mod.ts";
import { parse as markedParse } from "npm:marked@13.0.2";
import { parse as jsoncParse } from "jsr:@std/jsonc@0.224.0";
import { parse as flagsParse } from "jsr:@std/flags@0.224.0";
import { decorateColor } from "./utils/color.ts";
import { validateSetup } from "./utils/validator.ts";
import { existsSync } from "jsr:@std/fs@0.229.3";
// パラメータを検証
const flags = flagsParse(Deno.args) as { [key: string]: string[] };
if (flags["_"] && flags["_"].length === 0) {
console.error(decorateColor("red", "ERROR: Please provide a setup file"));
Deno.exit(1);
}
const setupFileName = flags["_"].toString();
if (!setupFileName) {
console.error(decorateColor("red", `ERROR: File not found: ${flags["_"]}`));
Deno.exit(1);
}
// 設定ファイルの読み込み
const setupText = Deno.readTextFileSync(setupFileName);
console.info(`setup file = ${setupFileName} \n ${setupText}`);
const setup = jsoncParse(setupText);
// 設定ファイルの内容を検証
const { success, data, error } = validateSetup(setup);
if (!success) {
console.error(decorateColor("red", `ERROR: Invalid setup file: ${error}`));
Deno.exit(1);
}
// 設定ファイルに書かれたファイルの読み込み
const markdownTexts: string[] = [];
for (const document of data.documents) {
const mdText = Deno.readTextFileSync(document.fileName);
markdownTexts.push(mdText);
if (document.pageBreak) {
markdownTexts.push('<div class="page-break"></div>');
}
}
const cssHtml = data.css ? Deno.readTextFileSync(data.css) : "";
const headerHtml = data.header ? Deno.readTextFileSync(data.header) : undefined;
const footerHtml = data.footer ? Deno.readTextFileSync(data.footer) : undefined;
const displayHeaderFooter = data.header !== undefined ||
data.footer !== undefined;
const html = markedParse(markdownTexts.join("\n\n"));
const pageContent = "<html><head>" + cssHtml + "</head><body>" + html + "</body></html>";
// HTMLファイルの生成モードの実行
if (flags["html"]) {
const outputFileName = `${data.title}-${(new Date()).getTime()}.html`;
Deno.writeTextFileSync(outputFileName, pageContent);
console.info(
decorateColor("green", `HTML file has been generated. '${outputFileName}'`),
);
Deno.exit();
}
// PDFファイルの生成モードの実行
const browser = await puppeteer.launch({});
const page = await browser.newPage();
const outputFileName = `${data.title}-${(new Date()).getTime()}.pdf`;
// 設定ファイルに記載されているファイルが存在するか確認
const notFoundFiles: string[] = [];
const testFileNames = [
...data.documents.map((d) => d.fileName),
data.header,
data.footer,
data.css,
].filter((doc) => doc !== undefined) as string[];
for (const testFileName of testFileNames) {
if (!existsSync(testFileName)) {
notFoundFiles.push(testFileName);
}
}
await page.setContent(pageContent);
await page.pdf({
path: outputFileName,
format: "A4",
margin: { top: "60px", bottom: "60px" },
displayHeaderFooter,
headerTemplate: headerHtml,
footerTemplate: footerHtml,
});
console.info(
decorateColor("green", `PDF file has been generated. '${outputFileName}'`),
);
page.close();
browser.close();
Deno.exit();
Copyright (C) 2025 Toranoana Lab Inc. All Rights Reserved.
虎の穴ラボでの導入事例 - 実装 - ソースコード
import puppeteer from "https://deno.land/x/puppeteer@16.2.0/mod.ts";
import { parse as markedParse } from "npm:marked@13.0.2";
import { parse as jsoncParse } from "jsr:@std/jsonc@0.224.0";
import { parse as flagsParse } from "jsr:@std/flags@0.224.0";
import { decorateColor } from "./utils/color.ts";
import { validateSetup } from "./utils/validator.ts";
import { existsSync } from "jsr:@std/fs@0.229.3";
-
開発時期がJSRの公開前後の時期であったため
https://deno.land/x、 npm:、 jsr: を、混ぜながら導入。
-
jsr:@std でかなりフォローできることが見える。
-
開発は数時間でFix
-
数ファイル 100数行で構成
このくらいの規模感で始めるのがいいのでは?
Copyright (C) 2025 Toranoana Lab Inc. All Rights Reserved.
まとめ - Deno で作るcliアプリは、 組織へのDenoの導入の取っ掛かりになるべく小さく始めるのオススメ - Deno ならランタイムの導入が簡単/環境構築が簡単 - 本体だけでバージョン管理までフォローする - ランタイムだけで非エンジニアに提供するバイナリも作成できる。 - npm の資産も使える 「面倒なことは Deno と TS にやらせよう!!」 Copyright (C) 2025 Toranoana Lab Inc. All Rights Reserved.
ありがとうございました Copyright (C) 2025 Toranoana Lab Inc. All Rights Reserved.