|
| 1 | +# Extensions API |
| 2 | + |
| 3 | +> As of Harper v4.6, a new iteration of the extension API was released called **Plugins**. They are simultaneously a simplification and an extensibility upgrade. Plugins are **experimental**, but we encourage developers to consider developing with the [plugin API](./plugins.md) instead of the extension API. In time we plan to deprecate the concept of extensions in favor of plugins, but for now, both are supported. |
| 4 | +
|
| 5 | +There are two key types of Extensions: **Resource Extension** and **Protocol Extensions**. The key difference is a **Protocol Extensions** can return a **Resource Extension**. |
| 6 | + |
| 7 | +Furthermore, what defines an extension separately from a component is that it leverages any of the [Resource Extension](#resource-extension-api) or [Protocol Extension](#protocol-extension-api) APIs. |
| 8 | + |
| 9 | +All extensions must define a `config.yaml` file and declare an `extensionModule` option. This must be a path to the extension module source code. The path must resolve from the root of the module directory. |
| 10 | + |
| 11 | +For example, the [Harper Next.js Extension](https://github.com/HarperDB/nextjs) `config.yaml` specifies `extensionModule: ./extension.js`. |
| 12 | + |
| 13 | +If the plugin is being written in something other than JavaScript (such as TypeScript), ensure that the path resolves to the built version, (i.e. `extensionModule: ./dist/index.js`) |
| 14 | + |
| 15 | +## Resource Extension |
| 16 | + |
| 17 | +A Resource Extension is for processing a certain type of file or directory. For example, the built-in [jsResource](./built-in.md#jsresource) extension handles executing JavaScript files. |
| 18 | + |
| 19 | +Resource Extensions are comprised of four distinct function exports, [`handleFile()`](#handlefilecontents-urlpath-absolutepath-resources-void--promisevoid), [`handleDirectory()`](#handledirectoryurlpath-absolutepath-resources-boolean--void--promiseboolean--void), [`setupFile()`](#setupfilecontents-urlpath-absolutepath-resources-void--promisevoid), and [`setupDirectory()`](#setupdirectoryurlpath-absolutepath-resources-boolean--void--promiseboolean--void). The `handleFile()` and `handleDirectory()` methods are executed on **all worker threads**, and are _executed again during restarts_. The `setupFile()` and `setupDirectory()` methods are only executed **once** on the **main thread** during the initial system start sequence. |
| 20 | + |
| 21 | +> Keep in mind that the CLI command `harperdb restart` or CLI argument `restart=true` only restarts the worker threads. If a component is deployed using `harperdb deploy`, the code within the `setupFile()` and `setupDirectory()` methods will not be executed until the system is completely shutdown and turned back on. |
| 22 | +
|
| 23 | +Other than their execution behavior, the `handleFile()` and `setupFile()` methods, and `handleDirectory()` and `setupDirectory()` methods have identical function definitions (arguments and return value behavior). |
| 24 | + |
| 25 | +### Resource Extension Configuration |
| 26 | + |
| 27 | +Any [Resource Extension](#resource-extension) can be configured with the `files` and `urlPath` options. These options control how _files_ and _directories_ are resolved in order to be passed to the extension's `handleFile()`, `setupFile()`, `handleDirectory()`, and `setupDirectory()` methods. |
| 28 | + |
| 29 | +> Harper relies on the [fast-glob](https://github.com/mrmlnc/fast-glob) library for glob pattern matching. |
| 30 | +
|
| 31 | +- **files** - `string | string[] | Object` - *required* - A [glob pattern](https://github.com/mrmlnc/fast-glob?tab=readme-ov-file#pattern-syntax) string, array of glob pattern strings, or a more expressive glob options object determining the set of files and directories to be resolved for the extension. If specified as an object, the `source` property is required. By default, Harper **matches files and directories**; this is configurable using the `only` option. |
| 32 | + - **source** - `string | string[]` - *required* - The glob pattern string or array of strings. |
| 33 | + - **only** - `'all' | 'files' | 'directories'` - *optional* - The glob pattern will match only the specified entry type. Defaults to `'all'`. |
| 34 | + - **ignore** - `string[]` - *optional* - An array of glob patterns to exclude from matches. This is an alternative way to use negative patterns. Defaults to `[]`. |
| 35 | +- **urlPath** - `string` - *optional* - A base URL path to prepend to the resolved `files` entries. |
| 36 | + - If the value starts with `./`, such as `'./static/'`, the component name will be included in the base url path |
| 37 | + - If the value is `.`, then the component name will be the base url path |
| 38 | + - Note: `..` is an invalid pattern and will result in an error |
| 39 | + - Otherwise, the value here will be base url path. Leading and trailing `/` characters will be handled automatically (`/static/`, `/static`, and `static/` are all equivalent to `static`) |
| 40 | + |
| 41 | +For example, to configure the [static](./built-in.md#static) component to serve all HTML files from the `web` source directory on the `static` URL endpoint: |
| 42 | + |
| 43 | +```yaml |
| 44 | +static: |
| 45 | + files: 'web/*.html' |
| 46 | + urlPath: 'static' |
| 47 | +``` |
| 48 | +
|
| 49 | +If there are files such as `web/index.html` and `web/blog.html`, they would be available at `localhost/static/index.html` and `localhost/static/blog.html` respectively. |
| 50 | + |
| 51 | +Furthermore, if the component is located in the `test-component` directory, and the `urlPath` was set to `'./static/'` instead, then the files would be served from `localhost/test-component/static/*` instead. |
| 52 | + |
| 53 | +The `urlPath` is optional, for example to configure the [graphqlSchema](./built-in.md#graphqlschema) component to load all schemas within the `src/schema` directory, only specifying a `files` glob pattern is required: |
| 54 | + |
| 55 | +```yaml |
| 56 | +graphqlSchema: |
| 57 | + files: 'src/schema/*.schema' |
| 58 | +``` |
| 59 | + |
| 60 | +The `files` option also supports a more complex options object. These additional fields enable finer control of the glob pattern matching. |
| 61 | + |
| 62 | +For example, to match files within `web`, and omit any within the `web/images` directory, the configuration could be: |
| 63 | +```yaml |
| 64 | +static: |
| 65 | + files: |
| 66 | + source: 'web/**/*' |
| 67 | + ignore: ['web/images'] |
| 68 | +``` |
| 69 | + |
| 70 | +In order to match only files: |
| 71 | + |
| 72 | +```yaml |
| 73 | +test-component: |
| 74 | + files: |
| 75 | + source: 'dir/**/*' |
| 76 | + only: 'files' |
| 77 | +``` |
| 78 | + |
| 79 | +### Resource Extension API |
| 80 | + |
| 81 | +In order for an extension to be classified as a Resource Extension it must implement at least one of the `handleFile()`, `handleDirectory()`, `setupFile()`, or `setupDirectory()` methods. As a standalone extension, these methods should be named and exported directly. For example: |
| 82 | + |
| 83 | +```js |
| 84 | +// ESM |
| 85 | +export function handleFile() {} |
| 86 | +export function setupDirectory() {} |
| 87 | +
|
| 88 | +// or CJS |
| 89 | +function handleDirectory() {} |
| 90 | +function setupFile() {} |
| 91 | +
|
| 92 | +module.exports = { handleDirectory, setupFile } |
| 93 | +``` |
| 94 | + |
| 95 | +When returned by a [Protocol Extension](#protocol-extension), these methods should be defined on the object instead: |
| 96 | + |
| 97 | +```js |
| 98 | +export function start() { |
| 99 | + return { |
| 100 | + handleFile () {} |
| 101 | + } |
| 102 | +} |
| 103 | +``` |
| 104 | + |
| 105 | +#### `handleFile(contents, urlPath, absolutePath, resources): void | Promise<void>` |
| 106 | +#### `setupFile(contents, urlPath, absolutePath, resources): void | Promise<void>` |
| 107 | + |
| 108 | +These methods are for processing individual files. They can be async. |
| 109 | + |
| 110 | +> Remember! |
| 111 | +> |
| 112 | +> `setupFile()` is executed **once** on the **main thread** during the main start sequence. |
| 113 | +> |
| 114 | +> `handleFile()` is executed on **worker threads** and is executed again during restarts. |
| 115 | + |
| 116 | +Parameters: |
| 117 | + |
| 118 | +- **contents** - `Buffer` - The contents of the file |
| 119 | +- **urlPath** - `string` - The recommended URL path of the file |
| 120 | +- **absolutePath** - `string` - The absolute path of the file |
| 121 | + <!-- TODO: Replace the Object type here with a more specific type representing the resources argument of loadComponent() --> |
| 122 | +- **resources** - `Object` - A collection of the currently loaded resources |
| 123 | + |
| 124 | +Returns: `void | Promise<void>` |
| 125 | + |
| 126 | +#### `handleDirectory(urlPath, absolutePath, resources): boolean | void | Promise<boolean | void>` |
| 127 | +#### `setupDirectory(urlPath, absolutePath, resources): boolean | void | Promise<boolean | void>` |
| 128 | + |
| 129 | +These methods are for processing directories. They can be async. |
| 130 | + |
| 131 | +If the function returns or resolves a truthy value, then the component loading sequence will end and no other entries within the directory will be processed. |
| 132 | + |
| 133 | +> Remember! |
| 134 | +> |
| 135 | +> `setupFile()` is executed **once** on the **main thread** during the main start sequence. |
| 136 | +> |
| 137 | +> `handleFile()` is executed on **worker threads** and is executed again during restarts. |
| 138 | + |
| 139 | +Parameters: |
| 140 | + |
| 141 | +- **urlPath** - `string` - The recommended URL path of the directory |
| 142 | +- **absolutePath** - `string` - The absolute path of the directory |
| 143 | + <!-- TODO: Replace the Object type here with a more specific type representing the resources argument of loadComponent() --> |
| 144 | +- **resources** - `Object` - A collection of the currently loaded resources |
| 145 | + |
| 146 | +Returns: `boolean | void | Promise<boolean | void>` |
| 147 | + |
| 148 | +## Protocol Extension |
| 149 | + |
| 150 | +A Protocol Extension is a more advanced form of a Resource Extension and is mainly used for implementing higher level protocols. For example, the [Harper Next.js Extension](https://github.com/HarperDB/nextjs) handles building and running a Next.js project. A Protocol Extension is particularly useful for adding custom networking handlers (see the [`server`](../globals.md#server) global API documentation for more information). |
| 151 | + |
| 152 | +### Protocol Extension Configuration |
| 153 | + |
| 154 | +In addition to the `files` and `urlPath` [Resource Extension configuration](#resource-extension-configuration) options, and the `package` [Custom Component configuration](#custom-component-configuration) option, Protocol Extensions can also specify additional configuration options. Any options added to the extension configuration (in `config.yaml`), will be passed through to the `options` object of the `start()` and `startOnMainThread()` methods. |
| 155 | + |
| 156 | +For example, the [Harper Next.js Extension](https://github.com/HarperDB/nextjs#options) specifies multiple option that can be included in its configuration. For example, a Next.js app using `@harperdb/nextjs` may specify the following `config.yaml`: |
| 157 | + |
| 158 | +```yaml |
| 159 | +'@harperdb/nextjs': |
| 160 | + package: '@harperdb/nextjs' |
| 161 | + files: './' |
| 162 | + prebuilt: true |
| 163 | + dev: false |
| 164 | +``` |
| 165 | + |
| 166 | +Many protocol extensions will use the `port` and `securePort` options for configuring networking handlers. Many of the [`server`](../../technical-details/reference/globals.md#server) global APIs accept `port` and `securePort` options, so components replicated this for simpler pass-through. |
| 167 | + |
| 168 | +### Protocol Extension API |
| 169 | + |
| 170 | +A Protocol Extension is made up of two distinct methods, [`start()`](#startoptions-resourceextension--promiseresourceextension) and [`startOnMainThread()`](#startonmainthreadoptions-resourceextension--promiseresourceextension). Similar to a Resource Extension, the `start()` method is executed on _all worker threads_, and _executed again on restarts_. The `startOnMainThread()` method is **only** executed **once** during the initial system start sequence. These methods have identical `options` object parameter, and can both return a Resource Extension (i.e. an object containing one or more of the methods listed above). |
| 171 | + |
| 172 | +#### `start(options): ResourceExtension | Promise<ResourceExtension>` |
| 173 | +#### `startOnMainThread(options): ResourceExtension | Promise<ResourceExtension>` |
| 174 | + |
| 175 | +Parameters: |
| 176 | + |
| 177 | +- **options** - `Object` - An object representation of the extension's configuration options. |
| 178 | + |
| 179 | +Returns: `Object` - An object that implements any of the [Resource Extension APIs](#resource-extension-api) |
0 commit comments