@starci/eslint-canon-fe 9.8.5 → 9.9.2

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.
Files changed (39) hide show
  1. package/CHANGELOG.md +24 -3
  2. package/README.md +2 -4
  3. package/dist/button-intent.d.ts +0 -1
  4. package/dist/button-intent.js +1 -16
  5. package/dist/file-layout.js +2 -1
  6. package/dist/index.js +2 -0
  7. package/dist/lib/config.js +3 -2
  8. package/dist/lib/scope.d.ts +1 -1
  9. package/dist/lib/scope.js +1 -1
  10. package/dist/lib/why.d.ts +20 -5
  11. package/dist/lib/why.js +26 -11
  12. package/dist/loading.js +1 -1
  13. package/dist/props-and-slots.d.ts +6 -0
  14. package/dist/props-and-slots.js +41 -0
  15. package/dist/shape-slot.js +6 -6
  16. package/dist/skeleton-states.d.ts +16 -0
  17. package/dist/skeleton-states.js +230 -0
  18. package/dist/surface-rules.d.ts +2 -5
  19. package/dist/surface-rules.js +28 -121
  20. package/dist/ui-rulings.js +1 -1
  21. package/docs/button-intent.md +0 -1
  22. package/docs/size-and-state-budget.md +1 -1
  23. package/docs/surface-rules.md +17 -32
  24. package/package.json +1 -1
  25. package/runtime/bundle.json +4 -0
  26. package/runtime/knowledge/hfs/canon-pins.yaml +7 -7
  27. package/runtime/knowledge/hfs/slots.yaml +5 -5
  28. package/runtime/knowledge/patterns/fe/folder.yaml +1 -1
  29. package/runtime/scripts/hfs/architecture/clones.mjs +12 -2
  30. package/runtime/scripts/hfs/architecture/component-tiers.mjs +161 -0
  31. package/runtime/scripts/hfs/architecture/config.mjs +1 -1
  32. package/runtime/scripts/hfs/architecture/frontend-world.mjs +1 -1
  33. package/runtime/scripts/hfs/architecture/frontend.mjs +2 -2
  34. package/runtime/scripts/hfs/architecture/index.mjs +6 -0
  35. package/runtime/scripts/hfs/architecture/render-graph.mjs +449 -0
  36. package/runtime/scripts/hfs/architecture/render-prop-across-server.mjs +58 -0
  37. package/runtime/scripts/hfs/architecture/surface-nesting.mjs +127 -0
  38. package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +12 -5
  39. package/runtime/scripts/hfs/slot-manifest-shape.mjs +16 -1
@@ -44,6 +44,7 @@
44
44
  "scripts/hfs/architecture/client-reaches-server.mjs",
45
45
  "scripts/hfs/architecture/clones.mjs",
46
46
  "scripts/hfs/architecture/config-unread.mjs",
47
+ "scripts/hfs/architecture/component-tiers.mjs",
47
48
  "scripts/hfs/architecture/config.mjs",
48
49
  "scripts/hfs/architecture/connection-map.mjs",
49
50
  "scripts/hfs/architecture/constructor-deps.mjs",
@@ -96,6 +97,8 @@
96
97
  "scripts/hfs/architecture/reachability.mjs",
97
98
  "scripts/hfs/architecture/register-once.mjs",
98
99
  "scripts/hfs/architecture/registration.mjs",
100
+ "scripts/hfs/architecture/render-graph.mjs",
101
+ "scripts/hfs/architecture/render-prop-across-server.mjs",
99
102
  "scripts/hfs/architecture/required-files.mjs",
100
103
  "scripts/hfs/architecture/route-files-thin.mjs",
101
104
  "scripts/hfs/architecture/schema-owner.mjs",
@@ -109,6 +112,7 @@
109
112
  "scripts/hfs/architecture/sql-token-forms.mjs",
110
113
  "scripts/hfs/architecture/sql-tokens.mjs",
111
114
  "scripts/hfs/architecture/staged-consumers.mjs",
115
+ "scripts/hfs/architecture/surface-nesting.mjs",
112
116
  "scripts/hfs/architecture/surface.mjs",
113
117
  "scripts/hfs/architecture/symbols.mjs",
114
118
  "scripts/hfs/architecture/test-world-files.mjs",
@@ -31,42 +31,42 @@ 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.20.2
34
+ version: 0.21.2
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.11.6
41
+ version: 3.11.9
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.8.5
48
+ version: 9.9.2
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.20.2
55
+ version: 0.21.2
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.20.2
62
+ version: 0.21.2
63
63
  group: starci
64
64
  install: registry
65
65
  side: fe
66
66
  source: packages/eslint-grammar-offset-pop/package.json
67
67
  why: 'the Offset Pop Grammar component API lint, versioned with @starci/grammar; its shared component contract comes from the StarCi family lint without copying rules.'
68
68
  '@starci/stylelint-canon':
69
- version: 2.4.0
69
+ version: 2.5.0
70
70
  group: starci
71
71
  install: registry
72
72
  side: fe
@@ -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: 4.16.5
109
+ version: 4.17.2
110
110
  group: starci
111
111
  install: registry
112
112
  side: both
@@ -196,7 +196,7 @@ tiers:
196
196
  feature: {mayImport: [feature, components, hooks, foundation, modules, package], mayImportKinds: {feature: {from: [pages, layouts], to: [overlays]}}} # pages and layouts compose overlays and blocks; data goes through hooks/swr
197
197
  # A component never reaches data: config, routes, i18n and types are foundation; every other module and the api
198
198
  # transport are reached through a hook (owner decision 2026-09-29).
199
- components: {mayImport: [components, hooks, foundation, package], lowerLayerOnly: true} # blocks, composites, branches, leaves
199
+ components: {mayImport: [components, hooks, foundation, package], lowerLayerOnly: true} # blocks, frames, composites, branches, leaves
200
200
  # Across domains a hook is imported only through the other domain's public index (hooks/<domain>/index.ts), never deep.
201
201
  hooks: {mayImport: [hooks, foundation, modules, transport, package], mayImportSlots: {transport: {from: [fe.hooks.swr]}}} # only hooks/swr imports an operation
202
202
  foundation: {mayImport: [foundation, package], acyclic: true} # modules/{config,i18n,routes,types}
@@ -2227,12 +2227,12 @@ slots:
2227
2227
  - id: fe.components
2228
2228
  since: 3.1.0
2229
2229
  profiles: [fe]
2230
- path: "apps/<app>/src/components/{blocks,composites,branches,leaves}/<name>/"
2230
+ path: "apps/<app>/src/components/{blocks,frames,composites,branches,leaves}/<name>/"
2231
2231
  presence: optional
2232
2232
  tracked: tracked
2233
2233
  tier: components
2234
2234
  owner: true
2235
- layers: [blocks, composites, branches, leaves] # a layer imports only the layers after it
2235
+ layers: [blocks, frames, composites, branches, leaves] # a layer imports only the layers after it
2236
2236
  requires: [index.tsx]
2237
2237
  allows: [index.tsx, component.tsx, classNames.ts]
2238
2238
  roles: {entry: index.tsx, drawing: component.tsx, styles: classNames.ts} # the connected entry, the pure drawing half and the styling declarations of a component owner
@@ -2575,12 +2575,12 @@ slots:
2575
2575
  tracked: tracked
2576
2576
  tier: package
2577
2577
  owner: true
2578
- kinds: [composites, branches, leaves] # the layer folders under src/ (a package owns no blocks)
2578
+ kinds: [frames, composites, branches, leaves] # the layer folders under src/ (a package owns no blocks)
2579
2579
  roles: {entry: index.tsx, drawing: component.tsx, styles: classNames.ts}
2580
2580
  requires: [package.json, src/index.ts, tsconfig.json]
2581
2581
  tests: none
2582
2582
  budget: {component.tsx: 300, index.tsx: 200, dataHooks: 6, useState: 6}
