babel-preset-expo 11.1.0-canary-20241008-90b13ad → 11.1.0-canary-20241018-4f8243a

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.
@@ -1 +1,5 @@
1
- export declare function reactClientReferencesPlugin(): babel.PluginObj;
1
+ /**
2
+ * Copyright © 2024 650 Industries.
3
+ */
4
+ import { ConfigAPI } from '@babel/core';
5
+ export declare function reactClientReferencesPlugin(api: ConfigAPI): babel.PluginObj;
@@ -9,7 +9,9 @@ exports.reactClientReferencesPlugin = void 0;
9
9
  */
10
10
  const core_1 = require("@babel/core");
11
11
  const url_1 = __importDefault(require("url"));
12
- function reactClientReferencesPlugin() {
12
+ const common_1 = require("./common");
13
+ function reactClientReferencesPlugin(api) {
14
+ const isReactServer = api.caller(common_1.getIsReactServer);
13
15
  return {
14
16
  name: 'expo-client-references',
15
17
  visitor: {
@@ -22,90 +24,145 @@ function reactClientReferencesPlugin() {
22
24
  if (isUseClient && isUseServer) {
23
25
  throw path.buildCodeFrameError("It's not possible to have both `use client` and `use server` directives in the same file.");
24
26
  }
27
+ if (!isUseClient && !isUseServer) {
28
+ return;
29
+ }
25
30
  const filePath = state.file.opts.filename;
26
31
  if (!filePath) {
27
32
  // This can happen in tests or systems that use Babel standalone.
28
33
  throw new Error('[Babel] Expected a filename to be set in the state');
29
34
  }
30
- // File starts with "use client" directive.
31
- if (!isUseClient) {
32
- // Do nothing for code that isn't marked as a client component.
33
- return;
34
- }
35
35
  const outputKey = url_1.default.pathToFileURL(filePath).href;
36
- // We need to add all of the exports to support `export * from './module'` which iterates the keys of the module.
37
- const proxyModule = [
38
- `const proxy = /*@__PURE__*/ require("react-server-dom-webpack/server").createClientModuleProxy(${JSON.stringify(outputKey)});`,
39
- `module.exports = proxy;`,
40
- ];
41
- const getProxy = (exportName) => {
42
- return `(/*@__PURE__*/ proxy[${JSON.stringify(exportName)}])`;
43
- };
44
- const proxyExports = new Set();
45
- const pushProxy = (exportName) => {
46
- proxyExports.add(exportName);
47
- if (exportName === 'default') {
48
- proxyModule.push(`export default ${getProxy(exportName)};`);
49
- }
50
- else {
51
- proxyModule.push(`export const ${exportName} = ${getProxy(exportName)};`);
52
- }
53
- };
54
- // Collect all of the exports
55
- path.traverse({
56
- ExportNamedDeclaration(exportPath) {
57
- if (exportPath.node.declaration) {
58
- if (exportPath.node.declaration.type === 'VariableDeclaration') {
59
- exportPath.node.declaration.declarations.forEach((declaration) => {
60
- if (declaration.id.type === 'Identifier') {
61
- const exportName = declaration.id.name;
62
- pushProxy(exportName);
36
+ function iterateExports(callback, type) {
37
+ const exportNames = new Set();
38
+ // Collect all of the exports
39
+ path.traverse({
40
+ ExportNamedDeclaration(exportPath) {
41
+ if (exportPath.node.declaration) {
42
+ if (exportPath.node.declaration.type === 'VariableDeclaration') {
43
+ exportPath.node.declaration.declarations.forEach((declaration) => {
44
+ if (declaration.id.type === 'Identifier') {
45
+ const exportName = declaration.id.name;
46
+ exportNames.add(exportName);
47
+ callback(exportName);
48
+ }
49
+ });
50
+ }
51
+ else if (exportPath.node.declaration.type === 'FunctionDeclaration') {
52
+ const exportName = exportPath.node.declaration.id?.name;
53
+ if (exportName) {
54
+ exportNames.add(exportName);
55
+ callback(exportName);
63
56
  }
64
- });
65
- }
66
- else if (exportPath.node.declaration.type === 'FunctionDeclaration') {
67
- const exportName = exportPath.node.declaration.id?.name;
68
- if (exportName) {
69
- pushProxy(exportName);
70
57
  }
71
- }
72
- else if (exportPath.node.declaration.type === 'ClassDeclaration') {
73
- const exportName = exportPath.node.declaration.id?.name;
74
- if (exportName) {
75
- pushProxy(exportName);
58
+ else if (exportPath.node.declaration.type === 'ClassDeclaration') {
59
+ const exportName = exportPath.node.declaration.id?.name;
60
+ if (exportName) {
61
+ exportNames.add(exportName);
62
+ callback(exportName);
63
+ }
64
+ }
65
+ else if (!['InterfaceDeclaration', 'TSTypeAliasDeclaration', 'TypeAlias'].includes(exportPath.node.declaration.type)) {
66
+ // TODO: What is this type?
67
+ console.warn(`[babel-preset-expo] Unsupported export specifier for "use ${type}":`, exportPath.node.declaration.type);
76
68
  }
77
69
  }
78
- else if (!['InterfaceDeclaration', 'TSTypeAliasDeclaration', 'TypeAlias'].includes(exportPath.node.declaration.type)) {
79
- // TODO: What is this type?
80
- console.warn('[babel-preset-expo] Unsupported export specifier for "use client":', exportPath.node.declaration.type);
70
+ else {
71
+ exportPath.node.specifiers.forEach((specifier) => {
72
+ if (core_1.types.isIdentifier(specifier.exported)) {
73
+ const exportName = specifier.exported.name;
74
+ exportNames.add(exportName);
75
+ callback(exportName);
76
+ }
77
+ else {
78
+ // TODO: What is this type?
79
+ console.warn(`[babel-preset-expo] Unsupported export specifier for "use ${type}":`, specifier);
80
+ }
81
+ });
81
82
  }
83
+ },
84
+ ExportDefaultDeclaration() {
85
+ exportNames.add('default');
86
+ callback('default');
87
+ },
88
+ });
89
+ return exportNames;
90
+ }
91
+ // File starts with "use client" directive.
92
+ if (isUseServer) {
93
+ if (isReactServer) {
94
+ // The "use server" transform for react-server is in a different plugin.
95
+ return;
96
+ }
97
+ // Handle "use server" in the client.
98
+ const proxyModule = [
99
+ `import { createServerReference } from 'react-server-dom-webpack/client';`,
100
+ `import { callServerRSC } from 'expo-router/rsc/internal';`,
101
+ ];
102
+ const getProxy = (exportName) => {
103
+ return `createServerReference(${JSON.stringify(`${outputKey}#${exportName}`)}, callServerRSC)`;
104
+ };
105
+ const pushProxy = (exportName) => {
106
+ if (exportName === 'default') {
107
+ proxyModule.push(`export default ${getProxy(exportName)};`);
82
108
  }
83
109
  else {
84
- exportPath.node.specifiers.forEach((specifier) => {
85
- if (core_1.types.isIdentifier(specifier.exported)) {
86
- const exportName = specifier.exported.name;
87
- pushProxy(exportName);
88
- }
89
- else {
90
- // TODO: What is this type?
91
- console.warn('[babel-preset-expo] Unsupported export specifier for "use client":', specifier);
92
- }
93
- });
110
+ proxyModule.push(`export const ${exportName} = ${getProxy(exportName)};`);
94
111
  }
95
- },
96
- ExportDefaultDeclaration() {
97
- pushProxy('default');
98
- },
99
- });
100
- // Clear the body
101
- path.node.body = [];
102
- path.node.directives = [];
103
- path.pushContainer('body', core_1.template.ast(proxyModule.join('\n')));
104
- assertExpoMetadata(state.file.metadata);
105
- // Save the client reference in the metadata.
106
- state.file.metadata.reactClientReference = outputKey;
107
- // Store the proxy export names for testing purposes.
108
- state.file.metadata.proxyExports = [...proxyExports];
112
+ };
113
+ // We need to add all of the exports to support `export * from './module'` which iterates the keys of the module.
114
+ // Collect all of the exports
115
+ const proxyExports = iterateExports(pushProxy, 'client');
116
+ // Clear the body
117
+ path.node.body = [];
118
+ path.node.directives = [];
119
+ path.pushContainer('body', core_1.template.ast(proxyModule.join('\n')));
120
+ assertExpoMetadata(state.file.metadata);
121
+ // Store the proxy export names for testing purposes.
122
+ state.file.metadata.proxyExports = [...proxyExports];
123
+ // Save the server action reference in the metadata.
124
+ state.file.metadata.reactServerReference = outputKey;
125
+ }
126
+ else if (isUseClient) {
127
+ if (!isReactServer) {
128
+ // Do nothing for "use client" on the client.
129
+ return;
130
+ }
131
+ // HACK: Mock out the polyfill that doesn't run through the normal bundler pipeline.
132
+ if (filePath.endsWith('@react-native/js-polyfills/console.js')) {
133
+ // Clear the body
134
+ path.node.body = [];
135
+ path.node.directives = [];
136
+ return;
137
+ }
138
+ // We need to add all of the exports to support `export * from './module'` which iterates the keys of the module.
139
+ const proxyModule = [
140
+ `const proxy = /*@__PURE__*/ require("react-server-dom-webpack/server").createClientModuleProxy(${JSON.stringify(outputKey)});`,
141
+ `module.exports = proxy;`,
142
+ ];
143
+ const getProxy = (exportName) => {
144
+ return `(/*@__PURE__*/ proxy[${JSON.stringify(exportName)}])`;
145
+ };
146
+ const pushProxy = (exportName) => {
147
+ if (exportName === 'default') {
148
+ proxyModule.push(`export default ${getProxy(exportName)};`);
149
+ }
150
+ else {
151
+ proxyModule.push(`export const ${exportName} = ${getProxy(exportName)};`);
152
+ }
153
+ };
154
+ // Collect all of the exports
155
+ const proxyExports = iterateExports(pushProxy, 'client');
156
+ // Clear the body
157
+ path.node.body = [];
158
+ path.node.directives = [];
159
+ path.pushContainer('body', core_1.template.ast(proxyModule.join('\n')));
160
+ assertExpoMetadata(state.file.metadata);
161
+ // Store the proxy export names for testing purposes.
162
+ state.file.metadata.proxyExports = [...proxyExports];
163
+ // Save the client reference in the metadata.
164
+ state.file.metadata.reactClientReference = outputKey;
165
+ }
109
166
  },
110
167
  },
111
168
  };
package/build/index.js CHANGED
@@ -8,6 +8,7 @@ const expo_router_plugin_1 = require("./expo-router-plugin");
8
8
  const inline_env_vars_1 = require("./inline-env-vars");
9
9
  const lazyImports_1 = require("./lazyImports");
10
10
  const restricted_react_api_plugin_1 = require("./restricted-react-api-plugin");
11
+ const server_actions_plugin_1 = require("./server-actions-plugin");
11
12
  const use_dom_directive_plugin_1 = require("./use-dom-directive-plugin");
12
13
  function getOptions(options, platform) {
13
14
  const tag = platform === 'web' ? 'web' : 'native';
@@ -39,6 +40,8 @@ function babelPresetExpo(api, options = {}) {
39
40
  if (!platform && isWebpack) {
40
41
  platform = 'web';
41
42
  }
43
+ // Use the simpler babel preset for web and server environments (both web and native SSR).
44
+ const isModernEngine = platform === 'web' || isServerEnv;
42
45
  const platformOptions = getOptions(options, platform);
43
46
  if (platformOptions.useTransformReactJSXExperimental != null) {
44
47
  throw new Error(`babel-preset-expo: The option 'useTransformReactJSXExperimental' has been removed in favor of { jsxRuntime: 'classic' }.`);
@@ -97,13 +100,11 @@ function babelPresetExpo(api, options = {}) {
97
100
  { loose: true, useBuiltIns: true },
98
101
  ]);
99
102
  }
100
- else {
101
- if (platform !== 'web' && !isServerEnv) {
102
- // This is added back on hermes to ensure the react-jsx-dev plugin (`@babel/preset-react`) works as expected when
103
- // JSX is used in a function body. This is technically not required in production, but we
104
- // should retain the same behavior since it's hard to debug the differences.
105
- extraPlugins.push(require('@babel/plugin-transform-parameters'));
106
- }
103
+ else if (!isModernEngine) {
104
+ // This is added back on hermes to ensure the react-jsx-dev plugin (`@babel/preset-react`) works as expected when
105
+ // JSX is used in a function body. This is technically not required in production, but we
106
+ // should retain the same behavior since it's hard to debug the differences.
107
+ extraPlugins.push(require('@babel/plugin-transform-parameters'));
107
108
  }
108
109
  const inlines = {
109
110
  'process.env.EXPO_OS': platform,
@@ -156,10 +157,11 @@ function babelPresetExpo(api, options = {}) {
156
157
  if ((0, common_1.hasModule)('expo-router')) {
157
158
  extraPlugins.push(expo_router_plugin_1.expoRouterBabelPlugin);
158
159
  }
160
+ extraPlugins.push(client_module_proxy_plugin_1.reactClientReferencesPlugin);
159
161
  // Ensure these only run when the user opts-in to bundling for a react server to prevent unexpected behavior for
160
162
  // users who are bundling using the client-only system.
161
163
  if (isReactServer) {
162
- extraPlugins.push(client_module_proxy_plugin_1.reactClientReferencesPlugin);
164
+ extraPlugins.push(server_actions_plugin_1.reactServerActionsPlugin);
163
165
  extraPlugins.push(restricted_react_api_plugin_1.environmentRestrictedReactAPIsPlugin);
164
166
  }
165
167
  else {
@@ -180,8 +182,6 @@ function babelPresetExpo(api, options = {}) {
180
182
  if (platformOptions.disableImportExportTransform) {
181
183
  extraPlugins.push([require('./detect-dynamic-exports').detectDynamicExports]);
182
184
  }
183
- // Use the simpler babel preset for web and server environments (both web and native SSR).
184
- const isModernEngine = platform === 'web' || isServerEnv;
185
185
  return {
186
186
  presets: [
187
187
  [
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Copyright © 2024 650 Industries.
3
+ * Copyright © 2024 2023 lubieowoce
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ *
8
+ * https://github.com/lubieowoce/tangle/blob/5229666fb317d0da9363363fc46dc542ba51e4f7/packages/babel-rsc/src/babel-rsc-actions.ts#L1C1-L909C25
9
+ */
10
+ import { ConfigAPI, types, type PluginObj, type PluginPass } from '@babel/core';
11
+ export declare function reactServerActionsPlugin(api: ConfigAPI & {
12
+ types: typeof types;
13
+ }): PluginObj<PluginPass>;
@@ -0,0 +1,518 @@
1
+ "use strict";
2
+ /**
3
+ * Copyright © 2024 650 Industries.
4
+ * Copyright © 2024 2023 lubieowoce
5
+ *
6
+ * This source code is licensed under the MIT license found in the
7
+ * LICENSE file in the root directory of this source tree.
8
+ *
9
+ * https://github.com/lubieowoce/tangle/blob/5229666fb317d0da9363363fc46dc542ba51e4f7/packages/babel-rsc/src/babel-rsc-actions.ts#L1C1-L909C25
10
+ */
11
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
12
+ if (k2 === undefined) k2 = k;
13
+ var desc = Object.getOwnPropertyDescriptor(m, k);
14
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
15
+ desc = { enumerable: true, get: function() { return m[k]; } };
16
+ }
17
+ Object.defineProperty(o, k2, desc);
18
+ }) : (function(o, m, k, k2) {
19
+ if (k2 === undefined) k2 = k;
20
+ o[k2] = m[k];
21
+ }));
22
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
23
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
24
+ }) : function(o, v) {
25
+ o["default"] = v;
26
+ });
27
+ var __importStar = (this && this.__importStar) || function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ var __importDefault = (this && this.__importDefault) || function (mod) {
35
+ return (mod && mod.__esModule) ? mod : { "default": mod };
36
+ };
37
+ Object.defineProperty(exports, "__esModule", { value: true });
38
+ exports.reactServerActionsPlugin = void 0;
39
+ const core_1 = require("@babel/core");
40
+ // @ts-expect-error: missing types
41
+ const helper_module_imports_1 = require("@babel/helper-module-imports");
42
+ const t = __importStar(require("@babel/types"));
43
+ const node_path_1 = require("node:path");
44
+ const node_url_1 = require("node:url");
45
+ const url_1 = __importDefault(require("url"));
46
+ const common_1 = require("./common");
47
+ const debug = require('debug')('expo:babel:server-actions');
48
+ const LAZY_WRAPPER_VALUE_KEY = 'value';
49
+ // React doesn't like non-enumerable properties on serialized objects (see `isSimpleObject`),
50
+ // so we have to use closure scope for the cache (instead of a non-enumerable `this._cache`)
51
+ const _buildLazyWrapperHelper = (0, core_1.template)(`(thunk) => {
52
+ let cache;
53
+ return {
54
+ get ${LAZY_WRAPPER_VALUE_KEY}() {
55
+ return cache || (cache = thunk());
56
+ }
57
+ }
58
+ }`);
59
+ const buildLazyWrapperHelper = () => {
60
+ return _buildLazyWrapperHelper().expression;
61
+ };
62
+ function reactServerActionsPlugin(api) {
63
+ const possibleProjectRoot = api.caller(common_1.getPossibleProjectRoot);
64
+ let addReactImport;
65
+ let wrapBoundArgs;
66
+ let getActionModuleId;
67
+ const extractInlineActionToTopLevel = (path, _state, { body, freeVariables, }) => {
68
+ const actionModuleId = getActionModuleId();
69
+ const moduleScope = path.scope.getProgramParent();
70
+ const extractedIdentifier = moduleScope.generateUidIdentifier('$$INLINE_ACTION');
71
+ let extractedFunctionParams = [...path.node.params];
72
+ let extractedFunctionBody = body.body;
73
+ if (freeVariables.length > 0) {
74
+ // only add a closure object if we're not closing over anything.
75
+ // const [x, y, z] = await _decryptActionBoundArgs(await $$CLOSURE.value);
76
+ const closureParam = path.scope.generateUidIdentifier('$$CLOSURE');
77
+ const freeVarsPat = t.arrayPattern(freeVariables.map((variable) => t.identifier(variable)));
78
+ const closureExpr = t.memberExpression(closureParam, t.identifier(LAZY_WRAPPER_VALUE_KEY));
79
+ extractedFunctionParams = [closureParam, ...path.node.params];
80
+ extractedFunctionBody = [
81
+ t.variableDeclaration('var', [
82
+ t.variableDeclarator(t.assignmentPattern(freeVarsPat, closureExpr)),
83
+ ]),
84
+ ...extractedFunctionBody,
85
+ ];
86
+ }
87
+ const wrapInRegister = (expr, exportedName) => {
88
+ const expoRegisterServerReferenceId = addReactImport();
89
+ return t.callExpression(expoRegisterServerReferenceId, [
90
+ expr,
91
+ t.stringLiteral(actionModuleId),
92
+ t.stringLiteral(exportedName),
93
+ ]);
94
+ };
95
+ const isArrowFn = path.isArrowFunctionExpression();
96
+ const extractedFunctionExpr = wrapInRegister(isArrowFn
97
+ ? t.arrowFunctionExpression(extractedFunctionParams, t.blockStatement(extractedFunctionBody), true)
98
+ : t.functionExpression(path.node.id, extractedFunctionParams, t.blockStatement(extractedFunctionBody), false, true), extractedIdentifier.name);
99
+ // Create a top-level declaration for the extracted function.
100
+ const bindingKind = 'const';
101
+ const functionDeclaration = t.exportNamedDeclaration(t.variableDeclaration(bindingKind, [
102
+ t.variableDeclarator(extractedIdentifier, extractedFunctionExpr),
103
+ ]));
104
+ // TODO: this is cacheable, no need to recompute
105
+ const programBody = moduleScope.path.get('body');
106
+ const lastImportPath = findLast(Array.isArray(programBody) ? programBody : [programBody], (stmt) => stmt.isImportDeclaration());
107
+ const [inserted] = lastImportPath.insertAfter(functionDeclaration);
108
+ moduleScope.registerBinding(bindingKind, inserted);
109
+ inserted.addComment('leading', ' hoisted action: ' + (getFnPathName(path) ?? '<anonymous>'), true);
110
+ return {
111
+ extractedIdentifier,
112
+ getReplacement: () => getInlineActionReplacement({
113
+ id: extractedIdentifier,
114
+ freeVariables,
115
+ }),
116
+ };
117
+ };
118
+ const getInlineActionReplacement = ({ id, freeVariables, }) => {
119
+ if (freeVariables.length === 0) {
120
+ return id;
121
+ }
122
+ const capturedVarsExpr = t.arrayExpression(freeVariables.map((variable) => t.identifier(variable)));
123
+ const boundArgs = wrapBoundArgs(capturedVarsExpr);
124
+ // _ACTION.bind(null, { get value() { return _encryptActionBoundArgs([x, y, z]) } })
125
+ return t.callExpression(t.memberExpression(id, t.identifier('bind')), [
126
+ t.nullLiteral(),
127
+ boundArgs,
128
+ ]);
129
+ };
130
+ return {
131
+ name: 'expo-server-actions',
132
+ pre(file) {
133
+ const projectRoot = possibleProjectRoot || file.opts.root || '';
134
+ if (!file.code.includes('use server')) {
135
+ file.path.skip();
136
+ return;
137
+ }
138
+ assertExpoMetadata(file.metadata);
139
+ file.metadata.extractedActions = [];
140
+ file.metadata.isModuleMarkedWithUseServerDirective = false;
141
+ const addNamedImportOnce = createAddNamedImportOnce(t);
142
+ addReactImport = () => {
143
+ return addNamedImportOnce(file.path, 'registerServerReference', 'react-server-dom-webpack/server');
144
+ };
145
+ getActionModuleId = once(() => {
146
+ // Create relative file path hash.
147
+ return (0, node_url_1.pathToFileURL)((0, node_path_1.relative)(projectRoot, file.opts.filename)).href;
148
+ });
149
+ const defineBoundArgsWrapperHelper = once(() => {
150
+ const id = this.file.path.scope.generateUidIdentifier('wrapBoundArgs');
151
+ this.file.path.scope.push({
152
+ id,
153
+ kind: 'var',
154
+ init: buildLazyWrapperHelper(),
155
+ });
156
+ return id;
157
+ });
158
+ wrapBoundArgs = (expr) => {
159
+ const wrapperFn = t.cloneNode(defineBoundArgsWrapperHelper());
160
+ return t.callExpression(wrapperFn, [t.arrowFunctionExpression([], expr)]);
161
+ };
162
+ },
163
+ visitor: {
164
+ Program(path, state) {
165
+ if (path.node.directives.some((d) => d.value.value === 'use server')) {
166
+ assertExpoMetadata(state.file.metadata);
167
+ state.file.metadata.isModuleMarkedWithUseServerDirective = true;
168
+ // remove the directive so that downstream consumers don't transform the module again.
169
+ path.node.directives = path.node.directives.filter((d) => d.value.value !== 'use server');
170
+ }
171
+ },
172
+ // `() => {}`
173
+ ArrowFunctionExpression(path, state) {
174
+ const { body } = path.node;
175
+ if (!t.isBlockStatement(body) || !hasUseServerDirective(path)) {
176
+ return;
177
+ }
178
+ assertIsAsyncFn(path);
179
+ const freeVariables = getFreeVariables(path);
180
+ const tlb = getTopLevelBinding(path);
181
+ const { extractedIdentifier, getReplacement } = extractInlineActionToTopLevel(path, state, {
182
+ freeVariables,
183
+ body,
184
+ });
185
+ path.replaceWith(getReplacement());
186
+ assertExpoMetadata(state.file.metadata);
187
+ state.file.metadata.extractedActions.push({
188
+ localName: tlb?.identifier.name,
189
+ exportedName: extractedIdentifier.name,
190
+ });
191
+ },
192
+ // `function foo() { ... }`
193
+ FunctionDeclaration(path, state) {
194
+ if (!hasUseServerDirective(path)) {
195
+ return;
196
+ }
197
+ assertIsAsyncFn(path);
198
+ const fnId = path.node.id;
199
+ if (!fnId) {
200
+ throw path.buildCodeFrameError('Internal error: expected FunctionDeclaration to have a name');
201
+ }
202
+ const freeVariables = getFreeVariables(path);
203
+ const { extractedIdentifier, getReplacement } = extractInlineActionToTopLevel(path, state, {
204
+ freeVariables,
205
+ body: path.node.body,
206
+ });
207
+ const tlb = getTopLevelBinding(path);
208
+ if (tlb) {
209
+ // we're at the top level, and we might be enclosed within a `export` decl.
210
+ // we have to keep the export in place, because it might be used elsewhere,
211
+ // so we can't just remove this node.
212
+ // replace the function decl with a (hopefully) equivalent var declaration
213
+ // `var [name] = $$INLINE_ACTION_{N}`
214
+ // TODO: this'll almost certainly break when using default exports,
215
+ // but tangle's build doesn't support those anyway
216
+ const bindingKind = 'var';
217
+ const [inserted] = path.replaceWith(t.variableDeclaration(bindingKind, [t.variableDeclarator(fnId, extractedIdentifier)]));
218
+ tlb.scope.registerBinding(bindingKind, inserted);
219
+ }
220
+ else {
221
+ // note: if we do this *after* adding the new declaration, the bindings get messed up
222
+ path.remove();
223
+ // add a declaration in the place where the function decl would be hoisted to.
224
+ // (this avoids issues with functions defined after `return`, see `test-cases/named-after-return.jsx`)
225
+ path.scope.push({
226
+ id: fnId,
227
+ init: getReplacement(),
228
+ kind: 'var',
229
+ unique: true,
230
+ });
231
+ }
232
+ assertExpoMetadata(state.file.metadata);
233
+ state.file.metadata.extractedActions.push({
234
+ localName: tlb?.identifier.name,
235
+ exportedName: extractedIdentifier.name,
236
+ });
237
+ },
238
+ // `const foo = function() { ... }`
239
+ FunctionExpression(path, state) {
240
+ if (!hasUseServerDirective(path)) {
241
+ return;
242
+ }
243
+ assertIsAsyncFn(path);
244
+ const { body } = path.node;
245
+ const freeVariables = getFreeVariables(path);
246
+ // TODO: look for usages of the name (if present), that's technically possible
247
+ // const fnId = path.node.id;
248
+ const { extractedIdentifier, getReplacement } = extractInlineActionToTopLevel(path, state, {
249
+ freeVariables,
250
+ body,
251
+ });
252
+ const tlb = getTopLevelBinding(path);
253
+ assertExpoMetadata(state.file.metadata);
254
+ path.replaceWith(getReplacement());
255
+ state.file.metadata.extractedActions.push({
256
+ localName: tlb?.identifier.name,
257
+ exportedName: extractedIdentifier.name,
258
+ });
259
+ },
260
+ // Top-level "use server"
261
+ ExportDefaultDeclaration(path, state) {
262
+ assertExpoMetadata(state.file.metadata);
263
+ if (!state.file.metadata.isModuleMarkedWithUseServerDirective) {
264
+ return;
265
+ }
266
+ // TODO: support variable functions
267
+ throw path.buildCodeFrameError(`Not implemented: 'export default' declarations in "use server" files. Try using 'export { name as default }' instead.`);
268
+ },
269
+ ExportNamedDeclaration(path, state) {
270
+ assertExpoMetadata(state.file.metadata);
271
+ if (!state.file.metadata.isModuleMarkedWithUseServerDirective) {
272
+ return;
273
+ }
274
+ const registerServerReferenceId = addReactImport();
275
+ const actionModuleId = getActionModuleId();
276
+ const createRegisterCall = (identifier, exported = identifier) => {
277
+ const exportedName = t.isIdentifier(exported) ? exported.name : exported.value;
278
+ const call = t.callExpression(registerServerReferenceId, [
279
+ identifier,
280
+ t.stringLiteral(actionModuleId),
281
+ t.stringLiteral(exportedName),
282
+ ]);
283
+ // Wrap call with `;(() => { ... })();` to avoid issues with ASI
284
+ return t.expressionStatement(t.callExpression(t.arrowFunctionExpression([], call), []));
285
+ };
286
+ if (path.node.specifiers.length > 0) {
287
+ for (const specifier of path.node.specifiers) {
288
+ // `export * as ns from './foo';`
289
+ if (t.isExportNamespaceSpecifier(specifier)) {
290
+ throw path.buildCodeFrameError('Not implemented: Namespace exports');
291
+ }
292
+ else if (t.isExportDefaultSpecifier(specifier)) {
293
+ // `export default foo;`
294
+ throw path.buildCodeFrameError('Not implemented (ExportDefaultSpecifier in ExportNamedDeclaration)');
295
+ }
296
+ else if (t.isExportSpecifier(specifier)) {
297
+ // `export { foo };`
298
+ // `export { foo as [bar|default] };`
299
+ const localName = specifier.local.name;
300
+ const exportedName = t.isIdentifier(specifier.exported)
301
+ ? specifier.exported.name
302
+ : specifier.exported.value;
303
+ // if we're reexporting an existing action under a new name, we shouldn't register() it again.
304
+ if (!state.file.metadata.extractedActions.some((info) => info.localName === localName)) {
305
+ // referencing the function's local identifier here *should* be safe (w.r.t. TDZ) because
306
+ // 1. if it's a `export async function foo() {}`, the declaration will be hoisted,
307
+ // so it's safe to reference no matter how the declarations are ordered
308
+ // 2. if it's an `export const foo = async () => {}`, then the standalone `export { foo }`
309
+ // has to follow the definition, so we can reference it right before the export decl as well
310
+ path.insertBefore(createRegisterCall(specifier.local, specifier.exported));
311
+ }
312
+ state.file.metadata.extractedActions.push({ localName, exportedName });
313
+ }
314
+ else {
315
+ throw path.buildCodeFrameError('Not implemented: whatever this is');
316
+ }
317
+ }
318
+ return;
319
+ }
320
+ if (!path.node.declaration) {
321
+ throw path.buildCodeFrameError(`Internal error: Unexpected 'ExportNamedDeclaration' without declarations`);
322
+ }
323
+ const identifiers = (() => {
324
+ const innerPath = path.get('declaration');
325
+ if (innerPath.isVariableDeclaration()) {
326
+ return innerPath.get('declarations').map((d) => {
327
+ // TODO: insert `typeof <identifier> === 'function'` check -- it's a variable, so it could be anything
328
+ const id = d.node.id;
329
+ if (!t.isIdentifier(id)) {
330
+ // TODO
331
+ throw innerPath.buildCodeFrameError('Unimplemented');
332
+ }
333
+ return id;
334
+ });
335
+ }
336
+ else if (innerPath.isFunctionDeclaration()) {
337
+ if (!innerPath.get('async')) {
338
+ throw innerPath.buildCodeFrameError(`Functions exported from "use server" files must be async.`);
339
+ }
340
+ return [innerPath.get('id').node];
341
+ }
342
+ else {
343
+ throw innerPath.buildCodeFrameError(`Unimplemented server action export`);
344
+ }
345
+ })();
346
+ path.insertAfter(identifiers.map((identifier) => createRegisterCall(identifier)));
347
+ for (const identifier of identifiers) {
348
+ state.file.metadata.extractedActions.push({
349
+ localName: identifier.name,
350
+ exportedName: identifier.name,
351
+ });
352
+ }
353
+ },
354
+ },
355
+ post(file) {
356
+ assertExpoMetadata(file.metadata);
357
+ if (!file.metadata.extractedActions?.length) {
358
+ return;
359
+ }
360
+ debug('extracted actions', file.metadata.extractedActions);
361
+ const payload = {
362
+ id: getActionModuleId(),
363
+ names: file.metadata.extractedActions.map((e) => e.exportedName),
364
+ };
365
+ const stashedData = 'rsc/actions: ' + JSON.stringify(payload);
366
+ // Add comment for debugging the bundle, we use the babel metadata for accessing the data.
367
+ file.path.addComment('leading', stashedData);
368
+ const filePath = file.opts.filename;
369
+ if (!filePath) {
370
+ // This can happen in tests or systems that use Babel standalone.
371
+ throw new Error('[Babel] Expected a filename to be set in the state');
372
+ }
373
+ const outputKey = url_1.default.pathToFileURL(filePath).href;
374
+ file.metadata.reactServerActions = payload;
375
+ file.metadata.reactServerReference = outputKey;
376
+ },
377
+ };
378
+ }
379
+ exports.reactServerActionsPlugin = reactServerActionsPlugin;
380
+ const getFreeVariables = (path) => {
381
+ const freeVariablesSet = new Set();
382
+ const programScope = path.scope.getProgramParent();
383
+ path.traverse({
384
+ Identifier(innerPath) {
385
+ const { name } = innerPath.node;
386
+ if (!innerPath.isReferencedIdentifier()) {
387
+ debug('skipping - not referenced');
388
+ return;
389
+ }
390
+ if (freeVariablesSet.has(name)) {
391
+ // we've already determined this name to be a free var. no point in recomputing.
392
+ debug('skipping - already registered');
393
+ return;
394
+ }
395
+ const binding = innerPath.scope.getBinding(name);
396
+ if (!binding) {
397
+ // probably a global, or an unbound variable. ignore it.
398
+ debug('skipping - global or unbound, skipping');
399
+ return;
400
+ }
401
+ if (binding.scope === programScope) {
402
+ // module-level declaration. no need to close over it.
403
+ debug('skipping - module-level binding');
404
+ return;
405
+ }
406
+ if (
407
+ // function args or a var at the top-level of its body
408
+ binding.scope === path.scope ||
409
+ // decls from blocks within the function
410
+ isChildScope({
411
+ parent: path.scope,
412
+ child: binding.scope,
413
+ root: programScope,
414
+ })) {
415
+ // the binding came from within the function = it's not closed-over, so don't add it.
416
+ debug('skipping - declared within function');
417
+ return;
418
+ }
419
+ // we've (hopefully) eliminated all the other cases, so we should treat this as a free var.
420
+ debug('adding');
421
+ freeVariablesSet.add(name);
422
+ },
423
+ });
424
+ return [...freeVariablesSet].sort();
425
+ };
426
+ const getFnPathName = (path) => {
427
+ return path.isArrowFunctionExpression() ? undefined : path.node.id.name;
428
+ };
429
+ const isChildScope = ({ root, parent, child, }) => {
430
+ let curScope = child;
431
+ while (curScope !== root) {
432
+ if (curScope.parent === parent) {
433
+ return true;
434
+ }
435
+ curScope = curScope.parent;
436
+ }
437
+ return false;
438
+ };
439
+ const findLast = (arr, predicate) => {
440
+ for (let i = arr.length - 1; i >= 0; i--) {
441
+ if (predicate(arr[i]))
442
+ return arr[i];
443
+ }
444
+ return undefined;
445
+ };
446
+ function findImmediatelyEnclosingDeclaration(path) {
447
+ let currentPath = path;
448
+ while (!currentPath.isProgram()) {
449
+ if (
450
+ // const foo = async () => { ... }
451
+ // ^^^^^^^^^^^^^^^^^^^^^^^^^
452
+ currentPath.isVariableDeclarator() ||
453
+ // async function foo() { ... }
454
+ // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
455
+ currentPath.isDeclaration()) {
456
+ return currentPath;
457
+ }
458
+ // if we encounter an expression on the way, this isn't a top level decl, and needs to be hoisted.
459
+ // e.g. `export const foo = withAuth(async () => { ... })`
460
+ if (currentPath !== path && currentPath.isExpression()) {
461
+ return null;
462
+ }
463
+ if (!currentPath.parentPath) {
464
+ return null;
465
+ }
466
+ currentPath = currentPath.parentPath;
467
+ }
468
+ return null;
469
+ }
470
+ const getTopLevelBinding = (path) => {
471
+ const decl = findImmediatelyEnclosingDeclaration(path);
472
+ if (!decl || !('id' in decl.node) || !decl.node.id || !('name' in decl.node.id))
473
+ return null;
474
+ const declBinding = decl.scope.getBinding(decl.node.id.name);
475
+ return declBinding.scope === path.scope.getProgramParent() ? declBinding : null;
476
+ };
477
+ const assertIsAsyncFn = (path) => {
478
+ if (!path.node.async) {
479
+ throw path.buildCodeFrameError(`functions marked with "use server" must be async`);
480
+ }
481
+ };
482
+ const once = (fn) => {
483
+ let cache = { has: false };
484
+ return () => {
485
+ if (cache.has)
486
+ return cache.value;
487
+ cache = { has: true, value: fn() };
488
+ return cache.value;
489
+ };
490
+ };
491
+ function assertExpoMetadata(metadata) {
492
+ if (!metadata || typeof metadata !== 'object') {
493
+ throw new Error('Expected Babel state.file.metadata to be an object');
494
+ }
495
+ }
496
+ const getOrCreateInMap = (map, key, create) => {
497
+ if (!map.has(key)) {
498
+ const result = create();
499
+ map.set(key, result);
500
+ return [result, true];
501
+ }
502
+ return [map.get(key), false];
503
+ };
504
+ function hasUseServerDirective(path) {
505
+ const { body } = path.node;
506
+ return t.isBlockStatement(body) && body.directives.some((d) => d.value.value === 'use server');
507
+ }
508
+ const createAddNamedImportOnce = (t) => {
509
+ const addedImportsCache = new Map();
510
+ return function addNamedImportOnce(path, name, source) {
511
+ const [sourceCache] = getOrCreateInMap(addedImportsCache, source, () => new Map());
512
+ const [identifier, didCreate] = getOrCreateInMap(sourceCache, name, () => (0, helper_module_imports_1.addNamed)(path, name, source));
513
+ // for cached imports, we need to clone the resulting identifier, because otherwise
514
+ // '@babel/plugin-transform-modules-commonjs' won't replace the references to the import for some reason.
515
+ // this is a helper for that.
516
+ return didCreate ? identifier : t.cloneNode(identifier);
517
+ };
518
+ };
@@ -1,5 +1,7 @@
1
1
  /**
2
2
  * Copyright © 2024 650 Industries.
3
3
  */
4
- import { ConfigAPI } from '@babel/core';
5
- export declare function expoUseDomDirectivePlugin(api: ConfigAPI): babel.PluginObj;
4
+ import { ConfigAPI, types } from '@babel/core';
5
+ export declare function expoUseDomDirectivePlugin(api: ConfigAPI & {
6
+ types: typeof types;
7
+ }): babel.PluginObj;
@@ -13,6 +13,7 @@ const path_1 = require("path");
13
13
  const url_1 = __importDefault(require("url"));
14
14
  const common_1 = require("./common");
15
15
  function expoUseDomDirectivePlugin(api) {
16
+ const { types: t } = api;
16
17
  // TODO: Is exporting
17
18
  const isProduction = api.caller(common_1.getIsProd);
18
19
  const platform = api.caller((caller) => caller?.platform);
@@ -41,6 +42,14 @@ function expoUseDomDirectivePlugin(api) {
41
42
  // Collect all of the exports
42
43
  path.traverse({
43
44
  ExportNamedDeclaration(path) {
45
+ const declaration = path.node.declaration;
46
+ if (t.isTypeAlias(declaration) ||
47
+ t.isInterfaceDeclaration(declaration) ||
48
+ t.isTSTypeAliasDeclaration(declaration) ||
49
+ t.isTSInterfaceDeclaration(declaration)) {
50
+ // Allows type exports
51
+ return;
52
+ }
44
53
  throw path.buildCodeFrameError('Modules with the "use dom" directive only support a single default export.');
45
54
  },
46
55
  ExportDefaultDeclaration() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "babel-preset-expo",
3
- "version": "11.1.0-canary-20241008-90b13ad",
3
+ "version": "11.1.0-canary-20241018-4f8243a",
4
4
  "description": "The Babel preset for Expo projects",
5
5
  "main": "build/index.js",
6
6
  "files": [
@@ -48,8 +48,8 @@
48
48
  "@babel/plugin-transform-parameters": "^7.22.15",
49
49
  "@babel/preset-react": "^7.22.15",
50
50
  "@babel/preset-typescript": "^7.23.0",
51
- "@react-native/babel-preset": "0.75.2",
52
- "babel-plugin-react-native-web": "~0.19.10",
51
+ "@react-native/babel-preset": "0.76.0-rc.6",
52
+ "babel-plugin-react-native-web": "~0.19.13",
53
53
  "react-refresh": "^0.14.2"
54
54
  },
55
55
  "peerDependencies": {
@@ -63,8 +63,8 @@
63
63
  "devDependencies": {
64
64
  "@babel/core": "^7.20.0",
65
65
  "babel-plugin-react-compiler": "0.0.0-experimental-334f00b-20240725",
66
- "expo-module-scripts": "3.6.0-canary-20241008-90b13ad",
66
+ "expo-module-scripts": "3.6.0-canary-20241018-4f8243a",
67
67
  "jest": "^29.2.1"
68
68
  },
69
- "gitHead": "90b13ad9d0dd3469556ac776d8b74643375b1d97"
69
+ "gitHead": "4f8243a009855c0cb389307401fb6b1fc9ce03b9"
70
70
  }