signalk-server
Version:
An implementation of a [Signal K](http://signalk.org) server for boats.
291 lines (289 loc) • 11.4 kB
JavaScript
;
/* 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
};