@starci/eslint-canon-fe 8.4.43 → 9.0.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 +14 -1
- package/README.md +36 -84
- package/brand-values.mjs +1 -1
- package/docs/hooks-folder.md +5 -3
- package/docs/native-controls.md +1 -5
- package/docs/next-conventions.md +8 -8
- package/docs/size-and-state-budget.md +22 -0
- package/docs/surface-rules.md +11 -9
- package/drawing.mjs +4 -66
- package/hooks-folder.mjs +5 -3
- package/index.mjs +7 -25
- package/landmark.mjs +0 -3
- package/lib/class-text.mjs +29 -0
- package/lib/config.mjs +4 -6
- package/lib/drawing-scope.mjs +15 -0
- package/lib/typed-class-values.mjs +10 -1
- package/lib/why.mjs +10 -40
- package/next-conventions.mjs +33 -16
- package/package.json +4 -3
- package/runtime/knowledge/grammars/index.yaml +102 -0
- package/runtime/knowledge/hfs/canon-pins.yaml +22 -8
- package/runtime/knowledge/hfs/fe-stack.yaml +7 -0
- package/runtime/knowledge/hfs/slots.yaml +3 -3
- package/runtime/modules/kernel/removed-vocabulary.yaml +29 -0
- package/runtime/scripts/hfs/architecture/config.mjs +8 -8
- package/runtime/scripts/hfs/declaration-shape.mjs +8 -2
- package/runtime/scripts/hfs/rule-catalog.mjs +2 -2
- package/runtime/scripts/hfs/slots.mjs +1 -0
- package/runtime/scripts/hfs/view.mjs +1 -0
- package/runtime/scripts/lib/grammar-lint.mjs +48 -0
- package/size-and-state-budget.mjs +116 -4
- package/surface-rules.mjs +159 -26
- package/tokens.mjs +5 -217
- package/app-shell.mjs +0 -161
- package/docs/app-shell.md +0 -50
- package/docs/grammar-boundary.md +0 -57
- package/docs/status-colors.md +0 -32
- package/grammar-boundary.mjs +0 -203
- package/icon.mjs +0 -310
- package/lib/app-shell-cluster.mjs +0 -66
- package/lib/app-shell-create.mjs +0 -33
- package/lib/app-shell-owner-source.mjs +0 -124
- package/lib/app-shell-owner.mjs +0 -65
- package/lib/app-shell-scope.mjs +0 -79
- package/lib/app-shell-visible.mjs +0 -92
- package/lib/app-shell.mjs +0 -148
- package/lib/status-tones.generated.mjs +0 -9
- package/menu-entries.mjs +0 -133
- package/runtime/modules/goal/source-phrases.yaml +0 -95
- package/runtime/scripts/lib/source-phrases.mjs +0 -34
- package/status-colors.mjs +0 -137
- package/typography.mjs +0 -78
- package/vendor-boundary.mjs +0 -26
package/landmark.mjs
CHANGED
|
@@ -5,9 +5,6 @@
|
|
|
5
5
|
* application may pass children through layouts and pages normally.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
/** Named branch owners that may provide landmark semantics. */
|
|
9
|
-
export const LANDMARK_BRANCHES = new Set(["Main"])
|
|
10
|
-
|
|
11
8
|
/** No custom landmark AST rule is needed; semantic ownership is enforced by the component code. */
|
|
12
9
|
export const rules = {}
|
|
13
10
|
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** Shared static class-string readers for token discipline and Grammar scale checks. */
|
|
2
|
+
import { isClassAttribute, staticText } from "./ast.mjs"
|
|
3
|
+
import { isProductSource } from "./scope.mjs"
|
|
4
|
+
|
|
5
|
+
/** Static strings in JSX, constants and arrays, preserving the authored class order. */
|
|
6
|
+
const classText = (value) => {
|
|
7
|
+
if (value?.type === "JSXExpressionContainer") return classText(value.expression)
|
|
8
|
+
if (value?.type === "ArrayExpression") {
|
|
9
|
+
const members = value.elements.map((element) => classText(element)).filter(Boolean)
|
|
10
|
+
return members.length > 0 ? members.join(" ") : null
|
|
11
|
+
}
|
|
12
|
+
return staticText(value)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Visit class strings in product JSX, declarations and named class arrays. */
|
|
16
|
+
export const classTextVisitors = (context, report) => {
|
|
17
|
+
if (!isProductSource(context)) return {}
|
|
18
|
+
return {
|
|
19
|
+
JSXAttribute(node) {
|
|
20
|
+
if (isClassAttribute(node)) report(node, classText(node.value))
|
|
21
|
+
},
|
|
22
|
+
VariableDeclarator(node) {
|
|
23
|
+
report(node, classText(node.init))
|
|
24
|
+
},
|
|
25
|
+
Property(node) {
|
|
26
|
+
if (!node.computed && node.key.type === "Identifier" && node.key.name === "classes") report(node, classText(node.value))
|
|
27
|
+
},
|
|
28
|
+
}
|
|
29
|
+
}
|
package/lib/config.mjs
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The one flat config of a StarCi front end.
|
|
3
3
|
*
|
|
4
|
-
* A repository's `eslint.config.mjs` is
|
|
5
|
-
*
|
|
6
|
-
* import { loadHfs, starciFeConfig } from "@starci/eslint-canon-fe"
|
|
7
|
-
* export default starciFeConfig({ hfs: loadHfs(import.meta.url) })
|
|
4
|
+
* A repository's `eslint.config.mjs` is managed by `starci app sync` (R05/R16/R17). Its template composes this universal
|
|
5
|
+
* factory with the explicitly selected Grammar family's factory, passing the same HFS view to both.
|
|
8
6
|
*
|
|
9
7
|
* The factory owns everything else: which files are linted (the source of every app and every workspace package), typed linting on the source (`parserOptions.projectService`, so a rule asks what a node IS rather than what
|
|
10
8
|
* it is called), the ignores, the linter options that make a disable comment impossible, every canon rule at `error`,
|
|
@@ -97,10 +95,10 @@ export const SLOT_FREE_RULES = Object.freeze([
|
|
|
97
95
|
"client-no-server-import", "no-server-action", "no-dangerous-html", "response-cookie-attributes", "require-export-jsdoc", "no-vietnamese-in-source",
|
|
98
96
|
"no-hardcoded-endpoint-fallback", "no-runtime-namespace", "list-item-has-key", "no-index-key", "no-inline-literal-prop-in-list", "prefer-arrow-export",
|
|
99
97
|
"handler-on-prefix", "no-native-form-control", "no-native-img", "image-has-size", "no-second-i18n-stack", "html-lang-from-locale", "no-null-suspense-fallback",
|
|
100
|
-
"no-native-anchor", "no-inline-parameter-type", "no-hand-rolled-polling", "
|
|
98
|
+
"no-native-anchor", "no-inline-parameter-type", "no-hand-rolled-polling", "no-copy-in-call", "no-alert", "no-failure-collapse",
|
|
101
99
|
"outcome-kinds-exhaustive", "no-await-in-loop", "prefer-code-point", "prefer-string-raw", "no-nested-conditional", "no-void-operator", "no-unused-import",
|
|
102
100
|
"prefer-export-from", "no-unused-prop-types", "media-has-caption", "max-params",
|
|
103
|
-
"nested-control-variant", "no-list-panel-padding",
|
|
101
|
+
"nested-control-variant", "no-list-panel-padding", "no-app-surface-paint",
|
|
104
102
|
])
|
|
105
103
|
|
|
106
104
|
/** The source of a plain front-end tree: its `src/`. */
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Shared HFS scope of connected/presentational component pairs. */
|
|
2
|
+
import { hfsOf } from "./hfs.mjs"
|
|
3
|
+
import { fileOf, inSlot, kindOfFile, roleOfFile } from "./scope.mjs"
|
|
4
|
+
|
|
5
|
+
const SPLIT_KINDS = new Set(["blocks", "pages", "layouts", "overlays"])
|
|
6
|
+
|
|
7
|
+
/** The owning surface folder of the linted entry or presentation file. */
|
|
8
|
+
const surfaceName = (context) => hfsOf(context).relative(fileOf(context)).split("/").slice(-2)[0]
|
|
9
|
+
|
|
10
|
+
/** Whether the file belongs to an HFS owner with a connected/presentational pair. */
|
|
11
|
+
export const inSplitTier = (context) => inSlot(context, "fe.components", "fe.feature") && SPLIT_KINDS.has(kindOfFile(context))
|
|
12
|
+
|
|
13
|
+
/** The paired presentation file's component kind, or null outside that scope. */
|
|
14
|
+
export const pureHalf = (context) =>
|
|
15
|
+
inSplitTier(context) && roleOfFile(context) === "drawing" && /^[A-Z][A-Za-z0-9]*$/.test(surfaceName(context)) ? kindOfFile(context) : null
|
|
@@ -5,7 +5,7 @@ import ts from "typescript"
|
|
|
5
5
|
const fragments = (checker, node, seen, mode) => {
|
|
6
6
|
if (!node || seen.has(node)) return []
|
|
7
7
|
seen.add(node)
|
|
8
|
-
if (ts.isStringLiteralLike(node) || ts.isTemplateHead(node) || ts.isTemplateMiddle(node) || ts.isTemplateTail(node)) return [node.text]
|
|
8
|
+
if (ts.isStringLiteralLike(node) || ts.isTemplateHead(node) || ts.isTemplateMiddle(node) || ts.isTemplateTail(node)) return mode === "style-keys" ? [] : [node.text]
|
|
9
9
|
if (ts.isAsExpression(node) || ts.isTypeAssertionExpression(node) || ts.isSatisfiesExpression(node) || ts.isParenthesizedExpression(node) || ts.isNonNullExpression(node)) return fragments(checker, node.expression, seen, mode)
|
|
10
10
|
if (ts.isIdentifier(node) || ts.isPropertyAccessExpression(node) || ts.isElementAccessExpression(node)) {
|
|
11
11
|
const symbol = ts.isElementAccessExpression(node) && ts.isStringLiteralLike(node.argumentExpression)
|
|
@@ -13,6 +13,15 @@ const fragments = (checker, node, seen, mode) => {
|
|
|
13
13
|
: checker.getSymbolAtLocation(ts.isPropertyAccessExpression(node) ? node.name : node)
|
|
14
14
|
return symbolFragments(checker, symbol, seen, mode)
|
|
15
15
|
}
|
|
16
|
+
if (mode === "style-keys" && ts.isObjectLiteralExpression(node)) {
|
|
17
|
+
return node.properties.flatMap((property) => {
|
|
18
|
+
if (ts.isSpreadAssignment(property)) return fragments(checker, property.expression, seen, mode)
|
|
19
|
+
if (!ts.isPropertyAssignment(property) && !ts.isShorthandPropertyAssignment(property)) return []
|
|
20
|
+
return ts.isComputedPropertyName(property.name)
|
|
21
|
+
? fragments(checker, property.name.expression, seen, "variant")
|
|
22
|
+
: [property.name.text]
|
|
23
|
+
})
|
|
24
|
+
}
|
|
16
25
|
return fragmentChildren(node, mode).flatMap((child) => fragments(checker, child, seen, mode))
|
|
17
26
|
}
|
|
18
27
|
|
package/lib/why.mjs
CHANGED
|
@@ -21,39 +21,19 @@
|
|
|
21
21
|
export const why = {
|
|
22
22
|
"nested-control-variant": {
|
|
23
23
|
code: "FE_NESTED_VARIANT",
|
|
24
|
-
en: "`<file>`
|
|
25
|
-
fix: "
|
|
24
|
+
en: "`<file>` sets a static Grammar field variant, or forces primary Tabs inside a painted surface.",
|
|
25
|
+
fix: "Omit the field or nested Tabs variant so Grammar supplies the default from its surface context.",
|
|
26
|
+
},
|
|
27
|
+
"no-app-surface-paint": {
|
|
28
|
+
code: "FE_GRAMMAR_LAYOUT_ONLY",
|
|
29
|
+
en: "`<file>` sets paint or typography in app CSS instead of composing Grammar's published components and props.",
|
|
30
|
+
fix: "Keep app CSS layout-only and compose a painted Grammar surface such as SurfaceCard.",
|
|
26
31
|
},
|
|
27
32
|
"no-list-panel-padding": {
|
|
28
33
|
code: "FE_LIST_PANEL_PADDING",
|
|
29
34
|
en: "`<file>` adds horizontal wrapper padding to a Grammar list or its section caption, stacking the content inset.",
|
|
30
35
|
fix: "Remove the wrapper padding and use ListBox inset=\"panel\" so headers, captions and rows share the panel content edge.",
|
|
31
36
|
},
|
|
32
|
-
"app-shell-frame": {
|
|
33
|
-
code: "FE_APP_SHELL_FRAME",
|
|
34
|
-
en: "The app frame must use Grammar WorkspaceShell with its fixed viewport.",
|
|
35
|
-
fix: "Compose WorkspaceShell viewport=\"fixed\" and supply its header, navigation and primary slots.",
|
|
36
|
-
},
|
|
37
|
-
"no-shell-bottom-tabs": {
|
|
38
|
-
code: "FE_APP_SHELL_BOTTOM_TABS",
|
|
39
|
-
en: "Bottom tab navigation replaces the required mobile sidebar drawer.",
|
|
40
|
-
fix: "Use WorkspaceShell navigationDrawer and Sidebar presentation=\"drawer\".",
|
|
41
|
-
},
|
|
42
|
-
"navbar-control-size": {
|
|
43
|
-
code: "FE_NAVBAR_CONTROL_SIZE",
|
|
44
|
-
en: "A navbar control does not use Grammar's shared control height.",
|
|
45
|
-
fix: "Set size=\"control\" on every visible bar control.",
|
|
46
|
-
},
|
|
47
|
-
"navbar-cluster": {
|
|
48
|
-
code: "FE_NAVBAR_CLUSTER",
|
|
49
|
-
en: "Navbar action component kinds or order differ from the app-shell canon.",
|
|
50
|
-
fix: "Compose Language IconButton, theme Switch, notification IconButton and account AvatarButton in that order.",
|
|
51
|
-
},
|
|
52
|
-
"no-navbar-create": {
|
|
53
|
-
code: "FE_NAVBAR_CREATE",
|
|
54
|
-
en: "A create action is composed into the global navbar.",
|
|
55
|
-
fix: "Move the create action to the page that owns it.",
|
|
56
|
-
},
|
|
57
37
|
"no-env-outside-config": {
|
|
58
38
|
code: "FE_ENV_OWNER",
|
|
59
39
|
en: "`<file>` reads an environment variable. Only `modules/config` may read env.",
|
|
@@ -134,11 +114,6 @@ export const why = {
|
|
|
134
114
|
en: "`<file>` uses a raw colour/length value (`<what>`). Colour and spacing come only from grammar tokens; brand colour lives only in `brand.css`.",
|
|
135
115
|
fix: "Use a grammar token class or variable; a colour value is declared only in `modules/brand/brand.css`.",
|
|
136
116
|
},
|
|
137
|
-
"status-text-uses-soft-foreground": {
|
|
138
|
-
code: "FE_STYLE_TOKEN_ONLY",
|
|
139
|
-
en: "`<file>` paints text or an icon with a solid status tone (`<what>`, e.g. `text-success`). A solid tone is a background colour; used as a text colour it reads below 4.5:1 on the page background.",
|
|
140
|
-
fix: "Switch to the soft pair: `text-<tone>-soft-foreground` (with `bg-<tone>-soft` when it sits on a light surface); a solid tone is used only with `bg-<tone>` and `text-<tone>-foreground`.",
|
|
141
|
-
},
|
|
142
117
|
"no-native-form-control": {
|
|
143
118
|
code: "FE_NATIVE_FORM_CONTROL",
|
|
144
119
|
en: "Raw `<tag>` in `<file>`. Use the grammar renderer.",
|
|
@@ -232,12 +207,12 @@ export const why = {
|
|
|
232
207
|
"navigation-from-intl": {
|
|
233
208
|
code: "FE_I18N_NAVIGATION",
|
|
234
209
|
en: "`<file>` imports `Link`, `useRouter`, `usePathname` or `redirect` from Next, which does not know the locale: the path loses the `[locale]` prefix.",
|
|
235
|
-
fix: "Import from
|
|
210
|
+
fix: "Import from the locale-aware public routes entry or shared i18n package, built from the one routing configuration.",
|
|
236
211
|
},
|
|
237
212
|
"no-native-anchor": {
|
|
238
213
|
code: "FE_I18N_NAVIGATION",
|
|
239
214
|
en: "`<a>` in `<file>` points at an internal route (reloads the whole page, loses the locale, loses prefetch) or opens a new tab without `rel`.",
|
|
240
|
-
fix: "Use
|
|
215
|
+
fix: "Use the locale-aware Link with an href built by modules/routes; a _blank link adds rel=\"noopener noreferrer\".",
|
|
241
216
|
},
|
|
242
217
|
"use-intl-formatter": {
|
|
243
218
|
code: "FE_I18N_FORMATTER",
|
|
@@ -307,12 +282,7 @@ export const why = {
|
|
|
307
282
|
"i18n-stack-in-one-module": {
|
|
308
283
|
code: "FE_I18N_PLACEMENT",
|
|
309
284
|
en: "`<file>` builds a layer of the next-intl stack itself (`defineRouting`, `createNavigation`, `getRequestConfig`, `createMiddleware`). The stack is written once for the whole repo.",
|
|
310
|
-
fix: "
|
|
311
|
-
},
|
|
312
|
-
"no-raw-structural-element": {
|
|
313
|
-
code: "FE_NATIVE_FORM_CONTROL",
|
|
314
|
-
en: "Raw `<tag>` in `<file>`. Page structure and text are composed from grammar components, not hand-written HTML.",
|
|
315
|
-
fix: "Replace it with the matching grammar component (`Heading`, `Text`, `SurfaceCard`, ...); if grammar lacks it, add it to grammar or the ui package instead of drawing it in place.",
|
|
285
|
+
fix: "Use the shared i18n factory; a single app keeps routing/request config in modules/i18n and navigation in modules/routes. The proxy imports routing without navigation.",
|
|
316
286
|
},
|
|
317
287
|
"no-app-relative-clock": {
|
|
318
288
|
code: "FE_SIZE_AND_STATE_BUDGET",
|
package/next-conventions.mjs
CHANGED
|
@@ -15,7 +15,7 @@ import { attribute, attributeValue, calleeName, leadingTextOf, stringOf } from "
|
|
|
15
15
|
import { variableOf } from "./lib/bindings.mjs"
|
|
16
16
|
import { forbiddingConcern } from "./lib/fe-stack.mjs"
|
|
17
17
|
import { hfsOf } from "./lib/hfs.mjs"
|
|
18
|
-
import { baseName, classOf, fileOf, inSlot, roleOfFile, slotOfFile } from "./lib/scope.mjs"
|
|
18
|
+
import { baseName, classOf, classifyImport, fileOf, inSlot, roleOfFile, slotOfFile } from "./lib/scope.mjs"
|
|
19
19
|
|
|
20
20
|
/** Another i18n stack: a second place that decides which language a reader sees (one the fe-stack catalog forbids by name is fe-stack-import's finding). */
|
|
21
21
|
const OTHER_I18N = /^(?:react-i18next|i18next|next-i18next|react-intl|next-translate|typesafe-i18n|rosetta)(?:\/.*)?$|^@lingui\/.+$/
|
|
@@ -214,26 +214,27 @@ export const noNullSuspenseFallback = {
|
|
|
214
214
|
|
|
215
215
|
// -- FE-NEXT-7 -------------------------------------------------------------------------------------
|
|
216
216
|
|
|
217
|
-
/**
|
|
217
|
+
/** The locale-aware navigation owner and framework boundaries outside its provider. */
|
|
218
218
|
const isNavigationOwner = (context) =>
|
|
219
|
-
inSlot(context, "fe.
|
|
219
|
+
inSlot(context, "fe.package.i18n") ||
|
|
220
|
+
(inSlot(context, "fe.modules.routes") && hfsOf(context).apps.length === 1) ||
|
|
220
221
|
(inSlot(context, "fe.source-root-pinned") && roleOfFile(context) === "proxy") ||
|
|
221
222
|
(inSlot(context, "fe.route") && roleOfFile(context) === "global-error")
|
|
222
223
|
|
|
223
224
|
/** The router helpers that are locale-blind in `next/navigation`. */
|
|
224
225
|
const LOCALE_BLIND = new Set(["useRouter", "usePathname", "redirect", "permanentRedirect"])
|
|
225
226
|
|
|
226
|
-
/** Navigation helpers come from
|
|
227
|
+
/** Navigation helpers come from the locale-aware public routes or shared i18n entry. */
|
|
227
228
|
export const navigationFromIntl = {
|
|
228
229
|
meta: {
|
|
229
230
|
type: "problem",
|
|
230
|
-
docs: { description: "`Link`, `useRouter`, `usePathname` and `redirect` come from
|
|
231
|
+
docs: { description: "`Link`, `useRouter`, `usePathname` and `redirect` come from the locale-aware routes or shared i18n public entry, not from Next." },
|
|
231
232
|
schema: [],
|
|
232
233
|
messages: {
|
|
233
234
|
link:
|
|
234
|
-
"`next/link` does not know the locale: a link built with it drops the `[locale]` prefix
|
|
235
|
+
"`next/link` does not know the locale: a link built with it drops the `[locale]` prefix. Import Link from the locale-aware public routes entry or shared i18n package, built from the one routing configuration.",
|
|
235
236
|
helper:
|
|
236
|
-
"`{{name}}` from `next/navigation` does not know the locale
|
|
237
|
+
"`{{name}}` from `next/navigation` does not know the locale. Import it from the locale-aware public routes entry or shared i18n package, built from the one routing configuration.",
|
|
237
238
|
},
|
|
238
239
|
},
|
|
239
240
|
create(context) {
|
|
@@ -263,11 +264,11 @@ const isExternalHref = (text) => /^(?:[a-z][a-z0-9+.-]*:|\/\/|#)/i.test(text)
|
|
|
263
264
|
export const noNativeAnchor = {
|
|
264
265
|
meta: {
|
|
265
266
|
type: "problem",
|
|
266
|
-
docs: { description: "An internal link
|
|
267
|
+
docs: { description: "An internal link uses the locale-aware public navigation entry; an external `_blank` link carries `rel`." },
|
|
267
268
|
schema: [],
|
|
268
269
|
messages: {
|
|
269
270
|
internal:
|
|
270
|
-
"`<a href=\"{{href}}\">` for a route of this app
|
|
271
|
+
"`<a href=\"{{href}}\">` for a route of this app reloads the page, skips prefetch and drops the locale prefix. Use the locale-aware Link with an href built by modules/routes.",
|
|
271
272
|
rel:
|
|
272
273
|
"`target=\"_blank\"` with no `rel`. The opened page can reach back through `window.opener`. Add `rel=\"noopener noreferrer\"`.",
|
|
273
274
|
},
|
|
@@ -376,9 +377,9 @@ const stackFactoryOf = (context, callee) => {
|
|
|
376
377
|
*
|
|
377
378
|
* `defineRouting` (`next-intl/routing`), `createNavigation` (`next-intl/navigation`), `getRequestConfig` (`next-intl/server`) and
|
|
378
379
|
* `createMiddleware` (`next-intl/middleware`), resolved by the import that binds the called name (a renamed import or a namespace
|
|
379
|
-
* member counts),
|
|
380
|
-
*
|
|
381
|
-
*
|
|
380
|
+
* member counts), have one owner. A shared i18n package owns its stack. In a one-app repository, routing and request
|
|
381
|
+
* config belong to modules/i18n while createNavigation belongs to modules/routes. The proxy imports routing without
|
|
382
|
+
* evaluating navigation: next-intl's navigation module selects React runtime exports unavailable in the proxy.
|
|
382
383
|
*
|
|
383
384
|
* It does not replace `no-second-i18n-stack`: that rule refuses another i18n LIBRARY at its import, this one refuses a second copy of
|
|
384
385
|
* next-intl's own stack. Different evidence, different remedy.
|
|
@@ -386,26 +387,42 @@ const stackFactoryOf = (context, callee) => {
|
|
|
386
387
|
export const i18nStackInOneModule = {
|
|
387
388
|
meta: {
|
|
388
389
|
type: "problem",
|
|
389
|
-
docs: { description: "
|
|
390
|
+
docs: { description: "A shared i18n package owns the stack; one app keeps routing/request config in i18n and navigation in routes, with a routing-only proxy entry." },
|
|
390
391
|
schema: [],
|
|
391
392
|
messages: {
|
|
392
393
|
stack:
|
|
393
|
-
"`{{name}}` builds
|
|
394
|
+
"`{{name}}` builds the next-intl stack outside its owner. Keep {{where}} and import the owned result through its public entry.",
|
|
395
|
+
proxyNavigation:
|
|
396
|
+
"The proxy imports navigation runtime through {{source}}. Import routing from the i18n public entry, or use the shared package's routing-only proxy export; the proxy must not evaluate the routes/navigation barrel.",
|
|
394
397
|
},
|
|
395
398
|
},
|
|
396
399
|
create(context) {
|
|
397
400
|
const hfs = hfsOf(context)
|
|
398
401
|
const slot = slotOfFile(context)
|
|
399
402
|
const single = hfs.apps.length === 1
|
|
400
|
-
if (slot === "fe.package.i18n" || (slot === "fe.modules.i18n" && single)) return {}
|
|
401
403
|
const proxy = single && slot === "fe.source-root-pinned" && roleOfFile(context) === "proxy"
|
|
402
|
-
const
|
|
404
|
+
const proxyFile = slot === "fe.source-root-pinned" && roleOfFile(context) === "proxy"
|
|
405
|
+
const where = single ? "routing/request config in modules/i18n and navigation in modules/routes" : "the stack in the shared i18n package that every app calls"
|
|
406
|
+
const safeProxyImport = (node) => {
|
|
407
|
+
if (!proxyFile || !node.source || node.importKind === "type" || node.exportKind === "type") return
|
|
408
|
+
if (node.specifiers?.length && node.specifiers.every((entry) => entry.importKind === "type" || entry.exportKind === "type")) return
|
|
409
|
+
const source = String(node.source.value)
|
|
410
|
+
if (source === "next-intl/navigation" || classifyImport(context, source)?.slot === "fe.modules.routes") {
|
|
411
|
+
context.report({ node, messageId: "proxyNavigation", data: { source } })
|
|
412
|
+
}
|
|
413
|
+
}
|
|
403
414
|
return {
|
|
415
|
+
ImportDeclaration: safeProxyImport,
|
|
416
|
+
ExportNamedDeclaration: safeProxyImport,
|
|
417
|
+
ExportAllDeclaration: safeProxyImport,
|
|
404
418
|
CallExpression(node) {
|
|
405
419
|
const factory = stackFactoryOf(context, node.callee)
|
|
406
420
|
// In a one-app repository the proxy (role `proxy`) mounts the locale middleware built from the one routing; a shared repository
|
|
407
421
|
// builds it in the i18n package and the proxy re-exports it.
|
|
408
422
|
if (factory?.name === "createMiddleware" && proxy) return
|
|
423
|
+
if (slot === "fe.package.i18n") return
|
|
424
|
+
if (single && factory?.name === "createNavigation" && slot === "fe.modules.routes") return
|
|
425
|
+
if (single && factory?.name !== "createNavigation" && slot === "fe.modules.i18n") return
|
|
409
426
|
if (factory) context.report({ node, messageId: "stack", data: { name: factory.name, where } })
|
|
410
427
|
},
|
|
411
428
|
}
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@starci/eslint-canon-fe",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "9.0.0",
|
|
4
|
+
"description": "Universal front-end lint laws for TypeScript and Next.js, independent of the selected Grammar family.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"private": false,
|
|
8
8
|
"exports": {
|
|
9
|
-
".": "./index.mjs"
|
|
9
|
+
".": "./index.mjs",
|
|
10
|
+
"./lib/*": "./lib/*"
|
|
10
11
|
},
|
|
11
12
|
"main": "./index.mjs",
|
|
12
13
|
"files": [
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
schema: starci/knowledge-source@1
|
|
2
|
+
id: grammars
|
|
3
|
+
title: Grammar families
|
|
4
|
+
purpose: |
|
|
5
|
+
Catalog of authored UI Grammar families. Inventory only — not a default-family rule.
|
|
6
|
+
The selected design specification names the applicable installed family. The app binds that choice
|
|
7
|
+
explicitly in sides.fe.grammar; the catalog resolves its lint package and component contract.
|
|
8
|
+
A missing or ambiguous family never silently resolves to a family. DNA snapshots describe the
|
|
9
|
+
observed implementation; they do not install packages or prove current rendered behavior.
|
|
10
|
+
appliesTo:
|
|
11
|
+
- grammar
|
|
12
|
+
provenance:
|
|
13
|
+
limitations: |
|
|
14
|
+
Component names, tokens and gap tables describe the reference family at observation time.
|
|
15
|
+
Verify actual package identity/version, exported types, CSS and renderer source in the selected
|
|
16
|
+
repository before using snapshot APIs or gaps. Record current source references in the selected
|
|
17
|
+
.starciwork node; an old source census is not current UAT evidence. The current public consumer
|
|
18
|
+
entry observed for FE work is @starci/grammar/common — confirm installed exports before import.
|
|
19
|
+
The `common` entry is a DNA snapshot of that semantic entry and nothing more: it declares no palette,
|
|
20
|
+
no family boundary and no composition playbook, because the entry itself ships none of them.
|
|
21
|
+
guidance:
|
|
22
|
+
- id: catalog-not-default
|
|
23
|
+
requirement: |
|
|
24
|
+
Do not treat this catalog as selecting a default Grammar family. Family choice belongs to the
|
|
25
|
+
current design specification and its explicit sides.fe.grammar declaration.
|
|
26
|
+
rationale: |
|
|
27
|
+
Silent fallback to StarCi would invent product direction and override an absent/ambiguous design.
|
|
28
|
+
- id: verify-before-use
|
|
29
|
+
requirement: |
|
|
30
|
+
Before applying snapshot APIs or recorded gaps, inspect the selected repository's package
|
|
31
|
+
identity/version, exported types, CSS and renderer source.
|
|
32
|
+
rationale: |
|
|
33
|
+
Snapshots are historical; installed exports may differ. Inventing missing APIs is forbidden.
|
|
34
|
+
- id: ui-owns-universal
|
|
35
|
+
requirement: |
|
|
36
|
+
Common semantics, renderer anatomy, state and accessibility remain owned by knowledge/ui.
|
|
37
|
+
A family owns its scoped visual realization and promoted idioms; the product owns its facts,
|
|
38
|
+
content, routes and approved composition.
|
|
39
|
+
Apply FE-RULE-OWNERSHIP in knowledge/patterns/fe/index.yaml when placing requirements and checks.
|
|
40
|
+
rationale: |
|
|
41
|
+
Keeps universal UI law separate from family-scoped DNA and product composition.
|
|
42
|
+
topics:
|
|
43
|
+
- id: common
|
|
44
|
+
path: common/DNA.yaml
|
|
45
|
+
title: Common Grammar — DNA
|
|
46
|
+
summary: |
|
|
47
|
+
Generated DNA snapshot of the `@starci/grammar/common` public entry: custom properties,
|
|
48
|
+
the 110 renderers of its registry, their classes, stamped rule ids and recorded gaps. The DNA owns the observed package
|
|
49
|
+
version and source inventory. Common supplies anatomy, not colour. The snapshot is drift-checked.
|
|
50
|
+
- id: starci
|
|
51
|
+
path: starci/index.yaml
|
|
52
|
+
title: StarCi Core Grammar
|
|
53
|
+
summary: |
|
|
54
|
+
Historical snapshot of the StarCi Core family — DNA, idioms, playbook (including composing the
|
|
55
|
+
0.5.0 forms, overlays and navigation chrome) and family boundaries.
|
|
56
|
+
- id: offset-pop
|
|
57
|
+
path: offset-pop/index.yaml
|
|
58
|
+
title: Offset Pop Grammar
|
|
59
|
+
summary: |
|
|
60
|
+
Snapshot of the product-neutral Offset Pop family — cream canvas, ink outline plus hard offset
|
|
61
|
+
shadow, one pink decision accent with a text-safe accent for text — with DNA, idioms, a
|
|
62
|
+
mobile-first playbook for playful learning or consumer apps, and family boundaries including two
|
|
63
|
+
decisions flagged for the family owner.
|
|
64
|
+
families:
|
|
65
|
+
- id: common
|
|
66
|
+
title: common
|
|
67
|
+
componentContract:
|
|
68
|
+
package: '@starci/grammar'
|
|
69
|
+
entry: '@starci/grammar/common'
|
|
70
|
+
styleEntry: '@starci/grammar/common.css'
|
|
71
|
+
peers: [react, '@heroui/react', '@xyflow/react']
|
|
72
|
+
packageSnapshot: common/DNA.yaml
|
|
73
|
+
scope: |
|
|
74
|
+
DNA snapshot only. This catalog carries no Common-side family boundary, idiom set or composition
|
|
75
|
+
playbook: `@starci/grammar/common` is the semantic/props entry every visual family is defined over
|
|
76
|
+
(`defineGrammarFamily`), not a family that ships a palette, so `authority`, `reusableStyle`,
|
|
77
|
+
`productComposition` and `index` have nothing to name here. Universal semantics, renderer anatomy,
|
|
78
|
+
state and accessibility remain owned by knowledge/ui.
|
|
79
|
+
- id: starci
|
|
80
|
+
title: starci
|
|
81
|
+
contractRef: common
|
|
82
|
+
lint:
|
|
83
|
+
package: '@starci/eslint-grammar-starci'
|
|
84
|
+
factory: starciGrammarConfig
|
|
85
|
+
prefix: starci-grammar
|
|
86
|
+
authority: starci/family.yaml
|
|
87
|
+
packageSnapshot: starci/DNA.yaml
|
|
88
|
+
reusableStyle: starci/idioms.yaml
|
|
89
|
+
productComposition: starci/playbook.yaml
|
|
90
|
+
index: starci/index.yaml
|
|
91
|
+
- id: offset-pop
|
|
92
|
+
title: offset-pop
|
|
93
|
+
contractRef: common
|
|
94
|
+
lint:
|
|
95
|
+
package: '@starci/eslint-grammar-offset-pop'
|
|
96
|
+
factory: offsetPopGrammarConfig
|
|
97
|
+
prefix: starci-grammar
|
|
98
|
+
authority: offset-pop/family.yaml
|
|
99
|
+
packageSnapshot: offset-pop/DNA.yaml
|
|
100
|
+
reusableStyle: offset-pop/idioms.yaml
|
|
101
|
+
productComposition: offset-pop/playbook.yaml
|
|
102
|
+
index: offset-pop/index.yaml
|
|
@@ -21,30 +21,44 @@ pins:
|
|
|
21
21
|
install: registry
|
|
22
22
|
side: fe
|
|
23
23
|
source: packages/heroicons/package.json
|
|
24
|
-
why: 'StarCi custom Heroicons-compatible cuts; eslint-
|
|
24
|
+
why: 'StarCi custom Heroicons-compatible cuts; eslint-grammar-starci (icon.mjs) admits @starci/heroicons/24/outline and /16/solid as icon sources, so an app that uses them gets this exact version.'
|
|
25
25
|
'@starci/grammar':
|
|
26
|
-
version: 0.12.
|
|
26
|
+
version: 0.12.3
|
|
27
27
|
group: starci
|
|
28
28
|
install: registry
|
|
29
29
|
side: fe
|
|
30
30
|
source: packages/grammar/package.json
|
|
31
31
|
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.'
|
|
32
32
|
'@starci/eslint-canon-be':
|
|
33
|
-
version: 3.4.
|
|
33
|
+
version: 3.4.44
|
|
34
34
|
group: starci
|
|
35
35
|
install: registry
|
|
36
36
|
side: be
|
|
37
37
|
source: packages/eslint/be/package.json
|
|
38
38
|
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.'
|
|
39
39
|
'@starci/eslint-canon-fe':
|
|
40
|
-
version:
|
|
40
|
+
version: 9.0.0
|
|
41
41
|
group: starci
|
|
42
42
|
install: registry
|
|
43
43
|
side: fe
|
|
44
44
|
source: packages/eslint/fe/package.json
|
|
45
45
|
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.'
|
|
46
|
+
'@starci/eslint-grammar-starci':
|
|
47
|
+
version: 0.12.3
|
|
48
|
+
group: starci
|
|
49
|
+
install: registry
|
|
50
|
+
side: fe
|
|
51
|
+
source: packages/eslint/grammar-starci/package.json
|
|
52
|
+
why: 'the StarCi Grammar component API lint, versioned with @starci/grammar; front ends compose this family package with the universal FE canon.'
|
|
53
|
+
'@starci/eslint-grammar-offset-pop':
|
|
54
|
+
version: 0.12.3
|
|
55
|
+
group: starci
|
|
56
|
+
install: registry
|
|
57
|
+
side: fe
|
|
58
|
+
source: packages/eslint/grammar-offset-pop/package.json
|
|
59
|
+
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.'
|
|
46
60
|
'@starci/stylelint-canon':
|
|
47
|
-
version: 2.0.
|
|
61
|
+
version: 2.0.12
|
|
48
62
|
group: starci
|
|
49
63
|
install: registry
|
|
50
64
|
side: fe
|
|
@@ -77,14 +91,14 @@ pins:
|
|
|
77
91
|
source: packages/test-world/package.json
|
|
78
92
|
why: 'the shared e2e library of every back end (R47, R48), a devDependency of every back end: the warm stack behind toxiproxy, the network-edge fakes, the Nest boot, the typed useTestWorld handle, useSandbox for contract specs and the outage lock. Kafka is real own infra (the one digest-pinned apache/kafka KRaft image, a broker listener and proxy per slot, slot-prefixed topics, consumer groups and client ids) and Postgres is schema-per-context (a schema and a login role per context in a shared database). Each worker has its own data slot (<ns>_w<k>) with its own databases, Keycloak realm, leased Redis DB, bucket/collection/topic prefixes, fakes host, toxiproxy proxies and outage lock; a run provisions min(jest workers, workers cap, default 2) slots, and state file protocol 2 pairs exactly with @starci/jest-preset (TEST_WORLD_PAIR_MISMATCH otherwise). A modules world boots real peer apps beside its modules, world.apps.<name>.during(fn) is the outage of a peer app, w.keycloak.clientSecret(client) answers the run-generated secret of a confidential realm client, world.keycloak.events/sessions read the realm user events and live sessions, world.infra.postgresql.connection(name) takes one database connection down while the others serve, and world.resolve and scope.resolve take any Nest token. Every path a declaration names (`stack`, seeds, the realm, Dockerfiles) resolves from the app root (the directory of hfs.json); the globalSetup registers the path aliases as TypeScript resolves them. Its CLI main is exported at ./cli for starci app stack.'
|
|
79
93
|
'@starci/cli':
|
|
80
|
-
version: 1.7.
|
|
94
|
+
version: 1.7.35
|
|
81
95
|
group: starci
|
|
82
96
|
install: registry
|
|
83
97
|
side: both
|
|
84
98
|
source: packages/cli/package.json
|
|
85
99
|
why: 'the only StarCi command package; product repositories install it for starci app, while @starci/hfs remains its pinned transitive implementation.'
|
|
86
100
|
'@starci/hfs':
|
|
87
|
-
version: 4.7.
|
|
101
|
+
version: 4.7.35
|
|
88
102
|
group: starci
|
|
89
103
|
install: registry
|
|
90
104
|
side: both
|
|
@@ -380,7 +394,7 @@ pins:
|
|
|
380
394
|
version: 2.2.0
|
|
381
395
|
group: ui
|
|
382
396
|
side: fe
|
|
383
|
-
why: 'the base of the one application icon set (fe-stack icons, with @starci/heroicons for the glyphs it lacks); eslint-
|
|
397
|
+
why: 'the base of the one application icon set (fe-stack icons, with @starci/heroicons for the glyphs it lacks); eslint-grammar-starci (icon.mjs) admits @heroicons/react/24/outline and /16/solid.'
|
|
384
398
|
'@dicebear/core':
|
|
385
399
|
version: 10.4.0
|
|
386
400
|
group: ui
|
|
@@ -237,6 +237,13 @@ concerns:
|
|
|
237
237
|
pattern: FE-LINT
|
|
238
238
|
forbids: []
|
|
239
239
|
why: the canon lint and format configuration.
|
|
240
|
+
- id: grammar-lint
|
|
241
|
+
tier: opt
|
|
242
|
+
packages: ['@starci/eslint-grammar-starci', '@starci/eslint-grammar-offset-pop']
|
|
243
|
+
home: [fe.source-root-pinned]
|
|
244
|
+
pattern: FE-RULE-OWNERSHIP
|
|
245
|
+
forbids: []
|
|
246
|
+
why: The selected Grammar family's lint package runs beside the universal front-end canon; the family catalog declares the package and factory.
|
|
240
247
|
- id: auth-supabase
|
|
241
248
|
tier: opt
|
|
242
249
|
packages: ['@supabase/supabase-js', '@supabase/ssr']
|
|
@@ -2396,9 +2396,9 @@ slots:
|
|
|
2396
2396
|
presence: required
|
|
2397
2397
|
tracked: tracked
|
|
2398
2398
|
tier: foundation
|
|
2399
|
-
# The app's
|
|
2400
|
-
#
|
|
2401
|
-
#
|
|
2399
|
+
# The app's routing, request config and catalogs, or its adapter to the shared i18n factory. A single app
|
|
2400
|
+
# owns createNavigation in fe.modules.routes, keeping this public entry safe for its proxy. Shared stacks
|
|
2401
|
+
# belong to fe.package.i18n and expose a routing-only proxy entry separately from navigation.
|
|
2402
2402
|
requires: [index.ts]
|
|
2403
2403
|
tests: none
|
|
2404
2404
|
rules: [FE_I18N_PLACEMENT, FE_I18N_CATALOG]
|
|
@@ -8,6 +8,22 @@
|
|
|
8
8
|
# a word to scan for).
|
|
9
9
|
kinds: [config-key, bias-field, verb, flag, code, vocabulary, file]
|
|
10
10
|
removed:
|
|
11
|
+
- id: fe-landmark-branch-inventory
|
|
12
|
+
kind: vocabulary
|
|
13
|
+
name: LANDMARK_BRANCHES
|
|
14
|
+
use: 'the selected Grammar lint package owns its component vocabulary; the universal FE canon exposes no component inventory'
|
|
15
|
+
since: 1.0.0-alpha.31
|
|
16
|
+
- id: fe-grammar-rule-namespace
|
|
17
|
+
kind: vocabulary
|
|
18
|
+
name: starci-fe Grammar component rules
|
|
19
|
+
match: 'starci-fe/(?:app-shell-frame|no-shell-bottom-tabs|navbar-control-size|navbar-cluster|no-navbar-create|no-raw-structural-element|no-heading-tag-outside-heading-component|status-text-uses-soft-foreground|vendor-primitive-has-named-owner|no-vendor-icon-outside-icon-leaf|heroicons-is-the-glyph-vendor|no-off-scale-glyph-size|no-icon-size-class|destructive-menu-entry-needs-danger-tone|no-decorative-icon-in-metric-cell|no-inline-svg-outside-brand-mark-leaf|no-fractional-step|no-hand-rolled-heading|no-unresolved-token-class|slot-status-through-slotview)\b'
|
|
20
|
+
use: 'the same rule id under starci-grammar/ from the selected Grammar lint package; FE-RULE-OWNERSHIP governs placement'
|
|
21
|
+
since: 1.0.0-alpha.31
|
|
22
|
+
- id: offset-pop-toaster-content-guidance
|
|
23
|
+
kind: vocabulary
|
|
24
|
+
name: toaster-clear-of-the-dock
|
|
25
|
+
use: 'toaster-clear-of-content in the Offset Pop playbook'
|
|
26
|
+
since: 1.0.0-alpha.31
|
|
11
27
|
- id: cut-seam-command
|
|
12
28
|
kind: flag
|
|
13
29
|
name: cut-seam --command
|
|
@@ -401,3 +417,16 @@ removed:
|
|
|
401
417
|
use: 'the release tag is always <release.tagPrefix><package.json version>; delete the key'
|
|
402
418
|
since: 1.0.0-alpha.31
|
|
403
419
|
match: '\btagMatchesVersion\b'
|
|
420
|
+
|
|
421
|
+
- id: grammar-surfacecard-manual-layer
|
|
422
|
+
kind: vocabulary
|
|
423
|
+
name: SurfaceCard manual depth and unpainted frame
|
|
424
|
+
use: 'SurfaceCard derives its depth from the painted parent surface; use an unpainted layout container for grouping and a bounded SurfaceCard for a painted face'
|
|
425
|
+
since: 1.0.0-alpha.31
|
|
426
|
+
match: 'SurfaceCard[^\r\n>]*\s(?:depth=|frame="frameless")'
|
|
427
|
+
- id: grammar-dropdown-negative-tone
|
|
428
|
+
kind: vocabulary
|
|
429
|
+
name: DropdownMenuItem negative tone
|
|
430
|
+
use: 'DropdownMenuItem.tone uses neutral or danger; destructive commands use danger'
|
|
431
|
+
since: 1.0.0-alpha.31
|
|
432
|
+
match: 'DropdownMenuItem(?:Tone|\.tone)[^\r\n]*["'']negative["'']'
|