動かざることバグの如し

近づきたいよ 君の理想に

テスト書くならtsconfigのpathsじゃなくてsubpath imports使おうよ

環境

  • nodejs v24
  • typescript v5

まずはpathsとsubpath importsをそれぞれ押さえてみる。

paths

pathsbaseUrltsconfig.json を基準に、TypeScript の importrequire の解決先を再マップする機能だ。 ただしこれはあくまで TypeScript 側の解決規則であって、実行時の Node.js にそのまま伝わるわけではない。

設定例

// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "rootDir": "./src",
    "outDir": "./dist",
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"]
    }
  },
  "include": ["src/**/*.ts"]
}
// src/lib/math.ts
export function add(a: number, b: number) {
  return a + b;
}
// src/index.ts
import { add } from "@/lib/math";

console.log(add(1, 2));
  • メリットは、TypeScript に対して短い import 名の解決先を宣言できるので、型チェック用のエイリアスとして扱いやすいことだ
  • デメリットは、tsc@/lib/math のような specifier を出力時に直さないので、Node.js で動かすには bundler、loader、post-build 変換など別の仕組みが必要になることだ

subpath imports

Node.js の subpath imports は package.jsonimports フィールドで定義する private mapping で、同じ package の内側からだけ使え、キーは必ず # で始まる。 TypeScript v5 では node16nodenextbundler の解決モードでこの imports を参照するから、Node と TypeScript の解決規則を package.json に寄せやすい。

設定例

// package.json
{
  "name": "sample-app",
  "type": "module",
  "imports": {
    "#lib/*.js": "./dist/lib/*.js"
  },
  "scripts": {
    "build": "tsc -p tsconfig.json",
    "start": "node dist/index.js"
  }
}
// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "rootDir": "./src",
    "outDir": "./dist",
    "resolvePackageJsonImports": true
  },
  "include": ["src/**/*.ts"]
}
// src/lib/math.ts
export function add(a: number, b: number) {
  return a + b;
}
// src/index.ts
import { add } from "#lib/math.js";

console.log(add(1, 2));
  • メリットは、Node.js 自身が imports を理解するので、実行時の解決と TypeScript の解決を揃えやすいことだ
  • また importsexports と同様のルールで解決され、条件分岐を使え、しかも exports と違って外部 package へのマッピングも許されるのもメリット
  • デメリットは、package の外からは使えず、specifier が #... 形式に制限されることだ
  • さらに Node.js の import 解決は拡張子探索をせず、subpath pattern も特別な拡張子補完をしないから、#lib/math.js のように .js まで含めて書く前提で設計したほうが安全だ

テスト時のモッキングで差が出る

モックする際は subpath imports のほうが有利な場合がある。

paths を使っている場合、テスト時に依存を差し替えたいときに以下のような問題が起きる。

// src/services/calculator.ts
import { add } from "@/lib/math";

export function calculate(x: number, y: number) {
  return add(x, y) * 2;
}

テストで add をモックしようとしても、@/lib/math という文字列そのものを解決する必要があるため、テストランナーやローダーの設定が複雑になる。Jest なら moduleNameMapper、Vitest なら resolve.alias の設定が必要だ。

一方で subpath imports を使うと、imports フィールドに条件分岐を書ける。

// package.json
{
  "imports": {
    "#lib/*.js": {
      "default": "./dist/lib/*.js",
      "test": "./dist/lib/__mocks__/*.js"
    }
  }
}

このように設定すると、NODE_ENV=test などで実行したときは自動的に __mocks__ ディレクトリの実装を参照するようになる。TypeScript も同じ package.json を参照するため、型チェックと実行時の動作が自然に一致する。

// src/lib/__mocks__/math.ts
export function add(a: number, b: number) {
  return 100; // テスト用の固定値
}

ビルドやバンドラーの設定をいじらずに、Node.js 標準の機能だけでモック切り替えが実現できるのは大きな利点だ。

参考リンク

既存のColimaインスタンスのメモリサイズを変更したい

環境

  • colima v0.10.1

やりたいこと

既に起動しているColimaインスタンスのメモリを減らしたい。再起動は必要らしいがデータは失いたくない。

コマンド

起動中の場合は停止する

colima stop

新しいメモリ設定で起動する。1GBに設定したい場合は以下

colima start --memory 1

または設定ファイルを編集する方法もある

colima start --edit

参考リンク

OpenAIのAPI無料枠にGPT-5.4が追加された

OpenAIのAPI無料枠とは

OpenAIの「complimentary daily tokens」とは、APIの入力・出力データ共有を有効にした一部の組織向けに提供される日次の無料トークン枠のことだ。これはChatGPTの無料プランとは別物で、OpenAI APIを利用する開発者向けの仕組みとして案内されている。

対象かどうかはデータ共有設定ページで確認できる。「You're eligible for free daily usage on traffic shared with OpenAI」や「You're enrolled for complimentary daily tokens」という表示が出ていれば対象だ。

無料トークンを使うには、まず組織オーナーがAPIの入出力データ共有を有効化する必要がある。データ共有は「全プロジェクト」または「選択したプロジェクトのみ」で設定でき、無料枠も共有を有効にしたプロジェクトの通信にのみ適用される。条件を満たしていれば共有を有効にした時点で自動適用されるので、別途クーポン入力などは不要だ。

ただし注意点として、APIを使うにはアカウント残高が正である必要があり、無料枠があっても残高ゼロでは使えないと案内されている。

無料枠の量は以下の通りだ。

  • 主要モデル群:Tier 3〜5の場合は1日最大100万トークン、Tier 1〜2の場合は25万トークン
  • 小型モデル群:Tier 3〜5は1日最大1000万トークン、Tier 1〜2は250万トークン

上限はモデルごとではなく、同じグループ内の対象モデルで合算される仕組みだ。無料枠のカウンターは毎日00:00 UTCにリセットされる。

設定画面の/data-controls/sharingから確認できる

platform.openai.com

gpt-5.4も追加された

GPT-5.2はなかなか追加されずGPT-5.1止まりか…と思っていた時期もあったが、GPT-5.4はリリース後すぐに解放された。

picture 0

GPT-5.4は性能高いと評価高いが、トークン単価はGPT5のなかで一番高くなっているのでこれは嬉しい。

日本から撤退した海外フードデリバリー一覧

フードデリバリーサービスのWoltが日本から撤退することが決定した。

press.wolt.com

普段フードデリバリーを多用しているわけじゃない自分にとっても、大手サービスだっただけに結構衝撃だった。

そういえば他にどんなサービスが参入して散っていったのか気になったので調べてみた。調べてみると2026年03月現在、Wolt、foodpanda、FOODNEKO、DiDi Food、DoorDash、Coupangの6サービスについて確認できたので、まとめてみた。

日本参入・撤退の一覧表

日本に参入して撤退した(あるいはブランドとして終了した)これら6サービスは、いずれも2020年前後に参入し、最長でも約6年(Wolt)、短いものは数カ月〜2年程度で撤退・統合されている。

