@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,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shapes of the HFS view a rule reads from `settings.starci.hfs` (runtime/scripts/hfs/view.mjs builds it): the slot that
|
|
3
|
+
* owns a file, the slot definitions of the manifest and the rule parameters of the side. Only the fields the canon reads are
|
|
4
|
+
* named; the runtime objects carry more.
|
|
5
|
+
*/
|
|
6
|
+
/** A slot definition of the manifest (knowledge/hfs/slots.yaml), the fields the canon reads. */
|
|
7
|
+
export interface HfsSlot {
|
|
8
|
+
readonly id: string;
|
|
9
|
+
readonly path: string;
|
|
10
|
+
readonly tier?: string;
|
|
11
|
+
readonly kinds?: readonly string[];
|
|
12
|
+
readonly layers?: readonly string[];
|
|
13
|
+
readonly roles?: Readonly<Record<string, string>>;
|
|
14
|
+
readonly budget?: Readonly<Record<string, number>>;
|
|
15
|
+
readonly allows?: readonly string[];
|
|
16
|
+
readonly outcomeHome?: boolean;
|
|
17
|
+
}
|
|
18
|
+
/** The slot that owns a path, or why none does (`status`). */
|
|
19
|
+
export interface HfsClassification {
|
|
20
|
+
readonly path: string;
|
|
21
|
+
readonly status: "owned" | "forbidden" | "not-enabled" | "ambiguous" | "no-slot";
|
|
22
|
+
readonly slot?: string;
|
|
23
|
+
readonly root?: string;
|
|
24
|
+
readonly bindings?: Readonly<Record<string, string>>;
|
|
25
|
+
readonly kind?: string;
|
|
26
|
+
readonly role?: string;
|
|
27
|
+
readonly nearest?: {
|
|
28
|
+
readonly slot: string;
|
|
29
|
+
readonly matchedPrefix: string;
|
|
30
|
+
} | null;
|
|
31
|
+
}
|
|
32
|
+
/** What the slot that owns a file says about it under its `allows`. */
|
|
33
|
+
export interface HfsAllowance {
|
|
34
|
+
readonly slot: string;
|
|
35
|
+
readonly root: string;
|
|
36
|
+
readonly relative: string;
|
|
37
|
+
readonly allowed: boolean;
|
|
38
|
+
readonly allows: readonly string[];
|
|
39
|
+
}
|
|
40
|
+
/** The line budget of a file: the soft limit and whether growth past it is refused. */
|
|
41
|
+
export interface HfsFileLines {
|
|
42
|
+
readonly soft: number;
|
|
43
|
+
readonly hardGrowth?: boolean;
|
|
44
|
+
}
|
|
45
|
+
/** The rule parameters of the side (ruleParams of the manifest, merged for the profile and edition). */
|
|
46
|
+
export interface HfsRuleParams {
|
|
47
|
+
readonly fileLines: HfsFileLines;
|
|
48
|
+
readonly functionLines?: number;
|
|
49
|
+
readonly [name: string]: unknown;
|
|
50
|
+
}
|
|
51
|
+
/** An app of the side (hfs.json sides.<side>.apps). */
|
|
52
|
+
export interface HfsApp {
|
|
53
|
+
readonly name: string;
|
|
54
|
+
readonly kind: string;
|
|
55
|
+
}
|
|
56
|
+
/** The frozen view of one side of an app the lint factories hand their rules. */
|
|
57
|
+
export interface HfsView {
|
|
58
|
+
readonly repoRoot: string;
|
|
59
|
+
readonly profile: string;
|
|
60
|
+
readonly side: string | null;
|
|
61
|
+
readonly edition: string;
|
|
62
|
+
readonly grammar?: string;
|
|
63
|
+
readonly apps: readonly HfsApp[];
|
|
64
|
+
readonly connections: readonly unknown[];
|
|
65
|
+
readonly patterns: readonly string[];
|
|
66
|
+
readonly ruleParams: HfsRuleParams;
|
|
67
|
+
relative(file: string): string;
|
|
68
|
+
classify(file: string): HfsClassification;
|
|
69
|
+
slotOf(file: string): string | null;
|
|
70
|
+
tierOf(file: string): string | null;
|
|
71
|
+
ownerOf(file: string): string | null;
|
|
72
|
+
allows(file: string): HfsAllowance | null;
|
|
73
|
+
slot(id: string): HfsSlot | null;
|
|
74
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shapes of the HFS view a rule reads from `settings.starci.hfs` (runtime/scripts/hfs/view.mjs builds it): the slot that
|
|
3
|
+
* owns a file, the slot definitions of the manifest and the rule parameters of the side. Only the fields the canon reads are
|
|
4
|
+
* named; the runtime objects carry more.
|
|
5
|
+
*/
|
|
6
|
+
export {};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { HfsClassification, HfsSlot, HfsView } from "./hfs-view.js";
|
|
2
|
+
import type { AnyContext } from "./rule.js";
|
|
3
|
+
export type { HfsAllowance, HfsApp, HfsClassification, HfsRuleParams, HfsSlot, HfsView } from "./hfs-view.js";
|
|
4
|
+
/**
|
|
5
|
+
* The HFS view of the side whose `eslint.config.mjs` (in fe/) calls this.
|
|
6
|
+
*
|
|
7
|
+
* @param configUrl - `import.meta.url` of the repository's `eslint.config.mjs`.
|
|
8
|
+
* @returns The frozen HFS view.
|
|
9
|
+
*/
|
|
10
|
+
export declare const loadHfs: (configUrl: string | URL) => HfsView;
|
|
11
|
+
/**
|
|
12
|
+
* The HFS view of the fe side of an in-memory app declaration, for rule tests.
|
|
13
|
+
*
|
|
14
|
+
* @param declaration - An app `hfs.json` object.
|
|
15
|
+
* @param repoRoot - The absolute fe side folder the linted filenames live under.
|
|
16
|
+
* @returns The frozen HFS view.
|
|
17
|
+
*/
|
|
18
|
+
export declare const hfsFromDeclaration: (declaration: unknown, repoRoot: string) => HfsView;
|
|
19
|
+
/** True for a value that carries the view's resolver (`slotOf`), the test the canon has always made of `settings.starci.hfs`. */
|
|
20
|
+
export declare const isHfsView: (value: unknown) => value is HfsView;
|
|
21
|
+
/**
|
|
22
|
+
* The HFS view a rule runs under; a rule that needs it and gets none refuses to guess.
|
|
23
|
+
*
|
|
24
|
+
* @param context - The ESLint rule context.
|
|
25
|
+
* @returns The HFS view from `settings.starci.hfs`.
|
|
26
|
+
*/
|
|
27
|
+
export declare const hfsOf: (context: AnyContext) => HfsView;
|
|
28
|
+
/**
|
|
29
|
+
* The root of a classification the caller already knows is owned: a rule asks for it only after its scope test placed the
|
|
30
|
+
* file in a slot, so a missing root is a broken manifest and stops the run, as reading it always has.
|
|
31
|
+
*/
|
|
32
|
+
export declare const requiredRoot: (found: HfsClassification) => string;
|
|
33
|
+
/** A slot the manifest always declares (`fe.app.next`, `fe.package.ui`, ...): a missing one is a broken manifest and stops the run. */
|
|
34
|
+
export declare const requiredSlot: (slot: HfsSlot | null, id: string) => HfsSlot;
|
package/dist/lib/hfs.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
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 } from "node:path";
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
16
|
+
import { declaredHfsView, openHfsView } from "#runtime/scripts/hfs/view.mjs";
|
|
17
|
+
import { RUNTIME_ROOT } from "./package-root.js";
|
|
18
|
+
const RUNTIME = RUNTIME_ROOT;
|
|
19
|
+
/**
|
|
20
|
+
* The HFS view of the side whose `eslint.config.mjs` (in fe/) calls this.
|
|
21
|
+
*
|
|
22
|
+
* @param configUrl - `import.meta.url` of the repository's `eslint.config.mjs`.
|
|
23
|
+
* @returns The frozen HFS view.
|
|
24
|
+
*/
|
|
25
|
+
export const loadHfs = (configUrl) => openHfsView({ runtimeRoot: RUNTIME, repoRoot: dirname(fileURLToPath(configUrl)) });
|
|
26
|
+
/**
|
|
27
|
+
* The HFS view of the fe side of an in-memory app declaration, for rule tests.
|
|
28
|
+
*
|
|
29
|
+
* @param declaration - An app `hfs.json` object.
|
|
30
|
+
* @param repoRoot - The absolute fe side folder the linted filenames live under.
|
|
31
|
+
* @returns The frozen HFS view.
|
|
32
|
+
*/
|
|
33
|
+
export const hfsFromDeclaration = (declaration, repoRoot) => declaredHfsView({ runtimeRoot: RUNTIME, declaration, repoRoot, side: "fe" });
|
|
34
|
+
/** True for a value that carries the view's resolver (`slotOf`), the test the canon has always made of `settings.starci.hfs`. */
|
|
35
|
+
export const isHfsView = (value) => typeof value === "object" && value !== null && "slotOf" in value && typeof value.slotOf === "function";
|
|
36
|
+
/** The `starci.hfs` entry of a settings object, read without trusting its shape. */
|
|
37
|
+
const settingsHfs = (settings) => {
|
|
38
|
+
if (typeof settings !== "object" || settings === null || !("starci" in settings))
|
|
39
|
+
return undefined;
|
|
40
|
+
const starci = settings.starci;
|
|
41
|
+
if (typeof starci !== "object" || starci === null || !("hfs" in starci))
|
|
42
|
+
return undefined;
|
|
43
|
+
return starci.hfs;
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* The HFS view a rule runs under; a rule that needs it and gets none refuses to guess.
|
|
47
|
+
*
|
|
48
|
+
* @param context - The ESLint rule context.
|
|
49
|
+
* @returns The HFS view from `settings.starci.hfs`.
|
|
50
|
+
*/
|
|
51
|
+
export const hfsOf = (context) => {
|
|
52
|
+
const hfs = settingsHfs(context.settings);
|
|
53
|
+
if (!isHfsView(hfs)) {
|
|
54
|
+
throw new Error(`${context.id ?? "a starci-fe rule"} needs settings.starci.hfs: lint through starciFeConfig({ hfs: loadHfs(import.meta.url) })`);
|
|
55
|
+
}
|
|
56
|
+
return hfs;
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* The root of a classification the caller already knows is owned: a rule asks for it only after its scope test placed the
|
|
60
|
+
* file in a slot, so a missing root is a broken manifest and stops the run, as reading it always has.
|
|
61
|
+
*/
|
|
62
|
+
export const requiredRoot = (found) => {
|
|
63
|
+
if (found.root === undefined)
|
|
64
|
+
throw new TypeError(`no slot root for ${found.path} (${found.status})`);
|
|
65
|
+
return found.root;
|
|
66
|
+
};
|
|
67
|
+
/** A slot the manifest always declares (`fe.app.next`, `fe.package.ui`, ...): a missing one is a broken manifest and stops the run. */
|
|
68
|
+
export const requiredSlot = (slot, id) => {
|
|
69
|
+
if (slot === null)
|
|
70
|
+
throw new TypeError(`the slot manifest declares no ${id}`);
|
|
71
|
+
return slot;
|
|
72
|
+
};
|
package/dist/lib/next.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
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
|
+
import { NEXT_RESERVED_EXPORTS as RESERVED, NEXT_ROUTE_SEGMENT_STEMS as STEMS } from "#runtime/scripts/lib/next-contract.mjs";
|
|
7
|
+
/** The exports Next.js reserves in a route segment file. */
|
|
8
|
+
export const NEXT_RESERVED_EXPORTS = RESERVED;
|
|
9
|
+
/** The stems of Next.js route segment files. */
|
|
10
|
+
export const NEXT_ROUTE_SEGMENT_STEMS = STEMS;
|
|
@@ -0,0 +1,17 @@
|
|
|
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 type { TSESLint } from "@typescript-eslint/utils";
|
|
11
|
+
import ts from "typescript";
|
|
12
|
+
import type { AnyContext } from "./rule.js";
|
|
13
|
+
/** The definition of an import binding. */
|
|
14
|
+
export type ImportBindingDefinition = TSESLint.Scope.Definitions.ImportBindingDefinition;
|
|
15
|
+
export declare function reaching(program: ts.Program, filename: string, targetFile: string): ts.Program;
|
|
16
|
+
/** True when the import binding is a runtime `use*` import that resolves to a hook declared in a `hooks/` folder of the app. */
|
|
17
|
+
export declare function importedOwnHook(context: AnyContext, definition: ImportBindingDefinition): boolean;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
import { programFor } from "./program-for.js";
|
|
3
|
+
const HOOKS_FOLDER = /(^|\/)hooks\//;
|
|
4
|
+
const isHookName = (name) => /^use[A-Z0-9]/.test(name);
|
|
5
|
+
/** 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. */
|
|
6
|
+
const widened = new WeakMap();
|
|
7
|
+
export function reaching(program, filename, targetFile) {
|
|
8
|
+
if (program.getSourceFile(targetFile))
|
|
9
|
+
return program;
|
|
10
|
+
const key = `${filename}\0${targetFile}`;
|
|
11
|
+
const cache = widened.get(program) ?? new Map();
|
|
12
|
+
widened.set(program, cache);
|
|
13
|
+
const known = cache.get(key);
|
|
14
|
+
if (known !== undefined)
|
|
15
|
+
return known;
|
|
16
|
+
const created = ts.createProgram([filename, targetFile], program.getCompilerOptions());
|
|
17
|
+
cache.set(key, created);
|
|
18
|
+
return created;
|
|
19
|
+
}
|
|
20
|
+
/** A declaration of a hook value: a function or a variable, not a type, interface, class or enum. */
|
|
21
|
+
const hookValue = (declaration) => ts.isFunctionDeclaration(declaration) || ts.isVariableDeclaration(declaration);
|
|
22
|
+
const inAppHooksFolder = (file) => {
|
|
23
|
+
const fileName = file.fileName.replaceAll("\\", "/");
|
|
24
|
+
return !fileName.includes("/node_modules/") && !file.isDeclarationFile && HOOKS_FOLDER.test(fileName);
|
|
25
|
+
};
|
|
26
|
+
/** True when the import binding is a runtime `use*` import that resolves to a hook declared in a `hooks/` folder of the app. */
|
|
27
|
+
export function importedOwnHook(context, definition) {
|
|
28
|
+
const specifier = definition.node;
|
|
29
|
+
const parent = definition.parent;
|
|
30
|
+
if (specifier.type !== "ImportSpecifier" || parent.importKind === "type" || specifier.importKind === "type")
|
|
31
|
+
return false;
|
|
32
|
+
// an import specifier always sits in an import declaration
|
|
33
|
+
if (parent.type !== "ImportDeclaration")
|
|
34
|
+
return false;
|
|
35
|
+
const name = specifier.imported.type === "Identifier" ? specifier.imported.name : specifier.imported.value;
|
|
36
|
+
if (!isHookName(name))
|
|
37
|
+
return false;
|
|
38
|
+
const source = String(parent.source.value);
|
|
39
|
+
const base = programFor(context);
|
|
40
|
+
const target = ts.resolveModuleName(source, context.filename, base.getCompilerOptions(), ts.sys).resolvedModule;
|
|
41
|
+
if (!target || target.isExternalLibraryImport)
|
|
42
|
+
return false;
|
|
43
|
+
const program = reaching(base, context.filename, target.resolvedFileName);
|
|
44
|
+
const file = program.getSourceFile(target.resolvedFileName);
|
|
45
|
+
const checker = program.getTypeChecker();
|
|
46
|
+
const moduleSymbol = file && checker.getSymbolAtLocation(file);
|
|
47
|
+
const exported = moduleSymbol ? checker.getExportsOfModule(moduleSymbol).find((symbol) => symbol.name === name) : undefined;
|
|
48
|
+
if (!exported)
|
|
49
|
+
return false;
|
|
50
|
+
const symbol = (exported.flags & ts.SymbolFlags.Alias) !== 0 ? checker.getAliasedSymbol(exported) : exported;
|
|
51
|
+
return (symbol.declarations ?? []).some((declaration) => hookValue(declaration) && inAppHooksFolder(declaration.getSourceFile()));
|
|
52
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The package's own root, the same from the TypeScript source (`src/lib`) and from the build (`dist/lib`): both sit two
|
|
3
|
+
* levels below it, so every file the package reads beside its code (the bundled `runtime/`, the fixtures) resolves here.
|
|
4
|
+
*/
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
/** The folder holding this package's package.json. */
|
|
8
|
+
export const PACKAGE_ROOT = fileURLToPath(new URL("../..", import.meta.url));
|
|
9
|
+
/** The bundled runtime the canon reads the slot manifest, the architecture machine and its catalogs from. */
|
|
10
|
+
export const RUNTIME_ROOT = join(PACKAGE_ROOT, "runtime");
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** Forward-slash form of a filename, so Windows paths compare like every other path. */
|
|
2
|
-
export const normalizePath = (filename) => String(filename || "").replace(/\\/g, "/")
|
|
2
|
+
export const normalizePath = (filename) => String(filename || "").replace(/\\/g, "/");
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { TSESTree } from "@typescript-eslint/utils";
|
|
2
|
+
import type { AnyContext, Node } from "./rule.js";
|
|
3
|
+
/**
|
|
4
|
+
* True when the symbol at a node (an identifier, or the property of a member access) is declared by the platform.
|
|
5
|
+
*
|
|
6
|
+
* @param {object} context - The ESLint rule context.
|
|
7
|
+
* @param {object} node - An ESTree identifier, or a member expression (its property is asked).
|
|
8
|
+
* @returns {boolean} Whether every declaration of the symbol is a platform (global, ambient) declaration.
|
|
9
|
+
*/
|
|
10
|
+
export declare const isPlatform: (context: AnyContext, node: TSESTree.Identifier | TSESTree.MemberExpression | TSESTree.JSXIdentifier) => boolean;
|
|
11
|
+
/**
|
|
12
|
+
* True when the type of an expression carries no information (`any`, `unknown` or an error type).
|
|
13
|
+
*
|
|
14
|
+
* @param {object} context - The ESLint rule context.
|
|
15
|
+
* @param {object} node - An ESTree expression.
|
|
16
|
+
* @returns {boolean} Whether the checker could not decide what the expression is.
|
|
17
|
+
*/
|
|
18
|
+
export declare const isUnresolvedType: (context: AnyContext, node: Node) => boolean;
|
|
19
|
+
/**
|
|
20
|
+
* The property names of the type of an expression, after union members are read.
|
|
21
|
+
*
|
|
22
|
+
* @param {object} context - The ESLint rule context.
|
|
23
|
+
* @param {object} node - An ESTree expression.
|
|
24
|
+
* @returns {Set<string>} The names of the properties every member of the type could carry.
|
|
25
|
+
*/
|
|
26
|
+
export declare const propertyNamesOf: (context: AnyContext, node: Node) => Set<string>;
|
|
@@ -3,19 +3,19 @@
|
|
|
3
3
|
* TypeScript symbol it binds to is declared in a default library file or in an ambient global declaration, so a local
|
|
4
4
|
* function that happens to be called `setTimeout` or `fetch` is not mistaken for the browser's.
|
|
5
5
|
*/
|
|
6
|
-
import ts from "typescript"
|
|
7
|
-
import { typed } from "./types.
|
|
8
|
-
|
|
6
|
+
import ts from "typescript";
|
|
7
|
+
import { typed } from "./types.js";
|
|
9
8
|
/** True when a declaration sits in the default lib, in a global script declaration file, or in a `declare global` block. */
|
|
10
9
|
const isGlobalDeclaration = (program, declaration) => {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
}
|
|
18
|
-
|
|
10
|
+
const file = declaration.getSourceFile();
|
|
11
|
+
if (program.isSourceFileDefaultLibrary(file))
|
|
12
|
+
return true;
|
|
13
|
+
for (let current = declaration.parent; current; current = current.parent) {
|
|
14
|
+
if (ts.isModuleDeclaration(current) && (current.flags & ts.NodeFlags.GlobalAugmentation) !== 0)
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
return file.isDeclarationFile && !ts.isExternalModule(file);
|
|
18
|
+
};
|
|
19
19
|
/**
|
|
20
20
|
* True when the symbol at a node (an identifier, or the property of a member access) is declared by the platform.
|
|
21
21
|
*
|
|
@@ -24,15 +24,15 @@ const isGlobalDeclaration = (program, declaration) => {
|
|
|
24
24
|
* @returns {boolean} Whether every declaration of the symbol is a platform (global, ambient) declaration.
|
|
25
25
|
*/
|
|
26
26
|
export const isPlatform = (context, node) => {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
27
|
+
const { program, checker, toTs } = typed(context);
|
|
28
|
+
const target = node.type === "MemberExpression" ? node.property : node;
|
|
29
|
+
const tsNode = toTs(target);
|
|
30
|
+
if (!tsNode)
|
|
31
|
+
return false;
|
|
32
|
+
const symbol = checker.getSymbolAtLocation(tsNode);
|
|
33
|
+
const declarations = symbol?.getDeclarations() ?? [];
|
|
34
|
+
return declarations.length > 0 && declarations.every((declaration) => isGlobalDeclaration(program, declaration));
|
|
35
|
+
};
|
|
36
36
|
/**
|
|
37
37
|
* True when the type of an expression carries no information (`any`, `unknown` or an error type).
|
|
38
38
|
*
|
|
@@ -41,13 +41,13 @@ export const isPlatform = (context, node) => {
|
|
|
41
41
|
* @returns {boolean} Whether the checker could not decide what the expression is.
|
|
42
42
|
*/
|
|
43
43
|
export const isUnresolvedType = (context, node) => {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
44
|
+
const { checker, toTs } = typed(context);
|
|
45
|
+
const tsNode = toTs(node);
|
|
46
|
+
if (!tsNode)
|
|
47
|
+
return true;
|
|
48
|
+
const type = checker.getTypeAtLocation(tsNode);
|
|
49
|
+
return Boolean(type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown));
|
|
50
|
+
};
|
|
51
51
|
/**
|
|
52
52
|
* The property names of the type of an expression, after union members are read.
|
|
53
53
|
*
|
|
@@ -56,11 +56,14 @@ export const isUnresolvedType = (context, node) => {
|
|
|
56
56
|
* @returns {Set<string>} The names of the properties every member of the type could carry.
|
|
57
57
|
*/
|
|
58
58
|
export const propertyNamesOf = (context, node) => {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
59
|
+
const { checker, toTs } = typed(context);
|
|
60
|
+
const tsNode = toTs(node);
|
|
61
|
+
const names = new Set();
|
|
62
|
+
if (!tsNode)
|
|
63
|
+
return names;
|
|
64
|
+
const type = checker.getTypeAtLocation(tsNode);
|
|
65
|
+
for (const part of type.isUnion() ? type.types : [type])
|
|
66
|
+
for (const property of part.getProperties())
|
|
67
|
+
names.add(property.getName());
|
|
68
|
+
return names;
|
|
69
|
+
};
|
|
@@ -0,0 +1,5 @@
|
|
|
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 type { AnyContext } from "./rule.js";
|
|
4
|
+
/** The program for a rule context; a program built here is remembered per context. */
|
|
5
|
+
export declare function programFor(context: AnyContext): ts.Program;
|
|
@@ -0,0 +1,23 @@
|
|
|
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
|
+
const programs = new WeakMap();
|
|
5
|
+
/** The program for a rule context; a program built here is remembered per context. */
|
|
6
|
+
export function programFor(context) {
|
|
7
|
+
const remembered = programs.get(context);
|
|
8
|
+
if (remembered !== undefined)
|
|
9
|
+
return remembered;
|
|
10
|
+
const services = context.sourceCode?.parserServices;
|
|
11
|
+
if (services?.program)
|
|
12
|
+
return services.program;
|
|
13
|
+
const filename = context.filename;
|
|
14
|
+
const configPath = ts.findConfigFile(path.dirname(filename), ts.sys.fileExists);
|
|
15
|
+
let options = { moduleResolution: ts.ModuleResolutionKind.Bundler, module: ts.ModuleKind.ESNext };
|
|
16
|
+
if (configPath) {
|
|
17
|
+
const config = ts.readConfigFile(configPath, ts.sys.readFile);
|
|
18
|
+
options = { ...options, ...ts.parseJsonConfigFileContent(config.config, ts.sys, path.dirname(configPath)).options };
|
|
19
|
+
}
|
|
20
|
+
const program = ts.createProgram([filename], options);
|
|
21
|
+
programs.set(context, program);
|
|
22
|
+
return program;
|
|
23
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { AnyContext } from "./rule.js";
|
|
2
|
+
/**
|
|
3
|
+
* What an import of the callback's owner modules is: `"pure"` (a pure text-returning helper), `"routes"` (a static route table) or null.
|
|
4
|
+
*
|
|
5
|
+
* @param context - The ESLint rule context.
|
|
6
|
+
* @param source - The import specifier.
|
|
7
|
+
* @param name - The imported name.
|
|
8
|
+
* @returns The kind.
|
|
9
|
+
*/
|
|
10
|
+
export declare function ownerExportKind(context: AnyContext, source: string, name: string): "pure" | "routes" | null;
|
|
@@ -0,0 +1,156 @@
|
|
|
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.js";
|
|
13
|
+
import { programFor } from "./program-for.js";
|
|
14
|
+
import { resolveImport } from "./scope.js";
|
|
15
|
+
/** Globals that reach the network, storage, the page, the process or the console. */
|
|
16
|
+
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"]);
|
|
17
|
+
const OWNER_FILE = /\/modules\/(?:routes|auth)(?:\/|\.tsx?$)/;
|
|
18
|
+
const CONFIG_FILE = /\/modules\/config(?:\/|\.tsx?$)/;
|
|
19
|
+
const EXTENSIONS = ["", ".ts", ".tsx", "/index.ts", "/index.tsx"];
|
|
20
|
+
const normal = (file) => file.fileName.replaceAll("\\", "/");
|
|
21
|
+
/** The module file an import specifier names, or null when none exists on disk. */
|
|
22
|
+
const moduleFile = (context, source) => {
|
|
23
|
+
const target = resolveImport(context, source);
|
|
24
|
+
if (target === null)
|
|
25
|
+
return null;
|
|
26
|
+
return EXTENSIONS.map((extension) => `${target}${extension}`).find((candidate) => ts.sys.fileExists(candidate)) ?? null;
|
|
27
|
+
};
|
|
28
|
+
/** The declaration an export of the module resolves to, after barrels and re-exports. */
|
|
29
|
+
const exportedDeclaration = (context, source, name) => {
|
|
30
|
+
const file = moduleFile(context, source);
|
|
31
|
+
if (file === null)
|
|
32
|
+
return null;
|
|
33
|
+
const program = reaching(programFor(context), context.filename, file);
|
|
34
|
+
const checker = program.getTypeChecker();
|
|
35
|
+
const sourceFile = program.getSourceFile(file);
|
|
36
|
+
const moduleSymbol = sourceFile ? checker.getSymbolAtLocation(sourceFile) : undefined;
|
|
37
|
+
const exported = moduleSymbol ? checker.getExportsOfModule(moduleSymbol).find((symbol) => symbol.name === name) : undefined;
|
|
38
|
+
if (!exported)
|
|
39
|
+
return null;
|
|
40
|
+
const symbol = (exported.flags & ts.SymbolFlags.Alias) !== 0 ? checker.getAliasedSymbol(exported) : exported;
|
|
41
|
+
return { checker, declaration: symbol.declarations?.[0] ?? null };
|
|
42
|
+
};
|
|
43
|
+
/** Strip `as`, `satisfies` and parentheses. */
|
|
44
|
+
const bare = (node) => {
|
|
45
|
+
let current = node;
|
|
46
|
+
while (current && (ts.isAsExpression(current) || ts.isSatisfiesExpression(current) || ts.isParenthesizedExpression(current)))
|
|
47
|
+
current = current.expression;
|
|
48
|
+
return current;
|
|
49
|
+
};
|
|
50
|
+
/** The function-like node of a function declaration or of a variable initialised with an arrow or function expression. */
|
|
51
|
+
const functionOf = (declaration) => {
|
|
52
|
+
if (ts.isFunctionDeclaration(declaration))
|
|
53
|
+
return declaration;
|
|
54
|
+
const init = ts.isVariableDeclaration(declaration) ? bare(declaration.initializer) : undefined;
|
|
55
|
+
return init !== undefined && (ts.isArrowFunction(init) || ts.isFunctionExpression(init)) ? init : null;
|
|
56
|
+
};
|
|
57
|
+
const isInside = (node, root) => node.getSourceFile() === root.getSourceFile() && node.pos >= root.pos && node.end <= root.end;
|
|
58
|
+
/** Whether an identifier names a declaration, a property or a member rather than referring to a binding. */
|
|
59
|
+
const isNameOnly = (node) => {
|
|
60
|
+
const parent = node.parent;
|
|
61
|
+
if (ts.isPropertyAccessExpression(parent))
|
|
62
|
+
return parent.name === node;
|
|
63
|
+
if (ts.isPropertyAssignment(parent) || ts.isMethodDeclaration(parent) || ts.isPropertySignature(parent))
|
|
64
|
+
return parent.name === node;
|
|
65
|
+
return (ts.isParameter(parent) || ts.isVariableDeclaration(parent) || ts.isFunctionDeclaration(parent) || ts.isBindingElement(parent)) && parent.name === node;
|
|
66
|
+
};
|
|
67
|
+
/** A declaration of another file: an owner helper is followed, the environment owner is a leaf, everything else is impure. */
|
|
68
|
+
const foreignPure = (env, declaration, root) => {
|
|
69
|
+
const file = normal(declaration.getSourceFile());
|
|
70
|
+
if (CONFIG_FILE.test(file))
|
|
71
|
+
return true;
|
|
72
|
+
if (!OWNER_FILE.test(file) || declaration.getSourceFile().isDeclarationFile)
|
|
73
|
+
return false;
|
|
74
|
+
return declarationPure(env, declaration, root);
|
|
75
|
+
};
|
|
76
|
+
/** A global of the language or of the lib files is pure unless it reaches I/O. */
|
|
77
|
+
const globalPure = (symbol) => !IO_GLOBALS.has(symbol.getName());
|
|
78
|
+
/** Whether one identifier reference stays inside the pure surface. */
|
|
79
|
+
const referencePure = (env, node, root) => {
|
|
80
|
+
const symbol = env.checker.getSymbolAtLocation(node);
|
|
81
|
+
if (!symbol)
|
|
82
|
+
return node.text === "undefined";
|
|
83
|
+
const target = (symbol.flags & ts.SymbolFlags.Alias) !== 0 ? env.checker.getAliasedSymbol(symbol) : symbol;
|
|
84
|
+
const declarations = target.declarations ?? [];
|
|
85
|
+
if (declarations.length === 0)
|
|
86
|
+
return node.text === "undefined";
|
|
87
|
+
return declarations.every((declaration) => {
|
|
88
|
+
const file = declaration.getSourceFile();
|
|
89
|
+
if (isInside(declaration, root))
|
|
90
|
+
return true;
|
|
91
|
+
if (!ts.isExternalModule(file))
|
|
92
|
+
return globalPure(target);
|
|
93
|
+
return foreignPure(env, declaration, root);
|
|
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))
|
|
99
|
+
return false;
|
|
100
|
+
if (ts.isTypeNode(node) && !ts.isExpressionWithTypeArguments(node))
|
|
101
|
+
return true;
|
|
102
|
+
if (ts.isIdentifier(node) && !isNameOnly(node) && !referencePure(env, node, root))
|
|
103
|
+
return false;
|
|
104
|
+
return ts.forEachChild(node, (child) => (nodePure(env, child, root) ? undefined : true)) !== true;
|
|
105
|
+
};
|
|
106
|
+
/** A declaration is pure when its function (or its constant initialiser) is. */
|
|
107
|
+
function declarationPure(env, declaration, _root) {
|
|
108
|
+
if (env.seen.has(declaration))
|
|
109
|
+
return true;
|
|
110
|
+
env.seen.add(declaration);
|
|
111
|
+
const fn = functionOf(declaration);
|
|
112
|
+
if (fn)
|
|
113
|
+
return !("asteriskToken" in fn && fn.asteriskToken) && !fn.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword) && (fn.body ? nodePure(env, fn.body, fn) : false);
|
|
114
|
+
if (ts.isVariableDeclaration(declaration) && declaration.initializer)
|
|
115
|
+
return nodePure(env, declaration.initializer, declaration.initializer);
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
/** Whether a function's declared or inferred return type is text, null or undefined. */
|
|
119
|
+
const returnsText = (checker, declaration) => {
|
|
120
|
+
const signature = checker.getSignaturesOfType(checker.getTypeAtLocation(declaration), ts.SignatureKind.Call)[0];
|
|
121
|
+
if (!signature)
|
|
122
|
+
return false;
|
|
123
|
+
const type = checker.getReturnTypeOfSignature(signature);
|
|
124
|
+
const parts = type.isUnion() ? type.types : [type];
|
|
125
|
+
return parts.every((part) => (part.flags & (ts.TypeFlags.StringLike | ts.TypeFlags.Null | ts.TypeFlags.Undefined)) !== 0);
|
|
126
|
+
};
|
|
127
|
+
/** A static table: a constant object whose leaves are string literals or plain templates. */
|
|
128
|
+
const isStaticTable = (node) => {
|
|
129
|
+
const value = bare(node);
|
|
130
|
+
if (!value)
|
|
131
|
+
return false;
|
|
132
|
+
if (ts.isStringLiteralLike(value))
|
|
133
|
+
return true;
|
|
134
|
+
if (ts.isObjectLiteralExpression(value))
|
|
135
|
+
return value.properties.every((property) => ts.isPropertyAssignment(property) && isStaticTable(property.initializer));
|
|
136
|
+
return false;
|
|
137
|
+
};
|
|
138
|
+
/**
|
|
139
|
+
* What an import of the callback's owner modules is: `"pure"` (a pure text-returning helper), `"routes"` (a static route table) or null.
|
|
140
|
+
*
|
|
141
|
+
* @param context - The ESLint rule context.
|
|
142
|
+
* @param source - The import specifier.
|
|
143
|
+
* @param name - The imported name.
|
|
144
|
+
* @returns The kind.
|
|
145
|
+
*/
|
|
146
|
+
export function ownerExportKind(context, source, name) {
|
|
147
|
+
const found = exportedDeclaration(context, source, name);
|
|
148
|
+
if (!found?.declaration)
|
|
149
|
+
return null;
|
|
150
|
+
const { checker, declaration } = found;
|
|
151
|
+
if (ts.isVariableDeclaration(declaration) && !functionOf(declaration))
|
|
152
|
+
return (declaration.parent.flags & ts.NodeFlags.Const) !== 0 && isStaticTable(declaration.initializer) ? "routes" : null;
|
|
153
|
+
const fn = functionOf(declaration);
|
|
154
|
+
const env = { checker, seen: new Set() };
|
|
155
|
+
return fn !== null && returnsText(checker, fn) && declarationPure(env, declaration, fn) ? "pure" : null;
|
|
156
|
+
}
|