feat(smart-app): implement complete mobile app MVP

- App.tsx: full navigation (Auth stack + Main tabs with 5 screens)
- Auth: LoginScreen, RegisterScreen, ForgotPasswordScreen
- HomeScreen: dashboard with IoT metrics, weather widget, alerts, quick actions, sensors
- MapScreen: interactive map with layer toggles (6 layers)
- MarketplaceScreen: categories (6), products (5), search
- ChatScreen: AI chat with quick prompts (4), bot responses
- ProfileScreen: user info, stats, menu (9 items), logout
- AlertsScreen: alert list with severity, acknowledge
- SensorsScreen: sensor list with type filters (6 types), search
- ZonesScreen: zone cards with stats
- SettingsScreen: language picker (FR/EN/ES/DE), privacy, about
- Stores: iotStore (sensors, zones, alerts), notificationStore, uiStore + i18n
- Hooks: useSensors, useAlerts, useNotifications, useLocation
- Components: Card, Button, LoadingSpinner, ErrorBoundary, Header
- Services: iotService, notificationService (with axios API client)
- Utils: formatters (temp, AQI, noise, dates), validators (email, password, IBAN)
- Theme: colors.ts with full design system (Blue Ocean palette)
- Ditto: fixed MongoDB connection, new JWT secrets, official gateway image
This commit is contained in:
Eric FELIXINE
2026-06-01 18:00:35 -04:00
parent 08ca495bde
commit e30ae8ed09
35578 changed files with 3703534 additions and 43 deletions

View File

@@ -0,0 +1 @@
export declare function reactClientReferencesPlugin(): babel.PluginObj;

View File

@@ -0,0 +1,120 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.reactClientReferencesPlugin = void 0;
/**
* Copyright © 2024 650 Industries.
*/
const core_1 = require("@babel/core");
const url_1 = __importDefault(require("url"));
function reactClientReferencesPlugin() {
return {
name: 'expo-client-references',
visitor: {
Program(path, state) {
const isUseClient = path.node.directives.some((directive) => directive.value.value === 'use client');
// TODO: use server can be added to scopes inside of the file. https://github.com/facebook/react/blob/29fbf6f62625c4262035f931681c7b7822ca9843/packages/react-server-dom-webpack/src/ReactFlightWebpackNodeRegister.js#L55
const isUseServer = path.node.directives.some((directive) => directive.value.value === 'use server');
if (isUseClient && isUseServer) {
throw path.buildCodeFrameError("It's not possible to have both `use client` and `use server` directives in the same file.");
}
const filePath = state.file.opts.filename;
if (!filePath) {
// This can happen in tests or systems that use Babel standalone.
throw new Error('[Babel] Expected a filename to be set in the state');
}
// File starts with "use client" directive.
if (!isUseClient) {
// Do nothing for code that isn't marked as a client component.
return;
}
const outputKey = url_1.default.pathToFileURL(filePath).href;
// We need to add all of the exports to support `export * from './module'` which iterates the keys of the module.
const proxyModule = [
`const proxy = /*@__PURE__*/ require("react-server-dom-webpack/server").createClientModuleProxy(${JSON.stringify(outputKey)});`,
`module.exports = proxy;`,
];
const getProxy = (exportName) => {
return `(/*@__PURE__*/ proxy[${JSON.stringify(exportName)}])`;
};
const proxyExports = new Set();
const pushProxy = (exportName) => {
proxyExports.add(exportName);
if (exportName === 'default') {
proxyModule.push(`export default ${getProxy(exportName)};`);
}
else {
proxyModule.push(`export const ${exportName} = ${getProxy(exportName)};`);
}
};
// Collect all of the exports
path.traverse({
ExportNamedDeclaration(exportPath) {
if (exportPath.node.declaration) {
if (exportPath.node.declaration.type === 'VariableDeclaration') {
exportPath.node.declaration.declarations.forEach((declaration) => {
if (declaration.id.type === 'Identifier') {
const exportName = declaration.id.name;
pushProxy(exportName);
}
});
}
else if (exportPath.node.declaration.type === 'FunctionDeclaration') {
const exportName = exportPath.node.declaration.id?.name;
if (exportName) {
pushProxy(exportName);
}
}
else if (exportPath.node.declaration.type === 'ClassDeclaration') {
const exportName = exportPath.node.declaration.id?.name;
if (exportName) {
pushProxy(exportName);
}
}
else if (!['InterfaceDeclaration', 'TypeAlias'].includes(exportPath.node.declaration.type)) {
// TODO: What is this type?
console.warn('[babel-preset-expo] Unsupported export specifier for "use client":', exportPath.node.declaration.type);
}
}
else {
exportPath.node.specifiers.forEach((specifier) => {
if (core_1.types.isIdentifier(specifier.exported)) {
const exportName = specifier.exported.name;
pushProxy(exportName);
}
else {
// TODO: What is this type?
console.warn('[babel-preset-expo] Unsupported export specifier for "use client":', specifier);
}
});
}
},
ExportDefaultDeclaration() {
pushProxy('default');
},
});
// Clear the body
path.node.body = [];
path.node.directives = [];
path.pushContainer('body', core_1.template.ast(proxyModule.join('\n')));
assertExpoMetadata(state.file.metadata);
// Save the client reference in the metadata.
if (!state.file.metadata.clientReferences) {
state.file.metadata.clientReferences ??= [];
}
state.file.metadata.clientReferences.push(outputKey);
// Store the proxy export names for testing purposes.
state.file.metadata.proxyExports = [...proxyExports];
},
},
};
}
exports.reactClientReferencesPlugin = reactClientReferencesPlugin;
function assertExpoMetadata(metadata) {
if (metadata && typeof metadata === 'object') {
return;
}
throw new Error('Expected Babel state.file.metadata to be an object');
}

View File

@@ -0,0 +1,17 @@
export declare function hasModule(name: string): boolean;
/** Determine which bundler is being used. */
export declare function getBundler(caller?: any): "metro" | "webpack" | null;
export declare function getPlatform(caller?: any): string | null | undefined;
export declare function getPossibleProjectRoot(caller?: any): string | null | undefined;
/** If bundling for a react-server target. */
export declare function getIsReactServer(caller?: any): boolean;
export declare function getIsDev(caller?: any): boolean;
export declare function getIsFastRefreshEnabled(caller?: any): boolean | undefined;
export declare function getIsProd(caller?: any): boolean;
export declare function getIsNodeModule(caller?: any): boolean;
export declare function getBaseUrl(caller?: any): string;
export declare function getReactCompiler(caller?: any): boolean;
export declare function getIsServer(caller?: any): boolean;
export declare function getExpoRouterAbsoluteAppRoot(caller?: any): string;
export declare function getInlineEnvVarsEnabled(caller?: any): boolean;
export declare function getAsyncRoutes(caller?: any): boolean;

View File

@@ -0,0 +1,146 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.getAsyncRoutes = exports.getInlineEnvVarsEnabled = exports.getExpoRouterAbsoluteAppRoot = exports.getIsServer = exports.getReactCompiler = exports.getBaseUrl = exports.getIsNodeModule = exports.getIsProd = exports.getIsFastRefreshEnabled = exports.getIsDev = exports.getIsReactServer = exports.getPossibleProjectRoot = exports.getPlatform = exports.getBundler = exports.hasModule = void 0;
const path_1 = __importDefault(require("path"));
function hasModule(name) {
try {
return !!require.resolve(name);
}
catch (error) {
if (error.code === 'MODULE_NOT_FOUND' && error.message.includes(name)) {
return false;
}
throw error;
}
}
exports.hasModule = hasModule;
/** Determine which bundler is being used. */
function getBundler(caller) {
assertExpoBabelCaller(caller);
if (!caller)
return null;
if (caller.bundler)
return caller.bundler;
if (
// Known tools that use `webpack`-mode via `babel-loader`: `@expo/webpack-config`, Next.js <10
caller.name === 'babel-loader' ||
// NextJS 11 uses this custom caller name.
caller.name === 'next-babel-turbo-loader') {
return 'webpack';
}
// Assume anything else is Metro.
return 'metro';
}
exports.getBundler = getBundler;
function getPlatform(caller) {
assertExpoBabelCaller(caller);
if (!caller)
return null;
if (caller.platform)
return caller.platform;
const bundler = getBundler(caller);
if (bundler === 'webpack') {
return 'web';
}
// unknown
return caller.platform;
}
exports.getPlatform = getPlatform;
function getPossibleProjectRoot(caller) {
assertExpoBabelCaller(caller);
if (!caller)
return null;
if (caller.projectRoot)
return caller.projectRoot;
// unknown
return process.env.EXPO_PROJECT_ROOT;
}
exports.getPossibleProjectRoot = getPossibleProjectRoot;
/** If bundling for a react-server target. */
function getIsReactServer(caller) {
assertExpoBabelCaller(caller);
return caller?.isReactServer ?? false;
}
exports.getIsReactServer = getIsReactServer;
function assertExpoBabelCaller(caller) { }
function getIsDev(caller) {
assertExpoBabelCaller(caller);
if (caller?.isDev != null)
return caller.isDev;
// https://babeljs.io/docs/options#envname
return process.env.BABEL_ENV === 'development' || process.env.NODE_ENV === 'development';
}
exports.getIsDev = getIsDev;
function getIsFastRefreshEnabled(caller) {
assertExpoBabelCaller(caller);
if (!caller)
return false;
return caller.isHMREnabled && !caller.isServer && !caller.isNodeModule && getIsDev(caller);
}
exports.getIsFastRefreshEnabled = getIsFastRefreshEnabled;
function getIsProd(caller) {
assertExpoBabelCaller(caller);
if (caller?.isDev != null)
return caller.isDev === false;
// https://babeljs.io/docs/options#envname
return process.env.BABEL_ENV === 'production' || process.env.NODE_ENV === 'production';
}
exports.getIsProd = getIsProd;
function getIsNodeModule(caller) {
return caller?.isNodeModule ?? false;
}
exports.getIsNodeModule = getIsNodeModule;
function getBaseUrl(caller) {
assertExpoBabelCaller(caller);
return caller?.baseUrl ?? '';
}
exports.getBaseUrl = getBaseUrl;
function getReactCompiler(caller) {
assertExpoBabelCaller(caller);
return caller?.supportsReactCompiler ?? false;
}
exports.getReactCompiler = getReactCompiler;
function getIsServer(caller) {
assertExpoBabelCaller(caller);
return caller?.isServer ?? false;
}
exports.getIsServer = getIsServer;
function getExpoRouterAbsoluteAppRoot(caller) {
assertExpoBabelCaller(caller);
const rootModuleId = caller?.routerRoot ?? './app';
if (path_1.default.isAbsolute(rootModuleId)) {
return rootModuleId;
}
const projectRoot = getPossibleProjectRoot(caller) || '/';
return path_1.default.join(projectRoot, rootModuleId);
}
exports.getExpoRouterAbsoluteAppRoot = getExpoRouterAbsoluteAppRoot;
function getInlineEnvVarsEnabled(caller) {
assertExpoBabelCaller(caller);
const isWebpack = getBundler(caller) === 'webpack';
const isDev = getIsDev(caller);
const isServer = getIsServer(caller);
const isNodeModule = getIsNodeModule(caller);
const preserveEnvVars = caller?.preserveEnvVars;
// Development env vars are added in the serializer to avoid caching issues in development.
// Servers have env vars left as-is to read from the environment.
return !isNodeModule && !isWebpack && !isDev && !isServer && !preserveEnvVars;
}
exports.getInlineEnvVarsEnabled = getInlineEnvVarsEnabled;
function getAsyncRoutes(caller) {
assertExpoBabelCaller(caller);
const isServer = getIsServer(caller);
if (isServer) {
return false;
}
const isProd = getIsProd(caller);
const platform = getPlatform(caller);
if (platform !== 'web' && isProd) {
return false;
}
return caller?.asyncRoutes ?? false;
}
exports.getAsyncRoutes = getAsyncRoutes;

View File

