feat(providence): improve dashboard

This commit is contained in:
Thijs Louisse 2021-11-16 11:48:13 +01:00
parent 5d275205c6
commit 96ae18c487
17 changed files with 332 additions and 209 deletions

View file

@ -0,0 +1,12 @@
---
'providence-analytics': minor
---
Improved dashboard:
- allows to configure categories in `providence.conf.(m)js`that show up in dashboard
- exposes dashboard in cli: `npx providence dashboard`
BREAKING CHANGES:
- `providence.conf.(m)js` must be in ESM format.

View file

@ -12,7 +12,7 @@ trim_trailing_whitespace = true
indent_size = unset indent_size = unset
trim_trailing_whitespace = false trim_trailing_whitespace = false
[*.{html,js,md,mdx}] [*.{html,js,mjs,md,mdx}]
block_comment_start = /** block_comment_start = /**
block_comment = * block_comment = *
block_comment_end = */ block_comment_end = */

View file

@ -1,7 +1,11 @@
# Node Tools >> Providence Analytics >> Local configuration ||40 # Node Tools >> Providence Analytics >> Local configuration ||40
The file `providence.conf.js` is read by providence cli and by the dashboard to get all The Providence configuration file is read by providence cli (optional) and by the dashboard (required).
default configurations. It has a few requirements:
- it must be called `providence.conf.js` or `providence.conf.mjs`
- it must be in ESM format
- it must be located in the root of a repository (under `process.cwd()`)
## Meta data ## Meta data
@ -11,6 +15,7 @@ Based on the filePath of a result, a category can be added.
For example: For example:
```js ```js
export default {
metaConfig: { metaConfig: {
categoryConfig: [ categoryConfig: [
{ {
@ -29,6 +34,7 @@ For example:
}, },
], ],
}, },
}
``` ```
> N.B. category info is regarded as subjective, therefore it's advised to move this away from > N.B. category info is regarded as subjective, therefore it's advised to move this away from
@ -38,7 +44,7 @@ For example:
### referenceCollections ### referenceCollections
A list of file system paths. They can be defined relative from the current project root (`process.cwd()`) or they can be full paths. A list of file system paths. They can be defined relative from the current project root or they can be full paths.
When a [MatchAnalyzer](../../../docs/node-tools/providence-analytics/analyzer.md) like `match-imports` or `match-subclasses` is used, the default reference(s) can be configured here. For instance: ['/path/to/@lion/form'] When a [MatchAnalyzer](../../../docs/node-tools/providence-analytics/analyzer.md) like `match-imports` or `match-subclasses` is used, the default reference(s) can be configured here. For instance: ['/path/to/@lion/form']
An example: An example:
@ -57,6 +63,6 @@ An example:
### searchTargetCollections ### searchTargetCollections
A list of file system paths. They can be defined relative from the current project root A list of file system paths. They can be defined relative from the current project root
(`process.cwd()`) or they can be full paths. or they can be full paths.
When not defined, the current project will be the search target (this is most common when When not defined, the current project will be the search target (this is most common when
providence is used as a dev dependency). providence is used as a dev dependency).

View file

@ -6,19 +6,17 @@ application.
## Run ## Run
Start the dashboard via `npm run dashboard` to automatically open the browser and start the dashboard. Start the dashboard via `providence dashboard` to automatically open the browser and start the dashboard.
## Interface ## Interface
- Select all reference projects - Select all reference projects
- Select all target projects - Select all target projects
Press `show table` to see the result based on the updated configuration.
### Generate csv ### Generate csv
When `get csv` is pressed, a `.csv` will be downloaded that can be loaded into Excel. When `get csv` is pressed, a `.csv` will be downloaded that can be loaded into Excel.
## Analyzer support ## Analyzer support
Currently, only the `match-imports` is supported, more analyzers will be added in the future. Currently, `match-imports` and `match-subclasses` are supported, more analyzers will be added in the future.

View file

@ -38,11 +38,11 @@ npm i --save-dev providence-analytics
```json ```json
"scripts": { "scripts": {
"providence": "providence analyze match-imports -r 'node_modules/@lion/*'", "providence:match-imports": "providence analyze match-imports -r 'node_modules/@lion/*'",
} }
``` ```
> The example above illustrates how to run the "match-imports" analyzer for reference project 'lion-based-ui'. Note that it is possible to run other analyzers and configurations supported by providence as well. For a full overview of cli options, run `providence --help`. All supported analyzers will be viewed when running `providence analyze` > The example above illustrates how to run the "match-imports" analyzer for reference project 'lion-based-ui'. Note that it is possible to run other analyzers and configurations supported by providence as well. For a full overview of cli options, run `npx providence --help`. All supported analyzers will be viewed when running `npx providence analyze`
You are now ready to use providence in your project. All You are now ready to use providence in your project. All
data will be stored in json files in the folder `./providence-output` data will be stored in json files in the folder `./providence-output`
@ -57,20 +57,21 @@ data will be stored in json files in the folder `./providence-output`
... ...
"scripts": { "scripts": {
... ...
"providence:dashboard": "node node_modules/providence/dashboard/src/server.js" "providence:dashboard": "providence dashboard"
} }
``` ```
### Add providence.conf.js ### Add providence.conf.js
```js ```js
const providenceConfig = { export default {
referenceCollections: { referenceCollections: {
'lion-based-ui collection': ['./node_modules/lion-based-ui'], 'lion-based-ui-collection': [
'./node_modules/lion-based-ui/packages/x',
'./node_modules/lion-based-ui/packages/y',
],
}, },
}; };
module.exports = providenceConfig;
``` ```
Run `npm run providence:dashboard` Run `npm run providence:dashboard`
@ -124,16 +125,16 @@ Providence requires a queries as input.
Queries are defined as objects and can be of two types: Queries are defined as objects and can be of two types:
- feature-query - feature-query
- analyzer - ast-analyzer
A `queryConfig` is required as input to run the `providenceMain` function. A `QueryConfig` is required as input to run the `providenceMain` function.
This object specifies the type of query and contains the relevant meta This object specifies the type of query and contains the relevant meta
information that will later be outputted in the `QueryResult` (the JSON object that information that will later be outputted in the `QueryResult` (the JSON object that
the `providenceMain` function returns.) the `providenceMain` function returns.)
## Analyzer Query ## Analyzer Query
Analyzers queries are also created via `queryConfig`s. Analyzer queries are also created via `QueryConfig`s.
Analyzers can be described as predefined queries that use AST traversal. Analyzers can be described as predefined queries that use AST traversal.
@ -147,12 +148,14 @@ Now you will get a list of all predefined analyzers:
- find-imports - find-imports
- find-exports - find-exports
- find-classes
- match-imports - match-imports
- find-subclasses - match-subclasses
- etc... - etc...
![Analyzer query](./assets/analyzer-query.gif 'Analyzer query') ![Analyzer query](./assets/analyzer-query.gif 'Analyzer query')
<!--
## Running providence from its own repo ## Running providence from its own repo
### How to add a new search target project ### How to add a new search target project
@ -186,3 +189,4 @@ Please run:
```bash ```bash
sh ./rm-submodule.sh <path/to/submodule> sh ./rm-submodule.sh <path/to/submodule>
``` ```
-->

View file

