diff --git a/app/src/examples/WorkletFactoryCrashExample.tsx b/app/src/examples/WorkletFactoryCrashExample.tsx
new file mode 100644
index 00000000000..15633a28a64
--- /dev/null
+++ b/app/src/examples/WorkletFactoryCrashExample.tsx
@@ -0,0 +1,35 @@
+/* eslint-disable @typescript-eslint/no-unused-vars */
+import React from 'react';
+import { Button, StyleSheet, View, Text } from 'react-native';
+
+export default function App() {
+ function handleOnPress() {
+ function badWorklet() {
+ 'worklet';
+ // @ts-expect-error
+ unexistingVariable;
+ }
+ }
+
+ return (
+
+
+ Clicking the button below should give a RedBox with stack trace that
+ denotes the error in `badWorklet` function, pointing to
+ `unexistingVariable` usage.
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ text: {
+ textAlign: 'center',
+ },
+});
diff --git a/app/src/examples/index.ts b/app/src/examples/index.ts
index 982680eec48..176bb26faa7 100644
--- a/app/src/examples/index.ts
+++ b/app/src/examples/index.ts
@@ -109,6 +109,7 @@ import WobbleExample from './WobbleExample';
import WorkletExample from './WorkletExample';
import WorkletRuntimeExample from './WorkletRuntimeExample';
import NestedLayoutAnimationConfig from './LayoutAnimations/NestedLayoutAnimationConfig';
+import WorkletFactoryCrash from './WorkletFactoryCrashExample';
interface Example {
icon?: string;
@@ -418,6 +419,11 @@ export const EXAMPLES: Record = {
title: 'Log test',
screen: LogExample,
},
+ WorkletFactoryCrash: {
+ icon: '🏭',
+ title: 'Worklet factory crash',
+ screen: WorkletFactoryCrash,
+ },
// Old examples
diff --git a/plugin/build/plugin.js b/plugin/build/plugin.js
index 799991dafc4..7c6c7925b1b 100644
--- a/plugin/build/plugin.js
+++ b/plugin/build/plugin.js
@@ -425,6 +425,7 @@ var require_makeWorklet = __commonJS({
}
function makeArrayFromCapturedBindings(ast, fun) {
const closure = /* @__PURE__ */ new Map();
+ const isLocationAssignedMap = /* @__PURE__ */ new Map();
(0, core_1.traverse)(ast, {
Identifier(path) {
if (!path.isReferencedIdentifier()) {
@@ -452,6 +453,20 @@ var require_makeWorklet = __commonJS({
currentScope = currentScope.parent;
}
closure.set(name, path.node);
+ isLocationAssignedMap.set(name, false);
+ }
+ });
+ fun.traverse({
+ Identifier(path) {
+ if (!path.isReferencedIdentifier()) {
+ return;
+ }
+ const node = closure.get(path.node.name);
+ if (!node || isLocationAssignedMap.get(path.node.name)) {
+ return;
+ }
+ node.loc = path.node.loc;
+ isLocationAssignedMap.set(path.node.name, true);
}
});
return Array.from(closure.values());
@@ -494,12 +509,20 @@ var require_processIfWorkletFunction = __commonJS({
}
exports2.processIfWorkletFunction = processIfWorkletFunction;
function processWorkletFunction(path, state) {
- const newFun = (0, makeWorklet_1.makeWorklet)(path, state);
- const replacement = (0, types_1.callExpression)(newFun, []);
+ const workletFactory = (0, makeWorklet_1.makeWorklet)(path, state);
+ const workletFactoryCall = (0, types_1.callExpression)(workletFactory, []);
+ const originalWorkletLocation = path.node.loc;
+ if (originalWorkletLocation) {
+ workletFactoryCall.callee.loc = {
+ start: originalWorkletLocation.start,
+ end: originalWorkletLocation.start
+ };
+ }
const needDeclaration = (0, types_1.isScopable)(path.parent) || (0, types_1.isExportNamedDeclaration)(path.parent);
- path.replaceWith("id" in path.node && path.node.id && needDeclaration ? (0, types_1.variableDeclaration)("const", [
- (0, types_1.variableDeclarator)(path.node.id, replacement)
- ]) : replacement);
+ const replacement = "id" in path.node && path.node.id && needDeclaration ? (0, types_1.variableDeclaration)("const", [
+ (0, types_1.variableDeclarator)(path.node.id, workletFactoryCall)
+ ]) : workletFactoryCall;
+ path.replaceWith(replacement);
}
}
});
diff --git a/plugin/src/makeWorklet.ts b/plugin/src/makeWorklet.ts
index dec28e12e9e..9e079f7e236 100644
--- a/plugin/src/makeWorklet.ts
+++ b/plugin/src/makeWorklet.ts
@@ -319,6 +319,7 @@ function makeArrayFromCapturedBindings(
fun: NodePath
) {
const closure = new Map();
+ const isLocationAssignedMap = new Map();
// this traversal looks for variables to capture
traverse(ast, {
@@ -368,6 +369,30 @@ function makeArrayFromCapturedBindings(
currentScope = currentScope.parent;
}
closure.set(name, path.node);
+ isLocationAssignedMap.set(name, false);
+ },
+ });
+
+ /*
+ For reasons I don't exactly understand, the above traversal will cause the whole
+ bundle to crash if we traversed original node instead of generated
+ AST. This is why we need to traverse it again, but this time we set
+ location for each identifier that was captured to their original counterpart, since
+ AST has its location set relative as if it was a separate file.
+ */
+ fun.traverse({
+ Identifier(path) {
+ // So it won't refer to something like:
+ // const obj = {unexistingVariable: 1};
+ if (!path.isReferencedIdentifier()) {
+ return;
+ }
+ const node = closure.get(path.node.name);
+ if (!node || isLocationAssignedMap.get(path.node.name)) {
+ return;
+ }
+ node.loc = path.node.loc;
+ isLocationAssignedMap.set(path.node.name, true);
},
});
diff --git a/plugin/src/processIfWorkletFunction.ts b/plugin/src/processIfWorkletFunction.ts
index 419c2ee3060..1c67582d2fe 100644
--- a/plugin/src/processIfWorkletFunction.ts
+++ b/plugin/src/processIfWorkletFunction.ts
@@ -1,4 +1,4 @@
-import type { NodePath, Node } from '@babel/core';
+import type { NodePath } from '@babel/core';
import {
callExpression,
isScopable,
@@ -9,11 +9,12 @@ import {
import type { ExplicitWorklet, ReanimatedPluginPass } from './types';
import { makeWorklet } from './makeWorklet';
-// Replaces FunctionDeclaration, FunctionExpression or ArrowFunctionExpression
-// with a workletized version of itself.
-
+/**
+ * Replaces `FunctionDeclaration`, `FunctionExpression` or `ArrowFunctionExpression`
+ * with a workletized version of itself.
+ */
export function processIfWorkletFunction(
- path: NodePath,
+ path: NodePath,
state: ReanimatedPluginPass
) {
if (
@@ -29,9 +30,33 @@ function processWorkletFunction(
path: NodePath,
state: ReanimatedPluginPass
) {
- const newFun = makeWorklet(path, state);
+ const workletFactory = makeWorklet(path, state);
+
+ const workletFactoryCall = callExpression(workletFactory, []);
- const replacement = callExpression(newFun, []);
+ /*
+ If for some reason the code of the worklet is so bad that it
+ causes the worklet factory to crash, eg.:
+
+ function foo() {
+ 'worklet'
+ unexistingVariable;
+ };
+
+ Such function will cause the factory to crash on closure creation because
+ of reference to `unexistingVariable`.
+
+ With this we are able to give a meaningful stack trace - we use `start` twice on purpose, since
+ crashing on the factory leads to its end on the stack trace - the closing bracket. It's more
+ approachable this way, when it points to the start of the original function.
+ */
+ const originalWorkletLocation = path.node.loc;
+ if (originalWorkletLocation) {
+ workletFactoryCall.callee.loc = {
+ start: originalWorkletLocation.start,
+ end: originalWorkletLocation.start,
+ };
+ }
// we check if function needs to be assigned to variable declaration.
// This is needed if function definition directly in a scope. Some other ways
@@ -40,11 +65,13 @@ function processWorkletFunction(
// ^ in such a case we don't need to define variable for the function
const needDeclaration =
isScopable(path.parent) || isExportNamedDeclaration(path.parent);
- path.replaceWith(
+
+ const replacement =
'id' in path.node && path.node.id && needDeclaration
? variableDeclaration('const', [
- variableDeclarator(path.node.id, replacement),
+ variableDeclarator(path.node.id, workletFactoryCall),
])
- : replacement
- );
+ : workletFactoryCall;
+
+ path.replaceWith(replacement);
}