This commit is contained in:
gongwenxin
2025-08-28 16:20:24 +08:00
parent 61e533cddf
commit 73e887ebf4
1539 changed files with 360926 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
src
docs
+178
View File
@@ -0,0 +1,178 @@
Copyright OpenJS Foundation and other contributors, https://openjsf.org/
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
+11
View File
@@ -0,0 +1,11 @@
@node-red/registry
====================
Node-RED node registry module.
This provides the node registry, responsible for discovering and managing the node
modules available to the Node-RED runtime.
### Source
The main Node-RED modules are maintained as a monorepo on [GitHub](https://github.com/node-red/node-red).
@@ -0,0 +1,65 @@
/**
* Copyright JS Foundation and other contributors, http://js.foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
/*
* This provides a list of node types that have at one time been included with
* the core of Node-RED but have since moved out to their own module.
*
* If a user has a flow that depends on one of these types and they do not have
* the new module installed, this will help them identify the missing module.
*/
var nodes = {
"irc in": {module:"node-red-node-irc"},
"irc out": {module:"node-red-node-irc"},
"irc-server": {module:"node-red-node-irc"},
"arduino in": {module:"node-red-node-arduino"},
"arduino out": {module:"node-red-node-arduino"},
"arduino-board": {module:"node-red-node-arduino"},
"redis out": {module:"node-red-node-redis"},
"mongodb": {module:"node-red-node-mongodb"},
"mongodb out": {module:"node-red-node-mongodb"},
"serial in": {module:"node-red-node-serialport"},
"serial out": {module:"node-red-node-serialport"},
"serial-port": {module:"node-red-node-serialport"},
"twitter-credentials": {module:"node-red-node-twitter"},
"twitter in": {module:"node-red-node-twitter"},
"twitter out": {module:"node-red-node-twitter"},
"e-mail": {module:"node-red-node-email"},
"e-mail in": {module:"node-red-node-email"},
"feedparse": {module:"node-red-node-feedparser"},
"sentiment": {module:"node-red-node-sentiment"},
"tail": {module:"node-red-node-tail"},
"rpi-gpio in": {module:"node-red-node-pi-gpio"},
"rpi-gpio out": {module:"node-red-node-pi-gpio"},
"rpi-mouse": {module:"node-red-node-pi-gpio"},
"rpi-keyboard": {module:"node-red-node-pi-gpio"}
}
module.exports = {
get: function(id) {
return nodes[id];
}
}
@@ -0,0 +1,308 @@
// This module handles the management of modules required by the runtime and flows.
// Essentially this means keeping track of what extra modules a flow requires,
// ensuring those modules are installed and providing a standard way for nodes
// to require those modules safely.
const fs = require("fs-extra");
const registryUtil = require("./util");
const path = require("path");
const clone = require("clone");
const exec = require("@node-red/util").exec;
const log = require("@node-red/util").log;
const hooks = require("@node-red/util").hooks;
const url = require("url");
const { createRequire } = require("module");
const BUILTIN_MODULES = require('module').builtinModules;
// TODO: outsource running npm to a plugin
const NPM_COMMAND = (process.platform === "win32") ? "npm.cmd" : "npm";
let registeredTypes = {};
let subflowTypes = {};
let settings;
let knownExternalModules = {};
let installEnabled = true;
let installAllowList = ['*'];
let installDenyList = [];
function getInstallDir() {
return path.resolve(settings.userDir || process.env.NODE_RED_HOME || ".");
}
let loggedLegacyWarning = false;
async function refreshExternalModules() {
if (!loggedLegacyWarning) {
loggedLegacyWarning = true;
const oldExternalModulesDir = path.join(path.resolve(settings.userDir || process.env.NODE_RED_HOME || "."),"externalModules");
if (fs.existsSync(oldExternalModulesDir)) {
try {
log.warn(log._("server.install.old-ext-mod-dir-warning",{oldDir:oldExternalModulesDir, newDir:getInstallDir()}))
} catch(err) {console.log(err)}
}
}
const externalModuleDir = getInstallDir();
try {
const pkgFile = JSON.parse(await fs.readFile(path.join(externalModuleDir,"package.json"),"utf-8"));
knownExternalModules = pkgFile.dependencies || {};
} catch(err) {
knownExternalModules = {};
}
}
function init(_settings) {
settings = _settings;
knownExternalModules = {};
installEnabled = true;
if (settings.externalModules && settings.externalModules.modules) {
if (settings.externalModules.modules.allowList || settings.externalModules.modules.denyList) {
installAllowList = settings.externalModules.modules.allowList;
installDenyList = settings.externalModules.modules.denyList;
}
if (settings.externalModules.modules.hasOwnProperty("allowInstall")) {
installEnabled = settings.externalModules.modules.allowInstall
}
}
installAllowList = registryUtil.parseModuleList(installAllowList);
installDenyList = registryUtil.parseModuleList(installDenyList);
}
function register(type, dynamicModuleListProperty) {
registeredTypes[type] = dynamicModuleListProperty;
}
function registerSubflow(type, subflowConfig) {
subflowTypes[type] = subflowConfig;
}
function requireModule(module) {
if (!registryUtil.checkModuleAllowed( module, null,installAllowList,installDenyList)) {
const e = new Error("Module not allowed");
e.code = "module_not_allowed";
throw e;
}
const parsedModule = parseModuleName(module);
if (parsedModule.builtin) {
return require(parsedModule.module + parsedModule.subpath);
}
if (!knownExternalModules[parsedModule.module]) {
const e = new Error("Module not allowed");
e.code = "module_not_allowed";
throw e;
}
const externalModuleDir = getInstallDir();
const moduleDir = path.join(externalModuleDir,"node_modules",module);
return require(moduleDir);
}
function importModule(module) {
if (!registryUtil.checkModuleAllowed( module, null,installAllowList,installDenyList)) {
const e = new Error("Module not allowed");
e.code = "module_not_allowed";
throw e;
}
const parsedModule = parseModuleName(module);
if (parsedModule.builtin) {
return import(parsedModule.module + parsedModule.subpath);
}
if (!knownExternalModules[parsedModule.module]) {
const e = new Error("Module not allowed");
e.code = "module_not_allowed";
throw e;
}
const externalModuleDir = getInstallDir();
const moduleDir = path.join(externalModuleDir,"node_modules",module);
// To handle both CJS and ESM we need to resolve the module to the
// specific file that is loaded when the module is required/imported
// As this won't be on the natural module search path, we use createRequire
// to access the module
const modulePath = createRequire(moduleDir).resolve(module)
// Import needs the full path to the module's main .js file
// It also needs to be a file:// url for Windows
const moduleUrl = url.pathToFileURL(modulePath);
return import(moduleUrl);
}
function parseModuleName(module) {
const match = /((?:@[^/]+\/)?[^/@]+)(\/[^/@]+)?(?:@([\s\S]+))?/.exec(module);
if (match) {
const moduleName = match[1]
let isBuiltIn = false
if (/^node:/.test(moduleName)) {
isBuiltIn = BUILTIN_MODULES.includes(moduleName.substring(5))
} else {
isBuiltIn = BUILTIN_MODULES.includes(moduleName)
}
return {
spec: module,
module: moduleName,
subpath: match[2] || '',
version: match[3],
builtin: isBuiltIn,
known: !!knownExternalModules[moduleName]
}
}
return null;
}
function isInstalled(moduleDetails) {
return moduleDetails.builtin || moduleDetails.known;
}
async function checkFlowDependencies(flowConfig) {
let nodes = clone(flowConfig);
await refreshExternalModules();
const checkedModules = {};
const promises = [];
const errors = [];
const checkedSubflows = {};
while (nodes.length > 0) {
let n = nodes.shift();
if (subflowTypes[n.type] && !checkedSubflows[n.type]) {
checkedSubflows[n.type] = true;
nodes = nodes.concat(subflowTypes[n.type].flow)
} else if (registeredTypes[n.type]) {
let nodeModules = n[registeredTypes[n.type]] || [];
if (!Array.isArray(nodeModules)) {
nodeModules = [nodeModules]
}
nodeModules.forEach(module => {
if (typeof module !== 'string') {
module = module.module || "";
}
if (module) {
let moduleDetails = parseModuleName(module)
if (moduleDetails && checkedModules[moduleDetails.module] === undefined) {
checkedModules[moduleDetails.module] = isInstalled(moduleDetails)
if (!checkedModules[moduleDetails.module]) {
if (installEnabled) {
promises.push(installModule(moduleDetails).catch(err => {
errors.push({module: moduleDetails,error:err});
}))
} else if (!installEnabled) {
const e = new Error("Module install disabled - externalModules.modules.allowInstall=false");
e.code = "install_not_allowed";
errors.push({module: moduleDetails,error:e});
}
} else if (!registryUtil.checkModuleAllowed( moduleDetails.module, moduleDetails.version,installAllowList,installDenyList)) {
const e = new Error("Module not allowed");
e.code = "module_not_allowed";
errors.push({module: moduleDetails,error:e});
}
}
}
})
}
}
return Promise.all(promises).then(refreshExternalModules).then(() => {
if (errors.length > 0) {
throw errors;
}
})
}
async function ensureModuleDir() {
const installDir = getInstallDir();
if (!fs.existsSync(installDir)) {
await fs.ensureDir(installDir);
}
const pkgFile = path.join(installDir,"package.json");
if (!fs.existsSync(pkgFile)) {
await fs.writeFile(path.join(installDir,"package.json"),`{
"name": "Node-RED-External-Modules",
"description": "These modules are automatically installed by Node-RED to use in Function nodes.",
"version": "1.0.0",
"private": true,
"dependencies": {}
}`)
}
}
let installLock = Promise.resolve()
async function installModule(moduleDetails) {
const result = installLock.then(async () => {
let installSpec = moduleDetails.module;
if (!registryUtil.checkModuleAllowed( moduleDetails.module, moduleDetails.version,installAllowList,installDenyList)) {
const e = new Error("Install not allowed");
e.code = "install_not_allowed";
throw e;
}
if (moduleDetails.version) {
installSpec = installSpec+"@"+moduleDetails.version;
}
log.info(log._("server.install.installing",{name: moduleDetails.module,version: moduleDetails.version||"latest"}));
const installDir = getInstallDir();
await ensureModuleDir();
let triggerPayload = {
"module": moduleDetails.module,
"version": moduleDetails.version,
"dir": installDir,
"args": ["--omit=dev","--engine-strict"]
}
return hooks.trigger("preInstall", triggerPayload).then((result) => {
// preInstall passed
// - run install
if (result !== false) {
let extraArgs = triggerPayload.args || [];
let args = ['install', ...extraArgs, installSpec]
log.trace(NPM_COMMAND + JSON.stringify(args));
return exec.run(NPM_COMMAND, args, { cwd: installDir, shell: true },true)
} else {
log.trace("skipping npm install");
}
}).then(() => {
return hooks.trigger("postInstall", triggerPayload)
}).then(() => {
log.info(log._("server.install.installed", { name: installSpec }));
const runtimeInstalledModules = settings.get("modules") || {};
runtimeInstalledModules[moduleDetails.module] = moduleDetails;
settings.set("modules",runtimeInstalledModules)
log.audit({event: "modules.install",module:moduleDetails.module, version:moduleDetails.version});
}).catch(result => {
var output = result.stderr || result.toString();
var e;
if (/E404/.test(output) || /ETARGET/.test(output)) {
log.error(log._("server.install.install-failed-not-found",{name:installSpec}));
e = new Error("Module not found");
e.code = 404;
throw e;
} else {
log.error(log._("server.install.install-failed-long",{name:installSpec}));
log.error("------------------------------------------");
log.error(output);
log.error("------------------------------------------");
e = new Error(log._("server.install.install-failed"));
e.code = "unexpected_error";
throw e;
}
})
})
installLock = result.catch(() => {})
return result
}
module.exports = {
init,
register,
registerSubflow,
checkFlowDependencies,
require: requireModule,
import: importModule
}
+328
View File
@@ -0,0 +1,328 @@
/*!
* Copyright JS Foundation and other contributors, http://js.foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
/**
* This module provides the node registry for the Node-RED runtime.
*
* It is responsible for loading node modules and making them available
* to the runtime.
*
* @namespace @node-red/registry
*/
var registry = require("./registry");
var loader = require("./loader");
var installer = require("./installer");
var library = require("./library");
const externalModules = require("./externalModules")
var plugins = require("./plugins");
/**
* Initialise the registry with a reference to a runtime object
* @param {Object} runtime - a runtime object
* @memberof @node-red/registry
*/
function init(runtime) {
installer.init(runtime.settings);
// Loader requires the full runtime object because it initialises
// the util module it. The Util module is responsible for constructing the
// RED object passed to node modules when they are loaded.
loader.init(runtime);
plugins.init(runtime.settings);
registry.init(runtime.settings,loader);
library.init();
externalModules.init(runtime.settings);
}
/**
* Triggers the intial discovery and loading of all Node-RED node modules.
* found on the node path.
* @return {Promise} - resolves when the registry has finised discovering node modules.
* @memberof @node-red/registry
*/
function load() {
registry.load();
return installer.checkPrereq().then(loader.load);
}
function addModule(module) {
return loader.addModule(module).then(function() {
return registry.getModuleInfo(module);
});
}
function enableNodeSet(typeOrId) {
return registry.enableNodeSet(typeOrId).then(function() {
var nodeSet = registry.getNodeInfo(typeOrId);
if (!nodeSet.loaded) {
return loader.loadNodeSet(registry.getFullNodeInfo(typeOrId)).then(function() {
return registry.getNodeInfo(typeOrId);
});
}
return Promise.resolve(nodeSet);
});
}
module.exports = {
init:init,
load:load,
clear: registry.clear,
/**
* Register a node constructor function.
*
* @param {Object} nodeSet - the Node Set object the constructor is for
* @param {String} type - the node type
* @param {Function} constructor - the node constructor function
* @function
* @memberof @node-red/registry
*/
registerType: registry.registerNodeConstructor,
/**
* Get a node constructor function.
*
* @param {String} type - the node type
* @return {Function} the node constructor function
* @function
* @memberof @node-red/registry
*/
get: registry.getNodeConstructor,
registerSubflow: registry.registerSubflow,
/**
* Get a node's set information.
*
* @param {String} type - the node type or set identifier
* @return {Object} the node set information
* @function
* @memberof @node-red/registry
*/
getNodeInfo: registry.getNodeInfo,
/**
* Get a list of all nodes in the registry.
*
* @return {Object} the node list
* @function
* @memberof @node-red/registry
*/
getNodeList: registry.getNodeList,
/**
* Get a modules's information.
*
* @param {String} type - the module identifier
* @return {Object} the module information
* @function
* @memberof @node-red/registry
*/
getModuleInfo: registry.getModuleInfo,
/**
* Get a list of all moduless in the registry.
*
* @return {Object} the module list
* @function
* @memberof @node-red/registry
*/
getModuleList: registry.getModuleList,
/**
* Get the HTML configs for all nodes in the registry.
*
* @param {String} lang - the language to return, default `en-US`
* @return {String} the node configs
* @function
* @memberof @node-red/registry
*/
getNodeConfigs: registry.getAllNodeConfigs,
/**
* Get the HTML config for a single node set.
*
* @param {String} id - the node identifier
* @param {String} lang - the language to return, default `en-US`
* @return {String} the node config
* @function
* @memberof @node-red/registry
*/
getNodeConfig: registry.getNodeConfig,
/**
* Get the local path to a node's icon file.
*
* @param {String} module - the module that provides the icon
* @param {String} icon - the name of the icon
* @return {String} the local path to the icon
* @function
* @memberof @node-red/registry
*/
getNodeIconPath: registry.getNodeIconPath,
/**
* Get the full list of all icons available.
*
* @return {String} the icon list
* @function
* @memberof @node-red/registry
*/
getNodeIcons: registry.getNodeIcons,
/**
* Enables a node set, making it available for use.
*
* @param {String} type - the node type or set identifier
* @return {Promise} A promise that resolves when the node set has been enabled
* @throws if the identifier is not recognised or runtime settings are unavailable
* @function
* @memberof @node-red/registry
*/
enableNode: enableNodeSet,
/**
* Disables a node set, making it unavailable for use.
*
* @param {String} type - the node type or set identifier
* @return {Promise} A promise that resolves when the node set has been disabled
* @throws if the identifier is not recognised or runtime settings are unavailable
* @function
* @memberof @node-red/registry
*/
disableNode: registry.disableNodeSet,
/**
* Loads a new module into the registry.
*
* This will rescan the node module paths looking for this module.
*
* @param {String} module - the name of the module to add
* @return {Promise<Object>} A promise that resolves with the module information once it has been added
* @throws if the module has already been added or the runtime settings are unavailable
* @function
* @memberof @node-red/registry
*/
addModule: addModule,
/**
* Removes a module from the registry.
*
* @param {String} module - the name of the module to remove
* @return {Promise<Array>} A promise that resolves with the list of removed node sets
* @throws if the module is not found or the runtime settings are unavailable
* @function
* @memberof @node-red/registry
*/
removeModule: registry.removeModule,
/**
* Installs a new node module using npm and then add to the registry
*
* @param {String|Buffer} module - the name of the module to install, or a Buffer containing a module tar file
* @param {String} version - the version of the module to install, default: `latest`
* @param {String} url - (optional) a url to install the module from
* @return {Promise<Array>} A promise that resolves with the module information once it has been installed
* @function
* @memberof @node-red/registry
*/
installModule: installer.installModule,
/**
* Uninstalls a module using npm
*
* @param {String} module - the name of the module to uninstall
* @return {Promise<Array>} A promise that resolves when the module has been removed
* @function
* @memberof @node-red/registry
*/
uninstallModule: installer.uninstallModule,
/**
* Update to internal list of available modules based on what has been actually
* loaded.
*
* The `externalModules.autoInstall` (previously `autoInstallModules`)
* runtime option means the runtime may try to install
* missing modules after the initial load is complete. If that flag is not set
* this function is used to remove the modules from the registry's saved list.
* @function
* @memberof @node-red/registry
*/
cleanModuleList: registry.cleanModuleList,
/**
* Check if the regisrty is able to install/remove modules.
*
* This is based on whether it has found `npm` on the command-line.
* @return {Boolean} whether the installer is enabled
*
* @function
* @memberof @node-red/registry
*/
installerEnabled: installer.installerEnabled,
/**
* Get a list of all example flows provided by nodes in the registry.
* @return {Object} an object, indexed by module, listing all example flows
*
* @function
* @memberof @node-red/registry
*/
getNodeExampleFlows: library.getExampleFlows,
/**
* Gets the full path to a node example
* @param {String} module - the name of the module providing the example
* @param {String} path - the relative path of the example
* @return {String} the full path to the example
*
* @function
* @memberof @node-red/registry
*/
getNodeExampleFlowPath: library.getExampleFlowPath,
/**
* Gets the full path to a module's resource file
* @param {String} module - the name of the module providing the resource file
* @param {String} path - the relative path of the resource file
* @return {String} the full path to the resource file
*
* @function
* @memberof @node-red/registry
*/
getModuleResource: registry.getModuleResource,
checkFlowDependencies: externalModules.checkFlowDependencies,
registerPlugin: plugins.registerPlugin,
getPlugin: plugins.getPlugin,
getPluginsByType: plugins.getPluginsByType,
getPluginList: plugins.getPluginList,
getPluginConfigs: plugins.getPluginConfigs,
getPluginConfig: plugins.getPluginConfig,
exportPluginSettings: plugins.exportPluginSettings,
deprecated: require("./deprecated")
};
@@ -0,0 +1,625 @@
/**
* Copyright JS Foundation and other contributors, http://js.foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
const path = require("path");
const os = require("os");
const fs = require("fs-extra");
const tar = require("tar");
const registry = require("./registry");
const registryUtil = require("./util");
const library = require("./library");
const {exec,log,events,hooks} = require("@node-red/util");
const child_process = require('child_process');
const plugins = require("./plugins");
const isWindows = process.platform === 'win32'
const npmCommand = isWindows ? 'npm.cmd' : 'npm';
let installerEnabled = false;
let settings;
const moduleRe = /^(@[^/@]+?[/])?[^/@]+?$/;
const slashRe = isWindows ? /\\|[/]/ : /[/]/;
const pkgurlRe = /^(https?|git(|\+https?|\+ssh|\+file)):\/\//;
const localtgzRe = /^([a-zA-Z]:|\/).+tgz$/;
// Default allow/deny lists
let installAllowList = ['*'];
let installDenyList = [];
let installAllAllowed = true;
let installVersionRestricted = false;
let updateAllowed = true;
let updateAllowList = ['*'];
let updateDenyList = [];
let updateAllAllowed = true;
function init(_settings) {
settings = _settings;
// TODO: This is duplicated in localfilesystem.js
// Should it *all* be managed by util?
installAllowList = ['*'];
installDenyList = [];
installAllAllowed = true;
installVersionRestricted = false;
updateAllowed = true;
updateAllowList = ['*'];
updateDenyList = [];
updateAllAllowed = true;
if (settings.externalModules && settings.externalModules.palette) {
if (settings.externalModules.palette.allowList || settings.externalModules.palette.denyList) {
installAllowList = settings.externalModules.palette.allowList;
installDenyList = settings.externalModules.palette.denyList;
}
if (settings.externalModules.palette.hasOwnProperty('allowUpdate')) {
updateAllowed = !!settings.externalModules.palette.allowUpdate;
}
if (settings.externalModules.palette.allowUpdateList || settings.externalModules.palette.denyUpdateList) {
updateAllowList = settings.externalModules.palette.allowUpdateList;
updateDenyList = settings.externalModules.palette.denyUpdateList;
}
}
installAllowList = registryUtil.parseModuleList(installAllowList);
installDenyList = registryUtil.parseModuleList(installDenyList);
installAllAllowed = installDenyList.length === 0;
if (!installAllAllowed) {
installAllowList.forEach(function(rule) {
installVersionRestricted = installVersionRestricted || (!!rule.version);
})
if (!installVersionRestricted) {
installDenyList.forEach(function(rule) {
installVersionRestricted = installVersionRestricted || (!!rule.version);
})
}
}
updateAllowList = registryUtil.parseModuleList(updateAllowList);
updateDenyList = registryUtil.parseModuleList(updateDenyList);
updateAllAllowed = updateAllowed ? updateDenyList.length === 0 : false;
}
var activePromise = Promise.resolve();
function checkModulePath(folder) {
var moduleName;
var moduleVersion;
var err;
var fullPath = path.resolve(folder);
var packageFile = path.join(fullPath,'package.json');
try {
var pkg = require(packageFile);
moduleName = pkg.name;
moduleVersion = pkg.version;
if (!pkg['node-red']) {
// TODO: nls
err = new Error("Invalid Node-RED module");
err.code = 'invalid_module';
throw err;
}
} catch(err2) {
err = new Error("Module not found");
err.code = 404;
throw err;
}
return {
name: moduleName,
version: moduleVersion
};
}
async function installModule(module,version,url) {
if (Buffer.isBuffer(module)) {
return installTarball(module)
}
module = module || "";
activePromise = activePromise.then(async function() {
//TODO: ensure module is 'safe'
var installName = module;
let isRegistryPackage = true;
var isUpgrade = false;
var isExisting = false;
if (url) {
if (pkgurlRe.test(url) || localtgzRe.test(url)) {
// Git remote url or Tarball url - check the valid package url
installName = localtgzRe.test(url) && slashRe.test(url) ? `"${url}"` : url;
isRegistryPackage = false;
} else {
log.warn(log._("server.install.install-failed-url",{name:module,url:url}));
const e = new Error("Invalid url");
e.code = "invalid_module_url";
throw e;
}
} else if (moduleRe.test(module)) {
// Simple module name - assume it can be npm installed
if (version) {
installName += "@"+version;
}
} else if (slashRe.test(module)) {
// A path - check if there's a valid package.json
installName = module;
let info = checkModulePath(module);
module = info.name;
isRegistryPackage = false;
} else {
log.warn(log._("server.install.install-failed-name",{name:module}));
const e = new Error("Invalid module name");
e.code = "invalid_module_name";
throw e;
}
if (!installAllAllowed) {
let installVersion = version;
if (installVersionRestricted && isRegistryPackage) {
installVersion = await getModuleVersionFromNPM(module, version);
}
if (!registryUtil.checkModuleAllowed(module,installVersion,installAllowList,installDenyList)) {
const e = new Error("Install not allowed");
e.code = "install_not_allowed";
throw e;
}
}
var info = registry.getModuleInfo(module);
if (info) {
if (!info.user) {
log.debug(`Installing existing module: ${module}`)
isExisting = true;
} else if (!version || info.version === version) {
var err = new Error("Module already loaded");
err.code = "module_already_loaded";
throw err;
}
isUpgrade = true;
} else {
isUpgrade = false;
}
if (isUpgrade && !updateAllAllowed) {
// Check this module is allowed to be upgraded...
if (!updateAllowed || !registryUtil.checkModuleAllowed(module,null,updateAllowList,updateDenyList)) {
const e = new Error("Update not allowed");
e.code = "update_not_allowed";
throw e;
}
}
if (!isUpgrade) {
log.info(log._("server.install.installing",{name: module,version: version||"latest"}));
} else {
log.info(log._("server.install.upgrading",{name: module,version: version||"latest"}));
}
var installDir = settings.userDir || process.env.NODE_RED_HOME || ".";
let triggerPayload = {
"module": module,
"version": version,
"url": url,
"dir": installDir,
"isExisting": isExisting,
"isUpgrade": isUpgrade,
"args": ['--no-audit','--no-update-notifier','--no-fund','--save','--save-prefix=~','--omit=dev','--engine-strict']
}
return hooks.trigger("preInstall", triggerPayload).then((result) => {
// preInstall passed
// - run install
if (result !== false) {
let extraArgs = triggerPayload.args || [];
let args = ['install', ...extraArgs, installName]
log.trace(npmCommand + JSON.stringify(args));
return exec.run(npmCommand,args,{ cwd: installDir, shell: true }, true)
} else {
log.trace("skipping npm install");
}
}).then(() => {
return hooks.trigger("postInstall", triggerPayload)
}).then(() => {
if (isExisting) {
// This is a module we already have installed as a non-user module.
// That means it was discovered when loading, but was not listed
// in package.json and has been hidden from the editor.
// The user has requested to install this module. Having run
// the npm install above, it will now be listed in package.json.
// Update the registry to mark it as a user module so it will
// be available to the editor.
log.info(log._("server.install.installed",{name:module}));
return require("./registry").setUserInstalled(module,true).then(reportAddedModules);
} else if (!isUpgrade) {
log.info(log._("server.install.installed",{name:module}));
return require("./index").addModule(module).then(reportAddedModules);
} else {
log.info(log._("server.install.upgraded",{name:module, version:version}));
events.emit("runtime-event",{id:"restart-required",payload:{type:"warning",text:"notification.warnings.restartRequired"},retain:true});
return require("./registry").setModulePendingUpdated(module,version);
}
}).catch(err => {
let e;
if (err.hook) {
// preInstall failed
log.warn(log._("server.install.install-failed-long",{name:module}));
log.warn("------------------------------------------");
log.warn(err.toString());
log.warn("------------------------------------------");
e = new Error(log._("server.install.install-failed")+": "+err.toString());
if (err.hook === "postInstall") {
return exec.run(npmCommand,["remove",module],{ cwd: installDir, shell: true }, false).finally(() => {
throw e;
})
}
} else {
// npm install failed
let output = err.stderr;
let lookFor404 = new RegExp(" 404 .*"+module,"m");
let lookForVersionNotFound = new RegExp("version not found: "+module+"@"+version,"m");
if (lookFor404.test(output)) {
log.warn(log._("server.install.install-failed-not-found",{name:module}));
e = new Error("Module not found");
e.code = 404;
} else if (isUpgrade && lookForVersionNotFound.test(output)) {
log.warn(log._("server.install.upgrade-failed-not-found",{name:module}));
e = new Error("Module not found");
e.code = 404;
} else {
log.warn(log._("server.install.install-failed-long",{name:module}));
log.warn("------------------------------------------");
log.warn(output);
log.warn("------------------------------------------");
e = new Error(log._("server.install.install-failed"));
}
}
if (e) {
throw e;
}
});
}).catch(err => {
// In case of error, reset activePromise to be resolvable
activePromise = Promise.resolve();
throw err;
});
return activePromise;
}
function reportAddedModules(info) {
if (info.nodes.length > 0) {
const installedTypes = [];
const errorSets = [];
for (var i=0;i<info.nodes.length;i++) {
const typeCount = info.nodes[i].types.length;
if (typeCount > 0) {
for (var j=0;j<typeCount;j++) {
installedTypes.push(" - "+
(info.nodes[i].module?info.nodes[i].module+":":"")+
info.nodes[i].types[j]+
(info.nodes[i].err?" : "+info.nodes[i].err:"")
);
}
} else if (info.nodes[i].err) {
errorSets.push(`[${info.nodes[i].id}] ${info.nodes[i].err}`)
}
}
if (errorSets.length > 0) {
errorSets.forEach(l => log.warn(l))
}
if (installedTypes.length > 0) {
log.info(log._("server.added-types"));
installedTypes.forEach(l => log.info(l))
}
}
return info;
}
function reportRemovedModules(removedNodes) {
//comms.publish("node/removed",removedNodes,false);
log.info(log._("server.removed-types"));
for (var j=0;j<removedNodes.length;j++) {
for (var i=0;i<removedNodes[j].types?.length;i++) {
log.info(" - "+(removedNodes[j].module?removedNodes[j].module+":":"")+removedNodes[j].types[i]);
}
}
log.info(log._("server.removed-plugins"));
for (let j=0;j<removedNodes.length;j++) {
for (var i=0;i<removedNodes[j].plugins?.length;i++) {
log.info(" - "+(removedNodes[j].module?removedNodes[j].module+":":"")+removedNodes[j].plugins[i].id);
}
}
return removedNodes;
}
async function getExistingPackageVersion(moduleName) {
try {
const packageFilename = path.join(settings.userDir || process.env.NODE_RED_HOME || "." , "package.json");
const pkg = await fs.readJson(packageFilename);
if (pkg.dependencies) {
return pkg.dependencies[moduleName];
}
} catch(err) {
}
return null;
}
async function getModuleVersionFromNPM(module, version) {
let installName = module;
if (version) {
installName += "@" + version;
}
return new Promise((resolve, reject) => {
child_process.execFile(`${npmCommand} info --json ${installName}`, { shell: true },function(err,stdout,stderr) {
try {
if (!stdout) {
log.warn(log._("server.install.install-failed-not-found",{name:module}));
e = new Error("Version not found");
e.code = 404;
reject(e);
return;
}
const response = JSON.parse(stdout);
if (response.error) {
if (response.error.code === "E404") {
log.warn(log._("server.install.install-failed-not-found",{name:module}));
e = new Error("Module not found");
e.code = 404;
reject(e);
} else {
log.warn(log._("server.install.install-failed-long",{name:module}));
log.warn("------------------------------------------");
log.warn(response.error.summary);
log.warn("------------------------------------------");
reject(new Error(log._("server.install.install-failed")));
}
return;
} else {
resolve(response.version);
}
} catch(err) {
log.warn(log._("server.install.install-failed-long",{name:module}));
log.warn("------------------------------------------");
if (stdout) {
log.warn(stdout);
}
if (stderr) {
log.warn(stderr);
}
log.warn(err);
log.warn("------------------------------------------");
reject(new Error(log._("server.install.install-failed")));
}
});
})
}
async function installTarball(tarball) {
if (settings.externalModules && settings.externalModules.palette && settings.externalModules.palette.allowUpload === false) {
throw new Error("Module upload disabled")
}
// Check this tarball contains a valid node-red module.
// Get its module name/version
const moduleInfo = await getTarballModuleInfo(tarball);
// Write the tarball to <userDir>/nodes/<filename.tgz>
// where the filename is the normalised form based on module name/version
let normalisedModuleName = moduleInfo.name[0] === '@'
? moduleInfo.name.substr(1).replace(/\//g, '-')
: moduleInfo.name
const tarballFile = `${normalisedModuleName}-${moduleInfo.version}.tgz`;
let tarballPath = path.resolve(path.join(settings.userDir || process.env.NODE_RED_HOME || ".", "nodes", tarballFile));
// (from fs-extra - move to writeFile with promise once Node 8 dropped)
await fs.outputFile(tarballPath, tarball);
// Next, need to check to see if this module is listed in `<userDir>/package.json`
let existingVersion = await getExistingPackageVersion(moduleInfo.name);
let existingFile = null;
let isUpdate = false;
// If this is a known module, need to check if there will be an old tarball
// to remove after the install of this one
if (existingVersion) {
// - Known module
if (/^file:nodes\//.test(existingVersion)) {
existingFile = existingVersion.substring(11);
isUpdate = true;
if (tarballFile === existingFile) {
// Edge case: a tar with the same name has bee uploaded.
// Carry on with the install, but don't remove the 'old' file
// as it will have been overwritten by the new one
existingFile = null;
}
}
}
// Install the tgz
return installModule(moduleInfo.name, moduleInfo.version, tarballPath).then(function(info) {
if (existingFile) {
// Remove the old file
return fs.remove(path.resolve(path.join(settings.userDir || process.env.NODE_RED_HOME || ".", "nodes",existingFile))).then(() => info).catch(() => info)
}
return info;
})
}
async function getTarballModuleInfo(tarball) {
const tarballDir = fs.mkdtempSync(path.join(os.tmpdir(),"nr-tarball-"));
const removeExtractedTar = function(done) {
fs.remove(tarballDir, err => {
done();
})
}
return new Promise((resolve,reject) => {
var writeStream = tar.x({
cwd: tarballDir
}).on('error', err => {
reject(err);
}).on('finish', () => {
try {
let moduleInfo = checkModulePath(path.join(tarballDir,"package"));
removeExtractedTar(err => {
resolve(moduleInfo);
})
} catch(err) {
removeExtractedTar(() => {
reject(err);
});
}
});
writeStream.end(tarball);
});
}
function uninstallModule(module) {
activePromise = activePromise.then(() => {
return new Promise((resolve,reject) => {
if (/[\s;]/.test(module)) {
reject(new Error(log._("server.install.invalid")));
return;
}
var installDir = settings.userDir || process.env.NODE_RED_HOME || ".";
var moduleDir = path.join(installDir,"node_modules",module);
try {
fs.statSync(moduleDir);
} catch(err) {
return reject(new Error(log._("server.install.uninstall-failed",{name:module})));
}
// need to remove the plugins first,
// as registry data necessary to perform this operation
var list = plugins.removeModule(module);
list = list.concat(registry.removeModule(module));
log.info(log._("server.install.uninstalling",{name:module}));
let triggerPayload = {
"module": module,
"dir": installDir,
"args": ['--no-audit','--no-update-notifier','--no-fund','--save']
}
return hooks.trigger("preUninstall", triggerPayload).then((result) => {
// preUninstall passed
// - run uninstall
if (result !== false) {
let extraArgs = triggerPayload.args || [];
let args = ['remove', ...extraArgs, module]
log.trace(npmCommand + JSON.stringify(args));
return exec.run(npmCommand,args,{ cwd: installDir, shell: true }, true)
} else {
log.trace("skipping npm uninstall");
}
}).then(() => {
log.info(log._("server.install.uninstalled",{name:module}));
reportRemovedModules(list);
library.removeExamplesDir(module);
return hooks.trigger("postUninstall", triggerPayload).catch((err)=>{
log.warn("------------------------------------------");
log.warn(err.toString());
log.warn("------------------------------------------");
}).finally(() => {
resolve(list);
})
}).catch(result => {
let output = result.stderr || result;
log.warn(log._("server.install.uninstall-failed-long",{name:module}));
log.warn("------------------------------------------");
log.warn(output.toString());
log.warn("------------------------------------------");
reject(new Error(log._("server.install.uninstall-failed",{name:module})));
});
});
}).catch(err => {
// In case of error, reset activePromise to be resolvable
activePromise = Promise.resolve();
throw err;
});
return activePromise;
}
async function checkPrereq() {
if (settings.editorTheme && settings.editorTheme.palette) {
if (settings.editorTheme.palette.hasOwnProperty("editable")) {
log.warn(log._("server.deprecatedOption",{old:"editorTheme.palette.editable", new:"externalModules.palette.allowInstall"}));
}
if (settings.editorTheme.palette.hasOwnProperty("upload")) {
log.warn(log._("server.deprecatedOption",{old:"editorTheme.palette.upload", new:"externalModules.palette.allowUpload"}));
}
}
try {
if (settings.editorTheme.palette.editable === false) {
log.info(log._("server.palette-editor.disabled"));
installerEnabled = false;
return
}
} catch(err) {}
try {
if (settings.externalModules.palette.allowInstall === false) {
log.info(log._("server.palette-editor.disabled"));
installerEnabled = false;
return
}
} catch(err) {}
if (settings.hasOwnProperty('editorTheme') &&
settings.editorTheme.hasOwnProperty('palette') &&
settings.editorTheme.palette.hasOwnProperty('editable') &&
settings.editorTheme.palette.editable === false
) {
log.info(log._("server.palette-editor.disabled"));
installerEnabled = false;
} else {
return new Promise(resolve => {
child_process.execFile(`${npmCommand} -v`,{ shell: true },function(err,stdout) {
if (err) {
log.info(log._("server.palette-editor.npm-not-found"));
installerEnabled = false;
} else {
if (parseInt(stdout.split(".")[0]) < 3) {
log.info(log._("server.palette-editor.npm-too-old"));
installerEnabled = false;
} else {
installerEnabled = true;
}
}
resolve();
});
})
}
}
module.exports = {
init: init,
checkPrereq: checkPrereq,
installModule: installModule,
uninstallModule: uninstallModule,
installerEnabled: function() {
return installerEnabled
}
}
+98
View File
@@ -0,0 +1,98 @@
/**
* Copyright JS Foundation and other contributors, http://js.foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
var fs = require('fs');
var fspath = require('path');
var runtime;
var exampleRoots = {};
var exampleFlows = null;
async function getFlowsFromPath(path) {
var result = {};
var validFiles = [];
return fs.promises.readdir(path).then(files => {
var promises = [];
if (files) {
files.forEach(function(file) {
var fullPath = fspath.join(path,file);
var stats = fs.lstatSync(fullPath);
if (stats.isDirectory()) {
validFiles.push(file);
promises.push(getFlowsFromPath(fullPath));
} else if (/\.json$/.test(file)){
validFiles.push(file);
promises.push(Promise.resolve(file.replace(/\.json$/, '')))
}
})
}
return Promise.all(promises)
}).then(results => {
results.forEach(function(r,i) {
if (typeof r === 'string') {
result.f = result.f||[];
result.f.push(r);
} else {
result.d = result.d||{};
result.d[validFiles[i]] = r;
}
})
return result;
})
}
function addNodeExamplesDir(module,path) {
exampleRoots[module] = path;
return getFlowsFromPath(path).then(function(result) {
if (JSON.stringify(result).indexOf('{"f":') === -1) { return; }
exampleFlows = exampleFlows||{};
exampleFlows[module] = result;
});
}
function removeNodeExamplesDir(module) {
delete exampleRoots[module];
if (exampleFlows) {
delete exampleFlows[module];
}
if (exampleFlows && Object.keys(exampleFlows).length === 0) {
exampleFlows = null;
}
}
function init() {
exampleRoots = {};
exampleFlows = null;
}
function getExampleFlows() {
return exampleFlows;
}
function getExampleFlowPath(module,path) {
if (exampleRoots[module]) {
return fspath.join(exampleRoots[module],path)+".json";
}
return null;
}
module.exports = {
init: init,
addExamplesDir: addNodeExamplesDir,
removeExamplesDir: removeNodeExamplesDir,
getExampleFlows: getExampleFlows,
getExampleFlowPath: getExampleFlowPath
}
+563
View File
@@ -0,0 +1,563 @@
/**
* Copyright JS Foundation and other contributors, http://js.foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
var fs = require("fs-extra");
var path = require("path");
var semver = require("semver");
var localfilesystem = require("./localfilesystem");
var registry = require("./registry");
var registryUtil = require("./util")
var i18n = require("@node-red/util").i18n;
var log = require("@node-red/util").log;
var settings;
function init(_runtime) {
settings = _runtime.settings;
localfilesystem.init(settings);
registryUtil.init(_runtime);
}
function load(disableNodePathScan) {
// To skip node scan, the following line will use the stored node list.
// We should expose that as an option at some point, although the
// performance gains are minimal.
//return loadModuleFiles(registry.getModuleList());
log.info(log._("server.loading"));
var modules = localfilesystem.getNodeFiles(disableNodePathScan);
return loadModuleFiles(modules);
}
function splitPath(p) {
return path.posix.normalize((p || '').replace(/\\/g, path.sep)).split(path.sep)
}
function loadModuleTypeFiles(module, type) {
const things = module[type];
let first = true;
const promises = [];
for (let thingName in things) {
/* istanbul ignore else */
if (things.hasOwnProperty(thingName)) {
const thing = things[thingName]
if (module.name != "node-red" && first) {
// Check the module directory exists
first = false;
let moduleFn = module.path
const fn = thing.file
const parts = splitPath(fn)
const nmi = parts.indexOf('node_modules')
if(nmi > -1) {
moduleFn = parts.slice(0,nmi+2).join(path.sep);
}
if (!moduleFn) {
// shortcut - skip calling statSync on empty string
break; // Module not found, don't attempt to load its nodes
}
try {
const stat = fs.statSync(moduleFn);
} catch(err) {
break; // Module not found, don't attempt to load its nodes
}
}
try {
let promise;
if (type === "nodes") {
promise = loadNodeConfig(thing);
} else if (type === "plugins") {
promise = loadPluginConfig(thing);
}
promises.push(
promise.then(
(function() {
const n = thingName;
return function(nodeSet) {
things[n] = nodeSet;
return nodeSet;
}
})()
).catch(err => {
// This shouldn't fail - but if it does, we don't want to crash the loader
// But we also need to log it to have some chance of figuring out what went wrong
console.log(thing.name, err)
})
);
} catch(err) {
console.log(thing.name, err)
}
}
}
return promises;
}
function loadModuleFiles(modules) {
var pluginPromises = [];
var nodePromises = [];
for (var module in modules) {
/* istanbul ignore else */
if (modules.hasOwnProperty(module)) {
if (modules[module].redVersion &&
!semver.satisfies((settings.version||"0.0.0").replace(/(\-[1-9A-Za-z-][0-9A-Za-z-\.]*)?(\+[0-9A-Za-z-\.]+)?$/,""), modules[module].redVersion)) {
//TODO: log it
log.warn("["+module+"] "+log._("server.node-version-mismatch",{version:modules[module].redVersion}));
modules[module].err = "version_mismatch";
continue;
}
if (module == "node-red" || !registry.getModuleInfo(module)) {
if (modules[module].nodes) {
nodePromises = nodePromises.concat(loadModuleTypeFiles(modules[module], "nodes"));
}
if (modules[module].plugins) {
pluginPromises = pluginPromises.concat(loadModuleTypeFiles(modules[module], "plugins"));
}
}
}
}
var pluginList;
var nodeList;
return Promise.all(pluginPromises).then(function(results) {
pluginList = results.filter(r => !!r);
return Promise.all(nodePromises)
}).then(function(results) {
nodeList = results.filter(r => !!r);
// Initial node load has happened. Ensure remaining modules are in the registry
for (var module in modules) {
if (modules.hasOwnProperty(module)) {
if (!modules[module].err) {
registry.addModule(modules[module]);
}
}
}
}).then(function() {
return loadNodeSetList(pluginList);
}).then(function() {
return loadNodeSetList(nodeList);
}).then(function () {
if (settings.available()) {
return registry.saveNodeList();
} else {
return
}
})
}
async function loadPluginTemplate(plugin) {
return fs.readFile(plugin.template,'utf8').then(content => {
plugin.config = content;
return plugin;
}).catch(err => {
if (err.code === 'ENOENT') {
plugin.err = "Error: "+plugin.template+" does not exist";
} else {
plugin.err = err.toString();
}
return plugin;
});
}
async function loadNodeTemplate(node) {
return fs.readFile(node.template,'utf8').then(content => {
var types = [];
var regExp = /<script (?:[^>]*)data-template-name\s*=\s*['"]([^'"]*)['"]/gi;
var match = null;
while ((match = regExp.exec(content)) !== null) {
types.push(match[1]);
}
node.types = types;
var langRegExp = /^<script[^>]* data-lang\s*=\s*['"](.+?)['"]/i;
regExp = /(<script[^>]* data-help-name=[\s\S]*?<\/script>)/gi;
match = null;
var mainContent = "";
var helpContent = {};
var index = 0;
while ((match = regExp.exec(content)) !== null) {
mainContent += content.substring(index,regExp.lastIndex-match[1].length);
index = regExp.lastIndex;
var help = content.substring(regExp.lastIndex-match[1].length,regExp.lastIndex);
var lang = i18n.defaultLang;
if ((match = langRegExp.exec(help)) !== null) {
lang = match[1];
}
if (!helpContent.hasOwnProperty(lang)) {
helpContent[lang] = "";
}
helpContent[lang] += help;
}
mainContent += content.substring(index);
node.config = mainContent;
node.help = helpContent;
// TODO: parse out the javascript portion of the template
//node.script = "";
for (var i=0;i<node.types.length;i++) {
if (registry.getTypeId(node.types[i])) {
node.err = node.types[i]+" already registered";
break;
}
}
return node
}).catch(err => {
// ENOENT means no html file. We can live with that. But any other error
// should be fatal
// node.err = "Error: "+node.template+" does not exist";
node.types = node.types || [];
if (err.code !== 'ENOENT') {
node.err = err.toString();
}
return node;
});
}
async function loadNodeLocales(node) {
if (node.module === 'node-red') {
// do not look up locales directory for core nodes
node.namespace = node.module;
return node
}
const baseFile = node.file||node.template;
return fs.stat(path.join(path.dirname(baseFile),"locales")).then(stat => {
node.namespace = node.id;
return i18n.registerMessageCatalog(node.id,
path.join(path.dirname(baseFile),"locales"),
path.basename(baseFile).replace(/\.[^.]+$/,".json"))
.then(() => node);
}).catch(err => {
node.namespace = node.module;
return node;
});
}
async function loadNodeConfig(fileInfo) {
var file = fileInfo.file;
var module = fileInfo.module;
var name = fileInfo.name;
var version = fileInfo.version;
var id = module + "/" + name;
var info = registry.getNodeInfo(id);
var isEnabled = true;
if (info) {
if (info.hasOwnProperty("loaded")) {
throw new Error(file+" already loaded");
}
isEnabled = !(info.enabled === false);
}
var node = {
type: "node",
id: id,
module: module,
name: name,
file: file,
template: file.replace(/\.c?js$/,".html"),
enabled: isEnabled,
loaded:false,
version: version,
local: fileInfo.local,
types: [],
config: "",
help: {}
};
if (fileInfo.hasOwnProperty("types")) {
node.types = fileInfo.types;
}
await loadNodeLocales(node)
if (!settings.disableEditor) {
return loadNodeTemplate(node);
}
return node;
}
async function loadPluginConfig(fileInfo) {
var file = fileInfo.file;
var module = fileInfo.module;
var name = fileInfo.name;
var version = fileInfo.version;
var id = module + "/" + name;
var isEnabled = true;
// TODO: registry.getPluginInfo
// var info = registry.getPluginInfo(id);
// if (info) {
// if (info.hasOwnProperty("loaded")) {
// throw new Error(file+" already loaded");
// }
// isEnabled = info.enabled;
// }
if (!fs.existsSync(jsFile)) {
}
var plugin = {
type: "plugin",
id: id,
module: module,
name: name,
enabled: isEnabled,
loaded:false,
version: version,
local: fileInfo.local,
plugins: [],
config: "",
help: {}
};
var jsFile = file.replace(/\.[^.]+$/,".js");
var htmlFile = file.replace(/\.[^.]+$/,".html");
if (fs.existsSync(jsFile)) {
plugin.file = jsFile;
} else {
jsFile = file.replace(/\.[^.]+$/,".cjs")
if (fs.existsSync(jsFile)) {
plugin.file = jsFile;
}
}
if (fs.existsSync(htmlFile)) {
plugin.template = htmlFile;
}
await loadNodeLocales(plugin)
if (plugin.template && !settings.disableEditor) {
return loadPluginTemplate(plugin);
}
return plugin
}
/**
* Loads the specified node into the runtime
* @param node a node info object - see loadNodeConfig
* @return a promise that resolves to an update node info object. The object
* has the following properties added:
* err: any error encountered whilst loading the node
*
*/
function loadNodeSet(node) {
if (!node.enabled) {
return Promise.resolve(node);
} else {
}
try {
var loadPromise = null;
var r = require(node.file);
r = r.__esModule ? r.default : r
if (typeof r === "function") {
var red = registryUtil.createNodeApi(node);
var promise = r(red);
if (promise != null && typeof promise.then === "function") {
loadPromise = promise.then(function() {
node.enabled = true;
node.loaded = true;
return node;
}).catch(function(err) {
node.err = err;
return node;
});
}
}
if (loadPromise == null) {
node.enabled = true;
node.loaded = true;
loadPromise = Promise.resolve(node);
}
return loadPromise;
} catch(err) {
node.err = err;
var stack = err.stack;
var message;
if (stack) {
var filePath = node.file;
try {
filePath = fs.realpathSync(filePath);
}
catch (e) {
// ignore canonicalization error
}
var i = stack.indexOf(filePath);
if (i > -1) {
var excerpt = stack.substring(i+filePath.length+1,i+filePath.length+20);
var m = /^(\d+)/.exec(excerpt);
if (m) {
node.err = err+" (line:"+m[1]+")";
}
}
}
return Promise.resolve(node);
}
}
async function loadPlugin(plugin) {
if (!plugin.file) {
// No runtime component - nothing to load
return plugin;
}
try {
var r = require(plugin.file);
r = r.__esModule ? r.default : r
if (typeof r === "function") {
var red = registryUtil.createNodeApi(plugin);
var promise = r(red);
if (promise != null && typeof promise.then === "function") {
return promise.then(function() {
plugin.enabled = true;
plugin.loaded = true;
return plugin;
}).catch(function(err) {
plugin.err = err;
return plugin;
});
}
}
plugin.enabled = true;
plugin.loaded = true;
return plugin;
} catch(err) {
console.log(err);
plugin.err = err;
var stack = err.stack;
var message;
if (stack) {
var i = stack.indexOf(plugin.file);
if (i > -1) {
var excerpt = stack.substring(i+node.file.length+1,i+plugin.file.length+20);
var m = /^(\d+):(\d+)/.exec(excerpt);
if (m) {
plugin.err = err+" (line:"+m[1]+")";
}
}
}
return plugin;
}
}
let invocation = 0
function loadNodeSetList(nodes) {
var promises = [];
nodes.forEach(function(node) {
if (!node.err) {
if (node.type === "plugin") {
promises.push(loadPlugin(node).catch(err => {}));
} else {
promises.push(loadNodeSet(node).catch(err => {}));
}
} else {
promises.push(node);
}
});
return Promise.all(promises)
}
function addModule(module) {
if (!settings.available()) {
throw new Error("Settings unavailable");
}
var nodes = [];
var existingInfo = registry.getModuleInfo(module);
if (existingInfo) {
// TODO: nls
var e = new Error("module_already_loaded");
e.code = "module_already_loaded";
return Promise.reject(e);
}
try {
var moduleFiles = {};
var moduleStack = [module];
while(moduleStack.length > 0) {
var moduleToLoad = moduleStack.shift();
var files = localfilesystem.getModuleFiles(moduleToLoad);
if (files[moduleToLoad]) {
moduleFiles[moduleToLoad] = files[moduleToLoad];
if (moduleFiles[moduleToLoad].dependencies) {
log.debug(`Loading dependencies for ${module}`)
for (var i=0; i<moduleFiles[moduleToLoad].dependencies.length; i++) {
var dep = moduleFiles[moduleToLoad].dependencies[i]
if (!registry.getModuleInfo(dep)) {
log.debug(` - load ${dep}`)
moduleStack.push(dep);
} else {
log.debug(` - already loaded ${dep}`)
registry.addModuleDependency(dep,moduleToLoad)
}
}
}
}
}
return loadModuleFiles(moduleFiles).then(() => module)
} catch(err) {
return Promise.reject(err);
}
}
function loadNodeHelp(node,lang) {
var base = path.basename(node.template);
var localePath;
if (node.module === 'node-red') {
var cat_dir = path.dirname(node.template);
var cat = path.basename(cat_dir);
var dir = path.dirname(cat_dir);
localePath = path.join(dir, "..", "locales", lang, cat, base)
}
else {
var dir = path.dirname(node.template);
localePath = path.join(dir,"locales",lang,base);
}
try {
// TODO: make this async
var content = fs.readFileSync(localePath, "utf8")
return content;
} catch(err) {
return null;
}
}
function getNodeHelp(node,lang) {
if (!node.help[lang]) {
var help = loadNodeHelp(node,lang);
if (help == null) {
var langParts = lang.split("-");
if (langParts.length == 2) {
help = loadNodeHelp(node,langParts[0]);
}
}
if (help) {
node.help[lang] = help;
} else if (lang === i18n.defaultLang) {
return null;
} else {
node.help[lang] = getNodeHelp(node, i18n.defaultLang);
}
}
return node.help[lang];
}
module.exports = {
init: init,
load: load,
addModule: addModule,
loadNodeSet: loadNodeSet,
getNodeHelp: getNodeHelp
}
@@ -0,0 +1,595 @@
/**
* Copyright JS Foundation and other contributors, http://js.foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
const fs = require("fs");
const path = require("path");
const log = require("@node-red/util").log;
const i18n = require("@node-red/util").i18n;
const registryUtil = require("./util");
// Default allow/deny lists
let loadAllowList = ['*'];
let loadDenyList = [];
var settings;
var disableNodePathScan = false;
var iconFileExtensions = [".png", ".gif", ".svg"];
var packageList = {};
function init(_settings) {
settings = _settings;
// TODO: This is duplicated in installer.js
// Should it *all* be managed by util?
if (settings.externalModules && settings.externalModules.palette) {
if (settings.externalModules.palette.allowList || settings.externalModules.palette.denyList) {
loadAllowList = settings.externalModules.palette.allowList;
loadDenyList = settings.externalModules.palette.denyList;
}
}
loadAllowList = registryUtil.parseModuleList(loadAllowList);
loadDenyList = registryUtil.parseModuleList(loadDenyList);
}
function isIncluded(name) {
if (settings.nodesIncludes) {
for (var i=0;i<settings.nodesIncludes.length;i++) {
if (settings.nodesIncludes[i] == name) {
return true;
}
}
} else {
return true;
}
return false;
}
function isExcluded(name) {
if (settings.nodesExcludes) {
for (var i=0;i<settings.nodesExcludes.length;i++) {
if (settings.nodesExcludes[i] == name) {
return true;
}
}
}
return false;
}
function getLocalFile(file) {
if (!isIncluded(path.basename(file)) || isExcluded(path.basename(file))) {
return null;
}
try {
fs.statSync(file.replace(/\.c?js$/,".html"));
return {
file: file,
module: "node-red",
name: path.basename(file).replace(/^\d+-/,"").replace(/\.c?js$/,""),
version: settings.version
};
} catch(err) {
return null;
}
}
/**
* Synchronously walks the directory looking for node files.
* @param dir the directory to search
* @param skipValidNodeRedModules a flag to skip lading icons & files if the directory a valid node-red module
* @return an array of fully-qualified paths to .js files
*/
function getLocalNodeFiles(dir, skipValidNodeRedModules) {
dir = path.resolve(dir);
var result = [];
var files = [];
var icons = [];
try {
files = fs.readdirSync(dir);
} catch(err) {
return {files: [], icons: []};
}
files.sort();
// when loading local files, if the path is a valid node-red module
// dont include it (will be picked up in scanTreeForNodesModules)
if(skipValidNodeRedModules && files.indexOf("package.json") >= 0) {
const packageDetails = getPackageDetails(dir)
if(packageDetails.isNodeRedModule) {
return {files: [], icons: []};
}
}
files.forEach(function(fn) {
var stats = fs.statSync(path.join(dir,fn));
if (stats.isFile()) {
if (/\.c?js$/.test(fn)) {
var info = getLocalFile(path.join(dir,fn));
if (info) {
result.push(info);
}
}
} else if (stats.isDirectory()) {
// Ignore /.dirs/, /lib/ /node_modules/
if (!/^(\..*|lib|icons|node_modules|test|locales)$/.test(fn)) {
var subDirResults = getLocalNodeFiles(path.join(dir,fn), skipValidNodeRedModules);
result = result.concat(subDirResults.files);
icons = icons.concat(subDirResults.icons);
} else if (fn === "icons") {
var iconList = scanIconDir(path.join(dir,fn));
icons.push({path:path.join(dir,fn),icons:iconList});
}
}
});
return {files: result, icons: icons}
}
function scanDirForNodesModules(dir,moduleName,packageDetails) {
let results = [];
let scopeName;
let files
try {
let isNodeRedModule = false
if(packageDetails) {
dir = path.join(packageDetails.moduleDir,'..')
files = [path.basename(packageDetails.moduleDir)]
moduleName = (packageDetails.package ? packageDetails.package.name : null) || moduleName
isNodeRedModule = packageDetails.isNodeRedModule
} else {
files = fs.readdirSync(dir);
if (moduleName) {
var m = /^(?:(@[^/]+)[/])?([^@/]+)/.exec(moduleName);
if (m) {
scopeName = m[1];
moduleName = m[2];
}
}
}
// if we have found a package.json, this IS a node_module, lets see if it is a node-red node
if (!isNodeRedModule && files.indexOf('package.json') > -1) {
packageDetails = getPackageDetails(dir) // get package details
if(packageDetails && packageDetails.isNodeRedModule) {
isNodeRedModule = true
files = ['package.json'] // shortcut the file scan
}
}
for (let i=0;i<files.length;i++) {
let fn = files[i];
if (!isNodeRedModule && /^@/.test(fn)) {
if (scopeName && scopeName === fn) {
// Looking for a specific scope/module
results = results.concat(scanDirForNodesModules(path.join(dir,fn),moduleName));
break;
} else {
results = results.concat(scanDirForNodesModules(path.join(dir,fn),moduleName));
}
} else {
if ((isNodeRedModule || (!moduleName || fn == moduleName)) && (isIncluded(fn) && !isExcluded(fn))) {
try {
const moduleDir = isNodeRedModule ? packageDetails.moduleDir : path.join(dir,fn);
const pkg = packageDetails || getPackageDetails(moduleDir)
if(pkg.error) {
throw pkg.error
}
if (pkg.isNodeRedModule) {
if (!pkg.allowed) {
log.debug("! Module: "+pkg.package.name+" "+pkg.package.version+ " *ignored due to denyList*");
} else {
results.push({dir:moduleDir,package:pkg.package});
}
}
} catch(err) {
if (err.code != "MODULE_NOT_FOUND") {
// TODO: handle unexpected error
}
}
if (fn == moduleName) {
break;
}
}
}
}
} catch(err) {
}
return results;
}
/**
* Scans the node_modules path for nodes
* @param moduleName the name of the module to be found
* @return a list of node modules: {dir,package}
*/
function scanTreeForNodesModules(moduleName) {
let coreNodesDir = settings.coreNodesDir;
let results = [];
let userDir;
let nodesDir;
if(settings.nodesDir) {
nodesDir = Array.isArray(settings.nodesDir) ? settings.nodesDir : [settings.nodesDir]
}
if (settings.userDir) {
packageList = getPackageList();
userDir = path.join(settings.userDir,"node_modules");
results = scanDirForNodesModules(userDir,moduleName);
results.forEach(function(r) {
// If it was found in <userDir>/node_modules then it is considered
// a local module.
// Also check to see if it is listed in the package.json file as a user-installed
// module. This distinguishes modules installed as a dependency
r.local = true;
r.user = !!packageList[r.package.name];
});
}
if (coreNodesDir) {
var up = path.resolve(path.join(coreNodesDir,".."));
while (up !== coreNodesDir) {
var pm = path.join(coreNodesDir,"node_modules");
if (pm != userDir) {
results = results.concat(scanDirForNodesModules(pm,moduleName));
}
coreNodesDir = up;
up = path.resolve(path.join(coreNodesDir,".."));
}
}
// scan nodesDir for any node-red modules
/*
1. if !exist(package.json) || !package.json.has(node-red) => look for node_modules
2. exist(package.json) && package.json.has(node-red) => load this only
3. in original scan of nodesDir, ignore if:(exist(package.json) && package.json.has(node-red))
*/
if (nodesDir) {
for (let dirIndex = 0; dirIndex < nodesDir.length; dirIndex++) {
const nodeDir = nodesDir[dirIndex];
const packageDetails = getPackageDetails(nodeDir)
if(packageDetails.isNodeRedModule) {
//we have found a node-red module, scan it
const nrModules = scanDirForNodesModules(nodeDir, packageDetails.package.name, packageDetails);
results = results.concat(nrModules);
} else if (packageDetails.has_node_modules) {
//If this dir has a `node_modues` dir, scan it
const nodeModulesDir = path.join(nodeDir, 'node_modules')
const nrModules = scanDirForNodesModules(nodeModulesDir, moduleName );
results = results.concat(nrModules);
} else {
//If this is not a node-red module AND it does NOT have a node_modules dir,
//it may be a directory of project directories or a node_modules dir?
//scan this instead
const nrModules = scanDirForNodesModules(nodeDir, moduleName);
results = results.concat(nrModules);
}
}
}
return results;
}
function getModuleNodeFiles(module) {
var moduleDir = module.dir;
var pkg = module.package;
var iconDirs = [];
var iconList = [];
function scanTypes(types) {
const files = [];
for (var n in types) {
/* istanbul ignore else */
if (types.hasOwnProperty(n)) {
var file = path.join(moduleDir,types[n]);
files.push({
file: file,
module: pkg.name,
name: n,
version: pkg.version
});
var iconDir = path.join(moduleDir,path.dirname(types[n]),"icons");
if (iconDirs.indexOf(iconDir) == -1) {
try {
fs.statSync(iconDir);
var icons = scanIconDir(iconDir);
iconList.push({path:iconDir,icons:icons});
iconDirs.push(iconDir);
} catch(err) {
}
}
}
}
return files;
}
var result = {
nodeFiles:scanTypes(pkg['node-red'].nodes||{}),
pluginFiles:scanTypes(pkg['node-red'].plugins||{}),
icons:iconList
};
var examplesDir = path.join(moduleDir,"examples");
try {
fs.statSync(examplesDir)
result.examples = {path:examplesDir};
} catch(err) {
}
var resourcesDir = path.join(moduleDir,"resources");
try {
fs.statSync(resourcesDir)
result.resources = {path:resourcesDir};
} catch(err) {
}
return result;
}
function getNodeFiles(disableNodePathScan) {
// Find all of the nodes to load
let results;
let nodesDir;
if(settings.nodesDir) {
nodesDir = Array.isArray(settings.nodesDir) ? settings.nodesDir : [settings.nodesDir]
}
let dir;
let nodeFiles = [];
let iconList = [];
if (settings.coreNodesDir) {
results = getLocalNodeFiles(path.resolve(settings.coreNodesDir));
nodeFiles = nodeFiles.concat(results.files);
iconList = iconList.concat(results.icons);
let defaultLocalesPath = path.join(settings.coreNodesDir,"locales");
i18n.registerMessageCatalog("node-red",defaultLocalesPath,"messages.json");
}
if (settings.userDir) {
dir = path.join(settings.userDir,"lib","icons");
let icons = scanIconDir(dir);
if (icons.length > 0) {
iconList.push({path:dir,icons:icons});
}
dir = path.join(settings.userDir,"nodes");
results = getLocalNodeFiles(path.resolve(dir));
nodeFiles = nodeFiles.concat(results.files);
iconList = iconList.concat(results.icons);
}
if (nodesDir) {
for (let i = 0; i < nodesDir.length; i++) {
results = getLocalNodeFiles(nodesDir[i], true);
nodeFiles = nodeFiles.concat(results.files);
iconList = iconList.concat(results.icons);
}
}
var coreNodeEntry = {
name: "node-red",
version: settings.version,
nodes: {},
icons: iconList
}
var nodeList = {
"node-red": coreNodeEntry
};
nodeFiles.forEach(function(node) {
coreNodeEntry.nodes[node.name] = node;
});
if (settings.coreNodesDir) {
var examplesDir = path.join(settings.coreNodesDir,"examples");
coreNodeEntry.examples = {path: examplesDir};
}
if (!disableNodePathScan) {
var moduleFiles = scanTreeForNodesModules();
// Filter the module list to ignore global modules
// that have also been installed locally - allowing the user to
// update a module they may not otherwise be able to touch
moduleFiles.sort(function(A,B) {
if (A.local && !B.local) {
return -1
} else if (!A.local && B.local) {
return 1
}
return 0;
})
var knownModules = {};
moduleFiles = moduleFiles.filter(function(mod) {
var result;
if (!knownModules[mod.package.name]) {
knownModules[mod.package.name] = mod;
result = true;
} else {
result = false;
}
log.debug((result?"":"! ")+"Module: "+mod.package.name+" "+mod.package.version+" "+mod.dir+(result?"":" *ignored due to local copy*"));
return result;
});
// Do a second pass to check we have all the declared node dependencies
// As this is only done as part of the initial palette load, `knownModules` will
// contain a list of everything discovered during this phase. This means
// we can check for missing dependencies here.
moduleFiles = moduleFiles.filter(function(mod) {
if (Array.isArray(mod.package["node-red"].dependencies)) {
const deps = mod.package["node-red"].dependencies;
const missingDeps = mod.package["node-red"].dependencies.filter(dep => {
if (knownModules[dep]) {
knownModules[dep].usedBy = knownModules[dep].usedBy || [];
knownModules[dep].usedBy.push(mod.package.name)
} else {
return true;
}
})
if (missingDeps.length > 0) {
log.error(`Module: ${mod.package.name} missing dependencies:`);
missingDeps.forEach(m => { log.error(` - ${m}`)});
return false;
}
}
return true;
});
nodeList = convertModuleFileListToObject(moduleFiles, nodeList);
} else {
// console.log("node path scan disabled");
}
return nodeList;
}
function getModuleFiles(module) {
// Update the package list
var moduleFiles = scanTreeForNodesModules(module);
if (moduleFiles.length === 0) {
var err = new Error(log._("nodes.registry.localfilesystem.module-not-found", {module:module}));
err.code = 'MODULE_NOT_FOUND';
throw err;
}
// Unlike when doing the initial palette load, this call cannot verify the
// dependencies of the new module as it doesn't have visiblity of what
// is in the registry. That will have to be done be the caller in loader.js
return convertModuleFileListToObject(moduleFiles);
}
function convertModuleFileListToObject(moduleFiles,seedObject) {
const nodeList = seedObject || {};
moduleFiles.forEach(function(moduleFile) {
var nodeModuleFiles = getModuleNodeFiles(moduleFile);
nodeList[moduleFile.package.name] = {
name: moduleFile.package.name,
version: moduleFile.package.version,
path: moduleFile.dir,
local: moduleFile.local||false,
user: moduleFile.user||false,
nodes: {},
plugins: {},
resources: nodeModuleFiles.resources,
icons: nodeModuleFiles.icons,
examples: nodeModuleFiles.examples
};
if (moduleFile.package['node-red'].version) {
nodeList[moduleFile.package.name].redVersion = moduleFile.package['node-red'].version;
}
if (moduleFile.package['node-red'].dependencies) {
nodeList[moduleFile.package.name].dependencies = moduleFile.package['node-red'].dependencies;
}
if (moduleFile.usedBy) {
nodeList[moduleFile.package.name].usedBy = moduleFile.usedBy;
}
nodeModuleFiles.nodeFiles.forEach(function(node) {
nodeList[moduleFile.package.name].nodes[node.name] = node;
nodeList[moduleFile.package.name].nodes[node.name].local = moduleFile.local || false;
});
nodeModuleFiles.pluginFiles.forEach(function(plugin) {
nodeList[moduleFile.package.name].plugins[plugin.name] = plugin;
nodeList[moduleFile.package.name].plugins[plugin.name].local = moduleFile.local || false;
});
});
return nodeList;
}
// If this finds an svg and a png with the same name, it will only list the svg
function scanIconDir(dir) {
var iconList = [];
var svgs = {};
try {
var files = fs.readdirSync(dir);
files.forEach(function(file) {
var stats = fs.statSync(path.join(dir, file));
var ext = path.extname(file).toLowerCase();
if (stats.isFile() && iconFileExtensions.indexOf(ext) !== -1) {
iconList.push(file);
if (ext === ".svg") {
svgs[file.substring(0,file.length-4)] = true;
}
}
});
} catch(err) {
}
iconList = iconList.filter(f => {
return /.svg$/i.test(f) || !svgs[f.substring(0,f.length-4)]
})
return iconList;
}
/**
* Gets the list of modules installed in this runtime as reported by package.json
* Note: these may include non-Node-RED modules
*/
function getPackageList() {
var list = {};
if (settings.userDir) {
try {
var userPackage = path.join(settings.userDir,"package.json");
var pkg = JSON.parse(fs.readFileSync(userPackage,"utf-8"));
return pkg.dependencies || {};
} catch(err) {
log.error(err);
}
}
return list;
}
/**
* Gets the package json object for the supplied `dir`.
* If there is no package.json or the `node-red` section is missing, `result.isNodeRedModule` will be `false`.
* If there is no package.json `isPackage` will be `false`.
* If an error occurs, `result.error` will contain the error.
* @param {string} dir The directory to inspect
*/
function getPackageDetails(dir) {
const result = {
/** @type {string} The package directory */
moduleDir: dir,
/** @type {string} The full file path of package.json for this package */
packageFile: null,
/** @type {boolean} True if this is a valid node-red module */
isNodeRedModule: false,
/** @type {boolean} True if a package.json file is present */
isPackage: false,
/** @type {boolean} True if this a node-red module and passes the checks */
allowed: false,
/** @type {object} The contents of package.json */
package: null,
}
if (!dir) { return result }
try {
const packagefile = path.join(dir,'package.json')
result.has_node_modules = fs.existsSync(path.join(dir,'node_modules'))
if(!fs.existsSync(packagefile)) {
return result
}
result.packageFile = packagefile
const pkg = require(packagefile)
result.package = pkg
if(result.package) {
result.allowed = true
result.isPackage = true
result.isNodeRedModule = typeof result.package['node-red'] === 'object'
if(result.isNodeRedModule) {
result.isNodeRedModule = true;
result.allowed = registryUtil.checkModuleAllowed(pkg.name,pkg.version,loadAllowList,loadDenyList)
}
}
} catch(err) {
result.error = err; // this is not a package we are interested in!
}
return result || result;
}
module.exports = {
init: init,
getNodeFiles: getNodeFiles,
getLocalFile: getLocalFile,
getModuleFiles: getModuleFiles
}
+212
View File
@@ -0,0 +1,212 @@
const registry = require("./registry");
const {events} = require("@node-red/util");
const clone = require("clone");
let pluginConfigCache = {};
let pluginToId = {};
let plugins = {};
let pluginsByType = {};
let pluginSettings = {};
let settings;
function init(_settings) {
settings = _settings;
plugins = {};
pluginConfigCache = {};
pluginToId = {};
pluginsByType = {};
pluginSettings = {};
}
function registerPlugin(nodeSetId,id,definition) {
var moduleId = registry.getModuleFromSetId(nodeSetId);
var pluginId = registry.getNodeFromSetId(nodeSetId);
definition.id = id;
definition.module = moduleId;
pluginToId[id] = nodeSetId;
plugins[id] = definition;
var module = registry.getModule(moduleId);
definition.path = module.path;
module.plugins[pluginId].plugins.push(definition);
if (definition.type) {
pluginsByType[definition.type] = pluginsByType[definition.type] || [];
pluginsByType[definition.type].push(definition);
}
if (definition.settings) {
pluginSettings[id] = definition.settings;
}
// reset the cache when a new plugin is incoming!
pluginConfigCache = {};
if (definition.onadd && typeof definition.onadd === 'function') {
definition.onadd();
}
events.emit("registry:plugin-added",id);
}
function getPlugin(id) {
return plugins[id]
}
function getPluginsByType(type) {
return pluginsByType[type] || [];
}
function getPluginConfigs(lang) {
// we're not re-using getPluginConfig() here,
// to avoid calling registry.getModuleList() multiple times!
if (!pluginConfigCache[lang]) {
var result = "";
var moduleConfigs = registry.getModuleList();
for (var module in moduleConfigs) {
/* istanbul ignore else */
if (moduleConfigs.hasOwnProperty(module)) {
result += generateModulePluginConfig(moduleConfigs[module]);
}
}
pluginConfigCache[lang] = result;
}
return pluginConfigCache[lang];
}
function getPluginConfig(id, lang) {
let result = '';
let moduleConfigs = registry.getModuleList();
if (moduleConfigs.hasOwnProperty(id)) {
result = generateModulePluginConfig(moduleConfigs[id]);
}
return result;
}
function generateModulePluginConfig(module) {
let result = '';
const plugins = module.plugins
for (let plugin in plugins) {
if (plugins.hasOwnProperty(plugin)) {
let config = plugins[plugin];
if (config.enabled && !config.err && config.config) {
result += "\n<!-- --- [red-plugin:"+config.id+"] --- -->\n";
result += config.config;
}
}
}
return result;
}
function getPluginList() {
var list = [];
var moduleConfigs = registry.getModuleList();
for (var module in moduleConfigs) {
/* istanbul ignore else */
if (moduleConfigs.hasOwnProperty(module)) {
var plugins = moduleConfigs[module].plugins;
for (var plugin in plugins) {
/* istanbul ignore else */
if (plugins.hasOwnProperty(plugin)) {
var pluginInfo = registry.filterNodeInfo(plugins[plugin]);
pluginInfo.version = moduleConfigs[module].version;
// if (moduleConfigs[module].pending_version) {
// nodeInfo.pending_version = moduleConfigs[module].pending_version;
// }
list.push(pluginInfo);
}
}
}
}
return list;
}
function exportPluginSettings(safeSettings) {
for (let id in pluginSettings) {
if (pluginSettings.hasOwnProperty(id)) {
if (settings.hasOwnProperty(id) && !safeSettings.hasOwnProperty(id)) {
let pluginTypeSettings = pluginSettings[id];
let exportedSet = {};
let defaultExportable = false;
if (pluginTypeSettings['*'] && pluginTypeSettings['*'].hasOwnProperty("exportable")) {
defaultExportable = pluginTypeSettings['*'].exportable;
}
if (defaultExportable) {
exportedSet = clone(settings[id]);
}
for (let property in pluginTypeSettings) {
if (pluginTypeSettings.hasOwnProperty(property)) {
let setting = pluginTypeSettings[property];
if (defaultExportable) {
if (setting.exportable === false) {
delete exportedSet[property]
} else if (!exportedSet.hasOwnProperty(property) && setting.hasOwnProperty('value')) {
exportedSet[property] = setting.value;
}
} else if (setting.exportable) {
if (settings[id].hasOwnProperty(property)) {
exportedSet[property] = settings[id][property];
} else if (setting.hasOwnProperty('value')) {
exportedSet[property] = setting.value;
}
}
}
}
if (Object.keys(exportedSet).length > 0) {
safeSettings[id] = exportedSet;
}
}
}
}
return safeSettings;
}
function removeModule(moduleId) {
// clean the (plugin) registry when a module is removed / uninstalled
let pluginList = [];
let module = registry.getModule(moduleId);
let keys = Object.keys(module.plugins ?? {});
keys.forEach( key => {
let _plugins = module.plugins[key].plugins ?? [];
_plugins.forEach( plugin => {
let id = plugin.id;
if (plugin.onremove && typeof plugin.onremove === 'function') {
plugin.onremove();
}
delete pluginToId[id];
delete plugins[id];
delete pluginSettings[id];
pluginConfigCache = {};
let psbtype = pluginsByType[plugin.type] ?? [];
for (let i=psbtype.length; i>0; i--) {
let pbt = psbtype[i-1];
if (pbt.id == id) {
psbtype.splice(i-1, 1);
}
}
})
pluginList.push(registry.filterNodeInfo(module.plugins[key]));
})
return pluginList;
}
module.exports = {
init,
registerPlugin,
getPlugin,
getPluginsByType,
getPluginConfigs,
getPluginConfig,
getPluginList,
exportPluginSettings,
removeModule
}
+778
View File
@@ -0,0 +1,778 @@
/**
* Copyright JS Foundation and other contributors, http://js.foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
//var UglifyJS = require("uglify-js");
const path = require("path");
const fs = require("fs");
var library = require("./library");
const {events} = require("@node-red/util")
var subflows = require("./subflow");
var externalModules = require("./externalModules")
var settings;
var loader;
var nodeConfigCache = {};
var moduleConfigs = {};
var nodeList = [];
var nodeConstructors = {};
var nodeOptions = {};
var subflowModules = {};
var nodeTypeToId = {};
var moduleNodes = {};
function init(_settings,_loader) {
settings = _settings;
loader = _loader;
clear();
}
function load() {
if (settings.available()) {
moduleConfigs = loadNodeConfigs();
} else {
moduleConfigs = {};
}
}
function filterNodeInfo(n) {
var r = {
id: n.id||n.module+"/"+n.name,
name: n.name,
types: n.types,
enabled: n.enabled,
local: n.local||false,
user: n.user || false
};
if (n.hasOwnProperty("module")) {
r.module = n.module;
}
if (n.hasOwnProperty("err")) {
r.err = n.err;
}
if (n.hasOwnProperty("plugins")) {
r.plugins = n.plugins.map(p => {
return {
id: p.id,
type: p.type,
module: p.module
}
});
}
if (n.type === "plugin") {
r.editor = !!n.template;
r.runtime = !!n.file;
}
return r;
}
function getModuleFromSetId(id) {
var parts = id.split("/");
return parts.slice(0,parts.length-1).join("/");
}
function getNodeFromSetId(id) {
var parts = id.split("/");
return parts[parts.length-1];
}
function saveNodeList() {
var moduleList = {};
var hadPending = false;
var hasPending = false;
for (var module in moduleConfigs) {
/* istanbul ignore else */
if (moduleConfigs.hasOwnProperty(module)) {
if (Object.keys(moduleConfigs[module].nodes).length > 0) {
if (!moduleList[module]) {
moduleList[module] = {
name: module,
version: moduleConfigs[module].version,
local: moduleConfigs[module].local||false,
user: moduleConfigs[module].user||false,
nodes: {}
};
if (moduleConfigs[module].hasOwnProperty('pending_version')) {
hadPending = true;
if (moduleConfigs[module].pending_version !== moduleConfigs[module].version) {
moduleList[module].pending_version = moduleConfigs[module].pending_version;
hasPending = true;
} else {
delete moduleConfigs[module].pending_version;
}
}
}
var nodes = moduleConfigs[module].nodes;
for(var node in nodes) {
/* istanbul ignore else */
if (nodes.hasOwnProperty(node)) {
var config = nodes[node];
var n = filterNodeInfo(config);
delete n.err;
delete n.file;
delete n.id;
n.file = config.file;
moduleList[module].nodes[node] = n;
}
}
}
}
}
if (hadPending && !hasPending) {
events.emit("runtime-event",{id:"restart-required",retain: true});
}
if (settings.available()) {
return settings.set("nodes",moduleList);
} else {
return Promise.reject("Settings unavailable");
}
}
function loadNodeConfigs() {
var configs = settings.get("nodes");
if (!configs) {
return {};
} else if (configs['node-red']) {
return configs;
} else {
// Migrate from the 0.9.1 format of settings
var newConfigs = {};
for (var id in configs) {
/* istanbul ignore else */
if (configs.hasOwnProperty(id)) {
var nodeConfig = configs[id];
var moduleName;
var nodeSetName;
if (nodeConfig.module) {
moduleName = nodeConfig.module;
nodeSetName = nodeConfig.name.split(":")[1];
} else {
moduleName = "node-red";
nodeSetName = nodeConfig.name.replace(/^\d+-/,"").replace(/\.js$/,"");
}
if (!newConfigs[moduleName]) {
newConfigs[moduleName] = {
name: moduleName,
nodes:{}
};
}
newConfigs[moduleName].nodes[nodeSetName] = {
name: nodeSetName,
types: nodeConfig.types,
enabled: nodeConfig.enabled,
module: moduleName
};
}
}
settings.set("nodes",newConfigs);
return newConfigs;
}
}
function addModule(module) {
moduleNodes[module.name] = [];
moduleConfigs[module.name] = module;
for (const setName in module.nodes) {
if (module.nodes.hasOwnProperty(setName)) {
const set = module.nodes[setName];
if (!set.types) {
const err = new Error("Set has no types")
err.code = "set_has_no_types"
err.details = {
...set
}
set.err = err
}
moduleNodes[module.name].push(set.name);
nodeList.push(set.id);
if (!set.err) {
set.types.forEach(function(t) {
if (nodeTypeToId.hasOwnProperty(t)) {
set.err = new Error("Type already registered");
set.err.code = "type_already_registered";
set.err.details = {
type: t,
moduleA: getNodeInfo(t).module,
moduleB: set.module
}
}
});
if (!set.err) {
set.types.forEach(function(t) {
nodeTypeToId[t] = set.id;
});
}
}
}
}
if (module.icons) {
icon_paths[module.name] = icon_paths[module.name] || [];
module.icons.forEach(icon=>icon_paths[module.name].push(path.resolve(icon.path)) )
}
if (module.examples) {
library.addExamplesDir(module.name,module.examples.path);
}
nodeConfigCache = {};
}
function removeNode(id) {
var config = moduleConfigs[getModuleFromSetId(id)].nodes[getNodeFromSetId(id)];
if (!config) {
throw new Error("Unrecognised id: "+id);
}
delete moduleConfigs[getModuleFromSetId(id)].nodes[getNodeFromSetId(id)];
var i = nodeList.indexOf(id);
if (i > -1) {
nodeList.splice(i,1);
}
config.types.forEach(function(t) {
var typeId = nodeTypeToId[t];
if (typeId === id) {
delete subflowModules[t];
delete nodeConstructors[t];
delete nodeOptions[t];
delete nodeTypeToId[t];
}
});
config.enabled = false;
config.loaded = false;
nodeConfigCache = {};
return filterNodeInfo(config);
}
function removeModule(name,skipSave) {
if (!settings.available()) {
throw new Error("Settings unavailable");
}
var infoList = [];
var module = moduleConfigs[name];
var nodes = moduleNodes[name];
if (!nodes) {
throw new Error("Unrecognised module: "+name);
}
if (module.usedBy && module.usedBy > 0) {
// We are removing a module that is used by other modules... so whilst
// this module should be removed from the editor palette, it needs to
// stay in the runtime... for now.
module.user = false;
for (var i=0;i<nodes.length;i++) {
infoList.push(filterNodeInfo(nodes[i]));
}
} else {
if (module.dependencies) {
module.dependencies.forEach(function(dep) {
// Check each dependency of this module to see if it is a non-user-installed
// module that we can expect to disappear once npm uninstall is run
if (!moduleConfigs[dep].user) {
moduleConfigs[dep].usedBy = moduleConfigs[dep].usedBy.filter(m => m !== name);
if (moduleConfigs[dep].usedBy.length === 0) {
// Remove the dependency
removeModule(dep,true);
}
}
});
}
for (var i=0;i<nodes.length;i++) {
infoList.push(removeNode(name+"/"+nodes[i]));
}
delete moduleNodes[name];
delete moduleConfigs[name];
}
if (!skipSave) {
saveNodeList();
}
return infoList;
}
function getNodeInfo(typeOrId) {
var id = typeOrId;
if (nodeTypeToId.hasOwnProperty(typeOrId)) {
id = nodeTypeToId[typeOrId];
}
/* istanbul ignore else */
if (id) {
var module = moduleConfigs[getModuleFromSetId(id)];
if (module) {
var config = module.nodes[getNodeFromSetId(id)];
if (config) {
var info = filterNodeInfo(config);
if (config.hasOwnProperty("loaded")) {
info.loaded = config.loaded;
}
if (module.pending_version) {
info.pending_version = module.pending_version;
}
info.version = module.version;
return info;
}
}
}
return null;
}
function getFullNodeInfo(typeOrId) {
// Used by index.enableNodeSet so that .file can be retrieved to pass
// to loader.loadNodeSet
var id = typeOrId;
if (nodeTypeToId.hasOwnProperty(typeOrId)) {
id = nodeTypeToId[typeOrId];
}
/* istanbul ignore else */
if (id) {
var module = moduleConfigs[getModuleFromSetId(id)];
if (module) {
return module.nodes[getNodeFromSetId(id)];
}
}
return null;
}
function getNodeList(filter) {
var list = [];
for (var module in moduleConfigs) {
/* istanbul ignore else */
if (moduleConfigs.hasOwnProperty(module)) {
if (!moduleConfigs[module].user && (moduleConfigs[module].usedBy && moduleConfigs[module].usedBy.length > 0)) {
continue;
}
var nodes = moduleConfigs[module].nodes;
for (var node in nodes) {
/* istanbul ignore else */
if (nodes.hasOwnProperty(node)) {
var nodeInfo = filterNodeInfo(nodes[node]);
nodeInfo.version = moduleConfigs[module].version;
if (moduleConfigs[module].pending_version) {
nodeInfo.pending_version = moduleConfigs[module].pending_version;
}
if (!filter || filter(nodes[node])) {
list.push(nodeInfo);
}
}
}
}
}
return list;
}
function getModuleList() {
return moduleConfigs;
}
function getModule(id) {
return moduleConfigs[id];
}
function getModuleInfo(module) {
if (moduleNodes[module]) {
var nodes = moduleNodes[module];
var m = {
name: module,
version: moduleConfigs[module].version,
local: moduleConfigs[module].local,
user: moduleConfigs[module].user,
path: moduleConfigs[module].path,
nodes: [],
plugins: []
};
if (moduleConfigs[module].dependencies) {
m.dependencies = moduleConfigs[module].dependencies;
}
if (moduleConfigs[module] && moduleConfigs[module].pending_version) {
m.pending_version = moduleConfigs[module].pending_version;
}
for (var i = 0; i < nodes.length; ++i) {
var nodeInfo = filterNodeInfo(moduleConfigs[module].nodes[nodes[i]]);
nodeInfo.version = m.version;
m.nodes.push(nodeInfo);
}
let plugins = Object.values(moduleConfigs[module].plugins ?? {});
plugins.forEach((plugin) => {
let nodeInfo = filterNodeInfo(plugin);
nodeInfo.version = m.version;
m.plugins.push(nodeInfo);
});
return m;
} else {
return null;
}
}
function getCaller(){
var orig = Error.prepareStackTrace;
Error.prepareStackTrace = function(_, stack){ return stack; };
var err = new Error();
Error.captureStackTrace(err, arguments.callee);
var stack = err.stack;
Error.prepareStackTrace = orig;
stack.shift();
stack.shift();
return stack[0].getFileName();
}
function registerNodeConstructor(nodeSet,type,constructor,options) {
if (nodeConstructors.hasOwnProperty(type)) {
throw new Error(type+" already registered");
}
//TODO: Ensure type is known - but doing so will break some tests
// that don't have a way to register a node template ahead
// of registering the constructor
var nodeSetInfo = getFullNodeInfo(nodeSet);
if (nodeSetInfo) {
if (nodeSetInfo.types.indexOf(type) === -1) {
// A type is being registered for a known set, but for some reason
// we didn't spot it when parsing the HTML file.
// Registered a type is the definitive action - not the presence
// of an edit template. Ensure it is on the list of known types.
nodeSetInfo.types.push(type);
}
}
nodeConstructors[type] = constructor;
nodeOptions[type] = options;
if (options) {
if (options.dynamicModuleList) {
externalModules.register(type,options.dynamicModuleList);
}
}
events.emit("type-registered",type);
}
function registerSubflow(nodeSet, subflow) {
var nodeSetInfo = getFullNodeInfo(nodeSet);
const result = subflows.register(nodeSet,subflow);
if (subflowModules.hasOwnProperty(result.type)) {
throw new Error(result.type+" already registered");
}
if (nodeSetInfo) {
if (nodeSetInfo.types.indexOf(result.type) === -1) {
nodeSetInfo.types.push(result.type);
nodeTypeToId[result.type] = nodeSetInfo.id;
}
nodeSetInfo.config = result.config;
}
subflowModules[result.type] = result;
externalModules.registerSubflow(result.type,subflow);
events.emit("type-registered",result.type);
return result;
}
function getAllNodeConfigs(lang) {
if (!nodeConfigCache[lang]) {
var result = "";
var script = "";
for (var i=0;i<nodeList.length;i++) {
var id = nodeList[i];
var module = moduleConfigs[getModuleFromSetId(id)]
if (!module.user && (module.usedBy && module.usedBy.length > 0)) {
continue;
}
var config = module.nodes[getNodeFromSetId(id)];
if (config.enabled && !config.err) {
result += "\n<!-- --- [red-module:"+id+"] --- -->\n";
result += config.config;
result += loader.getNodeHelp(config,lang||"en-US")||"";
//script += config.script;
}
}
//if (script.length > 0) {
// result += '<script type="text/javascript">';
// result += UglifyJS.minify(script, {fromString: true}).code;
// result += '</script>';
//}
nodeConfigCache[lang] = result;
}
return nodeConfigCache[lang];
}
function getNodeConfig(id,lang) {
var config = moduleConfigs[getModuleFromSetId(id)];
if (!config) {
return null;
}
config = config.nodes[getNodeFromSetId(id)];
if (config) {
var result = "<!-- --- [red-module:"+id+"] --- -->\n"+config.config;
result += loader.getNodeHelp(config,lang||"en-US")
//if (config.script) {
// result += '<script type="text/javascript">'+config.script+'</script>';
//}
return result;
} else {
return null;
}
}
function getNodeConstructor(type) {
var id = nodeTypeToId[type];
var config;
if (typeof id === "undefined") {
config = undefined;
} else {
config = moduleConfigs[getModuleFromSetId(id)].nodes[getNodeFromSetId(id)];
}
if (!config || (config.enabled && !config.err)) {
return nodeConstructors[type] || subflowModules[type];
}
return null;
}
function clear() {
nodeConfigCache = {};
moduleConfigs = {};
nodeList = [];
nodeConstructors = {};
nodeOptions = {};
subflowModules = {};
nodeTypeToId = {};
}
function getTypeId(type) {
if (nodeTypeToId.hasOwnProperty(type)) {
return nodeTypeToId[type];
} else {
return null;
}
}
function enableNodeSet(typeOrId) {
if (!settings.available()) {
throw new Error("Settings unavailable");
}
var id = typeOrId;
if (nodeTypeToId.hasOwnProperty(typeOrId)) {
id = nodeTypeToId[typeOrId];
}
var config;
try {
config = moduleConfigs[getModuleFromSetId(id)].nodes[getNodeFromSetId(id)];
delete config.err;
config.enabled = true;
nodeConfigCache = {};
settings.enableNodeSettings(config.types);
return saveNodeList().then(function() {
return filterNodeInfo(config);
});
} catch (err) {
throw new Error("Unrecognised id: "+typeOrId);
}
}
function disableNodeSet(typeOrId) {
if (!settings.available()) {
throw new Error("Settings unavailable");
}
var id = typeOrId;
if (nodeTypeToId.hasOwnProperty(typeOrId)) {
id = nodeTypeToId[typeOrId];
}
var config;
try {
config = moduleConfigs[getModuleFromSetId(id)].nodes[getNodeFromSetId(id)];
// TODO: persist setting
config.enabled = false;
nodeConfigCache = {};
settings.disableNodeSettings(config.types);
return saveNodeList().then(function() {
return filterNodeInfo(config);
});
} catch (err) {
throw new Error("Unrecognised id: "+id);
}
}
function cleanModuleList() {
var removed = false;
for (var mod in moduleConfigs) {
/* istanbul ignore else */
if (moduleConfigs.hasOwnProperty(mod)) {
var nodes = moduleConfigs[mod].nodes;
var node;
if (mod == "node-red") {
// For core nodes, look for nodes that are enabled, !loaded and !errored
for (node in nodes) {
/* istanbul ignore else */
if (nodes.hasOwnProperty(node)) {
var n = nodes[node];
if (n.enabled && !n.err && !n.loaded) {
removeNode(mod+"/"+node);
removed = true;
}
}
}
} else {
if (moduleConfigs[mod] && !moduleNodes[mod]) {
// For node modules, look for missing ones
for (node in nodes) {
/* istanbul ignore else */
if (nodes.hasOwnProperty(node)) {
removeNode(mod+"/"+node);
removed = true;
}
}
delete moduleConfigs[mod];
}
}
}
}
if (removed) {
saveNodeList();
}
}
function setModulePendingUpdated(module,version) {
moduleConfigs[module].pending_version = version;
return saveNodeList().then(function() {
return getModuleInfo(module);
});
}
function setUserInstalled(module,userInstalled) {
moduleConfigs[module].user = userInstalled;
return saveNodeList().then(function() {
return getModuleInfo(module);
});
}
function addModuleDependency(module,usedBy) {
moduleConfigs[module].usedBy = moduleConfigs[module].usedBy || [];
moduleConfigs[module].usedBy.push(usedBy);
}
var icon_paths = { };
var iconCache = {};
function getNodeIconPath(module,icon) {
if (/\.\./.test(icon)) {
throw new Error();
}
var iconName = module+"/"+icon;
if (iconCache[iconName]) {
return iconCache[iconName];
} else {
var paths = icon_paths[module];
if (paths) {
for (var p=0;p<paths.length;p++) {
var iconPath = path.join(paths[p],icon);
try {
fs.statSync(iconPath);
iconCache[iconName] = iconPath;
return iconPath;
} catch(err) {
// iconPath doesn't exist
}
}
}
if (module !== "node-red") {
return getNodeIconPath("node-red", icon);
}
return null;
}
}
function getNodeIcons() {
var iconList = {};
for (var module in moduleConfigs) {
if (moduleConfigs.hasOwnProperty(module)) {
if (moduleConfigs[module].icons) {
iconList[module] = [];
moduleConfigs[module].icons.forEach(icon=>{ iconList[module] = iconList[module].concat(icon.icons)})
}
}
}
return iconList;
}
function getModuleResource(module, resourcePath) {
let mod = moduleConfigs[module];
if (mod && mod.resources) {
let basePath = mod.resources.path;
let fullPath = path.join(basePath,resourcePath);
if (/^\.\./.test(path.relative(basePath,fullPath))) {
return null;
}
if (fs.existsSync(fullPath)) {
return fullPath;
}
}
return null;
}
var registry = module.exports = {
init: init,
load: load,
clear: clear,
registerNodeConstructor: registerNodeConstructor,
getNodeConstructor: getNodeConstructor,
registerSubflow: registerSubflow,
addModule: addModule,
enableNodeSet: enableNodeSet,
disableNodeSet: disableNodeSet,
setModulePendingUpdated: setModulePendingUpdated,
setUserInstalled: setUserInstalled,
addModuleDependency:addModuleDependency,
removeModule: removeModule,
getNodeInfo: getNodeInfo,
getFullNodeInfo: getFullNodeInfo,
getNodeList: getNodeList,
getModuleList: getModuleList,
getModule: getModule,
getModuleInfo: getModuleInfo,
getNodeIconPath: getNodeIconPath,
getNodeIcons: getNodeIcons,
getModuleResource: getModuleResource,
/**
* Gets all of the node template configs
* @return all of the node templates in a single string
*/
getAllNodeConfigs: getAllNodeConfigs,
getNodeConfig: getNodeConfig,
getTypeId: getTypeId,
saveNodeList: saveNodeList,
cleanModuleList: cleanModuleList,
getModuleFromSetId: getModuleFromSetId,
getNodeFromSetId: getNodeFromSetId,
filterNodeInfo: filterNodeInfo
};
+127
View File
@@ -0,0 +1,127 @@
function getSubflowType(subflow) {
if (subflow.meta && subflow.meta.type) {
return subflow.meta.type
}
return "sf:"+subflow.id
}
function generateSubflowConfig(subflow) {
const subflowType = getSubflowType(subflow)
const label = subflow.name || subflowType;
const category = subflow.category || "function";
const color = subflow.color || "#C0DEED";
const inputCount = subflow.in?subflow.in.length:0;
const outputCount = subflow.out?subflow.out.length:0;
const icon = subflow.icon || "arrow-in.svg";
const defaults = {
name: {value: ""}
}
const credentials = {}
if (subflow.env) {
subflow.env.forEach(prop => {
var defaultValue;
switch(prop.type) {
case "cred": defaultValue = ""; break;
case "str": defaultValue = prop.value||""; break;
case "bool": defaultValue = (typeof prop.value === 'boolean')?prop.value:prop.value === "true" ; break;
case "num": defaultValue = (typeof prop.value === 'number')?prop.value:Number(prop.value); break;
default:
defaultValue = {
type: prop.type,
value: prop.value||""
}
}
defaults[prop.name] = {
value: defaultValue,
ui: prop.ui
}
if (prop.type === 'cred') {
defaults[prop.name].ui.type = "cred";
credentials[prop.name] = {type:"password"}
}
})
}
const defaultString = JSON.stringify(defaults);
const credentialsString = JSON.stringify(credentials);
let nodeHelp = "";
if (subflow.info) {
nodeHelp = `<script type="text/markdown" data-help-name="${subflowType}">${subflow.info}</script>`
}
return `<script type="text/javascript">
RED.nodes.registerType("${subflowType}",{
subflowModule: true,
category: "${category}",
color: "${color}",
defaults: ${defaultString},
credentials: ${credentialsString},
inputs:${inputCount},
outputs:${outputCount},
icon: "${icon}",
paletteLabel: "${label}",
label: function() {
return this.name||"${label}";
},
labelStyle: function() {
return this.name?"node_label_italic":"";
},
oneditprepare: function() {
RED.subflow.buildEditForm('subflow', this);
},
oneditsave: function() {
var props = RED.subflow.exportSubflowInstanceEnv(this);
var i=0,l=props.length;
for (;i<l;i++) {
var prop = props[i];
if (this._def.defaults[prop.name].ui && this._def.defaults[prop.name].ui.type === "cred") {
this[prop.name] = "";
this.credentials[prop.name] = prop.value || "";
this.credentials['has_' + prop.name] = (this.credentials[prop.name] !== "");
} else {
switch(prop.type) {
case "str": case "conf-type": this[prop.name] = prop.value||""; break;
case "bool": this[prop.name] = (typeof prop.value === 'boolean')?prop.value:prop.value === "true" ; break;
case "num": this[prop.name] = (typeof prop.value === 'number')?prop.value:Number(prop.value); break;
default:
this[prop.name] = {
type: prop.type,
value: prop.value||""
}
}
}
}
}
});
</script>
<script type="text/x-red" data-template-name="${subflowType}">
<div class="form-row">
<label for="node-input-name" data-i18n="[append]editor:common.label.name"><i class="fa fa-tag"></i> </label>
<input type="text" id="node-input-name" data-i18n="[placeholder]editor:common.label.name">
</div>
<div id="subflow-input-ui"></div>
</script>
${nodeHelp}
`
}
function register(id,subflow) {
return {
subflow: subflow,
type: getSubflowType(subflow),
config: generateSubflowConfig(subflow)
}
}
module.exports = {
register: register
}
+207
View File
@@ -0,0 +1,207 @@
/**
* Copyright JS Foundation and other contributors, http://js.foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
const path = require("path");
const semver = require("semver");
const url = require("url");
const {events,i18n,log} = require("@node-red/util");
var runtime;
function copyObjectProperties(src,dst,copyList,blockList) {
if (!src) {
return;
}
if (copyList && !blockList) {
copyList.forEach(function(i) {
if (src.hasOwnProperty(i)) {
var propDescriptor = Object.getOwnPropertyDescriptor(src,i);
Object.defineProperty(dst,i,propDescriptor);
}
});
} else if (!copyList && blockList) {
for (var i in src) {
if (src.hasOwnProperty(i) && blockList.indexOf(i) === -1) {
var propDescriptor = Object.getOwnPropertyDescriptor(src,i);
Object.defineProperty(dst,i,propDescriptor);
}
}
}
}
function requireModule(name) {
var moduleInfo = require("./index").getModuleInfo(name);
if (moduleInfo && moduleInfo.path) {
var relPath = path.relative(__dirname, moduleInfo.path);
return require(relPath);
} else {
// Require it here to avoid the circular dependency
return require("./externalModules").require(name);
}
}
function importModule(name) {
var moduleInfo = require("./index").getModuleInfo(name);
if (moduleInfo && moduleInfo.path) {
const moduleFile = url.pathToFileURL(require.resolve(moduleInfo.path));
return import(moduleFile);
} else {
// Require it here to avoid the circular dependency
return require("./externalModules").import(name);
}
}
function createNodeApi(node) {
var red = {
nodes: {},
log: {},
settings: {},
events: events,
hooks: runtime.hooks,
util: runtime.util,
version: runtime.version,
require: requireModule,
import: importModule,
comms: {
publish: function(topic,data,retain) {
events.emit("comms",{
topic: topic,
data: data,
retain: retain
})
}
},
plugins: {
registerPlugin: function(id,definition) {
return runtime.plugins.registerPlugin(node.id,id,definition);
},
get: function(id) {
return runtime.plugins.getPlugin(id);
},
getByType: function(type) {
return runtime.plugins.getPluginsByType(type);
}
},
library: {
register: function(type) {
return runtime.library.register(node.id,type);
}
},
httpNode: runtime.nodeApp,
httpAdmin: runtime.adminApp,
server: runtime.server
}
copyObjectProperties(runtime.nodes,red.nodes,["createNode","getNode","eachNode","addCredentials","getCredentials","deleteCredentials"]);
red.nodes.registerType = function(type,constructor,opts) {
runtime.nodes.registerType(node.id,type,constructor,opts);
}
red.nodes.registerSubflow = function(subflowDef) {
runtime.nodes.registerSubflow(node.id,subflowDef)
}
copyObjectProperties(log,red.log,null,["init"]);
copyObjectProperties(runtime.settings,red.settings,null,["init","load","reset"]);
if (runtime.adminApi) {
red.auth = runtime.adminApi.auth;
} else {
//TODO: runtime.adminApi is always stubbed if not enabled, so this block
// is unused - but may be needed for the unit tests
red.auth = {
needsPermission: function(v) { return function(req,res,next) {next()} }
};
// TODO: stub out httpAdmin/httpNode/server
}
red["_"] = function() {
var args = Array.prototype.slice.call(arguments, 0);
if (args[0].indexOf(":") === -1) {
args[0] = node.namespace+":"+args[0];
}
return i18n._.apply(null,args);
}
return red;
}
function checkAgainstList(module,version,list) {
for (let i=0;i<list.length;i++) {
let rule = list[i];
if (rule.module.test(module)) {
if (version && rule.version) {
if (semver.satisfies(version,rule.version)) {
return rule;
}
} else {
return rule;
}
}
}
}
function checkModuleAllowed(module,version,allowList,denyList) {
if (!allowList && !denyList) {
// Default to allow
return true;
}
if (allowList.length === 0 && denyList.length === 0) {
return true;
}
var allowedRule = checkAgainstList(module,version,allowList);
var deniedRule = checkAgainstList(module,version,denyList);
// console.log("A",allowedRule)
// console.log("D",deniedRule)
if (allowedRule && !deniedRule) {
return true;
}
if (!allowedRule && deniedRule) {
return false;
}
if (!allowedRule && !deniedRule) {
return true;
}
if (allowedRule.wildcardPos !== deniedRule.wildcardPos) {
return allowedRule.wildcardPos > deniedRule.wildcardPos
} else {
// First wildcard in same position.
// Go with the longer matching rule. This isn't going to be 100%
// right, but we are deep into edge cases at this point.
return allowedRule.module.toString().length > deniedRule.module.toString().length
}
return false;
}
function parseModuleList(list) {
list = list || ["*"];
return list.map(rule => {
let m = /^(.+?)(?:@(.*))?$/.exec(rule);
let wildcardPos = m[1].indexOf("*");
wildcardPos = wildcardPos===-1?Infinity:wildcardPos;
return {
module: new RegExp("^"+m[1].replace(/\*/g,".*")+"$"),
version: m[2],
wildcardPos: wildcardPos
}
})
}
module.exports = {
init: function(_runtime) {
runtime = _runtime;
},
createNodeApi: createNodeApi,
parseModuleList: parseModuleList,
checkModuleAllowed: checkModuleAllowed
}
+26
View File
@@ -0,0 +1,26 @@
{
"name": "@node-red/registry",
"version": "4.1.0",
"license": "Apache-2.0",
"main": "./lib/index.js",
"repository": {
"type": "git",
"url": "https://github.com/node-red/node-red.git"
},
"contributors": [
{
"name": "Nick O'Leary"
},
{
"name": "Dave Conway-Jones"
}
],
"dependencies": {
"@node-red/util": "4.1.0",
"clone": "2.1.2",
"fs-extra": "11.3.0",
"semver": "7.7.1",
"tar": "7.4.3",
"uglify-js": "3.19.3"
}
}