サービス名 本社国・運営企業(グループ) 日本参入(サービス開始) 日本撤退(サービス終了) 補足
Wolt(ウォルト) フィンランド・Wolt Enterprises Oy(現 DoorDash Inc グループ) 2020年3月26日 広島で正式サービス開始 2026年3月4日 日本でのサービス終了 2021年11月に米DoorDashが買収、日本ではWoltブランドで継続後、約6年で撤退。
foodpanda(フードパンダ) ドイツ・Delivery Hero SE/日本法人 Delivery Hero Japan株式会社 2020年9月17日 神戸・横浜・名古屋でサービス開始 2022年1月31日 日本でのサービス終了 アジア太平洋中心のフードデリバリーブランド。日本では約1年4カ月で撤退。2021年4月にFOODNEKOを統合。
FOODNEKO(フードネコ) 韓国・Woowa Brothers Corp.(日本法人 株式会社ダブリュービージェー) 2020年12月8日 東京(港区・渋谷区・新宿区)でサービス開始 2021年4月27日 サービス終了、foodpandaへ統合 韓国「配達の民族(BAEMIN)」運営会社の日本ブランド。開始から約4カ月でfoodpanda側へ統合。
DiDi Food(ディディフード) 中国・DiDi Chuxing(滴滴出行)/日本法人 DiDiフードジャパン株式会社 2020年4月7日 大阪市内6区で実証実験開始→同年6月23日に本格展開 2022年5月25日 日本でのサービス終了 タクシー配車「DiDi」発のデリバリー。日本上陸から約2年で事業終了し、タクシー配車事業へ集中。
DoorDash(ドアダッシュ) 米国・DoorDash, Inc.(日本法人 DoorDash Technologies Japan株式会社) 2021年6月9日 宮城県仙台市で日本初進出 2022年8月31日 DoorDashブランドとしての日本サービス終了(Woltへ集約) 2021年6月に仙台でローンチ。2022年6月のWolt買収完了に伴い、日本ではWoltブランドに一本化。
Coupang(クーパン)*第1次進出 韓国・Coupang, Inc.(クーパンジャパン合同会社) 2021年6月 東京(品川区など)でクイックコマース型デリバリー開始 2023年3月21日 日本でのクイックコマースサービス終了 食品・日用品を即配するネットスーパー型。約21カ月で撤退し、韓国・台湾に経営資源を集中。
参考:Uber Eats(ウーバーイーツ) 米国:Uber Technologies Inc./日本運営:Uber Eats Japan合同会社 2016年9月29日、東京都心(渋谷区・港区など)でサービス開始 (2026年時点)サービス継続中 2016年9月29日に東京でスタートし、日本ではUber Eats Japan合同会社が運営。数年で全国47都道府県に展開し、主要なフードデリバリーの一つとして存続している

Wolt(ウォルト)

  • フィンランド・ヘルシンキ発のフードデリバリー企業で、2014年に本国でサービス開始、欧州を中心に多国展開してきた。
  • 日本では2020年3月26日、広島市で「日本初上陸」として正式サービスを開始し、当初は広島市中区・西区・南区・東区の一部で約80店舗を扱った。
  • その後、札幌・仙台・東京など全国にエリアを拡大し、2020年10月22日には東京都内でもサービス開始している。
  • 2022年6月、米DoorDashによるWolt買収が完了し、日本を含めDoorDashグループの一員となったが、日本ではブランドとしてはWoltが継続し、DoorDashブランドは後述の通り終了した。
  • 2026年2月末、日本法人が日本でのサービスを2026年3月4日で終了すると発表し、公式サイトやニュースでも「2020年3月開始から約6年で撤退」と報じられている。

foodpanda(フードパンダ)

  • foodpandaはドイツ・ベルリン本社のDelivery Hero SE傘下ブランドで、アジア太平洋地域を中心に展開するフードデリバリープラットフォーム。
  • 日本ではDelivery Hero Japan株式会社が運営し、2020年9月17日に神戸・横浜・名古屋の3都市でサービスを開始した。
  • その後、札幌・福岡・広島・大阪・京都・東京などに一気に拡大し、2021年には全国多数都市をカバーしていた。
  • 2021年4月16日には、FOODNEKOを運営するダブリュービージェーとのサービス統合を発表し、FOODNEKOのブランドをfoodpanda側へ取り込んだ。
  • 親会社Delivery Heroは2021年末、日本事業売却・整理方針を示し、公式発表・報道により2022年1月31日をもって日本でのサービスを終了したことが明らかになっている。

FOODNEKO(フードネコ)

  • FOODNEKOは、韓国のWoowa Brothers Corp.(「配達の民族/BAEMIN」の運営会社)の日本法人・株式会社ダブリュービージェーが展開したフードデリバリーサービス。
  • 2020年12月8日、東京の港区・渋谷区・新宿区を対象としたサービスとしてスタートし、「一人前から注文可能」「サービス料無料」「1.5kmまで配達料無料」「最低注文金額なし」といった特徴で打ち出された。
  • 配達員を「ネコライダー」と呼び、ライダーセンターを設置するなど、コミュニティ重視・サポート体制が特徴的だった。
  • しかし2021年4月16日、foodpandaを運営するDelivery Hero JapanがFOODNEKOとのブランド統合を発表し、FOODNEKOは2021年4月27日付でサービス終了、foodpandaブランドへ統合された。
  • 同統合の背景として、Delivery Hero SEがWoowa Brothersをグループ傘下に入れ、アジア太平洋地域で戦略的パートナーシップを結んだことが挙げられている。

DiDi Food(ディディフード)

  • DiDi Foodは、中国の配車プラットフォーム企業Didi Chuxing(滴滴出行)が運営するフードデリバリーサービスで、日本ではDiDiフードジャパン株式会社が展開した。
  • 2020年4月7日、大阪市内6区(福島・北・中央・西・浪速・天王寺)で実証実験を開始し、同年6月23日に大阪市の一部地域で本格展開に移行したとされている。
  • その後、兵庫・広島・福岡・京都・愛知などへ徐々にエリアを拡大し、多くのチェーン店・ローカル店と提携した。
  • 2022年4月20日、配達パートナー向け通知等でサービス終了が告知され、2022年5月25日をもって日本でのDiDi Food事業終了が発表された。
  • 終了理由としては、日本の市場環境の変化を踏まえタクシー配車プラットフォームに集中するためと説明され、ニュースでは「日本上陸から約2年で幕」とまとめられている。

DoorDash(ドアダッシュ)

  • DoorDashは、米カリフォルニア州サンフランシスコ本社のオンデマンドフードデリバリー企業で、米国では業界最大手とされる。
  • 日本向けには100%子会社としてDoorDash Technologies Japan株式会社が設立され、2021年6月9日、宮城県仙台市で日本でのサービスを正式に開始した。
  • 仙台を初進出地に選んだ理由として、人口規模や都市と郊外のバランスの良さなどが挙げられており、日本では地方・郊外にも焦点を当てた展開戦略だったとされる。
  • 2021年11月、DoorDashはフィンランドのWolt Enterprises Oyの買収に合意し、2022年6月1日に買収完了を発表した。
  • この買収完了発表の中で、日本市場ではDoorDashブランドの事業を停止しWoltブランドへ集約する方針が示され、日本で存続するのはWoltのみとなることが明らかにされた。
  • 日本向けDoorDashアプリおよびDoorDashブランドのサービスは2022年8月31日で終了とされ、ブランドとしての日本展開は約1年3カ月という短期間にとどまった。

Coupang(クーパン)〔第1次進出〕

  • Coupangは韓国発の大手EC企業で、「韓国のAmazon」とも呼ばれ、ロケット配送と呼ばれる翌日配送サービスなどで急成長してきた。
  • 日本ではCoupang Japan合同会社が2021年4月に設立され、同年6月から東京都品川区を皮切りに、食品・日用品を最短10〜20分で届けるクイックコマース型サービスを開始した。
  • ダークストアを都内に設け、目黒区・世田谷区など6区程度に展開し、アプリダウンロード数は10万件規模と伝えられている。
  • しかし、2023年3月21日に東京・目黒区や世田谷区などで展開していた生鮮・日用品の即時配送サービスを終了し、日本から撤退することが報じられた。
  • 撤退理由として、コンビニエンスストア網の強さや高齢化によりオンライン食料品購入のニーズが相対的に低いことが指摘され、経営資源を韓国および台湾市場に集中する判断が示されている。

調べてみるとフードパンダとかフードネコとか知らなかったw

