背景
別リポジトリ 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;
振る舞い
{{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 #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 #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等)。