@@ -0,0 +1,12 @@
/**
* Copyright © 2024 650 Industries.
* Copyright (c) 2016 Formidable
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import * as t from '@babel/types';
declare const plugin: ({ types }: {
types: typeof t;
}) => babel.PluginObj;
export default plugin;

View File

@@ -0,0 +1,141 @@
"use strict";
/**
* Copyright © 2024 650 Industries.
* Copyright (c) 2016 Formidable
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
const t = __importStar(require("@babel/types"));
/**
* Replace a node with a given value. If the replacement results in a BinaryExpression, it will be
* evaluated. For example, if the result of the replacement is `var x = "production" === "production"`
* The evaluation will make a second replacement resulting in `var x = true`
*/
function replaceAndEvaluateNode(nodePath, replacement) {
nodePath.replaceWith(t.valueToNode(replacement));
if (nodePath.parentPath && nodePath.parentPath.isBinaryExpression()) {
const result = nodePath.parentPath.evaluate();
if (result.confident) {
nodePath.parentPath.replaceWith(t.valueToNode(result.value));
}
}
}
/**
* Checks if the given identifier is an ES module import
* @param {babelNode} identifierNodePath The node to check
* @return {boolean} Indicates if the provided node is an import specifier or references one
*/
const isImportIdentifier = (identifierNodePath) => {
if (identifierNodePath.container &&
!Array.isArray(identifierNodePath.container) &&
'type' in identifierNodePath.container) {
return (identifierNodePath.container.type === 'ImportDefaultSpecifier' ||
identifierNodePath.container.type === 'ImportSpecifier');
}
return false;
};
const memberExpressionComparator = (nodePath, value) => nodePath.matchesPattern(value);
const identifierComparator = (nodePath, value) => 'name' in nodePath.node && nodePath.node.name === value;
const unaryExpressionComparator = (nodePath, value) => {
if ('argument' in nodePath.node && nodePath.node.argument && 'name' in nodePath.node?.argument) {
return nodePath.node.argument.name === value;
}
return false;
};
const isLeftHandSideOfAssignmentExpression = (node, parent) => t.isAssignmentExpression(parent) && parent.left === node;
const TYPEOF_PREFIX = 'typeof ';
const plugin = ({ types }) => {
const processNode = (replacements, nodePath, comparator) => {
const replacementKey = Object.keys(replacements).find((value) => comparator(nodePath, value));
if (typeof replacementKey === 'string' &&
replacements != null &&
replacementKey in replacements) {
replaceAndEvaluateNode(nodePath, replacements[replacementKey]);
}
};
return {
name: 'expo-define-globals',
visitor: {
// process.env.NODE_ENV;
MemberExpression(nodePath, state) {
if (
// Prevent rewriting if the member expression is on the left-hand side of an assignment
isLeftHandSideOfAssignmentExpression(nodePath.node, nodePath.parent)) {
return;
}
const replacements = state.opts;
assertOptions(replacements);
processNode(replacements, nodePath, memberExpressionComparator);
},
// const x = { version: VERSION };
// @ts-expect-error: Virtual type `ReferencedIdentifier` is not on types.
ReferencedIdentifier(nodePath, state) {
const binding = nodePath.scope?.getBinding(nodePath.node.name);
if (binding ||
// Don't transform import identifiers. This is meant to mimic webpack's
// DefinePlugin behavior.
isImportIdentifier(nodePath) ||
// Do not transform Object keys / properties unless they are computed like {[key]: value}
(nodePath.key === 'key' &&
nodePath.parent &&
'computed' in nodePath.parent &&
nodePath.parent.computed === false) ||
(nodePath.key === 'property' &&
nodePath.parent &&
'computed' in nodePath.parent &&
nodePath.parent.computed === false)) {
return;
}
const replacements = state.opts;
assertOptions(replacements);
processNode(replacements, nodePath, identifierComparator);
},
// typeof window
UnaryExpression(nodePath, state) {
if (nodePath.node.operator !== 'typeof') {
return;
}
const replacements = state.opts;
assertOptions(replacements);
const typeofValues = {};
Object.keys(replacements).forEach((key) => {
if (key.substring(0, TYPEOF_PREFIX.length) === TYPEOF_PREFIX) {
typeofValues[key.substring(TYPEOF_PREFIX.length)] = replacements[key];
}
});
processNode(typeofValues, nodePath, unaryExpressionComparator);
},
},
};
};
function assertOptions(opts) {
if (opts == null || typeof opts !== 'object') {
throw new Error('define plugin expects an object as options');
}
}
exports.default = plugin;

View File

@@ -0,0 +1,8 @@
/**
* Copyright © 2024 650 Industries.
*/
import { ConfigAPI, types } from '@babel/core';
/** Prevent importing certain known imports in given environments. This is for sanity to ensure a module never accidentally gets imported unexpectedly. */
export declare function environmentRestrictedImportsPlugin(api: ConfigAPI & {
types: typeof types;
}): babel.PluginObj;

View File

@@ -0,0 +1,61 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.environmentRestrictedImportsPlugin = void 0;
const common_1 = require("./common");
const FORBIDDEN_CLIENT_IMPORTS = ['server-only'];
const FORBIDDEN_REACT_SERVER_IMPORTS = ['client-only'];
/** Prevent importing certain known imports in given environments. This is for sanity to ensure a module never accidentally gets imported unexpectedly. */
function environmentRestrictedImportsPlugin(api) {
const { types: t } = api;
const isReactServer = api.caller(common_1.getIsReactServer);
const forbiddenPackages = isReactServer
? FORBIDDEN_REACT_SERVER_IMPORTS
: FORBIDDEN_CLIENT_IMPORTS;
function checkSource(source, path) {
forbiddenPackages.forEach((forbiddenImport) => {
if (source === forbiddenImport) {
if (isReactServer) {
throw path.buildCodeFrameError(`Importing '${forbiddenImport}' module is not allowed in a React server bundle. Add the "use client" directive to this file or one of the parent modules to allow importing this module.`);
}
else {
throw path.buildCodeFrameError(`Importing '${forbiddenImport}' module is not allowed in a client component.`);
}
}
});
}
return {
name: 'expo-environment-restricted-imports-plugin',
visitor: {
ImportDeclaration(path) {
checkSource(path.node.source.value, path);
},
ExportAllDeclaration(path) {
if (path.node.source) {
checkSource(path.node.source.value, path);
}
},
ExportNamedDeclaration(path) {
if (path.node.source) {
checkSource(path.node.source.value, path);
}
},
CallExpression(path) {
if ((('name' in path.node.callee && path.node.callee.name === 'require') ||
(t.isMemberExpression(path.node.callee) &&
'name' in path.node.callee.property &&
['resolveWeak', 'importAll', 'importDefault'].includes(path.node.callee.property.name))) &&
path.node.arguments.length > 0 &&
t.isStringLiteral(path.node.arguments[0])) {
checkSource(path.node.arguments[0].value, path);
}
// Handle dynamic import() syntax
else if (path.node.callee.type === 'Import' &&
path.node.arguments.length > 0 &&
t.isStringLiteral(path.node.arguments[0])) {
checkSource(path.node.arguments[0].value, path);
}
},
},
};
}
exports.environmentRestrictedImportsPlugin = environmentRestrictedImportsPlugin;

View File

@@ -0,0 +1,9 @@
import { ConfigAPI } from '@babel/core';
export declare function expoInlineManifestPlugin(api: ConfigAPI & {
types: any;
}): {
name: string;
visitor: {
MemberExpression(path: any, state: any): void;
};
};

View File

@@ -0,0 +1,149 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.expoInlineManifestPlugin = void 0;
const config_1 = require("expo/config");
const common_1 = require("./common");
const debug = require('debug')('expo:babel:inline-manifest');
// Convert expo value to PWA value
function ensurePWAorientation(orientation) {
if (orientation) {
const webOrientation = orientation.toLowerCase();
if (webOrientation !== 'default') {
return webOrientation;
}
}
return undefined;
}
const RESTRICTED_MANIFEST_FIELDS = [
'androidNavigationBar',
'androidStatusBar',
'privacy',
// Remove iOS and Android.
'ios',
'android',
// Hide internal / build values
'plugins',
'hooks',
'_internal',
// Remove metro-specific values
'assetBundlePatterns',
];
function getExpoConstantsManifest(projectRoot) {
const { exp } = getConfigMemo(projectRoot);
const manifest = applyWebDefaults(exp);
for (const field of RESTRICTED_MANIFEST_FIELDS) {
delete manifest[field];
}
return manifest;
}
function applyWebDefaults(appJSON) {
// For RN CLI support
const { web: webManifest = {}, splash = {}, ios = {}, android = {} } = appJSON;
// rn-cli apps use a displayName value as well.
const { appName, webName } = (0, config_1.getNameFromConfig)(appJSON);
const languageISOCode = webManifest.lang;
const primaryColor = appJSON.primaryColor;
const description = appJSON.description;
// The theme_color sets the color of the tool bar, and may be reflected in the app's preview in task switchers.
const webThemeColor = webManifest.themeColor || primaryColor;
const dir = webManifest.dir;
const shortName = webManifest.shortName || webName;
const display = webManifest.display;
const startUrl = webManifest.startUrl;
const { scope, crossorigin } = webManifest;
const barStyle = webManifest.barStyle;
const orientation = ensurePWAorientation(webManifest.orientation || appJSON.orientation);
/**
* **Splash screen background color**
* `https://developers.google.com/web/fundamentals/web-app-manifest/#splash-screen`
* The background_color should be the same color as the load page,
* to provide a smooth transition from the splash screen to your app.
*/
const backgroundColor = webManifest.backgroundColor || splash.backgroundColor; // No default background color
return {
...appJSON,
name: appName,
description,
primaryColor,
// Ensure these objects exist
ios: {
...ios,
},
android: {
...android,
},
web: {
...webManifest,
meta: undefined,
build: undefined,
scope,
crossorigin,
description,
startUrl,
shortName,
display,
orientation,
dir,
barStyle,
backgroundColor,
themeColor: webThemeColor,
lang: languageISOCode,
name: webName,
},
};
}
function getExpoAppManifest(projectRoot) {
if (process.env.APP_MANIFEST) {
return process.env.APP_MANIFEST;
}
const exp = getExpoConstantsManifest(projectRoot);
debug('public manifest', exp);
return JSON.stringify(exp);
}
let config;
function getConfigMemo(projectRoot) {
if (!config) {
config = (0, config_1.getConfig)(projectRoot, {
isPublicConfig: true,
skipSDKVersionRequirement: true,
});
}
return config;
}
// Convert `process.env.APP_MANIFEST` to a modified web-specific variation of the app.json public manifest.
function expoInlineManifestPlugin(api) {
const { types: t } = api;
const platform = api.caller(common_1.getPlatform);
const possibleProjectRoot = api.caller(common_1.getPossibleProjectRoot);
return {
name: 'expo-inline-manifest-plugin',
visitor: {
MemberExpression(path, state) {
// Web-only feature, the native manifest is provided dynamically by the client.
if (platform !== 'web') {
return;
}
if (!t.isIdentifier(path.node.object, { name: 'process' }) ||
!t.isIdentifier(path.node.property, { name: 'env' })) {
return;
}
const parent = path.parentPath;
if (!t.isMemberExpression(parent.node)) {
return;
}
const projectRoot = possibleProjectRoot || state.file.opts.root || '';
if (
// Surfaces the `app.json` (config) as an environment variable which is then parsed by
// `expo-constants` https://docs.expo.dev/versions/latest/sdk/constants/
t.isIdentifier(parent.node.property, {
name: 'APP_MANIFEST',
}) &&
!parent.parentPath.isAssignmentExpression()) {
const manifest = getExpoAppManifest(projectRoot);
parent.replaceWith(t.stringLiteral(manifest));
}
},
},
};
}
exports.expoInlineManifestPlugin = expoInlineManifestPlugin;

View File

@@ -0,0 +1,18 @@
import { ConfigAPI, types } from '@babel/core';
/**
* Inlines environment variables to configure the process:
*
* EXPO_PROJECT_ROOT
* EXPO_PUBLIC_USE_STATIC
* EXPO_ROUTER_ABS_APP_ROOT
* EXPO_ROUTER_APP_ROOT
* EXPO_ROUTER_IMPORT_MODE
*/
export declare function expoRouterBabelPlugin(api: ConfigAPI & {
types: typeof types;
}): {
name: string;
visitor: {
MemberExpression(path: any, state: any): void;
};
};

View File

@@ -0,0 +1,81 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.expoRouterBabelPlugin = void 0;
const core_1 = require("@babel/core");
const path_1 = __importDefault(require("path"));
const resolve_from_1 = __importDefault(require("resolve-from"));
const common_1 = require("./common");
const debug = require('debug')('expo:babel:router');
function getExpoRouterAppRoot(projectRoot, appFolder) {
// TODO: We should have cache invalidation if the expo-router/entry file location changes.
const routerEntry = (0, resolve_from_1.default)(projectRoot, 'expo-router/entry');
const appRoot = path_1.default.relative(path_1.default.dirname(routerEntry), appFolder);
debug('routerEntry', routerEntry, appFolder, appRoot);
return appRoot;
}
/**
* Inlines environment variables to configure the process:
*
* EXPO_PROJECT_ROOT
* EXPO_PUBLIC_USE_STATIC
* EXPO_ROUTER_ABS_APP_ROOT
* EXPO_ROUTER_APP_ROOT
* EXPO_ROUTER_IMPORT_MODE
*/
function expoRouterBabelPlugin(api) {
const { types: t } = api;
const platform = api.caller(common_1.getPlatform);
const possibleProjectRoot = api.caller(common_1.getPossibleProjectRoot);
const asyncRoutes = api.caller(common_1.getAsyncRoutes);
const routerAbsoluteRoot = api.caller(common_1.getExpoRouterAbsoluteAppRoot);
function isFirstInAssign(path) {
return core_1.types.isAssignmentExpression(path.parent) && path.parent.left === path.node;
}
return {
name: 'expo-router',
visitor: {
MemberExpression(path, state) {
const projectRoot = possibleProjectRoot || state.file.opts.root || '';
if (path.get('object').matchesPattern('process.env')) {
const key = path.toComputedKey();
if (t.isStringLiteral(key) && !isFirstInAssign(path)) {
// Used for log box on web.
if (key.value.startsWith('EXPO_PROJECT_ROOT')) {
path.replaceWith(t.stringLiteral(projectRoot));
}
else if (
// TODO: Add cache invalidation.
key.value.startsWith('EXPO_PUBLIC_USE_STATIC')) {
if (platform === 'web') {
const isStatic = process.env.EXPO_PUBLIC_USE_STATIC === 'true' ||
process.env.EXPO_PUBLIC_USE_STATIC === '1';
path.replaceWith(t.booleanLiteral(isStatic));
}
else {
path.replaceWith(t.booleanLiteral(false));
}
}
else if (key.value.startsWith('EXPO_ROUTER_IMPORT_MODE')) {
path.replaceWith(t.stringLiteral(asyncRoutes ? 'lazy' : 'sync'));
}
if (
// Skip loading the app root in tests.
// This is handled by the testing-library utils
process.env.NODE_ENV !== 'test') {
if (key.value.startsWith('EXPO_ROUTER_ABS_APP_ROOT')) {
path.replaceWith(t.stringLiteral(routerAbsoluteRoot));
}
else if (key.value.startsWith('EXPO_ROUTER_APP_ROOT')) {
path.replaceWith(t.stringLiteral(getExpoRouterAppRoot(projectRoot, routerAbsoluteRoot)));
}
}
}
}
},
},
};
}
exports.expoRouterBabelPlugin = expoRouterBabelPlugin;