@ -1,3 +1,4 @@
/* eslint-disable lit-a11y/no-invalid-change-handler */
/* eslint-disable max-classes-per-file */ /* eslint-disable max-classes-per-file */
import { LitElement, html, css } from 'lit-element'; import { LitElement, html, css } from 'lit-element';
import { tooltip as tooltipStyles } from './styles/tooltip.css.js'; import { tooltip as tooltipStyles } from './styles/tooltip.css.js';
@ -15,6 +16,31 @@ PTable.decorateStyles(tableDecoration);
customElements.define('p-table', PTable); customElements.define('p-table', PTable);
/**
*
* @param {{ project:string, filePath:string, name:string }} specifierRes
* @param {{ categoryConfig:object }} metaConfig
* @returns {string[]}
*/
function getCategoriesForMatchedSpecifier(specifierRes, { metaConfig }) {
const resultCats = [];
if (metaConfig && metaConfig.categoryConfig) {
const { project, filePath, name } = specifierRes.exportSpecifier;
// First of all, do we have a matching project?
// TODO: we should allow different configs for different (major) versions
const match = metaConfig.categoryConfig.find(cat => cat.project === project);
if (match) {
Object.entries(match.categories).forEach(([categoryName, matchFn]) => {
if (matchFn(filePath, name)) {
resultCats.push(categoryName);
}
});
}
}
return resultCats;
}
function checkedValues(checkboxOrNodeList) { function checkedValues(checkboxOrNodeList) {
if (!checkboxOrNodeList.length) { if (!checkboxOrNodeList.length) {
return checkboxOrNodeList.checked && checkboxOrNodeList.value; return checkboxOrNodeList.checked && checkboxOrNodeList.value;
@ -148,7 +174,7 @@ class PBoard extends DecorateMixin(LitElement) {
_activeAnalyzerSelectTemplate() { _activeAnalyzerSelectTemplate() {
return html` return html`
<select id="active-analyzer"> <select id="active-analyzer" @change="${this._onActiveAnalyzerChanged}">
${Object.keys(this.__resultFiles).map( ${Object.keys(this.__resultFiles).map(
analyzerName => html` <option value="${analyzerName}">${analyzerName}</option> `, analyzerName => html` <option value="${analyzerName}">${analyzerName}</option> `,
)} )}
@ -156,6 +182,10 @@ class PBoard extends DecorateMixin(LitElement) {
`; `;
} }
_onActiveAnalyzerChanged() {
this._aggregateResults();
}
get _selectionMenuFormNode() { get _selectionMenuFormNode() {
return this.shadowRoot.getElementById('selection-menu-form'); return this.shadowRoot.getElementById('selection-menu-form');
} }
@ -228,7 +258,7 @@ class PBoard extends DecorateMixin(LitElement) {
async __init() { async __init() {
await this.__fetchMenuData(); await this.__fetchMenuData();
await this.__fetchResults(); await this.__fetchResults();
// await this.__fetchProvidenceConf(); await this.__fetchProvidenceConf();
this._enrichMenuData(); this._enrichMenuData();
} }
@ -258,36 +288,20 @@ class PBoard extends DecorateMixin(LitElement) {
const activeRepos = [...new Set(checkedValues(repos))]; const activeRepos = [...new Set(checkedValues(repos))];
const activeAnalyzer = this._activeAnalyzerNode.value; const activeAnalyzer = this._activeAnalyzerNode.value;
const totalQueryOutput = this.__aggregateResultData(activeRefs, activeRepos, activeAnalyzer); const totalQueryOutput = this.__aggregateResultData(activeRefs, activeRepos, activeAnalyzer);
// function addCategories(specifierRes, metaConfig) {
// const resultCats = [];
// if (metaConfig.categoryConfig) {
// const { project, filePath, name } = specifierRes.exportSpecifier;
// // First of all, do we have a matching project?
// // TODO: we should allow different configs for different (major) versions
// const match = metaConfig.categoryConfig.find(cat => cat.project === project);
// console.log('match', match);
// if (match) {
// Object.entries(match.categories, ([categoryName, matchFn]) => {
// if (matchFn(filePath, name)) {
// resultCats.push(categoryName);
// }
// });
// }
// }
// console.log('resultCats', resultCats, metaConfig);
// return resultCats;
// }
// Prepare viewData // Prepare viewData
const dataResult = []; const dataResult = [];
// When we support more analyzers than match-imports and match-subclasses, make a switch // When we support more analyzers than match-imports and match-subclasses, make a switch
// here // here
totalQueryOutput.forEach((specifierRes, i) => { totalQueryOutput.forEach((specifierRes, i) => {
dataResult[i] = {}; dataResult[i] = {};
dataResult[i].specifier = specifierRes.exportSpecifier; dataResult[i].specifier = specifierRes.exportSpecifier;
dataResult[i].sourceProject = specifierRes.exportSpecifier.project; dataResult[i].sourceProject = specifierRes.exportSpecifier.project;
// dataResult[i].categories = undefined; // addCategories(specifierRes, this.__providenceConf); dataResult[i].categories = getCategoriesForMatchedSpecifier(
specifierRes,
this.__providenceConf,
);
dataResult[i].type = specifierRes.exportSpecifier.name === '[file]' ? 'file' : 'specifier'; dataResult[i].type = specifierRes.exportSpecifier.name === '[file]' ? 'file' : 'specifier';
dataResult[i].count = specifierRes.matchesPerProject dataResult[i].count = specifierRes.matchesPerProject
.map(mpp => mpp.files) .map(mpp => mpp.files)
@ -299,6 +313,7 @@ class PBoard extends DecorateMixin(LitElement) {
__aggregateResultData(activeRefs, activeRepos, activeAnalyzer) { __aggregateResultData(activeRefs, activeRepos, activeAnalyzer) {
const jsonResultsActiveFilter = []; const jsonResultsActiveFilter = [];
activeRefs.forEach(ref => { activeRefs.forEach(ref => {
const refSearch = `_${ref.replace('#', '_')}_`; const refSearch = `_${ref.replace('#', '_')}_`;
activeRepos.forEach(dep => { activeRepos.forEach(dep => {
@ -419,13 +434,15 @@ class PBoard extends DecorateMixin(LitElement) {
} }
async __fetchMenuData() { async __fetchMenuData() {
// Derived from providence.conf.js // Derived from providence.conf.js, generated in server.mjs
this.__initialMenuData = await fetch('/menu-data').then(response => response.json()); this.__initialMenuData = await fetch('/menu-data').then(response => response.json());
} }
async __fetchProvidenceConf() { async __fetchProvidenceConf() {
// Gets an // Gets the providence conf as defined by the end user in providence-conf.(m)js
this.__providenceConf = await fetch('/providence.conf.js').then(response => response.json()); // @ts-ignore
// eslint-disable-next-line import/no-absolute-path
this.__providenceConf = (await import('/providence-conf.js')).default;
} }
async __fetchResults() { async __fetchResults() {

View file

@ -0,0 +1,9 @@
// @ts-ignore
const { LogService } = require('../../src/program/services/LogService.js');
LogService.warn(
'Running via "dashboard/src/server.js" is deprecated. Please run "providence dashboard" instead.',
);
// @ts-ignore
import('./server.mjs');

View file

@ -0,0 +1,161 @@
import fs from 'fs';
import pathLib, { dirname } from 'path';
import { fileURLToPath } from 'url';
import { createConfig, startServer } from 'es-dev-server';
// eslint-disable-next-line import/no-unresolved
import { ReportService } from '../../src/program/services/ReportService.js';
// eslint-disable-next-line import/no-unresolved
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);
})();

View file

@ -1,134 +0,0 @@
const fs = require('fs');
const pathLib = require('path');
const { createConfig, startServer } = require('es-dev-server');
const { ReportService } = require('../../src/program/services/ReportService.js');
const { LogService } = require('../../src/program/services/LogService.js');
// eslint-disable-next-line import/no-dynamic-require
const providenceConf = require(`${pathLib.join(process.cwd(), 'providence.conf.js')}`);
let outputFilePaths;
try {
outputFilePaths = fs.readdirSync(ReportService.outputPath);
} catch (_) {
LogService.error(
`Please make sure providence results can be found in ${ReportService.outputPath}`,
);
process.exit(1);
}
const resultFiles = {};
let searchTargetDeps;
const supportedAnalyzers = ['match-imports', 'match-subclasses'];
outputFilePaths.forEach(fileName => {
const content = JSON.parse(
fs.readFileSync(pathLib.join(ReportService.outputPath, 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 });
}
});
function getPackageJson(projectPath) {
let pkgJson;
try {
const file = pathLib.resolve(projectPath, 'package.json');
pkgJson = JSON.parse(fs.readFileSync(file, 'utf8'));
} catch (_) {
// eslint-disable-next-line no-empty
}
return pkgJson;
}
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)}`;
// 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: [
// 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') {
// We need to fetch it via server, since it's CommonJS vs es modules...
// require("@babel/core").transform("code", {
// plugins: ["@babel/plugin-transform-modules-commonjs"]
// });
// Gives back categories from providence.conf
ctx.body = providenceConf.metaConfig;
} else {
await next();
}
},
],
});
(async () => {
await startServer(config);
})();

View file

@ -12,7 +12,7 @@
}, },
"main": "./src/program/providence.js", "main": "./src/program/providence.js",
"bin": { "bin": {
"providence": "./src/cli/index.js" "providence": "./src/cli/index.mjs"
}, },
"files": [ "files": [
"dashboard/src", "dashboard/src",

View file

@ -1,5 +1,5 @@
const pathLib = require('path'); import pathLib from 'path';
const fs = require('fs'); import fs from 'fs';
// This file is read by dashboard and cli and needs to be present under process.cwd() // This file is read by dashboard and cli and needs to be present under process.cwd()
// It mainly serves as an example and it allows to run the dashboard locally // It mainly serves as an example and it allows to run the dashboard locally
@ -30,7 +30,7 @@ function getAllLionScopedPackagePaths() {
const lionScopedPackagePaths = getAllLionScopedPackagePaths(); const lionScopedPackagePaths = getAllLionScopedPackagePaths();
const providenceConfig = { export default {
metaConfig: { metaConfig: {
categoryConfig: [ categoryConfig: [
{ {
@ -65,5 +65,3 @@ const providenceConfig = {
'@lion-references': lionScopedPackagePaths, '@lion-references': lionScopedPackagePaths,
}, },
}; };
module.exports = providenceConfig;

View file

@ -13,11 +13,11 @@ const cliHelpers = require('./cli-helpers.js');
const extendDocsModule = require('./launch-providence-with-extend-docs.js'); const extendDocsModule = require('./launch-providence-with-extend-docs.js');
const { toPosixPath } = require('../program/utils/to-posix-path.js'); const { toPosixPath } = require('../program/utils/to-posix-path.js');
const { extensionsFromCs, setQueryMethod, targetDefault, installDeps } = cliHelpers; const { extensionsFromCs, setQueryMethod, targetDefault, installDeps, spawnProcess } = cliHelpers;
const { version } = require('../../package.json'); const { version } = require('../../package.json');
async function cli({ cwd } = {}) { async function cli({ cwd, providenceConf } = {}) {
let resolveCli; let resolveCli;
let rejectCli; let rejectCli;
@ -35,7 +35,8 @@ async function cli({ cwd } = {}) {
/** @type {object} */ /** @type {object} */
let regexSearchOptions; let regexSearchOptions;
const externalConfig = InputDataService.getExternalConfig(); // TODO: change back to "InputDataService.getExternalConfig();" once full package ESM
const externalConfig = providenceConf;
async function getQueryInputData( async function getQueryInputData(
/* eslint-disable no-shadow */ /* eslint-disable no-shadow */
@ -153,6 +154,15 @@ async function cli({ cwd } = {}) {
} }
} }
async function runDashboard() {
const pathFromServerRootToDashboard = `${pathLib.relative(
process.cwd(),
pathLib.resolve(__dirname, '../../dashboard'),
)}`;
spawnProcess(`node ${pathFromServerRootToDashboard}/src/server.mjs`);
}
commander commander
.version(version, '-v, --version') .version(version, '-v, --version')
.option('-e, --extensions [extensions]', 'extensions like "js,html"', extensionsFromCs, [ .option('-e, --extensions [extensions]', 'extensions like "js,html"', extensionsFromCs, [
@ -323,6 +333,16 @@ async function cli({ cwd } = {}) {
manageSearchTargets(options); manageSearchTargets(options);
}); });
commander
.command('dashboard')
.description(
`Runs an interactive dashboard that shows all aggregated data from proivdence-output, configured
via providence.conf`,
)
.action(() => {
runDashboard();
});
commander.parse(process.argv); commander.parse(process.argv);
await cliPromise; await cliPromise;

View file

@ -1,4 +0,0 @@
#!/usr/bin/env node
const { cli } = require('./cli.js');
cli();

View file

@ -0,0 +1,9 @@
#!/usr/bin/env node
import { cli } from './cli.js';
import { getProvidenceConf } from '../program/utils/get-providence-conf.mjs';
(async () => {
// We need to provide config to cli, until whole package is rewritten as ESM.
const { providenceConf } = await getProvidenceConf();
cli({ providenceConf });
})();

View file

@ -152,13 +152,6 @@ function multiGlobSync(patterns, { keepDirs = false, root } = {}) {
return Array.from(res); return Array.from(res);
} }
/**
* @typedef {Object} ProjectData
* @property {string} project project name
* @property {string} path full path to project folder
* @property {string[]} entries all file paths within project folder
*/
/** /**
* To be used in main program. * To be used in main program.
* It creates an instance on which the 'files' array is stored. * It creates an instance on which the 'files' array is stored.
@ -456,12 +449,9 @@ class InputDataService {
* @desc Allows the user to provide a providence.conf.js file in its repository root * @desc Allows the user to provide a providence.conf.js file in its repository root
*/ */
static getExternalConfig() { static getExternalConfig() {
try { throw new Error(
// eslint-disable-next-line import/no-dynamic-require, global-require `[InputDataService.getExternalConfig]: Until fully ESM: use 'src/program/utils/get-providence=conf.mjs instead`,
return require(`${process.cwd()}/providence.conf.js`); );
} catch (e) {
return null;
}
} }
/** /**

View file

@ -0,0 +1,37 @@
import pathLib from 'path';
import fs from 'fs';
/**
* @returns {Promise<object|null>}
*/
export async function getProvidenceConf() {
const confPathWithoutExtension = `${pathLib.join(process.cwd(), 'providence.conf')}`;
let confPathFound;
try {
if (fs.existsSync(`${confPathWithoutExtension}.js`)) {
confPathFound = `${confPathWithoutExtension}.js`;
} else if (fs.existsSync(`${confPathWithoutExtension}.mjs`)) {
confPathFound = `${confPathWithoutExtension}.mjs`;
}
} catch (_) {
throw new Error(
`Please provide ${confPathWithoutExtension}.js or ${confPathWithoutExtension}.mjs`,
);
}
if (!confPathFound) {
return null;
}
const { default: providenceConf } = await import(confPathFound);
if (!providenceConf) {
throw new Error(
`providence.conf.js file should be in es module format (so it can be read by a browser).
So use "export default {}" instead of "module.exports = {}"`,
);
}
const providenceConfRaw = fs.readFileSync(confPathFound, 'utf8');
return { providenceConf, providenceConfRaw };
}

View file

@ -322,13 +322,13 @@ describe('Providence CLI', () => {
it('"-c --config"', async () => { it('"-c --config"', async () => {
await runCli(`analyze mock-analyzer -c {"a":"2"}`, rootDir); await runCli(`analyze mock-analyzer -c {"a":"2"}`, rootDir);
expect(qConfStub.args[0][0]).to.equal('mock-analyzer'); expect(qConfStub.args[0][0]).to.equal('mock-analyzer');
expect(qConfStub.args[0][1]).to.eql({ a: '2', metaConfig: undefined }); expect(qConfStub.args[0][1]).to.eql({ a: '2', metaConfig: {} });
qConfStub.resetHistory(); qConfStub.resetHistory();
await runCli(`analyze mock-analyzer --config {"a":"2"}`, rootDir); await runCli(`analyze mock-analyzer --config {"a":"2"}`, rootDir);
expect(qConfStub.args[0][0]).to.equal('mock-analyzer'); expect(qConfStub.args[0][0]).to.equal('mock-analyzer');
expect(qConfStub.args[0][1]).to.eql({ a: '2', metaConfig: undefined }); expect(qConfStub.args[0][1]).to.eql({ a: '2', metaConfig: {} });
}); });
it('calls "promptAnalyzerConfigMenu" without config given', async () => { it('calls "promptAnalyzerConfigMenu" without config given', async () => {