"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');
    }
}
