@starci/eslint-canon-fe 1.4.1 → 1.4.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.
- package/contract.mjs +1363 -1295
- package/contracts.ts +1 -1
- package/file-layout.mjs +414 -310
- package/icon.mjs +10 -96
- package/landmark.mjs +242 -242
- package/lint-adoption.mjs +116 -116
- package/naming.mjs +36 -7
- package/package.json +38 -38
- package/props-and-slots.mjs +278 -14
- package/props.ts +33 -33
- package/served-locale.mjs +1 -1
- package/the-split.mjs +2 -2
- package/typography.mjs +83 -83
- package/vendor-boundary.mjs +85 -62
package/vendor-boundary.mjs
CHANGED
|
@@ -27,24 +27,15 @@ const memberName = (node) => {
|
|
|
27
27
|
/** The component library this boundary owns. Its glyph counterpart belongs to `icon.mjs`. */
|
|
28
28
|
const VENDOR_PACKAGE_PREFIX = "@heroui/"
|
|
29
29
|
|
|
30
|
-
/**
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
* Three own a vendor interaction machine and ignore their interior. `RouteShell` owns a framework
|
|
34
|
-
* one: a Next segment layout is handed its page as `children`, no tier below a shell may take one,
|
|
35
|
-
* and a server component cannot convert it because a function does not serialise across that
|
|
36
|
-
* boundary. It imports no vendor at all, which is why this rule can admit it without widening what
|
|
37
|
-
* a vendor import means.
|
|
38
|
-
*/
|
|
39
|
-
const SHELL_DIR = "/src/components/shells/"
|
|
40
|
-
const FRAMEWORK_SHELL = /\/src\/components\/shells\/RouteShell\//
|
|
41
|
-
const SHELL_FILE = /\/src\/components\/shells\/(?:ModalShell|DrawerShell|DropdownShell|RouteShell)\//
|
|
30
|
+
/** Named mechanics branches own the closed vendor interaction machines. */
|
|
31
|
+
const MECHANICS_BRANCH = /\/src\/components\/branches\/(?:ModalBranch|DrawerBranch|DropdownBranch)\//
|
|
32
|
+
const LEGACY_SHELL_DIR = "/src/components/shells/"
|
|
42
33
|
|
|
43
34
|
/** Named surface branches may own the vendor wrapper they project a content contract into. */
|
|
44
35
|
const SURFACE_BRANCH = /\/src\/components\/branches\/(?:SurfaceCard|SurfaceAccordionCard|SurfaceListCard|SurfaceFormCard)\//
|
|
45
36
|
|
|
46
37
|
const isAllowedVendorOwner = (file) =>
|
|
47
|
-
file.includes("/src/components/leaves/") ||
|
|
38
|
+
file.includes("/src/components/leaves/") || MECHANICS_BRANCH.test(file) || SURFACE_BRANCH.test(file)
|
|
48
39
|
|
|
49
40
|
/** True for a file inside the component tree, where the boundary applies. */
|
|
50
41
|
const isComponentFile = (filename) => normalizePath(filename).includes("/src/components/")
|
|
@@ -65,15 +56,15 @@ const jsxMemberName = (node) => {
|
|
|
65
56
|
export const vendorBoundary = {
|
|
66
57
|
meta: {
|
|
67
58
|
type: "problem",
|
|
68
|
-
docs: { description: "Vendor imports belong to leaves
|
|
59
|
+
docs: { description: "Vendor imports belong to leaves and named mechanics or surface branches." },
|
|
69
60
|
schema: [],
|
|
70
61
|
messages: {
|
|
71
62
|
outside:
|
|
72
|
-
"This component does not own a vendor primitive. Vendor imports belong to leaves,
|
|
73
|
-
|
|
74
|
-
"
|
|
75
|
-
|
|
76
|
-
"
|
|
63
|
+
"This component does not own a vendor primitive. Vendor imports belong to leaves, ModalBranch/DrawerBranch/DropdownBranch, and the named SurfaceCard family branches that project contracts into vendor bodies.",
|
|
64
|
+
emptyBranch:
|
|
65
|
+
"ModalBranch/DrawerBranch/DropdownBranch must wrap their vendor interaction primitive; otherwise this is an ordinary branch in a privileged mechanics path.",
|
|
66
|
+
legacyShell:
|
|
67
|
+
"`components/shells` is a deleted structural tier. Move vendor mechanics into the named Branch owner; framework route content closes outside the component tier into a named contract projection.",
|
|
77
68
|
},
|
|
78
69
|
},
|
|
79
70
|
create(context) {
|
|
@@ -81,8 +72,8 @@ export const vendorBoundary = {
|
|
|
81
72
|
// Outside the component tree, a provider standing the library up for the whole application is a
|
|
82
73
|
// different thing from a component pulling in a widget.
|
|
83
74
|
if (!isComponentFile(file)) return {}
|
|
84
|
-
const
|
|
85
|
-
const
|
|
75
|
+
const isLegacyShell = file.includes(LEGACY_SHELL_DIR)
|
|
76
|
+
const isMechanicsBranch = MECHANICS_BRANCH.test(file)
|
|
86
77
|
const isTestFile = /\.(?:test|spec)\.[jt]sx?$/.test(file)
|
|
87
78
|
const isAllowed = isAllowedVendorOwner(file)
|
|
88
79
|
let importsVendor = false
|
|
@@ -95,14 +86,8 @@ export const vendorBoundary = {
|
|
|
95
86
|
},
|
|
96
87
|
"Program:exit"(node) {
|
|
97
88
|
if (isTestFile) return
|
|
98
|
-
if (
|
|
99
|
-
|
|
100
|
-
* A shell with no vendor import is empty - except the one whose mechanic is the FRAMEWORK's.
|
|
101
|
-
* `RouteShell` converts the children a Next layout is handed into the component every tier
|
|
102
|
-
* below expects, and it imports nothing to do it. Demanding a vendor primitive there would
|
|
103
|
-
* force an import that means nothing, which is how a rule teaches somebody to add noise.
|
|
104
|
-
*/
|
|
105
|
-
else if (isShell && !importsVendor && !FRAMEWORK_SHELL.test(file)) context.report({ node, messageId: "emptyShell" })
|
|
89
|
+
if (isLegacyShell) context.report({ node, messageId: "legacyShell" })
|
|
90
|
+
else if (isMechanicsBranch && !importsVendor) context.report({ node, messageId: "emptyBranch" })
|
|
106
91
|
},
|
|
107
92
|
}
|
|
108
93
|
},
|
|
@@ -110,22 +95,22 @@ export const vendorBoundary = {
|
|
|
110
95
|
|
|
111
96
|
// -- VENDOR-6 --------------------------------------------------------------------------------------
|
|
112
97
|
|
|
113
|
-
/**
|
|
114
|
-
export const
|
|
98
|
+
/** The modal mechanics branch owns one zero-inset vendor body as the scroll region. */
|
|
99
|
+
export const modalBranchOwnsScrollBody = {
|
|
115
100
|
meta: {
|
|
116
101
|
type: "problem",
|
|
117
|
-
docs: { description: "
|
|
102
|
+
docs: { description: "ModalBranch keeps one zero-inset Modal.Body as its scroll region." },
|
|
118
103
|
schema: [],
|
|
119
104
|
messages: {
|
|
120
105
|
missing:
|
|
121
|
-
"
|
|
106
|
+
"ModalBranch must keep one `Modal.Body` around its named content projection: that vendor body is the dialog's scroll region, not a second content surface.",
|
|
122
107
|
inset:
|
|
123
|
-
"
|
|
108
|
+
"ModalBranch's body must be `className=\"p-0\"`. The mounted contract owns layout; vendor body padding plus contract padding creates a doubled inset.",
|
|
124
109
|
},
|
|
125
110
|
},
|
|
126
111
|
create(context) {
|
|
127
112
|
const file = normalizePath(context.filename || context.getFilename())
|
|
128
|
-
if (!/\/src\/components\/
|
|
113
|
+
if (!/\/src\/components\/branches\/ModalBranch\/index\.tsx$/.test(file)) return {}
|
|
129
114
|
let hasBody = false
|
|
130
115
|
return {
|
|
131
116
|
JSXOpeningElement(node) {
|
|
@@ -295,24 +280,24 @@ export const textLinkUsesHeroLink = {
|
|
|
295
280
|
|
|
296
281
|
// -- VENDOR-11 -------------------------------------------------------------------------------------
|
|
297
282
|
|
|
298
|
-
/** The account block owns the product choices;
|
|
283
|
+
/** The account block owns the product choices; DropdownBranch alone owns HeroUI mechanics. */
|
|
299
284
|
export const accountControlOwnsDropdown = {
|
|
300
285
|
meta: {
|
|
301
286
|
type: "problem",
|
|
302
|
-
docs: { description: "AccountMenu is a block over
|
|
287
|
+
docs: { description: "AccountMenu is a block over DropdownBranch; only the branch imports HeroUI Dropdown." },
|
|
303
288
|
schema: [],
|
|
304
289
|
messages: {
|
|
305
|
-
dropdown: "
|
|
306
|
-
|
|
307
|
-
vendor: "AccountMenu is a product block. Import
|
|
290
|
+
dropdown: "DropdownBranch must import HeroUI `Dropdown`; vendor mechanics belong to the branch.",
|
|
291
|
+
branch: "AccountMenu's pure block half must compose DropdownBranch instead of importing HeroUI directly.",
|
|
292
|
+
vendor: "AccountMenu is a product block. Import DropdownBranch; do not own HeroUI Dropdown here.",
|
|
308
293
|
menu: "ShellNav must render the AccountMenu block from `components/blocks/auth/AccountMenu`.",
|
|
309
294
|
direct: "The account IconButton may not carry an action in ShellNav. Guests open AccountMenu; its Sign in or Sign up choice opens the modal.",
|
|
310
|
-
pieces: "AccountMenu passes typed section data to
|
|
295
|
+
pieces: "AccountMenu passes typed section data to DropdownBranch; it must not import Branch Section or Item pieces and rebuild vendor anatomy itself.",
|
|
311
296
|
},
|
|
312
297
|
},
|
|
313
298
|
create(context) {
|
|
314
299
|
const file = normalizePath(context.filename || context.getFilename())
|
|
315
|
-
const isDropdown = /\/src\/components\/
|
|
300
|
+
const isDropdown = /\/src\/components\/branches\/DropdownBranch\/index\.tsx$/.test(file)
|
|
316
301
|
const isMenu = /\/src\/components\/blocks\/auth\/AccountMenu\/component\.tsx$/.test(file)
|
|
317
302
|
const isNav = /\/src\/components\/layouts\/ShellNav\/component\.tsx$/.test(file)
|
|
318
303
|
if (!isDropdown && !isMenu && !isNav) return {}
|
|
@@ -321,7 +306,7 @@ export const accountControlOwnsDropdown = {
|
|
|
321
306
|
ExportNamedDeclaration(node) {
|
|
322
307
|
if (!isDropdown) return
|
|
323
308
|
const source = context.sourceCode || context.getSourceCode()
|
|
324
|
-
|
|
309
|
+
if (/\bDropdownBranch(?:Item|Section)\b/.test(source.getText(node))) {
|
|
325
310
|
context.report({ node, messageId: "pieces" })
|
|
326
311
|
}
|
|
327
312
|
},
|
|
@@ -330,10 +315,10 @@ export const accountControlOwnsDropdown = {
|
|
|
330
315
|
hasOwner ||= (node.specifiers || []).some((specifier) => specifier.imported?.name === "Dropdown")
|
|
331
316
|
}
|
|
332
317
|
if (isMenu && node.source?.value === "@heroui/react") context.report({ node, messageId: "vendor" })
|
|
333
|
-
if (isMenu && /\/components\/
|
|
318
|
+
if (isMenu && /\/components\/branches\/DropdownBranch$/.test(normalizePath(node.source?.value))) {
|
|
334
319
|
hasOwner = true
|
|
335
320
|
if ((node.specifiers || []).some((specifier) =>
|
|
336
|
-
/^(?:
|
|
321
|
+
/^(?:DropdownBranchSection|DropdownBranchItem)/.test(specifier.imported?.name || ""))) {
|
|
337
322
|
context.report({ node, messageId: "pieces" })
|
|
338
323
|
}
|
|
339
324
|
}
|
|
@@ -350,7 +335,7 @@ export const accountControlOwnsDropdown = {
|
|
|
350
335
|
},
|
|
351
336
|
"Program:exit"(node) {
|
|
352
337
|
if (hasOwner) return
|
|
353
|
-
context.report({ node, messageId: isDropdown ? "dropdown" : isMenu ? "
|
|
338
|
+
context.report({ node, messageId: isDropdown ? "dropdown" : isMenu ? "branch" : "menu" })
|
|
354
339
|
},
|
|
355
340
|
}
|
|
356
341
|
},
|
|
@@ -455,12 +440,12 @@ export const noInternalStarciHref = {
|
|
|
455
440
|
export const authOverlayOwnsSingleContentHost = {
|
|
456
441
|
meta: {
|
|
457
442
|
type: "problem",
|
|
458
|
-
docs: { description: "Authentication has one zero-inset content host inside
|
|
443
|
+
docs: { description: "Authentication has one zero-inset content host inside ModalBranch or DrawerBranch." },
|
|
459
444
|
schema: [],
|
|
460
445
|
messages: {
|
|
461
|
-
duplicate: "SignInOverlay must project
|
|
462
|
-
missing: "SignInOverlay must
|
|
463
|
-
inset: "`centred-page-column` must not add py/pt/pb.
|
|
446
|
+
duplicate: "SignInOverlay must project through one host only: do not open a second `Tree`/`ContractContent` inside a branch that already receives `contract`/`render` directly, and do not open `Tree` by hand instead of importing `ContractContent`.",
|
|
447
|
+
missing: "SignInOverlay must host its content either by importing `ContractContent` for its already-hosted projection, or by passing `contract` and `render` props directly to `ModalBranch`/`DrawerBranch`.",
|
|
448
|
+
inset: "`centred-page-column` must not add py/pt/pb. ModalBranch is zero-inset and the auth content touches that scroll region without a second vertical padding band.",
|
|
464
449
|
},
|
|
465
450
|
},
|
|
466
451
|
create(context) {
|
|
@@ -469,24 +454,63 @@ export const authOverlayOwnsSingleContentHost = {
|
|
|
469
454
|
const isContracts = /\/src\/components\/contracts\/index\.ts$/.test(file)
|
|
470
455
|
if (!isOverlay && !isContracts) return {}
|
|
471
456
|
let hasContent = false
|
|
457
|
+
|
|
458
|
+
/*
|
|
459
|
+
* Two call shapes satisfy the same invariant - one contract-driven content host, zero extra
|
|
460
|
+
* vertical inset - and both are recognised here.
|
|
461
|
+
*
|
|
462
|
+
* (a) THE OLD SHAPE: the overlay imports `ContractContent` and mounts it itself as the branch's
|
|
463
|
+
* children. `ContractContent` IS the checked host, so importing it is enough to credit the
|
|
464
|
+
* overlay with owning one.
|
|
465
|
+
*
|
|
466
|
+
* (b) THE NEW SHAPE: `ModalBranch`/`DrawerBranch` were migrated to take `{contract, render}`
|
|
467
|
+
* directly and build `<Tree contract render />` internally (SLOTS-4). An overlay that hands both
|
|
468
|
+
* props straight to the branch has delegated the same host to the branch itself; there is
|
|
469
|
+
* nothing left for the overlay to import.
|
|
470
|
+
*
|
|
471
|
+
* Matched by JSX ATTRIBUTE NAME, not by where `ModalBranch`/`DrawerBranch` resolve from - same
|
|
472
|
+
* reasoning VENDOR-12 already applies to `ContractContent`/`Tree` below: canon vocabulary means
|
|
473
|
+
* the same thing wherever it is imported from.
|
|
474
|
+
*/
|
|
475
|
+
const MECHANICS_BRANCH_NAMES = new Set(["ModalBranch", "DrawerBranch"])
|
|
476
|
+
const hasJsxAttribute = (attributes, attrName) =>
|
|
477
|
+
(attributes || []).some((attribute) =>
|
|
478
|
+
attribute.type === "JSXAttribute"
|
|
479
|
+
&& attribute.name?.type === "JSXIdentifier"
|
|
480
|
+
&& attribute.name.name === attrName)
|
|
481
|
+
const jsxChildName = (child) => {
|
|
482
|
+
if (child?.type !== "JSXElement") return null
|
|
483
|
+
const name = child.openingElement?.name
|
|
484
|
+
return name?.type === "JSXIdentifier" ? name.name : null
|
|
485
|
+
}
|
|
486
|
+
|
|
472
487
|
return {
|
|
473
488
|
ImportDeclaration(node) {
|
|
474
489
|
if (!isOverlay) return
|
|
475
490
|
/*
|
|
476
|
-
* Matched by the NAME imported, not by where it came from.
|
|
477
|
-
*
|
|
478
|
-
* The path test this replaced only recognised `components/branches/Tree`,
|
|
479
|
-
* which is the single-app layout. A workspace that publishes the same
|
|
480
|
-
* branches from a package satisfied the law and failed the rule, with
|
|
481
|
-
* the only available remedy being an import of a module that does not
|
|
482
|
-
* exist there. `ContractContent` and `Tree` are canon vocabulary: a file
|
|
483
|
-
* importing either means the same thing wherever it resolves from.
|
|
491
|
+
* Matched by the NAME imported, not by where it came from - see the comment above.
|
|
484
492
|
*/
|
|
485
493
|
for (const specifier of node.specifiers || []) {
|
|
486
494
|
if (specifier.imported?.name === "ContractContent") hasContent = true
|
|
495
|
+
// Opening `Tree` by hand is the one thing neither shape ever does: both shapes close the
|
|
496
|
+
// host either inside ContractContent or inside the mechanics branch itself.
|
|
487
497
|
if (specifier.imported?.name === "Tree") context.report({ node: specifier, messageId: "duplicate" })
|
|
488
498
|
}
|
|
489
499
|
},
|
|
500
|
+
JSXElement(node) {
|
|
501
|
+
if (!isOverlay) return
|
|
502
|
+
const name = node.openingElement?.name
|
|
503
|
+
if (name?.type !== "JSXIdentifier" || !MECHANICS_BRANCH_NAMES.has(name.name)) return
|
|
504
|
+
const attributes = node.openingElement.attributes
|
|
505
|
+
if (!hasJsxAttribute(attributes, "contract") || !hasJsxAttribute(attributes, "render")) return
|
|
506
|
+
// The branch itself now owns the host: shape (b).
|
|
507
|
+
hasContent = true
|
|
508
|
+
// A second content host nested as children, when the branch already receives contract/render
|
|
509
|
+
// directly, duplicates the host the branch already opened.
|
|
510
|
+
const nestedHost = (node.children || []).some((child) =>
|
|
511
|
+
jsxChildName(child) === "Tree" || jsxChildName(child) === "ContractContent")
|
|
512
|
+
if (nestedHost) context.report({ node, messageId: "duplicate" })
|
|
513
|
+
},
|
|
490
514
|
Property(node) {
|
|
491
515
|
if (!isContracts) return
|
|
492
516
|
const key = node.key?.type === "Literal" ? node.key.value : node.key?.name
|
|
@@ -504,7 +528,7 @@ export const authOverlayOwnsSingleContentHost = {
|
|
|
504
528
|
/** The rules this law contributes to the plugin. */
|
|
505
529
|
export const rules = {
|
|
506
530
|
"vendor-boundary": vendorBoundary,
|
|
507
|
-
"modal-
|
|
531
|
+
"modal-branch-owns-scroll-body": modalBranchOwnsScrollBody,
|
|
508
532
|
"field-input-uses-secondary-variant": fieldInputUsesSecondaryVariant,
|
|
509
533
|
"field-label-is-text-only": fieldLabelIsTextOnly,
|
|
510
534
|
"no-surface-branch-in-overlay": noSurfaceBranchInOverlay,
|
|
@@ -519,8 +543,7 @@ export const rules = {
|
|
|
519
543
|
* The level this law asks for, as the plugin's own opinion.
|
|
520
544
|
*
|
|
521
545
|
* A repository adopting this with history should expect the outward half to report and the inward
|
|
522
|
-
* half to be silent
|
|
523
|
-
*
|
|
524
|
-
* inward check meaningful afterwards.
|
|
546
|
+
* half to be silent until named mechanics branches exist. Creating those owners is the migration;
|
|
547
|
+
* no anonymous wrapper tier is introduced.
|
|
525
548
|
*/
|
|
526
549
|
export const recommended = Object.fromEntries(Object.keys(rules).map((name) => [`starci-fe/${name}`, "error"]))
|