From 96ae18c487bb1d95f06ba1c94e9d8ebdd7aaab37 Mon Sep 17 00:00:00 2001 From: Thijs Louisse Date: Tue, 16 Nov 2021 11:48:13 +0100 Subject: [PATCH] feat(providence): improve dashboard --- .changeset/giant-ears-try.md | 12 ++ .editorconfig | 4 +- .../LocalConfiguration.md | 14 +- .../providence-analytics/dashboard.md | 6 +- .../providence-analytics/overview.md | 26 +-- .../dashboard/src/app/p-board.js | 69 +++++--- .../dashboard/src/app/server.js | 9 + .../dashboard/src/app/server.mjs | 161 ++++++++++++++++++ .../dashboard/src/server.js | 134 --------------- .../providence-analytics/package.json | 2 +- ...providence.conf.js => providence.conf.mjs} | 8 +- .../providence-analytics/src/cli/cli.js | 26 ++- .../providence-analytics/src/cli/index.js | 4 - .../providence-analytics/src/cli/index.mjs | 9 + .../src/program/services/InputDataService.js | 16 +- .../src/program/utils/get-providence-conf.mjs | 37 ++++ .../test-node/cli/cli.test.js | 4 +- 17 files changed, 332 insertions(+), 209 deletions(-) create mode 100644 .changeset/giant-ears-try.md create mode 100644 packages-node/providence-analytics/dashboard/src/app/server.js create mode 100644 packages-node/providence-analytics/dashboard/src/app/server.mjs delete mode 100644 packages-node/providence-analytics/dashboard/src/server.js rename packages-node/providence-analytics/{providence.conf.js => providence.conf.mjs} (94%) delete mode 100755 packages-node/providence-analytics/src/cli/index.js create mode 100755 packages-node/providence-analytics/src/cli/index.mjs create mode 100644 packages-node/providence-analytics/src/program/utils/get-providence-conf.mjs diff --git a/.changeset/giant-ears-try.md b/.changeset/giant-ears-try.md new file mode 100644 index 000000000..0bb6ccf64 --- /dev/null +++ b/.changeset/giant-ears-try.md @@ -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. diff --git a/.editorconfig b/.editorconfig index 105d8d7d1..725d4feb1 100644 --- a/.editorconfig +++ b/.editorconfig @@ -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 = */ @@ -23,4 +23,4 @@ indent_style = unset indent_size = unset end_of_line = unset insert_final_newline = unset -trim_trailing_whitespace = unset \ No newline at end of file +trim_trailing_whitespace = unset diff --git a/docs/docs/node-tools/providence-analytics/LocalConfiguration.md b/docs/docs/node-tools/providence-analytics/LocalConfiguration.md index 52b0b48f1..1323596c9 100644 --- a/docs/docs/node-tools/providence-analytics/LocalConfiguration.md +++ b/docs/docs/node-tools/providence-analytics/LocalConfiguration.md @@ -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). diff --git a/docs/docs/node-tools/providence-analytics/dashboard.md b/docs/docs/node-tools/providence-analytics/dashboard.md index 8db4aa850..14c6bc1a3 100644 --- a/docs/docs/node-tools/providence-analytics/dashboard.md +++ b/docs/docs/node-tools/providence-analytics/dashboard.md @@ -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. diff --git a/docs/docs/node-tools/providence-analytics/overview.md b/docs/docs/node-tools/providence-analytics/overview.md index f9abbc1b8..ae75bb892 100644 --- a/docs/docs/node-tools/providence-analytics/overview.md +++ b/docs/docs/node-tools/providence-analytics/overview.md @@ -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... ![Analyzer query](./assets/analyzer-query.gif 'Analyzer query') + diff --git a/packages-node/providence-analytics/dashboard/src/app/p-board.js b/packages-node/providence-analytics/dashboard/src/app/p-board.js index 1baa1603f..f54922698 100644 --- a/packages-node/providence-analytics/dashboard/src/app/p-board.js +++ b/packages-node/providence-analytics/dashboard/src/app/p-board.js @@ -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` - ${Object.keys(this.__resultFiles).map( analyzerName => html` `, )} @@ -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() { diff --git a/packages-node/providence-analytics/dashboard/src/app/server.js b/packages-node/providence-analytics/dashboard/src/app/server.js new file mode 100644 index 000000000..622c05f4c --- /dev/null +++ b/packages-node/providence-analytics/dashboard/src/app/server.js @@ -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'); diff --git a/packages-node/providence-analytics/dashboard/src/app/server.mjs b/packages-node/providence-analytics/dashboard/src/app/server.mjs new file mode 100644 index 000000000..c8e63970b --- /dev/null +++ b/packages-node/providence-analytics/dashboard/src/app/server.mjs @@ -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); +})(); diff --git a/packages-node/providence-analytics/dashboard/src/server.js b/packages-node/providence-analytics/dashboard/src/server.js deleted file mode 100644 index 2543ecf14..000000000 --- a/packages-node/providence-analytics/dashboard/src/server.js +++ /dev/null @@ -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); -})(); diff --git a/packages-node/providence-analytics/package.json b/packages-node/providence-analytics/package.json index 371db4a56..5203e1c4f 100644 --- a/packages-node/providence-analytics/package.json +++ b/packages-node/providence-analytics/package.json @@ -12,7 +12,7 @@ }, "main": "./src/program/providence.js", "bin": { - "providence": "./src/cli/index.js" + "providence": "./src/cli/index.mjs" }, "files": [ "dashboard/src", diff --git a/packages-node/providence-analytics/providence.conf.js b/packages-node/providence-analytics/providence.conf.mjs similarity index 94% rename from packages-node/providence-analytics/providence.conf.js rename to packages-node/providence-analytics/providence.conf.mjs index e59652ac0..aad770c56 100644 --- a/packages-node/providence-analytics/providence.conf.js +++ b/packages-node/providence-analytics/providence.conf.mjs @@ -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; diff --git a/packages-node/providence-analytics/src/cli/cli.js b/packages-node/providence-analytics/src/cli/cli.js index 2a050a803..f3500bf7c 100755 --- a/packages-node/providence-analytics/src/cli/cli.js +++ b/packages-node/providence-analytics/src/cli/cli.js @@ -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; diff --git a/packages-node/providence-analytics/src/cli/index.js b/packages-node/providence-analytics/src/cli/index.js deleted file mode 100755 index 8d26466e3..000000000 --- a/packages-node/providence-analytics/src/cli/index.js +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env node -const { cli } = require('./cli.js'); - -cli(); diff --git a/packages-node/providence-analytics/src/cli/index.mjs b/packages-node/providence-analytics/src/cli/index.mjs new file mode 100755 index 000000000..dc5babdb2 --- /dev/null +++ b/packages-node/providence-analytics/src/cli/index.mjs @@ -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 }); +})(); diff --git a/packages-node/providence-analytics/src/program/services/InputDataService.js b/packages-node/providence-analytics/src/program/services/InputDataService.js index 842d17ed7..3abc5dbd2 100644 --- a/packages-node/providence-analytics/src/program/services/InputDataService.js +++ b/packages-node/providence-analytics/src/program/services/InputDataService.js @@ -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`, + ); } /** diff --git a/packages-node/providence-analytics/src/program/utils/get-providence-conf.mjs b/packages-node/providence-analytics/src/program/utils/get-providence-conf.mjs new file mode 100644 index 000000000..f631b9d7a --- /dev/null +++ b/packages-node/providence-analytics/src/program/utils/get-providence-conf.mjs @@ -0,0 +1,37 @@ +import pathLib from 'path'; +import fs from 'fs'; + +/** + * @returns {Promise} + */ +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 }; +} diff --git a/packages-node/providence-analytics/test-node/cli/cli.test.js b/packages-node/providence-analytics/test-node/cli/cli.test.js index 5080d8f14..007a0b77c 100644 --- a/packages-node/providence-analytics/test-node/cli/cli.test.js +++ b/packages-node/providence-analytics/test-node/cli/cli.test.js @@ -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 () => {