View File

@@ -0,0 +1,73 @@
import { ConfigAPI, TransformOptions } from '@babel/core';
type BabelPresetExpoPlatformOptions = {
/** Enable or disable adding the Reanimated plugin by default. @default `true` */
reanimated?: boolean;
/** @deprecated Set `jsxRuntime: 'classic'` to disable automatic JSX handling. */
useTransformReactJSXExperimental?: boolean;
/** Change the policy for handling JSX in a file. Passed to `plugin-transform-react-jsx`. @default `'automatic'` */
jsxRuntime?: 'classic' | 'automatic';
/** Change the source module ID to use when importing an automatic JSX import. Only applied when `jsxRuntime` is `'automatic'` (default). Passed to `plugin-transform-react-jsx`. @default `'react'` */
jsxImportSource?: string;
lazyImports?: boolean;
disableImportExportTransform?: boolean;
disableFlowStripTypesTransform?: boolean;
enableBabelRuntime?: boolean;
unstable_transformProfile?: 'default' | 'hermes-stable' | 'hermes-canary';
/** Settings to pass to `babel-plugin-react-compiler`. Set as `false` to disable the plugin. */
'react-compiler'?: false | {
enableUseMemoCachePolyfill?: boolean;
compilationMode?: 'infer' | 'strict';
panicThreshold?: 'none' | 'all_errors' | 'critical_errors';
logger?: any;
environment?: {
customHooks?: unknown;
enableResetCacheOnSourceFileChanges?: boolean;
enablePreserveExistingMemoizationGuarantees?: boolean;
/** @default true */
validatePreserveExistingMemoizationGuarantees?: boolean;
enableForest?: boolean;
enableUseTypeAnnotations?: boolean;
/** @default true */
enableReactiveScopesInHIR?: boolean;
/** @default true */
validateHooksUsage?: boolean;
validateRefAccessDuringRender?: boolean;
/** @default true */
validateNoSetStateInRender?: boolean;
validateMemoizedEffectDependencies?: boolean;
validateNoCapitalizedCalls?: string[] | null;
/** @default true */
enableAssumeHooksFollowRulesOfReact?: boolean;
/** @default true */
enableTransitivelyFreezeFunctionExpressions: boolean;
enableEmitFreeze?: unknown;
enableEmitHookGuards?: unknown;
enableEmitInstrumentForget?: unknown;
assertValidMutableRanges?: boolean;
enableChangeVariableCodegen?: boolean;
enableMemoizationComments?: boolean;
throwUnknownException__testonly?: boolean;
enableTreatFunctionDepsAsConditional?: boolean;
/** Automatically enabled when reanimated plugin is added. */
enableCustomTypeDefinitionForReanimated?: boolean;
/** @default `null` */
hookPattern?: string | null;
};
gating?: unknown;
noEmit?: boolean;
runtimeModule?: string | null;
eslintSuppressionRules?: unknown | null;
flowSuppressions?: boolean;
ignoreUseNoForget?: boolean;
};
/** Enable `typeof window` runtime checks. The default behavior is to minify `typeof window` on web clients to `"object"` and `"undefined"` on servers. */
minifyTypeofWindow?: boolean;
};
export type BabelPresetExpoOptions = BabelPresetExpoPlatformOptions & {
/** Web-specific settings. */
web?: BabelPresetExpoPlatformOptions;
/** Native-specific settings. */
native?: BabelPresetExpoPlatformOptions;
};
declare function babelPresetExpo(api: ConfigAPI, options?: BabelPresetExpoOptions): TransformOptions;
export default babelPresetExpo;

View File

