feat(providence): improve dashboard
This commit is contained in:
parent
5d275205c6
commit
96ae18c487
17 changed files with 332 additions and 209 deletions
12
.changeset/giant-ears-try.md
Normal file
12
.changeset/giant-ears-try.md
Normal 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.
|
||||||
|
|
@ -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 = */
|
||||||
|
|
|
||||||
|
|
@ -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).
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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...
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
<!--
|
||||||
## 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>
|
||||||
```
|
```
|
||||||
|
-->
|
||||||
|
|
|
||||||
|
|
@ -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() {
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
161
packages-node/providence-analytics/dashboard/src/app/server.mjs
Normal file
161
packages-node/providence-analytics/dashboard/src/app/server.mjs
Normal 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);
|
||||||
|
})();
|
||||||
|
|
@ -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);
|
|
||||||
})();
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
#!/usr/bin/env node
|
|
||||||
const { cli } = require('./cli.js');
|
|
||||||
|
|
||||||
cli();
|
|
||||||
9
packages-node/providence-analytics/src/cli/index.mjs
Executable file
9
packages-node/providence-analytics/src/cli/index.mjs
Executable 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 });
|
||||||
|
})();
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -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 };
|
||||||
|
}
|
||||||
|
|
@ -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 () => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue