@starci/eslint-canon-fe 9.9.12 → 9.11.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 CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 9.11.0 - 2026-11-03
4
+
5
+ - Changed: `modules/auth` holds the front-end transport rules like `modules/api`: no auth door file takes axios or `fetch`, and no file keeps module-level mutable state (the access token lives in the `@starci/auth` client); the stack concern `auth` names `@starci/auth`.
6
+
7
+ ## 9.10.0
8
+
9
+ - Added: `starci-fe/directive-first` (R369, `FE_DIRECTIVE_FIRST`) at error: a `"use client"` or `"use server"` directive is the first statement of its module; below an import it does nothing and `next build` refuses the file.
10
+ - Added: `starci-fe/explicit-export-surface` (R368, `HFS_EXPLICIT_EXPORT_SURFACE`) at error: an exported function's return type, an exported variable's type and a parameter's type are written, not inferred from a body or from other files; a component that returns only JSX, `null` or `undefined` is explicit. It reports each export the isolated declaration emit cannot write, at the offending span, and says what to annotate. It reads the classifier the lint result cache reads.
11
+ - Added: `projectGraphSnapshot`, the findings of the project graph the process has already built, which the ESLint child hands to the cache; `projectGraph` stays the fresh build.
12
+ - Changed: carry the bundled runtime of @starci/hfs 5.1.0 (the classifier, the rule factory, the rule catalog with R368, the canon pins).
13
+ - Fixed: `no-swr-default-array` (R367) judges a `?? []` inside a `useMemo` callback or a `slotOfReads` projector by the hook that holds it, so a slot carrying the read's status is no longer reported.
14
+
3
15
  ## 9.9.12
4
16
 
5
17
  - Changed: Carry the bundled runtime of @starci/hfs 5.0.8 (the rule texts name `app check`, the read-set helper of the rule memo) and its canon pins; the comments name `app check` for the removed lint verb.
package/README.md CHANGED
@@ -62,6 +62,16 @@ canonical page renderer. Only those exact input members may take `children`; ord
62
62
  renderers and shape records containing rendered trees remain refused. The selected canonical Grammar catalog owns
63
63
  semantic renderer bindings; universal code carries no concrete renderer spelling.
64
64
 
65
+ The [explicit exported surface](../../contexts/code/shared/typing.md) (R368, SHARED-TYPING-5) is `explicit-export-surface`
66
+ (`HFS_EXPLICIT_EXPORT_SURFACE`): an exported function's return type, an exported variable's type and a parameter's type are
67
+ written where they are exported, never inferred from a body or from other files; a component that returns only JSX, `null` or
68
+ `undefined` is explicit. It reports each export TypeScript's isolated declaration emit cannot write, at the offending span, and says
69
+ which annotation to add. It reads the classifier the lint result cache reads (`declarationSurface`, the bundled
70
+ `runtime/scripts/lib/declaration-surface.mjs`), so it reports exactly the files the cache counts as open.
71
+
72
+ `projectGraph` and `projectGraphSnapshot` are the cache's two readers of the project graph the graph rules share: the first builds it
73
+ afresh, the second answers from the graph the process has already built, which is how the ESLint child hands its graph to the cache.
74
+
65
75
  Regression specs belong to this package. Application front ends and Grammar libraries have no specs.
66
76
 
67
77
  ## The runtime copy
