"use strict";
Object.defineProperty(exports, "__esModule", {
    value: true
});
Object.defineProperty(exports, "BundlerDevServer", {
    enumerable: true,
    get: ()=>BundlerDevServer
});
function _assert() {
    const data = /*#__PURE__*/ _interopRequireDefault(require("assert"));
    _assert = function() {
        return data;
    };
    return data;
}
function _resolveFrom() {
    const data = /*#__PURE__*/ _interopRequireDefault(require("resolve-from"));
    _resolveFrom = function() {
        return data;
    };
    return data;
}
const _asyncNgrok = require("./AsyncNgrok");
const _asyncWsTunnel = require("./AsyncWsTunnel");
const _devToolsPluginManager = /*#__PURE__*/ _interopRequireDefault(require("./DevToolsPluginManager"));
const _developmentSession = require("./DevelopmentSession");
const _urlCreator = require("./UrlCreator");
const _log = /*#__PURE__*/ _interopRequireWildcard(require("../../log"));
const _fileNotifier = require("../../utils/FileNotifier");
const _delay = require("../../utils/delay");
const _env = require("../../utils/env");
const _errors = require("../../utils/errors");
const _open = require("../../utils/open");
function _interopRequireDefault(obj) {
    return obj && obj.__esModule ? obj : {
        default: obj
    };
}
function _getRequireWildcardCache(nodeInterop) {
    if (typeof WeakMap !== "function") return null;
    var cacheBabelInterop = new WeakMap();
    var cacheNodeInterop = new WeakMap();
    return (_getRequireWildcardCache = function(nodeInterop) {
        return nodeInterop ? cacheNodeInterop : cacheBabelInterop;
    })(nodeInterop);
}
function _interopRequireWildcard(obj, nodeInterop) {
    if (!nodeInterop && obj && obj.__esModule) {
        return obj;
    }
    if (obj === null || typeof obj !== "object" && typeof obj !== "function") {
        return {
            default: obj
        };
    }
    var cache = _getRequireWildcardCache(nodeInterop);
    if (cache && cache.has(obj)) {
        return cache.get(obj);
    }
    var newObj = {};
    var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor;
    for(var key in obj){
        if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) {
            var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null;
            if (desc && (desc.get || desc.set)) {
                Object.defineProperty(newObj, key, desc);
            } else {
                newObj[key] = obj[key];
            }
        }
    }
    newObj.default = obj;
    if (cache) {
        cache.set(obj, newObj);
    }
    return newObj;
}
const debug = require("debug")("expo:start:server:devServer");
const PLATFORM_MANAGERS = {
    simulator: ()=>require("../platforms/ios/ApplePlatformManager").ApplePlatformManager,
    emulator: ()=>require("../platforms/android/AndroidPlatformManager").AndroidPlatformManager
};
class BundlerDevServer {
    constructor(projectRoot, platformBundlers, /** Advanced options */ options){
        this.projectRoot = projectRoot;
        this.platformBundlers = platformBundlers;
        this.tunnel = null;
        this.devSession = null;
        this.instance = null;
        this.platformManagers = {};
        this.urlCreator = null;
        this.notifier = null;
        this.devToolsPluginManager = (options == null ? void 0 : options.devToolsPluginManager) ?? new _devToolsPluginManager.default(projectRoot);
        this.isDevClient = (options == null ? void 0 : options.isDevClient) ?? false;
    }
    setInstance(instance) {
        this.instance = instance;
    }
    /** Get the manifest middleware function. */ async getManifestMiddlewareAsync(options = {}) {
        const Middleware = require("./middleware/ExpoGoManifestHandlerMiddleware").ExpoGoManifestHandlerMiddleware;
        const urlCreator = this.getUrlCreator();
        const middleware = new Middleware(this.projectRoot, {
            constructUrl: urlCreator.constructUrl.bind(urlCreator),
            mode: options.mode,
            minify: options.minify,
            isNativeWebpack: this.name === "webpack" && this.isTargetingNative(),
            privateKeyPath: options.privateKeyPath
        });
        return middleware;
    }
    /** Start the dev server using settings defined in the start command. */ async startAsync(options) {
        await this.stopAsync();
        let instance;
        if (options.headless) {
            instance = await this.startHeadlessAsync(options);
        } else {
            instance = await this.startImplementationAsync(options);
        }
        this.setInstance(instance);
        await this.postStartAsync(options);
        return instance;
    }
    async waitForTypeScriptAsync() {
        return false;
    }
    async watchEnvironmentVariables() {
    // noop -- We've only implemented this functionality in Metro.
    }
    /**
   * Creates a mock server representation that can be used to estimate URLs for a server started in another process.
   * This is used for the run commands where you can reuse the server from a previous run.
   */ async startHeadlessAsync(options) {
        if (!options.port) throw new _errors.CommandError("HEADLESS_SERVER", "headless dev server requires a port option");
        this.urlCreator = this.getUrlCreator(options);
        return {
            // Create a mock server
            server: {
                close: (callback)=>{
                    this.instance = null;
                    callback == null ? void 0 : callback();
                },
                addListener () {}
            },
            location: {
                // The port is the main thing we want to send back.
                port: options.port,
                // localhost isn't always correct.
                host: "localhost",
                // http is the only supported protocol on native.
                url: `http://localhost:${options.port}`,
                protocol: "http"
            },
            middleware: {},
            messageSocket: {
                broadcast: ()=>{
                    throw new _errors.CommandError("HEADLESS_SERVER", "Cannot broadcast messages to headless server");
                }
            }
        };
    }
    /**
   * Runs after the `startAsync` function, performing any additional common operations.
   * You can assume the dev server is started by the time this function is called.
   */ async postStartAsync(options) {
        if (options.location.hostType === "tunnel" && !_env.env.EXPO_OFFLINE && // This is a hack to prevent using tunnel on web since we block it upstream for some reason.
        this.isTargetingNative()) {
            await this._startTunnelAsync();
        } else if ((0, _env.envIsWebcontainer)()) {
            await this._startTunnelAsync();
        }
        if (!options.isExporting) {
            await this.startDevSessionAsync();
            this.watchConfig();
        }
    }
    watchConfig() {
        var ref;
        (ref = this.notifier) == null ? void 0 : ref.stopObserving();
        this.notifier = new _fileNotifier.FileNotifier(this.projectRoot, this.getConfigModuleIds());
        this.notifier.startObserving();
    }
    /** Create ngrok instance and start the tunnel server. Exposed for testing. */ async _startTunnelAsync() {
        var ref;
        const port = (ref = this.getInstance()) == null ? void 0 : ref.location.port;
        if (!port) return null;
        debug("[tunnel] connect to port: " + port);
        this.tunnel = (0, _env.envIsWebcontainer)() ? new _asyncWsTunnel.AsyncWsTunnel(this.projectRoot, port) : new _asyncNgrok.AsyncNgrok(this.projectRoot, port);
        await this.tunnel.startAsync();
        return this.tunnel;
    }
    async startDevSessionAsync() {
        // This is used to make Expo Go open the project in either Expo Go, or the web browser.
        // Must come after ngrok (`startTunnelAsync`) setup.
        this.devSession = new _developmentSession.DevelopmentSession(this.projectRoot, // This URL will be used on external devices so the computer IP won't be relevant.
        this.isTargetingNative() ? this.getNativeRuntimeUrl() : this.getDevServerUrl({
            hostType: "localhost"
        }));
        await this.devSession.startAsync({
            runtime: this.isTargetingNative() ? "native" : "web"
        });
    }
    isTargetingNative() {
        // Temporary hack while we implement multi-bundler dev server proxy.
        return true;
    }
    isTargetingWeb() {
        return this.platformBundlers.web === this.name;
    }
    /**
   * Sends a message over web sockets to any connected device,
   * does nothing when the dev server is not running.
   *
   * @param method name of the command. In RN projects `reload`, and `devMenu` are available. In Expo Go, `sendDevCommand` is available.
   * @param params
   */ broadcastMessage(method, params) {
        var ref;
        (ref = this.getInstance()) == null ? void 0 : ref.messageSocket.broadcast(method, params);
    }
    /** Get the running dev server instance. */ getInstance() {
        return this.instance;
    }
    /** Stop the running dev server instance. */ async stopAsync() {
        var // Stop file watching.
        ref, ref1, ref2;
        (ref = this.notifier) == null ? void 0 : ref.stopObserving();
        // Stop the dev session timer and tell Expo API to remove dev session.
        await ((ref1 = this.devSession) == null ? void 0 : ref1.closeAsync());
        // Stop tunnel if running.
        await ((ref2 = this.tunnel) == null ? void 0 : ref2.stopAsync().catch((e)=>{
            _log.error(`Error stopping tunnel:`);
            _log.exception(e);
        }));
        return (0, _delay.resolveWithTimeout)(()=>{
            return new Promise((resolve, reject)=>{
                var ref;
                // Close the server.
                debug(`Stopping dev server (bundler: ${this.name})`);
                if ((ref = this.instance) == null ? void 0 : ref.server) {
                    // Check if server is even running.
                    this.instance.server.close((error)=>{
                        debug(`Stopped dev server (bundler: ${this.name})`);
                        this.instance = null;
                        if (error) {
                            if ("code" in error && error.code === "ERR_SERVER_NOT_RUNNING") {
                                resolve();
                            } else {
                                reject(error);
                            }
                        } else {
                            resolve();
                        }
                    });
                } else {
                    debug(`Stopped dev server (bundler: ${this.name})`);
                    this.instance = null;
                    resolve();
                }
            });
        }, {
            // NOTE(Bacon): Metro dev server doesn't seem to be closing in time.
            timeout: 1000,
            errorMessage: `Timeout waiting for '${this.name}' dev server to close`
        });
    }
    getUrlCreator(options = {}) {
        if (!this.urlCreator) {
            (0, _assert().default)(options == null ? void 0 : options.port, "Dev server instance not found");
            this.urlCreator = new _urlCreator.UrlCreator(options.location, {
                port: options.port,
                getTunnelUrl: this.getTunnelUrl.bind(this)
            });
        }
        return this.urlCreator;
    }
    getNativeRuntimeUrl(opts = {}) {
        return this.isDevClient ? this.getUrlCreator().constructDevClientUrl(opts) ?? this.getDevServerUrl() : this.getUrlCreator().constructUrl({
            ...opts,
            scheme: "exp"
        });
    }
    /** Get the URL for the running instance of the dev server. */ getDevServerUrl(options = {}) {
        const instance = this.getInstance();
        if (!(instance == null ? void 0 : instance.location)) {
            return null;
        }
        // If we have an active WS tunnel instance, we always need to return the tunnel location.
        if (this.tunnel && this.tunnel instanceof _asyncWsTunnel.AsyncWsTunnel) {
            return this.getUrlCreator().constructUrl();
        }
        const { location  } = instance;
        if (options.hostType === "localhost") {
            return `${location.protocol}://localhost:${location.port}`;
        }
        return location.url ?? null;
    }
    getDevServerUrlOrAssert(options = {}) {
        const instance = this.getDevServerUrl(options);
        if (!instance) {
            throw new _errors.CommandError("DEV_SERVER", `Cannot get the dev server URL before the server has started - bundler[${this.name}]`);
        }
        return instance;
    }
    /** Get the base URL for JS inspector */ getJsInspectorBaseUrl() {
        if (this.name !== "metro") {
            throw new _errors.CommandError("DEV_SERVER", `Cannot get the JS inspector base url - bundler[${this.name}]`);
        }
        return this.getUrlCreator().constructUrl({
            scheme: "http"
        });
    }
    /** Get the tunnel URL from the tunnel. */ getTunnelUrl() {
        var ref;
        return ((ref = this.tunnel) == null ? void 0 : ref.getActiveUrl()) ?? null;
    }
    /** Open the dev server in a runtime. */ async openPlatformAsync(launchTarget, resolver = {}) {
        if (launchTarget === "desktop") {
            const serverUrl = this.getDevServerUrl({
                hostType: "localhost"
            });
            // Allow opening the tunnel URL when using Metro web.
            const url = this.name === "metro" ? this.getTunnelUrl() ?? serverUrl : serverUrl;
            await (0, _open.openBrowserAsync)(url);
            return {
                url
            };
        }
        const runtime = this.isTargetingNative() ? this.isDevClient ? "custom" : "expo" : "web";
        const manager = await this.getPlatformManagerAsync(launchTarget);
        return manager.openAsync({
            runtime
        }, resolver);
    }
    /** Open the dev server in a runtime. */ async openCustomRuntimeAsync(launchTarget, launchProps = {}, resolver = {}) {
        const runtime = this.isTargetingNative() ? this.isDevClient ? "custom" : "expo" : "web";
        if (runtime !== "custom") {
            throw new _errors.CommandError(`dev server cannot open custom runtimes either because it does not target native platforms or because it is not targeting dev clients. (target: ${runtime})`);
        }
        const manager = await this.getPlatformManagerAsync(launchTarget);
        return manager.openAsync({
            runtime: "custom",
            props: launchProps
        }, resolver);
    }
    /** Get the URL for opening in Expo Go. */ getExpoGoUrl() {
        return this.getUrlCreator().constructUrl({
            scheme: "exp"
        });
    }
    /** Should use the interstitial page for selecting which runtime to use. */ isRedirectPageEnabled() {
        return !_env.env.EXPO_NO_REDIRECT_PAGE && // if user passed --dev-client flag, skip interstitial page
        !this.isDevClient && // Checks if dev client is installed.
        !!_resolveFrom().default.silent(this.projectRoot, "expo-dev-client");
    }
    /** Get the redirect URL when redirecting is enabled. */ getRedirectUrl(platform = null) {
        if (!this.isRedirectPageEnabled()) {
            debug("Redirect page is disabled");
            return null;
        }
        return this.getUrlCreator().constructLoadingUrl({}, platform === "emulator" ? "android" : platform === "simulator" ? "ios" : null) ?? null;
    }
    getReactDevToolsUrl() {
        return new URL("_expo/react-devtools", this.getUrlCreator().constructUrl({
            scheme: "http"
        })).toString();
    }
    async getPlatformManagerAsync(platform) {
        if (!this.platformManagers[platform]) {
            var ref;
            const Manager = PLATFORM_MANAGERS[platform]();
            const port = (ref = this.getInstance()) == null ? void 0 : ref.location.port;
            if (!port || !this.urlCreator) {
                throw new _errors.CommandError("DEV_SERVER", "Cannot interact with native platforms until dev server has started");
            }
            debug(`Creating platform manager (platform: ${platform}, port: ${port})`);
            this.platformManagers[platform] = new Manager(this.projectRoot, port, {
                getCustomRuntimeUrl: this.urlCreator.constructDevClientUrl.bind(this.urlCreator),
                getExpoGoUrl: this.getExpoGoUrl.bind(this),
                getRedirectUrl: this.getRedirectUrl.bind(this, platform),
                getDevServerUrl: this.getDevServerUrl.bind(this, {
                    hostType: "localhost"
                })
            });
        }
        return this.platformManagers[platform];
    }
}

//# sourceMappingURL=BundlerDevServer.js.map