@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 +12 -0
- package/README.md +10 -0
- package/dist/directive-first.d.ts +16 -0
- package/dist/directive-first.js +37 -0
- package/dist/explicit-surface.d.ts +7 -0
- package/dist/explicit-surface.js +19 -0
- package/dist/fe-stack.d.ts +5 -0
- package/dist/fe-stack.js +34 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +5 -1
- package/dist/lib/config.js +1 -1
- package/dist/lib/scope.d.ts +0 -8
- package/dist/lib/scope.js +0 -10
- package/dist/lib/why.d.ts +15 -0
- package/dist/lib/why.js +15 -0
- package/dist/project-graph.d.ts +14 -0
- package/dist/project-graph.js +16 -1
- package/dist/skeleton-states.js +9 -4
- package/dist/transport.d.ts +2 -1
- package/dist/transport.js +4 -28
- package/package.json +1 -1
- package/runtime/bundle.json +2 -0
- package/runtime/knowledge/hfs/canon-pins.yaml +31 -10
- package/runtime/knowledge/hfs/fe-stack.yaml +7 -7
- package/runtime/knowledge/hfs/slots.yaml +71 -22
- package/runtime/scripts/hfs/architecture/identity.mjs +18 -83
- package/runtime/scripts/hfs/architecture/test-world-files.mjs +26 -7
- package/runtime/scripts/hfs/architecture/transport-owner.mjs +3 -6
- package/runtime/scripts/hfs/explicit-surface-rule.mjs +59 -0
- package/runtime/scripts/hfs/manifest-shape.mjs +1 -1
- package/runtime/scripts/hfs/slots.mjs +1 -1
- package/runtime/scripts/lib/declaration-surface.mjs +112 -0
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
|
+
};
|
package/dist/fe-stack.d.ts
CHANGED
|
@@ -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";
|
package/dist/lib/config.js
CHANGED
|
@@ -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/`. */
|
package/dist/lib/scope.d.ts
CHANGED
|
@@ -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.",
|
package/dist/project-graph.d.ts
CHANGED
|
@@ -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;
|
package/dist/project-graph.js
CHANGED
|
@@ -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
|
-
|
|
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) {
|
package/dist/skeleton-states.js
CHANGED
|
@@ -29,12 +29,17 @@ const rootOf = (input) => {
|
|
|
29
29
|
}
|
|
30
30
|
return null;
|
|
31
31
|
};
|
|
32
|
-
|
|
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
|
-
|
|
40
|
+
found = current;
|
|
36
41
|
}
|
|
37
|
-
return
|
|
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 =
|
|
87
|
+
const fn = hookFunction(node);
|
|
83
88
|
if (!fn)
|
|
84
89
|
return;
|
|
85
90
|
const text = source.getText(fn);
|
package/dist/transport.d.ts
CHANGED
|
@@ -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"
|
|
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,
|
|
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/`)
|
|
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
|
|
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) && !
|
|
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
package/runtime/bundle.json
CHANGED
|
@@ -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",
|