Skip to content

feat(shared): add resolve-id-template utility for resolving page-id template tokens #884

@YusukeHirao

Description

@YusukeHirao

背景

別リポジトリ d-zero-dev/frontend-env@d-zero/site-migrator が、移植元サイトの内部ページ参照を後段ビルドツール用の {{<id>}} テンプレート token で置き換える rewritePageRefs を持っている(packages/@d-zero/site-migrator/src/page-extractor/rewrite-page-refs.ts)。これに対応する解決側ユーティリティ resolveIdTemplate を、PR d-zero-dev/frontend-env#906 で site-migrator 側にも実装した。

ただし @d-zero/site-migrator@nitpicker/crawler 経由で puppeteer(Chromium 100MB+ の postinstall)を引きずる重量級パッケージで、後段の @d-zero/scaffold(kamado テンプレート)から @d-zero/site-migrator を依存させると、scaffold を使う全プロジェクトに puppeteer が伝播してしまう。site-migrator の他 API を一切使わない純文字列処理のためにこの代償は割が悪い。

そのため、純関数 resolveIdTemplate@d-zero/shared に置き、site-migrator と scaffold の両方からそこを参照する形にしたい。本イシューはその受け入れ作業を @d-zero/shared(このリポジトリ)側でトラックする。

関連:

  • frontend-env PR #906 — @d-zero/site-migrator への resolveIdTemplate 暫定追加
  • frontend-env Issue #907 — scaffold 統合と依存膨張回避の方針整理(このイシューの親)

ゴール

@d-zero/shared/resolve-id-template サブパスとして純関数 resolveIdTemplate を export する。実装は I/O ゼロ、外部依存ゼロ、純文字列処理だけで完結する。

API 仕様

export interface ResolveIdTemplateOptions {
	/** HTML(または任意のテキスト)。`{{<id>}}` token を含み得る。 */
	readonly html: string;
	/** ページ id から解決後 URL へのマップ。 */
	readonly idMap: ReadonlyMap<number, string>;
	/**
	 * 未解決 token に遭遇した時に呼ぶコールバック。token はそのまま残し、
	 * このコールバックを通じて警告/収集/例外化を呼び出し側に委ねる。
	 */
	readonly onUnresolved?: (id: number) => void;
}

export function resolveIdTemplate(options: ResolveIdTemplateOptions): string;

振る舞い

  1. {{N}}(N は 1 桁以上の連続する数字)を全件マッチして、idMap.get(Number(N)) の値で置換する。
  2. 末尾の ?query / #fragment は token の外側にあるため自然に保持される。例: {{42}}?q=1#topidMap.get(42) === '/about/' なら /about/?q=1#top
  3. URL 値そのものに ? が含まれている場合のクエリマージはしない(呼び出し側責務)。
  4. {{name}} のような非数字 mustache は触らない。
  5. 解決できない id は {{N}} のまま残し、onUnresolved(N) を都度呼ぶ(重複抑制は呼び出し側の責務)。

参考実装(PR #906 の暫定版、ほぼそのまま移植できる)

const TOKEN_RE = /\{\{(\d+)\}\}/g;

export function resolveIdTemplate(options: ResolveIdTemplateOptions): string {
	const { html, idMap, onUnresolved } = options;
	return html.replaceAll(TOKEN_RE, (match, idDigits: string) => {
		const id = Number(idDigits);
		const url = idMap.get(id);
		if (url === undefined) {
			onUnresolved?.(id);
			return match;
		}
		return url;
	});
}

ファイル配置(このリポジトリの既存パターンに合わせる)

@d-zero/sharedpackage.json は 1 関数 1 ファイル + サブパス export 方式を採用済み(例: ./remove-matches./sort/path./encode-resource-path 等)。本ユーティリティも同様に:

  • packages/@d-zero/shared/src/resolve-id-template.ts — 実装
  • packages/@d-zero/shared/src/resolve-id-template.spec.ts — テスト(vitest または既存のテストフレームワークに合わせる)
  • packages/@d-zero/shared/package.jsonexports"./resolve-id-template" を追記
  • 主要 export を src/index.ts に持っているなら再 export

受け入れテスト(spec)

PR #906 の spec とほぼ同じケースを移植してほしい。最低限以下のケースを満たすこと:

  1. 単独 token の置換: <a href="{{42}}"> + idMap = { 42 → '/about/' }<a href="/about/">
  2. 末尾 ?query#fragment の保持: {{42}}?q=foo#top/about/?q=foo#top
  3. 未解決 id は token をそのまま残し onUnresolved を呼ぶ
  4. 複数トークン(重複含む)の置換: {{10}}-{{20}}-{{10}}/x/-/y/-/x/
  5. 非数字 mustache は触らない: {{name}} はそのまま、{{42}} だけ置換
  6. id 0 を実キーとして扱う(falsy zero と undefined を区別する)
  7. token のない HTML はそのまま返す
  8. 同じ未解決 id が複数回現れたら onUnresolved も回数分呼ぶ

既知の論点(PR レビューで上がっている)

これらは shared 側で対応するか、呼び出し側で対応するか判断してほしい。本イシューでは「shared では純関数として最小実装に留め、各論点は呼び出し側で対応する」前提でスコープを切っている。

論点 スコープ判断(提案)
Number.MAX_SAFE_INTEGER を超える桁数で精度欠落 呼び出し側(assignPageIds 由来なら起こり得ない)。明示的に検知したいなら Number.isSafeInteger チェックを呼び出し側で。
同一 id 重複時の警告 呼び出し側(id を Map に詰める時点で検知)。
id: "10000"(文字列)frontmatter の扱い 呼び出し側(Map に詰める段階で型を正規化)。
URL 値の HTML 属性エスケープ 呼び出し側(kamado の CompilableFile.url はパス文字列で属性メタ文字を含まない前提だが、外部入力を受ける呼び出し側はエスケープ必須)。
隣接 token {{1}}{{1}} の境界 仕様カバー範囲。spec ケースで明示しておく。

公開後のフォローアップ

  1. @d-zero/shared を新バージョンで publish(このリポジトリ)。
  2. frontend-env 側 (d-zero-dev/frontend-env) で:
    • @d-zero/site-migratorpackage.json@d-zero/shared バージョンをバンプ。
    • src/page-extractor/resolve-id-template.ts@d-zero/shared/resolve-id-template の再 export に置き換え(または該当ファイルを削除して src/index.ts から直接 export { resolveIdTemplate } from '@d-zero/shared/resolve-id-template')。
    • 既存 spec はそのまま残しても良いし、shared 側に重複するなら削除しても良い。
    • @d-zero/scaffoldkamado.config.ts でも @d-zero/shared/resolve-id-template を import して使う(Issue #907 で別途実装)。

AI エージェントが引き継ぐ際のヒント

  • このリポジトリ(d-zero-dev/tools)の packages/@d-zero/shared/ 配下の他ファイル(例: src/encode-resource-path.ts)の作法に揃えること。
  • 実装は packages/@d-zero/shared/src/resolve-id-template.ts の 1 ファイル。replaceAll + 正規表現の純文字列処理。20 行以内に収まる。
  • package.jsonexports を更新する際は、既存のサブパス export と同じ JSON 構造を守る(import / types のペア)。
  • テストは既存の spec ファイルと同じフレームワーク・記法に揃える。describe / test / expect のスタイル、JSDoc/コメント方針は他ファイルを参照。
  • tsc --build でビルドが通ること。
  • 「customer 名」「案件名」「案件固有ディレクトリ名」は absolutely 書かない。テスト URL は example.com + 中立名(aboutnewsproducts 等)。

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions