From 2dc85b14d372b59acc9e45400db73976301863cf Mon Sep 17 00:00:00 2001 From: Thijs Louisse Date: Tue, 13 Oct 2020 15:24:24 +0200 Subject: [PATCH] feat(providence-analytics): monorepo support extend-docs --- .changeset/proud-dryers-doubt.md | 13 ++ packages/providence-analytics/src/cli/cli.js | 3 +- .../src/cli/generate-extend-docs-data.js | 56 ------- .../cli/launch-providence-with-extend-docs.js | 121 ++++++++++++++++ .../src/program/analyzers/match-paths.js | 3 +- .../src/program/analyzers/types.d.ts | 133 ++++++++--------- .../src/program/services/InputDataService.js | 77 +++++++++- .../src/program/utils/async-array-utils.js | 8 +- .../src/program/utils/get-hash.js | 1 - .../src/program/utils/jsdoc-comment-parser.js | 1 + .../src/program/utils/memoize.js | 16 ++ .../read-package-tree-with-bower-support.js | 1 + .../test-helpers/mock-project-helpers.js | 7 +- .../test-node/cli/cli.test.js | 137 +++++++++++++++++- .../program/analyzers/match-paths.test.js | 75 ++++++++++ .../program/services/InputDataService.test.js | 48 +++++- 16 files changed, 552 insertions(+), 148 deletions(-) create mode 100644 .changeset/proud-dryers-doubt.md delete mode 100644 packages/providence-analytics/src/cli/generate-extend-docs-data.js create mode 100644 packages/providence-analytics/src/cli/launch-providence-with-extend-docs.js diff --git a/.changeset/proud-dryers-doubt.md b/.changeset/proud-dryers-doubt.md new file mode 100644 index 000000000..04c858257 --- /dev/null +++ b/.changeset/proud-dryers-doubt.md @@ -0,0 +1,13 @@ +--- +'providence-analytics': minor +--- + +Monorepo support for extend-docs + +### Features + +- add monorepo support for extend-docs + +### Fixes + +- allow custom element and class definitions to be in same file for 'match-paths' diff --git a/packages/providence-analytics/src/cli/cli.js b/packages/providence-analytics/src/cli/cli.js index b943d86d0..36c7a3147 100755 --- a/packages/providence-analytics/src/cli/cli.js +++ b/packages/providence-analytics/src/cli/cli.js @@ -10,7 +10,7 @@ const { QueryService } = require('../program/services/QueryService.js'); const { InputDataService } = require('../program/services/InputDataService.js'); const promptModule = require('./prompt-analyzer-menu.js'); const cliHelpers = require('./cli-helpers.js'); -const extendDocsModule = require('./generate-extend-docs-data.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; @@ -304,6 +304,7 @@ async function cli({ cwd } = {}) { extensions: commander.extensions, allowlist: commander.allowlist, allowlistReference: commander.allowlistReference, + cwd, }) .then(resolveCli) .catch(rejectCli); diff --git a/packages/providence-analytics/src/cli/generate-extend-docs-data.js b/packages/providence-analytics/src/cli/generate-extend-docs-data.js deleted file mode 100644 index 2ba32b45e..000000000 --- a/packages/providence-analytics/src/cli/generate-extend-docs-data.js +++ /dev/null @@ -1,56 +0,0 @@ -/* eslint-disable import/no-extraneous-dependencies */ -const fs = require('fs'); -const pathLib = require('path'); -const { performance } = require('perf_hooks'); -const { providence } = require('../program/providence.js'); -const { QueryService } = require('../program/services/QueryService.js'); -const { LogService } = require('../program/services/LogService.js'); -const { flatten } = require('./cli-helpers.js'); - -async function launchProvidenceWithExtendDocs({ - referenceProjectPaths, - prefixCfg, - outputFolder, - extensions, - allowlist, - allowlistReference, -}) { - const t0 = performance.now(); - - const results = await providence( - QueryService.getQueryConfigFromAnalyzer('match-paths', { prefix: prefixCfg }), - { - gatherFilesConfig: { - extensions: extensions || ['.js'], - allowlist: allowlist || ['!coverage', '!test'], - }, - gatherFilesConfigReference: { - extensions: extensions || ['.js'], - allowlist: allowlistReference || ['!coverage', '!test'], - }, - queryMethod: 'ast', - report: false, - targetProjectPaths: [pathLib.resolve(process.cwd())], - referenceProjectPaths, - }, - ); - - const outputFilePath = pathLib.join(outputFolder, 'providence-extend-docs-data.json'); - const queryOutputs = flatten( - results.map(result => result.queryOutput).filter(o => typeof o !== 'string'), // filter out '[no-dependency]' etc. - ); - if (fs.existsSync(outputFilePath)) { - fs.unlinkSync(outputFilePath); - } - fs.writeFile(outputFilePath, JSON.stringify(queryOutputs, null, 2), err => { - if (err) { - throw err; - } - }); - const t1 = performance.now(); - LogService.info(`"extend-docs" completed in ${Math.round((t1 - t0) / 1000)} seconds`); -} - -module.exports = { - launchProvidenceWithExtendDocs, -}; diff --git a/packages/providence-analytics/src/cli/launch-providence-with-extend-docs.js b/packages/providence-analytics/src/cli/launch-providence-with-extend-docs.js new file mode 100644 index 000000000..64307e2b2 --- /dev/null +++ b/packages/providence-analytics/src/cli/launch-providence-with-extend-docs.js @@ -0,0 +1,121 @@ +/* eslint-disable import/no-extraneous-dependencies */ +const fs = require('fs'); +const pathLib = require('path'); +const { performance } = require('perf_hooks'); +const { providence } = require('../program/providence.js'); +const { QueryService } = require('../program/services/QueryService.js'); +const { InputDataService } = require('../program/services/InputDataService.js'); +const { LogService } = require('../program/services/LogService.js'); +const { flatten } = require('./cli-helpers.js'); + +async function getExtendDocsResults({ + referenceProjectPaths, + prefixCfg, + extensions, + allowlist, + allowlistReference, + cwd, +}) { + const results = await providence( + QueryService.getQueryConfigFromAnalyzer('match-paths', { prefix: prefixCfg }), + { + gatherFilesConfig: { + extensions: extensions || ['.js'], + allowlist: allowlist || ['!coverage', '!test'], + }, + gatherFilesConfigReference: { + extensions: extensions || ['.js'], + allowlist: allowlistReference || ['!coverage', '!test'], + }, + queryMethod: 'ast', + report: false, + targetProjectPaths: [pathLib.resolve(cwd)], + referenceProjectPaths, + }, + ); + + const queryOutputs = flatten( + results.map(result => result.queryOutput).filter(o => typeof o !== 'string'), // filter out '[no-dependency]' etc. + ); + + /** + * @param {string} pathStr ./packages/lea-tabs/lea-tabs.js + * @param {string[]} pkgs ['packages/lea-tabs', ...] + */ + function replaceToMonoRepoPath(pathStr, pkgs) { + let result = pathStr; + pkgs.some(({ path: p, name }) => { + // for instance ./packages/lea-tabs/lea-tabs.js starts with 'packages/lea-tabs' + const normalizedP = `./${p}`; + if (pathStr.startsWith(normalizedP)) { + const localPath = pathStr.replace(normalizedP, ''); // 'lea-tabs.js' + result = `${name}/${localPath}`; // 'lea-tabs/lea-tabs.js' + return true; + } + return false; + }); + return result; + } + + const pkgs = InputDataService.getMonoRepoPackages(cwd); + + if (pkgs) { + queryOutputs.forEach(resultObj => { + if (resultObj.variable) { + resultObj.variable.paths.forEach(pathObj => { + // eslint-disable-next-line no-param-reassign + pathObj.to = replaceToMonoRepoPath(pathObj.to, pkgs); + }); + } + if (resultObj.tag) { + resultObj.tag.paths.forEach(pathObj => { + // eslint-disable-next-line no-param-reassign + pathObj.to = replaceToMonoRepoPath(pathObj.to, pkgs); + }); + } + }); + } + + return queryOutputs; +} + +async function launchProvidenceWithExtendDocs({ + referenceProjectPaths, + prefixCfg, + outputFolder, + extensions, + allowlist, + allowlistReference, + cwd = process.cwd(), +}) { + const t0 = performance.now(); + + const queryOutputs = await getExtendDocsResults({ + referenceProjectPaths, + prefixCfg, + extensions, + allowlist, + allowlistReference, + cwd, + }); + + // Write results + const outputFilePath = pathLib.join(outputFolder, 'providence-extend-docs-data.json'); + + if (fs.existsSync(outputFilePath)) { + fs.unlinkSync(outputFilePath); + } + fs.writeFile(outputFilePath, JSON.stringify(queryOutputs, null, 2), err => { + if (err) { + throw err; + } + }); + + const t1 = performance.now(); + LogService.info(`"extend-docs" completed in ${Math.round((t1 - t0) / 1000)} seconds`); +} + +module.exports = { + launchProvidenceWithExtendDocs, + getExtendDocsResults, +}; diff --git a/packages/providence-analytics/src/program/analyzers/match-paths.js b/packages/providence-analytics/src/program/analyzers/match-paths.js index 229152260..bcfeff01d 100644 --- a/packages/providence-analytics/src/program/analyzers/match-paths.js +++ b/packages/providence-analytics/src/program/analyzers/match-paths.js @@ -216,7 +216,8 @@ function getTagPaths( let targetResult; targetFindCustomelementsResult.queryOutput.some(({ file, result }) => { const targetPathMatch = result.find(entry => { - const sameRoot = entry.rootFile.file === targetMatchedFile; + const sameRoot = + entry.rootFile.file === targetMatchedFile || entry.rootFile.file === '[current]'; const sameIdentifier = entry.rootFile.specifier === toClass; return sameRoot && sameIdentifier; }); diff --git a/packages/providence-analytics/src/program/analyzers/types.d.ts b/packages/providence-analytics/src/program/analyzers/types.d.ts index fe0d68582..7ad16fc8d 100644 --- a/packages/providence-analytics/src/program/analyzers/types.d.ts +++ b/packages/providence-analytics/src/program/analyzers/types.d.ts @@ -1,6 +1,4 @@ -import { ClassMethod } from "@babel/types"; -import { ProjectReference } from "typescript"; - +import { ProjectReference } from 'typescript'; export interface RootFile { /** the file path containing declaration, for instance './target-src/direct-imports.js'. Can also contain keyword '[current]' */ @@ -9,7 +7,6 @@ export interface RootFile { specifier: string; } - export interface AnalyzerResult { /** meta info object */ meta: Meta; @@ -24,7 +21,6 @@ export interface AnalyzerOutputFile { result: array; } - // TODO: make sure that data structures of JSON output (generated in ReportService) // and data structure generated in Analyzer.prototype._finalize match exactly (move logic from ReportSerivce to _finalize) // so that these type definitions can be used to generate a json schema: https://www.npmjs.com/package/typescript-json-schema @@ -80,26 +76,26 @@ export interface MatchSubclassesAnalyzerOutputEntryMatch { export interface MatchSubclassesAnalyzerOutputEntryMatchFile { /** - * The local filepath that contains the matched class inside the target project - * like `./src/ExtendedClass.js` - */ + * The local filepath that contains the matched class inside the target project + * like `./src/ExtendedClass.js` + */ file: string; /** - * The local Identifier inside matched file that is exported - * @example - * - `ExtendedClass` for `export ExtendedClass extends RefClass {};` - * - `[default]` for `export default ExtendedClass extends RefClass {};` - */ + * The local Identifier inside matched file that is exported + * @example + * - `ExtendedClass` for `export ExtendedClass extends RefClass {};` + * - `[default]` for `export default ExtendedClass extends RefClass {};` + */ identifier: string; } export interface MatchedExportSpecifier extends AnalyzerResult { /** The exported Identifier name. - * - * For instance - * - `export { X as Y } from 'q'` => `Y` - * - `export default class Z {}` => `[default]` - */ + * + * For instance + * - `export { X as Y } from 'q'` => `Y` + * - `export default class Z {}` => `[default]` + */ name: string; /** Project name as found in package.json */ project: string; @@ -109,7 +105,6 @@ export interface MatchedExportSpecifier extends AnalyzerResult { id: string; } - // "find-customelements" export interface FindCustomelementsAnalyzerResult extends AnalyzerResult { @@ -125,14 +120,14 @@ export interface FindCustomelementsAnalyzerOutputFile extends AnalyzerOutputFile export interface FindCustomelementsAnalyzerEntry { /** - * Tag name found in CE definition: - * `customElements.define('my-name', MyConstructor)` => 'my-name' - */ + * Tag name found in CE definition: + * `customElements.define('my-name', MyConstructor)` => 'my-name' + */ tagName: string; /** - * Identifier found in CE definition: - * `customElements.define('my-name', MyConstructor)` => MyConstructor - */ + * Identifier found in CE definition: + * `customElements.define('my-name', MyConstructor)` => MyConstructor + */ constructorIdentifier: string; /** Rootfile traced for constuctorIdentifier found in CE definition */ rootFile: RootFile; @@ -153,42 +148,41 @@ export interface FindExportsAnalyzerOutputFile extends AnalyzerOutputFile { export interface FindExportsAnalyzerEntry { /** - * The specifiers found in an export statement. - * - * For example: - * - file `export class X {}` gives `['X']` - * - file `export default const y = 0` gives `['[default]']` - * - file `export { y, z } from 'project'` gives `['y', 'z']` - */ + * The specifiers found in an export statement. + * + * For example: + * - file `export class X {}` gives `['X']` + * - file `export default const y = 0` gives `['[default]']` + * - file `export { y, z } from 'project'` gives `['y', 'z']` + */ exportSpecifiers: string[]; /** - * The original "source" string belonging to specifier. - * For example: - * - file `export { x } from './my/file';` gives `"./my/file"` - * - file `export { x } from 'project';` gives `"project"` - */ + * The original "source" string belonging to specifier. + * For example: + * - file `export { x } from './my/file';` gives `"./my/file"` + * - file `export { x } from 'project';` gives `"project"` + */ source: string; /** - * The normalized "source" string belonging to specifier - * (based on file system information, resolves right names and extensions). - * For example: - * - file `export { x } from './my/file';` gives `"./my/file.js"` - * - file `export { x } from 'project';` gives `"project"` (only files in current project are resolved) - * - file `export { x } from '../';` gives `"../index.js"` - */ + * The normalized "source" string belonging to specifier + * (based on file system information, resolves right names and extensions). + * For example: + * - file `export { x } from './my/file';` gives `"./my/file.js"` + * - file `export { x } from 'project';` gives `"project"` (only files in current project are resolved) + * - file `export { x } from '../';` gives `"../index.js"` + */ normalizedSource: string; /** map of tracked down Identifiers */ rootFileMap: RootFileMapEntry[]; } - export interface RootFileMapEntry { /** This is the local name in the file we track from */ currentFileSpecifier: string; /** - * The file that contains the original declaration of a certain Identifier/Specifier. - * Contains file(filePath) and specifier keys - */ + * The file that contains the original declaration of a certain Identifier/Specifier. + * Contains file(filePath) and specifier keys + */ rootFile: RootFile; } @@ -207,29 +201,29 @@ export interface FindImportsAnalyzerOutputFile extends AnalyzerOutputFile { export interface FindImportsAnalyzerEntry { /** - * The specifiers found in an import statement. - * - * For example: - * - file `import { X } from 'project'` gives `['X']` - * - file `import X from 'project'` gives `['[default]']` - * - file `import x, { y, z } from 'project'` gives `['[default]', 'y', 'z']` - */ + * The specifiers found in an import statement. + * + * For example: + * - file `import { X } from 'project'` gives `['X']` + * - file `import X from 'project'` gives `['[default]']` + * - file `import x, { y, z } from 'project'` gives `['[default]', 'y', 'z']` + */ importSpecifiers: string[]; /** - * The original "source" string belonging to specifier. - * For example: - * - file `import { x } from './my/file';` gives `"./my/file"` - * - file `import { x } from 'project';` gives `"project"` - */ + * The original "source" string belonging to specifier. + * For example: + * - file `import { x } from './my/file';` gives `"./my/file"` + * - file `import { x } from 'project';` gives `"project"` + */ source: string; /** - * The normalized "source" string belonging to specifier - * (based on file system information, resolves right names and extensions). - * For example: - * - file `import { x } from './my/file';` gives `"./my/file.js"` - * - file `import { x } from 'project';` gives `"project"` (only files in current project are resolved) - * - file `import { x } from '../';` gives `"../index.js"` - */ + * The normalized "source" string belonging to specifier + * (based on file system information, resolves right names and extensions). + * For example: + * - file `import { x } from './my/file';` gives `"./my/file.js"` + * - file `import { x } from 'project';` gives `"project"` (only files in current project are resolved) + * - file `import { x } from '../';` gives `"../index.js"` + */ normalizedSource: string; } @@ -287,8 +281,6 @@ export interface SuperClass { rootFile: RootFile; } - - export interface FindClassesConfig { /** search target paths */ targetProjectPath: string; @@ -300,9 +292,6 @@ export interface AnalyzerConfig { gatherFilesConfig: GatherFilesConfig; } - - - export interface MatchAnalyzerConfig extends AnalyzerConfig { /** reference project path, used to match reference against target */ referenceProjectPath: string; diff --git a/packages/providence-analytics/src/program/services/InputDataService.js b/packages/providence-analytics/src/program/services/InputDataService.js index 037710f8c..1a3b5c9d6 100644 --- a/packages/providence-analytics/src/program/services/InputDataService.js +++ b/packages/providence-analytics/src/program/services/InputDataService.js @@ -14,6 +14,57 @@ const { AstService } = require('./AstService.js'); const { getFilePathRelativeFromRoot } = require('../utils/get-file-path-relative-from-root.js'); const { toPosixPath } = require('../utils/to-posix-path.js'); +// TODO: memoize +function getPackageJson(rootPath) { + try { + const fileContent = fs.readFileSync(`${rootPath}/package.json`, 'utf8'); + return JSON.parse(fileContent); + } catch (_) { + return undefined; + } +} + +function getLernaJson(rootPath) { + try { + const fileContent = fs.readFileSync(`${rootPath}/lerna.json`, 'utf8'); + return JSON.parse(fileContent); + } catch (_) { + return undefined; + } +} + +/** + * + * @param {string[]} list + * @param {string} rootPath + * @returns {{path:string, name:string}[]} + */ +function getPathsFromGlobList(list, rootPath) { + const results = []; + list.forEach(pathOrGlob => { + if (!pathOrGlob.endsWith('/')) { + // eslint-disable-next-line no-param-reassign + pathOrGlob = `${pathOrGlob}/`; + } + + if (pathOrGlob.includes('*')) { + const globResults = glob.sync(pathOrGlob, { cwd: rootPath, absolute: false }); + globResults.forEach(r => { + results.push(r); + }); + } else { + results.push(pathOrGlob); + } + }); + return results.map(path => { + const packageRoot = pathLib.resolve(rootPath, path); + const basename = pathLib.basename(path); + const pkgJson = getPackageJson(packageRoot); + const name = (pkgJson && pkgJson.name) || basename; + return { name, path }; + }); +} + function getGitignoreFile(rootPath) { try { return fs.readFileSync(`${rootPath}/.gitignore`, 'utf8'); @@ -61,11 +112,8 @@ function getGitIgnorePaths(rootPath) { * Gives back all files and folders that need to be added to npm artifact */ function getNpmPackagePaths(rootPath) { - let pkgJson; - try { - const fileContent = fs.readFileSync(`${rootPath}/package.json`, 'utf8'); - pkgJson = JSON.parse(fileContent); - } catch (_) { + const pkgJson = getPackageJson(rootPath); + if (!pkgJson) { return []; } if (pkgJson.files) { @@ -154,6 +202,7 @@ class InputDataService { /** * @param {string} projectPath + * @returns { { path:string, name?:string, mainEntry?:string, version?: string, commitHash?:string }} */ static getProjectMeta(projectPath) { const project = { path: projectPath }; @@ -414,6 +463,24 @@ class InputDataService { return null; } } + + /** + * Gives back all monorepo package paths + */ + static getMonoRepoPackages(rootPath) { + // [1] Look for yarn workspaces + const pkgJson = getPackageJson(rootPath); + if (pkgJson && pkgJson.workspaces) { + return getPathsFromGlobList(pkgJson.workspaces, rootPath); + } + // [2] Look for lerna packages + const lernaJson = getLernaJson(rootPath); + if (lernaJson && lernaJson.packages) { + return getPathsFromGlobList(lernaJson.packages, rootPath); + } + // TODO: support forward compatibility for npm? + return undefined; + } } InputDataService.cacheDisabled = false; diff --git a/packages/providence-analytics/src/program/utils/async-array-utils.js b/packages/providence-analytics/src/program/utils/async-array-utils.js index a188899b7..992888207 100644 --- a/packages/providence-analytics/src/program/utils/async-array-utils.js +++ b/packages/providence-analytics/src/program/utils/async-array-utils.js @@ -2,7 +2,7 @@ * @desc Readable way to do an async forEach * Since predictability matters, all array items will be handled in a queue, * one after another - * @param {array} array + * @param {any[]} array * @param {function} callback */ async function aForEach(array, callback) { @@ -15,8 +15,8 @@ async function aForEach(array, callback) { * @desc Readable way to do an async forEach * If predictability does not matter, this method will traverse array items concurrently, * leading to a better performance - * @param {array} array - * @param {function} callback + * @param {any[]} array + * @param {(value:any, index:number) => {}} callback */ async function aForEachNonSequential(array, callback) { return Promise.all(array.map(callback)); @@ -25,7 +25,7 @@ async function aForEachNonSequential(array, callback) { * @desc Readable way to do an async map * Since predictability is crucial for a map, all array items will be handled in a queue, * one after anotoher - * @param {array} array + * @param {any[]} array * @param {function} callback */ async function aMap(array, callback) { diff --git a/packages/providence-analytics/src/program/utils/get-hash.js b/packages/providence-analytics/src/program/utils/get-hash.js index c1f13c718..47d2a7a38 100644 --- a/packages/providence-analytics/src/program/utils/get-hash.js +++ b/packages/providence-analytics/src/program/utils/get-hash.js @@ -1,5 +1,4 @@ /** - * * @param {string|object} inputValue * @returns {number} */ diff --git a/packages/providence-analytics/src/program/utils/jsdoc-comment-parser.js b/packages/providence-analytics/src/program/utils/jsdoc-comment-parser.js index 85d4bf069..eead6cd0f 100644 --- a/packages/providence-analytics/src/program/utils/jsdoc-comment-parser.js +++ b/packages/providence-analytics/src/program/utils/jsdoc-comment-parser.js @@ -1,3 +1,4 @@ +// @ts-nocheck /* eslint-disable */ /** diff --git a/packages/providence-analytics/src/program/utils/memoize.js b/packages/providence-analytics/src/program/utils/memoize.js index 6a34b52ec..d1cd7f852 100644 --- a/packages/providence-analytics/src/program/utils/memoize.js +++ b/packages/providence-analytics/src/program/utils/memoize.js @@ -1,5 +1,9 @@ const { InputDataService } = require('../services/InputDataService.js'); +/** + * @param {function} func + * @param {{}} externalStorage + */ function memoize(func, externalStorage) { const storage = externalStorage || {}; // eslint-disable-next-line func-names @@ -7,15 +11,23 @@ function memoize(func, externalStorage) { // eslint-disable-next-line prefer-rest-params const args = [...arguments]; // Allow disabling of cache for testing purposes + // @ts-ignore if (!InputDataService.cacheDisabled && args in storage) { + // @ts-ignore return storage[args]; } + // @ts-ignore const outcome = func.apply(this, args); + // @ts-ignore storage[args] = outcome; return outcome; }; } +/** + * @param {function} func + * @param {{}} externalStorage + */ function memoizeAsync(func, externalStorage) { const storage = externalStorage || {}; // eslint-disable-next-line func-names @@ -23,10 +35,14 @@ function memoizeAsync(func, externalStorage) { // eslint-disable-next-line prefer-rest-params const args = [...arguments]; // Allow disabling of cache for testing purposes + // @ts-ignore if (!InputDataService.cacheDisabled && args in storage) { + // @ts-ignore return storage[args]; } + // @ts-ignore const outcome = await func.apply(this, args); + // @ts-ignore storage[args] = outcome; return outcome; }; diff --git a/packages/providence-analytics/src/program/utils/read-package-tree-with-bower-support.js b/packages/providence-analytics/src/program/utils/read-package-tree-with-bower-support.js index 5fae5891e..6f73b121e 100644 --- a/packages/providence-analytics/src/program/utils/read-package-tree-with-bower-support.js +++ b/packages/providence-analytics/src/program/utils/read-package-tree-with-bower-support.js @@ -1,3 +1,4 @@ +// @ts-nocheck /* eslint-disable */ /** * This is a modified version of https://github.com/npm/read-package-tree/blob/master/rpt.js diff --git a/packages/providence-analytics/test-helpers/mock-project-helpers.js b/packages/providence-analytics/test-helpers/mock-project-helpers.js index b17f58a6c..73f48fe97 100644 --- a/packages/providence-analytics/test-helpers/mock-project-helpers.js +++ b/packages/providence-analytics/test-helpers/mock-project-helpers.js @@ -7,9 +7,9 @@ const path = require('path'); * "InputDataService.createDataObject", it gives back a mocked response. * @param {string[]|object} files all the code that will be run trhough AST * @param {object} [cfg] - * @param {string} [cfg.project='fictional-project'] + * @param {string} [cfg.projectName='fictional-project'] * @param {string} [cfg.projectPath='/fictional/project'] - * @param {string[]} [cfg.filePath=`/fictional/project/test-file-${i}.js`] The indexes of the file + * @param {string[]} [cfg.filePaths=`[/fictional/project/test-file-${i}.js]`] The indexes of the file * paths match with the indexes of the files * @param {object} existingMock config for mock-fs, so the previous config is not overridden */ @@ -18,6 +18,9 @@ function mockProject(files, cfg = {}, existingMock = {}) { const projPath = cfg.projectPath || '/fictional/project'; // Create obj structure for mock-fs + /** + * @param {object} files + */ // eslint-disable-next-line no-shadow function createFilesObjForFolder(files) { let projFilesObj = {}; diff --git a/packages/providence-analytics/test-node/cli/cli.test.js b/packages/providence-analytics/test-node/cli/cli.test.js index 377e71f3f..fe1e37a6f 100644 --- a/packages/providence-analytics/test-node/cli/cli.test.js +++ b/packages/providence-analytics/test-node/cli/cli.test.js @@ -5,6 +5,7 @@ const commander = require('commander'); const { mockProject, restoreMockedProjects, + mockTargetAndReferenceProject, } = require('../../test-helpers/mock-project-helpers.js'); const { mockWriteToJson, @@ -17,11 +18,12 @@ const { const { InputDataService } = require('../../src/program/services/InputDataService.js'); const { QueryService } = require('../../src/program/services/QueryService.js'); const providenceModule = require('../../src/program/providence.js'); -const extendDocsModule = require('../../src/cli/generate-extend-docs-data.js'); +const extendDocsModule = require('../../src/cli/launch-providence-with-extend-docs.js'); const cliHelpersModule = require('../../src/cli/cli-helpers.js'); const { cli } = require('../../src/cli/cli.js'); const promptAnalyzerModule = require('../../src/cli/prompt-analyzer-menu.js'); const { toPosixPath } = require('../../src/program/utils/to-posix-path.js'); +const { getExtendDocsResults } = require('../../src/cli/launch-providence-with-extend-docs.js'); const { pathsArrayFromCs, @@ -383,6 +385,7 @@ describe('Providence CLI', () => { extensions: ['.bla'], allowlist: [`${rootDir}/al`], allowlistReference: [`${rootDir}/alr`], + cwd: undefined, }); }); }); @@ -507,4 +510,136 @@ describe('CLI helpers', () => { ]); }); }); + + describe('Extend docs', () => { + afterEach(() => { + restoreMockedProjects(); + }); + + it('rewrites monorepo package paths when analysis is run from monorepo root', async () => { + const theirProjectFiles = { + './package.json': JSON.stringify({ + name: 'their-components', + version: '1.0.0', + }), + './src/TheirButton.js': `export class TheirButton extends HTMLElement {}`, + './src/TheirTooltip.js': `export class TheirTooltip extends HTMLElement {}`, + './their-button.js': ` + import { TheirButton } from './src/TheirButton.js'; + + customElements.define('their-button', TheirButton); + `, + './demo.js': ` + import { TheirTooltip } from './src/TheirTooltip.js'; + import './their-button.js'; + `, + }; + + const myProjectFiles = { + './package.json': JSON.stringify({ + name: '@my/root', + workspaces: ['packages/*', 'another-folder/my-tooltip'], + dependencies: { + 'their-components': '1.0.0', + }, + }), + // Package 1: @my/button + './packages/button/package.json': JSON.stringify({ + name: '@my/button', + }), + './packages/button/src/MyButton.js': ` + import { TheirButton } from 'their-components/src/TheirButton.js'; + + export class MyButton extends TheirButton {} + `, + './packages/button/src/my-button.js': ` + import { MyButton } from './MyButton.js'; + + customElements.define('my-button', MyButton); + `, + + // Package 2: @my/tooltip + './packages/tooltip/package.json': JSON.stringify({ + name: '@my/tooltip', + }), + './packages/tooltip/src/MyTooltip.js': ` + import { TheirTooltip } from 'their-components/src/TheirTooltip.js'; + + export class MyTooltip extends TheirTooltip {} + `, + }; + + const theirProject = { + path: '/their-components', + name: 'their-components', + files: Object.entries(theirProjectFiles).map(([file, code]) => ({ file, code })), + }; + + const myProject = { + path: '/my-components', + name: 'my-components', + files: Object.entries(myProjectFiles).map(([file, code]) => ({ file, code })), + }; + + mockTargetAndReferenceProject(theirProject, myProject); + + const result = await getExtendDocsResults({ + referenceProjectPaths: ['/their-components'], + prefixCfg: { from: 'their', to: 'my' }, + extensions: ['.js'], + cwd: '/my-components', + }); + + expect(result).to.eql([ + { + name: 'TheirButton', + variable: { + from: 'TheirButton', + to: 'MyButton', + paths: [ + { + from: './src/TheirButton.js', + to: '@my/button/src/MyButton.js', // rewritten from './packages/button/src/MyButton.js', + }, + { + from: 'their-components/src/TheirButton.js', + to: '@my/button/src/MyButton.js', // rewritten from './packages/button/src/MyButton.js', + }, + ], + }, + tag: { + from: 'their-button', + to: 'my-button', + paths: [ + { + from: './their-button.js', + to: '@my/button/src/my-button.js', // rewritten from './packages/button/src/MyButton.js', + }, + { + from: 'their-components/their-button.js', + to: '@my/button/src/my-button.js', // rewritten from './packages/button/src/MyButton.js', + }, + ], + }, + }, + { + name: 'TheirTooltip', + variable: { + from: 'TheirTooltip', + to: 'MyTooltip', + paths: [ + { + from: './src/TheirTooltip.js', + to: '@my/tooltip/src/MyTooltip.js', // './packages/tooltip/src/MyTooltip.js', + }, + { + from: 'their-components/src/TheirTooltip.js', + to: '@my/tooltip/src/MyTooltip.js', // './packages/tooltip/src/MyTooltip.js', + }, + ], + }, + }, + ]); + }); + }); }); diff --git a/packages/providence-analytics/test-node/program/analyzers/match-paths.test.js b/packages/providence-analytics/test-node/program/analyzers/match-paths.test.js index 500061fb4..78aac693b 100644 --- a/packages/providence-analytics/test-node/program/analyzers/match-paths.test.js +++ b/packages/providence-analytics/test-node/program/analyzers/match-paths.test.js @@ -543,6 +543,81 @@ describe('Analyzer "match-paths"', () => { expect(queryResult.queryOutput[1].tag).to.eql(expectedMatches[1]); }); + // TODO: test works in isolation, but some side effects occur when run in suite + it.skip(`allows class definition and customElement to be in same file`, async () => { + const theirProjectFiles = { + './package.json': JSON.stringify({ + name: 'their-components', + version: '1.0.0', + }), + './src/TheirButton.js': `export class TheirButton extends HTMLElement {}`, + './src/TheirTooltip.js': `export class TheirTooltip extends HTMLElement {}`, + './their-button.js': ` + import { TheirButton } from './src/TheirButton.js'; + + customElements.define('their-button', TheirButton); + `, + './demo.js': ` + import { TheirTooltip } from './src/TheirTooltip.js'; + import './their-button.js'; + `, + }; + + const myProjectFiles = { + './package.json': JSON.stringify({ + name: 'my-components', + dependencies: { + 'their-components': '1.0.0', + }, + }), + './src/button/MyButton.js': ` + import { TheirButton } from 'their-components/src/TheirButton.js'; + + export class MyButton extends TheirButton {} + customElements.define('my-button', MyButton); + `, + }; + + const theirProject = { + path: '/their-components', + name: 'their-components', + files: Object.entries(theirProjectFiles).map(([file, code]) => ({ file, code })), + }; + + const myProject = { + path: '/my-components', + name: 'my-components', + files: Object.entries(myProjectFiles).map(([file, code]) => ({ file, code })), + }; + + mockTargetAndReferenceProject(theirProject, myProject); + + const providenceCfg = { + targetProjectPaths: ['/my-components'], + referenceProjectPaths: ['/their-components'], + }; + + await providence( + { ...matchPathsQueryConfig, prefix: { from: 'their', to: 'my' } }, + providenceCfg, + ); + const queryResult = queryResults[0]; + expect(queryResult.queryOutput[0].tag).to.eql({ + from: 'their-button', + to: 'my-button', + paths: [ + { + from: './their-button.js', + to: './src/button/MyButton.js', + }, + { + from: 'their-components/their-button.js', + to: './src/button/MyButton.js', + }, + ], + }); + }); + describe('Features', () => { it(`identifies all "from" and "to" tagnames`, async () => { mockTargetAndReferenceProject(searchTargetProject, referenceProject); diff --git a/packages/providence-analytics/test-node/program/services/InputDataService.test.js b/packages/providence-analytics/test-node/program/services/InputDataService.test.js index 910647446..19c3302f5 100644 --- a/packages/providence-analytics/test-node/program/services/InputDataService.test.js +++ b/packages/providence-analytics/test-node/program/services/InputDataService.test.js @@ -61,15 +61,53 @@ describe('InputDataService', () => { ); }); - it('mocked "createDataObject"', async () => { - // By testing the output of our mocked method against the data of the real method, we - // make sure the tests don't run sucessfully undeserved - }); - it('"getTargetProjectPaths"', async () => {}); it('"getReferenceProjectPaths"', async () => {}); + describe('"getMonoRepoPackages"', async () => { + it('supports yarn workspaces', async () => { + mockProject({ + './package.json': JSON.stringify({ + workspaces: ['packages/*', 'another-folder/another-package'], + }), + './packages/pkg1/package.json': '{ "name": "package1" }', + './packages/pkg2/package.json': '', + './packages/pkg3/package.json': '{ "name": "@scope/pkg3" }', + './another-folder/another-package/package.json': + '{ "name": "@another-scope/another-package" }', + }); + + expect(InputDataService.getMonoRepoPackages('/fictional/project')).to.eql([ + { path: 'packages/pkg1/', name: 'package1' }, + { path: 'packages/pkg2/', name: 'pkg2' }, // fallback when no package.json + { path: 'packages/pkg3/', name: '@scope/pkg3' }, + { path: 'another-folder/another-package/', name: '@another-scope/another-package' }, + ]); + }); + + it('supports lerna', async () => { + mockProject({ + './package.json': JSON.stringify({}), + './lerna.json': JSON.stringify({ + packages: ['packages/*', 'another-folder/another-package'], + }), + './packages/pkg1/package.json': '{ "name": "package1" }', + './packages/pkg2/package.json': '', + './packages/pkg3/package.json': '{ "name": "@scope/pkg3" }', + './another-folder/another-package/package.json': + '{ "name": "@another-scope/another-package" }', + }); + + expect(InputDataService.getMonoRepoPackages('/fictional/project')).to.eql([ + { path: 'packages/pkg1/', name: 'package1' }, + { path: 'packages/pkg2/', name: 'pkg2' }, // fallback when no package.json + { path: 'packages/pkg3/', name: '@scope/pkg3' }, + { path: 'another-folder/another-package/', name: '@another-scope/another-package' }, + ]); + }); + }); + describe('"gatherFilesFromDir"', async () => { beforeEach(() => { mockProject({