背景
別リポジトリ 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 ;
振る舞い
{{N}}(N は 1 桁以上の連続する数字)を全件マッチして、idMap.get(Number(N)) の値で置換する。
末尾の ?query / #fragment は token の外側にあるため自然に保持される。例: {{42}}?q=1#top で idMap.get(42) === '/about/' なら /about/?q=1#top。
URL 値そのものに ? が含まれている場合のクエリマージはしない(呼び出し側責務)。
{{name}} のような非数字 mustache は触らない。
解決できない 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/shared の package.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.json の exports に "./resolve-id-template" を追記
主要 export を src/index.ts に持っているなら再 export
受け入れテスト(spec)
PR #906 の spec とほぼ同じケースを移植してほしい。最低限以下のケースを満たすこと:
単独 token の置換: <a href="{{42}}"> + idMap = { 42 → '/about/' } → <a href="/about/">
末尾 ?query#fragment の保持: {{42}}?q=foo#top → /about/?q=foo#top
未解決 id は token をそのまま残し onUnresolved を呼ぶ
複数トークン(重複含む)の置換: {{10}}-{{20}}-{{10}} → /x/-/y/-/x/
非数字 mustache は触らない: {{name}} はそのまま、{{42}} だけ置換
id 0 を実キーとして扱う(falsy zero と undefined を区別する)
token のない HTML はそのまま返す
同じ未解決 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 ケースで明示しておく。
公開後のフォローアップ
@d-zero/shared を新バージョンで publish(このリポジトリ)。
frontend-env 側 (d-zero-dev/frontend-env) で:
@d-zero/site-migrator の package.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/scaffold の kamado.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.json の exports を更新する際は、既存のサブパス export と同じ JSON 構造を守る(import / types のペア)。
テストは既存の spec ファイルと同じフレームワーク・記法に揃える。describe / test / expect のスタイル、JSDoc/コメント方針は他ファイルを参照。
tsc --build でビルドが通ること。
「customer 名」「案件名」「案件固有ディレクトリ名」は absolutely 書かない。テスト URL は example.com + 中立名(about、news、products 等)。
背景
別リポジトリ
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/site-migratorへのresolveIdTemplate暫定追加ゴール
@d-zero/shared/resolve-id-templateサブパスとして純関数resolveIdTemplateを export する。実装は I/O ゼロ、外部依存ゼロ、純文字列処理だけで完結する。API 仕様
型
振る舞い
{{N}}(N は 1 桁以上の連続する数字)を全件マッチして、idMap.get(Number(N))の値で置換する。?query/#fragmentは token の外側にあるため自然に保持される。例:{{42}}?q=1#topでidMap.get(42) === '/about/'なら/about/?q=1#top。?が含まれている場合のクエリマージはしない(呼び出し側責務)。{{name}}のような非数字 mustache は触らない。{{N}}のまま残し、onUnresolved(N)を都度呼ぶ(重複抑制は呼び出し側の責務)。参考実装(PR #906 の暫定版、ほぼそのまま移植できる)
ファイル配置(このリポジトリの既存パターンに合わせる)
@d-zero/sharedのpackage.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.jsonのexportsに"./resolve-id-template"を追記src/index.tsに持っているなら再 export受け入れテスト(spec)
PR #906 の spec とほぼ同じケースを移植してほしい。最低限以下のケースを満たすこと:
<a href="{{42}}">+idMap = { 42 → '/about/' }→<a href="/about/">?query#fragmentの保持:{{42}}?q=foo#top→/about/?q=foo#toponUnresolvedを呼ぶ{{10}}-{{20}}-{{10}}→/x/-/y/-/x/{{name}}はそのまま、{{42}}だけ置換onUnresolvedも回数分呼ぶ既知の論点(PR レビューで上がっている)
これらは shared 側で対応するか、呼び出し側で対応するか判断してほしい。本イシューでは「shared では純関数として最小実装に留め、各論点は呼び出し側で対応する」前提でスコープを切っている。
Number.MAX_SAFE_INTEGERを超える桁数で精度欠落Number.isSafeIntegerチェックを呼び出し側で。id: "10000"(文字列)frontmatter の扱いCompilableFile.urlはパス文字列で属性メタ文字を含まない前提だが、外部入力を受ける呼び出し側はエスケープ必須)。{{1}}{{1}}の境界公開後のフォローアップ
@d-zero/sharedを新バージョンで publish(このリポジトリ)。d-zero-dev/frontend-env) で:@d-zero/site-migratorのpackage.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')。@d-zero/scaffoldのkamado.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.jsonのexportsを更新する際は、既存のサブパス export と同じ JSON 構造を守る(import/typesのペア)。describe/test/expectのスタイル、JSDoc/コメント方針は他ファイルを参照。tsc --buildでビルドが通ること。example.com+ 中立名(about、news、products等)。