UNPKG

signalk-server

Version:

An implementation of a [Signal K](http://signalk.org) server for boats.

291 lines (289 loc) 11.4 kB
"use strict"; /* eslint-disable @typescript-eslint/no-explicit-any */ /* * Copyright 2017 Teppo Kurki <teppo.kurki@iki.fi> * * 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 __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.modulesWithKeyword = modulesWithKeyword; exports.restoreModules = restoreModules; exports.checkForNewServerVersion = checkForNewServerVersion; exports.getAuthor = getAuthor; exports.getKeywords = getKeywords; exports.importOrRequire = importOrRequire; const child_process_1 = require("child_process"); const fs_1 = __importDefault(require("fs")); const lodash_1 = __importDefault(require("lodash")); const path_1 = __importDefault(require("path")); const semver_1 = __importStar(require("semver")); const debug_1 = require("./debug"); const debug = (0, debug_1.createDebug)('signalk:modules'); const npmDebug = (0, debug_1.createDebug)('signalk:modules:npm'); function findModulesInDir(dir, keyword) { // If no directory by name return empty array. if (!fs_1.default.existsSync(dir)) { return []; } debug('findModulesInDir: ' + dir); return fs_1.default .readdirSync(dir) .filter((name) => name !== '.bin') .reduce((result, filename) => { if (filename.indexOf('@') === 0) { return result.concat(findModulesInDir(dir + filename + '/', keyword).map((entry) => { return { module: entry.module, metadata: entry.metadata, location: dir }; })); } else { let metadata; try { // eslint-disable-next-line @typescript-eslint/no-require-imports metadata = require(path_1.default.join(dir, filename, 'package.json')); } catch (err) { debug(err); } if (metadata && metadata.keywords && metadata.keywords.includes(keyword)) { result.push({ module: metadata.name, metadata, location: dir }); } } return result; }, []); } // Extract unique directory paths from app object. function getModulePaths(config) { // appPath is the app working directory. const { appPath, configPath } = config; return (appPath === configPath ? [appPath] : [configPath, appPath]).map((pathOption) => path_1.default.join(pathOption, 'node_modules/')); } const getModuleSortName = (x) => (x.module || '').replace('@signalk', ' '); // Sort handler that puts strings with '@signalk' first. const priorityPrefix = (a, b) => getModuleSortName(a).localeCompare(getModuleSortName(b)); // Searches for installed modules that contain `keyword`. function modulesWithKeyword(config, keyword) { return lodash_1.default.uniqBy( // _.flatten since values are inside an array. [[modules...], [modules...]] lodash_1.default.flatten(getModulePaths(config).map((pathOption) => findModulesInDir(pathOption, keyword))), (moduleData) => moduleData.module).sort(priorityPrefix); } function installModule(config, name, version, onData, onErr, onClose) { runNpm(config, name, version, 'install', onData, onErr, onClose); } function removeModule(config, name, version, onData, onErr, onClose) { runNpm(config, name, null, 'remove', onData, onErr, onClose); } function restoreModules(config, onData, onErr, onClose) { runNpm(config, null, null, 'remove', onData, onErr, onClose); } function runNpm(config, name, version, command, onData, onErr, onClose) { let npm; const opts = {}; let packageString; if (name) { packageString = version ? `${name}@${version}` : name; } else { packageString = ''; } debug(`${command}: ${packageString}`); if (isTheServerModule(name, config)) { if (process.platform === 'win32') { npm = (0, child_process_1.spawn)('cmd', ['/c', `npm ${command} -g ${packageString} `], opts); } else { npm = (0, child_process_1.spawn)('sudo', ['npm', command, '-g', packageString], opts); } } else { opts.cwd = config.configPath; if (process.platform === 'win32') { npm = (0, child_process_1.spawn)('cmd', ['/c', `npm --save ${command} ${packageString}`], opts); } else { npm = (0, child_process_1.spawn)('npm', ['--save', command, packageString], opts); } } npm.stdout.on('data', onData); npm.stderr.on('data', onErr); npm.on('close', onClose); npm.on('error', (err) => { onErr(err); onClose(-1); }); } function isTheServerModule(moduleName, config) { return moduleName === config.name; } const modulesByKeyword = {}; async function findModulesWithKeyword(keyword) { if (modulesByKeyword[keyword] && Date.now() - modulesByKeyword[keyword].time < 60 * 1000) { return modulesByKeyword[keyword].packages; } const moduleData = await searchByKeyword(keyword); npmDebug(`npm search returned ${moduleData.length} modules with keyword ${keyword}`); const result = moduleData.reduce((acc, module) => { const name = module.package.name; if (!acc[name] || semver_1.default.gt(module.package.version, acc[name].package.version)) { acc[name] = module; } return acc; }, {}); const packages = Object.values(result); modulesByKeyword[keyword] = { time: Date.now(), packages }; return packages; } async function searchByKeyword(keyword) { let fetchedCount = 0; let toFetchCount = 1; let moduleData = []; while (fetchedCount < toFetchCount) { npmDebug(`searching ${keyword} from ${fetchedCount + 1} of ${toFetchCount}`); const res = await fetch(`https://registry.npmjs.org/-/v1/search?size=250&from=${fetchedCount > 0 ? fetchedCount : 0}&text=keywords:${keyword}`); const parsed = (await res.json()); moduleData = moduleData.concat(parsed.objects); fetchedCount += parsed.objects.length; toFetchCount = parsed.total; } return moduleData; } function doFetchDistTags() { return fetch('https://registry.npmjs.org/-/package/signalk-server/dist-tags'); } async function getLatestServerVersion(currentVersion, distTags = doFetchDistTags) { const versions = (await (await distTags()).json()); const prereleaseData = semver_1.default.prerelease(currentVersion); if (prereleaseData) { if (semver_1.default.satisfies(versions.latest, `>${currentVersion}`)) { return versions.latest; } else { return versions[prereleaseData[0]]; } } else { return versions.latest; } } function checkForNewServerVersion(currentVersion, serverUpgradeIsAvailable, getLatestServerVersionP = getLatestServerVersion) { getLatestServerVersionP(currentVersion) .then((version) => { if (semver_1.default.satisfies(new semver_1.SemVer(version), `>${currentVersion}`)) { serverUpgradeIsAvailable(undefined, version); } }) .catch((err) => { serverUpgradeIsAvailable(`unable to check for new server version: ${err}`); }); } function getAuthor(thePackage) { return `${thePackage.publisher?.username}${thePackage.name.startsWith('@signalk/') ? ' (Signal K team)' : ''}`; } function getKeywords(thePackage) { const keywords = thePackage.keywords; debug('%s keywords: %j', thePackage.name, keywords); return keywords; } async function importOrRequire(moduleDir) { try { // eslint-disable-next-line @typescript-eslint/no-require-imports const mod = require(moduleDir); // Starting with version 20.19.0 and 22 Node will load ESM modules with require // https://nodejs.org/en/blog/release/v20.19.0 return mod.default ?? mod; } catch (err) { debug(`Failed to require("${moduleDir}") module, trying import()`); // `import()` only works with file paths or npm module names. It can't // directly load a path to a directory. One solution would be to refactor // module loading to update `NODE_PATH` with plugin directories, and // then import/require them here using just their module name (e.g. // `import("@signalk/plugin-name")`), which would allow NodeJS to resolve // and load the module. This would be a little more extensive refactoring // that may be worth while once the whole project is entirely using ESM. // For now, this `esm-resolve` package work const { buildResolver } = await import('esm-resolve'); const resolver = buildResolver(moduleDir, { isDir: true, resolveToAbsolute: true }); const modulePath = resolver('.'); if (modulePath) { const module = await import(modulePath); return module.default; } else { // Could not resolve, throw the original error. throw err; } } } module.exports = { modulesWithKeyword, installModule, removeModule, isTheServerModule, findModulesWithKeyword, getLatestServerVersion, checkForNewServerVersion, getAuthor, getKeywords, restoreModules, importOrRequire };