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.
- package/AGENTS.md +8 -0
- package/CLAUDE.md +8 -0
- package/DESIGN.md +4 -0
- package/MIGRATION.md +43 -0
- package/bin/check-migration.js +517 -0
- package/bin/init.js +3 -2
- package/bin/lint.js +21 -1
- package/contracts/components.json +105 -21
- package/contracts/deprecations.json +92 -0
- package/contracts/token-hex-cache.json +1 -1
- package/dist/index.js +4739 -4409
- package/dist/native/ui.js +1745 -1603
- package/dist/{native-VkkXnB0e.js → native-DlR32_Lk.js} +8 -0
- package/dist/native.js +1 -1
- package/dist/types/components/icons/info-circle.d.ts +23 -0
- package/dist/types/components/patterns/admin/data-table.d.ts +53 -4
- package/dist/types/components/patterns/chip-selector.d.ts +23 -7
- package/dist/types/components/patterns/chip.d.ts +1 -1
- package/dist/types/components/patterns/list-item.d.ts +35 -2
- package/dist/types/components/patterns/quick-action-grid.d.ts +25 -3
- package/dist/types/components/patterns/search-bar.d.ts +9 -1
- package/dist/types/components/patterns/swipe-row.d.ts +7 -1
- package/dist/types/components/ui/alert-dialog.d.ts +16 -3
- package/dist/types/components/ui/card.d.ts +1 -1
- package/dist/types/components/ui/dialog.d.ts +54 -6
- package/dist/types/components/ui/dropdown-menu.d.ts +1 -1
- package/dist/types/components/ui/pagination.d.ts +21 -4
- package/dist/types/components/ui/portal-container.d.ts +50 -0
- package/dist/types/components/ui/sheet.d.ts +52 -27
- package/dist/types/index.d.ts +13 -3
- package/dist/types/lib/build-page-items.d.ts +22 -0
- package/dist/types/lib/modal-stack.d.ts +45 -0
- package/dist/types/lib/title-level.d.ts +60 -0
- package/dist/types/native/components/AppHeader.d.ts +9 -1
- package/dist/types/native/components/Card.d.ts +7 -1
- package/dist/types/native/components/ChipSelector.d.ts +22 -1
- package/dist/types/native/components/Dialog.d.ts +17 -1
- package/dist/types/native/components/ListItem.d.ts +28 -1
- package/dist/types/native/components/QuickActionGrid.d.ts +22 -2
- package/dist/types/native/components/Sheet.d.ts +10 -0
- package/dist/types/native/components/SwipeRow.d.ts +23 -1
- package/dist/types/native/components/index.d.ts +4 -4
- package/dist/types/native/index.d.ts +2 -0
- package/dist/types/native/safe-area.d.ts +62 -0
- package/dist/types/native/theme/SafeAreaInsetsProvider.d.ts +18 -0
- package/dist/types/tokens/native/scales.d.ts +8 -0
- package/package.json +5 -2
- package/scripts/codemod/README.md +10 -8
- package/src/components/COMPONENT_LOOKUP.md +11 -10
- package/src/preset.css +10 -4
- package/src/styles/sheet-keyboard.css +34 -4
- package/src/styles/source-safelist.css +21 -3
- package/tokens.json +3 -1
- 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("
|
|
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
|
|