@@ -0,0 +1,247 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const client_module_proxy_plugin_1 = require("./client-module-proxy-plugin");
const common_1 = require("./common");
const environment_restricted_imports_1 = require("./environment-restricted-imports");
const expo_inline_manifest_plugin_1 = require("./expo-inline-manifest-plugin");
const expo_router_plugin_1 = require("./expo-router-plugin");
const inline_env_vars_1 = require("./inline-env-vars");
const lazyImports_1 = require("./lazyImports");
const restricted_react_api_plugin_1 = require("./restricted-react-api-plugin");
function getOptions(options, platform) {
const tag = platform === 'web' ? 'web' : 'native';
return {
...options,
...options[tag],
};
}
function babelPresetExpo(api, options = {}) {
const bundler = api.caller(common_1.getBundler);
const isWebpack = bundler === 'webpack';
let platform = api.caller((caller) => caller?.platform);
const engine = api.caller((caller) => caller?.engine) ?? 'default';
const isDev = api.caller(common_1.getIsDev);
const isNodeModule = api.caller(common_1.getIsNodeModule);
const isServer = api.caller(common_1.getIsServer);
const isReactServer = api.caller(common_1.getIsReactServer);
const isFastRefreshEnabled = api.caller(common_1.getIsFastRefreshEnabled);
const isReactCompilerEnabled = api.caller(common_1.getReactCompiler);
const baseUrl = api.caller(common_1.getBaseUrl);
const supportsStaticESM = api.caller((caller) => caller?.supportsStaticESM);
const isServerEnv = isServer || isReactServer;
// Unlike `isDev`, this will be `true` when the bundler is explicitly set to `production`,
// i.e. `false` when testing, development, or used with a bundler that doesn't specify the correct inputs.
const isProduction = api.caller(common_1.getIsProd);
const inlineEnvironmentVariables = api.caller(common_1.getInlineEnvVarsEnabled);
// If the `platform` prop is not defined then this must be a custom config that isn't
// defining a platform in the babel-loader. Currently this may happen with Next.js + Expo web.
if (!platform && isWebpack) {
platform = 'web';
}
const platformOptions = getOptions(options, platform);
if (platformOptions.useTransformReactJSXExperimental != null) {
throw new Error(`babel-preset-expo: The option 'useTransformReactJSXExperimental' has been removed in favor of { jsxRuntime: 'classic' }.`);
}
if (platformOptions.disableImportExportTransform == null) {
if (platform === 'web') {
// Only disable import/export transform when Webpack is used because
// Metro does not support tree-shaking.
platformOptions.disableImportExportTransform = supportsStaticESM ?? isWebpack;
}
else {
platformOptions.disableImportExportTransform = supportsStaticESM ?? false;
}
}
if (platformOptions.unstable_transformProfile == null) {
platformOptions.unstable_transformProfile = engine === 'hermes' ? 'hermes-stable' : 'default';
}
// Note that if `options.lazyImports` is not set (i.e., `null` or `undefined`),
// `@react-native/babel-preset` will handle it.
const lazyImportsOption = platformOptions?.lazyImports;
const extraPlugins = [];
// Add compiler as soon as possible to prevent other plugins from modifying the code.
if (isReactCompilerEnabled &&
// Don't run compiler on node modules, it can only safely be run on the user's code.
!isNodeModule &&
// Only run for client code. It's unclear if compiler has any benefits for React Server Components.
// NOTE: We might want to allow running it to prevent hydration errors.
!isServerEnv &&
// Give users the ability to opt-out of the feature, per-platform.
platformOptions['react-compiler'] !== false) {
extraPlugins.push([
require('babel-plugin-react-compiler'),
{
runtimeModule: 'babel-preset-expo/react-compiler-runtime.js',
// enableUseMemoCachePolyfill: true,
// compilationMode: 'infer',
environment: {
enableResetCacheOnSourceFileChanges: !isProduction,
...(platformOptions['react-compiler']?.environment ?? {}),
},
panicThreshold: isDev ? undefined : 'NONE',
...platformOptions['react-compiler'],
},
]);
}
if (engine !== 'hermes') {
// `@react-native/babel-preset` configures this plugin with `{ loose: true }`, which breaks all
// getters and setters in spread objects. We need to add this plugin ourself without that option.
// @see https://github.com/expo/expo/pull/11960#issuecomment-887796455
extraPlugins.push([
require('@babel/plugin-transform-object-rest-spread'),
// Assume no dependence on getters or evaluation order. See https://github.com/babel/babel/pull/11520
{ loose: true, useBuiltIns: true },
]);
}
else if (!isServerEnv) {
// This is added back on hermes to ensure the react-jsx-dev plugin (`@babel/preset-react`) works as expected when
// JSX is used in a function body. This is technically not required in production, but we
// should retain the same behavior since it's hard to debug the differences.
extraPlugins.push(require('@babel/plugin-transform-parameters'));
}
const inlines = {
'process.env.EXPO_OS': platform,
// 'typeof document': isServerEnv ? 'undefined' : 'object',
};
// `typeof window` is left in place for native + client environments.
const minifyTypeofWindow = (platformOptions.minifyTypeofWindow ?? isServerEnv) || platform === 'web';
if (minifyTypeofWindow !== false) {
// This nets out slightly faster in development when considering the cost of bundling server dependencies.
inlines['typeof window'] = isServerEnv ? 'undefined' : 'object';
}
if (isProduction) {
inlines['process.env.NODE_ENV'] = 'production';
inlines['__DEV__'] = false;
inlines['Platform.OS'] = platform;
}
if (process.env.NODE_ENV !== 'test') {
inlines['process.env.EXPO_BASE_URL'] = baseUrl;
}
extraPlugins.push([require('./define-plugin'), inlines]);
if (isProduction) {
// Metro applies a version of this plugin too but it does it after the Platform modules have been transformed to CJS, this breaks the transform.
// Here, we'll apply it before the commonjs transform, in production only, to ensure `Platform.OS` is replaced with a string literal.
extraPlugins.push([
require('./minify-platform-select-plugin'),
{
platform,
},
]);
}
if (platformOptions.useTransformReactJSXExperimental != null) {
throw new Error(`babel-preset-expo: The option 'useTransformReactJSXExperimental' has been removed in favor of { jsxRuntime: 'classic' }.`);
}
// 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.
// Development uses an uncached serializer.
// Servers read from the environment.
// Users who disable the feature may be using a different babel plugin.
if (inlineEnvironmentVariables) {
extraPlugins.push(inline_env_vars_1.expoInlineEnvVars);
}
if (platform === 'web') {
extraPlugins.push(require('babel-plugin-react-native-web'));
// Webpack uses the DefinePlugin to provide the manifest to `expo-constants`.
if (bundler !== 'webpack') {
extraPlugins.push(expo_inline_manifest_plugin_1.expoInlineManifestPlugin);
}
}
if ((0, common_1.hasModule)('expo-router')) {
extraPlugins.push(expo_router_plugin_1.expoRouterBabelPlugin);
}
// Ensure these only run when the user opts-in to bundling for a react server to prevent unexpected behavior for
// users who are bundling using the client-only system.
if (isReactServer) {
extraPlugins.push(client_module_proxy_plugin_1.reactClientReferencesPlugin);
extraPlugins.push(restricted_react_api_plugin_1.environmentRestrictedReactAPIsPlugin);
}
// This plugin is fine to run whenever as the server-only imports were introduced as part of RSC and shouldn't be used in any client code.
extraPlugins.push(environment_restricted_imports_1.environmentRestrictedImportsPlugin);
if (isFastRefreshEnabled) {
extraPlugins.push([
require('react-refresh/babel'),
{
// We perform the env check to enable `isFastRefreshEnabled`.
skipEnvCheck: true,
},
]);
}
// Use the simpler babel preset for web and server environments (both web and native SSR).
const isModernEngine = platform === 'web' || isServerEnv;
return {
presets: [
[
// We use `require` here instead of directly using the package name because we want to
// specifically use the `@react-native/babel-preset` installed by this package (ex:
// `babel-preset-expo/node_modules/`). This way the preset will not change unintentionally.
// Reference: https://github.com/expo/expo/pull/4685#discussion_r307143920
isModernEngine ? require('./web-preset') : require('@react-native/babel-preset'),
{
// Defaults to undefined, set to `true` to disable `@babel/plugin-transform-flow-strip-types`
disableFlowStripTypesTransform: platformOptions.disableFlowStripTypesTransform,
// Defaults to undefined, set to `false` to disable `@babel/plugin-transform-runtime`
enableBabelRuntime: platformOptions.enableBabelRuntime,
// This reduces the amount of transforms required, as Hermes supports many modern language features.
unstable_transformProfile: platformOptions.unstable_transformProfile,
// Set true to disable `@babel/plugin-transform-react-jsx` and
// the deprecated packages `@babel/plugin-transform-react-jsx-self`, and `@babel/plugin-transform-react-jsx-source`.
//
// Otherwise, you'll sometime get errors like the following (starting in Expo SDK 43, React Native 64, React 17):
//
// TransformError App.js: /path/to/App.js: Duplicate __self prop found. You are most likely using the deprecated transform-react-jsx-self Babel plugin.
// Both __source and __self are automatically set when using the automatic jsxRuntime. Please remove transform-react-jsx-source and transform-react-jsx-self from your Babel config.
useTransformReactJSXExperimental: true,
// This will never be used regardless because `useTransformReactJSXExperimental` is set to `true`.
// https://github.com/facebook/react-native/blob/a4a8695cec640e5cf12be36a0c871115fbce9c87/packages/react-native-babel-preset/src/configs/main.js#L151
withDevTools: false,
disableImportExportTransform: platformOptions.disableImportExportTransform,
lazyImportExportTransform: lazyImportsOption === true
? (importModuleSpecifier) => {
// Do not lazy-initialize packages that are local imports (similar to `lazy: true`
// behavior) or are in the blacklist.
return !(importModuleSpecifier.includes('./') || lazyImports_1.lazyImports.has(importModuleSpecifier));
}
: // Pass the option directly to `@react-native/babel-preset`, which in turn
// passes it to `babel-plugin-transform-modules-commonjs`
lazyImportsOption,
},
],
// React support with similar options to Metro.
// We override this logic outside of the metro preset so we can add support for
// React 17 automatic JSX transformations.
// The only known issue is the plugin `@babel/plugin-transform-react-display-name` will be run twice,
// once in the Metro plugin, and another time here.
[
require('@babel/preset-react'),
{
development: isDev,
// Defaults to `automatic`, pass in `classic` to disable auto JSX transformations.
runtime: platformOptions?.jsxRuntime || 'automatic',
...(platformOptions &&
platformOptions.jsxRuntime !== 'classic' && {
importSource: (platformOptions && platformOptions.jsxImportSource) || 'react',
}),
// NOTE: Unexposed props:
// pragma?: string;
// pragmaFrag?: string;
// pure?: string;
// throwIfNamespace?: boolean;
// useBuiltIns?: boolean;
// useSpread?: boolean;
},
],
],
plugins: [
...extraPlugins,
// TODO: Remove
[require('@babel/plugin-proposal-decorators'), { legacy: true }],
require('@babel/plugin-transform-export-namespace-from'),
// Automatically add `react-native-reanimated/plugin` when the package is installed.
// TODO: Move to be a customTransformOption.
(0, common_1.hasModule)('react-native-reanimated') &&
platformOptions.reanimated !== false && [require('react-native-reanimated/plugin')],
].filter(Boolean),
};
}
exports.default = babelPresetExpo;
module.exports = babelPresetExpo;

