basalt-ui 1.32.1 → 1.34.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 +1 -1
- package/CHANGELOG.md +65 -0
- package/MIGRATING.md +218 -25
- package/README.md +3 -0
- package/agent/rules/basalt-state.md +2 -2
- package/agent/rules/basalt-tokens.md +1 -1
- package/agent/skills/basalt-app/SKILL.md +5 -3
- package/configs/oxlint-basalt.json +33 -0
- package/configs/oxlint-plugin.js +89 -67
- package/configs/oxlint-plugin.test.ts +224 -19
- package/configs/oxlint.json +2 -30
- package/dist/agent/coalesce.d.ts.map +1 -1
- package/dist/agent/coalesce.js +1 -2
- package/dist/agent/merge.d.ts.map +1 -1
- package/dist/agent/merge.js +1 -2
- package/dist/agent-chat/relative-time.d.ts +9 -0
- package/dist/agent-chat/relative-time.d.ts.map +1 -1
- package/dist/agent-chat/relative-time.js +1 -2
- package/dist/agent-chat/thread-message.d.ts.map +1 -1
- package/dist/agent-chat/thread-message.js +1 -2
- package/dist/charts/kinds/BandStrip.d.ts.map +1 -1
- package/dist/charts/kinds/BandStrip.js +3 -3
- package/dist/charts/kinds/Bars.d.ts.map +1 -1
- package/dist/charts/kinds/Bars.js +1 -2
- package/dist/charts/kinds/StackedArea.d.ts.map +1 -1
- package/dist/charts/kinds/StackedArea.js +1 -2
- package/dist/charts/primitives/legend-disclosure.d.ts.map +1 -1
- package/dist/cli/audit-allows.d.ts +1 -1
- package/dist/cli/audit-allows.d.ts.map +1 -1
- package/dist/cli/audit-allows.js +4 -4
- package/dist/cli/check-theme.d.ts.map +1 -1
- package/dist/cli/check-theme.js +38 -38
- package/dist/cli/config.d.ts +197 -0
- package/dist/cli/config.d.ts.map +1 -0
- package/dist/cli/config.js +20 -0
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/doctor.js +4 -6
- package/dist/cli/guard-hook.d.ts.map +1 -1
- package/dist/cli/guard-hook.js +7 -41
- package/dist/cli/index.d.ts +31 -257
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +64 -37
- package/dist/cli/sync.d.ts.map +1 -1
- package/dist/cli/sync.js +7 -16
- package/dist/commands/define-commands.d.ts.map +1 -1
- package/dist/commands/define-commands.js +2 -2
- package/dist/commands/define-overlays.d.ts.map +1 -1
- package/dist/commands/define-overlays.js +1 -2
- package/dist/common/index.d.ts +0 -1
- package/dist/common/index.d.ts.map +1 -1
- package/dist/common/index.js +0 -2
- package/dist/common/validate.d.ts +2 -2
- package/dist/common/validate.d.ts.map +1 -1
- package/dist/common/validate.js +2 -3
- package/dist/common/vx-families.d.ts +7 -0
- package/dist/common/vx-families.d.ts.map +1 -0
- package/dist/common/vx-families.js +32 -0
- package/dist/common/vx-family.d.ts +11 -0
- package/dist/common/vx-family.d.ts.map +1 -0
- package/dist/common/vx-family.js +12 -0
- package/dist/content/article-card.d.ts.map +1 -1
- package/dist/content/article-card.js +4 -1
- package/dist/controls/enum-filter.d.ts.map +1 -1
- package/dist/controls/enum-filter.js +3 -13
- package/dist/controls/filter-pill.d.ts +8 -0
- package/dist/controls/filter-pill.d.ts.map +1 -1
- package/dist/controls/filter-pill.js +15 -0
- package/dist/controls/multi-select-filter.d.ts.map +1 -1
- package/dist/controls/multi-select-filter.js +3 -13
- package/dist/controls/number-filter.d.ts +3 -0
- package/dist/controls/number-filter.d.ts.map +1 -1
- package/dist/controls/number-filter.js +12 -4
- package/dist/controls/range-filter.d.ts.map +1 -1
- package/dist/controls/range-filter.js +1 -2
- package/dist/data/data-table.d.ts.map +1 -1
- package/dist/data/data-table.js +2 -3
- package/dist/guard/index.d.ts +17 -3
- package/dist/guard/index.d.ts.map +1 -1
- package/dist/guard/index.js +62 -23
- package/dist/guard/types.d.ts +3 -3
- package/dist/guard/types.d.ts.map +1 -1
- package/dist/guard/vx-names.d.ts +3 -0
- package/dist/guard/vx-names.d.ts.map +1 -0
- package/dist/guard/vx-names.js +257 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -2
- package/dist/notifications/mount-guard.d.ts.map +1 -1
- package/dist/notifications/mount-guard.js +1 -2
- package/dist/provider/index.d.ts.map +1 -1
- package/dist/provider/index.js +2 -3
- package/dist/provider/lab-theme.d.ts +5 -4
- package/dist/provider/lab-theme.d.ts.map +1 -1
- package/dist/provider/lab-theme.js +1 -3
- package/dist/router-tanstack/search-store.d.ts +18 -0
- package/dist/router-tanstack/search-store.d.ts.map +1 -1
- package/dist/router-tanstack/search-store.js +33 -1
- package/dist/shell/app-mobile-nav.d.ts.map +1 -1
- package/dist/shell/app-mobile-nav.js +2 -1
- package/dist/shell/app-mobile-nav.module.css +12 -0
- package/dist/shell/app-sidebar.d.ts.map +1 -1
- package/dist/shell/app-sidebar.js +32 -36
- package/dist/shell/app-sidebar.module.css +6 -0
- package/dist/shell/index.d.ts +0 -1
- package/dist/shell/index.d.ts.map +1 -1
- package/dist/shell/index.js +0 -2
- package/dist/shell/mobile-nav-model.d.ts.map +1 -1
- package/dist/shell/mobile-nav-model.js +3 -4
- package/dist/shell/nav-glyph.d.ts +14 -0
- package/dist/shell/nav-glyph.d.ts.map +1 -0
- package/dist/shell/nav-glyph.js +21 -0
- package/dist/shell/nav-types.d.ts +1 -0
- package/dist/shell/nav-types.d.ts.map +1 -1
- package/dist/shell/page-bar.d.ts.map +1 -1
- package/dist/shell/page-bar.js +1 -2
- package/dist/shell/sidebar-search.d.ts.map +1 -1
- package/dist/shell/sidebar-search.js +1 -3
- package/dist/surfaces.d.ts +3 -3
- package/dist/surfaces.d.ts.map +1 -1
- package/dist/surfaces.js +4 -2
- package/dist/tokens/index.d.ts +1 -1
- package/dist/tokens/index.d.ts.map +1 -1
- package/dist/tokens/index.js +24 -2
- package/dist/tokens/size-classes.d.ts +25 -0
- package/dist/tokens/size-classes.d.ts.map +1 -1
- package/dist/tokens/size-classes.js +14 -0
- package/llms.txt +4 -4
- package/package.json +1 -1
- package/src/agent/coalesce.ts +1 -2
- package/src/agent/merge.ts +1 -2
- package/src/agent-chat/relative-time.ts +1 -2
- package/src/agent-chat/thread-message.tsx +2 -3
- package/src/charts/kinds/BandStrip.tsx +7 -3
- package/src/charts/kinds/Bars.tsx +1 -2
- package/src/charts/kinds/StackedArea.tsx +4 -2
- package/src/charts/primitives/legend-disclosure.tsx +1 -0
- package/src/cli/audit-allows.ts +10 -9
- package/src/cli/check-theme.ts +61 -39
- package/src/cli/config.ts +209 -0
- package/src/cli/doctor.ts +5 -7
- package/src/cli/guard-hook.ts +9 -42
- package/src/cli/index.ts +101 -294
- package/src/cli/sync.ts +7 -16
- package/src/commands/define-commands.ts +2 -2
- package/src/commands/define-overlays.ts +1 -2
- package/src/common/index.ts +0 -1
- package/src/common/validate.ts +4 -5
- package/src/common/vx-families.ts +36 -0
- package/src/common/vx-family.ts +22 -0
- package/src/content/article-card.tsx +1 -0
- package/src/controls/enum-filter.tsx +3 -13
- package/src/controls/filter-pill.tsx +20 -0
- package/src/controls/multi-select-filter.tsx +3 -13
- package/src/controls/number-filter.tsx +24 -3
- package/src/controls/range-filter.tsx +1 -2
- package/src/data/data-table.tsx +2 -3
- package/src/guard/index.ts +134 -47
- package/src/guard/types.ts +3 -2
- package/src/guard/vx-names.ts +257 -0
- package/src/index.ts +0 -2
- package/src/notifications/mount-guard.ts +1 -2
- package/src/provider/index.tsx +2 -3
- package/src/provider/lab-theme.ts +28 -21
- package/src/router-tanstack/search-store.ts +73 -1
- package/src/shell/app-mobile-nav.module.css +12 -0
- package/src/shell/app-mobile-nav.tsx +8 -1
- package/src/shell/app-sidebar.module.css +6 -0
- package/src/shell/app-sidebar.tsx +52 -50
- package/src/shell/index.tsx +0 -1
- package/src/shell/mobile-nav-model.ts +3 -5
- package/src/shell/nav-glyph.tsx +38 -0
- package/src/shell/nav-types.ts +1 -0
- package/src/shell/page-bar.tsx +1 -4
- package/src/shell/sidebar-search.tsx +1 -5
- package/src/surfaces.ts +4 -2
- package/src/tokens/index.ts +34 -2
- package/src/tokens/size-classes.ts +27 -0
- package/dist/common/is-dev.d.ts +0 -3
- package/dist/common/is-dev.d.ts.map +0 -1
- package/dist/common/is-dev.js +0 -6
- package/dist/shell/use-breakpoint.d.ts +0 -10
- package/dist/shell/use-breakpoint.d.ts.map +0 -1
- package/dist/shell/use-breakpoint.js +0 -11
- package/src/common/is-dev.ts +0 -4
- package/src/shell/use-breakpoint.ts +0 -37
package/src/agent/coalesce.ts
CHANGED
|
@@ -38,7 +38,6 @@
|
|
|
38
38
|
* <PartList parts={coalesceParts(parts)} />
|
|
39
39
|
*/
|
|
40
40
|
import type { AgentPart, ReasoningPart, TextPart, ToolCallPart } from './parts'
|
|
41
|
-
import { isDev } from '../common/is-dev'
|
|
42
41
|
|
|
43
42
|
type TextLike = TextPart | ReasoningPart
|
|
44
43
|
|
|
@@ -170,7 +169,7 @@ function mergeToolPart(existing: ToolCallPart, next: ToolCallPart): ToolCallPart
|
|
|
170
169
|
// and make every field read below a compile error, so the assertion is what makes the
|
|
171
170
|
// runtime value usable. It asserts nothing tsc has not already proved on the line above.
|
|
172
171
|
const fallback = unhandled as ToolCallPart
|
|
173
|
-
if (
|
|
172
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
174
173
|
console.warn(
|
|
175
174
|
`[basalt] coalesceParts: unrecognized tool state ${JSON.stringify(fallback.state)} — kept, merged best-effort`,
|
|
176
175
|
)
|
package/src/agent/merge.ts
CHANGED
|
@@ -29,7 +29,6 @@
|
|
|
29
29
|
* // parts === [{ id: 'p1', type: 'text', text: 'Hello' }]
|
|
30
30
|
*/
|
|
31
31
|
import type { ReasoningPart, TextPart } from './parts'
|
|
32
|
-
import { isDev } from '../common/is-dev'
|
|
33
32
|
|
|
34
33
|
type TextLike = TextPart | ReasoningPart
|
|
35
34
|
|
|
@@ -79,7 +78,7 @@ function clampOffset(offset: number, length: number): number {
|
|
|
79
78
|
// A non-finite offset would make every slice() return '' and blank the part outright — it is
|
|
80
79
|
// out of range like any other, and appending is the safe reading of "position unknown".
|
|
81
80
|
const clamped = Number.isFinite(offset) ? Math.min(Math.max(offset, 0), length) : length
|
|
82
|
-
if (clamped !== offset &&
|
|
81
|
+
if (clamped !== offset && process.env.NODE_ENV !== 'production') {
|
|
83
82
|
console.warn(
|
|
84
83
|
`[basalt] mergePart: text offset ${offset} out of range [0, ${length}] — clamped to ${clamped}`,
|
|
85
84
|
)
|
|
@@ -7,7 +7,6 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Internal to `agent-chat/` — not part of the public barrel.
|
|
9
9
|
*/
|
|
10
|
-
import { isDev } from '../common/is-dev'
|
|
11
10
|
|
|
12
11
|
const RELATIVE_TIME_FORMAT = new Intl.RelativeTimeFormat('en', { numeric: 'auto' })
|
|
13
12
|
|
|
@@ -41,7 +40,7 @@ const RELATIVE_TIME_UNITS: readonly {
|
|
|
41
40
|
*/
|
|
42
41
|
export function formatRelativeTime(timestamp: number): string {
|
|
43
42
|
if (!Number.isFinite(timestamp)) {
|
|
44
|
-
if (
|
|
43
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
45
44
|
console.warn(
|
|
46
45
|
`[basalt] formatRelativeTime: non-finite timestamp ${String(timestamp)} — rendering empty string`,
|
|
47
46
|
)
|
|
@@ -60,7 +60,6 @@ import { formatRelativeTime } from './relative-time'
|
|
|
60
60
|
import { ToolChip } from './tool-chip'
|
|
61
61
|
import { resolveVirtualize } from './virtualize'
|
|
62
62
|
import type { VirtualizeOptions, VirtualizeProps } from './virtualize'
|
|
63
|
-
import { isDev } from '../common/is-dev'
|
|
64
63
|
|
|
65
64
|
/** The mono, uppercase, letter-spaced micro-label idiom (docs/DESIGN-SPEC.md §3) — shared by the
|
|
66
65
|
* transcript's role labels and the reasoning/tool-call headers below. */
|
|
@@ -189,11 +188,11 @@ function UnknownPartChip({ part }: { part: ForeignPart }): JSX.Element {
|
|
|
189
188
|
)
|
|
190
189
|
}
|
|
191
190
|
|
|
192
|
-
/** Consulted only when `ThreadTranscript` gets no `fallbackRenderer` prop.
|
|
191
|
+
/** Consulted only when `ThreadTranscript` gets no `fallbackRenderer` prop. The gate reads
|
|
193
192
|
* `process.env.NODE_ENV` (not `import.meta.env` — basalt-ui bans it) INSIDE the render so tests can
|
|
194
193
|
* flip it. */
|
|
195
194
|
const DEFAULT_FALLBACK_RENDERER: PartRenderer<ForeignPart> = ({ part }) =>
|
|
196
|
-
|
|
195
|
+
process.env.NODE_ENV !== 'production' ? <UnknownPartChip part={part} /> : null
|
|
197
196
|
|
|
198
197
|
// ── Part resolution — the three-step order that is the whole design ───────────
|
|
199
198
|
//
|
|
@@ -17,7 +17,6 @@ import type { ChartState } from '../primitives/ChartPending'
|
|
|
17
17
|
import type { ChartLegendConfig, SeriesStyle } from '../series'
|
|
18
18
|
import { fmtAxisDate } from '../utils/format'
|
|
19
19
|
import { BandAxisOverlay, BandTooltip, clampFraction } from './band-chrome'
|
|
20
|
-
import { isDev } from '../../common/is-dev'
|
|
21
20
|
|
|
22
21
|
/**
|
|
23
22
|
* A state a strip can draw. Extends `SeriesStyle` rather than `ChartSeries` because a band has no
|
|
@@ -266,7 +265,8 @@ function BandStripPlot<T>(props: BandStripPlotProps<T>) {
|
|
|
266
265
|
const style = styleByKey.get(span.state)
|
|
267
266
|
const x = i * step
|
|
268
267
|
if (style === undefined) {
|
|
269
|
-
if (
|
|
268
|
+
if (process.env.NODE_ENV !== 'production')
|
|
269
|
+
throw unknownStateError('BandSpan.state', span.state, styleByKey.keys())
|
|
270
270
|
out.push(
|
|
271
271
|
<rect
|
|
272
272
|
key={getX(d)}
|
|
@@ -298,7 +298,11 @@ function BandStripPlot<T>(props: BandStripPlotProps<T>) {
|
|
|
298
298
|
const hatchWidth = bandWidth - measuredWidth
|
|
299
299
|
const markerKey = span.marker?.state ?? span.state
|
|
300
300
|
const markerColor = styleByKey.get(markerKey)?.color
|
|
301
|
-
if (
|
|
301
|
+
if (
|
|
302
|
+
span.marker !== undefined &&
|
|
303
|
+
markerColor === undefined &&
|
|
304
|
+
process.env.NODE_ENV !== 'production'
|
|
305
|
+
) {
|
|
302
306
|
throw unknownStateError('BandSpan.marker.state', markerKey, styleByKey.keys())
|
|
303
307
|
}
|
|
304
308
|
const inset = span.marker?.inset ?? DEFAULT_MARKER_INSET
|
|
@@ -13,7 +13,6 @@ import type { ChartState } from '../primitives/ChartPending'
|
|
|
13
13
|
import { curveFor, LINE_OVERLAY_STROKE_WIDTH } from '../series'
|
|
14
14
|
import type { ChartLegendConfig, ChartSeries, SeriesCurve } from '../series'
|
|
15
15
|
import { padAutoLower } from '../utils/domain'
|
|
16
|
-
import { isDev } from '../../common/is-dev'
|
|
17
16
|
|
|
18
17
|
/**
|
|
19
18
|
* `T = unknown` is load-bearing, not a shortcut: it keeps a consumer's existing
|
|
@@ -196,7 +195,7 @@ function BarsInner<T>(props: BarsProps<T>) {
|
|
|
196
195
|
// Stacking sums bar heights via `posOffset`/`negOffset` in yScale units directly — a log axis
|
|
197
196
|
// has no additive zero, so a stacked total is meaningless on it (`docs/CHARTS-SPEC.md`'s null-gap
|
|
198
197
|
// + log contract). `grouped` bars only ever read one bar's own scaled value, so they're unaffected.
|
|
199
|
-
if (
|
|
198
|
+
if (process.env.NODE_ENV !== 'production' && barLayout === 'stacked' && y?.scale === 'log') {
|
|
200
199
|
throw new Error(
|
|
201
200
|
'Bars: stacked bars cannot use a log axis (y.scale: "log") — the stack sums bar heights, ' +
|
|
202
201
|
"and a log axis has no additive zero to sum from. Use barLayout: 'grouped' instead.",
|
|
@@ -9,7 +9,6 @@ import type { ResponsiveChartHeight } from '../primitives/ChartFrame'
|
|
|
9
9
|
import type { ChartState } from '../primitives/ChartPending'
|
|
10
10
|
import { curveFor } from '../series'
|
|
11
11
|
import type { ChartLegendConfig, ChartSeries } from '../series'
|
|
12
|
-
import { isDev } from '../../common/is-dev'
|
|
13
12
|
|
|
14
13
|
export type StackedAreaProps<T> = BasaltProps & {
|
|
15
14
|
data: T[]
|
|
@@ -113,7 +112,10 @@ function StackedAreaInner<T>(props: StackedAreaProps<T>) {
|
|
|
113
112
|
|
|
114
113
|
// The stack sums band heights via `getValue` totals directly — a log axis has no additive zero
|
|
115
114
|
// to sum from (`docs/CHARTS-SPEC.md`'s null-gap + log contract, mirrored from `Bars`' own guard).
|
|
116
|
-
if (
|
|
115
|
+
if (
|
|
116
|
+
process.env.NODE_ENV !== 'production' &&
|
|
117
|
+
(y as { scale?: string } | undefined)?.scale === 'log'
|
|
118
|
+
) {
|
|
117
119
|
throw new Error(
|
|
118
120
|
'StackedArea: cannot use a log axis (y.scale: "log") — the stack sums band heights, ' +
|
|
119
121
|
'and a log axis has no additive zero to sum from.',
|
|
@@ -43,6 +43,7 @@ function disclosurePlacement(chip: DOMRect, coarse: boolean): CSSProperties {
|
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
45
|
const below = window.innerHeight - chip.bottom
|
|
46
|
+
// theme-allow raw-breakpoint — clamps a fixed panel inside the viewport; a position, not a breakpoint
|
|
46
47
|
const left = Math.max(8, Math.min(chip.left, window.innerWidth - 248))
|
|
47
48
|
const base = { left, right: 'auto', minWidth: 200, maxWidth: 'calc(100vw - 16px)' }
|
|
48
49
|
return below >= 200 || below >= chip.top
|
package/src/cli/audit-allows.ts
CHANGED
|
@@ -13,8 +13,8 @@ import {
|
|
|
13
13
|
unmatchedExemptPatterns,
|
|
14
14
|
} from '../guard'
|
|
15
15
|
import type { Finding, GuardConfig, GuardKind } from '../guard'
|
|
16
|
-
import
|
|
17
|
-
import {
|
|
16
|
+
import { exemptRulePaths, exemptRuleReason, resolveRoots } from './config'
|
|
17
|
+
import type { BasaltConfig, ExemptRuleEntry } from './config'
|
|
18
18
|
|
|
19
19
|
// ── check-theme --audit-allows — every waiver, and whether it still suppresses anything ─────────
|
|
20
20
|
|
|
@@ -30,7 +30,7 @@ type PluginProbe = {
|
|
|
30
30
|
readonly rel: string
|
|
31
31
|
/** 1-based line of the annotation, as {@link findAllowAnnotations} reports it. */
|
|
32
32
|
readonly line: number
|
|
33
|
-
readonly
|
|
33
|
+
readonly reach: string
|
|
34
34
|
readonly site: string
|
|
35
35
|
readonly suffix: string
|
|
36
36
|
}
|
|
@@ -231,14 +231,15 @@ export function auditAllows(
|
|
|
231
231
|
waiverLines.push(`${label} suppresses ${[...new Set(revealed)].join(', ')}${suffix}`)
|
|
232
232
|
continue
|
|
233
233
|
}
|
|
234
|
-
// Nothing
|
|
235
|
-
|
|
236
|
-
if (site.pluginRules.length > 0 && site.guardKinds.length === 0) {
|
|
234
|
+
// Nothing the guard reaches moved: oxlint decides it (a placeholder keeps the report order).
|
|
235
|
+
if (site.oxlintRules.length > 0) {
|
|
237
236
|
pluginProbes.push({
|
|
238
237
|
slot: waiverLines.length,
|
|
239
238
|
rel,
|
|
240
239
|
line: site.line,
|
|
241
|
-
|
|
240
|
+
reach: site.bare
|
|
241
|
+
? 'a bare annotation, waiving every oxlint plugin rule on its line'
|
|
242
|
+
: `scoped to ${site.oxlintRules.join(', ')}, which oxlint judges`,
|
|
242
243
|
site: label,
|
|
243
244
|
suffix,
|
|
244
245
|
})
|
|
@@ -266,8 +267,8 @@ export function auditAllows(
|
|
|
266
267
|
if (revealed === undefined) {
|
|
267
268
|
outOfReach++
|
|
268
269
|
waiverLines[probe.slot] =
|
|
269
|
-
`${probe.site}
|
|
270
|
-
`
|
|
270
|
+
`${probe.site} ${probe.reach} — and oxlint could not be run here, so this audit ` +
|
|
271
|
+
`cannot judge it${probe.suffix}`
|
|
271
272
|
continue
|
|
272
273
|
}
|
|
273
274
|
if (revealed.length > 0) {
|
package/src/cli/check-theme.ts
CHANGED
|
@@ -3,32 +3,80 @@
|
|
|
3
3
|
* Imports its shared plumbing (config/project resolution) back from `./index`, and its
|
|
4
4
|
* `--audit-allows` mode from `./audit-allows`.
|
|
5
5
|
*/
|
|
6
|
-
import { readFileSync } from 'node:fs'
|
|
7
|
-
import { resolve } from 'node:path'
|
|
6
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
7
|
+
import { dirname, resolve } from 'node:path'
|
|
8
8
|
|
|
9
9
|
import {
|
|
10
10
|
checkSource,
|
|
11
|
-
DEFAULT_GUARD_CONFIG,
|
|
12
11
|
guardKindRemedy,
|
|
13
12
|
TOKENS_ONLY_DISABLED_KINDS,
|
|
14
13
|
unmatchedExemptPatterns,
|
|
15
14
|
} from '../guard'
|
|
16
|
-
import type { Finding
|
|
15
|
+
import type { Finding } from '../guard'
|
|
17
16
|
import { auditAllows } from './audit-allows'
|
|
17
|
+
import { DEFAULT_ROOTS, resolveRoots } from './config'
|
|
18
18
|
import {
|
|
19
|
+
basaltPresetEntry,
|
|
19
20
|
conflictingProfileFlags,
|
|
21
|
+
findRepoRoot,
|
|
20
22
|
declaredProfile,
|
|
21
|
-
DEFAULT_ROOTS,
|
|
22
23
|
hasBasaltKey,
|
|
24
|
+
namesShipped,
|
|
25
|
+
noteRelocation,
|
|
26
|
+
OXLINT_FILES,
|
|
27
|
+
packageRoot,
|
|
28
|
+
parseJsonc,
|
|
23
29
|
readBasaltConfig,
|
|
24
|
-
|
|
30
|
+
readIfExists,
|
|
31
|
+
readSource,
|
|
32
|
+
resolveGuardConfig,
|
|
25
33
|
resolveProjectDir,
|
|
26
|
-
resolveRoots,
|
|
27
34
|
scannableFiles,
|
|
28
35
|
SERIES_MODULE_HINT_RE,
|
|
29
36
|
waiverHintFor,
|
|
30
37
|
} from './index'
|
|
31
38
|
|
|
39
|
+
/**
|
|
40
|
+
* What the `.oxlintrc.json` in `dir` does for the AST lane. A plugin load alone enables no rule, so
|
|
41
|
+
* it counts only beside a `basalt/*` rule of its own; an extends counts when it is a shipped preset.
|
|
42
|
+
*/
|
|
43
|
+
function oxlintLaneAt(dir: string): 'absent' | 'wired' | 'unwired' {
|
|
44
|
+
const raw = readIfExists(resolve(dir, '.oxlintrc.json'))
|
|
45
|
+
if (raw === null) return 'absent'
|
|
46
|
+
const rc = parseJsonc(raw)
|
|
47
|
+
const ownRules = Object.keys(Object(rc?.['rules'])).some((id) => id.startsWith('basalt/'))
|
|
48
|
+
const plugin = ownRules
|
|
49
|
+
? [rc?.['jsPlugins']].flat().find((e) => namesShipped(e, OXLINT_FILES.plugin))
|
|
50
|
+
: undefined
|
|
51
|
+
const entry = basaltPresetEntry(rc?.['extends']) ?? plugin
|
|
52
|
+
return entry !== undefined && existsSync(resolve(dir, entry)) ? 'wired' : 'unwired'
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* One line when the nearest `.oxlintrc.json` up to the repo root does not wire the AST lane: the
|
|
57
|
+
* basalt/* rules — two of the 1.33.0 promotions among them — live only there, and a
|
|
58
|
+
* check-theme-only CI otherwise reads as the whole guard (emailgw r4). Doctor names the fix.
|
|
59
|
+
*/
|
|
60
|
+
function astLaneNotice(cwd: string): string | null {
|
|
61
|
+
const root = findRepoRoot(cwd)
|
|
62
|
+
for (let dir = cwd; ; dir = dirname(dir)) {
|
|
63
|
+
const lane = oxlintLaneAt(dir)
|
|
64
|
+
if (lane === 'wired') return null
|
|
65
|
+
if (lane === 'unwired' || dir === root || dirname(dir) === dir) break
|
|
66
|
+
}
|
|
67
|
+
const preset = readSource(packageRoot(), OXLINT_FILES.guards)
|
|
68
|
+
const count =
|
|
69
|
+
preset === null
|
|
70
|
+
? `every (${OXLINT_FILES.guards} unreadable, so uncounted)`
|
|
71
|
+
: String(preset.match(/"basalt\/[\w-]+":/g)?.length ?? 0)
|
|
72
|
+
return (
|
|
73
|
+
`ℹ basalt-ui check-theme: AST lane not enforced — ${count} basalt/* rule ids unguarded ` +
|
|
74
|
+
'(incl. the promoted control-outside-home, raw-breakpoint): no .oxlintrc.json up to the repo ' +
|
|
75
|
+
'root extends basalt-ui/configs/oxlint.json (or the guards-only oxlint-basalt.json). `basalt-ui ' +
|
|
76
|
+
'doctor` names the fix.'
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
|
|
32
80
|
/**
|
|
33
81
|
* Theme guard — thin FS walker over the headless `../guard` core. Reads BasaltConfig, builds a
|
|
34
82
|
* GuardConfig, walks roots, calls checkSource per file, collects Finding[], groups/reports, returns
|
|
@@ -41,19 +89,10 @@ export function checkTheme(
|
|
|
41
89
|
invocationCwd: string = process.cwd(),
|
|
42
90
|
flags: readonly string[] = [],
|
|
43
91
|
): number {
|
|
44
|
-
if (conflictingProfileFlags(flags))
|
|
45
|
-
console.error(
|
|
46
|
-
'basalt-ui check-theme: --tokens-only and --framework are alternatives — pass one.',
|
|
47
|
-
)
|
|
48
|
-
return 1
|
|
49
|
-
}
|
|
92
|
+
if (conflictingProfileFlags('check-theme', flags)) return 1
|
|
50
93
|
const project = resolveProjectDir(invocationCwd)
|
|
51
94
|
const cwd = project.dir
|
|
52
|
-
|
|
53
|
-
console.log(
|
|
54
|
-
`basalt-ui check-theme: BASALT_CWD relocated from ${project.relocatedFrom} to ${cwd}.`,
|
|
55
|
-
)
|
|
56
|
-
}
|
|
95
|
+
noteRelocation('check-theme', project)
|
|
57
96
|
const cfg = readBasaltConfig(cwd)
|
|
58
97
|
const roots = resolveRoots(cfg)
|
|
59
98
|
const profile = declaredProfile(cfg, flags)
|
|
@@ -65,27 +104,7 @@ export function checkTheme(
|
|
|
65
104
|
)
|
|
66
105
|
}
|
|
67
106
|
|
|
68
|
-
const guardCfg
|
|
69
|
-
spacingSteps: cfg.spacingSteps ?? DEFAULT_GUARD_CONFIG.spacingSteps,
|
|
70
|
-
rawRadius: cfg.rawRadius ?? DEFAULT_GUARD_CONFIG.rawRadius,
|
|
71
|
-
forbiddenAccents: cfg.forbiddenAccents ?? DEFAULT_GUARD_CONFIG.forbiddenAccents,
|
|
72
|
-
mantineShadeIndex: cfg.mantineShadeIndex ?? DEFAULT_GUARD_CONFIG.mantineShadeIndex,
|
|
73
|
-
rawSurface: cfg.rawSurface ?? DEFAULT_GUARD_CONFIG.rawSurface,
|
|
74
|
-
cardWithBorder: cfg.cardWithBorder ?? DEFAULT_GUARD_CONFIG.cardWithBorder,
|
|
75
|
-
offSystemSurfaceVar: cfg.offSystemSurfaceVar ?? DEFAULT_GUARD_CONFIG.offSystemSurfaceVar,
|
|
76
|
-
rawHtmlLayout: cfg.rawHtmlLayout ?? DEFAULT_GUARD_CONFIG.rawHtmlLayout,
|
|
77
|
-
inlineSpacing: cfg.inlineSpacing ?? DEFAULT_GUARD_CONFIG.inlineSpacing,
|
|
78
|
-
inlineDisplay: cfg.inlineDisplay ?? DEFAULT_GUARD_CONFIG.inlineDisplay,
|
|
79
|
-
rawVisxAxis: cfg.rawVisxAxis ?? DEFAULT_GUARD_CONFIG.rawVisxAxis,
|
|
80
|
-
rawMotionValue: cfg.rawMotionValue ?? DEFAULT_GUARD_CONFIG.rawMotionValue,
|
|
81
|
-
chartMissingAriaLabel: cfg.chartMissingAriaLabel ?? DEFAULT_GUARD_CONFIG.chartMissingAriaLabel,
|
|
82
|
-
rawFormControl: cfg.rawFormControl ?? DEFAULT_GUARD_CONFIG.rawFormControl,
|
|
83
|
-
sub16InputFont: cfg.sub16InputFont ?? DEFAULT_GUARD_CONFIG.sub16InputFont,
|
|
84
|
-
allowComment: 'theme-allow',
|
|
85
|
-
exemptRules: resolveExemptRules(cfg),
|
|
86
|
-
severity: cfg.severity ?? DEFAULT_GUARD_CONFIG.severity,
|
|
87
|
-
...(profile === 'tokens-only' ? { profile: 'tokens-only' as const } : {}),
|
|
88
|
-
}
|
|
107
|
+
const guardCfg = resolveGuardConfig(cfg, profile)
|
|
89
108
|
|
|
90
109
|
const findings: Finding[] = []
|
|
91
110
|
const scanned = scannableFiles(cwd, cfg)
|
|
@@ -143,6 +162,9 @@ export function checkTheme(
|
|
|
143
162
|
)
|
|
144
163
|
}
|
|
145
164
|
|
|
165
|
+
const notice = profile === 'tokens-only' ? null : astLaneNotice(cwd)
|
|
166
|
+
if (notice !== null) console.error(notice)
|
|
167
|
+
|
|
146
168
|
if (findings.length === 0) {
|
|
147
169
|
console.log('✓ Theme guard: no off-palette colors.')
|
|
148
170
|
return 0
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
// The `basalt` config shape and helpers, shared by the dispatcher and `--audit-allows`. A leaf: it
|
|
2
|
+
// imports nothing from `./index`, so audit-allows → index → check-theme → audit-allows is no cycle.
|
|
3
|
+
import type { GuardKind, GuardSeverity } from '../guard'
|
|
4
|
+
|
|
5
|
+
/** Shape of the optional `"basalt"` key in a consumer's package.json. */
|
|
6
|
+
export type BasaltConfig = {
|
|
7
|
+
/** Source roots to scan. Default: `['src']`. Set explicitly for a monorepo layout. */
|
|
8
|
+
roots?: string[]
|
|
9
|
+
/** Files exempt from the scan (they ARE the palette source). Default: argo's exempt set. */
|
|
10
|
+
exempt?: string[]
|
|
11
|
+
/**
|
|
12
|
+
* Declares this package a tokens-only consumer: it uses the `--vx-*` layer and no Mantine. Turns
|
|
13
|
+
* off every guard kind whose remedy is a Mantine component, a Mantine prop or the React theme
|
|
14
|
+
* factory, leaving the color and typography kinds live. Default: `'mantine'`.
|
|
15
|
+
*
|
|
16
|
+
* Deliberately not inferred from a missing `@mantine/core`: silencing 17 rules (the live count is
|
|
17
|
+
* `TOKENS_ONLY_DISABLED_KINDS.size`, which the check-theme banner prints) is a decision, and
|
|
18
|
+
* a repo that keeps Mantine in a workspace package would otherwise switch off half its own guard
|
|
19
|
+
* without saying so. `basalt-ui doctor` detects the shape and tells you to set this.
|
|
20
|
+
*/
|
|
21
|
+
profile?: 'mantine' | 'tokens-only'
|
|
22
|
+
/**
|
|
23
|
+
* Extra files to scan, named individually and relative to this package — for a design surface
|
|
24
|
+
* that lives outside every root and outside the `index.html` / `public/` conventions the walker
|
|
25
|
+
* already derives. Also the ONLY way a `.json` is ever scanned: the guard understands JSON as
|
|
26
|
+
* markup, but a consumer repo's JSON is overwhelmingly config, fixtures and lockfiles, so
|
|
27
|
+
* blanket-scanning it is a false-positive generator rather than a guard.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* { include: ['app/manifest.json', 'emails/template.html'] }
|
|
31
|
+
*/
|
|
32
|
+
include?: string[]
|
|
33
|
+
/** Named spacing-scale steps (px) flagged when used as a raw spacing prop. Default: 10/12/16/20/32. */
|
|
34
|
+
spacingSteps?: number[]
|
|
35
|
+
/** Per-kind severity override — `'warn'` reports without failing, `'error'` fails the build.
|
|
36
|
+
* See `GuardSeverity` in ../guard/types for the grace-minor doctrine behind it. */
|
|
37
|
+
severity?: Partial<Record<GuardKind, GuardSeverity>>
|
|
38
|
+
/** Off-identity Mantine accent families forbidden as chrome accents. Default: argo's set. */
|
|
39
|
+
forbiddenAccents?: string[]
|
|
40
|
+
/** Earned accent hue recorded in DESIGN.md `{{ACCENT_HUE}}`. Default: `blue`. */
|
|
41
|
+
accentHue?: string
|
|
42
|
+
/**
|
|
43
|
+
* Flag any numeric radius prop literal (radius={6}) — use the radius token scale instead.
|
|
44
|
+
* Default: `true` (ON). Set `false` to disable the `raw-radius` check (e.g. a repo that DEFINES
|
|
45
|
+
* the radius primitives).
|
|
46
|
+
*/
|
|
47
|
+
rawRadius?: boolean
|
|
48
|
+
/**
|
|
49
|
+
* Flag ad-hoc inline surface styling (`border`/`borderRadius`/`boxShadow` literals in a `style={{}}`)
|
|
50
|
+
* that bypasses the radius-token + `VX.surface.*` + `VX.shadowCard` system. Default: `true` (ON).
|
|
51
|
+
* Set `false` to disable the `raw-surface` check.
|
|
52
|
+
*/
|
|
53
|
+
rawSurface?: boolean
|
|
54
|
+
/**
|
|
55
|
+
* Flag `withBorder` on a `Card` / `Paper`. Card depth is `--vx-shadow-card` — a whisper shadow
|
|
56
|
+
* with the 1px ring baked into the SAME value — so `withBorder` draws a SECOND, real border over
|
|
57
|
+
* that ring and the card reads heavy/boxed (docs/DESIGN-SPEC.md doctrine inversion #1).
|
|
58
|
+
* `withBorder={false}` and `<Card.Section withBorder>` (a section divider) both pass.
|
|
59
|
+
* Default: `true` (ON). Set `false` to disable the `card-with-border` check.
|
|
60
|
+
*/
|
|
61
|
+
cardWithBorder?: boolean
|
|
62
|
+
/**
|
|
63
|
+
* Flag references to a raw Mantine ramp step used for surface color
|
|
64
|
+
* (`var(--mantine-color-gray-N)` / `var(--mantine-color-dark-N)`) — these bypass the basalt
|
|
65
|
+
* surface tokens. Default: `true` (ON). Set `false` to disable the `off-system-surface-var` check.
|
|
66
|
+
*/
|
|
67
|
+
offSystemSurfaceVar?: boolean
|
|
68
|
+
/**
|
|
69
|
+
* Flag a SHADE-PINNED Mantine color — `c="yellow.7"`, `bg="blue.4"`,
|
|
70
|
+
* `var(--mantine-color-red-6)`. A pinned step is one fixed swatch in BOTH color schemes, so it
|
|
71
|
+
* cannot stay legible in either; route a verdict color through `VX.status.*` / `--vx-status-*`,
|
|
72
|
+
* or drop the index (`c="red"`) and let the theme resolve the shade per scheme. `gray-*`/`dark-*`
|
|
73
|
+
* in `var()` form belong to `off-system-surface-var` instead. Default: `true` (ON). Set `false`
|
|
74
|
+
* to disable the `mantine-shade-index` check.
|
|
75
|
+
*/
|
|
76
|
+
mantineShadeIndex?: boolean
|
|
77
|
+
/**
|
|
78
|
+
* Flag raw lowercase JSX layout/surface elements (`div`/`span`/`section`/…) carrying an inline
|
|
79
|
+
* `style={{}}` with a layout/surface property — steer to a Mantine layout primitive
|
|
80
|
+
* (`Box`/`Flex`/`Grid`/`Stack`/`Group`/`SimpleGrid`/`Paper`). Default: `true` (ON). Set `false`
|
|
81
|
+
* to disable the `raw-html-layout` check.
|
|
82
|
+
*/
|
|
83
|
+
rawHtmlLayout?: boolean
|
|
84
|
+
/**
|
|
85
|
+
* Flag spacing/sizing literals inside an inline `style={{}}` (`padding`/`margin`/`gap`/…) — the
|
|
86
|
+
* `raw-spacing` check only catches the Mantine prop syntax. Default: `true` (ON). Set `false` to
|
|
87
|
+
* disable the `inline-spacing` check.
|
|
88
|
+
*/
|
|
89
|
+
inlineSpacing?: boolean
|
|
90
|
+
/**
|
|
91
|
+
* Flag `display: 'flex' | 'grid' | 'inline-flex' | 'inline-grid'` in an inline `style={{}}` —
|
|
92
|
+
* steer to `<Flex>`/`<Grid>`/`<Group>`. Default: `true` (ON). Set `false` to disable the
|
|
93
|
+
* `inline-display` check.
|
|
94
|
+
*/
|
|
95
|
+
inlineDisplay?: boolean
|
|
96
|
+
/**
|
|
97
|
+
* Flag raw `<AxisLeft>` / `<AxisBottom>` / `<AxisRight>` visx JSX inside chart files (a path
|
|
98
|
+
* containing `/charts/`) — these bypass the `AxisLeftNumeric` / `AxisBottomDate` /
|
|
99
|
+
* `AxisRightNumeric` primitives that carry the theme tokens + smart ticks. The legitimate wrapper
|
|
100
|
+
* (`Axes.tsx`, which IS the primitive) is exempt. Default: `true` (ON). Set `false` to disable the
|
|
101
|
+
* `raw-visx-axis` check.
|
|
102
|
+
*/
|
|
103
|
+
rawVisxAxis?: boolean
|
|
104
|
+
/**
|
|
105
|
+
* Flag a hardcoded duration/spring/ease literal inline in a `transition={{...}}` prop — route it
|
|
106
|
+
* through `MOTION_DURATION` / `MOTION_SPRING` / `MOTION_EASE_STANDARD` instead. Default: `true`
|
|
107
|
+
* (ON). Set `false` to disable the `raw-motion-value` check.
|
|
108
|
+
*/
|
|
109
|
+
rawMotionValue?: boolean
|
|
110
|
+
/**
|
|
111
|
+
* Flag a chart entry-point JSX tag (`MultiLine`/`Bars`/`Donut`/`DualPanel`/`Heatmap`/`ZonedLine`/
|
|
112
|
+
* `StackedArea`/`LineSparkline`/`BarSparkline`) missing an `ariaLabel` prop. Default: `true`
|
|
113
|
+
* (ON). Set `false` to disable the `chart-missing-aria-label` check.
|
|
114
|
+
*/
|
|
115
|
+
chartMissingAriaLabel?: boolean
|
|
116
|
+
/**
|
|
117
|
+
* Flag a raw lowercase `<input>`/`<select>`/`<textarea>` JSX element — it bypasses the entire
|
|
118
|
+
* theme, not just the iOS font-size floor. Use the Mantine equivalents (`TextInput`,
|
|
119
|
+
* `NumberInput`, `Select`, `Textarea`, …) instead. Default: `true` (ON). Set `false` to disable
|
|
120
|
+
* the `raw-form-control` check.
|
|
121
|
+
*/
|
|
122
|
+
rawFormControl?: boolean
|
|
123
|
+
/**
|
|
124
|
+
* Flag a `fontSize`/`font-size` literal below 16 inside a `style={{…}}` on a raw form control,
|
|
125
|
+
* or a Mantine `styles={{ input: {…} }}` per-part style — the `styles.css` iOS floor is
|
|
126
|
+
* `!important`, so the override is dead code. Default: `true` (ON). Set `false` to disable the
|
|
127
|
+
* `sub-16-input-font` check.
|
|
128
|
+
*/
|
|
129
|
+
sub16InputFont?: boolean
|
|
130
|
+
/**
|
|
131
|
+
* Path of the consumer's guard-exempt series file, for DESIGN.md `{{SERIES_MODULE_PATH}}`.
|
|
132
|
+
* Default: `<first basalt.root>/lib/series.ts` — see `resolveSeriesModulePath`.
|
|
133
|
+
*/
|
|
134
|
+
seriesModulePath?: string
|
|
135
|
+
/**
|
|
136
|
+
* Per-rule, per-path exemptions — complements whole-file `exempt` (which skips ALL rules for a
|
|
137
|
+
* file) and the hardcoded per-kind `appliesTo` scoping (e.g. `raw-visx-axis` → chart files
|
|
138
|
+
* only). Each value is a list of patterns matched against a finding's relative path. A pattern
|
|
139
|
+
* may be a whole path segment (`'agent'` matches `src/agent/x.tsx`, not `src/agenting.ts`), a
|
|
140
|
+
* relative path (`'public/site.webmanifest'`), a directory prefix, or a glob — `*` stops at `/`,
|
|
141
|
+
* `**` does not, and a slash-free glob also matches the basename, so `'*.module.css'` works.
|
|
142
|
+
* A trailing `/` is stripped. An entry that suppresses nothing is reported, and
|
|
143
|
+
* `check-theme --audit-allows` exits 1 on it. Default: `{}` (no exemptions).
|
|
144
|
+
*
|
|
145
|
+
* Two forms per kind. The bare array is paths only. The object form adds the REASON, which is
|
|
146
|
+
* the half a `theme-allow` carries and this key could not: JSON has no comments, so a
|
|
147
|
+
* `.webmanifest` / `.json` finding's only escape is this key, and it was un-reviewable by
|
|
148
|
+
* construction — the rationale ended up in a CLAUDE.md paragraph nobody reads next to the diff.
|
|
149
|
+
* `check-theme --audit-allows` prints the reason (or names its absence) beside what the entry
|
|
150
|
+
* still suppresses. The array form stays supported and unchanged; nothing here is required.
|
|
151
|
+
*
|
|
152
|
+
* @example
|
|
153
|
+
* { exemptRules: { 'inline-display': ['agent'] } } // inline-display never fires under src/agent/**
|
|
154
|
+
* @example
|
|
155
|
+
* { exemptRules: { 'raw-hex': { paths: ['site.webmanifest'],
|
|
156
|
+
* reason: 'a PWA manifest theme_color MUST be a literal hex — JSON cannot reference a CSS var' } } }
|
|
157
|
+
*/
|
|
158
|
+
exemptRules?: Partial<Record<GuardKind, ExemptRuleEntry>>
|
|
159
|
+
/**
|
|
160
|
+
* Declares an intentional `ai` package major-version skew across workspace packages, exempting
|
|
161
|
+
* `doctor`'s `ai-major-parity` hard check. The value IS the reason — a bare `true` is rejected,
|
|
162
|
+
* because the whole point of this key is that the pairing is written down, not just switched off.
|
|
163
|
+
* A written declaration is the pin `basalt/ai-sdk-major`'s own comment notes is otherwise missing.
|
|
164
|
+
*
|
|
165
|
+
* Semantics: absent (or present but not a non-empty string) → the skew still hard-fails exactly as
|
|
166
|
+
* without this key. A non-empty string with a skew present → `doctor` passes, echoing both the
|
|
167
|
+
* skew and this reason. A non-empty string with NO skew present → `doctor` warns that the
|
|
168
|
+
* exemption is stale and can be deleted (an exemption nobody re-checks is how a real, later skew
|
|
169
|
+
* slips through unnoticed).
|
|
170
|
+
*
|
|
171
|
+
* @example
|
|
172
|
+
* // apps/api streams on ai@5; apps/dashboard parses on ai@7; a producer-side TransformStream
|
|
173
|
+
* // neutralizes the one enum value that differs between the two majors.
|
|
174
|
+
* { aiMajorSkewReason: 'apps/api pinned to ai@5, apps/dashboard on ai@7 — the skew is neutralized '
|
|
175
|
+
* + 'by a producer-side TransformStream in apps/api/src/stream-transform.ts' }
|
|
176
|
+
*/
|
|
177
|
+
aiMajorSkewReason?: string
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* One `basalt.exemptRules` value: the historical bare path list, or the same list with the reason
|
|
182
|
+
* it exists. See `BasaltConfig.exemptRules`.
|
|
183
|
+
*/
|
|
184
|
+
export type ExemptRuleEntry = string[] | { paths: string[]; reason: string }
|
|
185
|
+
|
|
186
|
+
/** The paths half of an exemption entry, whichever form it was written in. */
|
|
187
|
+
export function exemptRulePaths(entry: ExemptRuleEntry | undefined): string[] {
|
|
188
|
+
if (entry === undefined) return []
|
|
189
|
+
return Array.isArray(entry) ? entry : entry.paths
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** The recorded reason for an exemption entry, or null for the bare-array form. */
|
|
193
|
+
export function exemptRuleReason(entry: ExemptRuleEntry | undefined): string | null {
|
|
194
|
+
if (entry === undefined || Array.isArray(entry)) return null
|
|
195
|
+
return entry.reason.trim().length > 0 ? entry.reason.trim() : null
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export const DEFAULT_ROOT = 'src'
|
|
199
|
+
export const DEFAULT_ROOTS = [DEFAULT_ROOT]
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* The configured source roots, or the built-in default. The ONE resolution every roots-derived seed
|
|
203
|
+
* reads — an empty `roots: []` falls back rather than resolving to nothing, because a bare `??`
|
|
204
|
+
* would let `[]` through and render an empty oxfmt glob into the seeded CI, reproducing the exact
|
|
205
|
+
* "matches zero files" break this derivation exists to prevent.
|
|
206
|
+
*/
|
|
207
|
+
export function resolveRoots(cfg: BasaltConfig): string[] {
|
|
208
|
+
return cfg.roots?.length ? cfg.roots : DEFAULT_ROOTS
|
|
209
|
+
}
|
package/src/cli/doctor.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { existsSync } from 'node:fs'
|
|
7
7
|
import { relative, resolve } from 'node:path'
|
|
8
8
|
|
|
9
|
-
import type { BasaltConfig } from './
|
|
9
|
+
import type { BasaltConfig } from './config'
|
|
10
10
|
import {
|
|
11
11
|
MANIFEST_PATH,
|
|
12
12
|
basaltBinCommand,
|
|
@@ -16,15 +16,16 @@ import {
|
|
|
16
16
|
findBasaltInstall,
|
|
17
17
|
findRepoRoot,
|
|
18
18
|
inspectLefthookGate,
|
|
19
|
+
OXLINT_FILES,
|
|
19
20
|
parseJsonc,
|
|
20
21
|
readBasaltConfig,
|
|
21
22
|
readIfExists,
|
|
22
23
|
readManifest,
|
|
23
24
|
resolveProjectDir,
|
|
24
|
-
resolveRoots,
|
|
25
25
|
scannableFiles,
|
|
26
26
|
shippedAssetPath,
|
|
27
27
|
} from './index'
|
|
28
|
+
import { resolveRoots } from './config'
|
|
28
29
|
import { findManifestAbove, parentInstallAdvice } from './sync'
|
|
29
30
|
|
|
30
31
|
export type DoctorResult = {
|
|
@@ -140,10 +141,7 @@ function resolveAiMajorSkewReason(cfg: BasaltConfig): {
|
|
|
140
141
|
* Returns the exit code: 0 = all good, 1 = one or more hard failures.
|
|
141
142
|
*/
|
|
142
143
|
export function doctor(invocationCwd: string = process.cwd(), flags: string[] = []): number {
|
|
143
|
-
if (conflictingProfileFlags(flags))
|
|
144
|
-
console.error('basalt-ui doctor: --tokens-only and --framework are alternatives — pass one.')
|
|
145
|
-
return 1
|
|
146
|
-
}
|
|
144
|
+
if (conflictingProfileFlags('doctor', flags)) return 1
|
|
147
145
|
const project = resolveProjectDir(invocationCwd)
|
|
148
146
|
const cwd = project.dir
|
|
149
147
|
const cfg = readBasaltConfig(cwd)
|
|
@@ -256,7 +254,7 @@ export function doctor(invocationCwd: string = process.cwd(), flags: string[] =
|
|
|
256
254
|
)
|
|
257
255
|
} else {
|
|
258
256
|
const entry = basaltPresetEntry(parsed['extends'])
|
|
259
|
-
const correct = shippedAssetPath(install, cwd,
|
|
257
|
+
const correct = shippedAssetPath(install, cwd, OXLINT_FILES.preset)
|
|
260
258
|
if (entry === null) {
|
|
261
259
|
fail(
|
|
262
260
|
'.oxlintrc.json does NOT extend the shipped preset — every basalt/* design rule is off ' +
|