Skip to content

Commit f8ba507

Browse files
committed
♻️ refactor(store)!: fix types
Signed-off-by: Pauline <git@ethanlibs.co>
1 parent 45d42c1 commit f8ba507

25 files changed

+2224
-1499
lines changed

package.json

+21-20
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"type": "module",
55
"version": "5.0.0",
66
"private": true,
7-
"packageManager": "pnpm@9.15.0",
7+
"packageManager": "pnpm@9.15.3",
88
"engineStrict": true,
99
"author": "@flowr",
1010
"contributors": [
@@ -35,43 +35,44 @@
3535
"yarn": "pnpm"
3636
},
3737
"scripts": {
38-
"build": "turbo run build --concurrency=4",
39-
"build:affected": "turbo run build --filter=...[origin/main] --concurrency=4",
40-
"start": "turbo run start --concurrency=4",
41-
"start:affected": "turbo run start --filter=...[origin/main] --concurrency=4",
42-
"docs": "turbo run docs --concurrency=4",
43-
"docs:affected": "turbo run docs --filter=...[origin/main] --concurrency=4",
38+
"build": "pnpm turbo run build --concurrency=4",
39+
"build:affected": "pnpm turbo run build --filter=...[origin/main] --concurrency=4",
40+
"start": "pnpm turbo run start --concurrency=4",
41+
"start:affected": "pnpm turbo run start --filter=...[origin/main] --concurrency=4",
42+
"docs": "pnpm turbo run docs --concurrency=4",
43+
"docs:affected": "pnpm turbo run docs --filter=...[origin/main] --concurrency=4",
4444
"lint": "eslint --cache . --flag unstable_ts_config",
4545
"lint:fix": "pnpm lint --fix",
4646
"test": "vitest run",
4747
"test:update": "vitest --update",
48-
"meta:release": "bumpp -r && turbo run release --concurrency=4",
49-
"meta:create": "turbo gen create-package --args"
48+
"meta:release": "bumpp -r",
49+
"turbo": "TURBO_TELEMETRY_DISABLED=1 turbo",
50+
"meta:create": "pnpm turbo gen create-package --args"
5051
},
5152
"devDependencies": {
52-
"@arethetypeswrong/cli": "^0.17.1",
53+
"@arethetypeswrong/cli": "^0.17.2",
5354
"@flowr/eslint": "workspace:^",
5455
"@turbo/gen": "^2.3.3",
5556
"@types/jsdom": "^21.1.7",
56-
"@types/node": "^22.10.1",
57+
"@types/node": "^22.10.5",
5758
"@vitest/coverage-v8": "^2.1.8",
5859
"@vitest/ui": "^2.1.8",
59-
"bumpp": "^9.9.0",
60+
"bumpp": "^9.9.2",
6061
"colorette": "^2.0.20",
6162
"destr": "^2.0.3",
6263
"esbuild": "^0.24.0",
6364
"esbuild-plugin-file-path-extensions": "^2.1.4",
64-
"eslint": "^9.16.0",
65-
"jiti": "^2.4.1",
65+
"eslint": "^9.17.0",
66+
"jiti": "^2.4.2",
6667
"jsdom": "^25.0.1",
6768
"jsr": "^0.13.2",
68-
"msw": "^2.6.8",
69-
"pathe": "^1.1.2",
69+
"msw": "^2.7.0",
70+
"pathe": "^2.0.0",
7071
"tsup": "^8.3.5",
7172
"tsx": "^4.19.2",
7273
"turbo": "^2.3.3",
7374
"typescript": "^5.7.2",
74-
"vite": "^6.0.3",
75+
"vite": "^6.0.7",
7576
"vitest": "^2.1.8"
7677
},
7778
"pnpm": {
@@ -81,9 +82,9 @@
8182
},
8283
"resolutions": {
8384
"@eslint-community/eslint-utils": "^4.4.1",
84-
"@typescript-eslint/utils": "^8.17.0",
85-
"esbuild": "^0.24.0",
86-
"eslint": "^9.16.0",
85+
"@typescript-eslint/utils": "^8.19.1",
86+
"esbuild": "^0.24.2",
87+
"eslint": "^9.17.0",
8788
"tsx": "^4.19.2"
8889
}
8990
}

packages/eslint-plugin/package.json

+3-3
Original file line numberDiff line numberDiff line change
@@ -61,12 +61,12 @@
6161
"build": "tsup"
6262
},
6363
"peerDependencies": {
64-
"eslint": "^9.16.0"
64+
"eslint": "^9.17.0"
6565
},
6666
"devDependencies": {
6767
"@flowr/utilities": "workspace:^",
68-
"@typescript-eslint/utils": "^8.17.0",
69-
"eslint": "^9.16.0",
68+
"@typescript-eslint/utils": "^8.19.1",
69+
"eslint": "^9.17.0",
7070
"eslint-vitest-rule-tester": "^0.7.1",
7171
"jsonc-eslint-parser": "^2.4.0"
7272
},

packages/eslint-plugin/src/rules/only-export-components.ts

+39-36
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,16 @@ export type Options = [{
1515
allowConstantExport?: boolean;
1616
checkJS?: boolean;
1717
allowExportNames?: string[];
18+
customHOCs?: string[];
1819
}];
1920

20-
const defaultOptions: Options = [{}];
21-
const possibleRegex = /^[A-Z][a-zA-Z0-9]*$/u;
22-
const strictRegex = /^[A-Z][\dA-Z]*[a-z][\dA-Za-z]*$/u;
23-
const reactHOCs = new Set(['forwardRef', 'memo']);
21+
const defaultOptions: Options = [{
22+
allowConstantExport: false,
23+
checkJS: false,
24+
allowExportNames: [],
25+
customHOCs: [],
26+
}];
27+
const reactComponentNameRE = /^[A-Z][a-zA-Z0-9]*$/u;
2428
type ToString<Type> = Type extends `${infer String}` ? String : never;
2529
const notReactComponentExpression: Set<ToString<TSESTree.Expression['type']>> = new Set([
2630
'ArrayExpression',
@@ -49,20 +53,30 @@ export default createEslintRule<Options, MessageIds>({
4953

5054
return {
5155
Program: (program) => {
52-
const ruleContext = {
53-
hasExports: false,
54-
mayHaveReactExport: false,
55-
reactIsInScope: false,
56-
};
56+
const ruleContext = { hasExports: false, hasReactExport: false, reactIsInScope: false };
5757
const localComponents: TSESTree.Identifier[] = [];
5858
const nonComponentExports: Array<TSESTree.BindingName | TSESTree.StringLiteral> = [];
59-
const allowExportNamesSet = options.allowExportNames ? new Set(options.allowExportNames) : undefined;
59+
const allowExportNames = new Set(options.allowExportNames);
60+
const reactHOCs = new Set(['forwardRef', 'memo', ...options.customHOCs!]);
6061
const reactContextExports: TSESTree.Identifier[] = [];
6162

63+
const canBeReactFunctionComponent = (init: TSESTree.VariableDeclaratorMaybeInit['init']): boolean => {
64+
if (!init)
65+
return false;
66+
67+
if (init.type === 'ArrowFunctionExpression')
68+
return true;
69+
70+
if (init.type === 'CallExpression' && init.callee.type === 'Identifier')
71+
return reactHOCs.has(init.callee.name);
72+
73+
return false;
74+
};
75+
6276
const handleLocalIdentifier = (id: TSESTree.BindingName): void => {
6377
if (id.type !== 'Identifier')
6478
return;
65-
if (possibleRegex.test(id.name))
79+
if (reactComponentNameRE.test(id.name))
6680
localComponents.push(id);
6781
};
6882

@@ -72,16 +86,16 @@ export default createEslintRule<Options, MessageIds>({
7286
return;
7387
}
7488

75-
if (allowExportNamesSet?.has(id.name))
89+
if (allowExportNames.has(id.name))
7690
return;
7791

7892
// Literal: 1, 'foo', UnaryExpression: -1, TemplateLiteral: `Some ${template}`, BinaryExpression: 24 * 60.
7993
if (options.allowConstantExport && init && ['BinaryExpression', 'Literal', 'TemplateLiteral', 'UnaryExpression'].includes(init.type))
8094
return;
8195

8296
if (isFn) {
83-
if (possibleRegex.test(id.name))
84-
ruleContext.mayHaveReactExport = true;
97+
if (reactComponentNameRE.test(id.name))
98+
ruleContext.hasReactExport = true;
8599
else nonComponentExports.push(id);
86100
}
87101
else {
@@ -100,9 +114,9 @@ export default createEslintRule<Options, MessageIds>({
100114
nonComponentExports.push(id);
101115
return;
102116
}
103-
if (!ruleContext.mayHaveReactExport && possibleRegex.test(id.name))
104-
ruleContext.mayHaveReactExport = true;
105-
if (!strictRegex.test(id.name))
117+
if (reactComponentNameRE.test(id.name))
118+
ruleContext.hasReactExport = true;
119+
else
106120
nonComponentExports.push(id);
107121
}
108122
};
@@ -116,17 +130,17 @@ export default createEslintRule<Options, MessageIds>({
116130
else handleExportIdentifier(node.id, true);
117131
else if (node.type === 'CallExpression')
118132
if (node.callee.type === 'CallExpression' && node.callee.callee.type === 'Identifier' && node.callee.callee.name === 'connect')
119-
ruleContext.mayHaveReactExport = true;
133+
ruleContext.hasReactExport = true;
120134
else if (node.callee.type !== 'Identifier')
121135
if (node.callee.type === 'MemberExpression' && node.callee.property.type === 'Identifier' && reactHOCs.has(node.callee.property.name))
122-
ruleContext.mayHaveReactExport = true;
136+
ruleContext.hasReactExport = true;
123137
else context.report({ messageId: 'anonymousExport', node });
124138
else if (!reactHOCs.has(node.callee.name))
125139
context.report({ messageId: 'anonymousExport', node });
126140
else if (node.arguments[0].type === 'FunctionExpression' && node.arguments[0].id)
127141
handleExportIdentifier(node.arguments[0].id, true);
128142
else if (node.arguments[0]?.type === 'Identifier')
129-
ruleContext.mayHaveReactExport = true;
143+
ruleContext.hasReactExport = true;
130144
else context.report({ messageId: 'anonymousExport', node });
131145
else if (node.type === 'TSEnumDeclaration')
132146
nonComponentExports.push(node.id);
@@ -141,11 +155,10 @@ export default createEslintRule<Options, MessageIds>({
141155
}
142156
else if (node.type === 'ExportDefaultDeclaration') {
143157
ruleContext.hasExports = true;
144-
const declaration
145-
= node.declaration.type === 'TSAsExpression'
158+
const declaration = node.declaration.type === 'TSAsExpression'
146159
|| node.declaration.type === 'TSSatisfiesExpression'
147-
? node.declaration.expression
148-
: node.declaration;
160+
? node.declaration.expression
161+
: node.declaration;
149162
if (
150163
declaration.type === 'VariableDeclaration'
151164
|| declaration.type === 'FunctionDeclaration'
@@ -181,7 +194,7 @@ export default createEslintRule<Options, MessageIds>({
181194
return;
182195

183196
if (ruleContext.hasExports)
184-
if (ruleContext.mayHaveReactExport) {
197+
if (ruleContext.hasReactExport) {
185198
nonComponentExports.forEach(node => context.report({ messageId: 'namedExport', node }));
186199
reactContextExports.forEach(node => context.report({ messageId: 'reactContext', node }));
187200
}
@@ -214,21 +227,11 @@ export default createEslintRule<Options, MessageIds>({
214227
allowConstantExport: { type: 'boolean' },
215228
allowExportNames: { items: { type: 'string' }, type: 'array' },
216229
checkJS: { type: 'boolean' },
230+
customHOCs: { type: 'array', items: { type: 'string' } },
217231
} satisfies Readonly<Record<keyof Options[0], JSONSchema4>>,
218232
type: 'object',
219233
}],
220234
type: 'problem',
221235
},
222236
name: RULE_NAME,
223237
});
224-
225-
function canBeReactFunctionComponent(init: TSESTree.Expression | null): boolean {
226-
if (!init)
227-
return false;
228-
if (init.type === 'ArrowFunctionExpression')
229-
return true;
230-
if (init.type === 'CallExpression' && init.callee.type === 'Identifier')
231-
return ['forwardRef', 'memo'].includes(init.callee.name);
232-
233-
return false;
234-
}

packages/eslint-plugin/tests/rules/only-export-components.test.ts

+10
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,11 @@ const valids: ValidTestCase[] = [
185185
name: 'Only React context',
186186
code: 'export const MyContext = createContext(\'test\');',
187187
},
188+
{
189+
name: 'Custom HOCs like mobx observer',
190+
code: 'const MyComponent = () => {}; export default observer(MyComponent);',
191+
options: [{ customHOCs: ['observer'] }],
192+
},
188193
];
189194

190195
const invalid: InvalidTestCase[] = [
@@ -286,6 +291,11 @@ const invalid: InvalidTestCase[] = [
286291
code: 'export const MyComponent = () => {}; export const MyContext = React.createContext(\'test\');',
287292
errors: [{ messageId: 'reactContext' }],
288293
},
294+
{
295+
name: 'should be invalid when custom HOC is used without adding it to the rule configuration',
296+
code: 'const MyComponent = () => {}; export default observer(MyComponent);',
297+
errors: [{ messageId: 'localComponents' }, { messageId: 'anonymousExport' }],
298+
},
289299
];
290300

291301
run({

0 commit comments

Comments
 (0)