birdofpreyru / react-native-static-server

Embedded HTTP server for React Native
https://dr.pogodin.studio/docs/react-native-static-server
Other
168 stars 22 forks source link
android file ios java javascript js mobile native objective-c react reactjs server static

React Native Static Server

Latest NPM Release NPM Downloads CircleCI GitHub Repo stars Dr. Pogodin Studio

Embedded HTTP server for React Native applications for Android, iOS, Mac (Catalyst), and Windows platforms. Powered by Lighttpd server, supports both new and old RN architectures.

Sponsor

Sponsored By:

Luna4 Enterprises Inc. Luna4 Enterprises Inc. — The support for Windows and Mac Catalyst systems would not happen any time soon without their interest and financial contribution.
Integreat App Integreat — Kindly sponsored the support of older Android SDKs (23–27).

Contributors

Content

Getting Started

Bundling-in Server Assets Into an App Statically

The assets to be served by the server may come to the target device in different ways, for example, they may be generated during the app's runtime, or downloaded to the device by the app from a remote location. They also may be statically bundled-in into the app's bundle at the build time, and it is this option covered in this section.

Let's assume the assets to be served by the server are located in the app's codebase inside the folder assets/webroot (the path relative to the codebase root), outside android, ios, and windows project folders, as we presumably want to reuse the same assets in both projects, thus it makes sense to keep them outside platform-specific sub-folders.

Enabling Alias Module

Lighttpd module mod_alias is used to specify a special document root for a given url-subset. To enable it just use extraConfig option of Server to load and configure it, for example:

extraConfig: `
  server.modules += ("mod_alias")
  alias.url = ("/sample/url" => "/special/root/path")
`,

Enabling Rewrite Module

Lighttpd's module mod_rewrite can be used for interal redirects, URL rewrites by the server. To enable it just use extraConfig option of Server to load and configure it, for example:

extraConfig: `
  server.modules += ("mod_rewrite")
  url.rewrite-once = ("/some/path/(.*)" => "/$1")
`,

// With such configuration, for example, a request
// GET "/some/path/file"
// will be redirected to
// GET "/file"

Enabling SetEnv Module

Lighttpd's built-in module mod_setenv allows to modify request and response headers. To enable it just use extraConfig option of Server to load and configure it, for example:

extraConfig: `
  server.modules += ("mod_setenv")
  setenv.add-response-header = (
    "My-Custom-Header" => "my-custom-value"
    "Another-Custom-Header" => "another-custom-value"
  )
  setenv.add-request-header = ("X-Proxy" => "my.server.name")
`,

Enabling WebDAV Module

Lighttpd's optional module mod_webdav provides WebDAV — a set of HTTP extensions that provides a framework allowing to create, change, and move documents on a server — essentially an easy way to enable POST, PUT, etc. functionality for selected routes.

BEWARE: As of now, props and locks are not supported.

BEWARE: _If you have set up the server to serve static assets bundled into the app, the chances are your server works with a readonly location on most platforms (in the case of Android it is anyway necessary to unpack bundled assets to the regular filesystem, thus there the server might be serving from a writeable location already). The easiest way around it is to use mod_alias to point URLs configured for mod_webdav to a writeable filesystem location, different from that of the served static assets._

To enable mod_webdav in the library you need (1) configure your host RN app to build Lighttpd with mod_webdav included; (2) opt-in to use it for selected routes when you create Server instance, using extraConfig option.

  1. Android: Edit android/gradle.properties file of your app, adding this flag in there:

    ReactNativeStaticServer_webdav = true

    iOS: Use environment variable RN_STATIC_SERVER_WEBDAV=1 when installing or updating the pods (i.e. when doing pod install or pod update).

    macOS (Catalyst): The same as for iOS.

    Windows: Does not require a special setup — the pre-compiled DLL for WebDAV module is always packed with the library, and loaded if opted for by Server's constructor().

  2. Use extraConfig option of Server's constructor() to load mod_webdav and use it for selected routes of the created server instance, for example:

    extraConfig: `
      server.modules += ("mod_webdav")
      $HTTP["url"] =~ "^/dav/($|/)" {
        webdav.activate = "enable"
      }
    `,

Connecting to an Active Server in the Native Layer

When this library is used the regular way, the Lighttpd server in the native layer is launched when the .start() method of a Server instance is triggered on the JavaScript (TypeScript) side, and the native server is terminated when the .stop() method is called on the JS side. In the JS layer we hold most of the server-related information (hostname, port, fileDir, etc.), and take care of the high-level server control (i.e. the optional pause / resume of the server when the app enters background / foreground). If JS engine is restarted (or just related JS modules are reloaded) the regular course of action is to explictly terminate the active server just before it, and to re-create, and re-launch it afterwards. If it is not done, the Lighttpd server will remain active in the native layer across the JS engine restart, and it won't be possible to launch a new server instance after the restart, as the library only supports at most one active Lighttpd server, and it throws an error if the server launch command arrives to the native layer while Lighttpd server is already active.

