basalt-ui 1.41.0 → 2.0.1
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 +46 -43
- package/CHANGELOG.md +542 -518
- package/MIGRATING.md +964 -37
- package/README.md +171 -366
- package/agent/rules/basalt-batteries.md +19 -74
- package/agent/rules/basalt-charts.md +21 -95
- package/agent/rules/basalt-controls.md +23 -131
- package/agent/rules/basalt-mantine.md +26 -99
- package/agent/rules/basalt-state.md +21 -103
- package/agent/rules/basalt-tokens.md +33 -114
- package/agent/skills/basalt-app/SKILL.md +229 -72
- package/agent/skills/basalt-charts/SKILL.md +23 -57
- package/agent/skills/basalt-design/SKILL.md +12 -70
- package/agent/templates/AGENTS-block.md.tpl +16 -26
- package/agent/templates/DESIGN.md.tpl +1 -1
- package/agent/templates/__root.tpl +9 -16
- package/agent/templates/main.tpl +21 -0
- package/agent/templates/nav.tpl +18 -0
- package/configs/oxlint-basalt.json +4 -10
- package/configs/oxlint-plugin.js +465 -844
- package/configs/oxlint-plugin.test.ts +661 -709
- package/configs/oxlint.json +10 -2
- package/dist/agent/ai-sdk-transport.d.ts +19 -7
- package/dist/agent/ai-sdk-transport.d.ts.map +1 -1
- package/dist/agent/ai-sdk-transport.js +10 -7
- package/dist/{agent-chat → agent/chat}/composer.d.ts +16 -16
- package/dist/agent/chat/composer.d.ts.map +1 -0
- package/dist/{agent-chat → agent/chat}/composer.js +10 -9
- package/dist/{agent-chat → agent/chat}/message-affordances.d.ts +7 -6
- package/dist/agent/chat/message-affordances.d.ts.map +1 -0
- package/dist/agent/chat/relative-time.d.ts.map +1 -0
- package/dist/{agent-chat → agent/chat}/thread-feed-row.d.ts +18 -16
- package/dist/agent/chat/thread-feed-row.d.ts.map +1 -0
- package/dist/{agent-chat → agent/chat}/thread-feed-row.js +12 -11
- package/dist/{agent-chat → agent/chat}/thread-feed.d.ts +8 -8
- package/dist/agent/chat/thread-feed.d.ts.map +1 -0
- package/dist/{agent-chat → agent/chat}/thread-feed.js +12 -10
- package/dist/{agent-chat → agent/chat}/thread-message.d.ts +12 -9
- package/dist/agent/chat/thread-message.d.ts.map +1 -0
- package/dist/{agent-chat → agent/chat}/thread-message.js +15 -13
- package/dist/{agent-chat → agent/chat}/thread-outcome-card.d.ts +2 -2
- package/dist/agent/chat/thread-outcome-card.d.ts.map +1 -0
- package/dist/{agent-chat → agent/chat}/thread-outcome-card.js +3 -3
- package/dist/{agent-chat → agent/chat}/tool-chip.d.ts +5 -5
- package/dist/agent/chat/tool-chip.d.ts.map +1 -0
- package/dist/{agent-chat → agent/chat}/tool-chip.js +4 -4
- package/dist/agent/chat/use-keyboard-inset.d.ts.map +1 -0
- package/dist/{agent-chat → agent/chat}/use-keyboard-inset.js +1 -1
- package/dist/{agent-chat → agent/chat}/virtualize.d.ts +7 -7
- package/dist/agent/chat/virtualize.d.ts.map +1 -0
- package/dist/agent/error.d.ts +31 -0
- package/dist/agent/error.d.ts.map +1 -0
- package/dist/agent/error.js +18 -0
- package/dist/agent/history.d.ts +2 -36
- package/dist/agent/history.d.ts.map +1 -1
- package/dist/agent/history.js +0 -32
- package/dist/agent/id.d.ts +4 -5
- package/dist/agent/id.d.ts.map +1 -1
- package/dist/agent/index.d.ts +19 -52
- package/dist/agent/index.d.ts.map +1 -1
- package/dist/agent/index.js +10 -32
- package/dist/agent/outcome.d.ts +3 -14
- package/dist/agent/outcome.d.ts.map +1 -1
- package/dist/agent/outcome.js +0 -30
- package/dist/agent/part-list.d.ts +3 -3
- package/dist/agent/part-list.d.ts.map +1 -1
- package/dist/agent/parts.d.ts +23 -24
- package/dist/agent/parts.d.ts.map +1 -1
- package/dist/agent/run-committer.d.ts +23 -0
- package/dist/agent/run-committer.d.ts.map +1 -0
- package/dist/agent/run-committer.js +41 -0
- package/dist/agent/run-records.d.ts +77 -0
- package/dist/agent/run-records.d.ts.map +1 -0
- package/dist/agent/run-records.js +69 -0
- package/dist/agent/thread.d.ts +23 -47
- package/dist/agent/thread.d.ts.map +1 -1
- package/dist/agent/thread.js +3 -134
- package/dist/agent/transport.d.ts +6 -69
- package/dist/agent/transport.d.ts.map +1 -1
- package/dist/agent/transport.js +0 -18
- package/dist/agent/use-agent-thread-runs.d.ts +63 -26
- package/dist/agent/use-agent-thread-runs.d.ts.map +1 -1
- package/dist/agent/use-agent-thread-runs.js +202 -66
- package/dist/charts/BandStrip.d.ts +46 -0
- package/dist/charts/BandStrip.d.ts.map +1 -0
- package/dist/charts/BandStrip.js +151 -0
- package/dist/charts/Bars.d.ts +19 -0
- package/dist/charts/Bars.d.ts.map +1 -0
- package/dist/charts/Bars.js +77 -0
- package/dist/charts/CartesianChart.d.ts +57 -0
- package/dist/charts/CartesianChart.d.ts.map +1 -0
- package/dist/charts/CartesianChart.js +121 -0
- package/dist/charts/ChartCard.d.ts +34 -0
- package/dist/charts/ChartCard.d.ts.map +1 -0
- package/dist/charts/ChartCard.js +115 -0
- package/dist/charts/Donut.d.ts +25 -0
- package/dist/charts/Donut.d.ts.map +1 -0
- package/dist/charts/Donut.js +191 -0
- package/dist/charts/DualPanel.d.ts +47 -0
- package/dist/charts/DualPanel.d.ts.map +1 -0
- package/dist/charts/DualPanel.js +214 -0
- package/dist/charts/Heatmap.d.ts +41 -0
- package/dist/charts/Heatmap.d.ts.map +1 -0
- package/dist/charts/Heatmap.js +216 -0
- package/dist/charts/Lines.d.ts +11 -0
- package/dist/charts/Lines.d.ts.map +1 -0
- package/dist/charts/Lines.js +114 -0
- package/dist/charts/MirroredBars.d.ts +44 -0
- package/dist/charts/MirroredBars.d.ts.map +1 -0
- package/dist/charts/MirroredBars.js +190 -0
- package/dist/charts/Sparkline.d.ts +22 -0
- package/dist/charts/Sparkline.d.ts.map +1 -0
- package/dist/charts/Sparkline.js +129 -0
- package/dist/charts/StackedArea.d.ts +15 -0
- package/dist/charts/StackedArea.d.ts.map +1 -0
- package/dist/charts/StackedArea.js +46 -0
- package/dist/charts/box.d.ts +38 -0
- package/dist/charts/box.d.ts.map +1 -0
- package/dist/charts/box.js +72 -0
- package/dist/charts/card-context.d.ts +3 -0
- package/dist/charts/card-context.d.ts.map +1 -0
- package/dist/charts/card-context.js +7 -0
- package/dist/charts/chart-box.module.css +64 -0
- package/{src/content/article-card.module.css.d.ts → dist/charts/chart-box.module.css.d.ts} +5 -9
- package/dist/charts/chart-card.module.css +25 -0
- package/dist/{dashboard/widget-grid.module.css.d.ts → charts/chart-card.module.css.d.ts} +1 -3
- package/dist/charts/chart-legend.module.css +38 -0
- package/{src/dashboard/widget-grid.module.css.d.ts → dist/charts/chart-legend.module.css.d.ts} +3 -3
- package/dist/charts/compile.d.ts +62 -0
- package/dist/charts/compile.d.ts.map +1 -0
- package/dist/charts/compile.js +386 -0
- package/dist/charts/cursor.d.ts +50 -0
- package/dist/charts/cursor.d.ts.map +1 -0
- package/dist/charts/cursor.js +157 -0
- package/dist/charts/curves.d.ts +12 -0
- package/dist/charts/curves.d.ts.map +1 -0
- package/dist/charts/curves.js +86 -0
- package/dist/charts/donut.module.css +54 -0
- package/dist/{content/article-card.module.css.d.ts → charts/donut.module.css.d.ts} +2 -9
- package/dist/charts/host.d.ts +17 -0
- package/dist/charts/host.d.ts.map +1 -0
- package/dist/charts/host.js +35 -0
- package/dist/charts/index.d.ts +18 -53
- package/dist/charts/index.d.ts.map +1 -1
- package/dist/charts/index.js +17 -189
- package/dist/charts/legend.d.ts +23 -0
- package/dist/charts/legend.d.ts.map +1 -0
- package/dist/charts/legend.js +160 -0
- package/dist/charts/marks.d.ts +25 -0
- package/dist/charts/marks.d.ts.map +1 -0
- package/dist/charts/marks.js +128 -0
- package/dist/charts/panel.d.ts +56 -0
- package/dist/charts/panel.d.ts.map +1 -0
- package/dist/charts/panel.js +123 -0
- package/dist/charts/scales.d.ts +44 -0
- package/dist/charts/scales.d.ts.map +1 -0
- package/dist/charts/scales.js +163 -0
- package/dist/charts/sizing.d.ts +12 -0
- package/dist/charts/sizing.d.ts.map +1 -0
- package/dist/charts/sizing.js +33 -0
- package/dist/charts/state.d.ts +17 -0
- package/dist/charts/state.d.ts.map +1 -0
- package/dist/charts/state.js +53 -0
- package/dist/charts/theme.d.ts +38 -19
- package/dist/charts/theme.d.ts.map +1 -1
- package/dist/charts/theme.js +48 -26
- package/dist/charts/types.d.ts +227 -0
- package/dist/charts/types.d.ts.map +1 -0
- package/dist/charts/types.js +4 -0
- package/dist/cli/audit-allows.d.ts +4 -17
- package/dist/cli/audit-allows.d.ts.map +1 -1
- package/dist/cli/config.d.ts +18 -43
- package/dist/cli/config.d.ts.map +1 -1
- package/dist/cli/config.js +6 -0
- package/dist/cli/doctor-upgrade.d.ts +43 -0
- package/dist/cli/doctor-upgrade.d.ts.map +1 -0
- package/dist/cli/doctor-upgrade.js +314 -0
- package/dist/cli/doctor.d.ts +14 -29
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/doctor.js +58 -9
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +2 -4
- package/dist/cli/index.d.ts +48 -172
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +39 -132
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +43 -37
- package/dist/cli/sync.d.ts +5 -21
- package/dist/cli/sync.d.ts.map +1 -1
- package/dist/cli/sync.js +4 -23
- package/dist/cli/tokens-css.d.ts +7 -28
- package/dist/cli/tokens-css.d.ts.map +1 -1
- package/dist/commands/ShortcutsHelp.d.ts.map +1 -1
- package/dist/commands/ShortcutsHelp.js +4 -5
- package/dist/commands/define-commands.d.ts +15 -31
- package/dist/commands/define-commands.d.ts.map +1 -1
- package/dist/commands/define-commands.js +1 -11
- package/dist/commands/define-overlays.d.ts +5 -19
- package/dist/commands/define-overlays.d.ts.map +1 -1
- package/dist/commands/define-overlays.js +3 -11
- package/dist/commands/index.d.ts +10 -40
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/index.js +9 -46
- package/dist/{notifications → commands/notifications}/bell.d.ts +1 -1
- package/dist/commands/notifications/bell.d.ts.map +1 -0
- package/dist/{notifications → commands/notifications}/center.d.ts +2 -2
- package/dist/commands/notifications/center.d.ts.map +1 -0
- package/dist/{notifications → commands/notifications}/center.js +2 -2
- package/dist/{notifications → commands/notifications}/define-notifications.d.ts +3 -3
- package/dist/commands/notifications/define-notifications.d.ts.map +1 -0
- package/dist/commands/notifications/notify.d.ts +44 -0
- package/dist/commands/notifications/notify.d.ts.map +1 -0
- package/dist/commands/notifications/notify.js +79 -0
- package/dist/{notifications → commands/notifications}/store.d.ts +2 -0
- package/dist/commands/notifications/store.d.ts.map +1 -0
- package/dist/{notifications → commands/notifications}/store.js +4 -0
- package/dist/commands/projectors.d.ts +1 -131
- package/dist/commands/projectors.d.ts.map +1 -1
- package/dist/commands/projectors.js +2 -73
- package/dist/commands/shell-bridge.d.ts +3 -3
- package/dist/commands/shortcut-format.d.ts +5 -4
- package/dist/commands/shortcut-format.d.ts.map +1 -1
- package/dist/common/errors.d.ts +3 -3
- package/dist/common/use-measured-widths.d.ts.map +1 -1
- package/dist/common/use-measured-widths.js +5 -3
- package/dist/common/validate.d.ts +2 -2
- package/dist/content/article-layout.d.ts.map +1 -1
- package/dist/content/article-layout.js +3 -3
- package/dist/content/article-model.d.ts +1 -46
- package/dist/content/article-model.d.ts.map +1 -1
- package/dist/content/article-model.js +1 -29
- package/dist/content/index.d.ts +11 -41
- package/dist/content/index.d.ts.map +1 -1
- package/dist/content/index.js +3 -35
- package/dist/content/markdown.d.ts +1 -1
- package/dist/content/markdown.d.ts.map +1 -1
- package/dist/content/markdown.js +1 -1
- package/dist/content/mermaid.d.ts.map +1 -1
- package/dist/content/mermaid.js +7 -7
- package/dist/content/slug.d.ts +1 -8
- package/dist/content/slug.d.ts.map +1 -1
- package/dist/content/slug.js +1 -8
- package/dist/content/url-hardening.d.ts +2 -2
- package/dist/content/url-hardening.d.ts.map +1 -1
- package/dist/content/url-hardening.js +2 -0
- package/dist/controls/actions.d.ts +16 -1
- package/dist/controls/actions.d.ts.map +1 -1
- package/dist/controls/enum-filter.d.ts +1 -1
- package/dist/controls/enum-filter.d.ts.map +1 -1
- package/dist/controls/filter-context.d.ts.map +1 -1
- package/dist/controls/filter-context.js +3 -1
- package/dist/controls/filter-set.js +4 -6
- package/dist/controls/form-layout.d.ts +1 -1
- package/dist/controls/form-layout.d.ts.map +1 -1
- package/dist/controls/form-layout.js +1 -1
- package/dist/controls/glyphs.d.ts +0 -2
- package/dist/controls/glyphs.d.ts.map +1 -1
- package/dist/controls/glyphs.js +0 -7
- package/dist/controls/index.d.ts +12 -17
- package/dist/controls/index.d.ts.map +1 -1
- package/dist/controls/index.js +5 -39
- package/dist/controls/multi-select-filter.d.ts +4 -3
- package/dist/controls/multi-select-filter.d.ts.map +1 -1
- package/dist/controls/number-filter.d.ts +4 -3
- package/dist/controls/number-filter.d.ts.map +1 -1
- package/dist/controls/number-filter.js +3 -1
- package/dist/controls/range-filter.d.ts +4 -3
- package/dist/controls/range-filter.d.ts.map +1 -1
- package/dist/controls/search-filter.d.ts +4 -3
- package/dist/controls/search-filter.d.ts.map +1 -1
- package/dist/controls/search-filter.js +19 -14
- package/dist/controls/select-filter.d.ts +4 -3
- package/dist/controls/select-filter.d.ts.map +1 -1
- package/dist/controls/sync-button.d.ts.map +1 -1
- package/dist/controls/sync-button.js +12 -15
- package/dist/controls/toggle-filter.d.ts +4 -3
- package/dist/controls/toggle-filter.d.ts.map +1 -1
- package/dist/controls/uncontrolled.d.ts +12 -0
- package/dist/controls/uncontrolled.d.ts.map +1 -0
- package/dist/controls/view-tabs.d.ts +2 -2
- package/dist/controls/view-tabs.d.ts.map +1 -1
- package/dist/controls-dates/index.d.ts +0 -1
- package/dist/controls-dates/index.d.ts.map +1 -1
- package/dist/dashboard/index.d.ts +7 -9
- package/dist/dashboard/index.d.ts.map +1 -1
- package/dist/dashboard/index.js +3 -23
- package/dist/dashboard/section.d.ts +4 -6
- package/dist/dashboard/section.d.ts.map +1 -1
- package/dist/dashboard/section.js +4 -10
- package/dist/dashboard/stat-card.d.ts +4 -4
- package/dist/dashboard/stat-card.d.ts.map +1 -1
- package/dist/dashboard/stat-card.js +3 -3
- package/dist/dashboard/widget-header.module.css +9 -7
- package/dist/dev-dock.d.ts.map +1 -1
- package/dist/dev-dock.js +6 -1
- package/dist/format/date.d.ts +0 -34
- package/dist/format/date.d.ts.map +1 -1
- package/dist/format/date.js +0 -54
- package/dist/format/index.d.ts +7 -22
- package/dist/format/index.d.ts.map +1 -1
- package/dist/format/index.js +2 -43
- package/dist/format/number.d.ts +3 -14
- package/dist/format/number.d.ts.map +1 -1
- package/dist/format/number.js +0 -13
- package/dist/format/shared.d.ts +2 -3
- package/dist/format/shared.d.ts.map +1 -1
- package/dist/forms/index.d.ts +0 -11
- package/dist/forms/index.d.ts.map +1 -1
- package/dist/forms/index.js +1 -14
- package/dist/guard/index.d.ts +6 -21
- package/dist/guard/index.d.ts.map +1 -1
- package/dist/guard/index.js +27 -115
- package/dist/guard/types.d.ts +7 -18
- package/dist/guard/types.d.ts.map +1 -1
- package/dist/index.d.ts +16 -48
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +21 -117
- package/dist/provider/create-router.d.ts +44 -0
- package/dist/provider/create-router.d.ts.map +1 -0
- package/dist/provider/create-router.js +62 -0
- package/dist/provider/hotkeys.d.ts +8 -0
- package/dist/provider/hotkeys.d.ts.map +1 -0
- package/dist/provider/hotkeys.js +50 -0
- package/dist/provider/index.d.ts +33 -110
- package/dist/provider/index.d.ts.map +1 -1
- package/dist/provider/index.js +182 -82
- package/dist/provider/lab-theme.d.ts.map +1 -1
- package/dist/provider/route-transition.d.ts +76 -0
- package/dist/provider/route-transition.d.ts.map +1 -0
- package/dist/provider/route-transition.js +98 -0
- package/dist/provider/spotlight-actions.d.ts +10 -0
- package/dist/provider/spotlight-actions.d.ts.map +1 -0
- package/dist/provider/spotlight-actions.js +55 -0
- package/dist/provider/test-router.d.ts +6 -0
- package/dist/provider/test-router.d.ts.map +1 -0
- package/dist/provider/test-router.js +18 -0
- package/dist/query-client.d.ts +4 -20
- package/dist/query-client.d.ts.map +1 -1
- package/dist/query-client.js +2 -2
- package/dist/router-tanstack/index.d.ts +18 -64
- package/dist/router-tanstack/index.d.ts.map +1 -1
- package/dist/router-tanstack/index.js +2 -16
- package/dist/router-tanstack/nav.d.ts +12 -1
- package/dist/router-tanstack/nav.d.ts.map +1 -1
- package/dist/router-tanstack/nav.js +8 -1
- package/dist/router-tanstack/search-store.d.ts +1 -1
- package/dist/router-tanstack/search-store.d.ts.map +1 -1
- package/dist/router-tanstack/use-nav.d.ts +8 -9
- package/dist/router-tanstack/use-nav.d.ts.map +1 -1
- package/dist/router-tanstack/use-nav.js +20 -1
- package/dist/shell/app-mobile-nav.d.ts.map +1 -1
- package/dist/shell/app-sidebar.js +1 -1
- package/dist/shell/index.d.ts +31 -10
- package/dist/shell/index.d.ts.map +1 -1
- package/dist/shell/index.js +14 -3
- package/dist/shell/page-aside.d.ts +2 -2
- package/dist/shell/page-aside.d.ts.map +1 -1
- package/dist/shell/page-aside.js +31 -12
- package/dist/shell/page-bar.d.ts +2 -2
- package/dist/shell/page-bar.d.ts.map +1 -1
- package/dist/shell/page-bar.js +1 -1
- package/dist/state.d.ts +10 -12
- package/dist/state.d.ts.map +1 -1
- package/dist/state.js +4 -6
- package/dist/styles.css +107 -7
- package/dist/surfaces.d.ts +28 -103
- package/dist/surfaces.d.ts.map +1 -1
- package/dist/surfaces.js +54 -143
- package/dist/{data → table}/data-table-state.d.ts +1 -1
- package/dist/table/data-table-state.d.ts.map +1 -0
- package/dist/{data → table}/data-table-state.js +4 -3
- package/dist/{data → table}/data-table.d.ts +28 -49
- package/dist/table/data-table.d.ts.map +1 -0
- package/dist/{data → table}/data-table.js +74 -60
- package/dist/table/index.d.ts +18 -0
- package/dist/table/index.d.ts.map +1 -0
- package/dist/{data/table.js → table/index.js} +1 -1
- package/dist/table/query-branch.d.ts.map +1 -0
- package/dist/table/table-features.d.ts +69 -0
- package/dist/table/table-features.d.ts.map +1 -0
- package/dist/table/table-features.js +49 -0
- package/dist/telemetry.d.ts +77 -0
- package/dist/telemetry.d.ts.map +1 -0
- package/dist/telemetry.js +33 -0
- package/dist/theme/index.d.ts +10 -21
- package/dist/theme/index.d.ts.map +1 -1
- package/dist/theme/index.js +3 -14
- package/dist/theme/theme-toggle.d.ts.map +1 -1
- package/dist/theme/theme-toggle.js +7 -33
- package/dist/theme/theme-toggle.module.css +30 -0
- package/dist/theme/theme-toggle.module.css.d.ts +10 -0
- package/dist/theme-lab/derive-controls.d.ts.map +1 -1
- package/dist/theme-lab/derive-controls.js +5 -3
- package/dist/theme-lab/derive-state.d.ts.map +1 -1
- package/dist/theme-lab/derive-state.js +2 -1
- package/dist/tokens/core.d.ts +360 -0
- package/dist/tokens/core.d.ts.map +1 -0
- package/dist/tokens/core.js +708 -0
- package/dist/tokens/derive.d.ts +1 -1
- package/dist/tokens/derive.d.ts.map +1 -1
- package/dist/tokens/index.d.ts +5 -373
- package/dist/tokens/index.d.ts.map +1 -1
- package/dist/tokens/index.js +8 -700
- package/dist/tokens/palette.d.ts +2 -6
- package/dist/tokens/palette.d.ts.map +1 -1
- package/dist/tokens/palette.js +1 -4
- package/dist/vite.d.ts +4 -3
- package/dist/vite.d.ts.map +1 -1
- package/llms.txt +43 -85
- package/package.json +80 -173
- package/src/agent/ai-sdk-transport.ts +57 -37
- package/src/{agent-chat → agent/chat}/composer.tsx +29 -30
- package/src/{agent-chat → agent/chat}/message-affordances.ts +8 -5
- package/src/{agent-chat → agent/chat}/thread-feed-row.tsx +30 -35
- package/src/{agent-chat → agent/chat}/thread-feed.tsx +40 -35
- package/src/{agent-chat → agent/chat}/thread-message.tsx +37 -35
- package/src/{agent-chat → agent/chat}/thread-outcome-card.tsx +5 -5
- package/src/{agent-chat → agent/chat}/tool-chip.tsx +12 -12
- package/src/{agent-chat → agent/chat}/use-keyboard-inset.ts +1 -1
- package/src/{agent-chat → agent/chat}/virtualize.ts +5 -5
- package/src/agent/error.ts +43 -0
- package/src/agent/history.ts +2 -107
- package/src/agent/id.ts +4 -5
- package/src/agent/index.ts +23 -112
- package/src/agent/outcome.ts +3 -56
- package/src/agent/part-list.tsx +3 -3
- package/src/agent/parts.ts +23 -22
- package/src/agent/run-committer.ts +64 -0
- package/src/agent/run-records.ts +147 -0
- package/src/agent/thread.ts +24 -231
- package/src/agent/transport.ts +6 -96
- package/src/agent/use-agent-thread-runs.ts +364 -199
- package/src/charts/BandStrip.tsx +276 -0
- package/src/charts/Bars.tsx +141 -0
- package/src/charts/CartesianChart.tsx +206 -0
- package/src/charts/ChartCard.tsx +178 -0
- package/src/charts/Donut.tsx +256 -0
- package/src/charts/DualPanel.tsx +326 -0
- package/src/charts/Heatmap.tsx +282 -0
- package/src/charts/Lines.tsx +151 -0
- package/src/charts/MirroredBars.tsx +303 -0
- package/src/charts/Sparkline.tsx +176 -0
- package/src/charts/StackedArea.tsx +80 -0
- package/src/charts/box.tsx +96 -0
- package/src/charts/card-context.ts +11 -0
- package/src/charts/chart-box.module.css +64 -0
- package/src/charts/chart-box.module.css.d.ts +12 -0
- package/src/charts/chart-card.module.css +25 -0
- package/src/charts/chart-card.module.css.d.ts +8 -0
- package/src/charts/chart-legend.module.css +38 -0
- package/src/charts/chart-legend.module.css.d.ts +10 -0
- package/src/charts/compile.ts +585 -0
- package/src/charts/cursor.ts +282 -0
- package/src/charts/curves.ts +111 -0
- package/src/charts/donut.module.css +54 -0
- package/src/charts/donut.module.css.d.ts +9 -0
- package/src/charts/host.tsx +60 -0
- package/src/charts/index.ts +41 -178
- package/src/charts/legend.tsx +204 -0
- package/src/charts/marks.ts +201 -0
- package/src/charts/panel.tsx +227 -0
- package/src/charts/scales.ts +237 -0
- package/src/charts/sizing.ts +47 -0
- package/src/charts/state.tsx +80 -0
- package/src/charts/theme.ts +65 -0
- package/src/charts/types.ts +242 -0
- package/src/cli/audit-allows.ts +8 -30
- package/src/cli/config.ts +26 -43
- package/src/cli/doctor-upgrade.ts +457 -0
- package/src/cli/doctor.ts +89 -72
- package/src/cli/help.ts +2 -4
- package/src/cli/index.ts +100 -384
- package/src/cli/init.ts +60 -51
- package/src/cli/sync.ts +13 -68
- package/src/cli/tokens-css.ts +7 -28
- package/src/commands/ShortcutsHelp.tsx +8 -9
- package/src/commands/define-commands.ts +16 -43
- package/src/commands/define-overlays.ts +12 -36
- package/src/commands/index.ts +11 -94
- package/src/{notifications → commands/notifications}/bell.tsx +2 -2
- package/src/{notifications → commands/notifications}/center.tsx +7 -5
- package/src/{notifications → commands/notifications}/define-notifications.ts +3 -3
- package/src/commands/notifications/notify.tsx +158 -0
- package/src/{notifications → commands/notifications}/store.ts +10 -2
- package/src/commands/projectors.ts +5 -225
- package/src/commands/shell-bridge.ts +5 -5
- package/src/commands/shortcut-format.ts +5 -4
- package/src/common/errors.ts +3 -3
- package/src/common/use-measured-widths.ts +6 -3
- package/src/common/validate.ts +2 -2
- package/src/content/article-layout.tsx +37 -38
- package/src/content/article-model.ts +1 -84
- package/src/content/index.ts +12 -83
- package/src/content/markdown.tsx +4 -3
- package/src/content/mermaid.tsx +9 -7
- package/src/content/slug.ts +1 -16
- package/src/content/url-hardening.ts +8 -2
- package/src/controls/actions.tsx +24 -1
- package/src/controls/enum-filter.tsx +1 -1
- package/src/controls/filter-context.tsx +3 -1
- package/src/controls/filter-set.tsx +5 -7
- package/src/controls/form-layout.tsx +2 -2
- package/src/controls/glyphs.tsx +0 -10
- package/src/controls/index.ts +13 -64
- package/src/controls/multi-select-filter.tsx +5 -4
- package/src/controls/number-filter.tsx +8 -4
- package/src/controls/range-filter.tsx +5 -4
- package/src/controls/search-filter.tsx +25 -19
- package/src/controls/select-filter.tsx +5 -4
- package/src/controls/sync-button.tsx +18 -20
- package/src/controls/toggle-filter.tsx +5 -4
- package/src/controls/uncontrolled.ts +12 -0
- package/src/controls/view-tabs.tsx +3 -3
- package/src/controls-dates/date-range-picker.tsx +1 -1
- package/src/controls-dates/index.ts +0 -1
- package/src/dashboard/index.ts +7 -48
- package/src/dashboard/section.tsx +11 -40
- package/src/dashboard/stat-card.tsx +14 -16
- package/src/dashboard/widget-header.module.css +9 -7
- package/src/dev-dock.tsx +14 -27
- package/src/format/date.ts +2 -92
- package/src/format/index.ts +7 -53
- package/src/format/number.ts +3 -26
- package/src/format/shared.ts +2 -3
- package/src/forms/index.ts +0 -23
- package/src/guard/index.ts +36 -191
- package/src/guard/types.ts +6 -19
- package/src/index.ts +30 -282
- package/src/provider/create-router.tsx +138 -0
- package/src/provider/hotkeys.ts +70 -0
- package/src/provider/index.tsx +260 -230
- package/src/provider/lab-theme.ts +3 -2
- package/src/provider/route-transition.ts +199 -0
- package/src/provider/spotlight-actions.tsx +69 -0
- package/src/provider/test-router.tsx +25 -0
- package/src/query-client.ts +4 -22
- package/src/router-tanstack/index.ts +18 -112
- package/src/router-tanstack/nav.ts +28 -2
- package/src/router-tanstack/search-store.ts +9 -2
- package/src/router-tanstack/use-nav.ts +47 -13
- package/src/shell/app-mobile-nav.tsx +3 -2
- package/src/shell/app-sidebar.tsx +1 -1
- package/src/shell/aside-docking.ts +1 -1
- package/src/shell/index.tsx +61 -20
- package/src/shell/page-aside.tsx +47 -23
- package/src/shell/page-bar.tsx +3 -3
- package/src/shell/page-title.tsx +1 -1
- package/src/state.ts +11 -37
- package/src/styles.css +107 -7
- package/src/surfaces.ts +56 -151
- package/src/{data → table}/data-table-state.ts +5 -4
- package/src/{data → table}/data-table.tsx +170 -161
- package/src/table/index.ts +18 -0
- package/src/table/table-features.ts +102 -0
- package/src/telemetry.ts +110 -0
- package/src/theme/index.ts +22 -47
- package/src/theme/theme-toggle.module.css +30 -0
- package/src/theme/theme-toggle.module.css.d.ts +10 -0
- package/src/theme/theme-toggle.tsx +15 -43
- package/src/theme-lab/derive-controls.tsx +5 -3
- package/src/theme-lab/derive-state.ts +2 -1
- package/src/tokens/core.ts +1163 -0
- package/src/tokens/derive.ts +1 -1
- package/src/tokens/index.ts +14 -1181
- package/src/tokens/palette.ts +3 -7
- package/src/vite.ts +3 -3
- package/agent/templates/query-client.ts.tpl +0 -26
- package/dist/agent/adapter.d.ts +0 -150
- package/dist/agent/adapter.d.ts.map +0 -1
- package/dist/agent/adapter.js +0 -482
- package/dist/agent/use-agent-stream.d.ts +0 -35
- package/dist/agent/use-agent-stream.d.ts.map +0 -1
- package/dist/agent/use-agent-stream.js +0 -59
- package/dist/agent-chat/composer.d.ts.map +0 -1
- package/dist/agent-chat/index.d.ts +0 -31
- package/dist/agent-chat/index.d.ts.map +0 -1
- package/dist/agent-chat/index.js +0 -19
- package/dist/agent-chat/message-affordances.d.ts.map +0 -1
- package/dist/agent-chat/relative-time.d.ts.map +0 -1
- package/dist/agent-chat/thread-detail-panel.d.ts +0 -28
- package/dist/agent-chat/thread-detail-panel.d.ts.map +0 -1
- package/dist/agent-chat/thread-detail-panel.js +0 -115
- package/dist/agent-chat/thread-feed-row.d.ts.map +0 -1
- package/dist/agent-chat/thread-feed.d.ts.map +0 -1
- package/dist/agent-chat/thread-message.d.ts.map +0 -1
- package/dist/agent-chat/thread-outcome-card.d.ts.map +0 -1
- package/dist/agent-chat/thread-workspace.d.ts +0 -44
- package/dist/agent-chat/thread-workspace.d.ts.map +0 -1
- package/dist/agent-chat/thread-workspace.js +0 -153
- package/dist/agent-chat/tool-chip.d.ts.map +0 -1
- package/dist/agent-chat/use-keyboard-inset.d.ts.map +0 -1
- package/dist/agent-chat/virtualize.d.ts.map +0 -1
- package/dist/charts/cursor/resolve.d.ts +0 -75
- package/dist/charts/cursor/resolve.d.ts.map +0 -1
- package/dist/charts/cursor/resolve.js +0 -91
- package/dist/charts/cursor/scope.d.ts +0 -20
- package/dist/charts/cursor/scope.d.ts.map +0 -1
- package/dist/charts/cursor/scope.js +0 -20
- package/dist/charts/cursor/store.d.ts +0 -35
- package/dist/charts/cursor/store.d.ts.map +0 -1
- package/dist/charts/cursor/store.js +0 -25
- package/dist/charts/cursor/touch-pin.d.ts +0 -45
- package/dist/charts/cursor/touch-pin.d.ts.map +0 -1
- package/dist/charts/cursor/touch-pin.js +0 -79
- package/dist/charts/hooks/use-discrete-cursor.d.ts +0 -66
- package/dist/charts/hooks/use-discrete-cursor.d.ts.map +0 -1
- package/dist/charts/hooks/use-discrete-cursor.js +0 -198
- package/dist/charts/hooks/use-hysteresis.d.ts +0 -18
- package/dist/charts/hooks/use-hysteresis.d.ts.map +0 -1
- package/dist/charts/hooks/use-hysteresis.js +0 -12
- package/dist/charts/hooks/useBandPlot.d.ts +0 -145
- package/dist/charts/hooks/useBandPlot.d.ts.map +0 -1
- package/dist/charts/hooks/useBandPlot.js +0 -178
- package/dist/charts/hooks/useChartCursor.d.ts +0 -75
- package/dist/charts/hooks/useChartCursor.d.ts.map +0 -1
- package/dist/charts/hooks/useChartCursor.js +0 -200
- package/dist/charts/hooks/useChartSize.d.ts +0 -29
- package/dist/charts/hooks/useChartSize.d.ts.map +0 -1
- package/dist/charts/hooks/useChartSize.js +0 -8
- package/dist/charts/kinds/BandStrip.d.ts +0 -132
- package/dist/charts/kinds/BandStrip.d.ts.map +0 -1
- package/dist/charts/kinds/BandStrip.js +0 -241
- package/dist/charts/kinds/Bars.d.ts +0 -141
- package/dist/charts/kinds/Bars.d.ts.map +0 -1
- package/dist/charts/kinds/Bars.js +0 -343
- package/dist/charts/kinds/Donut.d.ts +0 -77
- package/dist/charts/kinds/Donut.d.ts.map +0 -1
- package/dist/charts/kinds/Donut.js +0 -245
- package/dist/charts/kinds/DualPanel.d.ts +0 -111
- package/dist/charts/kinds/DualPanel.d.ts.map +0 -1
- package/dist/charts/kinds/DualPanel.js +0 -471
- package/dist/charts/kinds/Heatmap.d.ts +0 -85
- package/dist/charts/kinds/Heatmap.d.ts.map +0 -1
- package/dist/charts/kinds/Heatmap.js +0 -243
- package/dist/charts/kinds/MirroredBars.d.ts +0 -98
- package/dist/charts/kinds/MirroredBars.d.ts.map +0 -1
- package/dist/charts/kinds/MirroredBars.js +0 -292
- package/dist/charts/kinds/MultiLine.d.ts +0 -87
- package/dist/charts/kinds/MultiLine.d.ts.map +0 -1
- package/dist/charts/kinds/MultiLine.js +0 -176
- package/dist/charts/kinds/StackedArea.d.ts +0 -73
- package/dist/charts/kinds/StackedArea.d.ts.map +0 -1
- package/dist/charts/kinds/StackedArea.js +0 -135
- package/dist/charts/kinds/ZonedLine.d.ts +0 -97
- package/dist/charts/kinds/ZonedLine.d.ts.map +0 -1
- package/dist/charts/kinds/ZonedLine.js +0 -155
- package/dist/charts/kinds/band-chrome.d.ts +0 -31
- package/dist/charts/kinds/band-chrome.d.ts.map +0 -1
- package/dist/charts/kinds/band-chrome.js +0 -71
- package/dist/charts/kinds/donut-layout.d.ts +0 -47
- package/dist/charts/kinds/donut-layout.d.ts.map +0 -1
- package/dist/charts/kinds/donut-layout.js +0 -52
- package/dist/charts/layout/auto-margin.d.ts +0 -122
- package/dist/charts/layout/auto-margin.d.ts.map +0 -1
- package/dist/charts/layout/auto-margin.js +0 -81
- package/dist/charts/layout/end-labels.d.ts +0 -64
- package/dist/charts/layout/end-labels.d.ts.map +0 -1
- package/dist/charts/layout/end-labels.js +0 -76
- package/dist/charts/layout/log-ticks.d.ts +0 -27
- package/dist/charts/layout/log-ticks.d.ts.map +0 -1
- package/dist/charts/layout/log-ticks.js +0 -42
- package/dist/charts/primitives/AreaGradient.d.ts +0 -18
- package/dist/charts/primitives/AreaGradient.d.ts.map +0 -1
- package/dist/charts/primitives/AreaGradient.js +0 -13
- package/dist/charts/primitives/Axes.d.ts +0 -83
- package/dist/charts/primitives/Axes.d.ts.map +0 -1
- package/dist/charts/primitives/Axes.js +0 -168
- package/dist/charts/primitives/CartesianChart.d.ts +0 -257
- package/dist/charts/primitives/CartesianChart.d.ts.map +0 -1
- package/dist/charts/primitives/CartesianChart.js +0 -549
- package/dist/charts/primitives/ChartCard.d.ts +0 -66
- package/dist/charts/primitives/ChartCard.d.ts.map +0 -1
- package/dist/charts/primitives/ChartCard.js +0 -143
- package/dist/charts/primitives/ChartFrame.d.ts +0 -156
- package/dist/charts/primitives/ChartFrame.d.ts.map +0 -1
- package/dist/charts/primitives/ChartFrame.js +0 -241
- package/dist/charts/primitives/ChartLegend.d.ts +0 -67
- package/dist/charts/primitives/ChartLegend.d.ts.map +0 -1
- package/dist/charts/primitives/ChartLegend.js +0 -292
- package/dist/charts/primitives/ChartPending.d.ts +0 -113
- package/dist/charts/primitives/ChartPending.d.ts.map +0 -1
- package/dist/charts/primitives/ChartPending.js +0 -117
- package/dist/charts/primitives/ChartTooltip.d.ts +0 -59
- package/dist/charts/primitives/ChartTooltip.d.ts.map +0 -1
- package/dist/charts/primitives/ChartTooltip.js +0 -197
- package/dist/charts/primitives/Crosshair.d.ts +0 -26
- package/dist/charts/primitives/Crosshair.d.ts.map +0 -1
- package/dist/charts/primitives/Crosshair.js +0 -23
- package/dist/charts/primitives/HatchPattern.d.ts +0 -31
- package/dist/charts/primitives/HatchPattern.d.ts.map +0 -1
- package/dist/charts/primitives/HatchPattern.js +0 -31
- package/dist/charts/primitives/HoverOverlay.d.ts +0 -45
- package/dist/charts/primitives/HoverOverlay.d.ts.map +0 -1
- package/dist/charts/primitives/HoverOverlay.js +0 -72
- package/dist/charts/primitives/HoverOverlay.module.css +0 -13
- package/dist/charts/primitives/HoverOverlay.module.css.d.ts +0 -8
- package/dist/charts/primitives/XZoneRects.d.ts +0 -54
- package/dist/charts/primitives/XZoneRects.d.ts.map +0 -1
- package/dist/charts/primitives/XZoneRects.js +0 -65
- package/dist/charts/primitives/ZoneRects.d.ts +0 -38
- package/dist/charts/primitives/ZoneRects.d.ts.map +0 -1
- package/dist/charts/primitives/ZoneRects.js +0 -30
- package/dist/charts/primitives/chart-card-context.d.ts +0 -33
- package/dist/charts/primitives/chart-card-context.d.ts.map +0 -1
- package/dist/charts/primitives/chart-card-context.js +0 -18
- package/dist/charts/primitives/chart-frame-layout.d.ts +0 -185
- package/dist/charts/primitives/chart-frame-layout.d.ts.map +0 -1
- package/dist/charts/primitives/chart-frame-layout.js +0 -175
- package/dist/charts/primitives/chart-layout.d.ts +0 -112
- package/dist/charts/primitives/chart-layout.d.ts.map +0 -1
- package/dist/charts/primitives/chart-layout.js +0 -175
- package/dist/charts/primitives/chart-tier.d.ts +0 -16
- package/dist/charts/primitives/chart-tier.d.ts.map +0 -1
- package/dist/charts/primitives/chart-tier.js +0 -44
- package/dist/charts/primitives/legend-disclosure.d.ts +0 -27
- package/dist/charts/primitives/legend-disclosure.d.ts.map +0 -1
- package/dist/charts/primitives/legend-disclosure.js +0 -153
- package/dist/charts/series.d.ts +0 -189
- package/dist/charts/series.d.ts.map +0 -1
- package/dist/charts/series.js +0 -84
- package/dist/charts/sparklines/BarSparkline.d.ts +0 -37
- package/dist/charts/sparklines/BarSparkline.d.ts.map +0 -1
- package/dist/charts/sparklines/BarSparkline.js +0 -53
- package/dist/charts/sparklines/LineSparkline.d.ts +0 -15
- package/dist/charts/sparklines/LineSparkline.d.ts.map +0 -1
- package/dist/charts/sparklines/LineSparkline.js +0 -51
- package/dist/charts/sparklines/index.d.ts +0 -3
- package/dist/charts/sparklines/index.d.ts.map +0 -1
- package/dist/charts/sparklines/index.js +0 -6
- package/dist/charts/utils/domain.d.ts +0 -35
- package/dist/charts/utils/domain.d.ts.map +0 -1
- package/dist/charts/utils/domain.js +0 -12
- package/dist/charts/utils/format.d.ts +0 -8
- package/dist/charts/utils/format.d.ts.map +0 -1
- package/dist/charts/utils/format.js +0 -20
- package/dist/charts/utils/measure-text.d.ts +0 -25
- package/dist/charts/utils/measure-text.d.ts.map +0 -1
- package/dist/charts/utils/measure-text.js +0 -64
- package/dist/charts/utils/ticks.d.ts +0 -103
- package/dist/charts/utils/ticks.d.ts.map +0 -1
- package/dist/charts/utils/ticks.js +0 -92
- package/dist/commands/overlays-mount.d.ts +0 -122
- package/dist/commands/overlays-mount.d.ts.map +0 -1
- package/dist/commands/overlays-mount.js +0 -125
- package/dist/commands/useCommandHotkeys.d.ts +0 -14
- package/dist/commands/useCommandHotkeys.d.ts.map +0 -1
- package/dist/commands/useCommandHotkeys.js +0 -45
- package/dist/content/article-actions.d.ts +0 -39
- package/dist/content/article-actions.d.ts.map +0 -1
- package/dist/content/article-actions.js +0 -28
- package/dist/content/article-card.d.ts +0 -58
- package/dist/content/article-card.d.ts.map +0 -1
- package/dist/content/article-card.js +0 -79
- package/dist/content/article-card.module.css +0 -111
- package/dist/content/mdx.d.ts +0 -20
- package/dist/content/mdx.d.ts.map +0 -1
- package/dist/content/mdx.js +0 -64
- package/dist/controls/compare-filter.d.ts +0 -42
- package/dist/controls/compare-filter.d.ts.map +0 -1
- package/dist/controls/compare-filter.js +0 -38
- package/dist/controls/slider-control.d.ts +0 -36
- package/dist/controls/slider-control.d.ts.map +0 -1
- package/dist/controls/slider-control.js +0 -64
- package/dist/dashboard/widget-grid.d.ts +0 -27
- package/dist/dashboard/widget-grid.d.ts.map +0 -1
- package/dist/dashboard/widget-grid.js +0 -40
- package/dist/dashboard/widget-grid.module.css +0 -69
- package/dist/data/data-table-state.d.ts.map +0 -1
- package/dist/data/data-table.d.ts.map +0 -1
- package/dist/data/query-branch.d.ts.map +0 -1
- package/dist/data/table.d.ts +0 -24
- package/dist/data/table.d.ts.map +0 -1
- package/dist/data/virtual-list.d.ts +0 -133
- package/dist/data/virtual-list.d.ts.map +0 -1
- package/dist/data/virtual-list.js +0 -157
- package/dist/data/virtual.d.ts +0 -21
- package/dist/data/virtual.d.ts.map +0 -1
- package/dist/data/virtual.js +0 -4
- package/dist/forms/FormErrorSummary.d.ts +0 -30
- package/dist/forms/FormErrorSummary.d.ts.map +0 -1
- package/dist/forms/FormErrorSummary.js +0 -30
- package/dist/forms/use-field-array.d.ts +0 -60
- package/dist/forms/use-field-array.d.ts.map +0 -1
- package/dist/forms/use-field-array.js +0 -20
- package/dist/forms/use-form-draft.d.ts +0 -76
- package/dist/forms/use-form-draft.d.ts.map +0 -1
- package/dist/forms/use-form-draft.js +0 -92
- package/dist/forms/use-form-submit.d.ts +0 -88
- package/dist/forms/use-form-submit.d.ts.map +0 -1
- package/dist/forms/use-form-submit.js +0 -63
- package/dist/notifications/bell.d.ts.map +0 -1
- package/dist/notifications/center.d.ts.map +0 -1
- package/dist/notifications/define-notifications.d.ts.map +0 -1
- package/dist/notifications/index.d.ts +0 -40
- package/dist/notifications/index.d.ts.map +0 -1
- package/dist/notifications/index.js +0 -54
- package/dist/notifications/mount-guard.d.ts +0 -8
- package/dist/notifications/mount-guard.d.ts.map +0 -1
- package/dist/notifications/mount-guard.js +0 -18
- package/dist/notifications/notify.d.ts +0 -149
- package/dist/notifications/notify.d.ts.map +0 -1
- package/dist/notifications/notify.js +0 -171
- package/dist/notifications/store.d.ts.map +0 -1
- package/dist/provider/connectivity-indicator.d.ts +0 -13
- package/dist/provider/connectivity-indicator.d.ts.map +0 -1
- package/dist/provider/connectivity-indicator.js +0 -205
- package/dist/provider/connectivity-provider.d.ts +0 -7
- package/dist/provider/connectivity-provider.d.ts.map +0 -1
- package/dist/provider/connectivity-provider.js +0 -128
- package/dist/provider/connectivity-types.d.ts +0 -36
- package/dist/provider/connectivity-types.d.ts.map +0 -1
- package/dist/provider/use-connectivity.d.ts +0 -3
- package/dist/provider/use-connectivity.d.ts.map +0 -1
- package/dist/provider/use-connectivity.js +0 -14
- package/dist/query-devtools.d.ts +0 -21
- package/dist/query-devtools.d.ts.map +0 -1
- package/dist/query-devtools.js +0 -13
- package/src/agent/adapter.ts +0 -945
- package/src/agent/use-agent-stream.ts +0 -154
- package/src/agent-chat/index.ts +0 -54
- package/src/agent-chat/thread-detail-panel.tsx +0 -201
- package/src/agent-chat/thread-workspace.tsx +0 -279
- package/src/charts/cursor/resolve.ts +0 -198
- package/src/charts/cursor/scope.tsx +0 -35
- package/src/charts/cursor/store.ts +0 -58
- package/src/charts/cursor/touch-pin.ts +0 -138
- package/src/charts/hooks/use-discrete-cursor.ts +0 -335
- package/src/charts/hooks/use-hysteresis.ts +0 -26
- package/src/charts/hooks/useBandPlot.ts +0 -383
- package/src/charts/hooks/useChartCursor.ts +0 -367
- package/src/charts/hooks/useChartSize.ts +0 -35
- package/src/charts/kinds/BandStrip.tsx +0 -449
- package/src/charts/kinds/Bars.tsx +0 -589
- package/src/charts/kinds/Donut.tsx +0 -368
- package/src/charts/kinds/DualPanel.tsx +0 -712
- package/src/charts/kinds/Heatmap.tsx +0 -396
- package/src/charts/kinds/MirroredBars.tsx +0 -469
- package/src/charts/kinds/MultiLine.tsx +0 -322
- package/src/charts/kinds/StackedArea.tsx +0 -267
- package/src/charts/kinds/ZonedLine.tsx +0 -301
- package/src/charts/kinds/band-chrome.tsx +0 -91
- package/src/charts/kinds/donut-layout.ts +0 -82
- package/src/charts/layout/auto-margin.ts +0 -242
- package/src/charts/layout/end-labels.ts +0 -135
- package/src/charts/layout/log-ticks.ts +0 -90
- package/src/charts/primitives/AreaGradient.tsx +0 -26
- package/src/charts/primitives/Axes.tsx +0 -271
- package/src/charts/primitives/CartesianChart.tsx +0 -1002
- package/src/charts/primitives/ChartCard.tsx +0 -280
- package/src/charts/primitives/ChartFrame.tsx +0 -493
- package/src/charts/primitives/ChartLegend.tsx +0 -422
- package/src/charts/primitives/ChartPending.tsx +0 -243
- package/src/charts/primitives/ChartTooltip.tsx +0 -265
- package/src/charts/primitives/Crosshair.tsx +0 -43
- package/src/charts/primitives/HatchPattern.tsx +0 -55
- package/src/charts/primitives/HoverOverlay.module.css +0 -13
- package/src/charts/primitives/HoverOverlay.module.css.d.ts +0 -8
- package/src/charts/primitives/HoverOverlay.tsx +0 -109
- package/src/charts/primitives/XZoneRects.tsx +0 -132
- package/src/charts/primitives/ZoneRects.tsx +0 -67
- package/src/charts/primitives/chart-card-context.ts +0 -46
- package/src/charts/primitives/chart-frame-layout.ts +0 -430
- package/src/charts/primitives/chart-layout.ts +0 -318
- package/src/charts/primitives/chart-tier.tsx +0 -76
- package/src/charts/primitives/legend-disclosure.tsx +0 -178
- package/src/charts/series.ts +0 -276
- package/src/charts/sparklines/BarSparkline.tsx +0 -94
- package/src/charts/sparklines/LineSparkline.tsx +0 -71
- package/src/charts/sparklines/index.ts +0 -2
- package/src/charts/theme.tsx +0 -53
- package/src/charts/utils/domain.ts +0 -41
- package/src/charts/utils/format.ts +0 -19
- package/src/charts/utils/measure-text.ts +0 -109
- package/src/charts/utils/ticks.ts +0 -239
- package/src/commands/overlays-mount.tsx +0 -333
- package/src/commands/useCommandHotkeys.ts +0 -114
- package/src/content/article-actions.ts +0 -72
- package/src/content/article-card.module.css +0 -111
- package/src/content/article-card.tsx +0 -174
- package/src/content/mdx.tsx +0 -127
- package/src/controls/compare-filter.tsx +0 -81
- package/src/controls/slider-control.tsx +0 -159
- package/src/dashboard/widget-grid.module.css +0 -69
- package/src/dashboard/widget-grid.tsx +0 -146
- package/src/data/table.ts +0 -39
- package/src/data/virtual-list.tsx +0 -356
- package/src/data/virtual.ts +0 -26
- package/src/forms/FormErrorSummary.tsx +0 -71
- package/src/forms/use-field-array.ts +0 -94
- package/src/forms/use-form-draft.ts +0 -262
- package/src/forms/use-form-submit.ts +0 -199
- package/src/notifications/index.ts +0 -88
- package/src/notifications/mount-guard.ts +0 -38
- package/src/notifications/notify.tsx +0 -379
- package/src/provider/connectivity-indicator.tsx +0 -267
- package/src/provider/connectivity-provider.tsx +0 -189
- package/src/provider/connectivity-types.ts +0 -42
- package/src/provider/use-connectivity.ts +0 -26
- package/src/query-devtools.tsx +0 -40
- /package/dist/{agent-chat → agent/chat}/message-affordances.js +0 -0
- /package/dist/{agent-chat → agent/chat}/relative-time.d.ts +0 -0
- /package/dist/{agent-chat → agent/chat}/relative-time.js +0 -0
- /package/dist/{agent-chat → agent/chat}/use-keyboard-inset.d.ts +0 -0
- /package/dist/{agent-chat → agent/chat}/virtualize.js +0 -0
- /package/dist/{notifications → commands/notifications}/bell.js +0 -0
- /package/dist/{notifications → commands/notifications}/define-notifications.js +0 -0
- /package/dist/{provider/connectivity-types.js → controls/uncontrolled.js} +0 -0
- /package/dist/{data → table}/data-table.module.css +0 -0
- /package/dist/{data → table}/data-table.module.css.d.ts +0 -0
- /package/dist/{data → table}/query-branch.d.ts +0 -0
- /package/dist/{data → table}/query-branch.js +0 -0
- /package/src/{agent-chat → agent/chat}/relative-time.ts +0 -0
- /package/src/{data → table}/data-table.module.css +0 -0
- /package/src/{data → table}/data-table.module.css.d.ts +0 -0
- /package/src/{data → table}/query-branch.ts +0 -0
package/configs/oxlint-plugin.js
CHANGED
|
@@ -17,9 +17,9 @@
|
|
|
17
17
|
* scope has to be spelled. The three boundary rules deliberately honour neither — see their own
|
|
18
18
|
* comment for why.
|
|
19
19
|
*
|
|
20
|
-
* ── The
|
|
20
|
+
* ── The agent-chat guard rule (`agent-no-raw-usechat`) + `ai-sdk-major` ──
|
|
21
21
|
* These are also correctness/architecture boundaries, not design guidance, so they do NOT honour
|
|
22
|
-
* `theme-allow`.
|
|
22
|
+
* `theme-allow`. Both instead honour their own line-comment escape, `basalt-agent-allow`
|
|
23
23
|
* (`hasAgentAllow`, a copy of `hasThemeAllow` with a different needle) — deliberately a separate
|
|
24
24
|
* token, so a color exemption can never double as a license to switch off a streaming guard or an
|
|
25
25
|
* intentional `ai` major skew. `ai-sdk-major`'s escape is line-level and per-import, distinct from
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
* filesystem (reads ancestor `package.json`s, memoized per directory so a lint run does one walk
|
|
31
31
|
* per directory, not per file) — every other rule here is pure AST + filename-string matching.
|
|
32
32
|
*
|
|
33
|
-
* ── The three boundary rules (`
|
|
33
|
+
* ── The three boundary rules (`chart-engine-boundary` / `chart-host-single` / `token-layer-boundary`) ─────────
|
|
34
34
|
* These used to be one bundled `import-boundary` rule sharing a single on/off toggle — that meant
|
|
35
35
|
* a consumer disabling the one check they disagreed with silently dropped the other two as well.
|
|
36
36
|
* Each is now its own rule id with its own `meta`/`create`/registration, so a consumer can only
|
|
@@ -66,8 +66,7 @@
|
|
|
66
66
|
* per-glob last-writer-wins for `no-restricted-imports` — a base ban does NOT merge into an
|
|
67
67
|
* override glob, so any ban that must also hold inside an override is duplicated into that
|
|
68
68
|
* override, AND a consumer's own `no-restricted-imports` override on an overlapping glob silently
|
|
69
|
-
* REPLACES basalt's ban rather than merging with it. That is exactly why the
|
|
70
|
-
* charts and token-layer charts/tokens boundaries live in plugin rules below instead of
|
|
69
|
+
* REPLACES basalt's ban rather than merging with it. That is exactly why the chart-engine and token-layer charts/tokens boundaries live in plugin rules below instead of
|
|
71
70
|
* `no-restricted-imports`: a jsPlugin rule has its own rule id, so a consumer can only turn it off
|
|
72
71
|
* explicitly, by name — never silently. `configs/oxlint.json` is generated from `SURFACES` by
|
|
73
72
|
* `scripts/gen-oxlint.ts` (`--check` is the CI drift gate) — its top-level keys are limited to what
|
|
@@ -86,8 +85,7 @@ import { fileURLToPath } from 'node:url'
|
|
|
86
85
|
* `check-theme --audit-allows`, never as a typo.
|
|
87
86
|
*
|
|
88
87
|
* - `unframed-chart` (regex: a `<ChartLegend items={[...]}>` array literal) →
|
|
89
|
-
* `basalt/chart-legend-literal`,
|
|
90
|
-
* regex never could reach.
|
|
88
|
+
* `basalt/chart-legend-literal`, itself retired in 2.0 with the public `ChartLegend`.
|
|
91
89
|
* - `duplicate-notifications-mount` — its only case, `<BasaltNotifications />` mounted beside
|
|
92
90
|
* `<BasaltOverlays notifications>`, is unreachable now that `BasaltNotifications` no longer
|
|
93
91
|
* exists as a standalone export; the rule had nothing left to fire on.
|
|
@@ -98,8 +96,21 @@ import { fileURLToPath } from 'node:url'
|
|
|
98
96
|
*/
|
|
99
97
|
export const RETIRED_RULE_IDS = new Set([
|
|
100
98
|
'unframed-chart',
|
|
99
|
+
// 2.0 (V2-SPEC § 9.8): the visx lint/guard layer, replaced by `chart-engine-boundary` or made
|
|
100
|
+
// unrepresentable by the rung.
|
|
101
|
+
'visx-boundary',
|
|
102
|
+
'visx-tooltip',
|
|
103
|
+
'hand-rolled-plot',
|
|
104
|
+
'chart-legend-literal',
|
|
105
|
+
'raw-visx-axis',
|
|
106
|
+
'chart-missing-aria-label',
|
|
101
107
|
'duplicate-notifications-mount',
|
|
102
108
|
'query-dual-import',
|
|
109
|
+
// 2.0 (V2-SPEC § 3): the concern is unrepresentable.
|
|
110
|
+
'provider-above-router', // BasaltProvider mounts RouterProvider
|
|
111
|
+
'page-bar-budget', // PageBarActions / SectionActions tuple types
|
|
112
|
+
'agent-resume-guard', // resumeStream only exists on useChat/Chat, banned by agent-no-raw-usechat
|
|
113
|
+
'deprecated-export', // 2.0.0 ships no deprecations; returns with the first 2.x one
|
|
103
114
|
])
|
|
104
115
|
|
|
105
116
|
/**
|
|
@@ -119,8 +130,6 @@ export const KNOWN_RULE_IDS = new Set([
|
|
|
119
130
|
'raw-size-literal',
|
|
120
131
|
'card-inset',
|
|
121
132
|
'chart-in-raw-surface',
|
|
122
|
-
'hand-rolled-plot',
|
|
123
|
-
'chart-legend-literal',
|
|
124
133
|
'hand-rolled-shell',
|
|
125
134
|
'shadow-basalt-export',
|
|
126
135
|
'raw-scroll-container',
|
|
@@ -128,25 +137,23 @@ export const KNOWN_RULE_IDS = new Set([
|
|
|
128
137
|
'control-outside-home',
|
|
129
138
|
'bound-control-outside-home',
|
|
130
139
|
'control-size-literal',
|
|
131
|
-
'page-bar-budget',
|
|
132
140
|
'responsive-twin',
|
|
133
141
|
'search-literal-link',
|
|
134
142
|
'use-search-from-literal',
|
|
135
|
-
'provider-above-router',
|
|
136
143
|
'query-dual-import',
|
|
137
144
|
'query-fn-unwrap',
|
|
138
|
-
'deprecated-export',
|
|
139
145
|
'forms-field-key',
|
|
140
146
|
// `in-body-page-title` is BOTH a plugin rule and a guard kind — one id, two lanes, so one
|
|
141
147
|
// annotation waives both. It is listed once, below, with the guard kinds.
|
|
142
|
-
'
|
|
143
|
-
'
|
|
148
|
+
'chart-engine-boundary',
|
|
149
|
+
'chart-host-single',
|
|
144
150
|
'token-layer-boundary',
|
|
145
151
|
'no-import-meta-env',
|
|
146
152
|
'raw-breakpoint',
|
|
147
|
-
|
|
153
|
+
'no-dev-helper',
|
|
154
|
+
'route-view-transition',
|
|
155
|
+
// These two honour `basalt-agent-allow`, never `theme-allow` — but they are real ids, so an
|
|
148
156
|
// annotation naming one must parse as a (useless) scoped annotation rather than as prose.
|
|
149
|
-
'agent-resume-guard',
|
|
150
157
|
'agent-no-raw-usechat',
|
|
151
158
|
'ai-sdk-major',
|
|
152
159
|
// src/guard kinds
|
|
@@ -163,9 +170,7 @@ export const KNOWN_RULE_IDS = new Set([
|
|
|
163
170
|
'raw-html-layout',
|
|
164
171
|
'inline-spacing',
|
|
165
172
|
'inline-display',
|
|
166
|
-
'raw-visx-axis',
|
|
167
173
|
'raw-motion-value',
|
|
168
|
-
'chart-missing-aria-label',
|
|
169
174
|
'raw-form-control',
|
|
170
175
|
'sub-16-input-font',
|
|
171
176
|
'raw-font-family',
|
|
@@ -665,11 +670,9 @@ const CHART_TAGS = new Set([
|
|
|
665
670
|
'DualPanel',
|
|
666
671
|
'Heatmap',
|
|
667
672
|
'MirroredBars',
|
|
668
|
-
'
|
|
673
|
+
'Lines',
|
|
669
674
|
'StackedArea',
|
|
670
|
-
'
|
|
671
|
-
'BarSparkline',
|
|
672
|
-
'LineSparkline',
|
|
675
|
+
'Sparkline',
|
|
673
676
|
])
|
|
674
677
|
|
|
675
678
|
const CHART_IN_RAW_SURFACE_MESSAGE =
|
|
@@ -767,7 +770,7 @@ const RAW_SCROLL_CONTAINER_HOME_MESSAGE =
|
|
|
767
770
|
*
|
|
768
771
|
* Scoped by ANCESTRY only, with no hoisted-binding lookup: a style object hoisted to a `const` has
|
|
769
772
|
* no JSX ancestor at all, so a hoisted `{ overflowX: 'auto' }` handed to a slot is a known false
|
|
770
|
-
* negative here — the same gap `chart-legend-literal`
|
|
773
|
+
* negative here — the same gap the retired `chart-legend-literal` had for a hoisted `items` array, and
|
|
771
774
|
* for the same reason (following it means local flow analysis for a bypass nobody reaches by
|
|
772
775
|
* accident).
|
|
773
776
|
*/
|
|
@@ -865,12 +868,6 @@ const rawScrollContainer = {
|
|
|
865
868
|
|
|
866
869
|
// Message bodies mirror the wording that used to live in the shipped `no-restricted-imports`
|
|
867
870
|
// overrides (configs/oxlint.json) verbatim, so the DX doesn't regress.
|
|
868
|
-
const VISX_BOUNDARY_MESSAGE =
|
|
869
|
-
'Direct @visx/* imports are only allowed inside the charts boundary (**/charts/**). ' +
|
|
870
|
-
'(basalt/visx-boundary)'
|
|
871
|
-
const VISX_TOOLTIP_MESSAGE =
|
|
872
|
-
'Use ChartTooltip + TooltipHeader/Row/Body from basalt-ui charts primitives. ' +
|
|
873
|
-
'(basalt/visx-tooltip)'
|
|
874
871
|
const TOKEN_LAYER_BOUNDARY_MESSAGES = {
|
|
875
872
|
charts:
|
|
876
873
|
'charts must stay upstream-of-Mantine (no Mantine imports) — read color via --vx-* tokens, ' +
|
|
@@ -923,58 +920,141 @@ function boundaryVisitor(reportFor) {
|
|
|
923
920
|
ExportAllDeclaration(node) {
|
|
924
921
|
reportFor(node, exportSource(node))
|
|
925
922
|
},
|
|
923
|
+
// CommonJS `require('<spec>')` with a literal specifier; a computed one cannot be judged.
|
|
924
|
+
CallExpression(node) {
|
|
925
|
+
const [arg] = node.arguments
|
|
926
|
+
if (node.callee?.type !== 'Identifier' || node.callee.name !== 'require') return
|
|
927
|
+
if (arg?.type === 'Literal') reportFor(node, arg.value)
|
|
928
|
+
},
|
|
926
929
|
}
|
|
927
930
|
}
|
|
928
931
|
|
|
929
|
-
// Rule 5 —
|
|
930
|
-
//
|
|
931
|
-
//
|
|
932
|
-
// is the
|
|
932
|
+
// Rule 5 — chart-engine-boundary (V2-SPEC § 9.8): `@visx/*` is banned everywhere; outside basalt's
|
|
933
|
+
// own `src/`, `@tanstack/charts` is an ALLOWLIST of mark/scale/transform subpaths. The bare root
|
|
934
|
+
// re-exports `defineChart`, `createChartRuntime`, `mountChart`, `renderChartSvg` and `createMark`
|
|
935
|
+
// (a second chart runtime), `/react` is the host (basalt owns the one), and any NEW engine
|
|
936
|
+
// subpath is an error until `oxlint-plugin.test.ts` classifies it against the engine's `exports`.
|
|
937
|
+
// Inside `packages/basalt-ui/src/` only the visx ban applies — `basalt/chart-host-single` polices
|
|
938
|
+
// the one place `/react` may be imported. Deliberately NO `hasThemeAllow` escape (see
|
|
939
|
+
// token-layer-boundary below).
|
|
940
|
+
const visxBanned = (source) => source === '@visx' || source.startsWith('@visx/')
|
|
941
|
+
|
|
942
|
+
/** Exact `@tanstack/charts` subpaths a consumer may import. Kept in step with V2-SPEC § 9.8. */
|
|
943
|
+
export const CHART_ENGINE_ALLOWED_SUBPATHS = new Set([
|
|
944
|
+
'line',
|
|
945
|
+
'area',
|
|
946
|
+
'area-x',
|
|
947
|
+
'bar',
|
|
948
|
+
'rect',
|
|
949
|
+
'rule',
|
|
950
|
+
'dot',
|
|
951
|
+
'tick',
|
|
952
|
+
'text',
|
|
953
|
+
'vector',
|
|
954
|
+
'link',
|
|
955
|
+
'arrow',
|
|
956
|
+
'difference',
|
|
957
|
+
'stack',
|
|
958
|
+
'mark/decorative',
|
|
959
|
+
'd3/shape',
|
|
960
|
+
'types',
|
|
961
|
+
])
|
|
962
|
+
|
|
963
|
+
/** Allowed subpath FAMILIES: `scales/*` and `transform/*` (the bare `transform` is not one). */
|
|
964
|
+
export const CHART_ENGINE_ALLOWED_PREFIXES = ['scales/', 'transform/']
|
|
965
|
+
|
|
966
|
+
/**
|
|
967
|
+
* True when `subpath` (the part after `@tanstack/charts/`) is on the allowlist. A `.`, `..` or empty
|
|
968
|
+
* segment is rejected first: `scales/../react` starts with an allowed family but resolves outside
|
|
969
|
+
* it. Within a family the length guard rejects the bare prefix (`scales/`), which names no module.
|
|
970
|
+
*/
|
|
971
|
+
export function isAllowedChartEngineSubpath(subpath) {
|
|
972
|
+
if (subpath.split('/').some((segment) => segment === '' || segment === '.' || segment === '..'))
|
|
973
|
+
return false
|
|
974
|
+
return (
|
|
975
|
+
CHART_ENGINE_ALLOWED_SUBPATHS.has(subpath) ||
|
|
976
|
+
CHART_ENGINE_ALLOWED_PREFIXES.some(
|
|
977
|
+
(prefix) => subpath.startsWith(prefix) && subpath.length > prefix.length,
|
|
978
|
+
)
|
|
979
|
+
)
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
const CHART_ENGINE_ALLOWED_LIST = [
|
|
983
|
+
...CHART_ENGINE_ALLOWED_SUBPATHS,
|
|
984
|
+
...CHART_ENGINE_ALLOWED_PREFIXES.map((p) => `${p}*`),
|
|
985
|
+
].join(', ')
|
|
986
|
+
const CHART_ENGINE_MESSAGE =
|
|
987
|
+
"Import chart kinds from 'basalt-ui/charts', or the rung's `marks` (CartesianChart) for custom " +
|
|
988
|
+
'marks — only these @tanstack/charts subpaths are allowed: ' +
|
|
989
|
+
CHART_ENGINE_ALLOWED_LIST +
|
|
990
|
+
". The bare root, /react and every other subpath are basalt's to own. (basalt/chart-engine-boundary)"
|
|
991
|
+
const VISX_BANNED_MESSAGE =
|
|
992
|
+
"@visx/* is gone in 2.0 — use 'basalt-ui/charts' kinds, or the rung's `marks` built from the " +
|
|
993
|
+
'allowed @tanstack/charts subpaths. (basalt/chart-engine-boundary)'
|
|
994
|
+
|
|
995
|
+
/** True for basalt's own package source — the one place the engine allowlist does not apply. */
|
|
996
|
+
function inBasaltSource(filename) {
|
|
997
|
+
return filename.replaceAll('\\', '/').includes('/packages/basalt-ui/src/')
|
|
998
|
+
}
|
|
999
|
+
|
|
933
1000
|
// Ships: error
|
|
934
|
-
const
|
|
1001
|
+
const chartEngineBoundary = {
|
|
935
1002
|
meta: {
|
|
936
|
-
type: '
|
|
1003
|
+
type: 'problem',
|
|
937
1004
|
docs: {
|
|
938
|
-
description:
|
|
1005
|
+
description:
|
|
1006
|
+
'Ban @visx/* everywhere and restrict @tanstack/charts to the allowed mark subpaths.',
|
|
939
1007
|
},
|
|
940
1008
|
schema: [],
|
|
941
1009
|
},
|
|
942
1010
|
create(context) {
|
|
943
|
-
const
|
|
944
|
-
|
|
945
|
-
// Deliberately NO `hasThemeAllow` escape here (unlike the four design-guard rules above).
|
|
946
|
-
// `theme-allow` exists so the theme/palette source can opt out of DESIGN guidance (raw font
|
|
947
|
-
// sizes, raw colors) — it is not a license to punch through an ARCHITECTURE boundary. The whole
|
|
948
|
-
// point of moving this check into its own plugin rule instead of `no-restricted-imports` was
|
|
949
|
-
// that it can only be turned off explicitly, by rule id (see the file-header comment); a stray
|
|
950
|
-
// `theme-allow` comment (which also matches the line above the flagged node) must not silently
|
|
951
|
-
// bypass it. Do not "restore consistency" by adding it back.
|
|
1011
|
+
const ownSource = inBasaltSource(getFilename(context))
|
|
1012
|
+
|
|
952
1013
|
function reportFor(node, source) {
|
|
953
|
-
if (typeof source !== 'string'
|
|
954
|
-
if (source
|
|
955
|
-
|
|
1014
|
+
if (typeof source !== 'string') return
|
|
1015
|
+
if (visxBanned(source)) {
|
|
1016
|
+
context.report({ node, message: VISX_BANNED_MESSAGE })
|
|
1017
|
+
return
|
|
1018
|
+
}
|
|
1019
|
+
if (ownSource) return
|
|
1020
|
+
if (source !== '@tanstack/charts' && !source.startsWith('@tanstack/charts/')) return
|
|
1021
|
+
const subpath = source.slice('@tanstack/charts/'.length)
|
|
1022
|
+
if (source !== '@tanstack/charts' && isAllowedChartEngineSubpath(subpath)) return
|
|
1023
|
+
context.report({ node, message: CHART_ENGINE_MESSAGE })
|
|
956
1024
|
}
|
|
957
1025
|
|
|
958
1026
|
return boundaryVisitor(reportFor)
|
|
959
1027
|
},
|
|
960
1028
|
}
|
|
961
1029
|
|
|
962
|
-
// Rule 6 —
|
|
963
|
-
//
|
|
964
|
-
//
|
|
965
|
-
|
|
966
|
-
const
|
|
1030
|
+
// Rule 6 — chart-host-single (V2-SPEC § 9.8): `@tanstack/charts/react` is imported by
|
|
1031
|
+
// `packages/basalt-ui/src/charts/host.tsx` alone, so measured sizing, legend and state chrome live
|
|
1032
|
+
// in one component. Repo-local only, like token-layer-boundary — never in the consumer preset.
|
|
1033
|
+
const CHART_HOST_FILE = 'packages/basalt-ui/src/charts/host.tsx'
|
|
1034
|
+
const CHART_HOST_MESSAGE =
|
|
1035
|
+
'@tanstack/charts/react may only be imported by ' +
|
|
1036
|
+
CHART_HOST_FILE +
|
|
1037
|
+
' — render charts through the host. (basalt/chart-host-single)'
|
|
1038
|
+
// Ships: repo-local only
|
|
1039
|
+
const chartHostSingle = {
|
|
967
1040
|
meta: {
|
|
968
|
-
type: '
|
|
1041
|
+
type: 'problem',
|
|
969
1042
|
docs: {
|
|
970
|
-
description: '
|
|
1043
|
+
description: 'Allow @tanstack/charts/react only in packages/basalt-ui/src/charts/host.tsx.',
|
|
971
1044
|
},
|
|
972
1045
|
schema: [],
|
|
973
1046
|
},
|
|
974
1047
|
create(context) {
|
|
1048
|
+
const filename = getFilename(context)
|
|
1049
|
+
// Scoped to basalt source: elsewhere `chart-engine-boundary` already bans /react, one diagnostic.
|
|
1050
|
+
const exempt =
|
|
1051
|
+
!inBasaltSource(filename) || filename.replaceAll('\\', '/').endsWith('/' + CHART_HOST_FILE)
|
|
1052
|
+
|
|
975
1053
|
function reportFor(node, source) {
|
|
976
|
-
if (source !== '
|
|
977
|
-
|
|
1054
|
+
if (typeof source !== 'string' || exempt) return
|
|
1055
|
+
if (source === '@tanstack/charts/react' || source.startsWith('@tanstack/charts/react/')) {
|
|
1056
|
+
context.report({ node, message: CHART_HOST_MESSAGE })
|
|
1057
|
+
}
|
|
978
1058
|
}
|
|
979
1059
|
|
|
980
1060
|
return boundaryVisitor(reportFor)
|
|
@@ -1000,7 +1080,7 @@ const tokenLayerBoundary = {
|
|
|
1000
1080
|
const inCharts = hasPathSegment(filename, 'charts')
|
|
1001
1081
|
const inTokens = hasPathSegment(filename, 'tokens')
|
|
1002
1082
|
|
|
1003
|
-
// Deliberately NO `hasThemeAllow` escape — see
|
|
1083
|
+
// Deliberately NO `hasThemeAllow` escape — see the chart-engine-boundary comment above for why.
|
|
1004
1084
|
function reportFor(node, source) {
|
|
1005
1085
|
if (typeof source !== 'string') return
|
|
1006
1086
|
if (
|
|
@@ -1021,23 +1101,6 @@ const tokenLayerBoundary = {
|
|
|
1021
1101
|
},
|
|
1022
1102
|
}
|
|
1023
1103
|
|
|
1024
|
-
// ── Rule 8 — agent-resume-guard ─────────────────────────────────────────────────────────────────
|
|
1025
|
-
|
|
1026
|
-
const AGENT_RESUME_GUARD_MESSAGE =
|
|
1027
|
-
'Unguarded stream resume — useAgentThreadRuns owns single-consumer discipline and ' +
|
|
1028
|
-
'StrictMode-safe reconnection; a raw resume re-fires on every effect re-run (vercel/ai#7891, no ' +
|
|
1029
|
-
"merged fix). Mark the line 'basalt-agent-allow' if you own the guard. (basalt/agent-resume-guard)"
|
|
1030
|
-
|
|
1031
|
-
/** True for `resumeStream()` and `<anything>.resumeStream()` call expressions. */
|
|
1032
|
-
function isResumeStreamCall(callee) {
|
|
1033
|
-
if (callee.type === 'Identifier') return callee.name === 'resumeStream'
|
|
1034
|
-
return (
|
|
1035
|
-
callee.type === 'MemberExpression' &&
|
|
1036
|
-
callee.property.type === 'Identifier' &&
|
|
1037
|
-
callee.property.name === 'resumeStream'
|
|
1038
|
-
)
|
|
1039
|
-
}
|
|
1040
|
-
|
|
1041
1104
|
/** The `key` of an object Property as a string, covering both Identifier and Literal keys. */
|
|
1042
1105
|
function propertyKeyName(node) {
|
|
1043
1106
|
const key = node.key
|
|
@@ -1046,53 +1109,15 @@ function propertyKeyName(node) {
|
|
|
1046
1109
|
return undefined
|
|
1047
1110
|
}
|
|
1048
1111
|
|
|
1049
|
-
// Deliberately NO `hasThemeAllow` escape — see basalt/visx-boundary's comment for why these
|
|
1050
|
-
// guards use their own token (`basalt-agent-allow`, see the file-header comment) instead.
|
|
1051
|
-
// Ships: error
|
|
1052
|
-
const agentResumeGuard = {
|
|
1053
|
-
meta: {
|
|
1054
|
-
type: 'problem',
|
|
1055
|
-
docs: {
|
|
1056
|
-
description:
|
|
1057
|
-
'Disallow unguarded useChat({ resume: true }) / resumeStream() calls outside useAgentThreadRuns.',
|
|
1058
|
-
},
|
|
1059
|
-
schema: [],
|
|
1060
|
-
},
|
|
1061
|
-
create(context) {
|
|
1062
|
-
return {
|
|
1063
|
-
CallExpression(node) {
|
|
1064
|
-
const callee = node.callee
|
|
1065
|
-
|
|
1066
|
-
if (callee.type === 'Identifier' && callee.name === 'useChat') {
|
|
1067
|
-
const arg0 = node.arguments[0]
|
|
1068
|
-
if (arg0 === undefined || arg0.type !== 'ObjectExpression') return
|
|
1069
|
-
for (const prop of arg0.properties) {
|
|
1070
|
-
if (prop.type !== 'Property') continue
|
|
1071
|
-
if (propertyKeyName(prop) !== 'resume') continue
|
|
1072
|
-
if (prop.value.type !== 'Literal' || prop.value.value !== true) continue
|
|
1073
|
-
if (hasAgentAllow(context, prop)) continue
|
|
1074
|
-
context.report({ node: prop, message: AGENT_RESUME_GUARD_MESSAGE })
|
|
1075
|
-
}
|
|
1076
|
-
return
|
|
1077
|
-
}
|
|
1078
|
-
|
|
1079
|
-
if (!isResumeStreamCall(callee)) return
|
|
1080
|
-
if (hasAgentAllow(context, node)) return
|
|
1081
|
-
context.report({ node, message: AGENT_RESUME_GUARD_MESSAGE })
|
|
1082
|
-
},
|
|
1083
|
-
}
|
|
1084
|
-
},
|
|
1085
|
-
}
|
|
1086
|
-
|
|
1087
1112
|
// ── Rule 9 — agent-no-raw-usechat ───────────────────────────────────────────────────────────────
|
|
1088
1113
|
|
|
1089
1114
|
const AGENT_NO_RAW_USECHAT_MESSAGE =
|
|
1090
|
-
'Raw @ai-sdk/react useChat — use useAgentStream / useAgentThreadRuns over aiSdkTransport ' +
|
|
1115
|
+
'Raw @ai-sdk/react useChat / useCompletion / Chat — use useAgentStream / useAgentThreadRuns over aiSdkTransport ' +
|
|
1091
1116
|
"(unmount abort, supersede guards, single-consumer resume). Mark the line 'basalt-agent-allow' to " +
|
|
1092
1117
|
'opt out. (basalt/agent-no-raw-usechat)'
|
|
1093
1118
|
|
|
1094
1119
|
const RAW_USE_CHAT_SOURCES = new Set(['@ai-sdk/react', 'ai/react'])
|
|
1095
|
-
const RAW_USE_CHAT_NAMES = new Set(['useChat', 'useCompletion'])
|
|
1120
|
+
const RAW_USE_CHAT_NAMES = new Set(['useChat', 'useCompletion', 'Chat'])
|
|
1096
1121
|
|
|
1097
1122
|
// Deliberately NO `hasThemeAllow` escape — see the file-header comment.
|
|
1098
1123
|
// Ships: error
|
|
@@ -1101,11 +1126,18 @@ const agentNoRawUseChat = {
|
|
|
1101
1126
|
type: 'problem',
|
|
1102
1127
|
docs: {
|
|
1103
1128
|
description:
|
|
1104
|
-
'Disallow importing useChat/useCompletion directly from @ai-sdk/react or ai/react.',
|
|
1129
|
+
'Disallow importing useChat/useCompletion/Chat directly from @ai-sdk/react or ai/react.',
|
|
1105
1130
|
},
|
|
1106
1131
|
schema: [],
|
|
1107
1132
|
},
|
|
1108
1133
|
create(context) {
|
|
1134
|
+
const sourceCode = context.sourceCode ?? context.getSourceCode?.()
|
|
1135
|
+
const nameOf = (node) => (node.type === 'Identifier' ? node.name : node.value)
|
|
1136
|
+
const report = (node) => {
|
|
1137
|
+
if (!hasAgentAllow(context, node)) {
|
|
1138
|
+
context.report({ node, message: AGENT_NO_RAW_USECHAT_MESSAGE })
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1109
1141
|
return {
|
|
1110
1142
|
ImportDeclaration(node) {
|
|
1111
1143
|
const source = node.source?.value
|
|
@@ -1117,13 +1149,32 @@ const agentNoRawUseChat = {
|
|
|
1117
1149
|
if (specifier.type !== 'ImportSpecifier') continue
|
|
1118
1150
|
// `import { type useChat } from '…'` — the individual specifier is type-only.
|
|
1119
1151
|
if (specifier.importKind === 'type') continue
|
|
1120
|
-
const
|
|
1121
|
-
const importedName = imported.type === 'Identifier' ? imported.name : imported.value
|
|
1152
|
+
const importedName = nameOf(specifier.imported)
|
|
1122
1153
|
if (typeof importedName !== 'string' || !RAW_USE_CHAT_NAMES.has(importedName)) continue
|
|
1123
|
-
|
|
1124
|
-
|
|
1154
|
+
report(specifier)
|
|
1155
|
+
}
|
|
1156
|
+
},
|
|
1157
|
+
// `import * as AI from '@ai-sdk/react'` then `AI.useChat` / `new AI.Chat`.
|
|
1158
|
+
MemberExpression(node) {
|
|
1159
|
+
if (node.computed || node.object?.type !== 'Identifier') return
|
|
1160
|
+
if (!RAW_USE_CHAT_NAMES.has(node.property?.name)) return
|
|
1161
|
+
const def = soleDefinition(sourceCode, node.object)
|
|
1162
|
+
if (def?.type !== 'ImportBinding' || def.node.type !== 'ImportNamespaceSpecifier') return
|
|
1163
|
+
if (def.parent?.importKind === 'type') return
|
|
1164
|
+
if (!RAW_USE_CHAT_SOURCES.has(def.parent?.source?.value)) return
|
|
1165
|
+
report(node)
|
|
1166
|
+
},
|
|
1167
|
+
// `export { Chat } from '@ai-sdk/react'` and `export * from '@ai-sdk/react'`.
|
|
1168
|
+
ExportNamedDeclaration(node) {
|
|
1169
|
+
if (!RAW_USE_CHAT_SOURCES.has(node.source?.value) || node.exportKind === 'type') return
|
|
1170
|
+
for (const specifier of node.specifiers ?? []) {
|
|
1171
|
+
if (specifier.exportKind === 'type') continue
|
|
1172
|
+
if (RAW_USE_CHAT_NAMES.has(nameOf(specifier.local))) report(specifier)
|
|
1125
1173
|
}
|
|
1126
1174
|
},
|
|
1175
|
+
ExportAllDeclaration(node) {
|
|
1176
|
+
if (RAW_USE_CHAT_SOURCES.has(node.source?.value) && node.exportKind !== 'type') report(node)
|
|
1177
|
+
},
|
|
1127
1178
|
}
|
|
1128
1179
|
},
|
|
1129
1180
|
}
|
|
@@ -1287,7 +1338,7 @@ function aiSdkMajorMessage(consumerMajor) {
|
|
|
1287
1338
|
)
|
|
1288
1339
|
}
|
|
1289
1340
|
|
|
1290
|
-
// Honours `basalt-agent-allow` (NOT `theme-allow`) like
|
|
1341
|
+
// Honours `basalt-agent-allow` (NOT `theme-allow`) like `agent-no-raw-usechat` above — a
|
|
1291
1342
|
// version skew is still a fact about two package.json files, but a written, intentional
|
|
1292
1343
|
// producer/consumer pair (see `doctor`'s `aiMajorSkewReason`) needs a way to say so at the import
|
|
1293
1344
|
// site too, not just be permanently blocked.
|
|
@@ -1323,227 +1374,6 @@ const aiSdkMajor = {
|
|
|
1323
1374
|
},
|
|
1324
1375
|
}
|
|
1325
1376
|
|
|
1326
|
-
// ── Rule 12 — hand-rolled-plot ───────────────────────────────────────────────────────────────────
|
|
1327
|
-
|
|
1328
|
-
/**
|
|
1329
|
-
* The primitives `CartesianChart` assembles. Rendering one of these directly means the file is
|
|
1330
|
-
* re-implementing the plot assembly — margins, scales, axes, cursor, crosshair, tooltip — that the
|
|
1331
|
-
* primitive owns for every other chart, which is exactly how charts drift apart from each other.
|
|
1332
|
-
*/
|
|
1333
|
-
const PLOT_ASSEMBLY_TAGS = new Set([
|
|
1334
|
-
'AxisLeftNumeric',
|
|
1335
|
-
'AxisRightNumeric',
|
|
1336
|
-
'AxisBottomDate',
|
|
1337
|
-
'HoverOverlay',
|
|
1338
|
-
'Crosshair',
|
|
1339
|
-
])
|
|
1340
|
-
|
|
1341
|
-
/**
|
|
1342
|
-
* KNOWN LIMIT, deliberately not closed: a chart drawn out of DOM instead of the chart layer is
|
|
1343
|
-
* structurally invisible here.
|
|
1344
|
-
*
|
|
1345
|
-
* argo has two — a 3×3 Mantine `SimpleGrid` matrix and absolutely-positioned `Box` bullet bars —
|
|
1346
|
-
* that import only `ChartCard` and `VX`, render no basalt primitive, and therefore cannot trip any
|
|
1347
|
-
* tag-name check. The candidate widenings all fail on false positives in a rule that SHIPS to
|
|
1348
|
-
* consumers: adding `ChartTooltipFloat`/`useChartSize` to this set flags `Donut` and `Heatmap`,
|
|
1349
|
-
* which compose them legitimately; "renders `ChartCard` and a raw `<svg>`" flags an inline icon in
|
|
1350
|
-
* a card header. Detecting "this Box grid is a chart" needs intent, not syntax. Naming the gap here
|
|
1351
|
-
* is the honest answer — a noisy rule gets switched off, and then it guards nothing at all.
|
|
1352
|
-
*/
|
|
1353
|
-
|
|
1354
|
-
/**
|
|
1355
|
-
* Composing this means the file is USING the chart system rather than re-implementing it.
|
|
1356
|
-
* `ChartFrame` deliberately does NOT count: composing the frame directly and then hand-assembling
|
|
1357
|
-
* axes and a cursor on top of it IS the drift this rule exists to catch. A genuinely non-single-
|
|
1358
|
-
* plot shape does exactly that — and declares it with a `theme-allow` comment, so the exception is
|
|
1359
|
-
* a decision someone wrote down rather than a default anyone can fall into.
|
|
1360
|
-
*/
|
|
1361
|
-
const PLOT_OWNER_TAGS = new Set(['CartesianChart'])
|
|
1362
|
-
|
|
1363
|
-
/**
|
|
1364
|
-
* Import sources that make a JSX tag one of BASALT's chart primitives rather than a component the
|
|
1365
|
-
* consumer happens to have named `Crosshair`. Matching bare tag names would false-positive on any
|
|
1366
|
-
* app with its own overlay component — and this rule ships to consumers, so that matters.
|
|
1367
|
-
*/
|
|
1368
|
-
const CHART_IMPORT_SOURCE = /(^|\/)basalt-ui\/charts|(^|\/)charts(\/|$)|(^|\.\.\/)primitives\//
|
|
1369
|
-
|
|
1370
|
-
/** Local names imported from a basalt charts entry — the only tags either chart rule considers. */
|
|
1371
|
-
function collectChartImports(node, into) {
|
|
1372
|
-
if (!CHART_IMPORT_SOURCE.test(node.source?.value ?? '')) return
|
|
1373
|
-
for (const spec of node.specifiers ?? []) {
|
|
1374
|
-
if (spec.local?.name !== undefined) into.add(spec.local.name)
|
|
1375
|
-
}
|
|
1376
|
-
}
|
|
1377
|
-
|
|
1378
|
-
const HAND_ROLLED_PLOT_MESSAGE =
|
|
1379
|
-
'Chart assembly primitive rendered without composing CartesianChart — that primitive already ' +
|
|
1380
|
-
'owns the measured margins, both y scales, the axes, grid, shared cursor, crosshair and ' +
|
|
1381
|
-
'tooltip. Hand-assembling them is how two charts stop matching. Compose CartesianChart and ' +
|
|
1382
|
-
'render only marks. A genuinely non-single-plot shape (multi-pane, radial, matrix) composes ' +
|
|
1383
|
-
'ChartFrame instead — declare that with `theme-allow-file hand-rolled-plot — <why>` anywhere in ' +
|
|
1384
|
-
'the file. To waive just THIS node and stay policed on the rest, write `theme-allow ' +
|
|
1385
|
-
'hand-rolled-plot — <why>` on its own line above it. (basalt/hand-rolled-plot)'
|
|
1386
|
-
|
|
1387
|
-
// Ships: error
|
|
1388
|
-
const handRolledPlot = {
|
|
1389
|
-
meta: {
|
|
1390
|
-
type: 'suggestion',
|
|
1391
|
-
docs: {
|
|
1392
|
-
description:
|
|
1393
|
-
'Disallow assembling a cartesian plot by hand instead of composing CartesianChart.',
|
|
1394
|
-
},
|
|
1395
|
-
schema: [],
|
|
1396
|
-
},
|
|
1397
|
-
create(context) {
|
|
1398
|
-
if (isTestFile(context)) return {}
|
|
1399
|
-
// File-scoped, not node-scoped: the verdict depends on whether a plot OWNER appears anywhere
|
|
1400
|
-
// in the file, which is only known once the whole file has been walked.
|
|
1401
|
-
const assemblyNodes = []
|
|
1402
|
-
const chartImports = new Set()
|
|
1403
|
-
let ownsPlot = false
|
|
1404
|
-
let definesOwner = false
|
|
1405
|
-
|
|
1406
|
-
// A rule that says "compose CartesianChart" cannot fire inside the module that DEFINES
|
|
1407
|
-
// CartesianChart — that file is the assembly. Detected by declaration rather than by path so
|
|
1408
|
-
// it stays honest: this is definitional, not basalt exempting itself for convenience (the
|
|
1409
|
-
// self-exemption habit is what let the 1.4.0 regression reach consumers — see docs/STATUS.md).
|
|
1410
|
-
const notesOwnerDefinition = (name) => {
|
|
1411
|
-
if (name === 'CartesianChart') definesOwner = true
|
|
1412
|
-
}
|
|
1413
|
-
|
|
1414
|
-
return {
|
|
1415
|
-
ImportDeclaration(node) {
|
|
1416
|
-
collectChartImports(node, chartImports)
|
|
1417
|
-
},
|
|
1418
|
-
FunctionDeclaration(node) {
|
|
1419
|
-
notesOwnerDefinition(node.id?.name)
|
|
1420
|
-
},
|
|
1421
|
-
VariableDeclarator(node) {
|
|
1422
|
-
notesOwnerDefinition(node.id?.name)
|
|
1423
|
-
},
|
|
1424
|
-
JSXOpeningElement(node) {
|
|
1425
|
-
const tagName = node.name?.name
|
|
1426
|
-
if (PLOT_OWNER_TAGS.has(tagName)) ownsPlot = true
|
|
1427
|
-
else if (PLOT_ASSEMBLY_TAGS.has(tagName)) assemblyNodes.push(node)
|
|
1428
|
-
},
|
|
1429
|
-
'Program:exit'() {
|
|
1430
|
-
if (ownsPlot || definesOwner) return
|
|
1431
|
-
// The CHECK stays file-scoped — forced by the shape of the correct pattern:
|
|
1432
|
-
// `<CartesianChart>{() => <AxisLeftNumeric …/>}</CartesianChart>` puts the axis inside a
|
|
1433
|
-
// render-prop arrow, a different function from the component that owns the chart, so
|
|
1434
|
-
// component scope would flag the canonical usage.
|
|
1435
|
-
//
|
|
1436
|
-
// The SUPPRESSION no longer is. Reporting only the first node and testing the waiver there
|
|
1437
|
-
// meant one comment — any comment, about anything — bought a whole file permanent immunity:
|
|
1438
|
-
// every axis, overlay and crosshair added to that file afterwards went unreported forever
|
|
1439
|
-
// (linewatch: three files, one 604 lines). Now every assembly node is reported and waived on
|
|
1440
|
-
// its own, EXCEPT when the file carries a written declaration — a `theme-allow` that names
|
|
1441
|
-
// `basalt/hand-rolled-plot` and gives a reason. That is the shape a genuinely non-single-plot
|
|
1442
|
-
// kind already uses (`DualPanel`), and it is the difference between a line waiver and a
|
|
1443
|
-
// decision about the file.
|
|
1444
|
-
const nodes = assemblyNodes.filter((node) => chartImports.has(node.name?.name))
|
|
1445
|
-
if (nodes.length === 0) return
|
|
1446
|
-
if (hasFileDeclaration(context, 'hand-rolled-plot')) return
|
|
1447
|
-
for (const node of nodes) {
|
|
1448
|
-
if (hasThemeAllow(context, node, 'hand-rolled-plot')) continue
|
|
1449
|
-
context.report({ node, message: HAND_ROLLED_PLOT_MESSAGE })
|
|
1450
|
-
}
|
|
1451
|
-
},
|
|
1452
|
-
}
|
|
1453
|
-
},
|
|
1454
|
-
}
|
|
1455
|
-
|
|
1456
|
-
// ── Rule 13 — chart-legend-literal ───────────────────────────────────────────────────────────────
|
|
1457
|
-
|
|
1458
|
-
const CHART_LEGEND_LITERAL_MESSAGE =
|
|
1459
|
-
'Hand-authored ChartLegend items — the legend must be DERIVED from the same `series` array the ' +
|
|
1460
|
-
'chart draws (deriveLegend, or just let ChartFrame/CartesianChart do it). A hand-written list ' +
|
|
1461
|
-
'is a second source of truth that silently goes stale: it keeps naming a series after the plot ' +
|
|
1462
|
-
'stops drawing it. (basalt/chart-legend-literal)'
|
|
1463
|
-
|
|
1464
|
-
/**
|
|
1465
|
-
* Does this expression derive from the chart's own `series`?
|
|
1466
|
-
*
|
|
1467
|
-
* The contract is derivation from `series` — not derivation from *an* array. `items={refLines.map(
|
|
1468
|
-
* (r) => ({ key, label, color, shape }))}` and `items={PACE_ZONES.map(…)}` are hand-authored
|
|
1469
|
-
* legends by any reading: every field is written at the call site, and the list can name a band the
|
|
1470
|
-
* plot no longer draws exactly like a `[...]` literal can. Walks the base of a member/call chain so
|
|
1471
|
-
* `visibleSeries.filter(…).map(…)` and `deriveLegend(series)` both read as derived.
|
|
1472
|
-
*/
|
|
1473
|
-
function derivesFromSeries(node, depth = 0) {
|
|
1474
|
-
if (node === null || node === undefined || depth > 8) return false
|
|
1475
|
-
if (node.type === 'Identifier') return /series$/i.test(node.name ?? '')
|
|
1476
|
-
if (node.type === 'MemberExpression') {
|
|
1477
|
-
return derivesFromSeries(node.object, depth + 1) || derivesFromSeries(node.property, depth + 1)
|
|
1478
|
-
}
|
|
1479
|
-
if (node.type === 'CallExpression') {
|
|
1480
|
-
if (node.callee?.type === 'Identifier' && node.callee.name === 'deriveLegend') return true
|
|
1481
|
-
return (
|
|
1482
|
-
derivesFromSeries(node.callee, depth + 1) ||
|
|
1483
|
-
(node.arguments ?? []).some((arg) => derivesFromSeries(arg, depth + 1))
|
|
1484
|
-
)
|
|
1485
|
-
}
|
|
1486
|
-
return false
|
|
1487
|
-
}
|
|
1488
|
-
|
|
1489
|
-
/** `X.map(…)` — the derivation-shaped form that is only derivation when `X` is the series. */
|
|
1490
|
-
function isMapCall(node) {
|
|
1491
|
-
return (
|
|
1492
|
-
node?.type === 'CallExpression' &&
|
|
1493
|
-
node.callee?.type === 'MemberExpression' &&
|
|
1494
|
-
node.callee.property?.name === 'map'
|
|
1495
|
-
)
|
|
1496
|
-
}
|
|
1497
|
-
|
|
1498
|
-
// Ships: error
|
|
1499
|
-
const chartLegendLiteral = {
|
|
1500
|
-
meta: {
|
|
1501
|
-
type: 'suggestion',
|
|
1502
|
-
docs: { description: 'Require ChartLegend items to be derived from the series descriptor.' },
|
|
1503
|
-
schema: [],
|
|
1504
|
-
},
|
|
1505
|
-
create(context) {
|
|
1506
|
-
if (isTestFile(context)) return {}
|
|
1507
|
-
const chartImports = new Set()
|
|
1508
|
-
|
|
1509
|
-
return {
|
|
1510
|
-
ImportDeclaration(node) {
|
|
1511
|
-
collectChartImports(node, chartImports)
|
|
1512
|
-
},
|
|
1513
|
-
JSXOpeningElement(node) {
|
|
1514
|
-
if (node.name?.name !== 'ChartLegend' || !chartImports.has('ChartLegend')) return
|
|
1515
|
-
const items = node.attributes?.find(
|
|
1516
|
-
(attr) => attr.type === 'JSXAttribute' && attr.name?.name === 'items',
|
|
1517
|
-
)
|
|
1518
|
-
const expression = items?.value?.expression
|
|
1519
|
-
// The `.map()` half — a list built by mapping over something that is NOT the series. See
|
|
1520
|
-
// `derivesFromSeries`. Reported under the same id as the array-literal half because it is
|
|
1521
|
-
// the same defect (a second source of truth for the legend), just written as a call.
|
|
1522
|
-
if (isMapCall(expression) && !derivesFromSeries(expression.callee?.object)) {
|
|
1523
|
-
if (hasThemeAllow(context, node, 'chart-legend-literal')) return
|
|
1524
|
-
context.report({ node, message: CHART_LEGEND_LITERAL_MESSAGE })
|
|
1525
|
-
return
|
|
1526
|
-
}
|
|
1527
|
-
if (expression?.type !== 'ArrayExpression') return
|
|
1528
|
-
// Only a list that is ENTIRELY object literals is a hand-authored legend. An array that
|
|
1529
|
-
// spreads or calls something (`[...deriveLegend(series), note]`) is derived-and-extended,
|
|
1530
|
-
// which is legitimate — flagging it would push people to disable the rule.
|
|
1531
|
-
//
|
|
1532
|
-
// Known and accepted gap: hoisting the literal to a `const` one line up
|
|
1533
|
-
// (`const items = [...]; <ChartLegend items={items} />`) reads as an Identifier here and
|
|
1534
|
-
// is not flagged. Following that would mean local flow analysis for a bypass nobody
|
|
1535
|
-
// reaches by accident; the rule targets the shape people actually write.
|
|
1536
|
-
const allObjectLiterals =
|
|
1537
|
-
expression.elements.length > 0 &&
|
|
1538
|
-
expression.elements.every((el) => el?.type === 'ObjectExpression')
|
|
1539
|
-
if (!allObjectLiterals) return
|
|
1540
|
-
if (hasThemeAllow(context, node, 'chart-legend-literal')) return
|
|
1541
|
-
context.report({ node, message: CHART_LEGEND_LITERAL_MESSAGE })
|
|
1542
|
-
},
|
|
1543
|
-
}
|
|
1544
|
-
},
|
|
1545
|
-
}
|
|
1546
|
-
|
|
1547
1377
|
// ── Rule 14 — shadow-basalt-export ───────────────────────────────────────────────────────────────
|
|
1548
1378
|
|
|
1549
1379
|
/** The package root — one directory above `configs/`, repo-locally AND inside node_modules. */
|
|
@@ -1649,7 +1479,7 @@ const BASALT_EXPORTS = (() => {
|
|
|
1649
1479
|
* barrel read, this table is hand-maintained by construction: there is no artifact to derive
|
|
1650
1480
|
* "someone else's word for `Section`" from. Keep it in step with `docs/CONTROLS-SPEC.md` §6.
|
|
1651
1481
|
*
|
|
1652
|
-
*
|
|
1482
|
+
* A name is evidence, never proof — which is why the table is exact, not fuzzy.
|
|
1653
1483
|
*/
|
|
1654
1484
|
const SHADOW_ALIASES = {
|
|
1655
1485
|
PageSection: 'Section',
|
|
@@ -1759,14 +1589,16 @@ function shadowBasaltExportMessage(name) {
|
|
|
1759
1589
|
* The cheapest possible detector for the whole absorption class: argo shipped its own `EmptyState`
|
|
1760
1590
|
* (a full duplicate of the shipped one) and rb a local `StatCard`, and both passed `check-theme`,
|
|
1761
1591
|
* `oxlint` and `doctor` clean — off-palette code fails, a forked component that uses the correct
|
|
1762
|
-
* tokens does not. `
|
|
1763
|
-
*
|
|
1592
|
+
* tokens does not. `error` since 2.0 (the major is the channel); a genuinely different `Composer`
|
|
1593
|
+
* takes a `theme-allow shadow-basalt-export — <why>`.
|
|
1764
1594
|
*/
|
|
1765
|
-
// Ships:
|
|
1595
|
+
// Ships: error
|
|
1766
1596
|
const shadowBasaltExport = {
|
|
1767
1597
|
meta: {
|
|
1768
1598
|
type: 'suggestion',
|
|
1769
|
-
docs: {
|
|
1599
|
+
docs: {
|
|
1600
|
+
description: 'Disallow a local component whose name collides with a basalt-ui export.',
|
|
1601
|
+
},
|
|
1770
1602
|
schema: [],
|
|
1771
1603
|
},
|
|
1772
1604
|
create(context) {
|
|
@@ -1878,7 +1710,7 @@ const HAND_ROLLED_SHELL_MESSAGE =
|
|
|
1878
1710
|
'Application shell assembled by hand — AppShell parts / Burger / NavLink in a file that does not ' +
|
|
1879
1711
|
'render BasaltShell. The shell is the framework: BasaltShell owns the sidebar, the mobile tab ' +
|
|
1880
1712
|
'bar and its More sheet, breadcrumbs, the page header and the collapse state, all driven by ONE ' +
|
|
1881
|
-
'typed nav definition (defineNav +
|
|
1713
|
+
'typed nav definition (defineNav + BasaltShell nav={NAV}). A hand-rolled AppShell + burger drawer is the exact ' +
|
|
1882
1714
|
'layer the 1.19 nav contract replaces, and no palette guard can see it — it uses the right ' +
|
|
1883
1715
|
'tokens. Declare a deliberate exception with `theme-allow-file hand-rolled-shell — <why>`. ' +
|
|
1884
1716
|
'(basalt/hand-rolled-shell)'
|
|
@@ -2399,20 +2231,13 @@ function hasAncestorTag(node, tags) {
|
|
|
2399
2231
|
* {@link slotOwnerBinding} does: whether that `PanelRow` is basalt's is answered by the import list,
|
|
2400
2232
|
* which is only complete at `Program:exit`, while the ancestry is only walkable during the visit.
|
|
2401
2233
|
*
|
|
2402
|
-
* `tags` is OPTIONAL, and omitting it is the second real use rather than a convenience: a rule
|
|
2403
|
-
* whose ancestor set is not known until the import list is complete cannot filter by tag NAME at
|
|
2404
|
-
* all (`RouterProvider as Router` is an ordinary thing to write), so it takes every binding and
|
|
2405
|
-
* resolves them at exit. `provider-above-router` is that caller.
|
|
2406
2234
|
*/
|
|
2407
2235
|
function ancestorTagBindings(node, tags) {
|
|
2408
2236
|
const found = []
|
|
2409
2237
|
let current = node.parent
|
|
2410
2238
|
for (let depth = 0; current !== null && current !== undefined; depth++) {
|
|
2411
2239
|
if (depth > ANCESTRY_MAX_DEPTH || current.type === 'Program') break
|
|
2412
|
-
if (
|
|
2413
|
-
current.type === 'JSXElement' &&
|
|
2414
|
-
(tags === undefined || tags.has(jsxTagName(current.openingElement?.name) ?? ''))
|
|
2415
|
-
) {
|
|
2240
|
+
if (current.type === 'JSXElement' && tags.has(jsxTagName(current.openingElement?.name) ?? '')) {
|
|
2416
2241
|
const root = jsxRootName(current.openingElement?.name)
|
|
2417
2242
|
if (root !== undefined) found.push(root)
|
|
2418
2243
|
}
|
|
@@ -2911,166 +2736,6 @@ const controlSizeLiteral = {
|
|
|
2911
2736
|
},
|
|
2912
2737
|
}
|
|
2913
2738
|
|
|
2914
|
-
// ── Rule 19 — page-bar-budget ───────────────────────────────────────────────────────────────────
|
|
2915
|
-
|
|
2916
|
-
/** `PageBar.actions.secondary` — 4, because the one `primary` makes 5 (law C6). */
|
|
2917
|
-
const PAGE_BAR_SECONDARY_BUDGET = 4
|
|
2918
|
-
/** A `Section`'s `actions` — 3 (law C6). */
|
|
2919
|
-
const SECTION_ACTION_BUDGET = 3
|
|
2920
|
-
|
|
2921
|
-
const PAGE_BAR_BUDGET_MESSAGES = {
|
|
2922
|
-
duplicate:
|
|
2923
|
-
'Second PageBar in the same returned tree — a page has exactly one page bar, and row 1 portals ' +
|
|
2924
|
-
'into the app shell header, so two bars mounted TOGETHER race for the same node (law C6). An ' +
|
|
2925
|
-
'early-return loading state and two page components in one file each get their own tree and ' +
|
|
2926
|
-
'never report. (basalt/page-bar-budget)',
|
|
2927
|
-
secondary:
|
|
2928
|
-
`More than ${PAGE_BAR_SECONDARY_BUDGET} secondary PageBar actions — the bar holds ≤5 entries ` +
|
|
2929
|
-
'including the one `primary`, and everything past that folds into the `More` menu basalt ' +
|
|
2930
|
-
'computes from the typed BarAction[] (laws C6/C7). Move the rest into a `kind: "menu"` entry. ' +
|
|
2931
|
-
'(basalt/page-bar-budget)',
|
|
2932
|
-
section:
|
|
2933
|
-
`More than ${SECTION_ACTION_BUDGET} Section actions — a section header holds ≤3 (law C6). ` +
|
|
2934
|
-
'(basalt/page-bar-budget)',
|
|
2935
|
-
filled:
|
|
2936
|
-
'Second filled Button/ActionIcon inside one slot — a home has exactly one primary action, and ' +
|
|
2937
|
-
'two filled buttons side by side name neither (law C6). A filled Badge/ThemeIcon (a count) and ' +
|
|
2938
|
-
"an overlay's own submit button are not actions in the bar and never report. " +
|
|
2939
|
-
'(basalt/page-bar-budget)',
|
|
2940
|
-
}
|
|
2941
|
-
|
|
2942
|
-
/** The two tags a `variant="filled"` makes a PRIMARY ACTION — a Badge is a count, not an action. */
|
|
2943
|
-
const PRIMARY_ACTION_TAGS = new Set(['Button', 'ActionIcon'])
|
|
2944
|
-
|
|
2945
|
-
/**
|
|
2946
|
-
* The JSX TREE `node` is returned in — the nearest `ReturnStatement`, concise arrow body, or
|
|
2947
|
-
* `const` binding, whichever the walk reaches first.
|
|
2948
|
-
*
|
|
2949
|
-
* This is what "two PageBars race for the same node" actually requires: they must MOUNT together.
|
|
2950
|
-
* Counting per FILE made an `if (!data) return <PageBar title="Jobs"/>` loading state, and two page
|
|
2951
|
-
* components exported from one file, an `error` on code that never renders two bars — with a
|
|
2952
|
-
* `theme-allow` on correct code as the only escape.
|
|
2953
|
-
*/
|
|
2954
|
-
function enclosingReturnedTree(node) {
|
|
2955
|
-
let current = node.parent
|
|
2956
|
-
for (let depth = 0; current !== null && current !== undefined; depth++) {
|
|
2957
|
-
if (depth > ANCESTRY_MAX_DEPTH || current.type === 'Program') return undefined
|
|
2958
|
-
if (current.type === 'ReturnStatement' || current.type === 'VariableDeclarator') return current
|
|
2959
|
-
if (current.type === 'ArrowFunctionExpression' && current.body?.type !== 'BlockStatement') {
|
|
2960
|
-
return current
|
|
2961
|
-
}
|
|
2962
|
-
current = current.parent
|
|
2963
|
-
}
|
|
2964
|
-
return undefined
|
|
2965
|
-
}
|
|
2966
|
-
|
|
2967
|
-
/** The `actions` attribute of a JSXOpeningElement, unwrapped, or undefined. */
|
|
2968
|
-
function actionsAttributeValue(node) {
|
|
2969
|
-
for (const attr of node.attributes ?? []) {
|
|
2970
|
-
if (attr.type !== 'JSXAttribute' || attr.name?.name !== 'actions') continue
|
|
2971
|
-
return { attr, value: unwrapExpressionContainer(attr.value) }
|
|
2972
|
-
}
|
|
2973
|
-
return undefined
|
|
2974
|
-
}
|
|
2975
|
-
|
|
2976
|
-
/**
|
|
2977
|
-
* The four countable halves of C6. `error` from the start: every one of them is arithmetic over a
|
|
2978
|
-
* literal the file wrote down, with no heuristic and no layout intent to guess at.
|
|
2979
|
-
*/
|
|
2980
|
-
// Ships: error
|
|
2981
|
-
const pageBarBudget = {
|
|
2982
|
-
meta: {
|
|
2983
|
-
type: 'problem',
|
|
2984
|
-
docs: { description: 'Enforce the PageBar / Section action budgets and the single primary.' },
|
|
2985
|
-
schema: [],
|
|
2986
|
-
},
|
|
2987
|
-
create(context) {
|
|
2988
|
-
if (isTestFile(context)) return {}
|
|
2989
|
-
const slots = createSlotContext(context)
|
|
2990
|
-
const mantineImports = new Map()
|
|
2991
|
-
const pageBars = []
|
|
2992
|
-
const findings = []
|
|
2993
|
-
/** Slot attribute node → its owner binding + the `variant="filled"` attributes inside it. */
|
|
2994
|
-
const filledPerSlot = new Map()
|
|
2995
|
-
|
|
2996
|
-
return {
|
|
2997
|
-
ImportDeclaration(node) {
|
|
2998
|
-
collectMantineImports(node, mantineImports)
|
|
2999
|
-
slots.noteImport(node)
|
|
3000
|
-
},
|
|
3001
|
-
JSXOpeningElement(node) {
|
|
3002
|
-
const tag = jsxTagName(node.name)
|
|
3003
|
-
const owner = jsxRootName(node.name)
|
|
3004
|
-
if (tag === 'PageBar') {
|
|
3005
|
-
pageBars.push({ node, owner, tree: enclosingReturnedTree(node) })
|
|
3006
|
-
const actions = actionsAttributeValue(node)
|
|
3007
|
-
if (actions?.value?.type === 'ObjectExpression') {
|
|
3008
|
-
for (const prop of actions.value.properties) {
|
|
3009
|
-
if (prop.type !== 'Property' || propertyKeyName(prop) !== 'secondary') continue
|
|
3010
|
-
if (prop.value?.type !== 'ArrayExpression') continue
|
|
3011
|
-
if (prop.value.elements.length <= PAGE_BAR_SECONDARY_BUDGET) continue
|
|
3012
|
-
findings.push({ node: prop, owner, message: PAGE_BAR_BUDGET_MESSAGES.secondary })
|
|
3013
|
-
}
|
|
3014
|
-
}
|
|
3015
|
-
return
|
|
3016
|
-
}
|
|
3017
|
-
if (tag !== 'Section') return
|
|
3018
|
-
const actions = actionsAttributeValue(node)
|
|
3019
|
-
if (actions?.value?.type !== 'ArrayExpression') return
|
|
3020
|
-
if (actions.value.elements.length <= SECTION_ACTION_BUDGET) return
|
|
3021
|
-
findings.push({ node: actions.attr, owner, message: PAGE_BAR_BUDGET_MESSAGES.section })
|
|
3022
|
-
},
|
|
3023
|
-
// The object-spread spelling of a slot — see {@link spreadSlotOwnerBinding}. `slots.note`
|
|
3024
|
-
// accepts either node shape; feeding both is what keeps ONE home model across the lanes.
|
|
3025
|
-
Property(node) {
|
|
3026
|
-
slots.note(node)
|
|
3027
|
-
},
|
|
3028
|
-
JSXAttribute(node) {
|
|
3029
|
-
slots.note(node)
|
|
3030
|
-
if (node.name?.name !== 'variant') return
|
|
3031
|
-
const value = unwrapExpressionContainer(node.value)
|
|
3032
|
-
if (!isStringLiteral(value) || value.value !== 'filled') return
|
|
3033
|
-
// An overlay's own submit button is not an action in the bar — see hostedInsideSlot.
|
|
3034
|
-
if (hostedInsideSlot(node)) return
|
|
3035
|
-
const element = node.parent
|
|
3036
|
-
if (element === null || element === undefined || element.type !== 'JSXOpeningElement')
|
|
3037
|
-
return
|
|
3038
|
-
const slot = enclosingSlotAttribute(node)
|
|
3039
|
-
if (slot === undefined) return
|
|
3040
|
-
const entry = filledPerSlot.get(slot) ?? { owner: slotOwnerBinding(slot), written: [] }
|
|
3041
|
-
entry.written.push({ node, name: element.name })
|
|
3042
|
-
filledPerSlot.set(slot, entry)
|
|
3043
|
-
},
|
|
3044
|
-
'Program:exit'() {
|
|
3045
|
-
/** Returned tree → the PageBars mounted in it. Two in one tree mount together; see C6. */
|
|
3046
|
-
const barsPerTree = new Map()
|
|
3047
|
-
for (const bar of pageBars) {
|
|
3048
|
-
if (!slots.isHome(bar.owner)) continue
|
|
3049
|
-
barsPerTree.set(bar.tree, [...(barsPerTree.get(bar.tree) ?? []), bar])
|
|
3050
|
-
}
|
|
3051
|
-
for (const bars of barsPerTree.values()) {
|
|
3052
|
-
for (const { node, owner } of bars.slice(1)) {
|
|
3053
|
-
findings.push({ node, owner, message: PAGE_BAR_BUDGET_MESSAGES.duplicate })
|
|
3054
|
-
}
|
|
3055
|
-
}
|
|
3056
|
-
for (const { owner, written } of filledPerSlot.values()) {
|
|
3057
|
-
const actions = written.filter(({ name }) =>
|
|
3058
|
-
PRIMARY_ACTION_TAGS.has(resolveMantineTag(name, mantineImports) ?? ''),
|
|
3059
|
-
)
|
|
3060
|
-
for (const { node } of actions.slice(1)) {
|
|
3061
|
-
findings.push({ node, owner, message: PAGE_BAR_BUDGET_MESSAGES.filled })
|
|
3062
|
-
}
|
|
3063
|
-
}
|
|
3064
|
-
for (const { node, owner, message } of findings) {
|
|
3065
|
-
if (!slots.isHome(owner)) continue
|
|
3066
|
-
if (hasThemeAllow(context, node, 'page-bar-budget')) continue
|
|
3067
|
-
context.report({ node, message })
|
|
3068
|
-
}
|
|
3069
|
-
},
|
|
3070
|
-
}
|
|
3071
|
-
},
|
|
3072
|
-
}
|
|
3073
|
-
|
|
3074
2739
|
// ── Rule 20 — in-body-page-title ────────────────────────────────────────────────────────────────
|
|
3075
2740
|
|
|
3076
2741
|
/** Where an `order={1|2}` Title is a DOCUMENT heading rather than a page title. */
|
|
@@ -3462,78 +3127,6 @@ const boundControlOutsideHome = {
|
|
|
3462
3127
|
},
|
|
3463
3128
|
}
|
|
3464
3129
|
|
|
3465
|
-
// ── Rule 25 — provider-above-router ─────────────────────────────────────────────────────────────
|
|
3466
|
-
|
|
3467
|
-
/** `@tanstack/react-router` and its subpaths — never `@tanstack/react-router-devtools`. */
|
|
3468
|
-
const TANSTACK_ROUTER_SOURCE = /^@tanstack\/react-router(?:\/|$)/
|
|
3469
|
-
|
|
3470
|
-
const PROVIDER_ABOVE_ROUTER_MESSAGE =
|
|
3471
|
-
'<BasaltProvider> is rendered INSIDE <RouterProvider> — invert them. Mantine context has to ' +
|
|
3472
|
-
'exist before any route renders, so the mount order is BasaltProvider → the data layer → ' +
|
|
3473
|
-
'RouterProvider (agent/rules/basalt-mantine.md). A route that renders a themed component ' +
|
|
3474
|
-
'before the provider mounts reads Mantine defaults, not the basalt theme. ' +
|
|
3475
|
-
'(basalt/provider-above-router)'
|
|
3476
|
-
|
|
3477
|
-
/**
|
|
3478
|
-
* `BasaltProvider` mounted BELOW a TanStack `RouterProvider` — F5, the first of the three
|
|
3479
|
-
* `basalt-mantine.md` laws that shipped under a `not guarded: —` banner.
|
|
3480
|
-
*
|
|
3481
|
-
* Both tags are provenance-gated: `BasaltProvider` through {@link collectBasaltImportMap} (so a
|
|
3482
|
-
* consumer's own component of that name is not this one) and `RouterProvider` through an
|
|
3483
|
-
* `@tanstack/react-router` import, by its LOCAL binding rather than its written name. The inverse
|
|
3484
|
-
* nesting — the correct one — is not a finding, and neither is either tag alone.
|
|
3485
|
-
*
|
|
3486
|
-
* **STATIC JSX ancestry only, and that is the whole heuristic.** The two providers have to be
|
|
3487
|
-
* written in one JSX tree in one file for this to see anything. The common real shape (`main.tsx`
|
|
3488
|
-
* renders `<BasaltProvider><App/></BasaltProvider>` and `App` renders `<RouterProvider/>`) is
|
|
3489
|
-
* INVERTED cross-file and correct; the inverted-and-wrong cross-file shape is equally invisible.
|
|
3490
|
-
* A rule that followed the composition would need the module graph, which an oxlint rule does not
|
|
3491
|
-
* have — so this catches the one-file case and the law stays advisory for the rest. That is why it
|
|
3492
|
-
* lands at `warn`.
|
|
3493
|
-
*/
|
|
3494
|
-
// Ships: error
|
|
3495
|
-
const providerAboveRouter = {
|
|
3496
|
-
meta: {
|
|
3497
|
-
type: 'suggestion',
|
|
3498
|
-
docs: { description: 'Warn on a BasaltProvider rendered inside a TanStack RouterProvider.' },
|
|
3499
|
-
schema: [],
|
|
3500
|
-
},
|
|
3501
|
-
create(context) {
|
|
3502
|
-
if (isTestFile(context)) return {}
|
|
3503
|
-
const ownTree = isBasaltOwnSource(getFilename(context))
|
|
3504
|
-
const basalt = new Map()
|
|
3505
|
-
const routers = new Set()
|
|
3506
|
-
const candidates = []
|
|
3507
|
-
|
|
3508
|
-
return {
|
|
3509
|
-
ImportDeclaration(node) {
|
|
3510
|
-
collectBasaltImportMap(node, basalt, ownTree)
|
|
3511
|
-
const source = node.source?.value
|
|
3512
|
-
if (typeof source !== 'string' || !TANSTACK_ROUTER_SOURCE.test(source)) return
|
|
3513
|
-
for (const spec of node.specifiers ?? []) {
|
|
3514
|
-
const imported = spec.imported?.name ?? spec.imported?.value
|
|
3515
|
-
if (imported === 'RouterProvider' && typeof spec.local?.name === 'string') {
|
|
3516
|
-
routers.add(spec.local.name)
|
|
3517
|
-
}
|
|
3518
|
-
}
|
|
3519
|
-
},
|
|
3520
|
-
JSXOpeningElement(node) {
|
|
3521
|
-
const root = jsxRootName(node.name)
|
|
3522
|
-
if (root === undefined || !basalt.has(root)) return
|
|
3523
|
-
candidates.push({ node, root, ancestors: ancestorTagBindings(node) })
|
|
3524
|
-
},
|
|
3525
|
-
'Program:exit'() {
|
|
3526
|
-
for (const { node, root, ancestors } of candidates) {
|
|
3527
|
-
if (basalt.get(root) !== 'BasaltProvider') continue
|
|
3528
|
-
if (!ancestors.some((name) => routers.has(name))) continue
|
|
3529
|
-
if (hasThemeAllow(context, node, 'provider-above-router')) continue
|
|
3530
|
-
context.report({ node, message: PROVIDER_ABOVE_ROUTER_MESSAGE })
|
|
3531
|
-
}
|
|
3532
|
-
},
|
|
3533
|
-
}
|
|
3534
|
-
},
|
|
3535
|
-
}
|
|
3536
|
-
|
|
3537
3130
|
// Rule 27 — query-dual-import — RETIRED. 1.29.0 dropped every raw TanStack re-export from
|
|
3538
3131
|
// `basalt-ui/query` (the subpath itself is gone), so "dual-importing `@tanstack/react-query`
|
|
3539
3132
|
// beside `basalt-ui/query`" is no longer a thing a consumer can even do — a consumer MUST import
|
|
@@ -3615,210 +3208,6 @@ const queryFnUnwrap = {
|
|
|
3615
3208
|
},
|
|
3616
3209
|
}
|
|
3617
3210
|
|
|
3618
|
-
// ── Rule 29 — deprecated-export ─────────────────────────────────────────────────────────────────
|
|
3619
|
-
|
|
3620
|
-
/**
|
|
3621
|
-
* @typedef {{
|
|
3622
|
-
* subpath: string
|
|
3623
|
-
* name: string
|
|
3624
|
-
* prop?: string
|
|
3625
|
-
* replacement: string
|
|
3626
|
-
* removeIn: string
|
|
3627
|
-
* removed?: true
|
|
3628
|
-
* fix?: false
|
|
3629
|
-
* }} DeprecatedExport
|
|
3630
|
-
*/
|
|
3631
|
-
|
|
3632
|
-
/**
|
|
3633
|
-
* The deprecation ledger — every export that is `@deprecated` but still SHIPPED, with what to
|
|
3634
|
-
* write instead and the minor it goes away in. B4: majors are banned here, so a rename cannot be
|
|
3635
|
-
* signalled by a version number; the lifecycle is instead "ship the alias, lint the old name, write
|
|
3636
|
-
* the MIGRATING row, remove it a minor later" (`../MAINTAINING.md`, "Deprecation lifecycle").
|
|
3637
|
-
*
|
|
3638
|
-
* A row without `prop` is a NAMED IMPORT from `subpath` — reported on the specifier, with an
|
|
3639
|
-
* autofix that rewrites it to `{ <replacement> as <local> }` so no call site has to move in the
|
|
3640
|
-
* same edit. **That autofix is the row's contract**: only a DROP-IN rename belongs here. A
|
|
3641
|
-
* deprecation whose replacement takes a different shape (`createSearchParamStore` →
|
|
3642
|
-
* `createSearchStore` + a `fields` object) would need the fix suppressed before it could be a row,
|
|
3643
|
-
* and until then it lives in `MIGRATING.md` alone. A MOVE between subpaths (`FormRow` from
|
|
3644
|
-
* `basalt-ui/forms` → `basalt-ui/controls`) is the same case: the fix only rewrites the specifier
|
|
3645
|
-
* inside its own import statement and cannot change its module, so such a row says `fix: false` and
|
|
3646
|
-
* reports without rewriting. A row WITH `prop` is a JSX ATTRIBUTE on the component `name`, imported from `subpath`
|
|
3647
|
-
* — no autofix, because the three connectivity props collapse into ONE object prop and a
|
|
3648
|
-
* per-attribute rewrite would produce three `connectivity=` attributes on one tag.
|
|
3649
|
-
*
|
|
3650
|
-
* **Adding a row is the whole job.** Deprecate the export in its JSDoc, add the row here with a
|
|
3651
|
-
* `removeIn` at least one minor out, add the `MIGRATING.md` row, and this rule starts nudging.
|
|
3652
|
-
* `oxlint-plugin.test.ts` scans every `src/**\/index.ts(x)` barrel for a JSDoc `@deprecated` and
|
|
3653
|
-
* fails when one has no row here, so the barrel half cannot be forgotten. Removing the export is a
|
|
3654
|
-
* separate commit that either deletes the row (the fix is manual from then on — say so in the
|
|
3655
|
-
* removal's MIGRATING row) or keeps it one minor past the removal with `removed: true` and
|
|
3656
|
-
* `removeIn` set to the removing minor, so the message reads "was removed in X" instead of "still
|
|
3657
|
-
* resolves" — and never autofixes, whatever `fix` says. `scripts/check-grace.ts` refuses a release
|
|
3658
|
-
* at or past a `removeIn` whose export still ships — the gap that let `useBreakpoint` outlive its
|
|
3659
|
-
* date twice.
|
|
3660
|
-
*
|
|
3661
|
-
* @type {readonly DeprecatedExport[]}
|
|
3662
|
-
*/
|
|
3663
|
-
export const DEPRECATED_EXPORTS = [
|
|
3664
|
-
// The chart phone-tier API (`resolveChartTier`/`chartTierMetrics`/`useChartTier`/
|
|
3665
|
-
// `useChartTierMetrics`/`ChartTierMetrics`/`phoneChartWidth`) is NOT here: MIGRATING.md's own
|
|
3666
|
-
// Unreleased section states it a hard removal with no grace window (zero consumers), and these
|
|
3667
|
-
// exports are no longer reachable from any public barrel in `src/charts`/`src/tokens` — a row
|
|
3668
|
-
// here would nudge a rewrite to an import that no longer resolves, i.e. "it still resolves today"
|
|
3669
|
-
// would be false.
|
|
3670
|
-
{
|
|
3671
|
-
subpath: 'basalt-ui/forms',
|
|
3672
|
-
name: 'FormSection',
|
|
3673
|
-
replacement: "FormSection from 'basalt-ui/controls'",
|
|
3674
|
-
removeIn: '1.39.0',
|
|
3675
|
-
removed: true,
|
|
3676
|
-
fix: false,
|
|
3677
|
-
},
|
|
3678
|
-
{
|
|
3679
|
-
subpath: 'basalt-ui/forms',
|
|
3680
|
-
name: 'FormRow',
|
|
3681
|
-
replacement: "FormRow from 'basalt-ui/controls'",
|
|
3682
|
-
removeIn: '1.39.0',
|
|
3683
|
-
removed: true,
|
|
3684
|
-
fix: false,
|
|
3685
|
-
},
|
|
3686
|
-
{
|
|
3687
|
-
subpath: 'basalt-ui/forms',
|
|
3688
|
-
name: 'FormGroup',
|
|
3689
|
-
replacement: "FormGroup from 'basalt-ui/controls'",
|
|
3690
|
-
removeIn: '1.39.0',
|
|
3691
|
-
removed: true,
|
|
3692
|
-
fix: false,
|
|
3693
|
-
},
|
|
3694
|
-
{
|
|
3695
|
-
subpath: 'basalt-ui/forms',
|
|
3696
|
-
name: 'FormActions',
|
|
3697
|
-
replacement: "FormActions from 'basalt-ui/controls'",
|
|
3698
|
-
removeIn: '1.39.0',
|
|
3699
|
-
removed: true,
|
|
3700
|
-
fix: false,
|
|
3701
|
-
},
|
|
3702
|
-
{
|
|
3703
|
-
subpath: 'basalt-ui/forms',
|
|
3704
|
-
name: 'useForm',
|
|
3705
|
-
replacement: "useForm from '@mantine/form'",
|
|
3706
|
-
removeIn: '1.39.0',
|
|
3707
|
-
removed: true,
|
|
3708
|
-
fix: false,
|
|
3709
|
-
},
|
|
3710
|
-
{
|
|
3711
|
-
subpath: 'basalt-ui/forms',
|
|
3712
|
-
name: 'schemaResolver',
|
|
3713
|
-
replacement: "schemaResolver from '@mantine/form'",
|
|
3714
|
-
removeIn: '1.39.0',
|
|
3715
|
-
removed: true,
|
|
3716
|
-
fix: false,
|
|
3717
|
-
},
|
|
3718
|
-
{
|
|
3719
|
-
subpath: 'basalt-ui',
|
|
3720
|
-
name: 'useBreakpoint',
|
|
3721
|
-
replacement:
|
|
3722
|
-
'useSizeClass() for shell chrome; a page’s own layout swap is a container decision — a type="container" SimpleGrid/Grid or an @container rule',
|
|
3723
|
-
removeIn: '1.34.0',
|
|
3724
|
-
removed: true,
|
|
3725
|
-
},
|
|
3726
|
-
]
|
|
3727
|
-
|
|
3728
|
-
function deprecatedExportMessage(row) {
|
|
3729
|
-
const what =
|
|
3730
|
-
row.prop === undefined
|
|
3731
|
-
? `\`${row.name}\` from ${row.subpath}`
|
|
3732
|
-
: `\`${row.prop}\` on <${row.name}>`
|
|
3733
|
-
if (row.removed === true) {
|
|
3734
|
-
return (
|
|
3735
|
-
`Removed basalt export — ${what} was removed in ${row.removeIn} and no longer resolves; use ` +
|
|
3736
|
-
`\`${row.replacement}\` instead (packages/basalt-ui/MIGRATING.md carries the row). ` +
|
|
3737
|
-
'(basalt/deprecated-export)'
|
|
3738
|
-
)
|
|
3739
|
-
}
|
|
3740
|
-
return (
|
|
3741
|
-
`Deprecated basalt export — ${what} is superseded by \`${row.replacement}\` and is removed in ` +
|
|
3742
|
-
`${row.removeIn}. It still resolves today, so nothing is broken yet; migrate before that minor ` +
|
|
3743
|
-
'(packages/basalt-ui/MIGRATING.md carries the row). ' +
|
|
3744
|
-
'(basalt/deprecated-export)'
|
|
3745
|
-
)
|
|
3746
|
-
}
|
|
3747
|
-
|
|
3748
|
-
/**
|
|
3749
|
-
* A shipped-but-`@deprecated` export still being imported or written — B4, the sunset half of the
|
|
3750
|
-
* lifecycle whose absence the blueprint audit found (§8): the grace mechanism covers a rule getting
|
|
3751
|
-
* STRICTER, and nothing covered an export going away.
|
|
3752
|
-
*
|
|
3753
|
-
* Permanently `warn` ({@link PLUGIN_RULE_ADVISORY}), for the same reason `shadow-basalt-export` is:
|
|
3754
|
-
* a deprecation is a schedule, not a defect. The code compiles and runs; promoting this to `error`
|
|
3755
|
-
* would break a consumer's build for something that still works, which is the one thing the
|
|
3756
|
-
* no-majors doctrine exists to avoid. The `removeIn` minor is where it becomes an error — by the
|
|
3757
|
-
* export ceasing to exist.
|
|
3758
|
-
*/
|
|
3759
|
-
// Ships: warn (advisory)
|
|
3760
|
-
const deprecatedExport = {
|
|
3761
|
-
meta: {
|
|
3762
|
-
type: 'suggestion',
|
|
3763
|
-
docs: { description: 'Warn on an import or prop that basalt has deprecated but still ships.' },
|
|
3764
|
-
schema: [],
|
|
3765
|
-
fixable: 'code',
|
|
3766
|
-
},
|
|
3767
|
-
create(context) {
|
|
3768
|
-
if (isTestFile(context)) return {}
|
|
3769
|
-
if (isBasaltOwnSource(getFilename(context))) return {}
|
|
3770
|
-
const importRows = DEPRECATED_EXPORTS.filter((row) => row.prop === undefined)
|
|
3771
|
-
const propRows = DEPRECATED_EXPORTS.filter((row) => row.prop !== undefined)
|
|
3772
|
-
const components = new Map()
|
|
3773
|
-
const candidates = []
|
|
3774
|
-
|
|
3775
|
-
return {
|
|
3776
|
-
ImportDeclaration(node) {
|
|
3777
|
-
const source = node.source?.value
|
|
3778
|
-
if (typeof source !== 'string') return
|
|
3779
|
-
for (const spec of node.specifiers ?? []) {
|
|
3780
|
-
const imported = spec.imported?.name ?? spec.imported?.value
|
|
3781
|
-
const local = spec.local?.name
|
|
3782
|
-
if (typeof imported !== 'string' || typeof local !== 'string') continue
|
|
3783
|
-
for (const row of propRows) {
|
|
3784
|
-
if (source === row.subpath && imported === row.name) components.set(local, row.name)
|
|
3785
|
-
}
|
|
3786
|
-
const row = importRows.find((r) => r.subpath === source && r.name === imported)
|
|
3787
|
-
if (row === undefined) continue
|
|
3788
|
-
if (hasThemeAllow(context, spec, 'deprecated-export')) continue
|
|
3789
|
-
context.report({
|
|
3790
|
-
node: spec,
|
|
3791
|
-
message: deprecatedExportMessage(row),
|
|
3792
|
-
// A removed row's replacement is prose for a gone import, never a drop-in name.
|
|
3793
|
-
...(row.fix === false || row.removed === true
|
|
3794
|
-
? {}
|
|
3795
|
-
: { fix: (fixer) => fixer.replaceText(spec, `${row.replacement} as ${local}`) }),
|
|
3796
|
-
})
|
|
3797
|
-
}
|
|
3798
|
-
},
|
|
3799
|
-
JSXAttribute(node) {
|
|
3800
|
-
const name = node.name?.name
|
|
3801
|
-
if (typeof name !== 'string') return
|
|
3802
|
-
const owner = node.parent
|
|
3803
|
-
if (owner === null || owner === undefined || owner.type !== 'JSXOpeningElement') return
|
|
3804
|
-
const root = jsxRootName(owner.name)
|
|
3805
|
-
if (root === undefined) return
|
|
3806
|
-
candidates.push({ node, root, name })
|
|
3807
|
-
},
|
|
3808
|
-
'Program:exit'() {
|
|
3809
|
-
for (const { node, root, name } of candidates) {
|
|
3810
|
-
const component = components.get(root)
|
|
3811
|
-
if (component === undefined) continue
|
|
3812
|
-
const row = propRows.find((r) => r.name === component && r.prop === name)
|
|
3813
|
-
if (row === undefined) continue
|
|
3814
|
-
if (hasThemeAllow(context, node, 'deprecated-export')) continue
|
|
3815
|
-
context.report({ node, message: deprecatedExportMessage(row) })
|
|
3816
|
-
}
|
|
3817
|
-
},
|
|
3818
|
-
}
|
|
3819
|
-
},
|
|
3820
|
-
}
|
|
3821
|
-
|
|
3822
3211
|
// ── Rule 30 — forms-field-key ───────────────────────────────────────────────────────────────────
|
|
3823
3212
|
|
|
3824
3213
|
/** The subpath the forms idiom is imported from — the only provenance this rule accepts. */
|
|
@@ -3872,8 +3261,7 @@ function hasKeyAttribute(opening) {
|
|
|
3872
3261
|
* - `{...field(…)}` — the pre-migration call site. Reported ALWAYS, key or no key, and deliberately
|
|
3873
3262
|
* NOT autofixed: the honest edit rewrites the IMPORT as well as the call site, and a fixer that
|
|
3874
3263
|
* only touched the spread would leave an unresolved `field` behind. This arm is the whole reason
|
|
3875
|
-
* the rule still knows the name — 1.29.0 deleted `field
|
|
3876
|
-
* went empty with it, so a consumer skipping 1.28.x reaches 1.29.x with a hard compile error and
|
|
3264
|
+
* the rule still knows the name — 1.29.0 deleted `field` with no autofix left, so a consumer skipping 1.28.x reaches 1.29.x with a hard compile error and
|
|
3877
3265
|
* nothing but tsc to explain it. The message says that in so many words; it used to describe the
|
|
3878
3266
|
* alias as still shipping and still bundling `key`, which read as "nothing is broken yet".
|
|
3879
3267
|
*
|
|
@@ -3985,10 +3373,9 @@ const NO_IMPORT_META_ENV_MESSAGE =
|
|
|
3985
3373
|
/**
|
|
3986
3374
|
* `import.meta.env` — a MemberExpression whose object is the `import.meta` MetaProperty and whose
|
|
3987
3375
|
* property is `env`. Repo-local only: a CONSUMER's own app code legitimately uses
|
|
3988
|
-
* `import.meta.env.VITE_
|
|
3989
|
-
*
|
|
3990
|
-
*
|
|
3991
|
-
* `token-layer-boundary`, which every package resolves the plugin from and so has to self-scope).
|
|
3376
|
+
* `import.meta.env.VITE_*`, so it is wired in `/.oxlintrc.json` alone and SELF-SCOPES to
|
|
3377
|
+
* `packages/basalt-ui/src/**` by path (like `token-layer-boundary`): the playground, which is
|
|
3378
|
+
* a consumer, keeps its own `import.meta.env`.
|
|
3992
3379
|
*/
|
|
3993
3380
|
// Ships: repo-local only
|
|
3994
3381
|
const noImportMetaEnv = {
|
|
@@ -4000,7 +3387,7 @@ const noImportMetaEnv = {
|
|
|
4000
3387
|
schema: [],
|
|
4001
3388
|
},
|
|
4002
3389
|
create(context) {
|
|
4003
|
-
if (isTestFile(context)) return {}
|
|
3390
|
+
if (isTestFile(context) || !isBasaltOwnSource(getFilename(context))) return {}
|
|
4004
3391
|
|
|
4005
3392
|
return {
|
|
4006
3393
|
MemberExpression(node) {
|
|
@@ -4470,11 +3857,283 @@ const rawBreakpoint = {
|
|
|
4470
3857
|
},
|
|
4471
3858
|
}
|
|
4472
3859
|
|
|
3860
|
+
// ── Rule 34 — no-dev-helper ─────────────────────────────────────────────────────────────────────
|
|
3861
|
+
|
|
3862
|
+
const NO_DEV_HELPER_MESSAGE =
|
|
3863
|
+
'Dev-only import() outside a literal `process.env.NODE_ENV` gate — bundlers fold the dev branch ' +
|
|
3864
|
+
"away only for a literal `process.env.NODE_ENV !== 'production'` (or `=== 'development'`) read " +
|
|
3865
|
+
'at the use site; `isDev()` or any helper/const leaks the whole dev module into the production ' +
|
|
3866
|
+
'first-paint chunk (the check-budgets provider-only budget pins it). (basalt/no-dev-helper)'
|
|
3867
|
+
|
|
3868
|
+
/** Specifiers of dev-only modules: the dev dock, the theme lab, any `*-devtools` package. */
|
|
3869
|
+
const DEV_MODULE_SPECIFIER = /(?:^|[/.-])(?:dev-dock|theme-lab|devtools)(?:$|[/.-])/
|
|
3870
|
+
/** TS wrappers that still READ the value they wrap (`Dock as X`, `Dock!`, `Dock satisfies X`). */
|
|
3871
|
+
const TS_VALUE_WRAPPERS = new Set([
|
|
3872
|
+
'TSAsExpression',
|
|
3873
|
+
'TSNonNullExpression',
|
|
3874
|
+
'TSSatisfiesExpression',
|
|
3875
|
+
'TSInstantiationExpression',
|
|
3876
|
+
'TSTypeAssertion',
|
|
3877
|
+
])
|
|
3878
|
+
|
|
3879
|
+
/** The dev modules themselves — reachable only through a gated import, so their own imports are exempt. */
|
|
3880
|
+
const DEV_MODULE_FILE = /(?:^|[\\/])(?:dev-dock\.[cm]?[jt]sx?|theme-lab[\\/])/
|
|
3881
|
+
|
|
3882
|
+
/** `process.env.NODE_ENV` / `process.env['NODE_ENV']` as a literal read. */
|
|
3883
|
+
function isNodeEnvRead(node) {
|
|
3884
|
+
if (node?.type !== 'MemberExpression') return false
|
|
3885
|
+
const property = node.property
|
|
3886
|
+
const named = node.computed
|
|
3887
|
+
? isStringLiteral(property) && property.value === 'NODE_ENV'
|
|
3888
|
+
: property?.name === 'NODE_ENV'
|
|
3889
|
+
const env = node.object
|
|
3890
|
+
return (
|
|
3891
|
+
named &&
|
|
3892
|
+
env?.type === 'MemberExpression' &&
|
|
3893
|
+
!env.computed &&
|
|
3894
|
+
env.property?.name === 'env' &&
|
|
3895
|
+
env.object?.type === 'Identifier' &&
|
|
3896
|
+
env.object.name === 'process'
|
|
3897
|
+
)
|
|
3898
|
+
}
|
|
3899
|
+
|
|
3900
|
+
const flip = (env) => (env === 'dev' ? 'prod' : env === 'prod' ? 'dev' : undefined)
|
|
3901
|
+
|
|
3902
|
+
/** The environment `test` pins when it is TRUTHY: 'dev', 'prod' or undefined. */
|
|
3903
|
+
function truthyEnv(test) {
|
|
3904
|
+
if (test?.type === 'BinaryExpression') return comparisonEnv(test)
|
|
3905
|
+
if (test?.type === 'UnaryExpression' && test.operator === '!') return falsyEnv(test.argument)
|
|
3906
|
+
if (test?.type === 'LogicalExpression' && test.operator === '&&') {
|
|
3907
|
+
return truthyEnv(test.left) ?? truthyEnv(test.right)
|
|
3908
|
+
}
|
|
3909
|
+
return undefined
|
|
3910
|
+
}
|
|
3911
|
+
|
|
3912
|
+
/** The environment `test` pins when it is FALSY — `a && env` pins nothing, `a || env` pins both. */
|
|
3913
|
+
function falsyEnv(test) {
|
|
3914
|
+
if (test?.type === 'BinaryExpression') return flip(comparisonEnv(test))
|
|
3915
|
+
if (test?.type === 'UnaryExpression' && test.operator === '!') return truthyEnv(test.argument)
|
|
3916
|
+
if (test?.type === 'LogicalExpression' && test.operator === '||') {
|
|
3917
|
+
return falsyEnv(test.left) ?? falsyEnv(test.right)
|
|
3918
|
+
}
|
|
3919
|
+
return undefined
|
|
3920
|
+
}
|
|
3921
|
+
|
|
3922
|
+
/** 'dev' when the literal NODE_ENV comparison is true in development, 'prod' when in production. */
|
|
3923
|
+
function comparisonEnv(test) {
|
|
3924
|
+
const [read, literal] = isNodeEnvRead(test.left)
|
|
3925
|
+
? [test.left, test.right]
|
|
3926
|
+
: [test.right, test.left]
|
|
3927
|
+
if (!isNodeEnvRead(read) || !isStringLiteral(literal)) return undefined
|
|
3928
|
+
const equal = test.operator === '===' || test.operator === '=='
|
|
3929
|
+
const unequal = test.operator === '!==' || test.operator === '!='
|
|
3930
|
+
if (!equal && !unequal) return undefined
|
|
3931
|
+
if (literal.value === 'production') return equal ? 'prod' : 'dev'
|
|
3932
|
+
if (literal.value === 'development') return equal ? 'dev' : 'prod'
|
|
3933
|
+
return undefined
|
|
3934
|
+
}
|
|
3935
|
+
|
|
3936
|
+
/** True when `node` sits in a branch that only runs in development, by a literal NODE_ENV test. */
|
|
3937
|
+
function underDevGate(node) {
|
|
3938
|
+
let child = node
|
|
3939
|
+
for (let parent = node.parent; parent; child = parent, parent = parent.parent) {
|
|
3940
|
+
if (parent.type === 'IfStatement' || parent.type === 'ConditionalExpression') {
|
|
3941
|
+
if (parent.consequent === child && truthyEnv(parent.test) === 'dev') return true
|
|
3942
|
+
if (parent.alternate === child && falsyEnv(parent.test) === 'dev') return true
|
|
3943
|
+
}
|
|
3944
|
+
if (parent.type === 'LogicalExpression' && parent.right === child) {
|
|
3945
|
+
if (parent.operator === '&&' && truthyEnv(parent.left) === 'dev') return true
|
|
3946
|
+
if (parent.operator === '||' && falsyEnv(parent.left) === 'dev') return true
|
|
3947
|
+
}
|
|
3948
|
+
}
|
|
3949
|
+
return false
|
|
3950
|
+
}
|
|
3951
|
+
|
|
3952
|
+
const FUNCTION_TYPES = new Set([
|
|
3953
|
+
'ArrowFunctionExpression',
|
|
3954
|
+
'FunctionExpression',
|
|
3955
|
+
'FunctionDeclaration',
|
|
3956
|
+
])
|
|
3957
|
+
|
|
3958
|
+
/**
|
|
3959
|
+
* The identifier a LOADER function is bound to: `const Dock = lazy(() => import(dev))`,
|
|
3960
|
+
* `const load = () => import(dev)`, `function load() { return import(dev) }`. An import with no
|
|
3961
|
+
* function boundary (`const m = import(dev)`) or one inside an anonymous callback is not bound to
|
|
3962
|
+
* anything, so only its own site can gate it.
|
|
3963
|
+
*/
|
|
3964
|
+
function loaderBinding(node) {
|
|
3965
|
+
let fn = node.parent
|
|
3966
|
+
while (fn && !FUNCTION_TYPES.has(fn.type)) fn = fn.parent
|
|
3967
|
+
if (!fn) return undefined
|
|
3968
|
+
if (fn.type === 'FunctionDeclaration') return fn.id ?? undefined
|
|
3969
|
+
let holder = fn.parent
|
|
3970
|
+
if (holder?.type === 'CallExpression') holder = holder.parent
|
|
3971
|
+
return holder?.type === 'VariableDeclarator' && holder.id?.type === 'Identifier'
|
|
3972
|
+
? holder.id
|
|
3973
|
+
: undefined
|
|
3974
|
+
}
|
|
3975
|
+
|
|
3976
|
+
/**
|
|
3977
|
+
* Is this identifier a USE of a loader binding? Stricter than {@link isValueReferencePosition}
|
|
3978
|
+
* (which only has to tell an import from a render): a loader's uses are judged for gating, so
|
|
3979
|
+
* property keys, class member names, JSX closers and declaration ids must not count, while TS
|
|
3980
|
+
* value wrappers (`Dock!`, `Dock as X`) still do.
|
|
3981
|
+
*/
|
|
3982
|
+
function isLoaderUsePosition(node) {
|
|
3983
|
+
const parent = node.parent
|
|
3984
|
+
if (parent === null || parent === undefined) return false
|
|
3985
|
+
switch (parent.type) {
|
|
3986
|
+
case 'MemberExpression':
|
|
3987
|
+
return parent.object === node || parent.computed
|
|
3988
|
+
case 'Property':
|
|
3989
|
+
case 'MethodDefinition':
|
|
3990
|
+
case 'PropertyDefinition':
|
|
3991
|
+
case 'AccessorProperty':
|
|
3992
|
+
return parent.value === node || (parent.computed && parent.key === node)
|
|
3993
|
+
case 'VariableDeclarator':
|
|
3994
|
+
return parent.init === node
|
|
3995
|
+
case 'FunctionDeclaration':
|
|
3996
|
+
case 'ImportSpecifier':
|
|
3997
|
+
case 'ImportDefaultSpecifier':
|
|
3998
|
+
case 'ImportNamespaceSpecifier':
|
|
3999
|
+
case 'JSXAttribute':
|
|
4000
|
+
case 'JSXClosingElement':
|
|
4001
|
+
case 'JSXNamespacedName':
|
|
4002
|
+
case 'LabeledStatement':
|
|
4003
|
+
return false
|
|
4004
|
+
case 'JSXMemberExpression':
|
|
4005
|
+
return parent.object === node
|
|
4006
|
+
default:
|
|
4007
|
+
return TS_VALUE_WRAPPERS.has(parent.type) || !parent.type.startsWith('TS')
|
|
4008
|
+
}
|
|
4009
|
+
}
|
|
4010
|
+
|
|
4011
|
+
// Ships: repo-local only
|
|
4012
|
+
const noDevHelper = {
|
|
4013
|
+
meta: {
|
|
4014
|
+
type: 'problem',
|
|
4015
|
+
docs: {
|
|
4016
|
+
description:
|
|
4017
|
+
'Require a literal process.env.NODE_ENV gate on dynamic import() of dev-only modules.',
|
|
4018
|
+
},
|
|
4019
|
+
schema: [],
|
|
4020
|
+
},
|
|
4021
|
+
create(context) {
|
|
4022
|
+
if (isTestFile(context) || DEV_MODULE_FILE.test(getFilename(context))) return {}
|
|
4023
|
+
const sourceCode = context.sourceCode ?? context.getSourceCode?.()
|
|
4024
|
+
/** Loader bindings whose own references must all sit under the gate. */
|
|
4025
|
+
const loaders = []
|
|
4026
|
+
/** Every reference candidate, by name — judged against its binding at `Program:exit`. */
|
|
4027
|
+
const references = new Map()
|
|
4028
|
+
const addReference = (node) => {
|
|
4029
|
+
if (!isLoaderUsePosition(node)) return
|
|
4030
|
+
const same = references.get(node.name)
|
|
4031
|
+
if (same === undefined) references.set(node.name, [node])
|
|
4032
|
+
else same.push(node)
|
|
4033
|
+
}
|
|
4034
|
+
/** True when `reference` provably resolves to the declaration `id`; unresolvable is NOT a use. */
|
|
4035
|
+
const resolvesTo = (reference, id) => soleDefinition(sourceCode, reference)?.name === id
|
|
4036
|
+
return {
|
|
4037
|
+
Identifier: addReference,
|
|
4038
|
+
JSXIdentifier: addReference,
|
|
4039
|
+
ImportExpression(node) {
|
|
4040
|
+
const source = node.source
|
|
4041
|
+
let specifier
|
|
4042
|
+
if (isStringLiteral(source)) specifier = source.value
|
|
4043
|
+
else if (source?.quasis?.length === 1) specifier = source.quasis[0].value.cooked
|
|
4044
|
+
if (typeof specifier !== 'string' || !DEV_MODULE_SPECIFIER.test(specifier)) return
|
|
4045
|
+
if (underDevGate(node)) return
|
|
4046
|
+
const id = loaderBinding(node)
|
|
4047
|
+
if (id !== undefined) {
|
|
4048
|
+
loaders.push({ node, id })
|
|
4049
|
+
return
|
|
4050
|
+
}
|
|
4051
|
+
if (!hasThemeAllow(context, node, 'no-dev-helper')) {
|
|
4052
|
+
context.report({ node, message: NO_DEV_HELPER_MESSAGE })
|
|
4053
|
+
}
|
|
4054
|
+
},
|
|
4055
|
+
'Program:exit'() {
|
|
4056
|
+
for (const { node, id } of loaders) {
|
|
4057
|
+
const uses = (references.get(id.name) ?? []).filter((use) => resolvesTo(use, id))
|
|
4058
|
+
if (uses.length > 0 && uses.every(underDevGate)) continue
|
|
4059
|
+
if (hasThemeAllow(context, node, 'no-dev-helper')) continue
|
|
4060
|
+
context.report({ node, message: NO_DEV_HELPER_MESSAGE })
|
|
4061
|
+
}
|
|
4062
|
+
},
|
|
4063
|
+
}
|
|
4064
|
+
},
|
|
4065
|
+
}
|
|
4066
|
+
|
|
4067
|
+
// ── Rule 35 — route-view-transition ─────────────────────────────────────────────────────────────
|
|
4068
|
+
|
|
4069
|
+
const ROUTE_VIEW_TRANSITION_MESSAGE =
|
|
4070
|
+
'A React <ViewTransition> around <Outlet> never fires — the router commits through ' +
|
|
4071
|
+
"useSyncExternalStore, which is not a transition — and it competes with the router's own. Route " +
|
|
4072
|
+
"transitions are the router's: createBasaltRouter sets defaultViewTransition. Remove the wrapper. " +
|
|
4073
|
+
'(basalt/route-view-transition)'
|
|
4074
|
+
|
|
4075
|
+
/**
|
|
4076
|
+
* True when the JSX name `nameNode` is `imported` from `source`: a named import (aliased or not),
|
|
4077
|
+
* or `ns.imported` / `Default.imported` off a namespace or default import. Type-only imports never
|
|
4078
|
+
* count. Unresolved or shadowed names do not match.
|
|
4079
|
+
*/
|
|
4080
|
+
function isImportedJsxName(sourceCode, nameNode, source, imported) {
|
|
4081
|
+
const fromSource = (def) =>
|
|
4082
|
+
def?.type === 'ImportBinding' &&
|
|
4083
|
+
def.parent?.importKind !== 'type' &&
|
|
4084
|
+
def.parent?.source?.value === source
|
|
4085
|
+
if (nameNode?.type === 'JSXIdentifier') {
|
|
4086
|
+
const def = soleDefinition(sourceCode, nameNode)
|
|
4087
|
+
if (!fromSource(def) || def.node.type !== 'ImportSpecifier' || def.node.importKind === 'type')
|
|
4088
|
+
return false
|
|
4089
|
+
const name = def.node.imported
|
|
4090
|
+
return (name?.type === 'Identifier' ? name.name : name?.value) === imported
|
|
4091
|
+
}
|
|
4092
|
+
if (nameNode?.type === 'JSXMemberExpression' && nameNode.object?.type === 'JSXIdentifier') {
|
|
4093
|
+
if (nameNode.property?.name !== imported) return false
|
|
4094
|
+
const def = soleDefinition(sourceCode, nameNode.object)
|
|
4095
|
+
return (
|
|
4096
|
+
fromSource(def) &&
|
|
4097
|
+
(def.node.type === 'ImportNamespaceSpecifier' || def.node.type === 'ImportDefaultSpecifier')
|
|
4098
|
+
)
|
|
4099
|
+
}
|
|
4100
|
+
return false
|
|
4101
|
+
}
|
|
4102
|
+
|
|
4103
|
+
// Ships: error
|
|
4104
|
+
const routeViewTransition = {
|
|
4105
|
+
meta: {
|
|
4106
|
+
type: 'problem',
|
|
4107
|
+
docs: {
|
|
4108
|
+
description: "Disallow a React <ViewTransition> around the router's <Outlet>.",
|
|
4109
|
+
},
|
|
4110
|
+
schema: [],
|
|
4111
|
+
},
|
|
4112
|
+
create(context) {
|
|
4113
|
+
if (isTestFile(context)) return {}
|
|
4114
|
+
const sourceCode = context.sourceCode ?? context.getSourceCode?.()
|
|
4115
|
+
const reported = new Set()
|
|
4116
|
+
return {
|
|
4117
|
+
JSXOpeningElement(node) {
|
|
4118
|
+
if (!isImportedJsxName(sourceCode, node.name, '@tanstack/react-router', 'Outlet')) return
|
|
4119
|
+
// Walk up: every enclosing <ViewTransition> has this <Outlet> in its subtree.
|
|
4120
|
+
for (let up = node.parent?.parent; up; up = up.parent) {
|
|
4121
|
+
if (up.type !== 'JSXElement' || reported.has(up)) continue
|
|
4122
|
+
const opening = up.openingElement
|
|
4123
|
+
if (!isImportedJsxName(sourceCode, opening?.name, 'react', 'ViewTransition')) continue
|
|
4124
|
+
reported.add(up)
|
|
4125
|
+
if (hasThemeAllow(context, opening, 'route-view-transition')) continue
|
|
4126
|
+
context.report({ node: opening, message: ROUTE_VIEW_TRANSITION_MESSAGE })
|
|
4127
|
+
}
|
|
4128
|
+
},
|
|
4129
|
+
}
|
|
4130
|
+
},
|
|
4131
|
+
}
|
|
4132
|
+
|
|
4473
4133
|
// ── Grace ledger ────────────────────────────────────────────────────────────────────────────────
|
|
4474
4134
|
|
|
4475
4135
|
/**
|
|
4476
4136
|
* @typedef {{ since: string; promote: string; why: string }} GraceEntry
|
|
4477
|
-
* @typedef {{ since: string; why: string }} AdvisoryEntry
|
|
4478
4137
|
*/
|
|
4479
4138
|
|
|
4480
4139
|
/**
|
|
@@ -4484,9 +4143,8 @@ const rawBreakpoint = {
|
|
|
4484
4143
|
* nothing tracking them: `configs/oxlint.json` is generated and its top-level keys are fixed by
|
|
4485
4144
|
* oxlint's own parser, so the ledger cannot live there.
|
|
4486
4145
|
*
|
|
4487
|
-
* `oxlint-plugin.test.ts` asserts the two agree in both directions — a rule listed here
|
|
4488
|
-
*
|
|
4489
|
-
* be `error`. Deleting a `PLUGIN_RULE_GRACE` entry IS the promotion, and the test makes the config
|
|
4146
|
+
* `oxlint-plugin.test.ts` asserts the two agree in both directions — a rule listed here must be
|
|
4147
|
+
* `warn` in the shipped preset, and one absent must be `error`. Deleting a `PLUGIN_RULE_GRACE` entry IS the promotion, and the test makes the config
|
|
4490
4148
|
* change mandatory in the same commit.
|
|
4491
4149
|
*
|
|
4492
4150
|
* Each entry is `{ since, promote, why }` (semver strings, C16 — `docs/CONTROLS-SPEC.md` §1): a
|
|
@@ -4502,39 +4160,6 @@ const rawBreakpoint = {
|
|
|
4502
4160
|
*/
|
|
4503
4161
|
export const PLUGIN_RULE_GRACE = {}
|
|
4504
4162
|
|
|
4505
|
-
/**
|
|
4506
|
-
* Plugin rules that stay `warn` in the shipped preset PERMANENTLY — outside the C16 version gate,
|
|
4507
|
-
* and not expected to ever promote. `oxlint-plugin.test.ts` still requires `warn` in the shipped
|
|
4508
|
-
* preset and a `why` long enough to justify it, but skips these when checking `promote`.
|
|
4509
|
-
*
|
|
4510
|
-
* `shadow-basalt-export` is the one entry: a name collision is strong evidence a component was
|
|
4511
|
-
* forked rather than imported, but not proof, and renaming the fork defeats the check outright — so
|
|
4512
|
-
* there is no version at which flipping it to `error` is safe to promise in advance.
|
|
4513
|
-
*
|
|
4514
|
-
* @type {Record<string, AdvisoryEntry>}
|
|
4515
|
-
*/
|
|
4516
|
-
export const PLUGIN_RULE_ADVISORY = {
|
|
4517
|
-
'shadow-basalt-export': {
|
|
4518
|
-
since: '1.20.0',
|
|
4519
|
-
why:
|
|
4520
|
-
'new in the round-4 guard minor (1.20.0), widened in round-5 (1.21.0 — reads every ' +
|
|
4521
|
-
'published barrel, not just the root one, so a chart-layer fork is visible), narrowed at ' +
|
|
4522
|
-
'1.22.0 (`isBasaltScopedFile` plus a component-shaped declaration). A name collision is ' +
|
|
4523
|
-
'strong evidence and not proof, and renaming defeats it outright, so this stays a permanent ' +
|
|
4524
|
-
'warn rather than a grace entry with no honest promote date.',
|
|
4525
|
-
},
|
|
4526
|
-
'deprecated-export': {
|
|
4527
|
-
since: '1.28.0',
|
|
4528
|
-
why:
|
|
4529
|
-
'B4, the sunset half of the deprecation lifecycle (`../MAINTAINING.md`). A deprecation is a ' +
|
|
4530
|
-
'SCHEDULE, not a defect: the code compiles, runs and is correct until the `removeIn` minor, ' +
|
|
4531
|
-
'so promoting this to `error` would fail a consumer build over something that still works — ' +
|
|
4532
|
-
'the one outcome the no-majors doctrine exists to avoid. There is no version at which that ' +
|
|
4533
|
-
'stops being true, which is what makes it advisory rather than a grace entry: enforcement ' +
|
|
4534
|
-
'arrives by the export ceasing to exist, not by this rule changing level.',
|
|
4535
|
-
},
|
|
4536
|
-
}
|
|
4537
|
-
|
|
4538
4163
|
// ── Plugin export ───────────────────────────────────────────────────────────────────────────────
|
|
4539
4164
|
|
|
4540
4165
|
export default {
|
|
@@ -4544,8 +4169,6 @@ export default {
|
|
|
4544
4169
|
'raw-size-literal': rawSizeLiteral,
|
|
4545
4170
|
'card-inset': cardInset,
|
|
4546
4171
|
'chart-in-raw-surface': chartInRawSurface,
|
|
4547
|
-
'hand-rolled-plot': handRolledPlot,
|
|
4548
|
-
'chart-legend-literal': chartLegendLiteral,
|
|
4549
4172
|
'shadow-basalt-export': shadowBasaltExport,
|
|
4550
4173
|
'hand-rolled-shell': handRolledShell,
|
|
4551
4174
|
'raw-scroll-container': rawScrollContainer,
|
|
@@ -4553,22 +4176,20 @@ export default {
|
|
|
4553
4176
|
'control-outside-home': controlOutsideHome,
|
|
4554
4177
|
'bound-control-outside-home': boundControlOutsideHome,
|
|
4555
4178
|
'control-size-literal': controlSizeLiteral,
|
|
4556
|
-
'page-bar-budget': pageBarBudget,
|
|
4557
4179
|
'in-body-page-title': inBodyPageTitle,
|
|
4558
4180
|
'responsive-twin': responsiveTwin,
|
|
4559
4181
|
'search-literal-link': searchLiteralLink,
|
|
4560
4182
|
'use-search-from-literal': useSearchFromLiteral,
|
|
4561
|
-
'provider-above-router': providerAboveRouter,
|
|
4562
4183
|
'query-fn-unwrap': queryFnUnwrap,
|
|
4563
|
-
'deprecated-export': deprecatedExport,
|
|
4564
4184
|
'forms-field-key': formsFieldKey,
|
|
4565
|
-
'
|
|
4566
|
-
'
|
|
4185
|
+
'chart-engine-boundary': chartEngineBoundary,
|
|
4186
|
+
'chart-host-single': chartHostSingle,
|
|
4567
4187
|
'token-layer-boundary': tokenLayerBoundary,
|
|
4568
|
-
'agent-resume-guard': agentResumeGuard,
|
|
4569
4188
|
'agent-no-raw-usechat': agentNoRawUseChat,
|
|
4570
4189
|
'ai-sdk-major': aiSdkMajor,
|
|
4571
4190
|
'no-import-meta-env': noImportMetaEnv,
|
|
4572
4191
|
'raw-breakpoint': rawBreakpoint,
|
|
4192
|
+
'no-dev-helper': noDevHelper,
|
|
4193
|
+
'route-view-transition': routeViewTransition,
|
|
4573
4194
|
},
|
|
4574
4195
|
}
|