@blumintinc/eslint-plugin-blumint 1.20.102 → 1.20.103

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
@@ -256,7 +256,7 @@ full closed loop is documented in agora's `.claude/skills/eslint-autonomy/SKILL.
256
256
  | [prevent-children-clobber](docs/rules/prevent-children-clobber.md) | Prevent JSX spreads from silently discarding props.children | ✅ | | | | |
257
257
  | [react-memoize-literals](docs/rules/react-memoize-literals.md) | Detect object, array, and function literals created in React components or hooks that create new references every render. Prefer memoized values (useMemo/useCallback) or module-level constants to keep referential stability. | ✅ | | | 💡 | |
258
258
  | [react-usememo-should-be-component](docs/rules/react-usememo-should-be-component.md) | Enforce that useMemo hooks explicitly returning JSX should be abstracted into separate React components | ✅ | | | | |
259
- | [require-dynamic-firebase-imports](docs/rules/require-dynamic-firebase-imports.md) | Enforce dynamic imports for Firebase dependencies | ✅ | | 🔧 | | |
259
+ | [require-dynamic-firebase-imports](docs/rules/require-dynamic-firebase-imports.md) | Enforce dynamic imports for Firebase dependencies | ✅ | | | | |
260
260
  | [require-hooks-default-params](docs/rules/require-hooks-default-params.md) | Enforce React hooks with optional parameters to default to an empty object | ✅ | | 🔧 | | |
261
261
  | [require-https-error](docs/rules/require-https-error.md) | Enforce using proprietary HttpsError instead of throw new Error or firebase-admin HttpsError in functions/src | ✅ | | | | |
262
262
  | [require-https-error-cause](docs/rules/require-https-error-cause.md) | Ensure HttpsError calls inside catch blocks pass the caught error as the fourth "cause" argument to preserve stack traces for monitoring. | ✅ | | | | |