View File

@@ -0,0 +1,4 @@
import { ConfigAPI, PluginObj, types } from '@babel/core';
export declare function expoInlineEnvVars(api: ConfigAPI & {
types: typeof types;
}): PluginObj;

View File

@@ -0,0 +1,29 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.expoInlineEnvVars = void 0;
const debug = require('debug')('expo:babel:env-vars');
function expoInlineEnvVars(api) {
const { types: t } = api;
function isFirstInAssign(path) {
return t.isAssignmentExpression(path.parent) && path.parent.left === path.node;
}
return {
name: 'expo-inline-production-environment-variables',
visitor: {
MemberExpression(path, state) {
const filename = state.filename;
if (path.get('object').matchesPattern('process.env')) {
// @ts-expect-error: missing types
const key = path.toComputedKey();
if (t.isStringLiteral(key) &&
!isFirstInAssign(path) &&
key.value.startsWith('EXPO_PUBLIC_')) {
debug('Inlining environment variable in %s: %s', filename, key.value);
path.replaceWith(t.valueToNode(process.env[key.value]));
}
}
},
},
};
}
exports.expoInlineEnvVars = expoInlineEnvVars;

View File

@@ -0,0 +1,2 @@
/** These Expo packages may have side-effects and should not be lazily initialized. */
export declare const lazyImports: Set<string>;

View File

@@ -0,0 +1,5 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.lazyImports = void 0;
/** These Expo packages may have side-effects and should not be lazily initialized. */
exports.lazyImports = new Set(['expo', 'expo-asset', 'expo-task-manager']);

View File

@@ -0,0 +1,11 @@
/**
* Copyright © 2024 650 Industries.
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import { ConfigAPI, types } from '@babel/core';
export default function minifyPlatformSelectPlugin({ types: t, }: ConfigAPI & {
types: typeof types;
}): babel.PluginObj;

View File

@@ -0,0 +1,73 @@
"use strict";
/**
* Copyright © 2024 650 Industries.
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
Object.defineProperty(exports, "__esModule", { value: true });
const core_1 = require("@babel/core");
function minifyPlatformSelectPlugin({ types: t, }) {
return {
visitor: {
CallExpression(path, state) {
const node = path.node;
const arg = node.arguments[0];
const opts = state.opts;
if (isPlatformSelect(path) && core_1.types.isObjectExpression(arg)) {
if (hasStaticProperties(arg)) {
let fallback;
if (opts.platform === 'web') {
fallback = () => findProperty(arg, 'default', () => t.identifier('undefined'));
}
else {
fallback = () => findProperty(arg, 'native', () => findProperty(arg, 'default', () => t.identifier('undefined')));
}
path.replaceWith(findProperty(arg, opts.platform, fallback));
}
}
},
},
};
}
exports.default = minifyPlatformSelectPlugin;
function isPlatformSelect(path) {
return (core_1.types.isMemberExpression(path.node.callee) &&
core_1.types.isIdentifier(path.node.callee.object) &&
core_1.types.isIdentifier(path.node.callee.property) &&
path.node.callee.object.name === 'Platform' &&
path.node.callee.property.name === 'select' &&
core_1.types.isObjectExpression(path.node.arguments[0]));
}
function findProperty(objectExpression, key, fallback) {
let value = null;
for (const p of objectExpression.properties) {
if (!core_1.types.isObjectProperty(p) && !core_1.types.isObjectMethod(p)) {
continue;
}
if ((core_1.types.isIdentifier(p.key) && p.key.name === key) ||
(core_1.types.isStringLiteral(p.key) && p.key.value === key)) {
if (core_1.types.isObjectProperty(p)) {
value = p.value;
break;
}
else if (core_1.types.isObjectMethod(p)) {
value = core_1.types.toExpression(p);
break;
}
}
}
return value ?? fallback();
}
function hasStaticProperties(objectExpression) {
return objectExpression.properties.every((p) => {
if (('computed' in p && p.computed) || core_1.types.isSpreadElement(p)) {
return false;
}
if (core_1.types.isObjectMethod(p) && p.kind !== 'method') {
return false;
}
return core_1.types.isIdentifier(p.key) || core_1.types.isStringLiteral(p.key);
});
}

View File

@@ -0,0 +1,7 @@
/**
* Copyright © 2024 650 Industries.
*/
import { ConfigAPI, types } from '@babel/core';
export declare function environmentRestrictedReactAPIsPlugin(api: ConfigAPI & {
types: typeof types;
}): babel.PluginObj;

View File

