@starci/eslint-canon-fe 9.2.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 +14 -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 +22 -14
- package/runtime/scripts/lib/package-at.mjs +5 -5
- 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 -315
- 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/drawing.mjs
DELETED
|
@@ -1,315 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The rules that hold the drawing law (the `drawing` slot role: a surface's pure `component.tsx`).
|
|
3
|
-
*
|
|
4
|
-
* TWO AXES, AND ONLY ONE IS DRAWN. A surface's `state` is its SHAPE: a layout the owner approves as
|
|
5
|
-
* one drawing. A slot's data status (loading, forbidden, error, empty) is not a shape; it is a
|
|
6
|
-
* status recipe applied per resource, and it is never a drawn shape. Mixing the two is what made
|
|
7
|
-
* every block ship four hand-drawn variants that disagreed with each other.
|
|
8
|
-
*
|
|
9
|
-
* THE PURE HALF TAKES ATOMS. `component.tsx` receives `{ state, props, on }`: `props` is data
|
|
10
|
-
* (strings, numbers, booleans, arrays, plain objects), `on` is actions whose arguments are data,
|
|
11
|
-
* and feature composition may also take named `render` callbacks inside `props` to render supplied
|
|
12
|
-
* blocks. A layout alone also takes `children`, because the router puts the page there.
|
|
13
|
-
*
|
|
14
|
-
* ONLY ITS OWN INDEX MAY IMPORT IT. Other tiers use the connected export, so the pure half stays a
|
|
15
|
-
* private drawing surface that audit can render from a fixture.
|
|
16
|
-
*
|
|
17
|
-
* Every rule reads one file. Types imported from elsewhere are trusted as data; only names this
|
|
18
|
-
* file declares are followed, which is exact for the tree the convention produces and silent,
|
|
19
|
-
* never wrong, beyond it.
|
|
20
|
-
*/
|
|
21
|
-
|
|
22
|
-
import { classifyImport, roleOfFile } from "./lib/scope.mjs"
|
|
23
|
-
|
|
24
|
-
import { inSplitTier, pureHalf } from "./lib/drawing-scope.mjs"
|
|
25
|
-
|
|
26
|
-
/** Type names that carry a rendered tree rather than data. */
|
|
27
|
-
const NON_ATOM_TYPE = /^(?:ReactNode|ReactElement|ReactPortal|JSX\.Element|Element|ComponentType|FC|FunctionComponent|ElementType|ComponentProps|RenderFunction)$/
|
|
28
|
-
|
|
29
|
-
/** Feature composition's render callbacks return a rendered value and take only data. */
|
|
30
|
-
const RENDER_RESULT_TYPE = /^(?:ReactNode|ReactElement|JSX\.Element)$/
|
|
31
|
-
const RENDER_PROP_NAME = /^render(?:[A-Z][A-Za-z0-9]*)?$/
|
|
32
|
-
|
|
33
|
-
/** The printed name of a type reference, qualified names included. */
|
|
34
|
-
const typeNameOf = (node) => {
|
|
35
|
-
const name = node?.typeName
|
|
36
|
-
if (!name) return ""
|
|
37
|
-
if (name.type === "Identifier") return name.name
|
|
38
|
-
if (name.type === "TSQualifiedName") return `${typeNameOf({ typeName: name.left })}.${name.right.name}`
|
|
39
|
-
return ""
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/** Every type this file declares, by name, so references can be followed locally. */
|
|
43
|
-
const collectLocalTypes = (program) => {
|
|
44
|
-
const types = new Map()
|
|
45
|
-
const visit = (statement) => {
|
|
46
|
-
const declaration = statement?.type === "ExportNamedDeclaration" ? statement.declaration : statement
|
|
47
|
-
if (declaration?.type === "TSTypeAliasDeclaration") types.set(declaration.id.name, declaration.typeAnnotation)
|
|
48
|
-
if (declaration?.type === "TSInterfaceDeclaration") types.set(declaration.id.name, declaration.body)
|
|
49
|
-
}
|
|
50
|
-
for (const statement of program.body) visit(statement)
|
|
51
|
-
return types
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/** The members of an object-shaped type node, following local names and intersections. */
|
|
55
|
-
const membersOf = (node, types, seen = new Set()) => {
|
|
56
|
-
if (!node) return []
|
|
57
|
-
if (node.type === "TSTypeLiteral") return node.members
|
|
58
|
-
if (node.type === "TSInterfaceBody") return node.body
|
|
59
|
-
if (node.type === "TSIntersectionType") return node.types.flatMap((part) => membersOf(part, types, seen))
|
|
60
|
-
if (node.type === "TSTypeReference") {
|
|
61
|
-
const name = typeNameOf(node)
|
|
62
|
-
if (seen.has(name) || !types.has(name)) return []
|
|
63
|
-
seen.add(name)
|
|
64
|
-
return membersOf(types.get(name), types, seen)
|
|
65
|
-
}
|
|
66
|
-
return []
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/** The key a property signature declares. */
|
|
70
|
-
const keyOf = (member) => (member.key?.type === "Identifier" ? member.key.name : member.key?.value)
|
|
71
|
-
|
|
72
|
-
// -- DRAWING-1 ----------------------------------------------------------------------------------
|
|
73
|
-
|
|
74
|
-
/** The pure half takes atom data and actions; feature composition may receive named render callbacks. */
|
|
75
|
-
export const basePropsAtom = {
|
|
76
|
-
meta: {
|
|
77
|
-
type: "problem",
|
|
78
|
-
docs: { description: "A pure `XBase` takes `state`, atom `props` and `on` actions; feature composition may receive named render callbacks in `props`." },
|
|
79
|
-
schema: [],
|
|
80
|
-
messages: {
|
|
81
|
-
tree:
|
|
82
|
-
"`{{path}}` hands a rendered tree (`{{type}}`) into the pure half. Write the child inside `component.tsx` and pass it atoms (ids, strings, flags); a tree handed in cannot be drawn, audited or reused.",
|
|
83
|
-
action:
|
|
84
|
-
"`{{path}}` is a function inside `props`. Actions live in `on`; feature composition may receive named `render` callbacks that take atoms and return rendered blocks.",
|
|
85
|
-
onData:
|
|
86
|
-
"`on.{{key}}` is not an action. `on` holds functions only; data belongs in `props`.",
|
|
87
|
-
member:
|
|
88
|
-
"`{{key}}` is not part of the pure contract. A pure half takes `state`, `props` and `on`{{children}}.",
|
|
89
|
-
},
|
|
90
|
-
},
|
|
91
|
-
create(context) {
|
|
92
|
-
const tier = pureHalf(context)
|
|
93
|
-
if (!tier) return {}
|
|
94
|
-
let types = new Map()
|
|
95
|
-
|
|
96
|
-
/** Resolve render results locally; an optional empty result does not make data a render prop. */
|
|
97
|
-
const isRenderResult = (node, seen = new Set()) => {
|
|
98
|
-
if (!node) return false
|
|
99
|
-
if (node.type === "TSUnionType") {
|
|
100
|
-
const empty = (part) => ["TSNullKeyword", "TSUndefinedKeyword"].includes(part.type)
|
|
101
|
-
|| part.type === "TSLiteralType" && part.literal.value === null
|
|
102
|
-
return node.types.some((part) => isRenderResult(part, new Set(seen)))
|
|
103
|
-
&& node.types.every((part) => empty(part) || isRenderResult(part, new Set(seen)))
|
|
104
|
-
}
|
|
105
|
-
if (node.type !== "TSTypeReference") return false
|
|
106
|
-
const name = typeNameOf(node)
|
|
107
|
-
if (RENDER_RESULT_TYPE.test(name.replace(/^React\./, ""))) return true
|
|
108
|
-
if (!types.has(name) || seen.has(name)) return false
|
|
109
|
-
return isRenderResult(types.get(name), new Set(seen).add(name))
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
const checkRenderCallback = (node, path, allowed) => {
|
|
113
|
-
if (!allowed || !isRenderResult(node.returnType?.typeAnnotation)) return false
|
|
114
|
-
for (const parameter of node.params ?? node.parameters ?? []) {
|
|
115
|
-
checkData(parameter.typeAnnotation?.typeAnnotation, `${path}(${parameter.name ?? "arg"})`, new Set(), false, true)
|
|
116
|
-
}
|
|
117
|
-
return true
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const checkDataReference = (node, path, seen, renderProp, renderParameter) => {
|
|
121
|
-
const name = typeNameOf(node)
|
|
122
|
-
if (NON_ATOM_TYPE.test(renderParameter ? name.replace(/^React\./, "") : name)) {
|
|
123
|
-
context.report({ node, messageId: "tree", data: { path, type: name } })
|
|
124
|
-
return
|
|
125
|
-
}
|
|
126
|
-
for (const argument of node.typeArguments?.params ?? node.typeParameters?.params ?? []) checkData(argument, path, seen, false, renderParameter)
|
|
127
|
-
if (types.has(name) && !seen.has(name)) {
|
|
128
|
-
const nextSeen = renderProp || renderParameter ? new Set(seen) : seen
|
|
129
|
-
nextSeen.add(name)
|
|
130
|
-
checkData(types.get(name), path, nextSeen, renderProp, renderParameter)
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
const checkDataMember = (member, path, seen, renderParameter) => {
|
|
135
|
-
const memberPath = `${path}.${keyOf(member)}`
|
|
136
|
-
const allowsRender = tier !== "blocks" && path === "props" && RENDER_PROP_NAME.test(keyOf(member))
|
|
137
|
-
if (member.type === "TSMethodSignature") {
|
|
138
|
-
if (!checkRenderCallback(member, memberPath, allowsRender)) context.report({ node: member, messageId: "action", data: { path: memberPath } })
|
|
139
|
-
} else if (member.type === "TSPropertySignature") {
|
|
140
|
-
checkData(member.typeAnnotation?.typeAnnotation, memberPath, seen, allowsRender, renderParameter)
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/** Walk a data type: report trees and functions anywhere inside it. */
|
|
145
|
-
const checkData = (node, path, seen, renderProp = false, renderParameter = false) => {
|
|
146
|
-
if (!node) return
|
|
147
|
-
switch (node.type) {
|
|
148
|
-
case "TSFunctionType":
|
|
149
|
-
if (checkRenderCallback(node, path, renderProp)) return
|
|
150
|
-
context.report({ node, messageId: "action", data: { path } })
|
|
151
|
-
return
|
|
152
|
-
case "TSTypeReference":
|
|
153
|
-
checkDataReference(node, path, seen, renderProp, renderParameter)
|
|
154
|
-
return
|
|
155
|
-
case "TSUnionType":
|
|
156
|
-
case "TSIntersectionType":
|
|
157
|
-
for (const part of node.types) checkData(part, path, seen, renderProp, renderParameter)
|
|
158
|
-
return
|
|
159
|
-
case "TSArrayType":
|
|
160
|
-
checkData(node.elementType, path, seen, false, renderParameter)
|
|
161
|
-
return
|
|
162
|
-
case "TSTypeOperator":
|
|
163
|
-
checkData(node.typeAnnotation, path, seen, false, renderParameter)
|
|
164
|
-
return
|
|
165
|
-
case "TSTypeLiteral":
|
|
166
|
-
case "TSInterfaceBody":
|
|
167
|
-
for (const member of node.members ?? node.body) checkDataMember(member, path, seen, renderParameter)
|
|
168
|
-
return
|
|
169
|
-
default:
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/** Walk `on`: every member is a function whose parameters are data. */
|
|
174
|
-
const checkActions = (node) => {
|
|
175
|
-
for (const member of membersOf(node, types)) {
|
|
176
|
-
const key = keyOf(member)
|
|
177
|
-
const type = member.type === "TSMethodSignature" ? member : member.typeAnnotation?.typeAnnotation
|
|
178
|
-
if (type?.type !== "TSFunctionType" && type?.type !== "TSMethodSignature") {
|
|
179
|
-
context.report({ node: member, messageId: "onData", data: { key } })
|
|
180
|
-
continue
|
|
181
|
-
}
|
|
182
|
-
for (const parameter of type.params ?? type.parameters ?? []) {
|
|
183
|
-
checkData(parameter.typeAnnotation?.typeAnnotation, `on.${key}(${parameter.name ?? "arg"})`, new Set())
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
return {
|
|
189
|
-
Program(node) {
|
|
190
|
-
types = collectLocalTypes(node)
|
|
191
|
-
},
|
|
192
|
-
VariableDeclarator(node) {
|
|
193
|
-
if (node.id?.type !== "Identifier" || !/^[A-Z][A-Za-z0-9]*Base$/.test(node.id.name)) return
|
|
194
|
-
const fn = node.init
|
|
195
|
-
if (fn?.type !== "ArrowFunctionExpression" && fn?.type !== "FunctionExpression") return
|
|
196
|
-
const annotation = fn.params?.[0]?.typeAnnotation?.typeAnnotation
|
|
197
|
-
for (const member of membersOf(annotation, types)) {
|
|
198
|
-
const key = keyOf(member)
|
|
199
|
-
const type = member.typeAnnotation?.typeAnnotation
|
|
200
|
-
if (key === "state") continue
|
|
201
|
-
if (key === "props") checkData(type, "props", new Set())
|
|
202
|
-
else if (key === "on") checkActions(type)
|
|
203
|
-
else if (key === "children" && tier === "layouts") continue
|
|
204
|
-
else {
|
|
205
|
-
context.report({
|
|
206
|
-
node: member,
|
|
207
|
-
messageId: "member",
|
|
208
|
-
data: { key, children: tier === "layouts" ? ", plus the router's `children`" : "" },
|
|
209
|
-
})
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
},
|
|
213
|
-
}
|
|
214
|
-
},
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
// -- DRAWING-2 ----------------------------------------------------------------------------------
|
|
218
|
-
|
|
219
|
-
/** Only the sibling `index.tsx` (and its specs) may reach `component.tsx`; only `X` leaves the folder. */
|
|
220
|
-
export const baseImportPair = {
|
|
221
|
-
meta: {
|
|
222
|
-
type: "problem",
|
|
223
|
-
docs: { description: "A pure `component.tsx` is imported only by its sibling `index.tsx`, which never re-exports `XBase`." },
|
|
224
|
-
schema: [],
|
|
225
|
-
messages: {
|
|
226
|
-
reach:
|
|
227
|
-
"`{{source}}` reaches into another surface's pure half. Import that surface's connected export from its folder; the pure half is a private drawing surface.",
|
|
228
|
-
reexport:
|
|
229
|
-
"`index.tsx` re-exports `{{name}}`. The folder exports only its connected `X`; the pure `XBase` stays behind it so no other tier can bypass the owner.",
|
|
230
|
-
},
|
|
231
|
-
},
|
|
232
|
-
create(context) {
|
|
233
|
-
const isOwnIndex = roleOfFile(context) === "entry"
|
|
234
|
-
const inTier = inSplitTier(context)
|
|
235
|
-
// An import reaches a pure half when the file it resolves to has the `drawing` role of its slot.
|
|
236
|
-
const reachesPure = (source) => classifyImport(context, source)?.role === "drawing"
|
|
237
|
-
|
|
238
|
-
const check = (node, source) => {
|
|
239
|
-
if (typeof source !== "string" || !reachesPure(source)) return
|
|
240
|
-
const isSibling = /^\.\/[^/]+$/.test(source)
|
|
241
|
-
if (isSibling && isOwnIndex) return
|
|
242
|
-
if (isSibling && !inTier) return
|
|
243
|
-
context.report({ node, messageId: "reach", data: { source } })
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
return {
|
|
247
|
-
ImportDeclaration(node) {
|
|
248
|
-
check(node, node.source?.value)
|
|
249
|
-
},
|
|
250
|
-
ExportAllDeclaration(node) {
|
|
251
|
-
check(node, node.source?.value)
|
|
252
|
-
},
|
|
253
|
-
ExportNamedDeclaration(node) {
|
|
254
|
-
const source = node.source?.value
|
|
255
|
-
if (!source) return
|
|
256
|
-
check(node, source)
|
|
257
|
-
if (!isOwnIndex || !inTier || node.exportKind === "type") return
|
|
258
|
-
for (const specifier of node.specifiers ?? []) {
|
|
259
|
-
const name = specifier.exported?.name ?? specifier.local?.name
|
|
260
|
-
if (specifier.exportKind !== "type" && /^[A-Z][A-Za-z0-9]*Base$/.test(name)) {
|
|
261
|
-
context.report({ node: specifier, messageId: "reexport", data: { name } })
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
},
|
|
265
|
-
}
|
|
266
|
-
},
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
// -- DRAWING-3 ----------------------------------------------------------------------------------
|
|
270
|
-
|
|
271
|
-
/** Words that name a data status or an open/closed lifecycle, never a drawn shape. */
|
|
272
|
-
const DATA_STATUS = new Set([
|
|
273
|
-
"pending", "loading", "failed", "error", "errored", "empty", "forbidden", "unavailable",
|
|
274
|
-
"skeleton", "closed", "idle", "refreshing", "retrying",
|
|
275
|
-
])
|
|
276
|
-
|
|
277
|
-
/** A surface's `XState` names shapes only. */
|
|
278
|
-
export const noDataStatusShape = {
|
|
279
|
-
meta: {
|
|
280
|
-
type: "problem",
|
|
281
|
-
docs: { description: "`XState` of a pure half names drawn shapes, never a data status or open/closed." },
|
|
282
|
-
schema: [],
|
|
283
|
-
messages: {
|
|
284
|
-
status:
|
|
285
|
-
"`{{name}}` lists \"{{value}}\" as a shape. Loading, forbidden, error and empty describe resource status: pass them as data to the owned status renderer. Open/closed is a boolean atom. A shape is a layout the owner approves as one drawing.",
|
|
286
|
-
},
|
|
287
|
-
},
|
|
288
|
-
create(context) {
|
|
289
|
-
if (!pureHalf(context)) return {}
|
|
290
|
-
const check = (name, node) => {
|
|
291
|
-
const members = node?.type === "TSUnionType" ? node.types : [node]
|
|
292
|
-
for (const member of members) {
|
|
293
|
-
const value = member?.type === "TSLiteralType" ? member.literal?.value : undefined
|
|
294
|
-
if (typeof value === "string" && DATA_STATUS.has(value)) {
|
|
295
|
-
context.report({ node: member, messageId: "status", data: { name, value } })
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
return {
|
|
300
|
-
TSTypeAliasDeclaration(node) {
|
|
301
|
-
if (/^[A-Z][A-Za-z0-9]*State$/.test(node.id.name)) check(node.id.name, node.typeAnnotation)
|
|
302
|
-
},
|
|
303
|
-
}
|
|
304
|
-
},
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
/** The rules this law contributes to the plugin. */
|
|
308
|
-
export const rules = {
|
|
309
|
-
"base-props-atom": basePropsAtom,
|
|
310
|
-
"base-import-pair": baseImportPair,
|
|
311
|
-
"no-data-status-shape": noDataStatusShape,
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
/** Every FE canon rule is an error; existing debt is fixed before adoption. */
|
|
315
|
-
export const recommended = Object.fromEntries(Object.keys(rules).map((name) => [`starci-fe/${name}`, "error"]))
|
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"]))
|