ksk-design-system 1.55.0 → 1.57.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/AGENTS.md +8 -0
  2. package/CLAUDE.md +8 -0
  3. package/DESIGN.md +4 -0
  4. package/MIGRATION.md +43 -0
  5. package/bin/check-migration.js +517 -0
  6. package/bin/init.js +3 -2
  7. package/bin/lint.js +21 -1
  8. package/contracts/components.json +105 -21
  9. package/contracts/deprecations.json +92 -0
  10. package/contracts/token-hex-cache.json +1 -1
  11. package/dist/index.js +4739 -4409
  12. package/dist/native/ui.js +1745 -1603
  13. package/dist/{native-VkkXnB0e.js → native-DlR32_Lk.js} +8 -0
  14. package/dist/native.js +1 -1
  15. package/dist/types/components/icons/info-circle.d.ts +23 -0
  16. package/dist/types/components/patterns/admin/data-table.d.ts +53 -4
  17. package/dist/types/components/patterns/chip-selector.d.ts +23 -7
  18. package/dist/types/components/patterns/chip.d.ts +1 -1
  19. package/dist/types/components/patterns/list-item.d.ts +35 -2
  20. package/dist/types/components/patterns/quick-action-grid.d.ts +25 -3
  21. package/dist/types/components/patterns/search-bar.d.ts +9 -1
  22. package/dist/types/components/patterns/swipe-row.d.ts +7 -1
  23. package/dist/types/components/ui/alert-dialog.d.ts +16 -3
  24. package/dist/types/components/ui/card.d.ts +1 -1
  25. package/dist/types/components/ui/dialog.d.ts +54 -6
  26. package/dist/types/components/ui/dropdown-menu.d.ts +1 -1
  27. package/dist/types/components/ui/pagination.d.ts +21 -4
  28. package/dist/types/components/ui/portal-container.d.ts +50 -0
  29. package/dist/types/components/ui/sheet.d.ts +52 -27
  30. package/dist/types/index.d.ts +13 -3
  31. package/dist/types/lib/build-page-items.d.ts +22 -0
  32. package/dist/types/lib/modal-stack.d.ts +45 -0
  33. package/dist/types/lib/title-level.d.ts +60 -0
  34. package/dist/types/native/components/AppHeader.d.ts +9 -1
  35. package/dist/types/native/components/Card.d.ts +7 -1
  36. package/dist/types/native/components/ChipSelector.d.ts +22 -1
  37. package/dist/types/native/components/Dialog.d.ts +17 -1
  38. package/dist/types/native/components/ListItem.d.ts +28 -1
  39. package/dist/types/native/components/QuickActionGrid.d.ts +22 -2
  40. package/dist/types/native/components/Sheet.d.ts +10 -0
  41. package/dist/types/native/components/SwipeRow.d.ts +23 -1
  42. package/dist/types/native/components/index.d.ts +4 -4
  43. package/dist/types/native/index.d.ts +2 -0
  44. package/dist/types/native/safe-area.d.ts +62 -0
  45. package/dist/types/native/theme/SafeAreaInsetsProvider.d.ts +18 -0
  46. package/dist/types/tokens/native/scales.d.ts +8 -0
  47. package/package.json +5 -2
  48. package/scripts/codemod/README.md +10 -8
  49. package/src/components/COMPONENT_LOOKUP.md +11 -10
  50. package/src/preset.css +10 -4
  51. package/src/styles/sheet-keyboard.css +34 -4
  52. package/src/styles/source-safelist.css +21 -3
  53. package/tokens.json +3 -1
  54. package/scripts/codemod/check-migration.mjs +0 -129
package/AGENTS.md CHANGED
@@ -33,6 +33,7 @@ UI を書く前に必ず確認すること:
33
33
  - [ ] アニメーションは `duration-[var(--Motion-Duration-*)]` / `ease-[var(--Motion-Easing-*)]` か(`duration-200` や生 `cubic-bezier` の直書きは禁止。トークン参照でないと `prefers-reduced-motion` の一括制御から漏れる)
34
34
  - [ ] 重なり順は `z-[var(--Z-*)]` か(`z-50` 一律だと Portal のマウント順で勝敗が決まる。`z-10` / `z-20` のコンポーネント内部の重なりは対象外。順序は DESIGN.md の Layering 節)
35
35
  - [ ] アイコンは `iconsax-reactjs` か(`lucide-react` / `heroicons` は使わない)
