@sanity/schema-lint 0.0.1 → 0.0.3
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/dist/index.cjs +640 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +133 -0
- package/dist/testing.cjs +18530 -0
- package/dist/testing.cjs.map +1 -0
- package/dist/testing.d.cts +81 -0
- package/dist/types-C3OVyCP6.d.cts +121 -0
- package/package.json +7 -5
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,640 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
arrayMissingConstraints: () => arrayMissingConstraints,
|
|
24
|
+
booleanInsteadOfList: () => booleanInsteadOfList,
|
|
25
|
+
headingLevelInSchema: () => headingLevelInSchema,
|
|
26
|
+
lint: () => lint,
|
|
27
|
+
lintSchemas: () => lintSchemas,
|
|
28
|
+
missingDefineField: () => missingDefineField,
|
|
29
|
+
missingDefineType: () => missingDefineType,
|
|
30
|
+
missingDescription: () => missingDescription,
|
|
31
|
+
missingIcon: () => missingIcon,
|
|
32
|
+
missingRequiredValidation: () => missingRequiredValidation,
|
|
33
|
+
missingSlugSource: () => missingSlugSource,
|
|
34
|
+
missingTitle: () => missingTitle,
|
|
35
|
+
presentationFieldName: () => presentationFieldName,
|
|
36
|
+
reservedFieldName: () => reservedFieldName,
|
|
37
|
+
rules: () => rules,
|
|
38
|
+
unnecessaryReference: () => unnecessaryReference
|
|
39
|
+
});
|
|
40
|
+
module.exports = __toCommonJS(index_exports);
|
|
41
|
+
|
|
42
|
+
// src/linter.ts
|
|
43
|
+
function getEffectiveSeverity(rule, config) {
|
|
44
|
+
if (!config?.rules) {
|
|
45
|
+
return rule.severity;
|
|
46
|
+
}
|
|
47
|
+
const ruleConfig = config.rules[rule.id];
|
|
48
|
+
if (ruleConfig === false) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
if (ruleConfig === true) {
|
|
52
|
+
return rule.severity;
|
|
53
|
+
}
|
|
54
|
+
if (typeof ruleConfig === "object") {
|
|
55
|
+
if (ruleConfig.enabled === false) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
return ruleConfig.severity ?? rule.severity;
|
|
59
|
+
}
|
|
60
|
+
return rule.severity;
|
|
61
|
+
}
|
|
62
|
+
function lint(schema, allRules, options = {}) {
|
|
63
|
+
const { rules: rules2 = allRules, config, filePath = "" } = options;
|
|
64
|
+
const firedRules = /* @__PURE__ */ new Set();
|
|
65
|
+
const allFindings = [];
|
|
66
|
+
for (const rule of rules2) {
|
|
67
|
+
const severity = getEffectiveSeverity(rule, config);
|
|
68
|
+
if (severity === null) {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
const ruleFindings = [];
|
|
72
|
+
const context = {
|
|
73
|
+
filePath,
|
|
74
|
+
report: (finding) => {
|
|
75
|
+
ruleFindings.push({
|
|
76
|
+
...finding,
|
|
77
|
+
ruleId: rule.id,
|
|
78
|
+
severity: finding.severity ?? severity
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
try {
|
|
83
|
+
rule.check(schema, context);
|
|
84
|
+
} catch (error) {
|
|
85
|
+
console.error(`Rule ${rule.id} threw an error:`, error);
|
|
86
|
+
}
|
|
87
|
+
if (ruleFindings.length > 0) {
|
|
88
|
+
firedRules.add(rule.id);
|
|
89
|
+
allFindings.push(...ruleFindings);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
const findings = allFindings.filter((finding) => {
|
|
93
|
+
const isSuperseded = rules2.some(
|
|
94
|
+
(r) => r.supersedes?.includes(finding.ruleId) && firedRules.has(r.id)
|
|
95
|
+
);
|
|
96
|
+
return !isSuperseded;
|
|
97
|
+
});
|
|
98
|
+
return { findings };
|
|
99
|
+
}
|
|
100
|
+
function lintSchemas(schemas, allRules, options = {}) {
|
|
101
|
+
const findings = [];
|
|
102
|
+
for (const schema of schemas) {
|
|
103
|
+
const result = lint(schema, allRules, options);
|
|
104
|
+
findings.push(...result.findings);
|
|
105
|
+
}
|
|
106
|
+
return { findings };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// src/rules/missing-define-type.ts
|
|
110
|
+
var missingDefineType = {
|
|
111
|
+
id: "missing-define-type",
|
|
112
|
+
name: "Missing defineType()",
|
|
113
|
+
description: "Schema types should use defineType() wrapper",
|
|
114
|
+
severity: "error",
|
|
115
|
+
category: "correctness",
|
|
116
|
+
check(schema, context) {
|
|
117
|
+
if (!schema.usesDefineType) {
|
|
118
|
+
context.report({
|
|
119
|
+
message: `Schema type "${schema.name}" should be wrapped with defineType()`,
|
|
120
|
+
severity: "error",
|
|
121
|
+
...schema.span && { span: schema.span },
|
|
122
|
+
help: 'Import defineType from "sanity" and wrap your schema: export const myType = defineType({ ... })'
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
// src/rules/missing-define-field.ts
|
|
129
|
+
var missingDefineField = {
|
|
130
|
+
id: "missing-define-field",
|
|
131
|
+
name: "Missing defineField()",
|
|
132
|
+
description: "Fields should use defineField() wrapper",
|
|
133
|
+
severity: "error",
|
|
134
|
+
category: "correctness",
|
|
135
|
+
check(schema, context) {
|
|
136
|
+
if (!schema.usesDefineType) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (schema.usesDefineField === false && schema.fields && schema.fields.length > 0) {
|
|
140
|
+
context.report({
|
|
141
|
+
message: `Schema type "${schema.name}" has fields not wrapped with defineField()`,
|
|
142
|
+
severity: "error",
|
|
143
|
+
...schema.span && { span: schema.span },
|
|
144
|
+
help: 'Import defineField from "sanity" and wrap each field: defineField({ name: "...", type: "..." })'
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
// src/rules/missing-icon.ts
|
|
151
|
+
var missingIcon = {
|
|
152
|
+
id: "missing-icon",
|
|
153
|
+
name: "Missing icon",
|
|
154
|
+
description: "Document and object types should have an icon",
|
|
155
|
+
severity: "warning",
|
|
156
|
+
category: "style",
|
|
157
|
+
check(schema, context) {
|
|
158
|
+
if (schema.type !== "document" && schema.type !== "object") {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
if (!schema.hasIcon) {
|
|
162
|
+
context.report({
|
|
163
|
+
message: `Schema type "${schema.name}" should have an icon`,
|
|
164
|
+
severity: "warning",
|
|
165
|
+
...schema.span && { span: schema.span },
|
|
166
|
+
help: "Add an icon from @sanity/icons: icon: DocumentIcon"
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
// src/rules/missing-title.ts
|
|
173
|
+
var missingTitle = {
|
|
174
|
+
id: "missing-title",
|
|
175
|
+
name: "Missing title",
|
|
176
|
+
description: "Schema types should have a title property",
|
|
177
|
+
severity: "warning",
|
|
178
|
+
category: "style",
|
|
179
|
+
check(schema, context) {
|
|
180
|
+
if (schema.type !== "document" && schema.type !== "object") {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if (!schema.title) {
|
|
184
|
+
context.report({
|
|
185
|
+
message: `Schema type "${schema.name}" should have a title property`,
|
|
186
|
+
severity: "warning",
|
|
187
|
+
...schema.span && { span: schema.span },
|
|
188
|
+
help: 'Add a human-readable title: title: "Blog Post"'
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
// src/rules/missing-description.ts
|
|
195
|
+
var missingDescription = {
|
|
196
|
+
id: "missing-description",
|
|
197
|
+
name: "Missing field description",
|
|
198
|
+
description: "Fields should have descriptions for better editor UX",
|
|
199
|
+
severity: "info",
|
|
200
|
+
category: "style",
|
|
201
|
+
check(schema, context) {
|
|
202
|
+
if (!schema.fields) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
const fieldsWithoutDescription = [];
|
|
206
|
+
for (const field of schema.fields) {
|
|
207
|
+
if (field.hidden) {
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
if (["_id", "_type", "_rev", "_createdAt", "_updatedAt"].includes(field.name)) {
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
if (!field.description) {
|
|
214
|
+
fieldsWithoutDescription.push(field);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
const visibleFields = schema.fields.filter(
|
|
218
|
+
(f) => !f.hidden && !["_id", "_type", "_rev", "_createdAt", "_updatedAt"].includes(f.name)
|
|
219
|
+
);
|
|
220
|
+
if (fieldsWithoutDescription.length > 0 && fieldsWithoutDescription.length >= visibleFields.length / 2) {
|
|
221
|
+
const fieldNames = fieldsWithoutDescription.map((f) => f.name).slice(0, 3);
|
|
222
|
+
const moreCount = fieldsWithoutDescription.length - 3;
|
|
223
|
+
context.report({
|
|
224
|
+
message: `Fields in "${schema.name}" lack descriptions: ${fieldNames.join(", ")}${moreCount > 0 ? ` and ${moreCount} more` : ""}`,
|
|
225
|
+
severity: "info",
|
|
226
|
+
...schema.span && { span: schema.span },
|
|
227
|
+
help: "Add description to fields to help content editors understand what to enter"
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
// src/rules/presentation-field-name.ts
|
|
234
|
+
var PRESENTATION_PATTERNS = [
|
|
235
|
+
// Color-based names
|
|
236
|
+
/^(red|blue|green|yellow|white|black|gray|grey|dark|light)/i,
|
|
237
|
+
// Size-based names
|
|
238
|
+
/^(big|small|large|tiny|huge|medium)/i,
|
|
239
|
+
/(Big|Small|Large|Tiny|Huge|Medium)$/,
|
|
240
|
+
// Layout-based names
|
|
241
|
+
/^(left|right|center|top|bottom)/i,
|
|
242
|
+
/(Left|Right|Center|Top|Bottom)$/,
|
|
243
|
+
// Column/row names
|
|
244
|
+
/(two|three|four|five|six)(Column|Row)/i,
|
|
245
|
+
/^(column|row)\d+$/i,
|
|
246
|
+
// Explicit presentation terms
|
|
247
|
+
/(Button|Text|Image|Box|Container|Wrapper|Section|Block|Card|Banner|Hero)$/,
|
|
248
|
+
/^(hero|banner|sidebar|header|footer)/i
|
|
249
|
+
];
|
|
250
|
+
var SUGGESTIONS = {
|
|
251
|
+
bigText: "headline, title, heading",
|
|
252
|
+
smallText: "caption, subtitle, description",
|
|
253
|
+
redButton: "primaryAction, callToAction",
|
|
254
|
+
heroSection: "featuredContent, highlight",
|
|
255
|
+
leftColumn: "primaryContent, mainContent",
|
|
256
|
+
rightColumn: "secondaryContent, sidebar",
|
|
257
|
+
threeColumnRow: "featuresGrid, comparisonSection",
|
|
258
|
+
headerImage: "featuredImage, coverImage",
|
|
259
|
+
bannerText: "announcement, promotion"
|
|
260
|
+
};
|
|
261
|
+
var presentationFieldName = {
|
|
262
|
+
id: "presentation-field-name",
|
|
263
|
+
name: "Presentation-focused field name",
|
|
264
|
+
description: "Avoid presentation-focused field names; use semantic names instead",
|
|
265
|
+
severity: "warning",
|
|
266
|
+
category: "style",
|
|
267
|
+
check(schema, context) {
|
|
268
|
+
if (!schema.fields) {
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
for (const field of schema.fields) {
|
|
272
|
+
const name = field.name;
|
|
273
|
+
for (const pattern of PRESENTATION_PATTERNS) {
|
|
274
|
+
if (pattern.test(name)) {
|
|
275
|
+
const suggestion = SUGGESTIONS[name] || "a semantic name describing the content purpose";
|
|
276
|
+
context.report({
|
|
277
|
+
message: `Field "${name}" uses presentation-focused naming`,
|
|
278
|
+
severity: "warning",
|
|
279
|
+
...field.span && { span: field.span },
|
|
280
|
+
help: `Consider using ${suggestion}. Field names should describe content purpose, not visual presentation.`
|
|
281
|
+
});
|
|
282
|
+
break;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
// src/rules/boolean-instead-of-list.ts
|
|
290
|
+
var EXPANDABLE_BOOLEAN_PATTERNS = [
|
|
291
|
+
/^is[A-Z]/,
|
|
292
|
+
// isActive, isPublished
|
|
293
|
+
/^has[A-Z]/,
|
|
294
|
+
// hasFeatured, hasDiscount
|
|
295
|
+
/^show[A-Z]/,
|
|
296
|
+
// showBanner, showSidebar
|
|
297
|
+
/^enable[A-Z]/,
|
|
298
|
+
// enableComments, enableSharing
|
|
299
|
+
/^allow[A-Z]/,
|
|
300
|
+
// allowComments, allowGuests
|
|
301
|
+
/Status$/,
|
|
302
|
+
// publicationStatus, reviewStatus
|
|
303
|
+
/State$/,
|
|
304
|
+
// publishState, approvalState
|
|
305
|
+
/Mode$/
|
|
306
|
+
// displayMode, editMode
|
|
307
|
+
];
|
|
308
|
+
var booleanInsteadOfList = {
|
|
309
|
+
id: "boolean-instead-of-list",
|
|
310
|
+
name: "Boolean instead of list",
|
|
311
|
+
description: "Consider using options.list instead of boolean for expandable states",
|
|
312
|
+
severity: "info",
|
|
313
|
+
category: "style",
|
|
314
|
+
check(schema, context) {
|
|
315
|
+
if (!schema.fields) {
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
for (const field of schema.fields) {
|
|
319
|
+
if (field.type !== "boolean") {
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
const name = field.name;
|
|
323
|
+
for (const pattern of EXPANDABLE_BOOLEAN_PATTERNS) {
|
|
324
|
+
if (pattern.test(name)) {
|
|
325
|
+
context.report({
|
|
326
|
+
message: `Boolean field "${name}" might be better as a string with options.list`,
|
|
327
|
+
severity: "info",
|
|
328
|
+
...field.span && { span: field.span },
|
|
329
|
+
help: 'Consider using type: "string" with options: { list: [...], layout: "radio" } for future expandability'
|
|
330
|
+
});
|
|
331
|
+
break;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
// src/rules/missing-slug-source.ts
|
|
339
|
+
var missingSlugSource = {
|
|
340
|
+
id: "missing-slug-source",
|
|
341
|
+
name: "Missing slug source",
|
|
342
|
+
description: "Slug fields should have options.source for auto-generation",
|
|
343
|
+
severity: "warning",
|
|
344
|
+
category: "style",
|
|
345
|
+
check(schema, context) {
|
|
346
|
+
if (!schema.fields) {
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
for (const field of schema.fields) {
|
|
350
|
+
if (field.type !== "slug") {
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
if (!field.options?.source) {
|
|
354
|
+
context.report({
|
|
355
|
+
message: `Slug field "${field.name}" should have options.source`,
|
|
356
|
+
severity: "warning",
|
|
357
|
+
...field.span && { span: field.span },
|
|
358
|
+
help: 'Add options: { source: "title" } to auto-generate slug from another field'
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
// src/rules/missing-required-validation.ts
|
|
366
|
+
var TYPICALLY_REQUIRED_FIELDS = ["title", "name", "slug", "email", "url", "headline", "heading"];
|
|
367
|
+
var missingRequiredValidation = {
|
|
368
|
+
id: "missing-required-validation",
|
|
369
|
+
name: "Missing required validation",
|
|
370
|
+
description: "Critical fields should have validation rules",
|
|
371
|
+
severity: "warning",
|
|
372
|
+
category: "correctness",
|
|
373
|
+
check(schema, context) {
|
|
374
|
+
if (!schema.fields) {
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
if (schema.type !== "document") {
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
for (const field of schema.fields) {
|
|
381
|
+
const name = field.name.toLowerCase();
|
|
382
|
+
if (TYPICALLY_REQUIRED_FIELDS.includes(name) && !field.hasValidation) {
|
|
383
|
+
context.report({
|
|
384
|
+
message: `Field "${field.name}" in document type should have validation`,
|
|
385
|
+
severity: "warning",
|
|
386
|
+
...field.span && { span: field.span },
|
|
387
|
+
help: "Add validation: (rule) => rule.required() for critical fields"
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
// src/rules/reserved-field-name.ts
|
|
395
|
+
var RESERVED_PREFIXES = ["_"];
|
|
396
|
+
var RESERVED_NAMES = [
|
|
397
|
+
"_id",
|
|
398
|
+
"_type",
|
|
399
|
+
"_rev",
|
|
400
|
+
"_key",
|
|
401
|
+
"_createdAt",
|
|
402
|
+
"_updatedAt",
|
|
403
|
+
"_ref",
|
|
404
|
+
"_weak",
|
|
405
|
+
"_strengthenOnPublish"
|
|
406
|
+
];
|
|
407
|
+
var reservedFieldName = {
|
|
408
|
+
id: "reserved-field-name",
|
|
409
|
+
name: "Reserved field name",
|
|
410
|
+
description: "Field names starting with _ are reserved by Sanity",
|
|
411
|
+
severity: "error",
|
|
412
|
+
category: "correctness",
|
|
413
|
+
check(schema, context) {
|
|
414
|
+
if (!schema.fields) {
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
for (const field of schema.fields) {
|
|
418
|
+
const name = field.name;
|
|
419
|
+
for (const prefix of RESERVED_PREFIXES) {
|
|
420
|
+
if (name.startsWith(prefix)) {
|
|
421
|
+
const isExactReserved = RESERVED_NAMES.includes(name);
|
|
422
|
+
context.report({
|
|
423
|
+
message: `Field name "${name}" uses reserved prefix "${prefix}"`,
|
|
424
|
+
severity: "error",
|
|
425
|
+
...field.span && { span: field.span },
|
|
426
|
+
help: isExactReserved ? `"${name}" is a Sanity system field. Remove this field definition.` : `Field names starting with "${prefix}" are reserved. Use a different name.`
|
|
427
|
+
});
|
|
428
|
+
break;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
// src/rules/array-missing-constraints.ts
|
|
436
|
+
var arrayMissingConstraints = {
|
|
437
|
+
id: "array-missing-constraints",
|
|
438
|
+
name: "Array missing constraints",
|
|
439
|
+
description: "Array fields should have min/max/unique validation constraints",
|
|
440
|
+
severity: "info",
|
|
441
|
+
category: "style",
|
|
442
|
+
check(schema, context) {
|
|
443
|
+
if (!schema.fields) {
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
for (const field of schema.fields) {
|
|
447
|
+
if (field.type !== "array") {
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
if (field.hasValidation) {
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
context.report({
|
|
454
|
+
message: `Array field "${field.name}" has no validation constraints`,
|
|
455
|
+
severity: "info",
|
|
456
|
+
...field.span && { span: field.span },
|
|
457
|
+
help: "Consider adding validation: (rule) => rule.min(1).max(10).unique() to prevent unbounded arrays"
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
// src/rules/heading-level-in-schema.ts
|
|
464
|
+
var HEADING_LEVEL_PATTERNS = [/^(heading|header)Level$/i, /^h[1-6]$/i, /Level$/];
|
|
465
|
+
var HEADING_LEVEL_VALUES = ["h1", "h2", "h3", "h4", "h5", "h6"];
|
|
466
|
+
var headingLevelInSchema = {
|
|
467
|
+
id: "heading-level-in-schema",
|
|
468
|
+
name: "Heading level in schema",
|
|
469
|
+
description: "Heading levels should be computed dynamically, not stored in schema",
|
|
470
|
+
severity: "warning",
|
|
471
|
+
category: "style",
|
|
472
|
+
check(schema, context) {
|
|
473
|
+
if (!schema.fields) {
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
for (const field of schema.fields) {
|
|
477
|
+
for (const pattern of HEADING_LEVEL_PATTERNS) {
|
|
478
|
+
if (pattern.test(field.name)) {
|
|
479
|
+
context.report({
|
|
480
|
+
message: `Field "${field.name}" appears to store heading levels`,
|
|
481
|
+
severity: "warning",
|
|
482
|
+
...field.span && { span: field.span },
|
|
483
|
+
help: "Heading levels should be computed dynamically in frontend components based on document structure, not stored in content."
|
|
484
|
+
});
|
|
485
|
+
break;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
if (field.options?.list && Array.isArray(field.options.list)) {
|
|
489
|
+
const hasHeadingLevels = field.options.list.some((item) => {
|
|
490
|
+
const value = typeof item === "object" && item !== null ? item.value : item;
|
|
491
|
+
return typeof value === "string" && HEADING_LEVEL_VALUES.includes(value.toLowerCase());
|
|
492
|
+
});
|
|
493
|
+
if (hasHeadingLevels) {
|
|
494
|
+
context.report({
|
|
495
|
+
message: `Field "${field.name}" contains heading level options (h1, h2, etc.)`,
|
|
496
|
+
severity: "warning",
|
|
497
|
+
...field.span && { span: field.span },
|
|
498
|
+
help: "Heading levels should be computed dynamically in frontend components based on document structure, not stored in content."
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
};
|
|
505
|
+
|
|
506
|
+
// src/rules/unnecessary-reference.ts
|
|
507
|
+
var SHARED_TYPE_PATTERNS = [
|
|
508
|
+
/^author$/i,
|
|
509
|
+
/^category$/i,
|
|
510
|
+
/^tag$/i,
|
|
511
|
+
/^user$/i,
|
|
512
|
+
/^person$/i,
|
|
513
|
+
/^organization$/i,
|
|
514
|
+
/^company$/i,
|
|
515
|
+
/^brand$/i,
|
|
516
|
+
/^product$/i,
|
|
517
|
+
/^page$/i,
|
|
518
|
+
/^post$/i,
|
|
519
|
+
/^article$/i,
|
|
520
|
+
/^media$/i,
|
|
521
|
+
/^asset$/i,
|
|
522
|
+
/^file$/i,
|
|
523
|
+
/^image$/i,
|
|
524
|
+
/^video$/i,
|
|
525
|
+
/^document$/i,
|
|
526
|
+
/^location$/i,
|
|
527
|
+
/^venue$/i,
|
|
528
|
+
/^event$/i
|
|
529
|
+
];
|
|
530
|
+
var NON_SHARED_SUFFIXES = [
|
|
531
|
+
"Settings",
|
|
532
|
+
"Config",
|
|
533
|
+
"Configuration",
|
|
534
|
+
"Options",
|
|
535
|
+
"Metadata",
|
|
536
|
+
"Data",
|
|
537
|
+
"Info",
|
|
538
|
+
"Details",
|
|
539
|
+
"Content",
|
|
540
|
+
"Block",
|
|
541
|
+
"Section",
|
|
542
|
+
"Item",
|
|
543
|
+
"Entry"
|
|
544
|
+
];
|
|
545
|
+
function isLikelySharedType(typeName) {
|
|
546
|
+
return SHARED_TYPE_PATTERNS.some((pattern) => pattern.test(typeName));
|
|
547
|
+
}
|
|
548
|
+
function hasNonSharedSuffix(typeName) {
|
|
549
|
+
return NON_SHARED_SUFFIXES.some(
|
|
550
|
+
(suffix) => typeName.endsWith(suffix) && typeName.length > suffix.length
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
function isTypeSpecificToParent(parentName, referencedType) {
|
|
554
|
+
const parentLower = parentName.toLowerCase();
|
|
555
|
+
const refLower = referencedType.toLowerCase();
|
|
556
|
+
return refLower.startsWith(parentLower) && refLower !== parentLower;
|
|
557
|
+
}
|
|
558
|
+
var unnecessaryReference = {
|
|
559
|
+
id: "unnecessary-reference",
|
|
560
|
+
name: "Unnecessary reference",
|
|
561
|
+
description: "Consider using an embedded object instead of a reference for non-shared content",
|
|
562
|
+
severity: "info",
|
|
563
|
+
category: "style",
|
|
564
|
+
check(schema, context) {
|
|
565
|
+
if (!schema.fields) {
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
for (const field of schema.fields) {
|
|
569
|
+
if (field.type !== "reference") {
|
|
570
|
+
continue;
|
|
571
|
+
}
|
|
572
|
+
if (!field.to || field.to.length === 0) {
|
|
573
|
+
continue;
|
|
574
|
+
}
|
|
575
|
+
for (const ref of field.to) {
|
|
576
|
+
const refType = ref.type;
|
|
577
|
+
if (isLikelySharedType(refType)) {
|
|
578
|
+
continue;
|
|
579
|
+
}
|
|
580
|
+
if (isTypeSpecificToParent(schema.name, refType)) {
|
|
581
|
+
context.report({
|
|
582
|
+
message: `Reference to "${refType}" may be unnecessary`,
|
|
583
|
+
severity: "info",
|
|
584
|
+
...field.span && { span: field.span },
|
|
585
|
+
help: `"${refType}" appears specific to "${schema.name}". Consider embedding the object directly instead of using a reference.`
|
|
586
|
+
});
|
|
587
|
+
continue;
|
|
588
|
+
}
|
|
589
|
+
if (hasNonSharedSuffix(refType)) {
|
|
590
|
+
context.report({
|
|
591
|
+
message: `Reference to "${refType}" may be unnecessary`,
|
|
592
|
+
severity: "info",
|
|
593
|
+
...field.span && { span: field.span },
|
|
594
|
+
help: `Types ending in "Settings", "Config", etc. are often better as embedded objects unless shared across multiple documents.`
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
};
|
|
601
|
+
|
|
602
|
+
// src/rules/index.ts
|
|
603
|
+
var rules = [
|
|
604
|
+
// Core rules
|
|
605
|
+
missingDefineType,
|
|
606
|
+
missingDefineField,
|
|
607
|
+
missingIcon,
|
|
608
|
+
missingTitle,
|
|
609
|
+
missingDescription,
|
|
610
|
+
presentationFieldName,
|
|
611
|
+
// Field rules
|
|
612
|
+
booleanInsteadOfList,
|
|
613
|
+
missingSlugSource,
|
|
614
|
+
missingRequiredValidation,
|
|
615
|
+
reservedFieldName,
|
|
616
|
+
// Array & Reference rules
|
|
617
|
+
arrayMissingConstraints,
|
|
618
|
+
headingLevelInSchema,
|
|
619
|
+
unnecessaryReference
|
|
620
|
+
];
|
|
621
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
622
|
+
0 && (module.exports = {
|
|
623
|
+
arrayMissingConstraints,
|
|
624
|
+
booleanInsteadOfList,
|
|
625
|
+
headingLevelInSchema,
|
|
626
|
+
lint,
|
|
627
|
+
lintSchemas,
|
|
628
|
+
missingDefineField,
|
|
629
|
+
missingDefineType,
|
|
630
|
+
missingDescription,
|
|
631
|
+
missingIcon,
|
|
632
|
+
missingRequiredValidation,
|
|
633
|
+
missingSlugSource,
|
|
634
|
+
missingTitle,
|
|
635
|
+
presentationFieldName,
|
|
636
|
+
reservedFieldName,
|
|
637
|
+
rules,
|
|
638
|
+
unnecessaryReference
|
|
639
|
+
});
|
|
640
|
+
//# sourceMappingURL=index.cjs.map
|