lion/packages-node/providence-analytics/dashboard/src/server.mjs
2021-11-17 00:07:04 +01:00

159 lines
5.3 KiB
JavaScript

import fs from 'fs';
import pathLib, { dirname } from 'path';
import { fileURLToPath } from 'url';
import { createConfig, startServer } from 'es-dev-server';
import { ReportService } from '../../src/program/services/ReportService.js';
import { getProvidenceConf } from '../../src/program/utils/get-providence-conf.mjs';
const __dirname = dirname(fileURLToPath(import.meta.url));
/**
* Gets all results found in cache folder with all results
* @param {{ supportedAnalyzers: `match-${string}`[], resultsPath: string }} options
*/
async function getCachedProvidenceResults({
supportedAnalyzers = ['match-imports', 'match-subclasses'],
resultsPath = ReportService.outputPath,
} = {}) {
/**
* Paths of every individual cachde result
* @type {string[]}
*/
let outputFilePaths;
try {
outputFilePaths = fs.readdirSync(resultsPath);
} catch (_) {
throw new Error(`Please make sure providence results can be found in ${resultsPath}`);
}
const resultFiles = {};
let searchTargetDeps;
outputFilePaths.forEach(fileName => {
const content = JSON.parse(fs.readFileSync(pathLib.join(resultsPath, fileName), 'utf-8'));
if (fileName === 'search-target-deps-file.json') {
searchTargetDeps = content;
} else {
const analyzerName = fileName.split('_-_')[0];
if (!supportedAnalyzers.includes(analyzerName)) {
return;
}
if (!resultFiles[analyzerName]) {
resultFiles[analyzerName] = [];
}
resultFiles[analyzerName].push({ fileName, content });
}
});
return { searchTargetDeps, resultFiles };
}
/**
* @param {{ providenceConf: object; earchTargetDeps: object; resultFiles: string[]; }}
*/
function createMiddleWares({ providenceConf, providenceConfRaw, searchTargetDeps, resultFiles }) {
/**
* @param {string} projectPath
* @returns {object|null}
*/
function getPackageJson(projectPath) {
try {
const file = pathLib.resolve(projectPath, 'package.json');
return JSON.parse(fs.readFileSync(file, 'utf8'));
} catch (_) {
return null;
}
}
/**
* @param {object[]} collections
* @returns {{[keu as string]: }}
*/
function transformToProjectNames(collections) {
const res = {};
// eslint-disable-next-line array-callback-return
Object.entries(collections).map(([key, val]) => {
res[key] = val.map(c => {
const pkg = getPackageJson(c);
return pkg && pkg.name;
});
});
return res;
}
const pathFromServerRootToHere = `/${pathLib.relative(process.cwd(), __dirname)}`;
return [
// eslint-disable-next-line consistent-return
async (ctx, next) => {
// TODO: Quick and dirty solution: refactor in a nicer way
if (ctx.url.startsWith('/app')) {
ctx.url = `${pathFromServerRootToHere}/${ctx.url}`;
return next();
}
if (ctx.url === '/') {
ctx.url = `${pathFromServerRootToHere}/index.html`;
return next();
}
if (ctx.url === '/results') {
ctx.body = resultFiles;
} else if (ctx.url === '/menu-data') {
// Gathers all data that are relevant to create a configuration menu
// at the top of the dashboard:
// - referenceCollections as defined in providence.conf.js
// - searchTargetCollections (aka programs) as defined in providence.conf.js
// - searchTargetDeps as found in search-target-deps-file.json
// Also do some processing on the presentation of a project, so that it can be easily
// outputted in frontend
let searchTargetCollections;
if (providenceConf.searchTargetCollections) {
searchTargetCollections = transformToProjectNames(providenceConf.searchTargetCollections);
} else {
searchTargetCollections = Object.keys(searchTargetDeps).map(d => d.split('#')[0]);
}
const menuData = {
// N.B. theoratically there can be a mismatch between basename and pkgJson.name,
// but we assume folder names and pkgJson.names to be similar
searchTargetCollections,
referenceCollections: transformToProjectNames(providenceConf.referenceCollections),
searchTargetDeps,
};
ctx.body = menuData;
} else if (ctx.url === '/providence-conf.js') {
// Alloes frontend dasbboard app to find categoriesand other configs
ctx.type = 'text/javascript';
ctx.body = providenceConfRaw;
} else {
await next();
}
},
];
}
(async function main() {
const { providenceConf, providenceConfRaw } = await getProvidenceConf();
const { searchTargetDeps, resultFiles } = await getCachedProvidenceResults();
// Needed for dev purposes (we call it from ./packages-node/providence-analytics/ instead of ./)
// Allows es-dev-server to find the right moduleDirs
const fromPackageRoot = process.argv.includes('--serve-from-package-root');
const moduleRoot = fromPackageRoot ? pathLib.resolve(process.cwd(), '../../') : process.cwd();
const config = createConfig({
port: 8080,
appIndex: pathLib.resolve(__dirname, 'index.html'),
rootDir: moduleRoot,
nodeResolve: true,
moduleDirs: pathLib.resolve(moduleRoot, 'node_modules'),
watch: false,
open: true,
middlewares: createMiddleWares({
providenceConf,
providenceConfRaw,
searchTargetDeps,
resultFiles,
}),
});
await startServer(config);
})();