@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/lib/config.mjs
DELETED
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The one flat config of a StarCi front end.
|
|
3
|
-
*
|
|
4
|
-
* A repository's `eslint.config.mjs` is managed by `starci app sync` (R05/R16/R17). Its template composes this universal
|
|
5
|
-
* factory with the explicitly selected Grammar family's factory, passing the same HFS view to both.
|
|
6
|
-
*
|
|
7
|
-
* The factory owns everything else: which files are linted (the source of every app and every workspace package), typed linting on the source (`parserOptions.projectService`, so a rule asks what a node IS rather than what
|
|
8
|
-
* it is called), the ignores, the linter options that make a disable comment impossible, every canon rule at `error`,
|
|
9
|
-
* the React Hooks rules at `error`, and `settings.starci.hfs` - the slot view each path-scoped rule asks instead of a path
|
|
10
|
-
* pattern. It takes no rule overrides, globs or ignores: there is no parameter through which a repository could weaken
|
|
11
|
-
* the canon.
|
|
12
|
-
*/
|
|
13
|
-
import tsParser from "@typescript-eslint/parser"
|
|
14
|
-
import { OUTSIDE_THE_SCAN_FILES } from "../runtime/scripts/lib/sonar-syntax-rules.mjs"
|
|
15
|
-
import { sonarWayRules } from "../runtime/scripts/lib/sonar-way.mjs"
|
|
16
|
-
|
|
17
|
-
/** A disable directive is a weakening of lint, and one that suppresses nothing is dead weight. */
|
|
18
|
-
export const linterOptions = Object.freeze({ noInlineConfig: true, reportUnusedDisableDirectives: "error" })
|
|
19
|
-
|
|
20
|
-
/** The product source of a front end: every app's `src/` and every workspace package's `src/` (slots fe.* and repo.packages). */
|
|
21
|
-
export const SOURCE_FILES = Object.freeze(["apps/*/src/**/*.{ts,tsx}", "packages/*/src/**/*.{ts,tsx}"])
|
|
22
|
-
|
|
23
|
-
/** Build output, installed packages, generated wire types and the harness's report directory are never linted. */
|
|
24
|
-
export const IGNORED = Object.freeze(["**/node_modules/**", "**/.next/**", "**/dist/**", "**/coverage/**", "**/__generated__/**", ".starci/**"])
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* The type-aware typescript-eslint rules the canon borrows, all `error` and the same set the back-end canon borrows for the type-aware laws: a promise is awaited or
|
|
28
|
-
* handled (`no-floating-promises`, `no-misused-promises`), a `switch` over a union names every member, nothing flows out of an `any` (the `no-unsafe-*` family),
|
|
29
|
-
* and the Sonar rules that need the checker: S1874 (a deprecated API), S6551 (an object turned into text) and S7503 (an `async` function that never awaits).
|
|
30
|
-
*/
|
|
31
|
-
export const BORROWED = Object.freeze({
|
|
32
|
-
"@typescript-eslint/no-floating-promises": "error",
|
|
33
|
-
"@typescript-eslint/no-misused-promises": "error",
|
|
34
|
-
"@typescript-eslint/switch-exhaustiveness-check": "error",
|
|
35
|
-
"@typescript-eslint/no-unsafe-assignment": "error",
|
|
36
|
-
"@typescript-eslint/no-unsafe-member-access": "error",
|
|
37
|
-
"@typescript-eslint/no-unsafe-call": "error",
|
|
38
|
-
"@typescript-eslint/no-unsafe-return": "error",
|
|
39
|
-
"@typescript-eslint/no-unsafe-argument": "error",
|
|
40
|
-
"@typescript-eslint/no-deprecated": "error",
|
|
41
|
-
"@typescript-eslint/no-base-to-string": "error",
|
|
42
|
-
"@typescript-eslint/require-await": "error",
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
const levelOf = (setting) => (Array.isArray(setting) ? setting[0] : setting)
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* The flat config of one front-end repository.
|
|
49
|
-
*
|
|
50
|
-
* @param {object} input - Attachment options.
|
|
51
|
-
* @param {object} input.hfs - The HFS view of the repository (`loadHfs(import.meta.url)`).
|
|
52
|
-
* @param {object} input.plugin - The canon plugin.
|
|
53
|
-
* @param {Record<string, unknown>} input.source - The source-tree levels (canon rules plus the React Hooks rules).
|
|
54
|
-
* @param {object} input.reactHooks - The React Hooks plugin.
|
|
55
|
-
* @param {object} input.tsPlugin - The typescript-eslint plugin.
|
|
56
|
-
* @param {{ sonarjs: object, unicorn: object }} input.sonar - The plugins of the Sonar way rules (scripts/lib/sonar-way.mjs).
|
|
57
|
-
* @returns {Array<object>} The flat config: one ignore block, one typed source block and the Sonar way block.
|
|
58
|
-
*/
|
|
59
|
-
export const buildFeConfig = ({ hfs, plugin, source, reactHooks, tsPlugin, sonar }) => {
|
|
60
|
-
if (!hfs || typeof hfs.slotOf !== "function" || typeof hfs.repoRoot !== "string") {
|
|
61
|
-
throw new Error("starciFeConfig needs { hfs: loadHfs(import.meta.url) } - the rules read the repository's slots through it")
|
|
62
|
-
}
|
|
63
|
-
if (hfs.profile !== "fe") throw new Error(`starciFeConfig lints the fe side of an app (its eslint.config.mjs lives in fe/); this view is ${hfs.side ? `the ${hfs.side} side` : "the app root"}`)
|
|
64
|
-
if (Object.keys(source).length === 0) throw new Error("starciFeConfig received an empty source recommendation - a config with no rules is not adoption")
|
|
65
|
-
const weak = Object.entries(source).filter(([, setting]) => levelOf(setting) !== "error").map(([name]) => name)
|
|
66
|
-
if (weak.length > 0) throw new Error(`starciFeConfig refuses source rules not at error: ${weak.join(", ")}`)
|
|
67
|
-
const settings = { starci: { hfs } }
|
|
68
|
-
return [
|
|
69
|
-
{ ignores: [...IGNORED] },
|
|
70
|
-
{
|
|
71
|
-
files: [...SOURCE_FILES],
|
|
72
|
-
languageOptions: {
|
|
73
|
-
parser: tsParser,
|
|
74
|
-
ecmaVersion: "latest",
|
|
75
|
-
sourceType: "module",
|
|
76
|
-
parserOptions: { projectService: true, tsconfigRootDir: hfs.repoRoot, ecmaFeatures: { jsx: true } },
|
|
77
|
-
},
|
|
78
|
-
linterOptions: { ...linterOptions },
|
|
79
|
-
plugins: { "starci-fe": plugin, "react-hooks": reactHooks, "@typescript-eslint": tsPlugin },
|
|
80
|
-
settings,
|
|
81
|
-
rules: { ...source, ...BORROWED },
|
|
82
|
-
},
|
|
83
|
-
// Sonar way: what the scan raises is refused while the code is written; a spec is outside the scan, so outside this block.
|
|
84
|
-
{ files: [...SOURCE_FILES], ignores: [...OUTSIDE_THE_SCAN_FILES], plugins: { sonarjs: sonar.sonarjs, unicorn: sonar.unicorn }, rules: sonarWayRules(sonar.sonarjs, "fe").rules },
|
|
85
|
-
]
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* The canon rules that need no slot view: they judge a node and its types, never where the file sits in an app. A tree that is no HFS front end (a
|
|
90
|
-
* runtime package, the harness UI) is linted by exactly these plus the type-aware, React Hooks and Sonar way rules. The set is closed by
|
|
91
|
-
* config.spec.mjs: every other rule of the plugin refuses to run without `settings.starci.hfs`, so a rule added to the plugin is either listed here
|
|
92
|
-
* or proven slot-bound there.
|
|
93
|
-
*/
|
|
94
|
-
export const SLOT_FREE_RULES = Object.freeze([
|
|
95
|
-
"client-no-server-import", "no-server-action", "no-dangerous-html", "response-cookie-attributes", "require-export-jsdoc", "no-vietnamese-in-source",
|
|
96
|
-
"no-hardcoded-endpoint-fallback", "no-runtime-namespace", "list-item-has-key", "no-index-key", "no-inline-literal-prop-in-list", "prefer-arrow-export",
|
|
97
|
-
"handler-on-prefix", "no-native-form-control", "no-native-img", "image-has-size", "no-second-i18n-stack", "html-lang-from-locale", "no-null-suspense-fallback",
|
|
98
|
-
"no-native-anchor", "no-inline-parameter-type", "no-hand-rolled-polling", "no-copy-in-call", "no-alert", "no-failure-collapse",
|
|
99
|
-
"outcome-kinds-exhaustive", "no-await-in-loop", "prefer-code-point", "prefer-string-raw", "no-nested-conditional", "no-void-operator", "no-unused-import",
|
|
100
|
-
"prefer-export-from", "no-unused-prop-types", "media-has-caption", "max-params",
|
|
101
|
-
"nested-control-variant", "no-list-panel-padding", "no-app-surface-paint", "divider-full-bleed", "control-height", "selection-fill-no-check", "no-grammar-shim",
|
|
102
|
-
"button-intent-required", "button-no-variant", "destructive-confirm-in-dialog", "single-commit-per-render", "no-commit-in-collection-item",
|
|
103
|
-
])
|
|
104
|
-
|
|
105
|
-
/** The source of a plain front-end tree: its `src/`. */
|
|
106
|
-
export const PLAIN_SOURCE_FILES = Object.freeze(["src/**/*.{ts,tsx}"])
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* The flat config of a TypeScript front-end tree that is no HFS app: the same fences as an app (typed linting, no inline directive, every rule at
|
|
110
|
-
* `error`) over the rules that need no slot view. It takes the root of the tree and nothing else.
|
|
111
|
-
*
|
|
112
|
-
* @param {object} input - Attachment options.
|
|
113
|
-
* @param {string} input.root - The absolute directory holding the tree's `tsconfig.json` and `src/`.
|
|
114
|
-
* @param {object} input.plugin - The canon plugin.
|
|
115
|
-
* @param {Record<string, unknown>} input.hooks - The React Hooks rules at error.
|
|
116
|
-
* @param {object} input.reactHooks - The React Hooks plugin.
|
|
117
|
-
* @param {object} input.tsPlugin - The typescript-eslint plugin.
|
|
118
|
-
* @param {{ sonarjs: object, unicorn: object }} input.sonar - The plugins of the Sonar way rules.
|
|
119
|
-
* @returns {Array<object>} The flat config: one ignore block, one typed source block and the Sonar way block.
|
|
120
|
-
*/
|
|
121
|
-
export const buildPlainSourceConfig = ({ root, plugin, hooks, reactHooks, tsPlugin, sonar, canon = true }) => {
|
|
122
|
-
if (typeof root !== "string" || root === "") throw new Error("starciPlainSourceConfig needs { root } - the directory of the tree's tsconfig.json and src/")
|
|
123
|
-
const canonRules = canon ? Object.fromEntries(SLOT_FREE_RULES.map((name) => [`starci-fe/${name}`, "error"])) : {}
|
|
124
|
-
const missing = SLOT_FREE_RULES.filter((name) => !(name in plugin.rules))
|
|
125
|
-
if (missing.length > 0) throw new Error(`starciPlainSourceConfig lists rules the canon plugin does not publish: ${missing.join(", ")}`)
|
|
126
|
-
return [
|
|
127
|
-
{ ignores: [...IGNORED] },
|
|
128
|
-
{
|
|
129
|
-
files: [...PLAIN_SOURCE_FILES],
|
|
130
|
-
languageOptions: {
|
|
131
|
-
parser: tsParser,
|
|
132
|
-
ecmaVersion: "latest",
|
|
133
|
-
sourceType: "module",
|
|
134
|
-
parserOptions: { projectService: true, tsconfigRootDir: root, ecmaFeatures: { jsx: true } },
|
|
135
|
-
},
|
|
136
|
-
linterOptions: { ...linterOptions },
|
|
137
|
-
plugins: { "starci-fe": plugin, "react-hooks": reactHooks, "@typescript-eslint": tsPlugin },
|
|
138
|
-
rules: { ...canonRules, ...hooks, ...BORROWED },
|
|
139
|
-
},
|
|
140
|
-
{ files: [...PLAIN_SOURCE_FILES], ignores: [...OUTSIDE_THE_SCAN_FILES], plugins: { sonarjs: sonar.sonarjs, unicorn: sonar.unicorn }, rules: sonarWayRules(sonar.sonarjs, "fe").rules },
|
|
141
|
-
]
|
|
142
|
-
}
|
package/lib/fe-stack.mjs
DELETED
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The one door between the front-end lint rules and the fe-stack catalog (`knowledge/hfs/fe-stack.yaml`): for each common
|
|
3
|
-
* front-end concern the ONE canonical library, its HFS home and the alternatives a front end never imports.
|
|
4
|
-
*
|
|
5
|
-
* The catalog is read from this package's `runtime/` copy, the same way the slot manifest and the canon pins are (a byte copy
|
|
6
|
-
* of the runtime's knowledge file, refreshed by `scripts/hfs/sync-runtime.mjs`), so a rule in a product repository judges
|
|
7
|
-
* against the catalog of the canon version it installed. A rule that needs the catalog and finds none stops the lint run
|
|
8
|
-
* with an error naming the fix: a rule must not guess which libraries are forbidden.
|
|
9
|
-
*/
|
|
10
|
-
import { readFileSync } from "node:fs"
|
|
11
|
-
import { dirname, join } from "node:path"
|
|
12
|
-
import { fileURLToPath } from "node:url"
|
|
13
|
-
import { parseYaml } from "../runtime/engine/yaml.mjs"
|
|
14
|
-
import { globExpression } from "../runtime/scripts/lib/glob.mjs"
|
|
15
|
-
|
|
16
|
-
const CATALOG = join(dirname(fileURLToPath(import.meta.url)), "..", "runtime", "knowledge", "hfs", "fe-stack.yaml")
|
|
17
|
-
|
|
18
|
-
/** @type {{ concerns: ReadonlyArray<object>, forbidden: ReadonlyMap<string, object>, patterns: ReadonlyArray<object> } | null} */
|
|
19
|
-
let loaded = null
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* The parsed catalog: its concerns, and every forbidden package mapped to the concern that forbids it.
|
|
23
|
-
*
|
|
24
|
-
* @returns {{ concerns: ReadonlyArray<object>, forbidden: ReadonlyMap<string, object>, patterns: ReadonlyArray<object> }} The catalog.
|
|
25
|
-
*/
|
|
26
|
-
export const feStack = () => {
|
|
27
|
-
if (loaded) return loaded
|
|
28
|
-
let text
|
|
29
|
-
try {
|
|
30
|
-
text = readFileSync(CATALOG, "utf8")
|
|
31
|
-
} catch (error) {
|
|
32
|
-
throw new Error(`@starci/eslint-canon-fe reads the fe-stack catalog from runtime/knowledge/hfs/fe-stack.yaml and it is missing (${error.code ?? error.message}): regenerate the runtime copy (scripts/hfs/sync-runtime.mjs)`)
|
|
33
|
-
}
|
|
34
|
-
const concerns = parseYaml(text)?.concerns
|
|
35
|
-
if (!Array.isArray(concerns) || concerns.length === 0) throw new Error("runtime/knowledge/hfs/fe-stack.yaml declares no concerns: the fe-stack catalog is malformed")
|
|
36
|
-
const forbidden = new Map()
|
|
37
|
-
for (const concern of concerns) for (const name of concern.forbids ?? []) forbidden.set(String(name), concern)
|
|
38
|
-
const patterns = [...forbidden].filter(([name]) => name.includes("*")).map(([name, concern]) => ({ expression: globExpression(name), concern }))
|
|
39
|
-
loaded = Object.freeze({ concerns: Object.freeze(concerns), forbidden, patterns: Object.freeze(patterns) })
|
|
40
|
-
return loaded
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* The package a module specifier names (`@scope/name` or `name`, its subpath dropped), or null for a relative, aliased,
|
|
45
|
-
* node-builtin or empty specifier.
|
|
46
|
-
*
|
|
47
|
-
* @param {unknown} specifier - The module specifier.
|
|
48
|
-
* @returns {string | null} The package name.
|
|
49
|
-
*/
|
|
50
|
-
export const packageNameOf = (specifier) => {
|
|
51
|
-
const text = typeof specifier === "string" ? specifier : ""
|
|
52
|
-
if (text === "" || text.startsWith(".") || text.startsWith("/") || text.startsWith("@/") || text.startsWith("~/") || text.startsWith("node:")) return null
|
|
53
|
-
const parts = text.split("/")
|
|
54
|
-
if (text.startsWith("@")) return parts.length >= 2 && parts[1] !== "" ? `${parts[0]}/${parts[1]}` : null
|
|
55
|
-
return parts[0]
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* The concern whose `forbids` lists the package a specifier names, else null.
|
|
60
|
-
*
|
|
61
|
-
* @param {unknown} specifier - The module specifier.
|
|
62
|
-
* @returns {object | null} The concern (`{ id, packages, home, pattern, forbids, why }`).
|
|
63
|
-
*/
|
|
64
|
-
export const forbiddingConcern = (specifier) => {
|
|
65
|
-
const name = packageNameOf(specifier)
|
|
66
|
-
if (name === null) return null
|
|
67
|
-
const { forbidden, patterns } = feStack()
|
|
68
|
-
return forbidden.get(name) ?? patterns.find(({ expression }) => expression.test(name))?.concern ?? null
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* The canonical packages of a concern.
|
|
73
|
-
*
|
|
74
|
-
* @param {string} id - The concern id (`icons`, `graphql`, ...).
|
|
75
|
-
* @returns {ReadonlyArray<string>} Its packages; empty for an unknown concern.
|
|
76
|
-
*/
|
|
77
|
-
export const canonicalPackages = (id) => feStack().concerns.find((concern) => concern.id === id)?.packages ?? []
|
package/lib/hfs.mjs
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The one door between the front-end lint rules and the HFS slot manifest of the repository being linted.
|
|
3
|
-
*
|
|
4
|
-
* A consuming `eslint.config.mjs` is the managed one-liner `export default starciFeConfig({ hfs: loadHfs(import.meta.url) })`.
|
|
5
|
-
* The config lives in the fe/ side folder of an app; `loadHfs` finds the app-root `hfs.json` one level up and gives every file
|
|
6
|
-
* linted under that folder the view of the fe side (paths relative to fe/), with the slot manifest this package ships in
|
|
7
|
-
* `runtime/` (a byte copy of the runtime's `knowledge/hfs/slots.yaml`, `scripts/hfs/slots.mjs` and
|
|
8
|
-
* `scripts/hfs/view.mjs`, refreshed by `scripts/hfs/sync-runtime.mjs`), and returns the view the factory puts
|
|
9
|
-
* in `settings.starci.hfs`.
|
|
10
|
-
*
|
|
11
|
-
* Every path-scoped rule asks `slotOf` / `tierOf` here instead of testing a path with a regular expression. A rule that
|
|
12
|
-
* needs the view and finds none stops the lint run with an error naming the fix: a rule must not guess.
|
|
13
|
-
*/
|
|
14
|
-
import { dirname, join } from "node:path"
|
|
15
|
-
import { fileURLToPath } from "node:url"
|
|
16
|
-
import { declaredHfsView, openHfsView } from "../runtime/scripts/hfs/view.mjs"
|
|
17
|
-
|
|
18
|
-
const RUNTIME = join(dirname(fileURLToPath(import.meta.url)), "..", "runtime")
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* The HFS view of the side whose `eslint.config.mjs` (in fe/) calls this.
|
|
22
|
-
*
|
|
23
|
-
* @param {string} configUrl - `import.meta.url` of the repository's `eslint.config.mjs`.
|
|
24
|
-
* @returns {object} The frozen HFS view.
|
|
25
|
-
*/
|
|
26
|
-
export const loadHfs = (configUrl) => openHfsView({ runtimeRoot: RUNTIME, repoRoot: dirname(fileURLToPath(configUrl)) })
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* The HFS view of the fe side of an in-memory app declaration, for rule tests.
|
|
30
|
-
*
|
|
31
|
-
* @param {object} declaration - An app `hfs.json` object.
|
|
32
|
-
* @param {string} repoRoot - The absolute fe side folder the linted filenames live under.
|
|
33
|
-
* @returns {object} The frozen HFS view.
|
|
34
|
-
*/
|
|
35
|
-
export const hfsFromDeclaration = (declaration, repoRoot) => declaredHfsView({ runtimeRoot: RUNTIME, declaration, repoRoot, side: "fe" })
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* The HFS view a rule runs under; a rule that needs it and gets none refuses to guess.
|
|
39
|
-
*
|
|
40
|
-
* @param {object} context - The ESLint rule context.
|
|
41
|
-
* @returns {object} The HFS view from `settings.starci.hfs`.
|
|
42
|
-
*/
|
|
43
|
-
export const hfsOf = (context) => {
|
|
44
|
-
const hfs = context.settings?.starci?.hfs
|
|
45
|
-
if (!hfs || typeof hfs.slotOf !== "function") {
|
|
46
|
-
throw new Error(`${context.id ?? "a starci-fe rule"} needs settings.starci.hfs: lint through starciFeConfig({ hfs: loadHfs(import.meta.url) })`)
|
|
47
|
-
}
|
|
48
|
-
return hfs
|
|
49
|
-
}
|
package/lib/next.mjs
DELETED
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Next.js's own contract for route segment files, read from the runtime's one statement of it
|
|
3
|
-
* (`scripts/lib/next-contract.mjs`, shipped in `runtime/` by `scripts/hfs/sync-runtime.mjs`), so every law
|
|
4
|
-
* holds the same list by construction.
|
|
5
|
-
*/
|
|
6
|
-
export { NEXT_RESERVED_EXPORTS, NEXT_ROUTE_SEGMENT_STEMS } from "../runtime/scripts/lib/next-contract.mjs"
|
package/lib/own-hook-origin.mjs
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Resolve a `use*` import that comes from the app's own hooks folders (`hooks/swr`, `hooks/<domain>`, `hooks/zustand/<store>`...) by
|
|
3
|
-
* TypeScript symbol resolution, never by the spelling of the name (R56 `FE_HOOKS_ARE_HOOKS`, owner ruling 2026-10-16 "Theo y tro").
|
|
4
|
-
*
|
|
5
|
-
* A composite product hook is built from the hooks beneath it: `hooks/<domain>/use<Name>.ts` composes the `use{Query,Mutate}<X>Swr`
|
|
6
|
-
* wrappers of `hooks/swr`. The import is followed through barrels and re-exports to the declaration, and counts only when that declaration
|
|
7
|
-
* is a `use*` value that lives in a `hooks/` folder of the app (never in `node_modules`). The hook's own file is judged by this same
|
|
8
|
-
* rule when it is linted, so a fake one is refused where it is written.
|
|
9
|
-
*/
|
|
10
|
-
import ts from "typescript"
|
|
11
|
-
import { programFor } from "./program-for.mjs"
|
|
12
|
-
|
|
13
|
-
const HOOKS_FOLDER = /(^|\/)hooks\//
|
|
14
|
-
const isHookName = (name) => /^use[A-Z0-9]/.test(name)
|
|
15
|
-
|
|
16
|
-
/** The program that holds the import target: the project program has it; a program rooted at the linted file alone (its disk text may differ from the linted text) is widened with it. */
|
|
17
|
-
const widened = new WeakMap()
|
|
18
|
-
export function reaching(program, filename, targetFile) {
|
|
19
|
-
if (program.getSourceFile(targetFile)) return program
|
|
20
|
-
const key = `${filename}\0${targetFile}`
|
|
21
|
-
const cache = widened.get(program) ?? new Map()
|
|
22
|
-
widened.set(program, cache)
|
|
23
|
-
if (!cache.has(key)) cache.set(key, ts.createProgram([filename, targetFile], program.getCompilerOptions()))
|
|
24
|
-
return cache.get(key)
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/** A declaration of a hook value: a function or a variable, not a type, interface, class or enum. */
|
|
28
|
-
const hookValue = (declaration) => ts.isFunctionDeclaration(declaration) || ts.isVariableDeclaration(declaration)
|
|
29
|
-
|
|
30
|
-
const inAppHooksFolder = (file) => {
|
|
31
|
-
const fileName = file.fileName.replaceAll("\\", "/")
|
|
32
|
-
return !fileName.includes("/node_modules/") && !file.isDeclarationFile && HOOKS_FOLDER.test(fileName)
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/** True when the import binding is a runtime `use*` import that resolves to a hook declared in a `hooks/` folder of the app. */
|
|
36
|
-
export function importedOwnHook(context, definition) {
|
|
37
|
-
const specifier = definition.node
|
|
38
|
-
if (specifier.type !== "ImportSpecifier" || definition.parent.importKind === "type" || specifier.importKind === "type") return false
|
|
39
|
-
const name = specifier.imported.name ?? specifier.imported.value
|
|
40
|
-
if (!isHookName(name)) return false
|
|
41
|
-
const source = String(definition.parent.source.value)
|
|
42
|
-
const base = programFor(context)
|
|
43
|
-
const target = ts.resolveModuleName(source, context.filename, base.getCompilerOptions(), ts.sys).resolvedModule
|
|
44
|
-
if (!target || target.isExternalLibraryImport) return false
|
|
45
|
-
const program = reaching(base, context.filename, target.resolvedFileName)
|
|
46
|
-
const file = program.getSourceFile(target.resolvedFileName)
|
|
47
|
-
const checker = program.getTypeChecker()
|
|
48
|
-
const moduleSymbol = file && checker.getSymbolAtLocation(file)
|
|
49
|
-
const exported = moduleSymbol && checker.getExportsOfModule(moduleSymbol).find((symbol) => symbol.name === name)
|
|
50
|
-
if (!exported) return false
|
|
51
|
-
const symbol = exported.flags & ts.SymbolFlags.Alias ? checker.getAliasedSymbol(exported) : exported
|
|
52
|
-
return (symbol.declarations ?? []).some((declaration) => hookValue(declaration) && inAppHooksFolder(declaration.getSourceFile()))
|
|
53
|
-
}
|
package/lib/program-for.mjs
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
/** The TypeScript program a rule resolves symbols through: the lint run's own when typed linting is on, else one rooted at the linted file. */
|
|
2
|
-
import ts from "typescript"
|
|
3
|
-
import path from "node:path"
|
|
4
|
-
|
|
5
|
-
const programs = new WeakMap()
|
|
6
|
-
|
|
7
|
-
/** The program for a rule context; a program built here is remembered per context. */
|
|
8
|
-
export function programFor(context) {
|
|
9
|
-
if (programs.has(context)) return programs.get(context)
|
|
10
|
-
const services = context.sourceCode?.parserServices
|
|
11
|
-
if (services?.program) return services.program
|
|
12
|
-
const filename = context.filename
|
|
13
|
-
const configPath = ts.findConfigFile(path.dirname(filename), ts.sys.fileExists)
|
|
14
|
-
let options = { moduleResolution: ts.ModuleResolutionKind.Bundler, module: ts.ModuleKind.ESNext }
|
|
15
|
-
if (configPath) {
|
|
16
|
-
const config = ts.readConfigFile(configPath, ts.sys.readFile)
|
|
17
|
-
options = { ...options, ...ts.parseJsonConfigFileContent(config.config, ts.sys, path.dirname(configPath)).options }
|
|
18
|
-
}
|
|
19
|
-
const program = ts.createProgram([filename], options)
|
|
20
|
-
programs.set(context, program)
|
|
21
|
-
return program
|
|
22
|
-
}
|
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The owner exports an auth callback may use besides the exchange (R287 `FE_CALLBACK_SHAPE`, owner ruling `auth-callback-pure-helpers`,
|
|
3
|
-
* 2026-10-17 "Theo y tro"): a PURE helper of the app's `modules/routes` or `modules/auth` that computes the redirect target or validates
|
|
4
|
-
* `next`, and a static route table. Both are found by TypeScript symbol resolution to the declaration, never by the spelling of the name.
|
|
5
|
-
*
|
|
6
|
-
* A pure helper is a synchronous function that returns text (or null), holds no `await` or `yield`, reaches no I/O global (network, storage,
|
|
7
|
-
* DOM, `process`, `require`, `console`) and calls only locals, language built-ins and other pure helpers of `modules/routes` or
|
|
8
|
-
* `modules/auth` (followed through barrels). `modules/config` is the environment owner: a call to its readers is a configuration read, not I/O.
|
|
9
|
-
* Anything else it imports, a library value included, makes it impure.
|
|
10
|
-
*/
|
|
11
|
-
import ts from "typescript"
|
|
12
|
-
import { reaching } from "./own-hook-origin.mjs"
|
|
13
|
-
import { programFor } from "./program-for.mjs"
|
|
14
|
-
import { resolveImport } from "./scope.mjs"
|
|
15
|
-
|
|
16
|
-
/** Globals that reach the network, storage, the page, the process or the console. */
|
|
17
|
-
const IO_GLOBALS = new Set(["fetch", "XMLHttpRequest", "WebSocket", "EventSource", "Request", "navigator", "localStorage", "sessionStorage", "indexedDB", "document", "window", "self", "globalThis", "process", "require", "console", "setTimeout", "setInterval", "queueMicrotask", "Worker", "BroadcastChannel"])
|
|
18
|
-
|
|
19
|
-
const OWNER_FILE = /\/modules\/(?:routes|auth)(?:\/|\.tsx?$)/
|
|
20
|
-
const CONFIG_FILE = /\/modules\/config(?:\/|\.tsx?$)/
|
|
21
|
-
const EXTENSIONS = ["", ".ts", ".tsx", "/index.ts", "/index.tsx"]
|
|
22
|
-
|
|
23
|
-
const normal = (file) => file.fileName.replaceAll("\\", "/")
|
|
24
|
-
|
|
25
|
-
/** The module file an import specifier names, or null when none exists on disk. */
|
|
26
|
-
const moduleFile = (context, source) => {
|
|
27
|
-
const target = resolveImport(context, source)
|
|
28
|
-
if (target === null) return null
|
|
29
|
-
return EXTENSIONS.map((extension) => `${target}${extension}`).find((candidate) => ts.sys.fileExists(candidate)) ?? null
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/** The declaration an export of the module resolves to, after barrels and re-exports. */
|
|
33
|
-
const exportedDeclaration = (context, source, name) => {
|
|
34
|
-
const file = moduleFile(context, source)
|
|
35
|
-
if (file === null) return null
|
|
36
|
-
const program = reaching(programFor(context), context.filename, file)
|
|
37
|
-
const checker = program.getTypeChecker()
|
|
38
|
-
const sourceFile = program.getSourceFile(file)
|
|
39
|
-
const moduleSymbol = sourceFile && checker.getSymbolAtLocation(sourceFile)
|
|
40
|
-
const exported = moduleSymbol && checker.getExportsOfModule(moduleSymbol).find((symbol) => symbol.name === name)
|
|
41
|
-
if (!exported) return null
|
|
42
|
-
const symbol = exported.flags & ts.SymbolFlags.Alias ? checker.getAliasedSymbol(exported) : exported
|
|
43
|
-
return { checker, declaration: symbol.declarations?.[0] ?? null }
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/** Strip `as`, `satisfies` and parentheses. */
|
|
47
|
-
const bare = (node) => {
|
|
48
|
-
let current = node
|
|
49
|
-
while (current && (ts.isAsExpression(current) || ts.isSatisfiesExpression(current) || ts.isParenthesizedExpression(current))) current = current.expression
|
|
50
|
-
return current
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/** The function-like node of a function declaration or of a variable initialised with an arrow or function expression. */
|
|
54
|
-
const functionOf = (declaration) => {
|
|
55
|
-
if (ts.isFunctionDeclaration(declaration)) return declaration
|
|
56
|
-
const init = ts.isVariableDeclaration(declaration) ? bare(declaration.initializer) : null
|
|
57
|
-
return init && (ts.isArrowFunction(init) || ts.isFunctionExpression(init)) ? init : null
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const isInside = (node, root) => node.getSourceFile() === root.getSourceFile() && node.pos >= root.pos && node.end <= root.end
|
|
61
|
-
|
|
62
|
-
/** Whether an identifier names a declaration, a property or a member rather than referring to a binding. */
|
|
63
|
-
const isNameOnly = (node) => {
|
|
64
|
-
const parent = node.parent
|
|
65
|
-
if (ts.isPropertyAccessExpression(parent)) return parent.name === node
|
|
66
|
-
if (ts.isPropertyAssignment(parent) || ts.isMethodDeclaration(parent) || ts.isPropertySignature(parent)) return parent.name === node
|
|
67
|
-
return (ts.isParameter(parent) || ts.isVariableDeclaration(parent) || ts.isFunctionDeclaration(parent) || ts.isBindingElement(parent)) && parent.name === node
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/** A declaration of another file: an owner helper is followed, the environment owner is a leaf, everything else is impure. */
|
|
71
|
-
const foreignPure = (env, declaration, root) => {
|
|
72
|
-
const file = normal(declaration.getSourceFile())
|
|
73
|
-
if (CONFIG_FILE.test(file)) return true
|
|
74
|
-
if (!OWNER_FILE.test(file) || declaration.getSourceFile().isDeclarationFile) return false
|
|
75
|
-
return declarationPure(env, declaration, root)
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/** A global of the language or of the lib files is pure unless it reaches I/O. */
|
|
79
|
-
const globalPure = (symbol) => !IO_GLOBALS.has(symbol.getName())
|
|
80
|
-
|
|
81
|
-
/** Whether one identifier reference stays inside the pure surface. */
|
|
82
|
-
const referencePure = (env, node, root) => {
|
|
83
|
-
const symbol = env.checker.getSymbolAtLocation(node)
|
|
84
|
-
if (!symbol) return node.text === "undefined"
|
|
85
|
-
const target = symbol.flags & ts.SymbolFlags.Alias ? env.checker.getAliasedSymbol(symbol) : symbol
|
|
86
|
-
const declarations = target.declarations ?? []
|
|
87
|
-
if (declarations.length === 0) return node.text === "undefined"
|
|
88
|
-
return declarations.every((declaration) => {
|
|
89
|
-
const file = declaration.getSourceFile()
|
|
90
|
-
if (isInside(declaration, root)) return true
|
|
91
|
-
if (!ts.isExternalModule(file)) return globalPure(target)
|
|
92
|
-
return foreignPure(env, declaration, root)
|
|
93
|
-
})
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/** Walk a node: no await or yield, no impure reference; type positions are not behaviour. */
|
|
97
|
-
const nodePure = (env, node, root) => {
|
|
98
|
-
if (ts.isAwaitExpression(node) || ts.isYieldExpression(node)) return false
|
|
99
|
-
if (ts.isTypeNode(node) && !ts.isExpressionWithTypeArguments(node)) return true
|
|
100
|
-
if (ts.isIdentifier(node) && !isNameOnly(node) && !referencePure(env, node, root)) return false
|
|
101
|
-
return ts.forEachChild(node, (child) => (nodePure(env, child, root) ? undefined : true)) !== true
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/** A declaration is pure when its function (or its constant initialiser) is. */
|
|
105
|
-
function declarationPure(env, declaration, root) {
|
|
106
|
-
if (env.seen.has(declaration)) return true
|
|
107
|
-
env.seen.add(declaration)
|
|
108
|
-
const fn = functionOf(declaration)
|
|
109
|
-
if (fn) return !fn.asteriskToken && !fn.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword) && (fn.body ? nodePure(env, fn.body, fn) : false)
|
|
110
|
-
if (ts.isVariableDeclaration(declaration) && declaration.initializer) return nodePure(env, declaration.initializer, declaration.initializer)
|
|
111
|
-
return false
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/** Whether a function's declared or inferred return type is text, null or undefined. */
|
|
115
|
-
const returnsText = (checker, declaration) => {
|
|
116
|
-
const signature = checker.getSignaturesOfType(checker.getTypeAtLocation(declaration), ts.SignatureKind.Call)[0]
|
|
117
|
-
if (!signature) return false
|
|
118
|
-
const type = checker.getReturnTypeOfSignature(signature)
|
|
119
|
-
const parts = type.isUnion() ? type.types : [type]
|
|
120
|
-
return parts.every((part) => part.flags & (ts.TypeFlags.StringLike | ts.TypeFlags.Null | ts.TypeFlags.Undefined))
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/** A static table: a constant object whose leaves are string literals or plain templates. */
|
|
124
|
-
const isStaticTable = (node) => {
|
|
125
|
-
const value = bare(node)
|
|
126
|
-
if (!value) return false
|
|
127
|
-
if (ts.isStringLiteralLike(value)) return true
|
|
128
|
-
if (ts.isObjectLiteralExpression(value)) return value.properties.every((property) => ts.isPropertyAssignment(property) && isStaticTable(property.initializer))
|
|
129
|
-
return false
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* What an import of the callback's owner modules is: `"pure"` (a pure text-returning helper), `"routes"` (a static route table) or null.
|
|
134
|
-
*
|
|
135
|
-
* @param {object} context - The ESLint rule context.
|
|
136
|
-
* @param {string} source - The import specifier.
|
|
137
|
-
* @param {string} name - The imported name.
|
|
138
|
-
* @returns {"pure" | "routes" | null} The kind.
|
|
139
|
-
*/
|
|
140
|
-
export function ownerExportKind(context, source, name) {
|
|
141
|
-
const found = exportedDeclaration(context, source, name)
|
|
142
|
-
if (!found?.declaration) return null
|
|
143
|
-
const { checker, declaration } = found
|
|
144
|
-
if (ts.isVariableDeclaration(declaration) && !functionOf(declaration)) return declaration.parent.flags & ts.NodeFlags.Const && isStaticTable(declaration.initializer) ? "routes" : null
|
|
145
|
-
const fn = functionOf(declaration)
|
|
146
|
-
const env = { checker, seen: new Set() }
|
|
147
|
-
return fn && returnsText(checker, fn) && declarationPure(env, declaration, fn) ? "pure" : null
|
|
148
|
-
}
|
package/lib/react.mjs
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* React hooks of the front-end canon, decided by the import: a call is `useEffect` when its callee is the `useEffect`
|
|
3
|
-
* export of `react` (named import, alias, default or namespace member), not when something is spelled that way.
|
|
4
|
-
*/
|
|
5
|
-
import { importOf, unwrap } from "./bindings.mjs"
|
|
6
|
-
|
|
7
|
-
/** The hooks that run work after render and may return its cleanup. */
|
|
8
|
-
export const EFFECT_HOOKS = new Set(["useEffect", "useLayoutEffect"])
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* The React export a call invokes, or null when the callee is not an export of `react`.
|
|
12
|
-
*
|
|
13
|
-
* @param {object} context - The ESLint rule context.
|
|
14
|
-
* @param {object} call - An ESTree call expression.
|
|
15
|
-
* @returns {string|null} The export name (`useEffect`, `useSyncExternalStore`, ...).
|
|
16
|
-
*/
|
|
17
|
-
export const reactHookOf = (context, call) => {
|
|
18
|
-
const callee = unwrap(call.callee)
|
|
19
|
-
if (callee.type === "Identifier") {
|
|
20
|
-
const from = importOf(context, callee)
|
|
21
|
-
return from && from.source === "react" && from.imported ? from.imported : null
|
|
22
|
-
}
|
|
23
|
-
if (callee.type === "MemberExpression" && !callee.computed && callee.object.type === "Identifier" && callee.property.type === "Identifier") {
|
|
24
|
-
const from = importOf(context, callee.object)
|
|
25
|
-
return from && from.source === "react" && from.imported === null ? callee.property.name : null
|
|
26
|
-
}
|
|
27
|
-
return null
|
|
28
|
-
}
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
/** Resolve typed Redux hook factories by their import provenance, never by a local use* name. */
|
|
2
|
-
import ts from "typescript"
|
|
3
|
-
import { programFor } from "./program-for.mjs"
|
|
4
|
-
|
|
5
|
-
const REDUX_HOOKS = new Set(["useSelector", "useDispatch", "useStore"])
|
|
6
|
-
|
|
7
|
-
function reduxImport(declaration) {
|
|
8
|
-
if (!ts.isImportSpecifier(declaration) || declaration.isTypeOnly) return false
|
|
9
|
-
const clause = declaration.parent.parent
|
|
10
|
-
return !clause.isTypeOnly && clause.parent.moduleSpecifier.text === "react-redux"
|
|
11
|
-
&& REDUX_HOOKS.has((declaration.propertyName ?? declaration.name).text)
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function typedFactory(checker, declaration) {
|
|
15
|
-
if (!ts.isVariableDeclaration(declaration) || !(declaration.parent.flags & ts.NodeFlags.Const)) return false
|
|
16
|
-
const call = declaration.initializer
|
|
17
|
-
if (!call || !ts.isCallExpression(call) || call.arguments.length !== 0) return false
|
|
18
|
-
const member = call.expression
|
|
19
|
-
return ts.isPropertyAccessExpression(member) && member.name.text === "withTypes"
|
|
20
|
-
&& reduxBinding(checker, member.expression)
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function reduxBinding(checker, node) {
|
|
24
|
-
const symbol = checker.getSymbolAtLocation(node)
|
|
25
|
-
return (symbol?.declarations ?? []).some(reduxImport)
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/** A relative/barrel import must resolve to a const initialized by a real Redux hook's withTypes(). */
|
|
29
|
-
export function importedReduxHook(context, definition) {
|
|
30
|
-
const specifier = definition.node
|
|
31
|
-
if (specifier.type !== "ImportSpecifier" || definition.parent.importKind === "type" || specifier.importKind === "type") return false
|
|
32
|
-
const source = String(definition.parent.source.value)
|
|
33
|
-
const name = specifier.imported.name ?? specifier.imported.value
|
|
34
|
-
if (source === "react-redux") return REDUX_HOOKS.has(name)
|
|
35
|
-
const program = programFor(context)
|
|
36
|
-
const target = ts.resolveModuleName(source, context.filename, program.getCompilerOptions(), ts.sys).resolvedModule
|
|
37
|
-
const file = target && program.getSourceFile(target.resolvedFileName)
|
|
38
|
-
const checker = program.getTypeChecker()
|
|
39
|
-
const moduleSymbol = file && checker.getSymbolAtLocation(file)
|
|
40
|
-
const exported = moduleSymbol && checker.getExportsOfModule(moduleSymbol).find(symbol => symbol.name === name)
|
|
41
|
-
if (!exported) return false
|
|
42
|
-
const symbol = exported.flags & ts.SymbolFlags.Alias ? checker.getAliasedSymbol(exported) : exported
|
|
43
|
-
return (symbol.declarations ?? []).some(declaration => typedFactory(checker, declaration))
|
|
44
|
-
}
|