2583
- why: built to dist with explicit named exports; only composites, branches and leaves; no unused export; no colour literal
2583
+ why: built to dist with explicit named exports; only frames, composites, branches and leaves; no unused export; no colour literal
2584
2584
  rules: [FE_PACKAGE_SHAPE, HFS_UNUSED_EXPORT, FE_STYLE_TOKEN_ONLY]
2585
2585
  - id: fe.package.api
2586
2586
  since: 3.1.0
@@ -364,7 +364,7 @@ rules:
364
364
  cases:
365
365
  - id: case-1
366
366
  when: "A Grammar component (runtime package)"
367
- write: "packages/grammar/src/core/<primitive|composite|branch|composition>/<Name>/index.tsx"
367
+ write: "packages/grammar/src/core/<leaf|branch|composite|frame>/<Name>/index.tsx"
368
368
  - id: case-2
369
369
  when: "A Grammar component's class strings and spec (runtime package)"
370
370
  write: "sibling classNames.ts and index.spec.tsx"
@@ -255,9 +255,19 @@ const cloneCoverage = (entries, blocks, N, T) => {
255
255
  return { status: 'checked', minLines: N, minTokens: T, files: entries.length, cloneBlocks: blocks.length, ownersInvolved: owners.size, duplicatedLines,
256
256
  ...(blocks.length > MAX_VIOLATIONS ? { truncated: true } : {}) };
257
257
  };
