@starci/eslint-canon-fe 8.0.7 → 8.0.9
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 +5 -4
- package/client-boundary.mjs +4 -2
- package/comments.mjs +1 -1
- package/docs/grammar-boundary.md +1 -1
- package/docs/next-conventions.md +1 -1
- package/docs/translation.md +1 -1
- package/docs/transport.md +2 -2
- package/{shape-slot.mjs → drawing.mjs} +5 -5
- package/file-layout.mjs +11 -7
- package/grammar-boundary.mjs +4 -4
- package/icon.mjs +1 -23
- package/index.mjs +2 -2
- package/lib/ast.mjs +14 -11
- package/lib/config.mjs +1 -1
- package/lib/hfs.mjs +3 -3
- package/lib/next.mjs +1 -1
- package/lib/scope.mjs +6 -2
- package/lib/why.mjs +113 -110
- package/naming.mjs +4 -4
- package/package.json +5 -3
- package/project-graph.mjs +4 -4
- package/props-and-slots.mjs +4 -3
- package/runtime/engine/config.mjs +494 -0
- package/runtime/engine/invalid-config.mjs +15 -0
- package/runtime/engine/orca-config.mjs +27 -0
- package/runtime/engine/runtime-root.mjs +5 -0
- package/runtime/engine/yaml.mjs +3 -3
- package/runtime/knowledge/hfs/canon-pins.yaml +163 -32
- package/runtime/knowledge/hfs/slots.yaml +915 -123
- package/runtime/knowledge/patterns/fe/folder.yaml +133 -27
- package/runtime/knowledge/sonar-gate.yaml +53 -19
- package/runtime/modules/models/registry.yaml +591 -0
- package/runtime/modules/models/runtimes.yaml +701 -0
- package/runtime/packages/hfs/templates/app/package-scripts/package.json +3 -10
- package/runtime/scripts/api/git/config-get.mjs +10 -0
- package/runtime/scripts/api/git/lib.mjs +17 -27
- package/runtime/scripts/api/git/ls-files.mjs +6 -0
- package/runtime/scripts/api/git/recorded-lines.mjs +19 -0
- package/runtime/scripts/api/git/remote.mjs +5 -0
- package/runtime/scripts/api/git/rev-parse-query.mjs +5 -0
- package/runtime/scripts/{lib/hfs-allows.mjs → hfs/allows.mjs} +3 -3
- package/runtime/scripts/hfs/architecture/ast-walks.mjs +220 -0
- package/runtime/scripts/{checks → hfs}/architecture/backend.mjs +17 -24
- package/runtime/scripts/{checks → hfs}/architecture/background-unowned.mjs +16 -34
- package/runtime/scripts/{checks → hfs}/architecture/client-reaches-server.mjs +3 -1
- package/runtime/scripts/{checks → hfs}/architecture/clones.mjs +4 -4
- package/runtime/scripts/{checks → hfs}/architecture/config.mjs +11 -8
- package/runtime/scripts/{checks → hfs}/architecture/connection-map.mjs +21 -9
- package/runtime/scripts/hfs/architecture/context-coupling.mjs +88 -0
- package/runtime/scripts/hfs/architecture/context-map.mjs +237 -0
- package/runtime/scripts/hfs/architecture/context-owner.mjs +82 -0
- package/runtime/scripts/hfs/architecture/context-platform-tables.mjs +47 -0
- package/runtime/scripts/hfs/architecture/context-transaction.mjs +60 -0
- package/runtime/scripts/{checks → hfs}/architecture/contract-fixture-guard.mjs +5 -11
- package/runtime/scripts/{checks → hfs}/architecture/contracts.mjs +33 -162
- package/runtime/scripts/{checks → hfs}/architecture/dead-exports.mjs +58 -29
- package/runtime/scripts/{checks → hfs}/architecture/doc-language.mjs +1 -1
- package/runtime/scripts/{checks → hfs}/architecture/entrypoint.mjs +2 -1
- package/runtime/scripts/{checks → hfs}/architecture/fe-slot-allows.mjs +4 -4
- package/runtime/scripts/{checks → hfs}/architecture/feature-shape.mjs +6 -3
- package/runtime/scripts/{checks → hfs}/architecture/framework-pinned.mjs +1 -1
- package/runtime/scripts/{checks → hfs}/architecture/frontend.mjs +17 -43
- package/runtime/scripts/{checks → hfs}/architecture/hfs-graph.mjs +1 -1
- package/runtime/scripts/{checks → hfs}/architecture/hfs.mjs +27 -42
- package/runtime/scripts/{checks → hfs}/architecture/i18n-keys.mjs +1 -1
- package/runtime/scripts/{checks → hfs}/architecture/index.mjs +17 -4
- package/runtime/scripts/{checks → hfs}/architecture/machine-ast.mjs +44 -11
- package/runtime/scripts/hfs/architecture/managed-scripts.mjs +27 -0
- package/runtime/scripts/{checks → hfs}/architecture/module-per-transport.mjs +30 -6
- package/runtime/scripts/hfs/architecture/next-data-contract.mjs +96 -0
- package/runtime/scripts/{checks → hfs}/architecture/next-data.mjs +40 -168
- package/runtime/scripts/{checks → hfs}/architecture/owners.mjs +35 -15
- package/runtime/scripts/{checks → hfs}/architecture/register-once.mjs +2 -1
- package/runtime/scripts/{checks → hfs}/architecture/registration.mjs +23 -81
- package/runtime/scripts/{checks → hfs}/architecture/required-files.mjs +21 -13
- package/runtime/scripts/{checks → hfs}/architecture/schema-owner.mjs +17 -114
- package/runtime/scripts/hfs/architecture/server-action.mjs +24 -0
- package/runtime/scripts/hfs/architecture/side-boundary.mjs +16 -0
- package/runtime/scripts/{checks → hfs}/architecture/source-names.mjs +27 -147
- package/runtime/scripts/{checks → hfs}/architecture/sql-owner.mjs +59 -18
- package/runtime/scripts/hfs/architecture/sql-returning.mjs +64 -0
- package/runtime/scripts/hfs/architecture/supabase-ast.mjs +111 -0
- package/runtime/scripts/hfs/architecture/supabase-be.mjs +131 -0
- package/runtime/scripts/hfs/architecture/supabase-tables.mjs +42 -0
- package/runtime/scripts/hfs/architecture/supabase.mjs +434 -0
- package/runtime/scripts/{checks → hfs}/architecture/surface.mjs +10 -4
- package/runtime/scripts/{checks → hfs}/architecture/test-world-files.mjs +11 -21
- package/runtime/scripts/{checks → hfs}/architecture/tiers.mjs +47 -5
- package/runtime/scripts/{checks → hfs}/architecture/transport-owner.mjs +3 -3
- package/runtime/scripts/{checks → hfs}/architecture/typescript.mjs +13 -27
- package/runtime/scripts/hfs/declaration-shape.mjs +62 -0
- package/runtime/scripts/hfs/declaration-slots.mjs +75 -0
- package/runtime/scripts/hfs/edition-slots.mjs +66 -0
- package/runtime/scripts/hfs/manifest-shape.mjs +247 -0
- package/runtime/scripts/hfs/param-names.mjs +18 -0
- package/runtime/scripts/{lib/hfs-path-findings.mjs → hfs/path-findings.mjs} +7 -7
- package/runtime/scripts/{lib → hfs}/project-graph.mjs +0 -0
- package/runtime/scripts/{lib → hfs}/project-rule.mjs +3 -3
- package/runtime/scripts/{lib → hfs}/repo-identity.mjs +9 -6
- package/runtime/scripts/hfs/rule-params-shape.mjs +50 -0
- package/runtime/scripts/{lib/hfs-rules → hfs/rules}/fe-no-tests.mjs +6 -5
- package/runtime/scripts/{lib/hfs-rules → hfs/rules}/read.mjs +1 -1
- package/runtime/scripts/{lib/hfs-rules → hfs/rules}/secrets.mjs +4 -4
- package/runtime/scripts/{lib/hfs-rules → hfs/rules}/spec-placement.mjs +1 -1
- package/runtime/scripts/{lib/hfs-slots.mjs → hfs/slots.mjs} +114 -150
- package/runtime/scripts/hfs/test-world-slot.mjs +6 -0
- package/runtime/scripts/{checks → hfs}/typescript-programs.mjs +0 -5
- package/runtime/scripts/{lib/hfs-view.mjs → hfs/view.mjs} +6 -5
- package/runtime/scripts/lib/dockerfile.mjs +124 -0
- package/runtime/scripts/lib/git.mjs +36 -0
- package/runtime/scripts/lib/i18n.mjs +86 -0
- package/runtime/scripts/lib/json.mjs +13 -3
- package/runtime/scripts/lib/language.mjs +26 -7
- package/runtime/scripts/lib/line-count.mjs +9 -0
- package/runtime/scripts/lib/list.mjs +20 -0
- package/runtime/scripts/lib/path-key.mjs +59 -2
- package/runtime/scripts/lib/read-text.mjs +34 -0
- package/runtime/scripts/lib/regex.mjs +4 -0
- package/runtime/scripts/lib/secret-patterns.mjs +4 -3
- package/runtime/scripts/lib/sops-envelope.mjs +58 -0
- package/runtime/scripts/lib/stack-services.mjs +7 -31
- package/runtime/scripts/lib/ts-ast.mjs +35 -0
- package/runtime/scripts/{checks/common.mjs → lib/walk.mjs} +5 -15
- package/size-and-state-budget.mjs +1 -1
- package/size-growth.mjs +2 -1
- package/tokens.mjs +10 -18
- package/translation.mjs +1 -1
- package/transport.mjs +2 -2
- package/runtime/scripts/api/sops/decrypt.mjs +0 -16
- package/runtime/scripts/lib/recorded-lines.mjs +0 -27
- package/runtime/scripts/lib/test-secrets.mjs +0 -116
- /package/runtime/scripts/{checks → hfs}/architecture/config-unread.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/constructor-deps.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/cross-app-duplicate.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/default-deny.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/error-codes.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/error-masked.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/hooks-are-hooks.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/injection-token-exported.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/package-shape.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/reachability.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/route-files-thin.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/sql-tokens.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/symbols.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/unit-spec-providers.mjs +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
- Changed: `one-outcome-union` reads the Outcome home from the slot manifest (`outcomeHome` on `fe.transport.outcome`, `fe.package.api.outcome` and the lite `fe.modules.db.outcome`, i.e. `modules/db/outcome.ts`) instead of two slot ids in code; a lite app's db owner is its transport, so its `modules/db/outcome.ts` is THE union and a second one anywhere else is still refused. Full is unchanged.
|
|
6
|
+
- Changed: `public-component-signature` accepts a component with no parameter; `no-raw-structural-element` names `Region`, `NavLandmark`, `List` and `ListItem` as owners of section, nav, ul/ol and li; FE-I18N-1 and FE-TYPING-6 canon text (request locale through `next/root-params`, no `void props`); the FE_SWR_KEY_IDENTITY check reads `&&` and `||` gates in its bundled runtime.
|
|
7
|
+
## Unreleased (C0 batch)
|
|
8
|
+
|
|
9
|
+
- Changed (contract change `sonar-coverage-exclusions`): `route-slot-fixed-name` holds `error.tsx` to `ErrorBoundary`, no longer `Error`. `Error` shadows the global Error constructor (Sonar typescript:S2137, a bug), and React calls this component an error boundary. A slot still named `Error` is now reported.
|
|
10
|
+
|
|
3
11
|
## 8.0.7 - 2026-10-01
|
|
4
12
|
|
|
5
13
|
- Changed: the same bundled runtime layer move.
|
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ and colocated class-name composition consistent without prescribing a custom ren
|
|
|
8
8
|
npm i -D @starci/eslint-canon-fe eslint-plugin-react-hooks typescript
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
A repository's `eslint.config.mjs` is the managed one-liner `
|
|
11
|
+
A repository's `eslint.config.mjs` is the managed one-liner `starci app sync` renders (and `starci app check` compares, R05/R17):
|
|
12
12
|
|
|
13
13
|
```js
|
|
14
14
|
import { loadHfs, starciFeConfig } from "@starci/eslint-canon-fe"
|
|
@@ -42,12 +42,13 @@ naming, props, served locales, component splits, design tokens, type safety, typ
|
|
|
42
42
|
class-name ownership. Reusable class names belong in a colocated `classNames.ts` module and should
|
|
43
43
|
be composed with HeroUI `cn` using one utility token per argument.
|
|
44
44
|
|
|
45
|
-
The `
|
|
45
|
+
The `drawing` law holds the split surfaces (blocks, pages, layouts, overlays under `components/`
|
|
46
46
|
or `features/`): the pure `XBase` takes `{ state, props, on }` of atoms (`base-props-atom`), only its
|
|
47
47
|
sibling `index.tsx` imports it and never re-exports it (`base-import-pair`), `XState` names drawn
|
|
48
48
|
shapes and never a data status (`no-data-status-shape`), and a block renders each slot's
|
|
49
|
-
loading/forbidden/error/empty through `SlotView` (`slot-status-through-slotview`).
|
|
50
|
-
|
|
49
|
+
loading/forbidden/error/empty through `SlotView` (`slot-status-through-slotview`). The law's name is
|
|
50
|
+
the `drawing` slot role of `component.tsx`; a split-surface reference tree lives under
|
|
51
|
+
`.claude/examples/`.
|
|
51
52
|
|
|
52
53
|
The one locale pattern everywhere is `next-intl`, a `[locale]` segment, default `vi`,
|
|
53
54
|
`localePrefix: "as-needed"`, and `proxy.ts` instead of `middleware.ts`.
|
package/client-boundary.mjs
CHANGED
|
@@ -139,7 +139,8 @@ const isMarkerStatement = (statement) =>
|
|
|
139
139
|
|
|
140
140
|
/** True when a TypeScript source file opens with `import "server-only"`. */
|
|
141
141
|
const tsFileIsMarked = (file) => {
|
|
142
|
-
|
|
142
|
+
// a file-level directive ("use server") is the prologue: the marker is the first statement after it
|
|
143
|
+
const first = file.statements.find((statement) => !(ts.isExpressionStatement(statement) && ts.isStringLiteral(statement.expression)))
|
|
143
144
|
return Boolean(first) && ts.isImportDeclaration(first) && !first.importClause && ts.isStringLiteral(first.moduleSpecifier) && first.moduleSpecifier.text === "server-only"
|
|
144
145
|
}
|
|
145
146
|
|
|
@@ -183,7 +184,8 @@ export const serverModuleMarksServerOnly = {
|
|
|
183
184
|
return {
|
|
184
185
|
Program(program) {
|
|
185
186
|
client = clientDirective(program) !== null
|
|
186
|
-
|
|
187
|
+
// a file-level directive ("use server") is the prologue, not a statement: the marker is the first statement after it
|
|
188
|
+
marked = isMarkerStatement(program.body.find((statement) => !(statement.type === "ExpressionStatement" && typeof statement.directive === "string")))
|
|
187
189
|
},
|
|
188
190
|
"ImportDeclaration, ExportNamedDeclaration, ExportAllDeclaration"(node) {
|
|
189
191
|
if (!node.source || node.importKind === "type" || node.exportKind === "type") return
|
package/comments.mjs
CHANGED
|
@@ -25,7 +25,7 @@ import { fileOf, inSlot } from "./lib/scope.mjs"
|
|
|
25
25
|
* - a single class covering both trips the misleading-character-class rule, and a rule that has to
|
|
26
26
|
* be silenced to exist is one nobody trusts.
|
|
27
27
|
*/
|
|
28
|
-
|
|
28
|
+
const hasEmoji = (text) =>
|
|
29
29
|
typeof text === "string" &&
|
|
30
30
|
(/\p{Extended_Pictographic}/u.test(text) || /[\u{1F1E6}-\u{1F1FF}]{2}/u.test(text))
|
|
31
31
|
|
package/docs/grammar-boundary.md
CHANGED
|
@@ -12,7 +12,7 @@ Page structure and text are grammar components, not raw `div`-family HTML; only
|
|
|
12
12
|
|
|
13
13
|
Applies to app source that composes the grammar (slots `fe.feature`, `fe.components`, `fe.route`, `fe.hooks`) and to every workspace package except the `<family>-ui` package (`fe.package.ui`), whose components are the renderers. Spec files render raw HTML as a harness and are not judged; a file no slot owns is not judged.
|
|
14
14
|
|
|
15
|
-
The refused tags are exactly those `@starci/grammar` has a component for (the twin test reads the package source): `h1-h6` (`Heading`), `p` and `span` (`Text`), `hr` (`Divider`), `label` (`Label`), `form` (`Form`), `fieldset` (`Fieldset`), `dl` `dt` `dd` (`DescriptionList`), `table` `thead` `tbody` `tr` `td` `th` (`DataTable`), `ul` `ol` `li` (`SurfaceListCard`, `StaticStateRow`), `header` (`SectionHeader`, `TopBar`), `footer` (`Footer`), `nav` (`Subnav`, `Breadcrumbs`, `NavigationFeatureNav`), `section` (`SurfaceCard`), `main` (`WorkspaceShell`), `figure` and `figcaption` (`MediaFrame`). `div`, `br`, `aside` and `article` are not refused: the grammar has no component for them.
|
|
15
|
+
The refused tags are exactly those `@starci/grammar` has a component for (the twin test reads the package source): `h1-h6` (`Heading`), `p` and `span` (`Text`), `hr` (`Divider`), `label` (`Label`), `form` (`Form`), `fieldset` (`Fieldset`), `dl` `dt` `dd` (`DescriptionList`), `table` `thead` `tbody` `tr` `td` `th` (`DataTable`), `ul` `ol` `li` (`List` and `ListItem` for a plain semantic list, `SurfaceListCard`, `StaticStateRow`), `header` (`SectionHeader`, `TopBar`), `footer` (`Footer`), `nav` (`NavLandmark` for a named row of links, `Subnav`, `Breadcrumbs`, `NavigationFeatureNav`), `section` (`Region` for a named non-card region, `SurfaceCard`), `main` (`WorkspaceShell`), `figure` and `figcaption` (`MediaFrame`). `div`, `br`, `aside` and `article` are not refused: the grammar has no component for them.
|
|
16
16
|
|
|
17
17
|
**Invalid** (`src/features/pages/home/component.tsx`)
|
|
18
18
|
|
package/docs/next-conventions.md
CHANGED
|
@@ -221,7 +221,7 @@ export default getRequestConfig(async () => ({ locale: "vi", messages: {} }))
|
|
|
221
221
|
**Valid** (`apps/web/src/modules/i18n/index.ts`)
|
|
222
222
|
|
|
223
223
|
```ts
|
|
224
|
-
import { createAppI18n } from "@
|
|
224
|
+
import { createAppI18n } from "@acme/i18n"
|
|
225
225
|
export const i18n = createAppI18n({ locales: ["vi"] })
|
|
226
226
|
```
|
|
227
227
|
|
package/docs/translation.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Law module: `translation.mjs`. Catalogue: R58 FE_I18N_LITERAL.
|
|
4
4
|
|
|
5
|
-
No text a reader can see or hear is written in source, at any tier and in any language: it is read from a `next-intl` catalogue through `t()`. There is no `vn-ok` pragma, no endonym exemption and no `resources/` copy folder; the catalogues (`messages/<locale>.json`), fixtures and specs are the only content paths. The notion of a literal is the same as
|
|
5
|
+
No text a reader can see or hear is written in source, at any tier and in any language: it is read from a `next-intl` catalogue through `t()`. There is no `vn-ok` pragma, no endonym exemption and no `resources/` copy folder; the catalogues (`messages/<locale>.json`), fixtures and specs are the only content paths. The notion of a literal is the same as the repository's i18n catalog check: a word is two or more letters, and the copy attributes are `label`, `title`, `alt`, `placeholder` and the `aria-*` set.
|
|
6
6
|
|
|
7
7
|
Every rule below is an error in `starciFeConfig`; none can be switched off or suppressed inline.
|
|
8
8
|
|
package/docs/transport.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Law module: `transport.mjs`. Catalogue: R50 FE_TRANSPORT_OWNER, R51 FE_HTTP_STATUS_COLLAPSE, R52 FE_WIRE_GENERATED.
|
|
4
4
|
|
|
5
|
-
One repository, one client, one result vocabulary. The client is the file of HFS slot `fe.transport.client` (`apps/<app>/src/modules/api/client.ts`, a one-app repository) or `fe.package.api.client` (`packages/<family>-api/src/client.ts`, the shared client of a multi-app repository); the vocabulary is `Outcome<T>` (`ok`, `refused`, `invalid`, `not-found`, `unavailable`), declared in the `fe.transport.outcome
|
|
5
|
+
One repository, one client, one result vocabulary. The client is the file of HFS slot `fe.transport.client` (`apps/<app>/src/modules/api/client.ts`, a one-app repository) or `fe.package.api.client` (`packages/<family>-api/src/client.ts`, the shared client of a multi-app repository); the vocabulary is `Outcome<T>` (`ok`, `refused`, `invalid`, `not-found`, `unavailable`), declared in the file of the slot the manifest marks `outcomeHome`: `fe.transport.outcome`, `fe.package.api.outcome`, or `fe.modules.db.outcome` (`modules/db/outcome.ts`) in a lite app, whose db owner is the transport. The rules read the slot of the file being linted, never its path. 401 and 403 become `refused`; nothing collapses a status into `null`; wire types are generated from the contract copy in `modules/api/contract/`.
|
|
6
6
|
|
|
7
7
|
Every rule below is an error in `starciFeConfig`; none can be switched off or suppressed inline.
|
|
8
8
|
|
|
@@ -171,7 +171,7 @@ switch (outcome.kind) {
|
|
|
171
171
|
|
|
172
172
|
## `starci-fe/one-outcome-union`
|
|
173
173
|
|
|
174
|
-
A result union (`ok` / `kind` discriminant) is declared only in the outcome slot; elsewhere the code composes `Outcome<T>`.
|
|
174
|
+
A result union (`ok` / `kind` discriminant) is declared only in the outcome-home slot (`outcomeHome: true` in the slot manifest); elsewhere the code composes `Outcome<T>`.
|
|
175
175
|
|
|
176
176
|
A union type alias is a result union when its resolved members are all object types that carry the same literal-typed discriminant in the result vocabulary: `ok` (each member pins it to one value, `true` and `false` both present) or `kind` (at least one literal of the Outcome vocabulary: `ok`, `refused`, `forbidden`, `invalid`, `not-found`, `unavailable`). The members are read through the type checker, so `Ok<T> | Failure` and intersections are seen as what they are; no name (`*Outcome`, `*Result`) decides anything. UI state unions (`status`, `state`, `type`, a `kind` of tree nodes or menu items) are not result vocabulary and pass; so does an alias that only composes the one union (`type Read = Outcome<Course>`, `Exclude<Outcome<T>, ...>`). `Outcome<T> | { ok: false; kind: "conflict" }` adds an arm to the one union and is refused.
|
|
177
177
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The rules that hold the
|
|
2
|
+
* The rules that hold the drawing law (the `drawing` slot role: a surface's pure `component.tsx`).
|
|
3
3
|
*
|
|
4
4
|
* TWO AXES, AND ONLY ONE IS DRAWN. A surface's `state` is its SHAPE: a layout the owner approves as
|
|
5
5
|
* one drawing. A slot's data status (loading, forbidden, error, empty) is not a shape; it is a
|
|
@@ -77,7 +77,7 @@ const membersOf = (node, types, seen = new Set()) => {
|
|
|
77
77
|
/** The key a property signature declares. */
|
|
78
78
|
const keyOf = (member) => (member.key?.type === "Identifier" ? member.key.name : member.key?.value)
|
|
79
79
|
|
|
80
|
-
// --
|
|
80
|
+
// -- DRAWING-1 ----------------------------------------------------------------------------------
|
|
81
81
|
|
|
82
82
|
/** The pure half takes `{ state, props, on }` of atoms; only a layout also takes `children`. */
|
|
83
83
|
export const basePropsAtom = {
|
|
@@ -189,7 +189,7 @@ export const basePropsAtom = {
|
|
|
189
189
|
},
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
-
// --
|
|
192
|
+
// -- DRAWING-2 ----------------------------------------------------------------------------------
|
|
193
193
|
|
|
194
194
|
/** Only the sibling `index.tsx` (and its specs) may reach `component.tsx`; only `X` leaves the folder. */
|
|
195
195
|
export const baseImportPair = {
|
|
@@ -241,7 +241,7 @@ export const baseImportPair = {
|
|
|
241
241
|
},
|
|
242
242
|
}
|
|
243
243
|
|
|
244
|
-
// --
|
|
244
|
+
// -- DRAWING-3 ----------------------------------------------------------------------------------
|
|
245
245
|
|
|
246
246
|
/** Words that name a data status or an open/closed lifecycle, never a drawn shape. */
|
|
247
247
|
const DATA_STATUS = new Set([
|
|
@@ -279,7 +279,7 @@ export const noDataStatusShape = {
|
|
|
279
279
|
},
|
|
280
280
|
}
|
|
281
281
|
|
|
282
|
-
// --
|
|
282
|
+
// -- DRAWING-4 ----------------------------------------------------------------------------------
|
|
283
283
|
|
|
284
284
|
/** Flags that are a slot's data status. */
|
|
285
285
|
const STATUS_FLAG = /^(?:isLoading|isError|isForbidden|isPending|isValidating|isEmpty)$/
|
package/file-layout.mjs
CHANGED
|
@@ -205,14 +205,15 @@ const featureTiers = (hfs) => {
|
|
|
205
205
|
}
|
|
206
206
|
|
|
207
207
|
/**
|
|
208
|
-
* In a
|
|
208
|
+
* In a front end with two or more apps, the shared package stops below the block.
|
|
209
209
|
*
|
|
210
210
|
* WHY THE SLOT IS ENOUGH HERE. The feature line is already drawn by the layer: a leaf knows
|
|
211
211
|
* no domain and a block is a domain sentence, so which side of the workspace each belongs on
|
|
212
212
|
* follows from the layer the slot reports. The layers a shared package may hold are the `kinds` of
|
|
213
|
-
* `fe.package.ui`; an app holds the shared ones only when the repository declares that package
|
|
213
|
+
* `fe.package.ui`; an app holds the shared ones only when the repository declares that package and another app can
|
|
214
|
+
* consume them. A one-app front end has nothing to share, so every tier may stay with that app.
|
|
214
215
|
*
|
|
215
|
-
* WHAT IT COST TO LEARN. A shared package carried one `blocks/
|
|
216
|
+
* WHAT IT COST TO LEARN. A shared package carried one `blocks/WorkerRow/` while its own header
|
|
216
217
|
* insisted that "a block carries feature meaning and therefore belongs to the app that owns the
|
|
217
218
|
* feature". The document was right, the tree disagreed, and nothing turned red for either of them -
|
|
218
219
|
* which is the whole argument for this rule rather than the paragraph.
|
|
@@ -222,18 +223,19 @@ export const monorepoTierBelongsToItsSide = {
|
|
|
222
223
|
type: "problem",
|
|
223
224
|
docs: {
|
|
224
225
|
description:
|
|
225
|
-
"
|
|
226
|
+
"With two or more apps, the shared package holds leaves, composites and branches; blocks, overlays, layouts and pages belong to the app that owns the feature.",
|
|
226
227
|
},
|
|
227
228
|
schema: [],
|
|
228
229
|
messages: {
|
|
229
230
|
featureInPackage:
|
|
230
231
|
"`{{tier}}/` knows a feature, so it belongs to the app that owns that feature, not to the shared package - every app that never wanted this domain now ships it. Move it to `apps/<app>/src/components/{{tier}}/`.",
|
|
231
232
|
vocabularyInApp:
|
|
232
|
-
"`{{tier}}/` knows no feature, so one copy belongs to every app - left here,
|
|
233
|
+
"`{{tier}}/` knows no feature, and this repository has multiple apps, so one copy belongs to every app - left here, another app writes it again and the two drift with nothing to notice. Move it to `packages/ui/src/{{tier}}/`.",
|
|
233
234
|
},
|
|
234
235
|
},
|
|
235
236
|
create(context) {
|
|
236
237
|
const hfs = hfsOf(context)
|
|
238
|
+
if (hfs.apps.length < 2) return {}
|
|
237
239
|
const folder = unknownLayerFolder(context)
|
|
238
240
|
const inPackage = inSlot(context, "fe.package.ui") && folder !== null && featureTiers(hfs).has(folder) ? folder : null
|
|
239
241
|
const tier = kindOfFile(context)
|
|
@@ -257,7 +259,7 @@ export const monorepoTierBelongsToItsSide = {
|
|
|
257
259
|
*
|
|
258
260
|
* WHY THIS RULE EXISTS RATHER THAN THE PARAGRAPH ALONE. The law said "the route file mounts and
|
|
259
261
|
* nothing else" and had said so for as long as the tier list existed. Nothing checked it. A page
|
|
260
|
-
* owner was written to `app/<segment>/
|
|
262
|
+
* owner was written to `app/<segment>/workers-page.tsx`, carried through a build, a lint run, a
|
|
261
263
|
* typecheck, four sealed screenshots and an approval, and arrived at the edge of a production write
|
|
262
264
|
* with every gate green - because every gate was reading rules, and this one was only prose.
|
|
263
265
|
*
|
|
@@ -381,7 +383,9 @@ const ROUTE_SLOT_NAME = {
|
|
|
381
383
|
layout: "Layout",
|
|
382
384
|
template: "Template",
|
|
383
385
|
loading: "Loading",
|
|
384
|
-
|
|
386
|
+
// Not `Error`: that name shadows the global Error constructor (Sonar typescript:S2137, a bug), and React calls this
|
|
387
|
+
// component an error boundary.
|
|
388
|
+
error: "ErrorBoundary",
|
|
385
389
|
"global-error": "GlobalError",
|
|
386
390
|
"not-found": "NotFound",
|
|
387
391
|
}
|
package/grammar-boundary.mjs
CHANGED
|
@@ -21,7 +21,7 @@ import { hfsOf } from "./lib/hfs.mjs"
|
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
23
|
* The intrinsic elements of page structure and text, each with the grammar components that render it.
|
|
24
|
-
* Read against packages/grammar/src (`export const <Component>`); `grammar-boundary.
|
|
24
|
+
* Read against packages/grammar/src (`export const <Component>`); `grammar-boundary.spec.mjs` fails when one is not exported.
|
|
25
25
|
*/
|
|
26
26
|
export const GRAMMAR_OWNERS = Object.freeze({
|
|
27
27
|
h1: "Heading", h2: "Heading", h3: "Heading", h4: "Heading", h5: "Heading", h6: "Heading",
|
|
@@ -32,11 +32,11 @@ export const GRAMMAR_OWNERS = Object.freeze({
|
|
|
32
32
|
fieldset: "Fieldset",
|
|
33
33
|
dl: "DescriptionList", dt: "DescriptionList", dd: "DescriptionList",
|
|
34
34
|
table: "DataTable", thead: "DataTable", tbody: "DataTable", tr: "DataTable", td: "DataTable", th: "DataTable",
|
|
35
|
-
ul: "SurfaceListCard or StaticStateRow", ol: "SurfaceListCard or StaticStateRow", li: "SurfaceListCard or StaticStateRow",
|
|
35
|
+
ul: "List, SurfaceListCard or StaticStateRow", ol: "List, SurfaceListCard or StaticStateRow", li: "ListItem, SurfaceListCard or StaticStateRow",
|
|
36
36
|
header: "SectionHeader or TopBar",
|
|
37
37
|
footer: "Footer",
|
|
38
|
-
nav: "Subnav, Breadcrumbs or NavigationFeatureNav",
|
|
39
|
-
section: "SurfaceCard",
|
|
38
|
+
nav: "NavLandmark, Subnav, Breadcrumbs or NavigationFeatureNav",
|
|
39
|
+
section: "Region or SurfaceCard",
|
|
40
40
|
main: "WorkspaceShell",
|
|
41
41
|
figure: "MediaFrame", figcaption: "MediaFrame",
|
|
42
42
|
})
|
package/icon.mjs
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* generalises on its own, and the gap between them is exactly where a third step gets invented.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
+
import { isClassAttribute, staticText } from "./lib/ast.mjs"
|
|
15
16
|
import { hfsOf } from "./lib/hfs.mjs"
|
|
16
17
|
import { fileOf, isComponentFile, isProductSource, kindOfFile, roleOfFile } from "./lib/scope.mjs"
|
|
17
18
|
|
|
@@ -51,14 +52,6 @@ const HEROICON_PACKAGES = new Set([
|
|
|
51
52
|
"@starci/heroicons/16/solid",
|
|
52
53
|
])
|
|
53
54
|
|
|
54
|
-
/**
|
|
55
|
-
* Product reactions are attributed artwork rather than glyph-library imports.
|
|
56
|
-
*
|
|
57
|
-
* Their leaf owns this closed vocabulary and maps it to checked-in Fluent Emoji Flat SVGs under
|
|
58
|
-
* `public/reactions/`; callers pass identities, never Unicode pictographs or asset paths.
|
|
59
|
-
*/
|
|
60
|
-
export const REACTION_ASSET_NAMES = new Set(["like", "love", "haha", "wow", "sad", "angry"])
|
|
61
|
-
|
|
62
55
|
/**
|
|
63
56
|
* True when the file is the entry (slot role `entry`) of the component owner named `owner` in the layer `kind`.
|
|
64
57
|
* The slot says which layer and which role; the owner's own folder name is the one fact it cannot say, so it is compared.
|
|
@@ -84,21 +77,6 @@ const isGlyphImport = (source) => {
|
|
|
84
77
|
/** A glyph size written as a fraction of a step, or as an arbitrary value. */
|
|
85
78
|
const OFF_SCALE_GLYPH = /\bsize-(?:\d+\.\d+|\[[^\]]+\])/
|
|
86
79
|
|
|
87
|
-
/** True for a `className` / `class` JSX attribute. */
|
|
88
|
-
const isClassAttribute = (node) =>
|
|
89
|
-
node.type === "JSXAttribute" && node.name && (node.name.name === "className" || node.name.name === "class")
|
|
90
|
-
|
|
91
|
-
/** Static string carried by a JSX attribute, or by a module constant holding a class string. */
|
|
92
|
-
const staticText = (value) => {
|
|
93
|
-
if (!value) return null
|
|
94
|
-
if (value.type === "Literal" && typeof value.value === "string") return value.value
|
|
95
|
-
if (value.type === "TemplateLiteral" && value.expressions.length === 0) {
|
|
96
|
-
return value.quasis.map((quasi) => quasi.value.cooked).join(" ")
|
|
97
|
-
}
|
|
98
|
-
if (value.type === "JSXExpressionContainer") return staticText(value.expression)
|
|
99
|
-
return null
|
|
100
|
-
}
|
|
101
|
-
|
|
102
80
|
// -- ICON-6 ----------------------------------------------------------------------------------------
|
|
103
81
|
|
|
104
82
|
/** A glyph library is named in one file, and a caller asks for a MEANING instead. */
|
package/index.mjs
CHANGED
|
@@ -35,7 +35,7 @@ import { recommended as nativeControlsRecommended, rules as nativeControlsRules
|
|
|
35
35
|
import { recommended as nextConventionsRecommended, rules as nextConventionsRules } from "./next-conventions.mjs"
|
|
36
36
|
import { recommended as propsRecommended, rules as propsRules } from "./props-and-slots.mjs"
|
|
37
37
|
import { recommended as servedLocaleRecommended, rules as servedLocaleRules } from "./served-locale.mjs"
|
|
38
|
-
import { recommended as
|
|
38
|
+
import { recommended as drawingRecommended, rules as drawingRules } from "./drawing.mjs"
|
|
39
39
|
import { recommended as sizeBudgetRecommended, rules as sizeBudgetRules } from "./size-and-state-budget.mjs"
|
|
40
40
|
import { recommended as sizeGrowthRecommended, rules as sizeGrowthRules } from "./size-growth.mjs"
|
|
41
41
|
import { recommended as statusColorsRecommended, rules as statusColorsRules } from "./status-colors.mjs"
|
|
@@ -77,7 +77,7 @@ const CONTRIBUTIONS = [
|
|
|
77
77
|
{ law: "next-conventions", rules: nextConventionsRules, recommended: nextConventionsRecommended },
|
|
78
78
|
{ law: "props-and-slots", rules: propsRules, recommended: propsRecommended },
|
|
79
79
|
{ law: "served-locale", rules: servedLocaleRules, recommended: servedLocaleRecommended },
|
|
80
|
-
{ law: "
|
|
80
|
+
{ law: "drawing", rules: drawingRules, recommended: drawingRecommended },
|
|
81
81
|
{ law: "size-and-state-budget", rules: sizeBudgetRules, recommended: sizeBudgetRecommended },
|
|
82
82
|
{ law: "size-growth", rules: sizeGrowthRules, recommended: sizeGrowthRecommended },
|
|
83
83
|
{ law: "status-colors", rules: statusColorsRules, recommended: statusColorsRecommended },
|
package/lib/ast.mjs
CHANGED
|
@@ -47,20 +47,23 @@ export const calleeName = (node) => {
|
|
|
47
47
|
/** The name of a JSX element for a plain tag or a component; null for a member or namespaced name. */
|
|
48
48
|
export const tagName = (opening) => (opening.name.type === "JSXIdentifier" ? opening.name.name : null)
|
|
49
49
|
|
|
50
|
-
/** True for a
|
|
51
|
-
export const
|
|
50
|
+
/** True for a `className` / `class` JSX attribute. */
|
|
51
|
+
export const isClassAttribute = (node) =>
|
|
52
|
+
node.type === "JSXAttribute" && node.name && (node.name.name === "className" || node.name.name === "class")
|
|
52
53
|
|
|
53
|
-
/**
|
|
54
|
-
export const
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
/** Static string carried by a JSX attribute or by a module constant holding a class string. */
|
|
55
|
+
export const staticText = (value) => {
|
|
56
|
+
if (!value) return null
|
|
57
|
+
if (value.type === "Literal" && typeof value.value === "string") return value.value
|
|
58
|
+
if (value.type === "TemplateLiteral" && value.expressions.length === 0) {
|
|
59
|
+
return value.quasis.map((quasi) => quasi.value.cooked).join(" ")
|
|
60
|
+
}
|
|
61
|
+
if (value.type === "JSXExpressionContainer") return staticText(value.expression)
|
|
62
|
+
return null
|
|
57
63
|
}
|
|
58
64
|
|
|
59
|
-
/**
|
|
60
|
-
export const
|
|
61
|
-
const first = node.arguments[0]
|
|
62
|
-
return first && isFunction(first) ? first : null
|
|
63
|
-
}
|
|
65
|
+
/** True for a function expression or an arrow function. */
|
|
66
|
+
export const isFunction = (node) => node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression"
|
|
64
67
|
|
|
65
68
|
/**
|
|
66
69
|
* Every node under `root`, depth first, through the visitor keys of the parser in use.
|
package/lib/config.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The one flat config of a StarCi front end.
|
|
3
3
|
*
|
|
4
|
-
* A repository's `eslint.config.mjs` is a managed file (R05/R16/R17), rendered by `
|
|
4
|
+
* A repository's `eslint.config.mjs` is a managed file (R05/R16/R17), rendered by `starci app sync` as exactly:
|
|
5
5
|
*
|
|
6
6
|
* import { loadHfs, starciFeConfig } from "@starci/eslint-canon-fe"
|
|
7
7
|
* export default starciFeConfig({ hfs: loadHfs(import.meta.url) })
|
package/lib/hfs.mjs
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* A consuming `eslint.config.mjs` is the managed one-liner `export default starciFeConfig({ hfs: loadHfs(import.meta.url) })`.
|
|
5
5
|
* The config lives in the fe/ side folder of an app; `loadHfs` finds the app-root `hfs.json` one level up and gives every file
|
|
6
6
|
* linted under that folder the view of the fe side (paths relative to fe/), with the slot manifest this package ships in
|
|
7
|
-
* `runtime/` (a byte copy of the runtime's `knowledge/hfs/slots.yaml`, `scripts/
|
|
8
|
-
* `scripts/
|
|
7
|
+
* `runtime/` (a byte copy of the runtime's `knowledge/hfs/slots.yaml`, `scripts/hfs/slots.mjs` and
|
|
8
|
+
* `scripts/hfs/view.mjs`, refreshed by `scripts/hfs/sync-runtime.mjs`), and returns the view the factory puts
|
|
9
9
|
* in `settings.starci.hfs`.
|
|
10
10
|
*
|
|
11
11
|
* Every path-scoped rule asks `slotOf` / `tierOf` here instead of testing a path with a regular expression. A rule that
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { dirname, join } from "node:path"
|
|
15
15
|
import { fileURLToPath } from "node:url"
|
|
16
|
-
import { declaredHfsView, openHfsView } from "../runtime/scripts/
|
|
16
|
+
import { declaredHfsView, openHfsView } from "../runtime/scripts/hfs/view.mjs"
|
|
17
17
|
|
|
18
18
|
const RUNTIME = join(dirname(fileURLToPath(import.meta.url)), "..", "runtime")
|
|
19
19
|
|
package/lib/next.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Next.js's own contract for route segment files, read from the runtime's one statement of it
|
|
3
|
-
* (`scripts/lib/next-contract.mjs`, shipped in `runtime/` by `
|
|
3
|
+
* (`scripts/lib/next-contract.mjs`, shipped in `runtime/` by `scripts/hfs/sync-runtime.mjs`), so every law
|
|
4
4
|
* holds the same list by construction.
|
|
5
5
|
*/
|
|
6
6
|
export { NEXT_RESERVED_EXPORTS, NEXT_ROUTE_SEGMENT_STEMS } from "../runtime/scripts/lib/next-contract.mjs"
|
package/lib/scope.mjs
CHANGED
|
@@ -63,8 +63,12 @@ export const isConfigModule = (context) => inSlot(context, "fe.modules.config")
|
|
|
63
63
|
/** The one place that may call `fetch`: slot `fe.transport.client` (one-app repository) or `fe.package.api.client` (shared package). */
|
|
64
64
|
export const isApiClient = (context) => inSlot(context, "fe.transport.client", "fe.package.api.client")
|
|
65
65
|
|
|
66
|
-
/** The file that may declare the one Outcome union: slot
|
|
67
|
-
export const isOutcomeModule = (context) =>
|
|
66
|
+
/** The file that may declare the one Outcome union: the file of the slot the manifest marks `outcomeHome`. */
|
|
67
|
+
export const isOutcomeModule = (context) => {
|
|
68
|
+
const hfs = hfsOf(context)
|
|
69
|
+
const slot = slotOfFile(context)
|
|
70
|
+
return slot !== null && hfs.slot(slot)?.outcomeHome === true
|
|
71
|
+
}
|
|
68
72
|
|
|
69
73
|
/** The basename of a path without directories. */
|
|
70
74
|
export const baseName = (filename) => normalizePath(filename).split("/").pop() ?? ""
|