Skip to content

Commit 3b7208e

Browse files
feat: pressto eslint plugin (#15)
* feat: set up pressto plugin * feat: integrate eslint-plugin-pressto and update documentation * chore: downgrade eslint-plugin-pressto version to 0.1.0 * docs: enhance README with ESLint plugin recommendation for 'worklet' directive * docs: update README to remove ESLint plugin installation details and add documentation link
1 parent 9e0a1b1 commit 3b7208e

12 files changed

Lines changed: 509 additions & 402 deletions

File tree

README.md

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ function BasicPressablesExample() {
5656
import { createAnimatedPressable } from 'pressto';
5757

5858
const PressableRotate = createAnimatedPressable((progress) => {
59-
'worklet';
59+
'worklet'; // I recommend installing the eslint plugin below to avoid forgetting the worklet.
6060
return {
6161
transform: [{ rotate: `${(progress * Math.PI) / 4}rad` }],
6262
};
@@ -74,6 +74,28 @@ function CustomPressableExample() {
7474
}
7575
```
7676

77+
> **⚠️ Important:** Notice the `'worklet';` directive at the start of the animation function. This is **required** for the function to run on the UI thread with React Native Reanimated.
78+
79+
#### ESLint Plugin (Recommended)
80+
81+
Install the ESLint plugin to automatically catch missing `'worklet'` directives:
82+
83+
```sh
84+
npm install -D eslint-plugin-pressto
85+
# or
86+
bun add -D eslint-plugin-pressto
87+
```
88+
89+
**Why you need this:** Forgetting the `'worklet'` directive causes:
90+
91+
- ❌ Runtime errors or crashes
92+
- ❌ Animations running on the JS thread (poor performance)
93+
- ❌ Unexpected behavior with Reanimated shared values
94+
95+
The ESLint plugin catches these issues **at development time**, saving you debugging time. See the [ESLint Plugin section](#eslint-plugin) below for configuration.
96+
97+
See the [eslint-plugin-pressto documentation](https://github.com/enzomanuelmangano/pressto/tree/main/eslint-plugin-pressto) for more details.
98+
7799
### Advanced: Using interaction states
78100

79101
```jsx
@@ -310,6 +332,14 @@ function App() {
310332
}
311333
```
312334

335+
## Repository Structure
336+
337+
This is a monorepo containing:
338+
339+
- **pressto** - The main library (root package)
340+
- **eslint-plugin-pressto** - Standalone ESLint plugin
341+
- **example** - Example app
342+
313343
## Contributing
314344

315345
Contributions are welcome! Please see our [contributing guide](CONTRIBUTING.md) for more details.

bun.lock

Lines changed: 130 additions & 377 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

eslint-plugin-pressto/LICENSE

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
MIT License
2+
3+
Copyright (c) 2024 Enzo Manuel Mangano
4+
Permission is hereby granted, free of charge, to any person obtaining a copy
5+
of this software and associated documentation files (the "Software"), to deal
6+
in the Software without restriction, including without limitation the rights
7+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8+
copies of the Software, and to permit persons to whom the Software is
9+
furnished to do so, subject to the following conditions:
10+
11+
The above copyright notice and this permission notice shall be included in all
12+
copies or substantial portions of the Software.
13+
14+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20+
SOFTWARE.

eslint-plugin-pressto/README.md

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
# eslint-plugin-pressto
2+
3+
ESLint plugin for [pressto](https://github.com/enzomanuelmangano/pressto) - enforces best practices when using pressto's animated components.
4+
5+
## Installation
6+
7+
```bash
8+
npm install --save-dev eslint-plugin-pressto
9+
# or
10+
yarn add -D eslint-plugin-pressto
11+
# or
12+
bun add -D eslint-plugin-pressto
13+
```
14+
15+
## Usage
16+
17+
### With ESLint Flat Config (`eslint.config.js`)
18+
19+
```javascript
20+
const presstoPlugin = require('eslint-plugin-pressto');
21+
22+
module.exports = [
23+
{
24+
plugins: {
25+
pressto: presstoPlugin,
26+
},
27+
rules: {
28+
'pressto/require-worklet-directive': 'error',
29+
},
30+
},
31+
];
32+
```
33+
34+
### With Legacy Config (`.eslintrc.js`)
35+
36+
```javascript
37+
module.exports = {
38+
plugins: ['pressto'],
39+
rules: {
40+
'pressto/require-worklet-directive': 'error',
41+
},
42+
};
43+
```
44+
45+
## Rules
46+
47+
### `pressto/require-worklet-directive`
48+
49+
Enforces the use of `'worklet'` directive in functions passed to `createAnimatedPressable`.
50+
51+
When using `createAnimatedPressable`, the animation function must run on the UI thread using React Native Reanimated. This requires a `'worklet'` directive as the first statement in the function body.
52+
53+
#### Rule Details
54+
55+
❌ Examples of **incorrect** code:
56+
57+
```javascript
58+
const AnimatedButton = createAnimatedPressable(() => {
59+
// Missing 'worklet' directive
60+
return {
61+
opacity: withSpring(1),
62+
};
63+
});
64+
65+
// Arrow function with implicit return (not supported)
66+
const AnimatedButton = createAnimatedPressable(() => ({
67+
opacity: withSpring(1),
68+
}));
69+
```
70+
71+
✅ Examples of **correct** code:
72+
73+
```javascript
74+
const AnimatedButton = createAnimatedPressable(() => {
75+
'worklet';
76+
return {
77+
opacity: withSpring(1),
78+
};
79+
});
80+
81+
const AnimatedButton = createAnimatedPressable(function() {
82+
'worklet';
83+
return {
84+
opacity: withSpring(1),
85+
};
86+
});
87+
```
88+
89+
## Why This Plugin?
90+
91+
The `'worklet'` directive is required for functions that need to run on the UI thread in React Native Reanimated. Forgetting to add it can lead to runtime errors or unexpected behavior. This plugin helps catch these issues during development.
92+
93+
## Related
94+
95+
- [pressto](https://github.com/enzomanuelmangano/pressto) - Custom React Native touchables with animations
96+
- [React Native Reanimated](https://docs.swmansion.com/react-native-reanimated/) - React Native's Animated library reimplemented
97+
98+
## License
99+
100+
MIT

eslint-plugin-pressto/index.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/**
2+
* ESLint plugin for pressto
3+
* Enforces best practices when using pressto's createAnimatedPressable
4+
*/
5+
module.exports = {
6+
rules: {
7+
'require-worklet-directive': require('./rules/require-worklet-directive'),
8+
},
9+
};

eslint-plugin-pressto/package.json

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
{
2+
"name": "eslint-plugin-pressto",
3+
"version": "0.1.0",
4+
"description": "ESLint plugin for pressto - enforces worklet directives in createAnimatedPressable functions",
5+
"main": "index.js",
6+
"keywords": [
7+
"eslint",
8+
"eslint-plugin",
9+
"pressto",
10+
"react-native",
11+
"reanimated",
12+
"worklet",
13+
"linter"
14+
],
15+
"repository": {
16+
"type": "git",
17+
"url": "git+https://github.com/enzomanuelmangano/pressto.git",
18+
"directory": "eslint-plugin-pressto"
19+
},
20+
"author": "Enzo Manuel Mangano <enzomanuelmangano@gmail.com> (https://github.com/enzomanuelmangano)",
21+
"license": "MIT",
22+
"bugs": {
23+
"url": "https://github.com/enzomanuelmangano/pressto/issues"
24+
},
25+
"homepage": "https://github.com/enzomanuelmangano/pressto#readme",
26+
"peerDependencies": {
27+
"eslint": "^7.0.0 || ^8.0.0 || ^9.0.0"
28+
},
29+
"engines": {
30+
"node": ">=12.0.0"
31+
},
32+
"files": [
33+
"index.js",
34+
"rules/"
35+
]
36+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/**
2+
* ESLint rule to enforce 'worklet' directive in createAnimatedPressable functions
3+
* @type {import('eslint').Rule.RuleModule}
4+
*/
5+
module.exports = {
6+
meta: {
7+
type: 'problem',
8+
docs: {
9+
description:
10+
"Enforce 'worklet' directive in functions passed to createAnimatedPressable",
11+
category: 'Best Practices',
12+
recommended: true,
13+
},
14+
messages: {
15+
missingWorklet:
16+
"Missing 'worklet' directive in createAnimatedPressable function. Add 'worklet'; as the first statement in the function body.",
17+
},
18+
schema: [],
19+
},
20+
21+
create(context) {
22+
return {
23+
CallExpression(node) {
24+
// Check if this is a call to createAnimatedPressable
25+
if (
26+
node.callee.type === 'Identifier' &&
27+
node.callee.name === 'createAnimatedPressable'
28+
) {
29+
// Get the first argument (the animation function)
30+
const firstArg = node.arguments[0];
31+
32+
if (!firstArg) {
33+
return;
34+
}
35+
36+
// Check if it's an arrow function or function expression
37+
if (
38+
firstArg.type === 'ArrowFunctionExpression' ||
39+
firstArg.type === 'FunctionExpression'
40+
) {
41+
let body = firstArg.body;
42+
43+
// If it's an arrow function with implicit return (no block), it's invalid
44+
if (firstArg.type === 'ArrowFunctionExpression' && body.type !== 'BlockStatement') {
45+
context.report({
46+
node: firstArg,
47+
messageId: 'missingWorklet',
48+
});
49+
return;
50+
}
51+
52+
// Check if the function has a block statement
53+
if (body.type === 'BlockStatement') {
54+
const statements = body.body;
55+
56+
if (statements.length === 0) {
57+
context.report({
58+
node: firstArg,
59+
messageId: 'missingWorklet',
60+
});
61+
return;
62+
}
63+
64+
const firstStatement = statements[0];
65+
66+
// Check if the first statement is an expression statement with a 'worklet' directive
67+
if (
68+
firstStatement.type === 'ExpressionStatement' &&
69+
firstStatement.expression.type === 'Literal' &&
70+
firstStatement.expression.value === 'worklet'
71+
) {
72+
// Valid - has worklet directive
73+
return;
74+
}
75+
76+
// Missing worklet directive
77+
context.report({
78+
node: firstArg,
79+
messageId: 'missingWorklet',
80+
});
81+
}
82+
}
83+
}
84+
},
85+
};
86+
},
87+
};

eslint.config.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
const reactNativePlugin = require('@react-native/eslint-plugin');
2+
const reactPlugin = require('eslint-plugin-react');
3+
const reactHooksPlugin = require('eslint-plugin-react-hooks');
4+
const prettierConfig = require('eslint-config-prettier');
5+
const tsParser = require('@typescript-eslint/parser');
6+
const presstoPlugin = require('eslint-plugin-pressto');
7+
8+
module.exports = [
9+
{
10+
files: ['**/*.{js,jsx,ts,tsx}'],
11+
languageOptions: {
12+
parser: tsParser,
13+
parserOptions: {
14+
ecmaVersion: 'latest',
15+
sourceType: 'module',
16+
ecmaFeatures: {
17+
jsx: true,
18+
},
19+
},
20+
},
21+
plugins: {
22+
'react': reactPlugin,
23+
'react-hooks': reactHooksPlugin,
24+
'@react-native': reactNativePlugin,
25+
'pressto': presstoPlugin,
26+
},
27+
rules: {
28+
'react/react-in-jsx-scope': 'off',
29+
'pressto/require-worklet-directive': 'error',
30+
...prettierConfig.rules,
31+
},
32+
},
33+
{
34+
ignores: ['node_modules/', 'lib/', 'coverage/', 'dist/'],
35+
},
36+
];

example/.eslintignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
node_modules/
2+
.expo/
3+
dist/
4+
ios/
5+
android/

example/eslint.config.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
const reactPlugin = require('eslint-plugin-react');
2+
const reactHooksPlugin = require('eslint-plugin-react-hooks');
3+
const prettierConfig = require('eslint-config-prettier');
4+
const tsParser = require('@typescript-eslint/parser');
5+
const presstoPlugin = require('eslint-plugin-pressto');
6+
7+
module.exports = [
8+
{
9+
files: ['**/*.{js,jsx,ts,tsx}'],
10+
languageOptions: {
11+
parser: tsParser,
12+
parserOptions: {
13+
ecmaVersion: 'latest',
14+
sourceType: 'module',
15+
ecmaFeatures: {
16+
jsx: true,
17+
},
18+
},
19+
},
20+
plugins: {
21+
'react': reactPlugin,
22+
'react-hooks': reactHooksPlugin,
23+
'pressto': presstoPlugin,
24+
},
25+
rules: {
26+
'react/react-in-jsx-scope': 'off',
27+
'pressto/require-worklet-directive': 'error',
28+
...prettierConfig.rules,
29+
},
30+
},
31+
{
32+
ignores: ['node_modules/', '.expo/', 'dist/', 'ios/', 'android/'],
33+
},
34+
];

0 commit comments

Comments
 (0)