package/lib/index.js CHANGED
@@ -223,7 +223,7 @@ function noFrontendImportsFromFunctionsPatterns(pattern) {
223
223
  module.exports = {
224
224
  meta: {
225
225
  name: '@blumintinc/eslint-plugin-blumint',
226
- version: '1.20.102',
226
+ version: '1.20.103',
227
227
  },
228
228
  parseOptions: {
229
229
  ecmaVersion: 2020,
@@ -1,6 +1,6 @@
1
- import { TSESLint, TSESTree } from '@typescript-eslint/utils';
1
+ import { TSESTree } from '@typescript-eslint/utils';
2
2
  export declare const RULE_NAME = "require-dynamic-firebase-imports";
3
- declare const _default: TSESLint.RuleModule<"requireDynamicImport", never[], {
3
+ declare const _default: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"requireDynamicImport", never[], {
4
4
  ImportDeclaration(node: TSESTree.ImportDeclaration): void;
5
5
  }>;
6
6
  export default _default;
@@ -3,79 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.RULE_NAME = void 0;
4
4
  const utils_1 = require("@typescript-eslint/utils");
5
5
  const createRule_1 = require("../utils/createRule");
6
- const ASTHelpers_1 = require("../utils/ASTHelpers");
7
6
  exports.RULE_NAME = 'require-dynamic-firebase-imports';
8
- /**
9
- * A specifier is erased at compile time when either the whole statement is an
10
- * `import type` or the specifier carries an inline `type` marker. Only such a
11
- * specifier is interchangeable with the `import type` the fixer hoists.
12
- */
13
- const isErasableTypeSpecifier = (declaration, specifier) => declaration.importKind === 'type' || specifier.importKind === 'type';
14
- /**
15
- * Whether `specifier` binds the same local name to the same export of
16
- * `importSource` in type position as `target` — i.e. whether it is exactly the
17
- * binding the hoisted `import type` would introduce.
18
- */
19
- const bindsSameTypeImport = (specifier, target, importSource) => {
20
- if (specifier.type !== utils_1.AST_NODE_TYPES.ImportSpecifier ||
21
- specifier.local.name !== target.local.name ||
22
- specifier.imported.name !== target.imported.name) {
23
- return false;
24
- }
25
- const declaration = specifier.parent;
26
- return (declaration?.type === utils_1.AST_NODE_TYPES.ImportDeclaration &&
27
- declaration.source.value === importSource &&
28
- isErasableTypeSpecifier(declaration, specifier));
29
- };
30
- /**
31
- * Read the module-scope import state off `Program.body` rather than a traversal
32
- * flag: `--fix` runs multiple passes, and a sibling report in an earlier pass may
33
- * already have hoisted the same `import type`. A flag set by the
34
- * `ImportDeclaration` visitor also depends on source order, so it would be stale
35
- * for any import that precedes the one being rewritten.
36
- */
37
- const findHoistedTypeImport = (program, target, importSource) => {
38
- for (const statement of program.body) {
39
- if (statement.type !== utils_1.AST_NODE_TYPES.ImportDeclaration) {
40
- continue;
41
- }
42
- const match = statement.specifiers.find((specifier) => bindsSameTypeImport(specifier, target, importSource));
43
- if (match) {
44
- return match;
45
- }
46
- }
47
- return undefined;
48
- };
49
- /**
50
- * The binding that makes hoisting `target` unsafe, or `null` when the name is
51
- * free (or already bound to the very import being hoisted).
52
- *
53
- * Resolution walks the scope chain of the rewritten import instead of stopping
54
- * at module scope, because a narrower shadow raises no TypeScript diagnostic yet
55
- * silently binds the hoisted type to the wrong declaration. Each hop resolves a
56
- * single step further up: the rewritten statement binds its own inline `type`
57
- * specifier in the enclosing function scope, which would otherwise mask the
58
- * colliding declaration above it. Bindings without a definition are ambient
59
- * (TypeScript lib types, configured globals); a module-scope import legally
60
- * shadows those, so they are not collisions.
61
- */
62
- const findCollidingBinding = (scope, target, importSource) => {
63
- let current = scope;
64
- while (current) {
65
- const existing = ASTHelpers_1.ASTHelpers.findVariableInScope(current, target.local.name);
66
- if (!existing) {
67
- return null;
68
- }
69
- if (existing.defs.length > 0) {
70
- const bindsTheDesiredImport = existing.defs.every((def) => bindsSameTypeImport(def.node, target, importSource));
71
- if (!bindsTheDesiredImport) {
72
- return existing;
73
- }
74
- }
75
- current = existing.scope.upper;
76
- }
77
- return null;
78
- };
79
7
  exports.default = (0, createRule_1.createRule)({
80
8
  name: exports.RULE_NAME,
81
9
  meta: {
@@ -84,7 +12,6 @@ exports.default = (0, createRule_1.createRule)({
84
12
  description: 'Enforce dynamic imports for Firebase dependencies',
85
13
  recommended: 'error',
86
14
  },
87
- fixable: 'code',
88
15
  schema: [],
89
16
  messages: {
90
17
  requireDynamicImport: 'Static Firebase import from "{{importSource}}" keeps the Firebase SDK in the initial bundle and blocks route-level code splitting. Use an `await import(\'{{importSource}}\')` dynamic import so Firebase loads only on the code path that needs it; type-only imports remain allowed.',
@@ -96,64 +23,8 @@ exports.default = (0, createRule_1.createRule)({
96
23
  return (source.startsWith('firebase/') ||
97
24
  source.includes('config/firebase-client'));
98
25
  };
99
- /**
100
- * `await import()` is only valid inside an async function. Rewriting a
101
- * module-scope import would introduce top-level await, silently converting
102
- * a synchronous module into an async one (and failing outright on build
103
- * targets without top-level await support) — so those sites are reported
104
- * without a fix.
105
- */
106
- const isInsideAsyncFunction = (node) => {
107
- let current = node.parent;
108
- while (current) {
109
- if (current.type === utils_1.AST_NODE_TYPES.FunctionDeclaration ||
110
- current.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
111
- current.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression) {
112
- // The nearest enclosing function decides await validity; an async
113
- // ancestor beyond a sync function cannot host the await.
114
- return current.async;
115
- }
116
- current = current.parent;
117
- }
118
- return false;
119
- };
120
26
  const isTypeOnlySpecifier = (spec) => spec.type === utils_1.AST_NODE_TYPES.ImportSpecifier &&
121
27
  spec.importKind === 'type';
122
- const buildDynamicImport = (importSource, valueSpecifiers) => {
123
- if (valueSpecifiers.length === 0) {
124
- // Side-effect imports have no bindings to destructure.
125
- return `await import('${importSource}');`;
126
- }
127
- const namespaceSpecifier = valueSpecifiers.find((spec) => spec.type === utils_1.AST_NODE_TYPES.ImportNamespaceSpecifier);
128
- const defaultSpecifier = valueSpecifiers.find((spec) => spec.type === utils_1.AST_NODE_TYPES.ImportDefaultSpecifier);
129
- const namedSpecifiers = valueSpecifiers.filter((spec) => spec.type === utils_1.AST_NODE_TYPES.ImportSpecifier);
130
- if (namespaceSpecifier) {
131
- // The promise resolved by `import()` IS the module namespace object,
132
- // so the `* as ns` binding maps directly onto it.
133
- const namespaceDeclaration = `const ${namespaceSpecifier.local.name} = await import('${importSource}');`;
134
- if (defaultSpecifier) {
135
- return `${namespaceDeclaration} const ${defaultSpecifier.local.name} = ${namespaceSpecifier.local.name}.default;`;
136
- }
137
- return namespaceDeclaration;
138
- }
139
- const destructuredNames = namedSpecifiers.map((spec) => spec.imported.name === spec.local.name
140
- ? spec.local.name
141
- : `${spec.imported.name}: ${spec.local.name}`);
142
- if (defaultSpecifier) {
143
- if (destructuredNames.length === 0) {
144
- return `const ${defaultSpecifier.local.name} = (await import('${importSource}')).default;`;
145
- }
146
- // The namespace object exposes the default export under `default`.
147
- return `const { default: ${defaultSpecifier.local.name}, ${destructuredNames.join(', ')} } = await import('${importSource}');`;
148
- }
149
- return `const { ${destructuredNames.join(', ')} } = await import('${importSource}');`;
150
- };
151
- const buildStaticTypeImport = (importSource, typeSpecifiers) => {
152
- const names = typeSpecifiers.map((spec) => spec.imported.name === spec.local.name
153
- ? spec.local.name
154
- : `${spec.imported.name} as ${spec.local.name}`);
155
- return `import type { ${names.join(', ')} } from '${importSource}';\n`;
156
- };
157
28
  return {
158
29
  ImportDeclaration(node) {
159
30
  const importSource = node.source.value;
@@ -166,50 +37,23 @@ exports.default = (0, createRule_1.createRule)({
166
37
  if (node.importKind === 'type') {
167
38
  return;
168
39
  }
169
- const typeSpecifiers = node.specifiers.filter(isTypeOnlySpecifier);
170
40
  const valueSpecifiers = node.specifiers.filter((spec) => !isTypeOnlySpecifier(spec));
171
41
  // Inline `type` markers on every specifier make the whole statement
172
42
  // erasable, exactly like `import type` — nothing to report.
173
43
  if (node.specifiers.length > 0 && valueSpecifiers.length === 0) {
174
44
  return;
175
45
  }
46
+ // The rule is report-only. A static `import` declaration is legal only
47
+ // at the top level of a module or namespace, so every reported site is
48
+ // at module scope, where the `await import()` replacement would
49
+ // introduce top-level await — silently converting a synchronous module
50
+ // into an async one and breaking build targets that lack it. The
51
+ // author must instead move the import into the async code path that
52
+ // needs it, which is a restructuring no autofix can make safely.
176
53
  context.report({
177
54
  node,
178
55
  messageId: 'requireDynamicImport',
179
56
  data: { importSource },
180
- fix(fixer) {
181
- if (!isInsideAsyncFunction(node)) {
182
- return null;
183
- }
184
- // Type specifiers must not travel into the runtime destructuring:
185
- // they have no runtime value, and dropping the `type` marker turns
186
- // type references into dangling value bindings. They hoist into a
187
- // static `import type` at module scope, which is erased at compile
188
- // time.
189
- const scope = ASTHelpers_1.ASTHelpers.getScope(context, node);
190
- const program = context.getSourceCode().ast;
191
- const specifiersToHoist = [];
192
- for (const specifier of typeSpecifiers) {
193
- if (findCollidingBinding(scope, specifier, importSource)) {
194
- // Declining the whole fix keeps the file compiling: applying
195
- // the dynamic import while skipping a colliding hoist would
196
- // strand the type reference (TS2749) instead of duplicating an
197
- // identifier (TS2440/TS2300). The report stands so the author
198
- // resolves the name clash deliberately.
199
- return null;
200
- }
201
- if (!findHoistedTypeImport(program, specifier, importSource)) {
202
- specifiersToHoist.push(specifier);
203
- }
204
- }
205
- const fixes = [
206
- fixer.replaceText(node, buildDynamicImport(importSource, valueSpecifiers)),
207
- ];
208
- if (specifiersToHoist.length > 0) {
209
- fixes.push(fixer.insertTextBeforeRange([0, 0], buildStaticTypeImport(importSource, specifiersToHoist)));
210
- }
211
- return fixes;
212
- },
213
57
  });
214
58
  },
215
59
  };
@@ -505,6 +505,7 @@ const DEFAULT_BINDING = 'default';
505
505
  */
506
506
  const moduleResolutionCache = new Map();
507
507
  const propLessBindingCache = new Map();
508
+ const unionMemberCache = new Map();
508
509
  /**
509
510
  * Stat a candidate path, returning its identity stamp when it is a file. The
510
511
  * single stat both resolves existence and stamps the file, so proving a child
@@ -902,6 +903,170 @@ function getDependencyPropsSourceType(program, depName) {
902
903
  }
903
904
  return null;
904
905
  }
906
+ /**
907
+ * Name one arm of a union, and flatten whatever the arm turns out to be.
908
+ *
909
+ * Only an arm spelled as a type *reference* yields a name: an inline object arm
910
+ * has no name a parent could compose with, and an intersection arm's members
911
+ * describe a fragment of the arm rather than the arm itself. A named arm is
912
+ * followed to its definition too, because an alias of a union flattens into the
913
+ * enclosing union in TypeScript, so its own arms are arms here.
914
+ */
915
+ function collectUnionArmNames(arm, program, seenAliases, into) {
916
+ if (arm.type === utils_1.AST_NODE_TYPES.TSUnionType) {
917
+ for (const nested of arm.types) {
918
+ collectUnionArmNames(nested, program, seenAliases, into);
919
+ }
920
+ return;
921
+ }
922
+ if (arm.type !== utils_1.AST_NODE_TYPES.TSTypeReference) {
923
+ return;
924
+ }
925
+ const name = getTypeReferenceName(arm);
926
+ if (name === 'Readonly') {
927
+ // A `Readonly<X>` arm is the X arm: the wrapper adds no surface of its own.
928
+ const inner = arm.typeParameters?.params[0];
929
+ if (inner) {
930
+ collectUnionArmNames(inner, program, seenAliases, into);
931
+ }
932
+ return;
933
+ }
934
+ if (!name || seenAliases.has(name)) {
935
+ return;
936
+ }
937
+ into.add(name);
938
+ const alias = findPropsTypeAliasByName(program, name);
939
+ if (alias) {
940
+ const nextSeen = new Set(seenAliases);
941
+ nextSeen.add(name);
942
+ collectUnionArmNames(alias.typeAnnotation, program, nextSeen, into);
943
+ }
944
+ }
945
+ /**
946
+ * Collect the named arms of a props type that resolves to a union, following
947
+ * `Readonly<...>` wrappers and named-alias indirection within `program` to reach
948
+ * it. A props type that is not a union contributes nothing, so a single-shape
949
+ * child keeps demanding composition with its own name.
950
+ *
951
+ * Resolving *to* the union never names anything on the way: only an arm of a
952
+ * real union is a member, so an alias chain that ends at a single object type
953
+ * credits none of the names it passed through.
954
+ */
955
+ function collectUnionMemberNames(typeNode, program, seenAliases = new Set(), into = new Set()) {
956
+ if (!typeNode) {
957
+ return into;
958
+ }
959
+ if (typeNode.type === utils_1.AST_NODE_TYPES.TSUnionType) {
960
+ for (const arm of typeNode.types) {
961
+ collectUnionArmNames(arm, program, seenAliases, into);
962
+ }
963
+ return into;
964
+ }
965
+ if (typeNode.type === utils_1.AST_NODE_TYPES.TSTypeReference) {
966
+ const name = getTypeReferenceName(typeNode);
967
+ if (name === 'Readonly') {
968
+ const inner = typeNode.typeParameters?.params[0];
969
+ if (inner) {
970
+ collectUnionMemberNames(inner, program, seenAliases, into);
971
+ }
972
+ return into;
973
+ }
974
+ if (name && !seenAliases.has(name)) {
975
+ const alias = findPropsTypeAliasByName(program, name);
976
+ if (alias) {
977
+ const nextSeen = new Set(seenAliases);
978
+ nextSeen.add(name);
979
+ collectUnionMemberNames(alias.typeAnnotation, program, nextSeen, into);
980
+ }
981
+ }
982
+ }
983
+ return into;
984
+ }
985
+ /**
986
+ * The props type node a module exports alongside `binding`: its
987
+ * `{Binding}Props` alias when one exists, otherwise the exported component's
988
+ * own first-parameter annotation. Mirrors getDependencyPropsSourceType for a
989
+ * foreign program, resolving the export's local name first so
990
+ * `const X = …; export { X as Y }` reads X's props type.
991
+ */
992
+ function getExportedPropsSourceType(program, binding) {
993
+ if (binding === DEFAULT_BINDING) {
994
+ const fn = findDefaultExportFunction(program);
995
+ return fn ? getFirstParamTypeNode(fn) : null;
996
+ }
997
+ const local = findExportedLocalName(program, binding);
998
+ return local === null ? null : getDependencyPropsSourceType(program, local);
999
+ }
1000
+ /**
1001
+ * The union arms of a dependency's props type as declared in the sibling module
1002
+ * it is imported from. Resolution mirrors isPropLessImportedComponent — a
1003
+ * relative specifier, a file that exists on disk, no shadowing declaration
1004
+ * inside the rendering component — so anything the parse cannot decide yields
1005
+ * no members and the composition requirement stands.
1006
+ */
1007
+ function getImportedDependencyUnionMembers(program, localName, filename, componentRoot, cwd) {
1008
+ const relativeImport = findRelativeImport(program, localName);
1009
+ if (!relativeImport) {
1010
+ return [];
1011
+ }
1012
+ if (isNameDeclaredWithin(componentRoot, localName)) {
1013
+ return [];
1014
+ }
1015
+ try {
1016
+ const absolute = path_1.default.isAbsolute(filename)
1017
+ ? filename
1018
+ : path_1.default.resolve(cwd, filename);
1019
+ const resolved = resolveRelativeModule(path_1.default.dirname(absolute), relativeImport.source);
1020
+ if (!resolved) {
1021
+ return [];
1022
+ }
1023
+ const cacheKey = JSON.stringify([
1024
+ resolved.filePath,
1025
+ relativeImport.binding,
1026
+ ]);
1027
+ const cached = unionMemberCache.get(cacheKey);
1028
+ if (cached !== undefined &&
1029
+ cached.mtimeMs === resolved.mtimeMs &&
1030
+ cached.size === resolved.size) {
1031
+ return cached.members;
1032
+ }
1033
+ const childProgram = parseModuleProgram(resolved.filePath);
1034
+ const members = childProgram
1035
+ ? Array.from(collectUnionMemberNames(getExportedPropsSourceType(childProgram, relativeImport.binding), childProgram))
1036
+ : [];
1037
+ unionMemberCache.set(cacheKey, {
1038
+ mtimeMs: resolved.mtimeMs,
1039
+ size: resolved.size,
1040
+ members,
1041
+ });
1042
+ return members;
1043
+ }
1044
+ catch {
1045
+ return [];
1046
+ }
1047
+ }
1048
+ /**
1049
+ * Every name the file under lint can use to reference an imported type: the
1050
+ * exported name itself plus each `import { Exported as Local }` rename of it.
1051
+ * A union member declared in a sibling module is written with the local
1052
+ * spelling at the composition site.
1053
+ */
1054
+ function collectImportSpellings(program, importedName) {
1055
+ const spellings = [importedName];
1056
+ for (const stmt of program.body) {
1057
+ if (stmt.type !== utils_1.AST_NODE_TYPES.ImportDeclaration)
1058
+ continue;
1059
+ for (const specifier of stmt.specifiers) {
1060
+ if (specifier.type === utils_1.AST_NODE_TYPES.ImportSpecifier &&
1061
+ specifier.imported.type === utils_1.AST_NODE_TYPES.Identifier &&
1062
+ specifier.imported.name === importedName &&
1063
+ specifier.local.name !== importedName) {
1064
+ spellings.push(specifier.local.name);
1065
+ }
1066
+ }
1067
+ }
1068
+ return spellings;
1069
+ }
905
1070
  exports.requirePropsComposition = (0, createRule_1.createRule)({
906
1071
  name: 'require-props-composition',
907
1072
  meta: {
@@ -1019,6 +1184,25 @@ exports.requirePropsComposition = (0, createRule_1.createRule)({
1019
1184
  checkComponentWithProgram(componentName, node, node.id, prog);
1020
1185
  },
1021
1186
  };
1187
+ /**
1188
+ * Whether the parent's props type composes with a member of `dep`'s props
1189
+ * union. Both spellings the member can take are tried: the in-file
1190
+ * declaration, and the sibling module's export (under the exported name and
1191
+ * under any local rename the parent imports it as).
1192
+ *
1193
+ * A dependency whose props type is not a union yields no members, so this
1194
+ * never credits a parent that composes with nothing.
1195
+ */
1196
+ function composesWithUnionMember(dep, propsTypeNode, prog, componentRoot) {
1197
+ const localMembers = collectUnionMemberNames(getDependencyPropsSourceType(prog, dep), prog);
1198
+ for (const member of localMembers) {
1199
+ if (typeNodeComposesWithProps(propsTypeNode, member, prog)) {
1200
+ return true;
1201
+ }
1202
+ }
1203
+ const importedMembers = getImportedDependencyUnionMembers(prog, dep, rawFilename, componentRoot, cwd);
1204
+ return importedMembers.some((member) => collectImportSpellings(prog, member).some((spelling) => typeNodeComposesWithProps(propsTypeNode, spelling, prog)));
1205
+ }
1022
1206
  function checkComponentWithProgram(componentName, funcNode, reportNode, prog) {
1023
1207
  // Collect all JSX element names used in the component body
1024
1208
  const body = funcNode.body ?? funcNode;
@@ -1084,6 +1268,25 @@ exports.requirePropsComposition = (0, createRule_1.createRule)({
1084
1268
  missingComposition.push(dep);
1085
1269
  }
1086
1270
  }
1271
+ // A child whose props type is a union is composed by composing with any
1272
+ // one of its MEMBERS: `Pick<ChipToggleProps, 'label'>` inherits exactly
1273
+ // the surface the child accepts on that arm, so the DRY guarantee holds
1274
+ // just as it does for the union alias itself. This mirrors on the child
1275
+ // side the `.some` rule issue #1343 established on the parent side.
1276
+ //
1277
+ // Deferred behind the whole cheap pass because resolving a sibling
1278
+ // module's members reads it off disk: a file that already composes never
1279
+ // pays for it.
1280
+ if (missingComposition.length > 0 &&
1281
+ (requireAllDependencies || composedWith.size === 0)) {
1282
+ for (let index = missingComposition.length - 1; index >= 0; index--) {
1283
+ const dep = missingComposition[index];
1284
+ if (composesWithUnionMember(dep, propsTypeNode, prog, funcNode)) {
1285
+ composedWith.add(dep);
1286
+ missingComposition.splice(index, 1);
1287
+ }
1288
+ }
1289
+ }
1087
1290
  if (!requireAllDependencies) {
1088
1291
  // Only flag when NO dependency has composition
1089
1292
  if (composedWith.size > 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blumintinc/eslint-plugin-blumint",
3
- "version": "1.20.102",
3
+ "version": "1.20.103",
4
4
  "description": "Custom eslint rules for use within BluMint",
5
5
  "author": {
6
6
  "name": "Brodie McGuire",
@@ -1,4 +1,26 @@
1
1
  [
2
+ {
3
+ "version": "1.20.103",
4
+ "date": "2026-08-04T18:19:46.054Z",
5
+ "rules": [
6
+ {
7
+ "name": "require-dynamic-firebase-imports",
8
+ "changeType": "fix",
9
+ "issues": [
10
+ 1708
11
+ ],
12
+ "summary": "drop the unreachable autofix declaration (closes #1708)"
13
+ },
14
+ {
15
+ "name": "require-props-composition",
16
+ "changeType": "fix",
17
+ "issues": [
18
+ 1709
19
+ ],
20
+ "summary": "credit composition with a member of the child's props union (closes #1709)"
21
+ }
22
+ ]
23
+ },
2
24
  {
3
25
  "version": "1.20.102",
4
26
  "date": "2026-08-04T17:20:02.924Z",