From 21d7558876251d248168ef27281836478ca42df2 Mon Sep 17 00:00:00 2001 From: Gauthier Dandele <92022724+GogoVega@users.noreply.github.com> Date: Tue, 22 Sep 2026 14:35:51 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=A4=96=20Merge=20PR=20#75538=20[node-red?= =?UTF-8?q?=5F=5Futil]=20Update=20types=20to=20Node-RED=20v5=20by=20@GogoV?= =?UTF-8?q?ega?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- types/node-red__util/.npmignore | 1 + types/node-red__util/index.d.ts | 105 ++- types/node-red__util/node-red__util-tests.ts | 31 +- types/node-red__util/package.json | 8 +- types/node-red__util/v3/.npmignore | 5 + types/node-red__util/v3/index.d.ts | 630 ++++++++++++++++++ .../node-red__util/v3/node-red__util-tests.ts | 271 ++++++++ types/node-red__util/v3/package.json | 27 + types/node-red__util/v3/tsconfig.json | 19 + 9 files changed, 1073 insertions(+), 24 deletions(-) create mode 100644 types/node-red__util/v3/.npmignore create mode 100644 types/node-red__util/v3/index.d.ts create mode 100644 types/node-red__util/v3/node-red__util-tests.ts create mode 100644 types/node-red__util/v3/package.json create mode 100644 types/node-red__util/v3/tsconfig.json diff --git a/types/node-red__util/.npmignore b/types/node-red__util/.npmignore index 93e307400a5456..ca560e3b05de3c 100644 --- a/types/node-red__util/.npmignore +++ b/types/node-red__util/.npmignore @@ -3,3 +3,4 @@ !**/*.d.cts !**/*.d.mts !**/*.d.*.ts +/v3/ \ No newline at end of file diff --git a/types/node-red__util/index.d.ts b/types/node-red__util/index.d.ts index 05d511cf516e89..6845c6cd6d3b12 100644 --- a/types/node-red__util/index.d.ts +++ b/types/node-red__util/index.d.ts @@ -1,3 +1,4 @@ +import { SpawnOptions } from "child_process"; import { EventEmitter } from "events"; import { Expression as JsonataExpression } from "jsonata"; @@ -9,6 +10,44 @@ declare const util: util.UtilModule; export = util; declare namespace util { + /** + * Runtime events + */ + interface Events extends EventEmitter {} // eslint-disable-line @typescript-eslint/no-empty-interface + + interface ExecRunOptions extends SpawnOptions { + shell?: boolean; + } + + interface ExecRunResult { + code: number | null; + stdout: string; + stderr: string; + } + + /** + * Run a system command with stdout/err being emitted as 'event-log' events + * on the @node-red/util/events handler. + * + * The main arguments to this function are the same as passed to `child_process.spawn` + * + * @param command - the command to run + * @param args - arguments for the command + * @param options - options to pass child_process.spawn + * @param emit - whether to emit events to the event-log for each line of stdout/err + * @return A promise that resolves (rc=0) or rejects (rc!=0) when the command completes. The value + * of the promise is an object of the form: + * + * { + * code: , + * stdout: , + * stderr: + * } + */ + interface Exec { + run(command: string, args?: string[], options?: {}, emit?: boolean): Promise; + } + interface LogMessageObject { level: number; msg?: LogMessage | undefined; @@ -98,6 +137,10 @@ declare namespace util { // eslint-disable-next-line @typescript-eslint/naming-convention interface I18n { + /** + * The default language of the runtime + */ + readonly defaultLang: "en-US"; /** * Perform a message catalog lookup. */ @@ -126,7 +169,7 @@ declare namespace util { interface Util { /** - * Generates a psuedo-unique-random id. + * Generates a pseudo-unique-random id. * @returns a random-ish id */ generateId(): string; @@ -157,8 +200,8 @@ declare namespace util { /** * Compares two objects, handling various JavaScript types. * - * @param obj1 - * @param obj2 + * @param obj1 - the first object + * @param obj2 - the second object * @returns whether the two objects are the same */ compareObjects(obj1: object, obj2: object): boolean; @@ -181,32 +224,47 @@ declare namespace util { * @param msg - the message object to use for cross-references * @param toString - whether to convert the returned array to a string * @returns the normalised expression + * @throws Will throw an error if the expression is incorrect */ normalisePropertyExpression(str: string, msg?: registry.NodeMessage, toString?: false): Array; normalisePropertyExpression(str: string, msg: registry.NodeMessage, toString: true): string; /** * Gets a property of a message object. * - * Unlike `getObjectProperty`, this function will strip `msg.` from the + * Unlike {@link getObjectProperty}, this function will strip `msg.` from the * front of the property expression if present. * * @param msg - the message object * @param expr - the property expression * @returns the message property, or undefined if it does not exist + * @throws Will throw an error if the *parent* of the property does not exist */ getMessageProperty(msg: registry.NodeMessage, expr: string): any; /** * Gets a property of an object. + * Given the object: + * + * { + * "pet": { + * "type": "cat" + * } + * } + * + * - `pet.type` will return `"cat"`. + * - `pet.name` will return `undefined` + * - `car` will return `undefined` + * - `car.type` will throw an Error (as `car` does not exist) * * @param msg - the object * @param expr - the property expression * @returns the object property, or undefined if it does not exist + * @throws Will throw an error if the *parent* of the property does not exist */ getObjectProperty(msg: registry.NodeMessage, expr: string): any; /** * Sets a property of a message object. * - * Unlike `setObjectProperty`, this function will strip `msg.` from the + * Unlike {@link setObjectProperty}, this function will strip `msg.` from the * front of the property expression if present. * * @param msg - the message object @@ -228,20 +286,10 @@ declare namespace util { * Get value of environment variable. * @param node - accessing node * @param name - name of variable + * @param flow - accessing flow * @returns value of env var */ - getSetting(node: registry.Node, name: string): string; - /** - * Checks if a String contains any Environment Variable specifiers and returns - * it with their values substituted in place. - * - * For example, if the env var `WHO` is set to `Joe`, the string `Hello ${WHO}!` - * will return `Hello Joe!`. - * @param value - the string to parse - * @param node - the node evaluating the property - * @returns The parsed string - */ - evaluateEnvProperty(value: string, node: registry.Node): string; + getSetting(node: registry.Node, name: string, flow?: runtime.Flow): string | undefined; /** * Parses a context property string, as generated by the TypedInput, to extract * the store name if present. @@ -281,7 +329,7 @@ declare namespace util { prepareJSONataExpression(value: string, node: registry.Node): JsonataExpression; /** * Evaluates a JSONata expression. - * The expression must have been prepared with `prepareJSONataExpression` + * The expression must have been prepared with {@link prepareJSONataExpression} * before passing to this function. * * @param expr - the prepared JSONata expression @@ -318,6 +366,7 @@ declare namespace util { // Used `boolean` in PromiseLike instead of `false` because it caused problems with `async` functions type HandlerFunction = (payload: T, callback: (err?: any) => void) => void | false | PromiseLike; // eslint-disable-line @typescript-eslint/no-invalid-void-type + /** @link https://nodered.org/docs/api/hooks/ */ interface Hooks { /** * A node has called `node.send()` with one or more messages. @@ -461,7 +510,17 @@ declare namespace util { * To remove all hooks with a given label, `*.my-hooks` can be used. */ remove(hookName: string): void; + + /** + * Check if the hook has been registered. + * @param hookName the name of the hook + */ has(hookName: string): boolean; + + /** + * Clears all registered hook handlers. + */ + clear(): void; } // #region Hook Event Objects @@ -534,6 +593,16 @@ declare namespace util { */ init(settings: runtime.LocalSettings): void; + /** + * Runtime events + */ + events: Events; + + /** + * Run system commands with event-log integration + */ + exec: Exec; + /** * Logging utilities */ diff --git a/types/node-red__util/node-red__util-tests.ts b/types/node-red__util/node-red__util-tests.ts index 4d1b238c80a083..8710fdcde85cad 100644 --- a/types/node-red__util/node-red__util-tests.ts +++ b/types/node-red__util/node-red__util-tests.ts @@ -2,6 +2,32 @@ import utilModule = require("@node-red/util"); import { Node, NodeMessage } from "@node-red/registry"; import { EventEmitter } from "events"; +function eventsTests() { + const events = utilModule.events; + + // $ExpectType Events + events.on("event-name", (_) => {}); + + // $ExpectType Events + events.once("event-name", (_) => {}); + + // $ExpectType Events + events.off("event-name", (_) => {}); +} + +function execTests() { + const exec = utilModule.exec; + + // $ExpectType Promise + exec.run("echo test"); + + // $ExpectType Promise + exec.run("echo", ["test"]); + + // $ExpectType Promise + exec.run("echo", ["test"], { shell: false }, false); +} + function i18nTests() { const i18n = utilModule.i18n; @@ -101,12 +127,9 @@ function utilTests(someNode: Node) { // $ExpectType boolean util.setObjectProperty({}, "key", { dataKey: "dataVal" }, true); - // $ExpectType string + // $ExpectType string | undefined util.getSetting(someNode, "name"); - // $ExpectType string - util.evaluateEnvProperty("name", someNode); - // $ExpectType any util.evaluateNodeProperty("value", "type", someNode, {}); // $ExpectType void diff --git a/types/node-red__util/package.json b/types/node-red__util/package.json index 129d14278945b3..ce0367a1fcc02d 100644 --- a/types/node-red__util/package.json +++ b/types/node-red__util/package.json @@ -1,7 +1,7 @@ { "private": true, "name": "@types/node-red__util", - "version": "1.3.9999", + "version": "5.0.9999", "projects": [ "https://github.com/node-red/node-red/tree/master/packages/node_modules/%40node-red/util", "https://nodered.org/" @@ -9,7 +9,7 @@ "dependencies": { "@types/node-red__registry": "*", "@types/node-red__runtime": "*", - "jsonata": "2.0.5" + "jsonata": "2.2.2" }, "devDependencies": { "@types/node-red__util": "workspace:." @@ -22,6 +22,10 @@ { "name": "Tadeusz Wyrzykowski", "githubUsername": "Shaquu" + }, + { + "name": "Gauthier Dandele", + "githubUsername": "GogoVega" } ] } diff --git a/types/node-red__util/v3/.npmignore b/types/node-red__util/v3/.npmignore new file mode 100644 index 00000000000000..93e307400a5456 --- /dev/null +++ b/types/node-red__util/v3/.npmignore @@ -0,0 +1,5 @@ +* +!**/*.d.ts +!**/*.d.cts +!**/*.d.mts +!**/*.d.*.ts diff --git a/types/node-red__util/v3/index.d.ts b/types/node-red__util/v3/index.d.ts new file mode 100644 index 00000000000000..717aa153451b97 --- /dev/null +++ b/types/node-red__util/v3/index.d.ts @@ -0,0 +1,630 @@ +import { SpawnOptions } from "child_process"; +import { EventEmitter } from "events"; +import { Expression as JsonataExpression } from "jsonata"; + +import * as registry from "@node-red/registry"; +import * as runtime from "@node-red/runtime"; + +declare const util: util.UtilModule; + +export = util; + +declare namespace util { + /** + * Runtime events + */ + interface Events extends EventEmitter {} // eslint-disable-line @typescript-eslint/no-empty-interface + + interface ExecRunOptions extends SpawnOptions { + shell?: boolean; + } + + interface ExecRunResult { + code: number | null; + stdout: string; + stderr: string; + } + + /** + * Run a system command with stdout/err being emitted as 'event-log' events + * on the @node-red/util/events handler. + * + * The main arguments to this function are the same as passed to `child_process.spawn` + * + * @param command - the command to run + * @param args - arguments for the command + * @param options - options to pass child_process.spawn + * @param emit - whether to emit events to the event-log for each line of stdout/err + * @return A promise that resolves (rc=0) or rejects (rc!=0) when the command completes. The value + * of the promise is an object of the form: + * + * { + * code: , + * stdout: , + * stderr: + * } + */ + interface Exec { + run(command: string, args?: string[], options?: {}, emit?: boolean): Promise; + } + + interface LogMessageObject { + level: number; + msg?: LogMessage | undefined; + type?: string | undefined; + id?: string | undefined; + name?: string | undefined; + } + + type LogMessage = any; + + interface Log { + /** Fatal level */ + readonly FATAL: number; + /** Error level */ + readonly ERROR: number; + /** Warn level */ + readonly WARN: number; + /** Info level */ + readonly INFO: number; + /** Debug level */ + readonly DEBUG: number; + /** Trace level */ + readonly TRACE: number; + /** Audit level */ + readonly AUDIT: number; + /** Metric level */ + readonly METRIC: number; + + /** + * Perform a message catalog lookup. + */ + _: I18nTFunction; + + /** + * Add a log handler + * @param - event emitter with `(msg: LogMessageObject) => void` listener on `log` events + */ + addHandler(handler: EventEmitter): void; + /** + * Remove a log handler + */ + removeHandler(handler: EventEmitter): void; + /** + * Log a message object + */ + log(msg: LogMessageObject): void; + /** + * Log a message at INFO level + */ + info(msg: LogMessage): void; + /** + * Log a message at WARN level + */ + warn(msg: LogMessage): void; + /** + * Log a message at ERROR level + */ + error(msg: LogMessage): void; + /** + * Log a message at TRACE level + */ + trace(msg: LogMessage): void; + /** + * Log a message at DEBUG level + */ + debug(msg: LogMessage): void; + /** + * Check if metrics are enabled + */ + metric(): boolean; + /** + * Log an audit event. + */ + audit(msg: LogMessageObject, req?: object): void; + } + + interface MessageCatalog { + namespace: string; + dir: string; + file: string; + } + + // eslint-disable-next-line @typescript-eslint/naming-convention + interface I18nTFunction { + (id: string, tplStrs?: Record): string; + } + + // eslint-disable-next-line @typescript-eslint/naming-convention + interface I18n { + /** + * The default language of the runtime + */ + readonly defaultLang: "en-US"; + /** + * Perform a message catalog lookup. + */ + _: I18nTFunction; + + /** + * Register multiple message catalogs with i18n. + */ + registerMessageCatalogs(catalogs: MessageCatalog[]): Promise; + + /** + * Register a message catalog with i18n. + */ + registerMessageCatalog(namespace: string, dir: string, file: string): Promise; + + /** + * Gets a message catalog. + */ + catalog(namespace: string, lang: string): MessageCatalog; + + /** + * Gets a list of languages a given catalog is available in. + */ + availableLanguages(namespace: string): string[]; + } + + interface Util { + /** + * Generates a pseudo-unique-random id. + * @returns a random-ish id + */ + generateId(): string; + /** + * Converts the provided argument to a String, using type-dependent + * methods. + * + * @param o - the property to convert to a String + * @returns the stringified version + */ + ensureString(o: unknown): string; + /** + * Converts the provided argument to a Buffer, using type-dependent + * methods. + * + * @param o - the property to convert to a Buffer + * @returns the Buffer version + */ + ensureBuffer(o: unknown): Buffer; + /** + * Safely clones a message object. This handles msg.req/msg.res objects that must + * not be cloned. + * + * @param msg - the message object to clone + * @returns the cloned message + */ + cloneMessage(msg: TNodeMessage): TNodeMessage; + /** + * Compares two objects, handling various JavaScript types. + * + * @param obj1 - the first object + * @param obj2 - the second object + * @returns whether the two objects are the same + */ + compareObjects(obj1: object, obj2: object): boolean; + /** + * Parses a property expression, such as `msg.foo.bar[3]` to validate it + * and convert it to a canonical version expressed as an Array of property + * names. + * + * For example, `a["b"].c` returns `['a','b','c']` + * + * If `msg` is provided, any internal cross-references will be evaluated against that + * object. Otherwise, it will return a nested set of properties + * + * For example, without msg set, 'a[msg.foo]' returns `['a', [ 'msg', 'foo'] ]` + * But if msg is set to '{"foo": "bar"}', 'a[msg.foo]' returns `['a', 'bar' ]` + * + * If `toString` is set to true, the returned array will be converted to a string + * + * @param str - the property expression + * @param msg - the message object to use for cross-references + * @param toString - whether to convert the returned array to a string + * @returns the normalised expression + * @throws Will throw an error if the expression is incorrect + */ + normalisePropertyExpression(str: string, msg?: registry.NodeMessage, toString?: false): Array; + normalisePropertyExpression(str: string, msg: registry.NodeMessage, toString: true): string; + /** + * Gets a property of a message object. + * + * Unlike {@link getObjectProperty}, this function will strip `msg.` from the + * front of the property expression if present. + * + * @param msg - the message object + * @param expr - the property expression + * @returns the message property, or undefined if it does not exist + * @throws Will throw an error if the *parent* of the property does not exist + */ + getMessageProperty(msg: registry.NodeMessage, expr: string): any; + /** + * Gets a property of an object. + * Given the object: + * + * { + * "pet": { + * "type": "cat" + * } + * } + * + * - `pet.type` will return `"cat"`. + * - `pet.name` will return `undefined` + * - `car` will return `undefined` + * - `car.type` will throw an Error (as `car` does not exist) + * + * @param msg - the object + * @param expr - the property expression + * @returns the object property, or undefined if it does not exist + * @throws Will throw an error if the *parent* of the property does not exist + */ + getObjectProperty(msg: registry.NodeMessage, expr: string): any; + /** + * Sets a property of a message object. + * + * Unlike {@link setObjectProperty}, this function will strip `msg.` from the + * front of the property expression if present. + * + * @param msg - the message object + * @param prop - the property expression + * @param value - the value to set + * @param createMissing - whether to create missing parent properties + */ + setMessageProperty(msg: registry.NodeMessage, prop: string, value: any, createMissing?: boolean): boolean; + /** + * Sets a property of an object. + * + * @param msg - the object + * @param prop - the property expression + * @param value - the value to set + * @param createMissing - whether to create missing parent properties + */ + setObjectProperty(msg: registry.NodeMessage, prop: string, value: any, createMissing?: boolean): boolean; + /** + * Get value of environment variable. + * @param node - accessing node + * @param name - name of variable + * @param flow - accessing flow + * @returns value of env var + */ + getSetting(node: registry.Node, name: string, flow?: runtime.Flow): string | undefined; + /** + * Parses a context property string, as generated by the TypedInput, to extract + * the store name if present. + * + * For example, `#:(file)::foo` results in ` { store: "file", key: "foo" }`. + * + * @param key - the context property string to parse + * @returns The parsed property + */ + parseContextStore(key: string): { store?: string | undefined; key: string }; + /** + * Evaluates a property value according to its type. + * + * @param value - the raw value + * @param type - the type of the value + * @param node - the node evaluating the property + * @param msg - the message object to evaluate against + * @param callback - (optional) called when the property is evaluated + * @returns The evaluted property, if no `callback` is provided + */ + evaluateNodeProperty(value: string, type: string, node: registry.Node, msg: registry.NodeMessage): any; + evaluateNodeProperty( + value: string, + type: string, + node: registry.Node, + msg: registry.NodeMessage, + callback: (err: Error | null, result: any) => void, + ): void; + /** + * Prepares a JSONata expression for evaluation. + * This attaches Node-RED specific functions to the expression. + * + * @param value - the JSONata expression + * @param node - the node evaluating the property + * @returns The JSONata expression that can be evaluated + */ + prepareJSONataExpression(value: string, node: registry.Node): JsonataExpression; + /** + * Evaluates a JSONata expression. + * The expression must have been prepared with {@link prepareJSONataExpression} + * before passing to this function. + * + * @param expr - the prepared JSONata expression + * @param msg - the message object to evaluate against + * @param callback - (optional) a callback with the result of the expression + */ + evaluateJSONataExpression( + expr: JsonataExpression, + msg: registry.NodeMessage, + ): any; + evaluateJSONataExpression( + expr: JsonataExpression, + msg: registry.NodeMessage, + callback: (error: Error | null, result: any) => void, + ): void; + /** + * Normalise a node type name to camel case. + * + * For example: `a-random node type` will normalise to `aRandomNodeType` + * + * @param name - the node type + * @returns The normalised name + */ + normaliseNodeTypeName(name: string): string; + /** + * Encode an object to JSON without losing information about non-JSON types + * such as Buffer and Function. + * + * *This function is closely tied to its reverse within the editor* + * + * @param msg + * @param opts + * @returns the encoded object + */ + encodeObject(msg: { msg: any }, opts?: { maxLength?: number | undefined }): { format: string; msg: string }; + } + + // Used `boolean` in PromiseLike instead of `false` because it caused problems with `async` functions + type HandlerFunction = (payload: T, callback: (err?: any) => void) => void | false | PromiseLike; // eslint-disable-line @typescript-eslint/no-invalid-void-type + + /** @link https://nodered.org/docs/api/hooks/ */ + interface Hooks { + /** + * A node has called `node.send()` with one or more messages. + * + * The hook is passed an array of `SendEvent` objects. + * The messages inside these objects are exactly what the node has passed to `node.send` + * - meaning there could be duplicate references to the same message object. + * + * This hook should complete synchronously in order to avoid unexpected behaviour. + * + * If it needs to do asynchronously work, it must clone and replace the message object in the event it receives. + * It must also set the `cloneMessage` property to `false` to ensure no subsequent cloning happens on the message. + * + * If the hook returns `false`, the messages will not proceed any further. + */ + add(hookName: "onSend", hookHandler: HandlerFunction): void; + + /** + * A message is about to be routed to its destination. + * + * The hook is passed a single `SendEvent`. + * + * This hook should complete synchronously in order to avoid unexpected behaviour. + * + * If it needs to do asynchronously work, it must clone and replace + * the message object in the event it receives. + * It must also set the `cloneMessage` property to `false` to ensure no subsequent cloning happens on the message. + * + * If the hook returns `false`, the message will not proceed any further. + */ + add(hookName: "preRoute", handlerFunction: HandlerFunction): void; + + /** + * A message is about to be delivered + * + * The hook is passed a single `SendEvent`. + * At this point, the local router has identified the node it is going to send to and set the `destination.node` property of the `SendEvent`. + * + * The message will have been cloned if needed. + * + * If the hook returns `false`, the messages will not proceed any further. + */ + add(hookName: "preDeliver", handlerFunction: HandlerFunction): void; // tslint:disable-line:unified-signatures + + /** + * A message has been dispatched to its destination. + * + * The hook is passed a single `SendEvent`. The message is delivered asynchronously to the hooks execution. + */ + add(hookName: "postDeliver", handlerFunction: HandlerFunction): void; // tslint:disable-line:unified-signatures + + /** + * A message is about to be received by a node. + * + * The hook is passed a `ReceiveEvent`. + * + * If the hook returns `false`, the messages will not proceed any further. + */ + add(hookName: "onReceive", handlerFunction: HandlerFunction): void; + + /** + * A message has been received by a node. + * + * The hook is passed `ReceiveEvent` when the message has been given to the node’s `input` handler. + */ + add(hookName: "postReceive", handlerFunction: HandlerFunction): void; // tslint:disable-line:unified-signatures + + /** + * A node has completed with a message or logged an error for it. + * + * The hook is passed a `CompleteEvent`. + */ + add(hookName: "onComplete", handlerFunction: HandlerFunction): void; + + /** + * Called before running `npm install` to install an npm module. + * + * The hook is passed an `InstallEvent` object that contains information about the module to be installed. + * + * The hook can modify the InstallEvent to change how npm is run. + * For example, the `args` array can be modified to change what arguments are passed to `npm`. + * + * If the hook returns `false`, the `npm install` will be skipped and the processing continue as if it had been run. + * This would allow some alternative mechanism to be used - as long as it results in the module being installed under the expected `node_modules` directory. + * + * If the hook throws an error, the install will be cleanly failed. + */ + add(hookName: "preInstall", handlerFunction: HandlerFunction): void; + + /** + * Called after `npm install` finishes installing an npm module. + * + * Note if a `preInstall` hook returned `false`, `npm install` will not have been run, but this hook will still get invoked. + * + * This hook can be used to run any post-install activity needed. + * + * If the hook throws an error, the install will be cleanly failed. + * + * If the preceding `npm install` returned an error, this hook will not be invoked. + */ + add(hookName: "postInstall", handlerFunction: HandlerFunction): void; // tslint:disable-line:unified-signatures + + /** + * Called before running `npm remove` to uninstall an npm module. + * + * The hook is passed an `UninstallEvent` object that contains information about the module to be removed. + * + * The hook can modify the UninstallEvent to change how npm is run. + * For example, the args array can be modified to change what arguments are passed to npm. + * + * If the hook returns false, the npm remove will be skipped and the processing continue as if it had been run. + * This would allow some alternative mechanism to be used. + * + * If the hook throws an error, the uninstall will be cleanly failed. + */ + add(hookName: "preUninstall", handlerFunction: HandlerFunction): void; + + /** + * Called after `npm remove` finishes removing an npm module. + * + * Note if a `preUninstall` hook returned `false`, `npm remove` will not have been run, but this hook will still get invoked. + * + * This hook can be used to run any post-uninstall activity needed. + * + * If the hook throws an error, it will be logged, but the uninstall will complete cleanly as we cannot rollback an `npm remove` after it has completed. + */ + add(hookName: "postUninstall", handlerFunction: HandlerFunction): void; // tslint:disable-line:unified-signatures + + /** + * Register a new hook handler. + * + * @see https://nodered.org/docs/api/hooks/#methods-add + */ + add(hookName: string, handlerFunction: HandlerFunction): void; + + /** + * Remove a hook handler. + * + * Only handlers that were registered with a labelled name (for example `onSend.my-hooks`) can be removed. + * + * To remove all hooks with a given label, `*.my-hooks` can be used. + */ + remove(hookName: string): void; + + /** + * Check if the hook has been registered. + * @param hookName the name of the hook + */ + has(hookName: string): boolean; + + /** + * Clears all registered hook handlers. + */ + clear(): void; + } + + // #region Hook Event Objects + + interface SendEvent { + msg: registry.NodeMessage; + source: { + /** node id */ + id: string; + node: registry.Node; + /** index of port being sent on */ + port: number; + }; + destination: { + /** node id */ + id: string; + node: undefined; + }; + cloneMessage: boolean; + } + + interface ReceiveEvent { + msg: registry.NodeMessage; + destination: { + /** node id */ + id: string; + node: registry.Node; + }; + } + + interface CompleteEvent { + msg: registry.NodeMessage; + node: { + id: string; + node: registry.Node; + }; + error?: Error; + } + + interface InstallEvent { + /** npm module name */ + module: string; + /** Version of the module that is being installed */ + version: string; + /** Optional url to install from */ + url?: string; + /** Directory to run the install in */ + dir: string; + isExisting?: boolean; + isUpgrade?: boolean; + /** Array of args that will be passed to npm */ + args: string[]; + } + + interface UninstallEvent { + /** npm module name */ + module: string; + /** Directory to run the remove in */ + dir: string; + /** Array of args that will be passed to npm */ + args: string[]; + } + + // #endregion + + interface UtilModule { + /** + * Initialise the module with the runtime settings + * @param settings + */ + init(settings: runtime.LocalSettings): void; + + /** + * Runtime events + */ + events: Events; + + /** + * Run system commands with event-log integration + */ + exec: Exec; + + /** + * Logging utilities + */ + log: Log; + + /** + * Internationalization utilities + */ + i18n: I18n; + + /** + * General utilities + */ + util: Util; + + /** + * Runtime hooks engine + */ + hooks: Hooks; + } +} diff --git a/types/node-red__util/v3/node-red__util-tests.ts b/types/node-red__util/v3/node-red__util-tests.ts new file mode 100644 index 00000000000000..9c824c3e957f3c --- /dev/null +++ b/types/node-red__util/v3/node-red__util-tests.ts @@ -0,0 +1,271 @@ +import utilModule = require("@node-red/util/v3"); +import { Node, NodeMessage } from "@node-red/registry"; +import { EventEmitter } from "events"; + +function eventsTests() { + const events = utilModule.events; + + // $ExpectType Events + events.on("event-name", (_) => {}); + + // $ExpectType Events + events.once("event-name", (_) => {}); + + // $ExpectType Events + events.off("event-name", (_) => {}); +} + +function execTests() { + const exec = utilModule.exec; + + // $ExpectType Promise + exec.run("echo test"); + + // $ExpectType Promise + exec.run("echo", ["test"]); + + // $ExpectType Promise + exec.run("echo", ["test"], { shell: false }, false); +} + +function i18nTests() { + const i18n = utilModule.i18n; + + // $ExpectType string + i18n._("my.key1"); + + // $ExpectType string + i18n._("my.key2", { dataKey: "dataVal" }); + + // $ExpectType string[] + i18n.availableLanguages("editor"); +} + +function logTests() { + const log = utilModule.log; + + // $ExpectType string + log._("my.key1"); + // $ExpectType string + log._("my.key2", { dataKey: "dataVal" }); + + const logHandler = new EventEmitter(); + log.addHandler(logHandler); + log.removeHandler(logHandler); + + // $ExpectType boolean + log.metric(); + // @ts-expect-error + log.log({}); + log.log({ level: log.INFO, msg: "log" }); + log.info("log info"); + log.warn("log warn"); + log.error("log error"); + log.trace("log trace"); + log.debug("log debug"); + log.audit({ level: log.INFO, msg: "audit" }); +} + +function utilTests(someNode: Node) { + const util = utilModule.util; + + // $ExpectType string + util.generateId(); + + // $ExpectType string + util.ensureString(123); + // $ExpectType string + util.ensureString({}); + // $ExpectType string + util.ensureString("abc"); + + // $ExpectType Buffer || Buffer + util.ensureBuffer(123); + // $ExpectType Buffer || Buffer + util.ensureBuffer({}); + // $ExpectType Buffer || Buffer + util.ensureBuffer("abc"); + + interface SomeNodeMsg extends NodeMessage { + key: string; + } + const msg: SomeNodeMsg = { + key: "value", + }; + const msgClone = util.cloneMessage(msg); + // $ExpectType string + const msgKey = msgClone.key; + + // $ExpectType boolean + util.compareObjects({}, {}); + + // $ExpectType (string | number)[] + util.normalisePropertyExpression("a[\"b\"].c"); + + // $ExpectType (string | number)[] + util.normalisePropertyExpression("a[msg.foo]", msg); + + // $ExpectType string + util.normalisePropertyExpression("a[msg.foo]", msg, true); + + // $ExpectType (string | number)[] + util.normalisePropertyExpression("a[msg.foo]", msg, false); + + // $ExpectType any + util.getMessageProperty({}, "key"); + + // $ExpectType any + util.getObjectProperty({}, "key"); + + // $ExpectType boolean + util.setMessageProperty({}, "key", { dataKey: "dataVal" }); + // $ExpectType boolean + util.setMessageProperty({}, "key", { dataKey: "dataVal" }, true); + + // $ExpectType boolean + util.setObjectProperty({}, "key", { dataKey: "dataVal" }); + // $ExpectType boolean + util.setObjectProperty({}, "key", { dataKey: "dataVal" }, true); + + // $ExpectType string | undefined + util.getSetting(someNode, "name"); + + // $ExpectType any + util.evaluateNodeProperty("value", "type", someNode, {}); + // $ExpectType void + util.evaluateNodeProperty("value", "type", someNode, {}, (err: Error | null, res: any): void => {}); + + const parsedStore = util.parseContextStore("#:(file)::foo"); + // $ExpectType string | undefined + parsedStore.store; + // $ExpectType string + parsedStore.key; + + // $ExpectType Expression + const jsonataExpr = util.prepareJSONataExpression("expr", someNode); + + // $ExpectType any + util.evaluateJSONataExpression(jsonataExpr, {}); + // $ExpectType void + util.evaluateJSONataExpression(jsonataExpr, {}, (err: Error | null, res: any): void => {}); + + // $ExpectType string + util.normaliseNodeTypeName("a-random node type"); + + const encoded = util.encodeObject({ msg: 123 }); + // $ExpectType string + encoded.format; + // $ExpectType string + encoded.msg; +} + +function hookTests() { + const hooks = utilModule.hooks; + + // #region Hook payload types + hooks.add("onSend", payload => { + // $ExpectType SendEvent[] + payload; + }); + + hooks.add("preRoute", payload => { + // $ExpectType SendEvent + payload; + }); + + hooks.add("preDeliver", payload => { + // $ExpectType SendEvent + payload; + }); + + hooks.add("postDeliver", payload => { + // $ExpectType SendEvent + payload; + }); + + hooks.add("onReceive", payload => { + // $ExpectType ReceiveEvent + payload; + }); + + hooks.add("postReceive", payload => { + // $ExpectType ReceiveEvent + payload; + }); + + hooks.add("onComplete", payload => { + // $ExpectType CompleteEvent + payload; + }); + + hooks.add("preInstall", payload => { + // $ExpectType InstallEvent + payload; + }); + + hooks.add("postInstall", payload => { + // $ExpectType InstallEvent + payload; + }); + + hooks.add("preUninstall", payload => { + // $ExpectType UninstallEvent + payload; + }); + + hooks.add("postUninstall", payload => { + // $ExpectType UninstallEvent + payload; + }); + + hooks.add("customEvent", payload => { + // $ExpectType any + payload; + }); + + hooks.add("customEvent", (payload: string) => { + // $ExpectType string + payload; + }); + // #endregion + + // #region Hook handler finalization + hooks.add("onSend", payload => { + return; + }); + + hooks.add("onSend", (payload, done) => { + done(); + }); + + hooks.add("onSend", payload => { + return new Promise(resolve => { + resolve(); + }); + }); + + hooks.add("onSend", payload => { + return false; + }); + + hooks.add("onSend", (payload, done) => { + done(false); + }); + + hooks.add("onSend", payload => { + return new Promise(resolve => { + resolve(false); + }); + }); + + hooks.add("onSend", async payload => { + return false; + }); + + // any value in callback should be allowed + hooks.add("onSend", (payload, done) => { + done("Error"); + done(new Error("Error")); + }); + // #endregion +} diff --git a/types/node-red__util/v3/package.json b/types/node-red__util/v3/package.json new file mode 100644 index 00000000000000..45c2e553070585 --- /dev/null +++ b/types/node-red__util/v3/package.json @@ -0,0 +1,27 @@ +{ + "private": true, + "name": "@types/node-red__util", + "version": "3.1.9999", + "projects": [ + "https://github.com/node-red/node-red/tree/master/packages/node_modules/%40node-red/util", + "https://nodered.org/" + ], + "dependencies": { + "@types/node-red__registry": "*", + "@types/node-red__runtime": "*", + "jsonata": "1.8.6" + }, + "devDependencies": { + "@types/node-red__util": "workspace:." + }, + "owners": [ + { + "name": "Alex Kaul", + "githubUsername": "alexk111" + }, + { + "name": "Tadeusz Wyrzykowski", + "githubUsername": "Shaquu" + } + ] +} diff --git a/types/node-red__util/v3/tsconfig.json b/types/node-red__util/v3/tsconfig.json new file mode 100644 index 00000000000000..1f8d4ab69235d9 --- /dev/null +++ b/types/node-red__util/v3/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "module": "node16", + "lib": [ + "es6" + ], + "noImplicitAny": true, + "noImplicitThis": true, + "strictFunctionTypes": true, + "strictNullChecks": true, + "types": [], + "noEmit": true, + "forceConsistentCasingInFileNames": true + }, + "files": [ + "index.d.ts", + "node-red__util-tests.ts" + ] +}