@foxford/eslint-plugin-analytics 0.0.0-stage → 1.0.1
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.mdx +54 -0
- package/index.cjs +541 -0
- package/index.d.cts +17 -0
- package/index.d.ts +17 -0
- package/index.js +498 -0
- package/package.json +50 -3
- package/README.md +0 -3
package/README.mdx
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# @foxford/eslint-plugin-analytics
|
|
2
|
+
|
|
3
|
+
ESLint-плагин для валидации разметки `data-fxa-*` в JSX (система декларативной аналитики Foxford FXA).
|
|
4
|
+
|
|
5
|
+
## Правила
|
|
6
|
+
|
|
7
|
+
| Правило | Описание | Уровень |
|
|
8
|
+
|---------|----------|---------|
|
|
9
|
+
| `valid-fxa-format` | Проверка формата значений `data-fxa-*` (regex + длина) | error |
|
|
10
|
+
| `known-fxa-value` | Проверка значений по реестрам `@foxford/analytics-registries` | error |
|
|
11
|
+
| `valid-fxa-combo` | Combo `data-fxa-path` + `data-fxa-action` только на разрешённых тегах | error |
|
|
12
|
+
| `no-useless-fxa-kind` | Предупреждение на `data-fxa-kind` без `data-fxa-path` или на семантическом теге | warn |
|
|
13
|
+
| `registry-freshness` | Предупреждение при устаревших реестрах (>7 дней) | warn |
|
|
14
|
+
|
|
15
|
+
## Использование
|
|
16
|
+
|
|
17
|
+
### ESLint 9 (flat config)
|
|
18
|
+
|
|
19
|
+
```js
|
|
20
|
+
// eslint.config.mjs
|
|
21
|
+
import analyticsPlugin from '@foxford/eslint-plugin-analytics'
|
|
22
|
+
|
|
23
|
+
export default [
|
|
24
|
+
analyticsPlugin.configs.recommended,
|
|
25
|
+
]
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### ESLint 8 (legacy .eslintrc)
|
|
29
|
+
|
|
30
|
+
```js
|
|
31
|
+
// .eslintrc.js
|
|
32
|
+
module.exports = {
|
|
33
|
+
plugins: ['@foxford/analytics'],
|
|
34
|
+
extends: ['plugin:@foxford/analytics/legacy'],
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Или через явное указание правил:
|
|
39
|
+
|
|
40
|
+
```js
|
|
41
|
+
// .eslintrc.js
|
|
42
|
+
const analyticsPlugin = require('@foxford/eslint-plugin-analytics')
|
|
43
|
+
|
|
44
|
+
module.exports = {
|
|
45
|
+
plugins: ['@foxford/analytics'],
|
|
46
|
+
rules: analyticsPlugin.configs.legacy.rules,
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Сборка и публикация
|
|
51
|
+
|
|
52
|
+
`pnpm run build` собирает ESM, CommonJS и декларации типов в `build/`. Команда `fox deploy npm` публикует содержимое этого каталога; поле `files` в собранном `package.json` включает JavaScript и декларации относительно корня npm-пакета. Версия в `plugin.meta.version` берётся из исходного `package.json` при сборке.
|
|
53
|
+
|
|
54
|
+
`pnpm run test` проверяет состав npm-архива через `npm pack --dry-run` после свежей сборки в отдельном временном каталоге. Проверка также загружает упакованные файлы через `import` и `require` и сверяет версию плагина с манифестом.
|
package/index.cjs
ADDED
|
@@ -0,0 +1,541 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
default: () => plugin,
|
|
34
|
+
legacyConfig: () => legacyConfig,
|
|
35
|
+
recommendedConfig: () => recommendedConfig,
|
|
36
|
+
rules: () => rules
|
|
37
|
+
});
|
|
38
|
+
module.exports = __toCommonJS(index_exports);
|
|
39
|
+
|
|
40
|
+
// ../../node_modules/.pnpm/tsup@8.5.1_@swc+core@1.15.18_@swc+helpers@0.5.23__jiti@1.21.7_postcss@8.5.25_supports-c_091cfba73c92b8fea62c19628bddae8d/node_modules/tsup/assets/cjs_shims.js
|
|
41
|
+
var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${__filename}`).href : document.currentScript && document.currentScript.tagName.toUpperCase() === "SCRIPT" ? document.currentScript.src : new URL("main.js", document.baseURI).href;
|
|
42
|
+
var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
|
|
43
|
+
|
|
44
|
+
// src/utils/jsx-helpers.ts
|
|
45
|
+
function getJSXAttributeStringValue(attr) {
|
|
46
|
+
const { value } = attr;
|
|
47
|
+
if (!value) return null;
|
|
48
|
+
if (value.type === "Literal" && typeof value.value === "string") {
|
|
49
|
+
return value.value;
|
|
50
|
+
}
|
|
51
|
+
if (value.type === "JSXExpressionContainer" && value.expression.type === "Literal" && typeof value.expression.value === "string") {
|
|
52
|
+
return value.expression.value;
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
function getJSXElementName(node) {
|
|
57
|
+
const { name } = node;
|
|
58
|
+
if (name.type === "JSXIdentifier") {
|
|
59
|
+
return name.name;
|
|
60
|
+
}
|
|
61
|
+
if (name.type === "JSXNamespacedName") {
|
|
62
|
+
return `${name.namespace.name}:${name.name.name}`;
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
function findJSXAttribute(node, attrName) {
|
|
67
|
+
return node.attributes.find(
|
|
68
|
+
(attr) => attr.type === "JSXAttribute" && attr.name.type === "JSXIdentifier" && attr.name.name === attrName
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
function hasJSXAttribute(node, attrName) {
|
|
72
|
+
return findJSXAttribute(node, attrName) !== void 0;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// src/utils/registry-loader.ts
|
|
76
|
+
var import_node_fs = __toESM(require("fs"), 1);
|
|
77
|
+
var import_node_module = require("module");
|
|
78
|
+
|
|
79
|
+
// ../analytics-registries/src/registries/actions.json
|
|
80
|
+
var actions_default = [
|
|
81
|
+
"buy",
|
|
82
|
+
"open",
|
|
83
|
+
"close",
|
|
84
|
+
"submit-order",
|
|
85
|
+
"toggle",
|
|
86
|
+
"select-day",
|
|
87
|
+
"apply-promo",
|
|
88
|
+
"accept-invite",
|
|
89
|
+
"favorite",
|
|
90
|
+
"confirm",
|
|
91
|
+
"cancel",
|
|
92
|
+
"nav-profile",
|
|
93
|
+
"nav-settings",
|
|
94
|
+
"nav-billing",
|
|
95
|
+
"logout",
|
|
96
|
+
"select-tab",
|
|
97
|
+
"prev-month",
|
|
98
|
+
"next-month",
|
|
99
|
+
"input-name",
|
|
100
|
+
"input-email",
|
|
101
|
+
"select-card",
|
|
102
|
+
"select-sbp"
|
|
103
|
+
];
|
|
104
|
+
|
|
105
|
+
// ../analytics-registries/src/registries/kinds.json
|
|
106
|
+
var kinds_default = [
|
|
107
|
+
"modal",
|
|
108
|
+
"card",
|
|
109
|
+
"accordion",
|
|
110
|
+
"tabs",
|
|
111
|
+
"banner",
|
|
112
|
+
"tooltip",
|
|
113
|
+
"dropdown",
|
|
114
|
+
"menu",
|
|
115
|
+
"calendar"
|
|
116
|
+
];
|
|
117
|
+
|
|
118
|
+
// ../analytics-registries/src/registries/paths.json
|
|
119
|
+
var paths_default = [
|
|
120
|
+
"dashboard",
|
|
121
|
+
"checkout",
|
|
122
|
+
"course-card",
|
|
123
|
+
"invite-banner",
|
|
124
|
+
"my-courses",
|
|
125
|
+
"main-nav",
|
|
126
|
+
"search-results",
|
|
127
|
+
"site-header",
|
|
128
|
+
"recommendations",
|
|
129
|
+
"reschedule",
|
|
130
|
+
"user-menu",
|
|
131
|
+
"faq",
|
|
132
|
+
"date-picker",
|
|
133
|
+
"days-grid",
|
|
134
|
+
"footer",
|
|
135
|
+
"personal",
|
|
136
|
+
"payment",
|
|
137
|
+
"method",
|
|
138
|
+
"items"
|
|
139
|
+
];
|
|
140
|
+
|
|
141
|
+
// ../analytics-registries/src/types.ts
|
|
142
|
+
var actions = actions_default;
|
|
143
|
+
var kinds = kinds_default;
|
|
144
|
+
var paths = paths_default;
|
|
145
|
+
|
|
146
|
+
// ../analytics-registries/src/validation.ts
|
|
147
|
+
var MAX_VALUE_LENGTH = 32;
|
|
148
|
+
function sanitizeValue(value) {
|
|
149
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, MAX_VALUE_LENGTH);
|
|
150
|
+
}
|
|
151
|
+
var actionsSet = new Set(actions);
|
|
152
|
+
var kindsSet = new Set(kinds);
|
|
153
|
+
var pathsSet = new Set(paths);
|
|
154
|
+
|
|
155
|
+
// ../analytics-registries/src/constants.ts
|
|
156
|
+
var COMBO_TAG_WHITELIST = ["a", "form", "button", "label", "summary"];
|
|
157
|
+
|
|
158
|
+
// src/utils/registry-loader.ts
|
|
159
|
+
var REGISTRIES = {
|
|
160
|
+
actions,
|
|
161
|
+
kinds,
|
|
162
|
+
paths
|
|
163
|
+
};
|
|
164
|
+
var requireModule = (0, import_node_module.createRequire)(importMetaUrl);
|
|
165
|
+
function loadRegistry(name) {
|
|
166
|
+
return REGISTRIES[name];
|
|
167
|
+
}
|
|
168
|
+
function getRegistryMtime(_name) {
|
|
169
|
+
try {
|
|
170
|
+
const pkgPath = requireModule.resolve("@foxford/analytics-registries/package.json");
|
|
171
|
+
return import_node_fs.default.statSync(pkgPath).mtime;
|
|
172
|
+
} catch {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// src/rules/known-fxa-value.ts
|
|
178
|
+
var ATTR_TO_REGISTRY = {
|
|
179
|
+
"data-fxa-action": "actions",
|
|
180
|
+
"data-fxa-kind": "kinds",
|
|
181
|
+
"data-fxa-path": "paths"
|
|
182
|
+
};
|
|
183
|
+
var KNOWN_SETS = {
|
|
184
|
+
actions: new Set(loadRegistry("actions")),
|
|
185
|
+
kinds: new Set(loadRegistry("kinds")),
|
|
186
|
+
paths: new Set(loadRegistry("paths"))
|
|
187
|
+
};
|
|
188
|
+
var rule = {
|
|
189
|
+
create(context) {
|
|
190
|
+
return {
|
|
191
|
+
JSXOpeningElement(rawNode) {
|
|
192
|
+
const node = rawNode;
|
|
193
|
+
for (const [attrName, registryName] of Object.entries(ATTR_TO_REGISTRY)) {
|
|
194
|
+
const attr = findJSXAttribute(node, attrName);
|
|
195
|
+
if (!attr) continue;
|
|
196
|
+
const value = getJSXAttributeStringValue(attr);
|
|
197
|
+
if (value === null) continue;
|
|
198
|
+
const registry = loadRegistry(registryName);
|
|
199
|
+
const knownSet = KNOWN_SETS[registryName];
|
|
200
|
+
if (!knownSet.has(value)) {
|
|
201
|
+
const known = registry.slice(0, 10).join(", ") + (registry.length > 10 ? ", ..." : "");
|
|
202
|
+
context.report({
|
|
203
|
+
data: { attr: attrName, known, value },
|
|
204
|
+
messageId: "unknownValue",
|
|
205
|
+
node: attr
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
},
|
|
212
|
+
meta: {
|
|
213
|
+
docs: {
|
|
214
|
+
description: "\u041F\u0440\u043E\u0432\u0435\u0440\u044F\u0435\u0442 \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u044F \u0430\u0442\u0440\u0438\u0431\u0443\u0442\u043E\u0432 data-fxa-* \u043F\u043E \u0440\u0435\u0435\u0441\u0442\u0440\u0430\u043C @foxford/analytics-registries",
|
|
215
|
+
recommended: true,
|
|
216
|
+
url: "https://github.com/foxford/frontend-platform"
|
|
217
|
+
},
|
|
218
|
+
messages: {
|
|
219
|
+
unknownValue: '\u041D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043D\u043E\u0435 \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435 "{{value}}" \u0434\u043B\u044F {{attr}}. \u0414\u043E\u043F\u0443\u0441\u0442\u0438\u043C\u044B\u0435 \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u044F: {{known}}'
|
|
220
|
+
},
|
|
221
|
+
schema: [],
|
|
222
|
+
type: "problem"
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
// src/rules/no-useless-fxa-kind.ts
|
|
227
|
+
var SEMANTIC_TAGS_WITH_TYPE = /* @__PURE__ */ new Set([
|
|
228
|
+
"form",
|
|
229
|
+
"nav",
|
|
230
|
+
"header",
|
|
231
|
+
"footer",
|
|
232
|
+
"main",
|
|
233
|
+
"aside",
|
|
234
|
+
"section",
|
|
235
|
+
"article",
|
|
236
|
+
"dialog",
|
|
237
|
+
"figure",
|
|
238
|
+
"details"
|
|
239
|
+
]);
|
|
240
|
+
var rule2 = {
|
|
241
|
+
create(context) {
|
|
242
|
+
return {
|
|
243
|
+
JSXOpeningElement(rawNode) {
|
|
244
|
+
const node = rawNode;
|
|
245
|
+
const kindAttr = findJSXAttribute(node, "data-fxa-kind");
|
|
246
|
+
if (!kindAttr) return;
|
|
247
|
+
const kindNode = kindAttr;
|
|
248
|
+
const tagName = getJSXElementName(node);
|
|
249
|
+
if (tagName !== null && SEMANTIC_TAGS_WITH_TYPE.has(tagName)) {
|
|
250
|
+
context.report({
|
|
251
|
+
data: { tag: tagName },
|
|
252
|
+
messageId: "redundantKindOnSemanticTag",
|
|
253
|
+
node: kindNode
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
if (!hasJSXAttribute(node, "data-fxa-path")) {
|
|
257
|
+
context.report({
|
|
258
|
+
messageId: "kindWithoutPath",
|
|
259
|
+
node: kindNode
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
},
|
|
265
|
+
meta: {
|
|
266
|
+
docs: {
|
|
267
|
+
description: "\u041F\u0440\u0435\u0434\u0443\u043F\u0440\u0435\u0436\u0434\u0430\u0435\u0442 \u043E\u0431 \u0438\u0437\u0431\u044B\u0442\u043E\u0447\u043D\u043E\u043C data-fxa-kind: \u0431\u0435\u0437 data-fxa-path \u0438\u043B\u0438 \u043D\u0430 \u0441\u0435\u043C\u0430\u043D\u0442\u0438\u0447\u0435\u0441\u043A\u043E\u043C \u0442\u0435\u0433\u0435",
|
|
268
|
+
recommended: true,
|
|
269
|
+
url: "https://github.com/foxford/frontend-platform"
|
|
270
|
+
},
|
|
271
|
+
messages: {
|
|
272
|
+
kindWithoutPath: "data-fxa-kind \u0431\u0435\u0437 data-fxa-path \u043D\u0430 \u0442\u043E\u043C \u0436\u0435 \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u0435 \u043D\u0435 \u0438\u043C\u0435\u0435\u0442 \u0441\u043C\u044B\u0441\u043B\u0430",
|
|
273
|
+
redundantKindOnSemanticTag: "\u0418\u0437\u0431\u044B\u0442\u043E\u0447\u043D\u044B\u0439 data-fxa-kind \u043D\u0430 \u0442\u0435\u0433\u0435 <{{tag}}> \u2014 \u0442\u0435\u0433 \u0443\u0436\u0435 \u043F\u0440\u0435\u0434\u043E\u0441\u0442\u0430\u0432\u043B\u044F\u0435\u0442 \u0441\u0435\u043C\u0430\u043D\u0442\u0438\u0447\u0435\u0441\u043A\u0438\u0439 \u0442\u0438\u043F"
|
|
274
|
+
},
|
|
275
|
+
schema: [],
|
|
276
|
+
type: "suggestion"
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
// src/rules/registry-freshness.ts
|
|
281
|
+
var MAX_REGISTRY_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
282
|
+
var REGISTRY_NAMES = ["actions", "kinds", "paths"];
|
|
283
|
+
var freshnessCheckResult = null;
|
|
284
|
+
var rule3 = {
|
|
285
|
+
create(context) {
|
|
286
|
+
return {
|
|
287
|
+
Program(node) {
|
|
288
|
+
if (!freshnessCheckResult) {
|
|
289
|
+
const now = Date.now();
|
|
290
|
+
const stale = [];
|
|
291
|
+
for (const name of REGISTRY_NAMES) {
|
|
292
|
+
const mtime = getRegistryMtime(name);
|
|
293
|
+
if (mtime !== null && now - mtime.getTime() > MAX_REGISTRY_AGE_MS) {
|
|
294
|
+
stale.push(name);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
freshnessCheckResult = { stale };
|
|
298
|
+
}
|
|
299
|
+
if (freshnessCheckResult.stale.length > 0) {
|
|
300
|
+
context.report({
|
|
301
|
+
data: { registries: freshnessCheckResult.stale.join(", ") },
|
|
302
|
+
messageId: "staleRegistry",
|
|
303
|
+
node
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
},
|
|
309
|
+
meta: {
|
|
310
|
+
docs: {
|
|
311
|
+
description: "\u041F\u0440\u0435\u0434\u0443\u043F\u0440\u0435\u0436\u0434\u0430\u0435\u0442 \u043F\u0440\u0438 \u0443\u0441\u0442\u0430\u0440\u0435\u0432\u0448\u0438\u0445 \u0440\u0435\u0435\u0441\u0442\u0440\u0430\u0445 data-fxa-* (\u043D\u0435 \u043E\u0431\u043D\u043E\u0432\u043B\u044F\u043B\u0438\u0441\u044C >7 \u0434\u043D\u0435\u0439)",
|
|
312
|
+
recommended: true,
|
|
313
|
+
url: "https://github.com/foxford/frontend-platform"
|
|
314
|
+
},
|
|
315
|
+
messages: {
|
|
316
|
+
staleRegistry: "\u0420\u0435\u0435\u0441\u0442\u0440 {{registries}} \u043D\u0435 \u043E\u0431\u043D\u043E\u0432\u043B\u044F\u043B\u0441\u044F \u0431\u043E\u043B\u0435\u0435 7 \u0434\u043D\u0435\u0439. \u041F\u0440\u043E\u0432\u0435\u0440\u044C\u0442\u0435 \u0430\u043A\u0442\u0443\u0430\u043B\u044C\u043D\u043E\u0441\u0442\u044C \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0439 data-fxa-*."
|
|
317
|
+
},
|
|
318
|
+
schema: [],
|
|
319
|
+
type: "suggestion"
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
// src/rules/valid-fxa-combo.ts
|
|
324
|
+
var COMBO_ALLOWED_TAGS = new Set(COMBO_TAG_WHITELIST);
|
|
325
|
+
var rule4 = {
|
|
326
|
+
create(context) {
|
|
327
|
+
return {
|
|
328
|
+
JSXOpeningElement(rawNode) {
|
|
329
|
+
const node = rawNode;
|
|
330
|
+
const hasPath = hasJSXAttribute(node, "data-fxa-path");
|
|
331
|
+
const hasAction = hasJSXAttribute(node, "data-fxa-action");
|
|
332
|
+
if (!hasPath || !hasAction) return;
|
|
333
|
+
const tagName = getJSXElementName(node);
|
|
334
|
+
if (tagName === null) return;
|
|
335
|
+
if (COMBO_ALLOWED_TAGS.has(tagName)) return;
|
|
336
|
+
const actionAttr = findJSXAttribute(node, "data-fxa-action");
|
|
337
|
+
const reportNode = actionAttr ?? rawNode;
|
|
338
|
+
context.report({
|
|
339
|
+
data: {
|
|
340
|
+
allowed: Array.from(COMBO_ALLOWED_TAGS).join(", "),
|
|
341
|
+
tag: tagName
|
|
342
|
+
},
|
|
343
|
+
messageId: "invalidCombo",
|
|
344
|
+
node: reportNode
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
},
|
|
349
|
+
meta: {
|
|
350
|
+
docs: {
|
|
351
|
+
description: "Combo data-fxa-path + data-fxa-action \u0440\u0430\u0437\u0440\u0435\u0448\u0451\u043D \u0442\u043E\u043B\u044C\u043A\u043E \u043D\u0430 \u0442\u0435\u0433\u0430\u0445: a, form, button, label, summary",
|
|
352
|
+
recommended: true,
|
|
353
|
+
url: "https://github.com/foxford/frontend-platform"
|
|
354
|
+
},
|
|
355
|
+
messages: {
|
|
356
|
+
invalidCombo: "Combo data-fxa-path + data-fxa-action \u043D\u0430 \u0442\u0435\u0433\u0435 <{{tag}}> \u043D\u0435\u0434\u043E\u043F\u0443\u0441\u0442\u0438\u043C. \u0420\u0430\u0437\u0440\u0435\u0448\u0451\u043D\u043D\u044B\u0435 \u0442\u0435\u0433\u0438: {{allowed}}"
|
|
357
|
+
},
|
|
358
|
+
schema: [],
|
|
359
|
+
type: "problem"
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
// src/rules/valid-fxa-format.ts
|
|
364
|
+
var CHECKED_ATTRIBUTES = ["data-fxa-path", "data-fxa-action", "data-fxa-kind"];
|
|
365
|
+
var FORMAT_REGEX = /^[a-z0-9-]+$/;
|
|
366
|
+
var MAX_LENGTH = 32;
|
|
367
|
+
var rule5 = {
|
|
368
|
+
create(context) {
|
|
369
|
+
return {
|
|
370
|
+
JSXOpeningElement(rawNode) {
|
|
371
|
+
const node = rawNode;
|
|
372
|
+
for (const attrName of CHECKED_ATTRIBUTES) {
|
|
373
|
+
const attr = findJSXAttribute(node, attrName);
|
|
374
|
+
if (!attr) continue;
|
|
375
|
+
const value = getJSXAttributeStringValue(attr);
|
|
376
|
+
if (value === null) continue;
|
|
377
|
+
const isValid = value.length > 0 && value.length <= MAX_LENGTH && FORMAT_REGEX.test(value);
|
|
378
|
+
if (!isValid) {
|
|
379
|
+
const sanitized = sanitizeValue(value);
|
|
380
|
+
context.report({
|
|
381
|
+
data: { attr: attrName, value },
|
|
382
|
+
messageId: "invalidFormat",
|
|
383
|
+
node: attr,
|
|
384
|
+
suggest: sanitized ? [
|
|
385
|
+
{
|
|
386
|
+
data: { sanitized },
|
|
387
|
+
fix(fixer) {
|
|
388
|
+
const attrValue = attr.value;
|
|
389
|
+
if (!attrValue) return null;
|
|
390
|
+
if (attrValue.type === "Literal") {
|
|
391
|
+
return fixer.replaceText(attrValue, `"${sanitized}"`);
|
|
392
|
+
}
|
|
393
|
+
if (attrValue.type === "JSXExpressionContainer") {
|
|
394
|
+
return fixer.replaceText(attrValue, `"${sanitized}"`);
|
|
395
|
+
}
|
|
396
|
+
return null;
|
|
397
|
+
},
|
|
398
|
+
messageId: "suggestFix"
|
|
399
|
+
}
|
|
400
|
+
] : []
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
},
|
|
407
|
+
meta: {
|
|
408
|
+
docs: {
|
|
409
|
+
description: "\u041F\u0440\u043E\u0432\u0435\u0440\u044F\u0435\u0442 \u0444\u043E\u0440\u043C\u0430\u0442 \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0439 \u0430\u0442\u0440\u0438\u0431\u0443\u0442\u043E\u0432 data-fxa-* (regex ^[a-z0-9-]+$, \u0434\u043B\u0438\u043D\u0430 \u226432)",
|
|
410
|
+
recommended: true,
|
|
411
|
+
url: "https://github.com/foxford/frontend-platform"
|
|
412
|
+
},
|
|
413
|
+
hasSuggestions: true,
|
|
414
|
+
messages: {
|
|
415
|
+
invalidFormat: '\u0417\u043D\u0430\u0447\u0435\u043D\u0438\u0435 "{{value}}" \u0430\u0442\u0440\u0438\u0431\u0443\u0442\u0430 {{attr}} \u043D\u0435 \u0441\u043E\u043E\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u0435\u0442 \u0444\u043E\u0440\u043C\u0430\u0442\u0443 ^[a-z0-9-]+$ \u0438\u043B\u0438 \u043F\u0440\u0435\u0432\u044B\u0448\u0430\u0435\u0442 32 \u0441\u0438\u043C\u0432\u043E\u043B\u0430',
|
|
416
|
+
suggestFix: '\u0417\u0430\u043C\u0435\u043D\u0438\u0442\u044C \u043D\u0430 \u0441\u0430\u043D\u0438\u0442\u0438\u0437\u0438\u0440\u043E\u0432\u0430\u043D\u043D\u043E\u0435 \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435 "{{sanitized}}"'
|
|
417
|
+
},
|
|
418
|
+
schema: [],
|
|
419
|
+
type: "problem"
|
|
420
|
+
}
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
// package.json
|
|
424
|
+
var package_default = {
|
|
425
|
+
name: "@foxford/eslint-plugin-analytics",
|
|
426
|
+
version: "1.0.1",
|
|
427
|
+
description: "ESLint-\u043F\u043B\u0430\u0433\u0438\u043D \u0434\u043B\u044F \u0432\u0430\u043B\u0438\u0434\u0430\u0446\u0438\u0438 \u0440\u0430\u0437\u043C\u0435\u0442\u043A\u0438 data-fxa-* \u0432 JSX (\u0441\u0438\u0441\u0442\u0435\u043C\u0430 \u0434\u0435\u043A\u043B\u0430\u0440\u0430\u0442\u0438\u0432\u043D\u043E\u0439 \u0430\u043D\u0430\u043B\u0438\u0442\u0438\u043A\u0438 Foxford)",
|
|
428
|
+
keywords: [
|
|
429
|
+
"foxford",
|
|
430
|
+
"eslint-plugin",
|
|
431
|
+
"analytics",
|
|
432
|
+
"fxa"
|
|
433
|
+
],
|
|
434
|
+
homepage: "https://github.com/foxford/frontend-platform",
|
|
435
|
+
bugs: "https://github.com/foxford/frontend-platform/issues",
|
|
436
|
+
repository: {
|
|
437
|
+
type: "git",
|
|
438
|
+
url: "git+https://github.com/foxford/frontend-platform.git",
|
|
439
|
+
directory: "packages/eslint-plugin-analytics"
|
|
440
|
+
},
|
|
441
|
+
license: "MIT",
|
|
442
|
+
author: "Foxford Co",
|
|
443
|
+
maintainers: [
|
|
444
|
+
{
|
|
445
|
+
name: "Roman Olin",
|
|
446
|
+
email: "r.v.olin@foxford.ru",
|
|
447
|
+
team: "core-frontend"
|
|
448
|
+
}
|
|
449
|
+
],
|
|
450
|
+
main: "src/index.ts",
|
|
451
|
+
type: "module",
|
|
452
|
+
exports: {
|
|
453
|
+
".": {
|
|
454
|
+
import: "./src/index.ts",
|
|
455
|
+
default: "./src/index.ts"
|
|
456
|
+
}
|
|
457
|
+
},
|
|
458
|
+
files: [
|
|
459
|
+
"build"
|
|
460
|
+
],
|
|
461
|
+
scripts: {
|
|
462
|
+
"============================ BUILD =============================": "",
|
|
463
|
+
build: "tsup",
|
|
464
|
+
"============================ DEPLOY ============================": "",
|
|
465
|
+
deploy: "fox deploy npm",
|
|
466
|
+
"deploy:npm": "fox deploy npm --local-dir=build",
|
|
467
|
+
"============================ LINT ==============================": "",
|
|
468
|
+
"lint:clean": "rm -f .eslintcache",
|
|
469
|
+
"lint:type-check": "tsc --noEmit --pretty",
|
|
470
|
+
"lint:eslint": "eslint --cache --cache-strategy content --max-warnings=0 ./",
|
|
471
|
+
"============================ TEST ==============================": "",
|
|
472
|
+
test: "vitest run",
|
|
473
|
+
"test:watch": "vitest"
|
|
474
|
+
},
|
|
475
|
+
dependencies: {
|
|
476
|
+
"@foxford/analytics-registries": "workspace:*"
|
|
477
|
+
},
|
|
478
|
+
peerDependencies: {
|
|
479
|
+
eslint: ">=8.0.0"
|
|
480
|
+
},
|
|
481
|
+
devDependencies: {
|
|
482
|
+
"@foxford/eslint-config": "workspace:*",
|
|
483
|
+
"@foxford/prettier-config": "workspace:*",
|
|
484
|
+
"@foxford/typescript-config": "workspace:*",
|
|
485
|
+
"@foxford/vitest-config": "workspace:*",
|
|
486
|
+
"@types/node": "catalog:dev",
|
|
487
|
+
eslint: "catalog:linter",
|
|
488
|
+
tsup: "catalog:dev",
|
|
489
|
+
typescript: "catalog:dev",
|
|
490
|
+
vitest: "catalog:dev"
|
|
491
|
+
}
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
// src/index.ts
|
|
495
|
+
var { version } = package_default;
|
|
496
|
+
var rules = {
|
|
497
|
+
"known-fxa-value": rule,
|
|
498
|
+
"no-useless-fxa-kind": rule2,
|
|
499
|
+
"registry-freshness": rule3,
|
|
500
|
+
"valid-fxa-combo": rule4,
|
|
501
|
+
"valid-fxa-format": rule5
|
|
502
|
+
};
|
|
503
|
+
var recommendedConfig = {
|
|
504
|
+
plugins: {
|
|
505
|
+
"@foxford/analytics": { rules }
|
|
506
|
+
},
|
|
507
|
+
rules: {
|
|
508
|
+
"@foxford/analytics/known-fxa-value": "error",
|
|
509
|
+
"@foxford/analytics/no-useless-fxa-kind": "warn",
|
|
510
|
+
"@foxford/analytics/registry-freshness": "warn",
|
|
511
|
+
"@foxford/analytics/valid-fxa-combo": "error",
|
|
512
|
+
"@foxford/analytics/valid-fxa-format": "error"
|
|
513
|
+
}
|
|
514
|
+
};
|
|
515
|
+
var legacyConfig = {
|
|
516
|
+
plugins: ["@foxford/analytics"],
|
|
517
|
+
rules: {
|
|
518
|
+
"@foxford/analytics/known-fxa-value": "error",
|
|
519
|
+
"@foxford/analytics/no-useless-fxa-kind": "warn",
|
|
520
|
+
"@foxford/analytics/registry-freshness": "warn",
|
|
521
|
+
"@foxford/analytics/valid-fxa-combo": "error",
|
|
522
|
+
"@foxford/analytics/valid-fxa-format": "error"
|
|
523
|
+
}
|
|
524
|
+
};
|
|
525
|
+
var plugin = {
|
|
526
|
+
configs: {
|
|
527
|
+
legacy: legacyConfig,
|
|
528
|
+
recommended: recommendedConfig
|
|
529
|
+
},
|
|
530
|
+
meta: {
|
|
531
|
+
name: "@foxford/eslint-plugin-analytics",
|
|
532
|
+
version
|
|
533
|
+
},
|
|
534
|
+
rules
|
|
535
|
+
};
|
|
536
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
537
|
+
0 && (module.exports = {
|
|
538
|
+
legacyConfig,
|
|
539
|
+
recommendedConfig,
|
|
540
|
+
rules
|
|
541
|
+
});
|
package/index.d.cts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { ESLint, Linter, Rule } from 'eslint';
|
|
2
|
+
|
|
3
|
+
/** Карта правил плагина */
|
|
4
|
+
declare const rules: Record<string, Rule.RuleModule>;
|
|
5
|
+
/** Рекомендованная конфигурация плагина для ESLint 9 (flat config, eslint.config.mjs) */
|
|
6
|
+
declare const recommendedConfig: Linter.Config;
|
|
7
|
+
/** Конфигурация плагина для ESLint 8 (legacy .eslintrc) */
|
|
8
|
+
declare const legacyConfig: Linter.LegacyConfig;
|
|
9
|
+
/** ESLint-плагин `@foxford/analytics` для валидации разметки data-fxa-* в JSX */
|
|
10
|
+
declare const plugin: ESLint.Plugin & {
|
|
11
|
+
configs: {
|
|
12
|
+
recommended: Linter.Config;
|
|
13
|
+
legacy: Linter.LegacyConfig;
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export { plugin as default, legacyConfig, recommendedConfig, rules };
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { ESLint, Linter, Rule } from 'eslint';
|
|
2
|
+
|
|
3
|
+
/** Карта правил плагина */
|
|
4
|
+
declare const rules: Record<string, Rule.RuleModule>;
|
|
5
|
+
/** Рекомендованная конфигурация плагина для ESLint 9 (flat config, eslint.config.mjs) */
|
|
6
|
+
declare const recommendedConfig: Linter.Config;
|
|
7
|
+
/** Конфигурация плагина для ESLint 8 (legacy .eslintrc) */
|
|
8
|
+
declare const legacyConfig: Linter.LegacyConfig;
|
|
9
|
+
/** ESLint-плагин `@foxford/analytics` для валидации разметки data-fxa-* в JSX */
|
|
10
|
+
declare const plugin: ESLint.Plugin & {
|
|
11
|
+
configs: {
|
|
12
|
+
recommended: Linter.Config;
|
|
13
|
+
legacy: Linter.LegacyConfig;
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export { plugin as default, legacyConfig, recommendedConfig, rules };
|
package/index.js
ADDED
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
// src/utils/jsx-helpers.ts
|
|
2
|
+
function getJSXAttributeStringValue(attr) {
|
|
3
|
+
const { value } = attr;
|
|
4
|
+
if (!value) return null;
|
|
5
|
+
if (value.type === "Literal" && typeof value.value === "string") {
|
|
6
|
+
return value.value;
|
|
7
|
+
}
|
|
8
|
+
if (value.type === "JSXExpressionContainer" && value.expression.type === "Literal" && typeof value.expression.value === "string") {
|
|
9
|
+
return value.expression.value;
|
|
10
|
+
}
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
function getJSXElementName(node) {
|
|
14
|
+
const { name } = node;
|
|
15
|
+
if (name.type === "JSXIdentifier") {
|
|
16
|
+
return name.name;
|
|
17
|
+
}
|
|
18
|
+
if (name.type === "JSXNamespacedName") {
|
|
19
|
+
return `${name.namespace.name}:${name.name.name}`;
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
function findJSXAttribute(node, attrName) {
|
|
24
|
+
return node.attributes.find(
|
|
25
|
+
(attr) => attr.type === "JSXAttribute" && attr.name.type === "JSXIdentifier" && attr.name.name === attrName
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
function hasJSXAttribute(node, attrName) {
|
|
29
|
+
return findJSXAttribute(node, attrName) !== void 0;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// src/utils/registry-loader.ts
|
|
33
|
+
import fs from "fs";
|
|
34
|
+
import { createRequire } from "module";
|
|
35
|
+
|
|
36
|
+
// ../analytics-registries/src/registries/actions.json
|
|
37
|
+
var actions_default = [
|
|
38
|
+
"buy",
|
|
39
|
+
"open",
|
|
40
|
+
"close",
|
|
41
|
+
"submit-order",
|
|
42
|
+
"toggle",
|
|
43
|
+
"select-day",
|
|
44
|
+
"apply-promo",
|
|
45
|
+
"accept-invite",
|
|
46
|
+
"favorite",
|
|
47
|
+
"confirm",
|
|
48
|
+
"cancel",
|
|
49
|
+
"nav-profile",
|
|
50
|
+
"nav-settings",
|
|
51
|
+
"nav-billing",
|
|
52
|
+
"logout",
|
|
53
|
+
"select-tab",
|
|
54
|
+
"prev-month",
|
|
55
|
+
"next-month",
|
|
56
|
+
"input-name",
|
|
57
|
+
"input-email",
|
|
58
|
+
"select-card",
|
|
59
|
+
"select-sbp"
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
// ../analytics-registries/src/registries/kinds.json
|
|
63
|
+
var kinds_default = [
|
|
64
|
+
"modal",
|
|
65
|
+
"card",
|
|
66
|
+
"accordion",
|
|
67
|
+
"tabs",
|
|
68
|
+
"banner",
|
|
69
|
+
"tooltip",
|
|
70
|
+
"dropdown",
|
|
71
|
+
"menu",
|
|
72
|
+
"calendar"
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
// ../analytics-registries/src/registries/paths.json
|
|
76
|
+
var paths_default = [
|
|
77
|
+
"dashboard",
|
|
78
|
+
"checkout",
|
|
79
|
+
"course-card",
|
|
80
|
+
"invite-banner",
|
|
81
|
+
"my-courses",
|
|
82
|
+
"main-nav",
|
|
83
|
+
"search-results",
|
|
84
|
+
"site-header",
|
|
85
|
+
"recommendations",
|
|
86
|
+
"reschedule",
|
|
87
|
+
"user-menu",
|
|
88
|
+
"faq",
|
|
89
|
+
"date-picker",
|
|
90
|
+
"days-grid",
|
|
91
|
+
"footer",
|
|
92
|
+
"personal",
|
|
93
|
+
"payment",
|
|
94
|
+
"method",
|
|
95
|
+
"items"
|
|
96
|
+
];
|
|
97
|
+
|
|
98
|
+
// ../analytics-registries/src/types.ts
|
|
99
|
+
var actions = actions_default;
|
|
100
|
+
var kinds = kinds_default;
|
|
101
|
+
var paths = paths_default;
|
|
102
|
+
|
|
103
|
+
// ../analytics-registries/src/validation.ts
|
|
104
|
+
var MAX_VALUE_LENGTH = 32;
|
|
105
|
+
function sanitizeValue(value) {
|
|
106
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, MAX_VALUE_LENGTH);
|
|
107
|
+
}
|
|
108
|
+
var actionsSet = new Set(actions);
|
|
109
|
+
var kindsSet = new Set(kinds);
|
|
110
|
+
var pathsSet = new Set(paths);
|
|
111
|
+
|
|
112
|
+
// ../analytics-registries/src/constants.ts
|
|
113
|
+
var COMBO_TAG_WHITELIST = ["a", "form", "button", "label", "summary"];
|
|
114
|
+
|
|
115
|
+
// src/utils/registry-loader.ts
|
|
116
|
+
var REGISTRIES = {
|
|
117
|
+
actions,
|
|
118
|
+
kinds,
|
|
119
|
+
paths
|
|
120
|
+
};
|
|
121
|
+
var requireModule = createRequire(import.meta.url);
|
|
122
|
+
function loadRegistry(name) {
|
|
123
|
+
return REGISTRIES[name];
|
|
124
|
+
}
|
|
125
|
+
function getRegistryMtime(_name) {
|
|
126
|
+
try {
|
|
127
|
+
const pkgPath = requireModule.resolve("@foxford/analytics-registries/package.json");
|
|
128
|
+
return fs.statSync(pkgPath).mtime;
|
|
129
|
+
} catch {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// src/rules/known-fxa-value.ts
|
|
135
|
+
var ATTR_TO_REGISTRY = {
|
|
136
|
+
"data-fxa-action": "actions",
|
|
137
|
+
"data-fxa-kind": "kinds",
|
|
138
|
+
"data-fxa-path": "paths"
|
|
139
|
+
};
|
|
140
|
+
var KNOWN_SETS = {
|
|
141
|
+
actions: new Set(loadRegistry("actions")),
|
|
142
|
+
kinds: new Set(loadRegistry("kinds")),
|
|
143
|
+
paths: new Set(loadRegistry("paths"))
|
|
144
|
+
};
|
|
145
|
+
var rule = {
|
|
146
|
+
create(context) {
|
|
147
|
+
return {
|
|
148
|
+
JSXOpeningElement(rawNode) {
|
|
149
|
+
const node = rawNode;
|
|
150
|
+
for (const [attrName, registryName] of Object.entries(ATTR_TO_REGISTRY)) {
|
|
151
|
+
const attr = findJSXAttribute(node, attrName);
|
|
152
|
+
if (!attr) continue;
|
|
153
|
+
const value = getJSXAttributeStringValue(attr);
|
|
154
|
+
if (value === null) continue;
|
|
155
|
+
const registry = loadRegistry(registryName);
|
|
156
|
+
const knownSet = KNOWN_SETS[registryName];
|
|
157
|
+
if (!knownSet.has(value)) {
|
|
158
|
+
const known = registry.slice(0, 10).join(", ") + (registry.length > 10 ? ", ..." : "");
|
|
159
|
+
context.report({
|
|
160
|
+
data: { attr: attrName, known, value },
|
|
161
|
+
messageId: "unknownValue",
|
|
162
|
+
node: attr
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
},
|
|
169
|
+
meta: {
|
|
170
|
+
docs: {
|
|
171
|
+
description: "\u041F\u0440\u043E\u0432\u0435\u0440\u044F\u0435\u0442 \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u044F \u0430\u0442\u0440\u0438\u0431\u0443\u0442\u043E\u0432 data-fxa-* \u043F\u043E \u0440\u0435\u0435\u0441\u0442\u0440\u0430\u043C @foxford/analytics-registries",
|
|
172
|
+
recommended: true,
|
|
173
|
+
url: "https://github.com/foxford/frontend-platform"
|
|
174
|
+
},
|
|
175
|
+
messages: {
|
|
176
|
+
unknownValue: '\u041D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043D\u043E\u0435 \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435 "{{value}}" \u0434\u043B\u044F {{attr}}. \u0414\u043E\u043F\u0443\u0441\u0442\u0438\u043C\u044B\u0435 \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u044F: {{known}}'
|
|
177
|
+
},
|
|
178
|
+
schema: [],
|
|
179
|
+
type: "problem"
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
// src/rules/no-useless-fxa-kind.ts
|
|
184
|
+
var SEMANTIC_TAGS_WITH_TYPE = /* @__PURE__ */ new Set([
|
|
185
|
+
"form",
|
|
186
|
+
"nav",
|
|
187
|
+
"header",
|
|
188
|
+
"footer",
|
|
189
|
+
"main",
|
|
190
|
+
"aside",
|
|
191
|
+
"section",
|
|
192
|
+
"article",
|
|
193
|
+
"dialog",
|
|
194
|
+
"figure",
|
|
195
|
+
"details"
|
|
196
|
+
]);
|
|
197
|
+
var rule2 = {
|
|
198
|
+
create(context) {
|
|
199
|
+
return {
|
|
200
|
+
JSXOpeningElement(rawNode) {
|
|
201
|
+
const node = rawNode;
|
|
202
|
+
const kindAttr = findJSXAttribute(node, "data-fxa-kind");
|
|
203
|
+
if (!kindAttr) return;
|
|
204
|
+
const kindNode = kindAttr;
|
|
205
|
+
const tagName = getJSXElementName(node);
|
|
206
|
+
if (tagName !== null && SEMANTIC_TAGS_WITH_TYPE.has(tagName)) {
|
|
207
|
+
context.report({
|
|
208
|
+
data: { tag: tagName },
|
|
209
|
+
messageId: "redundantKindOnSemanticTag",
|
|
210
|
+
node: kindNode
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
if (!hasJSXAttribute(node, "data-fxa-path")) {
|
|
214
|
+
context.report({
|
|
215
|
+
messageId: "kindWithoutPath",
|
|
216
|
+
node: kindNode
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
},
|
|
222
|
+
meta: {
|
|
223
|
+
docs: {
|
|
224
|
+
description: "\u041F\u0440\u0435\u0434\u0443\u043F\u0440\u0435\u0436\u0434\u0430\u0435\u0442 \u043E\u0431 \u0438\u0437\u0431\u044B\u0442\u043E\u0447\u043D\u043E\u043C data-fxa-kind: \u0431\u0435\u0437 data-fxa-path \u0438\u043B\u0438 \u043D\u0430 \u0441\u0435\u043C\u0430\u043D\u0442\u0438\u0447\u0435\u0441\u043A\u043E\u043C \u0442\u0435\u0433\u0435",
|
|
225
|
+
recommended: true,
|
|
226
|
+
url: "https://github.com/foxford/frontend-platform"
|
|
227
|
+
},
|
|
228
|
+
messages: {
|
|
229
|
+
kindWithoutPath: "data-fxa-kind \u0431\u0435\u0437 data-fxa-path \u043D\u0430 \u0442\u043E\u043C \u0436\u0435 \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u0435 \u043D\u0435 \u0438\u043C\u0435\u0435\u0442 \u0441\u043C\u044B\u0441\u043B\u0430",
|
|
230
|
+
redundantKindOnSemanticTag: "\u0418\u0437\u0431\u044B\u0442\u043E\u0447\u043D\u044B\u0439 data-fxa-kind \u043D\u0430 \u0442\u0435\u0433\u0435 <{{tag}}> \u2014 \u0442\u0435\u0433 \u0443\u0436\u0435 \u043F\u0440\u0435\u0434\u043E\u0441\u0442\u0430\u0432\u043B\u044F\u0435\u0442 \u0441\u0435\u043C\u0430\u043D\u0442\u0438\u0447\u0435\u0441\u043A\u0438\u0439 \u0442\u0438\u043F"
|
|
231
|
+
},
|
|
232
|
+
schema: [],
|
|
233
|
+
type: "suggestion"
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
// src/rules/registry-freshness.ts
|
|
238
|
+
var MAX_REGISTRY_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
239
|
+
var REGISTRY_NAMES = ["actions", "kinds", "paths"];
|
|
240
|
+
var freshnessCheckResult = null;
|
|
241
|
+
var rule3 = {
|
|
242
|
+
create(context) {
|
|
243
|
+
return {
|
|
244
|
+
Program(node) {
|
|
245
|
+
if (!freshnessCheckResult) {
|
|
246
|
+
const now = Date.now();
|
|
247
|
+
const stale = [];
|
|
248
|
+
for (const name of REGISTRY_NAMES) {
|
|
249
|
+
const mtime = getRegistryMtime(name);
|
|
250
|
+
if (mtime !== null && now - mtime.getTime() > MAX_REGISTRY_AGE_MS) {
|
|
251
|
+
stale.push(name);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
freshnessCheckResult = { stale };
|
|
255
|
+
}
|
|
256
|
+
if (freshnessCheckResult.stale.length > 0) {
|
|
257
|
+
context.report({
|
|
258
|
+
data: { registries: freshnessCheckResult.stale.join(", ") },
|
|
259
|
+
messageId: "staleRegistry",
|
|
260
|
+
node
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
},
|
|
266
|
+
meta: {
|
|
267
|
+
docs: {
|
|
268
|
+
description: "\u041F\u0440\u0435\u0434\u0443\u043F\u0440\u0435\u0436\u0434\u0430\u0435\u0442 \u043F\u0440\u0438 \u0443\u0441\u0442\u0430\u0440\u0435\u0432\u0448\u0438\u0445 \u0440\u0435\u0435\u0441\u0442\u0440\u0430\u0445 data-fxa-* (\u043D\u0435 \u043E\u0431\u043D\u043E\u0432\u043B\u044F\u043B\u0438\u0441\u044C >7 \u0434\u043D\u0435\u0439)",
|
|
269
|
+
recommended: true,
|
|
270
|
+
url: "https://github.com/foxford/frontend-platform"
|
|
271
|
+
},
|
|
272
|
+
messages: {
|
|
273
|
+
staleRegistry: "\u0420\u0435\u0435\u0441\u0442\u0440 {{registries}} \u043D\u0435 \u043E\u0431\u043D\u043E\u0432\u043B\u044F\u043B\u0441\u044F \u0431\u043E\u043B\u0435\u0435 7 \u0434\u043D\u0435\u0439. \u041F\u0440\u043E\u0432\u0435\u0440\u044C\u0442\u0435 \u0430\u043A\u0442\u0443\u0430\u043B\u044C\u043D\u043E\u0441\u0442\u044C \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0439 data-fxa-*."
|
|
274
|
+
},
|
|
275
|
+
schema: [],
|
|
276
|
+
type: "suggestion"
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
// src/rules/valid-fxa-combo.ts
|
|
281
|
+
var COMBO_ALLOWED_TAGS = new Set(COMBO_TAG_WHITELIST);
|
|
282
|
+
var rule4 = {
|
|
283
|
+
create(context) {
|
|
284
|
+
return {
|
|
285
|
+
JSXOpeningElement(rawNode) {
|
|
286
|
+
const node = rawNode;
|
|
287
|
+
const hasPath = hasJSXAttribute(node, "data-fxa-path");
|
|
288
|
+
const hasAction = hasJSXAttribute(node, "data-fxa-action");
|
|
289
|
+
if (!hasPath || !hasAction) return;
|
|
290
|
+
const tagName = getJSXElementName(node);
|
|
291
|
+
if (tagName === null) return;
|
|
292
|
+
if (COMBO_ALLOWED_TAGS.has(tagName)) return;
|
|
293
|
+
const actionAttr = findJSXAttribute(node, "data-fxa-action");
|
|
294
|
+
const reportNode = actionAttr ?? rawNode;
|
|
295
|
+
context.report({
|
|
296
|
+
data: {
|
|
297
|
+
allowed: Array.from(COMBO_ALLOWED_TAGS).join(", "),
|
|
298
|
+
tag: tagName
|
|
299
|
+
},
|
|
300
|
+
messageId: "invalidCombo",
|
|
301
|
+
node: reportNode
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
},
|
|
306
|
+
meta: {
|
|
307
|
+
docs: {
|
|
308
|
+
description: "Combo data-fxa-path + data-fxa-action \u0440\u0430\u0437\u0440\u0435\u0448\u0451\u043D \u0442\u043E\u043B\u044C\u043A\u043E \u043D\u0430 \u0442\u0435\u0433\u0430\u0445: a, form, button, label, summary",
|
|
309
|
+
recommended: true,
|
|
310
|
+
url: "https://github.com/foxford/frontend-platform"
|
|
311
|
+
},
|
|
312
|
+
messages: {
|
|
313
|
+
invalidCombo: "Combo data-fxa-path + data-fxa-action \u043D\u0430 \u0442\u0435\u0433\u0435 <{{tag}}> \u043D\u0435\u0434\u043E\u043F\u0443\u0441\u0442\u0438\u043C. \u0420\u0430\u0437\u0440\u0435\u0448\u0451\u043D\u043D\u044B\u0435 \u0442\u0435\u0433\u0438: {{allowed}}"
|
|
314
|
+
},
|
|
315
|
+
schema: [],
|
|
316
|
+
type: "problem"
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
// src/rules/valid-fxa-format.ts
|
|
321
|
+
var CHECKED_ATTRIBUTES = ["data-fxa-path", "data-fxa-action", "data-fxa-kind"];
|
|
322
|
+
var FORMAT_REGEX = /^[a-z0-9-]+$/;
|
|
323
|
+
var MAX_LENGTH = 32;
|
|
324
|
+
var rule5 = {
|
|
325
|
+
create(context) {
|
|
326
|
+
return {
|
|
327
|
+
JSXOpeningElement(rawNode) {
|
|
328
|
+
const node = rawNode;
|
|
329
|
+
for (const attrName of CHECKED_ATTRIBUTES) {
|
|
330
|
+
const attr = findJSXAttribute(node, attrName);
|
|
331
|
+
if (!attr) continue;
|
|
332
|
+
const value = getJSXAttributeStringValue(attr);
|
|
333
|
+
if (value === null) continue;
|
|
334
|
+
const isValid = value.length > 0 && value.length <= MAX_LENGTH && FORMAT_REGEX.test(value);
|
|
335
|
+
if (!isValid) {
|
|
336
|
+
const sanitized = sanitizeValue(value);
|
|
337
|
+
context.report({
|
|
338
|
+
data: { attr: attrName, value },
|
|
339
|
+
messageId: "invalidFormat",
|
|
340
|
+
node: attr,
|
|
341
|
+
suggest: sanitized ? [
|
|
342
|
+
{
|
|
343
|
+
data: { sanitized },
|
|
344
|
+
fix(fixer) {
|
|
345
|
+
const attrValue = attr.value;
|
|
346
|
+
if (!attrValue) return null;
|
|
347
|
+
if (attrValue.type === "Literal") {
|
|
348
|
+
return fixer.replaceText(attrValue, `"${sanitized}"`);
|
|
349
|
+
}
|
|
350
|
+
if (attrValue.type === "JSXExpressionContainer") {
|
|
351
|
+
return fixer.replaceText(attrValue, `"${sanitized}"`);
|
|
352
|
+
}
|
|
353
|
+
return null;
|
|
354
|
+
},
|
|
355
|
+
messageId: "suggestFix"
|
|
356
|
+
}
|
|
357
|
+
] : []
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
},
|
|
364
|
+
meta: {
|
|
365
|
+
docs: {
|
|
366
|
+
description: "\u041F\u0440\u043E\u0432\u0435\u0440\u044F\u0435\u0442 \u0444\u043E\u0440\u043C\u0430\u0442 \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0439 \u0430\u0442\u0440\u0438\u0431\u0443\u0442\u043E\u0432 data-fxa-* (regex ^[a-z0-9-]+$, \u0434\u043B\u0438\u043D\u0430 \u226432)",
|
|
367
|
+
recommended: true,
|
|
368
|
+
url: "https://github.com/foxford/frontend-platform"
|
|
369
|
+
},
|
|
370
|
+
hasSuggestions: true,
|
|
371
|
+
messages: {
|
|
372
|
+
invalidFormat: '\u0417\u043D\u0430\u0447\u0435\u043D\u0438\u0435 "{{value}}" \u0430\u0442\u0440\u0438\u0431\u0443\u0442\u0430 {{attr}} \u043D\u0435 \u0441\u043E\u043E\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u0435\u0442 \u0444\u043E\u0440\u043C\u0430\u0442\u0443 ^[a-z0-9-]+$ \u0438\u043B\u0438 \u043F\u0440\u0435\u0432\u044B\u0448\u0430\u0435\u0442 32 \u0441\u0438\u043C\u0432\u043E\u043B\u0430',
|
|
373
|
+
suggestFix: '\u0417\u0430\u043C\u0435\u043D\u0438\u0442\u044C \u043D\u0430 \u0441\u0430\u043D\u0438\u0442\u0438\u0437\u0438\u0440\u043E\u0432\u0430\u043D\u043D\u043E\u0435 \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435 "{{sanitized}}"'
|
|
374
|
+
},
|
|
375
|
+
schema: [],
|
|
376
|
+
type: "problem"
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
// package.json
|
|
381
|
+
var package_default = {
|
|
382
|
+
name: "@foxford/eslint-plugin-analytics",
|
|
383
|
+
version: "1.0.1",
|
|
384
|
+
description: "ESLint-\u043F\u043B\u0430\u0433\u0438\u043D \u0434\u043B\u044F \u0432\u0430\u043B\u0438\u0434\u0430\u0446\u0438\u0438 \u0440\u0430\u0437\u043C\u0435\u0442\u043A\u0438 data-fxa-* \u0432 JSX (\u0441\u0438\u0441\u0442\u0435\u043C\u0430 \u0434\u0435\u043A\u043B\u0430\u0440\u0430\u0442\u0438\u0432\u043D\u043E\u0439 \u0430\u043D\u0430\u043B\u0438\u0442\u0438\u043A\u0438 Foxford)",
|
|
385
|
+
keywords: [
|
|
386
|
+
"foxford",
|
|
387
|
+
"eslint-plugin",
|
|
388
|
+
"analytics",
|
|
389
|
+
"fxa"
|
|
390
|
+
],
|
|
391
|
+
homepage: "https://github.com/foxford/frontend-platform",
|
|
392
|
+
bugs: "https://github.com/foxford/frontend-platform/issues",
|
|
393
|
+
repository: {
|
|
394
|
+
type: "git",
|
|
395
|
+
url: "git+https://github.com/foxford/frontend-platform.git",
|
|
396
|
+
directory: "packages/eslint-plugin-analytics"
|
|
397
|
+
},
|
|
398
|
+
license: "MIT",
|
|
399
|
+
author: "Foxford Co",
|
|
400
|
+
maintainers: [
|
|
401
|
+
{
|
|
402
|
+
name: "Roman Olin",
|
|
403
|
+
email: "r.v.olin@foxford.ru",
|
|
404
|
+
team: "core-frontend"
|
|
405
|
+
}
|
|
406
|
+
],
|
|
407
|
+
main: "src/index.ts",
|
|
408
|
+
type: "module",
|
|
409
|
+
exports: {
|
|
410
|
+
".": {
|
|
411
|
+
import: "./src/index.ts",
|
|
412
|
+
default: "./src/index.ts"
|
|
413
|
+
}
|
|
414
|
+
},
|
|
415
|
+
files: [
|
|
416
|
+
"build"
|
|
417
|
+
],
|
|
418
|
+
scripts: {
|
|
419
|
+
"============================ BUILD =============================": "",
|
|
420
|
+
build: "tsup",
|
|
421
|
+
"============================ DEPLOY ============================": "",
|
|
422
|
+
deploy: "fox deploy npm",
|
|
423
|
+
"deploy:npm": "fox deploy npm --local-dir=build",
|
|
424
|
+
"============================ LINT ==============================": "",
|
|
425
|
+
"lint:clean": "rm -f .eslintcache",
|
|
426
|
+
"lint:type-check": "tsc --noEmit --pretty",
|
|
427
|
+
"lint:eslint": "eslint --cache --cache-strategy content --max-warnings=0 ./",
|
|
428
|
+
"============================ TEST ==============================": "",
|
|
429
|
+
test: "vitest run",
|
|
430
|
+
"test:watch": "vitest"
|
|
431
|
+
},
|
|
432
|
+
dependencies: {
|
|
433
|
+
"@foxford/analytics-registries": "workspace:*"
|
|
434
|
+
},
|
|
435
|
+
peerDependencies: {
|
|
436
|
+
eslint: ">=8.0.0"
|
|
437
|
+
},
|
|
438
|
+
devDependencies: {
|
|
439
|
+
"@foxford/eslint-config": "workspace:*",
|
|
440
|
+
"@foxford/prettier-config": "workspace:*",
|
|
441
|
+
"@foxford/typescript-config": "workspace:*",
|
|
442
|
+
"@foxford/vitest-config": "workspace:*",
|
|
443
|
+
"@types/node": "catalog:dev",
|
|
444
|
+
eslint: "catalog:linter",
|
|
445
|
+
tsup: "catalog:dev",
|
|
446
|
+
typescript: "catalog:dev",
|
|
447
|
+
vitest: "catalog:dev"
|
|
448
|
+
}
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
// src/index.ts
|
|
452
|
+
var { version } = package_default;
|
|
453
|
+
var rules = {
|
|
454
|
+
"known-fxa-value": rule,
|
|
455
|
+
"no-useless-fxa-kind": rule2,
|
|
456
|
+
"registry-freshness": rule3,
|
|
457
|
+
"valid-fxa-combo": rule4,
|
|
458
|
+
"valid-fxa-format": rule5
|
|
459
|
+
};
|
|
460
|
+
var recommendedConfig = {
|
|
461
|
+
plugins: {
|
|
462
|
+
"@foxford/analytics": { rules }
|
|
463
|
+
},
|
|
464
|
+
rules: {
|
|
465
|
+
"@foxford/analytics/known-fxa-value": "error",
|
|
466
|
+
"@foxford/analytics/no-useless-fxa-kind": "warn",
|
|
467
|
+
"@foxford/analytics/registry-freshness": "warn",
|
|
468
|
+
"@foxford/analytics/valid-fxa-combo": "error",
|
|
469
|
+
"@foxford/analytics/valid-fxa-format": "error"
|
|
470
|
+
}
|
|
471
|
+
};
|
|
472
|
+
var legacyConfig = {
|
|
473
|
+
plugins: ["@foxford/analytics"],
|
|
474
|
+
rules: {
|
|
475
|
+
"@foxford/analytics/known-fxa-value": "error",
|
|
476
|
+
"@foxford/analytics/no-useless-fxa-kind": "warn",
|
|
477
|
+
"@foxford/analytics/registry-freshness": "warn",
|
|
478
|
+
"@foxford/analytics/valid-fxa-combo": "error",
|
|
479
|
+
"@foxford/analytics/valid-fxa-format": "error"
|
|
480
|
+
}
|
|
481
|
+
};
|
|
482
|
+
var plugin = {
|
|
483
|
+
configs: {
|
|
484
|
+
legacy: legacyConfig,
|
|
485
|
+
recommended: recommendedConfig
|
|
486
|
+
},
|
|
487
|
+
meta: {
|
|
488
|
+
name: "@foxford/eslint-plugin-analytics",
|
|
489
|
+
version
|
|
490
|
+
},
|
|
491
|
+
rules
|
|
492
|
+
};
|
|
493
|
+
export {
|
|
494
|
+
plugin as default,
|
|
495
|
+
legacyConfig,
|
|
496
|
+
recommendedConfig,
|
|
497
|
+
rules
|
|
498
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,53 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@foxford/eslint-plugin-analytics",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "ESLint-плагин для валидации разметки data-fxa-* в JSX (система декларативной аналитики Foxford)",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"foxford",
|
|
7
|
+
"eslint-plugin",
|
|
8
|
+
"analytics",
|
|
9
|
+
"fxa"
|
|
10
|
+
],
|
|
11
|
+
"homepage": "https://github.com/foxford/frontend-platform",
|
|
12
|
+
"bugs": "https://github.com/foxford/frontend-platform/issues",
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+https://github.com/foxford/frontend-platform.git",
|
|
16
|
+
"directory": "packages/eslint-plugin-analytics"
|
|
17
|
+
},
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"author": "Foxford Co",
|
|
20
|
+
"maintainers": [
|
|
21
|
+
{
|
|
22
|
+
"name": "Roman Olin",
|
|
23
|
+
"email": "r.v.olin@foxford.ru",
|
|
24
|
+
"team": "core-frontend"
|
|
25
|
+
}
|
|
26
|
+
],
|
|
27
|
+
"type": "module",
|
|
28
|
+
"files": [
|
|
29
|
+
"*.js",
|
|
30
|
+
"*.cjs",
|
|
31
|
+
"*.d.ts",
|
|
32
|
+
"*.d.cts",
|
|
33
|
+
"README.mdx"
|
|
34
|
+
],
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"@foxford/analytics-registries": "1.0.1"
|
|
37
|
+
},
|
|
38
|
+
"peerDependencies": {
|
|
39
|
+
"eslint": ">=8.0.0"
|
|
40
|
+
},
|
|
41
|
+
"main": "./index.cjs",
|
|
42
|
+
"module": "./index.js",
|
|
43
|
+
"types": "./index.d.ts",
|
|
44
|
+
"exports": {
|
|
45
|
+
".": {
|
|
46
|
+
"import": "./index.js",
|
|
47
|
+
"require": "./index.cjs",
|
|
48
|
+
"types": "./index.d.ts"
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
"sha": "6bf9ff8",
|
|
52
|
+
"scripts": {}
|
|
6
53
|
}
|
package/README.md
DELETED