@formio/uag 2.0.0-rc.7 → 2.0.0-rc.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -6
- package/bin/uag.js +43 -4
- package/lib/documents/converters.d.ts +3 -1
- package/lib/documents/converters.js +11 -1
- package/lib/index.d.ts +1 -1
- package/lib/index.js +4 -3
- package/lib/pluginFiles.d.ts +23 -11
- package/lib/pluginFiles.js +62 -23
- package/lib/testing.js +14 -4
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -243,7 +243,7 @@ Engines run inside the UAG, or on an [engine server](./packages/engine-server) (
|
|
|
243
243
|
| You run | You need |
|
|
244
244
|
|---|---|
|
|
245
245
|
| MCP and conversation forms (including a form's chat, `{form}/agent/chat`), on Form.io Open Source or a hosted project | Nothing |
|
|
246
|
-
| MCP and conversation forms, on a self-hosted Form.io Enterprise project | A Form.io license in `LICENSE_KEY` (development hostnames such as `localhost` excepted) |
|
|
246
|
+
| MCP and conversation forms, on a self-hosted Form.io Enterprise project | A Form.io license that includes the UAG, in `LICENSE_KEY` (development hostnames such as `localhost` excepted) |
|
|
247
247
|
| Agentic workflows and Agentic Formflow (the Agent API's `extract`, `decide` and `generate`, the **Agent: Run task** action, the `agent_decide` and `agent_provide` tools, `uag-eval`) on a self-hosted project, Open Source or Enterprise | A Form.io license that includes the UAG, in `LICENSE_KEY` |
|
|
248
248
|
| The same on a project on Form.io's hosted service (`https://*.form.io`) | Nothing, as the project runs under Form.io's license; except `uag-eval`, which runs offline and needs `LICENSE_KEY` |
|
|
249
249
|
| The UAG's own database (`MONGO`), and with it remote MCP sign-in (`MCP_OAUTH`), on any project | A Form.io license that includes the UAG, unless the project and the database are both on development hosts (`localhost`, `*.localhost`, `lvh.me`, `*.local`) |
|
|
@@ -253,7 +253,8 @@ A hosted project needs a license after all when the UAG weakens the proof that i
|
|
|
253
253
|
|
|
254
254
|
## Extending the UAG
|
|
255
255
|
|
|
256
|
-
- **Modules** add MCP tools, override the built-in tools and their response templates, add form actions (the UAG runs a module's own actions; Form.io runs its own), and ship a project template of forms and resources that the UAG installs. Mount one over `/app/module` in the container, or name it in `UAG_MODULE` for the `uag` command. See [Modules](https://ai.form.io/developers/modules), [`module/Readme.md`](./module/Readme.md) and the [Claude Desktop example](./demo/samples/custom-module).
|
|
256
|
+
- **Modules** add MCP tools, override the built-in tools and their response templates, add form actions (the UAG runs a module's own actions; Form.io runs its own), and ship a project template of forms and resources that the UAG installs. A module can also add routes on every form, formio.js components (served in the UAG's `/uag-js/uag.use.js`), plugins, and settings of its own. Mount one over `/app/module` in the container, or name it in `UAG_MODULE` for the `uag` command. See [Modules](https://ai.form.io/developers/modules), [`module/Readme.md`](./module/Readme.md) and the [Claude Desktop example](./demo/samples/custom-module).
|
|
257
|
+
- **Plugins** add capabilities of a known type, such as converters that read more file types and the document processes an AI Document runs. List them in a module, or in a plugins file: the one `UAG_PLUGINS` names, or in the image, one per folder under `/app/plugins`, where add-ons such as `formio/uag-documents` put theirs. Plugins and modules are trusted code: they run in the UAG process. See [Plugins](https://ai.form.io/developers/plugins) and [AI Document](https://ai.form.io/developers/ai-document).
|
|
257
258
|
- **Node.js.** Embed the UAG in an Express application, or run it with `npx uag`:
|
|
258
259
|
|
|
259
260
|
```js
|
|
@@ -319,10 +320,11 @@ The full table, with the Agent API's errors: [Troubleshooting](https://ai.form.i
|
|
|
319
320
|
| Package | Version | Works with UAG | What it is |
|
|
320
321
|
|---|---|---|---|
|
|
321
322
|
| [`@formio/uag`](https://www.npmjs.com/package/@formio/uag), image [`formio/uag`](https://hub.docker.com/r/formio/uag) | 2.x | — | The UAG. The image includes every official engine. |
|
|
322
|
-
| `@formio/uag-jev`, `-claude`, `-openai`, `-grok`, `-gemini` | 2.x | ≥ 2.0.0 | The [engines](./engines),
|
|
323
|
+
| `@formio/uag-jev`, `-claude`, `-openai`, `-grok`, `-gemini` | 2.x | ≥ 2.0.0 | The [engines](./engines), released with the UAG at the same version. |
|
|
323
324
|
| [`@formio/uag-sdk`](./packages/engine-sdk) | 2.x | ≥ 2.0.0 | Engine contract version 1, remote engine protocol version 1. |
|
|
324
325
|
| [`@formio/uag-engines`](./packages/engine-server), image `formio/uag-engines` | 2.x | ≥ 2.0.0 | The engine server. |
|
|
325
|
-
| [`@formio/uag-js`](./packages/uag-js) | 2.x | ≥ 2.0.0 | The formio.js module: saves `uag` forms through the UAG, adds Agent Input, and renders forms as a chat. Runs in the browser
|
|
326
|
+
| [`@formio/uag-js`](./packages/uag-js) | 2.x | ≥ 2.0.0 | The formio.js module: saves `uag` forms through the UAG, adds Agent Input, and renders forms as a chat. Runs in the browser; the UAG also serves it at `/uag-js/`. |
|
|
327
|
+
| [`@formio/uag-extras`](https://github.com/formio/uag-extras), image `formio/uag-documents` | 1.x | ≥ 2.0.0-rc.7 | Versioned on its own: the AI Document module, document plugins and converters (`npm install @formio/uag-extras@^1`). The image is an add-on that runs beside `formio/uag` and fills its plugins folder (`formio/uag-documents:1`): see [AI Document](https://ai.form.io/developers/ai-document#run-it-with-docker). |
|
|
326
328
|
| `@formio/uag-claude` | 1.3.x | 1.12.0 – 1.13.x | The retired Claude integration (against 2.0, only its direct commands). Security fixes only; see [MIGRATING-2.0.md](./MIGRATING-2.0.md). |
|
|
327
329
|
|
|
328
330
|
`@formio/uag-claude` 2.x is the Claude *engine*, not the 1.x integration; pin `@formio/uag-claude@^1` if you still install the integration. Upgrading from 1.x: [MIGRATING-2.0.md](./MIGRATING-2.0.md).
|
|
@@ -335,6 +337,6 @@ The developer documentation is published at [ai.form.io/developers](https://ai.f
|
|
|
335
337
|
|---|---|---|
|
|
336
338
|
| **Start here** | [How the UAG works](https://ai.form.io/developers) · [Agentic Coding Plugin](https://ai.form.io/developers/agentic-coding) · [Quickstart](https://ai.form.io/developers/quickstart) | [Demo site](./demo/README.md) |
|
|
337
339
|
| **Concepts** | [Concepts](https://ai.form.io/developers/concepts) · [Form properties](https://ai.form.io/developers/form-properties) | |
|
|
338
|
-
| **Build** | [MCP tools](https://ai.form.io/developers/mcp-tools) · [Agent API](https://ai.form.io/developers/agent-api) · [Workflows](https://ai.form.io/developers/workflows) · [JavaScript (formio.js)](https://ai.form.io/developers/renderer) · [Chat widget](https://ai.form.io/developers/chat) · [Connect Claude](https://ai.form.io/developers/connect-claude) | [`@formio/uag-js`](./packages/uag-js/README.md) · [Claude Desktop module](./demo/samples/custom-module/Readme.md) |
|
|
339
|
-
| **Extend** | [Node.js API](https://ai.form.io/developers/node-api) · [Modules](https://ai.form.io/developers/modules) · [Engines](https://ai.form.io/developers/engines) · [Engine SDK](https://ai.form.io/developers/engine-sdk) · [Engine server](https://ai.form.io/developers/engine-server) | [Modules](./module/Readme.md) · [Response templates](./demo/samples/custom-module/module/templates/Readme.md) · [Engines](./engines) · [Engine SDK](./packages/engine-sdk/README.md) · [Engine server](./packages/engine-server/README.md) · [Engine setups](./demo/samples/engines/Readme.md) |
|
|
340
|
+
| **Build** | [MCP tools](https://ai.form.io/developers/mcp-tools) · [Agent API](https://ai.form.io/developers/agent-api) · [Workflows](https://ai.form.io/developers/workflows) · [JavaScript (formio.js)](https://ai.form.io/developers/renderer) · [Chat widget](https://ai.form.io/developers/chat) · [AI Document](https://ai.form.io/developers/ai-document) · [Connect Claude](https://ai.form.io/developers/connect-claude) | [`@formio/uag-js`](./packages/uag-js/README.md) · [Claude Desktop module](./demo/samples/custom-module/Readme.md) |
|
|
341
|
+
| **Extend** | [Node.js API](https://ai.form.io/developers/node-api) · [Modules](https://ai.form.io/developers/modules) · [Plugins](https://ai.form.io/developers/plugins) · [Engines](https://ai.form.io/developers/engines) · [Engine SDK](https://ai.form.io/developers/engine-sdk) · [Engine server](https://ai.form.io/developers/engine-server) | [Modules](./module/Readme.md) · [Response templates](./demo/samples/custom-module/module/templates/Readme.md) · [Engines](./engines) · [Engine SDK](./packages/engine-sdk/README.md) · [Engine server](./packages/engine-server/README.md) · [Engine setups](./demo/samples/engines/Readme.md) |
|
|
340
342
|
| **Operate** | [Security](https://ai.form.io/developers/security) · [Deployment](https://ai.form.io/developers/deployment) · [Evaluation](https://ai.form.io/developers/evaluation) · [Troubleshooting](https://ai.form.io/developers/troubleshooting) | [Docker Hub](./DOCKERHUB.md) · [Migrating to 2.0](./MIGRATING-2.0.md) · [Security policy](./SECURITY.md) · [End-to-end tests](./test/e2e/Readme.md) |
|
package/bin/uag.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* Engines are found among the installed packages, so a custom image holds only
|
|
13
13
|
* the engines it installs:
|
|
14
14
|
*
|
|
15
|
-
* npm install @formio/uag @formio/uag-claude
|
|
15
|
+
* npm install @formio/uag@rc @formio/uag-claude@rc
|
|
16
16
|
* UAG_ENGINES=claude CLAUDE_API_KEY=... npx uag
|
|
17
17
|
*/
|
|
18
18
|
// Before anything else, so a module that reads the environment sees .env too.
|
|
@@ -42,7 +42,8 @@ async function start(uagModule) {
|
|
|
42
42
|
// A module may be exported as a promise (an async module).
|
|
43
43
|
const resolved = await uagModule;
|
|
44
44
|
if (resolved) server.use(resolved);
|
|
45
|
-
// Then the plugins
|
|
45
|
+
// Then the plugins files: UAG_PLUGINS, or the image's /app/plugins (its
|
|
46
|
+
// index.mjs, or each add-on's folder under it).
|
|
46
47
|
await usePluginFiles(server);
|
|
47
48
|
const app = Express();
|
|
48
49
|
// Behind a reverse proxy, its forwarded headers give the public URL (TRUST_PROXY).
|
|
@@ -52,11 +53,49 @@ async function start(uagModule) {
|
|
|
52
53
|
// names; a misconfigured engine stops startup with its name in the error.
|
|
53
54
|
app.use(await server.router());
|
|
54
55
|
const port = process.env.PORT || 3200;
|
|
55
|
-
await new Promise((resolve) =>
|
|
56
|
+
const listener = await new Promise((resolve) => {
|
|
57
|
+
const started = app.listen(port, () => resolve(started));
|
|
58
|
+
});
|
|
59
|
+
stopOnSignals(server, listener);
|
|
56
60
|
console.log(`Form.io UAG server running on port ${port}`);
|
|
57
61
|
console.log(`Visit http://localhost:${port} to access the application`);
|
|
58
62
|
}
|
|
59
63
|
|
|
64
|
+
/**
|
|
65
|
+
* On SIGTERM or SIGINT, stop taking connections and destroy the server, which
|
|
66
|
+
* calls each module's `close` and each plugin's, then exit. A shutdown that
|
|
67
|
+
* takes longer than `timeoutMs` exits with 1.
|
|
68
|
+
*/
|
|
69
|
+
function stopOnSignals(server, listener, { signals = ['SIGTERM', 'SIGINT'], exit = (code) => process.exit(code), on = process, timeoutMs = 10000 } = {}) {
|
|
70
|
+
let stopping = false;
|
|
71
|
+
const stop = async (signal) => {
|
|
72
|
+
if (stopping) return;
|
|
73
|
+
stopping = true;
|
|
74
|
+
console.log(`Received ${signal}: stopping the UAG.`);
|
|
75
|
+
const timer = setTimeout(() => {
|
|
76
|
+
console.error(`The UAG did not stop within ${timeoutMs} ms.`);
|
|
77
|
+
exit(1);
|
|
78
|
+
}, timeoutMs);
|
|
79
|
+
if (timer.unref) timer.unref();
|
|
80
|
+
if (listener) listener.close();
|
|
81
|
+
let code = 0;
|
|
82
|
+
try {
|
|
83
|
+
await server.destroy();
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
console.error('Failed to stop the UAG cleanly:', error);
|
|
87
|
+
code = 1;
|
|
88
|
+
}
|
|
89
|
+
clearTimeout(timer);
|
|
90
|
+
exit(code);
|
|
91
|
+
};
|
|
92
|
+
// Not once: a repeated signal (a terminal's Ctrl-C reaches the restarted
|
|
93
|
+
// process twice, from the terminal and from its parent) must not fall back
|
|
94
|
+
// to Node's default handler and end the shutdown early.
|
|
95
|
+
for (const signal of signals) on.on(signal, () => stop(signal));
|
|
96
|
+
return stop;
|
|
97
|
+
}
|
|
98
|
+
|
|
60
99
|
function fail(error) {
|
|
61
100
|
console.error('Failed to start server:', error);
|
|
62
101
|
process.exit(1);
|
|
@@ -74,4 +113,4 @@ if (require.main === module) {
|
|
|
74
113
|
}
|
|
75
114
|
}
|
|
76
115
|
|
|
77
|
-
module.exports = { start, fail };
|
|
116
|
+
module.exports = { start, fail, stopOnSignals };
|
|
@@ -12,7 +12,9 @@ export declare const textConverter: Converter;
|
|
|
12
12
|
* decoded). A PNG is decoded whole, in memory and synchronously, so its pixel
|
|
13
13
|
* count is checked from its header first: a small, highly compressed file can
|
|
14
14
|
* describe an image far larger than its size suggests. JPEGs are embedded
|
|
15
|
-
* without being decoded.
|
|
15
|
+
* without being decoded. The converter's `maxPixels` setting changes it
|
|
16
|
+
* (config.plugins.settings.converter['image-pdf'].maxPixels, or
|
|
17
|
+
* DOCUMENTS_MAX_IMAGE_PIXELS).
|
|
16
18
|
*/
|
|
17
19
|
export declare const MAX_IMAGE_PIXELS = 50000000;
|
|
18
20
|
/** A PNG's width and height, from its IHDR header; undefined when it has none. */
|
|
@@ -91,7 +91,9 @@ const PAGE = { width: 595.28, height: 841.89, margin: 36 };
|
|
|
91
91
|
* decoded). A PNG is decoded whole, in memory and synchronously, so its pixel
|
|
92
92
|
* count is checked from its header first: a small, highly compressed file can
|
|
93
93
|
* describe an image far larger than its size suggests. JPEGs are embedded
|
|
94
|
-
* without being decoded.
|
|
94
|
+
* without being decoded. The converter's `maxPixels` setting changes it
|
|
95
|
+
* (config.plugins.settings.converter['image-pdf'].maxPixels, or
|
|
96
|
+
* DOCUMENTS_MAX_IMAGE_PIXELS).
|
|
95
97
|
*/
|
|
96
98
|
exports.MAX_IMAGE_PIXELS = 50000000;
|
|
97
99
|
/** A PNG's width and height, from its IHDR header; undefined when it has none. */
|
|
@@ -110,6 +112,14 @@ exports.imagePdfConverter = {
|
|
|
110
112
|
apiVersion: 1,
|
|
111
113
|
accepts: [{ type: mime_1.MIME.png, ext: ['.png'] }, { type: mime_1.MIME.jpeg, ext: ['.jpg', '.jpeg'] }],
|
|
112
114
|
produces: ['pdf'],
|
|
115
|
+
settings: {
|
|
116
|
+
maxPixels: {
|
|
117
|
+
type: 'number',
|
|
118
|
+
env: 'DOCUMENTS_MAX_IMAGE_PIXELS',
|
|
119
|
+
default: exports.MAX_IMAGE_PIXELS,
|
|
120
|
+
description: 'The largest PNG, in pixels (width × height), converted; a larger one is refused before it is decoded.'
|
|
121
|
+
}
|
|
122
|
+
},
|
|
113
123
|
async convert(input, options) {
|
|
114
124
|
options.signal.throwIfAborted();
|
|
115
125
|
if (input.type === mime_1.MIME.png) {
|
package/lib/index.d.ts
CHANGED
|
@@ -108,7 +108,7 @@ export * from './tools';
|
|
|
108
108
|
export * from './documents';
|
|
109
109
|
export * from './modules';
|
|
110
110
|
export { sanitizeMarkdown } from './markdown';
|
|
111
|
-
export { usePluginFiles,
|
|
111
|
+
export { usePluginFiles, pluginFiles, pluginFilesIn, DEFAULT_PLUGINS_DIR } from './pluginFiles';
|
|
112
112
|
export * from './router';
|
|
113
113
|
export * from './template';
|
|
114
114
|
export { sanitizeRichText, isRichText, RICH_TEXT_TAGS, RICH_TEXT_RULE } from './richText';
|
package/lib/index.js
CHANGED
|
@@ -17,7 +17,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
17
17
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
18
18
|
};
|
|
19
19
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
-
exports.UAGFormInterface = exports.UAGProjectInterface = exports.RICH_TEXT_RULE = exports.RICH_TEXT_TAGS = exports.isRichText = exports.sanitizeRichText = exports.
|
|
20
|
+
exports.UAGFormInterface = exports.UAGProjectInterface = exports.RICH_TEXT_RULE = exports.RICH_TEXT_TAGS = exports.isRichText = exports.sanitizeRichText = exports.DEFAULT_PLUGINS_DIR = exports.pluginFilesIn = exports.pluginFiles = exports.usePluginFiles = exports.sanitizeMarkdown = exports.UAGServer = void 0;
|
|
21
21
|
exports.assertSingleCore = assertSingleCore;
|
|
22
22
|
exports.assertNodeSnapshotDisabled = assertNodeSnapshotDisabled;
|
|
23
23
|
const appserver_1 = require("@formio/appserver");
|
|
@@ -424,8 +424,9 @@ var markdown_1 = require("./markdown");
|
|
|
424
424
|
Object.defineProperty(exports, "sanitizeMarkdown", { enumerable: true, get: function () { return markdown_1.sanitizeMarkdown; } });
|
|
425
425
|
var pluginFiles_1 = require("./pluginFiles");
|
|
426
426
|
Object.defineProperty(exports, "usePluginFiles", { enumerable: true, get: function () { return pluginFiles_1.usePluginFiles; } });
|
|
427
|
-
Object.defineProperty(exports, "
|
|
428
|
-
Object.defineProperty(exports, "
|
|
427
|
+
Object.defineProperty(exports, "pluginFiles", { enumerable: true, get: function () { return pluginFiles_1.pluginFiles; } });
|
|
428
|
+
Object.defineProperty(exports, "pluginFilesIn", { enumerable: true, get: function () { return pluginFiles_1.pluginFilesIn; } });
|
|
429
|
+
Object.defineProperty(exports, "DEFAULT_PLUGINS_DIR", { enumerable: true, get: function () { return pluginFiles_1.DEFAULT_PLUGINS_DIR; } });
|
|
429
430
|
__exportStar(require("./router"), exports);
|
|
430
431
|
__exportStar(require("./template"), exports);
|
|
431
432
|
var richText_1 = require("./richText");
|
package/lib/pluginFiles.d.ts
CHANGED
|
@@ -1,16 +1,28 @@
|
|
|
1
1
|
import type { UAGServer } from './index';
|
|
2
|
-
/**
|
|
3
|
-
export declare const
|
|
2
|
+
/** The image's plugins folder: add-ons are mounted in folders under it. */
|
|
3
|
+
export declare const DEFAULT_PLUGINS_DIR = "/app/plugins";
|
|
4
4
|
/**
|
|
5
|
-
* The plugins
|
|
6
|
-
*
|
|
5
|
+
* The plugins files a path holds, in load order:
|
|
6
|
+
* - a file: that file;
|
|
7
|
+
* - a folder with an `index.mjs`: that file alone (one plugins folder);
|
|
8
|
+
* - any other folder: the `index.mjs` of each folder directly in it, in
|
|
9
|
+
* name order (one per add-on), skipping folders whose name starts with
|
|
10
|
+
* "." and `node_modules`.
|
|
7
11
|
*/
|
|
8
|
-
export declare function
|
|
12
|
+
export declare function pluginFilesIn(target: string): string[];
|
|
9
13
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* (an extra, with its plugins), or a list mixing modules and
|
|
13
|
-
* plugins, each module used in turn. Plugins are trusted code: they run in
|
|
14
|
-
* the UAG process.
|
|
14
|
+
* The plugins files to load: those UAG_PLUGINS names (a file, a folder, or
|
|
15
|
+
* "off"), or else those in the image's plugins folder when it exists.
|
|
15
16
|
*/
|
|
16
|
-
export declare function
|
|
17
|
+
export declare function pluginFiles(env?: Record<string, string | undefined>, defaultDir?: string): string[];
|
|
18
|
+
/**
|
|
19
|
+
* Load the plugins files, if any, and use each in turn, named by its path in
|
|
20
|
+
* error messages. A file's default export is a plugin or a list of plugins;
|
|
21
|
+
* or a module (an extra, with its plugins), or a list mixing modules and
|
|
22
|
+
* plugins, each module used in turn. Several files merge as several modules
|
|
23
|
+
* do: a module used again by name is used once, and two files that register
|
|
24
|
+
* the same plugin, plugin type, route or component stop startup, naming both.
|
|
25
|
+
* Plugins are trusted code: they run in the UAG process. Returns the files
|
|
26
|
+
* loaded.
|
|
27
|
+
*/
|
|
28
|
+
export declare function usePluginFiles(server: Pick<UAGServer, 'use'>, env?: Record<string, string | undefined>, defaultDir?: string): Promise<string[]>;
|
package/lib/pluginFiles.js
CHANGED
|
@@ -3,26 +3,59 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.
|
|
7
|
-
exports.
|
|
6
|
+
exports.DEFAULT_PLUGINS_DIR = void 0;
|
|
7
|
+
exports.pluginFilesIn = pluginFilesIn;
|
|
8
|
+
exports.pluginFiles = pluginFiles;
|
|
8
9
|
exports.usePluginFiles = usePluginFiles;
|
|
9
10
|
const fs_1 = __importDefault(require("fs"));
|
|
10
11
|
const path_1 = __importDefault(require("path"));
|
|
11
12
|
const url_1 = require("url");
|
|
12
13
|
const appserver_1 = require("@formio/appserver");
|
|
13
|
-
/**
|
|
14
|
-
exports.
|
|
14
|
+
/** The image's plugins folder: add-ons are mounted in folders under it. */
|
|
15
|
+
exports.DEFAULT_PLUGINS_DIR = '/app/plugins';
|
|
16
|
+
/** The file that makes a folder one plugins folder. */
|
|
17
|
+
const INDEX = 'index.mjs';
|
|
18
|
+
const isDirectory = (target) => {
|
|
19
|
+
try {
|
|
20
|
+
return fs_1.default.statSync(target).isDirectory();
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* The plugins files a path holds, in load order:
|
|
28
|
+
* - a file: that file;
|
|
29
|
+
* - a folder with an `index.mjs`: that file alone (one plugins folder);
|
|
30
|
+
* - any other folder: the `index.mjs` of each folder directly in it, in
|
|
31
|
+
* name order (one per add-on), skipping folders whose name starts with
|
|
32
|
+
* "." and `node_modules`.
|
|
33
|
+
*/
|
|
34
|
+
function pluginFilesIn(target) {
|
|
35
|
+
if (!isDirectory(target))
|
|
36
|
+
return [target];
|
|
37
|
+
const own = path_1.default.join(target, INDEX);
|
|
38
|
+
if (fs_1.default.existsSync(own))
|
|
39
|
+
return [own];
|
|
40
|
+
return fs_1.default.readdirSync(target, { withFileTypes: true })
|
|
41
|
+
.filter((entry) => !entry.name.startsWith('.') && entry.name !== 'node_modules')
|
|
42
|
+
.filter((entry) => entry.isDirectory() || (entry.isSymbolicLink() && isDirectory(path_1.default.join(target, entry.name))))
|
|
43
|
+
.map((entry) => entry.name)
|
|
44
|
+
.sort()
|
|
45
|
+
.map((name) => path_1.default.join(target, name, INDEX))
|
|
46
|
+
.filter((file) => fs_1.default.existsSync(file));
|
|
47
|
+
}
|
|
15
48
|
/**
|
|
16
|
-
* The plugins
|
|
17
|
-
* image's
|
|
49
|
+
* The plugins files to load: those UAG_PLUGINS names (a file, a folder, or
|
|
50
|
+
* "off"), or else those in the image's plugins folder when it exists.
|
|
18
51
|
*/
|
|
19
|
-
function
|
|
52
|
+
function pluginFiles(env = process.env, defaultDir = exports.DEFAULT_PLUGINS_DIR) {
|
|
20
53
|
const setting = String(env.UAG_PLUGINS || '').trim();
|
|
21
54
|
if (setting.toLowerCase() === 'off')
|
|
22
|
-
return
|
|
55
|
+
return [];
|
|
23
56
|
if (setting)
|
|
24
|
-
return path_1.default.resolve(setting);
|
|
25
|
-
return fs_1.default.existsSync(
|
|
57
|
+
return pluginFilesIn(path_1.default.resolve(setting));
|
|
58
|
+
return fs_1.default.existsSync(defaultDir) ? pluginFilesIn(defaultDir) : [];
|
|
26
59
|
}
|
|
27
60
|
/** A real dynamic import(), kept one after compilation to CommonJS. */
|
|
28
61
|
const dynamicImport = new Function('specifier', 'return import(specifier)');
|
|
@@ -36,27 +69,33 @@ const dynamicImport = new Function('specifier', 'return import(specifier)');
|
|
|
36
69
|
const MODULE_KEYS = ['uag', 'routes', 'components', 'initialize', 'plugins', 'pluginTypes', 'config', 'formRoutes', 'actions', 'hooks', 'providers', 'processors'];
|
|
37
70
|
const isModule = (entry) => !!entry && typeof entry === 'object' && !Array.isArray(entry) && typeof entry.type !== 'string'
|
|
38
71
|
&& (MODULE_KEYS.some((key) => key in entry) || typeof entry.settings === 'function');
|
|
39
|
-
/**
|
|
40
|
-
|
|
41
|
-
* messages. Its default export is a plugin or a list of plugins; or a module
|
|
42
|
-
* (an extra, with its plugins), or a list mixing modules and
|
|
43
|
-
* plugins, each module used in turn. Plugins are trusted code: they run in
|
|
44
|
-
* the UAG process.
|
|
45
|
-
*/
|
|
46
|
-
async function usePluginFiles(server, env = process.env, defaultFile = exports.DEFAULT_PLUGINS_FILE) {
|
|
47
|
-
const file = pluginsFile(env, defaultFile);
|
|
48
|
-
if (!file)
|
|
49
|
-
return undefined;
|
|
72
|
+
/** Use one plugins file, named by its path in error messages. */
|
|
73
|
+
async function usePluginFile(server, file) {
|
|
50
74
|
const exported = (await dynamicImport((0, url_1.pathToFileURL)(file).href))?.default;
|
|
51
75
|
const entries = [].concat(exported ?? []);
|
|
52
76
|
if (entries.some(isModule)) {
|
|
53
77
|
const plugins = entries.filter((entry) => !isModule(entry));
|
|
54
78
|
server.use([...entries.filter(isModule), ...(plugins.length ? [{ plugins }] : [])], file);
|
|
55
79
|
console.log(`[formio-uag] Plugins and modules loaded from ${file}.`);
|
|
56
|
-
return
|
|
80
|
+
return;
|
|
57
81
|
}
|
|
58
82
|
const plugins = await (0, appserver_1.loadPluginFile)(file);
|
|
59
83
|
server.use({ plugins }, file);
|
|
60
84
|
console.log(`[formio-uag] Plugins loaded from ${file}.`);
|
|
61
|
-
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Load the plugins files, if any, and use each in turn, named by its path in
|
|
88
|
+
* error messages. A file's default export is a plugin or a list of plugins;
|
|
89
|
+
* or a module (an extra, with its plugins), or a list mixing modules and
|
|
90
|
+
* plugins, each module used in turn. Several files merge as several modules
|
|
91
|
+
* do: a module used again by name is used once, and two files that register
|
|
92
|
+
* the same plugin, plugin type, route or component stop startup, naming both.
|
|
93
|
+
* Plugins are trusted code: they run in the UAG process. Returns the files
|
|
94
|
+
* loaded.
|
|
95
|
+
*/
|
|
96
|
+
async function usePluginFiles(server, env = process.env, defaultDir = exports.DEFAULT_PLUGINS_DIR) {
|
|
97
|
+
const files = pluginFiles(env, defaultDir);
|
|
98
|
+
for (const file of files)
|
|
99
|
+
await usePluginFile(server, file);
|
|
100
|
+
return files;
|
|
62
101
|
}
|
package/lib/testing.js
CHANGED
|
@@ -48,9 +48,10 @@ function testForm(definition, options = {}) {
|
|
|
48
48
|
}
|
|
49
49
|
return bytes;
|
|
50
50
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
const files = options.files || this?.files;
|
|
52
|
+
if (files)
|
|
53
|
+
return files(value, readOptions);
|
|
54
|
+
throw new Error('testForm reads base64 (data: URL) files only: give testForm, createTestModuleApi or mountModuleRoutes a "files" function for others.');
|
|
54
55
|
}
|
|
55
56
|
};
|
|
56
57
|
return form;
|
|
@@ -96,6 +97,10 @@ function settingsOf(module, options) {
|
|
|
96
97
|
throw new Error(`Module ${module.name}: "settings" returned a promise; it must return the settings themselves (no async)`);
|
|
97
98
|
return Object.freeze({ ...(settings || {}) });
|
|
98
99
|
}
|
|
100
|
+
/** The form, reading stored values that are not base64 through `files` unless it has its own. */
|
|
101
|
+
function withFiles(form, files) {
|
|
102
|
+
return files && form && typeof form === 'object' && !form.files ? { ...form, files } : form;
|
|
103
|
+
}
|
|
99
104
|
/** The UAG's module API for a module, in a test. */
|
|
100
105
|
async function createTestModuleApi(module, options = {}) {
|
|
101
106
|
const loaded = loadedOf(module, settingsOf(module, options));
|
|
@@ -121,6 +126,11 @@ async function createTestModuleApi(module, options = {}) {
|
|
|
121
126
|
if (options.plugins?.length)
|
|
122
127
|
registry.add(options.plugins, 'test');
|
|
123
128
|
await registry.initialize({ server: {} });
|
|
129
|
+
if (options.files) {
|
|
130
|
+
// A form without its own "files" function reads other stored values through this one.
|
|
131
|
+
const read = api.files.read;
|
|
132
|
+
api.files.read = (form, value, auth, readOptions) => read(withFiles(form, options.files), value, auth, readOptions);
|
|
133
|
+
}
|
|
124
134
|
return api;
|
|
125
135
|
}
|
|
126
136
|
/**
|
|
@@ -155,7 +165,7 @@ async function mountModuleRoutes(module, options) {
|
|
|
155
165
|
};
|
|
156
166
|
const router = (0, appserver_1.moduleFormRouter)({}, loaded, api, host);
|
|
157
167
|
const withForm = (req, res, next) => {
|
|
158
|
-
req.form = options.form;
|
|
168
|
+
req.form = withFiles(options.form, options.files);
|
|
159
169
|
req.auth = options.auth || testAuth();
|
|
160
170
|
next();
|
|
161
171
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@formio/uag",
|
|
3
|
-
"version": "2.0.0-rc.
|
|
3
|
+
"version": "2.0.0-rc.8",
|
|
4
4
|
"mcpName": "io.form/formio-uag",
|
|
5
5
|
"description": "The Form.io Universal Agent Gateway (UAG).",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -45,9 +45,9 @@
|
|
|
45
45
|
"author": "Form.io Developers",
|
|
46
46
|
"license": "MIT",
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@formio/appserver": "^2.26.
|
|
48
|
+
"@formio/appserver": "^2.26.1",
|
|
49
49
|
"@formio/core": "2.9.0",
|
|
50
|
-
"@formio/uag-sdk": "^2.0.0-rc.
|
|
50
|
+
"@formio/uag-sdk": "^2.0.0-rc.8",
|
|
51
51
|
"@modelcontextprotocol/sdk": "1.32.2",
|
|
52
52
|
"cors": "^2.8.6",
|
|
53
53
|
"debug": "^4.4.3",
|