UNPKG

@bencapp3/react-native-static-server

Version:
428 lines (406 loc) 15.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "ERROR_LOG_FILE", { enumerable: true, get: function () { return _config.ERROR_LOG_FILE; } }); Object.defineProperty(exports, "STATES", { enumerable: true, get: function () { return _constants.STATES; } }); Object.defineProperty(exports, "UPLOADS_DIR", { enumerable: true, get: function () { return _config.UPLOADS_DIR; } }); Object.defineProperty(exports, "WORK_DIR", { enumerable: true, get: function () { return _config.WORK_DIR; } }); exports.default = void 0; exports.extractBundledAssets = extractBundledAssets; exports.getActiveServer = getActiveServer; exports.getActiveServerId = void 0; Object.defineProperty(exports, "resolveAssetsPath", { enumerable: true, get: function () { return _utils.resolveAssetsPath; } }); var _reactNative = require("react-native"); var _reactNativeFs = require("@bencapp3/react-native-fs"); var _jsUtils = require("@dr.pogodin/js-utils"); var _config = require("./config.js"); var _constants = require("./constants.js"); var _ReactNativeStaticServer = _interopRequireDefault(require("./ReactNativeStaticServer.js")); var _utils = require("./utils.js"); function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; } // ID-to-StaticServer map for all potentially active server instances, // used to route native events back to JS server objects. const servers = {}; const nativeEventEmitter = new _reactNative.NativeEventEmitter(_ReactNativeStaticServer.default); const LOOPBACK_ADDRESS = '127.0.0.1'; nativeEventEmitter.addListener('RNStaticServer', ({ serverId, event, details }) => { const group = servers[serverId]; if (group) { switch (event) { case _constants.SIGNALS.CRASHED: // TODO: We probably can, and should, capture the native stack trace // and pass it along with the error. const error = Error(details); group.forEach(item => item._setState(_constants.STATES.CRASHED, details, error)); // TODO: Should we do here the following? // delete servers[this._id]; break; default: throw Error(`Unexpected signal ${event}`); } } }); // TODO: The idea for later implementation is to allow users to provide their // own lighttpd config files with completely custom configuration. To do so, // we'll probably split StaticServer class in two: the BaseServer will run // server with given config path, and it will contain the logic like server // state watching, stopInBackground, etc. The StaticServer class will extend it, // allowing the current interface (starting the server with default config, // and providing the most important config options via arguments), it will // hold the props for accesing those options, selected hostname and port, etc. // (as BaseServer will run the server on whatever port is given in provided // config, but it won't be able to tell user what address / port it was, etc.). class StaticServer { // NOTE: could be a private method, and we tried it, but it turns out that // Babel's @babel/plugin-proposal-private-methods causes many troubles in RN. // See: https://github.com/birdofpreyru/react-native-static-server/issues/6 // and: https://github.com/birdofpreyru/react-native-static-server/issues/9 _hostname = ''; /* DEPRECATED */ _origin = ''; _stateChangeEmitter = new _jsUtils.Emitter(); // TODO: It will be better to use UUID, but I believe "uuid" library // I would use won't work in RN without additional workarounds applied // to RN setup to get around some issues with randombytes support in // RN JS engine. Anyway, it should be double-checked later, but using // timestamps as ID will do for now. // It is used to serialize state change requests, thus ensuring that parallel // requests to start / stop the server won't result in a corrupt state. _sem = new _jsUtils.Semaphore(true); get errorLog() { return this._errorLog || false; } get fileDir() { return this._fileDir; } get hostname() { return this._hostname; } get id() { return this._id; } /** @deprecated */ get nonLocal() { return this._nonLocal; } get origin() { return this._origin; } get stopInBackground() { return this._stopInBackground; } get port() { return this._port; } get state() { return this._state; } _setState(neu, details = '', error) { this._state = neu; this._stateChangeEmitter.emit(neu, details, error); } /** * Creates a new Server instance. */ constructor({ errorLog = false, extraConfig = '', fileDir, hostname, // NOTE: For some reasons RN-Windows corrupts large numbers sent // as event arguments across JS / Native boundary. // Everything smaller than 65535 seems to work fine, so let's just // truncate these IDs for now. // See: https://github.com/microsoft/react-native-windows/issues/11322 id = Date.now() % 65535, /* DEPRECATED */nonLocal = false, port = 0, state = _constants.STATES.INACTIVE, stopInBackground = false, /* DEPRECATED */webdav }) { if (errorLog) this._errorLog = errorLog === true ? {} : errorLog; this._extraConfig = extraConfig; this._id = id; this._nonLocal = nonLocal; this._hostname = hostname || (nonLocal ? '' : LOOPBACK_ADDRESS); this._port = port; this._stopInBackground = stopInBackground; this._state = state; // NOTE: Normally, a server instance is connected to events from the native // side inside its .start() call, and it is disconnected from them inside // its .stop() call (or crash clean-up sequence). However, if the server is // created with a claim that it is already active, we should connect it to // the events rigth away here. switch (state) { case _constants.STATES.ACTIVE: case _constants.STATES.STARTING: { this._registerSelf(); break; } default: } if (!fileDir) throw Error('`fileDir` MUST BE a non-empty string'); this._fileDir = (0, _utils.resolveAssetsPath)(fileDir); this._webdav = webdav; } addStateListener(listener) { return this._stateChangeEmitter.addListener(listener); } _configureAppStateHandling() { if (this._stopInBackground) { if (!this._appStateSub) { this._appStateSub = _reactNative.AppState.addEventListener('change', this._handleAppStateChange.bind(this)); } } else if (this._appStateSub) { this._appStateSub.remove(); this._appStateSub = undefined; } } async _removeConfigFile() { if (this._configPath) { const p = this._configPath; // Resetting the field prior to the async unlink attempt is safer, // in case the caller does not await for this method to complete. this._configPath = undefined; try { await (0, _reactNativeFs.unlink)(p); } catch { // IGNORE } } } _registerSelf() { let group = servers[this._id]; if (group) group.add(this);else { group = new Set([this]); servers[this._id] = group; } } /** * This method throws if server is not in a "stable" state, i.e. not in one * of these: ACTIVE, CRASHED, INACTIVE. */ _stableStateGuard() { switch (this._state) { case _constants.STATES.ACTIVE: case _constants.STATES.CRASHED: case _constants.STATES.INACTIVE: return; default: throw Error(`Server is in unstable state ${this._state}`); } } /** * Removes all state listeners connected to this server instance. */ removeAllStateListeners() { this._stateChangeEmitter.removeAllListeners(); } /** * Removes given state listener, if it is connected to this server instance; * or does nothing if the listener is not connected to it. * @param listener */ removeStateListener(listener) { this._stateChangeEmitter.removeListener(listener); } /** * @param {string} [details] Optional. If provided, it will be added * to the STARTING message emitted to the server state change listeners. * @returns {Promise<string>} */ async start(details) { try { await this._sem.seize(); this._stableStateGuard(); if (this._state === _constants.STATES.ACTIVE) return this._origin; this._registerSelf(); this._setState(_constants.STATES.STARTING, details); this._configureAppStateHandling(); // NOTE: This is done at the first start only, to avoid hostname changes // when server is paused for background and automatically reactivated // later. Same for automatic port selection. if (!this._hostname) { this._hostname = await _ReactNativeStaticServer.default.getLocalIpAddress(); } if (!this._port) { this._port = await _ReactNativeStaticServer.default.getOpenPort(this._hostname); } this._origin = `http://${this._hostname}:${this._port}`; await this._removeConfigFile(); this._configPath = await (0, _config.newStandardConfigFile)({ errorLog: this._errorLog, extraConfig: this._extraConfig, fileDir: this._fileDir, hostname: this._hostname, port: this._port, webdav: this._webdav }); // Native implementations of .start() method must resolve only once // the server has been launched (ready to handle incoming requests). await _ReactNativeStaticServer.default.start(this._id, this._configPath, this._errorLog ? _config.ERROR_LOG_FILE : ''); this._setState(_constants.STATES.ACTIVE); this._removeConfigFile(); return this._origin; } catch (e) { const error = e instanceof Error ? e : Error(e.message, { cause: e }); this._setState(_constants.STATES.CRASHED, error.message, error); throw error; } finally { this._sem.setReady(true); } } /** * Soft-stop the server: if automatic pause for background is enabled, * it will be automatically reactivated when the app goes into foreground * again. End users are expected to use public .stop() method below, which * additionally cleans-up that automatic re-activation. * @param {string} [details] Optional. If provided, it will be added * to the STOPPING message emitted to the server state change listeners. * @returns {Promise<>} */ async _stop(details) { try { await this._sem.seize(); this._stableStateGuard(); if (this._state !== _constants.STATES.ACTIVE) return; this._setState(_constants.STATES.STOPPING, details); // Native implementations of .stop() method must resolve only once // the server has been completely shut down (released the port it listens). await _ReactNativeStaticServer.default.stop(); this._setState(_constants.STATES.INACTIVE); } catch (e) { const error = e instanceof Error ? e : Error(e.message, { cause: e }); this._setState(_constants.STATES.CRASHED, error.message, error); throw error; } finally { const set = servers[this._id]; set?.delete(this); if (!set?.size) delete servers[this._id]; this._sem.setReady(true); } } /** * Stops or pauses the server, if it is running, depending on whether it was * started with keepAlive option or not (note, keepAlive means that server is * not paused / restarted when the app goes into background). Pausing the * server means it will * automatically start again the next time the app transitions from background * to foreground. To ensure the server is stopped for good, pass in `kill` * flag. In that case only explicit call to .start() will start the server * again. * @param {string} [details] Optional. If provided, it will be added * to the STOPPING message emitted to the server state change listeners. * @returns {Promise<>} */ async stop(details) { if (this._appStateSub) { this._appStateSub.remove(); this._appStateSub = undefined; } await this._stop(details); } async _handleAppStateChange(appState) { const starting = appState === 'active' || appState === 'inactive'; try { if (starting) await this.start('App entered foreground');else await this._stop('App entered background'); } catch (e) { // If anything goes wrong within .start() or ._stop() calls, those methods // will move the server into the "CRASHED" state, and they'll notify all // server state listeners (see .addStateListener()) about the error, with // all related details. // // Thus, if any state listener is connected to the server, we assume it // handles possible errors, and we just silently ignore the error here // (otherwise some instrumentation, like e.g. Sentry, may catch and report // it as unhandled, which is confusing when the error has been handled // within the listener). // // However, if no listeners are connected, we throw an error to allow // the instrumentation, if any, to detect and report it as unhandled. // // In either case this very function is used internally, thus no way for // the library consumer to directly handle its possible rejections. if (!this._stateChangeEmitter.hasListeners) { throw Error(starting ? `Server (#${this._id}) auto-start on the app going into foreground failed` : `Server (#${this._id}) auto-stop on the app going into background failed`); } } } } var _default = exports.default = StaticServer; /** * Extracts bundled assets into the specified regular directory, * preserving asset folder structure, and overwriting any conflicting files * in the destination. * * This is an Android-specific function; it does nothing if called on iOS. * * @param {string} [into=''] Optional. The destination folder for extracted * assets. By default assets are extracted into the app's document folder. * @param {string} [from=''] Optional. Relative path of the root asset folder, * starting from which all assets contained in that folder and its subfolders * will be extracted into the destination folder, preserving asset folder * structure. By default all bundled assets will be extracted. * @return {Promise} Resolves once unpacking is completed. */ async function extractBundledAssets(into = _reactNativeFs.DocumentDirectoryPath, from = '') { console.warn('extractBundledAssets() is deprecated! See: https://github.com/birdofpreyru/react-native-static-server?tab=readme-ov-file#extractbundledassets'); if (_reactNative.Platform.OS !== 'android') return; await (0, _reactNativeFs.mkdir)(into); const assets = await (0, _reactNativeFs.readDirAssets)(from); for (let i = 0; i < assets.length; ++i) { const asset = assets[i]; const target = `${into}/${asset.name}`; if (asset.isDirectory()) await extractBundledAssets(target, asset.path);else await (0, _reactNativeFs.copyFileAssets)(asset.path, target); } } /** * Returns a server instance currently being in ACTIVE, STARTING, * or STOPPING state, if such server instance exists. * @return {StaticServer|undefined} */ function getActiveServer() { return Object.values(servers).find(group => { const server = group.values().next().value; const state = server?.state; return state !== _constants.STATES.INACTIVE && state !== _constants.STATES.CRASHED; }); } const getActiveServerId = exports.getActiveServerId = _ReactNativeStaticServer.default.getActiveServerId; //# sourceMappingURL=index.js.map