ksk-design-system 1.66.0 → 1.66.2
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/README.md +16 -0
- package/bin/lint.js +183 -0
- package/contracts/components.json +1 -1
- package/contracts/rules.json +1 -1
- package/contracts/token-hex-cache.json +1 -1
- package/package.json +5 -1
- package/src/styles/product-theme.css +5 -0
package/README.md
CHANGED
|
@@ -229,6 +229,22 @@ consumer が shadcn 既定の `src/components/ui/` に部品を置いても中
|
|
|
229
229
|
(v1.63.x までは単一の `excludes` をパスと行内容の両方に OR で当てていたため、
|
|
230
230
|
このディレクトリ名だけで 13 ルールが同時に黙っていました)。
|
|
231
231
|
|
|
232
|
+
`excludeLines` の各値は **literal 文字列**として `line.includes(value)` で判定されます
|
|
233
|
+
(正規表現ではありません)。`shadow-\[` のように正規表現エスケープした値を書くと、実ソースの
|
|
234
|
+
`shadow-[var(--shadow-md)]` と一致せず除外が一度も効きません(issue #463)。
|
|
235
|
+
`scripts/check-rules-contract.mjs`(`npm run check` に組み込み済み)が `excludeLines` /
|
|
236
|
+
`excludePaths` / `excludeDsPaths` 内のバックスラッシュ混入を検出します。
|
|
237
|
+
|
|
238
|
+
#### P029: テンプレートリテラル className の許容範囲(issue #464)
|
|
239
|
+
|
|
240
|
+
`className={\`...\${...}...\`}` の補間部(`${...}`)が**文字列リテラルのみで構成される式**
|
|
241
|
+
(単純三項 `cond ? "a" : "b"` / `&&` / ネスト三項)なら、Tailwind の静的クラス抽出を壊さないため
|
|
242
|
+
P029 の対象外です。CLAUDE.md の実装前セルフチェックにある「クラス名は完全な文字列で書く。分岐は
|
|
243
|
+
三項演算子か cva variant で」という方針と一致させています。
|
|
244
|
+
|
|
245
|
+
識別子・関数呼び出し・メンバーアクセスの補間(`` `bg-${color}` `` 等)や、テンプレートリテラルの
|
|
246
|
+
入れ子は静的抽出できないため引き続き検出します。`cn()` / `clsx()` / CVA を使ってください。
|
|
247
|
+
|
|
232
248
|
### Jest(CommonJS)でコンポーネントをテストする
|
|
233
249
|
|
|
234
250
|
このパッケージは **ESM-only** です。CJS との dual build は配布せず、Jest
|
package/bin/lint.js
CHANGED
|
@@ -789,6 +789,7 @@ function lintFile(file, cwd, rules, options = {}) {
|
|
|
789
789
|
if (matchesRuleExclude(rule, { file: rel, line: rawLine, isDsFile })) continue
|
|
790
790
|
const maskedLine = maskedLines[index] ?? ""
|
|
791
791
|
if (!regex.test(maskedLine)) continue
|
|
792
|
+
if (rule.id === "P029" && isP029TemplateLiteralExempt(rawLine)) continue
|
|
792
793
|
if (ignores.suppresses(rule.id ?? "UNKNOWN", index + 1)) continue
|
|
793
794
|
findings.push(toFinding(rule, rel, index + 1, platform))
|
|
794
795
|
}
|
|
@@ -1119,6 +1120,188 @@ function maskTemplateLiterals(source) {
|
|
|
1119
1120
|
return maskStrings(source, TEMPLATE_DELIMITER_ONLY)
|
|
1120
1121
|
}
|
|
1121
1122
|
|
|
1123
|
+
/**
|
|
1124
|
+
* P029(テンプレートリテラル className 禁止)の例外判定(issue #464)。
|
|
1125
|
+
*
|
|
1126
|
+
* `className={\`base ${cond ? "a" : "b"}\`}` のように、補間部(`${...}`)が
|
|
1127
|
+
* 「文字列リテラルのみで構成される式」(三項・&&・ネスト三項を含む)なら
|
|
1128
|
+
* Tailwind の静的クラス抽出を壊さないため対象外にする。`${color}` のような
|
|
1129
|
+
* 識別子・関数呼び出し・メンバーアクセスの補間は従来どおり検出する。
|
|
1130
|
+
*
|
|
1131
|
+
* 文字列一致ベースの簡易パーサーであり、TypeScript の完全な式パーサーではない。
|
|
1132
|
+
* 判定に迷うケース(バッククォートのネスト等)は安全側=「例外にしない」を返す。
|
|
1133
|
+
*/
|
|
1134
|
+
|
|
1135
|
+
/** 文字列/括弧の深さを見ながら、depth 0 の `?` `:` `&&` `||` の位置を集める */
|
|
1136
|
+
function scanTopLevelOperatorTokens(expr) {
|
|
1137
|
+
const tokens = []
|
|
1138
|
+
let depth = 0
|
|
1139
|
+
let quote = null
|
|
1140
|
+
for (let i = 0; i < expr.length; i++) {
|
|
1141
|
+
const ch = expr[i]
|
|
1142
|
+
if (quote) {
|
|
1143
|
+
if (ch === "\\") {
|
|
1144
|
+
i += 1
|
|
1145
|
+
continue
|
|
1146
|
+
}
|
|
1147
|
+
if (ch === quote) quote = null
|
|
1148
|
+
continue
|
|
1149
|
+
}
|
|
1150
|
+
if (ch === '"' || ch === "'") {
|
|
1151
|
+
quote = ch
|
|
1152
|
+
continue
|
|
1153
|
+
}
|
|
1154
|
+
if (ch === "(" || ch === "[" || ch === "{") {
|
|
1155
|
+
depth += 1
|
|
1156
|
+
continue
|
|
1157
|
+
}
|
|
1158
|
+
if (ch === ")" || ch === "]" || ch === "}") {
|
|
1159
|
+
depth -= 1
|
|
1160
|
+
continue
|
|
1161
|
+
}
|
|
1162
|
+
if (depth !== 0) continue
|
|
1163
|
+
if (ch === "?") tokens.push({ index: i, type: "?" })
|
|
1164
|
+
else if (ch === ":") tokens.push({ index: i, type: ":" })
|
|
1165
|
+
else if (ch === "&" && expr[i + 1] === "&") {
|
|
1166
|
+
tokens.push({ index: i, type: "&&" })
|
|
1167
|
+
i += 1
|
|
1168
|
+
} else if (ch === "|" && expr[i + 1] === "|") {
|
|
1169
|
+
tokens.push({ index: i, type: "||" })
|
|
1170
|
+
i += 1
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
return tokens
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
/** `cond ? a : b` を depth 0 の `?`/`:` のバランスで分割する(ネスト三項対応) */
|
|
1177
|
+
function splitTopLevelTernary(expr) {
|
|
1178
|
+
const tokens = scanTopLevelOperatorTokens(expr)
|
|
1179
|
+
const questionIndex = tokens.findIndex((token) => token.type === "?")
|
|
1180
|
+
if (questionIndex === -1) return null
|
|
1181
|
+
let balance = 1
|
|
1182
|
+
for (let i = questionIndex + 1; i < tokens.length; i++) {
|
|
1183
|
+
if (tokens[i].type === "?") balance += 1
|
|
1184
|
+
else if (tokens[i].type === ":") {
|
|
1185
|
+
balance -= 1
|
|
1186
|
+
if (balance === 0) {
|
|
1187
|
+
const questionPos = tokens[questionIndex].index
|
|
1188
|
+
const colonPos = tokens[i].index
|
|
1189
|
+
return {
|
|
1190
|
+
whenTrue: expr.slice(questionPos + 1, colonPos),
|
|
1191
|
+
whenFalse: expr.slice(colonPos + 1),
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
return null
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
/** `lhs && rhs` / `lhs || rhs` を最初の depth 0 演算子で分割する */
|
|
1200
|
+
function splitTopLevelLogical(expr) {
|
|
1201
|
+
const token = scanTopLevelOperatorTokens(expr).find(
|
|
1202
|
+
(candidate) => candidate.type === "&&" || candidate.type === "||",
|
|
1203
|
+
)
|
|
1204
|
+
if (!token) return null
|
|
1205
|
+
return { rhs: expr.slice(token.index + token.type.length) }
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
function isStringLiteralExpr(expr) {
|
|
1209
|
+
const trimmed = expr.trim()
|
|
1210
|
+
return /^"(?:[^"\\]|\\.)*"$/.test(trimmed) || /^'(?:[^'\\]|\\.)*'$/.test(trimmed)
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
/** `(expr)` のように括弧が式全体をちょうど一度だけ包んでいるか */
|
|
1214
|
+
function isFullyParenWrapped(expr) {
|
|
1215
|
+
if (expr[0] !== "(" || expr[expr.length - 1] !== ")") return false
|
|
1216
|
+
let depth = 0
|
|
1217
|
+
for (let i = 0; i < expr.length; i++) {
|
|
1218
|
+
if (expr[i] === "(") depth += 1
|
|
1219
|
+
else if (expr[i] === ")") {
|
|
1220
|
+
depth -= 1
|
|
1221
|
+
if (depth === 0) return i === expr.length - 1
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
return false
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
/** 補間式が「文字列リテラルのみで構成される式」か(issue #464) */
|
|
1228
|
+
export function isLiteralOnlyInterpolation(expr) {
|
|
1229
|
+
const trimmed = (expr ?? "").trim()
|
|
1230
|
+
if (trimmed === "") return false
|
|
1231
|
+
// ネストしたテンプレートリテラルは判定を諦めて安全側(=対象外にしない)に倒す
|
|
1232
|
+
if (trimmed.includes("`")) return false
|
|
1233
|
+
if (isStringLiteralExpr(trimmed)) return true
|
|
1234
|
+
if (isFullyParenWrapped(trimmed)) return isLiteralOnlyInterpolation(trimmed.slice(1, -1))
|
|
1235
|
+
const ternary = splitTopLevelTernary(trimmed)
|
|
1236
|
+
if (ternary) {
|
|
1237
|
+
return isLiteralOnlyInterpolation(ternary.whenTrue) && isLiteralOnlyInterpolation(ternary.whenFalse)
|
|
1238
|
+
}
|
|
1239
|
+
const logical = splitTopLevelLogical(trimmed)
|
|
1240
|
+
if (logical) return isLiteralOnlyInterpolation(logical.rhs)
|
|
1241
|
+
return false
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
/** テンプレートリテラル(バッククォート開始位置つき)の中身を取り出す。同一行内で閉じない場合は null */
|
|
1245
|
+
function extractTemplateLiteralBody(line, backtickIndex) {
|
|
1246
|
+
let i = backtickIndex + 1
|
|
1247
|
+
let braceDepth = 0
|
|
1248
|
+
let content = ""
|
|
1249
|
+
while (i < line.length) {
|
|
1250
|
+
const ch = line[i]
|
|
1251
|
+
if (ch === "\\") {
|
|
1252
|
+
content += ch + (line[i + 1] ?? "")
|
|
1253
|
+
i += 2
|
|
1254
|
+
continue
|
|
1255
|
+
}
|
|
1256
|
+
if (braceDepth === 0 && ch === "`") return content
|
|
1257
|
+
if (ch === "{") braceDepth += 1
|
|
1258
|
+
else if (ch === "}" && braceDepth > 0) braceDepth -= 1
|
|
1259
|
+
content += ch
|
|
1260
|
+
i += 1
|
|
1261
|
+
}
|
|
1262
|
+
return null
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
/** テンプレートリテラルの中身から `${...}` の中身だけを depth バランスで取り出す */
|
|
1266
|
+
function extractInterpolationExprs(content) {
|
|
1267
|
+
const exprs = []
|
|
1268
|
+
let i = content.indexOf("${")
|
|
1269
|
+
while (i !== -1) {
|
|
1270
|
+
const start = i + 2
|
|
1271
|
+
let depth = 1
|
|
1272
|
+
let j = start
|
|
1273
|
+
while (j < content.length && depth > 0) {
|
|
1274
|
+
if (content[j] === "{") depth += 1
|
|
1275
|
+
else if (content[j] === "}") depth -= 1
|
|
1276
|
+
j += 1
|
|
1277
|
+
}
|
|
1278
|
+
if (depth === 0) {
|
|
1279
|
+
exprs.push(content.slice(start, j - 1))
|
|
1280
|
+
i = content.indexOf("${", j)
|
|
1281
|
+
} else {
|
|
1282
|
+
exprs.push(content.slice(start))
|
|
1283
|
+
break
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
return exprs
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
/**
|
|
1290
|
+
* P029 が fire した行が「className の template literal で、補間部が全て
|
|
1291
|
+
* 文字列リテラルのみで構成される式」なら true(=対象外にしてよい)を返す。
|
|
1292
|
+
*/
|
|
1293
|
+
export function isP029TemplateLiteralExempt(line) {
|
|
1294
|
+
const classNameIndex = line.indexOf("className=")
|
|
1295
|
+
if (classNameIndex === -1) return false
|
|
1296
|
+
const backtickIndex = line.indexOf("`", classNameIndex)
|
|
1297
|
+
if (backtickIndex === -1) return false
|
|
1298
|
+
const content = extractTemplateLiteralBody(line, backtickIndex)
|
|
1299
|
+
if (content === null) return false
|
|
1300
|
+
const interpolations = extractInterpolationExprs(content)
|
|
1301
|
+
if (interpolations.length === 0) return false
|
|
1302
|
+
return interpolations.every((expr) => isLiteralOnlyInterpolation(expr))
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1122
1305
|
function excludeList(value) {
|
|
1123
1306
|
return Array.isArray(value) ? value : []
|
|
1124
1307
|
}
|
package/contracts/rules.json
CHANGED
|
@@ -228,7 +228,7 @@
|
|
|
228
228
|
"pattern": "shadow-md\\b|shadow-lg\\b|shadow-xl\\b|shadow-2xl\\b",
|
|
229
229
|
"excludePaths": [".stories."],
|
|
230
230
|
"excludeDsPaths": ["components/ui/", "components/patterns/"],
|
|
231
|
-
"excludeLines": ["shadow
|
|
231
|
+
"excludeLines": ["shadow-["],
|
|
232
232
|
"message": "DS 定義外のシャドウ禁止",
|
|
233
233
|
"fix": "shadow-[var(--shadow-sm/md/lg/dialog/tooltip)]"
|
|
234
234
|
},
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"meta": {
|
|
3
3
|
"name": "KSK Design System — Semantic Token Hex Cache",
|
|
4
|
-
"version": "1.66.
|
|
4
|
+
"version": "1.66.2",
|
|
5
5
|
"description": "semantic / semanticDark トークン(var(--Primitive-*) 参照)を実 hex に解決したサイドカー生成物。hex はデフォルト(Blue)テーマでの解決値であり、Brand 系(meta.themeDependentKeys に列挙)はテーマ差し替え(orange/green/violet 等)で実色が変わる。テーマ別の完全解決値は `ksk-design-system/native` エクスポート(バンドル済み native トークンモジュール)の themes を参照。tokens.json 本体のスキーマは変更せず、AI がこのファイルだけで実色を把握できるようにし、primitive 値の変更による semantic 実色のドリフトを --check で機械検出する。",
|
|
6
6
|
"generatedBy": "scripts/generate-token-hex-cache.mjs",
|
|
7
7
|
"theme": "default",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ksk-design-system",
|
|
3
|
-
"version": "1.66.
|
|
3
|
+
"version": "1.66.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "KSK Design System — フリーランス向けマルチテーマ対応デザインシステム",
|
|
6
6
|
"license": "MIT",
|
|
@@ -92,6 +92,10 @@
|
|
|
92
92
|
"import": "./src/styles/categorical.css",
|
|
93
93
|
"default": "./src/styles/categorical.css"
|
|
94
94
|
},
|
|
95
|
+
"./tokens/product-theme": {
|
|
96
|
+
"import": "./src/styles/product-theme.css",
|
|
97
|
+
"default": "./src/styles/product-theme.css"
|
|
98
|
+
},
|
|
95
99
|
"./glass": {
|
|
96
100
|
"import": "./src/styles/glass.css",
|
|
97
101
|
"default": "./src/styles/glass.css"
|
|
@@ -13,6 +13,11 @@
|
|
|
13
13
|
@import "./themes/my-client.css"; … Brand 10行(色)
|
|
14
14
|
@import "./product-theme.css"; … ここの変数(寸法・面)
|
|
15
15
|
|
|
16
|
+
preset を使わずトークン CSS を個別 import する消費側は、このファイルを
|
|
17
|
+
subpath export で直接読める(コピー同期は不要):
|
|
18
|
+
|
|
19
|
+
@import "ksk-design-system/tokens/product-theme";
|
|
20
|
+
|
|
16
21
|
機械可読な許可リストは contracts/product-theme-overrides.json が正本。
|
|
17
22
|
**ここに無い変数は公開契約ではない**(内部実装なので予告なく変わる)。
|
|
18
23
|
許可リスト外の上書きは `npx ksk-ds lint` の P049 が検出する。
|