@starci/eslint-canon-fe 9.9.12 → 9.10.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 +8 -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/index.d.ts +1 -1
- package/dist/index.js +5 -1
- package/dist/lib/config.js +1 -1
- package/dist/lib/why.d.ts +10 -0
- package/dist/lib/why.js +10 -0
- package/dist/project-graph.d.ts +14 -0
- package/dist/project-graph.js +16 -1
- package/dist/skeleton-states.js +9 -4
- package/package.json +1 -1
- package/runtime/bundle.json +2 -0
- package/runtime/knowledge/hfs/canon-pins.yaml +7 -7
- package/runtime/scripts/hfs/architecture/test-world-files.mjs +26 -7
- package/runtime/scripts/hfs/explicit-surface-rule.mjs +59 -0
- package/runtime/scripts/lib/declaration-surface.mjs +112 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 9.10.0
|
|
4
|
+
|
|
5
|
+
- 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.
|
|
6
|
+
- 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.
|
|
7
|
+
- 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.
|
|
8
|
+
- Changed: carry the bundled runtime of @starci/hfs 5.1.0 (the classifier, the rule factory, the rule catalog with R368, the canon pins).
|
|
9
|
+
- 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.
|
|
10
|
+
|
|
3
11
|
## 9.9.12
|
|
4
12
|
|
|
5
13
|
- 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/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/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;
|
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.",
|
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/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",
|
|
@@ -31,35 +31,35 @@ pins:
|
|
|
31
31
|
source: packages/event-stream/package.json
|
|
32
32
|
why: 'the one server-sent-events client of every front end (fe-stack event-stream, owner:event-stream-package): the transport client layer (modules/api/sse/clients) creates one instance for the app with the bearer token source and the stream operations follow on it; it carries Last-Event-ID resume, the capped doubling backoff, the heartbeat timeout, the single 401 renewal and the useSWRSubscription adapter, so no app keeps a hand-written reader.'
|
|
33
33
|
'@starci/grammar':
|
|
34
|
-
version: 0.21.
|
|
34
|
+
version: 0.21.13
|
|
35
35
|
group: starci
|
|
36
36
|
install: registry
|
|
37
37
|
side: fe
|
|
38
38
|
source: packages/grammar/package.json
|
|
39
39
|
why: 'the grammar front ends read: the brand layer sets `--font-sans` and `--font-mono` and the grammar reads them; it carries the Input tel kind, the copyable and read-only Input, the IconButton disclosure props and count badge, ChatBubble, FillViewport, Textarea mentions and Enter-to-send, the leading PrimaryRailLayout rail, the plate-less Image, ListRow, the soft Alert, Progress steps, the floating primary IconButton, FlowGraph, the STATE-8 loading views and the bordered selectable TagGroup chips. The runtime source version wins for a @starci package.'
|
|
40
40
|
'@starci/eslint-canon-be':
|
|
41
|
-
version: 3.
|
|
41
|
+
version: 3.12.0
|
|
42
42
|
group: starci
|
|
43
43
|
install: registry
|
|
44
44
|
side: be
|
|
45
45
|
source: packages/eslint-canon-be/package.json
|
|
46
46
|
why: 'the back-end ESLint canon: `loadHfs(import.meta.url)` of be/eslint.config.mjs finds the app-root hfs.json (kind app) and gives every linted file the view of the be side, and the project graph is built per side; its bundled runtime copies (slots, canon-pins, failure codes, the architecture machine) put .starcistacks and .sops.yaml at the app root; `starciBeConfig({ hfs: loadHfs(import.meta.url) })` is the typed factory and the BE-CONVENTION laws are its rules.'
|
|
47
47
|
'@starci/eslint-canon-fe':
|
|
48
|
-
version: 9.
|
|
48
|
+
version: 9.10.0
|
|
49
49
|
group: starci
|
|
50
50
|
install: registry
|
|
51
51
|
side: fe
|
|
52
52
|
source: packages/eslint-canon-fe/package.json
|
|
53
53
|
why: 'the front-end ESLint canon: `loadHfs(import.meta.url)` of fe/eslint.config.mjs finds the app-root hfs.json (kind app) and gives every linted file the view of the fe side, and the project graph is built per side; its bundled runtime copies (slots, canon-pins, failure codes, the architecture machine) put .starcistacks and .sops.yaml at the app root; the front end has no tests (FE_NO_TESTS R97 in hfs) and no-vietnamese-in-source (R91) applies.'
|
|
54
54
|
'@starci/eslint-grammar-core':
|
|
55
|
-
version: 0.21.
|
|
55
|
+
version: 0.21.13
|
|
56
56
|
group: starci
|
|
57
57
|
install: registry
|
|
58
58
|
side: fe
|
|
59
59
|
source: packages/eslint-grammar-core/package.json
|
|
60
60
|
why: 'the StarCi Grammar component API lint, versioned with @starci/grammar; front ends compose this family package with the universal FE canon.'
|
|
61
61
|
'@starci/eslint-grammar-offset-pop':
|
|
62
|
-
version: 0.21.
|
|
62
|
+
version: 0.21.13
|
|
63
63
|
group: starci
|
|
64
64
|
install: registry
|
|
65
65
|
side: fe
|
|
@@ -92,7 +92,7 @@ pins:
|
|
|
92
92
|
source: packages/jest-preset/package.json
|
|
93
93
|
why: 'the Jest preset of a back end: the world runner runs up to min(--maxWorkers, slots) world files at once, each in its own process bound to one data slot of the test world (STARCI_TEST_WORLD_SLOT), and reads test-world state protocol 2 only: it pairs exactly with @starci/test-world, and any other state file is JEST_PRESET_WORLD_PAIR_MISMATCH. The unit run writes the lcov Sonar and Codecov import (services only); the integration, e2e and contract projects run every spec file in a worker process of its own (world-runner.cjs), so a process-global framework registry (@nestjs/graphql type metadata) never leaks between e2e files; it ships the unit kit (mockEntityManager, fakeTransaction, fakeQueryBuilder, queueQueryBuilders, fakeIds, FakeClock, Outcome matchers) and the recordingOutbox claim side.'
|
|
94
94
|
'@starci/test-world':
|
|
95
|
-
version: 1.3.
|
|
95
|
+
version: 1.3.4
|
|
96
96
|
group: starci
|
|
97
97
|
install: registry
|
|
98
98
|
side: be
|
|
@@ -106,7 +106,7 @@ pins:
|
|
|
106
106
|
source: packages/ai-router/package.json
|
|
107
107
|
why: 'the AI pool router of a back end: a NestJS module that routes OpenAI-compatible chat, stream and embeddings calls across keys, models and providers, with @starci/ai-router/testing for its specs; @starci/jest-preset compiles it as an ESM-only dependency.'
|
|
108
108
|
'@starci/hfs':
|
|
109
|
-
version: 5.0
|
|
109
|
+
version: 5.1.0
|
|
110
110
|
group: starci
|
|
111
111
|
install: registry
|
|
112
112
|
side: both
|
|
@@ -15,9 +15,10 @@ import { byCodeUnit } from '../../lib/list.mjs';
|
|
|
15
15
|
* its own slot, be.tests.world.kit) plus files at its root whose role suffix is in ruleParams.be.suffixes
|
|
16
16
|
* (`stripe.client.ts`, `checkout.contracts.ts`, `test-world.config.ts`, ...). Every tracked file below the world root is
|
|
17
17
|
* matched by the slot's own entries through `allowsFile`.
|
|
18
|
-
* 2. The declaration. `test-world.config.ts` declares the world in
|
|
19
|
-
* `export const { useTestWorld, useSandbox } = defineTestWorld({ ... })
|
|
20
|
-
* `
|
|
18
|
+
* 2. The declaration. `test-world.config.ts` declares the world in a named form R89 allows: the destructured export
|
|
19
|
+
* `export const { useTestWorld, useSandbox } = defineTestWorld({ ... })`, or the typed form
|
|
20
|
+
* `const world: TestWorldApi = defineTestWorld({ ... })` whose `useTestWorld` and `useSandbox` are exported consts typed from
|
|
21
|
+
* `TestWorldApi` (a `const` initialized by a `defineTestWorld(<object literal>)` call). A config without it (a default export, or a declaration the machine cannot
|
|
21
22
|
* read) is refused: the world it declares could not be judged.
|
|
22
23
|
* 3. Fakes against the stack (owner refinement 2026-09-30). Every service the stack declares runs real in the world; a fake
|
|
23
24
|
* (a `fakes/<provider>/` folder, or a `fakes` entry of the declaration such as `smtp: smtpFake()`) is a network-edge fake
|
|
@@ -38,6 +39,7 @@ const FAKES_DIRECTORY = 'fakes/';
|
|
|
38
39
|
const CONFIG_FILE = 'test-world.config.ts';
|
|
39
40
|
const DEFINE = 'defineTestWorld';
|
|
40
41
|
const NAMED_FORM = `export const { useTestWorld, useSandbox } = ${DEFINE}({ ... })`;
|
|
42
|
+
const TYPED_FORM = `const world: TestWorldApi = ${DEFINE}({ ... })` + ' with export const useTestWorld / useSandbox typed from it';
|
|
41
43
|
const defaultEnvironment = DEFAULT_ENVIRONMENT;
|
|
42
44
|
const KEBAB = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
43
45
|
const unwrap = (ts, node) => unwrapEach(ts, node, [ts.isParenthesizedExpression, ts.isAsExpression, ts.isSatisfiesExpression]);
|
|
@@ -52,21 +54,38 @@ function defineLiteral(ts, expression) {
|
|
|
52
54
|
const literal = first ? unwrap(ts, first) : null;
|
|
53
55
|
return literal && ts.isObjectLiteralExpression(literal) ? literal : null;
|
|
54
56
|
}
|
|
55
|
-
/**
|
|
57
|
+
/**
|
|
58
|
+
* The declaration of the world: the object literal of a top-level `const` initialized by `defineTestWorld({ ... })`, or null. The config
|
|
59
|
+
* exports `useTestWorld` and `useSandbox` either by destructuring that call (the original form) or, typed, from the world the call
|
|
60
|
+
* built: `const world: TestWorldApi = defineTestWorld({ ... })` with `export const useTestWorld: TestWorldApi["useTestWorld"] = world.useTestWorld`.
|
|
61
|
+
* Both are read; a world no exported name reaches is refused by the caller.
|
|
62
|
+
*/
|
|
56
63
|
function configLiteral(ts, sourceFile) {
|
|
57
64
|
for (const statement of sourceFile.statements) {
|
|
58
|
-
if (!ts.isVariableStatement(statement)
|
|
65
|
+
if (!ts.isVariableStatement(statement))
|
|
59
66
|
continue;
|
|
60
67
|
if (!hasAnyFlag(statement.declarationList.flags, ts.NodeFlags.Const))
|
|
61
68
|
continue;
|
|
62
69
|
for (const declaration of statement.declarationList.declarations) {
|
|
63
70
|
const literal = declaration.initializer ? defineLiteral(ts, declaration.initializer) : null;
|
|
64
|
-
if (literal)
|
|
71
|
+
if (literal && (isExported(ts, statement) || exportsWorld(ts, sourceFile)))
|
|
65
72
|
return literal;
|
|
66
73
|
}
|
|
67
74
|
}
|
|
68
75
|
return null;
|
|
69
76
|
}
|
|
77
|
+
/** Whether the file exports both `useTestWorld` and `useSandbox` as consts (the typed form re-exports them from the built world). */
|
|
78
|
+
function exportsWorld(ts, sourceFile) {
|
|
79
|
+
const exported = new Set();
|
|
80
|
+
for (const statement of sourceFile.statements) {
|
|
81
|
+
if (!ts.isVariableStatement(statement) || !isExported(ts, statement))
|
|
82
|
+
continue;
|
|
83
|
+
for (const declaration of statement.declarationList.declarations)
|
|
84
|
+
if (ts.isIdentifier(declaration.name))
|
|
85
|
+
exported.add(declaration.name.text);
|
|
86
|
+
}
|
|
87
|
+
return exported.has('useTestWorld') && exported.has('useSandbox');
|
|
88
|
+
}
|
|
70
89
|
/** `stack` as the environment it names: the last segment of `".starcistacks/<env>"`; null when absent or not a literal. */
|
|
71
90
|
function environmentOf(ts, literal) {
|
|
72
91
|
const node = literal ? propertyOf(ts, literal, 'stack') : null;
|
|
@@ -152,7 +171,7 @@ function worldDeclaration(graph, ts, worldRoot, violations) {
|
|
|
152
171
|
if (configFile && !literal) {
|
|
153
172
|
violations.push({
|
|
154
173
|
ruleId: RULE, path: configPath, line: 1, column: 1, slot: WORLD_SLOT,
|
|
155
|
-
message: `${configPath} declares no world the machine can read. Declare it in the
|
|
174
|
+
message: `${configPath} declares no world the machine can read. Declare it in the named form \`${NAMED_FORM}\` or the typed form \`${TYPED_FORM}\` (a const of a ${DEFINE} call with an object literal, imported from @starci/test-world); a default export is refused by R89 and is not read.`,
|
|
156
175
|
});
|
|
157
176
|
}
|
|
158
177
|
return { configPath, configFile, literal };
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { declarationSurface, isEmittableSource } from '../lib/declaration-surface.mjs';
|
|
2
|
+
/** What to write, by what the classifier says to annotate. */
|
|
3
|
+
const HINTS = {
|
|
4
|
+
'return type': 'Write the return type after the parameter list: `(): Result =>` or `function name(): Result`.',
|
|
5
|
+
'variable type': 'Give the variable a type: `export const name: Result = ...`.',
|
|
6
|
+
'parameter type': 'Give the parameter a type: `(value: Input)`.',
|
|
7
|
+
'property type': 'Give the property a type: `name: Result = ...`.',
|
|
8
|
+
'accessor type': 'Give the accessor a type: `get name(): Result` (a setter takes it from its parameter).',
|
|
9
|
+
'destructured export': 'A destructuring pattern cannot carry a type per name: export each name as its own variable with a type, `export const name: Type = source.name`.',
|
|
10
|
+
'exported value': 'Declare the type (an interface or a type alias) and annotate the export with it: `export const name: Shape = ...`.',
|
|
11
|
+
};
|
|
12
|
+
/** The `--isolatedDeclarations` flag name TypeScript ends its reasons with says nothing to the reader. */
|
|
13
|
+
const plain = (reason) => reason.replace(/ with --isolatedDeclarations\.?/, '').replace(/\.$/, '');
|
|
14
|
+
/**
|
|
15
|
+
* The rule. `ts` is the canon's own TypeScript (its peer dependency); the side's `module` option comes from the typed program when the file is linted with one.
|
|
16
|
+
*
|
|
17
|
+
* @param input - `ts` is the compiler.
|
|
18
|
+
* @returns An ESLint rule reporting each export of a TypeScript source whose type is inferred.
|
|
19
|
+
*/
|
|
20
|
+
export const explicitSurfaceRule = ({ ts }) => ({
|
|
21
|
+
meta: {
|
|
22
|
+
type: 'problem',
|
|
23
|
+
docs: { description: 'An exported declaration states its type: the return type, variable type or parameter type is written, not inferred from a body or from other files.' },
|
|
24
|
+
schema: [],
|
|
25
|
+
messages: {
|
|
26
|
+
inferred: 'HFS_EXPLICIT_EXPORT_SURFACE: `{{name}}` is exported with an inferred type ({{reason}}), so importers cannot read it from this file and every one of them is linted again whenever the file changes. Annotate the {{annotate}}. {{hint}}',
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
create(context) {
|
|
30
|
+
return {
|
|
31
|
+
Program() {
|
|
32
|
+
const file = context.filename;
|
|
33
|
+
if (!isEmittableSource(file))
|
|
34
|
+
return;
|
|
35
|
+
const { sourceCode } = context;
|
|
36
|
+
const module = sourceCode.parserServices?.program?.getCompilerOptions().module;
|
|
37
|
+
const whole = { start: { line: 1, column: 0 }, end: { line: 1, column: 0 } };
|
|
38
|
+
let surface;
|
|
39
|
+
try {
|
|
40
|
+
surface = declarationSurface(ts, file, sourceCode.text, module);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
surface = null;
|
|
44
|
+
}
|
|
45
|
+
if (surface === null) {
|
|
46
|
+
context.report({ loc: whole, messageId: 'inferred', data: { name: 'this file', reason: 'the declaration emit could not read it', annotate: 'exported value', hint: HINTS['exported value'] } });
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
for (const problem of surface.problems) {
|
|
50
|
+
context.report({
|
|
51
|
+
loc: { start: sourceCode.getLocFromIndex(problem.start), end: sourceCode.getLocFromIndex(problem.start + problem.length) },
|
|
52
|
+
messageId: 'inferred',
|
|
53
|
+
data: { name: problem.name, reason: plain(problem.reason), annotate: problem.annotate, hint: HINTS[problem.annotate] },
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
},
|
|
59
|
+
});
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/** A script file (`.js`, `.mjs`, `.cjs`, `.jsx`): it has no declaration emit and stands for its content. */
|
|
2
|
+
export const SCRIPT = /\.(?:[cm]?jsx?)$/;
|
|
3
|
+
/** A declaration file: it is its own surface. */
|
|
4
|
+
export const DECLARATION = /\.d\.[cm]?ts$/;
|
|
5
|
+
/** A TypeScript source file whose surface the declaration emit writes. */
|
|
6
|
+
export const TYPESCRIPT = /\.(?:[cm]?tsx?)$/;
|
|
7
|
+
/** Whether `file` is a TypeScript source (not a declaration file) the declaration emit reads. */
|
|
8
|
+
export const isEmittableSource = (file) => TYPESCRIPT.test(file) && !DECLARATION.test(file);
|
|
9
|
+
/** Whether `expression` is a JSX element, fragment, `null` or `undefined`, or a conditional of those: its type does not depend on any other file. */
|
|
10
|
+
function isJsxValue(ts, expression) {
|
|
11
|
+
if (expression === undefined)
|
|
12
|
+
return false;
|
|
13
|
+
let node = expression;
|
|
14
|
+
while (ts.isParenthesizedExpression(node))
|
|
15
|
+
node = node.expression;
|
|
16
|
+
if (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node) || ts.isJsxFragment(node) || node.kind === ts.SyntaxKind.NullKeyword)
|
|
17
|
+
return true;
|
|
18
|
+
if (ts.isIdentifier(node))
|
|
19
|
+
return node.text === 'undefined';
|
|
20
|
+
return ts.isConditionalExpression(node) && isJsxValue(ts, node.whenTrue) && isJsxValue(ts, node.whenFalse);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Whether the function (or method) the declaration diagnostic at `start` points at has the type `JSX.Element | null | undefined` whatever its body
|
|
24
|
+
* reads: not async, not a generator, and every `return` of its own (and its concise body) a JSX value. A React component written without a return
|
|
25
|
+
* type is the common case; its inferred type is one the JSX namespace of the installed packages gives, which a lockfile change dirties as a whole.
|
|
26
|
+
*/
|
|
27
|
+
function returnsOnlyJsx(ts, source, start) {
|
|
28
|
+
let found;
|
|
29
|
+
const find = (node) => {
|
|
30
|
+
if (found !== undefined || start < node.getFullStart() || start >= node.end)
|
|
31
|
+
return;
|
|
32
|
+
const named = (ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node)) && node.name !== undefined && node.name.getStart(source) === start;
|
|
33
|
+
if (named || ((ts.isArrowFunction(node) || ts.isFunctionExpression(node)) && node.getStart(source) === start)) {
|
|
34
|
+
found = node;
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
ts.forEachChild(node, find);
|
|
38
|
+
};
|
|
39
|
+
find(source);
|
|
40
|
+
const fn = found;
|
|
41
|
+
if (fn === undefined || fn.type !== undefined || fn.body === undefined)
|
|
42
|
+
return false;
|
|
43
|
+
if (fn.asteriskToken !== undefined || (ts.getCombinedModifierFlags(fn) & ts.ModifierFlags.Async) !== 0)
|
|
44
|
+
return false;
|
|
45
|
+
if (!ts.isBlock(fn.body))
|
|
46
|
+
return isJsxValue(ts, fn.body);
|
|
47
|
+
let only = true;
|
|
48
|
+
const visit = (node) => {
|
|
49
|
+
if (!only || ts.isFunctionLike(node) || ts.isClassLike(node))
|
|
50
|
+
return;
|
|
51
|
+
if (ts.isReturnStatement(node) && node.expression !== undefined && !isJsxValue(ts, node.expression))
|
|
52
|
+
only = false;
|
|
53
|
+
ts.forEachChild(node, visit);
|
|
54
|
+
};
|
|
55
|
+
ts.forEachChild(fn.body, visit);
|
|
56
|
+
return only;
|
|
57
|
+
}
|
|
58
|
+
/** What to annotate for each isolated-declaration diagnostic code; any other code asks for the exported value's type. */
|
|
59
|
+
const ANNOTATE = new Map([
|
|
60
|
+
[9007, 'return type'], [9008, 'return type'], [9009, 'accessor type'], [9010, 'variable type'], [9011, 'parameter type'], [9012, 'property type'],
|
|
61
|
+
[9019, 'destructured export'], [9025, 'parameter type'],
|
|
62
|
+
]);
|
|
63
|
+
/** The declaration kinds whose name labels the export a diagnostic sits in, innermost first. */
|
|
64
|
+
function nameOf(ts, source, start) {
|
|
65
|
+
let innermost = source;
|
|
66
|
+
const descend = (node) => {
|
|
67
|
+
if (start < node.getFullStart() || start >= node.end)
|
|
68
|
+
return;
|
|
69
|
+
innermost = node;
|
|
70
|
+
ts.forEachChild(node, descend);
|
|
71
|
+
};
|
|
72
|
+
ts.forEachChild(source, descend);
|
|
73
|
+
for (let node = innermost; node !== undefined && node !== source; node = node.parent) {
|
|
74
|
+
if (ts.isExportAssignment(node))
|
|
75
|
+
return 'default export';
|
|
76
|
+
if (ts.isBindingElement(node) && ts.isIdentifier(node.name))
|
|
77
|
+
return node.name.text;
|
|
78
|
+
if ((ts.isVariableDeclaration(node) || ts.isFunctionDeclaration(node) || ts.isClassDeclaration(node) || ts.isMethodDeclaration(node) || ts.isPropertyDeclaration(node) || ts.isPropertyAssignment(node) || ts.isGetAccessorDeclaration(node)) && node.name !== undefined && ts.isIdentifier(node.name))
|
|
79
|
+
return node.name.text;
|
|
80
|
+
if (ts.isFunctionDeclaration(node) && (ts.getCombinedModifierFlags(node) & ts.ModifierFlags.Default) !== 0)
|
|
81
|
+
return 'default export';
|
|
82
|
+
}
|
|
83
|
+
return 'default export';
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* The declaration emit of one TypeScript source and the exports it could not write explicitly (a type inferred from bodies and other files), or null
|
|
87
|
+
* when the compiler has no `transpileDeclaration` (before TypeScript 5.5). A declaration whose only diagnostics are components returning JSX has the
|
|
88
|
+
* surface its emit shows. `module` is the side's `module` compiler option, which the emit reads. A throw from the compiler propagates: the caller
|
|
89
|
+
* decides what an unreadable file means.
|
|
90
|
+
*/
|
|
91
|
+
export function declarationSurface(ts, file, text, module) {
|
|
92
|
+
const emit = ts.transpileDeclaration;
|
|
93
|
+
if (emit === undefined)
|
|
94
|
+
return null;
|
|
95
|
+
const declared = emit.call(ts, text, { compilerOptions: { jsx: ts.JsxEmit.Preserve, target: ts.ScriptTarget.Latest, ...(module === undefined ? {} : { module }) }, fileName: file, reportDiagnostics: true });
|
|
96
|
+
const diagnostics = (declared.diagnostics ?? []);
|
|
97
|
+
if (diagnostics.length === 0)
|
|
98
|
+
return { outputText: declared.outputText, problems: [] };
|
|
99
|
+
const source = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, file.endsWith('x') ? ts.ScriptKind.TSX : ts.ScriptKind.TS);
|
|
100
|
+
const problems = [];
|
|
101
|
+
for (const diagnostic of diagnostics) {
|
|
102
|
+
const exempt = (diagnostic.code === 9007 || diagnostic.code === 9008) && diagnostic.start !== undefined && returnsOnlyJsx(ts, source, diagnostic.start);
|
|
103
|
+
if (exempt)
|
|
104
|
+
continue;
|
|
105
|
+
const start = diagnostic.start ?? 0;
|
|
106
|
+
problems.push({
|
|
107
|
+
code: diagnostic.code, start, length: diagnostic.length ?? 0, name: nameOf(ts, source, start), annotate: ANNOTATE.get(diagnostic.code) ?? 'exported value',
|
|
108
|
+
reason: ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n').split('\n')[0] ?? '',
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
return { outputText: declared.outputText, problems };
|
|
112
|
+
}
|