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
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.{html,js,md,mdx}]
|
||||
[*.{html,js,mjs,md,mdx}]
|
||||
block_comment_start = /**
|
||||
block_comment = *
|
||||
block_comment_end = */
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
# Node Tools >> Providence Analytics >> Local configuration ||40
|
||||
|
||||
The file `providence.conf.js` is read by providence cli and by the dashboard to get all
|
||||
default configurations.
|
||||
The Providence configuration file is read by providence cli (optional) and by the dashboard (required).
|
||||
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
|
||||
|
||||
|
|
@ -11,6 +15,7 @@ Based on the filePath of a result, a category can be added.
|
|||
For example:
|
||||
|
||||
```js
|
||||
export default {
|
||||
metaConfig: {
|
||||
categoryConfig: [
|
||||
{
|
||||
|
|
@ -29,6 +34,7 @@ For example:
|
|||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
> N.B. category info is regarded as subjective, therefore it's advised to move this away from
|
||||
|
|
@ -38,7 +44,7 @@ For example:
|
|||
|
||||
### 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']
|
||||
|
||||
An example:
|
||||
|
|
@ -57,6 +63,6 @@ An example:
|
|||
### searchTargetCollections
|
||||
|
||||
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
|
||||
providence is used as a dev dependency).
|
||||
|
|
|
|||
|
|
@ -6,19 +6,17 @@ application.
|
|||
|
||||
## 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
|
||||
|
||||
- Select all reference projects
|
||||
- Select all target projects
|
||||
|
||||
Press `show table` to see the result based on the updated configuration.
|
||||
|
||||
### Generate csv
|
||||
|
||||
When `get csv` is pressed, a `.csv` will be downloaded that can be loaded into Excel.
|
||||
|
||||
## 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
|
||||
"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
|
||||
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": {
|
||||
...
|
||||
"providence:dashboard": "node node_modules/providence/dashboard/src/server.js"
|
||||
"providence:dashboard": "providence dashboard"
|
||||
}
|
||||
```
|
||||
|
||||
### Add providence.conf.js
|
||||
|
||||
```js
|
||||
const providenceConfig = {
|
||||
export default {
|
||||
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`
|
||||
|
|
@ -124,16 +125,16 @@ Providence requires a queries as input.
|
|||
Queries are defined as objects and can be of two types:
|
||||
|
||||
- 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
|
||||
information that will later be outputted in the `QueryResult` (the JSON object that
|
||||
the `providenceMain` function returns.)
|
||||
|
||||
## 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.
|
||||
|
||||
|
|
@ -147,12 +148,14 @@ Now you will get a list of all predefined analyzers:
|
|||
|
||||
- find-imports
|
||||
- find-exports
|
||||
- find-classes
|
||||
- match-imports
|
||||
- find-subclasses
|
||||
- match-subclasses
|
||||
- etc...
|
||||
|
||||

|
||||
|
||||
<!--
|
||||
## Running providence from its own repo
|
||||
|
||||
### How to add a new search target project
|
||||
|
|
@ -186,3 +189,4 @@ Please run:
|
|||
```bash
|
||||
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 */
|
||||
import { LitElement, html, css } from 'lit-element';
|
||||
import { tooltip as tooltipStyles } from './styles/tooltip.css.js';
|
||||
|
|
@ -15,6 +16,31 @@ PTable.decorateStyles(tableDecoration);
|
|||
|
||||
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) {
|
||||
if (!checkboxOrNodeList.length) {
|
||||
return checkboxOrNodeList.checked && checkboxOrNodeList.value;
|
||||
|
|
@ -148,7 +174,7 @@ class PBoard extends DecorateMixin(LitElement) {
|
|||
|
||||
_activeAnalyzerSelectTemplate() {
|
||||
return html`
|
||||
<select id="active-analyzer">
|
||||
<select id="active-analyzer" @change="${this._onActiveAnalyzerChanged}">
|
||||
${Object.keys(this.__resultFiles).map(
|
||||
analyzerName => html` <option value="${analyzerName}">${analyzerName}</option> `,
|
||||
)}
|
||||
|
|
@ -156,6 +182,10 @@ class PBoard extends DecorateMixin(LitElement) {
|
|||
`;
|
||||
}
|
||||
|
||||
_onActiveAnalyzerChanged() {
|
||||
this._aggregateResults();
|
||||
}
|
||||
|
||||
get _selectionMenuFormNode() {
|
||||
return this.shadowRoot.getElementById('selection-menu-form');
|
||||
}
|
||||
|
|
@ -228,7 +258,7 @@ class PBoard extends DecorateMixin(LitElement) {
|
|||
async __init() {
|
||||
await this.__fetchMenuData();
|
||||
await this.__fetchResults();
|
||||
// await this.__fetchProvidenceConf();
|
||||
await this.__fetchProvidenceConf();
|
||||
this._enrichMenuData();
|
||||
}
|
||||
|
||||
|
|
@ -258,36 +288,20 @@ class PBoard extends DecorateMixin(LitElement) {
|
|||
const activeRepos = [...new Set(checkedValues(repos))];
|
||||
const activeAnalyzer = this._activeAnalyzerNode.value;
|
||||
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
|
||||
const dataResult = [];
|
||||
// When we support more analyzers than match-imports and match-subclasses, make a switch
|
||||
// here
|
||||
|
||||
totalQueryOutput.forEach((specifierRes, i) => {
|
||||
dataResult[i] = {};
|
||||
dataResult[i].specifier = specifierRes.exportSpecifier;
|
||||
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].count = specifierRes.matchesPerProject
|
||||
.map(mpp => mpp.files)
|
||||
|
|
@ -299,6 +313,7 @@ class PBoard extends DecorateMixin(LitElement) {
|
|||
|
||||
__aggregateResultData(activeRefs, activeRepos, activeAnalyzer) {
|
||||
const jsonResultsActiveFilter = [];
|
||||
|
||||
activeRefs.forEach(ref => {
|
||||
const refSearch = `_${ref.replace('#', '_')}_`;
|
||||
activeRepos.forEach(dep => {
|
||||
|
|
@ -419,13 +434,15 @@ class PBoard extends DecorateMixin(LitElement) {
|
|||
}
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
async __fetchProvidenceConf() {
|
||||
// Gets an
|
||||
this.__providenceConf = await fetch('/providence.conf.js').then(response => response.json());
|
||||
// Gets the providence conf as defined by the end user in providence-conf.(m)js
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line import/no-absolute-path
|
||||
this.__providenceConf = (await import('/providence-conf.js')).default;
|
||||
}
|
||||
|
||||
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",
|
||||
"bin": {
|
||||
"providence": "./src/cli/index.js"
|
||||
"providence": "./src/cli/index.mjs"
|
||||
},
|
||||
"files": [
|
||||
"dashboard/src",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
const pathLib = require('path');
|
||||
const fs = require('fs');
|
||||
import pathLib from 'path';
|
||||
import fs from 'fs';
|
||||
|
||||
// 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
|
||||
|
|
@ -30,7 +30,7 @@ function getAllLionScopedPackagePaths() {
|
|||
|
||||
const lionScopedPackagePaths = getAllLionScopedPackagePaths();
|
||||
|
||||
const providenceConfig = {
|
||||
export default {
|
||||
metaConfig: {
|
||||
categoryConfig: [
|
||||
{
|
||||
|
|
@ -65,5 +65,3 @@ const providenceConfig = {
|
|||
'@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 { 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');
|
||||
|
||||
async function cli({ cwd } = {}) {
|
||||
async function cli({ cwd, providenceConf } = {}) {
|
||||
let resolveCli;
|
||||
let rejectCli;
|
||||
|
||||
|
|
@ -35,7 +35,8 @@ async function cli({ cwd } = {}) {
|
|||
/** @type {object} */
|
||||
let regexSearchOptions;
|
||||
|
||||
const externalConfig = InputDataService.getExternalConfig();
|
||||
// TODO: change back to "InputDataService.getExternalConfig();" once full package ESM
|
||||
const externalConfig = providenceConf;
|
||||
|
||||
async function getQueryInputData(
|
||||
/* 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
|
||||
.version(version, '-v, --version')
|
||||
.option('-e, --extensions [extensions]', 'extensions like "js,html"', extensionsFromCs, [
|
||||
|
|
@ -323,6 +333,16 @@ async function cli({ cwd } = {}) {
|
|||
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);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* @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.
|
||||
* 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
|
||||
*/
|
||||
static getExternalConfig() {
|
||||
try {
|
||||
// eslint-disable-next-line import/no-dynamic-require, global-require
|
||||
return require(`${process.cwd()}/providence.conf.js`);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
throw new Error(
|
||||
`[InputDataService.getExternalConfig]: Until fully ESM: use 'src/program/utils/get-providence=conf.mjs instead`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
await runCli(`analyze mock-analyzer -c {"a":"2"}`, rootDir);
|
||||
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();
|
||||
|
||||
await runCli(`analyze mock-analyzer --config {"a":"2"}`, rootDir);
|
||||
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 () => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue