basalt-ui 1.19.1 → 1.20.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/AGENTS.md +15 -1
- package/CHANGELOG.md +20 -0
- package/MIGRATING.md +228 -0
- package/README.md +53 -9
- package/agent/rules/basalt-app.md +43 -0
- package/agent/rules/basalt-charts.md +26 -14
- package/agent/rules/basalt-mantine.md +25 -10
- package/agent/rules/basalt-router.md +22 -10
- package/agent/rules/basalt-tokens.md +75 -6
- package/agent/skills/basalt-app/SKILL.md +42 -6
- package/configs/check.yml +11 -4
- package/configs/lefthook.yml +21 -2
- package/configs/oxlint-plugin.js +515 -25
- package/configs/oxlint-plugin.test.ts +383 -10
- package/configs/oxlint.json +10 -1
- package/dist/agent/stick-to-bottom.js +1 -1
- package/dist/agent-chat/thread-detail-panel.js +1 -1
- package/dist/agent-chat/thread-message.js +2 -2
- package/dist/agent-chat/tool-chip.js +1 -1
- package/dist/charts/kinds/Donut.d.ts.map +1 -1
- package/dist/charts/kinds/Donut.js +1 -1
- package/dist/charts/primitives/ChartLegend.js +2 -2
- package/dist/charts/primitives/ChartTooltip.js +1 -1
- package/dist/cli/index.d.ts +107 -15
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +610 -72
- package/dist/cli/toolchain-wiring.test.d.ts +2 -0
- package/dist/cli/toolchain-wiring.test.d.ts.map +1 -0
- package/dist/dashboard/delta-badge.js +2 -2
- package/dist/dashboard/stat-card.js +1 -1
- package/dist/data/virtual-list.js +5 -5
- package/dist/guard/index.d.ts +56 -2
- package/dist/guard/index.d.ts.map +1 -1
- package/dist/guard/index.js +326 -59
- package/dist/guard/types.d.ts +25 -5
- package/dist/guard/types.d.ts.map +1 -1
- package/dist/register.d.ts +25 -0
- package/dist/register.d.ts.map +1 -1
- package/dist/shell/app-mobile-nav.module.css +11 -3
- package/dist/surfaces.d.ts +1 -1
- package/dist/surfaces.js +1 -1
- package/dist/tokens/index.d.ts +3 -3
- package/dist/tokens/index.d.ts.map +1 -1
- package/dist/tokens/index.js +2 -0
- package/dist/tokens/palette.d.ts +35 -27
- package/dist/tokens/palette.d.ts.map +1 -1
- package/dist/tokens/palette.js +24 -18
- package/dist/tokens.css +19 -17
- package/dist/vite-color-mix.js +2 -2
- package/llms.txt +3 -3
- package/package.json +4 -3
- package/src/agent/stick-to-bottom.tsx +1 -1
- package/src/agent-chat/thread-detail-panel.tsx +1 -1
- package/src/agent-chat/thread-message.tsx +3 -3
- package/src/agent-chat/tool-chip.tsx +1 -1
- package/src/charts/kinds/Donut.tsx +2 -1
- package/src/charts/primitives/ChartLegend.tsx +2 -2
- package/src/charts/primitives/ChartTooltip.tsx +1 -1
- package/src/cli/index.ts +1070 -99
- package/src/cli/init-sync.test.ts +19 -3
- package/src/cli/tokens-css.test.ts +27 -14
- package/src/cli/toolchain-wiring.test.ts +694 -0
- package/src/dashboard/delta-badge.tsx +3 -3
- package/src/dashboard/stat-card.tsx +1 -1
- package/src/data/virtual-list.tsx +8 -8
- package/src/guard/check-source.test.ts +663 -11
- package/src/guard/index.ts +727 -94
- package/src/guard/types.ts +29 -4
- package/src/register.ts +32 -0
- package/src/shell/app-mobile-nav.module.css +11 -3
- package/src/shell/app-mobile-nav.tsx +1 -1
- package/src/surfaces.ts +1 -1
- package/src/theme/density-relations.test.ts +8 -5
- package/src/theme/spacing.test.ts +2 -0
- package/src/theme-toggle/index.tsx +2 -2
- package/src/tokens/index.ts +5 -3
- package/src/tokens/palette.ts +24 -18
- package/src/vite-color-mix.ts +2 -2
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Unit tests for checkSource — the pure (text, relPath, cfg) → Finding[] core.
|
|
3
3
|
*
|
|
4
|
-
* Covers all
|
|
4
|
+
* Covers all 25 guard kinds. Co-located with the guard, excluded from tsc
|
|
5
5
|
* (tsconfig exclude: src/**\/*.test.ts), run via `bun test`.
|
|
6
6
|
*
|
|
7
7
|
* The walker/reporter half is covered by the integration test in
|
|
@@ -10,7 +10,13 @@
|
|
|
10
10
|
import { describe, expect, it } from 'bun:test'
|
|
11
11
|
import { pxRem } from '../tokens'
|
|
12
12
|
import { SPACE_SCALE } from '../tokens/palette'
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
checkSource,
|
|
15
|
+
DEFAULT_GUARD_CONFIG,
|
|
16
|
+
GENERATED_HEADER_LINE,
|
|
17
|
+
GUARD_RULES,
|
|
18
|
+
TOKENS_ONLY_DISABLED_KINDS,
|
|
19
|
+
} from './index'
|
|
14
20
|
import type { Finding, GuardKind } from './types'
|
|
15
21
|
|
|
16
22
|
const PATH = 'src/Dashboard.tsx'
|
|
@@ -1004,14 +1010,19 @@ describe('raw-font-family', () => {
|
|
|
1004
1010
|
expect(kinds(f)).toContain('raw-font-family')
|
|
1005
1011
|
})
|
|
1006
1012
|
|
|
1007
|
-
|
|
1013
|
+
// A var() REFERENCE is not a literal, whichever custom property it names. The escape used to be
|
|
1014
|
+
// restricted to `--basalt-font-*` / `--mantine-font-family-*`, which reported a framework-free
|
|
1015
|
+
// consumer's `font-family: var(--font-sans)` as "a hardcoded fontFamily literal" and pointed the
|
|
1016
|
+
// fix at the React-only `createBasaltTheme`. The single-entry-point invariant is doctrine for
|
|
1017
|
+
// basalt's own theme layer; this regex cannot tell it apart from a consumer's own indirection.
|
|
1018
|
+
it('does NOT flag a fontFamily bound to a var(...) reference outside basalt’s own prefixes', () => {
|
|
1008
1019
|
const f = find(`fontFamily: 'var(--some-other-var)'`)
|
|
1009
|
-
expect(kinds(f)).toContain('raw-font-family')
|
|
1020
|
+
expect(kinds(f)).not.toContain('raw-font-family')
|
|
1010
1021
|
})
|
|
1011
1022
|
|
|
1012
|
-
it('
|
|
1013
|
-
const f = find(`font-family: var(--
|
|
1014
|
-
expect(kinds(f)).toContain('raw-font-family')
|
|
1023
|
+
it('does NOT flag a kebab-case font-family bound to a consumer var(...) reference', () => {
|
|
1024
|
+
const f = find(`font-family: var(--font-sans);`)
|
|
1025
|
+
expect(kinds(f)).not.toContain('raw-font-family')
|
|
1015
1026
|
})
|
|
1016
1027
|
|
|
1017
1028
|
it('does NOT flag a kebab-case font-family bound to var(--mantine-font-family-…)', () => {
|
|
@@ -1599,6 +1610,59 @@ describe('exemptRules', () => {
|
|
|
1599
1610
|
expect(kinds(f)).toContain('inline-display')
|
|
1600
1611
|
expect(kinds(f)).toContain('raw-html-layout')
|
|
1601
1612
|
})
|
|
1613
|
+
|
|
1614
|
+
// ── EXEMPT_RULE_ALIASES: a grace-minor WIDENING inherits its parent's exemption ───────────────
|
|
1615
|
+
// A kind that exists only to widen an established one is the same rule to a consumer, so an
|
|
1616
|
+
// exemption already written for the parent has to cover it — otherwise the widening arrives as
|
|
1617
|
+
// noise in exactly the paths someone decided the rule does not apply to.
|
|
1618
|
+
|
|
1619
|
+
const HIDDEN =
|
|
1620
|
+
"const s = { display: 'flex', padding: 18 }\nexport const D = () => <div style={s} />"
|
|
1621
|
+
const SHADOW = 'const s = { boxShadow: `0 0 0 2px ${VX.accent}` }'
|
|
1622
|
+
const CSS_SURFACE = '.card { border-radius: 12px; }'
|
|
1623
|
+
|
|
1624
|
+
it("hidden-inline-style inherits raw-html-layout's exemption", () => {
|
|
1625
|
+
expect(kinds(find(HIDDEN, 'src/agent/x.tsx'))).toContain('hidden-inline-style')
|
|
1626
|
+
const f = checkSource(HIDDEN, 'src/agent/x.tsx', {
|
|
1627
|
+
...DEFAULT_GUARD_CONFIG,
|
|
1628
|
+
exemptRules: { 'raw-html-layout': ['agent'] },
|
|
1629
|
+
})
|
|
1630
|
+
expect(kinds(f)).not.toContain('hidden-inline-style')
|
|
1631
|
+
})
|
|
1632
|
+
|
|
1633
|
+
it("surface-shadow-override and css-raw-surface inherit raw-surface's exemption", () => {
|
|
1634
|
+
expect(kinds(find(SHADOW, 'src/agent/x.tsx'))).toContain('surface-shadow-override')
|
|
1635
|
+
expect(kinds(find(CSS_SURFACE, 'src/agent/x.css'))).toContain('css-raw-surface')
|
|
1636
|
+
const cfg = { ...DEFAULT_GUARD_CONFIG, exemptRules: { 'raw-surface': ['agent'] } }
|
|
1637
|
+
expect(kinds(checkSource(SHADOW, 'src/agent/x.tsx', cfg))).not.toContain(
|
|
1638
|
+
'surface-shadow-override',
|
|
1639
|
+
)
|
|
1640
|
+
expect(kinds(checkSource(CSS_SURFACE, 'src/agent/x.css', cfg))).not.toContain('css-raw-surface')
|
|
1641
|
+
})
|
|
1642
|
+
|
|
1643
|
+
it('the inheritance is one-way — exempting the CHILD leaves the parent enforced', () => {
|
|
1644
|
+
const f = checkSource(HIDDEN, 'src/agent/x.tsx', {
|
|
1645
|
+
...DEFAULT_GUARD_CONFIG,
|
|
1646
|
+
exemptRules: { 'hidden-inline-style': ['agent'] },
|
|
1647
|
+
})
|
|
1648
|
+
expect(kinds(f)).not.toContain('hidden-inline-style')
|
|
1649
|
+
expect(
|
|
1650
|
+
kinds(
|
|
1651
|
+
checkSource(TEXT, 'src/agent/x.tsx', {
|
|
1652
|
+
...DEFAULT_GUARD_CONFIG,
|
|
1653
|
+
exemptRules: { 'hidden-inline-style': ['agent'] },
|
|
1654
|
+
}),
|
|
1655
|
+
),
|
|
1656
|
+
).toContain('raw-html-layout')
|
|
1657
|
+
})
|
|
1658
|
+
|
|
1659
|
+
it('an alias does not exempt a path the parent exemption never named', () => {
|
|
1660
|
+
const f = checkSource(SHADOW, 'src/charts/x.tsx', {
|
|
1661
|
+
...DEFAULT_GUARD_CONFIG,
|
|
1662
|
+
exemptRules: { 'raw-surface': ['agent'] },
|
|
1663
|
+
})
|
|
1664
|
+
expect(kinds(f)).toContain('surface-shadow-override')
|
|
1665
|
+
})
|
|
1602
1666
|
})
|
|
1603
1667
|
|
|
1604
1668
|
// ── 20. severity ─────────────────────────────────────────────────────────────
|
|
@@ -1627,10 +1691,34 @@ describe('severity', () => {
|
|
|
1627
1691
|
expect(f.find((v) => v.kind === 'raw-spacing')?.severity).toBe('error')
|
|
1628
1692
|
})
|
|
1629
1693
|
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1694
|
+
// The grace ledger. Five kinds landed warn-only in the round-4 guard minor; deleting an entry
|
|
1695
|
+
// from GRACE_PERIOD_KINDS is the promotion, and it shows up here as an explicit diff.
|
|
1696
|
+
it.each([
|
|
1697
|
+
'theme-allow-unscoped',
|
|
1698
|
+
'surface-shadow-override',
|
|
1699
|
+
'css-raw-surface',
|
|
1700
|
+
'inline-font-size',
|
|
1701
|
+
'hidden-inline-style',
|
|
1702
|
+
])('%s is still in its grace minor (warn)', (kind) => {
|
|
1703
|
+
const cases: Record<string, [string, string]> = {
|
|
1704
|
+
'theme-allow-unscoped': ['const x = 1 // theme-allow', PATH],
|
|
1705
|
+
'surface-shadow-override': ['const s = { boxShadow: `0 0 0 2px ${VX.accent}` }', PATH],
|
|
1706
|
+
'css-raw-surface': ['.a { border-radius: 6px; }', 'src/a.module.css'],
|
|
1707
|
+
'inline-font-size': [`<div style={{ fontSize: 11 }} />`, PATH],
|
|
1708
|
+
'hidden-inline-style': [
|
|
1709
|
+
`const rowStyle = { display: 'flex', gap: 12 }\nexport const C = () => <div style={rowStyle} />`,
|
|
1710
|
+
PATH,
|
|
1711
|
+
],
|
|
1712
|
+
}
|
|
1713
|
+
const [src, path] = cases[kind] as [string, string]
|
|
1714
|
+
const hit = find(src, path).find((v) => v.kind === kind)
|
|
1715
|
+
expect(hit?.severity).toBe('warn')
|
|
1716
|
+
})
|
|
1717
|
+
|
|
1718
|
+
it('ships every OTHER kind past its grace period', () => {
|
|
1719
|
+
// Not a constraint, a LEDGER: when a new kind lands warn-only, this expectation changes in the
|
|
1720
|
+
// same commit, and changing it back is the promotion. A grace entry that outlives its minor
|
|
1721
|
+
// shows up here as an unexplained diff.
|
|
1634
1722
|
const cfg = DEFAULT_GUARD_CONFIG
|
|
1635
1723
|
const everyKind = checkSource(
|
|
1636
1724
|
`<Box p={${SPACE_SCALE.md}} radius={4} style={{ color: '#ff0000', gap: 3 }} />`,
|
|
@@ -1641,3 +1729,567 @@ describe('severity', () => {
|
|
|
1641
1729
|
expect(everyKind.every((v) => v.severity === 'error')).toBe(true)
|
|
1642
1730
|
})
|
|
1643
1731
|
})
|
|
1732
|
+
|
|
1733
|
+
// ── 21. theme-allow placement ────────────────────────────────────────────────
|
|
1734
|
+
|
|
1735
|
+
describe('theme-allow placement', () => {
|
|
1736
|
+
it('honours a standalone comment on the PRECEDING line (the JSX-legal form)', () => {
|
|
1737
|
+
const f = find(
|
|
1738
|
+
`export const C = () => (
|
|
1739
|
+
{/* theme-allow raw-hex — brand asset, fixed by the vendor */}
|
|
1740
|
+
<Box style={{ color: '#ff0000' }} />
|
|
1741
|
+
)`,
|
|
1742
|
+
)
|
|
1743
|
+
expect(kinds(f)).not.toContain('raw-hex')
|
|
1744
|
+
})
|
|
1745
|
+
|
|
1746
|
+
// A TRAILING comment stays scoped to its own line: `const a = '#f00' // theme-allow` must not
|
|
1747
|
+
// silently waive the next line as well.
|
|
1748
|
+
it('does NOT let a trailing comment waive the line below it', () => {
|
|
1749
|
+
const f = find(`const a = '#ff0000' // theme-allow raw-hex — vendor brand\nconst b = '#00ff00'`)
|
|
1750
|
+
expect(f.filter((x) => x.kind === 'raw-hex')).toHaveLength(1)
|
|
1751
|
+
})
|
|
1752
|
+
|
|
1753
|
+
// rb's defect: the shipped oxfmt reflows a long declaration and lands the comment on a different
|
|
1754
|
+
// line than the value, so a same-line-only escape silently stops working. Verified against real
|
|
1755
|
+
// oxfmt output — the hex ends up ABOVE the comment, which is why preceding-line support alone
|
|
1756
|
+
// does not fix it and the walk goes backwards over the declaration.
|
|
1757
|
+
it('reaches back over an oxfmt-reflowed CSS declaration', () => {
|
|
1758
|
+
const f = find(
|
|
1759
|
+
`.root {
|
|
1760
|
+
background-color: var(
|
|
1761
|
+
--mantine-color-body,
|
|
1762
|
+
#232326
|
|
1763
|
+
); /* theme-allow raw-hex — Mantine body fallback for the pre-paint value */
|
|
1764
|
+
}`,
|
|
1765
|
+
'src/styles.css',
|
|
1766
|
+
)
|
|
1767
|
+
expect(kinds(f)).not.toContain('raw-hex')
|
|
1768
|
+
})
|
|
1769
|
+
|
|
1770
|
+
it('stops the backward walk at the previous declaration', () => {
|
|
1771
|
+
const f = find(
|
|
1772
|
+
`.root {
|
|
1773
|
+
color: #112233;
|
|
1774
|
+
background: var(
|
|
1775
|
+
#232326
|
|
1776
|
+
); /* theme-allow raw-hex — fallback */
|
|
1777
|
+
}`,
|
|
1778
|
+
'src/styles.css',
|
|
1779
|
+
)
|
|
1780
|
+
expect(f.filter((x) => x.kind === 'raw-hex')).toHaveLength(1)
|
|
1781
|
+
expect(f.find((x) => x.kind === 'raw-hex')?.line).toBe(2)
|
|
1782
|
+
})
|
|
1783
|
+
})
|
|
1784
|
+
|
|
1785
|
+
// ── 22. theme-allow rule scoping + accountability ────────────────────────────
|
|
1786
|
+
|
|
1787
|
+
describe('theme-allow scoping', () => {
|
|
1788
|
+
it('a scoped annotation waives only the kind it names', () => {
|
|
1789
|
+
const f = find(
|
|
1790
|
+
`<Box p={${SPACE_SCALE.md}} style={{ color: '#ff0000' }} /> // theme-allow raw-hex — vendor brand`,
|
|
1791
|
+
)
|
|
1792
|
+
expect(kinds(f)).not.toContain('raw-hex')
|
|
1793
|
+
expect(kinds(f)).toContain('raw-spacing')
|
|
1794
|
+
})
|
|
1795
|
+
|
|
1796
|
+
it('a bare annotation still waives everything (back-compat) but is reported', () => {
|
|
1797
|
+
const f = find(`<Box p={${SPACE_SCALE.md}} style={{ color: '#ff0000' }} /> // theme-allow`)
|
|
1798
|
+
expect(kinds(f)).not.toContain('raw-hex')
|
|
1799
|
+
expect(kinds(f)).not.toContain('raw-spacing')
|
|
1800
|
+
expect(kinds(f)).toContain('theme-allow-unscoped')
|
|
1801
|
+
})
|
|
1802
|
+
|
|
1803
|
+
it('reports a reason-less scoped annotation too', () => {
|
|
1804
|
+
const f = find(`<Box style={{ color: '#ff0000' }} /> // theme-allow raw-hex`)
|
|
1805
|
+
expect(kinds(f)).toContain('theme-allow-unscoped')
|
|
1806
|
+
})
|
|
1807
|
+
|
|
1808
|
+
it('accepts the accountable form silently', () => {
|
|
1809
|
+
const f = find(
|
|
1810
|
+
`<Box style={{ color: '#ff0000' }} /> // theme-allow raw-hex — vendor brand asset`,
|
|
1811
|
+
)
|
|
1812
|
+
expect(f).toHaveLength(0)
|
|
1813
|
+
})
|
|
1814
|
+
|
|
1815
|
+
it('accepts a basalt/-prefixed plugin rule id as accountable', () => {
|
|
1816
|
+
const f = find(`const x = 1 // theme-allow basalt/hand-rolled-plot — multi-pane, not one plot`)
|
|
1817
|
+
expect(f).toHaveLength(0)
|
|
1818
|
+
})
|
|
1819
|
+
|
|
1820
|
+
// A prose reason is introduced with a separator — the shape every annotation in the wild
|
|
1821
|
+
// already has (`theme-allow: …`, `theme-allow — …`). That is what keeps prose out of the rule-id
|
|
1822
|
+
// slot without having to guess whether an unknown word is a reason or a typo.
|
|
1823
|
+
it('reads a separated reason as a reason, not as rule ids', () => {
|
|
1824
|
+
for (const annotation of ['theme-allow: legacy vendor asset', 'theme-allow — legacy vendor']) {
|
|
1825
|
+
const f = find(`<Box style={{ color: '#ff0000' }} /> // ${annotation}`)
|
|
1826
|
+
expect(kinds(f)).not.toContain('raw-hex')
|
|
1827
|
+
}
|
|
1828
|
+
})
|
|
1829
|
+
|
|
1830
|
+
// ── fail closed: a waiver that names a rule id must never widen into a blanket one ────────────
|
|
1831
|
+
|
|
1832
|
+
it('waives NOTHING when the named rule id is a typo', () => {
|
|
1833
|
+
const f = find(
|
|
1834
|
+
`<Box p={${SPACE_SCALE.md}} style={{ color: '#ff0000' }} /> // theme-allow raw-hexx — vendor brand`,
|
|
1835
|
+
)
|
|
1836
|
+
// The old parse consumed no id, fell through to the empty-`rules` branch, and read the whole
|
|
1837
|
+
// thing as the LEGACY BLANKET form: one mistyped character silenced every kind on the line.
|
|
1838
|
+
expect(kinds(f)).toContain('raw-hex')
|
|
1839
|
+
expect(kinds(f)).toContain('raw-spacing')
|
|
1840
|
+
})
|
|
1841
|
+
|
|
1842
|
+
it('names the unknown id rather than reporting it as "no rule id"', () => {
|
|
1843
|
+
const f = find(`const c = '#ff0000' // theme-allow raw-hexx — vendor brand`)
|
|
1844
|
+
const unscoped = f.find((v) => v.kind === 'theme-allow-unscoped')
|
|
1845
|
+
expect(unscoped?.token).toContain("unknown rule id 'raw-hexx'")
|
|
1846
|
+
})
|
|
1847
|
+
|
|
1848
|
+
it('a typo is never more permissive than the same annotation spelled correctly', () => {
|
|
1849
|
+
const correct = find(`const c = '#ff0000' // theme-allow raw-hex — vendor brand`)
|
|
1850
|
+
const typo = find(`const c = '#ff0000' // theme-allow raw-hexx — vendor brand`)
|
|
1851
|
+
expect(typo.length).toBeGreaterThanOrEqual(correct.length)
|
|
1852
|
+
})
|
|
1853
|
+
|
|
1854
|
+
it('still waives the ids it got right in a mixed list', () => {
|
|
1855
|
+
const f = find(
|
|
1856
|
+
`<Box p={${SPACE_SCALE.md}} style={{ color: '#ff0000' }} /> // theme-allow raw-hex, raw-spacingg — mixed`,
|
|
1857
|
+
)
|
|
1858
|
+
expect(kinds(f)).not.toContain('raw-hex')
|
|
1859
|
+
expect(kinds(f)).toContain('raw-spacing')
|
|
1860
|
+
})
|
|
1861
|
+
|
|
1862
|
+
it('says "not waived", not "waives nothing", when an id in the list DID resolve', () => {
|
|
1863
|
+
const f = find(`const c = '#ff0000' // theme-allow raw-hex, raw-spacingg — mixed`)
|
|
1864
|
+
const unscoped = f.find((v) => v.kind === 'theme-allow-unscoped')
|
|
1865
|
+
expect(unscoped?.token).toBe("theme-allow (unknown rule id 'raw-spacingg' — not waived)")
|
|
1866
|
+
// The claim has to match what the same run actually did: raw-hex IS waived here.
|
|
1867
|
+
expect(kinds(f)).not.toContain('raw-hex')
|
|
1868
|
+
})
|
|
1869
|
+
|
|
1870
|
+
// ── prose after a resolved id is prose, not a typo ────────────────────────────────────────────
|
|
1871
|
+
//
|
|
1872
|
+
// The id slot closes at the first space no comma opened. `theme-allow raw-surface sub-scale …`
|
|
1873
|
+
// is one em-dash away from annotations this package itself ships (see ChartLegend), and reading
|
|
1874
|
+
// `sub-scale` as an attempted id made a scoped, reasoned waiver report as unscoped.
|
|
1875
|
+
|
|
1876
|
+
it('reads an unknown word after a resolved id as the reason', () => {
|
|
1877
|
+
const f = find(
|
|
1878
|
+
'const s = { borderRadius: 3 } // theme-allow raw-surface sub-scale legend corner',
|
|
1879
|
+
)
|
|
1880
|
+
expect(f).toHaveLength(0)
|
|
1881
|
+
})
|
|
1882
|
+
|
|
1883
|
+
it('still fails closed on a typo in the FIRST id slot, separator or not', () => {
|
|
1884
|
+
for (const annotation of [
|
|
1885
|
+
'theme-allow raw-hexx — vendor',
|
|
1886
|
+
'theme-allow raw-hexx vendor asset',
|
|
1887
|
+
]) {
|
|
1888
|
+
expect(kinds(find(`const c = '#ff0000' // ${annotation}`))).toContain('raw-hex')
|
|
1889
|
+
}
|
|
1890
|
+
})
|
|
1891
|
+
|
|
1892
|
+
// Documented break, not a regression: a reason with NO separator introducing it sits in the id
|
|
1893
|
+
// slot, names no rule, and therefore waives nothing. Kept as a test so the behaviour is pinned
|
|
1894
|
+
// rather than rediscovered — MIGRATING.md carries the consumer-facing half.
|
|
1895
|
+
it('does not waive a reason written with no separator introducing it', () => {
|
|
1896
|
+
const f = find(`const c = '#ff0000' // theme-allow legacy vendor asset`)
|
|
1897
|
+
expect(f.find((v) => v.kind === 'raw-hex')?.severity).toBe('error')
|
|
1898
|
+
expect(f.find((v) => v.kind === 'theme-allow-unscoped')?.token).toContain(
|
|
1899
|
+
"unknown rule id 'legacy' — waives nothing",
|
|
1900
|
+
)
|
|
1901
|
+
})
|
|
1902
|
+
|
|
1903
|
+
// `in` walks the prototype chain, so `'constructor' in GUARD_RULES` was true and a reason
|
|
1904
|
+
// starting with that word scoped the waiver to a rule that does not exist.
|
|
1905
|
+
it('does not resolve an Object.prototype key as a rule id', () => {
|
|
1906
|
+
const f = find(`const c = '#ff0000' // theme-allow constructor — inherited key, not a rule`)
|
|
1907
|
+
expect(kinds(f)).toContain('raw-hex')
|
|
1908
|
+
})
|
|
1909
|
+
|
|
1910
|
+
it('accepts an id belonging to a plugin rule that does not honour theme-allow', () => {
|
|
1911
|
+
// Not a guard kind and not theme-allow-aware — but a REAL id, so it parses as accountable
|
|
1912
|
+
// rather than being read as a typo (or, before, as a blanket waiver).
|
|
1913
|
+
const f = find(`const x = 1 // theme-allow ai-sdk-major — intentional producer/consumer skew`)
|
|
1914
|
+
expect(f).toHaveLength(0)
|
|
1915
|
+
})
|
|
1916
|
+
|
|
1917
|
+
it('ships theme-allow-unscoped as a warning, not a build failure', () => {
|
|
1918
|
+
const f = find(`const x = 1 // theme-allow`)
|
|
1919
|
+
expect(f.every((v) => v.severity === 'warn')).toBe(true)
|
|
1920
|
+
})
|
|
1921
|
+
|
|
1922
|
+
// The annotation itself lives in a comment; a `theme-allow` in real code must not waive anything.
|
|
1923
|
+
it('ignores the token when it is not inside a comment', () => {
|
|
1924
|
+
const f = find(`const label = 'theme-allow'\nconst c = '#ff0000'`)
|
|
1925
|
+
expect(kinds(f)).toContain('raw-hex')
|
|
1926
|
+
})
|
|
1927
|
+
})
|
|
1928
|
+
|
|
1929
|
+
// ── 23. surface-shadow-override ──────────────────────────────────────────────
|
|
1930
|
+
|
|
1931
|
+
describe('surface-shadow-override', () => {
|
|
1932
|
+
it('flags a token-composed shadow that REPLACES card depth', () => {
|
|
1933
|
+
const f = find('const s = { boxShadow: `0 0 0 2px ${VX.accent}` }')
|
|
1934
|
+
expect(kinds(f)).toContain('surface-shadow-override')
|
|
1935
|
+
})
|
|
1936
|
+
|
|
1937
|
+
it('does NOT flag one that composes WITH card depth', () => {
|
|
1938
|
+
const f = find('const s = { boxShadow: `${VX.shadowCard}, 0 0 0 2px ${VX.accent}` }')
|
|
1939
|
+
expect(kinds(f)).not.toContain('surface-shadow-override')
|
|
1940
|
+
})
|
|
1941
|
+
|
|
1942
|
+
it('does NOT flag a var(--vx-shadow-…) reference', () => {
|
|
1943
|
+
const f = find(`const s = { boxShadow: 'var(--vx-shadow-card)' }`)
|
|
1944
|
+
expect(kinds(f)).not.toContain('surface-shadow-override')
|
|
1945
|
+
})
|
|
1946
|
+
|
|
1947
|
+
it('still reports a fully literal shadow as raw-surface, not the new kind', () => {
|
|
1948
|
+
const f = find(`const s = { boxShadow: '0 1px 3px black' }`)
|
|
1949
|
+
expect(kinds(f)).toContain('raw-surface')
|
|
1950
|
+
expect(kinds(f)).not.toContain('surface-shadow-override')
|
|
1951
|
+
})
|
|
1952
|
+
|
|
1953
|
+
it('ships as a warning for its grace minor', () => {
|
|
1954
|
+
const f = find('const s = { boxShadow: `0 0 0 2px ${VX.accent}` }')
|
|
1955
|
+
expect(f.find((v) => v.kind === 'surface-shadow-override')?.severity).toBe('warn')
|
|
1956
|
+
})
|
|
1957
|
+
})
|
|
1958
|
+
|
|
1959
|
+
// ── 24. css-raw-surface ──────────────────────────────────────────────────────
|
|
1960
|
+
|
|
1961
|
+
describe('css-raw-surface', () => {
|
|
1962
|
+
it('flags a kebab-case border-radius literal in CSS', () => {
|
|
1963
|
+
const f = find('.a { border-radius: 4px; }', 'src/a.module.css')
|
|
1964
|
+
expect(kinds(f)).toContain('css-raw-surface')
|
|
1965
|
+
})
|
|
1966
|
+
|
|
1967
|
+
it('does NOT flag a var(--vx-radius-…) reference', () => {
|
|
1968
|
+
const f = find('.a { border-radius: var(--vx-radius-card); }', 'src/a.module.css')
|
|
1969
|
+
expect(kinds(f)).not.toContain('css-raw-surface')
|
|
1970
|
+
})
|
|
1971
|
+
|
|
1972
|
+
it('does NOT flag a sub-scale micro-corner below the 4px floor', () => {
|
|
1973
|
+
const f = find('.a { border-radius: 2px; }', 'src/a.module.css')
|
|
1974
|
+
expect(kinds(f)).not.toContain('css-raw-surface')
|
|
1975
|
+
})
|
|
1976
|
+
|
|
1977
|
+
it('does NOT flag the circle/pill shape values', () => {
|
|
1978
|
+
const f = find('.a { border-radius: 50%; }\n.b { border-radius: 9999px; }', 'src/a.module.css')
|
|
1979
|
+
expect(kinds(f)).not.toContain('css-raw-surface')
|
|
1980
|
+
})
|
|
1981
|
+
|
|
1982
|
+
it('flags a literal box-shadow in CSS', () => {
|
|
1983
|
+
const f = find('.a { box-shadow: 0 1px 3px rgba(0,0,0,0.2); }', 'src/a.module.css')
|
|
1984
|
+
expect(kinds(f)).toContain('css-raw-surface')
|
|
1985
|
+
})
|
|
1986
|
+
|
|
1987
|
+
it('never fires in TSX (the camelCase dialect is raw-surface’s)', () => {
|
|
1988
|
+
const f = find(`<div style={{ borderRadius: 8 }} />`)
|
|
1989
|
+
expect(kinds(f)).not.toContain('css-raw-surface')
|
|
1990
|
+
expect(kinds(f)).toContain('raw-surface')
|
|
1991
|
+
})
|
|
1992
|
+
})
|
|
1993
|
+
|
|
1994
|
+
// ── 25. inline-font-size ─────────────────────────────────────────────────────
|
|
1995
|
+
|
|
1996
|
+
describe('inline-font-size', () => {
|
|
1997
|
+
it('flags a unitless inline fontSize (the check-theme-only CI gap)', () => {
|
|
1998
|
+
const f = find(`<div style={{ padding: '6px 8px', fontSize: 11 }} />`)
|
|
1999
|
+
expect(kinds(f)).toContain('inline-font-size')
|
|
2000
|
+
})
|
|
2001
|
+
|
|
2002
|
+
it('flags a px font-size in CSS', () => {
|
|
2003
|
+
const f = find('.a { font-size: 13px; }', 'src/a.module.css')
|
|
2004
|
+
expect(kinds(f)).toContain('inline-font-size')
|
|
2005
|
+
})
|
|
2006
|
+
|
|
2007
|
+
it('does NOT flag a token reference', () => {
|
|
2008
|
+
const f = find(`<div style={{ fontSize: VX.text.sm }} />`)
|
|
2009
|
+
expect(kinds(f)).not.toContain('inline-font-size')
|
|
2010
|
+
})
|
|
2011
|
+
|
|
2012
|
+
it('does NOT flag a relative unit', () => {
|
|
2013
|
+
const f = find('.a { font-size: 0.8rem; }', 'src/a.module.css')
|
|
2014
|
+
expect(kinds(f)).not.toContain('inline-font-size')
|
|
2015
|
+
})
|
|
2016
|
+
})
|
|
2017
|
+
|
|
2018
|
+
// ── 26. hidden-inline-style ──────────────────────────────────────────────────
|
|
2019
|
+
|
|
2020
|
+
describe('hidden-inline-style', () => {
|
|
2021
|
+
it('flags a multi-line-formatted raw div the line scan cannot see', () => {
|
|
2022
|
+
const f = find(
|
|
2023
|
+
`export const C = () => (
|
|
2024
|
+
<div
|
|
2025
|
+
className="x"
|
|
2026
|
+
style={{
|
|
2027
|
+
display: 'flex',
|
|
2028
|
+
padding: 12,
|
|
2029
|
+
}}
|
|
2030
|
+
/>
|
|
2031
|
+
)`,
|
|
2032
|
+
)
|
|
2033
|
+
expect(kinds(f)).toContain('hidden-inline-style')
|
|
2034
|
+
})
|
|
2035
|
+
|
|
2036
|
+
it('flags a hoisted style const passed by identifier', () => {
|
|
2037
|
+
const f = find(
|
|
2038
|
+
`const wrapperStyle = { position: 'relative', width: '100%' }
|
|
2039
|
+
export const C = () => <div className="x" style={wrapperStyle} />`,
|
|
2040
|
+
)
|
|
2041
|
+
expect(kinds(f)).toContain('hidden-inline-style')
|
|
2042
|
+
})
|
|
2043
|
+
|
|
2044
|
+
it('does NOT double-report what raw-html-layout already owns', () => {
|
|
2045
|
+
const f = find(`<div style={{ display: 'flex', padding: 12 }} />`)
|
|
2046
|
+
expect(kinds(f)).toContain('raw-html-layout')
|
|
2047
|
+
expect(kinds(f)).not.toContain('hidden-inline-style')
|
|
2048
|
+
})
|
|
2049
|
+
|
|
2050
|
+
it('does NOT flag a hoisted const with no layout/surface property', () => {
|
|
2051
|
+
const f = find(
|
|
2052
|
+
`const labelStyle = { fontWeight: 500 }
|
|
2053
|
+
export const C = () => <div style={labelStyle} />`,
|
|
2054
|
+
)
|
|
2055
|
+
expect(kinds(f)).not.toContain('hidden-inline-style')
|
|
2056
|
+
})
|
|
2057
|
+
|
|
2058
|
+
it('ships as a warning for its grace minor', () => {
|
|
2059
|
+
const f = find(
|
|
2060
|
+
`const wrapperStyle = { position: 'relative', width: '100%' }
|
|
2061
|
+
export const C = () => <div style={wrapperStyle} />`,
|
|
2062
|
+
)
|
|
2063
|
+
expect(f.find((v) => v.kind === 'hidden-inline-style')?.severity).toBe('warn')
|
|
2064
|
+
})
|
|
2065
|
+
})
|
|
2066
|
+
|
|
2067
|
+
// ── 27. raw-color-fn on computed colors ──────────────────────────────────────
|
|
2068
|
+
|
|
2069
|
+
describe('raw-color-fn — computed values', () => {
|
|
2070
|
+
it('does NOT flag a color function whose first channel is interpolated', () => {
|
|
2071
|
+
const f = find('const swatch = `rgb(${px[0]}, ${px[1]}, ${px[2]})`')
|
|
2072
|
+
expect(kinds(f)).not.toContain('raw-color-fn')
|
|
2073
|
+
})
|
|
2074
|
+
|
|
2075
|
+
it('still flags a literal color with only a variable alpha', () => {
|
|
2076
|
+
const f = find('const s = `rgba(0, 0, 0, ${opacity})`')
|
|
2077
|
+
expect(kinds(f)).toContain('raw-color-fn')
|
|
2078
|
+
})
|
|
2079
|
+
})
|
|
2080
|
+
|
|
2081
|
+
// ── 28. generated-file marker ────────────────────────────────────────────────
|
|
2082
|
+
|
|
2083
|
+
describe('@generated basalt-ui marker', () => {
|
|
2084
|
+
const HEADER = `${GENERATED_HEADER_LINE}\n/* basalt-ui 1.20.0 — \`basalt-ui tokens:css --only core\` */\n`
|
|
2085
|
+
const BODY = ':root {\n --vx-fill-gray: #717176;\n --vx-axis: rgba(255, 255, 255, 0.6);\n}\n'
|
|
2086
|
+
const EMITTED = `${HEADER}${BODY}`
|
|
2087
|
+
|
|
2088
|
+
it('skips a file basalt itself emitted', () => {
|
|
2089
|
+
expect(find(EMITTED, 'src/styles/basalt-tokens.css')).toHaveLength(0)
|
|
2090
|
+
})
|
|
2091
|
+
|
|
2092
|
+
// ── the forgery half: the marker was a whole-file bypass anyone could hand-write ──────────────
|
|
2093
|
+
|
|
2094
|
+
it('ignores the marker entirely outside a .css file', () => {
|
|
2095
|
+
const forged = `${HEADER}export const C = () => <div style={{ color: '#ff0000', padding: 18 }} />\n`
|
|
2096
|
+
expect(kinds(find(forged, 'src/a.tsx'))).toContain('raw-hex')
|
|
2097
|
+
expect(kinds(find(forged, 'src/a.tsx'))).toContain('inline-spacing')
|
|
2098
|
+
})
|
|
2099
|
+
|
|
2100
|
+
it('does not exempt hand-written CSS wearing the header', () => {
|
|
2101
|
+
const forged = `${HEADER}.btn { color: #ff0000; }\n`
|
|
2102
|
+
expect(kinds(find(forged, 'src/a.css'))).toContain('raw-hex')
|
|
2103
|
+
})
|
|
2104
|
+
|
|
2105
|
+
it('does not exempt a generated sheet with an ordinary rule smuggled onto the end', () => {
|
|
2106
|
+
const smuggled = `${EMITTED}.btn { color: #ff0000; }\n`
|
|
2107
|
+
expect(kinds(find(smuggled, 'src/tokens.css'))).toContain('raw-hex')
|
|
2108
|
+
})
|
|
2109
|
+
|
|
2110
|
+
it('requires the provenance line, not just the marker', () => {
|
|
2111
|
+
const bare = `${GENERATED_HEADER_LINE}\n${BODY}`
|
|
2112
|
+
expect(kinds(find(bare, 'src/tokens.css'))).toContain('raw-hex')
|
|
2113
|
+
const loose = `/* @generated basalt-ui */\n${BODY}`
|
|
2114
|
+
expect(kinds(find(loose, 'src/tokens.css'))).toContain('raw-hex')
|
|
2115
|
+
})
|
|
2116
|
+
|
|
2117
|
+
// ── the header WINDOW: exactly the two lines the emitter writes, and no more ──────────────────
|
|
2118
|
+
|
|
2119
|
+
it('honours the header only on lines 1-2', () => {
|
|
2120
|
+
const onLineOne = `${HEADER}${BODY}`
|
|
2121
|
+
expect(find(onLineOne, 'src/tokens.css')).toHaveLength(0)
|
|
2122
|
+
// One blank line ahead of it is already outside the window — the boundary is exact, not "the
|
|
2123
|
+
// first few lines", which is what let a marker be pasted in above unrelated content.
|
|
2124
|
+
const shifted = `\n${HEADER}${BODY}`
|
|
2125
|
+
expect(kinds(find(shifted, 'src/tokens.css'))).toContain('raw-hex')
|
|
2126
|
+
const buried = `${'\n'.repeat(8)}${HEADER}${BODY}`
|
|
2127
|
+
expect(kinds(find(buried, 'src/tokens.css'))).toContain('raw-hex')
|
|
2128
|
+
})
|
|
2129
|
+
|
|
2130
|
+
// ── the second forgery round: the line-shape allowlist itself was forgeable ───────────────────
|
|
2131
|
+
//
|
|
2132
|
+
// Both of the first two were demonstrated against the real `checkSource`, and both were valid,
|
|
2133
|
+
// browser-effective CSS bought a WHOLE-FILE exemption. Every case below must report — a passing
|
|
2134
|
+
// `toHaveLength(0)` here would mean the hole is open again.
|
|
2135
|
+
|
|
2136
|
+
const forgeries: ReadonlyArray<readonly [string, string]> = [
|
|
2137
|
+
// A `;` inside a custom-property value smuggled two ordinary declarations onto a line the
|
|
2138
|
+
// allowlist read as "a basalt custom property".
|
|
2139
|
+
[
|
|
2140
|
+
'a semicolon inside a custom-property value',
|
|
2141
|
+
`${HEADER}:root {\n --vx-pad: 0; box-shadow: 0 0 0 1px #ff0000; border-radius: 11px;\n}\n`,
|
|
2142
|
+
],
|
|
2143
|
+
// The comment branch was `\/\*.*` — it never required the comment to CLOSE, so anything after
|
|
2144
|
+
// `*/` rode along.
|
|
2145
|
+
['a comment that never closes', `${HEADER}/* x */ .btn { color: #ff0000; }\n`],
|
|
2146
|
+
// A comment reopened mid-line: closing and reopening keeps the line comment-SHAPED at both
|
|
2147
|
+
// ends while the middle is a live declaration.
|
|
2148
|
+
['a comment reopened mid-line', `${HEADER}:root {\n /* a */ color: #ff0000 /* b */\n}\n`],
|
|
2149
|
+
// The first line of a multi-line declaration ends in `,`, which is also how a selector list
|
|
2150
|
+
// continues — the depth check is what tells them apart.
|
|
2151
|
+
[
|
|
2152
|
+
'a multi-line declaration whose first line ends in a comma',
|
|
2153
|
+
`${HEADER}.btn {\n box-shadow: 0 0 0 1px #ff0000,\n 0 0 2px #00ff00;\n}\n`,
|
|
2154
|
+
],
|
|
2155
|
+
// The at-rule branch permitted `;`, so a declaration could ride behind `@…;`.
|
|
2156
|
+
['a declaration behind an at-rule', `${HEADER}@media all; color: #ff0000;\n`],
|
|
2157
|
+
// No trailing `;` anywhere, so nothing to split on.
|
|
2158
|
+
['a whole rule on one line with no semicolon', `${HEADER}.btn { color: #ff0000 }\n`],
|
|
2159
|
+
[
|
|
2160
|
+
'a rule reopened after a closing brace',
|
|
2161
|
+
`${HEADER}:root {\n --vx-a: #fff;\n} .btn { color: #ff0000; }\n`,
|
|
2162
|
+
],
|
|
2163
|
+
['a nested block', `${HEADER}:root {\n .btn {\n color: #ff0000;\n }\n}\n`],
|
|
2164
|
+
]
|
|
2165
|
+
|
|
2166
|
+
for (const [what, source] of forgeries) {
|
|
2167
|
+
it(`reports ${what}`, () => {
|
|
2168
|
+
expect(find(source, 'src/tokens.css').length).toBeGreaterThan(0)
|
|
2169
|
+
})
|
|
2170
|
+
}
|
|
2171
|
+
|
|
2172
|
+
it('never skips a line carrying theme-allow, so a hidden waiver still reports', () => {
|
|
2173
|
+
// Without this the annotation would waive the line below AND its own unscoped report, because
|
|
2174
|
+
// a bare `/* theme-allow */` is comment-SHAPED and would otherwise be a skippable line.
|
|
2175
|
+
const smuggled = `${HEADER}/* theme-allow */\n.btn { color: #ff0000; }\n`
|
|
2176
|
+
expect(kinds(find(smuggled, 'src/tokens.css'))).toContain('theme-allow-unscoped')
|
|
2177
|
+
})
|
|
2178
|
+
|
|
2179
|
+
it('exempts LINES, not the file — a smuggled rule reports without burying it in token values', () => {
|
|
2180
|
+
const smuggled = `${EMITTED}.btn { color: #ff0000; }\n`
|
|
2181
|
+
const f = find(smuggled, 'src/tokens.css')
|
|
2182
|
+
expect(f.map((x) => x.line)).toEqual([7])
|
|
2183
|
+
expect(kinds(f)).toEqual(['raw-hex'])
|
|
2184
|
+
})
|
|
2185
|
+
})
|
|
2186
|
+
|
|
2187
|
+
// ── 29. markup files ─────────────────────────────────────────────────────────
|
|
2188
|
+
|
|
2189
|
+
describe('markup files (index.html / *.webmanifest)', () => {
|
|
2190
|
+
it('flags raw hex in index.html', () => {
|
|
2191
|
+
const f = find(`<meta name="theme-color" content="#EDEFF2" />`, 'index.html')
|
|
2192
|
+
expect(kinds(f)).toContain('raw-hex')
|
|
2193
|
+
})
|
|
2194
|
+
|
|
2195
|
+
it('flags raw hex in a webmanifest', () => {
|
|
2196
|
+
const f = find(`{ "theme_color": "#242424" }`, 'public/site.webmanifest')
|
|
2197
|
+
expect(kinds(f)).toContain('raw-hex')
|
|
2198
|
+
})
|
|
2199
|
+
|
|
2200
|
+
it('does not apply the JSX/CSS-in-JS kinds there', () => {
|
|
2201
|
+
const f = find(`<div style="padding: 18px"><input /></div>`, 'index.html')
|
|
2202
|
+
expect(kinds(f)).not.toContain('raw-form-control')
|
|
2203
|
+
expect(kinds(f)).not.toContain('inline-spacing')
|
|
2204
|
+
})
|
|
2205
|
+
|
|
2206
|
+
it('honours a theme-allow inside an HTML comment', () => {
|
|
2207
|
+
const f = find(
|
|
2208
|
+
`<!-- theme-allow raw-hex — favicon mask color -->\n<meta content="#EDEFF2" />`,
|
|
2209
|
+
'index.html',
|
|
2210
|
+
)
|
|
2211
|
+
expect(kinds(f)).not.toContain('raw-hex')
|
|
2212
|
+
})
|
|
2213
|
+
})
|
|
2214
|
+
|
|
2215
|
+
// ── 30. tokens-only profile ──────────────────────────────────────────────────
|
|
2216
|
+
|
|
2217
|
+
describe("profile: 'tokens-only'", () => {
|
|
2218
|
+
const TOKENS_ONLY = { ...DEFAULT_GUARD_CONFIG, profile: 'tokens-only' as const }
|
|
2219
|
+
|
|
2220
|
+
it('drops the kinds whose remedy is a Mantine component', () => {
|
|
2221
|
+
const src = `<input /> // a Mantine-free consumer has no TextInput to reach for`
|
|
2222
|
+
expect(kinds(checkSource(src, PATH, DEFAULT_GUARD_CONFIG))).toContain('raw-form-control')
|
|
2223
|
+
expect(kinds(checkSource(src, PATH, TOKENS_ONLY))).not.toContain('raw-form-control')
|
|
2224
|
+
})
|
|
2225
|
+
|
|
2226
|
+
it('keeps the color kinds — the token layer IS what they consume', () => {
|
|
2227
|
+
const f = checkSource(`.a { color: #ff0000; }`, 'src/a.css', TOKENS_ONLY)
|
|
2228
|
+
expect(kinds(f)).toContain('raw-hex')
|
|
2229
|
+
})
|
|
2230
|
+
|
|
2231
|
+
/**
|
|
2232
|
+
* Every kind, classified by hand: is its remedy a Mantine component, a Mantine prop, or the React
|
|
2233
|
+
* theme factory? The exhaustive literal IS the test — the previous spot check of four kinds is
|
|
2234
|
+
* how `hidden-inline-style` shipped ENABLED under the profile, telling a Mantine-free app to
|
|
2235
|
+
* import Box/Flex in a message copied word for word from `raw-html-layout`, which is disabled.
|
|
2236
|
+
* A new kind fails the first assertion until it is classified here.
|
|
2237
|
+
*/
|
|
2238
|
+
const MANTINE_COUPLED: Record<GuardKind, boolean> = {
|
|
2239
|
+
'raw-hex': false,
|
|
2240
|
+
'raw-color-fn': false,
|
|
2241
|
+
'localstorage-theme': true,
|
|
2242
|
+
'off-identity-accent': true,
|
|
2243
|
+
'mantine-shade-index': true,
|
|
2244
|
+
'raw-spacing': true,
|
|
2245
|
+
'raw-radius': true,
|
|
2246
|
+
'raw-surface': false,
|
|
2247
|
+
'card-with-border': true,
|
|
2248
|
+
'off-system-surface-var': true,
|
|
2249
|
+
'raw-html-layout': true,
|
|
2250
|
+
'inline-spacing': true,
|
|
2251
|
+
'inline-display': true,
|
|
2252
|
+
'raw-visx-axis': true,
|
|
2253
|
+
'raw-motion-value': true,
|
|
2254
|
+
'unframed-chart': true,
|
|
2255
|
+
'chart-missing-aria-label': true,
|
|
2256
|
+
'raw-form-control': true,
|
|
2257
|
+
'sub-16-input-font': true,
|
|
2258
|
+
'raw-font-family': false,
|
|
2259
|
+
'theme-allow-unscoped': false,
|
|
2260
|
+
'surface-shadow-override': false,
|
|
2261
|
+
'css-raw-surface': false,
|
|
2262
|
+
'inline-font-size': false,
|
|
2263
|
+
'hidden-inline-style': true,
|
|
2264
|
+
}
|
|
2265
|
+
|
|
2266
|
+
it('classifies every kind in the registry — the table is exhaustive', () => {
|
|
2267
|
+
expect(Object.keys(MANTINE_COUPLED).toSorted()).toEqual(Object.keys(GUARD_RULES).toSorted())
|
|
2268
|
+
expect(Object.keys(MANTINE_COUPLED)).toHaveLength(25)
|
|
2269
|
+
})
|
|
2270
|
+
|
|
2271
|
+
it('the disabled set is exactly the Mantine-coupled half — a complete partition', () => {
|
|
2272
|
+
for (const [kind, coupled] of Object.entries(MANTINE_COUPLED) as [GuardKind, boolean][]) {
|
|
2273
|
+
expect([kind, TOKENS_ONLY_DISABLED_KINDS.has(kind)]).toEqual([kind, coupled])
|
|
2274
|
+
}
|
|
2275
|
+
expect(TOKENS_ONLY_DISABLED_KINDS.size).toBe(
|
|
2276
|
+
Object.values(MANTINE_COUPLED).filter(Boolean).length,
|
|
2277
|
+
)
|
|
2278
|
+
})
|
|
2279
|
+
|
|
2280
|
+
it('no surviving kind tells a Mantine-free app to reach for a Mantine component', () => {
|
|
2281
|
+
const MANTINE_REMEDY =
|
|
2282
|
+
/@mantine\/|\b(?:Box|Flex|Grid|Stack|Group|TextInput|NumberInput|Select|Textarea)\b/
|
|
2283
|
+
for (const kind of Object.keys(GUARD_RULES) as GuardKind[]) {
|
|
2284
|
+
if (TOKENS_ONLY_DISABLED_KINDS.has(kind)) continue
|
|
2285
|
+
expect([kind, MANTINE_REMEDY.test(GUARD_RULES[kind].message)]).toEqual([kind, false])
|
|
2286
|
+
}
|
|
2287
|
+
})
|
|
2288
|
+
|
|
2289
|
+
it("hidden-inline-style is off — its remedy is raw-html-layout's, word for word", () => {
|
|
2290
|
+
const src =
|
|
2291
|
+
"const s = { display: 'flex', padding: 18 }\nexport const D = () => <div style={s} />"
|
|
2292
|
+
expect(kinds(checkSource(src, PATH, DEFAULT_GUARD_CONFIG))).toContain('hidden-inline-style')
|
|
2293
|
+
expect(kinds(checkSource(src, PATH, TOKENS_ONLY))).not.toContain('hidden-inline-style')
|
|
2294
|
+
})
|
|
2295
|
+
})
|