@blumintinc/eslint-plugin-blumint 1.22.0 → 1.23.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/README.md CHANGED
@@ -267,6 +267,7 @@ full closed loop is documented in agora's `.claude/skills/eslint-autonomy/SKILL.
267
267
  | [require-migration-script-metadata](docs/rules/require-migration-script-metadata.md) | Enforce JSDoc migration metadata in callable scripts | ✅ | | | | |
268
268
  | [require-props-composition](docs/rules/require-props-composition.md) | Require React component Props types to compose (via Pick/Omit) with the props types of non-leaf child components rendered in JSX | ✅ | | | | |
269
269
  | [require-server-timestamp-for-firestore-dates](docs/rules/require-server-timestamp-for-firestore-dates.md) | Enforce serverTimestamp() instead of new Date() for Firestore timestamp fields | ✅ | | | | |
270
+ | [require-sx-props-theme](docs/rules/require-sx-props-theme.md) | Require MUI `SxProps` to carry the app theme as `SxProps<Theme>`, because the bare type defaults to `SxProps<{}>`: `sx` callbacks lose the theme's typing and TypeScript pays for a structural comparison against MUI's own `SxProps<Theme>` | ✅ | | 🔧 | | |
270
271
  | [semantic-function-prefixes](docs/rules/semantic-function-prefixes.md) | Require semantic function prefixes instead of generic verbs so callers know whether a function fetches data, transforms input, or mutates state | ✅ | | | | |
271
272
  | [sync-onwrite-name-func](docs/rules/sync-onwrite-name-func.md) | Ensure that the name field matches the func field in onWrite handlers | ✅ | | 🔧 | | |
272
273
  | [test-file-location-enforcement](docs/rules/test-file-location-enforcement.md) | Enforce colocating *.test.ts or *.test.tsx files with the code they cover. | ✅ | | | | |
package/lib/index.js CHANGED
@@ -198,6 +198,7 @@ const no_satisfies_in_frontend_bundle_1 = require("./rules/no-satisfies-in-front
198
198
  const prefer_utility_function_own_file_1 = require("./rules/prefer-utility-function-own-file");
199
199
  const no_render_function_components_1 = require("./rules/no-render-function-components");
200
200
  const enforce_use_flex_gap_on_wrap_1 = require("./rules/enforce-use-flex-gap-on-wrap");
201
+ const require_sx_props_theme_1 = require("./rules/require-sx-props-theme");
201
202
  const NO_FRONTEND_IMPORTS_FROM_FUNCTIONS_MESSAGE = 'Backend Cloud Functions (.f.ts under functions/) must not import frontend modules from the repo root src/**. Frontend code can depend on browser-only APIs and bundling it into Cloud Functions breaks server execution; move shared logic into functions/src or a shared package.';
202
203
  function noFrontendImportsFromFunctionsPatterns(pattern) {
203
204
  return [
@@ -224,7 +225,7 @@ function noFrontendImportsFromFunctionsPatterns(pattern) {
224
225
  module.exports = {
225
226
  meta: {
226
227
  name: '@blumintinc/eslint-plugin-blumint',
227
- version: '1.22.0',
228
+ version: '1.23.0',
228
229
  },
229
230
  parseOptions: {
230
231
  ecmaVersion: 2020,
@@ -433,6 +434,7 @@ module.exports = {
433
434
  '@blumintinc/blumint/prefer-utility-function-own-file': 'error',
434
435
  '@blumintinc/blumint/no-render-function-components': 'error',
435
436
  '@blumintinc/blumint/enforce-use-flex-gap-on-wrap': 'error',
437
+ '@blumintinc/blumint/require-sx-props-theme': 'error',
436
438
  },
437
439
  /**
438
440
  * Depth-specific overrides block only import strings that traverse to the
@@ -711,6 +713,7 @@ module.exports = {
711
713
  'prefer-utility-function-own-file': prefer_utility_function_own_file_1.preferUtilityFunctionOwnFile,
712
714
  'no-render-function-components': no_render_function_components_1.noRenderFunctionComponents,
713
715
  'enforce-use-flex-gap-on-wrap': enforce_use_flex_gap_on_wrap_1.enforceUseFlexGapOnWrap,
716
+ 'require-sx-props-theme': require_sx_props_theme_1.requireSxPropsTheme,
714
717
  },
715
718
  };
716
719
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,2 @@
1
+ import { TSESLint } from '@typescript-eslint/utils';
2
+ export declare const requireSxPropsTheme: TSESLint.RuleModule<"requireSxPropsTheme", [], TSESLint.RuleListener>;
@@ -0,0 +1,361 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.requireSxPropsTheme = void 0;
4
+ const utils_1 = require("@typescript-eslint/utils");
5
+ const ASTHelpers_1 = require("../utils/ASTHelpers");
6
+ const createRule_1 = require("../utils/createRule");
7
+ /**
8
+ * Every module MUI re-exports `SxProps` from. Matching on the module and the
9
+ * imported name, never the local name, is what lets a renamed import report
10
+ * and a same-named local type or shim stay silent.
11
+ */
12
+ const SX_PROPS_SOURCES = new Set([
13
+ '@mui/material',
14
+ '@mui/material/styles',
15
+ '@mui/system',
16
+ '@mui/system/styleFunctionSx',
17
+ ]);
18
+ /**
19
+ * The module a MUI app augments its theme through
20
+ * (`declare module '@mui/material/styles' { interface Theme { … } }`), so it is
21
+ * the only source whose `Theme` is provably the app theme, whichever module
22
+ * `SxProps` itself came from.
23
+ */
24
+ const THEME_SOURCE = '@mui/material/styles';
25
+ const SX_PROPS_NAME = 'SxProps';
26
+ const THEME_NAME = 'Theme';
27
+ /**
28
+ * The type argument list, read from whichever property the host
29
+ * `@typescript-eslint` version populates: v5 fills `typeParameters`, v6 adds
30
+ * `typeArguments`, and v8 drops `typeParameters` from `TSTypeReference`.
31
+ */
32
+ function hasTypeArguments(node) {
33
+ const { typeArguments } = node;
34
+ return Boolean(typeArguments ?? node.typeParameters);
35
+ }
36
+ /**
37
+ * `import { "SxProps" as Sx }` arrives as a string literal on parsers that
38
+ * support arbitrary module namespace names, so both spellings are read.
39
+ */
40
+ function importedNameOf(specifier) {
41
+ const imported = specifier.imported;
42
+ return imported.type === utils_1.AST_NODE_TYPES.Identifier
43
+ ? imported.name
44
+ : imported.value;
45
+ }
46
+ /**
47
+ * ES import declarations sit at the top level or directly inside an ambient
48
+ * `declare module '…' { }` block; nothing else can hold one.
49
+ */
50
+ function collectImportDeclarations(statements, into) {
51
+ for (const statement of statements) {
52
+ if (statement.type === utils_1.AST_NODE_TYPES.ImportDeclaration) {
53
+ into.push(statement);
54
+ }
55
+ else if (statement.type === utils_1.AST_NODE_TYPES.TSModuleDeclaration &&
56
+ statement.body?.type === utils_1.AST_NODE_TYPES.TSModuleBlock) {
57
+ collectImportDeclarations(statement.body.body, into);
58
+ }
59
+ }
60
+ }
61
+ /**
62
+ * The innermost binding of `name` visible from `scope`. A value-only binding
63
+ * stops the walk even though TypeScript would look past it for a type name;
64
+ * that errs toward silence, never toward a wrong report or fix.
65
+ */
66
+ function findVariable(scope, name) {
67
+ let current = scope;
68
+ while (current) {
69
+ const variable = current.set.get(name);
70
+ if (variable) {
71
+ return variable;
72
+ }
73
+ current = current.upper;
74
+ }
75
+ return undefined;
76
+ }
77
+ function importClauseOf(variable) {
78
+ if (!variable || variable.defs.length !== 1) {
79
+ return undefined;
80
+ }
81
+ const [definition] = variable.defs;
82
+ if (definition.type !== 'ImportBinding') {
83
+ return undefined;
84
+ }
85
+ const clause = definition.node;
86
+ return clause.type === utils_1.AST_NODE_TYPES.ImportSpecifier ||
87
+ clause.type === utils_1.AST_NODE_TYPES.ImportDefaultSpecifier ||
88
+ clause.type === utils_1.AST_NODE_TYPES.ImportNamespaceSpecifier
89
+ ? clause
90
+ : undefined;
91
+ }
92
+ function sourceOf(clause) {
93
+ return clause.parent.source.value;
94
+ }
95
+ function isNamedImportOf(clause, importedName, isAllowedSource) {
96
+ return (clause?.type === utils_1.AST_NODE_TYPES.ImportSpecifier &&
97
+ importedNameOf(clause) === importedName &&
98
+ isAllowedSource(sourceOf(clause)));
99
+ }
100
+ const isSxPropsSource = (source) => SX_PROPS_SOURCES.has(source);
101
+ const isThemeSource = (source) => source === THEME_SOURCE;
102
+ /**
103
+ * Inside `declare module '@mui/material/styles' { … }` the `Theme` in reach is
104
+ * the interface being augmented, and an import spliced into or above the
105
+ * ambient block changes what the declaration file means.
106
+ */
107
+ function isInsideThemeAugmentation(node) {
108
+ let current = node.parent;
109
+ while (current) {
110
+ if (current.type === utils_1.AST_NODE_TYPES.TSModuleDeclaration &&
111
+ current.id.type === utils_1.AST_NODE_TYPES.Literal &&
112
+ current.id.value === THEME_SOURCE) {
113
+ return true;
114
+ }
115
+ current = current.parent;
116
+ }
117
+ return false;
118
+ }
119
+ function isTypeOnlyImport(clause) {
120
+ const declaration = clause.parent;
121
+ return declaration.importKind === 'type' || clause.importKind === 'type';
122
+ }
123
+ exports.requireSxPropsTheme = (0, createRule_1.createRule)({
124
+ name: 'require-sx-props-theme',
125
+ meta: {
126
+ type: 'problem',
127
+ docs: {
128
+ description: "Require MUI `SxProps` to carry the app theme as `SxProps<Theme>`, because the bare type defaults to `SxProps<{}>`: `sx` callbacks lose the theme's typing and TypeScript pays for a structural comparison against MUI's own `SxProps<Theme>`",
129
+ recommended: 'error',
130
+ },
131
+ fixable: 'code',
132
+ schema: [],
133
+ messages: {
134
+ requireSxPropsTheme: "`{{typeName}}` has no theme argument, so MUI defaults it to `SxProps<{}>`: an `sx` callback's `theme` is typed `{}`, and TypeScript compares this type structurally against MUI's `SxProps<Theme>` wherever the two meet, which can cost seconds of type checking per file. Write `{{typeName}}<Theme>` with `Theme` from '@mui/material/styles'.",
135
+ },
136
+ },
137
+ defaultOptions: [],
138
+ create(context) {
139
+ const sourceCode = context.getSourceCode();
140
+ const program = sourceCode.ast;
141
+ const allImports = [];
142
+ collectImportDeclarations(program.body, allImports);
143
+ const sxPropsLocalNames = new Set();
144
+ const sxNamespaceLocalNames = new Set();
145
+ for (const declaration of allImports) {
146
+ if (!isSxPropsSource(declaration.source.value)) {
147
+ continue;
148
+ }
149
+ for (const specifier of declaration.specifiers) {
150
+ if (specifier.type === utils_1.AST_NODE_TYPES.ImportSpecifier &&
151
+ importedNameOf(specifier) === SX_PROPS_NAME) {
152
+ sxPropsLocalNames.add(specifier.local.name);
153
+ }
154
+ else if (specifier.type === utils_1.AST_NODE_TYPES.ImportNamespaceSpecifier) {
155
+ sxNamespaceLocalNames.add(specifier.local.name);
156
+ }
157
+ }
158
+ }
159
+ const importsSxProps = sxPropsLocalNames.size > 0 || sxNamespaceLocalNames.size > 0;
160
+ /**
161
+ * Only a top-level import can be extended or anchored after: one inside an
162
+ * ambient module block binds nothing outside it, and a file with no
163
+ * top-level import is a global script that a new import would turn into a
164
+ * module.
165
+ */
166
+ const topLevelImports = program.body.filter((statement) => statement.type === utils_1.AST_NODE_TYPES.ImportDeclaration);
167
+ const themeSourceImports = topLevelImports.filter((declaration) => isThemeSource(declaration.source.value));
168
+ /**
169
+ * An unresolved `Theme` elsewhere in the file (a global, or a name the file
170
+ * never declared) would silently change meaning once an import binds the
171
+ * name at module scope.
172
+ */
173
+ let hasUnresolvedTheme;
174
+ const themeIsReferencedUnresolved = () => {
175
+ if (hasUnresolvedTheme === undefined) {
176
+ const globalScope = sourceCode.scopeManager?.globalScope;
177
+ hasUnresolvedTheme = Boolean(globalScope?.through.some((reference) => reference.identifier.name === THEME_NAME));
178
+ }
179
+ return hasUnresolvedTheme;
180
+ };
181
+ function resolvesTo(scope, name, clause) {
182
+ return importClauseOf(findVariable(scope, name)) === clause;
183
+ }
184
+ function isReportableSxProps(typeName, scope) {
185
+ if (typeName.type === utils_1.AST_NODE_TYPES.Identifier) {
186
+ return (sxPropsLocalNames.has(typeName.name) &&
187
+ isNamedImportOf(importClauseOf(findVariable(scope, typeName.name)), SX_PROPS_NAME, isSxPropsSource));
188
+ }
189
+ if (typeName.type !== utils_1.AST_NODE_TYPES.TSQualifiedName ||
190
+ typeName.right.name !== SX_PROPS_NAME ||
191
+ typeName.left.type !== utils_1.AST_NODE_TYPES.Identifier ||
192
+ !sxNamespaceLocalNames.has(typeName.left.name)) {
193
+ return false;
194
+ }
195
+ const clause = importClauseOf(findVariable(scope, typeName.left.name));
196
+ return (clause?.type === utils_1.AST_NODE_TYPES.ImportNamespaceSpecifier &&
197
+ isSxPropsSource(sourceOf(clause)));
198
+ }
199
+ /** An existing `Theme` import, under whatever local name reaches the node. */
200
+ function existingThemeName(scope) {
201
+ if (isNamedImportOf(importClauseOf(findVariable(scope, THEME_NAME)), THEME_NAME, isThemeSource)) {
202
+ return THEME_NAME;
203
+ }
204
+ for (const declaration of themeSourceImports) {
205
+ for (const specifier of declaration.specifiers) {
206
+ if (specifier.type === utils_1.AST_NODE_TYPES.ImportSpecifier &&
207
+ importedNameOf(specifier) === THEME_NAME &&
208
+ resolvesTo(scope, specifier.local.name, specifier)) {
209
+ return specifier.local.name;
210
+ }
211
+ }
212
+ }
213
+ return undefined;
214
+ }
215
+ /** `Styles.Theme` through a namespace import, which adds no binding. */
216
+ function namespaceThemeText(scope) {
217
+ for (const declaration of themeSourceImports) {
218
+ for (const specifier of declaration.specifiers) {
219
+ if (specifier.type === utils_1.AST_NODE_TYPES.ImportNamespaceSpecifier &&
220
+ resolvesTo(scope, specifier.local.name, specifier)) {
221
+ return `${specifier.local.name}.${THEME_NAME}`;
222
+ }
223
+ }
224
+ }
225
+ return undefined;
226
+ }
227
+ /**
228
+ * Mirrors the declaration's own convention so a `verbatimModuleSyntax` or
229
+ * `consistent-type-imports` project stays clean: a type-only declaration
230
+ * takes a plain specifier, inline `type` specifiers call for another, and a
231
+ * declaration with no type marker follows how `SxProps` itself arrived.
232
+ */
233
+ function themeSpecifierText(declaration, sxPropsClause) {
234
+ if (declaration.importKind === 'type') {
235
+ return THEME_NAME;
236
+ }
237
+ const usesInlineType = declaration.specifiers.some((specifier) => specifier.type === utils_1.AST_NODE_TYPES.ImportSpecifier &&
238
+ specifier.importKind === 'type');
239
+ const sxPropsIsTypeOnly = sxPropsClause !== undefined &&
240
+ sxPropsClause.parent !== declaration &&
241
+ isTypeOnlyImport(sxPropsClause);
242
+ return usesInlineType || sxPropsIsTypeOnly
243
+ ? `type ${THEME_NAME}`
244
+ : THEME_NAME;
245
+ }
246
+ function extendNamedImport(declaration, specifiers, sxPropsClause) {
247
+ const text = themeSpecifierText(declaration, sxPropsClause);
248
+ const lastSpecifier = specifiers[specifiers.length - 1];
249
+ const openingBrace = sourceCode.getTokenBefore(specifiers[0]);
250
+ const isMultiline = openingBrace !== null &&
251
+ openingBrace.loc.end.line !== lastSpecifier.loc.start.line;
252
+ if (!isMultiline) {
253
+ return (fixer) => fixer.insertTextAfter(lastSpecifier, `, ${text}`);
254
+ }
255
+ const lineText = sourceCode.lines[lastSpecifier.loc.start.line - 1];
256
+ const indent = /^[\t ]*/.exec(lineText)?.[0] ?? '';
257
+ const trailingComma = sourceCode.getTokenAfter(lastSpecifier);
258
+ if (trailingComma?.value !== ',') {
259
+ return (fixer) => fixer.insertTextAfter(lastSpecifier, `,\n${indent}${text}`);
260
+ }
261
+ // A same-line comment after the trailing comma annotates the specifier
262
+ // above it, so the new specifier goes on the next line, below the comment.
263
+ let anchor = trailingComma;
264
+ for (const comment of sourceCode.getCommentsAfter(trailingComma)) {
265
+ if (comment.loc.start.line !== trailingComma.loc.end.line) {
266
+ break;
267
+ }
268
+ anchor = comment;
269
+ }
270
+ return (fixer) => fixer.insertTextAfterRange(anchor.range, `\n${indent}${text},`);
271
+ }
272
+ function insertNewImport(lastImport) {
273
+ const quote = lastImport.source.raw.startsWith('"') ? '"' : "'";
274
+ const lastToken = sourceCode.getLastToken(lastImport);
275
+ const semicolon = lastToken?.value === ';' ? ';' : '';
276
+ const statement = `import type { ${THEME_NAME} } from ${quote}${THEME_SOURCE}${quote}${semicolon}`;
277
+ // A trailing same-line comment stays with the import it annotates.
278
+ let anchor = lastImport;
279
+ for (const comment of sourceCode.getCommentsAfter(lastImport)) {
280
+ if (comment.loc.start.line !== lastImport.loc.end.line) {
281
+ break;
282
+ }
283
+ anchor = comment;
284
+ }
285
+ return (fixer) => fixer.insertTextAfterRange(anchor.range, `\n${statement}`);
286
+ }
287
+ function planFix(node, scope) {
288
+ if (isInsideThemeAugmentation(node)) {
289
+ return undefined;
290
+ }
291
+ const existing = existingThemeName(scope);
292
+ if (existing) {
293
+ return { themeText: existing };
294
+ }
295
+ // Adding a `Theme` binding is safe only where the name is free, both at
296
+ // the node and everywhere else in the file.
297
+ if (findVariable(scope, THEME_NAME) !== undefined ||
298
+ themeIsReferencedUnresolved()) {
299
+ const namespaced = namespaceThemeText(scope);
300
+ return namespaced ? { themeText: namespaced } : undefined;
301
+ }
302
+ const sxPropsClause = node.typeName.type === utils_1.AST_NODE_TYPES.Identifier
303
+ ? importClauseOf(findVariable(scope, node.typeName.name))
304
+ : undefined;
305
+ for (const declaration of themeSourceImports) {
306
+ const specifiers = declaration.specifiers.filter((specifier) => specifier.type === utils_1.AST_NODE_TYPES.ImportSpecifier);
307
+ if (specifiers.length > 0) {
308
+ return {
309
+ themeText: THEME_NAME,
310
+ importFix: extendNamedImport(declaration, specifiers, sxPropsClause),
311
+ };
312
+ }
313
+ }
314
+ const namespaced = namespaceThemeText(scope);
315
+ if (namespaced) {
316
+ return { themeText: namespaced };
317
+ }
318
+ const lastImport = topLevelImports[topLevelImports.length - 1];
319
+ if (!lastImport) {
320
+ return undefined;
321
+ }
322
+ return {
323
+ themeText: THEME_NAME,
324
+ importFix: insertNewImport(lastImport),
325
+ };
326
+ }
327
+ return {
328
+ TSTypeReference(node) {
329
+ if (!importsSxProps || hasTypeArguments(node)) {
330
+ return;
331
+ }
332
+ const scope = ASTHelpers_1.ASTHelpers.getScope(context, node);
333
+ if (!isReportableSxProps(node.typeName, scope)) {
334
+ return;
335
+ }
336
+ const plan = planFix(node, scope);
337
+ context.report({
338
+ node,
339
+ messageId: 'requireSxPropsTheme',
340
+ data: { typeName: sourceCode.getText(node.typeName) },
341
+ // Each report carries its own import edit so any single fix is
342
+ // complete on its own. Two reports editing the same import overlap,
343
+ // so ESLint applies one per pass and the next pass sees `Theme`
344
+ // imported and needs only the insertions.
345
+ fix: plan
346
+ ? (fixer) => {
347
+ const fixes = [
348
+ fixer.insertTextAfter(node.typeName, `<${plan.themeText}>`),
349
+ ];
350
+ if (plan.importFix) {
351
+ fixes.push(plan.importFix(fixer));
352
+ }
353
+ return fixes;
354
+ }
355
+ : null,
356
+ });
357
+ },
358
+ };
359
+ },
360
+ });
361
+ //# sourceMappingURL=require-sx-props-theme.js.map
@@ -465,6 +465,51 @@ declare module 'functions/src/util/assertSafe' {
465
465
  export function assertSafe<T extends PropertyKey>(key: T): T;
466
466
  }
467
467
  `;
468
+ /**
469
+ * `@mui/material/styles`, the module `require-sx-props-theme` draws `Theme`
470
+ * from, and `@mui/system/styleFunctionSx`, the deepest `SxProps` it rewrites.
471
+ * Under the wildcard alone a named import is a VALUE of type `any`, so writing
472
+ * `SxProps` or `Theme` in a type position is TS2709 on the input and output
473
+ * alike, and every pair of that fixer was held out. It earns a shape under the
474
+ * stated policy: the fix writes a type argument drawn from this module, and a
475
+ * wrong one — an unbound `Theme`, a second `Theme` import, a namespace member
476
+ * that does not exist — is a type error.
477
+ *
478
+ * `SxProps` is the shipped union from `styleFunctionSx.d.ts`, with
479
+ * `SystemStyleObject` reduced to an open record: the CSS property types add
480
+ * noise, not detection. `Theme` is open for the same reason, so a fixture
481
+ * reading `theme.palette.*` or augmenting the interface still compiles.
482
+ * `useTheme` and `createTheme` carry their shipped signatures because other
483
+ * rules' fixtures import them from here; the barrel `@mui/material` and
484
+ * `@mui/system` stay on the wildcard, since fixtures import members from them
485
+ * that exist in no typings (`FlexRow`, `CustomWidget`).
486
+ */
487
+ const MUI_SX_PROPS_SHAPE = `
488
+ type SystemStyleObject<Theme extends object = {}> = { [key: string]: any };
489
+ export type SxProps<Theme extends object = {}> =
490
+ | SystemStyleObject<Theme>
491
+ | ((theme: Theme) => SystemStyleObject<Theme>)
492
+ | ReadonlyArray<
493
+ | boolean
494
+ | SystemStyleObject<Theme>
495
+ | ((theme: Theme) => SystemStyleObject<Theme>)
496
+ >;
497
+ `;
498
+ const MUI_STYLES_STUB = `
499
+ declare module '@mui/material/styles' {
500
+ export interface Theme { [key: string]: any }
501
+ ${MUI_SX_PROPS_SHAPE}
502
+ export function useTheme<T = Theme>(): T;
503
+ export function createTheme(options?: any, ...args: object[]): Theme;
504
+ export const styled: any;
505
+ export type CSSObject = any;
506
+ export type Mixins = any;
507
+ export type Shadows = any;
508
+ }
509
+ declare module '@mui/system/styleFunctionSx' {
510
+ ${MUI_SX_PROPS_SHAPE}
511
+ }
512
+ `;
468
513
  exports.STUBS = `
469
514
  declare module '*';
470
515
  ${exports.REACT_STUB}
@@ -472,6 +517,7 @@ ${exports.FIRESTORE_ADMIN_STUB}
472
517
  ${exports.FIRESTORE_CLIENT_STUB}
473
518
  ${exports.SUBSTITUTION_PARTNER_STUBS}
474
519
  ${ASSERT_SAFE_STUB}
520
+ ${MUI_STYLES_STUB}
475
521
  declare namespace JSX {
476
522
  interface IntrinsicElements { [k: string]: any }
477
523
  interface Element {}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blumintinc/eslint-plugin-blumint",
3
- "version": "1.22.0",
3
+ "version": "1.23.0",
4
4
  "description": "Custom eslint rules for use within BluMint",
5
5
  "author": {
6
6
  "name": "Brodie McGuire",
@@ -1,4 +1,18 @@
1
1
  [
2
+ {
3
+ "version": "1.23.0",
4
+ "date": "2026-09-30T00:56:19.228Z",
5
+ "rules": [
6
+ {
7
+ "name": "require-sx-props-theme",
8
+ "changeType": "feat",
9
+ "issues": [
10
+ 2399
11
+ ],
12
+ "summary": "require a theme argument on MUI SxProps (closes #2399) (#2401)"
13
+ }
14
+ ]
15
+ },
2
16
  {
3
17
  "version": "1.22.0",
4
18
  "date": "2026-09-27T06:08:08.588Z",