babel-preset-expo 54.1.0-canary-20260105-6b962e6 → 54.1.0-canary-20260119-70f7c28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/common.d.ts CHANGED
@@ -13,6 +13,7 @@ export declare function getIsNodeModule(caller?: any): boolean;
13
13
  export declare function getBaseUrl(caller?: any): string;
14
14
  export declare function getReactCompiler(caller?: any): boolean;
15
15
  export declare function getIsServer(caller?: any): boolean;
16
+ export declare function getIsLoaderBundle(caller?: any): boolean;
16
17
  export declare function getMetroSourceType(caller?: any): "script" | "module" | "asset" | undefined;
17
18
  export declare function getBabelRuntimeVersion(caller?: any): string;
18
19
  export declare function getExpoRouterAbsoluteAppRoot(caller?: any): string;
package/build/common.js CHANGED
@@ -15,6 +15,7 @@ exports.getIsNodeModule = getIsNodeModule;
15
15
  exports.getBaseUrl = getBaseUrl;
16
16
  exports.getReactCompiler = getReactCompiler;
17
17
  exports.getIsServer = getIsServer;
18
+ exports.getIsLoaderBundle = getIsLoaderBundle;
18
19
  exports.getMetroSourceType = getMetroSourceType;
19
20
  exports.getBabelRuntimeVersion = getBabelRuntimeVersion;
20
21
  exports.getExpoRouterAbsoluteAppRoot = getExpoRouterAbsoluteAppRoot;
@@ -118,6 +119,10 @@ function getIsServer(caller) {
118
119
  assertExpoBabelCaller(caller);
119
120
  return caller?.isServer ?? false;
120
121
  }
