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

feat(babel): add process.env.EXPO_OS #27509

Merged
merged 4 commits into from Mar 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/pages/guides/tree-shaking.mdx
Expand Up @@ -92,6 +92,8 @@ module.exports = function (api) {

</Collapsible>

Starting in SDK 51, `process.env.EXPO_OS` can be used to detect the platform that the JavaScript was bundled for (cannot change at runtime). This value does not support platform shaking imports due to how Metro minifies code after dependency resolution.

## Remove development-only code

In your project, there might be code designed to help with the development process. It should be excluded from the production bundle. To handle these scenarios, use the `process.env.NODE_ENV `environment variable or the non-standard `__DEV__` global boolean.
Expand Down
1 change: 1 addition & 0 deletions packages/babel-preset-expo/CHANGELOG.md
Expand Up @@ -6,6 +6,7 @@

### 🎉 New features

- Add support for using `process.env.EXPO_OS` to detect the platform without platform shaking imports. ([#27509](https://github.com/expo/expo/pull/27509) by [@EvanBacon](https://github.com/EvanBacon))
- Add basic `react-server` support. ([#27264](https://github.com/expo/expo/pull/27264) by [@EvanBacon](https://github.com/EvanBacon))

### 🐛 Bug fixes
Expand Down
15 changes: 7 additions & 8 deletions packages/babel-preset-expo/build/index.js

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

104 changes: 104 additions & 0 deletions packages/babel-preset-expo/src/__tests__/minify-sanity.test.ts
@@ -0,0 +1,104 @@
// Run a number of basic operations on the minifier to ensure it works as expected
import * as babel from '@babel/core';

import { minifyLikeMetroAsync } from './minify-util';
import preset from '..';

function getCaller(props: Record<string, string | boolean>): babel.TransformCaller {
return props as unknown as babel.TransformCaller;
}

const DEFAULT_OPTS = {
babelrc: false,
presets: [[preset]],
plugins: [
// Fold constants to emulate Metro
require('metro-transform-plugins/src/constant-folding-plugin.js'),
],
sourceMaps: true,
filename: 'unknown',
configFile: false,
compact: true,
comments: false,
retainLines: false,
};

it(`removes unused functions in development`, async () => {
const options = {
...DEFAULT_OPTS,
caller: getCaller({
name: 'metro',
engine: 'hermes',
platform: 'android',
isDev: false,
}),
};

const src = `
function foo() {}
function bar() { foo() }
`;
expect((await minifyLikeMetroAsync(babel.transform(src, options)!)).code).toBe('');
});

it(`retains exported functions`, async () => {
const options = {
...DEFAULT_OPTS,
caller: getCaller({
name: 'metro',
engine: 'hermes',
platform: 'android',
isDev: false,
// Disable CJS
supportsStaticESM: true,
}),
};

const src = `
export function foo() {}
`;
expect((await minifyLikeMetroAsync(babel.transform(src, options)!)).code).toBe(
'export function foo(){}'
);
});

it(`does not remove top level variables due to module=false in the minifier (not passed from transformer)`, async () => {
const options = {
...DEFAULT_OPTS,
caller: getCaller({
name: 'metro',
engine: 'hermes',
platform: 'android',
isDev: false,
}),
};

const src = `
const a = 0;
`;
expect((await minifyLikeMetroAsync(babel.transform(src, options)!)).code).toBe('var a=0;');
});

it(`can remove unused functions based on platform-specific checks`, async () => {
const options = {
...DEFAULT_OPTS,
caller: getCaller({
name: 'metro',
engine: 'hermes',
platform: 'android',
isDev: false,
supportsStaticESM: true,
}),
};

// noop should be removed when bundling for android
const src = `
function noop() {}
function android() {}

export const value = process.env.EXPO_OS === 'android' ? android : noop;
`;
expect((await minifyLikeMetroAsync(babel.transform(src, options)!)).code).toBe(
'function android(){}export var value=android;'
);
});
26 changes: 26 additions & 0 deletions packages/babel-preset-expo/src/__tests__/minify-util.ts
@@ -0,0 +1,26 @@
// Rough estimation of how minifying works by default in Expo / Metro.
// We'll need to update this if we ever change the default minifier.
import getDefaultConfig from 'metro-config/src/defaults';
import metroMinify from 'metro-minify-terser';

export async function minifyLikeMetroAsync({
code,
map,
}: {
code?: string | null;
map?: any;
}): Promise<{ code?: string; map?: any }> {
if (code == null) throw new Error('code is required for minifying');
// @ts-expect-error: untyped function
const terserConfig = (await getDefaultConfig('/')).transformer.minifierConfig;
return metroMinify({
code,
map,
reserved: [],
config: {
...terserConfig,
// TODO: Enable module support
// compress: { module: true },
},
});
}
@@ -1,5 +1,6 @@
import * as babel from '@babel/core';

import { minifyLikeMetroAsync } from './minify-util';
import preset from '..';

function getCaller(props: Record<string, string | boolean>): babel.TransformCaller {
Expand Down Expand Up @@ -30,6 +31,31 @@ function stripReactNativeImport(code: string) {
.replace('var _reactNative=require("react-native");', '');
}

it(`does not remove Platform module in development`, () => {
const options = {
...DEFAULT_OPTS,
caller: getCaller({ name: 'metro', engine: 'hermes', platform: 'web', isDev: true }),
};

const sourceCode = `
import { Platform } from 'react-native';
if (Platform.OS === 'ios') {
console.log('ios')
}
Platform.select({
ios: () => console.log('ios'),
web: () => console.log('web'),
android: () => console.log('android'),
})
`;

expect(stripReactNativeImport(babel.transform(sourceCode, options)!.code!)).toMatch(
/_Platform\.default\.OS===/
);
});

it(`removes Platform module without import (undefined behavior)`, () => {
const options = {
...DEFAULT_OPTS,
Expand Down Expand Up @@ -155,5 +181,61 @@ it(`removes __DEV__ usage`, () => {
}
`;

// No minfication needed here, the babel plugin does it to ensure the imports are removed before dependencies are collected.
expect(babel.transform(sourceCode, options)!.code!).toEqual(``);
});

describe('process.env.EXPO_OS', () => {
const originalEnv = process.env;
beforeEach(() => {
process.env = { ...originalEnv };
});
afterAll(() => {
process.env = originalEnv;
});

[true, false].forEach((isDev) => {
['development', 'test', 'production'].forEach((env) => {
it(`inlines process.env.EXPO_OS usage in NODE_ENV=${env} when bundling for dev=${isDev}`, () => {
process.env.NODE_ENV = env;
const options = {
babelrc: false,
presets: [preset],
filename: 'unknown',
// Make the snapshot easier to read
retainLines: true,
caller: getCaller({ name: 'metro', platform: 'ios', isDev }),
};

expect(babel.transform('process.env.EXPO_OS', options)!.code).toBe('"ios";');
expect(
babel.transform('process.env.EXPO_OS', {
...options,
caller: getCaller({ name: 'metro', platform: 'web', isDev }),
})!.code
).toBe('"web";');
});
});
});

it(`can use process.env.EXPO_OS to minify`, async () => {
const options = {
babelrc: false,
presets: [preset],
filename: 'unknown',
// Make the snapshot easier to read
compact: true,
caller: getCaller({ name: 'metro', platform: 'ios', isDev: false }),
};

const src = `
if (process.env.EXPO_OS === 'ios') {
console.log('ios');
}
`;

const results = babel.transform(src, options)!;
const min = await minifyLikeMetroAsync(results);
expect(min.code).toBe("console.log('ios');");
});
});
16 changes: 8 additions & 8 deletions packages/babel-preset-expo/src/index.ts
Expand Up @@ -138,17 +138,17 @@ function babelPresetExpo(api: ConfigAPI, options: BabelPresetExpoOptions = {}):
);
}

const inlineEnv: Record<string, string> = {
EXPO_OS: platform,
};

// Allow jest tests to redefine the environment variables.
if (process.env.NODE_ENV !== 'test') {
extraPlugins.push([
expoInlineTransformEnvVars,
{
// These values should not be prefixed with `EXPO_PUBLIC_`, so we don't
// squat user-defined environment variables.
EXPO_BASE_URL: baseUrl,
},
]);
// These values should not be prefixed with `EXPO_PUBLIC_`, so we don't
// squat user-defined environment variables.
inlineEnv['EXPO_BASE_URL'] = baseUrl;
}
extraPlugins.push([expoInlineTransformEnvVars, inlineEnv]);

// Only apply in non-server, for metro-only, in production environments, when the user hasn't disabled the feature.
// Webpack uses DefinePlugin for environment variables.
Expand Down
2 changes: 2 additions & 0 deletions packages/expo/CHANGELOG.md
Expand Up @@ -6,6 +6,8 @@

### 🎉 New features

- Add global type for `process.env.EXPO_OS` value. ([#27509](https://github.com/expo/expo/pull/27509) by [@EvanBacon](https://github.com/EvanBacon))

### 🐛 Bug fixes

- Fixed breaking changes from React-Native 0.74. ([#26357](https://github.com/expo/expo/pull/26357) by [@kudo](https://github.com/kudo))
Expand Down
3 changes: 2 additions & 1 deletion packages/expo/build/environment/DevLoadingView.d.ts

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/build/environment/DevLoadingView.d.ts.map

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

3 changes: 3 additions & 0 deletions packages/expo/src/ts-declarations/process.d.ts
Expand Up @@ -12,6 +12,9 @@ declare const process: {

/** Maps to the `experiments.baseUrl` property in the project Expo config. This is injected by `babel-preset-expo` and supports automatic cache invalidation. */
EXPO_BASE_URL?: string;

/** Build-time representation of the `Platform.OS` value that the current JavaScript was bundled for. Does not support platform shaking wrapped require statements. */
EXPO_OS?: string;
};
[key: string]: any;
};