Skip to content

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

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(このリポジトリ)側でトラックする。

関連:

ゴール

@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 feat(page-cluster): landmark-based chrome extraction and clustering pipeline #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

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions