Skip to content

Commit

Permalink
Allow worklet referencing in plugin (#5911)
Browse files Browse the repository at this point in the history
## Why

Currently, when handling worklet callbacks, the user has either to mark
a function directly with `worklet` directive or define the worklet as an
inline argument.

```tsx
// This will work:
const styleFactory = () => {
  'worklet';
  return { backgroundColor: 'blue' };
};
const animatedStyle = useAnimatedStyle(styleFactory);

// This will work as well:
const animatedStyle = useAnimatedStyle(() => ({ backgroundColor: blue }));

// However this won't
const styleFactory = () => {
  return { backgroundColor: 'blue' };
};
const animatedStyle = useAnimatedStyle(styleFactory); // error - style factory is not a worklet!
```

This pull request allows the user to define a worklet outside of a hook
argument to get it autoworkletized. Keep in mind that it still has some
boundaries:

1. Worklet has to be defined in the same file. It cannot be imported.
2. Worklet has to be defined before it's used.
3. Worklet cannot be defined via an expression (for example, using an
`?` operator). This however could be adjusted in the future.

## What

We are allowing the following constructions to be autoworkletized.
`useAnimatedStyle` will be used as a example hook here, it will work
with every bit of our API that facilitates autoworkletization.

### Function declarations

```ts
function worklet() {
  // Some UI relevant code.
}

const animatedStyle = useAnimatedStyle(worklet);
```

### Function expressions

```ts
const worklet = function() {
  // Some UI relevant code.
}

const animatedStyle = useAnimatedStyle(worklet);
```

### Arrow function expression

```ts
const worklet = () => {
  // Some UI relevant code.
}

const animatedStyle = useAnimatedStyle(worklet);
```

### Object methods
This is a special case, used for example by `useAnimatedScrollHandler`.

```ts
const handler = {
  onScroll: () => {
    // Some UI relevant code.
  }
}

useAnimatedScrollHandler(handler);
```

It doesn't matter if the method is actually an arrow function or
function expression. Keep in mind that it actually has to be defined in
place. As of now we don't support such deep cases as:

```ts
const onScroll = () => {
  // Some UI relevant code.
}

const handler = {
  onScroll
}

useAnimatedScrollHandler(handler);
```

## How

Changes implemented here are quite simple. The only thing we do is
search the scope of a given context (that's usually a function) for a
referenced identifier. For example:

```ts
const animatedStyle = useAnimatedStyle(styleFactory);
```

The `styleFactory` identifier is referenced in the `useAnimatedStyle`
call. We then search the scope of the function for a reference to
`styleFactory` and look for variable declarations, variable assignments,
and function declarations.

1. Variable declarations:
   ```ts
   let styleFactory = () => {
     // Some UI relevant code.
   }
   ```
2. Variable assignments:
   ```ts
   let styleFactory;
   // ...
   styleFactory = () => {
     // Some UI relevant code.
   }
   ```
3. Function declarations:
   ```ts
   function styleFactory() {
     // Some UI relevant code.
   }
   ```

If we find one of these, that can be autoworkletized, we do it. If there
are multiple objects that can be autoworkletized, we pick the first one
using the following rules:

1. Function declarations are picked first.
2. Variable assignments are picked second. In case of multiple variable
assignments, we pick the last one.
3. Variable declarations are picked last.

Therefore in the following code:

```ts
let styleFactory = function foo() {
  // Some UI relevant code.
}

styleFactory = function bar() {
  // Some UI relevant code.
}

styleFactory = function baz() {
  // Some UI relevant code.
}

const animatedStyle = useAnimatedStyle(styleFactory);
```

Only the `function baz` will be autoworkletized. Keep in mind that this
is just an edge-case scenario. Please don't ever write such code when
using worklets!

### Scoping
We also support multiple scoping, for example:

```ts
function foo(){
  const styleFactory = () => {
    // Some UI relevant code.
  }
  function bar(){
    const animatedStyle = useAnimatedStyle(styleFactory);
  }
}
```

Will work as expected. It follows the same rules as above. For now we
don't support variable shadowing - expect undefined behavior there.

### Notes

Currently we expect the worklet variable not to be reassigned after it's
been used. For example, the following code will not work:

```ts
const styleFactory = () => {
  // Some UI relevant code.
}

const animatedStyle = useAnimatedStyle(styleFactory);

styleFactory = () => {
  // Some UI relevant code.
}
```

Because only the last assignment will be workletized. The first
assignment will not be transformed and `useAnimatedStyle` will throw an
error. This is desired behavior, since reassigning worklet variables is
considered an anti-pattern right now.

## Test plan

- [ ] Add tests for all the above cases.
- [ ] Confirm that current errors remain informative in cases where this
mechanism doesn't apply.
  • Loading branch information
tjzel authored Jun 10, 2024
1 parent 5cb258d commit c464a2f
Show file tree
Hide file tree
Showing 8 changed files with 943 additions and 167 deletions.

Large diffs are not rendered by default.

256 changes: 224 additions & 32 deletions packages/react-native-reanimated/__tests__/plugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ describe('babel plugin', () => {

const { code, ast } = runPlugin(input, { ast: true });
let closureBindings;
traverse(ast, {
traverse(ast!, {
enter(path) {
if (
path.isAssignmentExpression() &&
Expand Down Expand Up @@ -534,38 +534,16 @@ describe('babel plugin', () => {
expect(code).toHaveWorkletData();
expect(code).toMatchSnapshot();
});
});

describe('for runOnUI', () => {
it('workletizes ArrowFunctionExpression inside runOnUI automatically', () => {
const input = html`<script>
runOnUI(() => {
console.log('Hello from the UI thread!');
})();
</script>`;

const { code } = runPlugin(input);
expect(code).toHaveWorkletData();
expect(code).toMatchSnapshot();
});

it('workletizes unnamed FunctionExpression inside runOnUI automatically', () => {
it('workletizes hook wrapped worklet reference automatically', () => {
const input = html`<script>
runOnUI(function () {
console.log('Hello from the UI thread!');
})();
</script>`;

const { code } = runPlugin(input);
expect(code).toHaveWorkletData();
expect(code).toMatchSnapshot();
});

it('workletizes named FunctionExpression inside runOnUI automatically', () => {
const input = html`<script>
runOnUI(function hello() {
console.log('Hello from the UI thread!');
})();
const style = () => {
return {
color: 'red',
backgroundColor: 'blue',
};
};
const animatedStyle = useAnimatedStyle(style);
</script>`;

const { code } = runPlugin(input);
Expand Down Expand Up @@ -1013,7 +991,7 @@ describe('babel plugin', () => {

const { code, ast } = runPlugin(input, { ast: true });
let closureBindings;
traverse(ast, {
traverse(ast!, {
enter(path) {
if (
path.isAssignmentExpression() &&
Expand Down Expand Up @@ -1768,4 +1746,218 @@ describe('babel plugin', () => {
expect(code).toMatchSnapshot();
});
});

describe('for referenced worklets', () => {
it('workletizes ArrowFunctionExpression on its VariableDeclarator', () => {
const input = html`<script>
let styleFactory = () => ({});
const animatedStyle = useAnimatedStyle(styleFactory);
</script>`;

const { code } = runPlugin(input);
expect(code).toHaveWorkletData(1);
expect(code).toMatchSnapshot();
});

it('workletizes ArrowFunctionExpression on its AssignmentExpression', () => {
const input = html`<script>
let styleFactory;
styleFactory = () => ({});
animatedStyle = useAnimatedStyle(styleFactory);
</script>`;

const { code } = runPlugin(input);
expect(code).toHaveWorkletData(1);
expect(code).toMatchSnapshot();
});

it('workletizes ArrowFunctionExpression only on last AssignmentExpression', () => {
const input = html`<script>
let styleFactory;
styleFactory = () => 1;
styleFactory = () => 'AssignmentExpression';
animatedStyle = useAnimatedStyle(styleFactory);
</script>`;

const { code } = runPlugin(input);
expect(code).toHaveWorkletData(1);
expect(code).toIncludeInWorkletString('AssignmentExpression');
expect(code).toMatchSnapshot();
});

it('workletizes FunctionExpression on its VariableDeclarator', () => {
const input = html`<script>
let styleFactory = function () {
return {};
};
const animatedStyle = useAnimatedStyle(styleFactory);
</script>`;

const { code } = runPlugin(input);
expect(code).toHaveWorkletData(1);
expect(code).toMatchSnapshot();
});

it('workletizes FunctionExpression on its AssignmentExpression', () => {
const input = html`<script>
let styleFactory;
styleFactory = function () {
return {};
};
animatedStyle = useAnimatedStyle(styleFactory);
</script>`;

const { code } = runPlugin(input);
expect(code).toHaveWorkletData(1);
expect(code).toMatchSnapshot();
});

it('workletizes FunctionExpression only on last AssignmentExpression', () => {
const input = html`<script>
let styleFactory;
styleFactory = function () {
return 1;
};
styleFactory = function () {
return 'AssignmentExpression';
};
animatedStyle = useAnimatedStyle(styleFactory);
</script>`;

const { code } = runPlugin(input);
expect(code).toHaveWorkletData(1);
expect(code).toIncludeInWorkletString('AssignmentExpression');
expect(code).toMatchSnapshot();
});

it('workletizes FunctionDeclaration', () => {
const input = html`<script>
function styleFactory() {
return {};
}
const animatedStyle = useAnimatedStyle(styleFactory);
</script>`;

const { code } = runPlugin(input);
expect(code).toHaveWorkletData(1);
expect(code).toMatchSnapshot();
});

it('workletizes ObjectExpression on its VariableDeclarator', () => {
const input = html`<script>
let handler = {
onScroll: () => {},
};
const scrollHandler = useAnimatedScrollHandler(handler);
</script>`;

const { code } = runPlugin(input);
expect(code).toHaveWorkletData(1);
expect(code).toMatchSnapshot();
});

it('workletizes ObjectExpression on its AssignmentExpression', () => {
const input = html`<script>
let handler;
handler = {
onScroll: () => {},
};
const scrollHandler = useAnimatedScrollHandler(handler);
</script>`;

const { code } = runPlugin(input);
expect(code).toHaveWorkletData(1);
expect(code).toMatchSnapshot();
});

it('workletizes ObjectExpression only on last AssignmentExpression', () => {
const input = html`<script>
let handler;
handler = {
onScroll: () => 1,
};
handler = {
onScroll: () => 'AssignmentExpression',
};
const scrollHandler = useAnimatedScrollHandler(handler);
</script>`;

const { code } = runPlugin(input);
expect(code).toHaveWorkletData(1);
expect(code).toIncludeInWorkletString('AssignmentExpression');
expect(code).toMatchSnapshot();
});

it('prefers FunctionDeclaration over AssignmentExpression', () => {
const input = html`<script>
function styleFactory() {
return 'FunctionDeclaration';
}
styleFactory = () => 'AssignmentExpression';
animatedStyle = useAnimatedStyle(styleFactory);
</script>`;
console.log(input);
const { code } = runPlugin(input);
console.log(code);

expect(code).toHaveWorkletData(1);
expect(code).toIncludeInWorkletString('FunctionDeclaration');
expect(code).toMatchSnapshot();
});

it('prefers AssignmentExpression over VariableDeclarator', () => {
// This is an anti-pattern, but let's at least have a defined behavior here.
const input = html`<script>
let styleFactory = () => 1;
styleFactory = () => 'AssignmentExpression';
animatedStyle = useAnimatedStyle(styleFactory);
</script>`;

const { code } = runPlugin(input);
expect(code).toHaveWorkletData(1);
expect(code).toIncludeInWorkletString('AssignmentExpression');
expect(code).toMatchSnapshot();
});

it('workletizes in immediate scope', () => {
const input = html`<script>
let styleFactory = () => ({});
animatedStyle = useAnimatedStyle(styleFactory);
</script>`;

const { code } = runPlugin(input);
expect(code).toHaveWorkletData(1);
expect(code).toMatchSnapshot();
});

it('workletizes in nested scope', () => {
const input = html`<script>
function outerScope() {
let styleFactory = () => ({});
function innerScope() {
animatedStyle = useAnimatedStyle(styleFactory);
}
}
</script>`;

const { code } = runPlugin(input);
expect(code).toHaveWorkletData(1);
expect(code).toMatchSnapshot();
});

it('workletizes assigments that appear after the worklet is used', () => {
const input = html`<script>
let styleFactory = () => ({});
animatedStyle = useAnimatedStyle(styleFactory);
styleFactory = () => {
return 'AssignmentAfterUse';
};
</script>`;

const { code } = runPlugin(input);
expect(code).toHaveWorkletData(1);
expect(code).toIncludeInWorkletString('AssignmentAfterUse');
expect(code).toMatchSnapshot();
});
});
});
Loading

0 comments on commit c464a2f

Please sign in to comment.