Skip to content

Commit

Permalink
feat(babel-preset-expo): auto add reanimated babel plugin when availa…
Browse files Browse the repository at this point in the history
…ble (#23798)

# Why

One less thing to think about. However, installing the package would
mean you need to magically know about invalidating the Metro Babel
cache. To account for this the `expo/metro-config` will now
auto-invalidate when the reanimated version changes. This invalidation
also solves another common issue where the worklet format changes
between versions.

# Test Plan

- Added a babel test to ensure duplicates don't break the worklet
format.
- Manually tested by packing `babel-preset-expo` and installing a
tabs@49 template. If you install `react-native-reanimated`, then restart
the server, worklets will be transpiled.

# Checklist

<!--
Please check the appropriate items below if they apply to your diff.
This is required for changes to Expo modules.
-->

- [ ] Documentation is up to date to reflect these changes (eg:
https://docs.expo.dev and README.md).
- [ ] Conforms with the [Documentation Writing Style
Guide](https://github.com/expo/expo/blob/main/guides/Expo%20Documentation%20Writing%20Style%20Guide.md)
- [ ] This diff will work correctly for `npx expo prebuild` & EAS Build
(eg: updated a module plugin).

---------

Co-authored-by: Expo Bot <34669131+expo-bot@users.noreply.github.com>
  • Loading branch information
EvanBacon and expo-bot committed Aug 2, 2023
1 parent 74e0b8d commit 421d95b
Show file tree
Hide file tree
Showing 11 changed files with 133 additions and 55 deletions.
30 changes: 0 additions & 30 deletions docs/pages/versions/unversioned/sdk/reanimated.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -20,36 +20,6 @@ import PlatformsSection from '~/components/plugins/PlatformsSection';

<APIInstallSection href="https://docs.swmansion.com/react-native-reanimated/docs/fundamentals/installation" />

After the installation completes, you must also add the Babel plugin to **babel.config.js**:

```js babel.config.js
module.exports = function (api) {
api.cache(true);
return {
presets: ['babel-preset-expo'],
plugins: ['react-native-reanimated/plugin'],
};
};
```

After you add the Babel plugin, restart your development server and clear the bundler cache: `npx expo start --clear`.

> If you load other Babel plugins, the Reanimated plugin has to be the last item in the plugins array.
### Web support

**For web,** you'll have to install the [`@babel/plugin-proposal-export-namespace-from`](https://babeljs.io/docs/en/babel-plugin-proposal-export-namespace-from#installation) Babel plugin and update the **babel.config.js** to load it:

{/* prettier-ignore */}
```js babel.config.js
plugins: [
'@babel/plugin-proposal-export-namespace-from',
'react-native-reanimated/plugin',
],
```

After you add the Babel plugin, restart your development server and clear the bundler cache: `npx expo start --clear`.

## Usage

You should refer to the [react-native-reanimated docs](https://docs.swmansion.com/react-native-reanimated/docs/) for more information on the API and its usage. But the following example (courtesy of that repo) is a quick way to get started.
Expand Down
2 changes: 2 additions & 0 deletions packages/@expo/metro-config/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

### 🎉 New features

- Automatically invalidate cache when `react-native-reanimated` version changes or is added. ([#23798](https://github.com/expo/expo/pull/23798) by [@EvanBacon](https://github.com/EvanBacon))

### 🐛 Bug fixes

### 💡 Others
Expand Down
26 changes: 15 additions & 11 deletions packages/@expo/metro-config/build/ExpoMetroConfig.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/@expo/metro-config/build/ExpoMetroConfig.js.map

Large diffs are not rendered by default.

29 changes: 17 additions & 12 deletions packages/@expo/metro-config/src/ExpoMetroConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,11 @@ export function getDefaultConfig(
// Add support for cjs (without platform extensions).
sourceExts.push('cjs');

const reanimatedVersion = getPkgVersion(projectRoot, 'react-native-reanimated');

let sassVersion: string | null = null;
if (options.isCSSEnabled) {
sassVersion = getSassVersion(projectRoot);
sassVersion = getPkgVersion(projectRoot, 'sass');
// Enable SCSS by default so we can provide a better error message
// when sass isn't installed.
sourceExts.push('scss', 'sass', 'css');
Expand All @@ -114,6 +116,7 @@ export function getDefaultConfig(
console.log(`- Exotic: ${isExotic}`);
console.log(`- Env Files: ${envFiles}`);
console.log(`- Sass: ${sassVersion}`);
console.log(`- Reanimated: ${reanimatedVersion}`);
console.log();
}
const {
Expand Down Expand Up @@ -186,6 +189,8 @@ export function getDefaultConfig(
? stableHash(JSON.stringify(pkg.browserslist)).toString('hex')
: null,
sassVersion,
// Ensure invalidation when the version changes due to the Babel plugin.
reanimatedVersion,

// `require.context` support
unstable_allowRequireContext: true,
Expand Down Expand Up @@ -222,17 +227,17 @@ export { MetroConfig, INTERNAL_CALLSITES_REGEX };
// re-export for legacy cases.
export const EXPO_DEBUG = env.EXPO_DEBUG;

function getSassVersion(projectRoot: string): string | null {
const sassPkg = resolveFrom.silent(projectRoot, 'sass');
if (!sassPkg) return null;
const sassPkgJson = findUpPackageJson(sassPkg);
if (!sassPkgJson) return null;
const pkg = JsonFile.read(sassPkgJson);

debug('sass package.json:', sassPkgJson);
const sassVersion = pkg.version;
if (typeof sassVersion === 'string') {
return sassVersion;
function getPkgVersion(projectRoot: string, pkgName: string): string | null {
const targetPkg = resolveFrom.silent(projectRoot, pkgName);
if (!targetPkg) return null;
const targetPkgJson = findUpPackageJson(targetPkg);
if (!targetPkgJson) return null;
const pkg = JsonFile.read(targetPkgJson);

debug(`${pkgName} package.json:`, targetPkgJson);
const pkgVersion = pkg.version;
if (typeof pkgVersion === 'string') {
return pkgVersion;
}

return null;
Expand Down
2 changes: 2 additions & 0 deletions packages/babel-preset-expo/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

### 🎉 New features

- Automatically add `react-native-reanimated/plugin` when available. ([#23798](https://github.com/expo/expo/pull/23798) by [@EvanBacon](https://github.com/EvanBacon))

### 🐛 Bug fixes

### 💡 Others
Expand Down
4 changes: 4 additions & 0 deletions packages/babel-preset-expo/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ If the `bundler` is not defined, it will default to checking if a `babel-loader`

## Options

### `reanimated`

`boolean`, defaults to `true`. Set `reanimated: false` to disable adding the `react-native-reanimated/plugin` when `react-native-reanimated` is installed.

### [`jsxRuntime`](https://babeljs.io/docs/en/babel-plugin-transform-react-jsx#runtime)

`classic | automatic`, defaults to `automatic`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,22 @@ return React.createElement(_reactNative.View,null,React.createElement(_reactNati
}"
`;

exports[`metro supports disabling reanimated 1`] = `
"
function someWorklet(greeting){
'worklet';
console.log("Hey I'm running on the UI thread");
}"
`;

exports[`metro supports reanimated worklets 1`] = `
"var _worklet_2797951844470_init_data={code:"function someWorklet(greeting) {\\n console.log(\\"Hey I'm running on the UI thread\\");\\n}",location:"[mock]/worklet.js",sourceMap:"{\\"version\\":3,\\"mappings\\":\\"AAAA;EAAAA;AACA\\",\\"names\\":[\\"console\\"],\\"sources\\":[\\"[mock]/worklet.js\\"]}"};var
someWorklet=function(){var _e=[new global.Error(),1,-27];var _f=function _f(greeting){
console.log("Hey I'm running on the UI thread");
};_f._closure={};_f.__initData=_worklet_2797951844470_init_data;_f.__workletHash=2797951844470;_f.__stackDetails=_e;_f.__version="3.3.0";return _f;}();"
`;
exports[`metro transpiles non-standard exports 1`] = `"Object.defineProperty(exports,"__esModule",{value:true});exports.default=void 0;var _default=_interopRequireWildcard(require("./Animated"));exports.default=_default;function _getRequireWildcardCache(nodeInterop){if(typeof WeakMap!=="function")return null;var cacheBabelInterop=new WeakMap();var cacheNodeInterop=new WeakMap();return(_getRequireWildcardCache=function _getRequireWildcardCache(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop;})(nodeInterop);}function _interopRequireWildcard(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule){return obj;}if(obj===null||typeof obj!=="object"&&typeof obj!=="function"){return{default:obj};}var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj)){return cache.get(obj);}var newObj={};var hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj){if(key!=="default"&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;if(desc&&(desc.get||desc.set)){Object.defineProperty(newObj,key,desc);}else{newObj[key]=obj[key];}}}newObj.default=obj;if(cache){cache.set(obj,newObj);}return newObj;}"`;
exports[`metro uses the platform's react-native import 1`] = `
Expand Down Expand Up @@ -312,6 +328,22 @@ return React.createElement(View,null,React.createElement(Text,null,"Hello World"
}"
`;
exports[`webpack supports disabling reanimated 1`] = `
"
function someWorklet(greeting){
'worklet';
console.log("Hey I'm running on the UI thread");
}"
`;
exports[`webpack supports reanimated worklets 1`] = `
"var _worklet_2797951844470_init_data={code:"function someWorklet(greeting) {\\n console.log(\\"Hey I'm running on the UI thread\\");\\n}",location:"[mock]/worklet.js",sourceMap:"{\\"version\\":3,\\"mappings\\":\\"AAAA;EAAAA;AACA\\",\\"names\\":[\\"console\\"],\\"sources\\":[\\"[mock]/worklet.js\\"]}"};var
someWorklet=function(){var _e=[new global.Error(),1,-27];var _f=function _f(greeting){
console.log("Hey I'm running on the UI thread");
};_f._closure={};_f.__initData=_worklet_2797951844470_init_data;_f.__workletHash=2797951844470;_f.__stackDetails=_e;_f.__version="3.3.0";return _f;}();"
`;
exports[`webpack transpiles non-standard exports 1`] = `
"import*as _default from"./Animated";export{_default as
default};"
Expand Down
50 changes: 50 additions & 0 deletions packages/babel-preset-expo/__tests__/index-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,56 @@ export default function App() {
expect(code).toMatchSnapshot();
});

it(`supports disabling reanimated`, () => {
expect(require.resolve('react-native-reanimated/plugin')).toBeDefined();

const samplesPath = path.resolve(__dirname, 'samples/worklet.js');

const options = {
babelrc: false,
presets: [[preset, { reanimated: false, jsxRuntime: 'automatic' }]],
// Make the snapshot easier to read
retainLines: true,
caller,
};

const code = babel.transformFileSync(samplesPath, options).code;
expect(code).toContain("'worklet';");
expect(code).toMatchSnapshot();
});

it(`supports reanimated worklets`, () => {
expect(require.resolve('react-native-reanimated/plugin')).toBeDefined();

const samplesPath = path.resolve(__dirname, 'samples/worklet.js');

const options = {
babelrc: false,
presets: [[preset, { jsxRuntime: 'automatic' }]],
// Make the snapshot easier to read
retainLines: true,
caller,
};

function stablePaths(src) {
return src.replace(new RegExp(samplesPath, 'g'), '[mock]/worklet.js');
}

const code = stablePaths(babel.transformFileSync(samplesPath, options).code);

expect(code).toMatchSnapshot();

expect(
stablePaths(
babel.transformFileSync(samplesPath, {
...options,
// Test that duplicate plugins make no difference
plugins: [require.resolve('react-native-reanimated/plugin')],
}).code
)
).toBe(code);
});

it(`supports classic JSX runtime`, () => {
const options = {
babelrc: false,
Expand Down
5 changes: 5 additions & 0 deletions packages/babel-preset-expo/__tests__/samples/worklet.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// eslint-disable-next-line no-unused-vars
function someWorklet(greeting) {
'worklet';
console.log("Hey I'm running on the UI thread");
}
6 changes: 5 additions & 1 deletion packages/babel-preset-expo/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ const lazyImportsBlacklist = require('./lazy-imports-blacklist');
let hasWarnedJsxRename = false;

module.exports = function (api, options = {}) {
const { web = {}, native = {} } = options;
const { web = {}, native = {}, reanimated } = options;

const bundler = api.caller(getBundler);
const isWebpack = bundler === 'webpack';
Expand Down Expand Up @@ -112,6 +112,10 @@ module.exports = function (api, options = {}) {
platform === 'web' && [require.resolve('babel-plugin-react-native-web')],
isWebpack && platform !== 'web' && [require.resolve('./plugins/disable-ambiguous-requires')],
require.resolve('@babel/plugin-proposal-export-namespace-from'),

// Automatically add `react-native-reanimated/plugin` when the package is installed.
hasModule('react-native-reanimated') &&
reanimated !== false && [require.resolve('react-native-reanimated/plugin')],
].filter(Boolean),
};
};
Expand Down

0 comments on commit 421d95b

Please sign in to comment.