@@ -0,0 +1,16 @@
1
+ /**
2
+ * A module directive is the first statement of its module (HFS R369, `FE_DIRECTIVE_FIRST`).
3
+ *
4
+ * `"use client"` and `"use server"` are directives only in the prologue, before any import or other statement. Below an import the string is an
5
+ * expression statement that does nothing, Prettier wraps it as `("use client")`, and `next build` stops with "The 'use client' directive must be placed
6
+ * before other expressions". ESLint's parser marks a real directive with `directive`, so a string statement of that text without one is misplaced.
7
+ */
8
+ import { type Recommended } from "./lib/rule.js";
9
+ /** A module directive stays in the prologue. */
10
+ export declare const directiveFirst: import("./lib/rule.js").Rule<"misplaced", []>;
11
+ /** The rules this law contributes to the plugin. */
12
+ export declare const rules: {
13
+ "directive-first": import("./lib/rule.js").Rule<"misplaced", []>;
14
+ };
15
+ /** Every misplaced directive is a build error. */
16
+ export declare const recommended: Recommended;
@@ -0,0 +1,37 @@
1
+ /**
2
+ * A module directive is the first statement of its module (HFS R369, `FE_DIRECTIVE_FIRST`).
3
+ *
4
+ * `"use client"` and `"use server"` are directives only in the prologue, before any import or other statement. Below an import the string is an
5
+ * expression statement that does nothing, Prettier wraps it as `("use client")`, and `next build` stops with "The 'use client' directive must be placed
6
+ * before other expressions". ESLint's parser marks a real directive with `directive`, so a string statement of that text without one is misplaced.
7
+ */
8
+ import { defineRule } from "./lib/rule.js";
9
+ /** The directives Next.js reads from the prologue of a module. */
10
+ const DIRECTIVES = new Set(["use client", "use server"]);
11
+ /** A module directive stays in the prologue. */
12
+ export const directiveFirst = defineRule({
13
+ meta: {
14
+ type: "problem",
15
+ docs: { description: "R369: a 'use client' or 'use server' directive is the first statement of its module." },
16
+ schema: [],
17
+ messages: {
18
+ misplaced: 'FE_DIRECTIVE_FIRST: "{{directive}}" is not at the top of the module, so it is an expression that does nothing and `next build` refuses the file. Move it above every import and other statement.',
19
+ },
20
+ },
21
+ create(context) {
22
+ return {
23
+ ExpressionStatement(node) {
24
+ if (node.parent.type !== "Program" || "directive" in node && typeof node.directive === "string")
25
+ return;
26
+ const expression = node.expression;
27
+ if (expression.type !== "Literal" || typeof expression.value !== "string" || !DIRECTIVES.has(expression.value))
28
+ return;
29
+ context.report({ node, messageId: "misplaced", data: { directive: expression.value } });
30
+ },
31
+ };
32
+ },
33
+ });
34
+ /** The rules this law contributes to the plugin. */
35
+ export const rules = { "directive-first": directiveFirst };
36
+ /** Every misplaced directive is a build error. */
37
+ export const recommended = { "starci-fe/directive-first": "error" };
@@ -0,0 +1,7 @@
1
+ import type { Recommended } from "./lib/rule.js";
2
+ /** The rules this law contributes to the plugin. */
3
+ export declare const rules: {
4
+ "explicit-export-surface": import("@typescript-eslint/utils/ts-eslint").RuleModule<"inferred", readonly unknown[], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
5
+ };
6
+ /** Every exported type is written out, at error. */
7
+ export declare const recommended: Recommended;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * The explicit exported surface (HFS R368, `HFS_EXPLICIT_EXPORT_SURFACE`): an exported declaration states its type, in the file that exports it.
3
+ *
4
+ * A return type, variable type or parameter type left to inference is a surface importers read from the body and from the files the body uses, so
5
+ * the result cache of `starci app check --gate lint` lints every importer again whenever the file changes (check-cache.md, ESLint). The rule
6
+ * reports each export the isolated declaration emit (`ts.transpileDeclaration`) cannot write without inference, where it is written, and says
7
+ * what to annotate. A component that returns only JSX has the type the JSX namespace gives it and is not reported. It is the very classifier the
8
+ * cache reads (`declarationSurface` of the bundled runtime), so the rule reports exactly the files the cache counts as open.
9
+ */
10
+ import ts from "typescript";
11
+ import { explicitSurfaceRule } from "#runtime/scripts/hfs/explicit-surface-rule.mjs";
12
+ /** The rules this law contributes to the plugin. */
13
+ export const rules = {
14
+ "explicit-export-surface": explicitSurfaceRule({ ts }),
15
+ };
16
+ /** Every exported type is written out, at error. */
17
+ export const recommended = {
18
+ "starci-fe/explicit-export-surface": "error",
19
+ };
@@ -4,6 +4,11 @@ import { type Rules, type Recommended } from "./lib/rule.js";
4
4
  * No import (static, re-export, dynamic or `require`) of a package the fe-stack catalog forbids.
5
5
  */
6
6
  export declare const feStackImport: import("./lib/rule.js").Rule<"forbidden", []>;
7
+ /**
8
+ * The SWR extension points belong to `@starci/next-data`: `pollingRestart` is the one middleware and `createViewerCache` the one
9
+ * binding of the viewer's cache, so an app that imports the `Middleware` or `ScopedMutator` type is writing a second one.
10
+ */
11
+ export declare const swrExtensionFromPackage: import("./lib/rule.js").Rule<"own", []>;
7
12
  /** The rules this law contributes to the plugin. */
