@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
|
@@ -0,0 +1,291 @@
|
|
|
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
|
+
import { defineRule } from "./lib/rule.js";
|
|
15
|
+
import { isConfigModule } from "./lib/scope.js";
|
|
16
|
+
/** `process.env`, or `import.meta.env`. */
|
|
17
|
+
const isEnvObject = (node) => {
|
|
18
|
+
if (!node || node.type !== "MemberExpression" || node.computed)
|
|
19
|
+
return false;
|
|
20
|
+
const name = node.property && node.property.name;
|
|
21
|
+
if (name !== "env")
|
|
22
|
+
return false;
|
|
23
|
+
const object = node.object;
|
|
24
|
+
if (object.type === "Identifier" && object.name === "process")
|
|
25
|
+
return true;
|
|
26
|
+
return (object.type === "MetaProperty" && object.meta.name === "import" && object.property.name === "meta");
|
|
27
|
+
};
|
|
28
|
+
/** The variable name of `process.env.NAME` / `process.env["NAME"]`, or null. */
|
|
29
|
+
const envVariableName = (node) => {
|
|
30
|
+
if (!node || node.type !== "MemberExpression" || !isEnvObject(node.object))
|
|
31
|
+
return null;
|
|
32
|
+
if (!node.computed && node.property.type === "Identifier")
|
|
33
|
+
return node.property.name;
|
|
34
|
+
if (node.computed && node.property.type === "Literal" && typeof node.property.value === "string") {
|
|
35
|
+
return node.property.value;
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
};
|
|
39
|
+
/** Static text of a literal or an expression-free template, else null. */
|
|
40
|
+
const staticText = (node) => {
|
|
41
|
+
if (!node)
|
|
42
|
+
return null;
|
|
43
|
+
if (node.type === "Literal" && typeof node.value === "string")
|
|
44
|
+
return node.value;
|
|
45
|
+
if (node.type === "TemplateLiteral") {
|
|
46
|
+
return node.quasis.map((quasi) => quasi.value.cooked ?? "").join("");
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
};
|
|
50
|
+
/** An absolute or protocol-relative URL: what a fallback must never carry. */
|
|
51
|
+
const URL_TEXT = /^(?:https?:)?\/\/\S+/i;
|
|
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
|
+
/** Type assertions and optional chains preserve the identity of the wrapped value. */
|
|
55
|
+
const unwrapped = (node) => {
|
|
56
|
+
while (node && (node.type === "TSAsExpression" || node.type === "TSTypeAssertion" || node.type === "TSNonNullExpression" || node.type === "TSSatisfiesExpression" || node.type === "ChainExpression"))
|
|
57
|
+
node = node.expression;
|
|
58
|
+
return node;
|
|
59
|
+
};
|
|
60
|
+
/** The binding of this identifier, rather than every variable with the same spelling. */
|
|
61
|
+
const bindingOf = (sourceCode, identifier) => {
|
|
62
|
+
for (let scope = sourceCode.getScope(identifier); scope; scope = scope.upper) {
|
|
63
|
+
const variable = scope.set.get(identifier.name);
|
|
64
|
+
if (variable)
|
|
65
|
+
return variable;
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
};
|
|
69
|
+
/** Prior writes in the use's execution scope or its parents; a closure can capture a later initializer. */
|
|
70
|
+
const writeBefore = (sourceCode, variable, node) => {
|
|
71
|
+
const scopes = new Set();
|
|
72
|
+
for (let scope = sourceCode.getScope(node); scope; scope = scope.upper)
|
|
73
|
+
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
|
+
/** Property extraction between a write's source and its destructured binding. */
|
|
83
|
+
const propertyPath = (identifier) => {
|
|
84
|
+
const properties = [];
|
|
85
|
+
for (let node = identifier; node.parent; node = node.parent) {
|
|
86
|
+
const parent = node.parent;
|
|
87
|
+
if (parent.type === "Property" && parent.value === node)
|
|
88
|
+
properties.unshift(parent);
|
|
89
|
+
else if (parent.type !== "ObjectPattern" && !(parent.type === "AssignmentPattern" && parent.left === node))
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
return properties;
|
|
93
|
+
};
|
|
94
|
+
/** The first quasi of a template literal: the parser always gives one. */
|
|
95
|
+
const firstQuasi = (node) => {
|
|
96
|
+
const quasi = node.quasis[0];
|
|
97
|
+
if (quasi === undefined)
|
|
98
|
+
throw new TypeError("a template literal without a quasi");
|
|
99
|
+
return quasi;
|
|
100
|
+
};
|
|
101
|
+
/** The module an import binding comes from; an import-equals declaration has no `source`, which stops the run as it always has. */
|
|
102
|
+
const importSource = (parent) => {
|
|
103
|
+
if (parent.type !== "ImportDeclaration")
|
|
104
|
+
throw new TypeError("Cannot read properties of undefined (reading 'value')");
|
|
105
|
+
return parent.source.value;
|
|
106
|
+
};
|
|
107
|
+
/** True for the specifier of Node's process module. */
|
|
108
|
+
const isProcessModule = (source) => source === "process" || source === "node:process";
|
|
109
|
+
/** The import kind of the declaration that holds an import binding. */
|
|
110
|
+
const importKindOf = (parent) => parent.importKind;
|
|
111
|
+
/** Known environment containers and constant computed keys, followed through local aliases. */
|
|
112
|
+
const environmentIdentity = (sourceCode) => {
|
|
113
|
+
const textOf = (start, seen = new Set()) => {
|
|
114
|
+
let node = start;
|
|
115
|
+
node = unwrapped(node);
|
|
116
|
+
if (!node || seen.has(node))
|
|
117
|
+
return null;
|
|
118
|
+
if (node.type === "Literal" && typeof node.value === "string")
|
|
119
|
+
return node.value;
|
|
120
|
+
if (node.type === "TemplateLiteral" && node.expressions.length === 0)
|
|
121
|
+
return firstQuasi(node).value.cooked;
|
|
122
|
+
if (node.type !== "Identifier")
|
|
123
|
+
return null;
|
|
124
|
+
const variable = bindingOf(sourceCode, node);
|
|
125
|
+
if (!variable)
|
|
126
|
+
return null;
|
|
127
|
+
const write = writeBefore(sourceCode, variable, node);
|
|
128
|
+
if (!write?.writeExpr || propertyPath(write.identifier).length > 0)
|
|
129
|
+
return null;
|
|
130
|
+
return textOf(write.writeExpr, new Set([...seen, node]));
|
|
131
|
+
};
|
|
132
|
+
const keyOf = (node) => {
|
|
133
|
+
const key = node.type === "Property" ? node.key : node.property;
|
|
134
|
+
if (node.computed)
|
|
135
|
+
return textOf(key);
|
|
136
|
+
if (key.type === "Identifier" || key.type === "PrivateIdentifier")
|
|
137
|
+
return key.name;
|
|
138
|
+
return key.type === "Literal" ? key.value : undefined;
|
|
139
|
+
};
|
|
140
|
+
const propertyKind = (kind, key) => {
|
|
141
|
+
if (kind === "global" && key === "process")
|
|
142
|
+
return "process";
|
|
143
|
+
if ((kind === "process" || kind === "meta") && key === "env")
|
|
144
|
+
return "env";
|
|
145
|
+
return null;
|
|
146
|
+
};
|
|
147
|
+
const kindOf = (start, seen = new Set()) => {
|
|
148
|
+
const node = unwrapped(start);
|
|
149
|
+
if (!node || seen.has(node))
|
|
150
|
+
return null;
|
|
151
|
+
if (node.type === "MetaProperty" && node.meta.name === "import" && node.property.name === "meta")
|
|
152
|
+
return "meta";
|
|
153
|
+
if (node.type === "MemberExpression")
|
|
154
|
+
return propertyKind(kindOf(node.object, new Set([...seen, node])), keyOf(node));
|
|
155
|
+
if (node.type !== "Identifier")
|
|
156
|
+
return null;
|
|
157
|
+
const variable = bindingOf(sourceCode, node);
|
|
158
|
+
if (!variable || variable.defs.length === 0) {
|
|
159
|
+
if (node.name === "process")
|
|
160
|
+
return "process";
|
|
161
|
+
return ["globalThis", "global"].includes(node.name) ? "global" : null;
|
|
162
|
+
}
|
|
163
|
+
const imported = variable.defs.find((definition) => definition.type === "ImportBinding" && isProcessModule(importSource(definition.parent)));
|
|
164
|
+
if (imported && imported.type === "ImportBinding") {
|
|
165
|
+
if (importKindOf(imported.parent) === "type" || (imported.node.type === "ImportSpecifier" && imported.node.importKind === "type"))
|
|
166
|
+
return null;
|
|
167
|
+
if (imported.node.type !== "ImportSpecifier")
|
|
168
|
+
return "process";
|
|
169
|
+
const name = imported.node.imported.type === "Identifier" ? imported.node.imported.name : imported.node.imported.value;
|
|
170
|
+
return name === "default" ? "process" : name === "env" ? "env" : null;
|
|
171
|
+
}
|
|
172
|
+
const write = writeBefore(sourceCode, variable, node);
|
|
173
|
+
if (!write?.writeExpr)
|
|
174
|
+
return null;
|
|
175
|
+
let kind = kindOf(write.writeExpr, new Set([...seen, node]));
|
|
176
|
+
for (const property of propertyPath(write.identifier))
|
|
177
|
+
kind = propertyKind(kind, keyOf(property));
|
|
178
|
+
return kind;
|
|
179
|
+
};
|
|
180
|
+
const readsEnv = (pattern, kind) => {
|
|
181
|
+
if (pattern.type === "AssignmentPattern")
|
|
182
|
+
return readsEnv(pattern.left, kind);
|
|
183
|
+
if (pattern.type !== "ObjectPattern")
|
|
184
|
+
return false;
|
|
185
|
+
return pattern.properties.some((property) => {
|
|
186
|
+
if (property.type !== "Property")
|
|
187
|
+
return false;
|
|
188
|
+
const valueKind = propertyKind(kind, keyOf(property));
|
|
189
|
+
return valueKind === "env" || readsEnv(property.value, valueKind);
|
|
190
|
+
});
|
|
191
|
+
};
|
|
192
|
+
return { kindOf, readsEnv };
|
|
193
|
+
};
|
|
194
|
+
// -- FE-ENV-1 --------------------------------------------------------------------------------------
|
|
195
|
+
/** Only `modules/config` reads the environment. */
|
|
196
|
+
export const noEnvOutsideConfig = defineRule({
|
|
197
|
+
meta: {
|
|
198
|
+
type: "problem",
|
|
199
|
+
docs: { description: "`process.env` and `import.meta.env` are read only in `modules/config`." },
|
|
200
|
+
schema: [],
|
|
201
|
+
messages: {
|
|
202
|
+
env: "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.",
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
create(context) {
|
|
206
|
+
if (isConfigModule(context))
|
|
207
|
+
return {};
|
|
208
|
+
const members = [];
|
|
209
|
+
const destructures = [];
|
|
210
|
+
return {
|
|
211
|
+
ImportDeclaration(node) {
|
|
212
|
+
if (node.importKind === "type" || !["process", "node:process"].includes(node.source.value))
|
|
213
|
+
return;
|
|
214
|
+
for (const specifier of node.specifiers) {
|
|
215
|
+
if (specifier.type === "ImportSpecifier" && specifier.importKind !== "type" && (specifier.imported.type === "Identifier" ? specifier.imported.name : specifier.imported.value) === "env")
|
|
216
|
+
context.report({ node: specifier, messageId: "env" });
|
|
217
|
+
}
|
|
218
|
+
},
|
|
219
|
+
MemberExpression(node) {
|
|
220
|
+
members.push(node);
|
|
221
|
+
},
|
|
222
|
+
VariableDeclarator(node) {
|
|
223
|
+
if (node.id.type === "ObjectPattern" && node.init)
|
|
224
|
+
destructures.push({ node, pattern: node.id, value: node.init });
|
|
225
|
+
},
|
|
226
|
+
AssignmentExpression(node) {
|
|
227
|
+
if (node.operator === "=" && node.left.type === "ObjectPattern")
|
|
228
|
+
destructures.push({ node, pattern: node.left, value: node.right });
|
|
229
|
+
},
|
|
230
|
+
AssignmentPattern(node) {
|
|
231
|
+
if (node.left.type === "ObjectPattern")
|
|
232
|
+
destructures.push({ node, pattern: node.left, value: node.right });
|
|
233
|
+
},
|
|
234
|
+
"Program:exit"() {
|
|
235
|
+
const { kindOf, readsEnv } = environmentIdentity(context.sourceCode ?? context.getSourceCode());
|
|
236
|
+
for (const node of members) {
|
|
237
|
+
if (kindOf(node) === "env")
|
|
238
|
+
context.report({ node, messageId: "env" });
|
|
239
|
+
}
|
|
240
|
+
for (const { node, pattern, value } of destructures) {
|
|
241
|
+
if (readsEnv(pattern, kindOf(value)))
|
|
242
|
+
context.report({ node, messageId: "env" });
|
|
243
|
+
}
|
|
244
|
+
},
|
|
245
|
+
};
|
|
246
|
+
},
|
|
247
|
+
});
|
|
248
|
+
// -- FE-ENV-2 --------------------------------------------------------------------------------------
|
|
249
|
+
/** A fallback never stands in for a deployment address or secret. */
|
|
250
|
+
export const noHardcodedEndpointFallback = defineRule({
|
|
251
|
+
meta: {
|
|
252
|
+
type: "problem",
|
|
253
|
+
docs: { description: "No URL literal as a fallback, and no literal default for an address or secret variable." },
|
|
254
|
+
schema: [],
|
|
255
|
+
messages: {
|
|
256
|
+
url: "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.",
|
|
257
|
+
variable: "`{{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.",
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
create(context) {
|
|
261
|
+
return {
|
|
262
|
+
LogicalExpression(node) {
|
|
263
|
+
if (node.operator !== "??" && node.operator !== "||")
|
|
264
|
+
return;
|
|
265
|
+
const text = staticText(node.right);
|
|
266
|
+
if (text === null)
|
|
267
|
+
return;
|
|
268
|
+
if (URL_TEXT.test(text)) {
|
|
269
|
+
context.report({ node: node.right, messageId: "url" });
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
const name = envVariableName(node.left);
|
|
273
|
+
if (name && text !== "" && SENSITIVE_NAME.test(name)) {
|
|
274
|
+
context.report({ node: node.right, messageId: "variable", data: { name } });
|
|
275
|
+
}
|
|
276
|
+
},
|
|
277
|
+
AssignmentPattern(node) {
|
|
278
|
+
const text = staticText(node.right);
|
|
279
|
+
if (text !== null && URL_TEXT.test(text))
|
|
280
|
+
context.report({ node: node.right, messageId: "url" });
|
|
281
|
+
},
|
|
282
|
+
};
|
|
283
|
+
},
|
|
284
|
+
});
|
|
285
|
+
/** The rules this law contributes to the plugin. */
|
|
286
|
+
export const rules = {
|
|
287
|
+
"no-env-outside-config": noEnvOutsideConfig,
|
|
288
|
+
"no-hardcoded-endpoint-fallback": noHardcodedEndpointFallback,
|
|
289
|
+
};
|
|
290
|
+
/** Both are exact syntactic shapes, and both are errors. */
|
|
291
|
+
export const recommended = Object.fromEntries(Object.keys(rules).map((name) => [`starci-fe/${name}`, "error"]));
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type Rules, type Recommended } from "./lib/rule.js";
|
|
2
|
+
/**
|
|
3
|
+
* No import (static, re-export, dynamic or `require`) of a package the fe-stack catalog forbids, and no `@supabase/*` outside
|
|
4
|
+
* `modules/auth`. Type-only imports count: a type import still makes the package a dependency of the front end.
|
|
5
|
+
*/
|
|
6
|
+
export declare const feStackImport: import("./lib/rule.js").Rule<"forbidden" | "supabase", []>;
|
|
7
|
+
/** The rules this law contributes to the plugin. */
|
|
8
|
+
export declare const rules: Rules;
|
|
9
|
+
/** Every rule is an error: a second library for a concern is the defect, not a style. */
|
|
10
|
+
export declare const recommended: Recommended;
|
package/dist/fe-stack.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { defineRule } from "./lib/rule.js";
|
|
2
|
+
import { forbiddingConcern, packageNameOf } from "./lib/fe-stack.js";
|
|
3
|
+
import { globalReferences, inSlot, isProductSource } from "./lib/scope.js";
|
|
4
|
+
/** The package scope only the identity-provider client may import. */
|
|
5
|
+
const SUPABASE_SCOPE = "@supabase/";
|
|
6
|
+
/**
|
|
7
|
+
* No import (static, re-export, dynamic or `require`) of a package the fe-stack catalog forbids, and no `@supabase/*` outside
|
|
8
|
+
* `modules/auth`. Type-only imports count: a type import still makes the package a dependency of the front end.
|
|
9
|
+
*/
|
|
10
|
+
export const feStackImport = defineRule({
|
|
11
|
+
meta: {
|
|
12
|
+
type: "problem",
|
|
13
|
+
docs: { description: "A front end imports no library the fe-stack catalog forbids, and `@supabase/*` only in `modules/auth`." },
|
|
14
|
+
schema: [],
|
|
15
|
+
messages: {
|
|
16
|
+
forbidden: "`{{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}}",
|
|
17
|
+
supabase: "`{{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.",
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
create(context) {
|
|
21
|
+
if (!isProductSource(context))
|
|
22
|
+
return {};
|
|
23
|
+
const authOwner = inSlot(context, "fe.modules.auth");
|
|
24
|
+
const judge = (node, source) => {
|
|
25
|
+
const name = packageNameOf(source);
|
|
26
|
+
if (name === null || name === undefined)
|
|
27
|
+
return;
|
|
28
|
+
const concern = forbiddingConcern(source);
|
|
29
|
+
if (concern) {
|
|
30
|
+
const named = (concern.packages?.length ?? 0) > 0 ? concern.packages ?? [] : concern.home ?? [];
|
|
31
|
+
const canonical = named.map((entry) => `\`${entry}\``).join(", ");
|
|
32
|
+
context.report({ node, messageId: "forbidden", data: { name, concern: concern.id, canonical, pattern: concern.pattern ?? "", why: concern.why ?? "" } });
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
if (name.startsWith(SUPABASE_SCOPE) && !authOwner)
|
|
36
|
+
context.report({ node, messageId: "supabase", data: { name } });
|
|
37
|
+
};
|
|
38
|
+
return {
|
|
39
|
+
ImportDeclaration(node) {
|
|
40
|
+
judge(node, node.source.value);
|
|
41
|
+
},
|
|
42
|
+
ExportNamedDeclaration(node) {
|
|
43
|
+
if (node.source)
|
|
44
|
+
judge(node, node.source.value);
|
|
45
|
+
},
|
|
46
|
+
ExportAllDeclaration(node) {
|
|
47
|
+
judge(node, node.source.value);
|
|
48
|
+
},
|
|
49
|
+
ImportExpression(node) {
|
|
50
|
+
if (node.source.type === "Literal")
|
|
51
|
+
judge(node, node.source.value);
|
|
52
|
+
},
|
|
53
|
+
TSImportEqualsDeclaration(node) {
|
|
54
|
+
// `import x = require("moment")`
|
|
55
|
+
if (node.moduleReference?.type === "TSExternalModuleReference" && node.moduleReference.expression?.type === "Literal")
|
|
56
|
+
judge(node, node.moduleReference.expression.value);
|
|
57
|
+
},
|
|
58
|
+
CallExpression(node) {
|
|
59
|
+
// `require("moment")` where `require` is the CommonJS global.
|
|
60
|
+
if (node.callee.type !== "Identifier" || node.callee.name !== "require" || node.arguments[0]?.type !== "Literal")
|
|
61
|
+
return;
|
|
62
|
+
if (globalReferences(context, new Set(["require"])).includes(node.callee))
|
|
63
|
+
judge(node, node.arguments[0].value);
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
/** The rules this law contributes to the plugin. */
|
|
69
|
+
export const rules = {
|
|
70
|
+
"fe-stack-import": feStackImport,
|
|
71
|
+
};
|
|
72
|
+
/** Every rule is an error: a second library for a concern is the defect, not a style. */
|
|
73
|
+
export const recommended = Object.fromEntries(Object.keys(rules).map((name) => [`starci-fe/${name}`, "error"]));
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The rules that hold `file-layout.md`.
|
|
3
|
+
*
|
|
4
|
+
* Four rules locking the same habit: stuffing a cluster into the folder of one screen. It always
|
|
5
|
+
* starts harmlessly - "only this page uses it" - and ends as a page folder holding four components,
|
|
6
|
+
* a constants folder, a utils folder and three hand-copied resting shapes, at which point the
|
|
7
|
+
* screen is a second codebase with its own private vocabulary that nobody else can reuse.
|
|
8
|
+
*
|
|
9
|
+
* Every rule here asks the HFS slot view where the file sits (slot, layer kind, role, tier) rather than
|
|
10
|
+
* reading the contents, which is what makes them cheap and exact. The cost of that choice is stated where it
|
|
11
|
+
* bites: a slot cannot tell a component from a helper, only a folder from a folder, so each rule below names the
|
|
12
|
+
* destination it sends things to rather than merely refusing.
|
|
13
|
+
*/
|
|
14
|
+
import { type Rules, type Recommended } from "./lib/rule.js";
|
|
15
|
+
/** A page, layout or overlay folder holds its two halves and its colocated class-name module. */
|
|
16
|
+
export declare const surfaceFolderTwoFilesOnly: import("./lib/rule.js").Rule<"extra", []>;
|
|
17
|
+
/** What is not component code does not live in the component tree. */
|
|
18
|
+
export declare const noHelperFolderInComponents: import("./lib/rule.js").Rule<"helper", []>;
|
|
19
|
+
/** A PascalCase folder exports a family whose name belongs to the folder. */
|
|
20
|
+
export declare const exportMatchesFolder: import("./lib/rule.js").Rule<"mismatch", []>;
|
|
21
|
+
/** A folder exports a family, never one runtime object standing in for a namespace. */
|
|
22
|
+
export declare const noRuntimeNamespace: import("./lib/rule.js").Rule<"namespace", []>;
|
|
23
|
+
/**
|
|
24
|
+
* In a front end with two or more apps, the shared package stops below the block.
|
|
25
|
+
*
|
|
26
|
+
* WHY THE SLOT IS ENOUGH HERE. The feature line is already drawn by the layer: a leaf knows
|
|
27
|
+
* no domain and a block is a domain sentence, so which side of the workspace each belongs on
|
|
28
|
+
* follows from the layer the slot reports. The layers a shared package may hold are the `kinds` of
|
|
29
|
+
* `fe.package.ui`; an app holds the shared ones only when the repository declares that package and another app can
|
|
30
|
+
* consume them. A one-app front end has nothing to share, so every tier may stay with that app.
|
|
31
|
+
*
|
|
32
|
+
* WHAT IT COST TO LEARN. A shared package carried one `blocks/WorkerRow/` while its own header
|
|
33
|
+
* insisted that "a block carries feature meaning and therefore belongs to the app that owns the
|
|
34
|
+
* feature". The document was right, the tree disagreed, and nothing turned red for either of them -
|
|
35
|
+
* which is the whole argument for this rule rather than the paragraph.
|
|
36
|
+
*/
|
|
37
|
+
export declare const monorepoTierBelongsToItsSide: import("./lib/rule.js").Rule<"featureInPackage" | "vocabularyInApp", []>;
|
|
38
|
+
/**
|
|
39
|
+
* A file under `app/` names which page renders at which URL, and does nothing else.
|
|
40
|
+
*
|
|
41
|
+
* WHY THIS RULE EXISTS RATHER THAN THE PARAGRAPH ALONE. The law said "the route file mounts and
|
|
42
|
+
* nothing else" and had said so for as long as the tier list existed. Nothing checked it. A page
|
|
43
|
+
* owner was written to `app/<segment>/workers-page.tsx`, carried through a build, a lint run, a
|
|
44
|
+
* typecheck, four sealed screenshots and an approval, and arrived at the edge of a production write
|
|
45
|
+
* with every gate green - because every gate was reading rules, and this one was only prose.
|
|
46
|
+
*
|
|
47
|
+
* WHY IT READS THE SLOT AND NOT THE CONTENTS. "Drawing" is not a property this can measure: a
|
|
48
|
+
* route that mounts one component and a route that arranges six both return JSX. What CAN be
|
|
49
|
+
* measured exactly is whether a file in the routing tree (slot `fe.route`) is one of the framework's own slots. A page
|
|
50
|
+
* owner is a component, a component has a name, and a named component belongs in the tier that
|
|
51
|
+
* groups it with its siblings - `components/pages/<Name>/`, where the next author looks for it.
|
|
52
|
+
*
|
|
53
|
+
* The cost is stated where it bites: a route file that draws inside `page.tsx` still passes. That is
|
|
54
|
+
* FILE-2's `component.tsx` split to catch, and no path rule can see it.
|
|
55
|
+
*/
|
|
56
|
+
export declare const routeTreeHoldsRoutesOnly: import("./lib/rule.js").Rule<"stray", []>;
|
|
57
|
+
/** `meta.shape` must tell the same truth as the source path. */
|
|
58
|
+
export declare const sourceTierMarkerMatchesFolder: import("./lib/rule.js").Rule<"mismatch", []>;
|
|
59
|
+
/** `shells/` is not a tier; vendor mechanics are closed named branches. */
|
|
60
|
+
export declare const noShellTier: import("./lib/rule.js").Rule<"shell", []>;
|
|
61
|
+
export declare const routeSlotFixedName: import("./lib/rule.js").Rule<"missing" | "wrong" | "anonymous", []>;
|
|
62
|
+
export declare const rules: Rules;
|
|
63
|
+
/**
|
|
64
|
+
* The level this law asks for, as the plugin's own opinion.
|
|
65
|
+
*
|
|
66
|
+
* A consuming repository's `eslint.config.mjs` stays the authority on what is actually switched on.
|
|
67
|
+
* `export-matches-folder` is the one worth adopting at `warn` first in an existing tree: it fires
|
|
68
|
+
* on every folder whose convention predates the rule, and that count is a migration rather than a
|
|
69
|
+
* defect.
|
|
70
|
+
*/
|
|
71
|
+
export declare const recommended: Recommended;
|