@@ -0,0 +1,116 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.environmentRestrictedReactAPIsPlugin = void 0;
const INVALID_SERVER_REACT_DOM_APIS = [
'findDOMNode',
'flushSync',
'unstable_batchedUpdates',
'useFormStatus',
'useFormState',
];
// From the React docs: https://github.com/vercel/next.js/blob/d43a387d271263f2c1c4da6b9db826e382fc489c/packages/next-swc/crates/next-custom-transforms/src/transforms/react_server_components.rs#L665-L681
const INVALID_SERVER_REACT_APIS = [
'Component',
'createContext',
'createFactory',
'PureComponent',
'useDeferredValue',
'useEffect',
'useImperativeHandle',
'useInsertionEffect',
'useLayoutEffect',
'useReducer',
'useRef',
'useState',
'useSyncExternalStore',
'useTransition',
'useOptimistic',
];
function isNodeModule(path) {
return path != null && /[\\/]node_modules[\\/]/.test(path);
}
// Restricts imports from `react` and `react-dom` when using React Server Components.
const FORBIDDEN_IMPORTS = {
react: INVALID_SERVER_REACT_APIS,
'react-dom': INVALID_SERVER_REACT_DOM_APIS,
};
function environmentRestrictedReactAPIsPlugin(api) {
const { types: t } = api;
return {
name: 'expo-environment-restricted-react-api-plugin',
visitor: {
ImportDeclaration(path, state) {
// Skip node_modules
if (isNodeModule(state.file.opts.filename)) {
return;
}
const sourceValue = path.node.source.value;
const forbiddenList = FORBIDDEN_IMPORTS[sourceValue];
if (forbiddenList) {
path.node.specifiers.forEach((specifier) => {
if (t.isImportSpecifier(specifier)) {
const importName = t.isStringLiteral(specifier.imported)
? specifier.imported.value
: specifier.imported.name;
// Check for both named and namespace imports
const isForbidden = forbiddenList.includes(importName);
if (isForbidden) {
if (['Component', 'PureComponent'].includes(importName)) {
// Add special handling for `Component` since it is different to a function API.
throw path.buildCodeFrameError(`Client-only "${sourceValue}" API "${importName}" cannot be imported in a React server component. Add the "use client" directive to the top of this file or one of the parent files to enable running this stateful code on a user's device.`);
}
else {
const forbiddenImports = path.scope.getData('forbiddenImports') ?? new Map();
if (!forbiddenImports.has(sourceValue))
forbiddenImports.set(sourceValue, new Set());
forbiddenImports.get(sourceValue).add(importName);
path.scope.setData('forbiddenImports', forbiddenImports);
}
}
}
else {
const importName = t.isStringLiteral(specifier.local)
? specifier.local
: specifier.local.name;
// Save namespace import for later checks in MemberExpression
path.scope.setData('importedNamespace', { [importName]: sourceValue });
}
});
}
},
// Match against `var _useState = useState(0),`
VariableDeclarator(path) {
const importedSpecifiers = path.scope.getData('forbiddenImports');
if (!importedSpecifiers)
return;
importedSpecifiers.forEach((forbiddenApis, importName) => {
if (t.isCallExpression(path.node.init) && t.isIdentifier(path.node.init.callee)) {
if (forbiddenApis.has(path.node.init.callee.name)) {
throw path.buildCodeFrameError(`Client-only "useState" API cannot be used in a React server component. Add the "use client" directive to the top of this file or one of the parent files to enable running this stateful code on a user's device.`);
}
}
});
},
MemberExpression(path) {
const importedNamespaces = path.scope.getData('importedNamespace') || {};
Object.keys(importedNamespaces).forEach((namespace) => {
const library = importedNamespaces[namespace];
const forbiddenList = FORBIDDEN_IMPORTS[library];
const objectName = t.isIdentifier(path.node.object) ? path.node.object.name : null;
if (objectName === namespace &&
forbiddenList &&
t.isIdentifier(path.node.property) &&
forbiddenList.includes(path.node.property.name)) {
// Throw a special error for class components since it's not always clear why they cannot be used in RSC.
// e.g. https://x.com/Baconbrix/status/1749223042440392806?s=20
if (path.node.property.name === 'Component') {
throw path.buildCodeFrameError(`Class components cannot be used in a React server component due to their ability to contain stateful and interactive APIs that cannot be statically evaluated in non-interactive environments such as a server or at build-time. Migrate to a function component, or add the "use client" directive to the top of this file or one of the parent files to render this class component on a user's device.`);
}
throw path.buildCodeFrameError(`Client-only "${namespace}" API "${path.node.property.name}" cannot be used in a React server component. Add the "use client" directive to the top of this file or one of the parent files to enable running this stateful code on a user's device.`);
}
});
},
},
};
}
exports.environmentRestrictedReactAPIsPlugin = environmentRestrictedReactAPIsPlugin;

View File

@@ -0,0 +1,11 @@
/**
* Copyright © 2024 650 Industries.
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* A fork of `@react-native/babel-preset` but with everything unrelated to web/ssr removed.
* https://github.com/facebook/react-native/blob/2af1da42ff517232f1309efed7565fe9ddbbac77/packages/react-native-babel-preset/src/configs/main.js#L1
*/
export {};

View File

@@ -0,0 +1,73 @@
"use strict";
/**
* Copyright © 2024 650 Industries.
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* A fork of `@react-native/babel-preset` but with everything unrelated to web/ssr removed.
* https://github.com/facebook/react-native/blob/2af1da42ff517232f1309efed7565fe9ddbbac77/packages/react-native-babel-preset/src/configs/main.js#L1
*/
Object.defineProperty(exports, "__esModule", { value: true });
// use `this.foo = bar` instead of `this.defineProperty('foo', ...)`
const loose = true;
const defaultPlugins = [
[require('@babel/plugin-syntax-flow')],
[require('babel-plugin-transform-flow-enums')],
[require('@babel/plugin-transform-private-methods'), { loose }],
[require('@babel/plugin-transform-private-property-in-object'), { loose }],
// [require('@babel/plugin-syntax-dynamic-import')],
[require('@babel/plugin-syntax-export-default-from')],
// [require('@babel/plugin-syntax-nullish-coalescing-operator')],
// [require('@babel/plugin-syntax-optional-chaining')],
// [require('@babel/plugin-transform-unicode-regex')],
];
module.exports = function (babel, options) {
const extraPlugins = [];
// NOTE: We also remove `@react-native/babel-plugin-codegen` since it doesn't seem needed on web.
if (!options || !options.disableImportExportTransform) {
extraPlugins.push([require('@babel/plugin-proposal-export-default-from')], [
require('@babel/plugin-transform-modules-commonjs'),
{
strict: false,
strictMode: false,
lazy: options.lazyImportExportTransform,
allowTopLevelThis: true, // dont rewrite global `this` -> `undefined`
},
]);
}
if (!options || options.enableBabelRuntime !== false) {
// Allows configuring a specific runtime version to optimize output
const isVersion = typeof options?.enableBabelRuntime === 'string';
extraPlugins.push([
require('@babel/plugin-transform-runtime'),
{
helpers: true,
regenerator: false,
...(isVersion && { version: options.enableBabelRuntime }),
},
]);
}
return {
comments: false,
compact: true,
presets: [
// TypeScript support
[require('@babel/preset-typescript'), { allowNamespaces: true }],
],
overrides: [
// the flow strip types plugin must go BEFORE class properties!
// there'll be a test case that fails if you don't.
{
plugins: [require('@babel/plugin-transform-flow-strip-types')],
},
{
plugins: defaultPlugins,
},
{
plugins: extraPlugins,
},
],
};
};