@starci/eslint-canon-fe 6.0.2 → 7.0.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/CHANGELOG.md +12 -0
- package/README.md +3 -5
- package/client-boundary.mjs +2 -5
- package/comments.mjs +38 -15
- package/docs/brand-values.md +2 -2
- package/docs/client-boundary.md +10 -10
- package/docs/env-owner.md +4 -4
- package/docs/formatting.md +2 -2
- package/docs/grammar-boundary.md +2 -2
- package/docs/hooks-folder.md +2 -2
- package/docs/hygiene.md +8 -8
- package/docs/lint-escape-hatch.md +2 -2
- package/docs/lists.md +6 -6
- package/docs/native-controls.md +6 -6
- package/docs/next-conventions.md +20 -20
- package/docs/size-and-state-budget.md +6 -6
- package/docs/size-growth.md +2 -2
- package/docs/status-colors.md +2 -2
- package/docs/translation.md +2 -2
- package/docs/transport.md +16 -16
- package/docs/type-safety.md +8 -8
- package/env-owner.mjs +2 -3
- package/file-layout.mjs +3 -43
- package/formatting.mjs +2 -2
- package/grammar-boundary.mjs +1 -2
- package/hooks-folder.mjs +6 -7
- package/index.mjs +3 -27
- package/lib/config.mjs +6 -22
- package/lib/scope.mjs +3 -12
- package/lib/why.mjs +0 -65
- package/lint-escape-hatch.mjs +3 -3
- package/lists.mjs +0 -4
- package/loading.mjs +2 -2
- package/native-controls.mjs +0 -4
- package/next-conventions.mjs +3 -7
- package/package.json +1 -1
- package/props-and-slots.mjs +4 -5
- package/runtime/knowledge/hfs/slots.yaml +145 -107
- package/runtime/scripts/lib/hfs-allows.mjs +15 -0
- package/runtime/scripts/lib/hfs-slots.mjs +13 -6
- package/runtime/scripts/lib/language.mjs +107 -0
- package/served-locale.mjs +2 -2
- package/shape-slot.mjs +2 -3
- package/size-and-state-budget.mjs +1 -4
- package/size-growth.mjs +1 -2
- package/status-colors.mjs +0 -2
- package/tokens.mjs +1 -1
- package/translation.mjs +1 -29
- package/transport.mjs +4 -8
- package/docs/e2e-shape.md +0 -161
- package/docs/spec-quality.md +0 -147
- package/e2e-shape.mjs +0 -345
- package/spec-quality.mjs +0 -318
package/lint-escape-hatch.mjs
CHANGED
|
@@ -19,10 +19,10 @@
|
|
|
19
19
|
* nothing is dead weight that reads as if it did.
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
|
-
import {
|
|
22
|
+
import { isProductSource } from "./lib/scope.mjs"
|
|
23
23
|
|
|
24
|
-
/** Product source
|
|
25
|
-
const isGoverned = (context) => isProductSource(context)
|
|
24
|
+
/** Product source is governed; canon tests deliberately build forbidden directives. */
|
|
25
|
+
const isGoverned = (context) => isProductSource(context)
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
28
|
* Any directive that changes ESLint's active rule set inside a source file.
|
package/lists.mjs
CHANGED
|
@@ -16,7 +16,6 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import { attribute, attributeValue, calleeName, isFunction, mentions } from "./lib/ast.mjs"
|
|
19
|
-
import { isSpecFile } from "./lib/scope.mjs"
|
|
20
19
|
|
|
21
20
|
/** `.map(callback)` where the callback is written inline; the call node, else null. */
|
|
22
21
|
const mapCallback = (call) => {
|
|
@@ -86,7 +85,6 @@ export const listItemHasKey = {
|
|
|
86
85
|
},
|
|
87
86
|
},
|
|
88
87
|
create(context) {
|
|
89
|
-
if (isSpecFile(context.filename || context.getFilename())) return {}
|
|
90
88
|
return {
|
|
91
89
|
CallExpression(call) {
|
|
92
90
|
const callback = mapCallback(call)
|
|
@@ -116,7 +114,6 @@ export const noIndexKey = {
|
|
|
116
114
|
},
|
|
117
115
|
},
|
|
118
116
|
create(context) {
|
|
119
|
-
if (isSpecFile(context.filename || context.getFilename())) return {}
|
|
120
117
|
const source = context.sourceCode ?? context.getSourceCode()
|
|
121
118
|
return {
|
|
122
119
|
CallExpression(call) {
|
|
@@ -170,7 +167,6 @@ export const noInlineLiteralPropInList = {
|
|
|
170
167
|
},
|
|
171
168
|
},
|
|
172
169
|
create(context) {
|
|
173
|
-
if (isSpecFile(context.filename || context.getFilename())) return {}
|
|
174
170
|
return {
|
|
175
171
|
JSXAttribute(node) {
|
|
176
172
|
const opening = node.parent
|
package/loading.mjs
CHANGED
|
@@ -12,10 +12,10 @@
|
|
|
12
12
|
* lint rule is for - the one nothing else will ever report.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import { classOf, fileOf, isComponentFile
|
|
15
|
+
import { classOf, fileOf, isComponentFile } from "./lib/scope.mjs"
|
|
16
16
|
|
|
17
17
|
/** Product component source these rules govern: a component owner's file, and not a spec (a placeholder in a test fixture is a fixture, not a second tree). */
|
|
18
|
-
const isGoverned = (context) => isComponentFile(context)
|
|
18
|
+
const isGoverned = (context) => isComponentFile(context)
|
|
19
19
|
|
|
20
20
|
/** A flag that means "waiting", in the spellings this codebase uses. */
|
|
21
21
|
const WAITING_FLAG = /\bis(?:Loading|Skeleton|Pending)\b/
|
package/native-controls.mjs
CHANGED
|
@@ -13,7 +13,6 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import { attribute } from "./lib/ast.mjs"
|
|
16
|
-
import { isSpecFile } from "./lib/scope.mjs"
|
|
17
16
|
|
|
18
17
|
/** Intrinsic elements a reader operates, that the grammar renders for the product. */
|
|
19
18
|
const NATIVE = new Set(["select", "input", "textarea", "button"])
|
|
@@ -30,7 +29,6 @@ export const noNativeFormControl = {
|
|
|
30
29
|
},
|
|
31
30
|
},
|
|
32
31
|
create(context) {
|
|
33
|
-
if (isSpecFile(context.filename || context.getFilename())) return {}
|
|
34
32
|
return {
|
|
35
33
|
JSXOpeningElement(node) {
|
|
36
34
|
if (node.name.type !== "JSXIdentifier" || !NATIVE.has(node.name.name)) return
|
|
@@ -54,7 +52,6 @@ export const noNativeImg = {
|
|
|
54
52
|
},
|
|
55
53
|
},
|
|
56
54
|
create(context) {
|
|
57
|
-
if (isSpecFile(context.filename || context.getFilename())) return {}
|
|
58
55
|
return {
|
|
59
56
|
JSXOpeningElement(node) {
|
|
60
57
|
if (node.name.type === "JSXIdentifier" && node.name.name === "img") context.report({ node, messageId: "img" })
|
|
@@ -79,7 +76,6 @@ export const imageHasSize = {
|
|
|
79
76
|
},
|
|
80
77
|
},
|
|
81
78
|
create(context) {
|
|
82
|
-
if (isSpecFile(context.filename || context.getFilename())) return {}
|
|
83
79
|
const locals = new Set()
|
|
84
80
|
return {
|
|
85
81
|
ImportDeclaration(node) {
|
package/next-conventions.mjs
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
import { attribute, attributeValue, calleeName, leadingTextOf, stringOf } from "./lib/ast.mjs"
|
|
15
15
|
import { hfsOf } from "./lib/hfs.mjs"
|
|
16
|
-
import { baseName, classOf, fileOf, inSlot,
|
|
16
|
+
import { baseName, classOf, fileOf, inSlot, roleOfFile, slotOfFile, stem } from "./lib/scope.mjs"
|
|
17
17
|
|
|
18
18
|
/** Another i18n stack: a second place that decides which language a reader sees. */
|
|
19
19
|
const OTHER_I18N = /^(?:react-i18next|i18next|next-i18next|react-intl|next-translate|@lingui\/.+|typesafe-i18n|rosetta)$/
|
|
@@ -35,7 +35,6 @@ export const noMiddlewareFile = {
|
|
|
35
35
|
},
|
|
36
36
|
create(context) {
|
|
37
37
|
const file = fileOf(context)
|
|
38
|
-
if (isSpecFile(file)) return {}
|
|
39
38
|
// The retired name has a slot of its own (`fe.source-root-retired`, forbidden); the interceptor is the `proxy` role of `fe.source-root-pinned`.
|
|
40
39
|
if (inSlot(context, "fe.source-root-retired")) {
|
|
41
40
|
return { Program: (node) => context.report({ node, messageId: "file", data: { name: baseName(file) } }) }
|
|
@@ -189,7 +188,6 @@ export const noNullSuspenseFallback = {
|
|
|
189
188
|
},
|
|
190
189
|
},
|
|
191
190
|
create(context) {
|
|
192
|
-
if (isSpecFile(context.filename || context.getFilename())) return {}
|
|
193
191
|
return {
|
|
194
192
|
JSXOpeningElement(node) {
|
|
195
193
|
const name = node.name
|
|
@@ -235,7 +233,7 @@ export const navigationFromIntl = {
|
|
|
235
233
|
},
|
|
236
234
|
},
|
|
237
235
|
create(context) {
|
|
238
|
-
if (
|
|
236
|
+
if (isNavigationOwner(context)) return {}
|
|
239
237
|
return {
|
|
240
238
|
ImportDeclaration(node) {
|
|
241
239
|
if (node.importKind === "type") return
|
|
@@ -271,7 +269,6 @@ export const noNativeAnchor = {
|
|
|
271
269
|
},
|
|
272
270
|
},
|
|
273
271
|
create(context) {
|
|
274
|
-
if (isSpecFile(context.filename || context.getFilename())) return {}
|
|
275
272
|
return {
|
|
276
273
|
JSXOpeningElement(node) {
|
|
277
274
|
if (node.name.type !== "JSXIdentifier" || node.name.name !== "a") return
|
|
@@ -309,7 +306,7 @@ export const noHardcodedRoute = {
|
|
|
309
306
|
},
|
|
310
307
|
},
|
|
311
308
|
create(context) {
|
|
312
|
-
if (
|
|
309
|
+
if (isRoutesModule(context)) return {}
|
|
313
310
|
const source = context.sourceCode ?? context.getSourceCode()
|
|
314
311
|
const report = (node, text) => context.report({ node, messageId: "route", data: { path: text } })
|
|
315
312
|
return {
|
|
@@ -393,7 +390,6 @@ export const i18nStackInOneModule = {
|
|
|
393
390
|
},
|
|
394
391
|
},
|
|
395
392
|
create(context) {
|
|
396
|
-
if (isSpecFile(context.filename || context.getFilename())) return {}
|
|
397
393
|
const hfs = hfsOf(context)
|
|
398
394
|
const slot = slotOfFile(context)
|
|
399
395
|
const single = hfs.apps.length === 1
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@starci/eslint-canon-fe",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "7.0.0",
|
|
4
4
|
"description": "The StarCi front-end lint canon: every rule its laws publish, gathered as one flat-config plugin and attached by one factory.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "UNLICENSED",
|
package/props-and-slots.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** Rules for named React props, ordinary children, and local visual ownership. */
|
|
2
2
|
|
|
3
3
|
import { declarationsOf, fileOf as declarationFile } from "./lib/types.mjs"
|
|
4
|
-
import { fileOf, isComponentFile, isComponentPath, isProductSource,
|
|
4
|
+
import { fileOf, isComponentFile, isComponentPath, isProductSource, kindOfFile } from "./lib/scope.mjs"
|
|
5
5
|
|
|
6
6
|
const propertyName = (node) => {
|
|
7
7
|
const key = node?.key ?? node?.property
|
|
@@ -70,7 +70,7 @@ export const publicComponentSignature = {
|
|
|
70
70
|
const actual = type?.type === "TSTypeReference" && type.typeName?.type === "Identifier" ? type.typeName.name : null
|
|
71
71
|
if (!actual || !expected.includes(actual)) context.report({ node: params[0], messageId: "type", data: { name, expected: expected.join(" or ") } })
|
|
72
72
|
}
|
|
73
|
-
if (!isComponentFile(context)
|
|
73
|
+
if (!isComponentFile(context)) return {}
|
|
74
74
|
return {
|
|
75
75
|
VariableDeclarator(node) {
|
|
76
76
|
const name = componentNameOf(node)
|
|
@@ -93,7 +93,7 @@ export const noPerPartClassNameProp = {
|
|
|
93
93
|
messages: { perPart: "Keep internal part styling owned by the component." },
|
|
94
94
|
},
|
|
95
95
|
create(context) {
|
|
96
|
-
if (!isComponentFile(context)
|
|
96
|
+
if (!isComponentFile(context)) return {}
|
|
97
97
|
return { TSPropertySignature(node) { const name = propertyName(node); if (name && name !== "className" && /^[a-z][A-Za-z0-9]*ClassName$/.test(name)) context.report({ node, messageId: "perPart" }) } }
|
|
98
98
|
},
|
|
99
99
|
}
|
|
@@ -107,7 +107,6 @@ export const noPublicClassNameProp = {
|
|
|
107
107
|
messages: { declaration: "Component props must not expose {{prop}}.", usage: "Do not pass {{prop}} to house component {{component}}." },
|
|
108
108
|
},
|
|
109
109
|
create(context) {
|
|
110
|
-
if (isSpecFile(fileOf(context))) return {}
|
|
111
110
|
const declared = isComponentFile(context)
|
|
112
111
|
/** A house component: the identifier resolves (through its import) to a file of a component layer. */
|
|
113
112
|
const isHouseComponent = (identifier) => declarationsOf(context, identifier).some((declaration) => isComponentPath(context, declarationFile(declaration)))
|
|
@@ -128,7 +127,7 @@ export const noPublicClassNameProp = {
|
|
|
128
127
|
export const noPublicFrameCssProps = {
|
|
129
128
|
meta: { type: "problem", docs: { description: "Non-leaf component props do not expose CSS-shaped frame decisions." }, schema: [], messages: { css: "Move {{prop}} into component-owned layout behavior." } },
|
|
130
129
|
create(context) {
|
|
131
|
-
if (
|
|
130
|
+
if (!isComponentFile(context) || kindOfFile(context) === "leaves") return {}
|
|
132
131
|
const cssProps = new Set(["gap", "padding", "align", "justify", "className", "classNames", "style", "inline", "nested"])
|
|
133
132
|
return { TSPropertySignature(node) { const name = propertyName(node); if (cssProps.has(name)) context.report({ node, messageId: "css", data: { prop: name } }) } }
|
|
134
133
|
},
|