8
13
  export declare const rules: Rules;
9
14
  /** Every rule is an error: a second library for a concern is the defect, not a style. */
package/dist/fe-stack.js CHANGED
@@ -59,9 +59,43 @@ export const feStackImport = defineRule({
59
59
  };
60
60
  },
61
61
  });
62
+ /** The SWR types an app needs only to write its own middleware or to bind the writes of a viewer's cache by hand. */
63
+ const PACKAGE_OWNED_SWR_TYPES = new Set(["Middleware", "ScopedMutator"]);
64
+ /**
65
+ * The SWR extension points belong to `@starci/next-data`: `pollingRestart` is the one middleware and `createViewerCache` the one
66
+ * binding of the viewer's cache, so an app that imports the `Middleware` or `ScopedMutator` type is writing a second one.
67
+ */
68
+ export const swrExtensionFromPackage = defineRule({
69
+ meta: {
70
+ type: "problem",
71
+ docs: { description: "An app writes no SWR middleware and no cache binding of its own." },
72
+ schema: [],
73
+ messages: {
74
+ own: "`{{name}}` of `swr` is the type of an SWR extension the stack already ships. Use `pollingRestart`, `viewerSwrConfig` and `createViewerCache` of `@starci/next-data` (FE-DATA-SWR, FE-DATA-KEY) instead of writing a second one.",
75
+ },
76
+ },
77
+ create(context) {
78
+ if (!isProductSource(context))
79
+ return {};
80
+ return {
81
+ ImportDeclaration(node) {
82
+ if (node.source.value !== "swr")
83
+ return;
84
+ for (const specifier of node.specifiers) {
85
+ if (specifier.type !== "ImportSpecifier")
86
+ continue;
87
+ const name = specifier.imported.type === "Identifier" ? specifier.imported.name : String(specifier.imported.value);
88
+ if (PACKAGE_OWNED_SWR_TYPES.has(name))
89
+ context.report({ node: specifier, messageId: "own", data: { name } });
90
+ }
91
+ },
92
+ };
93
+ },
94
+ });
62
95
  /** The rules this law contributes to the plugin. */
63
96
  export const rules = {
64
97
  "fe-stack-import": feStackImport,
98
+ "swr-extension-from-package": swrExtensionFromPackage,
65
99
  };
66
100
  /** Every rule is an error: a second library for a concern is the defect, not a style. */
67
101
  export const recommended = Object.fromEntries(Object.keys(rules).map((name) => [`starci-fe/${name}`, "error"]));
package/dist/index.d.ts CHANGED
@@ -106,5 +106,5 @@ export declare const starciTypedSourceConfig: ({ root }?: {
106
106
  }) => Linter.Config[];
107
107
  export { crossFileInputs } from "./lib/cross-file-inputs.js";
108
108
  export type { CrossFileInput } from "./lib/cross-file-inputs.js";
109
- export { projectGraph } from "./project-graph.js";
109
+ export { projectGraph, projectGraphSnapshot } from "./project-graph.js";
110
110
  export type { ProjectGraphFinding, ProjectGraphSnapshot } from "./project-graph.js";
package/dist/index.js CHANGED
@@ -27,6 +27,8 @@ import { recommended as hygieneRecommended, rules as hygieneRules } from "./hygi
27
27
  import { recommended as landmarkRecommended, rules as landmarkRules } from "./landmark.js";
28
28
  import { recommended as listsRecommended, rules as listsRules } from "./lists.js";
29
29
  import { recommended as loadingRecommended, rules as loadingRules } from "./loading.js";
30
+ import { recommended as directiveFirstRecommended, rules as directiveFirstRules } from "./directive-first.js";
31
+ import { recommended as explicitSurfaceRecommended, rules as explicitSurfaceRules } from "./explicit-surface.js";
30
32
  import { recommended as skeletonStatesRecommended, rules as skeletonStatesRules } from "./skeleton-states.js";
31
33
  import { recommended as lintEscapeRecommended, rules as lintEscapeRules } from "./lint-escape-hatch.js";
32
34
  import { recommended as namingRecommended, rules as namingRules } from "./naming.js";
