Provide intellisense support for node.linkcall in monaco

function-call-subroutines
Steve-Mcl 2026-02-23 08:47:55 +00:00
parent 92e5594811
commit 72cdf3f1a0
2 changed files with 71 additions and 5 deletions

View File

@ -55,6 +55,7 @@ RED.editor.codeEditor.monaco = (function() {
const type = "monaco";
const monacoThemes = ["vs","vs-dark","hc-black"]; //TODO: consider setting hc-black autmatically based on acessability?
let userSelectedTheme;
let tsFuncData; //used to cache the original func.d.ts data for later use when updating linkcall targets
//TODO: get from externalModules.js For now this is enough for feature parity with ACE (and then some).
const knownModules = {
@ -143,6 +144,9 @@ RED.editor.codeEditor.monaco = (function() {
var typePath = "types/" + libPath;
$.get(typePath)
.done(function(data) {
if (libPath === "node-red/func.d.ts") {
tsFuncData = data; //cache the original func.d.ts data for later use when updating linkcall targets
}
modulesCache[libPath] = data;
if(!preloadOnly) {
loadedLibs.JS[libModule] = monaco.languages.typescript.javascriptDefaults.addExtraLib(data, "file://types/" + libPackage + "/" + libModule + "/index.d.ts");
@ -924,6 +928,62 @@ RED.editor.codeEditor.monaco = (function() {
//check if extraLibs are to be loaded (e.g. fs or os)
refreshModuleLibs(editorOptions.extraLibs)
// update func.d.ts definitions for up-to-date link-in target names in the node.linkcall() definition
if (tsFuncData && options.node && options.node.type === 'function') {
const getUpdatedLinkcallDefs = () => {
const linkInNodes = RED.nodes.filterNodes({type:'link in'})
const viableLinkTargets = []
for (const target of linkInNodes) {
// links on same flow/subflow as caller node are valid targets
// however links inside a (different) subflow are not valid targets.
if (target.z === options.node.z || !RED.nodes.subflow(target.z)) {
viableLinkTargets.push(target)
}
}
const targetsByName = [...new Set(viableLinkTargets.map(node => node.name || '').filter(name => name.length > 0))].map(s => JSON.stringify(s));
const targetsById = [...new Set(viableLinkTargets.map(node => node.id))].map(s => JSON.stringify(s));
const targets = [...targetsByName, ...targetsById].filter(s => s && s.length > 0).join("|") || 'string';
return `
// #region:linkcall
/**
* Utility function to call, a reusable flow defined as a subroutine (link-in ~ <nodes> ~ link-out).
* When the \`linkcall\` function resolves, it returns the \`msg\` object returned by the link-out
* (return) node of the subroutine.
*
* @param {string} target - the name or ID of the target link-in subroutine to call
* @param {Object} msg - the message object to pass to the subroutine
* @param {Object} [options] - call options
* @param {number} [options.timeout=5000] - the maximum time to wait for a response (default: 5000ms)
* @return {Promise<Object>} - resolves with the returned message
*
* @example Call "greeting-person" subroutine by name:
* \`\`\`javascript
* msg.payload = "Joe Bloggs";
* const resultMsg = await node.linkcall("greeting-person", msg, {timeout: 10000});
* msg.payload = resultMsg.payload; // payload = "Hello Joe Bloggs"
* return msg; // return updated msg
* \`\`\`
*
* @example Call "greeting-person" subroutine by node id:
* \`\`\`javascript
* msg.payload = "John Doe";
* const result = await node.linkcall("a1b2c3d4e5f6", msg); // result.payload will be "Hello John Doe"
* return result; // return new result object
* \`\`\`
*/
static linkcall(target: ${targets}, msg: object, options?: { timeout?: number; [key: string]: any }): Promise<object>;
// #endregion:linkcall
`
}
const newDefs = getUpdatedLinkcallDefs();
data = tsFuncData.replace(/\/\/ #region:linkcall[\s\S]*?\/\/ #endregion:linkcall/g, newDefs);
if (loadedLibs.JS.func) {
loadedLibs.JS.func.dispose();
loadedLibs.JS.func = null;
}
loadedLibs.JS.func = monaco.languages.typescript.javascriptDefaults.addExtraLib(data, 'file://types/node-red/func/index.d.ts');
}
function refreshModuleLibs(extraModuleLibs) {
var defs = [];
var imports = [];

View File

@ -483,9 +483,15 @@
}
});
var buildEditor = function(id, stateId, focus, value, defaultValue, extraLibs, offset) {
var editor = RED.editor.createEditor({
const buildEditor = function(id, node, focus, value, defaultValue, extraLibs, offset) {
const stateId = `${node.id}/${id}`;
const editor = RED.editor.createEditor({
id: id,
node: {
id: node.id,
type: node.type,
z: node.z
},
mode: 'ace/mode/nrjavascript',
value: value || defaultValue || "",
stateId: stateId,
@ -512,9 +518,9 @@
editor.__stateId = stateId;
return editor;
}
this.initEditor = buildEditor('node-input-init-editor', this.id + "/" + "initEditor", false, $("#node-input-initialize").val(), RED._("node-red:function.text.initialize"), undefined, 0);
this.editor = buildEditor('node-input-func-editor', this.id + "/" + "editor", true, $("#node-input-func").val(), undefined, that.libs || [], undefined, -1);
this.finalizeEditor = buildEditor('node-input-finalize-editor', this.id + "/" + "finalizeEditor", false, $("#node-input-finalize").val(), RED._("node-red:function.text.finalize"), undefined, 0);
this.initEditor = buildEditor('node-input-init-editor', this, false, $("#node-input-initialize").val(), RED._("node-red:function.text.initialize"), undefined, 0);
this.editor = buildEditor('node-input-func-editor', this, true, $("#node-input-func").val(), undefined, that.libs || [], undefined, -1);
this.finalizeEditor = buildEditor('node-input-finalize-editor', this, false, $("#node-input-finalize").val(), RED._("node-red:function.text.finalize"), undefined, 0);
RED.library.create({
url:"functions", // where to get the data from