However, in response to the ticket #95 we provide a way to reuse an active native server across JS engine restarts, without restarting the server. To do so you:

Note, this way it is possible to create multiple Server instances connected to the same active native server. As they have the same id, they all will represent the same server, thus calling .stop() and .start() commands on any of them will operate the same server, and update the states of all these JS server instances, without triggering the error related to the «at most one active server a time» (though, it has not been carefully tested yet).

API Reference

Server

import Server from '@dr.pogodin/react-native-static-server';

The Server class represents individual server instances.

BEWARE: At most one server instance can be active within an app at the same time. Attempts to start a new server instance will result in the crash of that new instance. That means, although you may have multiple instances of Server class created, you should not call .start() method of an instance unless all other server instances are stopped. You may use getActiveServer() function to check if there is any active server instance in the app, including a starting or stopping instance.

constructor()

const server = new Server(options: object);

Creates a new, inactive server instance. The following settings are supported within options argument:

.addStateListener()

server.addStateListener(listener: StateListener): Unsubscribe;

// where StateListener and Unsubscribe signatures are:
type StateListener = (state: string, details: string, error?: Error) => void;
type UnsubscribeFunction = () => void;

Adds given state listener to the server instance. The listener will be called each time the server state changes, with the following arguments:

This method returns "unsubscribe" function, call it to remove added listener from the server instance.

.removeAllStateListeners()

server.removeAllStateListeners()

Removes all state listeners connected to the server instance.

.removeStateListener()

server.removeStateListener(listener: StateListener)

Removes given state listener if it is connected to the server instance; does nothing otherwise.

.start()

server.start(details?: string): Promise<string>

Starts Server instance. It returns a Promise, which resolves to the server's origin once the server reaches ACTIVE state, thus becomes ready to handle requests. The promise rejects in case of start failure, i.e. if server ends up in the CRASHED state before becoming ACTIVE.

This method is safe to call no matter the current state of this server instance. If it is ACTIVE, the method just resolves to origin right away; if CRASHED, it attempts a new start of the server; otherwise (STARTING or STOPPING), it will wait until the server reaches one of resulting states (ACTIVE, CRASHED, or INACTIVE), then acts accordingly.

The optional details argument, if provided, will be added to the STARTING message emitted to the server state change listeners (see .addStateListener()) in the beginning of this method, if the server launch is necessary.

BEWARE: With the current library version, at most one server instance can be active within an app at any time. Calling .start() when another server instance is running will result in the start failure and CRASHED state. See also getActiveServer().

.stop()

server.stop(details?: string): Promise<>

Gracefully shuts down the server. It returns a Promise which resolve once the server is shut down, i.e. reached INACTIVE state. The promise rejects if an error happens during shutdown, and server ends up in the CRASHED state.

If server was created with pauseInBackground option, calling .stop() will also ensure that the stopped server won't be restarted when the app re-enters foreground. Once stopped, the server only can be re-launched by an explicit call of .start().

It is safe to call this method no matter the current state of this server. If it is INACTIVE or CRASHED, the call will just cancel automatic restart of the server, if one is scheduled by pauseInBackground option, as mentioned above. If it is STARTING or STOPPING, this method will wait till server reaching another state (ACTIVE, INACTIVE or CRASHED), then it will act accordingly.

The optional details argument, if provided, will be added to the STARTING message emitted to the server state change listeners (see .addStateListener()) in the beginning of this method, if the server launch is necessary.

.errorLog

server.errorLog: false | ErrorLogOptions;

Readonly property. It holds the error log configuration (see ErrorLogOptions), opted for at the time of this server instance construction. Note, it will be {} if errorLog option of constructor() was set true; and it will be false (default) if errorLog option was omitted in the constructor() call.

.fileDir

server.fileDir: string;

Readonly property. It holds fileDir value — the absolute path on target device from which static assets are served by the server.

.hostname

server.hostname: string;

Readonly property. It holds the hostname used by the server. If no hostname value was provided to the server's constructor(), this property will be:

.id

server.id: number;

Readonly. It holds unique ID number of the server instance, which is used internally for communication between JS an native layers, and also exposed to the library consumer, for debug.

BEWARE: In the current library implementation, this ID is generated simply as Date.now() % 65535, which is not random, and not truly unique — the ID collision probability across different server instances is high. This should be fine as long as you don't create many server instances in your app, and don't rely on the uniqueness of these IDs across different app launches. Switching to real UUIDs is on radar, but not the highest priority for now.

.nonLocal

server.nonLocal: boolean;

Readonly property. It holds nonLocal value provided to server constructor().

.origin

