From 77abc6ea4b5e5894c66370cab0adf3715856cbc8 Mon Sep 17 00:00:00 2001 From: andresvcc <43545168+andresvcc@users.noreply.github.com> Date: Fri, 10 May 2024 08:43:55 +0200 Subject: [PATCH] docs(thymio-suite-mobile): add documentation to TDM discovery services --- .../src/main/assets/webResources.stories.mdx | 86 ++ .../TdmDIscovery/tdmDiscovery.stories.mdx | 105 ++ .../src/vpl3/vpl-android.stories.mdx | 2 +- .../assets/webroot/static-server.stories.mdx | 959 ++++++++++++++++++ 4 files changed, 1151 insertions(+), 1 deletion(-) create mode 100644 apps/thymio-suite-mobile-android/android/app/src/main/assets/webResources.stories.mdx create mode 100644 apps/thymio-suite-mobile-android/src/packages/TdmDIscovery/tdmDiscovery.stories.mdx diff --git a/apps/thymio-suite-mobile-android/android/app/src/main/assets/webResources.stories.mdx b/apps/thymio-suite-mobile-android/android/app/src/main/assets/webResources.stories.mdx new file mode 100644 index 0000000..a3b6f2f --- /dev/null +++ b/apps/thymio-suite-mobile-android/android/app/src/main/assets/webResources.stories.mdx @@ -0,0 +1,86 @@ +import { Meta, Description } from '@storybook/addon-docs' + + + +# Accessing and Storing Web Resources +This documentation provides a concise overview of how to store and access web resources in a React Native application for Android, using the `file:///android_asset/` URL scheme. We will cover the internal workings of Android in this context, the method for using this feature in a high-level approach, and the mechanics of loading these files into a WebView. + +In Android, the `file:///android_asset/` URL provides a way to access files bundled within an app's APK. These files are placed in the `assets/` directory during app development, and they are read-only at runtime. The asset mechanism is used to bundle raw resources, such as HTML files, scripts, and images, which are not compiled into the application. + +### Internal Workflow + +#### 1. File Placement +During the development of an Android application, specifically when preparing to use React Native or native Android components that require local resources, developers need to manage their file organization meticulously. Here's how the file placement process typically unfolds: + +- **Directory Structure:** The `assets/` directory is a special folder in the Android project structure. It is located at `src/main/assets/` in the standard Android project hierarchy. This directory is not subjected to any processing by the Android build system, such as file compression, which preserves the original file format and structure. + +- **Types of Files:** Any file type can be placed in this directory, including HTML, JavaScript, CSS, images, and plain text files. It’s essential for developers to ensure that these files are organized in a way that mirrors how they will be referenced in the app, as the structure of folders and subfolders within the `assets/` directory will be maintained in the APK. + +- **Development Practices:** While developing, it's a common practice to regularly update and manage the files in the `assets/` directory as the application evolves. Changes to these files during development are reflected in the APK each time the application is built and packaged. + +#### 2. APK Packaging +When the application is built—either for testing or for release—the contents of the `assets/` directory are bundled into the APK without modification. The APK (Android Package) is essentially a ZIP file that Android devices use to install the application. Here’s what happens during this phase: + +- **Build Process:** During the build process, tools like Android Studio or the Gradle build system (used in Android development) take the files from the `assets/` directory and package them into the APK under a specific path that directly mirrors the original `assets/` directory's structure. + +- **Compression and Storage:** Unlike the `res/` directory, where resources are processed and optimized by the Android build system, files in the `assets/` directory are not compressed; they are stored in their original form. This is crucial for certain types of files that need to be read exactly as they are, such as custom fonts or script files. + +- **Accessibility:** Once packaged, these files become part of the application's APK and are accessible to the Android operating system and apps installed on the device, albeit in a read-only format. + +#### 3. Runtime Access +At runtime, when the application is running on an Android device, the assets packaged in the APK can be accessed via the `file:///android_asset/` URL. This phase involves the following aspects: + +- **URL Scheme:** The `file:///android_asset/` URL scheme is a special file path used in Android to refer to the contents of the `assets/` directory. This path is recognized by the Android system and allows applications to load content directly from the APK. + +- **WebView and Other Components:** Components like WebView, which can display web content, use this URL scheme to reference web resources (HTML, CSS, JavaScript) stored within the `assets/` directory. For example, loading an HTML file into a WebView component would involve setting the source to `file:///android_asset/path/to/file.html`. + +- **Security and Performance:** Accessing files from the APK is secure and efficient. Files are read directly from the device's storage and not over the internet, which not only enhances the app's performance by reducing load times but also increases security by minimizing exposure to remote exploits. + +## Interfacing with Native Android Code +Native Modules: React Native uses native modules to perform tasks that require direct interaction with the device's API, such as accessing the file:///android_asset/ directory. These modules are written in Java (or Kotlin) and are registered with React Native's native module system. +Bridging: The React Native bridge is responsible for communicating between the JavaScript environment and the native environment. When a React Native application needs to load an asset through the file:///android_asset/ URL, it invokes a native module method that handles this request. This method executes on the native side, interacting directly with Android's APIs. +Method Invocation: To access assets, a native module method is defined in Java, which uses Android's AssetManager to fetch the file from the assets/ directory. This method can be exposed to JavaScript using React Native's @ReactMethod annotation, allowing it to be called from your JavaScript code. + + +### Steps to Use: +- **Prepare Assets:** Place your HTML files, JavaScript, CSS, and images in the `assets/` directory of your Android project. +- **Load in WebView:** Use the WebView component from `react-native-webview` to load the URL pointing to an asset, e.g., `file:///android_asset/myfile.html`. + +## Loading Files in WebView +The WebView component in React Native is used to embed web pages inside your app. To load local assets in the WebView, follow these steps: + +### Example Code +```javascript +import React from 'react'; +import { WebView } from 'react-native-webview'; + +const LocalWebContent = () => { + return ( + + ); +}; + +export default LocalWebContent; +``` + +### Considerations +- **Relative Pathing:** Ensure that any resources linked from your HTML file (like images or scripts) use correct relative paths that reference the `assets/` directory. +- **MIME Types:** Android WebView may require correct MIME types to be set for certain file types to load properly. + +## Implications in React Native +Using `file:///android_asset/` in React Native has several implications: + +- **Performance:** Loading files from local assets can be faster than over the network, improving the performance of the WebView component. +- **Security:** It isolates web content from the network, enhancing security by preventing external attacks through web content. +- **Offline Access:** Enables your application to function offline, as the assets are packaged within the APK. diff --git a/apps/thymio-suite-mobile-android/src/packages/TdmDIscovery/tdmDiscovery.stories.mdx b/apps/thymio-suite-mobile-android/src/packages/TdmDIscovery/tdmDiscovery.stories.mdx new file mode 100644 index 0000000..bcbcbaa --- /dev/null +++ b/apps/thymio-suite-mobile-android/src/packages/TdmDIscovery/tdmDiscovery.stories.mdx @@ -0,0 +1,105 @@ +import { Meta, Description } from '@storybook/addon-docs' + + + +# Documentation for the TDM Discovery Module + +This documentation provides a detailed explanation of the TDM Discovery module, which is utilized in the discovery and communication of Thymio devices with iOS and Android tablets via network services. + +## Overview + +The TDM Discovery module leverages Zeroconf (also known as Bonjour) network technology to discover, resolve, and manage network services advertised by Thymio devices. This enables seamless communication between Thymio robots and controlling tablets without requiring manual network configuration. + +## Key Objects and Methods + +### `TdmDiscovery` + +This class encapsulates the logic for discovering Thymio devices using Zeroconf technology. It provides methods to start and stop discovery, manage service updates, and handle network statuses. + +#### Methods + +| Method | Parameters | Description | Return Type | +|-------------------|-------------------------------------------|--------------------------------------------------------------|--------------------| +| `constructor` | `nativeZeroConf: Zeroconf \| any` | Initializes a new TdmDiscovery instance with Zeroconf. | None | +| `onChange` | `fun: (services: {[key: string]: MobsyaService}) => void` | Sets a callback to receive updates when new services are found or updated. | None | +| `onStatusChange` | `fun: (status: StatusZeroConf) => void` | Sets a callback to receive updates on the discovery status. | None | +| `close` | None | Stops the ongoing discovery process and cleans up resources. | None | +| `scan` | None | Initiates the discovery process to find Thymio devices. | None | + +### `Zeroconf` + +This class provides the foundational network capabilities required for the discovery and management of network services in a local area network. + +#### Methods + +| Method | Parameters | Description | Return Type | +|-------------------|---------------------------------------------------------------|--------------------------------------------------------------|--------------------| +| `constructor` | `props: any` | Initializes a new Zeroconf instance. | None | +| `addDeviceListeners` | None | Registers event listeners for Zeroconf network events. | None | +| `removeDeviceListeners` | None | Removes all registered event listeners. | None | +| `scan` | `type: string, protocol: string, domain: string, implType: string` | Starts scanning for Zeroconf services based on specified parameters. | None | +| `stop` | `implType: string` | Stops the current scanning operation. | None | +| `publishService` | `type, protocol, domain, name, port, txt, implType` | Publishes a service that can be discovered by others. | None | +| `unpublishService`| `name, implType` | Stops publishing a previously registered service. | None | +| `getServices` | None | Retrieves a list of all resolved services. | `Array of services`| + +## Integration in the Context of Thymio and Tablet Communication + +In the context of Thymio robots and tablet communication, the TDM Discovery module serves as a crucial component for auto-discovery and connectivity. Here’s how it integrates: + +1. **Device Discovery:** When the TdmDiscovery's `scan` method is initiated from a tablet, it uses the underlying Zeroconf technology to listen for Thymio devices broadcasting their availability on the network. + +2. **Service Handling:** As Thymio devices are found and their services resolved, the `TdmDiscovery` class updates its internal state with these services, accessible via callbacks set through `onChange`. + +3. **Status Monitoring:** The discovery process's status is monitored through `onStatusChange`, enabling the tablet application to provide user feedback (e.g., scanning, connected, or errors). + +4. **Service Resolution:** The `Zeroconf` class processes and resolves the details of each Thymio service, ensuring that the tablet can establish a robust communication link with correct network parameters (IP address, port, etc.). + +## Event-Driven Communication and State Management in Zeroconf + +Zeroconf, a protocol designed to enable automatic discovery of computers, devices, and services on IP networks, employs an event-driven architecture. This approach simplifies the process of integrating devices into networks without requiring manual network configuration. Here’s a detailed exploration of how Zeroconf manages its state and data through event-oriented communication, particularly in the context of the TDM Discovery module which uses Zeroconf for discovering Thymio devices. + +### Core Concepts + +**Event-Driven Architecture**: In Zeroconf, operations are primarily reactive; they respond to network events such as the appearance of new services, changes in the network status, or errors. This model is highly effective for network communication where state changes are frequent and unpredictable. + +**State Management**: Zeroconf maintains a dynamic representation of the network state, tracking available services and their statuses. This state is updated based on the events triggered by the network environment. + +### Key Events and States in Zeroconf + +Zeroconf revolves around several key events that trigger state changes: + +1. **Start**: The discovery process begins, indicating that the system is actively scanning the network for services. +2. **Found**: A service is detected on the network. This event triggers an update in the internal state to include the new service. +3. **Resolved**: Additional details of a found service are resolved, such as its network address and port. This step is crucial for enabling actual communication with the service. +4. **Update**: The state or details of a service change, necessitating an update in the internal representation. +5. **Stop**: The discovery process is halted, either due to manual intervention or network conditions. +6. **Error**: An error occurs during the discovery process, affecting the current operation or the overall network state. + +### Mechanism of Event Handling and State Updates + +**Event Listeners**: Zeroconf employs listeners for each event type. These listeners are functions that execute in response to their respective events. For example: +- When the `found` event is triggered, the listener function updates the internal service registry with the new service. +- The `resolved` event's listener adds resolved details to the already registered service, such as IP addresses and ports. + +**State Transitions**: The transition between different states in Zeroconf is governed by the outcomes of these event responses. For instance: +- On receiving the `start` event, the state transitions to "scanning". +- On a `stop` event, the state changes to "stopped". +- An `error` event may move the system into an "error" state, prompting either a retry or a halt, depending on the error's nature. + +**Callback Functions**: Developers can hook into these events by registering callbacks. These callbacks allow external modules, like the TDM Discovery module, to react appropriately to changes in the network state. For example, updating a UI element to reflect the discovery status or handling the connectivity logic to communicate with a Thymio device. + +### Practical Implementation in TDM Discovery + +In the TDM Discovery module, the Zeroconf mechanism is encapsulated within a class that manages both the discovery and the communication setup with Thymio devices. Here’s how it integrates Zeroconf: +- **Initialization**: Upon instantiation, the `TdmDiscovery` class configures Zeroconf with necessary listeners for all relevant events. +- **Discovery Process**: When the `scan` method is called, it triggers Zeroconf to start the network scan, handling the `start`, `found`, and `resolved` events as they occur. +- **State Management**: The internal state of `TdmDiscovery` is updated based on these events, maintaining a current view of all Thymio devices available and their connectivity details. diff --git a/apps/thymio-suite-mobile-android/src/vpl3/vpl-android.stories.mdx b/apps/thymio-suite-mobile-android/src/vpl3/vpl-android.stories.mdx index d5ffbc9..bb99bd7 100644 --- a/apps/thymio-suite-mobile-android/src/vpl3/vpl-android.stories.mdx +++ b/apps/thymio-suite-mobile-android/src/vpl3/vpl-android.stories.mdx @@ -23,7 +23,7 @@ To integrate the VPL3 web interface into the Android mobile application, an unde To construct this URL, necessary data must first be acquired from the 'scanner' page, which can be accessed using the following URL pattern: ```javascript -http://127.0.0.1:3000/scanner/index.html?data=${ +file:///android_asset/scanner/index.html?data=${ JSON.stringify({ ...LTServices })}&gl=${JSON.stringify( diff --git a/apps/thymio-suite-mobile-ios/assets/webroot/static-server.stories.mdx b/apps/thymio-suite-mobile-ios/assets/webroot/static-server.stories.mdx index 7e765bb..98ada56 100644 --- a/apps/thymio-suite-mobile-ios/assets/webroot/static-server.stories.mdx +++ b/apps/thymio-suite-mobile-ios/assets/webroot/static-server.stories.mdx @@ -156,3 +156,962 @@ useEffect(() => { } }, 500); ``` + +- [Getting Started](#getting-started) + - [Bundling-in Server Assets Into an App Statically] + - [Enabling Alias module] + - [Enabling Rewrite module] + - [Enabling SetEnv module] + - [Enabling WebDAV module] + - [Connecting to an Active Server in the Native Layer] +- [API Reference](#api-reference) + - [Server] — Represents a server instance. + - [constructor()] — Creates a new [Server] instance. + - [.addStateListener()] — Adds state listener to the server instance. + - [.removeAllStateListeners()] — Removes all state listeners from this + server instance. + - [.removeStateListener()] — Removes specified state listener from this + server instance. + - [.start()] — Launches the server. + - [.stop()] — Stops the server. + - [.errorLog] — Holds `errorLog` configuration. + - [.fileDir] — Holds absolute path to static assets on target device. + - [.hostname] — Holds the hostname used by server. + - [.id] — Hold unique ID of the server instance. + - [.nonLocal] — Holds `nonLocal` value provided to [constructor()]. + - [.origin] — Holds server origin. + - [.port] — Holds the port used by server. + - [.state] — Holds the current server state. + - [.stopInBackground] — Holds `stopInBackground` value provided to + [constructor()]. + - ~~[extractBundledAssets()] — Extracts bundled assets into a regular folder +(Android-specific).~~ + - [getActiveServer()] — Gets currently active, starting, or stopping + server instance, if any, according to the TS layer data. + - [getActiveServerId()] — Gets ID of the currently active, starting, or + stopping server instance, if any, according to the Native layer data. + - [resolveAssetsPath()] — Resolves relative paths for bundled assets. + - [ERROR_LOG_FILE] — Location of the error log file. + - [STATES] — Enumerates possible states of [Server] instance. + - [UPLOADS_DIR] — Location for uploads. + - [WORK_DIR] — Location of the working files. + - [ErrorLogOptions] — Options for error logging. + +## Getting Started +[Getting Started]: #getting-started + +[CMake]: https://cmake.org +[Homebrew]: https://brew.sh + +- **Note:** + + - This library's repository includes [Example App]. + Have a look, try to build it, in addition to following the instructions + below. + + - The following host / build platforms are not currently supported officially, + and they won't be unless the support is provided or sponsored by somebody: + - Building for Android target on Windows host + ([open issues](https://github.com/birdofpreyru/react-native-static-server/issues?q=is%3Aissue+is%3Aopen+label%3A%22Windows+-%3E+Android%22)). + Prefer building for Android on macOS or Ubuntu host. + + - [Expo] ([open issues](https://github.com/birdofpreyru/react-native-static-server/issues?q=is%3Aissue+is%3Aopen+label%3AExpo)). + + Though, presumably the library in its current state already works fine + with [Expo] — see [Issue#8] and [Expo Example App] by [jole141]. + + - [tvOS](https://developer.apple.com/tvos) ([open issues](https://github.com/birdofpreyru/react-native-static-server/issues?q=is%3Aissue+is%3Aopen+label%3AtvOS)). + + +- [CMake] is required on the build host. + + - When building for **Android**, [CMake] should be installed as a part of your + _Android SDK_ (open _SDK Manager_, and look for [CMake] within + the _SDK Tools_ tab). + + - On **MacOS**, the `pkg-config` dependency is also needed. You can install both via [Homebrew], + by executing: + ```shell + $ brew install cmake pkg-config + ``` + **IMPORTANT:** [Homebrew] should have added `eval "$(/opt/homebrew/bin/brew shellenv)"'` + command to your `.zshrc` or `.bashrc`. Although this works for interactive terminals, + it might not work for sessions inside of other apps, such as XCode, therefore you might need to + manually create symbolic links: + + ```shell + $ sudo ln -s $(which cmake) /usr/local/bin/cmake + $ sudo ln -s $(which pkg-config) /usr/local/bin/pkg-config + ``` + + For details read: https://earthly.dev/blog/homebrew-on-m1, + and [Issue#29](https://github.com/birdofpreyru/react-native-static-server/issues/29). + +- Install the package and its peer dependencies: + ```shell + npx install-peerdeps @dr.pogodin/react-native-static-server + ``` + **Note:** _In case you prefer to install this library from its source code + (i.e. directly from its GitHub repo, or a local folder), mind that it depends + on several Git sub-modules, which should be clonned and checked out by this + command in the library's codebase root: + `$ git submodule update --init --recursive`. Released NPM packages of + the library have correct versions of the code from these sub-modules bundled + into the package, thus no need to clone and check them out after installation + from NPM._ + +- For **Android**: + - In the `build.gradle` file set `minSdkVersion` equal `28` + ([SDK 28 — Android 9](https://developer.android.com/studio/releases/platforms#9.0), + released in August 2018), or larger. \ + **Note:** _Support of older SDKs is technically possible, but it is not + a priority now._ + + - Android SDK 28 and above + [forbids Cleartext / Plaintext HTTP](https://developer.android.com/privacy-and-security/risks/cleartext) + by default. Thus, to access locally running server over HTTP from within + your app, you should either allow all uses of HTTP in your app by adding + `android:usesCleartextTraffic="true"` attribute to `` element + in the `AndroidManifest.xml` + ([see how it is done in the example app](https://github.com/birdofpreyru/react-native-static-server/blob/master/example/android/app/src/main/AndroidManifest.xml)); + or alternatively you should use + [network security configuration](https://developer.android.com/privacy-and-security/security-config) + to permit cleartext HTTP for selected domains only. + +- For **iOS**: + - After installing the package, enter `ios` folder of the app's codebase + and execute + ```shell + $ pod install + ``` + +- For [Expo]: + + Presumably, it works with some additional setup (see [Issue#8] and + [Expo Example App] by [jole141]; though it is not officially supported + (tested) for new releases. + +- For **Mac Catalyst**: + - Disable Flipper in your app's Podfile. + - Add _Incoming Connections (Server)_ entitlement to the _App Sandbox_ + (`com.apple.security.network.server` entitlement). + - If you bundle inside your app the assets to serve by the server, + keep in mind that in Mac Catalyst build they'll end up in a different + path, compared to the regular iOS bundle (see [Example App]): \ + iOS: "[MainBundlePath]`/webroot`"; \ + Mac Catalyst: "[MainBundlePath]`/Content/Resources/webroot`". + + Also keep in mind that `Platform.OS` value equals "`iOS`" both for the normal + iOS and for the Mac Catalyst builds, and you should use different methods + to distinguish them; for example relying on [getDeviceType()] method of + [react-native-device-info] library, which returns 'Desktop' in case of + Catalyst build. + +- For **Windows**: + - Add _Internet (Client & Server)_, _Internet (Client)_, + and _Private Networks (Client & Server)_ capabilities to your app. + + NOTE: _It seems, the core server functionality is able to work without these + capabilities, however some functions might be affected, and the error reporting + in the current Windows implementation probably won't make it clear that something + failed due to the lack of declared capabilities._ + +- Create and run a server instance: + + ```jsx + import { useEffect, useState } from 'react'; + import { Text, View } from 'react-native'; + import Server from '@dr.pogodin/react-native-static-server'; + + // We assume no more than one instance of this component is mounted in the App + // at any given time; otherwise some additional logic will be needed to ensure + // no more than a single server instance can be launched at a time. + // + // Also, keep in mind, the call to "server.stop()" without "await" does not + // guarantee that the server has shut down immediately after that call, thus + // if such component is unmounted and immediately re-mounted again, the new + // server instance may fail to launch because of it. + export default function ExampleComponent() { + const [origin, setOrigin] = useState(''); + + useEffect(() => { + let server = new Server({ + // See further in the docs how to statically bundle assets into the App, + // alernatively assets to serve might be created or downloaded during + // the app's runtime. + fileDir: '/path/to/static/assets/on/target/device', + }); + (async () => { + // You can do additional async preparations here; e.g. on Android + // it is a good place to extract bundled assets into an accessible + // location. + + // Note, on unmount this hook resets "server" variable to "undefined", + // thus if "undefined" the hook has unmounted while we were doing + // async operations above, and we don't need to launch + // the server anymore. + if (server) setOrigin(await server.start()); + })(); + + return () => { + setOrigin(''); + + // No harm to trigger .stop() even if server has not been launched yet. + server.stop(); + + server = undefined; + } + }, []); + + return ( + + Hello World! + Server is up at: {origin} + + ); + } + ``` + +### Bundling-in Server Assets Into an App Statically +[Bundling-in Server Assets Into an App Statically]: #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. + +- **Android** + - Inside `android/app/build.gradle` file look for `android.sourceSets` + section, or create one if it does no exist. To bundle-in our assets for + server, it should look like this (note, this way we'll also bundle-in all + other content of our `assets` folder, if there is anything beside `webroot` + subfolder). + ```gradle + android { + sourceSets { + main { + assets.srcDirs = [ + '../../assets' + // This array may contain additional asset folders to bundle-in. + // Paths in this array are relative to "build.gradle" file, and + // should be comma-separated. + ] + } + } + // ... Other stuff. + } + ``` + - On Android the server cannot access bundled assets as regular files, thus + before starting the server to serve them, these assets should be extracted + into a folder accessible to the server (_e.g._ app's document folder). + You can use [copyFileAssets()] function from [@dr.pogodin/react-native-fs] + library (v2.24.1 and above): + ```jsx + // TODO: To be updated, see a better code inside the example app. + + import { Platform } from 'react-native'; + + import { + copyFileAssets, + DocumentDirectoryPath, + exists, + resolveAssetsPath, + unlink, + } from '@dr.pogodin/react-native-fs'; + + async function prepareAssets() { + if (Platform.OS !== 'android') return; + + const targetWebrootPathOnDevice = resolveAssetsPath('webroot'); + + // It is use-case specific, but in general if target webroot path exists + // on the device, probably these assets have been extracted in a previous + // app launch, and there is no need to extract them again. However, in most + // locations these extracted files won't be delected automatically on + // the apps's update, thus you'll need to check it and act accordingly, + // which is abstracted as needsOverwrite() function in the condition. + const alreadyExtracted = await exists(targetWebrootPathOnDevice); + + // TODO: Give an example of needsOverwrite(), relying on app version + // stored in local files. Maybe we should provide with the library + // an utility function which writes to disk a version fingerprint of + // the app, thus allowing to detect app updates. For now, have + // a look at the example project in the repo, which demonstrates more + // realistic code. + if (!alreadyExtracted || needsOverwrite()) { + // TODO: Careful here, as on platforms different from Android we do not + // need to extract assets, we also should not remove them, thus we need + // a guard when entering this clean-up / re-extract block. + if (alreadyExtracted) await unlink(targetWebrootPathOnDevice); + + // This function is a noop on other platforms than Android, thus no need + // to guard against the platform. + await copyFileAssets('webroot', targetWebrootPathOnDevice); + } + + // "webroot" assets have been extracted into the target folder, which now + // can be served by the server. + } + ``` + +- **iOS** + - Open you project's workspace in XCode. + + - In the «_Project Navigator_» panel right-click on the project + name and select «_Add Files to "YOUR-PROJECT-NAME"..._» + (alternatively, you can find this option in the XCode head menu under _Files + > Add Files to "YOUR-PROJECT-NAME"..._). + + - In the opened menu do: + - Uncheck «_Copy items if needed_»; + - Select «_Create folder references_» + for «_Added folders_» switch; + - Select our `webroot` folder within the file system view; + - Press «_Add_» button to add "webroot" assets + to the project target. + + Here is how the dialog & options look, just before pressing + «_Add_» button, when adding `assets/webroot` folder + to the Xcode project of our [Example App]. + ![Dialog screenshot](https://raw.githubusercontent.com/birdofpreyru/react-native-static-server/master/.README/ios-bundling-webroot-folder.png) + + - The absolute path of `webroot` folder on the device, when added this way, + can be obtained as [`resolveAssetsPath('webroot')`][resolveAssetsPath()]. + +- **Mac Catalyst** + - The bundling for iOS explained above also bundles assets for Mac Catalyst; + beware, however, the bundled assets end up in a slightly different location + inside the bundle in this case (see details earlier in the [Getting Started] + section). + +- **Windows** + - Edit `PropertySheet.props` file inside your app's + `windows/YOUR_PROJECT_NAME` folder, adding the following nodes into its root + `` element: + ```xml + + <_CustomResource Include="..\..\assets\webroot\**\*"> + webroot\%(RecursiveDir)%(FileName)%(Extension) + true + + + + + + + + + ``` + +### Enabling Alias Module +[Enabling Alias module]: #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] [constructor()] to load and configure it, for example: + +```ts +extraConfig: ` + server.modules += ("mod_alias") + alias.url = ("/sample/url" => "/special/root/path") +`, +``` + +### Enabling Rewrite Module +[Enabling Rewrite module]: #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] [constructor()] to load and configure it, for example: + +```ts +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 +[Enabling SetEnv module]: #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] [constructor()] +to load and configure it, for example: +```ts +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 +[Enabling WebDAV module]: #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][Enabling Alias module] 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: + ```gradle + 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: + ```ts + extraConfig: ` + server.modules += ("mod_webdav") + $HTTP["url"] =~ "^/dav/($|/)" { + webdav.activate = "enable" + } + `, + ``` + +### Connecting to an Active Server in the Native Layer +[Connecting to an Active Server in the Native Layer]: #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](https://github.com/birdofpreyru/react-native-static-server/issues/95) +we provide a way to reuse an active native server across JS engine restarts, +without restarting the server. To do so you: +- Use [getActiveServerId()] method to check whether the native server is active + (if so, this method resolves to a non-_null_ ID). +- Create a new [Server] instance passing into its [constructor()] that server ID + as the `id` option, and [STATES]`.ACTIVE` as the `state` option. These options + (usually omitted when creating a regular [Server] instance) ensure that + the created [Server] instance is able to communicate with the already running + native server, and to correctly handle subsequent [.stop()] and [.start()] + calls. Beside these, it is up-to-you to set all other options to the values + you need (_i.e._ setting `id`, and `state` just «connects» + the newly created [Server] instance to the active native server, but it + does not restore any other information about the server — you should + restore or redefine it the way you see fit). + +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 +[Server]: #server +```js +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() +[constructor()]: #constructor +```ts +const server = new Server(options: object); +``` +Creates a new, inactive server instance. The following settings are supported +within `options` argument: + +- `fileDir` — **string** — The root path on target device from where + static assets should be served. Relative paths (those not starting with `/`, + neither `file:///`) are automatically prepended by the platform-dependent + base path (document directory on Android, or main bundle directory on other + platforms; see [resolveAssetsPath()]); however, empty `fileDir` value + is forbidden — if you really want to serve all content from the base + directory, provide it its absolute path explicitly. + +- `errorLog` — **boolean** | [ErrorLogOptions] — Optional. + If set **true** (treated equivalent to `{}`) the server instance will + output basic server state and error logs from the Lighttpd native core + into the [ERROR_LOG_FILE]. Passing in an [ErrorLogOptions] object with + additional flags allows to add additional debug output from Lighttpd core + into the log file. Default value is **false**, in which case the server + instance only outputs basic server state and error logs into the OS + system log; note that enabling the file logging with this option disables + the logging into the system log. + + **BEWARE:** If you opt for file logging with this option, it is up to you + to control and purge the [ERROR_LOG_FILE] as needed. + +- `extraConfig` — **string** — Optional. If given, it should be + a valid piece of + [Lighttpd configuration](https://redmine.lighttpd.net/projects/lighttpd/wiki/Docs_Configuration), + and it will be appended to the base Lighttpd config generated by this + library according to the other server options. + +- `hostname` — **string** — Optional. Sets the address for server + to bind to. + - By default, when `nonLocal` option is **false**, `hostname` is set equal + "`127.0.0.1`" — the server binds to the loopback address, + and it is accessible only from within the host app. + - If `nonLocal` option is **true**, and `hostname` was not given, it is + initialized with empty string, and later assigned to a library-selected + non-local IP address, at the first launch of the server. + - If `hostname` value is provided, the server will bind to the given address, + and it will ignore `nonLocal` option. + + _NOTE: In future we'll deprecate `nonLocal` option, and instead will use + special `hostname` values to ask the library to automatically select + appropriate non-local address._ + +- `id` — **number** — Optional. Allows to enforce a specific ID, + used to communicate with the server instance within the Native layer, thus + it allows to re-connect to an existing native server instance. + See «[Connecting to an Active Server in the Native Layer]» + for details. By default, an `id` is selected by the library. + +- `nonLocal` — **boolean** — Optional. By default, if `hostname` + option was not provided, the server starts at the "`127.0.0.1`" (loopback) + address, and it is only accessible within the host app. + With this flag set **true** the server will be started on an IP address + also accessible from outside the app. + + _NOTE: When `hostname` option is provided, + the `nonLocal` option is ignored. The plan is to deprecate `nonLocal` option + in future, in favour of special `hostname` values supporting the current + `nonLocal` functionality._ + +- `port` — **number** — Optional. The port at which to start the server. + If 0 (default) an available port will be automatically selected. + +- `state` — [STATES] — Optional. Allows to enforce the initial + server state value, which is necessary [when connecting to an existing + native server instance][Connecting to an Active Server in the Native Layer]. + Note, it only influence the logic behind subsequent [.start()] and [.stop()] + calls, _i.e._ the constructor does not attempt to put the server in this + state, nor does it check the value is consistent with the active server, + if any, in the native layer. By default, the state is initialized + to `STATES.INACTIVE`. + +- `stopInBackground` — **boolean** — Optional. + + By default, the server continues to work as usual when its host app enters + the background / returns to the foreground (better say, by default, it does + not attempt anything special in these situations, but the host OS may kill or + restrict it in the background forcefully, depending on OS and app configs). + + With this flag set **true**, an active server will stop automatically each + time the app enters the background, and then automatically launch again each + time the app re-enters the foreground. Note that calling [.stop()] explicitly + will stop the server for good — no matter `stopInBackground` value, + once [.stop()] is called the server won't restart automatically unless you + explicitly [.start()] it again. + + To faciliate debugging, when a server starts / stops automatically because of + the `stopInBackground` option and app transitions between the foreground and + the background; the corresponding `STARTING` and `STOPPING` messages emitted + to the server state listeners (see [.addStateListener()]) will have their + `details` values set equal "_App entered background_", + and "_App entered foreground_" strings. + +- **DEPRECATED**: `webdav` — **string[]** — It still works, but it + will be removed in future versions. Instead of it use `extraConfig` option to + enable and configure [WebDAV] as necessary (see [Enabling WebDAV module]). + +#### .addStateListener() +[.addStateListener()]: #addstatelistener +```ts +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: +- `state` — **string** — The new state, one of [STATES] values. +- `details` — **string** — Additional details about the state change, + if any can be provided; an empty string otherwise. +- `error` — [Error] — If server instance crashes, this will be + the related error object; otherwise undefined. + +This method returns "unsubscribe" function, call it to remove added +listener from the server instance. + +#### .removeAllStateListeners() +[.removeAllStateListeners()]: #removeallstatelisteners +```ts +server.removeAllStateListeners() +``` +Removes all state listeners connected to the server instance. + +#### .removeStateListener() +[.removeStateListener()]: #removestatelistener +```ts +server.removeStateListener(listener: StateListener) +``` +Removes given state `listener` if it is connected to the server instance; +does nothing otherwise. + +#### .start() +[.start()]: #start +```ts +server.start(details?: string): Promise +``` +Starts [Server] instance. It returns a [Promise], which resolves +to the server's [origin][.origin] once the server reaches `ACTIVE` +[state][.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][.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() +[.stop()]: #stop +```ts +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](.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 +[.errorLog]: #errorlog +```ts +server.errorLog: false | ErrorLogOptions; +``` +Readonly property. It holds the error log configuration (see [ErrorLogOptions]), +opted for at the time of this server instance [construction][constructor()]. +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 +[.fileDir]: #filedir +```ts +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 +[.hostname]: #hostname +```ts +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: +- Without `nonLocal` option it will be equal `127.0.0.1` (the loopback address) + from the very beginning; +- Otherwise, it will be an empty string until the first launch of the server + instance, after which it will become equal to the IP address selected by + the server automatically, and won't change upon subsequent server re-starts. + +#### .id +[.id]: #id +```ts +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 +[.nonLocal]: #nonlocal +```ts +server.nonLocal: boolean; +``` +Readonly property. It holds `nonLocal` value provided to server [constructor()]. + +#### .origin +[.origin]: #origin +```ts +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 +[.port]: #port +```ts +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 +[.state]: #state +```ts +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 +[.stopInBackground]: #stopinbackground +```ts +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!_ + +[extractBundledAssets()]: #extractbundledassets +```ts +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** +- `into` — **string** — Optional. The destination folder for + extracted assets. By default assets are extracted into the app's document + folder. +- `from` — **string** — Optional. Relative path to the root asset + folder, starting from which all assets contained in that folder and its + sub-folders will be extracted into the destination folder, preserving asset + folder structure. By default all bundled assets are extracted. + +**Returns** [Promise] which resolves once the extraction is completed. + +### getActiveServer() +[getActiveServer()]: #getactiveserver +```ts +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() +[getActiveServerId()]: #getactiveserverid +```ts +import {getActiveServerId} from '@dr.pogodin/react-native-static-server'; + +getActiveServerId(): Promise; +``` +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](https://github.com/birdofpreyru/react-native-static-server/issues/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() +[resolveAssetsPath()]: #resolveassetspath +```ts +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** +- `path` — **string** — Absolute or relative path. + +Returns **string** — The corresponding absolute path. + +### ERROR_LOG_FILE +[ERROR_LOG_FILE]: #error_log_file +```ts +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 +[STATES]: #states +```js +import {STATES} from '@dr.pogodin/react-native-static-server'; +``` +The [STATES] enumerator provides possible states of a server instance: +- `STATES.ACTIVE` — Up and running. +- `STATES.CRASHED` — Crashed and inactive. +- `STATES.INACTIVE` — Yet not started, or gracefully shut down. +- `STATES.STARTING` — Starting up. +- `STATES.STOPPING` — Shutting down. + +It also contains the backward mapping between state numeric values and their +human-readable names used above, _i.e._ +```js +console.log(STATES.ACTIVE); // Logs: 0 +console.log(STATES[0]); // Logs: ACTIVE +``` + +### UPLOADS_DIR +[UPLOADS_DIR]: #uploads_dir +```ts +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 +[WORK_DIR]: #work_dir +```ts +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 +[ErrorLogOptions]: #errorlogoptions +```ts +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](https://redmine.lighttpd.net/projects/lighttpd/wiki/DebugVariables): +- `conditionHandling` — **boolean** — Optional. +- `fileNotFound` — **boolean** — Optional. +- `requestHandling` — **boolean** — Optional. +- `requestHeader` — **boolean** — Optional. +- `requestHeaderOnError` — **boolean** — Optional. +- `responseHeader` — **boolean** — Optional. +- `timeouts` — **boolean** — Optional. + +Without any flag set the server instance will still output very basic state +and error messages into the log file. \ No newline at end of file