603 lines
30 KiB
JavaScript
603 lines
30 KiB
JavaScript
"use strict";
|
|
/**
|
|
* Copyright © 2024 650 Industries.
|
|
* Copyright © 2024 2023 lubieowoce
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*
|
|
* https://github.com/lubieowoce/tangle/blob/5229666fb317d0da9363363fc46dc542ba51e4f7/packages/babel-rsc/src/babel-rsc-actions.ts#L1C1-L909C25
|
|
*/
|
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
};
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.reactServerActionsPlugin = reactServerActionsPlugin;
|
|
const node_path_1 = require("node:path");
|
|
const node_url_1 = __importDefault(require("node:url"));
|
|
const common_1 = require("./common");
|
|
const debug = require('debug')('expo:babel:server-actions');
|
|
const LAZY_WRAPPER_VALUE_KEY = 'value';
|
|
function reactServerActionsPlugin(api) {
|
|
const { types: t } = api;
|
|
// React doesn't like non-enumerable properties on serialized objects (see `isSimpleObject`),
|
|
// so we have to use closure scope for the cache (instead of a non-enumerable `this._cache`)
|
|
const _buildLazyWrapperHelper = api.template(`(thunk) => {
|
|
let cache;
|
|
return {
|
|
get ${LAZY_WRAPPER_VALUE_KEY}() {
|
|
return cache || (cache = thunk());
|
|
}
|
|
}
|
|
}`);
|
|
const buildLazyWrapperHelper = () => {
|
|
return _buildLazyWrapperHelper().expression;
|
|
};
|
|
const possibleProjectRoot = api.caller(common_1.getPossibleProjectRoot);
|
|
let addReactImport;
|
|
let wrapBoundArgs;
|
|
let getActionModuleId;
|
|
const extractInlineActionToTopLevel = (path, _state, { body, freeVariables, }) => {
|
|
const actionModuleId = getActionModuleId();
|
|
const moduleScope = path.scope.getProgramParent();
|
|
const extractedIdentifier = moduleScope.generateUidIdentifier('$$INLINE_ACTION');
|
|
let extractedFunctionParams = [...path.node.params];
|
|
let extractedFunctionBody = body.body;
|
|
if (freeVariables.length > 0) {
|
|
// only add a closure object if we're not closing over anything.
|
|
// const [x, y, z] = await _decryptActionBoundArgs(await $$CLOSURE.value);
|
|
const closureParam = path.scope.generateUidIdentifier('$$CLOSURE');
|
|
const freeVarsPat = t.arrayPattern(freeVariables.map((variable) => t.identifier(variable)));
|
|
const closureExpr = t.memberExpression(closureParam, t.identifier(LAZY_WRAPPER_VALUE_KEY));
|
|
extractedFunctionParams = [closureParam, ...path.node.params];
|
|
extractedFunctionBody = [
|
|
t.variableDeclaration('var', [
|
|
t.variableDeclarator(t.assignmentPattern(freeVarsPat, closureExpr)),
|
|
]),
|
|
...extractedFunctionBody,
|
|
];
|
|
}
|
|
const wrapInRegister = (expr, exportedName) => {
|
|
const expoRegisterServerReferenceId = addReactImport();
|
|
return t.callExpression(expoRegisterServerReferenceId, [
|
|
expr,
|
|
t.stringLiteral(actionModuleId),
|
|
t.stringLiteral(exportedName),
|
|
]);
|
|
};
|
|
const isArrowFn = path.isArrowFunctionExpression();
|
|
const extractedFunctionExpr = wrapInRegister(isArrowFn
|
|
? t.arrowFunctionExpression(extractedFunctionParams, t.blockStatement(extractedFunctionBody), true)
|
|
: t.functionExpression(path.node.id, extractedFunctionParams, t.blockStatement(extractedFunctionBody), false, true), extractedIdentifier.name);
|
|
// Create a top-level declaration for the extracted function.
|
|
const bindingKind = 'var';
|
|
const functionDeclaration = t.exportNamedDeclaration(t.variableDeclaration(bindingKind, [
|
|
t.variableDeclarator(extractedIdentifier, extractedFunctionExpr),
|
|
]));
|
|
// Insert the declaration as close to the original declaration as possible.
|
|
const isPathFunctionInTopLevel = path.find((p) => p.isProgram()) === path;
|
|
const decl = isPathFunctionInTopLevel ? path : findImmediatelyEnclosingDeclaration(path);
|
|
let inserted;
|
|
const canInsertExportNextToPath = (decl) => {
|
|
if (!decl) {
|
|
return false;
|
|
}
|
|
if (decl.parentPath?.isProgram()) {
|
|
return true;
|
|
}
|
|
return false;
|
|
};
|
|
const findNearestPathThatSupportsInsertBefore = (decl) => {
|
|
let current = decl;
|
|
// Check if current scope is suitable for `export` insertion
|
|
while (current && !current.isProgram()) {
|
|
if (canInsertExportNextToPath(current)) {
|
|
return current;
|
|
}
|
|
const parentPath = current.parentPath;
|
|
if (!parentPath) {
|
|
return null;
|
|
}
|
|
current = parentPath;
|
|
}
|
|
if (current.isFunction()) {
|
|
// Don't insert exports inside functions
|
|
return null;
|
|
}
|
|
return current;
|
|
};
|
|
const topLevelDecl = decl ? findNearestPathThatSupportsInsertBefore(decl) : null;
|
|
if (topLevelDecl) {
|
|
// If it's a variable declaration, insert before its parent statement to avoid syntax errors
|
|
const targetPath = topLevelDecl.isVariableDeclarator()
|
|
? topLevelDecl.parentPath
|
|
: topLevelDecl;
|
|
[inserted] = targetPath.insertBefore(functionDeclaration);
|
|
moduleScope.registerBinding(bindingKind, inserted);
|
|
inserted.addComment('leading', ' hoisted action: ' + (getFnPathName(path) ?? '<anonymous>'), true);
|
|
}
|
|
else {
|
|
// Fallback to inserting after the last import if no enclosing declaration is found
|
|
const programBody = moduleScope.path.get('body');
|
|
const lastImportPath = (Array.isArray(programBody) ? programBody : [programBody]).findLast((statement) => {
|
|
return statement.isImportDeclaration();
|
|
});
|
|
[inserted] = lastImportPath.insertAfter(functionDeclaration);
|
|
moduleScope.registerBinding(bindingKind, inserted);
|
|
inserted.addComment('leading', ' hoisted action: ' + (getFnPathName(path) ?? '<anonymous>'), true);
|
|
}
|
|
return {
|
|
inserted,
|
|
extractedIdentifier,
|
|
getReplacement: () => getInlineActionReplacement({
|
|
id: extractedIdentifier,
|
|
freeVariables,
|
|
}),
|
|
};
|
|
};
|
|
const getInlineActionReplacement = ({ id, freeVariables, }) => {
|
|
if (freeVariables.length === 0) {
|
|
return id;
|
|
}
|
|
const capturedVarsExpr = t.arrayExpression(freeVariables.map((variable) => t.identifier(variable)));
|
|
const boundArgs = wrapBoundArgs(capturedVarsExpr);
|
|
// _ACTION.bind(null, { get value() { return _encryptActionBoundArgs([x, y, z]) } })
|
|
return t.callExpression(t.memberExpression(id, t.identifier('bind')), [
|
|
t.nullLiteral(),
|
|
boundArgs,
|
|
]);
|
|
};
|
|
function hasUseServerDirective(path) {
|
|
const { body } = path.node;
|
|
return t.isBlockStatement(body) && body.directives.some((d) => d.value.value === 'use server');
|
|
}
|
|
return {
|
|
name: 'expo-server-actions',
|
|
pre(file) {
|
|
const projectRoot = possibleProjectRoot || file.opts.root || '';
|
|
if (!file.code.includes('use server')) {
|
|
file.path.skip();
|
|
return;
|
|
}
|
|
assertExpoMetadata(file.metadata);
|
|
file.metadata.extractedActions = [];
|
|
file.metadata.isModuleMarkedWithUseServerDirective = false;
|
|
const addNamedImportOnce = (0, common_1.createAddNamedImportOnce)(t);
|
|
addReactImport = () => {
|
|
return addNamedImportOnce(file.path, 'registerServerReference', 'react-server-dom-webpack/server');
|
|
};
|
|
getActionModuleId = once(() => {
|
|
// Create relative file path hash.
|
|
return './' + (0, common_1.toPosixPath)((0, node_path_1.relative)(projectRoot, file.opts.filename));
|
|
});
|
|
const defineBoundArgsWrapperHelper = once(() => {
|
|
const id = this.file.path.scope.generateUidIdentifier('wrapBoundArgs');
|
|
this.file.path.scope.push({
|
|
id,
|
|
kind: 'var',
|
|
init: buildLazyWrapperHelper(),
|
|
});
|
|
return id;
|
|
});
|
|
wrapBoundArgs = (expr) => {
|
|
const wrapperFn = t.cloneNode(defineBoundArgsWrapperHelper());
|
|
return t.callExpression(wrapperFn, [t.arrowFunctionExpression([], expr)]);
|
|
};
|
|
},
|
|
visitor: {
|
|
Program(path, state) {
|
|
if (path.node.directives.some((d) => d.value.value === 'use server')) {
|
|
assertExpoMetadata(state.file.metadata);
|
|
state.file.metadata.isModuleMarkedWithUseServerDirective = true;
|
|
// remove the directive so that downstream consumers don't transform the module again.
|
|
path.node.directives = path.node.directives.filter((d) => d.value.value !== 'use server');
|
|
}
|
|
},
|
|
// `() => {}`
|
|
ArrowFunctionExpression(path, state) {
|
|
const { body } = path.node;
|
|
if (!t.isBlockStatement(body) || !hasUseServerDirective(path)) {
|
|
return;
|
|
}
|
|
assertIsAsyncFn(path);
|
|
const freeVariables = getFreeVariables(path);
|
|
const tlb = getTopLevelBinding(path);
|
|
const { extractedIdentifier, getReplacement } = extractInlineActionToTopLevel(path, state, {
|
|
freeVariables,
|
|
body,
|
|
});
|
|
path.replaceWith(getReplacement());
|
|
assertExpoMetadata(state.file.metadata);
|
|
state.file.metadata.extractedActions.push({
|
|
localName: tlb?.identifier.name,
|
|
exportedName: extractedIdentifier.name,
|
|
});
|
|
},
|
|
// `function foo() { ... }`
|
|
FunctionDeclaration(path, state) {
|
|
if (!hasUseServerDirective(path)) {
|
|
return;
|
|
}
|
|
assertIsAsyncFn(path);
|
|
const fnId = path.node.id;
|
|
if (!fnId) {
|
|
throw path.buildCodeFrameError('Internal error: expected FunctionDeclaration to have a name');
|
|
}
|
|
const freeVariables = getFreeVariables(path);
|
|
const { extractedIdentifier, getReplacement } = extractInlineActionToTopLevel(path, state, {
|
|
freeVariables,
|
|
body: path.node.body,
|
|
});
|
|
const tlb = getTopLevelBinding(path);
|
|
if (tlb) {
|
|
// we're at the top level, and we might be enclosed within a `export` decl.
|
|
// we have to keep the export in place, because it might be used elsewhere,
|
|
// so we can't just remove this node.
|
|
// replace the function decl with a (hopefully) equivalent var declaration
|
|
// `var [name] = $$INLINE_ACTION_{N}`
|
|
const bindingKind = 'var';
|
|
const [inserted] = path.replaceWith(t.variableDeclaration(bindingKind, [t.variableDeclarator(fnId, extractedIdentifier)]));
|
|
tlb.scope.registerBinding(bindingKind, inserted);
|
|
}
|
|
else {
|
|
// note: if we do this *after* adding the new declaration, the bindings get messed up
|
|
path.remove();
|
|
// add a declaration in the place where the function decl would be hoisted to.
|
|
// (this avoids issues with functions defined after `return`, see `test-cases/named-after-return.jsx`)
|
|
path.scope.push({
|
|
id: fnId,
|
|
init: getReplacement(),
|
|
kind: 'var',
|
|
unique: true,
|
|
});
|
|
}
|
|
assertExpoMetadata(state.file.metadata);
|
|
state.file.metadata.extractedActions.push({
|
|
localName: tlb?.identifier.name,
|
|
exportedName: extractedIdentifier.name,
|
|
});
|
|
},
|
|
// `const foo = function() { ... }`
|
|
FunctionExpression(path, state) {
|
|
if (!hasUseServerDirective(path)) {
|
|
return;
|
|
}
|
|
assertIsAsyncFn(path);
|
|
const { body } = path.node;
|
|
const freeVariables = getFreeVariables(path);
|
|
// TODO: look for usages of the name (if present), that's technically possible
|
|
// const fnId = path.node.id;
|
|
const { extractedIdentifier, getReplacement } = extractInlineActionToTopLevel(path, state, {
|
|
freeVariables,
|
|
body,
|
|
});
|
|
const tlb = getTopLevelBinding(path);
|
|
assertExpoMetadata(state.file.metadata);
|
|
path.replaceWith(getReplacement());
|
|
state.file.metadata.extractedActions.push({
|
|
localName: tlb?.identifier.name,
|
|
exportedName: extractedIdentifier.name,
|
|
});
|
|
},
|
|
// Top-level "use server"
|
|
ExportDefaultDeclaration(path, state) {
|
|
assertExpoMetadata(state.file.metadata);
|
|
if (!state.file.metadata.isModuleMarkedWithUseServerDirective) {
|
|
return;
|
|
}
|
|
// Convert `export default function foo() {}` to `function foo() {}; export { foo as default }`
|
|
if (path.node.declaration) {
|
|
if (t.isFunctionDeclaration(path.node.declaration)) {
|
|
let { id } = path.node.declaration;
|
|
if (id == null) {
|
|
const moduleScope = path.scope.getProgramParent();
|
|
const extractedIdentifier = moduleScope.generateUidIdentifier('$$INLINE_ACTION');
|
|
id = extractedIdentifier;
|
|
// Transform `async function () {}` to `async function $$INLINE_ACTION() {}`
|
|
path.node.declaration.id = extractedIdentifier;
|
|
}
|
|
const exportedSpecifier = t.exportSpecifier(id, t.identifier('default'));
|
|
path.replaceWith(path.node.declaration);
|
|
path.insertAfter(t.exportNamedDeclaration(null, [exportedSpecifier]));
|
|
}
|
|
else {
|
|
// Convert anonymous function expressions to named function expressions and export them as default.
|
|
// export default foo = async () => {}
|
|
// vvv
|
|
// const foo = async () => {}
|
|
// (() => _registerServerReference(foo, "file:///unknown", "default"))();
|
|
// export { foo as default };
|
|
if (t.isAssignmentExpression(path.node.declaration) &&
|
|
t.isArrowFunctionExpression(path.node.declaration.right)) {
|
|
if (!t.isIdentifier(path.node.declaration.left)) {
|
|
throw path.buildCodeFrameError(`Expected an assignment to an identifier but found ${path.node.declaration.left.type}.`);
|
|
}
|
|
const { left, right } = path.node.declaration;
|
|
const id = left;
|
|
const exportedSpecifier = t.exportSpecifier(id, t.identifier('default'));
|
|
// Replace `export default foo = async () => {}` with `const foo = async () => {}`
|
|
path.replaceWith(t.variableDeclaration('var', [t.variableDeclarator(id, right)]));
|
|
// Insert `(() => _registerServerReference(foo, "file:///unknown", "default"))();`
|
|
path.insertAfter(t.exportNamedDeclaration(null, [exportedSpecifier]));
|
|
}
|
|
else if (t.isArrowFunctionExpression(path.node.declaration) &&
|
|
path.node.declaration) {
|
|
// export default async () => {}
|
|
// Give the function a name
|
|
// const $$INLINE_ACTION = async () => {}
|
|
const moduleScope = path.scope.getProgramParent();
|
|
const extractedIdentifier = moduleScope.generateUidIdentifier('$$INLINE_ACTION');
|
|
// @ts-expect-error: Transform `export default async () => {}` to `const $$INLINE_ACTION = async () => {}`
|
|
path.node.declaration = t.variableDeclaration('var', [
|
|
t.variableDeclarator(extractedIdentifier, path.node.declaration),
|
|
]);
|
|
// Strip the `export default`
|
|
path.replaceWith(path.node.declaration);
|
|
// export { $$INLINE_ACTION as default }
|
|
const exportedSpecifier = t.exportSpecifier(extractedIdentifier, t.identifier('default'));
|
|
path.insertAfter(t.exportNamedDeclaration(null, [exportedSpecifier]));
|
|
}
|
|
else if (
|
|
// Match `export default foo;`
|
|
t.isIdentifier(path.node.declaration)) {
|
|
// Ensure the `path.node.declaration` is a function or a variable for a function.
|
|
const binding = path.scope.getBinding(path.node.declaration.name);
|
|
const isServerActionType = t.isFunctionDeclaration(binding?.path.node ?? path.node.declaration) ||
|
|
t.isArrowFunctionExpression(binding?.path.node ?? path.node.declaration) ||
|
|
// `const foo = async () => {}`
|
|
(t.isVariableDeclarator(binding?.path.node) &&
|
|
t.isArrowFunctionExpression(binding?.path.node.init));
|
|
if (isServerActionType) {
|
|
// Convert `export default foo;` to `export { foo as default };`
|
|
const exportedSpecifier = t.exportSpecifier(path.node.declaration, t.identifier('default'));
|
|
path.replaceWith(t.exportNamedDeclaration(null, [exportedSpecifier]));
|
|
}
|
|
}
|
|
else {
|
|
// Unclear when this happens.
|
|
throw path.buildCodeFrameError(`Cannot create server action. Expected a assignment expression but found ${path.node.declaration.type}.`);
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
// TODO: Unclear when this happens.
|
|
throw path.buildCodeFrameError(`Not implemented: 'export default' declarations in "use server" files. Try using 'export { name as default }' instead.`);
|
|
}
|
|
},
|
|
ExportNamedDeclaration(path, state) {
|
|
assertExpoMetadata(state.file.metadata);
|
|
if (!state.file.metadata.isModuleMarkedWithUseServerDirective) {
|
|
return;
|
|
}
|
|
// Skip type-only exports (`export type { Foo } from '...'` or `export { type Foo }`)
|
|
if (path.node.exportKind === 'type') {
|
|
return;
|
|
}
|
|
// This can happen with `export {};` and TypeScript types.
|
|
if (!path.node.declaration && !path.node.specifiers.length) {
|
|
return;
|
|
}
|
|
const actionModuleId = getActionModuleId();
|
|
const createRegisterCall = (identifier, exported = identifier) => {
|
|
const exportedName = t.isIdentifier(exported) ? exported.name : exported.value;
|
|
const call = t.callExpression(addReactImport(), [
|
|
identifier,
|
|
t.stringLiteral(actionModuleId),
|
|
t.stringLiteral(exportedName),
|
|
]);
|
|
// Wrap call with `;(() => { ... })();` to avoid issues with ASI
|
|
return t.expressionStatement(t.callExpression(t.arrowFunctionExpression([], call), []));
|
|
};
|
|
if (path.node.specifiers.length > 0) {
|
|
for (const specifier of path.node.specifiers) {
|
|
// `export * as ns from './foo';`
|
|
if (t.isExportNamespaceSpecifier(specifier)) {
|
|
throw path.buildCodeFrameError('Namespace exports for server actions are not supported. Re-export named actions instead: export { foo } from "./bar".');
|
|
}
|
|
else if (t.isExportDefaultSpecifier(specifier)) {
|
|
// NOTE: This is handled by ExportDefaultDeclaration
|
|
// `export default foo;`
|
|
throw path.buildCodeFrameError('Internal error while extracting server actions. Expected `export default variable;` to be extracted. (ExportDefaultSpecifier in ExportNamedDeclaration)');
|
|
}
|
|
else if (t.isExportSpecifier(specifier)) {
|
|
// Skip TypeScript type re-exports (e.g., `export { type Foo }`)
|
|
if (specifier.exportKind === 'type') {
|
|
continue;
|
|
}
|
|
// `export { foo };`
|
|
// `export { foo as [bar|default] };`
|
|
const localName = specifier.local.name;
|
|
const exportedName = t.isIdentifier(specifier.exported)
|
|
? specifier.exported.name
|
|
: specifier.exported.value;
|
|
// if we're reexporting an existing action under a new name, we shouldn't register() it again.
|
|
if (!state.file.metadata.extractedActions.some((info) => info.localName === localName)) {
|
|
// referencing the function's local identifier here *should* be safe (w.r.t. TDZ) because
|
|
// 1. if it's a `export async function foo() {}`, the declaration will be hoisted,
|
|
// so it's safe to reference no matter how the declarations are ordered
|
|
// 2. if it's an `export const foo = async () => {}`, then the standalone `export { foo }`
|
|
// has to follow the definition, so we can reference it right before the export decl as well
|
|
path.insertBefore(createRegisterCall(specifier.local, specifier.exported));
|
|
}
|
|
state.file.metadata.extractedActions.push({ localName, exportedName });
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
if (!path.node.declaration) {
|
|
throw path.buildCodeFrameError(`Internal error: Unexpected 'ExportNamedDeclaration' without declarations`);
|
|
}
|
|
const identifiers = (() => {
|
|
const innerPath = path.get('declaration');
|
|
if (innerPath.isVariableDeclaration()) {
|
|
return innerPath.get('declarations').map((d) => {
|
|
// TODO: insert `typeof <identifier> === 'function'` check -- it's a variable, so it could be anything
|
|
const id = d.node.id;
|
|
if (!t.isIdentifier(id)) {
|
|
// TODO
|
|
throw innerPath.buildCodeFrameError('Unimplemented');
|
|
}
|
|
return id;
|
|
});
|
|
}
|
|
else if (innerPath.isFunctionDeclaration()) {
|
|
if (!innerPath.get('async')) {
|
|
throw innerPath.buildCodeFrameError(`Functions exported from "use server" files must be async.`);
|
|
}
|
|
return [innerPath.get('id').node];
|
|
}
|
|
else if (
|
|
// TypeScript type exports
|
|
innerPath.isTypeAlias() ||
|
|
innerPath.isTSDeclareFunction() ||
|
|
innerPath.isTSInterfaceDeclaration() ||
|
|
innerPath.isTSTypeAliasDeclaration()) {
|
|
return [];
|
|
}
|
|
else {
|
|
throw innerPath.buildCodeFrameError(`Unimplemented server action export`);
|
|
}
|
|
})();
|
|
path.insertAfter(identifiers.map((identifier) => createRegisterCall(identifier)));
|
|
for (const identifier of identifiers) {
|
|
state.file.metadata.extractedActions.push({
|
|
localName: identifier.name,
|
|
exportedName: identifier.name,
|
|
});
|
|
}
|
|
},
|
|
},
|
|
post(file) {
|
|
assertExpoMetadata(file.metadata);
|
|
if (!file.metadata.extractedActions?.length) {
|
|
return;
|
|
}
|
|
debug('extracted actions', file.metadata.extractedActions);
|
|
const payload = {
|
|
id: getActionModuleId(),
|
|
names: file.metadata.extractedActions.map((e) => e.exportedName),
|
|
};
|
|
const stashedData = 'rsc/actions: ' + JSON.stringify(payload);
|
|
// Add comment for debugging the bundle, we use the babel metadata for accessing the data.
|
|
file.path.addComment('leading', stashedData);
|
|
const filePath = 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');
|
|
}
|
|
const outputKey = node_url_1.default.pathToFileURL(filePath).href;
|
|
file.metadata.reactServerActions = payload;
|
|
file.metadata.reactServerReference = outputKey;
|
|
},
|
|
};
|
|
}
|
|
const getFreeVariables = (path) => {
|
|
const freeVariablesSet = new Set();
|
|
const programScope = path.scope.getProgramParent();
|
|
path.traverse({
|
|
Identifier(innerPath) {
|
|
const { name } = innerPath.node;
|
|
if (!innerPath.isReferencedIdentifier()) {
|
|
debug('skipping - not referenced');
|
|
return;
|
|
}
|
|
if (freeVariablesSet.has(name)) {
|
|
// we've already determined this name to be a free var. no point in recomputing.
|
|
debug('skipping - already registered');
|
|
return;
|
|
}
|
|
const binding = innerPath.scope.getBinding(name);
|
|
if (!binding) {
|
|
// probably a global, or an unbound variable. ignore it.
|
|
debug('skipping - global or unbound, skipping');
|
|
return;
|
|
}
|
|
if (binding.scope === programScope) {
|
|
// module-level declaration. no need to close over it.
|
|
debug('skipping - module-level binding');
|
|
return;
|
|
}
|
|
if (
|
|
// function args or a var at the top-level of its body
|
|
binding.scope === path.scope ||
|
|
// decls from blocks within the function
|
|
isChildScope({
|
|
parent: path.scope,
|
|
child: binding.scope,
|
|
root: programScope,
|
|
})) {
|
|
// the binding came from within the function = it's not closed-over, so don't add it.
|
|
debug('skipping - declared within function');
|
|
return;
|
|
}
|
|
// we've (hopefully) eliminated all the other cases, so we should treat this as a free var.
|
|
debug('adding');
|
|
freeVariablesSet.add(name);
|
|
},
|
|
});
|
|
return [...freeVariablesSet].sort();
|
|
};
|
|
const getFnPathName = (path) => {
|
|
return path.isArrowFunctionExpression() ? undefined : path.node?.id?.name;
|
|
};
|
|
const isChildScope = ({ root, parent, child, }) => {
|
|
let curScope = child;
|
|
while (curScope !== root) {
|
|
if (curScope.parent === parent) {
|
|
return true;
|
|
}
|
|
curScope = curScope.parent;
|
|
}
|
|
return false;
|
|
};
|
|
function findImmediatelyEnclosingDeclaration(path) {
|
|
let currentPath = path;
|
|
while (!currentPath.isProgram()) {
|
|
if (
|
|
// const foo = async () => { ... }
|
|
// ^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
currentPath.isVariableDeclarator() ||
|
|
// async function foo() { ... }
|
|
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
currentPath.isDeclaration()) {
|
|
return currentPath;
|
|
}
|
|
// if we encounter an expression on the way, this isn't a top level decl, and needs to be hoisted.
|
|
// e.g. `export const foo = withAuth(async () => { ... })`
|
|
if (currentPath !== path && currentPath.isExpression()) {
|
|
return null;
|
|
}
|
|
if (!currentPath.parentPath) {
|
|
return null;
|
|
}
|
|
currentPath = currentPath.parentPath;
|
|
}
|
|
return null;
|
|
}
|
|
const getTopLevelBinding = (path) => {
|
|
const decl = findImmediatelyEnclosingDeclaration(path);
|
|
if (!decl || !('id' in decl.node) || !decl.node.id || !('name' in decl.node.id))
|
|
return null;
|
|
const declBinding = decl.scope.getBinding(decl.node.id.name);
|
|
return declBinding.scope === path.scope.getProgramParent() ? declBinding : null;
|
|
};
|
|
const assertIsAsyncFn = (path) => {
|
|
if (!path.node.async) {
|
|
throw path.buildCodeFrameError(`functions marked with "use server" must be async`);
|
|
}
|
|
};
|
|
const once = (fn) => {
|
|
let cache = { has: false };
|
|
return () => {
|
|
if (cache.has)
|
|
return cache.value;
|
|
cache = { has: true, value: fn() };
|
|
return cache.value;
|
|
};
|
|
};
|
|
function assertExpoMetadata(metadata) {
|
|
if (!metadata || typeof metadata !== 'object') {
|
|
throw new Error('Expected Babel state.file.metadata to be an object');
|
|
}
|
|
}
|