@starci/eslint-canon-fe 9.8.4 → 9.9.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 +25 -4
- package/README.md +4 -5
- package/dist/button-intent.d.ts +0 -1
- package/dist/button-intent.js +1 -16
- package/dist/file-layout.js +2 -1
- package/dist/index.js +2 -0
- package/dist/lib/config.js +3 -2
- package/dist/lib/scope.d.ts +1 -1
- package/dist/lib/scope.js +1 -1
- package/dist/lib/why.d.ts +40 -5
- package/dist/lib/why.js +44 -9
- package/dist/loading.js +1 -1
- package/dist/props-and-slots.d.ts +6 -0
- package/dist/props-and-slots.js +41 -0
- package/dist/shape-slot.js +6 -6
- package/dist/skeleton-states.d.ts +16 -0
- package/dist/skeleton-states.js +230 -0
- package/dist/surface-rules.d.ts +12 -3
- package/dist/surface-rules.js +190 -89
- package/dist/ui-rulings.js +1 -1
- package/docs/button-intent.md +0 -1
- package/docs/size-and-state-budget.md +1 -1
- package/docs/surface-rules.md +91 -18
- package/package.json +1 -1
- package/runtime/bundle.json +4 -0
- package/runtime/knowledge/hfs/canon-pins.yaml +7 -7
- package/runtime/knowledge/hfs/slots.yaml +5 -5
- package/runtime/knowledge/patterns/fe/folder.yaml +1 -1
- package/runtime/scripts/hfs/architecture/clones.mjs +12 -2
- package/runtime/scripts/hfs/architecture/component-tiers.mjs +161 -0
- package/runtime/scripts/hfs/architecture/config.mjs +1 -1
- package/runtime/scripts/hfs/architecture/frontend-world.mjs +1 -1
- package/runtime/scripts/hfs/architecture/frontend.mjs +2 -2
- package/runtime/scripts/hfs/architecture/index.mjs +6 -0
- package/runtime/scripts/hfs/architecture/render-graph.mjs +449 -0
- package/runtime/scripts/hfs/architecture/render-prop-across-server.mjs +58 -0
- package/runtime/scripts/hfs/architecture/surface-nesting.mjs +127 -0
- package/runtime/scripts/hfs/slot-manifest-shape.mjs +16 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
## 9.9.0 - 2026-11-02
|
|
6
|
+
|
|
7
|
+
- Added: `no-hand-skeleton` is published (R367) and requested at error: an app draws no `Skeleton`, `ChatBubbleSkeletons` or `MessageBubbleSkeletons`, because the Grammar components draw their loading structure from `isLoading`. Changed: `no-data-status-prop` lets a leaf, branch or composite take `isLoading` (the one flag below SlotView) and refuses `isError`, `isForbidden`, `isEmpty`, `isValidating`; `slotOfReads` counts as a slot builder.
|
|
8
|
+
|
|
9
|
+
- Removed: `NavigationFeatureNav` from the surface names and the bundled Core playbook (the component is gone; `TopBar` takes `subnavigation`).
|
|
10
|
+
- Added: `tier-children` (R92): only a unit of `components/frames` takes `children`; a leaf takes `children: string`; a block, a composite and a branch take none (`children?: never`) and `PropsWithChildren` is refused there.
|
|
11
|
+
- Added: `no-swr-default-array`, `no-foreign-slot-status`, `no-validating-as-loading`, `no-placeholder-object` (R367; `FE_SWR_DEFAULT_ARRAY`, `FE_FOREIGN_SLOT_STATUS`, `FE_VALIDATING_AS_LOADING`, `FE_PLACEHOLDER_OBJECT`): a loading state is drawn from the page's own reads. `no-hand-skeleton` is written and tested but not published until the Grammar owns the loading shape.
|
|
12
|
+
- Removed: `button-no-variant` and `nested-control-variant` (the `variant` prop is out of the Grammar types, so the props they refused cannot be written).
|
|
13
|
+
|
|
14
|
+
- Changed: `no-heading-above-card` (R354) follows the removal of `SectionHeader`: it refuses a `Section` (not `kind="list"`) whose only child is one card, and a card whose `ariaLabel` repeats the `Section` title. `no-hand-built-panes` reads a `Section` as a column where it read `Region`, and `no-commit-in-collection-item` no longer names the removed `CollectionItem`, `ItemActions` and `ListItem`.
|
|
15
|
+
|
|
16
|
+
- eslint-canon-fe: the surface rules follow the one card (SurfaceListCard and SurfaceAccordionCard are gone).
|
|
17
|
+
- Added: `frames` joins the component tiers (`components/frames/<Name>`, `meta.shape = 'frame'`); a frame is a lower tier for the world check (it owns no data, session or navigation).
|
|
18
|
+
|
|
19
|
+
## 9.8.5 - 2026-11-01
|
|
20
|
+
|
|
21
|
+
- Added: `no-heading-above-card` (R354, `FE_HEADING_ABOVE_CARD`): a `SectionHeader` directly above a single card, or a card whose `ariaLabel` repeats the header title, is refused; several cards under one header stay legal.
|
|
22
|
+
- Added: `no-selectable-tag-group` (R353, `FE_TAG_GROUP_SELECTION`): a selection prop on a Grammar `TagGroup` is refused; a filter is a `FilterButtonGroup`.
|
|
23
|
+
- Added: `no-fill-command-in-header` (R352, `FE_COMMAND_FILLS_HEADER`): a Grammar `Button` with `width="fill"` in the `actions` of a `PageHeader` or the `labelEnd` of a `SurfaceCard` or `SurfaceListCard` is refused (a page or pane command hugs its label).
|
|
24
|
+
- Added: `no-table-in-card` (R351, `FE_TABLE_IN_CARD`): a Grammar `DataTable` enclosed by a `SurfaceCard`, or rendered by a component of the same file that a `SurfaceCard` encloses, is refused; plain rows are a `SurfaceListCard` with the note in its `footer`. It is a slot-free surface law at error, with no inline exemption.
|
|
25
|
+
- Changed: the bundled canon pins name @starci/grammar 0.20.2, eslint-grammar-core and eslint-grammar-offset-pop 0.20.2, eslint-canon-be 3.11.6, eslint-canon-fe 9.8.5, stylelint-canon 2.4.0 and hfs 4.16.5 (RELEASE_VERSION_NOT_BUMPED: the previous versions are on npm). One rule law changes (above).
|
|
26
|
+
|
|
3
27
|
## 9.8.4 - 2026-11-01
|
|
4
28
|
|
|
5
29
|
- Changed: the bundled canon pins name @starci/grammar 0.20.1, eslint-grammar-core and eslint-grammar-offset-pop 0.20.1, eslint-canon-be 3.11.5, eslint-canon-fe 9.8.4, stylelint-canon 2.4.0 and hfs 4.16.4 (RELEASE_VERSION_NOT_BUMPED: the previous versions are on npm). No rule changes.
|
|
@@ -29,7 +53,7 @@
|
|
|
29
53
|
|
|
30
54
|
## 9.6.7 - 2026-10-31
|
|
31
55
|
|
|
32
|
-
- Changed: the bundled runtime copies follow @starci/hfs 4.14.0 (R334 one style, `ruleParams.be.sqlPassThrough`) and the canon pins name the cohort hfs 4.14.0, eslint-canon-be 3.11.0, eslint-canon-fe 9.6.7, @starci/grammar, eslint-grammar-core and eslint-grammar-offset-pop 0.16.6, with @starci/ai-router 0.5.0;
|
|
56
|
+
- Changed: the bundled runtime copies follow @starci/hfs 4.14.0 (R334 one style, `ruleParams.be.sqlPassThrough`) and the canon pins name the cohort hfs 4.14.0, eslint-canon-be 3.11.0, eslint-canon-fe 9.6.7, @starci/grammar, eslint-grammar-core and eslint-grammar-offset-pop 0.16.6, with @starci/ai-router 0.5.0; the status rule changes (below).
|
|
33
57
|
|
|
34
58
|
## 9.6.6 - 2026-10-31
|
|
35
59
|
|
|
@@ -162,7 +186,6 @@
|
|
|
162
186
|
- Removed: `callback-shape` (R287 `FE_CALLBACK_SHAPE`) with the `fe.route.callback` slot: the OAuth callback is a back-end door.
|
|
163
187
|
- Changed: the bundled slot manifest 3.9.0 replaces the lite `app.supabase*` slots with `app.db` (`db/migrations`, `db/migrations/archive`, `db/seed.sql`, the managed `db/bootstrap.sql`), `be.integrations.supabase` with `be.integrations.passport` and `be.integrations.object-storage`, removes `fe.route.callback`, sets lite `schemaAuthority: sql`, gives lite TypeORM entities in `be.persistence`, adds `ruleParams.be.dbFunctions`, and moves the S3 SDKs to `integrations/object-storage` and the identity libraries to `integrations/passport` in `infraOwners` (`@supabase/*`, `minio` and `bcrypt` nowhere); the bundled rule catalog retires R216, R217, R219, R254 and R287 and adds R328-R334; the bundled canon pins name hfs 4.10.0, eslint-canon-be 3.7.0, eslint-canon-fe 9.5.0 and the Grammar family 0.15.11, pin `@nestjs/passport` 11.0.5 (CommonJS on the Nest 11 line), `passport` 0.7.0, `passport-google-oauth20` 2.0.0, `passport-local` 1.0.0, `bcryptjs` 3.0.3 and their types (tokens are signed and verified with `jose`; `@nestjs/jwt` stays allowed in `integrations/passport` but unpinned), and drop `@supabase/ssr`, `@supabase/supabase-js` and the `supabase` CLI.
|
|
164
188
|
|
|
165
|
-
|
|
166
189
|
## 9.4.9 - 2026-10-29
|
|
167
190
|
|
|
168
191
|
- Changed: the bundled slot manifest gives the model libraries their owners in `ruleParams.be.infraOwners` (`@starci/ai-router` to `integrations/ai-gateway`, `@langchain/*` to `integrations/ai`; `openai`, `langchain`, `@langchain/langgraph`, `@langchain/community` and the provider SDKs nowhere) and lists R326 and R327 on `be.integrations`; the bundled canon pins name hfs 4.9.11, eslint-canon-be 3.6.8, eslint-canon-fe 9.4.9, the Grammar family 0.15.10 (grammar, eslint-grammar-core, eslint-grammar-offset-pop), and pin `@langchain/core` 1.3.1 and `@langchain/openai` 1.8.0. No rule changed.
|
|
@@ -425,7 +448,6 @@
|
|
|
425
448
|
## 8.2.17 - 2026-10-14
|
|
426
449
|
- Changed: `component-line-budget` gives a pure re-export `index.ts` the slot's `barrel` budget (400); `basePropsAtom` admits named `render*` callbacks in `props` of non-block pure halves for feature composition; the bundled machine carries the kind-scoped feature tier edge, canon-shape clone exclusions and in-segment i18n template reads.
|
|
427
450
|
|
|
428
|
-
|
|
429
451
|
- Changed: carry the runtime mirror of HFS manifest 3.1.0 (the back-end app composition standard, R245 and R246) and the canon pins of eslint-canon-be 3.3.0, HFS 4.3.0 and CLI 1.2.16. ESLint rule implementations are unchanged.
|
|
430
452
|
|
|
431
453
|
## 8.2.18 - 2026-10-14
|
|
@@ -678,7 +700,6 @@
|
|
|
678
700
|
|
|
679
701
|
## 5.1.2 - 2026-09-30
|
|
680
702
|
|
|
681
|
-
|
|
682
703
|
## 5.1.1 - 2026-09-30
|
|
683
704
|
|
|
684
705
|
- **Fix: `use-client-only-at-boundary` knew only the app layout.** A workspace package keeps its grammar tiers directly under `src/` (`packages/<pkg>/src/{composites,branches,leaves}`, the `fe.package.ui` slot), but the rule accepted the directive only under `/components/...`, so nivo-fe's `packages/nivo-ui` reported 13 false findings. The tier names come from `PACKAGE_TIERS` in `lib/scope.mjs` (the slot's layers, no repo name); a package branch or leaf is a boundary, a package composite is not, and the app layout is unchanged.
|
package/README.md
CHANGED
|
@@ -37,13 +37,12 @@ tree. `starciTypedSourceConfig({ root })` provides the shared typed, React Hooks
|
|
|
37
37
|
configuration for source that has not adopted the product composition laws.
|
|
38
38
|
|
|
39
39
|
The [surface composition law](docs/surface-rules.md) (R318) holds ANATOMY-2 and PADDING-0 for every
|
|
40
|
-
family that publishes the shared HeroUI surface and field API: `
|
|
41
|
-
variants on layer-aware fields and the pill (`variant="secondary"`) on Tabs, which are the underline bar (`FE_NESTED_VARIANT`),
|
|
42
|
-
`no-app-surface-paint` keeps surface paint in Grammar (`FE_GRAMMAR_LAYOUT_ONLY`) and
|
|
40
|
+
family that publishes the shared HeroUI surface and field API: `no-app-surface-paint` keeps surface paint in Grammar (`FE_GRAMMAR_LAYOUT_ONLY`) and
|
|
43
41
|
`no-list-panel-padding` refuses horizontal padding wrappers around Grammar lists and section captions
|
|
44
|
-
within the nearest painted surface, a `Pane` or a `PaneSection` included (`FE_LIST_PANEL_PADDING`),
|
|
42
|
+
within the nearest painted surface, a `Pane` or a `PaneSection` included (`FE_LIST_PANEL_PADDING`),
|
|
45
43
|
`no-hand-built-panes` refuses a multi-pane frame drawn by hand, a vertical divider as a column seam or
|
|
46
|
-
columns inside a joined card, where `PaneGrid` owns both (`FE_HAND_BUILT_PANES`)
|
|
44
|
+
columns inside a joined card, where `PaneGrid` owns both (`FE_HAND_BUILT_PANES`), and
|
|
45
|
+
`no-heading-above-card` refuses a `Section` around a single card (`FE_HEADING_ABOVE_CARD`), `no-selectable-tag-group` refuses a selection on a `TagGroup` (`FE_TAG_GROUP_SELECTION`), `no-fill-command-in-header` refuses a `width="fill"` Button in a page or card header slot (`FE_COMMAND_FILLS_HEADER`), and `no-table-in-card` refuses a `DataTable` inside a `SurfaceCard`, where a `SurfaceListCard` with its note in `footer` holds plain rows (`FE_TABLE_IN_CARD`). The R65 budget law's
|
|
47
46
|
`no-app-relative-clock` leaves live relative time and countdowns to Grammar's shared clock.
|
|
48
47
|
|
|
49
48
|
The spacing law (R350) leaves every distance to Grammar: `no-spacing-utility` refuses a padding, margin, gap
|
package/dist/button-intent.d.ts
CHANGED
|
@@ -2,7 +2,6 @@ import { type Recommended } from "./lib/rule.js";
|
|
|
2
2
|
/** Every intent law is universal across Grammar families and app editions. */
|
|
3
3
|
export declare const rules: {
|
|
4
4
|
"button-intent-required": import("./lib/rule.js").Rule<"FE_BUTTON_INTENT_REQUIRED", []>;
|
|
5
|
-
"button-no-variant": import("./lib/rule.js").Rule<"FE_BUTTON_NO_VARIANT", []>;
|
|
6
5
|
"destructive-confirm-in-dialog": import("./lib/rule.js").Rule<"FE_DESTRUCTIVE_CONFIRM_IN_DIALOG", []>;
|
|
7
6
|
"single-commit-per-render": import("./lib/rule.js").Rule<"FE_SINGLE_COMMIT_PER_RENDER", []>;
|
|
8
7
|
"no-commit-in-collection-item": import("./lib/rule.js").Rule<"FE_NO_COMMIT_IN_COLLECTION_ITEM", []>;
|
package/dist/button-intent.js
CHANGED
|
@@ -11,7 +11,7 @@ const memberName = (node) => (node.computed ? stringOf(node.property) : node.pro
|
|
|
11
11
|
const GRAMMAR_ENTRY = /^@starci\/grammar(?:\/(?:common|core|offset-pop))?$/;
|
|
12
12
|
const BUTTONS = new Set(["Button", "IconButton"]);
|
|
13
13
|
const DIALOGS = new Set(["AlertDialog", "ConfirmDialog"]);
|
|
14
|
-
const ITEMS = new Set(["
|
|
14
|
+
const ITEMS = new Set(["ListBox.Item", "GridList.Item", "Table.Row", "ListRow"]);
|
|
15
15
|
/** The consuming TypeScript program exposes literal intents and spread properties without executing app code. */
|
|
16
16
|
const typedInfo = (context, node, key) => {
|
|
17
17
|
const services = context.sourceCode.parserServices;
|
|
@@ -238,20 +238,6 @@ const canCoexist = (left, right) => {
|
|
|
238
238
|
return false;
|
|
239
239
|
return [...left.guards].every(([condition, value]) => !right.guards.has(condition) || right.guards.get(condition) === value);
|
|
240
240
|
};
|
|
241
|
-
/** No explicit visual prop: any authored variant, including a known props spread, is an app bypass. */
|
|
242
|
-
const buttonNoVariant = defineRule({
|
|
243
|
-
meta: { type: "problem", docs: { description: "CTA-6: app buttons never choose a variant." }, schema: [], messages: {
|
|
244
|
-
FE_BUTTON_NO_VARIANT: "FE_BUTTON_NO_VARIANT: declare Button intent instead of variant; Grammar resolves the variant from intent and placement.",
|
|
245
|
-
} },
|
|
246
|
-
create(context) {
|
|
247
|
-
return { JSXOpeningElement(opening) {
|
|
248
|
-
if (!isButton(context, opening))
|
|
249
|
-
return;
|
|
250
|
-
for (const node of new Set(propsOf(context, opening, "variant").map((prop) => prop.node)))
|
|
251
|
-
context.report({ node, messageId: "FE_BUTTON_NO_VARIANT" });
|
|
252
|
-
} };
|
|
253
|
-
},
|
|
254
|
-
});
|
|
255
241
|
/** Every app button states its command's intent explicitly. */
|
|
256
242
|
const buttonIntentRequired = defineRule({
|
|
257
243
|
meta: { type: "problem", docs: { description: "CTA-6: every app Button and IconButton declares intent." }, schema: [], messages: {
|
|
@@ -352,7 +338,6 @@ const noCommitInCollectionItem = defineRule({
|
|
|
352
338
|
/** Every intent law is universal across Grammar families and app editions. */
|
|
353
339
|
export const rules = {
|
|
354
340
|
"button-intent-required": buttonIntentRequired,
|
|
355
|
-
"button-no-variant": buttonNoVariant,
|
|
356
341
|
"destructive-confirm-in-dialog": destructiveConfirmInDialog,
|
|
357
342
|
"single-commit-per-render": singleCommitPerRender,
|
|
358
343
|
"no-commit-in-collection-item": noCommitInCollectionItem,
|
package/dist/file-layout.js
CHANGED
|
@@ -216,7 +216,7 @@ export const monorepoTierBelongsToItsSide = defineRule({
|
|
|
216
216
|
meta: {
|
|
217
217
|
type: "problem",
|
|
218
218
|
docs: {
|
|
219
|
-
description: "With two or more apps, the shared package holds leaves, composites and
|
|
219
|
+
description: "With two or more apps, the shared package holds leaves, branches, composites and frames; blocks, overlays, layouts and pages belong to the app that owns the feature.",
|
|
220
220
|
},
|
|
221
221
|
schema: [],
|
|
222
222
|
messages: {
|
|
@@ -312,6 +312,7 @@ export const sourceTierMarkerMatchesFolder = defineRule({
|
|
|
312
312
|
blocks: "block",
|
|
313
313
|
branches: "branch",
|
|
314
314
|
composites: "composite",
|
|
315
|
+
frames: "frame",
|
|
315
316
|
layouts: "layout",
|
|
316
317
|
leaves: "leaf",
|
|
317
318
|
overlays: "overlay",
|
package/dist/index.js
CHANGED
|
@@ -27,6 +27,7 @@ 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 skeletonStatesRecommended, rules as skeletonStatesRules } from "./skeleton-states.js";
|
|
30
31
|
import { recommended as lintEscapeRecommended, rules as lintEscapeRules } from "./lint-escape-hatch.js";
|
|
31
32
|
import { recommended as namingRecommended, rules as namingRules } from "./naming.js";
|
|
32
33
|
import { recommended as nativeControlsRecommended, rules as nativeControlsRules } from "./native-controls.js";
|
|
@@ -73,6 +74,7 @@ const CONTRIBUTIONS = [
|
|
|
73
74
|
{ law: "landmark", rules: landmarkRules, recommended: landmarkRecommended },
|
|
74
75
|
{ law: "lists", rules: listsRules, recommended: listsRecommended },
|
|
75
76
|
{ law: "loading", rules: loadingRules, recommended: loadingRecommended },
|
|
77
|
+
{ law: "skeleton-states", rules: skeletonStatesRules, recommended: skeletonStatesRecommended },
|
|
76
78
|
{ law: "lint-escape-hatch", rules: lintEscapeRules, recommended: lintEscapeRecommended },
|
|
77
79
|
{ law: "naming", rules: namingRules, recommended: namingRecommended },
|
|
78
80
|
{ law: "native-controls", rules: nativeControlsRules, recommended: nativeControlsRecommended },
|
package/dist/lib/config.js
CHANGED
|
@@ -104,8 +104,9 @@ export const SLOT_FREE_RULES = Object.freeze([
|
|
|
104
104
|
"no-native-anchor", "no-inline-parameter-type", "no-hand-rolled-polling", "no-copy-in-call", "no-alert", "no-failure-collapse",
|
|
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
|
-
"
|
|
108
|
-
"
|
|
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",
|
|
109
|
+
"button-intent-required", "destructive-confirm-in-dialog", "single-commit-per-render", "no-commit-in-collection-item",
|
|
109
110
|
]);
|
|
110
111
|
/** The source of a plain front-end tree: its `src/`. */
|
|
111
112
|
export const PLAIN_SOURCE_FILES = Object.freeze(["src/**/*.{ts,tsx}"]);
|
package/dist/lib/scope.d.ts
CHANGED
|
@@ -35,7 +35,7 @@ export declare const kindOfFile: (context: AnyContext) => string | null;
|
|
|
35
35
|
export declare const roleOfFile: (context: AnyContext) => string | null;
|
|
36
36
|
/** Product source: a file of a product tier. */
|
|
37
37
|
export declare const isProductSource: (context: AnyContext) => boolean;
|
|
38
|
-
/** A file of a component tier: slot `fe.components` or `fe.package.ui`, inside one of its layer folders (blocks, composites, branches, leaves). */
|
|
38
|
+
/** A file of a component tier: slot `fe.components` or `fe.package.ui`, inside one of its layer folders (blocks, frames, composites, branches, leaves). */
|
|
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;
|
package/dist/lib/scope.js
CHANGED
|
@@ -41,7 +41,7 @@ export const roleOfFile = (context) => classOf(context).role ?? null;
|
|
|
41
41
|
const PRODUCT_TIERS = new Set(["route", "feature", "components", "hooks", "foundation", "modules", "transport", "package"]);
|
|
42
42
|
/** Product source: a file of a product tier. */
|
|
43
43
|
export const isProductSource = (context) => PRODUCT_TIERS.has(tierOfFile(context));
|
|
44
|
-
/** A file of a component tier: slot `fe.components` or `fe.package.ui`, inside one of its layer folders (blocks, composites, branches, leaves). */
|
|
44
|
+
/** A file of a component tier: slot `fe.components` or `fe.package.ui`, inside one of its layer folders (blocks, frames, composites, branches, leaves). */
|
|
45
45
|
export const isComponentFile = (context) => inSlot(context, "fe.components", "fe.package.ui") && kindOfFile(context) !== null;
|
|
46
46
|
/** 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`. */
|
|
47
47
|
export const isComponentPath = (context, file) => {
|
package/dist/lib/why.d.ts
CHANGED
|
@@ -42,6 +42,26 @@ export declare const why: {
|
|
|
42
42
|
en: string;
|
|
43
43
|
fix: string;
|
|
44
44
|
};
|
|
45
|
+
"no-fill-command-in-header": {
|
|
46
|
+
code: string;
|
|
47
|
+
en: string;
|
|
48
|
+
fix: string;
|
|
49
|
+
};
|
|
50
|
+
"no-selectable-tag-group": {
|
|
51
|
+
code: string;
|
|
52
|
+
en: string;
|
|
53
|
+
fix: string;
|
|
54
|
+
};
|
|
55
|
+
"no-heading-above-card": {
|
|
56
|
+
code: string;
|
|
57
|
+
en: string;
|
|
58
|
+
fix: string;
|
|
59
|
+
};
|
|
60
|
+
"no-table-in-card": {
|
|
61
|
+
code: string;
|
|
62
|
+
en: string;
|
|
63
|
+
fix: string;
|
|
64
|
+
};
|
|
45
65
|
"button-intent-required": {
|
|
46
66
|
code: string;
|
|
47
67
|
en: string;
|
|
@@ -57,27 +77,42 @@ export declare const why: {
|
|
|
57
77
|
en: string;
|
|
58
78
|
fix: string;
|
|
59
79
|
};
|
|
60
|
-
"
|
|
80
|
+
"no-swr-default-array": {
|
|
61
81
|
code: string;
|
|
62
82
|
en: string;
|
|
63
83
|
fix: string;
|
|
64
84
|
};
|
|
65
|
-
"
|
|
85
|
+
"no-foreign-slot-status": {
|
|
66
86
|
code: string;
|
|
67
87
|
en: string;
|
|
68
88
|
fix: string;
|
|
69
89
|
};
|
|
70
|
-
"
|
|
90
|
+
"no-validating-as-loading": {
|
|
71
91
|
code: string;
|
|
72
92
|
en: string;
|
|
73
93
|
fix: string;
|
|
74
94
|
};
|
|
75
|
-
"no-
|
|
95
|
+
"no-placeholder-object": {
|
|
76
96
|
code: string;
|
|
77
97
|
en: string;
|
|
78
98
|
fix: string;
|
|
79
99
|
};
|
|
80
|
-
"
|
|
100
|
+
"no-hand-skeleton": {
|
|
101
|
+
code: string;
|
|
102
|
+
en: string;
|
|
103
|
+
fix: string;
|
|
104
|
+
};
|
|
105
|
+
"destructive-confirm-in-dialog": {
|
|
106
|
+
code: string;
|
|
107
|
+
en: string;
|
|
108
|
+
fix: string;
|
|
109
|
+
};
|
|
110
|
+
"single-commit-per-render": {
|
|
111
|
+
code: string;
|
|
112
|
+
en: string;
|
|
113
|
+
fix: string;
|
|
114
|
+
};
|
|
115
|
+
"no-commit-in-collection-item": {
|
|
81
116
|
code: string;
|
|
82
117
|
en: string;
|
|
83
118
|
fix: string;
|
package/dist/lib/why.js
CHANGED
|
@@ -42,6 +42,26 @@ export const why = {
|
|
|
42
42
|
en: "`<file>` draws a multi-pane frame by hand: a vertical Divider between columns of a flex row, or a flex row of columns inside a joined SurfaceCard.",
|
|
43
43
|
fix: "Compose the columns as Panes of a PaneGrid (areas per breakpoint, collapse=\"drawer\" for a side pane), mounted as Page chrome=\"panes\" on a tabbed app page.",
|
|
44
44
|
},
|
|
45
|
+
"no-fill-command-in-header": {
|
|
46
|
+
code: "FE_COMMAND_FILLS_HEADER",
|
|
47
|
+
en: "`<file>` stretches a command in a page or card header (`PageHeader actions`, `labelEnd`) with `width=\"fill\"`.",
|
|
48
|
+
fix: "A page or pane command hugs its label and sits at the end of its header row: drop `width`. `width=\"fill\"` is for a form submit, an auth panel and a phone action bar.",
|
|
49
|
+
},
|
|
50
|
+
"no-selectable-tag-group": {
|
|
51
|
+
code: "FE_TAG_GROUP_SELECTION",
|
|
52
|
+
en: "`<file>` gives a TagGroup a selection (`selectionMode`, `selectedIds`, `onSelectionChange`) to use it as a filter.",
|
|
53
|
+
fix: "A TagGroup holds values the user entered; peer values that filter a collection are a FilterButtonGroup (sm toggle buttons on one row, the rest behind a +n search list).",
|
|
54
|
+
},
|
|
55
|
+
"no-heading-above-card": {
|
|
56
|
+
code: "FE_HEADING_ABOVE_CARD",
|
|
57
|
+
en: "`<file>` wraps a single SurfaceCard in a Section, or gives the card an ariaLabel that repeats the Section's title.",
|
|
58
|
+
fix: "A card has a label by design: move the title to the card's label, the count to fact, the action to labelEnd and the note to footer.",
|
|
59
|
+
},
|
|
60
|
+
"no-table-in-card": {
|
|
61
|
+
code: "FE_TABLE_IN_CARD",
|
|
62
|
+
en: "`<file>` puts a DataTable inside a SurfaceCard (directly, or in a component of the file that a SurfaceCard encloses).",
|
|
63
|
+
fix: "Plain rows with a few facts are a SurfaceCard kind=\"list\" with the label outside and the note in its footer; a comparison table sits in its section with no card around it.",
|
|
64
|
+
},
|
|
45
65
|
"button-intent-required": {
|
|
46
66
|
code: "FE_BUTTON_INTENT_REQUIRED",
|
|
47
67
|
en: "`<file>` renders a Button or IconButton without the command's intent.",
|
|
@@ -57,10 +77,30 @@ export const why = {
|
|
|
57
77
|
en: "`<file>` re-exports a Grammar component under the app's own name.",
|
|
58
78
|
fix: "Delete the shim and import the component from the Grammar where it is rendered; report the Grammar defect the shim worked around (the op files blocked) so the Grammar fixes it.",
|
|
59
79
|
},
|
|
60
|
-
"
|
|
61
|
-
code: "
|
|
62
|
-
en: "`<file>`
|
|
63
|
-
fix: "
|
|
80
|
+
"no-swr-default-array": {
|
|
81
|
+
code: "FE_SWR_DEFAULT_ARRAY",
|
|
82
|
+
en: "`<file>` folds a read to an empty array and never returns whether that read is loading.",
|
|
83
|
+
fix: "Return the read's isLoading and error with the list, so the view draws a skeleton while loading and an error when it fails, never an empty state.",
|
|
84
|
+
},
|
|
85
|
+
"no-foreign-slot-status": {
|
|
86
|
+
code: "FE_FOREIGN_SLOT_STATUS",
|
|
87
|
+
en: "`<file>` builds a slot from another slot's loading or error.",
|
|
88
|
+
fix: "Build the slot from every read it draws with the one slotOfReads helper, so a key change reloads only the region keyed by it.",
|
|
89
|
+
},
|
|
90
|
+
"no-validating-as-loading": {
|
|
91
|
+
code: "FE_VALIDATING_AS_LOADING",
|
|
92
|
+
en: "`<file>` lets a background refresh (isValidating) drive a loading flag or a status.",
|
|
93
|
+
fix: "Drive the skeleton from isLoading alone; a refresh keeps the content on screen.",
|
|
94
|
+
},
|
|
95
|
+
"no-placeholder-object": {
|
|
96
|
+
code: "FE_PLACEHOLDER_OBJECT",
|
|
97
|
+
en: "`<file>` holds a placeholder object that is the ready data with its lists emptied.",
|
|
98
|
+
fix: "Delete the placeholder; pass a loading flag and let the Grammar component derive the skeleton from the structure it draws.",
|
|
99
|
+
},
|
|
100
|
+
"no-hand-skeleton": {
|
|
101
|
+
code: "FE_HAND_SKELETON",
|
|
102
|
+
en: "`<file>` draws a skeleton by hand.",
|
|
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).",
|
|
64
104
|
},
|
|
65
105
|
"destructive-confirm-in-dialog": {
|
|
66
106
|
code: "FE_DESTRUCTIVE_CONFIRM_IN_DIALOG",
|
|
@@ -77,11 +117,6 @@ export const why = {
|
|
|
77
117
|
en: "`<file>` repeats a commit inside a collection item render.",
|
|
78
118
|
fix: "Use alternative for the item's next action and keep the enclosing view's commit unique.",
|
|
79
119
|
},
|
|
80
|
-
"nested-control-variant": {
|
|
81
|
-
code: "FE_NESTED_VARIANT",
|
|
82
|
-
en: "`<file>` sets a static Grammar field variant, or forces primary Tabs inside a painted surface.",
|
|
83
|
-
fix: "Omit the field or nested Tabs variant so Grammar supplies the default from its surface context.",
|
|
84
|
-
},
|
|
85
120
|
"no-app-surface-paint": {
|
|
86
121
|
code: "FE_GRAMMAR_LAYOUT_ONLY",
|
|
87
122
|
en: "`<file>` sets paint or typography in app CSS instead of composing Grammar's published components and props.",
|
package/dist/loading.js
CHANGED
|
@@ -91,7 +91,7 @@ export const noPlaceholderProp = defineRule({
|
|
|
91
91
|
return;
|
|
92
92
|
for (const specifier of node.specifiers || []) {
|
|
93
93
|
const local = specifier.local && specifier.local.name;
|
|
94
|
-
// A bare `Skeleton` is the
|
|
94
|
+
// A bare `Skeleton` is the leaf a component rests WITH, not a twin of one.
|
|
95
95
|
if (!local || local === "Skeleton" || !TWIN_NAME.test(local))
|
|
96
96
|
continue;
|
|
97
97
|
context.report({ node: specifier, messageId: "import", data: { name: local } });
|
|
@@ -14,6 +14,12 @@ export declare const noPublicFrameCssProps: import("./lib/rule.js").Rule<"css",
|
|
|
14
14
|
export declare const noCssDoorTypeLaundering: import("./lib/rule.js").Rule<"utility", []>;
|
|
15
15
|
/** Props are readonly all the way down: every field, index signature, nested object and collection (FE-TYPING-2). */
|
|
16
16
|
export declare const propsFieldsReadonly: import("./lib/rule.js").Rule<"index" | "field" | "collection", []>;
|
|
17
|
+
/**
|
|
18
|
+
* Children belong to frames. A frame owns a region and draws what its caller places in it; a leaf's only child is its text label; a
|
|
19
|
+
* block, a composite and a branch draw a fixed anatomy from fields and named slots, so nested JSX under them is a type error for every
|
|
20
|
+
* caller (`children?: never`). The tier is the layer folder of the unit (FE-FUNCTION-2).
|
|
21
|
+
*/
|
|
22
|
+
export declare const tierChildren: import("./lib/rule.js").Rule<"leaf" | "none" | "withChildren", []>;
|
|
17
23
|
export declare const rules: Rules;
|
|
18
24
|
/** All rules in this law are mechanical errors once adopted. */
|
|
19
25
|
export declare const recommended: Recommended;
|
package/dist/props-and-slots.js
CHANGED
|
@@ -307,6 +307,46 @@ export const propsFieldsReadonly = defineRule({
|
|
|
307
307
|
};
|
|
308
308
|
},
|
|
309
309
|
});
|
|
310
|
+
/**
|
|
311
|
+
* Children belong to frames. A frame owns a region and draws what its caller places in it; a leaf's only child is its text label; a
|
|
312
|
+
* block, a composite and a branch draw a fixed anatomy from fields and named slots, so nested JSX under them is a type error for every
|
|
313
|
+
* caller (`children?: never`). The tier is the layer folder of the unit (FE-FUNCTION-2).
|
|
314
|
+
*/
|
|
315
|
+
export const tierChildren = defineRule({
|
|
316
|
+
meta: {
|
|
317
|
+
type: "problem",
|
|
318
|
+
docs: { description: "Only a frame takes children; a leaf takes its text label; blocks, composites and branches take none." },
|
|
319
|
+
schema: [],
|
|
320
|
+
messages: {
|
|
321
|
+
none: "A {{layer}} unit takes no children: name the slot for its role, or move the unit to components/frames when its job is to host content.",
|
|
322
|
+
leaf: "A leaf takes its text label as children: string, never a node: name the slot for the node, or move the unit to components/frames.",
|
|
323
|
+
withChildren: "PropsWithChildren gives a {{layer}} unit nested JSX: declare children?: never, or children: string for a leaf.",
|
|
324
|
+
},
|
|
325
|
+
},
|
|
326
|
+
create(context) {
|
|
327
|
+
if (!isComponentFile(context))
|
|
328
|
+
return {};
|
|
329
|
+
const layer = kindOfFile(context);
|
|
330
|
+
if (layer === null || layer === "frames")
|
|
331
|
+
return {};
|
|
332
|
+
const text = layer === "leaves";
|
|
333
|
+
return {
|
|
334
|
+
TSPropertySignature(node) {
|
|
335
|
+
if (propertyName(node) !== "children")
|
|
336
|
+
return;
|
|
337
|
+
const type = node.typeAnnotation?.typeAnnotation;
|
|
338
|
+
if (type?.type === "TSNeverKeyword" || (text && type?.type === "TSStringKeyword"))
|
|
339
|
+
return;
|
|
340
|
+
context.report({ node, messageId: text ? "leaf" : "none", data: { layer: layer.replace(/s$/, "") } });
|
|
341
|
+
},
|
|
342
|
+
TSTypeReference(node) {
|
|
343
|
+
const name = node.typeName.type === "Identifier" ? node.typeName.name : node.typeName.type === "TSQualifiedName" ? node.typeName.right.name : null;
|
|
344
|
+
if (name === "PropsWithChildren")
|
|
345
|
+
context.report({ node, messageId: "withChildren", data: { layer: layer.replace(/s$/, "") } });
|
|
346
|
+
},
|
|
347
|
+
};
|
|
348
|
+
},
|
|
349
|
+
});
|
|
310
350
|
export const rules = {
|
|
311
351
|
"no-inline-parameter-type": noInlineParameterType,
|
|
312
352
|
"props-fields-readonly": propsFieldsReadonly,
|
|
@@ -315,6 +355,7 @@ export const rules = {
|
|
|
315
355
|
"no-public-classname-prop": noPublicClassNameProp,
|
|
316
356
|
"no-public-frame-css-props": noPublicFrameCssProps,
|
|
317
357
|
"no-css-door-type-laundering": noCssDoorTypeLaundering,
|
|
358
|
+
"tier-children": tierChildren,
|
|
318
359
|
};
|
|
319
360
|
/** All rules in this law are mechanical errors once adopted. */
|
|
320
361
|
export const recommended = Object.fromEntries(Object.keys(rules).map((name) => [`starci-fe/${name}`, "error"]));
|
package/dist/shape-slot.js
CHANGED
|
@@ -146,19 +146,19 @@ export const exhaustiveShapeSwitch = defineRule({
|
|
|
146
146
|
},
|
|
147
147
|
});
|
|
148
148
|
// -- SLOT-1 -----------------------------------------------------------------------------------------
|
|
149
|
-
/** The data-status props only SlotView takes; a lower unit gets `
|
|
150
|
-
const STATUS_PROPS = new Set(["
|
|
149
|
+
/** The data-status props only SlotView takes; a lower unit gets `isLoading` (the one flag a leaf, branch or composite knows) and nothing else. */
|
|
150
|
+
const STATUS_PROPS = new Set(["isError", "isForbidden", "isEmpty", "isValidating", "isFailed", "failure"]);
|
|
151
151
|
/** The units that own the status recipe: the status view itself and a route's loading page. */
|
|
152
152
|
const isStatusOwner = (name) => name === "SlotView" || /LoadingPage$/.test(name);
|
|
153
153
|
/** A unit below the status view takes no data-status prop. */
|
|
154
154
|
export const noDataStatusProp = defineRule({
|
|
155
155
|
meta: {
|
|
156
156
|
type: "problem",
|
|
157
|
-
docs: { description: "A unit other than SlotView (and a route LoadingPage) takes no `
|
|
157
|
+
docs: { description: "A unit other than SlotView (and a route LoadingPage) takes no `isError`, `isForbidden`, `isEmpty`, `isValidating`, `isFailed` or `failure` prop; a leaf, branch or composite takes only `isLoading`." },
|
|
158
158
|
schema: [],
|
|
159
159
|
messages: {
|
|
160
|
-
status: "`{{key}}` is a data status handed to `{{unit}}`. A read's status is a DataSlot rendered through SlotView in the region's place; the parts below it take `
|
|
161
|
-
leaf: "`{{key}}` is a data status handed to the leaf `{{unit}}`. A leaf takes only `
|
|
160
|
+
status: "`{{key}}` is a data status handed to `{{unit}}`. A read's status is a DataSlot rendered through SlotView in the region's place; the parts below it take `isLoading` and rest as themselves.",
|
|
161
|
+
leaf: "`{{key}}` is a data status handed to the leaf `{{unit}}`. A leaf takes only `isLoading`: SlotView passes it while the region waits.",
|
|
162
162
|
},
|
|
163
163
|
},
|
|
164
164
|
create(context) {
|
|
@@ -180,7 +180,7 @@ export const noDataStatusProp = defineRule({
|
|
|
180
180
|
/** A data hook of `hooks/swr`: `useQueryOrdersSwr`, `useGetHandoffSwr`. */
|
|
181
181
|
const SWR_HOOK = /^use[A-Za-z0-9]*Swr$/;
|
|
182
182
|
/** The builders that fold a read into a DataSlot (`hooks/slot/slot.shared.ts`). */
|
|
183
|
-
const SLOT_BUILDER = /^(?:slotOfSwr|mapSlot|loadingSlot|toSlot)$/;
|
|
183
|
+
const SLOT_BUILDER = /^(?:slotOfSwr|slotOfReads|useSlotOfReads|mapSlot|loadingSlot|toSlot)$/;
|
|
184
184
|
/** A region that reads data hands its Base a DataSlot, and the Base renders it through SlotView. */
|
|
185
185
|
export const regionReadsThroughSlot = defineRule({
|
|
186
186
|
meta: {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type Recommended } from "./lib/rule.js";
|
|
2
|
+
/** Published with the Grammar `isLoading` flag: the Grammar components draw their own loading structure. */
|
|
3
|
+
export declare const noHandSkeleton: import("./lib/rule.js").Rule<"hand", []>;
|
|
4
|
+
/** The rules this law contributes to the plugin. */
|
|
5
|
+
export declare const rules: {
|
|
6
|
+
"no-swr-default-array": import("./lib/rule.js").Rule<"fold", []>;
|
|
7
|
+
"no-foreign-slot-status": import("./lib/rule.js").Rule<"foreign", []>;
|
|
8
|
+
"no-validating-as-loading": import("./lib/rule.js").Rule<"validating", []>;
|
|
9
|
+
"no-placeholder-object": import("./lib/rule.js").Rule<"placeholder", []>;
|
|
10
|
+
"no-hand-skeleton": import("./lib/rule.js").Rule<"hand", []>;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* The level this law asks for: every rule at error. `no-hand-skeleton` joined when the Grammar took the loading shape (`isLoading` on
|
|
14
|
+
* Section, the cards, ListBox, Timeline, DataTable, MessageBubble and PaneSection, with the `error` slot).
|
|
15
|
+
*/
|
|
16
|
+
export declare const recommended: Recommended;
|