@starci/eslint-canon-fe 9.3.0 → 9.4.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 +9 -0
- package/README.md +5 -1
- package/dist/brand-values.d.ts +28 -0
- package/dist/brand-values.js +172 -0
- package/dist/button-intent.d.ts +11 -0
- package/dist/button-intent.js +361 -0
- package/dist/callback-shape.d.ts +7 -0
- package/dist/callback-shape.js +396 -0
- package/dist/class-names.d.ts +19 -0
- package/dist/class-names.js +170 -0
- package/dist/client-boundary.d.ts +42 -0
- package/dist/client-boundary.js +404 -0
- package/dist/comments.d.ts +47 -0
- package/dist/comments.js +156 -0
- package/dist/control-height.d.ts +7 -0
- package/dist/control-height.js +83 -0
- package/dist/drawing.d.ts +31 -0
- package/dist/drawing.js +376 -0
- package/dist/env-owner.d.ts +22 -0
- package/dist/env-owner.js +291 -0
- package/dist/fe-stack.d.ts +10 -0
- package/dist/fe-stack.js +73 -0
- package/dist/file-layout.d.ts +71 -0
- package/dist/file-layout.js +465 -0
- package/dist/formatting.d.ts +25 -0
- package/dist/formatting.js +104 -0
- package/dist/function-budget.d.ts +9 -0
- package/dist/function-budget.js +48 -0
- package/dist/grammar-shim.d.ts +5 -0
- package/dist/grammar-shim.js +126 -0
- package/dist/hooks-folder.d.ts +34 -0
- package/dist/hooks-folder.js +320 -0
- package/dist/hygiene.d.ts +39 -0
- package/dist/hygiene.js +540 -0
- package/dist/index.d.ts +105 -0
- package/dist/index.js +310 -0
- package/{landmark.mjs → dist/landmark.d.ts} +4 -6
- package/dist/landmark.js +6 -0
- package/dist/lib/ast.d.ts +36 -0
- package/dist/lib/ast.js +94 -0
- package/dist/lib/bindings.d.ts +84 -0
- package/dist/lib/bindings.js +168 -0
- package/dist/lib/class-text.d.ts +3 -0
- package/dist/lib/class-text.js +30 -0
- package/dist/lib/config.d.ts +81 -0
- package/dist/lib/config.js +141 -0
- package/dist/lib/drawing-scope.d.ts +5 -0
- package/{lib/drawing-scope.mjs → dist/lib/drawing-scope.js} +6 -11
- package/dist/lib/eslint-boundary.d.ts +23 -0
- package/dist/lib/eslint-boundary.js +6 -0
- package/dist/lib/fe-stack.d.ts +49 -0
- package/dist/lib/fe-stack.js +98 -0
- package/dist/lib/hfs-view.d.ts +74 -0
- package/dist/lib/hfs-view.js +6 -0
- package/dist/lib/hfs.d.ts +34 -0
- package/dist/lib/hfs.js +72 -0
- package/dist/lib/next.d.ts +4 -0
- package/dist/lib/next.js +10 -0
- package/dist/lib/own-hook-origin.d.ts +17 -0
- package/dist/lib/own-hook-origin.js +52 -0
- package/dist/lib/package-root.d.ts +4 -0
- package/dist/lib/package-root.js +10 -0
- package/dist/lib/path.d.ts +2 -0
- package/{lib/path.mjs → dist/lib/path.js} +1 -1
- package/dist/lib/platform.d.ts +26 -0
- package/{lib/platform.mjs → dist/lib/platform.js} +38 -35
- package/dist/lib/program-for.d.ts +5 -0
- package/dist/lib/program-for.js +23 -0
- package/dist/lib/pure-owner-helper.d.ts +10 -0
- package/dist/lib/pure-owner-helper.js +156 -0
- package/dist/lib/react.d.ts +16 -0
- package/dist/lib/react.js +22 -0
- package/dist/lib/redux-hook-origin.d.ts +4 -0
- package/dist/lib/redux-hook-origin.js +50 -0
- package/dist/lib/rule.d.ts +28 -0
- package/dist/lib/rule.js +5 -0
- package/dist/lib/scope.d.ts +88 -0
- package/{lib/scope.mjs → dist/lib/scope.js} +83 -93
- package/dist/lib/typed-class-values.d.ts +13 -0
- package/dist/lib/typed-class-values.js +92 -0
- package/dist/lib/types.d.ts +44 -0
- package/dist/lib/types.js +64 -0
- package/dist/lib/why.d.ts +350 -0
- package/dist/lib/why.js +350 -0
- package/dist/lint-escape-hatch.d.ts +24 -0
- package/{lint-escape-hatch.mjs → dist/lint-escape-hatch.js} +38 -45
- package/dist/lists.d.ts +31 -0
- package/dist/lists.js +202 -0
- package/dist/loading.d.ts +34 -0
- package/dist/loading.js +157 -0
- package/dist/naming.d.ts +33 -0
- package/dist/naming.js +176 -0
- package/dist/native-controls.d.ts +24 -0
- package/dist/native-controls.js +105 -0
- package/dist/next-conventions.d.ts +48 -0
- package/dist/next-conventions.js +452 -0
- package/dist/project-graph.d.ts +5 -0
- package/{project-graph.mjs → dist/project-graph.js} +8 -12
- package/dist/props-and-slots.d.ts +19 -0
- package/dist/props-and-slots.js +320 -0
- package/dist/served-locale.d.ts +38 -0
- package/dist/served-locale.js +263 -0
- package/dist/shape-slot.d.ts +18 -0
- package/dist/shape-slot.js +324 -0
- package/dist/size-and-state-budget.d.ts +46 -0
- package/dist/size-and-state-budget.js +424 -0
- package/dist/size-growth.d.ts +24 -0
- package/dist/size-growth.js +64 -0
- package/dist/sonar-parity.d.ts +5 -0
- package/{sonar-parity.mjs → dist/sonar-parity.js} +11 -7
- package/dist/surface-rules.d.ts +18 -0
- package/dist/surface-rules.js +491 -0
- package/dist/swr-family.d.ts +15 -0
- package/dist/swr-family.js +419 -0
- package/dist/the-split.d.ts +27 -0
- package/dist/the-split.js +157 -0
- package/dist/tokens.d.ts +9 -0
- package/dist/tokens.js +35 -0
- package/dist/translation.d.ts +51 -0
- package/dist/translation.js +403 -0
- package/dist/transport.d.ts +73 -0
- package/dist/transport.js +885 -0
- package/dist/type-safety.d.ts +43 -0
- package/dist/type-safety.js +139 -0
- package/dist/ui-rulings.d.ts +11 -0
- package/dist/ui-rulings.js +121 -0
- package/package.json +31 -9
- package/runtime/knowledge/hfs/canon-pins.yaml +17 -10
- package/brand-values.mjs +0 -172
- package/button-intent.mjs +0 -320
- package/callback-shape.mjs +0 -332
- package/class-names.mjs +0 -149
- package/client-boundary.mjs +0 -400
- package/comments.mjs +0 -159
- package/control-height.mjs +0 -77
- package/drawing.mjs +0 -321
- package/env-owner.mjs +0 -244
- package/fe-stack.mjs +0 -86
- package/file-layout.mjs +0 -472
- package/formatting.mjs +0 -110
- package/function-budget.mjs +0 -49
- package/grammar-shim.mjs +0 -123
- package/hooks-folder.mjs +0 -300
- package/hygiene.mjs +0 -513
- package/index.mjs +0 -317
- package/lib/ast.mjs +0 -97
- package/lib/bindings.mjs +0 -159
- package/lib/class-text.mjs +0 -29
- package/lib/config.mjs +0 -142
- package/lib/fe-stack.mjs +0 -77
- package/lib/hfs.mjs +0 -49
- package/lib/next.mjs +0 -6
- package/lib/own-hook-origin.mjs +0 -53
- package/lib/program-for.mjs +0 -22
- package/lib/pure-owner-helper.mjs +0 -148
- package/lib/react.mjs +0 -28
- package/lib/redux-hook-origin.mjs +0 -44
- package/lib/typed-class-values.mjs +0 -81
- package/lib/types.mjs +0 -63
- package/lib/why.mjs +0 -352
- package/lists.mjs +0 -193
- package/loading.mjs +0 -156
- package/naming.mjs +0 -185
- package/native-controls.mjs +0 -108
- package/next-conventions.mjs +0 -447
- package/props-and-slots.mjs +0 -276
- package/served-locale.mjs +0 -243
- package/size-and-state-budget.mjs +0 -376
- package/size-growth.mjs +0 -62
- package/surface-rules.mjs +0 -443
- package/swr-family.mjs +0 -326
- package/the-split.mjs +0 -155
- package/tokens.mjs +0 -39
- package/translation.mjs +0 -392
- package/transport.mjs +0 -830
- package/type-safety.mjs +0 -147
- package/ui-rulings.mjs +0 -134
package/env-owner.mjs
DELETED
|
@@ -1,244 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The rules that hold `env-owner.md` (HFS R49, `FE_ENV_OWNER`).
|
|
3
|
-
*
|
|
4
|
-
* TWO FACTS, ONE OWNER. The environment is read in exactly one place, `modules/config`, and what it
|
|
5
|
-
* reads has no default that stands in for a real deployment value. They are one law because they
|
|
6
|
-
* fail together: an app with nine copies of `process.env.API_URL ?? "http://localhost:3068"` had
|
|
7
|
-
* nine readers and nine silent defaults, so a production build with the variable missing talked to
|
|
8
|
-
* a developer's laptop and looked healthy.
|
|
9
|
-
*
|
|
10
|
-
* The rules cannot see whether `modules/config` throws when a variable is missing in production -
|
|
11
|
-
* that is a behaviour, and the module's own spec is where it is proven. What they hold is the
|
|
12
|
-
* shape that makes it provable: one reader, and no literal fallback next to it.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import { isConfigModule } from "./lib/scope.mjs"
|
|
16
|
-
|
|
17
|
-
/** `process.env`, or `import.meta.env`. */
|
|
18
|
-
const isEnvObject = (node) => {
|
|
19
|
-
if (!node || node.type !== "MemberExpression" || node.computed) return false
|
|
20
|
-
const name = node.property && node.property.name
|
|
21
|
-
if (name !== "env") return false
|
|
22
|
-
const object = node.object
|
|
23
|
-
if (object.type === "Identifier" && object.name === "process") return true
|
|
24
|
-
return (
|
|
25
|
-
object.type === "MetaProperty" && object.meta.name === "import" && object.property.name === "meta"
|
|
26
|
-
)
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/** The variable name of `process.env.NAME` / `process.env["NAME"]`, or null. */
|
|
30
|
-
const envVariableName = (node) => {
|
|
31
|
-
if (!node || node.type !== "MemberExpression" || !isEnvObject(node.object)) return null
|
|
32
|
-
if (!node.computed && node.property.type === "Identifier") return node.property.name
|
|
33
|
-
if (node.computed && node.property.type === "Literal" && typeof node.property.value === "string") {
|
|
34
|
-
return node.property.value
|
|
35
|
-
}
|
|
36
|
-
return null
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/** Static text of a literal or an expression-free template, else null. */
|
|
40
|
-
const staticText = (node) => {
|
|
41
|
-
if (!node) return null
|
|
42
|
-
if (node.type === "Literal" && typeof node.value === "string") return node.value
|
|
43
|
-
if (node.type === "TemplateLiteral") {
|
|
44
|
-
return node.quasis.map((quasi) => quasi.value.cooked ?? "").join("")
|
|
45
|
-
}
|
|
46
|
-
return null
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/** An absolute or protocol-relative URL: what a fallback must never carry. */
|
|
50
|
-
const URL_TEXT = /^(?:https?:)?\/\/\S+/i
|
|
51
|
-
|
|
52
|
-
/** Variable names whose value is a secret or an address: a default for one is a hole in a deployment. */
|
|
53
|
-
const SENSITIVE_NAME = /(?:SECRET|TOKEN|PASSWORD|PASSWD|API_?KEY|PRIVATE_?KEY|URL|URI|HOST|ENDPOINT|ORIGIN)$/i
|
|
54
|
-
|
|
55
|
-
/** Type assertions and optional chains preserve the identity of the wrapped value. */
|
|
56
|
-
const unwrapped = (node) => {
|
|
57
|
-
while (node && ["TSAsExpression", "TSTypeAssertion", "TSNonNullExpression", "TSSatisfiesExpression", "ChainExpression"].includes(node.type)) node = node.expression
|
|
58
|
-
return node
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/** The binding of this identifier, rather than every variable with the same spelling. */
|
|
62
|
-
const bindingOf = (sourceCode, identifier) => {
|
|
63
|
-
for (let scope = sourceCode.getScope(identifier); scope; scope = scope.upper) {
|
|
64
|
-
const variable = scope.set.get(identifier.name)
|
|
65
|
-
if (variable) return variable
|
|
66
|
-
}
|
|
67
|
-
return null
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/** Prior writes in the use's execution scope or its parents; a closure can capture a later initializer. */
|
|
71
|
-
const writeBefore = (sourceCode, variable, node) => {
|
|
72
|
-
const scopes = new Set()
|
|
73
|
-
for (let scope = sourceCode.getScope(node); scope; scope = scope.upper) scopes.add(scope.variableScope)
|
|
74
|
-
const writes = variable.references.filter((reference) => reference.isWrite()
|
|
75
|
-
&& scopes.has(reference.from.variableScope)
|
|
76
|
-
&& !(reference.writeExpr && reference.writeExpr.range[0] <= node.range[0] && node.range[1] <= reference.writeExpr.range[1]))
|
|
77
|
-
return writes
|
|
78
|
-
.filter((reference) => reference.identifier.range[0] < node.range[0])
|
|
79
|
-
.sort((left, right) => right.identifier.range[0] - left.identifier.range[0])[0]
|
|
80
|
-
?? writes.find((reference) => reference.init)
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/** Property extraction between a write's source and its destructured binding. */
|
|
84
|
-
const propertyPath = (identifier) => {
|
|
85
|
-
const properties = []
|
|
86
|
-
for (let node = identifier; node.parent; node = node.parent) {
|
|
87
|
-
const parent = node.parent
|
|
88
|
-
if (parent.type === "Property" && parent.value === node) properties.unshift(parent)
|
|
89
|
-
else if (parent.type !== "ObjectPattern" && !(parent.type === "AssignmentPattern" && parent.left === node)) break
|
|
90
|
-
}
|
|
91
|
-
return properties
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/** Known environment containers and constant computed keys, followed through local aliases. */
|
|
95
|
-
const environmentIdentity = (sourceCode) => {
|
|
96
|
-
const textOf = (node, seen = new Set()) => {
|
|
97
|
-
node = unwrapped(node)
|
|
98
|
-
if (!node || seen.has(node)) return null
|
|
99
|
-
if (node.type === "Literal" && typeof node.value === "string") return node.value
|
|
100
|
-
if (node.type === "TemplateLiteral" && node.expressions.length === 0) return node.quasis[0].value.cooked
|
|
101
|
-
if (node.type !== "Identifier") return null
|
|
102
|
-
const variable = bindingOf(sourceCode, node)
|
|
103
|
-
if (!variable) return null
|
|
104
|
-
const write = writeBefore(sourceCode, variable, node)
|
|
105
|
-
if (!write?.writeExpr || propertyPath(write.identifier).length > 0) return null
|
|
106
|
-
return textOf(write.writeExpr, new Set([...seen, node]))
|
|
107
|
-
}
|
|
108
|
-
const keyOf = (node) => node.computed ? textOf(node.key ?? node.property) : (node.key ?? node.property).name ?? (node.key ?? node.property).value
|
|
109
|
-
const propertyKind = (kind, key) => {
|
|
110
|
-
if (kind === "global" && key === "process") return "process"
|
|
111
|
-
if ((kind === "process" || kind === "meta") && key === "env") return "env"
|
|
112
|
-
return null
|
|
113
|
-
}
|
|
114
|
-
const kindOf = (node, seen = new Set()) => {
|
|
115
|
-
node = unwrapped(node)
|
|
116
|
-
if (!node || seen.has(node)) return null
|
|
117
|
-
if (node.type === "MetaProperty" && node.meta.name === "import" && node.property.name === "meta") return "meta"
|
|
118
|
-
if (node.type === "MemberExpression") return propertyKind(kindOf(node.object, new Set([...seen, node])), keyOf(node))
|
|
119
|
-
if (node.type !== "Identifier") return null
|
|
120
|
-
const variable = bindingOf(sourceCode, node)
|
|
121
|
-
if (!variable || variable.defs.length === 0) {
|
|
122
|
-
if (node.name === "process") return "process"
|
|
123
|
-
return ["globalThis", "global"].includes(node.name) ? "global" : null
|
|
124
|
-
}
|
|
125
|
-
const imported = variable.defs.find((definition) => definition.type === "ImportBinding" && ["process", "node:process"].includes(definition.parent.source.value))
|
|
126
|
-
if (imported) {
|
|
127
|
-
if (imported.parent.importKind === "type" || imported.node.importKind === "type") return null
|
|
128
|
-
if (imported.node.type !== "ImportSpecifier") return "process"
|
|
129
|
-
const name = imported.node.imported.name ?? imported.node.imported.value
|
|
130
|
-
return name === "default" ? "process" : name === "env" ? "env" : null
|
|
131
|
-
}
|
|
132
|
-
const write = writeBefore(sourceCode, variable, node)
|
|
133
|
-
if (!write?.writeExpr) return null
|
|
134
|
-
let kind = kindOf(write.writeExpr, new Set([...seen, node]))
|
|
135
|
-
for (const property of propertyPath(write.identifier)) kind = propertyKind(kind, keyOf(property))
|
|
136
|
-
return kind
|
|
137
|
-
}
|
|
138
|
-
const readsEnv = (pattern, kind) => {
|
|
139
|
-
if (pattern.type === "AssignmentPattern") return readsEnv(pattern.left, kind)
|
|
140
|
-
if (pattern.type !== "ObjectPattern") return false
|
|
141
|
-
return pattern.properties.some((property) => {
|
|
142
|
-
if (property.type !== "Property") return false
|
|
143
|
-
const valueKind = propertyKind(kind, keyOf(property))
|
|
144
|
-
return valueKind === "env" || readsEnv(property.value, valueKind)
|
|
145
|
-
})
|
|
146
|
-
}
|
|
147
|
-
return { kindOf, readsEnv }
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// -- FE-ENV-1 --------------------------------------------------------------------------------------
|
|
151
|
-
|
|
152
|
-
/** Only `modules/config` reads the environment. */
|
|
153
|
-
export const noEnvOutsideConfig = {
|
|
154
|
-
meta: {
|
|
155
|
-
type: "problem",
|
|
156
|
-
docs: { description: "`process.env` and `import.meta.env` are read only in `modules/config`." },
|
|
157
|
-
schema: [],
|
|
158
|
-
messages: {
|
|
159
|
-
env:
|
|
160
|
-
"This file reads the environment. Only `modules/config` may: it is the one place that can fail loudly when a production variable is missing. Read the value from the config module's typed export instead.",
|
|
161
|
-
},
|
|
162
|
-
},
|
|
163
|
-
create(context) {
|
|
164
|
-
if (isConfigModule(context)) return {}
|
|
165
|
-
const members = []
|
|
166
|
-
const destructures = []
|
|
167
|
-
return {
|
|
168
|
-
ImportDeclaration(node) {
|
|
169
|
-
if (node.importKind === "type" || !["process", "node:process"].includes(node.source.value)) return
|
|
170
|
-
for (const specifier of node.specifiers) {
|
|
171
|
-
if (specifier.type === "ImportSpecifier" && specifier.importKind !== "type" && (specifier.imported.name ?? specifier.imported.value) === "env") context.report({ node: specifier, messageId: "env" })
|
|
172
|
-
}
|
|
173
|
-
},
|
|
174
|
-
MemberExpression(node) {
|
|
175
|
-
members.push(node)
|
|
176
|
-
},
|
|
177
|
-
VariableDeclarator(node) {
|
|
178
|
-
if (node.id.type === "ObjectPattern" && node.init) destructures.push({ node, pattern: node.id, value: node.init })
|
|
179
|
-
},
|
|
180
|
-
AssignmentExpression(node) {
|
|
181
|
-
if (node.operator === "=" && node.left.type === "ObjectPattern") destructures.push({ node, pattern: node.left, value: node.right })
|
|
182
|
-
},
|
|
183
|
-
AssignmentPattern(node) {
|
|
184
|
-
if (node.left.type === "ObjectPattern") destructures.push({ node, pattern: node.left, value: node.right })
|
|
185
|
-
},
|
|
186
|
-
"Program:exit"() {
|
|
187
|
-
const { kindOf, readsEnv } = environmentIdentity(context.sourceCode ?? context.getSourceCode())
|
|
188
|
-
for (const node of members) {
|
|
189
|
-
if (kindOf(node) === "env") context.report({ node, messageId: "env" })
|
|
190
|
-
}
|
|
191
|
-
for (const { node, pattern, value } of destructures) {
|
|
192
|
-
if (readsEnv(pattern, kindOf(value))) context.report({ node, messageId: "env" })
|
|
193
|
-
}
|
|
194
|
-
},
|
|
195
|
-
}
|
|
196
|
-
},
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// -- FE-ENV-2 --------------------------------------------------------------------------------------
|
|
200
|
-
|
|
201
|
-
/** A fallback never stands in for a deployment address or secret. */
|
|
202
|
-
export const noHardcodedEndpointFallback = {
|
|
203
|
-
meta: {
|
|
204
|
-
type: "problem",
|
|
205
|
-
docs: { description: "No URL literal as a fallback, and no literal default for an address or secret variable." },
|
|
206
|
-
schema: [],
|
|
207
|
-
messages: {
|
|
208
|
-
url:
|
|
209
|
-
"A URL literal used as a fallback (`?? \"http://...\"`, `|| \"http://...\"` or a parameter default). A missing variable then silently points production at a developer's machine. Read the address from `modules/config`, which throws when it is absent.",
|
|
210
|
-
variable:
|
|
211
|
-
"`{{name}}` is an address or secret variable with a literal default. A default here makes a misconfigured deployment look healthy. Remove the default and let `modules/config` fail loudly when it is missing.",
|
|
212
|
-
},
|
|
213
|
-
},
|
|
214
|
-
create(context) {
|
|
215
|
-
return {
|
|
216
|
-
LogicalExpression(node) {
|
|
217
|
-
if (node.operator !== "??" && node.operator !== "||") return
|
|
218
|
-
const text = staticText(node.right)
|
|
219
|
-
if (text === null) return
|
|
220
|
-
if (URL_TEXT.test(text)) {
|
|
221
|
-
context.report({ node: node.right, messageId: "url" })
|
|
222
|
-
return
|
|
223
|
-
}
|
|
224
|
-
const name = envVariableName(node.left)
|
|
225
|
-
if (name && text !== "" && SENSITIVE_NAME.test(name)) {
|
|
226
|
-
context.report({ node: node.right, messageId: "variable", data: { name } })
|
|
227
|
-
}
|
|
228
|
-
},
|
|
229
|
-
AssignmentPattern(node) {
|
|
230
|
-
const text = staticText(node.right)
|
|
231
|
-
if (text !== null && URL_TEXT.test(text)) context.report({ node: node.right, messageId: "url" })
|
|
232
|
-
},
|
|
233
|
-
}
|
|
234
|
-
},
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
/** The rules this law contributes to the plugin. */
|
|
238
|
-
export const rules = {
|
|
239
|
-
"no-env-outside-config": noEnvOutsideConfig,
|
|
240
|
-
"no-hardcoded-endpoint-fallback": noHardcodedEndpointFallback,
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
/** Both are exact syntactic shapes, and both are errors. */
|
|
244
|
-
export const recommended = Object.fromEntries(Object.keys(rules).map((name) => [`starci-fe/${name}`, "error"]))
|
package/fe-stack.mjs
DELETED
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The rule that holds the front-end stack (`knowledge/hfs/fe-stack.yaml`, failure code `FE_STACK_FORBIDDEN`).
|
|
3
|
-
*
|
|
4
|
-
* ONE LIBRARY PER CONCERN. Every common front-end concern - GraphQL, REST, data hooks, toast, forms, state, realtime, i18n, UI,
|
|
5
|
-
* icons, motion, dates, numbers, storage, charts, markdown, code - has one canonical library, the same for the lite and the full
|
|
6
|
-
* edition (owner ruling 2026-10-13). A second library for a concern is a second set of conventions a reader has to know and a
|
|
7
|
-
* second dependency a release has to keep current, and it always arrives one import at a time. The catalog names, per concern,
|
|
8
|
-
* the alternatives a front end never imports; this rule refuses exactly those, read from the catalog, so the list lives once.
|
|
9
|
-
*
|
|
10
|
-
* SUPABASE IS AUTH ONLY. With a Supabase connection the front end holds the Supabase Auth client and nothing else of Supabase
|
|
11
|
-
* (the data path is GraphQL to the back end): `@supabase/*` is imported only by `modules/auth` (slot `fe.modules.auth`).
|
|
12
|
-
*
|
|
13
|
-
* WHAT IS LEFT TO SIBLINGS. A canonical library imported in the wrong place (Apollo outside `modules/api`, axios outside its REST
|
|
14
|
-
* calls, socket.io-client outside `hooks/socketio`) and `fetch` outside the API layer are `fetch-only-in-api-client`'s findings;
|
|
15
|
-
* a glyph library outside the icon leaf is the icon law's. Each import is judged by one rule.
|
|
16
|
-
*/
|
|
17
|
-
import { forbiddingConcern, packageNameOf } from "./lib/fe-stack.mjs"
|
|
18
|
-
import { globalReferences, inSlot, isProductSource } from "./lib/scope.mjs"
|
|
19
|
-
|
|
20
|
-
/** The package scope only the identity-provider client may import. */
|
|
21
|
-
const SUPABASE_SCOPE = "@supabase/"
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* No import (static, re-export, dynamic or `require`) of a package the fe-stack catalog forbids, and no `@supabase/*` outside
|
|
25
|
-
* `modules/auth`. Type-only imports count: a type import still makes the package a dependency of the front end.
|
|
26
|
-
*/
|
|
27
|
-
export const feStackImport = {
|
|
28
|
-
meta: {
|
|
29
|
-
type: "problem",
|
|
30
|
-
docs: { description: "A front end imports no library the fe-stack catalog forbids, and `@supabase/*` only in `modules/auth`." },
|
|
31
|
-
schema: [],
|
|
32
|
-
messages: {
|
|
33
|
-
forbidden:
|
|
34
|
-
"`{{name}}` is a forbidden alternative for the {{concern}} concern of the front-end stack. The stack has one library per concern, the same in every app and both editions: use {{canonical}} ({{pattern}}). {{why}}",
|
|
35
|
-
supabase:
|
|
36
|
-
"`{{name}}` is imported outside `modules/auth`. Supabase on the front end is the auth client only, held by `modules/auth` (the session, the access token the Apollo link attaches, sign-in and sign-out); data is read and written through the GraphQL operations of `modules/api`, never through Supabase from the browser.",
|
|
37
|
-
},
|
|
38
|
-
},
|
|
39
|
-
create(context) {
|
|
40
|
-
if (!isProductSource(context)) return {}
|
|
41
|
-
const authOwner = inSlot(context, "fe.modules.auth")
|
|
42
|
-
const judge = (node, source) => {
|
|
43
|
-
const name = packageNameOf(source)
|
|
44
|
-
if (name === null) return
|
|
45
|
-
const concern = forbiddingConcern(source)
|
|
46
|
-
if (concern) {
|
|
47
|
-
const named = concern.packages?.length > 0 ? concern.packages : concern.home ?? []
|
|
48
|
-
const canonical = named.map((entry) => `\`${entry}\``).join(", ")
|
|
49
|
-
context.report({ node, messageId: "forbidden", data: { name, concern: concern.id, canonical, pattern: concern.pattern ?? "", why: concern.why ?? "" } })
|
|
50
|
-
return
|
|
51
|
-
}
|
|
52
|
-
if (name.startsWith(SUPABASE_SCOPE) && !authOwner) context.report({ node, messageId: "supabase", data: { name } })
|
|
53
|
-
}
|
|
54
|
-
return {
|
|
55
|
-
ImportDeclaration(node) {
|
|
56
|
-
judge(node, node.source.value)
|
|
57
|
-
},
|
|
58
|
-
ExportNamedDeclaration(node) {
|
|
59
|
-
if (node.source) judge(node, node.source.value)
|
|
60
|
-
},
|
|
61
|
-
ExportAllDeclaration(node) {
|
|
62
|
-
judge(node, node.source.value)
|
|
63
|
-
},
|
|
64
|
-
ImportExpression(node) {
|
|
65
|
-
if (node.source.type === "Literal") judge(node, node.source.value)
|
|
66
|
-
},
|
|
67
|
-
TSImportEqualsDeclaration(node) {
|
|
68
|
-
// `import x = require("moment")`
|
|
69
|
-
if (node.moduleReference?.type === "TSExternalModuleReference" && node.moduleReference.expression?.type === "Literal") judge(node, node.moduleReference.expression.value)
|
|
70
|
-
},
|
|
71
|
-
CallExpression(node) {
|
|
72
|
-
// `require("moment")` where `require` is the CommonJS global.
|
|
73
|
-
if (node.callee.type !== "Identifier" || node.callee.name !== "require" || node.arguments[0]?.type !== "Literal") return
|
|
74
|
-
if (globalReferences(context, new Set(["require"])).includes(node.callee)) judge(node, node.arguments[0].value)
|
|
75
|
-
},
|
|
76
|
-
}
|
|
77
|
-
},
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/** The rules this law contributes to the plugin. */
|
|
81
|
-
export const rules = {
|
|
82
|
-
"fe-stack-import": feStackImport,
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/** Every rule is an error: a second library for a concern is the defect, not a style. */
|
|
86
|
-
export const recommended = Object.fromEntries(Object.keys(rules).map((name) => [`starci-fe/${name}`, "error"]))
|