diff --git a/CHANGELOG.md b/CHANGELOG.md index 2763d9e..17e3c3b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,25 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.0.0-beta.4] - 2023-08-25 + +_If you are upgrading: please see [`UPGRADING.md`](UPGRADING.md)._ + +### Added + +- Add a types package for the `bridge` module ([#6](https://github.com/hampoelz/Capacitor-NodeJS/issues/6)) +- Allow manual startup of the Node.js runtime, pass environment variables and arguments ([#8](https://github.com/hampoelz/Capacitor-NodeJS/issues/8)) + - Add `startMode` configuration + - Add `start()` method +- Add API to get a writeable data directory on each platform + +### Changed + +- **Breaking:** Remove return type of `send()` method +- Rewrite the `bridge` module in TypeScript ([#14](https://github.com/hampoelz/Capacitor-NodeJS/issues/14)) +- Start the Node.js runtime as child process on Electron ([#12](https://github.com/hampoelz/Capacitor-NodeJS/issues/12), [#15](https://github.com/hampoelz/Capacitor-NodeJS/issues/15)) +- Change loading mechanism of the `bridge` module to a built-in module ([#12](https://github.com/hampoelz/Capacitor-NodeJS/issues/12)) + ## [1.0.0-beta.3] - 2023-08-19 _If you are upgrading: please see [`UPGRADING.md`](UPGRADING.md)._ diff --git a/README.md b/README.md index a28b2ce..aa1f168 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,44 @@ # đą Capacitor NodeJS-Integration -⥠A full-fledged [Node.js](https://nodejs.org/) runtime for [Capacitor](https://capacitorjs.com) apps. +:arrow_right: A full-fledged [Node.js](https://nodejs.org/) runtime for [Capacitor](https://capacitorjs.com) apps. + +> [!NOTE] +> This project uses the [Node.js for Mobile Apps](https://github.com/nodejs-mobile/nodejs-mobile) toolkit to add Node.js support in Android and iOS + +> [!WARNING] +> **WIP - Work in Progress** + +**Table of contents** + +* [Install](#install) + + [Supported Platforms](#supported-platforms) +* [Examples](#examples) +* [Getting Started](#getting-started) + + [Basics](#basics) + + [Minimal example](#minimal-example) + + [Inter-Process Communication](#inter-process-communication) +* [Complex Projects](#complex-projects) + + [Custom starting point](#custom-starting-point) + + [Install Node.js Modules](#install-nodejs-modules) + + [Improve Node.js loading times](#improve-nodejs-loading-times) + + [Manual Node.js runtime start](#manual-nodejs-runtime-start) + + [Data storage](#data-storage) +* [Mobile Node.js APIs differences](#mobile-nodejs-apis-differences) +* [Configuration](#configuration) +* [API - Bridge module](#api---bridge-module) +* [API - Capacitor layer](#api---capacitor-layer) -> âšī¸ This project uses the [Node.js for Mobile Apps](https://github.com/nodejs-mobile/nodejs-mobile) toolkit to add Node.js support in Android and IOS +## Install -> **â WIP - Work in Progress â ** -> -> The project is part of my diploma thesis, an overview can be found at [hampoelz/HTL_Diplomarbeit](https://github.com/hampoelz/HTL_Diplomarbeit). -> -> â° Planed Deadline: September 2023 -> -> Notes: -> - The project is still very unstable, if you have any problems or suggestions it would be nice if you create an issue. -> - When the project is stable it will be published on NPM. -> - Features like IOS support will be added in the future. -> - The node.js version used in this project depends on the [Node.js for Mobile Apps](https://github.com/nodejs-mobile/nodejs-mobile) toolkit. +**You've to use Capacitor v5 or newer. This project isn't compatible with lower versions of Capacitor.** + +```bash +npm install https://github.com/hampoelz/capacitor-nodejs/releases/download/v1.0.0-beta.4/capacitor-nodejs.tgz +npx cap sync +``` + +> [!NOTE] +> For now Android 32-bit x86 support is disabled since Capacitor-NodeJS v1.0.0-beta.2 _(based on node.js v16)_ as there is currently no support for it in the latest version of the nodejs-mobile core library. ### Supported Platforms @@ -24,90 +48,132 @@ - [x] Windows - [x] Linux - [x] macOS +- [ ] _Web (maybe in future with WebAssembly?)_ -## Install +## Examples -**You've to use Capacitor v5 or newer. This project isn't compatible with lower versions of Capacitor.** +Example projects can be found in the [hampoelz/Capacitor-NodeJS_Examples](https://github.com/hampoelz/Capacitor-NodeJS_Examples) repository. +Each example project is provided in a separate branch. -```bash -npm install https://github.com/hampoelz/capacitor-nodejs/releases/download/v1.0.0-beta.3/capacitor-nodejs.tgz -npx cap sync -``` +## Getting Started -> â Important -> -> For now Android 32-bit x86 support is disabled in Capacitor-NodeJS v1.0.0-beta.2 _(based on node.js v16)_ as there is currently no support for it in the latest version of the nodejs-mobile core library. However, you can use Capacitor-NodeJS v1.0.0-beta.1 which is based on node.js v12. +This guide shows how to add a minimal Node.js project to a Capacitor application and communicate between these processes. -## Examples +### Basics -Several example projects can be found in the [hampoelz/Capacitor-NodeJS_Examples](https://github.com/hampoelz/Capacitor-NodeJS_Examples) repository. +In the example below uses the Vite build system. However, any build system can be used as long as the following criteria are met: -Currently there is one example project available. One that uses the vitejs framework, located in the [`example/vite`](https://github.com/hampoelz/Capacitor-NodeJS_Examples/tree/example/vite) branch. More is coming soon. +1. The Node.js project (to be executed by the engine) must be located in a subdirectory named `nodejs` _(or the path set via `nodeDir`)_ of the Capacitor `webDir`. +2. The Node.js project must have a starting point, this can either be a script named `index.js` or a package.json with a `main` field. -## Getting Started +> For example if the Node.js project needs to be compiled or bundled then this output should be located in the subdirectory of the Capacitor `webDir`. + +### Minimal example + +In this example the directory for the app's source files is named `src`, the directory for static assets is named `static`, +the directory for the compiled files is named `dist`, and the directory for the Node.js project is named `nodejs`. -To add a Node.js project to your app, the following steps are required: - -1. Create a new directory called `nodejs` inside your app's source/public directory _(this is usually the `src` folder or if you use a build system the `public` folder)_. The `nodejs` dir will serve as your Node.js project folder. _(modules can be installed later in this folder)_ -2. Create a `package.json` file in it as the starting point of the Node.js integration: - ```json - { - "name": "capacitor-node-project", - "version": "1.0.0", - "description": "node part of the project", - "main": "main.js", - "author": "hampoelz", - "license": "MIT", - "dependencies": { - "bridge": "file:../../node_modules/capacitor-nodejs/assets/builtin_modules/bridge" - } +So the configurations should contain at least the following values: + +**Vite Configurations:** + +```typescript +// in vite.config.js or vite.config.ts +{ + root: './src', + publicDir: '../static', + build: { + outDir: '../dist' } - ``` -3. Create the main script of the Node.js integration _(in this case `main.js`)_, which could look like this: - ```javascript - const { channel } = require('bridge'); - - channel.addListener('msg-from-capacitor', message => { - console.log('[node] Message from Capacitor code: ' + message); - channel.send("msg-from-nodejs", "Replying to this message: " + message, "And optionally add further args"); - }); - ``` -4. Run `npm install --install-links` in your newly created Node.js project folder. +} +``` -After that, the project structure should look something like this: +**Capacitor Configurations:** +```typescript +// in capacitor.config.json or capacitor.config.ts +{ + "webDir": 'dist', + "plugins": { + "CapacitorNodeJS": { + "nodeDir": "nodejs" + } + } +} ``` -my-capacitor-app/ -âââ ... -âââ src/ # app source directory -â âââ ... -â âââ nodejs/ # Node.js project directory -â â âââ node_modules/ -â â âââ main.js # main script of the Node.js integration -â â âââ package.json # starting point of the Node.js integration -â âââ ... -â âââ index.html -âââ capacitor.config.json -âââ package.json -âââ README.md -âââ ... + + + +To meet the criteria from above using Vite, just create a new directory called `nodejs` inside the `static` directory. +And create a new file called `index.js` in it as the starting point. + +> Vite will copy assets from the `static` directory to the root of the `dist` directory as-is. +> So the created `nodejs` project directory will be placed in the Capacitor `webdir` after build. + + + +The project structure should now look something like this: + +```diff + capacitor-app/ + âââ ... + âââ dist/ # Capacitor webdir + âââ src/ # app source directory ++ âââ static/ # static assets ++ â âââ nodejs/ # Node.js project directory ++ â â âââ index.js # Node.js main script + âââ capacitor.config.json + âââ vite.config.ts + âââ ... ``` -Now you can communicate with the Node.js layer in your Capacitor app: + + +After building and syncing the project, the main script will be executed by the Node.js runtime when the app is launched. + +A guide for a more complex Node.js project can be found in the [Complex Projects](#complex-projects) section. + +### Inter-Process Communication + +A bridge module to communicate between the Capacitor layer and the Node.js process is built-in. + +Use the following code in a Node.js script to wait for messages from the Capacitor layer and send messages back: + +```javascript +const { channel } = require('bridge'); + +// Listens to "msg-from-capacitor" from the Capacitor layer. +channel.addListener('msg-from-capacitor', message => { + console.log('[Node.js] Message from Capacitor: ' + message); + + // Sends a message back to the Capacitor layer. + channel.send("msg-from-nodejs", `Replying to the message '${message}'.`, "And optionally add more arguments."); +}); +``` + + + +Now you can communicate with the Node.js process in the Capacitor app: ```typescript import { NodeJS } from 'capacitor-nodejs'; // Listens to "msg-from-nodejs" from the Node.js process. NodeJS.addListener('msg-from-nodejs', event => { - document.body.innerHTML = `
First argument: ${event.args[0]}
Second argument: ${event.args[1]}
+ Message from Capacitor
+ First argument: ${event.args[0]}
+ Second argument: ${event.args[1]}
+
string
| Relative path of the integrated Node.js project based on the Capacitor webdir. | "nodejs"
| 1.0.0 |
+| Prop | Type | Description | Default | Since |
+| --------------- | ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------- | ----- |
+| **`nodeDir`** | string
| Relative path of the integrated Node.js project based on the Capacitor webdir. | "nodejs"
| 1.0.0 |
+| **`startMode`** | 'auto' \| 'manual'
| Startup mode of the Node.js engine. The following values are accepted: `auto`: The Node.js engine starts automatically when the application is launched. `manual`: The Node.js engine is started via the `NodeJS.start()` method. | "auto"
| 1.0.0 |
### Examples
@@ -146,7 +516,8 @@ In `capacitor.config.json`:
{
"plugins": {
"CapacitorNodeJS": {
- "nodeDir": "custom-nodejs"
+ "nodeDir": "custom-nodejs",
+ "startMode": "manual"
}
}
}
@@ -163,6 +534,7 @@ const config: CapacitorConfig = {
plugins: {
CapacitorNodeJS: {
nodeDir: "custom-nodejs",
+ startMode: "manual",
},
},
};
@@ -172,44 +544,33 @@ export default config;
-If you change your `nodeDir` to `custom-nodejs`, then your project structure should look something like this:
+---
-```
-my-capacitor-app/
-âââ ...
-âââ src/ # app source directory
-â âââ ...
-â âââ custom-nodejs/ # the new Node.js project directory
-â â âââ node_modules/
-â â âââ main.js
-â â âââ package.json
-â âââ ...
-â âââ index.html
-âââ capacitor.config.json
-âââ package.json
-âââ README.md
-âââ ...
-```
+## API - Bridge module
-## Node Modules
+The `bridge` module is built-in. It provides an API to communicate between the Capacitor layer and the Node.js process, as well as an API to get a per-user application data directory on each platform.
-Node modules can be added to the project using npm. The Node modules have to be installed in the Node.js project folder in which the `package.json` file was created.
+TypeScript declarations for this `bridge` module can be manually installed as dev-dependency. If needed, the types-only package can be found under `node_modules/capacitor-nodejs/assets/types/bridge` in the root of the Capacitor project.
-Go to the Node.js project folder and proceed with the installation of the Node modules you want to add to your Node.js project.
-Sync and rebuild your Capacitor project so that the newly added Node modules are added to the application.
+--------------------
-On Android, the plugin extracts the project files and the Node modules from the APK assets in order to make them available to the Node.js for Mobile Apps engine. They are extracted from the APK and copied to a working folder (`context.getFilesDir().getAbsolutePath() + "/public/StartOptions
|
+
+**Since:** 1.0.0
+
+--------------------
+
+
### send(...)
```typescript
@@ -462,6 +844,23 @@ Removes all listeners, or those of the specified `eventName`, for this plugin.
### Interfaces
+#### StartOptions
+
+An interface containing the options used when starting the Node.js engine manually.
+
+| Prop | Type | Description | Since |
+| ------------- | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----- |
+| **`nodeDir`** | string
| Relative path of the integrated Node.js project based on the Capacitor webdir. Defaults to the `nodeDir` field of the global plugin configuration. If the `nodeDir` config is not set, `nodejs` in the Capacitor webdir is used as Node.js project directory. | 1.0.0 |
+| **`script`** | string
| The primary entry point to the Node.js program. This should be a module relative to the root of the Node.js project folder. Defaults to the `main` field in the project's package.json. If the `main` field is not set, `index.js` in the project's root folder is used. | 1.0.0 |
+| **`args`** | string[]
| A list of string arguments. | 1.0.0 |
+| **`env`** | NodeEnv
| Environment key-value pairs. | 1.0.0 |
+
+
+#### NodeEnv
+
+An interface that holds environment variables as string key-value pairs.
+
+
#### ChannelPayloadData
The payload data to send a message to the web page via `eventName`,
diff --git a/UPGRADING.md b/UPGRADING.md
index 55e97d4..5aa143a 100644
--- a/UPGRADING.md
+++ b/UPGRADING.md
@@ -1,51 +1,68 @@
+## Capacitor-NodeJS v1.0.0-beta.4
+
+Remove the `bridge` module from your Node.js project dependencies. It is now a built-in module on all platforms.
+
+```diff
+ {
+ "name": "capacitor-nodejs-project",
+ "version": "1.0.0",
+ "main": "./server.js"
+ "dependencies": {
+- "bridge": "file:../../node_modules/capacitor-nodejs/assets/builtin_modules/bridge"
+ }
+ }
+```
+
## Capacitor-NodeJS v1.0.0-beta.3
-1. Change the plugin name in your Capacitor configuration from `NodeJS` to `CapacitorNodeJS`. For example in `capacitor.config.json`:
- ```diff
+Change the plugin name in your Capacitor configuration from `NodeJS` to `CapacitorNodeJS`.
+For example in `capacitor.config.json`:
+
+```diff
{
"plugins": {
- - "NodeJS": {
- + "CapacitorNodeJS": {
- "nodeDir": "custom-nodejs"
+- "NodeJS": {
++ "CapacitorNodeJS": {
+ "nodeDir": "custom-nodejs"
+ }
}
}
}
- ```
+```
## Capacitor-NodeJS v1.0.0-beta.1
-1. Move your NodeJS project from you app's source directory to the subfolder `nodejs`. You can skip this step if you set a custom nodeDir in your capacitor configuration. Your project structure should then look something like this:
- ```
- my-capacitor-app/
- âââ ...
- âââ src/ # app source directory
- â âââ ...
- â âââ nodejs/ # NodeJS project directory
- â â âââ node_modules/
- â â âââ main.js
- â â âââ package.json
- â âââ ...
- â âââ index.html
- âââ capacitor.config.json
- âââ package.json
- âââ README.md
- âââ ...
- ```
+1. Move your NodeJS project from you app's Capacitor webDir to the subfolder `nodejs`. You can skip this step if you set a custom nodeDir in your capacitor configuration. Your project structure should then look something like this:
+
+```
+my-capacitor-app/
+âââ ...
+âââ src/ # Capacitor webDir
+â âââ ...
+â âââ nodejs/ # Node.js project directory
+â â âââ node_modules/
+â â âââ main.js
+â â âââ package.json
+â âââ ...
+â âââ index.html
+âââ capacitor.config.json
+âââ package.json
+âââ README.md
+âââ ...
+```
2. Add `"bridge": "file:../../node_modules/capacitor-nodejs/assets/builtin_modules/bridge"` to `dependencies` in your NodeJS project's package.json file. The file should then look something like this:
- ```json
+
+```diff
{
- "name": "capacitor-node-project",
- "version": "1.0.0",
- "description": "node part of the project",
- "main": "main.js",
- "author": "hampoelz",
- "license": "MIT",
- "dependencies": {
- "bridge": "file:../../node_modules/capacitor-nodejs/assets/builtin_modules/bridge"
- }
+ "name": "capacitor-nodejs-project",
+ "version": "1.0.0",
+ "main": "./server.js"
+ "dependencies": {
++ "bridge": "file:../../node_modules/capacitor-nodejs/assets/builtin_modules/bridge"
+ }
}
- ```
+```
3. Run `npm install` in your NodeJS project folder to install the added bridge module.
diff --git a/bridge/src/bridge.ts b/bridge/src/bridge.ts
index 7373f48..2a23282 100644
--- a/bridge/src/bridge.ts
+++ b/bridge/src/bridge.ts
@@ -63,6 +63,13 @@ class Channel extends EventEmitter {
});
}
+ /**
+ * Sends a message to the Capacitor layer via eventName, along with arguments.
+ * Arguments will be serialized with JSON.
+ *
+ * @param eventName The name of the event being send to.
+ * @param args The Array of arguments to send.
+ */
send(eventName: string, ...args: any[]) {
if (eventName === undefined || eventName === '') {
throw new Error("Required parameter 'eventName' was not specified");
@@ -87,12 +94,59 @@ class Channel extends EventEmitter {
self.emit(eventName, ...args);
});
}
+
+ /**
+ * Listens to `eventName` and calls `listener(args...)` when a new message arrives from the Capacitor layer.
+ */
+ override on(eventName: string, listener: (...args: any[]) => void): this {
+ return super.on(eventName, listener);
+ }
+
+ /**
+ * Listens one time to `eventName` and calls `listener(args...)` when a new message
+ * arrives from the Capacitor layer, after which it is removed.
+ */
+ override once(eventName: string, listener: (...args: any[]) => void): this {
+ return super.once(eventName, listener);
+ }
+
+ /**
+ * Alias for `channel.on(eventName, listener)`.
+ */
+ override addListener(eventName: string, listener: (...args: any[]) => void): this {
+ return super.once(eventName, listener);
+ }
+
+ /**
+ * Removes the specified `listener` from the listener array for the specified `eventName`.
+ */
+ override removeListener(eventName: string, listener: (...args: any[]) => void): this {
+ return super.removeListener(eventName, listener);
+ }
+
+ /**
+ * Removes all listeners, or those of the specified `eventName`.
+ *
+ * @param eventName The name of the event all listeners will be removed from.
+ */
+ override removeAllListeners(eventName?: string): this {
+ return super.removeAllListeners(eventName);
+ }
}
const appChannel = new Channel('APP_CHANNEL');
+
+/**
+ * Provides a few methods to send messages from the Node.js process to the Capacitor layer,
+ * and to receive replies from the Capacitor layer.
+ */
const eventChannel = new Channel('EVENT_CHANNEL');
-function getDataDir(): string {
+/**
+ * Returns a path for a per-user application data directory on each platform,
+ * where data can be read and written.
+ */
+function getDataPath(): string {
const path = process.env['DATADIR'];
if (!path) {
throw new Error('Unable to get a directory for persistent data storage.');
@@ -100,4 +154,4 @@ function getDataDir(): string {
return path;
}
-export { ChannelMessageCodec, appChannel, eventChannel, getDataDir };
+export { ChannelMessageCodec, appChannel, eventChannel, getDataPath };
diff --git a/bridge/src/index.ts b/bridge/src/index.ts
index d77003d..c87c138 100644
--- a/bridge/src/index.ts
+++ b/bridge/src/index.ts
@@ -1,5 +1,5 @@
-import { appChannel, eventChannel, getDataDir, platform } from './bridge';
+import { appChannel, eventChannel, getDataPath } from './bridge';
appChannel.send('ready');
-export { eventChannel as channel, getDataDir as datadir, platform };
+export { eventChannel as channel, getDataPath };
diff --git a/package.json b/package.json
index d300dbd..10f6e25 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "capacitor-nodejs",
- "version": "1.0.0-beta.3",
+ "version": "1.0.0-beta.4",
"description": "A full-fledged Node.js runtime for Capacitor apps",
"main": "dist/plugin.cjs.js",
"module": "dist/esm/index.js",
diff --git a/src/NodeJS.ts b/src/NodeJS.ts
index e08e5ba..d1e2a0c 100644
--- a/src/NodeJS.ts
+++ b/src/NodeJS.ts
@@ -2,10 +2,10 @@ import type { PluginListenerHandle } from '@capacitor/core';
import { Capacitor } from '@capacitor/core';
import type {
- ChannelPayloadData,
- ChannelCallbackData,
- ChannelListenerCallback,
- StartOptions
+ ChannelPayloadData,
+ ChannelCallbackData,
+ ChannelListenerCallback,
+ StartOptions
} from './definitions';
import { CapacitorNodeJS } from './implementation';
@@ -13,6 +13,8 @@ export interface NodeJSInterface {
/**
* Starts the Node.js engine with properties as set by the `options`.
*
+ * **Note:** This method is only available if the Node.js engine startup mode was set to `'manual'` via the plugin configuration.
+ *
* @since 1.0.0
*/
start(options?: StartOptions): Promise