@@ -74,6 +76,8 @@ const CONTRIBUTIONS = [
74
76
  { law: "landmark", rules: landmarkRules, recommended: landmarkRecommended },
75
77
  { law: "lists", rules: listsRules, recommended: listsRecommended },
76
78
  { law: "loading", rules: loadingRules, recommended: loadingRecommended },
79
+ { law: "directive-first", rules: directiveFirstRules, recommended: directiveFirstRecommended },
80
+ { law: "explicit-surface", rules: explicitSurfaceRules, recommended: explicitSurfaceRecommended },
77
81
  { law: "skeleton-states", rules: skeletonStatesRules, recommended: skeletonStatesRecommended },
78
82
  { law: "lint-escape-hatch", rules: lintEscapeRules, recommended: lintEscapeRecommended },
79
83
  { law: "naming", rules: namingRules, recommended: namingRecommended },
@@ -313,4 +317,4 @@ for (const addition of A14_CONTRIBUTIONS) {
313
317
  }
314
318
  // The result cache of `starci app check --gate lint` (contexts/project/check-cache.md) reads these two declarations of the installed canon.
315
319
  export { crossFileInputs } from "./lib/cross-file-inputs.js";
316
- export { projectGraph } from "./project-graph.js";
320
+ export { projectGraph, projectGraphSnapshot } from "./project-graph.js";
@@ -105,7 +105,7 @@ export const SLOT_FREE_RULES = Object.freeze([
105
105
  "outcome-kinds-exhaustive", "no-await-in-loop", "prefer-code-point", "prefer-string-raw", "no-nested-conditional", "no-void-operator", "no-unused-import",
106
106
  "prefer-export-from", "no-unused-prop-types", "media-has-caption", "max-params",
107
107
  "no-list-panel-padding", "no-hand-built-panes", "no-table-in-card", "no-heading-above-card", "no-selectable-tag-group", "no-fill-command-in-header", "no-app-surface-paint", "divider-full-bleed", "control-height", "selection-fill-no-check", "no-grammar-shim",
108
- "no-swr-default-array", "no-foreign-slot-status", "no-validating-as-loading", "no-placeholder-object", "no-hand-skeleton",
108
+ "no-swr-default-array", "no-foreign-slot-status", "no-validating-as-loading", "no-placeholder-object", "no-hand-skeleton", "explicit-export-surface", "directive-first",
109
109
  "button-intent-required", "destructive-confirm-in-dialog", "single-commit-per-render", "no-commit-in-collection-item",
110
110
  ]);
111
111
  /** The source of a plain front-end tree: its `src/`. */
@@ -39,14 +39,6 @@ export declare const isProductSource: (context: AnyContext) => boolean;
39
39
  export declare const isComponentFile: (context: AnyContext) => boolean;
40
40
  /** True when the given file (an absolute or repo-relative name, e.g. where an import resolves to) sits in a component layer of slot `fe.components` or `fe.package.ui`. */
41
41
  export declare const isComponentPath: (context: AnyContext, file: string) => boolean;
42
- /**
43
- * The one auth door of the session issuer's client: `modules/auth/auth-door.ts` (slot `fe.modules.auth`). It alone calls the app's
44
- * own back-end auth doors (login, signup, refresh, logout, password reset, resend verification) with the REST client, credentials
45
- * included; every other file of `modules/auth` and every other module stays outside the transport.
46
- */
47
- export declare const isAuthDoor: (context: AnyContext) => boolean;
48
- /** The in-memory access-token store of `modules/auth` (`modules/auth/token-store.ts`), the one module-level mutable state the auth client keeps. */
49
- export declare const isAuthTokenStore: (context: AnyContext) => boolean;
50
42
  /** The one place that may read the environment: slot `fe.modules.config`. */
51
43
  export declare const isConfigModule: (context: AnyContext) => boolean;
52
44
  /**
package/dist/lib/scope.js CHANGED
@@ -48,16 +48,6 @@ export const isComponentPath = (context, file) => {
48
48
  const found = hfsOf(context).classify(file);
49
49
  return (found.slot === "fe.components" || found.slot === "fe.package.ui") && found.kind !== undefined;
50
50
  };
51
- /** The base name of the linted file. */
52
- const baseNameOf = (context) => (context.filename || context.getFilename()).split(/[\\/]/).at(-1) ?? "";
53
- /**
54
- * The one auth door of the session issuer's client: `modules/auth/auth-door.ts` (slot `fe.modules.auth`). It alone calls the app's
55
- * own back-end auth doors (login, signup, refresh, logout, password reset, resend verification) with the REST client, credentials
56
- * included; every other file of `modules/auth` and every other module stays outside the transport.
57
- */
58
- export const isAuthDoor = (context) => inSlot(context, "fe.modules.auth") && baseNameOf(context) === "auth-door.ts";
59
- /** The in-memory access-token store of `modules/auth` (`modules/auth/token-store.ts`), the one module-level mutable state the auth client keeps. */
60
- export const isAuthTokenStore = (context) => inSlot(context, "fe.modules.auth") && baseNameOf(context) === "token-store.ts";
61
51
  /** The one place that may read the environment: slot `fe.modules.config`. */
62
52
  export const isConfigModule = (context) => inSlot(context, "fe.modules.config");
63
53
  /**
package/dist/lib/why.d.ts CHANGED
@@ -102,6 +102,16 @@ export declare const why: {
102
102
  en: string;
103
103
  fix: string;
104
104
  };
105
+ "directive-first": {
106
+ code: string;
107
+ en: string;
108
+ fix: string;
109
+ };
110
+ "explicit-export-surface": {
111
+ code: string;
112
+ en: string;
113
+ fix: string;
114
+ };
105
115
  "destructive-confirm-in-dialog": {
106
116
  code: string;
107
117
  en: string;
@@ -382,6 +392,11 @@ export declare const why: {
382
392
  en: string;
383
393
  fix: string;
384
394
  };
395
+ "swr-extension-from-package": {
396
+ code: string;
397
+ en: string;
398
+ fix: string;
399
+ };
385
400
  "fe-stack-import": {
386
401
  code: string;
387
402
  en: string;
package/dist/lib/why.js CHANGED
@@ -102,6 +102,16 @@ export const why = {
102
102
  en: "`<file>` draws a skeleton by hand.",
103
103
  fix: "Pass isLoading to the Grammar component and let it draw its loading structure; the block hands it the same-shape placeholder (FE-ERROR-1).",
104
104
  },
105
+ "directive-first": {
106
+ code: "FE_DIRECTIVE_FIRST",
107
+ en: "`<file>` has a `<what>` directive below another statement.",
108
+ fix: "Put the directive on the first line of the module, above every import; Next.js reads it only there.",
109
+ },
110
+ "explicit-export-surface": {
111
+ code: "HFS_EXPLICIT_EXPORT_SURFACE",
112
+ en: "`<file>` exports `<what>` with a type left to inference.",
113
+ fix: "Write the type where it is exported: the return type after the parameter list, the type of the exported variable, or the type of the parameter.",
114
+ },
105
115
  "destructive-confirm-in-dialog": {
106
116
  code: "FE_DESTRUCTIVE_CONFIRM_IN_DIALOG",
107
117
  en: "`<file>` renders a final destructive press outside its confirmation dialog.",
@@ -382,6 +392,11 @@ export const why = {
382
392
  en: "`<file>` starts a recurring timer that reads the wall clock for live time labels.",
383
393
  fix: "Render Grammar RelativeTime with the target dateTime, locale and mode; its shared ticker owns live relative time and countdown updates.",
384
394
  },
395
+ "swr-extension-from-package": {
396
+ code: "FE_STACK_FORBIDDEN",
397
+ en: "`<file>` imports the `Middleware` or `ScopedMutator` type of `swr`, so it writes an SWR middleware or a cache binding the stack already ships.",
398
+ fix: "List `viewerSwrConfig` (it carries `pollingRestart`) in the app's SWRConfig and write through the `createViewerCache()` instance of `@starci/next-data`.",
399
+ },
385
400
  "fe-stack-import": {
386
401
  code: "FE_STACK_FORBIDDEN",
387
402
  en: "`<file>` imports `<what>`, a package or scoped package family the front-end stack forbids.",
@@ -30,3 +30,17 @@ export declare const projectGraph: ({ repoRoot, injectedTypeScript }: {
30
30
  repoRoot: string;
31
31
  injectedTypeScript?: object;
32
32
  }) => ProjectGraphSnapshot;
33
+ /**
34
+ * The same findings from the graph this process has already built, so a process that linted the side hands them out without building the graph again.
35
+ *
36
+ * The rules read one graph per process and per side, built when the first file is linted. The ESLint child of `starci app check --gate lint` calls
37
+ * this after its run and writes the result beside its report; the result cache keeps the graph findings from it instead of building the graph a
38
+ * second time in the CLI. A process that has not built the graph yet builds it here, once, exactly as `projectGraph` does, and the value is the
39
+ * same: `projectGraph` is a fresh build, this is the build the rules used.
40
+ *
41
+ * @param input - The side folder (`repoRoot`, the folder of the side's `eslint.config.mjs`).
42
+ * @returns The findings of the rules of `recommended`, keyed by path relative to the side folder.
43
+ */
44
+ export declare const projectGraphSnapshot: ({ repoRoot }: {
45
+ repoRoot: string;
46
+ }) => ProjectGraphSnapshot;
@@ -30,7 +30,22 @@ export const recommended = Object.fromEntries(mine.map((e) => [`starci-fe/${e.id
30
30
  */
31
31
  export const projectGraph = ({ repoRoot, injectedTypeScript }) => {
32
32
  resetProjectGraphs();
33
- const graph = buildProjectGraph({ repoRoot, ...(injectedTypeScript === undefined ? {} : { injectedTypeScript }), runtimeRoot: RUNTIME });
33
+ return snapshotOf(buildProjectGraph({ repoRoot, ...(injectedTypeScript === undefined ? {} : { injectedTypeScript }), runtimeRoot: RUNTIME }));
34
+ };
35
+ /**
36
+ * The same findings from the graph this process has already built, so a process that linted the side hands them out without building the graph again.
37
+ *
38
+ * The rules read one graph per process and per side, built when the first file is linted. The ESLint child of `starci app check --gate lint` calls
39
+ * this after its run and writes the result beside its report; the result cache keeps the graph findings from it instead of building the graph a
40
+ * second time in the CLI. A process that has not built the graph yet builds it here, once, exactly as `projectGraph` does, and the value is the
41
+ * same: `projectGraph` is a fresh build, this is the build the rules used.
42
+ *
43
+ * @param input - The side folder (`repoRoot`, the folder of the side's `eslint.config.mjs`).
44
+ * @returns The findings of the rules of `recommended`, keyed by path relative to the side folder.
45
+ */
46
+ export const projectGraphSnapshot = ({ repoRoot }) => snapshotOf(buildProjectGraph({ repoRoot, runtimeRoot: RUNTIME }));
47
+ /** The findings of the rules of this canon in a built graph, by side-relative path, each list sorted by line, column, code and message. */
48
+ const snapshotOf = (graph) => {
34
49
  const byFile = {};
35
50
  for (const [file, findings] of graph.byFile) {
36
51
  for (const finding of findings) {
@@ -29,12 +29,17 @@ const rootOf = (input) => {
29
29
  }
30
30
  return null;
31
31
  };
32
- const enclosingFunction = (node) => {
32
+ /**
33
+ * The outermost function around a node: the hook itself. A fold inside a `useMemo` callback or a `slotOfReads` projector is judged by what the
34
+ * hook returns of the read's status and by the slot it builds, not by the callback alone, which holds neither.
35
+ */
36
+ const hookFunction = (node) => {
37
+ let found = null;
33
38
  for (let current = node.parent; current; current = current.parent) {
34
39
  if (current.type === "ArrowFunctionExpression" || current.type === "FunctionDeclaration" || current.type === "FunctionExpression")
35
- return current;
40
+ found = current;
36
41
  }
37
- return null;
42
+ return found;
38
43
  };
39
44
  const nameOfKey = (key) => (key.type === "Identifier" ? key.name : key.type === "Literal" && typeof key.value === "string" ? key.value : null);
40
45
  // -- no-swr-default-array -----------------------------------------------------------------------------------
@@ -79,7 +84,7 @@ const noSwrDefaultArray = defineRule({
79
84
  const root = rootOf(node.left);
80
85
  if (!root || !isHookResult(context, node, root))
81
86
  return;
82
- const fn = enclosingFunction(node);
87
+ const fn = hookFunction(node);
83
88
  if (!fn)
84
89
  return;
85
90
  const text = source.getText(fn);
@@ -19,6 +19,7 @@
19
19
  * standing in for a status.
20
20
  */
21
21
  import { type Recommended, type Rules } from "./lib/rule.js";
22
+ /** The HTTP methods of the REST client whose first argument is the request path. */
22
23
  /**
23
24
  * `fetch` is reached only in the API layer, each canonical transport library only in its home, and no other HTTP library exists.
24
25
  *
@@ -28,7 +29,7 @@ import { type Recommended, type Rules } from "./lib/rule.js";
28
29
  * `socket.io-client` only by `hooks/socketio/`. Native `WebSocket` is refused everywhere, including the API layer.
29
30
  * Every other file reaches the back end through an SWR hook over an operation.
30
31
  */
31
- export declare const fetchOnlyInApiClient: import("./lib/rule.js").Rule<"library" | "outside" | "channel" | "websocket" | "xhr" | "placed" | "authDoor", []>;
32
+ export declare const fetchOnlyInApiClient: import("./lib/rule.js").Rule<"library" | "outside" | "channel" | "websocket" | "xhr" | "placed", []>;
32
33
  /** Every `fetch` of the API layer (a custom fetch handed to the Apollo http link, a raw call) carries an abort signal, so a hung backend cannot hang a reader. */
33
34
  export declare const clientFetchHasSignal: import("./lib/rule.js").Rule<"signal", []>;
34
35
  /** No module-level mutable state in the API layer: a token or locale in a `let` leaks across requests. */
package/dist/transport.js CHANGED
@@ -23,7 +23,7 @@ import { isNode } from "./lib/ast.js";
23
23
  import { hfsOf } from "./lib/hfs.js";
24
24
  import { forbiddingConcern, packageNameOf } from "./lib/fe-stack.js";
25
25
  import { defineRule } from "./lib/rule.js";
26
- import { globalReferences, inSlot, isApiClient, isApiLayer, isApiRest, isAuthDoor, isAuthTokenStore, isOutcomeModule } from "./lib/scope.js";
26
+ import { globalReferences, inSlot, isApiClient, isApiLayer, isApiRest, isOutcomeModule } from "./lib/scope.js";
27
27
  import { typed } from "./lib/types.js";
28
28
  /**
29
29
  * The canonical transport libraries and the one place each may be imported (fe-stack graphql, rest and realtime): Apollo in
@@ -31,7 +31,7 @@ import { typed } from "./lib/types.js";
31
31
  */
32
32
  const PLACED_TRANSPORTS = new Map([
33
33
  ["@apollo/client", { where: "the API layer (`modules/api`)", allowed: (context) => isApiLayer(context) }],
34
- ["axios", { where: "the REST calls of the API layer (`modules/api/rest/`) and the auth door (`modules/auth/auth-door.ts`)", allowed: (context) => isApiRest(context) || isAuthDoor(context) }],
34
+ ["axios", { where: "the REST calls of the API layer (`modules/api/rest/`)", allowed: (context) => isApiRest(context) }],
35
35
  ["socket.io-client", { where: "`hooks/socketio/`", allowed: (context) => inSlot(context, "fe.hooks.socketio") }],
36
36
  ["graphql-ws", { where: "the Apollo clients of the API layer (`modules/api/graphql/clients/`, or the shared api package's `graphql/clients/`)", allowed: (context) => isApiClient(context) }],
37
37
  ]);
@@ -140,26 +140,6 @@ const transportUses = (context) => {
140
140
  };
141
141
  // -- FE-TRANSPORT-1 --------------------------------------------------------------------------------
142
142
  /** The HTTP methods of the REST client whose first argument is the request path. */
143
- const REST_METHODS = new Set(["get", "post", "put", "patch", "delete", "request"]);
144
- /** The static text of a path argument (a literal, or a template's fixed parts), or null when nothing about it is fixed. */
145
- const staticPath = (node) => {
146
- if (node?.type === "Literal" && typeof node.value === "string")
147
- return node.value;
148
- if (node?.type === "TemplateLiteral")
149
- return node.quasis.map((quasi) => quasi.value.cooked ?? "").join("<expression>");
150
- return null;
151
- };
152
- /** A path segment `auth/`: the app's own back-end auth doors. */
153
- const AUTH_DOOR_PATH = /(?:^|\/)auth\//;
154
- /** In the auth door, a REST call whose path the rule can read must name an `/auth/` door; a fully dynamic path is left to review. */
155
- const judgeAuthDoorCall = (context, node) => {
156
- if (node.callee.type !== "MemberExpression" || node.callee.computed || node.callee.property.type !== "Identifier" || !REST_METHODS.has(node.callee.property.name))
157
- return;
158
- const path = staticPath(node.arguments[0]);
159
- if (path === null || AUTH_DOOR_PATH.test(path))
160
- return;
161
- context.report({ node, messageId: "authDoor", data: { call: `.${node.callee.property.name}()`, path } });
162
- };
163
143
  /**
164
144
  * `fetch` is reached only in the API layer, each canonical transport library only in its home, and no other HTTP library exists.
165
145
  *
@@ -180,13 +160,11 @@ export const fetchOnlyInApiClient = defineRule({
180
160
  library: "`{{name}}` is a second HTTP transport. The front end talks to its back end through Apollo (`modules/api/graphql`) and the axios REST calls (`modules/api/rest`); another library is another set of timeout, retry and error rules. Use an operation of `modules/api`.",
181
161
  placed: "`{{name}}` is imported outside {{where}}, the one place the front-end stack gives it. A component or hook that holds the transport itself skips the link chain (token, locale, retry, timeout) and the one result vocabulary. Call an operation of `modules/api` through its SWR hook (`hooks/swr/`), or listen through `hooks/socketio/`.",
182
162
  xhr: "`XMLHttpRequest` is a second transport. Use an operation of the API layer (`modules/api`).",
183
- authDoor: "`{{call}}` calls `{{path}}` from the auth door. `modules/auth/auth-door.ts` calls only the app's own back-end auth doors (`/auth/...`: login, signup, refresh, logout, password reset, resend verification); every other REST call is `modules/api/rest/`.",
184
163
  websocket: "Native `WebSocket` is a second realtime transport. Use socket.io-client in `hooks/socketio/`, the front end's realtime owner.",
185
164
  },
186
165
  },
187
166
  create(context) {
188
167
  const layer = isApiLayer(context);
189
- const authDoor = isAuthDoor(context);
190
168
  const library = (node, source) => {
191
169
  const name = packageNameOf(source);
192
170
  if (name === null || forbiddingConcern(source))
@@ -231,8 +209,6 @@ export const fetchOnlyInApiClient = defineRule({
231
209
  library(node, node.source.value);
232
210
  },
233
211
  CallExpression(node) {
234
- if (authDoor)
235
- judgeAuthDoorCall(context, node);
236
212
  // `require("axios")` where `require` is the CommonJS global.
237
213
  const [first] = node.arguments;
238
214
  if (node.callee.type !== "Identifier" || node.callee.name !== "require" || first?.type !== "Literal")
@@ -290,14 +266,14 @@ const isTransportFile = (context) => isApiLayer(context) && !hfsOf(context).rela
290
266
  export const noSharedTransportState = defineRule({
291
267
  meta: {
292
268
  type: "problem",
293
- docs: { description: "The API layer (`modules/api/**`) and `modules/auth` hold no module-level `let`/`var`, but the in-memory access-token store `modules/auth/token-store.ts`." },
269
+ docs: { description: "The API layer (`modules/api/**`) and `modules/auth` hold no module-level `let`/`var`." },
294
270
  schema: [],
295
271
  messages: {
296
272
  shared: "A module-level `{{kind}}` in the API layer. A token or locale kept there is shared by every request on the server and by every reader in one tab, so one reader's credential can answer another's request. Pass the credential as a parameter or read it from context.",
297
273
  },
298
274
  },
299
275
  create(context) {
300
- if (!isTransportFile(context) && !(inSlot(context, "fe.modules.auth") && !isAuthTokenStore(context)))
276
+ if (!isTransportFile(context) && !inSlot(context, "fe.modules.auth"))
301
277
  return {};
302
278
  const check = (node) => {
303
279
  const declaration = node.type === "VariableDeclaration" ? node : node.type === "ExportNamedDeclaration" ? node.declaration : undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@starci/eslint-canon-fe",
3
- "version": "9.9.12",
3
+ "version": "9.11.0",
4
4
  "description": "Universal front-end lint laws for TypeScript and Next.js, independent of the selected Grammar family.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -125,6 +125,7 @@
125
125
  "scripts/hfs/declaration-shape.mjs",
126
126
  "scripts/hfs/declaration-slots.mjs",
127
127
  "scripts/hfs/edition-slots.mjs",
128
+ "scripts/hfs/explicit-surface-rule.mjs",
128
129
  "scripts/hfs/linear-text.mjs",
129
130
  "scripts/hfs/param-names.mjs",
130
131
  "scripts/hfs/path-findings.mjs",
@@ -150,6 +151,7 @@
150
151
  "scripts/hfs/trailing-slashes.mjs",
151
152
  "scripts/hfs/typescript-programs.mjs",
152
153
  "scripts/hfs/view.mjs",
154
+ "scripts/lib/declaration-surface.mjs",
153
155
  "scripts/lib/dockerfile.mjs",
154
156
  "scripts/lib/env.mjs",
155
157
  "scripts/lib/event-contract.mjs",