sparkle-design-cli 2.1.0 → 2.2.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/README.md +40 -0
- package/lib/check.js +9 -2
- package/lib/plugin-api.js +57 -2
- package/lib/plugin-helpers.js +165 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -209,6 +209,46 @@ export default defineAntiPatternPlugin({
|
|
|
209
209
|
});
|
|
210
210
|
```
|
|
211
211
|
|
|
212
|
+
##### 複雑な検出(`match` とヘルパー)
|
|
213
|
+
|
|
214
|
+
単純な正規表現では安全に書けない検出(accessible name の有無、prop の組み合わせ、JSX 式を意識した走査など)には、`check.pattern` の代わりに `check.match` を使います。`match` は `(content, helpers)` で呼ばれ、第 2 引数の `helpers` に JSX パースヘルパーが **CLI から注入** されます。各パッケージがパース処理を再実装して同じ false-positive / negative を踏むのを防ぐ仕組みです。
|
|
215
|
+
|
|
216
|
+
```js
|
|
217
|
+
// sparkle-design-cli を import しない(plain object を default export)
|
|
218
|
+
export default {
|
|
219
|
+
groups: [
|
|
220
|
+
{
|
|
221
|
+
id: 'your-org-avatar-accessible-name',
|
|
222
|
+
check: {
|
|
223
|
+
description: 'Avatar に accessible name がありません。',
|
|
224
|
+
recommendation: 'aria-label か alt を指定してください。',
|
|
225
|
+
// helpers = { matchOpeningTags, hasProp, isMultipleTypeProp, findOpeningTagEnd }
|
|
226
|
+
match: (content, { matchOpeningTags, hasProp }) =>
|
|
227
|
+
matchOpeningTags(
|
|
228
|
+
content,
|
|
229
|
+
'Avatar',
|
|
230
|
+
(props) =>
|
|
231
|
+
!hasProp(props, 'src') &&
|
|
232
|
+
!hasProp(props, 'aria-label') &&
|
|
233
|
+
!hasProp(props, 'aria-labelledby')
|
|
234
|
+
),
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
],
|
|
238
|
+
};
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
注入されるヘルパー(`check.match` の第 2 引数):
|
|
242
|
+
|
|
243
|
+
| ヘルパー | 用途 |
|
|
244
|
+
| ----------------------------------------------- | ------------------------------------------------------------------------------------ |
|
|
245
|
+
| `matchOpeningTags(content, tagName, predicate)` | `<Tag ...>` / `<Tag ... />` を走査し predicate が真のものを `{ index, text }` で返す |
|
|
246
|
+
| `hasProp(propsBlock, propName)` | prop が指定されているかを厳密判定(`aria-*` / `data-*` を誤検出しない) |
|
|
247
|
+
| `isMultipleTypeProp(propsBlock)` | `type` が静的に `"multiple"` か判定(動的式は `false`) |
|
|
248
|
+
| `findOpeningTagEnd(content, startIdx)` | 低レベル: 開きタグの閉じ `>` のオフセット(文字列 / JSX 式を考慮)。無ければ `-1` |
|
|
249
|
+
|
|
250
|
+
> 💡 `match` を使うプラグインは(上の `pattern` 例の `defineAntiPatternPlugin` import と違い)`sparkle-design-cli` を **import せず** plain object として default export してください。`helpers` はランタイムで CLI が注入するため import は不要で、生 JS のまま配布され npx 経由で実行される consumer 環境でも確実に解決されます。`pattern` と `match` は排他で、どちらか一方のみ指定します。
|
|
251
|
+
|
|
212
252
|
##### 自動 discovery
|
|
213
253
|
|
|
214
254
|
`sparkle-design-cli check` 実行時、CLI は consumer プロジェクトの `package.json` の `dependencies` / `devDependencies` / `peerDependencies` / `optionalDependencies` を走査し、`sparkleCli.antiPatterns` を宣言しているパッケージを発見次第ロードしてビルトインのルールにマージします。
|
package/lib/check.js
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
getManualReviewReminders,
|
|
9
9
|
} from './anti-pattern-rules.js';
|
|
10
10
|
import { loadAntiPatternPlugins } from './load-plugins.js';
|
|
11
|
+
import { MATCH_HELPERS } from './plugin-helpers.js';
|
|
11
12
|
import { REGEX, FONT_DOMAINS } from './constants.js';
|
|
12
13
|
|
|
13
14
|
const DEFAULT_TARGET = 'src';
|
|
@@ -163,10 +164,16 @@ function collectFindings(filePath, content, rules = BUILTIN_RULES) {
|
|
|
163
164
|
// type) must not crash the whole check pipeline.
|
|
164
165
|
try {
|
|
165
166
|
if (typeof rule.match === 'function') {
|
|
166
|
-
// rule.match(content) -> Array<{ index: number, text: string }>
|
|
167
|
+
// rule.match(content, helpers) -> Array<{ index: number, text: string }>
|
|
167
168
|
// 複雑な照合が必要なルール向けの opt-in API。2-pass 方式などで regex 単体の
|
|
168
169
|
// backtracking リスクを回避したいときに使う。index は content 内の絶対オフセット。
|
|
169
|
-
|
|
170
|
+
// 第 2 引数 helpers (MATCH_HELPERS) で JSX パースヘルパー(matchOpeningTags /
|
|
171
|
+
// hasProp / findOpeningTagEnd / isMultipleTypeProp)を注入し、各プラグインが
|
|
172
|
+
// 同じパース実装を再実装して同じ false-positive/negative を踏むのを防ぐ。
|
|
173
|
+
// 第 2 引数を無視する既存の match(content) も後方互換でそのまま動く。
|
|
174
|
+
// en: helpers (2nd arg) injects shared JSX parsing utilities so plugins don't
|
|
175
|
+
// re-derive the regex edge cases. Existing match(content) stays compatible.
|
|
176
|
+
const hits = rule.match(content, MATCH_HELPERS);
|
|
170
177
|
if (!Array.isArray(hits)) {
|
|
171
178
|
throw new TypeError(`rule.match must return an array, got ${typeof hits}`);
|
|
172
179
|
}
|
package/lib/plugin-api.js
CHANGED
|
@@ -31,11 +31,31 @@
|
|
|
31
31
|
* recommendation: string, // shown in `check` findings
|
|
32
32
|
* pattern?: RegExp, // simple regex check. MUST have the global (`g`) flag.
|
|
33
33
|
* // mutually exclusive with `match`.
|
|
34
|
-
* match?: (content: string) => Array<{ index: number, text: string }>,
|
|
35
|
-
* // opt-in API for complex matching (2-pass, AST, etc.)
|
|
34
|
+
* match?: (content: string, helpers: MatchHelpers) => Array<{ index: number, text: string }>,
|
|
35
|
+
* // opt-in API for complex matching (2-pass, AST, etc.).
|
|
36
|
+
* // `helpers` (2nd arg) is injected by the CLI — see ## MatchHelpers.
|
|
36
37
|
* },
|
|
37
38
|
* }
|
|
38
39
|
*
|
|
40
|
+
* ## MatchHelpers
|
|
41
|
+
*
|
|
42
|
+
* `check.match` receives a frozen `helpers` object as its second argument. Prefer these over
|
|
43
|
+
* hand-rolling JSX parsing — they are maintained and tested in the CLI, so plugins stay free of a
|
|
44
|
+
* `sparkle-design-cli` dependency (plugins ship as plain JS and are loaded by the CLI) and always
|
|
45
|
+
* run on the same parser as the CLI at runtime (no version skew across plugins).
|
|
46
|
+
*
|
|
47
|
+
* {
|
|
48
|
+
* // Iterate `<Tag ...>` / `<Tag ... />` openings; return `{ index, text }` for predicate hits.
|
|
49
|
+
* matchOpeningTags(content: string, tagName: string,
|
|
50
|
+
* predicate: (propsBlock: string) => boolean): Array<{ index: number, text: string }>,
|
|
51
|
+
* // Strict-equality check for a prop assignment (handles `aria-*` / `data-*` without `\b` bugs).
|
|
52
|
+
* hasProp(propsBlock: string, propName: string): boolean,
|
|
53
|
+
* // Statically resolve whether the `type` prop is "multiple" (dynamic exprs -> false).
|
|
54
|
+
* isMultipleTypeProp(propsBlock: string): boolean,
|
|
55
|
+
* // Low-level: offset of an opening tag's closing `>` (string / JSX-expr aware), or -1.
|
|
56
|
+
* findOpeningTagEnd(content: string, startIdx: number): number,
|
|
57
|
+
* }
|
|
58
|
+
*
|
|
39
59
|
* ## JSDocTarget
|
|
40
60
|
*
|
|
41
61
|
* {
|
|
@@ -228,10 +248,45 @@ See the JSDoc in \`lib/plugin-api.js\` for the full type contract:
|
|
|
228
248
|
- \`AntiPatternGroup.check\`: \`{ description, recommendation, pattern | match }\`
|
|
229
249
|
- \`pattern\` is a RegExp and **must include the global flag** (\`/foo/g\`); otherwise
|
|
230
250
|
the rule is rejected at load time.
|
|
251
|
+
- \`match\` is called as \`match(content, helpers)\`; \`helpers\` (matchOpeningTags / hasProp /
|
|
252
|
+
isMultipleTypeProp / findOpeningTagEnd) is injected by the CLI, so plugins need no import.
|
|
231
253
|
- \`pattern\` and \`match\` are mutually exclusive — provide exactly one.
|
|
232
254
|
- \`JSDocTarget\`: \`{ file, targetName, section: { bullets, example? } }\`
|
|
233
255
|
- \`ManualReviewReminder\`: \`{ id, message }\`
|
|
234
256
|
|
|
257
|
+
## Complex matching with injected helpers
|
|
258
|
+
|
|
259
|
+
For matches a single regex can't express safely (accessible-name presence, prop combinations,
|
|
260
|
+
JSX-expression-aware scanning), use \`check.match\` instead of \`check.pattern\`. \`match\` receives the
|
|
261
|
+
file \`content\` and a frozen \`helpers\` bundle as its **second argument**, so the plugin never imports
|
|
262
|
+
\`sparkle-design-cli\` — it ships as plain JS and the running CLI injects its own tested parser. This
|
|
263
|
+
keeps plugins dependency-free and guarantees every plugin uses the same parser at runtime.
|
|
264
|
+
|
|
265
|
+
\`\`\`js
|
|
266
|
+
// No import of sparkle-design-cli — default-export a plain object.
|
|
267
|
+
export default {
|
|
268
|
+
groups: [
|
|
269
|
+
{
|
|
270
|
+
id: 'internal-avatar-without-accessible-name',
|
|
271
|
+
check: {
|
|
272
|
+
description: 'Avatar に accessible name がありません。',
|
|
273
|
+
recommendation: 'aria-label か alt を指定してください。',
|
|
274
|
+
// helpers = { matchOpeningTags, hasProp, isMultipleTypeProp, findOpeningTagEnd }
|
|
275
|
+
match: (content, { matchOpeningTags, hasProp }) =>
|
|
276
|
+
matchOpeningTags(
|
|
277
|
+
content,
|
|
278
|
+
'Avatar',
|
|
279
|
+
(props) =>
|
|
280
|
+
!hasProp(props, 'src') &&
|
|
281
|
+
!hasProp(props, 'aria-label') &&
|
|
282
|
+
!hasProp(props, 'aria-labelledby')
|
|
283
|
+
),
|
|
284
|
+
},
|
|
285
|
+
},
|
|
286
|
+
],
|
|
287
|
+
};
|
|
288
|
+
\`\`\`
|
|
289
|
+
|
|
235
290
|
## ID namespace
|
|
236
291
|
|
|
237
292
|
Plugin rule IDs share a namespace with built-in rules and with each other. Choose a stable prefix
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sparkle-design-cli が anti-pattern プラグインの `check.match` に注入する JSX パース
|
|
3
|
+
* ヘルパー群。プラグイン側はこれらを import せず、`match(content, helpers)` の第 2 引数
|
|
4
|
+
* として受け取る。npx 実行された CLI 自身が自分の実装を渡すため、プラグインパッケージは
|
|
5
|
+
* sparkle-design-cli への依存を一切持たずに済み(生 JS のまま配布できる)、かつ全プラグインが
|
|
6
|
+
* 常に実行中 CLI と同一のパース実装で動く(バージョンずれによる検出差が原理的に起きない)。
|
|
7
|
+
*
|
|
8
|
+
* ここに集約する狙いは、各プラグインが正規表現ベース JSX 解析の罠
|
|
9
|
+
* (文字列リテラル / JSX 式 `{}` のバランス、escape backslash、`\b` 境界)を再実装して
|
|
10
|
+
* 同じ false-positive / negative を踏むのを防ぐこと。修正履歴は下の各 JSDoc に残す。
|
|
11
|
+
*
|
|
12
|
+
* en: JSX parsing helpers injected into a plugin's `check.match` as the second argument.
|
|
13
|
+
* Plugins never import these — the running CLI passes its own implementation, so plugins
|
|
14
|
+
* stay dependency-free and always run on the same parser as the CLI (no version skew).
|
|
15
|
+
* Centralizing them keeps every plugin on one tested implementation instead of re-deriving
|
|
16
|
+
* the regex edge cases (string / JSX-expression `{}` balance, escaped backslashes, `\b`).
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 開きタグの「最後の `>` の位置」を返すスキャナ。`[^>]*?` だと JSX prop 値中の
|
|
21
|
+
* `=>` / `>` / 子 JSX (`<X />`) で早期打ち切りになるため、文字列リテラル
|
|
22
|
+
* (`'`, `"`, `` ` ``)と JSX expression `{ ... }` の中括弧バランスを
|
|
23
|
+
* 意識した state machine で `>` を探す。
|
|
24
|
+
*
|
|
25
|
+
* 文字列内の closing quote は **直前の連続 backslash の数が偶数のときだけ**
|
|
26
|
+
* 終端として認める。`<Avatar title="C:\\" />` のように奇数個 backslash で
|
|
27
|
+
* 終わる場合に「閉じ quote が escape されたまま」と誤判定して全体を skip
|
|
28
|
+
* する false negative を防ぐ。
|
|
29
|
+
*
|
|
30
|
+
* en: Scanner returning the closing `>` of an opening JSX tag. Tracks string
|
|
31
|
+
* literals and `{}` nesting to avoid early termination on `=>` / `>` inside
|
|
32
|
+
* prop values or nested JSX. The closing quote is honored only when the count
|
|
33
|
+
* of preceding consecutive backslashes is even, so attribute values ending in
|
|
34
|
+
* a literal backslash (`"C:\\"`) don't make the scanner walk past EOF.
|
|
35
|
+
*
|
|
36
|
+
* @param {string} content
|
|
37
|
+
* @param {number} startIdx 走査開始オフセット(`<Tag` の直後を渡す)
|
|
38
|
+
* @returns {number} 開きタグを閉じる `>` の絶対オフセット。見つからなければ -1。
|
|
39
|
+
*/
|
|
40
|
+
function findOpeningTagEnd(content, startIdx) {
|
|
41
|
+
let depth = 0;
|
|
42
|
+
let inStr = null;
|
|
43
|
+
for (let i = startIdx; i < content.length; i += 1) {
|
|
44
|
+
const ch = content[i];
|
|
45
|
+
if (inStr) {
|
|
46
|
+
if (ch === inStr) {
|
|
47
|
+
// 直前の連続 backslash 数を数える。偶数(0 含む)なら closing quote。
|
|
48
|
+
// en: Count preceding consecutive backslashes; even means unescaped.
|
|
49
|
+
let backslashes = 0;
|
|
50
|
+
for (let j = i - 1; j >= startIdx && content[j] === '\\'; j -= 1) {
|
|
51
|
+
backslashes += 1;
|
|
52
|
+
}
|
|
53
|
+
if (backslashes % 2 === 0) inStr = null;
|
|
54
|
+
}
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (ch === '"' || ch === "'" || ch === '`') {
|
|
58
|
+
inStr = ch;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (ch === '{') depth += 1;
|
|
62
|
+
else if (ch === '}') depth -= 1;
|
|
63
|
+
else if (ch === '>' && depth === 0) return i;
|
|
64
|
+
}
|
|
65
|
+
return -1;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* 与えられた tagName の開きタグ `<Tag ...>` / `<Tag ... />` を全て走査し、
|
|
70
|
+
* 各 propsBlock で `predicate` が真になったものを CLI の `rule.match` API
|
|
71
|
+
* 形式 `{ index, text }` で返す。
|
|
72
|
+
*
|
|
73
|
+
* anchor は `<Tag` の直後が `\s` / `/` / `>` のいずれかであることを要求するため、
|
|
74
|
+
* `<Tagged` / `<TagOther` / `<Tag.Image` のような名前の prefix 違いには hit しない。
|
|
75
|
+
* 一方で JS line comment 中の `// <Avatar />` のような疑似 JSX には素直に hit する
|
|
76
|
+
* が、ノイズは低く `// sparkle-disable-*` で個別に suppress できるため許容している。
|
|
77
|
+
*
|
|
78
|
+
* en: Iterate all `<Tag ...>` / `<Tag ... />` openings of the given tag name
|
|
79
|
+
* and call `predicate(propsBlock)` for each. Returns `{ index, text }` items
|
|
80
|
+
* for hits in the shape expected by the CLI's `rule.match` API. The anchor
|
|
81
|
+
* requires `\s` / `/` / `>` right after `<Tag` so prefix-different names like
|
|
82
|
+
* `<Tagged` or `<Tag.Image` are not picked up. JS line comments aren't filtered
|
|
83
|
+
* out — `// sparkle-disable-*` covers that escape hatch.
|
|
84
|
+
*
|
|
85
|
+
* @param {string} content
|
|
86
|
+
* @param {string} tagName 例: `'Avatar'`
|
|
87
|
+
* @param {(propsBlock: string) => boolean} predicate propsBlock を受け取り hit 判定を返す
|
|
88
|
+
* @returns {Array<{ index: number, text: string }>}
|
|
89
|
+
*/
|
|
90
|
+
function matchOpeningTags(content, tagName, predicate) {
|
|
91
|
+
const anchor = new RegExp(`<${tagName}(?=[\\s/>])`, 'g');
|
|
92
|
+
const hits = [];
|
|
93
|
+
for (const opener of content.matchAll(anchor)) {
|
|
94
|
+
const start = opener.index ?? 0;
|
|
95
|
+
const tagBodyStart = start + opener[0].length;
|
|
96
|
+
const end = findOpeningTagEnd(content, tagBodyStart);
|
|
97
|
+
if (end === -1) continue;
|
|
98
|
+
const propsBlock = content.slice(tagBodyStart, end);
|
|
99
|
+
if (predicate(propsBlock)) {
|
|
100
|
+
hits.push({ index: start, text: `<${tagName}${propsBlock}>` });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return hits;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* propsBlock 内に prop 名 `propName` が代入されているかを厳密一致で判定する。
|
|
108
|
+
*
|
|
109
|
+
* 旧実装の `\b${propName}\s*=` は `\b` が letter と `-` の境界でも発火するため
|
|
110
|
+
* - `hasProp(..., 'label')` が `aria-label=` を拾う(label 未指定の見逃し)
|
|
111
|
+
* - `hasProp(..., 'initials')` が `data-initials=` を拾う(initials なしを HIT)
|
|
112
|
+
* の両方向の bug を抱えていた。prop 名の直前を `^` か whitespace か `{`
|
|
113
|
+
* のいずれかでアンカーすることで両方解消する。
|
|
114
|
+
*
|
|
115
|
+
* en: Strict-equality check for a prop assignment in `propsBlock`. The previous
|
|
116
|
+
* `\b${propName}\s*=` was broken in both directions because `\b` fires between
|
|
117
|
+
* a letter and `-`. Anchoring the prop name at start-of-block or a non-identifier
|
|
118
|
+
* character (whitespace / `{`) fixes both `aria-label` ↔ `label` and
|
|
119
|
+
* `data-initials` ↔ `initials` confusions.
|
|
120
|
+
*
|
|
121
|
+
* @param {string} propsBlock matchOpeningTags が渡す開きタグ内部のテキスト
|
|
122
|
+
* @param {string} propName 探す prop 名(`aria-label` など `-` を含んでも可)
|
|
123
|
+
* @returns {boolean}
|
|
124
|
+
*/
|
|
125
|
+
function hasProp(propsBlock, propName) {
|
|
126
|
+
const escaped = propName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
127
|
+
return new RegExp(`(?:^|[\\s{])${escaped}\\s*=`).test(propsBlock);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* propsBlock の `type` prop が静的に `"multiple"` と判定できるかを返す。
|
|
132
|
+
*
|
|
133
|
+
* 静的に "multiple" と判定できる 6 形式に対応(bare 3 + JSX 式中 3。`[`"']` が
|
|
134
|
+
* backtick / double / single の 3 quote をカバーする):
|
|
135
|
+
* bare: type="multiple" / type='multiple' / type=`multiple`
|
|
136
|
+
* JSX 式中: type={"multiple"} / type={'multiple'} / type={`multiple`}
|
|
137
|
+
* `type={someExpr}` のような動的指定は判定不能なので安全側に倒して `false`
|
|
138
|
+
* (= 非 multiple 扱い = rule HIT 候補)を返す。bare backtick は JSX としては不正だが、
|
|
139
|
+
* 正規表現上はマッチする(実害なく、誤検出は安全側)。
|
|
140
|
+
*
|
|
141
|
+
* en: Recognize all statically-resolvable "multiple" forms — bare and
|
|
142
|
+
* JSX-expression, each across the three quote styles (`/"/'). Dynamic
|
|
143
|
+
* expressions stay as HIT candidates (returns `false`) so we don't silently
|
|
144
|
+
* miss bugs.
|
|
145
|
+
*
|
|
146
|
+
* @param {string} propsBlock
|
|
147
|
+
* @returns {boolean}
|
|
148
|
+
*/
|
|
149
|
+
function isMultipleTypeProp(propsBlock) {
|
|
150
|
+
return /type\s*=\s*(?:[`"']multiple[`"']|\{\s*[`"']multiple[`"']\s*\})/.test(propsBlock);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* `check.match(content, helpers)` の第 2 引数としてプラグインに注入されるヘルパー集合。
|
|
155
|
+
* 凍結して渡すことで、プラグインが誤って実装を差し替えるのを防ぐ。
|
|
156
|
+
* en: Frozen helper bundle injected as the second argument of `check.match`.
|
|
157
|
+
*/
|
|
158
|
+
const MATCH_HELPERS = Object.freeze({
|
|
159
|
+
findOpeningTagEnd,
|
|
160
|
+
matchOpeningTags,
|
|
161
|
+
hasProp,
|
|
162
|
+
isMultipleTypeProp,
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
export { findOpeningTagEnd, matchOpeningTags, hasProp, isMultipleTypeProp, MATCH_HELPERS };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sparkle-design-cli",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "Sparkle Design CLI — プロジェクトセットアップ、CSS・フォント生成、アンチパターン検査、AI エージェント(Claude Code / Cursor / Codex)向けのガードと hook 設定まで一括で行う sparkle-design 公式 CLI。",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"registry": "https://registry.npmjs.org",
|