122
+ function getIsLoaderBundle(caller) {
123
+ assertExpoBabelCaller(caller);
124
+ return caller?.isLoaderBundle ?? false;
125
+ }
121
126
  function getMetroSourceType(caller) {
122
127
  assertExpoBabelCaller(caller);
123
128
  return caller?.metroSourceType;
@@ -5,6 +5,8 @@
5
5
  * This source code is licensed under the MIT license found in the
6
6
  * LICENSE file in the root directory of this source tree.
7
7
  */
8
- import type { ConfigAPI, PluginObj } from '@babel/core';
9
- declare function definePlugin({ types: t }: ConfigAPI & typeof import('@babel/core')): PluginObj;
8
+ import type { ConfigAPI, PluginObj, PluginPass } from '@babel/core';
9
+ declare function definePlugin({ types: t, }: ConfigAPI & typeof import('@babel/core')): PluginObj<PluginPass & {
10
+ opts: Record<string, null | boolean | string>;
11
+ }>;
10
12
  export default definePlugin;
@@ -7,30 +7,8 @@
7
7
  * LICENSE file in the root directory of this source tree.
8
8
  */
9
9
  Object.defineProperty(exports, "__esModule", { value: true });
10
- /**
11
- * Checks if the given identifier is an ES module import
12
- * @param {babelNode} identifierNodePath The node to check
13
- * @return {boolean} Indicates if the provided node is an import specifier or references one
14
- */
15
- const isImportIdentifier = (identifierNodePath) => {
16
- if (identifierNodePath.container &&
17
- !Array.isArray(identifierNodePath.container) &&
18
- 'type' in identifierNodePath.container) {
19
- return (identifierNodePath.container.type === 'ImportDefaultSpecifier' ||
20
- identifierNodePath.container.type === 'ImportSpecifier');
21
- }
22
- return false;
23
- };
24
- const memberExpressionComparator = (nodePath, value) => nodePath.matchesPattern(value);
25
- const identifierComparator = (nodePath, value) => 'name' in nodePath.node && nodePath.node.name === value;
26
- const unaryExpressionComparator = (nodePath, value) => {
27
- if ('argument' in nodePath.node && nodePath.node.argument && 'name' in nodePath.node?.argument) {
28
- return nodePath.node.argument.name === value;
29
- }
30
- return false;
31
- };
32
10
  const TYPEOF_PREFIX = 'typeof ';
33
- function definePlugin({ types: t }) {
11
+ function definePlugin({ types: t, }) {
34
12
  /**
35
13
  * Replace a node with a given value. If the replacement results in a BinaryExpression, it will be
36
14
  * evaluated. For example, if the result of the replacement is `var x = "production" === "production"`
@@ -38,79 +16,120 @@ function definePlugin({ types: t }) {
38
16
  */
39
17
  function replaceAndEvaluateNode(nodePath, replacement) {
40
18
  nodePath.replaceWith(t.valueToNode(replacement));
41
- if (nodePath.parentPath && nodePath.parentPath.isBinaryExpression()) {
19
+ if (nodePath.parentPath?.isBinaryExpression()) {
42
20
  const result = nodePath.parentPath.evaluate();
43
21
  if (result.confident) {
44
22
  nodePath.parentPath.replaceWith(t.valueToNode(result.value));
45
23
  }
46
24
  }
47
25
  }
48
- const isLeftHandSideOfAssignmentExpression = (node, parent) => t.isAssignmentExpression(parent) && parent.left === node;
49
- const processNode = (replacements, nodePath, comparator) => {
50
- const replacementKey = Object.keys(replacements).find((value) => comparator(nodePath, value));
51
- if (typeof replacementKey === 'string' &&
52
- replacements != null &&
53
- replacementKey in replacements) {
54
- replaceAndEvaluateNode(nodePath, replacements[replacementKey]);
26
+ /** Get the root identifier name from a member expression (e.g., "process" from "process.env.NODE_ENV") */
27
+ function getMemberExpressionRoot(node) {
28
+ let current = node;
29
+ while (t.isMemberExpression(current)) {
30
+ current = current.object;
55
31
  }
56
- };
32
+ return t.isIdentifier(current) ? current.name : null;
33
+ }
57
34
  return {
58
35
  name: 'expo-define-globals',
36
+ pre() {
37
+ // Pre-process replacements once per file
38
+ const identifiers = new Map();
39
+ const memberPatterns = [];
40
+ const typeofValues = new Map();
41
+ const memberRoots = new Set();
42
+ for (const key of Object.keys(this.opts)) {
43
+ const value = this.opts[key];
44
+ if (key.startsWith(TYPEOF_PREFIX)) {
45
+ // "typeof window" -> typeofValues["window"]
46
+ typeofValues.set(key.slice(TYPEOF_PREFIX.length), value);
47
+ }
48
+ else if (key.includes('.')) {
49
+ // "process.env.NODE_ENV" -> memberPatterns, extract "process" as root
50
+ memberPatterns.push([key, value]);
51
+ const root = key.split('.')[0];
52
+ memberRoots.add(root);
53
+ }
54
+ else {
55
+ // "__DEV__" -> identifiers
56
+ identifiers.set(key, value);
57
+ }
58
+ }
59
+ this.processed = {
60
+ identifiers,
61
+ memberPatterns,
62
+ typeofValues,
63
+ memberRoots,
64
+ };
65
+ },
59
66
  visitor: {
60
67
  // process.env.NODE_ENV;
61
68
  MemberExpression(nodePath, state) {
62
- if (
63
69
  // Prevent rewriting if the member expression is on the left-hand side of an assignment
64
- isLeftHandSideOfAssignmentExpression(nodePath.node, nodePath.parent)) {
70
+ if (t.isAssignmentExpression(nodePath.parent) && nodePath.parent.left === nodePath.node) {
65
71
  return;
66
72
  }
67
- const replacements = state.opts;
68
- assertOptions(replacements);
69
- processNode(replacements, nodePath, memberExpressionComparator);
73
+ const { memberPatterns, memberRoots } = state.processed;
74
+ if (memberPatterns.length === 0)
75
+ return;
76
+ // Quick filter: check if root matches any known pattern root
77
+ const root = getMemberExpressionRoot(nodePath.node);
78
+ if (!root || !memberRoots.has(root))
79
+ return;
80
+ // Check against patterns
81
+ for (const [pattern, replacement] of memberPatterns) {
82
+ if (nodePath.matchesPattern(pattern)) {
83
+ replaceAndEvaluateNode(nodePath, replacement);
84
+ return;
85
+ }
86
+ }
70
87
  },
71
88
  // const x = { version: VERSION };
72
89
  ReferencedIdentifier(nodePath, state) {
73
- const binding = nodePath.scope?.getBinding(nodePath.node.name);
74
- if (binding ||
75
- // Don't transform import identifiers. This is meant to mimic webpack's
76
- // DefinePlugin behavior.
77
- isImportIdentifier(nodePath) ||
78
- // Do not transform Object keys / properties unless they are computed like {[key]: value}
79
- (nodePath.key === 'key' &&
80
- nodePath.parent &&
81
- 'computed' in nodePath.parent &&
82
- nodePath.parent.computed === false) ||
83
- (nodePath.key === 'property' &&
84
- nodePath.parent &&
85
- 'computed' in nodePath.parent &&
86
- nodePath.parent.computed === false)) {
90
+ const { identifiers } = state.processed;
91
+ if (identifiers.size === 0)
92
+ return;
93
+ const name = nodePath.node.name;
94
+ // Quick check: is this identifier in our replacements?
95
+ if (!identifiers.has(name))
96
+ return;
97
+ // Check for binding (locally defined variable shadows replacement)
98
+ if (nodePath.scope?.getBinding(name))
99
+ return;
100
+ // Don't transform import identifiers (mimics webpack's DefinePlugin behavior)
101
+ const container = nodePath.container;
102
+ if (container &&
103
+ !Array.isArray(container) &&
104
+ 'type' in container &&
105
+ (container.type === 'ImportDefaultSpecifier' || container.type === 'ImportSpecifier')) {
106
+ return;
107
+ }
108
+ // Do not transform Object keys / properties unless they are computed like {[key]: value}
109
+ if ((nodePath.key === 'key' || nodePath.key === 'property') &&
110
+ nodePath.parent &&
111
+ 'computed' in nodePath.parent &&
112
+ nodePath.parent.computed === false) {
87
113
  return;
88
114
  }
89
- const replacements = state.opts;
90
- assertOptions(replacements);
91
- processNode(replacements, nodePath, identifierComparator);
115
+ replaceAndEvaluateNode(nodePath, identifiers.get(name));
92
116
  },
93
117
  // typeof window
94
118
  UnaryExpression(nodePath, state) {
95
- if (nodePath.node.operator !== 'typeof') {
119
+ if (nodePath.node.operator !== 'typeof')
120
+ return;
121
+ const { typeofValues } = state.processed;
122
+ if (typeofValues.size === 0)
123
+ return;
124
+ const argument = nodePath.node.argument;
125
+ if (!t.isIdentifier(argument))
96
126
  return;
127
+ const replacement = typeofValues.get(argument.name);
128
+ if (replacement !== undefined) {
129
+ replaceAndEvaluateNode(nodePath, replacement);
97
130
  }
98
- const replacements = state.opts;
99
- assertOptions(replacements);
100
- const typeofValues = {};
101
- Object.keys(replacements).forEach((key) => {
102
- if (key.substring(0, TYPEOF_PREFIX.length) === TYPEOF_PREFIX) {
103
- typeofValues[key.substring(TYPEOF_PREFIX.length)] = replacements[key];
104
- }
105
- });
106
- processNode(typeofValues, nodePath, unaryExpressionComparator);
107
131
  },
108
132
  },
109
133
  };
110
134
  }
111
- function assertOptions(opts) {
112
- if (opts == null || typeof opts !== 'object') {
113
- throw new Error('define plugin expects an object as options');
114
- }
115
- }
116
135
  exports.default = definePlugin;
@@ -1,2 +1,6 @@
1
- import type { ConfigAPI, PluginObj } from '@babel/core';
2
- export declare function expoInlineManifestPlugin(api: ConfigAPI & typeof import('@babel/core')): PluginObj;
1
+ import type { ConfigAPI, PluginObj, PluginPass } from '@babel/core';
2
+ interface InlineManifestState extends PluginPass {
3
+ projectRoot: string;
4
+ }
5
+ export declare function expoInlineManifestPlugin(api: ConfigAPI & typeof import('@babel/core')): PluginObj<InlineManifestState>;
6
+ export {};
@@ -4,7 +4,7 @@ exports.expoInlineManifestPlugin = expoInlineManifestPlugin;
4
4
  const common_1 = require("./common");
5
5
  const debug = require('debug')('expo:babel:inline-manifest');
6
6
  // Convert expo value to PWA value
7
- function ensurePWAorientation(orientation) {
7
+ function ensurePWAOrientation(orientation) {
8
8
  if (orientation) {
9
9
  const webOrientation = orientation.toLowerCase();
10
10
  if (webOrientation !== 'default') {
@@ -52,7 +52,7 @@ function applyWebDefaults({ config, appName, webName }) {
52
52
  const startUrl = webManifest.startUrl;
53
53
  const { scope, crossorigin } = webManifest;
54
54
  const barStyle = webManifest.barStyle;
55
- const orientation = ensurePWAorientation(webManifest.orientation || appJSON.orientation);
55
+ const orientation = ensurePWAOrientation(webManifest.orientation || appJSON.orientation);
56
56
  /**
57
57
  * **Splash screen background color**
58
58
  * `https://developers.google.com/web/fundamentals/web-app-manifest/#splash-screen`
@@ -144,34 +144,46 @@ function expoInlineManifestPlugin(api) {
144
144
  const platform = api.caller(common_1.getPlatform);
145
145
  const possibleProjectRoot = api.caller(common_1.getPossibleProjectRoot);
146
146
  const shouldInline = platform === 'web' || isReactServer;
147
+ // Early exit: return a no-op plugin if we're not going to inline anything
148
+ if (!shouldInline) {
149
+ return {
150
+ name: 'expo-inline-manifest-plugin',
151
+ visitor: {},
152
+ };
153
+ }
147
154
  return {
148
155
  name: 'expo-inline-manifest-plugin',
156
+ pre() {
157
+ this.projectRoot = possibleProjectRoot || this.file.opts.root || '';
158
+ },
149
159
  visitor: {
150
160
  MemberExpression(path, state) {
151
- // Web-only/React Server-only feature: the native manifest is provided dynamically by the client.
152
- if (!shouldInline) {
161
+ // We're looking for: process.env.APP_MANIFEST
162
+ // This visitor is called on every MemberExpression, so we need fast checks
163
+ // Quick check: the property we're looking for is 'APP_MANIFEST'
164
+ // The parent must be a MemberExpression with property 'APP_MANIFEST'
165
+ const parent = path.parentPath;
166
+ if (!parent?.isMemberExpression())
153
167
  return;
154
- }
155
- if (!t.isIdentifier(path.node.object, { name: 'process' }) ||
156
- !t.isIdentifier(path.node.property, { name: 'env' })) {
168
+ // Check if parent's property is APP_MANIFEST (most selective check first)
169
+ const parentProp = parent.node.property;
170
+ if (!t.isIdentifier(parentProp) || parentProp.name !== 'APP_MANIFEST')
157
171
  return;
158
- }
159
- const parent = path.parentPath;
160
- if (!t.isMemberExpression(parent.node)) {
172
+ // Now verify this is process.env
173
+ const obj = path.node.object;
174
+ const prop = path.node.property;
175
+ if (!t.isIdentifier(obj) || obj.name !== 'process')
176
+ return;
177
+ if (!t.isIdentifier(prop) || prop.name !== 'env')
178
+ return;
179
+ // Skip if this is an assignment target
180
+ if (parent.parentPath?.isAssignmentExpression())
161
181
  return;
162
- }
163
- const projectRoot = possibleProjectRoot || state.file.opts.root || '';
164
- if (
165
182
  // Surfaces the `app.json` (config) as an environment variable which is then parsed by
166
183
  // `expo-constants` https://docs.expo.dev/versions/latest/sdk/constants/
167
- t.isIdentifier(parent.node.property, {
168
- name: 'APP_MANIFEST',
169
- }) &&
170
- !parent.parentPath.isAssignmentExpression()) {
171
- const manifest = getExpoAppManifest(projectRoot);
172
- if (manifest !== null) {
173
- parent.replaceWith(t.stringLiteral(manifest));
174
- }
184
+ const manifest = getExpoAppManifest(state.projectRoot);
185
+ if (manifest !== null) {
186
+ parent.replaceWith(t.stringLiteral(manifest));
175
187
  }
176
188
  },
177
189
  },
@@ -8,11 +8,19 @@ const node_path_1 = __importDefault(require("node:path"));
8
8
  const resolve_from_1 = __importDefault(require("resolve-from"));
9
9
  const common_1 = require("./common");
10
10
  const debug = require('debug')('expo:babel:router');
11
+ // Cache for getExpoRouterAppRoot results (projectRoot -> appRoot)
12
+ const appRootCache = new Map();
11
13
  function getExpoRouterAppRoot(projectRoot, appFolder) {
14
+ const cacheKey = `${projectRoot}:${appFolder}`;
15
+ const cached = appRootCache.get(cacheKey);
16
+ if (cached !== undefined) {
17
+ return cached;
18
+ }
12
19
  // TODO: We should have cache invalidation if the expo-router/entry file location changes.
13
20
  const routerEntry = (0, resolve_from_1.default)(projectRoot, 'expo-router/entry');
14
21
  const appRoot = node_path_1.default.relative(node_path_1.default.dirname(routerEntry), appFolder);
15
22
  debug('routerEntry', routerEntry, appFolder, appRoot);
23
+ appRootCache.set(cacheKey, appRoot);
16
24
  return appRoot;
17
25
  }
18
26
  /**
@@ -28,36 +36,55 @@ function expoRouterBabelPlugin(api) {
28
36
  const possibleProjectRoot = api.caller(common_1.getPossibleProjectRoot);
29
37
  const asyncRoutes = api.caller(common_1.getAsyncRoutes);
30
38
  const routerAbsoluteRoot = api.caller(common_1.getExpoRouterAbsoluteAppRoot);
31
- function isFirstInAssign(path) {
32
- return t.isAssignmentExpression(path.parent) && path.parent.left === path.node;
33
- }
39
+ const importMode = asyncRoutes ? 'lazy' : 'sync';
34
40
  return {
35
41
  name: 'expo-router',
42
+ pre() {
43
+ const state = this;
44
+ state.projectRoot = possibleProjectRoot || this.file.opts.root || '';
45
+ // Check test env at transform time, not module load time
46
+ state.isTestEnv = process.env.NODE_ENV === 'test';
47
+ },
36
48
  visitor: {
37
49
  MemberExpression(path, state) {
38
- const projectRoot = possibleProjectRoot || state.file.opts.root || '';
39
- if (path.get('object').matchesPattern('process.env')) {
40
- const key = path.toComputedKey();
41
- if (t.isStringLiteral(key) && !isFirstInAssign(path)) {
42
- // Used for log box on web.
43
- if (key.value.startsWith('EXPO_PROJECT_ROOT')) {
44
- path.replaceWith(t.stringLiteral(projectRoot));
45
- }
46
- else if (key.value.startsWith('EXPO_ROUTER_IMPORT_MODE')) {
47
- path.replaceWith(t.stringLiteral(asyncRoutes ? 'lazy' : 'sync'));
48
- }
49
- if (
50
- // Skip loading the app root in tests.
51
- // This is handled by the testing-library utils
52
- process.env.NODE_ENV !== 'test') {
53
- if (key.value.startsWith('EXPO_ROUTER_ABS_APP_ROOT')) {
54
- path.replaceWith(t.stringLiteral(routerAbsoluteRoot));
55
- }
56
- else if (key.value.startsWith('EXPO_ROUTER_APP_ROOT')) {
57
- path.replaceWith(t.stringLiteral(getExpoRouterAppRoot(projectRoot, routerAbsoluteRoot)));
58
- }
59
- }
60
- }
50
+ // Quick check: skip if not accessing something on an object named 'process'
51
+ const object = path.node.object;
52
+ if (!t.isMemberExpression(object))
53
+ return;
54
+ const objectOfObject = object.object;
55
+ if (!t.isIdentifier(objectOfObject) || objectOfObject.name !== 'process')
56
+ return;
57
+ // Now check if it's process.env
58
+ if (!t.isIdentifier(object.property) || object.property.name !== 'env')
59
+ return;
60
+ // Skip if this is an assignment target
61
+ if (t.isAssignmentExpression(path.parent) && path.parent.left === path.node)
62
+ return;
63
+ // Get the property key
64
+ const key = path.toComputedKey();
65
+ if (!t.isStringLiteral(key))
66
+ return;
67
+ const keyValue = key.value;
68
+ // Check each possible env var
69
+ switch (keyValue) {
70
+ case 'EXPO_PROJECT_ROOT':
71
+ path.replaceWith(t.stringLiteral(state.projectRoot));
72
+ return;
73
+ case 'EXPO_ROUTER_IMPORT_MODE':
74
+ path.replaceWith(t.stringLiteral(importMode));
75
+ return;
76
+ default:
77
+ break;
78
+ }
79
+ // Skip app root transforms in tests (handled by testing-library utils)
80
+ if (state.isTestEnv)
81
+ return;
82
+ switch (keyValue) {
83
+ case 'EXPO_ROUTER_ABS_APP_ROOT':
84
+ path.replaceWith(t.stringLiteral(routerAbsoluteRoot));
85
+ return;
86
+ case 'EXPO_ROUTER_APP_ROOT':
87
+ path.replaceWith(t.stringLiteral(getExpoRouterAppRoot(state.projectRoot, routerAbsoluteRoot)));
61
88
  }
62
89
  },
63
90
  },
package/build/index.js CHANGED
@@ -163,8 +163,11 @@ function babelPresetExpo(api, options = {}) {
163
163
  }
164
164
  if ((0, common_1.hasModule)('expo-router')) {
165
165
  extraPlugins.push(expo_router_plugin_1.expoRouterBabelPlugin);
166
- // Strip loader() functions from client bundles
167
- if (!isServerEnv) {
166
+ // Process `loader()` functions for client, loader and server bundles (excluding RSC)
167
+ // - Client bundles: Remove loader exports, they run on server only
168
+ // - Server bundles: Keep loader exports (needed for SSG)
169
+ // - Loader-only bundles: Keep only loader exports, remove everything else
170
+ if (!isReactServer) {
168
171
  extraPlugins.push(server_data_loaders_plugin_1.serverDataLoadersPlugin);
169
172
  }
170
173
  }
@@ -8,11 +8,27 @@ function serverDataLoadersPlugin(api) {
8
8
  const { types: t } = api;
9
9
  const routerAbsoluteRoot = api.caller(common_1.getExpoRouterAbsoluteAppRoot);
10
10
  const isServer = api.caller(common_1.getIsServer);
11
+ const isLoaderBundle = api.caller(common_1.getIsLoaderBundle);
11
12
  return {
12
13
  name: 'expo-server-data-loaders',
13
14
  visitor: {
15
+ ExportDefaultDeclaration(path, state) {
16
+ // Early exit if file is not within the `app/` directory
17
+ if (!isInAppDirectory(state.file.opts.filename ?? '', routerAbsoluteRoot)) {
18
+ return;
19
+ }
20
+ // Only remove default exports in loader-only bundles
21
+ if (!isLoaderBundle) {
22
+ return;
23
+ }
24
+ debug('Loader bundle: removing default export from', state.file.opts.filename);
25
+ markForConstantFolding(state);
26
+ path.remove();
27
+ },
14
28
  ExportNamedDeclaration(path, state) {
15
- if (isServer) {
29
+ // NOTE(@hassankhan): Server bundles currently preserve loaders for SSG, a followup is
30
+ // required to remove them.
31
+ if (isServer && !isLoaderBundle) {
16
32
  return;
17
33
  }
18
34
  // Early exit if file is not within the `app/` directory
@@ -20,38 +36,73 @@ function serverDataLoadersPlugin(api) {
20
36
  debug('Skipping file outside app directory:', state.file.opts.filename);
21
37
  return;
22
38
  }
23
- debug(isServer ? 'Processing server bundle:' : 'Processing client bundle:', state.file.opts.filename);
39
+ debug(`Processing ${isLoaderBundle ? 'loader' : 'client'} bundle:`, state.file.opts.filename);
24
40
  const { declaration, specifiers } = path.node;
25
41
  // Is this a type export like `export type Foo`?
26
42
  const isTypeExport = path.node.exportKind === 'type';
27
- // Does this export with `export { loader }`?
28
43
  // NOTE(@hassankhan): We should add proper handling for specifiers too
29
44
  const hasSpecifiers = specifiers.length > 0;
30
45
  if (isTypeExport || hasSpecifiers) {
31
46
  return;
32
47
  }
33
- // Handles `export function loader() {}`
48
+ // Handles `export function loader() { ... }`
34
49
  if (t.isFunctionDeclaration(declaration)) {
35
50
  const name = declaration.id?.name;
36
51
  if (name && isLoaderIdentifier(name)) {
37
- debug('Found and removed loader function declaration');
52
+ // Mark the file as having a loader (for all bundle types)
53
+ markWithLoaderReference(state);
54
+ if (!isLoaderBundle) {
55
+ // Client bundles: remove loader
56
+ debug('Found and removed loader function declaration');
57
+ markForConstantFolding(state);
58
+ path.remove();
59
+ }
60
+ // Loader bundle: keep the loader
61
+ }
62
+ else if (name && isLoaderBundle) {
63
+ // Loader bundle: remove non-loader function declarations
64
+ debug('Loader bundle: removing non-loader function declaration:', name);
38
65
  markForConstantFolding(state);
39
66
  path.remove();
40
67
  }
41
68
  }
42
69
  // Handles `export const loader = ...`
43
70
  if (t.isVariableDeclaration(declaration)) {
44
- let hasRemovedLoader = false;
45
- declaration.declarations = declaration.declarations.filter((declarator) => {
71
+ let hasModified = false;
72
+ // Check if any declaration is a loader
73
+ const hasLoaderDeclaration = declaration.declarations.some((declarator) => {
46
74
  const name = t.isIdentifier(declarator.id) ? declarator.id.name : null;
47
- if (name && isLoaderIdentifier(name)) {
48
- debug('Found and removed loader variable declaration');
49
- hasRemovedLoader = true;
50
- return false;
51
- }
52
- return true;
75
+ return name && isLoaderIdentifier(name);
53
76
  });
54
- if (hasRemovedLoader) {
77
+ // Mark the file as having a loader (for all bundle types)
78
+ if (hasLoaderDeclaration) {
79
+ markWithLoaderReference(state);
80
+ }
81
+ if (isLoaderBundle) {
82
+ // Loader bundle: keep only loader declarations, remove others
83
+ declaration.declarations = declaration.declarations.filter((declarator) => {
84
+ const name = t.isIdentifier(declarator.id) ? declarator.id.name : null;
85
+ if (name && !isLoaderIdentifier(name)) {
86
+ debug('Loader bundle: removing non-loader variable declaration:', name);
87
+ hasModified = true;
88
+ return false;
89
+ }
90
+ return true;
91
+ });
92
+ }
93
+ else {
94
+ // Client bundles: remove loader declarations
95
+ declaration.declarations = declaration.declarations.filter((declarator) => {
96
+ const name = t.isIdentifier(declarator.id) ? declarator.id.name : null;
97
+ if (name && isLoaderIdentifier(name)) {
98
+ debug('Found and removed loader variable declaration');
99
+ hasModified = true;
100
+ return false;
101
+ }
102
+ return true;
103
+ });
104
+ }
105
+ if (hasModified) {
55
106
  markForConstantFolding(state);
56
107
  // If all declarations were removed, remove the export
57
108
  if (declaration.declarations.length === 0) {
@@ -92,3 +143,11 @@ function markForConstantFolding(state) {
92
143
  assertExpoMetadata(state.file.metadata);
93
144
  state.file.metadata.performConstantFolding = true;
94
145
  }
146
+ /**
147
+ * Sets the `loaderReference` metadata to the file path. This is used to collect all modules with
148
+ * loaders in the Metro serializer.
149
+ */
150
+ function markWithLoaderReference(state) {
151
+ assertExpoMetadata(state.file.metadata);
152
+ state.file.metadata.loaderReference = state.file.opts.filename ?? undefined;
153
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "babel-preset-expo",
3
- "version": "54.1.0-canary-20260105-6b962e6",
3
+ "version": "54.1.0-canary-20260119-70f7c28",
4
4
  "description": "The Babel preset for Expo projects",
5
5
  "main": "build/index.js",
6
6
  "files": [
@@ -43,7 +43,7 @@
43
43
  "peerDependencies": {
44
44
  "@babel/runtime": "^7.20.0",
45
45
  "react-refresh": ">=0.14.0 <1.0.0",
46
- "expo": "55.0.0-canary-20260105-6b962e6"
46
+ "expo": "55.0.0-canary-20260119-70f7c28"
47
47
  },
48
48
  "peerDependenciesMeta": {
49
49
  "@babel/runtime": {
@@ -81,7 +81,7 @@
81
81
  "@babel/core": "^7.26.0",
82
82
  "@types/babel__core": "^7.20.5",
83
83
  "@expo/metro": "~54.2.0",
84
- "expo-module-scripts": "5.1.0-canary-20260105-6b962e6",
84
+ "expo-module-scripts": "5.1.0-canary-20260119-70f7c28",
85
85
  "jest": "^29.2.1",
86
86
  "react-refresh": "^0.14.2"
87
87
  }