basalt-ui 1.32.1 → 1.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +65 -0
- package/MIGRATING.md +218 -25
- package/README.md +3 -0
- package/agent/rules/basalt-state.md +2 -2
- package/agent/rules/basalt-tokens.md +1 -1
- package/agent/skills/basalt-app/SKILL.md +5 -3
- package/configs/oxlint-basalt.json +33 -0
- package/configs/oxlint-plugin.js +89 -67
- package/configs/oxlint-plugin.test.ts +224 -19
- package/configs/oxlint.json +2 -30
- package/dist/agent/coalesce.d.ts.map +1 -1
- package/dist/agent/coalesce.js +1 -2
- package/dist/agent/merge.d.ts.map +1 -1
- package/dist/agent/merge.js +1 -2
- package/dist/agent-chat/relative-time.d.ts +9 -0
- package/dist/agent-chat/relative-time.d.ts.map +1 -1
- package/dist/agent-chat/relative-time.js +1 -2
- package/dist/agent-chat/thread-message.d.ts.map +1 -1
- package/dist/agent-chat/thread-message.js +1 -2
- package/dist/charts/kinds/BandStrip.d.ts.map +1 -1
- package/dist/charts/kinds/BandStrip.js +3 -3
- package/dist/charts/kinds/Bars.d.ts.map +1 -1
- package/dist/charts/kinds/Bars.js +1 -2
- package/dist/charts/kinds/StackedArea.d.ts.map +1 -1
- package/dist/charts/kinds/StackedArea.js +1 -2
- package/dist/charts/primitives/legend-disclosure.d.ts.map +1 -1
- package/dist/cli/audit-allows.d.ts +1 -1
- package/dist/cli/audit-allows.d.ts.map +1 -1
- package/dist/cli/audit-allows.js +4 -4
- package/dist/cli/check-theme.d.ts.map +1 -1
- package/dist/cli/check-theme.js +38 -38
- package/dist/cli/config.d.ts +197 -0
- package/dist/cli/config.d.ts.map +1 -0
- package/dist/cli/config.js +20 -0
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/doctor.js +4 -6
- package/dist/cli/guard-hook.d.ts.map +1 -1
- package/dist/cli/guard-hook.js +7 -41
- package/dist/cli/index.d.ts +31 -257
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +64 -37
- package/dist/cli/sync.d.ts.map +1 -1
- package/dist/cli/sync.js +7 -16
- package/dist/commands/define-commands.d.ts.map +1 -1
- package/dist/commands/define-commands.js +2 -2
- package/dist/commands/define-overlays.d.ts.map +1 -1
- package/dist/commands/define-overlays.js +1 -2
- package/dist/common/index.d.ts +0 -1
- package/dist/common/index.d.ts.map +1 -1
- package/dist/common/index.js +0 -2
- package/dist/common/validate.d.ts +2 -2
- package/dist/common/validate.d.ts.map +1 -1
- package/dist/common/validate.js +2 -3
- package/dist/common/vx-families.d.ts +7 -0
- package/dist/common/vx-families.d.ts.map +1 -0
- package/dist/common/vx-families.js +32 -0
- package/dist/common/vx-family.d.ts +11 -0
- package/dist/common/vx-family.d.ts.map +1 -0
- package/dist/common/vx-family.js +12 -0
- package/dist/content/article-card.d.ts.map +1 -1
- package/dist/content/article-card.js +4 -1
- package/dist/controls/enum-filter.d.ts.map +1 -1
- package/dist/controls/enum-filter.js +3 -13
- package/dist/controls/filter-pill.d.ts +8 -0
- package/dist/controls/filter-pill.d.ts.map +1 -1
- package/dist/controls/filter-pill.js +15 -0
- package/dist/controls/multi-select-filter.d.ts.map +1 -1
- package/dist/controls/multi-select-filter.js +3 -13
- package/dist/controls/number-filter.d.ts +3 -0
- package/dist/controls/number-filter.d.ts.map +1 -1
- package/dist/controls/number-filter.js +12 -4
- package/dist/controls/range-filter.d.ts.map +1 -1
- package/dist/controls/range-filter.js +1 -2
- package/dist/data/data-table.d.ts.map +1 -1
- package/dist/data/data-table.js +2 -3
- package/dist/guard/index.d.ts +17 -3
- package/dist/guard/index.d.ts.map +1 -1
- package/dist/guard/index.js +62 -23
- package/dist/guard/types.d.ts +3 -3
- package/dist/guard/types.d.ts.map +1 -1
- package/dist/guard/vx-names.d.ts +3 -0
- package/dist/guard/vx-names.d.ts.map +1 -0
- package/dist/guard/vx-names.js +257 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -2
- package/dist/notifications/mount-guard.d.ts.map +1 -1
- package/dist/notifications/mount-guard.js +1 -2
- package/dist/provider/index.d.ts.map +1 -1
- package/dist/provider/index.js +2 -3
- package/dist/provider/lab-theme.d.ts +5 -4
- package/dist/provider/lab-theme.d.ts.map +1 -1
- package/dist/provider/lab-theme.js +1 -3
- package/dist/router-tanstack/search-store.d.ts +18 -0
- package/dist/router-tanstack/search-store.d.ts.map +1 -1
- package/dist/router-tanstack/search-store.js +33 -1
- package/dist/shell/app-mobile-nav.d.ts.map +1 -1
- package/dist/shell/app-mobile-nav.js +2 -1
- package/dist/shell/app-mobile-nav.module.css +12 -0
- package/dist/shell/app-sidebar.d.ts.map +1 -1
- package/dist/shell/app-sidebar.js +32 -36
- package/dist/shell/app-sidebar.module.css +6 -0
- package/dist/shell/index.d.ts +0 -1
- package/dist/shell/index.d.ts.map +1 -1
- package/dist/shell/index.js +0 -2
- package/dist/shell/mobile-nav-model.d.ts.map +1 -1
- package/dist/shell/mobile-nav-model.js +3 -4
- package/dist/shell/nav-glyph.d.ts +14 -0
- package/dist/shell/nav-glyph.d.ts.map +1 -0
- package/dist/shell/nav-glyph.js +21 -0
- package/dist/shell/nav-types.d.ts +1 -0
- package/dist/shell/nav-types.d.ts.map +1 -1
- package/dist/shell/page-bar.d.ts.map +1 -1
- package/dist/shell/page-bar.js +1 -2
- package/dist/shell/sidebar-search.d.ts.map +1 -1
- package/dist/shell/sidebar-search.js +1 -3
- package/dist/surfaces.d.ts +3 -3
- package/dist/surfaces.d.ts.map +1 -1
- package/dist/surfaces.js +4 -2
- package/dist/tokens/index.d.ts +1 -1
- package/dist/tokens/index.d.ts.map +1 -1
- package/dist/tokens/index.js +24 -2
- package/dist/tokens/size-classes.d.ts +25 -0
- package/dist/tokens/size-classes.d.ts.map +1 -1
- package/dist/tokens/size-classes.js +14 -0
- package/llms.txt +4 -4
- package/package.json +1 -1
- package/src/agent/coalesce.ts +1 -2
- package/src/agent/merge.ts +1 -2
- package/src/agent-chat/relative-time.ts +1 -2
- package/src/agent-chat/thread-message.tsx +2 -3
- package/src/charts/kinds/BandStrip.tsx +7 -3
- package/src/charts/kinds/Bars.tsx +1 -2
- package/src/charts/kinds/StackedArea.tsx +4 -2
- package/src/charts/primitives/legend-disclosure.tsx +1 -0
- package/src/cli/audit-allows.ts +10 -9
- package/src/cli/check-theme.ts +61 -39
- package/src/cli/config.ts +209 -0
- package/src/cli/doctor.ts +5 -7
- package/src/cli/guard-hook.ts +9 -42
- package/src/cli/index.ts +101 -294
- package/src/cli/sync.ts +7 -16
- package/src/commands/define-commands.ts +2 -2
- package/src/commands/define-overlays.ts +1 -2
- package/src/common/index.ts +0 -1
- package/src/common/validate.ts +4 -5
- package/src/common/vx-families.ts +36 -0
- package/src/common/vx-family.ts +22 -0
- package/src/content/article-card.tsx +1 -0
- package/src/controls/enum-filter.tsx +3 -13
- package/src/controls/filter-pill.tsx +20 -0
- package/src/controls/multi-select-filter.tsx +3 -13
- package/src/controls/number-filter.tsx +24 -3
- package/src/controls/range-filter.tsx +1 -2
- package/src/data/data-table.tsx +2 -3
- package/src/guard/index.ts +134 -47
- package/src/guard/types.ts +3 -2
- package/src/guard/vx-names.ts +257 -0
- package/src/index.ts +0 -2
- package/src/notifications/mount-guard.ts +1 -2
- package/src/provider/index.tsx +2 -3
- package/src/provider/lab-theme.ts +28 -21
- package/src/router-tanstack/search-store.ts +73 -1
- package/src/shell/app-mobile-nav.module.css +12 -0
- package/src/shell/app-mobile-nav.tsx +8 -1
- package/src/shell/app-sidebar.module.css +6 -0
- package/src/shell/app-sidebar.tsx +52 -50
- package/src/shell/index.tsx +0 -1
- package/src/shell/mobile-nav-model.ts +3 -5
- package/src/shell/nav-glyph.tsx +38 -0
- package/src/shell/nav-types.ts +1 -0
- package/src/shell/page-bar.tsx +1 -4
- package/src/shell/sidebar-search.tsx +1 -5
- package/src/surfaces.ts +4 -2
- package/src/tokens/index.ts +34 -2
- package/src/tokens/size-classes.ts +27 -0
- package/dist/common/is-dev.d.ts +0 -3
- package/dist/common/is-dev.d.ts.map +0 -1
- package/dist/common/is-dev.js +0 -6
- package/dist/shell/use-breakpoint.d.ts +0 -10
- package/dist/shell/use-breakpoint.d.ts.map +0 -1
- package/dist/shell/use-breakpoint.js +0 -11
- package/src/common/is-dev.ts +0 -4
- package/src/shell/use-breakpoint.ts +0 -37
package/configs/oxlint-plugin.js
CHANGED
|
@@ -177,6 +177,7 @@ export const KNOWN_RULE_IDS = new Set([
|
|
|
177
177
|
'in-body-page-title',
|
|
178
178
|
'raw-selection-control',
|
|
179
179
|
'raw-media-query',
|
|
180
|
+
'unknown-vx-token',
|
|
180
181
|
// Retired guard kinds — no longer in GUARD_RULES, but a `theme-allow` naming one must still
|
|
181
182
|
// parse as a real (dead) waiver rather than an unknown-id typo. See RETIRED_RULE_IDS above.
|
|
182
183
|
...RETIRED_RULE_IDS,
|
|
@@ -2702,9 +2703,8 @@ const CONTROL_OUTSIDE_HOME_MESSAGE =
|
|
|
2702
2703
|
|
|
2703
2704
|
/**
|
|
2704
2705
|
* The cross-file half of C1, and the one rule here that is openly a HEURISTIC: "this control has
|
|
2705
|
-
* no home" is a claim about layout intent, which no AST can see.
|
|
2706
|
-
*
|
|
2707
|
-
* (docs/CONTROLS-SPEC.md §9 wave 7).
|
|
2706
|
+
* no home" is a claim about layout intent, which no AST can see. That is why it sat in grace from
|
|
2707
|
+
* 1.26.0 to 1.33.0, promoting only once basalt's tree and nine consumers measured at zero.
|
|
2708
2708
|
*
|
|
2709
2709
|
* Four exemptions carry the false-positive load: an overlay/settings-row/form-row ancestor
|
|
2710
2710
|
* ({@link CONTROL_HOST_TAGS}, `FormRow`/`FormGroup` included), an {@link isOverlayConventionFile}
|
|
@@ -2712,7 +2712,7 @@ const CONTROL_OUTSIDE_HOME_MESSAGE =
|
|
|
2712
2712
|
* `@mantine/form` (a form is the third home and its inputs are not filters), and the owner
|
|
2713
2713
|
* exemption — a file DEFINING a basalt control cannot be told to use one.
|
|
2714
2714
|
*/
|
|
2715
|
-
// Ships:
|
|
2715
|
+
// Ships: error
|
|
2716
2716
|
const controlOutsideHome = {
|
|
2717
2717
|
meta: {
|
|
2718
2718
|
type: 'suggestion',
|
|
@@ -3623,6 +3623,7 @@ const queryFnUnwrap = {
|
|
|
3623
3623
|
* prop?: string
|
|
3624
3624
|
* replacement: string
|
|
3625
3625
|
* removeIn: string
|
|
3626
|
+
* removed?: true
|
|
3626
3627
|
* fix?: false
|
|
3627
3628
|
* }} DeprecatedExport
|
|
3628
3629
|
*/
|
|
@@ -3646,7 +3647,12 @@ const queryFnUnwrap = {
|
|
|
3646
3647
|
* `removeIn` at least one minor out, add the `MIGRATING.md` row, and this rule starts nudging.
|
|
3647
3648
|
* `oxlint-plugin.test.ts` scans every `src/**\/index.ts(x)` barrel for a JSDoc `@deprecated` and
|
|
3648
3649
|
* fails when one has no row here, so the barrel half cannot be forgotten. Removing the export is a
|
|
3649
|
-
* separate commit that deletes the row in the
|
|
3650
|
+
* separate commit that either deletes the row (the fix is manual from then on — say so in the
|
|
3651
|
+
* removal's MIGRATING row) or keeps it one minor past the removal with `removed: true` and
|
|
3652
|
+
* `removeIn` set to the removing minor, so the message reads "was removed in X" instead of "still
|
|
3653
|
+
* resolves" — and never autofixes, whatever `fix` says. `scripts/check-grace.ts` refuses a release
|
|
3654
|
+
* at or past a `removeIn` whose export still ships — the gap that let `useBreakpoint` outlive its
|
|
3655
|
+
* date twice.
|
|
3650
3656
|
*
|
|
3651
3657
|
* @type {readonly DeprecatedExport[]}
|
|
3652
3658
|
*/
|
|
@@ -3662,8 +3668,8 @@ export const DEPRECATED_EXPORTS = [
|
|
|
3662
3668
|
name: 'useBreakpoint',
|
|
3663
3669
|
replacement:
|
|
3664
3670
|
'useSizeClass() for shell chrome; a page’s own layout swap is a container decision — a type="container" SimpleGrid/Grid or an @container rule',
|
|
3665
|
-
removeIn: '1.
|
|
3666
|
-
|
|
3671
|
+
removeIn: '1.34.0',
|
|
3672
|
+
removed: true,
|
|
3667
3673
|
},
|
|
3668
3674
|
]
|
|
3669
3675
|
|
|
@@ -3672,6 +3678,13 @@ function deprecatedExportMessage(row) {
|
|
|
3672
3678
|
row.prop === undefined
|
|
3673
3679
|
? `\`${row.name}\` from ${row.subpath}`
|
|
3674
3680
|
: `\`${row.prop}\` on <${row.name}>`
|
|
3681
|
+
if (row.removed === true) {
|
|
3682
|
+
return (
|
|
3683
|
+
`Removed basalt export — ${what} was removed in ${row.removeIn} and no longer resolves; use ` +
|
|
3684
|
+
`\`${row.replacement}\` instead (packages/basalt-ui/MIGRATING.md carries the row). ` +
|
|
3685
|
+
'(basalt/deprecated-export)'
|
|
3686
|
+
)
|
|
3687
|
+
}
|
|
3675
3688
|
return (
|
|
3676
3689
|
`Deprecated basalt export — ${what} is superseded by \`${row.replacement}\` and is removed in ` +
|
|
3677
3690
|
`${row.removeIn}. It still resolves today, so nothing is broken yet; migrate before that minor ` +
|
|
@@ -3724,7 +3737,8 @@ const deprecatedExport = {
|
|
|
3724
3737
|
context.report({
|
|
3725
3738
|
node: spec,
|
|
3726
3739
|
message: deprecatedExportMessage(row),
|
|
3727
|
-
|
|
3740
|
+
// A removed row's replacement is prose for a gone import, never a drop-in name.
|
|
3741
|
+
...(row.fix === false || row.removed === true
|
|
3728
3742
|
? {}
|
|
3729
3743
|
: { fix: (fixer) => fixer.replaceText(spec, `${row.replacement} as ${local}`) }),
|
|
3730
3744
|
})
|
|
@@ -4066,8 +4080,9 @@ const CONTAINER_ROUTED_PROPS = {
|
|
|
4066
4080
|
* `type` has to be the string literal `"container"`; a dynamic value is not a decision this rule
|
|
4067
4081
|
* can read. `Grid.Col` takes its grid's mode by walking to the nearest enclosing `Grid` element.
|
|
4068
4082
|
* `consts` is the rule's same-file name→object map, so `breakpoints={map}` resolves like any value.
|
|
4083
|
+
* The one IMPORTED map trusted is basalt's own `CONTAINER_GRID_BREAKPOINTS` ({@link isShippedGridMap}).
|
|
4069
4084
|
*/
|
|
4070
|
-
function containerGridMode(opening, mantineImports, consts) {
|
|
4085
|
+
function containerGridMode(opening, mantineImports, consts, sourceCode) {
|
|
4071
4086
|
let tag = resolveMantineTag(opening.name, mantineImports)
|
|
4072
4087
|
let grid = opening
|
|
4073
4088
|
if (tag === 'Grid.Col') {
|
|
@@ -4096,12 +4111,69 @@ function containerGridMode(opening, mantineImports, consts) {
|
|
|
4096
4111
|
const breakpoints = attr('breakpoints')
|
|
4097
4112
|
if (breakpoints === undefined) return 'fallback'
|
|
4098
4113
|
let map = attrValue(breakpoints)
|
|
4114
|
+
if (isShippedGridMap(sourceCode, map)) return 'container'
|
|
4099
4115
|
if (map?.type === 'Identifier') map = consts.get(map.name)
|
|
4100
4116
|
const nonEmpty =
|
|
4101
4117
|
map?.type === 'ObjectExpression' && map.properties.some((prop) => prop.type === 'Property')
|
|
4102
4118
|
return nonEmpty ? 'container' : 'fallback'
|
|
4103
4119
|
}
|
|
4104
4120
|
|
|
4121
|
+
/**
|
|
4122
|
+
* Whether `node` reads basalt's own `CONTAINER_GRID_BREAKPOINTS` — its value is known, so it is
|
|
4123
|
+
* the one imported `breakpoints` map trusted. Resolved through the scope manager to the BINDING the
|
|
4124
|
+
* identifier reads, never by local name: a parameter or inner `const` shadowing the import is not
|
|
4125
|
+
* the import. The binding has to be an `ImportSpecifier` naming it from `basalt-ui/tokens` (any
|
|
4126
|
+
* alias, not type-only), a `.CONTAINER_GRID_BREAKPOINTS` member of that module's namespace import,
|
|
4127
|
+
* or a `const` whose initializer resolves the same way. Without a scope manager nothing is trusted
|
|
4128
|
+
* — reported, the conservative answer.
|
|
4129
|
+
*/
|
|
4130
|
+
function isShippedGridMap(sourceCode, node, seen = new Set()) {
|
|
4131
|
+
if (node?.type === 'MemberExpression') return isShippedGridMapMember(sourceCode, node)
|
|
4132
|
+
if (node?.type !== 'Identifier' || seen.has(node)) return false
|
|
4133
|
+
seen.add(node)
|
|
4134
|
+
const def = soleDefinition(sourceCode, node)
|
|
4135
|
+
if (def?.type === 'ImportBinding') {
|
|
4136
|
+
const spec = def.node
|
|
4137
|
+
const decl = def.parent
|
|
4138
|
+
if (decl?.type !== 'ImportDeclaration' || decl.importKind === 'type') return false
|
|
4139
|
+
if (decl.source?.value !== 'basalt-ui/tokens') return false
|
|
4140
|
+
if (spec.type !== 'ImportSpecifier' || spec.importKind === 'type') return false
|
|
4141
|
+
const imported = spec.imported
|
|
4142
|
+
const importedName = imported.type === 'Identifier' ? imported.name : imported.value
|
|
4143
|
+
return importedName === 'CONTAINER_GRID_BREAKPOINTS'
|
|
4144
|
+
}
|
|
4145
|
+
if (def?.type === 'Variable' && def.parent?.kind === 'const')
|
|
4146
|
+
return isShippedGridMap(sourceCode, def.node.init, seen)
|
|
4147
|
+
return false
|
|
4148
|
+
}
|
|
4149
|
+
|
|
4150
|
+
/** `tokens.CONTAINER_GRID_BREAKPOINTS` (or `tokens['CONTAINER_GRID_BREAKPOINTS']`) on `import * as tokens from 'basalt-ui/tokens'`. */
|
|
4151
|
+
function isShippedGridMapMember(sourceCode, node) {
|
|
4152
|
+
const property = node.property
|
|
4153
|
+
const name = node.computed
|
|
4154
|
+
? isStringLiteral(property) && property.value
|
|
4155
|
+
: property?.type === 'Identifier' && property.name
|
|
4156
|
+
if (name !== 'CONTAINER_GRID_BREAKPOINTS' || node.object?.type !== 'Identifier') return false
|
|
4157
|
+
const def = soleDefinition(sourceCode, node.object)
|
|
4158
|
+
return (
|
|
4159
|
+
def?.type === 'ImportBinding' &&
|
|
4160
|
+
def.node.type === 'ImportNamespaceSpecifier' &&
|
|
4161
|
+
def.parent?.importKind !== 'type' &&
|
|
4162
|
+
def.parent?.source?.value === 'basalt-ui/tokens'
|
|
4163
|
+
)
|
|
4164
|
+
}
|
|
4165
|
+
|
|
4166
|
+
/** The one definition of the variable `identifier` reads, or `undefined` (unresolved, or more than one). */
|
|
4167
|
+
function soleDefinition(sourceCode, identifier) {
|
|
4168
|
+
if (typeof sourceCode?.getScope !== 'function') return undefined
|
|
4169
|
+
for (let scope = sourceCode.getScope(identifier); scope; scope = scope.upper) {
|
|
4170
|
+
const variable = scope.set.get(identifier.name)
|
|
4171
|
+
if (variable === undefined) continue
|
|
4172
|
+
return variable.defs.length === 1 ? variable.defs[0] : undefined
|
|
4173
|
+
}
|
|
4174
|
+
return undefined
|
|
4175
|
+
}
|
|
4176
|
+
|
|
4105
4177
|
/**
|
|
4106
4178
|
* The shell components that are `visibleFrom`/`hiddenFrom`'s legitimate home — the size-class
|
|
4107
4179
|
* axis's one JSX escape hatch, alongside overlays. Matched by DECLARATION name, the same
|
|
@@ -4128,13 +4200,14 @@ const RAW_BREAKPOINT_EXEMPT_FILE = /(?:^|[\\/])(?:use-media-query\.ts|ChartToolt
|
|
|
4128
4200
|
* (`CONTAINER_CLASSES`, `basalt-ui/tokens`: compact 240 / regular 480 / wide 800), never on the
|
|
4129
4201
|
* viewport — `useSizeClass()` is the shell's seam, not a page's. Mantine 9's own `type="container"`
|
|
4130
4202
|
* grids are the primitive: both self-wrap in an `inline-size` container, so they need no ancestor.
|
|
4131
|
-
* The px literals are `
|
|
4203
|
+
* The px literals are `CONTAINER_KEYS` written out (the plugin cannot import TS source);
|
|
4132
4204
|
* `oxlint-plugin.test.ts` asserts every one against it, and the MIGRATING recipe too.
|
|
4133
4205
|
*/
|
|
4134
4206
|
export const RAW_BREAKPOINT_RECIPE =
|
|
4135
4207
|
'A page’s own layout keys on its container: <SimpleGrid type="container" cols={{ base: 1, ' +
|
|
4136
|
-
|
|
4137
|
-
|
|
4208
|
+
'[CONTAINER_KEYS.regular]: 2, [CONTAINER_KEYS.wide]: 3 }}> (basalt-ui/tokens; the keys are ' +
|
|
4209
|
+
"the px lengths '480px'/'800px'), or <Grid " +
|
|
4210
|
+
'type="container" breakpoints={CONTAINER_GRID_BREAKPOINTS}> (basalt-ui/tokens), with Grid.Col ' +
|
|
4138
4211
|
'span={{ base: 12, sm: 6 }} keyed through that map; a Flex direction swap becomes that SimpleGrid (equal ' +
|
|
4139
4212
|
'columns), a wrapping Group, or a CSS-module @container basalt-grid rule. useSizeClass() is for ' +
|
|
4140
4213
|
'shell chrome only. Recipes: basalt-ui MIGRATING.md § basalt/raw-breakpoint.'
|
|
@@ -4204,7 +4277,7 @@ const RAW_BREAKPOINT_HOOK_NAMES = new Set(['useMediaQuery', 'useMatches', 'useVi
|
|
|
4204
4277
|
* declares a shell home (or a control's own C9 swap) is exempt for all four shapes uniformly — the
|
|
4205
4278
|
* shell's own responsive chrome is the sanctioned use, whatever shape it takes.
|
|
4206
4279
|
*/
|
|
4207
|
-
// Ships:
|
|
4280
|
+
// Ships: error
|
|
4208
4281
|
const rawBreakpoint = {
|
|
4209
4282
|
meta: {
|
|
4210
4283
|
type: 'suggestion',
|
|
@@ -4238,6 +4311,7 @@ const rawBreakpoint = {
|
|
|
4238
4311
|
// apart), so it is dropped from the map entirely rather than guessed at last-wins.
|
|
4239
4312
|
const responsiveObjectConsts = new Map()
|
|
4240
4313
|
const seenDeclaratorNames = new Set()
|
|
4314
|
+
const sourceCode = context.sourceCode ?? context.getSourceCode?.()
|
|
4241
4315
|
|
|
4242
4316
|
const noteShellHomeOwner = (name) => {
|
|
4243
4317
|
if (typeof name === 'string' && SHELL_HOME_NAMES.has(name)) definesShellHome = true
|
|
@@ -4322,7 +4396,7 @@ const rawBreakpoint = {
|
|
|
4322
4396
|
? CONTAINER_ROUTED_PROPS[tag]
|
|
4323
4397
|
: undefined
|
|
4324
4398
|
const mode = routed?.has(node.name.name)
|
|
4325
|
-
? containerGridMode(owner, mantineImports, responsiveObjectConsts)
|
|
4399
|
+
? containerGridMode(owner, mantineImports, responsiveObjectConsts, sourceCode)
|
|
4326
4400
|
: undefined
|
|
4327
4401
|
if (mode === 'container') continue
|
|
4328
4402
|
if (!containsResponsiveObjectValue(value, responsiveObjectConsts, mode !== undefined))
|
|
@@ -4373,59 +4447,7 @@ const rawBreakpoint = {
|
|
|
4373
4447
|
*
|
|
4374
4448
|
* @type {Record<string, GraceEntry>}
|
|
4375
4449
|
*/
|
|
4376
|
-
export const PLUGIN_RULE_GRACE = {
|
|
4377
|
-
'control-outside-home': {
|
|
4378
|
-
since: '1.26.0',
|
|
4379
|
-
promote: '1.33.0',
|
|
4380
|
-
why:
|
|
4381
|
-
"Extended 1.32.0 -> 1.33.0 at the 1.32.0 release, the last extension: basalt's own tree is at 0 (consumer-loop r1 dogfood) and argo is at 2, but RE-MEASURED 2026-09-30 the fleet carries ~33 (image-gen 11, rb 7, image-share 6, weatherorb 5, email-gateway 4, argo 2). The 1.32.0 fleet upgrade clears them while they warn, together with raw-breakpoint, so 1.33.0 promotes all four ids against a measured-clean fleet. " +
|
|
4382
|
-
'Extended 1.31.0 -> 1.32.0 at the 1.31.0 release: the basalt tree still carries incumbents (data-table, notifications center, theme-lab) and the argo sweep has not run. ' +
|
|
4383
|
-
'the wave-6 control guards (docs/CONTROLS-SPEC.md §6). The one openly HEURISTIC rule of the ' +
|
|
4384
|
-
'set — "this control has no home" is a claim about layout intent, so its false-positive load ' +
|
|
4385
|
-
'is carried by four exemptions (overlay/settings-row ancestor, the overlay FILENAME ' +
|
|
4386
|
-
'convention below, @mantine/form, owner definition) rather than by certainty. The other five ' +
|
|
4387
|
-
'wave-6 rules promoted at 1.27.0; this one has not, across three re-datings. ' +
|
|
4388
|
-
'RE-MEASURED 2026-09-09, every consumer freshly migrated to 1.29.2 and basalt built from ' +
|
|
4389
|
-
'master: 29 incumbents across five repos — image-gen 10, rb 8, image-share 6, the playground ' +
|
|
4390
|
-
'3, argo 2 — and zero in linewatch, meteo and obsidian. The 1.28.0 prediction held: ' +
|
|
4391
|
-
"{@link isOverlayConventionFile} took argo's 9 cross-file overlay warns down to 2, which is " +
|
|
4392
|
-
'the part of the remainder that was ever going to fall to an exemption. ' +
|
|
4393
|
-
'Extended to 1.31.0, and the argument is NOT the count — it is that 1.30.0 is the first ' +
|
|
4394
|
-
'release in which this rule names an answer the 29 can reach. Every home it listed was a ' +
|
|
4395
|
-
'shell slot (PageBar / Section / WidgetHeader), so a consumer that mounts `BasaltProvider` ' +
|
|
4396
|
-
'and no `BasaltShell` had no reachable home at all: image-gen is a Tauri window with a ' +
|
|
4397
|
-
'hand-rolled header and carries 10 of the 29 by itself, and `bound-control-outside-home` ' +
|
|
4398
|
-
'closed the `ViewTabs` escape it would otherwise have taken. 1.30.0 adds the missing ' +
|
|
4399
|
-
'sentence to both messages ({@link SHELL_LESS_HOME_HINT} — a `PageBar` outside a shell ' +
|
|
4400
|
-
'renders in flow, sticky, with its own title, which is what `src/shell/page-bar.tsx` ' +
|
|
4401
|
-
'actually does). Promoting a rule to `error` in the same minor that first makes it ' +
|
|
4402
|
-
'satisfiable hands a consumer zero minors to act on the answer, which is exactly the failure ' +
|
|
4403
|
-
'the grace mechanism exists to prevent. 1.31.0 is when the 29 have been through the new home ' +
|
|
4404
|
-
'and the remainder is measured against that number. A file outside the convention that is ' +
|
|
4405
|
-
"still an overlay's own body declares it with " +
|
|
4406
|
-
'`theme-allow-file control-outside-home — overlay`.',
|
|
4407
|
-
},
|
|
4408
|
-
'raw-breakpoint': {
|
|
4409
|
-
since: '1.31.0',
|
|
4410
|
-
promote: '1.33.0',
|
|
4411
|
-
why:
|
|
4412
|
-
'New in the wave-11 responsive/touch guards. Catches four independent shapes at once — a ' +
|
|
4413
|
-
'responsive-object Mantine prop, visibleFrom/hiddenFrom outside the three shell homes, the ' +
|
|
4414
|
-
'three raw viewport hooks, and window.matchMedia/innerWidth — none of which basalt policed ' +
|
|
4415
|
-
'before this wave, so every consumer on an earlier minor has a green build with all four ' +
|
|
4416
|
-
"shapes already in it. basalt-ui/content's own article-card.tsx (a `SimpleGrid cols={{ base: " +
|
|
4417
|
-
"1, sm: 2, lg }}`) was one incumbent measured in basalt's own tree at ship time — a real " +
|
|
4418
|
-
'number, not zero, so it shipped warn rather than error. ' +
|
|
4419
|
-
'Extended 1.32.0 -> 1.33.0: 1.31.0 shipped the rule naming "a container query" but no ' +
|
|
4420
|
-
'primitive, and MIGRATING never mentioned it (argo: 42 warns, no documented target). 1.32.0 ' +
|
|
4421
|
-
'is the first minor with a reachable, documented answer (Mantine type="container" grids keyed ' +
|
|
4422
|
-
'on CONTAINER_CLASSES, exempted here), and promoting in the minor that first makes a rule ' +
|
|
4423
|
-
'satisfiable hands a consumer zero minors to act on it — the control-outside-home ' +
|
|
4424
|
-
"extension's reasoning. 1.33.0 is when the incumbents are expected to have moved to a " +
|
|
4425
|
-
'container grid or a theme-allow with a stated reason. Promotes together with its CSS twin, ' +
|
|
4426
|
-
'the `raw-media-query` guard kind.',
|
|
4427
|
-
},
|
|
4428
|
-
}
|
|
4450
|
+
export const PLUGIN_RULE_GRACE = {}
|
|
4429
4451
|
|
|
4430
4452
|
/**
|
|
4431
4453
|
* Plugin rules that stay `warn` in the shipped preset PERMANENTLY — outside the C16 version gate,
|
|
@@ -644,7 +644,7 @@ describe('basalt/ai-sdk-major', () => {
|
|
|
644
644
|
describe('basalt/raw-scroll-container promotion', () => {
|
|
645
645
|
it('ships "error" (not "warn"/"off") in the consumer preset', () => {
|
|
646
646
|
const shipped = JSON.parse(
|
|
647
|
-
readFileSync(resolve(import.meta.dirname, 'oxlint.json'), 'utf8'),
|
|
647
|
+
readFileSync(resolve(import.meta.dirname, 'oxlint-basalt.json'), 'utf8'),
|
|
648
648
|
) as { rules: Record<string, unknown> }
|
|
649
649
|
expect(shipped.rules['basalt/raw-scroll-container']).toBe('error')
|
|
650
650
|
})
|
|
@@ -682,7 +682,7 @@ describe('basalt agent rules promotion', () => {
|
|
|
682
682
|
'%s ships "error" (not "warn"/"off") in the consumer preset',
|
|
683
683
|
(rule) => {
|
|
684
684
|
const shipped = JSON.parse(
|
|
685
|
-
readFileSync(resolve(import.meta.dirname, 'oxlint.json'), 'utf8'),
|
|
685
|
+
readFileSync(resolve(import.meta.dirname, 'oxlint-basalt.json'), 'utf8'),
|
|
686
686
|
) as { rules: Record<string, unknown> }
|
|
687
687
|
expect(shipped.rules[rule]).toBe('error')
|
|
688
688
|
},
|
|
@@ -1364,8 +1364,10 @@ describe('assertGraceLedger', () => {
|
|
|
1364
1364
|
// A guard kind and a plugin rule that enforce ONE law in two lanes (CSS/text vs. JSX AST) must
|
|
1365
1365
|
// promote together — a consumer failing one spelling and warned on the other is the confusion the
|
|
1366
1366
|
// pairing exists to prevent. `raw-media-query` split from `basalt/raw-breakpoint` exactly that way
|
|
1367
|
-
// (1.32.0 vs 1.33.0). Twins are DERIVED
|
|
1368
|
-
// `WAIVER_ID_ALIASES` pair, and a guard
|
|
1367
|
+
// (1.32.0 vs 1.33.0). Twins are DERIVED from declarations that outlive the grace window: a
|
|
1368
|
+
// `WAIVER_ID_ALIASES` pair, and a guard kind whose registry message names its `basalt/<id>` twin.
|
|
1369
|
+
// (Deriving them from the ledger, as this did until 1.33.0, went vacuous the moment the last pair
|
|
1370
|
+
// promoted.)
|
|
1369
1371
|
|
|
1370
1372
|
const pluginGrace = PLUGIN_RULE_GRACE as Record<string, GraceEntry>
|
|
1371
1373
|
const guardGrace = GRACE_PERIOD_KINDS as Record<string, GraceEntry>
|
|
@@ -1373,21 +1375,24 @@ const guardGrace = GRACE_PERIOD_KINDS as Record<string, GraceEntry>
|
|
|
1373
1375
|
function twinLanes(): Array<[kind: string, rule: string]> {
|
|
1374
1376
|
const pairs = new Map<string, string>()
|
|
1375
1377
|
for (const [kind, aliases] of Object.entries(WAIVER_ID_ALIASES)) {
|
|
1376
|
-
|
|
1378
|
+
if (!Object.hasOwn(GUARD_RULES, kind)) continue
|
|
1379
|
+
for (const rule of aliases) pairs.set(kind, rule)
|
|
1377
1380
|
}
|
|
1378
|
-
for (const [kind,
|
|
1379
|
-
const
|
|
1380
|
-
|
|
1381
|
+
for (const [kind, rule] of Object.entries(GUARD_RULES)) {
|
|
1382
|
+
for (const m of rule.message.matchAll(/basalt\/([a-z-]+)/g)) {
|
|
1383
|
+
if (m[1] !== undefined && m[1] in basaltPlugin.rules) pairs.set(kind, m[1])
|
|
1384
|
+
}
|
|
1381
1385
|
}
|
|
1382
|
-
return [...pairs]
|
|
1386
|
+
return [...pairs]
|
|
1383
1387
|
}
|
|
1384
1388
|
|
|
1385
1389
|
describe('twin lanes share one promotion', () => {
|
|
1386
|
-
it('derives the
|
|
1390
|
+
it('derives the known pairs (the derivation is not silently empty)', () => {
|
|
1387
1391
|
expect(twinLanes()).toContainEqual(['raw-media-query', 'raw-breakpoint'])
|
|
1392
|
+
expect(twinLanes()).toContainEqual(['raw-selection-control', 'control-outside-home'])
|
|
1388
1393
|
})
|
|
1389
1394
|
|
|
1390
|
-
it.each(twinLanes())('%s and basalt/%s
|
|
1395
|
+
it.each(twinLanes())('%s and basalt/%s share one grace state and promote', (kind, rule) => {
|
|
1391
1396
|
expect([kind, rule, guardGrace[kind]?.promote]).toEqual([
|
|
1392
1397
|
kind,
|
|
1393
1398
|
rule,
|
|
@@ -1397,7 +1402,9 @@ describe('twin lanes share one promotion', () => {
|
|
|
1397
1402
|
})
|
|
1398
1403
|
|
|
1399
1404
|
describe('PLUGIN_RULE_GRACE', () => {
|
|
1400
|
-
const shipped = JSON.parse(
|
|
1405
|
+
const shipped = JSON.parse(
|
|
1406
|
+
readFileSync(resolve(import.meta.dirname, 'oxlint-basalt.json'), 'utf8'),
|
|
1407
|
+
) as {
|
|
1401
1408
|
rules: Record<string, unknown>
|
|
1402
1409
|
}
|
|
1403
1410
|
const pkgVersion = (
|
|
@@ -3352,8 +3359,26 @@ describe('basalt/query-fn-unwrap', () => {
|
|
|
3352
3359
|
|
|
3353
3360
|
// ── deprecated-export (B4) ───────────────────────────────────────────────────
|
|
3354
3361
|
|
|
3355
|
-
|
|
3356
|
-
|
|
3362
|
+
describe('basalt/deprecated-export', () => {
|
|
3363
|
+
it('reports a removed row as gone, never as "still resolves"', () => {
|
|
3364
|
+
const { code, rules, output } = run(
|
|
3365
|
+
`import { useBreakpoint } from 'basalt-ui'\nexport const x = useBreakpoint\n`,
|
|
3366
|
+
)
|
|
3367
|
+
expect(code).toBe(1)
|
|
3368
|
+
expect(rules).toContain('deprecated-export')
|
|
3369
|
+
expect(output).toContain('was removed in 1.34.0')
|
|
3370
|
+
expect(output).not.toContain('still resolves')
|
|
3371
|
+
})
|
|
3372
|
+
|
|
3373
|
+
// The row no longer says `fix: false`; `removed` alone must suppress the rewrite, because a
|
|
3374
|
+
// removed row's replacement is prose (`useSizeClass() for shell chrome; …`), not a name.
|
|
3375
|
+
it('never autofixes a removed row', () => {
|
|
3376
|
+
const source = `import { useBreakpoint } from 'basalt-ui'\nexport const x = useBreakpoint\n`
|
|
3377
|
+
writeFileSync(resolve(dir, 'removed-fix.tsx'), source)
|
|
3378
|
+
Bun.spawnSync([OXLINT_BIN, '-c', '.oxlintrc.json', '--fix', 'removed-fix.tsx'], { cwd: dir })
|
|
3379
|
+
expect(readFileSync(resolve(dir, 'removed-fix.tsx'), 'utf8')).toBe(source)
|
|
3380
|
+
})
|
|
3381
|
+
})
|
|
3357
3382
|
|
|
3358
3383
|
// ── forms-field-key ──────────────────────────────────────────────────────────
|
|
3359
3384
|
|
|
@@ -3569,6 +3594,23 @@ describe('basalt/raw-breakpoint', () => {
|
|
|
3569
3594
|
).not.toContain('raw-breakpoint')
|
|
3570
3595
|
})
|
|
3571
3596
|
|
|
3597
|
+
it('does NOT flag CONTAINER_KEYS computed keys (named, aliased, namespace)', () => {
|
|
3598
|
+
const keyed = (imp: string, key: string) =>
|
|
3599
|
+
run(
|
|
3600
|
+
`import { SimpleGrid } from '@mantine/core'\n${imp}\n` +
|
|
3601
|
+
`export const C = () => <SimpleGrid type="container" cols={{ base: 1, [${key}.regular]: 2, [${key}.wide]: 3 }} />\n`,
|
|
3602
|
+
).rules
|
|
3603
|
+
expect(
|
|
3604
|
+
keyed(`import { CONTAINER_KEYS } from 'basalt-ui/tokens'`, 'CONTAINER_KEYS'),
|
|
3605
|
+
).not.toContain('raw-breakpoint')
|
|
3606
|
+
expect(keyed(`import { CONTAINER_KEYS as CQ } from 'basalt-ui/tokens'`, 'CQ')).not.toContain(
|
|
3607
|
+
'raw-breakpoint',
|
|
3608
|
+
)
|
|
3609
|
+
expect(
|
|
3610
|
+
keyed(`import * as tokens from 'basalt-ui/tokens'`, 'tokens.CONTAINER_KEYS'),
|
|
3611
|
+
).not.toContain('raw-breakpoint')
|
|
3612
|
+
})
|
|
3613
|
+
|
|
3572
3614
|
it('does NOT flag a lone `base` under type="container"', () => {
|
|
3573
3615
|
expect(grid(`<SimpleGrid type="container" cols={{ base: 2 }} />`).rules).not.toContain(
|
|
3574
3616
|
'raw-breakpoint',
|
|
@@ -3639,6 +3681,118 @@ describe('basalt/raw-breakpoint', () => {
|
|
|
3639
3681
|
expect(rules).not.toContain('raw-breakpoint')
|
|
3640
3682
|
})
|
|
3641
3683
|
|
|
3684
|
+
// The shipped map is the one IMPORTED map trusted: its value is known, so five route files no
|
|
3685
|
+
// longer copy the same literal. Provenance-gated on `basalt-ui/tokens`, alias-safe.
|
|
3686
|
+
const SPAN = `<Grid.Col span={{ base: 12, sm: 6 }}>x</Grid.Col>`
|
|
3687
|
+
it.each([
|
|
3688
|
+
[
|
|
3689
|
+
'the named import',
|
|
3690
|
+
`import { CONTAINER_GRID_BREAKPOINTS } from 'basalt-ui/tokens'`,
|
|
3691
|
+
'CONTAINER_GRID_BREAKPOINTS',
|
|
3692
|
+
],
|
|
3693
|
+
[
|
|
3694
|
+
'an aliased import',
|
|
3695
|
+
`import { CONTAINER_GRID_BREAKPOINTS as bp } from 'basalt-ui/tokens'`,
|
|
3696
|
+
'bp',
|
|
3697
|
+
],
|
|
3698
|
+
[
|
|
3699
|
+
'a same-file alias of it',
|
|
3700
|
+
`import { CONTAINER_GRID_BREAKPOINTS } from 'basalt-ui/tokens'\nconst bp = CONTAINER_GRID_BREAKPOINTS`,
|
|
3701
|
+
'bp',
|
|
3702
|
+
],
|
|
3703
|
+
])(
|
|
3704
|
+
'does NOT flag Grid.Col spans under breakpoints={CONTAINER_GRID_BREAKPOINTS} (%s)',
|
|
3705
|
+
(_, imp, ref) => {
|
|
3706
|
+
const { rules } = run(
|
|
3707
|
+
`import { Grid } from '@mantine/core'\n${imp}\n` +
|
|
3708
|
+
`export const C = () => <Grid type="container" breakpoints={${ref}}>${SPAN}</Grid>\n`,
|
|
3709
|
+
)
|
|
3710
|
+
expect(rules).not.toContain('raw-breakpoint')
|
|
3711
|
+
},
|
|
3712
|
+
)
|
|
3713
|
+
|
|
3714
|
+
it.each([
|
|
3715
|
+
['another module', `import { CONTAINER_GRID_BREAKPOINTS } from './tokens'`],
|
|
3716
|
+
['a type-only import', `import type { CONTAINER_GRID_BREAKPOINTS } from 'basalt-ui/tokens'`],
|
|
3717
|
+
[
|
|
3718
|
+
'another basalt-ui/tokens name',
|
|
3719
|
+
`import { CONTAINER_CLASSES as CONTAINER_GRID_BREAKPOINTS } from 'basalt-ui/tokens'`,
|
|
3720
|
+
],
|
|
3721
|
+
])('still flags an imported map of the same name from %s', (_, imp) => {
|
|
3722
|
+
const { rules, output } = run(
|
|
3723
|
+
`import { Grid } from '@mantine/core'\n${imp}\n` +
|
|
3724
|
+
`export const C = () => <Grid type="container" breakpoints={CONTAINER_GRID_BREAKPOINTS}>${SPAN}</Grid>\n`,
|
|
3725
|
+
)
|
|
3726
|
+
expect(rules).toContain('raw-breakpoint')
|
|
3727
|
+
expect(CONTAINER_MESSAGE.test(output)).toBe(true)
|
|
3728
|
+
})
|
|
3729
|
+
|
|
3730
|
+
const NS = `import * as tokens from 'basalt-ui/tokens'`
|
|
3731
|
+
it.each([
|
|
3732
|
+
['a member of the namespace import', NS, 'tokens.CONTAINER_GRID_BREAKPOINTS'],
|
|
3733
|
+
['a computed member', NS, `tokens['CONTAINER_GRID_BREAKPOINTS']`],
|
|
3734
|
+
['a const alias of the member', `${NS}\nconst bp = tokens.CONTAINER_GRID_BREAKPOINTS`, 'bp'],
|
|
3735
|
+
])('does NOT flag Grid.Col spans under %s', (_, imp, ref) => {
|
|
3736
|
+
const { rules } = run(
|
|
3737
|
+
`import { Grid } from '@mantine/core'\n${imp}\n` +
|
|
3738
|
+
`export const C = () => <Grid type="container" breakpoints={${ref}}>${SPAN}</Grid>\n`,
|
|
3739
|
+
)
|
|
3740
|
+
expect(rules).not.toContain('raw-breakpoint')
|
|
3741
|
+
})
|
|
3742
|
+
|
|
3743
|
+
it.each([
|
|
3744
|
+
['another module', `import * as tokens from './tokens'`, 'tokens.CONTAINER_GRID_BREAKPOINTS'],
|
|
3745
|
+
['another member', NS, 'tokens.CONTAINER_CLASSES'],
|
|
3746
|
+
[
|
|
3747
|
+
'a parameter shadowing the namespace',
|
|
3748
|
+
`${NS}\nexport const Used = () => tokens.VX`,
|
|
3749
|
+
'tokens.CONTAINER_GRID_BREAKPOINTS',
|
|
3750
|
+
'({ tokens }: { tokens: { CONTAINER_GRID_BREAKPOINTS: Record<string, string> } })',
|
|
3751
|
+
],
|
|
3752
|
+
])('still flags a namespace member from %s', (_, imp, ref, params = '()') => {
|
|
3753
|
+
const { rules, output } = run(
|
|
3754
|
+
`import { Grid } from '@mantine/core'\n${imp}\n` +
|
|
3755
|
+
`export const C = ${params} => <Grid type="container" breakpoints={${ref}}>${SPAN}</Grid>\n`,
|
|
3756
|
+
)
|
|
3757
|
+
expect(rules).toContain('raw-breakpoint')
|
|
3758
|
+
expect(CONTAINER_MESSAGE.test(output)).toBe(true)
|
|
3759
|
+
})
|
|
3760
|
+
|
|
3761
|
+
// Trust follows the BINDING, not the name: a shadow of the import is someone else's value.
|
|
3762
|
+
it.each([
|
|
3763
|
+
[
|
|
3764
|
+
'a parameter',
|
|
3765
|
+
`export const C = ({ bp }: { bp: Record<string, string> }) => <Grid type="container" breakpoints={bp}>${SPAN}</Grid>`,
|
|
3766
|
+
],
|
|
3767
|
+
[
|
|
3768
|
+
'an inner const',
|
|
3769
|
+
`export const C = () => {\n const bp = pick()\n return <Grid type="container" breakpoints={bp}>${SPAN}</Grid>\n}`,
|
|
3770
|
+
],
|
|
3771
|
+
[
|
|
3772
|
+
'an inner const aliasing a parameter',
|
|
3773
|
+
`export function C(props: { m: Record<string, string> }) {\n const bp = props.m\n return <Grid type="container" breakpoints={bp}>${SPAN}</Grid>\n}`,
|
|
3774
|
+
],
|
|
3775
|
+
])('flags breakpoints={bp} when bp is %s shadowing the aliased import', (_, component) => {
|
|
3776
|
+
const { rules, output } = run(
|
|
3777
|
+
`import { Grid } from '@mantine/core'\n` +
|
|
3778
|
+
`import { CONTAINER_GRID_BREAKPOINTS as bp } from 'basalt-ui/tokens'\n` +
|
|
3779
|
+
`export const Used = () => <Grid type="container" breakpoints={bp}>x</Grid>\n` +
|
|
3780
|
+
`${component}\n`,
|
|
3781
|
+
)
|
|
3782
|
+
expect(rules).toContain('raw-breakpoint')
|
|
3783
|
+
expect(CONTAINER_MESSAGE.test(output)).toBe(true)
|
|
3784
|
+
})
|
|
3785
|
+
|
|
3786
|
+
it('flags a parameter shadowing the unaliased import name', () => {
|
|
3787
|
+
const { rules } = run(
|
|
3788
|
+
`import { Grid } from '@mantine/core'\n` +
|
|
3789
|
+
`import { CONTAINER_GRID_BREAKPOINTS } from 'basalt-ui/tokens'\n` +
|
|
3790
|
+
`export const C = ({ CONTAINER_GRID_BREAKPOINTS }: { CONTAINER_GRID_BREAKPOINTS: Record<string, string> }) =>\n` +
|
|
3791
|
+
` <Grid type="container" breakpoints={CONTAINER_GRID_BREAKPOINTS}>${SPAN}</Grid>\n`,
|
|
3792
|
+
)
|
|
3793
|
+
expect(rules).toContain('raw-breakpoint')
|
|
3794
|
+
})
|
|
3795
|
+
|
|
3642
3796
|
// Only the grid-keyed props route through the container queries; Box style props on the same
|
|
3643
3797
|
// element still compile to viewport @media, so they keep the ordinary viewport message.
|
|
3644
3798
|
it.each([
|
|
@@ -3880,16 +4034,39 @@ describe('DEPRECATED_EXPORTS', () => {
|
|
|
3880
4034
|
}
|
|
3881
4035
|
).version
|
|
3882
4036
|
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
4037
|
+
const [major, minor] = pkgVersion.split('.').map(Number)
|
|
4038
|
+
const floor = `${major}.${(minor as number) + 1}.0`
|
|
4039
|
+
const surface = JSON.parse(
|
|
4040
|
+
readFileSync(resolve(import.meta.dirname, '..', 'scripts', 'export-surface.json'), 'utf8'),
|
|
4041
|
+
) as Record<string, string[]>
|
|
4042
|
+
const shipped = (row: (typeof DEPRECATED_EXPORTS)[number]) =>
|
|
4043
|
+
surface[row.subpath === 'basalt-ui' ? '.' : row.subpath.replace('basalt-ui', '.')]?.includes(
|
|
4044
|
+
row.name,
|
|
4045
|
+
) === true
|
|
4046
|
+
|
|
4047
|
+
it('carries a replacement and a well-formed removeIn on every row', () => {
|
|
3886
4048
|
for (const row of DEPRECATED_EXPORTS) {
|
|
3887
4049
|
expect([row.name, row.replacement.length > 0]).toEqual([row.name, true])
|
|
3888
4050
|
expect([row.name, /^\d+\.\d+\.\d+$/.test(row.removeIn)]).toEqual([row.name, true])
|
|
4051
|
+
}
|
|
4052
|
+
})
|
|
4053
|
+
|
|
4054
|
+
// A live row still ships, so its removal is still ahead: at least the next minor.
|
|
4055
|
+
it('dates every live row at least one minor out', () => {
|
|
4056
|
+
for (const row of DEPRECATED_EXPORTS.filter((r) => r.removed !== true)) {
|
|
3889
4057
|
expect([row.name, compareSemver(row.removeIn, floor) >= 0]).toEqual([row.name, true])
|
|
3890
4058
|
}
|
|
3891
4059
|
})
|
|
3892
4060
|
|
|
4061
|
+
// A removed row is kept past the removal only for its message: the export must really be gone,
|
|
4062
|
+
// and the removing minor is the next one at the latest — never a date still ahead.
|
|
4063
|
+
it('pins every removed row to an export that no longer ships', () => {
|
|
4064
|
+
for (const row of DEPRECATED_EXPORTS.filter((r) => r.removed === true)) {
|
|
4065
|
+
expect([row.name, compareSemver(row.removeIn, floor) <= 0]).toEqual([row.name, true])
|
|
4066
|
+
if (row.prop === undefined) expect([row.name, shipped(row)]).toEqual([row.name, false])
|
|
4067
|
+
}
|
|
4068
|
+
})
|
|
4069
|
+
|
|
3893
4070
|
it('names a real published subpath on every row', () => {
|
|
3894
4071
|
const exports = Object.keys(
|
|
3895
4072
|
(
|
|
@@ -3947,7 +4124,9 @@ describe('DEPRECATED_EXPORTS', () => {
|
|
|
3947
4124
|
describe('the `Ships:` line above each rule', () => {
|
|
3948
4125
|
const source = readFileSync(resolve(import.meta.dirname, 'oxlint-plugin.js'), 'utf8')
|
|
3949
4126
|
const lines = source.split('\n')
|
|
3950
|
-
const shipped = JSON.parse(
|
|
4127
|
+
const shipped = JSON.parse(
|
|
4128
|
+
readFileSync(resolve(import.meta.dirname, 'oxlint-basalt.json'), 'utf8'),
|
|
4129
|
+
) as {
|
|
3951
4130
|
rules: Record<string, unknown>
|
|
3952
4131
|
}
|
|
3953
4132
|
// The registry block maps rule id → the `const` implementing it.
|
|
@@ -3960,7 +4139,7 @@ describe('the `Ships:` line above each rule', () => {
|
|
|
3960
4139
|
})
|
|
3961
4140
|
|
|
3962
4141
|
for (const { id, binding } of registry) {
|
|
3963
|
-
it(`${id}: the claim matches oxlint.json`, () => {
|
|
4142
|
+
it(`${id}: the claim matches oxlint-basalt.json`, () => {
|
|
3964
4143
|
const at = lines.indexOf(`const ${binding} = {`)
|
|
3965
4144
|
expect([id, at]).not.toEqual([id, -1])
|
|
3966
4145
|
const claim = /^\/\/ Ships: (.+)$/.exec(lines[at - 1] as string)?.[1]
|
|
@@ -3985,3 +4164,29 @@ describe('the `Ships:` line above each rule', () => {
|
|
|
3985
4164
|
})
|
|
3986
4165
|
}
|
|
3987
4166
|
})
|
|
4167
|
+
|
|
4168
|
+
// emailgw r4b: wiring the full preset on a repo with history forced unrelated jsx-a11y/unicorn
|
|
4169
|
+
// errors before a single basalt/* rule could land. The guards-only preset is the guard lane alone,
|
|
4170
|
+
// and the full preset extends it, so every basalt/* level is stated in exactly one file.
|
|
4171
|
+
describe('configs/oxlint-basalt.json — the guards-only preset', () => {
|
|
4172
|
+
const read = (file: string) =>
|
|
4173
|
+
JSON.parse(readFileSync(resolve(import.meta.dirname, file), 'utf8')) as Record<string, unknown>
|
|
4174
|
+
const guards = read('oxlint-basalt.json')
|
|
4175
|
+
const full = read('oxlint.json')
|
|
4176
|
+
|
|
4177
|
+
it('carries the plugin and basalt/* rules, nothing from the generic preset', () => {
|
|
4178
|
+
expect(Object.keys(guards).toSorted()).toEqual(['jsPlugins', 'rules'])
|
|
4179
|
+
expect(guards['jsPlugins']).toEqual(['./oxlint-plugin.js'])
|
|
4180
|
+
const ids = Object.keys(guards['rules'] as object)
|
|
4181
|
+
expect(ids.filter((id) => !id.startsWith('basalt/'))).toEqual([])
|
|
4182
|
+
expect(ids.length).toBeGreaterThan(0)
|
|
4183
|
+
})
|
|
4184
|
+
|
|
4185
|
+
it('is extended by the full preset, which states no basalt/* level of its own', () => {
|
|
4186
|
+
expect(full['extends']).toEqual(['./oxlint-basalt.json'])
|
|
4187
|
+
expect(full['jsPlugins']).toBeUndefined()
|
|
4188
|
+
expect(Object.keys(full['rules'] as object).filter((id) => id.startsWith('basalt/'))).toEqual(
|
|
4189
|
+
[],
|
|
4190
|
+
)
|
|
4191
|
+
})
|
|
4192
|
+
})
|
package/configs/oxlint.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
+
"extends": ["./oxlint-basalt.json"],
|
|
2
3
|
"plugins": ["react", "typescript", "import", "unicorn", "jsx-a11y", "promise", "oxc"],
|
|
3
|
-
"jsPlugins": ["./oxlint-plugin.js"],
|
|
4
4
|
"categories": {
|
|
5
5
|
"correctness": "error",
|
|
6
6
|
"suspicious": "warn"
|
|
@@ -22,35 +22,7 @@
|
|
|
22
22
|
{
|
|
23
23
|
"allow": ["__APP_VERSION__"]
|
|
24
24
|
}
|
|
25
|
-
]
|
|
26
|
-
"basalt/no-raw-font-size": "error",
|
|
27
|
-
"basalt/raw-size-literal": "error",
|
|
28
|
-
"basalt/card-inset": "error",
|
|
29
|
-
"basalt/chart-in-raw-surface": "error",
|
|
30
|
-
"basalt/hand-rolled-plot": "error",
|
|
31
|
-
"basalt/chart-legend-literal": "error",
|
|
32
|
-
"basalt/shadow-basalt-export": "warn",
|
|
33
|
-
"basalt/hand-rolled-shell": "error",
|
|
34
|
-
"basalt/raw-scroll-container": "error",
|
|
35
|
-
"basalt/hand-rolled-filter": "error",
|
|
36
|
-
"basalt/control-outside-home": "warn",
|
|
37
|
-
"basalt/bound-control-outside-home": "error",
|
|
38
|
-
"basalt/control-size-literal": "error",
|
|
39
|
-
"basalt/page-bar-budget": "error",
|
|
40
|
-
"basalt/in-body-page-title": "error",
|
|
41
|
-
"basalt/responsive-twin": "error",
|
|
42
|
-
"basalt/search-literal-link": "error",
|
|
43
|
-
"basalt/use-search-from-literal": "error",
|
|
44
|
-
"basalt/provider-above-router": "error",
|
|
45
|
-
"basalt/query-fn-unwrap": "error",
|
|
46
|
-
"basalt/deprecated-export": "warn",
|
|
47
|
-
"basalt/forms-field-key": "error",
|
|
48
|
-
"basalt/visx-boundary": "error",
|
|
49
|
-
"basalt/visx-tooltip": "error",
|
|
50
|
-
"basalt/agent-resume-guard": "error",
|
|
51
|
-
"basalt/agent-no-raw-usechat": "error",
|
|
52
|
-
"basalt/ai-sdk-major": "error",
|
|
53
|
-
"basalt/raw-breakpoint": "warn"
|
|
25
|
+
]
|
|
54
26
|
},
|
|
55
27
|
"overrides": [
|
|
56
28
|
{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"coalesce.d.ts","sourceRoot":"","sources":["../../src/agent/coalesce.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,OAAO,KAAK,EAAE,SAAS,EAAyC,MAAM,SAAS,CAAA;
|
|
1
|
+
{"version":3,"file":"coalesce.d.ts","sourceRoot":"","sources":["../../src/agent/coalesce.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,OAAO,KAAK,EAAE,SAAS,EAAyC,MAAM,SAAS,CAAA;AA8I/E,wBAAgB,aAAa,CAAC,KAAK,SAAS,SAAS,EAAE,KAAK,EAAE,SAAS,KAAK,EAAE,GAAG,KAAK,EAAE,CA+CvF"}
|