node-red
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
src
|
||||
docs
|
||||
+178
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user