258
+ /**
259
+ * The threshold of the run. The app root (profile app) answers `ruleParams()` with null: its rule parameters are its sides', and
260
+ * duplicateBlock is the one value they share (ruleParams.common), so the root reads it from a side. A run on the app root
261
+ * (the root has its own tsconfig) threw "Cannot destructure property 'lines'" here before.
262
+ */
263
+ function duplicateBlockOf(resolver) {
264
+ const own = resolver.ruleParams()?.duplicateBlock;
265
+ const block = own ?? Object.values(resolver.sides ?? {}).map(side => side.ruleParams().duplicateBlock).find(found => found !== undefined);
266
+ // reason: the rule-params validators require duplicateBlock on every profile, so a side that runs this check carries it
267
+ return block;
268
+ }
258
269
  export function checkClones({ context, graph }) {
259
- // reason: the rule-params validators require duplicateBlock on every profile that runs this check
260
- const { lines: N, tokens: T } = graph.resolver.ruleParams()?.duplicateBlock;
270
+ const { lines: N, tokens: T } = duplicateBlockOf(graph.resolver);
261
271
  const ts = context.ts ?? context.loaded.ts;
262
272
  const entries = [];
263
273
  for (const rel of [...graph.files.keys()].sort(byCodeUnit)) {
@@ -0,0 +1,161 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { byCodeUnit } from '../../lib/list.mjs';
4
+ /**
5
+ * The component tier ladder, checked on the source of a component library and of a component tree.
6
+ *
7
+ * FE_GRAMMAR_TIER_DIRECTION a grammar component imports a tier above it (the ladder leaf, branch, composite, frame: a tier imports
8
+ * nothing above itself), type-only imports included, through a shared untiered module as well;
9
+ * `tiers.grammar` of knowledge/hfs/slots.yaml is the matrix
10
+ * FE_GRAMMAR_TIER_STAMP a `data-tier` stamp names a tier other than the folder the file sits in (`atom`, `primitive` and
11
+ * `composition` are not tiers)
12
+ * FE_TIER_DIRECTION (layers) a component imports a layer at or above its own: blocks, frames, composites, branches, leaves
13
+ *
14
+ * A library is a folder holding `src/core/<tier>/<Name>/`; a tree is a folder holding `components/<layer>/<Unit>/` that no hfs
15
+ * front-end slot already judges (an app without hfs.json, such as the harness desktop app).
16
+ */
17
+ export const COMPONENT_TIER_RULE_IDS = ['FE_GRAMMAR_TIER_DIRECTION', 'FE_GRAMMAR_TIER_STAMP'];
18
+ const SKIPPED = new Set(['node_modules', '.git', 'dist', '.next', 'storybook-static', 'proof']);
19
+ const SOURCE = /\.(?:ts|tsx)$/u;
20
+ const TEST_OR_STORY = /\.(?:stories|spec|test)\.[jt]sx?$/u;
21
+ const IMPORT = /(?:\bimport|\bexport)\b[^'"`;]*?\bfrom\s*['"]([^'"]+)['"]|\bimport\s*['"]([^'"]+)['"]|\bimport\(\s*['"]([^'"]+)['"]\s*\)/gu;
22
+ const STAMP = /["']?data-tier["']?\s*[=:]\s*\{?\s*["']([a-z-]+)["']/gu;
23
+ const LIBRARY_FILE = /^(.*?)src\/core\/(leaf|branch|composite|frame)\/([^/]+)\//u;
24
+ function walk(root, dir = '') {
25
+ const out = [];
26
+ for (const entry of fs.readdirSync(path.join(root, dir), { withFileTypes: true })) {
27
+ if (SKIPPED.has(entry.name))
28
+ continue;
29
+ const rel = dir === '' ? entry.name : `${dir}/${entry.name}`;
30
+ if (entry.isDirectory())
31
+ out.push(...walk(root, rel));
32
+ else if (SOURCE.test(entry.name) && !entry.name.endsWith('.d.ts') && !TEST_OR_STORY.test(entry.name))
33
+ out.push(rel);
34
+ }
35
+ return out;
36
+ }
37
+ const lineOf = (text, index) => text.slice(0, index).split('\n').length;
38
+ /** The file an import specifier names, as a repo-relative path in `files`, or null (a package, or a file outside the walk). */
39
+ function resolveImport(files, from, specifier) {
40
+ if (!specifier.startsWith('.'))
41
+ return null;
42
+ const base = path.posix.normalize(path.posix.join(path.posix.dirname(from), specifier)).replace(/\.(?:js|mjs|cjs)$/u, '');
43
+ for (const candidate of [`${base}.ts`, `${base}.tsx`, `${base}/index.ts`, `${base}/index.tsx`, base])
44
+ if (files.has(candidate))
45
+ return candidate;
46
+ return null;
47
+ }
48
+ function edgesOf(root, files, cache, file) {
49
+ const cached = cache.get(file);
50
+ if (cached)
51
+ return cached;
52
+ const text = fs.readFileSync(path.join(root, ...file.split('/')), 'utf8');
53
+ const edges = [];
54
+ for (const match of text.matchAll(IMPORT)) {
55
+ const specifier = match[1] ?? match[2] ?? match[3] ?? '';
56
+ const to = resolveImport(files, file, specifier);
57
+ if (to !== null)
58
+ edges.push({ to, line: lineOf(text, match.index ?? 0), specifier });
59
+ }
60
+ const entry = { text, edges };
61
+ cache.set(file, entry);
62
+ return entry;
63
+ }
64
+ /** The tier (the folder under `src/core`) and the component of a grammar source file, or null for a file outside the ladder. */
65
+ const gradeOf = (file) => {
66
+ const match = LIBRARY_FILE.exec(file);
67
+ return match === null ? null : { tier: match[2], unit: `${match[1]}${match[2]}/${match[3]}` };
68
+ };
69
+ /**
70
+ * Check the component libraries under `root` against `matrix` (`tiers.grammar`). Not applicable when no folder holds
71
+ * `src/core/<tier>/<Name>/`. A file outside the tier folders (a shared helper under `src/core`) is not a tier, but an import that
72
+ * reaches a higher tier through it is the importing tier's edge.
73
+ */
74
+ export function checkGrammarLibraries({ root, matrix }) {
75
+ const all = walk(root).filter(file => /(?:^|\/)src\//u.test(file));
76
+ const library = all.filter(file => gradeOf(file) !== null);
77
+ if (!library.length)
78
+ return { violations: [], coverage: { status: 'not-applicable' } };
79
+ const files = new Set(all);
80
+ const cache = new Map();
81
+ const violations = [];
82
+ const tiers = new Set(Object.keys(matrix));
83
+ let edgesChecked = 0;
84
+ let stamps = 0;
85
+ for (const file of library.sort(byCodeUnit)) {
86
+ const from = gradeOf(file);
87
+ const { text, edges } = edgesOf(root, files, cache, file);
88
+ for (const stamp of text.matchAll(STAMP)) {
89
+ stamps += 1;
90
+ if (stamp[1] !== from.tier) {
91
+ violations.push({ ruleId: 'FE_GRAMMAR_TIER_STAMP', path: file, line: lineOf(text, stamp.index ?? 0), column: 1,
92
+ message: `${file} stamps data-tier="${stamp[1]}" and sits in the ${from.tier} folder: the stamp is the folder (${[...tiers].join(', ')}).` });
93
+ }
94
+ }
95
+ // Follow shared untiered modules, so a leaf cannot reach a frame by one hop of indirection.
96
+ const queue = edges.map(edge => ({ edge, via: [] }));
97
+ const seen = new Set();
98
+ for (let next = queue.shift(); next !== undefined; next = queue.shift()) {
99
+ const { edge, via } = next;
100
+ const target = gradeOf(edge.to);
101
+ if (target === null) {
102
+ if (seen.has(edge.to) || !edge.to.includes('/src/') && !edge.to.startsWith('src/'))
103
+ continue;
104
+ seen.add(edge.to);
105
+ for (const onward of edgesOf(root, files, cache, edge.to).edges)
106
+ queue.push({ edge: { ...onward, line: edge.line }, via: [...via, edge.to] });
107
+ continue;
108
+ }
109
+ if (target.unit === from.unit)
110
+ continue;
111
+ edgesChecked += 1;
112
+ if ((matrix[from.tier]?.mayImport ?? []).includes(target.tier))
113
+ continue;
114
+ violations.push({ ruleId: 'FE_GRAMMAR_TIER_DIRECTION', path: file, line: edge.line, column: 1, specifier: edge.specifier, resolvedPath: edge.to,
115
+ message: `${file} (${from.tier}) imports ${edge.to} (${target.tier})${via.length ? ` through ${via.join(' -> ')}` : ''}: a ${from.tier} may import only ${(matrix[from.tier]?.mayImport ?? []).join(', ') || 'nothing'}. Type-only imports count.` });
116
+ }
117
+ }
118
+ const unique = new Map(violations.map(violation => [`${violation.ruleId}|${violation.path}|${violation.line}|${violation.resolvedPath ?? ''}`, violation]));
119
+ return { violations: [...unique.values()], coverage: { status: 'checked', files: library.length, edgesChecked, stamps } };
120
+ }
121
+ const LAYER_FILE = /^(.*?)components\/([^/]+)\/([^/]+)\//u;
122
+ /**
123
+ * Check a component tree under `root` that no hfs slot judges: a component imports only the layers after its own (`layers` in order,
124
+ * the first layer imports all below it). The layers are the folder names of slot fe.components.
125
+ */
126
+ export function checkComponentLayers({ root, layers }) {
127
+ const all = walk(root).filter(file => /(?:^|\/)src\//u.test(file));
128
+ const placed = (file) => {
129
+ const match = LAYER_FILE.exec(file);
130
+ const layer = match === null ? -1 : layers.indexOf(match[2]);
131
+ return match === null || layer < 0 ? null : { layer, unit: `${match[1]}components/${match[2]}/${match[3]}` };
132
+ };
133
+ const tree = all.filter(file => placed(file) !== null);
134
+ if (!tree.length)
135
+ return { violations: [], coverage: { status: 'not-applicable' } };
136
+ const files = new Set(all);
137
+ const cache = new Map();
138
+ const violations = [];
139
+ let edgesChecked = 0;
140
+ for (const file of tree.sort(byCodeUnit)) {
141
+ const from = placed(file);
142
+ for (const edge of edgesOf(root, files, cache, file).edges) {
143
+ const target = placed(edge.to);
144
+ if (target === null || target.unit === from.unit)
145
+ continue;
146
+ edgesChecked += 1;
147
+ if (target.layer > from.layer)
148
+ continue;
149
+ violations.push({ ruleId: 'FE_TIER_DIRECTION', path: file, line: edge.line, column: 1, specifier: edge.specifier, resolvedPath: edge.to,
150
+ message: `${file}: a ${layers[from.layer]} component may import only the layers after it (${layers.slice(from.layer + 1).join(', ') || 'none'}); it imports a ${layers[target.layer]}. Type-only imports count.` });
151
+ }
152
+ }
153
+ return { violations, coverage: { status: 'checked', files: tree.length, edgesChecked } };
154
+ }
155
+ /** The machine check: the grammar libraries of the repository, judged by `tiers.grammar` of the slot manifest. */
156
+ export function checkGrammarTiers({ config }) {
157
+ const matrix = config.hfs.manifest.tiers.grammar;
158
+ if (matrix === undefined)
159
+ return { violations: [], coverage: { status: 'not-applicable' } };
160
+ return checkGrammarLibraries({ root: config.root, matrix });
161
+ }
@@ -208,7 +208,7 @@ function inferredLayout(root, workspaces) {
208
208
  // A design-system workspace whose src carries tier dirs (leaves/branches/…) IS a component root;
209
209
  // collecting its src/components too would nest two roots in one role and fail the disjoint check.
210
210
  const tierSources = new Set(workspaces.map(prefix => `${prefix}/src`)
211
- .filter(source => ['leaves', 'branches', 'blocks', 'overlays', 'composites', 'layouts', 'product-shells', 'pages']
211
+ .filter(source => ['leaves', 'branches', 'blocks', 'frames', 'overlays', 'composites', 'layouts', 'product-shells', 'pages']
212
212
  .some(tier => existingDirectory(root, `${source}/${tier}`))));
213
213
  const components = collect('src/components')
214
214
  .filter(relative => !tierSources.has(path.posix.dirname(relative)));
@@ -5,7 +5,7 @@ import { WorldRenderAnalysis } from './frontend-world-render.mjs';
5
5
  const WORLD_NAVIGATION_CALLS = new Set(['useParams', 'usePathname', 'useRouter', 'useSearchParams', 'useSelectedLayoutSegment', 'useSelectedLayoutSegments']);
6
6
  const WORLD_INTL_CALLS = new Set(['useLocale', 'useMessages', 'useNow', 'useTimeZone', 'useTranslations']);
7
7
  const WORLD_SWR_CALLS = new Set(['default', 'useSWR', 'useSWRConfig', 'useSWRImmutable', 'useSWRMutation']);
8
- const LOWER_COMPONENT_TIERS = new Set(['composites', 'branches', 'leaves']);
8
+ const LOWER_COMPONENT_TIERS = new Set(['frames', 'composites', 'branches', 'leaves']);
9
9
  function selectedSymbol(ts, checker, expression) {
10
10
  const selected = unwrapExpression(ts, expression);
11
11
  if (!selected)
@@ -8,7 +8,7 @@ import { checkWorldRenderBoundaries } from './frontend-world.mjs';
8
8
  import { checkGrammar } from './frontend-grammar.mjs';
9
9
  import { checkRoute } from './frontend-routing.mjs';
10
10
  const FEATURE_TIERS = new Set(['pages', 'layouts', 'overlays']);
11
- const COMPONENT_TIERS = new Set(['blocks', 'composites', 'branches', 'leaves', 'providers']);
11
+ const COMPONENT_TIERS = new Set(['blocks', 'frames', 'composites', 'branches', 'leaves', 'providers']);
12
12
  function absolute(root, relative) {
13
13
  return path.resolve(root, ...relative.split('/'));
14
14
  }
@@ -82,7 +82,7 @@ function checkFrontendSourceLayout(config, sourceFile, roots) {
82
82
  valid = located.relative.includes('/') || roleEntry(located.relative);
83
83
  return valid ? [] : [{ ruleId: 'FE_SOURCE_LAYOUT_INVALID', path: relativePath(config.root, sourceFile.fileName), line: 1, column: 1,
84
84
  message: { features: 'Frontend features contain only pages, layouts and overlays plus an optional root public entry.',
85
- components: 'Frontend components contain only blocks, composites, branches, leaves and providers plus an optional root public entry.',
85
+ components: 'Frontend components contain only blocks, frames, composites, branches, leaves and providers plus an optional root public entry.',
86
86
  hooks: 'Every authored custom hook is grouped below a domain folder under hooks; only the root public entry may sit directly under hooks.' }[located.role] }];
87
87
  }
88
88
  function declarationName(ts, declaration) {
@@ -42,9 +42,12 @@ import { checkBackgroundUnowned, BACKGROUND_UNOWNED_RULE_IDS } from './backgroun
42
42
  import { checkTransportOwner, TRANSPORT_OWNER_RULE_IDS } from './transport-owner.mjs';
43
43
  import { checkRouteFilesThin, ROUTE_FILES_THIN_RULE_IDS } from './route-files-thin.mjs';
44
44
  import { checkHooksAreHooks, HOOKS_ARE_HOOKS_RULE_IDS } from './hooks-are-hooks.mjs';
45
+ import { checkRenderPropAcrossServer, RENDER_PROP_ACROSS_SERVER_RULE_IDS } from './render-prop-across-server.mjs';
46
+ import { checkSurfaceNesting, SURFACE_NESTING_RULE_IDS } from './surface-nesting.mjs';
45
47
  import { checkClientReachesServer, CLIENT_REACHES_SERVER_RULE_IDS } from './client-reaches-server.mjs';
46
48
  import { checkCrossAppDuplicate, CROSS_APP_DUPLICATE_RULE_IDS } from './cross-app-duplicate.mjs';
47
49
  import { checkPackageShape, PACKAGE_SHAPE_RULE_IDS } from './package-shape.mjs';
50
+ import { COMPONENT_TIER_RULE_IDS, checkGrammarTiers } from './component-tiers.mjs';
48
51
  import { checkFeSlotAllows, FE_SLOT_ALLOWS_RULE_IDS } from './fe-slot-allows.mjs';
49
52
  import { checkI18nKeys, I18N_KEYS_RULE_IDS } from './i18n-keys.mjs';
50
53
  import { checkDocLanguage, DOC_LANGUAGE_RULE_IDS } from './doc-language.mjs';
@@ -92,10 +95,13 @@ const FRONTEND_MACHINE = {
92
95
  hooksAreHooks: [checkHooksAreHooks, HOOKS_ARE_HOOKS_RULE_IDS],
93
96
  packageShape: [checkPackageShape, PACKAGE_SHAPE_RULE_IDS],
94
97
  clientReachesServer: [checkClientReachesServer, CLIENT_REACHES_SERVER_RULE_IDS],
98
+ surfaceNesting: [checkSurfaceNesting, SURFACE_NESTING_RULE_IDS],
99
+ renderPropAcrossServer: [checkRenderPropAcrossServer, RENDER_PROP_ACROSS_SERVER_RULE_IDS],
95
100
  crossAppDuplicate: [checkCrossAppDuplicate, CROSS_APP_DUPLICATE_RULE_IDS],
96
101
  feSlotAllows: [checkFeSlotAllows, FE_SLOT_ALLOWS_RULE_IDS],
97
102
  i18nKeys: [checkI18nKeys, I18N_KEYS_RULE_IDS],
98
103
  serverActions: [checkServerActions, SERVER_ACTION_RULE_IDS],
104
+ grammarTiers: [checkGrammarTiers, COMPONENT_TIER_RULE_IDS],
99
105
  };
100
106
  /** Errors the machine reports when a program cannot be built or an edge cannot be proven: it cannot judge, so it refuses. */
101
107
  const COMMON_REFUSAL_RULE_IDS = [