Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rewrite plugin to TypeScript #4096

Merged
merged 73 commits into from
Mar 8, 2023
Merged
Show file tree
Hide file tree
Changes from 58 commits
Commits
Show all changes
73 commits
Select commit Hold shift + click to select a range
9712156
working build
tjzel Feb 21, 2023
50b68fe
changed require to import
tjzel Feb 21, 2023
fd81dd1
changed ReferencedIdentifier to Identifier (I guess it is gonna break…
tjzel Feb 21, 2023
d7e7527
probable fix
tjzel Feb 21, 2023
059ffbb
fixed hash error
tjzel Feb 21, 2023
600d850
preparing for string worklet function
tjzel Feb 21, 2023
0a8a85f
added some comments to not lose things that need more thought
tjzel Feb 22, 2023
d7104b3
more changes
tjzel Feb 22, 2023
70ca4e8
almost fixed buildWorkletString
tjzel Feb 22, 2023
7b0ab43
working so far
tjzel Feb 22, 2023
dc1b045
need to rebuild the project
tjzel Feb 22, 2023
484170c
wrong ting in newFun?
tjzel Feb 22, 2023
431ec78
changes to makeWorklet
tjzel Feb 23, 2023
3f8803f
before major changes in processWorklets
tjzel Feb 23, 2023
e90bb52
almost ready for first draft
tjzel Feb 23, 2023
74dede5
first draft
tjzel Feb 23, 2023
0241d78
migrated to PluginPass
tjzel Feb 23, 2023
c4ff4fe
restore deleted file
tjzel Feb 23, 2023
deb4782
newline
tjzel Feb 23, 2023
a361911
typos & index.js update
tjzel Feb 23, 2023
2b265ff
prettied index.js
tjzel Feb 23, 2023
392378e
recompiled index.js
tjzel Feb 24, 2023
8772841
added tsconfig
tjzel Feb 24, 2023
014ed46
amended error messages
tjzel Feb 24, 2023
40e7ecc
Merge branch '@tjzel/ts-plugin-refactor' into @tjzel/ts-plugin-refact…
tjzel Feb 24, 2023
02bf855
will remove comments now
tjzel Feb 24, 2023
d328572
cleaner build with tsconfig
tjzel Feb 24, 2023
42efa28
Merge branch main into @tjzel/ts-plugin-refactor
tjzel Feb 27, 2023
f9043d4
included #4062 plugin changes
tjzel Feb 27, 2023
225c956
included #3970 plugin changes
tjzel Feb 27, 2023
0f8e340
included #4104 plugin changes
tjzel Feb 27, 2023
5d31a45
compiled index.js
tjzel Feb 27, 2023
4ef9d8c
apply formatting
tjzel Feb 27, 2023
21dfdff
add eslint ignore for commit ability
tjzel Feb 27, 2023
42ff15b
actually .eslintignore is not necessary
tjzel Feb 27, 2023
d3f3f3f
.eslintignore didnt get removed for some reason
tjzel Feb 27, 2023
3db07e8
formatting for easier diff on github
tjzel Feb 27, 2023
fcc30ed
reverted some naming changes for clarity
tjzel Feb 27, 2023
b41f25a
with new index.js
tjzel Feb 27, 2023
e841d93
added README file
tjzel Feb 27, 2023
6e027ee
added source map
tjzel Feb 27, 2023
4a08fc5
follow up
tjzel Feb 27, 2023
26f7725
amendments in regard to review
tjzel Feb 27, 2023
9b9fc13
improved some typechecking and errors
tjzel Feb 27, 2023
64eec77
added building scripts for plugin to package.json
tjzel Feb 28, 2023
e43cb1d
ES6 in tsconfig and removed unnecessary interface
tjzel Feb 28, 2023
f56bf06
changes from ES6 as target
tjzel Feb 28, 2023
e72fe0d
added plugins own package.json
tjzel Feb 28, 2023
7acac08
added dependencies
tjzel Feb 28, 2023
6a57c94
added dev dependencies
tjzel Feb 28, 2023
578acc2
import 'path'
tjzel Feb 28, 2023
1ecf504
guessing at this point
tjzel Feb 28, 2023
832eebb
force typing
tjzel Feb 28, 2023
0baa675
more commits
tjzel Feb 28, 2023
d766190
hope it works on windows now
tjzel Feb 28, 2023
a32406e
...
tjzel Feb 28, 2023
32dc5e9
added yarn.lock
tjzel Feb 28, 2023
d700a9e
added readme
tjzel Mar 6, 2023
39d9292
tomekzaw review
tjzel Mar 7, 2023
a084988
typo fix
tjzel Mar 7, 2023
5086fce
added plugin type:check to global package.json
tjzel Mar 7, 2023
fb00ed9
no more postinstall
tjzel Mar 7, 2023
f9daad2
added CI
tjzel Mar 7, 2023
0227710
added yarn in root for CI
tjzel Mar 7, 2023
7a30818
weird formatting bug
tjzel Mar 7, 2023
da0bf77
/..
tjzel Mar 7, 2023
bc2758b
m
tjzel Mar 7, 2023
6b5bf1c
removed sourcemap dependency
tjzel Mar 7, 2023
0fad557
Update .github/workflows/validate-plugin.yml
tjzel Mar 7, 2023
f3cacdf
Update plugin/README.md
tjzel Mar 7, 2023
23ea6d3
Apply suggestions from code review
tjzel Mar 7, 2023
5ad95b4
readme amendments
tjzel Mar 7, 2023
08e425b
Merge branch 'main' into @tjzel/ts-plugin-refactor
tjzel Mar 8, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions __tests__/__snapshots__/plugin.test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,39 @@ var f = function () {
}();"
`;

exports[`babel plugin doesn't remove nested 'worklets' 1`] = `
"var _worklet_1678749606628_init_data = {
code: \\"function foo(x){function bar(x){'worklet';return x+2;}return bar(x)+1;}\\",
location: \\"${ process.cwd() }/jest tests fixture\\"
};
var _worklet_16974800582491_init_data = {
code: \\"function bar(x){return x+2;}\\",
location: \\"${ process.cwd() }/jest tests fixture\\"
};
var foo = function () {
var _e = [new Error(), 1, -20];
var _f = function _f(x) {
var bar = function () {
var _e = [new Error(), 1, -20];
var _f = function _f(x) {
return x + 2;
};
_f._closure = {};
_f.__initData = _worklet_16974800582491_init_data;
_f.__workletHash = 16974800582491;
_f.__stackDetails = _e;
return _f;
}();
return bar(x) + 1;
};
_f._closure = {};
_f.__initData = _worklet_1678749606628_init_data;
_f.__workletHash = 1678749606628;
_f.__stackDetails = _e;
return _f;
}();"
`;

exports[`babel plugin doesn't show a warning if user writes something like style={styles.value} 1`] = `
"function App() {
return React.createElement(Animated.View, {
Expand Down
14 changes: 14 additions & 0 deletions __tests__/plugin.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,20 @@ describe('babel plugin', () => {
expect(code).toMatchSnapshot();
});

it("doesn't remove nested 'worklets'", () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
it("doesn't remove nested 'worklets'", () => {
it("doesn't transform nested worklets", () => {

const input = `
function foo(x){
'worklet';
function bar(x){
'worklet'
return x+2;
}
return bar(x)+1;
}`;
tomekzaw marked this conversation as resolved.
Show resolved Hide resolved
const { code } = runPlugin(input);
expect(code).toMatchSnapshot();
});

it('captures worklets environment', () => {
const input = `
const x = 5;
Expand Down
20 changes: 15 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,16 @@
"scripts": {
"test": "yarn run format:js && yarn run lint:js && yarn run test:unit",
"test:unit": "jest",
"lint": "yarn lint:js && yarn lint:cpp && yarn lint:java && yarn lint:ios && yarn lint:docs",
"lint": "yarn lint:js && yarn lint:plugin && yarn lint:cpp && yarn lint:java && yarn lint:ios && yarn lint:docs",
"lint:js": "eslint --ext '.js,.ts,.tsx' src/ && yarn prettier --check src/",
"lint:plugin": "eslint --ext '.ts' plugin && yarn prettier --check plugin/",
tomekzaw marked this conversation as resolved.
Show resolved Hide resolved
"lint:docs": "cd docs && yarn lint && cd ..",
"lint:java": "./android/gradlew -p android spotlessCheck -q",
"lint:cpp": "./scripts/cpplint.sh",
"lint:ios": "./scripts/validate-ios.sh && yarn format:ios --dry-run",
"format": "yarn format:js && yarn format:java && yarn format:ios && yarn format:android && yarn format:common",
"format": "yarn format:js && yarn format:plugin && yarn format:java && yarn format:ios && yarn format:android && yarn format:common",
"format:js": "prettier --write --list-different './src/'",
"format:plugin": "cd plugin && yarn format && cd ..",
"format:java": "node ./scripts/format-java.js",
"format:ios": "find ios/ -iname *.h -o -iname *.m -o -iname *.mm -o -iname *.cpp | xargs clang-format -i --Werror",
"format:android": "find android/src/ -iname *.h -o -iname *.cpp | xargs clang-format -i",
Expand All @@ -25,7 +27,9 @@
"clean": "rm -rf node_modules && cd Example && rm -rf node_modules && cd ios && pod deintegrate && cd ../..",
"reset": "yarn clean && yarn setup",
"clean:deep": "cd android && rm -rf .cxx .gradle build && cd ../Example/android && rm -rf .gradle build app/build && cd ../.. && yarn clean",
"reset:deep": "yarn clean:deep && yarn setup"
"reset:deep": "yarn clean:deep && yarn setup",
"plugin": "cd plugin && yarn && cd ..",
"postinstall": "yarn plugin"
},
"main": "lib/commonjs/index",
"module": "lib/module/index",
Expand Down Expand Up @@ -76,7 +80,7 @@
"dependencies": {
"@babel/plugin-transform-object-assign": "^7.16.7",
"@babel/preset-typescript": "^7.16.7",
"convert-source-map": "^1.7.0",
"convert-source-map": "^2.0.0",
"invariant": "^2.2.4",
"lodash.isequal": "^4.5.0",
"setimmediate": "^1.0.5",
Expand All @@ -97,6 +101,7 @@
"@testing-library/jest-native": "^4.0.4",
"@testing-library/react-hooks": "^8.0.0",
"@testing-library/react-native": "^7.1.0",
"@types/convert-source-map": "^2.0.0",
"@types/babel-types": "^7.0.11",
"@types/babel__core": "^7.1.18",
"@types/babel__generator": "^7.6.4",
Expand Down Expand Up @@ -134,9 +139,14 @@
},
"lint-staged": {
"*.(js|ts|tsx)": [
"eslint --ext '.js,.ts,.tsx' src/ --ignore-pattern src/reanimated1 --ignore-pattern react-native-reanimated.d.ts --ignore-pattern docs",
"eslint --ext '.js,.ts,.tsx' src/ --ignore-pattern src/reanimated1 --ignore-pattern react-native-reanimated.d.ts --ignore-pattern docs --ignore-pattern plugin",
tjzel marked this conversation as resolved.
Show resolved Hide resolved
tomekzaw marked this conversation as resolved.
Show resolved Hide resolved
"prettier --write"
],
"plugin/**/*.ts": [
"eslint",
"prettier --write"
tomekzaw marked this conversation as resolved.
Show resolved Hide resolved
],
"plugin/**/*.js": "prettier --write",
"**/*.{h,cpp}": "yarn lint:cpp",
"android/src/**/*.java": "yarn format:java",
"android/src/**/*.{h,cpp}": "yarn format:android",
Expand Down
141 changes: 141 additions & 0 deletions plugin/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
### ReanimatedPlugin is now in TypeScript.
tjzel marked this conversation as resolved.
Show resolved Hide resolved

To compile it, either use `yarn` or explicitly use `yarn plugin` in the root directory or `yarn` in `plugin/`.

# Why do we need this plugin?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use h2 or h3 for subsection titles instead (## or ###)


Reanimated is all about executing the code directly on the UI thread whenever possible to avoid expensive and troublesome communication between those two threads. Since UI and JS (React-Native) contexts are separate, we somehow need to pass the functions (and their arguments) from the JS thread to the UI thread. That's why we need **worklets**. If you haven't yet, we strongly recommend reading [the official documentation on worklets](https://docs.swmansion.com/react-native-reanimated/docs/fundamentals/worklets/) first.

# What is a worklet?

Worklets are **Arrow Function Expressions**, **Function Declarations**, **Function Expressions**, or **Object Methods** (for more information, refer to [official ECMAScript](https://262.ecma-international.org/), [types in Babel](https://babeljs.io/docs/babel-types), [AST Explorer](https://astexplorer.net/) - with @babel/parser) that contain a `worklet` directive at their very top. And that's it. It might be quite disappointing, but all the fun begins once we make such a worklet. It's a function that can be called on JS and UI threads. That's why Reanimated is able to produce smooth and responsive animations - functions that control those animations are not executed on JS thread, from where their results would need to be transported to UI thread and then applied.

# How does a worklet work?

The sole principle is (seemingly) simple. Once the Babel parser has transformed the code of the worklet function it appends some information to the function (since functions are objects in JavaScript). Once `runOnUI` has been called with our worklet, this extra information, which mostly is just JavaScript code, is injected into UI thread. Then UI thread calls `eval`, on this code and is happily able to run it autonomously. If it's just `eval` you might ask:

# Why transform?

Well, we have to remember that UI thread is a completely different context. It does not have access to the scope we called the function from, nor anything present on the JS thread. Obviously, we could just copy the entire JS thread and inject it into the UI, but that's in direct conflict with one of Reanimated's principles - _speed_. We want to inject _as little data as possible_ - that's why we don't copy the context and we don't copy all the function closures. We only copy the variables we need (that are used in the worklet) and pass _only them_. For that, we need some transforming and transforming only for the parts that we will need to inject. But we can do better. One of Reanimated's strong points is transparency. We can just inject some code into the UI thread, why not, but once something crashes, we'd like to know what. That's why we also append some debugging information - reference to the original code - using simple source maps. That allows us to get information about what failed in the worklet even though it was executed on another thread. Pretty neat, right?

To sum up, the process of worklet transformation appends to the JS function:

- its UI code (as a string),
- closure with only required variables,
- debugging information (source maps).

# Is that it?

Of course not. Reanimated Plugin does a lot more than that. We also perform automatic _workletization_. It's a functionality that allows the developers to have less boilerplate code. When you use `useAnimatedStyle` or `useAnimatedGestureHandler` we **cannot** give them non-worklet functions. So, simply put, Plugin detects if it should workletize a non-worklet function and then workletizes it. Thanks to it, we can type:

```TypeScript
const scrollHandler = useAnimatedScrollHandler({
onScroll: (e) => {
position.value = e.contentOffset.x;
},
onEndDrag: (e) => {
scrollToNearestItem(e.contentOffset.x);
},
onMomentumEnd: (e) => {
scrollToNearestItem(e.contentOffset.x);
},
});
```

instead of

```TypeScript
const scrollHandler = useAnimatedScrollHandler({
onScroll: (e) => {
`worklet`;
tjzel marked this conversation as resolved.
Show resolved Hide resolved
position.value = e.contentOffset.x;
},
onEndDrag: (e) => {
`worklet`;
scrollToNearestItem(e.contentOffset.x);
},
onMomentumEnd: (e) => {
`worklet`;
scrollToNearestItem(e.contentOffset.x);
},
});
```

This might not seem to be a lot but it really helps with development and allows us to delegate some tedious tasks to babel.

# Something doesn't work in Reanimated Plugin!

It's certainly possible. It's being created ad-hoc from our experience and needs, not pre-planned in general:

_We had a need to transform something -> we looked up how it is structured in AST explorer -> we added certain functionality to Plugin._

Some use cases might've been overlooked and on the other hand - some might work but were not designed (considered) to be transformed. That's why we strongly suggest that before you do something unusual with Reanimated you should check if it's conforming to Plugin. If it's not and you think it would be useful - you are more than welcome to contribute and submit a pull request on our [repo](https://www.github.com/software-mansion/react-native-reanimated).

# Plugin's applications

To get some more information about certain edge cases or use cases not listed here, try looking them up in our [tests](https://github.com/software-mansion/react-native-reanimated/blob/main/__tests__/plugin.test.js).

### What can be a worklet?

As stated in [this paragraph](#what-is-a-worklet), a worklet is supposed to be one of those:

- Arrow Function Expression:

```TypeScript
const foo = () => {
`worklet`;
console.log('Hello from ArrowFunctionExpression');
}
```

- Function Declaration:

```TypeScript
function foo (){
`worklet`;
console.log('Hello from FunctionDeclaration');
}
```

- Function Expression:

```TypeScript
const foo = function () {
`worklet`;
console.log('Hello from FunctionExpression');
};
```

- Object Method:

```TypeScript
const obj = {
foo() {
`worklet`;
console.log('Hello from ObjectMethod');
},
};
```

In addition, workletization will work with:

- Sequence Expression (only last element):

```TypeScript
function foo() {
(0, bar, foobar)({ // only foobar will get workletized!
barfoo() {
`worklet`;
console.log('Hello from Sequence Expression');
},
});
}
```

### Inline styles support

For more information read [official docs](https://docs.swmansion.com/react-native-reanimated/docs/fundamentals/animations/#animations-in-inline-styles).

### How to debug Reanimated Babel plugin?

It's simple. After compilation we have generated `.js.map` file. We strongly recommend using **Visual Studio Code** and **JavaScript Debug Terminal**. Just open a new debugging session, type in your terminal (in project's root directory) `npx babel <filename>` and voilà. Add some breakpoints in `plugin/index.ts` or just use step-by-step tools. Some knowledge of JavaScript's AST and babel will be required to understand what exactly is happening during code transformation.
Loading