server.origin: string;

Readonly property. It holds server origin. Initially it equals empty string, and after the first launch of server instance it becomes equal to its origin, i.e. "http://HOSTNAME:PORT", where HOSTNAME and PORT are selected hostname and port, also accessible via .hostname and .port properties.

.port

server.port: number;

Readonly property. It holds the port used by the server. Initially it equals the port value provided to constructor(), or 0 (default value), if it was not provided. If it is 0, it will change to the automatically selected port number once the server is started the first time. The selected port number does not change upon subsequent re-starts of the server.

.state

server.state: STATES;

Readonly property. It holds current server state, which is one of STATES values. Use .addStateListener() method to watch for server state changes.

.stopInBackground

server.stopInBackground: boolean;

Readonly property. It holds stopInBackground value provided to constructor().

extractBundledAssets()

DEPRECATED! Use instead copyFileAssets() from the @dr.pogodin/react-native-fs library v2.24.1 and above — it does the same job in a more efficient way (it is implemented entirely in the native layer, thus does not incur the overhead of recurrent communication between the native and JS layers during the operation).

The extractBundledAssets(), with its original implementation, will be kept around for backward compatibility, but it will be removed in future!

import {extractBundledAssets} from '@dr.pogodin/react-native-static-server';

extractBundledAssets(into, from): Promise<>;

Extracts bundled assets into the specified regular folder, preserving asset folder structure, and overwriting any conflicting files in the destination.

This is an Android-specific function; it does nothing on other platforms.

Arguments

Returns Promise which resolves once the extraction is completed.

getActiveServer()

import {getActiveServer} from '@dr.pogodin/react-native-static-server';

getActiveServer(): Server | undefined;

Returns currently active, starting, or stopping Server instance, if any exist in the app. It does not return, however, any inactive server instance which has been stopped automatically because of stopInBackground option, when the app entered background, and might be automatically started in future if the app enters foreground again prior to an explicit .stop() call for that instance.

NOTE: The result of this function is based on the TypeScript layer data (that's why it is synchronous), in contrast to the getActiveServerId() function below, which calls into the Native layer, and returns ID of the active server based on that.

getActiveServerId()

import {getActiveServerId} from '@dr.pogodin/react-native-static-server';

getActiveServerId(): Promise<number | null>;

Returns ID of the currently active, starting, or stopping server instance, if any exist in the Native layer.

This function is provided in response to the ticket #95, to allow «Connecting to an Active Server in the Native Layer». The ID returned by this function can be passed in into Server instance constructor() to create server instance communicating to the existing native layer server.

NOTE: It is different from getActiveServer() function above, which returns the acurrently active, starting, or stopping Server instance based on TypeScript layer data.

resolveAssetsPath()

import {resolveAssetsPath} from '@dr.pogodin/react-native-static-server';

resolveAssetsPath(path: string): string;

If given path is relative, it returns the corresponding absolute path, resolved relative to the platform-specific base location (document folder on Android; or main bundle folder on other platforms) for bundled assets; otherwise, it just returns given absolute path as is.

In other words, it exposes the same path resolution logic used by Server's constructor() for relative values of its fileDir argument.

Arguments

Returns string — The corresponding absolute path.

ERROR_LOG_FILE

import {ERROR_LOG_FILE} from '@dr.pogodin/react-native-static-server';

Constant string. It holds the filesystem location of the error log file (see errorLog option of Server's constructor()). The actual value is "WORK_DIR/errorlog.txt" — all server instances within an app output their logs, when opted, into the same file; and it is up to the host app to purge this file when needed.

STATES

import {STATES} from '@dr.pogodin/react-native-static-server';

The STATES enumerator provides possible states of a server instance:

It also contains the backward mapping between state numeric values and their human-readable names used above, i.e.

console.log(STATES.ACTIVE); // Logs: 0
console.log(STATES[0]);     // Logs: ACTIVE

UPLOADS_DIR

import {UPLOADS_DIR} from '@dr.pogodin/react-native-static-server';

Constant string. It holds the filesystem location where all server instances within an app keep any uploads to the server. The actual value is "WORK_DIR/uploads".

WORK_DIR

import {WORK_DIR} from '@dr.pogodin/react-native-static-server';

Constant string. It holds the filesystem location where all server instances within an app keep their working files (configs, logs, uploads). The actual value is "TemporaryDirectoryPath/__rn-static-server__", where TemporaryDirectoryPath is the temporary directory path for the app as reported by the @dr.pogodin/react-native-fs library.

ErrorLogOptions

import {type ErrorLogOptions} from '@dr.pogodin/react-native-static-server';

The type of errorLog option of the Server's constructor(). It describes an object with the following optional boolean flags; each of them enables the similarly named Lighttpd debug option:

Without any flag set the server instance will still output very basic state and error messages into the log file.