36
+ - [ ] **選んだアイコンの「絵」を実際に見たか**。iconsax は名前と絵が一致しないものがある。`Check` は小切手(checkbook)でチェックマークではない(PR #349 で出荷直前まで残った)。チェックマークは `TickSquare` / `TickCircle`。また **"i" のグリフは iconsax に存在せず**、`InfoCircle` / `Information` / `Danger` / `Warning2` はすべて感嘆符「!」の器違いなので、情報アイコンは DS 自前の `InfoCircleIcon` を使う。名前で選んだら描画結果を目視し、意味が変わると困る箇所は path で固定するテストを書く <!-- docs-drift-ignore: Check TickSquare TickCircle InfoCircle Information Danger Warning2 InfoCircleIcon -->
36
37
  - [ ] 生タグ(`<button>` / `<input>` / `<a href>`)でなく DS コンポーネントを使ったか
37
38
  - [ ] CSS でベンダープレフィックス併記する場合、**`-webkit-` を先・標準形を後**に書いたか(消費側の minifier が同一プロパティとして dedupe し後勝ちのみ残すため。逆順だと Firefox で静かに無効化。`node scripts/check-prefix-order.mjs` が CI で検出)
38
39
  - [ ] flex 行(flex-col でない flex)で shrink-0 の兄弟と可変テキストを並べるとき、テキスト側に `flex-1`(+ 必要なら `min-w-[...]` 下限)を付けたか(`min-w-0` だけだと 1 文字ずつ折り返すまで潰れる。issue #293。`node scripts/check-flex-shrink.mjs` が CI で検出、例外は `ksk-lint-ignore KFX001 -- 理由`)
@@ -219,6 +220,7 @@ Brand色を差し替え(10行)→ Primitive Layer → Semantic Layer → Bri
219
220
  | **src/components/COMPONENT_LOOKUP.md** | 全コンポーネントのバリアント・インポートパス一覧(自動生成) |
220
221
  | **DESIGN.md** | AI エージェント向け視覚言語サマリ(トークン+意図・voice・motion) |
221
222
  | **contracts/screen-patterns.json** | 画面実装前にどのシェル/パターンを使うかを決める decisionTree・crudMatrix |
223
+ | **contracts/deprecations.json** | 非推奨 API の正本台帳(移行先・削除予定。MIGRATION.md の一覧節と check-migration CLI の入力) |
222
224
  | **contracts/composition.json** | 選んだパターン内部の並べ方(骨格構造・余白リズム・カード階層・テキスト階層・CTA優先度) |
223
225
 
224
226
  ---
@@ -269,6 +271,12 @@ npm run generate:lookup
269
271
  # DESIGN.md contract 検査
270
272
  npm run lint:design
271
273
 
274
+ # 非推奨 API 台帳の整合検査(台帳 ⇔ 実ソースの @deprecated JSDoc)
275
+ npm run lint:deprecations
276
+
277
+ # MIGRATION.md の「非推奨 API 一覧」節を台帳から再生成
278
+ npm run generate:migration-doc
279
+
272
280
  # @source safelist 再生成(新しい Tailwind クラスを使ったら実行)
273
281
  npm run generate:safelist
274
282
 
package/CLAUDE.md CHANGED
@@ -33,6 +33,7 @@ UI を書く前に必ず確認すること:
33
33
  - [ ] アニメーションは `duration-[var(--Motion-Duration-*)]` / `ease-[var(--Motion-Easing-*)]` か(`duration-200` や生 `cubic-bezier` の直書きは禁止。トークン参照でないと `prefers-reduced-motion` の一括制御から漏れる)
34
34
  - [ ] 重なり順は `z-[var(--Z-*)]` か(`z-50` 一律だと Portal のマウント順で勝敗が決まる。`z-10` / `z-20` のコンポーネント内部の重なりは対象外。順序は DESIGN.md の Layering 節)
35
35
  - [ ] アイコンは `iconsax-reactjs` か(`lucide-react` / `heroicons` は使わない)
36
+ - [ ] **選んだアイコンの「絵」を実際に見たか**。iconsax は名前と絵が一致しないものがある。`Check` は小切手(checkbook)でチェックマークではない(PR #349 で出荷直前まで残った)。チェックマークは `TickSquare` / `TickCircle`。また **"i" のグリフは iconsax に存在せず**、`InfoCircle` / `Information` / `Danger` / `Warning2` はすべて感嘆符「!」の器違いなので、情報アイコンは DS 自前の `InfoCircleIcon` を使う。名前で選んだら描画結果を目視し、意味が変わると困る箇所は path で固定するテストを書く <!-- docs-drift-ignore: Check TickSquare TickCircle InfoCircle Information Danger Warning2 InfoCircleIcon -->
36
37
  - [ ] 生タグ(`<button>` / `<input>` / `<a href>`)でなく DS コンポーネントを使ったか
37
38
  - [ ] CSS でベンダープレフィックス併記する場合、**`-webkit-` を先・標準形を後**に書いたか(消費側の minifier が同一プロパティとして dedupe し後勝ちのみ残すため。逆順だと Firefox で静かに無効化。`node scripts/check-prefix-order.mjs` が CI で検出)
38
39
  - [ ] flex 行(flex-col でない flex)で shrink-0 の兄弟と可変テキストを並べるとき、テキスト側に `flex-1`(+ 必要なら `min-w-[...]` 下限)を付けたか(`min-w-0` だけだと 1 文字ずつ折り返すまで潰れる。issue #293。`node scripts/check-flex-shrink.mjs` が CI で検出、例外は `ksk-lint-ignore KFX001 -- 理由`)
@@ -219,6 +220,7 @@ Brand色を差し替え(10行)→ Primitive Layer → Semantic Layer → Bri
219
220
  | **src/components/COMPONENT_LOOKUP.md** | 全コンポーネントのバリアント・インポートパス一覧(自動生成) |
220
221
  | **DESIGN.md** | AI エージェント向け視覚言語サマリ(トークン+意図・voice・motion) |
221
222
  | **contracts/screen-patterns.json** | 画面実装前にどのシェル/パターンを使うかを決める decisionTree・crudMatrix |
223
+ | **contracts/deprecations.json** | 非推奨 API の正本台帳(移行先・削除予定。MIGRATION.md の一覧節と check-migration CLI の入力) |
222
224
  | **contracts/composition.json** | 選んだパターン内部の並べ方(骨格構造・余白リズム・カード階層・テキスト階層・CTA優先度) |
223
225
 
224
226
  ---
@@ -269,6 +271,12 @@ npm run generate:lookup
269
271
  # DESIGN.md contract 検査
270
272
  npm run lint:design
271
273
 
274
+ # 非推奨 API 台帳の整合検査(台帳 ⇔ 実ソースの @deprecated JSDoc)
275
+ npm run lint:deprecations
276
+
277
+ # MIGRATION.md の「非推奨 API 一覧」節を台帳から再生成
278
+ npm run generate:migration-doc
279
+
272
280
  # @source safelist 再生成(新しい Tailwind クラスを使ったら実行)
273
281
  npm run generate:safelist
274
282
 
package/DESIGN.md CHANGED
@@ -171,6 +171,10 @@ KSK の必須正本・publish 依存にせず、KSK 固有の multi-theme / nati
171
171
  影は5段(`--shadow-sm/md/lg/dialog/tooltip`)。面は md、浮く要素(dropdown/popover)は lg、
172
172
  モーダルは dialog。**境界は影+1px罫線**で表現し、濃い影の多用は避ける。
173
173
 
174
+ 横スクロール中の固定列だけは方向付きの影を使う(`--shadow-sticky-inline-start` /
175
+ `--shadow-sticky-inline-end`)。命名は論理方向で、inline-start = 行の先頭側に固定された列が
176
+ 末尾方向へ落とす影。生の `rgba()` を直書きせず、必ずこのトークンを参照する。
177
+
174
178
  ## Shapes
175
179
 
176
180
  角丸はトークン化(ベタ書き禁止)。**面 < モーダル < シート**の順で丸くなる。
package/MIGRATION.md CHANGED
@@ -3,6 +3,40 @@
3
3
  メジャーバージョン間の移行ガイド。
4
4
  patch / minor は原則破壊変更なし、自動アップグレード可(例外: **v1.34.0 で npm パッケージ名を変更**。import の置換が必要。下記参照)。
5
5
 
6
+ <!-- deprecations:start(自動生成・手で編集しない) -->
7
+
8
+ ## 非推奨 API 一覧
9
+
10
+ 正本は [`contracts/deprecations.json`](./contracts/deprecations.json)。この節はそこから生成しています。
11
+
12
+ 消費側での残存件数は次のコマンドで数えられます(read-only・残件があれば exit 1):
13
+
14
+ ```bash
15
+ npx ksk-ds check-migration ./src
16
+ ```
17
+
18
+ | API | 使われ方 | 移行先 | 非推奨にした版 | 削除予定 |
19
+ | --- | --- | --- | --- | --- |
20
+ | `ListItem.interactive` | `<ListItem interactive>` | href または onClick を ListItem 自体へ渡す | 1.46.0 | v2.0.0 |
21
+ | `ChipSelector.multiple` | `<ChipSelector multiple>` | selectionMode(multiple={false} は selectionMode="single"、multiple は selectionMode="multiple") | unreleased | v2.0.0 |
22
+ | `PillToggle.onValueChange` | `<PillToggle onValueChange>` | onChange | 1.49.0 | v2.0.0 |
23
+ | `ProductCard.deliveryLabel` | `<ProductCard deliveryLabel>` | なし(v1.30.0 以降は描画されないため、渡している箇所は削除する) | 1.30.1 | v2.0.0 |
24
+ | `Progress.tone` | `<Progress tone>` | variant | 1.40.1 | v2.0.0 |
25
+
26
+ 各エントリの補足:
27
+
28
+ - **ListItem.interactive**(issue #207) — 外側の Link / button でラップする既存コードの視覚互換用に残している。 実装: src/components/patterns/list-item.tsx
29
+ - **ChipSelector.multiple**(issue #352) — 既定が true(複数選択)で「渡し忘れると静かに壊れる側」に倒れているため新規実装では使わない。 実装: src/components/patterns/chip-selector.tsx / src/native/components/ChipSelector.tsx
30
+ - **PillToggle.onValueChange**(issue #264) — 後方互換エイリアス。onChange を併せて渡した場合は onChange が優先される。 実装: src/components/ui/pill-toggle.tsx
31
+ - **ProductCard.deliveryLabel** — 既存 consumer の型互換のためだけに残している no-op prop。 実装: src/components/patterns/commerce/product-card.tsx
32
+ - **Progress.tone** — React Native 版のみ。既存 RN consumer 向けの互換。 実装: src/native/components/Progress.tsx
33
+
34
+ 削除は「全消費リポで `check-migration` の残件が 0」を条件に、`削除予定` のメジャーリリースで行います。
35
+
36
+ <!-- deprecations:end -->
37
+
38
+ ---
39
+
6
40
  ## v2.0 (未リリース)
7
41
 
8
42
  まだメジャー破壊変更の予定はなし。
@@ -133,6 +167,15 @@ export default [
133
167
 
134
168
  これで codemod が拾えなかった旧 API の使用を検出できる。
135
169
 
170
+ ### Step 3.5. 非推奨 API の残存を数える
171
+
172
+ ```bash
173
+ npx ksk-ds check-migration ./src
174
+ ```
175
+
176
+ `contracts/deprecations.json` の非推奨 API が何件残っているかを、識別子別・ファイル別に出す
177
+ (read-only・残件があれば exit 1 なので CI にも置ける)。
178
+
136
179
  ### Step 4. 動作確認
137
180
 
138
181
  ```bash
@@ -0,0 +1,517 @@
1
+ #!/usr/bin/env node
2
+
3
+ // ksk-ds check-migration
4
+ // contracts/deprecations.json(正本台帳)に載っている非推奨 API が
5
+ // consumer のソースにどれだけ残っているかを数える read-only CLI。
6
+ //
7
+ // npx ksk-ds check-migration ./src
8
+ //
9
+ // 検出は TypeScript の AST で行う(正規表現ではない)。
10
+ // - kind=prop : DS 由来のコンポーネントに付いた JSX 属性
11
+ // - kind=export: DS 由来の import 指定子 / re-export 指定子
12
+ // コメント・文字列リテラル中の同名は AST 上そもそも識別子ではないため誤検出しない。
13
+ //
14
+ // 「DS 由来」の判定はローカル束縛を追う:
15
+ // 1. ksk-design-system(旧名 @ksk/design-system・サブパス含む)からの import
16
+ // 2. 相対 import 先のモジュールが DS からの re-export で同名を出している場合
17
+ // (移行期に consumer が置く互換バレルを取りこぼさないため。import だけでなく
18
+ // re-export も残件として数える、が要件)
19
+
20
+ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs"
21
+ import { dirname, extname, isAbsolute, join, relative, resolve } from "node:path"
22
+ import ts from "typescript"
23
+
24
+ const SOURCE_EXTENSIONS = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"])
25
+ const IGNORED_DIRECTORIES = new Set([
26
+ ".git",
27
+ ".next",
28
+ "build",
29
+ "coverage",
30
+ "dist",
31
+ "node_modules",
32
+ "storybook-static",
33
+ ])
34
+
35
+ /** DS のパッケージ名(改名前も拾う)。サブパス import も対象。 */
36
+ const DS_PACKAGE_RE = /^(?:ksk-design-system|@ksk\/design-system)(?:\/.*)?$/
37
+
38
+ export function isDsSpecifier(specifier) {
39
+ return DS_PACKAGE_RE.test(specifier)
40
+ }
41
+
42
+ // ─── 台帳 ───────────────────────────────────────────────────
43
+
44
+ export function loadDeprecations(pkgRoot) {
45
+ const ledgerPath = resolve(pkgRoot, "contracts/deprecations.json")
46
+ if (!existsSync(ledgerPath)) {
47
+ throw new Error(`contracts/deprecations.json が見つかりません: ${ledgerPath}`)
48
+ }
49
+ const ledger = JSON.parse(readFileSync(ledgerPath, "utf8"))
50
+ return ledger.deprecations ?? []
51
+ }
52
+
53
+ // ─── ファイル収集 ───────────────────────────────────────────
54
+
55
+ export function collectSourceFiles(target) {
56
+ const results = []
57
+ const stat = statSync(target)
58
+ if (stat.isFile()) return SOURCE_EXTENSIONS.has(extname(target)) ? [target] : []
59
+
60
+ const stack = [target]
61
+ while (stack.length > 0) {
62
+ const current = stack.pop()
63
+ for (const entry of readdirSync(current, { withFileTypes: true })) {
64
+ if (entry.name.startsWith(".")) continue
65
+ const full = join(current, entry.name)
66
+ if (entry.isDirectory()) {
67
+ if (IGNORED_DIRECTORIES.has(entry.name)) continue
68
+ stack.push(full)
69
+ continue
70
+ }
71
+ if (SOURCE_EXTENSIONS.has(extname(entry.name))) results.push(full)
72
+ }
73
+ }
74
+ return results.sort()
75
+ }
76
+
77
+ function parse(file, source) {
78
+ return ts.createSourceFile(file, source, ts.ScriptTarget.Latest, true, scriptKind(file))
79
+ }
80
+
81
+ function scriptKind(file) {
82
+ const ext = extname(file)
83
+ if (ext === ".tsx" || ext === ".jsx") return ts.ScriptKind.TSX
84
+ if (ext === ".js" || ext === ".mjs" || ext === ".cjs") return ts.ScriptKind.JS
85
+ return ts.ScriptKind.TS
86
+ }
87
+
88
+ // ─── 相対 import の解決 ─────────────────────────────────────
89
+
90
+ const RESOLVE_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]
91
+
92
+ function resolveRelative(fromFile, specifier) {
93
+ const base = resolve(dirname(fromFile), specifier)
94
+ const stripped = base.replace(/\.(m|c)?jsx?$/, "")
95
+ const candidates = [
96
+ base,
97
+ ...RESOLVE_EXTENSIONS.map((ext) => `${stripped}${ext}`),
98
+ ...RESOLVE_EXTENSIONS.map((ext) => join(base, `index${ext}`)),
99
+ ]
100
+ for (const candidate of candidates) {
101
+ if (existsSync(candidate) && statSync(candidate).isFile()) return candidate
102
+ }
103
+ return null
104
+ }
105
+
106
+ // ─── モジュールごとの「DS 由来 re-export」解析 ──────────────
107
+
108
+ /**
109
+ * ファイルが「その名前を DS 由来として外へ出している」かを判定できるようにする。
110
+ *
111
+ * 返り値:
112
+ * { names: Set<string>, wildcardFromDs: boolean, wildcardRelative: string[] }
113
+ */
114
+ function analyzeReExports(file, sourceFile) {
115
+ const names = new Set()
116
+ const relativeReExports = [] // { local: string|null, exported: string, from: string }
117
+ let wildcardFromDs = false
118
+ const wildcardRelative = []
119
+ const dsImportedLocals = new Map() // local 名 → 元の export 名(DS からの import)
120
+
121
+ for (const statement of sourceFile.statements) {
122
+ if (ts.isImportDeclaration(statement) && ts.isStringLiteral(statement.moduleSpecifier)) {
123
+ const spec = statement.moduleSpecifier.text
124
+ if (!isDsSpecifier(spec)) continue
125
+ const bindings = statement.importClause?.namedBindings
126
+ if (bindings && ts.isNamedImports(bindings)) {
127
+ for (const element of bindings.elements) {
128
+ dsImportedLocals.set(
129
+ element.name.text,
130
+ (element.propertyName ?? element.name).text,
131
+ )
132
+ }
133
+ }
134
+ continue
135
+ }
136
+
137
+ if (!ts.isExportDeclaration(statement)) continue
138
+ const spec =
139
+ statement.moduleSpecifier && ts.isStringLiteral(statement.moduleSpecifier)
140
+ ? statement.moduleSpecifier.text
141
+ : null
142
+
143
+ // export * from "..."
144
+ if (!statement.exportClause) {
145
+ if (spec && isDsSpecifier(spec)) wildcardFromDs = true
146
+ else if (spec && spec.startsWith(".")) wildcardRelative.push(spec)
147
+ continue
148
+ }
149
+ if (!ts.isNamedExports(statement.exportClause)) continue
150
+
151
+ for (const element of statement.exportClause.elements) {
152
+ const original = (element.propertyName ?? element.name).text
153
+ if (spec && isDsSpecifier(spec)) {
154
+ names.add(original)
155
+ names.add(element.name.text)
156
+ } else if (spec && spec.startsWith(".")) {
157
+ relativeReExports.push({ original, exported: element.name.text, from: spec })
158
+ } else if (!spec && dsImportedLocals.has(original)) {
159
+ // import { X } from "ksk-design-system"; export { X }
160
+ names.add(dsImportedLocals.get(original))
161
+ names.add(element.name.text)
162
+ }
163
+ }
164
+ }
165
+
166
+ return { file, names, wildcardFromDs, wildcardRelative, relativeReExports }
167
+ }
168
+
169
+ /**
170
+ * モジュールが name を DS 由来で re-export しているかを、相対経路を辿って判定する。
171
+ * 循環は visited で打ち切る。
172
+ */
173
+ function reExportsFromDs(file, name, modules, visited = new Set()) {
174
+ if (visited.has(file)) return false
175
+ visited.add(file)
176
+ const info = modules.get(file)
177
+ if (!info) return false
178
+ if (info.wildcardFromDs) return true
179
+ if (info.names.has(name)) return true
180
+ for (const entry of info.relativeReExports) {
181
+ if (entry.exported !== name) continue
182
+ const target = resolveRelative(file, entry.from)
183
+ if (target && reExportsFromDs(target, entry.original, modules, visited)) return true
184
+ }
185
+ for (const spec of info.wildcardRelative) {
186
+ const target = resolveRelative(file, spec)
187
+ if (target && reExportsFromDs(target, name, modules, visited)) return true
188
+ }
189
+ return false
190
+ }
191
+
192
+ // ─── 1 ファイルの検査 ───────────────────────────────────────
193
+
194
+ /**
195
+ * ファイル内の「DS 由来として束縛されたローカル名 → DS の export 名」を作る。
196
+ */
197
+ function dsBindings(file, sourceFile, modules) {
198
+ const bindings = new Map()
199
+ const namespaces = new Set()
200
+
201
+ for (const statement of sourceFile.statements) {
202
+ if (!ts.isImportDeclaration(statement)) continue
203
+ if (!ts.isStringLiteral(statement.moduleSpecifier)) continue
204
+ const spec = statement.moduleSpecifier.text
205
+ const fromDs = isDsSpecifier(spec)
206
+ const relativeTarget = spec.startsWith(".") ? resolveRelative(file, spec) : null
207
+ if (!fromDs && !relativeTarget) continue
208
+
209
+ const clause = statement.importClause
210
+ if (!clause) continue
211
+ const named = clause.namedBindings
212
+
213
+ if (named && ts.isNamespaceImport(named)) {
214
+ if (fromDs) namespaces.add(named.name.text)
215
+ continue
216
+ }
217
+ if (named && ts.isNamedImports(named)) {
218
+ for (const element of named.elements) {
219
+ const original = (element.propertyName ?? element.name).text
220
+ if (fromDs) {
221
+ bindings.set(element.name.text, original)
222
+ } else if (relativeTarget && reExportsFromDs(relativeTarget, original, modules)) {
223
+ bindings.set(element.name.text, original)
224
+ }
225
+ }
226
+ }
227
+ }
228
+
229
+ return { bindings, namespaces }
230
+ }
231
+
232
+ /** `<Foo.Bar prop />` の基底名(Foo)と全体名を返す */
233
+ function tagNames(tagName) {
234
+ if (ts.isIdentifier(tagName)) return { base: tagName.text, full: tagName.text }
235
+ if (ts.isPropertyAccessExpression(tagName)) {
236
+ let node = tagName
237
+ while (ts.isPropertyAccessExpression(node.expression)) node = node.expression
238
+ const base = ts.isIdentifier(node.expression) ? node.expression.text : null
239
+ return { base, full: tagName.getText() }
240
+ }
241
+ return { base: null, full: null }
242
+ }
243
+
244
+ function scanFile(file, sourceFile, deprecations, modules) {
245
+ const { bindings, namespaces } = dsBindings(file, sourceFile, modules)
246
+ const findings = []
247
+
248
+ const propEntries = deprecations.filter((d) => d.kind === "prop")
249
+ const exportEntries = deprecations.filter((d) => d.kind === "export")
250
+
251
+ const lineOf = (node) =>
252
+ sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1
253
+
254
+ // ── kind=prop: JSX 属性 ──────────────────────────────
255
+ const visitJsx = (node) => {
256
+ if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) {
257
+ const { base, full } = tagNames(node.tagName)
258
+ if (base) {
259
+ // Foo / DS.Foo のどちらでも、DS の export 名に解決した名前で照合する
260
+ let dsName = null
261
+ if (bindings.has(base) && full === base) dsName = bindings.get(base)
262
+ else if (namespaces.has(base) && full) dsName = full.slice(base.length + 1)
263
+
264
+ if (dsName) {
265
+ for (const attr of node.attributes.properties) {
266
+ if (!ts.isJsxAttribute(attr) || !ts.isIdentifier(attr.name)) continue
267
+ for (const entry of propEntries) {
268
+ if (entry.component !== dsName || entry.prop !== attr.name.text) continue
269
+ findings.push({ file, line: lineOf(attr), id: entry.id, usage: "prop", entry })
270
+ }
271
+ }
272
+ }
273
+ }
274
+ }
275
+ ts.forEachChild(node, visitJsx)
276
+ }
277
+ ts.forEachChild(sourceFile, visitJsx)
278
+
279
+ // ── kind=export: import / re-export ──────────────────
280
+ if (exportEntries.length > 0) {
281
+ const byIdentifier = new Map(exportEntries.map((e) => [e.identifier, e]))
282
+
283
+ for (const statement of sourceFile.statements) {
284
+ if (ts.isImportDeclaration(statement) && ts.isStringLiteral(statement.moduleSpecifier)) {
285
+ const spec = statement.moduleSpecifier.text
286
+ const relativeTarget = spec.startsWith(".") ? resolveRelative(file, spec) : null
287
+ if (!isDsSpecifier(spec) && !relativeTarget) continue
288
+ const named = statement.importClause?.namedBindings
289
+ if (!named || !ts.isNamedImports(named)) continue
290
+ for (const element of named.elements) {
291
+ const original = (element.propertyName ?? element.name).text
292
+ const entry = byIdentifier.get(original)
293
+ if (!entry) continue
294
+ if (
295
+ isDsSpecifier(spec) ||
296
+ (relativeTarget && reExportsFromDs(relativeTarget, original, modules))
297
+ ) {
298
+ findings.push({ file, line: lineOf(element), id: entry.id, usage: "import", entry })
299
+ }
300
+ }
301
+ continue
302
+ }
303
+
304
+ if (!ts.isExportDeclaration(statement)) continue
305
+ const spec =
306
+ statement.moduleSpecifier && ts.isStringLiteral(statement.moduleSpecifier)
307
+ ? statement.moduleSpecifier.text
308
+ : null
309
+ if (!statement.exportClause || !ts.isNamedExports(statement.exportClause)) continue
310
+ for (const element of statement.exportClause.elements) {
311
+ const original = (element.propertyName ?? element.name).text
312
+ const entry = byIdentifier.get(original)
313
+ if (!entry) continue
314
+ const relativeTarget = spec && spec.startsWith(".") ? resolveRelative(file, spec) : null
315
+ const fromDs =
316
+ (spec && isDsSpecifier(spec)) ||
317
+ (relativeTarget && reExportsFromDs(relativeTarget, original, modules)) ||
318
+ (!spec && bindings.has(original))
319
+ if (fromDs) {
320
+ findings.push({ file, line: lineOf(element), id: entry.id, usage: "re-export", entry })
321
+ }
322
+ }
323
+ }
324
+ }
325
+
326
+ return findings
327
+ }
328
+
329
+ // ─── 検査本体 ───────────────────────────────────────────────
330
+
331
+ /**
332
+ * @returns {{ total: number, findings: Array, byIdentifier: Array<{id, count, replacement, removeIn}>, byFile: Array<{file, count}>, filesScanned: number }}
333
+ */
334
+ export function checkMigration(target, deprecations, { cwd = process.cwd() } = {}) {
335
+ const files = collectSourceFiles(target)
336
+ const parsed = new Map()
337
+ const modules = new Map()
338
+
339
+ for (const file of files) {
340
+ const sourceFile = parse(file, readFileSync(file, "utf8"))
341
+ parsed.set(file, sourceFile)
342
+ modules.set(file, analyzeReExports(file, sourceFile))
343
+ }
344
+
345
+ const findings = []
346
+ for (const file of files) {
347
+ findings.push(...scanFile(file, parsed.get(file), deprecations, modules))
348
+ }
349
+
350
+ const counts = new Map()
351
+ for (const finding of findings) {
352
+ counts.set(finding.id, (counts.get(finding.id) ?? 0) + 1)
353
+ }
354
+ const byIdentifier = deprecations
355
+ .filter((d) => counts.has(d.id))
356
+ .map((d) => ({
357
+ id: d.id,
358
+ count: counts.get(d.id),
359
+ replacement: d.replacement,
360
+ removeIn: d.removeIn,
361
+ }))
362
+ .sort((a, b) => b.count - a.count || a.id.localeCompare(b.id))
363
+
364
+ const fileCounts = new Map()
365
+ for (const finding of findings) {
366
+ const rel = displayPath(finding.file, cwd)
367
+ fileCounts.set(rel, (fileCounts.get(rel) ?? 0) + 1)
368
+ }
369
+ const byFile = [...fileCounts.entries()]
370
+ .map(([file, count]) => ({ file, count }))
371
+ .sort((a, b) => b.count - a.count || a.file.localeCompare(b.file))
372
+
373
+ return {
374
+ total: findings.length,
375
+ findings,
376
+ byIdentifier,
377
+ byFile,
378
+ filesScanned: files.length,
379
+ }
380
+ }
381
+
382
+ function displayPath(file, cwd) {
383
+ const rel = relative(cwd, file)
384
+ return rel.startsWith("..") || isAbsolute(rel) ? file : rel
385
+ }
386
+
387
+ // ─── CLI ────────────────────────────────────────────────────
388
+
389
+ export function runCheckMigrationCli(
390
+ argv,
391
+ { cwd = process.cwd(), pkgRoot = resolve("."), deprecations } = {},
392
+ ) {
393
+ const options = parseArgs(argv)
394
+ if (options.help) {
395
+ printHelp()
396
+ return 0
397
+ }
398
+
399
+ const target = resolve(cwd, options.target)
400
+ if (!existsSync(target)) {
401
+ console.error(`検査対象が見つかりません: ${target}`)
402
+ return 1
403
+ }
404
+
405
+ let entries
406
+ try {
407
+ entries = deprecations ?? loadDeprecations(pkgRoot)
408
+ } catch (error) {
409
+ console.error(String(error.message ?? error))
410
+ return 1
411
+ }
412
+
413
+ const result = checkMigration(target, entries, { cwd })
414
+
415
+ if (options.format === "json") {
416
+ console.log(
417
+ JSON.stringify(
418
+ {
419
+ total: result.total,
420
+ filesScanned: result.filesScanned,
421
+ byIdentifier: result.byIdentifier,
422
+ byFile: result.byFile,
423
+ findings: result.findings.map((f) => ({
424
+ file: displayPath(f.file, cwd),
425
+ line: f.line,
426
+ id: f.id,
427
+ usage: f.usage,
428
+ })),
429
+ },
430
+ null,
431
+ 2,
432
+ ),
433
+ )
434
+ return result.total > 0 ? 1 : 0
435
+ }
436
+
437
+ printFindings(result, entries, cwd)
438
+ return result.total > 0 ? 1 : 0
439
+ }
440
+
441
+ function parseArgs(argv) {
442
+ let help = false
443
+ let format = "text"
444
+ let target = "./src"
445
+
446
+ for (const arg of argv) {
447
+ if (arg === "--help" || arg === "-h") {
448
+ help = true
449
+ continue
450
+ }
451
+ if (arg === "--format=json" || arg === "--json") {
452
+ format = "json"
453
+ continue
454
+ }
455
+ if (!arg.startsWith("-")) target = arg
456
+ }
457
+
458
+ return { help, format, target }
459
+ }
460
+
461
+ function printFindings(result, entries, cwd) {
462
+ if (result.total === 0) {
463
+ console.log(
464
+ `ksk-ds check-migration: 非推奨 API の残存はありません(${result.filesScanned} files / 台帳 ${entries.length} 件)`,
465
+ )
466
+ return
467
+ }
468
+
469
+ console.log(
470
+ `ksk-ds check-migration: 非推奨 API の残存を ${result.total} 件検出しました(${result.filesScanned} files)`,
471
+ )
472
+
473
+ console.log(`\n識別子別:`)
474
+ for (const item of result.byIdentifier) {
475
+ console.log(` ${String(item.count).padStart(4)} ${item.id}`)
476
+ console.log(` → ${item.replacement}(v${item.removeIn} で削除予定)`)
477
+ }
478
+
479
+ console.log(`\nファイル別:`)
480
+ for (const item of result.byFile) {
481
+ console.log(` ${String(item.count).padStart(4)} ${item.file}`)
482
+ }
483
+
484
+ console.log(`\n検出箇所:`)
485
+ for (const finding of result.findings) {
486
+ console.log(` ${displayPath(finding.file, cwd)}:${finding.line} ${finding.id} (${finding.usage})`)
487
+ }
488
+
489
+ console.log(`\n移行手順は MIGRATION.md の「非推奨 API 一覧」節を参照してください。`)
490
+ console.log(`残件が 0 になったバージョンで DS 側の互換コードを削除できます。`)
491
+ }
492
+
493
+ function printHelp() {
494
+ console.log(`ksk-ds check-migration
495
+
496
+ 使い方:
497
+ ksk-ds check-migration [DIR]
498
+ ksk-ds check-migration [DIR] --format=json
499
+
500
+ DIR の既定値:
501
+ ./src
502
+
503
+ 動作:
504
+ contracts/deprecations.json の非推奨 API が DIR 配下に残っていないかを
505
+ TypeScript の AST で数えます(コメント・文字列は誤検出しません)。
506
+ DS からの import に加え、相対 import 経由の re-export も残件として数えます。
507
+ 完全 read-only。残件があれば終了コード 1(CI で使えます)。
508
+ `)
509
+ }
510
+
511
+ const invokedDirectly =
512
+ process.argv[1] && resolve(process.argv[1]) === resolve(new URL(import.meta.url).pathname)
513
+
514
+ if (invokedDirectly) {
515
+ const pkgRoot = resolve(dirname(new URL(import.meta.url).pathname), "..")
516
+ process.exit(runCheckMigrationCli(process.argv.slice(2), { cwd: process.cwd(), pkgRoot }))
517
+ }
package/bin/init.js CHANGED
@@ -42,6 +42,7 @@ if (cmd === "help" || cmd === "--help" || cmd === "-h") {
42
42
  npx ksk-ds lint src --format json CI 向け JSON 出力
43
43
  npx ksk-ds lint --changed Git 差分のみ検査
44
44
  npx ksk-ds check-migration ./src 非推奨 API の残存を検査(read-only)
45
+ npx ksk-ds check-migration ./src --format=json CI 向け JSON 出力
45
46
  npx ksk-ds check-duplicates [DIR] DS と同名のローカル実装を検査
46
47
  npx ksk-ds check-duplicates --strict 検出時に exit 1(CI 向け)
47
48
  npx ksk-ds codemod 利用できる codemod を一覧
@@ -58,8 +59,8 @@ if (cmd === "lint") {
58
59
  }
59
60
 
60
61
  if (cmd === "check-migration") {
61
- const { runCheckMigrationCli } = await import("../scripts/codemod/check-migration.mjs")
62
- const status = runCheckMigrationCli(args.slice(1))
62
+ const { runCheckMigrationCli } = await import("./check-migration.js")
63
+ const status = runCheckMigrationCli(args.slice(1), { cwd: process.cwd(), pkgRoot })
63
64
  process.exit(status)
64
65
  }
65
66