@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
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { defineRule } from "./lib/rule.js";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { dirname, resolve } from "node:path";
|
|
4
|
+
import { unwrap, variableOf } from "./lib/bindings.js";
|
|
5
|
+
/** Every published entry of the Grammar package, the root and each subpath. */
|
|
6
|
+
const GRAMMAR_SOURCE = /^@starci\/grammar(?:\/|$)/;
|
|
7
|
+
/** The Grammar package's own manifest name; its source re-exports by design. */
|
|
8
|
+
const GRAMMAR_PACKAGE = "@starci/grammar";
|
|
9
|
+
/** The manifest name of the package owning each directory, read once per directory. */
|
|
10
|
+
const packageNames = new Map();
|
|
11
|
+
/** The `name` of the nearest `package.json` that declares one, or null at the filesystem root. */
|
|
12
|
+
const packageNameOf = (directory) => {
|
|
13
|
+
const known = packageNames.get(directory);
|
|
14
|
+
if (known !== undefined)
|
|
15
|
+
return known;
|
|
16
|
+
const parent = dirname(directory);
|
|
17
|
+
const manifest = resolve(directory, "package.json");
|
|
18
|
+
let name = null;
|
|
19
|
+
if (existsSync(manifest)) {
|
|
20
|
+
try {
|
|
21
|
+
const parsed = JSON.parse(readFileSync(manifest, "utf8"));
|
|
22
|
+
const declared = typeof parsed === "object" && parsed !== null && "name" in parsed ? parsed.name : undefined;
|
|
23
|
+
if (typeof declared === "string")
|
|
24
|
+
name = declared;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
name = null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (name === null && parent !== directory)
|
|
31
|
+
name = packageNameOf(parent);
|
|
32
|
+
packageNames.set(directory, name);
|
|
33
|
+
return name;
|
|
34
|
+
};
|
|
35
|
+
/** True for a type-only import or export, declaration-wide or per specifier; a type is no component to shim. */
|
|
36
|
+
const typeOnly = (...nodes) => nodes.some((node) => ("importKind" in node && node.importKind === "type") || ("exportKind" in node && node.exportKind === "type"));
|
|
37
|
+
/** The Grammar import an identifier binds, `{ source, name }`, or null for a local, foreign or type-only binding. */
|
|
38
|
+
const grammarImportOf = (context, identifier) => {
|
|
39
|
+
const definition = variableOf(context, identifier)?.defs.find((entry) => entry.type === "ImportBinding");
|
|
40
|
+
if (!definition || definition.type !== "ImportBinding")
|
|
41
|
+
return null;
|
|
42
|
+
if (definition.parent.type !== "ImportDeclaration")
|
|
43
|
+
throw new TypeError("Cannot read properties of undefined (reading 'value')");
|
|
44
|
+
const source = String(definition.parent.source.value);
|
|
45
|
+
if (!GRAMMAR_SOURCE.test(source) || typeOnly(definition.parent, definition.node))
|
|
46
|
+
return null;
|
|
47
|
+
const specifier = definition.node;
|
|
48
|
+
const name = specifier.type === "ImportSpecifier" ? spelled(specifier.imported) : identifier.name;
|
|
49
|
+
return { source, name };
|
|
50
|
+
};
|
|
51
|
+
/** The Grammar import an exported value is exactly (`X`, `G.X`), or null for anything computed or wrapped. */
|
|
52
|
+
const grammarValueOf = (context, expression) => {
|
|
53
|
+
let node = unwrap(expression);
|
|
54
|
+
const path = [];
|
|
55
|
+
while (node?.type === "MemberExpression" && !node.computed && node.property.type === "Identifier") {
|
|
56
|
+
path.unshift(node.property.name);
|
|
57
|
+
node = unwrap(node.object);
|
|
58
|
+
}
|
|
59
|
+
if (node?.type !== "Identifier")
|
|
60
|
+
return null;
|
|
61
|
+
const imported = grammarImportOf(context, node);
|
|
62
|
+
if (imported === null)
|
|
63
|
+
return null;
|
|
64
|
+
return path.length === 0 ? imported : { source: imported.source, name: path.join(".") };
|
|
65
|
+
};
|
|
66
|
+
/** The name an export specifier spells, identifier or string literal. */
|
|
67
|
+
const spelled = (node) => (node.type === "Identifier" ? node.name : String(node.value));
|
|
68
|
+
const grammarShim = defineRule({
|
|
69
|
+
meta: {
|
|
70
|
+
type: "problem",
|
|
71
|
+
docs: { description: "R321: an app never re-exports a Grammar export under its own name; a Grammar defect is reported to the Grammar, not shimmed in the app." },
|
|
72
|
+
schema: [],
|
|
73
|
+
messages: {
|
|
74
|
+
FE_GRAMMAR_SHIM: "FE_GRAMMAR_SHIM: this file re-exports {{name}} of {{source}} under the app's own name. A defect in a Grammar component belongs to the Grammar: report it (the op files `blocked` naming the package, the component and the error), never shim it in the app; import the component from {{source}} where it is rendered.",
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
create(context) {
|
|
78
|
+
const filename = resolve(context.cwd, context.physicalFilename);
|
|
79
|
+
if (packageNameOf(dirname(filename)) === GRAMMAR_PACKAGE)
|
|
80
|
+
return {};
|
|
81
|
+
const report = (node, name, source) => context.report({ node, messageId: "FE_GRAMMAR_SHIM", data: { name: name === null ? "every export" : `\`${name}\``, source } });
|
|
82
|
+
return {
|
|
83
|
+
ExportAllDeclaration(node) {
|
|
84
|
+
const source = String(node.source.value);
|
|
85
|
+
if (!GRAMMAR_SOURCE.test(source) || typeOnly(node))
|
|
86
|
+
return;
|
|
87
|
+
report(node, node.exported ? spelled(node.exported) : null, source);
|
|
88
|
+
},
|
|
89
|
+
ExportNamedDeclaration(node) {
|
|
90
|
+
if (typeOnly(node))
|
|
91
|
+
return;
|
|
92
|
+
if (node.source) {
|
|
93
|
+
const source = String(node.source.value);
|
|
94
|
+
if (!GRAMMAR_SOURCE.test(source))
|
|
95
|
+
return;
|
|
96
|
+
for (const specifier of node.specifiers) {
|
|
97
|
+
if (!typeOnly(specifier))
|
|
98
|
+
report(specifier, spelled(specifier.local), source);
|
|
99
|
+
}
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
for (const specifier of node.specifiers) {
|
|
103
|
+
const imported = typeOnly(specifier) || specifier.local.type !== "Identifier" ? null : grammarImportOf(context, specifier.local);
|
|
104
|
+
if (imported)
|
|
105
|
+
report(specifier, imported.name, imported.source);
|
|
106
|
+
}
|
|
107
|
+
if (node.declaration?.type !== "VariableDeclaration")
|
|
108
|
+
return;
|
|
109
|
+
for (const declarator of node.declaration.declarations) {
|
|
110
|
+
const imported = declarator.init ? grammarValueOf(context, declarator.init) : null;
|
|
111
|
+
if (imported)
|
|
112
|
+
report(declarator, imported.name, imported.source);
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
ExportDefaultDeclaration(node) {
|
|
116
|
+
const imported = grammarValueOf(context, node.declaration);
|
|
117
|
+
if (imported)
|
|
118
|
+
report(node, imported.name, imported.source);
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
/** The one rule of the law. */
|
|
124
|
+
export const rules = { "no-grammar-shim": grammarShim };
|
|
125
|
+
/** The rule reaches consumers at error. */
|
|
126
|
+
export const recommended = Object.fromEntries(Object.keys(rules).map((name) => [`starci-fe/${name}`, "error"]));
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The rule that holds `hooks-folder.md` (HFS R56 `FE_HOOKS_ARE_HOOKS`, lint half).
|
|
3
|
+
*
|
|
4
|
+
* `hooks/` MEANS REACT HOOKS. One app kept its server readers there - fetch, map, no React - and
|
|
5
|
+
* had a single real hook; another had a filename-equals-export rule that gave a helper shared by
|
|
6
|
+
* sixteen hooks nowhere to live, so it was copied sixteen times. Both came from the standard not
|
|
7
|
+
* saying where the non-hook code goes. It now does, per family of the front-end stack (each its own slot):
|
|
8
|
+
*
|
|
9
|
+
* hooks/swr/api/{graphql,rest}/{queries,mutations}/use{Query,Mutate,Get,Post,Put,Patch,Delete}<X>Swr.ts
|
|
10
|
+
* hooks/swr/api/sse/subscriptions/useSubscribe<X>Swr.ts
|
|
11
|
+
* one SWR hook per modules/api operation (fe.hooks.swr), plus SwrSideEffects.tsx
|
|
12
|
+
* hooks/rhf/use<X>Form.ts one form (fe.hooks.rhf)
|
|
13
|
+
* hooks/zustand/<store>/store.ts one local store and its use*.ts selectors (fe.hooks.zustand)
|
|
14
|
+
* hooks/socketio/ sockets.ts, SocketIoSideEffects.tsx, enums/, types/, use*.ts (fe.hooks.socketio)
|
|
15
|
+
* hooks/effects/use<X>.ts app-wide side effects (fe.hooks.effects)
|
|
16
|
+
* hooks/reuseables/use<X>.ts generic reusable hooks (fe.hooks.reuseables)
|
|
17
|
+
* hooks/<domain>/use<Name>.ts one hook per file (fe.hooks)
|
|
18
|
+
* hooks/<domain>/<domain>.shared.ts one file for the domain's non-hook helpers (key builders...)
|
|
19
|
+
*
|
|
20
|
+
* A call to the back end is never a hook's own code: it is an operation of `modules/api`, which an SWR hook wraps. The two
|
|
21
|
+
* `.tsx` files a family names (`SwrSideEffects.tsx`, `SocketIoSideEffects.tsx`, role `effects`) are the components a provider
|
|
22
|
+
* mounts once; every other file of a family carries the name its slot gives it.
|
|
23
|
+
*
|
|
24
|
+
* The rule holds the shape of one file at a time. Whether a folder holds exactly one `.shared.ts`
|
|
25
|
+
* and whether a helper name repeats across files is a repository fact, and belongs to the
|
|
26
|
+
* architecture machine that sees the whole tree.
|
|
27
|
+
*/
|
|
28
|
+
import { type Rules, type Recommended } from "./lib/rule.js";
|
|
29
|
+
/** `hooks/` holds React hooks, each under the name its family or domain gives it, and one shared helper file per domain. */
|
|
30
|
+
export declare const hooksFolderHoldsHooksOnly: import("./lib/rule.js").Rule<"server" | "missing" | "defaultExport" | "export" | "many" | "fake" | "domain" | "notHook" | "family" | "subscription" | "sharedHook", []>;
|
|
31
|
+
/** The rules this law contributes to the plugin. */
|
|
32
|
+
export declare const rules: Rules;
|
|
33
|
+
/** Every rule is an error. */
|
|
34
|
+
export declare const recommended: Recommended;
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The rule that holds `hooks-folder.md` (HFS R56 `FE_HOOKS_ARE_HOOKS`, lint half).
|
|
3
|
+
*
|
|
4
|
+
* `hooks/` MEANS REACT HOOKS. One app kept its server readers there - fetch, map, no React - and
|
|
5
|
+
* had a single real hook; another had a filename-equals-export rule that gave a helper shared by
|
|
6
|
+
* sixteen hooks nowhere to live, so it was copied sixteen times. Both came from the standard not
|
|
7
|
+
* saying where the non-hook code goes. It now does, per family of the front-end stack (each its own slot):
|
|
8
|
+
*
|
|
9
|
+
* hooks/swr/api/{graphql,rest}/{queries,mutations}/use{Query,Mutate,Get,Post,Put,Patch,Delete}<X>Swr.ts
|
|
10
|
+
* hooks/swr/api/sse/subscriptions/useSubscribe<X>Swr.ts
|
|
11
|
+
* one SWR hook per modules/api operation (fe.hooks.swr), plus SwrSideEffects.tsx
|
|
12
|
+
* hooks/rhf/use<X>Form.ts one form (fe.hooks.rhf)
|
|
13
|
+
* hooks/zustand/<store>/store.ts one local store and its use*.ts selectors (fe.hooks.zustand)
|
|
14
|
+
* hooks/socketio/ sockets.ts, SocketIoSideEffects.tsx, enums/, types/, use*.ts (fe.hooks.socketio)
|
|
15
|
+
* hooks/effects/use<X>.ts app-wide side effects (fe.hooks.effects)
|
|
16
|
+
* hooks/reuseables/use<X>.ts generic reusable hooks (fe.hooks.reuseables)
|
|
17
|
+
* hooks/<domain>/use<Name>.ts one hook per file (fe.hooks)
|
|
18
|
+
* hooks/<domain>/<domain>.shared.ts one file for the domain's non-hook helpers (key builders...)
|
|
19
|
+
*
|
|
20
|
+
* A call to the back end is never a hook's own code: it is an operation of `modules/api`, which an SWR hook wraps. The two
|
|
21
|
+
* `.tsx` files a family names (`SwrSideEffects.tsx`, `SocketIoSideEffects.tsx`, role `effects`) are the components a provider
|
|
22
|
+
* mounts once; every other file of a family carries the name its slot gives it.
|
|
23
|
+
*
|
|
24
|
+
* The rule holds the shape of one file at a time. Whether a folder holds exactly one `.shared.ts`
|
|
25
|
+
* and whether a helper name repeats across files is a repository fact, and belongs to the
|
|
26
|
+
* architecture machine that sees the whole tree.
|
|
27
|
+
*/
|
|
28
|
+
import { defineRule } from "./lib/rule.js";
|
|
29
|
+
import { importedReduxHook } from "./lib/redux-hook-origin.js";
|
|
30
|
+
import { importedOwnHook } from "./lib/own-hook-origin.js";
|
|
31
|
+
import { hfsOf } from "./lib/hfs.js";
|
|
32
|
+
import { walk } from "./lib/ast.js";
|
|
33
|
+
import { functionOf, isFn, unwrap, variableOf } from "./lib/bindings.js";
|
|
34
|
+
import { baseName, classOf, fileOf, inSlot } from "./lib/scope.js";
|
|
35
|
+
import { rules as swrFamilyRules } from "./swr-family.js";
|
|
36
|
+
/** Names that mark a hook. */
|
|
37
|
+
const isHookName = (name) => /^use[A-Z0-9]/.test(name);
|
|
38
|
+
/**
|
|
39
|
+
* The SWR key of a hook (`QUERY_<X>_SWR_KEY`, `MUTATE_<X>_SWR_KEY`, `SUBSCRIBE_<X>_SWR_KEY`, fe-stack data): the one constant a hook file exports beside its
|
|
40
|
+
* hook, so a mutation or a socket message revalidates exactly that cache entry.
|
|
41
|
+
*/
|
|
42
|
+
const isSwrKeyName = (name) => /^[A-Z][A-Z0-9_]*_SWR_KEY$/.test(name);
|
|
43
|
+
/** A hook file name: `use<Name>.ts`. */
|
|
44
|
+
const HOOK_FILE = new RegExp(String.raw `^use[A-Z0-9]\w*\.ts$`);
|
|
45
|
+
const isHookFileName = (name) => HOOK_FILE.test(name);
|
|
46
|
+
/** Modules that only run on the server: server code, not a hook. */
|
|
47
|
+
const SERVER_ONLY = new Set(["server-only", "next/headers"]);
|
|
48
|
+
/** The hooks families of the front-end stack: each slot names its files (`allows`) and its non-hook files (roles). */
|
|
49
|
+
const FAMILY_SLOTS = ["fe.hooks.swr", "fe.hooks.rhf", "fe.hooks.zustand", "fe.hooks.socketio", "fe.hooks.effects", "fe.hooks.reuseables"];
|
|
50
|
+
/** True for a statement that exports. */
|
|
51
|
+
const isExportNode = (node) => ["ExportNamedDeclaration", "ExportDefaultDeclaration", "ExportAllDeclaration"].includes(node.type);
|
|
52
|
+
/** The name an export specifier spells, identifier or string literal. */
|
|
53
|
+
const spelled = (node) => (node.type === "Identifier" ? node.name : node.value);
|
|
54
|
+
const exportedEntries = (node) => {
|
|
55
|
+
if (node.type === "ExportDefaultDeclaration") {
|
|
56
|
+
const declaration = node.declaration;
|
|
57
|
+
const id = "id" in declaration ? declaration.id : undefined;
|
|
58
|
+
return [{ node, name: id?.type === "Identifier" ? id.name : "default", local: declaration, default: true }];
|
|
59
|
+
}
|
|
60
|
+
if (node.exportKind === "type")
|
|
61
|
+
return [];
|
|
62
|
+
if (node.type === "ExportAllDeclaration")
|
|
63
|
+
return [{ node, name: "*", local: null }];
|
|
64
|
+
const declaration = node.declaration;
|
|
65
|
+
if (!declaration) {
|
|
66
|
+
return node.specifiers.filter((specifier) => specifier.exportKind !== "type").map((specifier) => ({
|
|
67
|
+
node, name: spelled(specifier.exported), local: node.source ? null : specifier.local,
|
|
68
|
+
default: spelled(specifier.exported) === "default",
|
|
69
|
+
}));
|
|
70
|
+
}
|
|
71
|
+
if (declaration.type === "FunctionDeclaration" && declaration.id)
|
|
72
|
+
return [{ node, name: declaration.id.name, local: declaration }];
|
|
73
|
+
if (declaration.type === "VariableDeclaration") {
|
|
74
|
+
return declaration.declarations.flatMap((d) => (d.id.type === "Identifier" ? [{ node, name: d.id.name, local: d.id }] : []));
|
|
75
|
+
}
|
|
76
|
+
return [];
|
|
77
|
+
};
|
|
78
|
+
/** Actual React hook APIs and SWR public entry points, rather than an arbitrary `use*` spelling. */
|
|
79
|
+
const REACT_HOOKS = new Set([
|
|
80
|
+
"use", "useState", "useReducer", "useEffect", "useLayoutEffect", "useInsertionEffect", "useRef", "useImperativeHandle",
|
|
81
|
+
"useMemo", "useCallback", "useContext", "useDebugValue", "useDeferredValue", "useTransition", "useId",
|
|
82
|
+
"useSyncExternalStore", "useActionState", "useOptimistic",
|
|
83
|
+
]);
|
|
84
|
+
const SWR_HOOKS = new Map([
|
|
85
|
+
["swr", new Set(["useSWR", "useSWRConfig"])],
|
|
86
|
+
["swr/immutable", new Set(["useSWRImmutable"])],
|
|
87
|
+
["swr/infinite", new Set(["useSWRInfinite"])],
|
|
88
|
+
["swr/mutation", new Set(["useSWRMutation"])],
|
|
89
|
+
["swr/subscription", new Set(["useSWRSubscription"])],
|
|
90
|
+
]);
|
|
91
|
+
const SWR_DEFAULTS = new Map([["swr", "useSWR"], ["swr/immutable", "useSWRImmutable"], ["swr/infinite", "useSWRInfinite"], ["swr/mutation", "useSWRMutation"], ["swr/subscription", "useSWRSubscription"]]);
|
|
92
|
+
/**
|
|
93
|
+
* The canonical library hooks a composite hook may call (R56, FE-THEME): next-intl, next/navigation,
|
|
94
|
+
* next-themes, react-hook-form and zustand's React entry points. A name outside the list is not a hook by its spelling.
|
|
95
|
+
*/
|
|
96
|
+
const LIBRARY_HOOKS = new Map([
|
|
97
|
+
["next-intl", new Set(["useTranslations", "useLocale", "useFormatter", "useNow", "useTimeZone", "useMessages"])],
|
|
98
|
+
["next/navigation", new Set(["useRouter", "usePathname", "useSearchParams", "useParams", "useSelectedLayoutSegment", "useSelectedLayoutSegments"])],
|
|
99
|
+
["next-themes", new Set(["useTheme"])],
|
|
100
|
+
["react-hook-form", new Set(["useForm", "useFormContext", "useWatch", "useController", "useFieldArray", "useFormState"])],
|
|
101
|
+
["zustand", new Set(["useStore"])],
|
|
102
|
+
["zustand/react/shallow", new Set(["useShallow"])],
|
|
103
|
+
]);
|
|
104
|
+
const OWN_HOOK = "own-hooks";
|
|
105
|
+
/** True when `set` holds `name`. */
|
|
106
|
+
const hasName = (set, name) => set !== undefined && typeof name === "string" && set.has(name);
|
|
107
|
+
const builtinHook = (origin) => {
|
|
108
|
+
if (origin === null)
|
|
109
|
+
return false;
|
|
110
|
+
return origin.source === OWN_HOOK || hasName(LIBRARY_HOOKS.get(origin.source), origin.name) || origin.source === "react-redux" || (origin.source === "react" ? hasName(REACT_HOOKS, origin.name) : hasName(SWR_HOOKS.get(origin.source), origin.name));
|
|
111
|
+
};
|
|
112
|
+
const propertyName = (node) => {
|
|
113
|
+
if (!node.computed && node.property.type === "Identifier")
|
|
114
|
+
return node.property.name;
|
|
115
|
+
return node.property.type === "Literal" ? node.property.value : null;
|
|
116
|
+
};
|
|
117
|
+
/** SWR request hooks are default exports; its root also names useSWRConfig. */
|
|
118
|
+
const namedOrigin = (source, name) => {
|
|
119
|
+
if (source === "react")
|
|
120
|
+
return { source, name };
|
|
121
|
+
if (hasName(LIBRARY_HOOKS.get(source), name))
|
|
122
|
+
return { source, name };
|
|
123
|
+
if (name === "default" && SWR_DEFAULTS.has(source))
|
|
124
|
+
return { source, name: SWR_DEFAULTS.get(source) };
|
|
125
|
+
return source === "swr" && name === "useSWRConfig" ? { source, name } : null;
|
|
126
|
+
};
|
|
127
|
+
/** A runtime import records whether its value is a hook or its module namespace. */
|
|
128
|
+
const importOrigin = (definition) => {
|
|
129
|
+
const specifier = definition.node;
|
|
130
|
+
if (definition.parent.type !== "ImportDeclaration")
|
|
131
|
+
throw new TypeError("Cannot read properties of undefined (reading 'value')");
|
|
132
|
+
const source = String(definition.parent.source.value);
|
|
133
|
+
if (definition.parent.importKind === "type" || ("importKind" in specifier && specifier.importKind === "type"))
|
|
134
|
+
return null;
|
|
135
|
+
if (source !== "react" && !SWR_HOOKS.has(source) && !LIBRARY_HOOKS.has(source))
|
|
136
|
+
return null;
|
|
137
|
+
if (source === "next-themes" && specifier.type === "ImportDefaultSpecifier")
|
|
138
|
+
return null;
|
|
139
|
+
if (specifier.type === "ImportSpecifier")
|
|
140
|
+
return namedOrigin(source, spelled(specifier.imported));
|
|
141
|
+
if (specifier.type === "ImportDefaultSpecifier" && SWR_DEFAULTS.has(source))
|
|
142
|
+
return { source, name: SWR_DEFAULTS.get(source) };
|
|
143
|
+
return { source, name: null };
|
|
144
|
+
};
|
|
145
|
+
/** Resolve a constant alias, including destructuring a React/SWR namespace. */
|
|
146
|
+
const constantOrigin = (context, definition, variable, seen) => {
|
|
147
|
+
if (definition.type !== "Variable" || definition.parent.kind !== "const")
|
|
148
|
+
return null;
|
|
149
|
+
const declaration = definition.node;
|
|
150
|
+
const origin = hookOrigin(context, declaration.init, seen);
|
|
151
|
+
if (declaration.id.type === "Identifier")
|
|
152
|
+
return origin;
|
|
153
|
+
if (declaration.id.type !== "ObjectPattern" || !origin || origin.name !== null)
|
|
154
|
+
return null;
|
|
155
|
+
const property = declaration.id.properties.find((entry) => entry.type === "Property" && entry.value.type === "Identifier" && entry.value.name === variable.name);
|
|
156
|
+
if (!property)
|
|
157
|
+
return null;
|
|
158
|
+
const key = property.key;
|
|
159
|
+
const name = property.computed || key.type === "Literal" ? ("value" in key ? key.value : undefined) : key.type === "Identifier" ? key.name : undefined;
|
|
160
|
+
return namedOrigin(origin.source, name);
|
|
161
|
+
};
|
|
162
|
+
/** Follow lexical bindings so shadowed, reassigned and unrelated `use*` values cannot prove a hook. */
|
|
163
|
+
const hookOrigin = (context, input, seen = new Set()) => {
|
|
164
|
+
const node = unwrap(input);
|
|
165
|
+
if (node?.type === "MemberExpression") {
|
|
166
|
+
const origin = hookOrigin(context, node.object, seen);
|
|
167
|
+
if (origin?.name !== null)
|
|
168
|
+
return null;
|
|
169
|
+
const name = propertyName(node);
|
|
170
|
+
return namedOrigin(origin.source, name);
|
|
171
|
+
}
|
|
172
|
+
if (node?.type !== "Identifier")
|
|
173
|
+
return null;
|
|
174
|
+
const variable = variableOf(context, node);
|
|
175
|
+
if (!variable || seen.has(variable) || variable.defs.length !== 1)
|
|
176
|
+
return null;
|
|
177
|
+
if (variable.references.some((reference) => reference.isWrite() && !reference.init))
|
|
178
|
+
return null;
|
|
179
|
+
const [definition] = variable.defs;
|
|
180
|
+
if (definition === undefined)
|
|
181
|
+
return null;
|
|
182
|
+
if (definition.type === "ImportBinding") {
|
|
183
|
+
const origin = importOrigin(definition);
|
|
184
|
+
if (origin)
|
|
185
|
+
return origin;
|
|
186
|
+
if (importedReduxHook(context, definition))
|
|
187
|
+
return { source: "react-redux", name: "hook" };
|
|
188
|
+
return importedOwnHook(context, definition) ? { source: OWN_HOOK, name: "hook" } : null;
|
|
189
|
+
}
|
|
190
|
+
return constantOrigin(context, definition, variable, new Set([...seen, variable]));
|
|
191
|
+
};
|
|
192
|
+
/** The exported function itself must call the hook; an unused nested callback is not evidence. */
|
|
193
|
+
const callsBuiltinHook = (context, local) => {
|
|
194
|
+
const hook = functionOf(context, local);
|
|
195
|
+
if (!hook)
|
|
196
|
+
return false;
|
|
197
|
+
let found = false;
|
|
198
|
+
walk(context.sourceCode, hook.body, (node) => {
|
|
199
|
+
if (found || isFn(node))
|
|
200
|
+
return false;
|
|
201
|
+
if (node.type === "CallExpression" && builtinHook(hookOrigin(context, node.callee)))
|
|
202
|
+
found = true;
|
|
203
|
+
return undefined;
|
|
204
|
+
});
|
|
205
|
+
return found;
|
|
206
|
+
};
|
|
207
|
+
const checkHookExports = (context, program, exported) => {
|
|
208
|
+
const hooks = exported.filter((item) => !item.default && isHookName(item.name));
|
|
209
|
+
const defaults = exported.filter((item) => item.default);
|
|
210
|
+
for (const item of defaults)
|
|
211
|
+
context.report({ node: item.node, messageId: "defaultExport" });
|
|
212
|
+
for (const item of exported) {
|
|
213
|
+
if (!item.default && !isHookName(item.name) && !isSwrKeyName(item.name)) {
|
|
214
|
+
context.report({ node: item.node, messageId: "export", data: { name: item.name } });
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if (hooks.length === 0 && defaults.length === 0)
|
|
218
|
+
context.report({ node: program, messageId: "missing" });
|
|
219
|
+
const second = hooks[1];
|
|
220
|
+
if (hooks.length > 1 && second !== undefined)
|
|
221
|
+
context.report({ node: second.node, messageId: "many", data: { count: hooks.length } });
|
|
222
|
+
for (const item of hooks) {
|
|
223
|
+
if (!callsBuiltinHook(context, item.local))
|
|
224
|
+
context.report({ node: item.node, messageId: "fake", data: { name: item.name } });
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
const hookFileOf = (context) => {
|
|
228
|
+
const hfs = hfsOf(context);
|
|
229
|
+
const file = fileOf(context);
|
|
230
|
+
const found = classOf(context);
|
|
231
|
+
const name = baseName(file);
|
|
232
|
+
if (inSlot(context, ...FAMILY_SLOTS)) {
|
|
233
|
+
const verdict = hfs.allows(file);
|
|
234
|
+
const stray = verdict !== null && !verdict.allowed;
|
|
235
|
+
const allows = (verdict?.allows ?? (found.slot === undefined ? null : hfs.slot(found.slot))?.allows ?? []).join("`, `");
|
|
236
|
+
// A role the slot names (`entry`, `effects`, `store`) or a non-hook file it allows (sockets.ts, enums/, types/, barrels) is no hook file.
|
|
237
|
+
const subscriptions = ["/api/sse/subscriptions/", "/api/graphql/subscriptions/"].some((folder) => file.split(String.fromCharCode(92)).join("/").includes(folder));
|
|
238
|
+
return { stray, directlyInHooks: false, hookFile: !stray && !found.role && isHookFileName(name), sharedFile: false, family: found.slot, allows, subscriptions };
|
|
239
|
+
}
|
|
240
|
+
if (!inSlot(context, "fe.hooks"))
|
|
241
|
+
return null;
|
|
242
|
+
// A file directly in `hooks/` is its own root: the slot binds the file name as the `domain`.
|
|
243
|
+
const directlyInHooks = hfs.relative(file) === found.root;
|
|
244
|
+
// The slot names the domain's entry (`index.ts`, role `entry`) and its shared helper file (`<domain>.shared.ts`, role `shared`).
|
|
245
|
+
// Both sit directly in the domain folder: a nested `x/index.ts` is a file the slot does not name.
|
|
246
|
+
const directlyInDomain = hfs.relative(file) === `${found.root}/${name}`;
|
|
247
|
+
const entryFile = directlyInDomain && found.role === "entry";
|
|
248
|
+
const sharedFile = directlyInDomain && found.role === "shared";
|
|
249
|
+
const hookFile = isHookFileName(name);
|
|
250
|
+
return { stray: !hookFile && !sharedFile && !entryFile, directlyInHooks, hookFile, sharedFile, family: null, allows: "" };
|
|
251
|
+
};
|
|
252
|
+
/** `hooks/` holds React hooks, each under the name its family or domain gives it, and one shared helper file per domain. */
|
|
253
|
+
export const hooksFolderHoldsHooksOnly = defineRule({
|
|
254
|
+
meta: {
|
|
255
|
+
type: "problem",
|
|
256
|
+
docs: { description: "`hooks/` holds `use*.ts` hooks under the names their family slot gives (`hooks/swr`, `hooks/rhf`, `hooks/zustand`, `hooks/socketio`, `hooks/effects`, `hooks/reuseables`) and, per domain, one `<domain>.shared.ts`; nothing else." },
|
|
257
|
+
schema: [],
|
|
258
|
+
messages: {
|
|
259
|
+
domain: "A file directly in `hooks/`. Hooks live in a family folder (`hooks/swr/`, `hooks/rhf/`, `hooks/zustand/<store>/`, `hooks/socketio/`, `hooks/effects/`, `hooks/reuseables/`) or in `hooks/<domain>/use<Name>.ts`; the folder is what keeps a hook findable and a shared helper next to the hooks that use it.",
|
|
260
|
+
notHook: "`{{name}}` is in `hooks/` but is not a React hook. Hooks are `use<Name>.ts`; the domain's non-hook helpers go in `<domain>.shared.ts`; a call to the back end is an operation of `modules/api`, wrapped by an SWR hook in `hooks/swr/`.",
|
|
261
|
+
family: "`{{name}}` is not a file `{{family}}` names. This family holds `{{allows}}`: an SWR hook is `use{Query,Mutate}<X>Swr.ts` (GraphQL) or `use{Get,Post,Put,Patch,Delete}<X>Swr.ts` (REST) in its `api/<protocol>/<queries|mutations>/` folder, a form is `use<X>Form.ts`. A name outside the list is a hook nobody finds by its family, or code that is not a hook.",
|
|
262
|
+
subscription: "`{{name}}` is not a file `{{family}}` names in a subscriptions folder. A stream or GraphQL subscription hook is `useSubscribe<X>Swr.ts` in `api/sse/subscriptions/` or `api/graphql/subscriptions/`, one hook per file, exporting at most its `SUBSCRIBE_<X>_SWR_KEY`.",
|
|
263
|
+
export: "`{{name}}` is exported from a hook file but is not a hook. A hook file exports the hook (a function named `use...`) and at most its SWR key (`QUERY_<X>_SWR_KEY`, `MUTATE_<X>_SWR_KEY`); a helper belongs in `<domain>.shared.ts`.",
|
|
264
|
+
many: "This file exports {{count}} hooks. One hook per file keeps a hook findable by its name and keeps a change to one from re-running the tests of the others.",
|
|
265
|
+
missing: "FE_HOOKS_ARE_HOOKS: a hook file exports exactly one named use* function that calls a hook (React, Redux, SWR, a listed library hook or a use* hook of the app's own hooks folders). Non-hook helpers belong in the domain's shared helper file.",
|
|
266
|
+
defaultExport: "FE_HOOKS_ARE_HOOKS: export the hook as a named use* function. A default export does not identify the hook this file owns.",
|
|
267
|
+
fake: "FE_HOOKS_ARE_HOOKS: `{{name}}` must be a function that calls a real hook: a React, Redux or SWR hook, a listed library hook (next-intl, next/navigation, next-themes useTheme, react-hook-form, zustand) or a hook of the app's own `hooks/` folders. A use* name alone does not make a reader or helper a hook.",
|
|
268
|
+
sharedHook: "`{{name}}` is a hook exported from the shared helper file. That file holds non-hook helpers; a hook there is a hook nobody will find. Give it its own `use<Name>.ts`.",
|
|
269
|
+
server: "`{{source}}` marks server-only code, which is not a hook. The front end reads its back end through the operations of `modules/api`, wrapped by the SWR hooks of `hooks/swr/`; request-scoped server APIs stay in server components.",
|
|
270
|
+
},
|
|
271
|
+
},
|
|
272
|
+
create(context) {
|
|
273
|
+
const judged = hookFileOf(context);
|
|
274
|
+
if (judged === null)
|
|
275
|
+
return {};
|
|
276
|
+
const name = baseName(fileOf(context));
|
|
277
|
+
return {
|
|
278
|
+
Program(program) {
|
|
279
|
+
if (judged.directlyInHooks)
|
|
280
|
+
return context.report({ node: program, messageId: "domain" });
|
|
281
|
+
if (!judged.stray)
|
|
282
|
+
return;
|
|
283
|
+
if (judged.family && judged.subscriptions)
|
|
284
|
+
return context.report({ node: program, messageId: "subscription", data: { name, family: judged.family } });
|
|
285
|
+
if (judged.family)
|
|
286
|
+
context.report({ node: program, messageId: "family", data: { name, family: judged.family, allows: judged.allows } });
|
|
287
|
+
else
|
|
288
|
+
context.report({ node: program, messageId: "notHook", data: { name } });
|
|
289
|
+
},
|
|
290
|
+
ImportDeclaration(node) {
|
|
291
|
+
if (SERVER_ONLY.has(String(node.source.value))) {
|
|
292
|
+
context.report({ node, messageId: "server", data: { source: node.source.value } });
|
|
293
|
+
}
|
|
294
|
+
},
|
|
295
|
+
"Program:exit"(program) {
|
|
296
|
+
if (judged.directlyInHooks || judged.stray || (!judged.hookFile && !judged.sharedFile))
|
|
297
|
+
return;
|
|
298
|
+
const exported = program.body
|
|
299
|
+
.filter(isExportNode)
|
|
300
|
+
.flatMap(exportedEntries);
|
|
301
|
+
if (judged.sharedFile) {
|
|
302
|
+
for (const item of exported) {
|
|
303
|
+
if (isHookName(item.name))
|
|
304
|
+
context.report({ node: item.node, messageId: "sharedHook", data: { name: item.name } });
|
|
305
|
+
}
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
checkHookExports(context, program, exported);
|
|
309
|
+
},
|
|
310
|
+
};
|
|
311
|
+
},
|
|
312
|
+
});
|
|
313
|
+
/** The rules this law contributes to the plugin. */
|
|
314
|
+
export const rules = {
|
|
315
|
+
"hooks-folder-holds-hooks-only": hooksFolderHoldsHooksOnly,
|
|
316
|
+
// Forward the family enforcers for the catalog's source-only CONTRIBUTIONS discovery.
|
|
317
|
+
...swrFamilyRules,
|
|
318
|
+
};
|
|
319
|
+
/** Every rule is an error. */
|
|
320
|
+
export const recommended = Object.fromEntries(Object.keys(rules).map((name) => [`starci-fe/${name}`, "error"]));
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The rules that hold `hygiene.md`: the small runtime habits that pass every type check and fail in a
|
|
3
|
+
* reader's browser (HFS R65 `FE_SIZE_AND_STATE_BUDGET` for the subscription, catch and console rules, R50
|
|
4
|
+
* `FE_TRANSPORT_OWNER` for the fetch rule).
|
|
5
|
+
*
|
|
6
|
+
* FOUR HABITS, ONE CAUSE: work started by a component that nobody owns the end of.
|
|
7
|
+
* - a timer, listener, observer, socket or subscription started by an effect that never releases it
|
|
8
|
+
* keeps running after the component is gone and sets state on nothing;
|
|
9
|
+
* - a fetch started in an effect has no cache, no dedupe, no error state and no cancel, which is
|
|
10
|
+
* what the SWR hooks over the `modules/api` operations already give for free (R50: reads use SWR);
|
|
11
|
+
* - an empty `catch` turns a failure into a blank screen with no trace;
|
|
12
|
+
* - `console.*` is a log nobody reads, in a place no log pipeline sees.
|
|
13
|
+
*
|
|
14
|
+
* DECIDED BY RESOLUTION, NOT BY SPELLING. An effect is a call whose callee IS React's `useEffect` / `useLayoutEffect`
|
|
15
|
+
* export (the import is followed); `setTimeout`, `addEventListener`, `new ResizeObserver` are the platform's when their
|
|
16
|
+
* symbol is declared by the default library, not by a homonym in the repository; "the same handle" is the same scope
|
|
17
|
+
* binding (or member path), not the same identifier text; "a promise" is what the type checker says.
|
|
18
|
+
*/
|
|
19
|
+
import { type Recommended } from "./lib/rule.js";
|
|
20
|
+
/**
|
|
21
|
+
* Every effect and `useSyncExternalStore` subscribe in the file starts things that must end with it: a resource started
|
|
22
|
+
* by one is released by the cleanup it returns, against the same handle, target and listener.
|
|
23
|
+
*/
|
|
24
|
+
export declare const effectSubscriptionNeedsCleanup: import("./lib/rule.js").Rule<"unreleased" | "orphan", []>;
|
|
25
|
+
/** No data loading inside `useEffect`; data is read through an SWR hook over an operation of `modules/api`. */
|
|
26
|
+
export declare const noDataFetchInEffect: import("./lib/rule.js").Rule<"fetch", []>;
|
|
27
|
+
/** A `catch` block with nothing in it, or a promise `.catch` handler with nothing in it. */
|
|
28
|
+
export declare const noEmptyCatch: import("./lib/rule.js").Rule<"empty" | "promise", []>;
|
|
29
|
+
/** No `console.*` in product source. */
|
|
30
|
+
export declare const noConsole: import("./lib/rule.js").Rule<"console", []>;
|
|
31
|
+
/** The rules this law contributes to the plugin. */
|
|
32
|
+
export declare const rules: {
|
|
33
|
+
"effect-subscription-needs-cleanup": import("./lib/rule.js").Rule<"unreleased" | "orphan", []>;
|
|
34
|
+
"no-data-fetch-in-effect": import("./lib/rule.js").Rule<"fetch", []>;
|
|
35
|
+
"no-empty-catch": import("./lib/rule.js").Rule<"empty" | "promise", []>;
|
|
36
|
+
"no-console": import("./lib/rule.js").Rule<"console", []>;
|
|
37
|
+
};
|
|
38
|
+
/** Every rule is an error. */
|
|
39
|
+
export declare const recommended: Recommended;
|