foodpandaのUX調査している記事を見かけた。変な話Uber Eatsより使いやすそうである。ただ、これだけじゃ勝ち残れないんだなと感じた。

qiita.com

画像抽出や圧縮、ローカルで動くPDFツール「Stirling PDF」を試す

環境

  • Stirling PDF 2.7.0

やりたいこと

PDFから画像を抽出したり、ファイルサイズを圧縮したい場面って意外とある。 Acrobat Readerだけ入れてても有料版でないと使えない機能が多いし、ネットに転がってる「PDF変換ツール」はどう見ても怪しい。

セキュリティを考えると、機密文書を謎のオンラインサービスにアップロードするわけにはいかない。 そこで見つけたのがStirling PDFだ。

これはDockerでローカル環境に構築できるセルフホスト型のPDFツール。ファイルを外部に送信せずに処理できるので、情報漏洩のリスクがゼロになる。

インターフェースは日本語対応で、裏側はJavaで動いているらしい。

機能をざっと見ると、かなりの充実ぶり。

picture 0

  • おもな機能
    • PDFテキストエディター、結合、比較、圧縮、変換、OCR/クリーンアップ、手動墨消し、PDFマルチツール、テキストを追加、画像の追加、注釈、フォーム入力
  • 文書セキュリティとレビュー
    • 文書セキュリティ: パスワードの追加、透かしの追加、PDFにスタンプを追加、無害化、平坦化、PDFフォームのロック解除、権限の変更
    • 文書レビュー: メタデータの変更、目次の編集、閲覧
    • 署名: 証明書による署名、署名
  • ページ整形と高度なフォーマット
    • ページ整形: PDFのトリミング、回転、分割、ページ再配置、ページ縮尺調整、ページ番号追加、マルチページレイアウト、小冊子面付け、PDFを単一の大きなページに変換、添付を追加
    • 高度なフォーマット: 色/コントラストの調整、修復、スキャン写真の検出と分割、PDFを重ね合わせ、色の置換と反転、スキャナー風効果
  • 抽出や削除などのツール類
    • 抽出: ページの抽出、画像の抽出
    • 削除: ページの削除、空白ページの削除、注釈の削除、画像の削除、パスワードの削除、証明書の署名削除
    • 自動化: PDFファイル名を自動変更、自動化

とりあえずDockerで動かしてみた。

docker-compose.yml

以下のdocker-compose.ymlを作成して起動する。

services:
  stirling-pdf:
    image: stirlingtools/stirling-pdf:latest
    ports:
      - '8080:8080'
    environment:
      - SECURITY_INITIALLOGIN_USERNAME=admin
      - SECURITY_INITIALLOGIN_PASSWORD=password

ブラウザで http://localhost:8080 にアクセスし、admin/passwordでログインできる。

環境変数で LANGS=ja_JP を指定したが、反映されなかった模様。

API機能

Stirling PDFにはAPI機能もある。設定からAPIキーが取得できるので

import requests

def split_pdf(pdf_path, output_zip_path, pages):
    url = "http://localhost:8080/api/v1/general/split-pages"

    headers = {
        "X-API-KEY": 'dummy-5252-4342-96b5-e0b60c03899f'
    }


    with open(pdf_path, "rb") as f:
        files = {"fileInput": f}
        data = {"pageNumbers": pages}

        response = requests.post(url, headers=headers, files=files, data=data)

    if response.status_code == 200:
        with open(output_zip_path, "wb") as out_f:
            out_f.write(response.content)
        print("分割が完了 指定したパスに保存された")
    else:
        print(f"失敗 ステータスコード: {response.status_code}")

# 例: "1-3,5,7-9" のように指定
split_pdf("input.pdf", "extracted.zip", "2-3")

公式ドキュメントは以下。

docs.stirlingpdf.com

https://app.swaggerhub.com/apis-docs/Frooodle/Stirling-PDF/0.45.0app.swaggerhub.com

参考リンク