feat(providence-analytics): monorepo support extend-docs

This commit is contained in:
Thijs Louisse 2020-10-13 15:24:24 +02:00 committed by GitHub
parent f0d11fee4a
commit 2dc85b14d3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 552 additions and 148 deletions

View file

@ -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'

View file

@ -10,7 +10,7 @@ const { QueryService } = require('../program/services/QueryService.js');
const { InputDataService } = require('../program/services/InputDataService.js'); const { InputDataService } = require('../program/services/InputDataService.js');
const promptModule = require('./prompt-analyzer-menu.js'); const promptModule = require('./prompt-analyzer-menu.js');
const cliHelpers = require('./cli-helpers.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 { toPosixPath } = require('../program/utils/to-posix-path.js');
const { extensionsFromCs, setQueryMethod, targetDefault, installDeps } = cliHelpers; const { extensionsFromCs, setQueryMethod, targetDefault, installDeps } = cliHelpers;
@ -304,6 +304,7 @@ async function cli({ cwd } = {}) {
extensions: commander.extensions, extensions: commander.extensions,
allowlist: commander.allowlist, allowlist: commander.allowlist,
allowlistReference: commander.allowlistReference, allowlistReference: commander.allowlistReference,
cwd,
}) })
.then(resolveCli) .then(resolveCli)
.catch(rejectCli); .catch(rejectCli);

View file

@ -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,
};

View file

@ -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,
};

View file

@ -216,7 +216,8 @@ function getTagPaths(
let targetResult; let targetResult;
targetFindCustomelementsResult.queryOutput.some(({ file, result }) => { targetFindCustomelementsResult.queryOutput.some(({ file, result }) => {
const targetPathMatch = result.find(entry => { 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; const sameIdentifier = entry.rootFile.specifier === toClass;
return sameRoot && sameIdentifier; return sameRoot && sameIdentifier;
}); });

View file

@ -1,6 +1,4 @@
import { ClassMethod } from "@babel/types"; import { ProjectReference } from 'typescript';
import { ProjectReference } from "typescript";
export interface RootFile { export interface RootFile {
/** the file path containing declaration, for instance './target-src/direct-imports.js'. Can also contain keyword '[current]' */ /** 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; specifier: string;
} }
export interface AnalyzerResult { export interface AnalyzerResult {
/** meta info object */ /** meta info object */
meta: Meta; meta: Meta;
@ -24,7 +21,6 @@ export interface AnalyzerOutputFile {
result: array; result: array;
} }
// TODO: make sure that data structures of JSON output (generated in ReportService) // 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) // 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 // 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 { export interface MatchSubclassesAnalyzerOutputEntryMatchFile {
/** /**
* The local filepath that contains the matched class inside the target project * The local filepath that contains the matched class inside the target project
* like `./src/ExtendedClass.js` * like `./src/ExtendedClass.js`
*/ */
file: string; file: string;
/** /**
* The local Identifier inside matched file that is exported * The local Identifier inside matched file that is exported
* @example * @example
* - `ExtendedClass` for `export ExtendedClass extends RefClass {};` * - `ExtendedClass` for `export ExtendedClass extends RefClass {};`
* - `[default]` for `export default ExtendedClass extends RefClass {};` * - `[default]` for `export default ExtendedClass extends RefClass {};`
*/ */
identifier: string; identifier: string;
} }
export interface MatchedExportSpecifier extends AnalyzerResult { export interface MatchedExportSpecifier extends AnalyzerResult {
/** The exported Identifier name. /** The exported Identifier name.
* *
* For instance * For instance
* - `export { X as Y } from 'q'` => `Y` * - `export { X as Y } from 'q'` => `Y`
* - `export default class Z {}` => `[default]` * - `export default class Z {}` => `[default]`
*/ */
name: string; name: string;
/** Project name as found in package.json */ /** Project name as found in package.json */
project: string; project: string;
@ -109,7 +105,6 @@ export interface MatchedExportSpecifier extends AnalyzerResult {
id: string; id: string;
} }
// "find-customelements" // "find-customelements"
export interface FindCustomelementsAnalyzerResult extends AnalyzerResult { export interface FindCustomelementsAnalyzerResult extends AnalyzerResult {
@ -125,14 +120,14 @@ export interface FindCustomelementsAnalyzerOutputFile extends AnalyzerOutputFile
export interface FindCustomelementsAnalyzerEntry { export interface FindCustomelementsAnalyzerEntry {
/** /**
* Tag name found in CE definition: * Tag name found in CE definition:
* `customElements.define('my-name', MyConstructor)` => 'my-name' * `customElements.define('my-name', MyConstructor)` => 'my-name'
*/ */
tagName: string; tagName: string;
/** /**
* Identifier found in CE definition: * Identifier found in CE definition:
* `customElements.define('my-name', MyConstructor)` => MyConstructor * `customElements.define('my-name', MyConstructor)` => MyConstructor
*/ */
constructorIdentifier: string; constructorIdentifier: string;
/** Rootfile traced for constuctorIdentifier found in CE definition */ /** Rootfile traced for constuctorIdentifier found in CE definition */
rootFile: RootFile; rootFile: RootFile;
@ -153,42 +148,41 @@ export interface FindExportsAnalyzerOutputFile extends AnalyzerOutputFile {
export interface FindExportsAnalyzerEntry { export interface FindExportsAnalyzerEntry {
/** /**
* The specifiers found in an export statement. * The specifiers found in an export statement.
* *
* For example: * For example:
* - file `export class X {}` gives `['X']` * - file `export class X {}` gives `['X']`
* - file `export default const y = 0` gives `['[default]']` * - file `export default const y = 0` gives `['[default]']`
* - file `export { y, z } from 'project'` gives `['y', 'z']` * - file `export { y, z } from 'project'` gives `['y', 'z']`
*/ */
exportSpecifiers: string[]; exportSpecifiers: string[];
/** /**
* The original "source" string belonging to specifier. * The original "source" string belonging to specifier.
* For example: * For example:
* - file `export { x } from './my/file';` gives `"./my/file"` * - file `export { x } from './my/file';` gives `"./my/file"`
* - file `export { x } from 'project';` gives `"project"` * - file `export { x } from 'project';` gives `"project"`
*/ */
source: string; source: string;
/** /**
* The normalized "source" string belonging to specifier * The normalized "source" string belonging to specifier
* (based on file system information, resolves right names and extensions). * (based on file system information, resolves right names and extensions).
* For example: * For example:
* - file `export { x } from './my/file';` gives `"./my/file.js"` * - 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 'project';` gives `"project"` (only files in current project are resolved)
* - file `export { x } from '../';` gives `"../index.js"` * - file `export { x } from '../';` gives `"../index.js"`
*/ */
normalizedSource: string; normalizedSource: string;
/** map of tracked down Identifiers */ /** map of tracked down Identifiers */
rootFileMap: RootFileMapEntry[]; rootFileMap: RootFileMapEntry[];
} }
export interface RootFileMapEntry { export interface RootFileMapEntry {
/** This is the local name in the file we track from */ /** This is the local name in the file we track from */
currentFileSpecifier: string; currentFileSpecifier: string;
/** /**
* The file that contains the original declaration of a certain Identifier/Specifier. * The file that contains the original declaration of a certain Identifier/Specifier.
* Contains file(filePath) and specifier keys * Contains file(filePath) and specifier keys
*/ */
rootFile: RootFile; rootFile: RootFile;
} }
@ -207,29 +201,29 @@ export interface FindImportsAnalyzerOutputFile extends AnalyzerOutputFile {
export interface FindImportsAnalyzerEntry { export interface FindImportsAnalyzerEntry {
/** /**
* The specifiers found in an import statement. * The specifiers found in an import statement.
* *
* For example: * For example:
* - file `import { X } from 'project'` gives `['X']` * - file `import { X } from 'project'` gives `['X']`
* - file `import X from 'project'` gives `['[default]']` * - file `import X from 'project'` gives `['[default]']`
* - file `import x, { y, z } from 'project'` gives `['[default]', 'y', 'z']` * - file `import x, { y, z } from 'project'` gives `['[default]', 'y', 'z']`
*/ */
importSpecifiers: string[]; importSpecifiers: string[];
/** /**
* The original "source" string belonging to specifier. * The original "source" string belonging to specifier.
* For example: * For example:
* - file `import { x } from './my/file';` gives `"./my/file"` * - file `import { x } from './my/file';` gives `"./my/file"`
* - file `import { x } from 'project';` gives `"project"` * - file `import { x } from 'project';` gives `"project"`
*/ */
source: string; source: string;
/** /**
* The normalized "source" string belonging to specifier * The normalized "source" string belonging to specifier
* (based on file system information, resolves right names and extensions). * (based on file system information, resolves right names and extensions).
* For example: * For example:
* - file `import { x } from './my/file';` gives `"./my/file.js"` * - 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 'project';` gives `"project"` (only files in current project are resolved)
* - file `import { x } from '../';` gives `"../index.js"` * - file `import { x } from '../';` gives `"../index.js"`
*/ */
normalizedSource: string; normalizedSource: string;
} }
@ -287,8 +281,6 @@ export interface SuperClass {
rootFile: RootFile; rootFile: RootFile;
} }
export interface FindClassesConfig { export interface FindClassesConfig {
/** search target paths */ /** search target paths */
targetProjectPath: string; targetProjectPath: string;
@ -300,9 +292,6 @@ export interface AnalyzerConfig {
gatherFilesConfig: GatherFilesConfig; gatherFilesConfig: GatherFilesConfig;
} }
export interface MatchAnalyzerConfig extends AnalyzerConfig { export interface MatchAnalyzerConfig extends AnalyzerConfig {
/** reference project path, used to match reference against target */ /** reference project path, used to match reference against target */
referenceProjectPath: string; referenceProjectPath: string;

View file

@ -14,6 +14,57 @@ const { AstService } = require('./AstService.js');
const { getFilePathRelativeFromRoot } = require('../utils/get-file-path-relative-from-root.js'); const { getFilePathRelativeFromRoot } = require('../utils/get-file-path-relative-from-root.js');
const { toPosixPath } = require('../utils/to-posix-path.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) { function getGitignoreFile(rootPath) {
try { try {
return fs.readFileSync(`${rootPath}/.gitignore`, 'utf8'); 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 * Gives back all files and folders that need to be added to npm artifact
*/ */
function getNpmPackagePaths(rootPath) { function getNpmPackagePaths(rootPath) {
let pkgJson; const pkgJson = getPackageJson(rootPath);
try { if (!pkgJson) {
const fileContent = fs.readFileSync(`${rootPath}/package.json`, 'utf8');
pkgJson = JSON.parse(fileContent);
} catch (_) {
return []; return [];
} }
if (pkgJson.files) { if (pkgJson.files) {
@ -154,6 +202,7 @@ class InputDataService {
/** /**
* @param {string} projectPath * @param {string} projectPath
* @returns { { path:string, name?:string, mainEntry?:string, version?: string, commitHash?:string }}
*/ */
static getProjectMeta(projectPath) { static getProjectMeta(projectPath) {
const project = { path: projectPath }; const project = { path: projectPath };
@ -414,6 +463,24 @@ class InputDataService {
return null; 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; InputDataService.cacheDisabled = false;

View file

@ -2,7 +2,7 @@
* @desc Readable way to do an async forEach * @desc Readable way to do an async forEach
* Since predictability matters, all array items will be handled in a queue, * Since predictability matters, all array items will be handled in a queue,
* one after another * one after another
* @param {array} array * @param {any[]} array
* @param {function} callback * @param {function} callback
*/ */
async function aForEach(array, callback) { async function aForEach(array, callback) {
@ -15,8 +15,8 @@ async function aForEach(array, callback) {
* @desc Readable way to do an async forEach * @desc Readable way to do an async forEach
* If predictability does not matter, this method will traverse array items concurrently, * If predictability does not matter, this method will traverse array items concurrently,
* leading to a better performance * leading to a better performance
* @param {array} array * @param {any[]} array
* @param {function} callback * @param {(value:any, index:number) => {}} callback
*/ */
async function aForEachNonSequential(array, callback) { async function aForEachNonSequential(array, callback) {
return Promise.all(array.map(callback)); return Promise.all(array.map(callback));
@ -25,7 +25,7 @@ async function aForEachNonSequential(array, callback) {
* @desc Readable way to do an async map * @desc Readable way to do an async map
* Since predictability is crucial for a map, all array items will be handled in a queue, * Since predictability is crucial for a map, all array items will be handled in a queue,
* one after anotoher * one after anotoher
* @param {array} array * @param {any[]} array
* @param {function} callback * @param {function} callback
*/ */
async function aMap(array, callback) { async function aMap(array, callback) {

View file

@ -1,5 +1,4 @@
/** /**
*
* @param {string|object} inputValue * @param {string|object} inputValue
* @returns {number} * @returns {number}
*/ */

View file

@ -1,3 +1,4 @@
// @ts-nocheck
/* eslint-disable */ /* eslint-disable */
/** /**

View file

@ -1,5 +1,9 @@
const { InputDataService } = require('../services/InputDataService.js'); const { InputDataService } = require('../services/InputDataService.js');
/**
* @param {function} func
* @param {{}} externalStorage
*/
function memoize(func, externalStorage) { function memoize(func, externalStorage) {
const storage = externalStorage || {}; const storage = externalStorage || {};
// eslint-disable-next-line func-names // eslint-disable-next-line func-names
@ -7,15 +11,23 @@ function memoize(func, externalStorage) {
// eslint-disable-next-line prefer-rest-params // eslint-disable-next-line prefer-rest-params
const args = [...arguments]; const args = [...arguments];
// Allow disabling of cache for testing purposes // Allow disabling of cache for testing purposes
// @ts-ignore
if (!InputDataService.cacheDisabled && args in storage) { if (!InputDataService.cacheDisabled && args in storage) {
// @ts-ignore
return storage[args]; return storage[args];
} }
// @ts-ignore
const outcome = func.apply(this, args); const outcome = func.apply(this, args);
// @ts-ignore
storage[args] = outcome; storage[args] = outcome;
return outcome; return outcome;
}; };
} }
/**
* @param {function} func
* @param {{}} externalStorage
*/
function memoizeAsync(func, externalStorage) { function memoizeAsync(func, externalStorage) {
const storage = externalStorage || {}; const storage = externalStorage || {};
// eslint-disable-next-line func-names // eslint-disable-next-line func-names
@ -23,10 +35,14 @@ function memoizeAsync(func, externalStorage) {
// eslint-disable-next-line prefer-rest-params // eslint-disable-next-line prefer-rest-params
const args = [...arguments]; const args = [...arguments];
// Allow disabling of cache for testing purposes // Allow disabling of cache for testing purposes
// @ts-ignore
if (!InputDataService.cacheDisabled && args in storage) { if (!InputDataService.cacheDisabled && args in storage) {
// @ts-ignore
return storage[args]; return storage[args];
} }
// @ts-ignore
const outcome = await func.apply(this, args); const outcome = await func.apply(this, args);
// @ts-ignore
storage[args] = outcome; storage[args] = outcome;
return outcome; return outcome;
}; };

View file

@ -1,3 +1,4 @@
// @ts-nocheck
/* eslint-disable */ /* eslint-disable */
/** /**
* This is a modified version of https://github.com/npm/read-package-tree/blob/master/rpt.js * This is a modified version of https://github.com/npm/read-package-tree/blob/master/rpt.js

View file

@ -7,9 +7,9 @@ const path = require('path');
* "InputDataService.createDataObject", it gives back a mocked response. * "InputDataService.createDataObject", it gives back a mocked response.
* @param {string[]|object} files all the code that will be run trhough AST * @param {string[]|object} files all the code that will be run trhough AST
* @param {object} [cfg] * @param {object} [cfg]
* @param {string} [cfg.project='fictional-project'] * @param {string} [cfg.projectName='fictional-project']
* @param {string} [cfg.projectPath='/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 * paths match with the indexes of the files
* @param {object} existingMock config for mock-fs, so the previous config is not overridden * @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'; const projPath = cfg.projectPath || '/fictional/project';
// Create obj structure for mock-fs // Create obj structure for mock-fs
/**
* @param {object} files
*/
// eslint-disable-next-line no-shadow // eslint-disable-next-line no-shadow
function createFilesObjForFolder(files) { function createFilesObjForFolder(files) {
let projFilesObj = {}; let projFilesObj = {};

View file

@ -5,6 +5,7 @@ const commander = require('commander');
const { const {
mockProject, mockProject,
restoreMockedProjects, restoreMockedProjects,
mockTargetAndReferenceProject,
} = require('../../test-helpers/mock-project-helpers.js'); } = require('../../test-helpers/mock-project-helpers.js');
const { const {
mockWriteToJson, mockWriteToJson,
@ -17,11 +18,12 @@ const {
const { InputDataService } = require('../../src/program/services/InputDataService.js'); const { InputDataService } = require('../../src/program/services/InputDataService.js');
const { QueryService } = require('../../src/program/services/QueryService.js'); const { QueryService } = require('../../src/program/services/QueryService.js');
const providenceModule = require('../../src/program/providence.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 cliHelpersModule = require('../../src/cli/cli-helpers.js');
const { cli } = require('../../src/cli/cli.js'); const { cli } = require('../../src/cli/cli.js');
const promptAnalyzerModule = require('../../src/cli/prompt-analyzer-menu.js'); const promptAnalyzerModule = require('../../src/cli/prompt-analyzer-menu.js');
const { toPosixPath } = require('../../src/program/utils/to-posix-path.js'); const { toPosixPath } = require('../../src/program/utils/to-posix-path.js');
const { getExtendDocsResults } = require('../../src/cli/launch-providence-with-extend-docs.js');
const { const {
pathsArrayFromCs, pathsArrayFromCs,
@ -383,6 +385,7 @@ describe('Providence CLI', () => {
extensions: ['.bla'], extensions: ['.bla'],
allowlist: [`${rootDir}/al`], allowlist: [`${rootDir}/al`],
allowlistReference: [`${rootDir}/alr`], 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',
},
],
},
},
]);
});
});
}); });

View file

@ -543,6 +543,81 @@ describe('Analyzer "match-paths"', () => {
expect(queryResult.queryOutput[1].tag).to.eql(expectedMatches[1]); 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', () => { describe('Features', () => {
it(`identifies all "from" and "to" tagnames`, async () => { it(`identifies all "from" and "to" tagnames`, async () => {
mockTargetAndReferenceProject(searchTargetProject, referenceProject); mockTargetAndReferenceProject(searchTargetProject, referenceProject);

View file

@ -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('"getTargetProjectPaths"', async () => {});
it('"getReferenceProjectPaths"', 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 () => { describe('"gatherFilesFromDir"', async () => {
beforeEach(() => { beforeEach(() => {
mockProject({ mockProject({