From 47b509473d1b10487603286df1b073ba26763150 Mon Sep 17 00:00:00 2001 From: PKulkoRaccoonGang Date: Wed, 27 Dec 2023 19:37:20 +0200 Subject: [PATCH 1/5] refactor: changed Paragon NPM package name and source --- .github/pull_request_template.md | 2 +- README.md | 29 ++++++++++++----------- catalog-info.yml | 2 +- example/module.config.js | 6 ++--- example/src/MyComponent.jsx | 4 ++-- example/src/index.scss | 2 +- package.json | 2 +- src/Button/index.jsx | 4 ++-- src/Chip/index.tsx | 4 ++-- src/Icon/README.md | 6 ++--- src/Icon/index.jsx | 2 +- src/IconButton/index.jsx | 2 +- src/SearchField/index.jsx | 4 ++-- www/src/components/IconsTable.tsx | 2 +- www/src/components/Menu.tsx | 4 ++-- www/src/pages/foundations/brand-icons.mdx | 4 ++-- www/src/pages/foundations/icons.mdx | 4 ++-- www/src/pages/guides/getting-started.mdx | 8 +++---- 18 files changed, 46 insertions(+), 45 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index e8b455b8c9..4a8d9d1edf 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -18,6 +18,6 @@ Include a direct link to your changes in this PR's deploy preview here (e.g., a ## Post-merge Checklist -* [ ] Verify your changes were released to [NPM](https://www.npmjs.com/package/@edx/paragon) at the expected version. +* [ ] Verify your changes were released to [NPM](https://www.npmjs.com/package/@openedx/paragon) at the expected version. * [ ] If you'd like, [share](https://github.com/openedx/paragon/discussions/new?category=show-and-tell) your contribution in [#show-and-tell](https://github.com/openedx/paragon/discussions/categories/show-and-tell). * [ ] 🎉 🙌 Celebrate! Thanks for your contribution. diff --git a/README.md b/README.md index 69d2ef32b6..761aa21017 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ # Paragon [![Build Status](https://github.com/openedx/paragon/actions/workflows/release.yml/badge.svg)](https://github.com/openedx/paragon/actions/workflows/release.yml) -[![npm_version](https://img.shields.io/npm/v/@edx/paragon.svg)](@edx/paragon) +[![npm_version](https://img.shields.io/npm/v/@openedx/paragon.svg)](@openedx/paragon) ![status](https://img.shields.io/badge/status-Maintained-brightgreen) ![license](https://img.shields.io/github/license/openedx/paragon.svg) [![codecov](https://codecov.io/gh/edx/paragon/branch/master/graph/badge.svg?token=x1tZmNduy9)](https://codecov.io/gh/edx/paragon) -[![NPM downloads](https://img.shields.io/npm/dw/@edx/paragon)](https://www.npmjs.com/package/@edx/paragon) +[![NPM downloads](https://img.shields.io/npm/dw/@openedx/paragon)](https://www.npmjs.com/package/@openedx/paragon) ## Purpose @@ -30,13 +30,13 @@ Paragon components require React 16 or higher. To install Paragon into your proj In terminal: ``` -npm i --save @edx/paragon +npm i --save @openedx/paragon ``` In your React project: ``` -import { ComponentName } from '@edx/paragon'; +import { ComponentName } from '@openedx/paragon'; ``` #### SCSS Foundation @@ -47,7 +47,7 @@ Usage for Open edX and others: ``` // ... Any custom SCSS variables should be defined here -@import '~@edx/paragon/scss/core/core.scss'; +@import '~@openedx/paragon/scss/core/core.scss'; ``` Usage on with `@edx/brand`: @@ -57,7 +57,7 @@ Usage on with `@edx/brand`: ``` @import '~@edx/brand/paragon/fonts.scss'; @import '~@edx/brand/paragon/variables.scss'; -@import '~@edx/paragon/scss/core/core.scss'; +@import '~@openedx/paragon/scss/core/core.scss'; @import '~@edx/brand/paragon/overrides.scss'; ``` @@ -81,7 +81,7 @@ Due to Paragon's dependence on ``react-intl``, that means that your whole app ne ```javascript import { IntlProvider } from 'react-intl'; - import { messages as paragonMessages } from '@edx/paragon'; + import { messages as paragonMessages } from '@openedx/paragon'; ReactDOM.render( @@ -96,7 +96,7 @@ Note that if you are using ``@edx/frontend-platform``'s ``AppProvider`` componen ```javascript import { APP_READY, subscribe, initialize } from '@edx/frontend-platform'; import { AppProvider } from '@edx/frontend-platform/react'; - import { messages as paragonMessages } from '@edx/paragon'; + import { messages as paragonMessages } from '@openedx/paragon'; import App from './App'; // this is your app's i18n messages import appMessages from './i18n'; @@ -148,17 +148,17 @@ module.exports = { dist: The sub-directory of the source code where it puts its build artifact. Often "dist". */ localModules: [ - { moduleName: '@edx/paragon/scss/core', dir: '../src/paragon', dist: 'scss/core' }, - { moduleName: '@edx/paragon/icons', dir: '../src/paragon', dist: 'icons' }, + { moduleName: '@openedx/paragon/scss/core', dir: '../src/paragon', dist: 'scss/core' }, + { moduleName: '@openedx/paragon/icons', dir: '../src/paragon', dist: 'icons' }, // Note that using dist: 'dist' will require you to run 'npm build' in Paragon // to add local changes to the 'dist' directory, so that they can be picked up by the MFE. // To avoid doing that you can use dist: 'src' to get any local changes hot reloaded on save in the MFE. - { moduleName: '@edx/paragon', dir: '../src/paragon', dist: 'dist' }, + { moduleName: '@openedx/paragon', dir: '../src/paragon', dist: 'dist' }, ], }; ``` -Then, when importing Paragon's core SCSS in your MFE the import needs to begin with a tilde `~` so that path to your local Paragon repository gets resolved correctly: `@import "~@edx/paragon/scss/core";` +Then, when importing Paragon's core SCSS in your MFE the import needs to begin with a tilde `~` so that path to your local Paragon repository gets resolved correctly: `@import "~@openedx/paragon/scss/core";` #### Internationalization @@ -227,7 +227,8 @@ When developing a new component you should generally follow three rules: variant="primary" /> ) - + } + export default MyFunctionComponent; ``` @@ -475,4 +476,4 @@ The assigned maintainers for this component and other project details may be fou ## Reporting Security Issues Please do not report security issues in public. Please email security@openedx.org. -We tend to prioritize security issues which impact the published `@edx/paragon` NPM library more so than the [documentation website](https://paragon-openedx.netlify.app/) or example React application. +We tend to prioritize security issues which impact the published `@openedx/paragon` NPM library more so than the [documentation website](https://paragon-openedx.netlify.app/) or example React application. diff --git a/catalog-info.yml b/catalog-info.yml index ab870f2316..6c557d5329 100644 --- a/catalog-info.yml +++ b/catalog-info.yml @@ -22,7 +22,7 @@ metadata: - url: "https://github.com/openedx/paragon/releases" title: "GitHub Releases" icon: "Source" - - url: "https://www.npmjs.com/package/@edx/paragon" + - url: "https://www.npmjs.com/package/@openedx/paragon" title: "NPM" icon: "Terminal" annotations: diff --git a/example/module.config.js b/example/module.config.js index 1f8e78c756..7bdeee9db1 100644 --- a/example/module.config.js +++ b/example/module.config.js @@ -1,7 +1,7 @@ module.exports = { localModules: [ - { moduleName: '@edx/paragon/scss/core', dir: '..', dist: 'scss/core' }, - { moduleName: '@edx/paragon/icons', dir: '..', dist: 'icons' }, - { moduleName: '@edx/paragon', dir: '..', dist: 'src' }, + { moduleName: '@openedx/paragon/scss/core', dir: '..', dist: 'scss/core' }, + { moduleName: '@openedx/paragon/icons', dir: '..', dist: 'icons' }, + { moduleName: '@openedx/paragon', dir: '..', dist: 'src' }, ], }; diff --git a/example/src/MyComponent.jsx b/example/src/MyComponent.jsx index 2b91c20d77..80c434ce5c 100644 --- a/example/src/MyComponent.jsx +++ b/example/src/MyComponent.jsx @@ -1,6 +1,6 @@ import React, { useState } from 'react'; -import { Button, Form, Icon, Bubble, Skeleton } from '@edx/paragon'; // eslint-disable-line -import { FavoriteBorder } from '@edx/paragon/icons'; // eslint-disable-line +import { Button, Form, Icon, Bubble, Skeleton } from '@openedx/paragon'; // eslint-disable-line +import { FavoriteBorder } from '@openedx/paragon/icons'; // eslint-disable-line const MyComponent = () => { const [value, setValue] = useState(''); diff --git a/example/src/index.scss b/example/src/index.scss index 5dcd142eb2..e55bd47605 100644 --- a/example/src/index.scss +++ b/example/src/index.scss @@ -1,4 +1,4 @@ @import "@edx/brand/paragon/fonts"; @import "@edx/brand/paragon/variables"; -@import "~@edx/paragon/scss/core/core"; +@import "~@openedx/paragon/scss/core/core"; @import "@edx/brand/paragon/overrides"; diff --git a/package.json b/package.json index 74ff0d7cfd..aab407589c 100644 --- a/package.json +++ b/package.json @@ -173,7 +173,7 @@ "/component-generator/" ], "transformIgnorePatterns": [ - "/node_modules/(?!(@edx/paragon)/).*/" + "/node_modules/(?!(@openedx/paragon)/).*/" ] }, "husky": { diff --git a/src/Button/index.jsx b/src/Button/index.jsx index 7180ff3bfa..13c444e9fa 100644 --- a/src/Button/index.jsx +++ b/src/Button/index.jsx @@ -51,10 +51,10 @@ Button.propTypes = { * */ variant: PropTypes.string, /** An icon component to render. - * Example import of a Paragon icon component: `import { Check } from '@edx/paragon/icons';` */ + * Example import of a Paragon icon component: `import { Check } from '@openedx/paragon/icons';` */ iconBefore: PropTypes.oneOfType([PropTypes.elementType, PropTypes.node]), /** An icon component to render. - * Example import of a Paragon icon component: `import { Check } from '@edx/paragon/icons';` */ + * Example import of a Paragon icon component: `import { Check } from '@openedx/paragon/icons';` */ iconAfter: PropTypes.oneOfType([PropTypes.elementType, PropTypes.node]), }; diff --git a/src/Chip/index.tsx b/src/Chip/index.tsx index 2966e0c738..189053d5d0 100644 --- a/src/Chip/index.tsx +++ b/src/Chip/index.tsx @@ -86,7 +86,7 @@ Chip.propTypes = { * An icon component to render before the content. * Example import of a Paragon icon component: * - * `import { Check } from '@edx/paragon/icons';` + * `import { Check } from '@openedx/paragon/icons';` */ iconBefore: PropTypes.oneOfType([PropTypes.element, PropTypes.func]), /** A click handler for the `Chip` icon before. */ @@ -95,7 +95,7 @@ Chip.propTypes = { * An icon component to render before after the content. * Example import of a Paragon icon component: * - * `import { Check } from '@edx/paragon/icons';` + * `import { Check } from '@openedx/paragon/icons';` */ iconAfter: PropTypes.oneOfType([PropTypes.element, PropTypes.func]), /** A click handler for the `Chip` icon after. */ diff --git a/src/Icon/README.md b/src/Icon/README.md index 75177f386a..8d104f7dbc 100644 --- a/src/Icon/README.md +++ b/src/Icon/README.md @@ -11,13 +11,13 @@ devStatus: 'Done' notes: | --- -Displays an svg icon from `@edx/paragon/icons`. See [Icons Foundation Documentation](/foundations/icons) for a list of all available icons. +Displays an svg icon from `@openedx/paragon/icons`. See [Icons Foundation Documentation](/foundations/icons) for a list of all available icons. ## Basic Usage ```jsx live // Included in this live jsx scope: -// import { Add, AddCircle } from '@edx/paragon/icons'; +// import { Add, AddCircle } from '@openedx/paragon/icons'; ``` ### With HTML attributes @@ -26,7 +26,7 @@ HTML attributes can be passed to this component allowing for customization of th ```jsx live // Included in this live jsx scope: -// import { Add, AddCircle } from '@edx/paragon/icons'; +// import { Add, AddCircle } from '@openedx/paragon/icons';
diff --git a/src/Icon/index.jsx b/src/Icon/index.jsx index f30732e9b8..6f0a7a3cf3 100644 --- a/src/Icon/index.jsx +++ b/src/Icon/index.jsx @@ -72,7 +72,7 @@ function Icon({ Icon.propTypes = { /** * An icon component to render. - * Example import of a Paragon icon component: `import { Check } from '@edx/paragon/icons';` + * Example import of a Paragon icon component: `import { Check } from '@openedx/paragon/icons';` */ src: PropTypes.oneOfType([PropTypes.element, PropTypes.elementType]), /** HTML element attributes to pass through to the underlying svg element */ diff --git a/src/IconButton/index.jsx b/src/IconButton/index.jsx index aa3cdc158c..bf25709577 100644 --- a/src/IconButton/index.jsx +++ b/src/IconButton/index.jsx @@ -78,7 +78,7 @@ IconButton.propTypes = { * but is going to be deprecated soon, please use Paragon's icons instead. */ iconAs: PropTypes.elementType, /** An icon component to render. Example import of a Paragon icon component: - * `import { Check } from '@edx/paragon/dist/icon';` + * `import { Check } from '@openedx/paragon/dist/icon';` * */ src: PropTypes.oneOfType([PropTypes.element, PropTypes.elementType]), /** Alt text for your icon. For best practice, avoid using alt text to describe diff --git a/src/SearchField/index.jsx b/src/SearchField/index.jsx index d4a842c111..b3c34dc543 100644 --- a/src/SearchField/index.jsx +++ b/src/SearchField/index.jsx @@ -128,8 +128,8 @@ SearchField.propTypes = { * * ```jsx * { - * submit: import {Search} from '@edx/paragon/icons';, - * clear: import {Close} from '@edx/paragon/icons'. + * submit: import {Search} from '@openedx/paragon/icons';, + * clear: import {Close} from '@openedx/paragon/icons'. * } * ``` */ diff --git a/www/src/components/IconsTable.tsx b/www/src/components/IconsTable.tsx index cff6c7cbc5..a95113b73d 100644 --- a/www/src/components/IconsTable.tsx +++ b/www/src/components/IconsTable.tsx @@ -68,7 +68,7 @@ function IconsTable({ iconNames }) { const [data, setData] = useState({ iconsList: iconNames, rowsCount: ROWS_PER_WINDOW }); const [currentIcon, setCurrentIcon] = useState(iconNames[0]); const [showToast, setShowToast] = useState(false); - const currentIconImport = `import { ${currentIcon} } from '@edx/paragon/icons';`; + const currentIconImport = `import { ${currentIcon} } from '@openedx/paragon/icons';`; const { rowsCount, iconsList } = data; const columnsCount = useMemo(() => Math.floor(tableWidth / COLUMN_WIDTH), [tableWidth]); diff --git a/www/src/components/Menu.tsx b/www/src/components/Menu.tsx index 1c267a7259..1a72e0328b 100644 --- a/www/src/components/Menu.tsx +++ b/www/src/components/Menu.tsx @@ -306,14 +306,14 @@ function Menu() {
npm_versionIcon Component for more information. +Displays a brand SVG icon from `@openedx/paragon/icons`. See the Icon Component for more information. ```jsx live // Included in this live jsx scope: -// import { BsApple, BsSpotify } from '@edx/paragon/icons'; +// import { BsApple, BsSpotify } from '@openedx/paragon/icons'; ``` diff --git a/www/src/pages/foundations/icons.mdx b/www/src/pages/foundations/icons.mdx index a3ae4f3622..01b519b058 100644 --- a/www/src/pages/foundations/icons.mdx +++ b/www/src/pages/foundations/icons.mdx @@ -12,11 +12,11 @@ import * as IconComponents from '~paragon-icons'; ## Icons -Displays an svg icon from `@edx/paragon/icons`. See the Icon Component for more information. +Displays an svg icon from `@openedx/paragon/icons`. See the Icon Component for more information. ```jsx live // Included in this live jsx scope: -// import { Add, AddCircle } from '@edx/paragon/icons'; +// import { Add, AddCircle } from '@openedx/paragon/icons'; ``` diff --git a/www/src/pages/guides/getting-started.mdx b/www/src/pages/guides/getting-started.mdx index 719c29ebe0..2f6de7caaf 100644 --- a/www/src/pages/guides/getting-started.mdx +++ b/www/src/pages/guides/getting-started.mdx @@ -11,13 +11,13 @@ Paragon components require React 16 or higher. To install Paragon into your proj In terminal: ``` -npm i --save @edx/paragon +npm i --save @openedx/paragon ``` In your React project: ```jsx -import { ComponentName } from '@edx/paragon'; +import { ComponentName } from '@openedx/paragon'; ``` ## SCSS Foundation @@ -25,7 +25,7 @@ import { ComponentName } from '@edx/paragon'; Usage with no theme: ```scss -@import "@edx/paragon/scss/core/core.scss"; +@import "@openedx/paragon/scss/core/core.scss"; ``` Usage with a theme: @@ -36,7 +36,7 @@ When working with a theme the order of imports is important: if you need to over @import "@my-brand/fonts.scss"; @import "@my-brand/variables.scss"; // Import the Paragon core after setting brand-themed variables. -@import "@edx/paragon/scss/core/core.scss"; +@import "@openedx/paragon/scss/core/core.scss"; // Import brand overrides after the Paragon core. @import "@my-brand/overrides.scss"; ``` From 56987c2a50e85b966e5861610f65818d3f91465d Mon Sep 17 00:00:00 2001 From: PKulkoRaccoonGang Date: Thu, 28 Dec 2023 11:26:48 +0200 Subject: [PATCH 2/5] refactor: example refactoring --- example/src/MyComponent.jsx | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/example/src/MyComponent.jsx b/example/src/MyComponent.jsx index 80c434ce5c..96ddd2cb92 100644 --- a/example/src/MyComponent.jsx +++ b/example/src/MyComponent.jsx @@ -1,21 +1,18 @@ import React, { useState } from 'react'; -import { Button, Form, Icon, Bubble, Skeleton } from '@openedx/paragon'; // eslint-disable-line +import { Button, Form, Icon, Bubble, Stack, Container } from '@openedx/paragon'; // eslint-disable-line import { FavoriteBorder } from '@openedx/paragon/icons'; // eslint-disable-line const MyComponent = () => { const [value, setValue] = useState(''); const handleChange = (e) => setValue(e.target.value); - // eslint-disable-next-line no-alert - const handleClick = () => alert('Form is submitted!'); + const handleClick = () => alert('Form is submitted!'); // eslint-disable-line no-alert return ( -
-
-

- My Form -

+ + +

My Form

1 -
+
{
- -
+ ); }; From 36a6ca74caf82932833a13216bf8933f9912fd5b Mon Sep 17 00:00:00 2001 From: PKulkoRaccoonGang Date: Thu, 28 Dec 2023 14:38:31 +0200 Subject: [PATCH 3/5] refactor: corrected deps usage analyzer --- dependent-usage-analyzer/index.js | 33 +++++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/dependent-usage-analyzer/index.js b/dependent-usage-analyzer/index.js index a12084ddd4..76c62ad4cf 100644 --- a/dependent-usage-analyzer/index.js +++ b/dependent-usage-analyzer/index.js @@ -54,10 +54,24 @@ function getDependencyVersion(dir, options = {}) { peerDependencies } = JSON.parse(fs.readFileSync(`${dir}/${packageFilename}`, { encoding: 'utf-8' })); + const getVersion = (depsObjectName, org = '@edx') => { + switch (depsObjectName) { + case 'packages': + return packages && packages[`node_modules/${org}/paragon`]?.version; + case 'dependencies': + return dependencies && dependencies[`${org}/paragon`]?.version; + case 'peerDependencies': + return peerDependencies && peerDependencies[`${org}/paragon`]?.version; + default: + console.error(`Unexpected organization: ${org} or dependence object name: ${depsObjectName}`); + return undefined; + } + }; + // first handle lockfileVersion 3 that contains all dependencies data in 'packages' key - const packagesDependencyVersion = packages && packages['node_modules/@edx/paragon']?.version; - const directDependencyVersion = dependencies && dependencies['@edx/paragon']?.version; - const peerDependencyVersion = peerDependencies && peerDependencies['@edx/paragon']?.version; + const packagesDependencyVersion = getVersion('packages') || getVersion('packages', '@openedx'); + const directDependencyVersion = getVersion('dependencies') || getVersion('dependencies', '@openedx'); + const peerDependencyVersion = getVersion('peerDependencies') || getVersion('peerDependencies', '@openedx'); const resolvedVersion = packagesDependencyVersion || directDependencyVersion || peerDependencyVersion; if (resolvedVersion) { return resolvedVersion; @@ -117,8 +131,9 @@ function getComponentUsagesInFiles(files, rootDir) { walk.simple({ // ImportDeclaration nodes contains data about imports in the files ImportDeclaration(node) { + const allowedPackages = ['@edx/paragon', '@edx/paragon/icons', '@openedx/paragon', '@openedx/paragon/icons']; // Ignore direct imports for now - if (node.source.value === '@edx/paragon' || node.source.value === '@edx/paragon/icons') { + if (allowedPackages.includes(node.source.value)) { node.specifiers.forEach(addParagonImport); } }, @@ -209,8 +224,14 @@ function findProjectsToAnalyze(dir) { // If paragon isn't included in the package.json file then skip analyzing it const packageJSONFilesWithParagon = packageJSONFiles.filter(packageJSONFile => { const { dependencies, peerDependencies } = JSON.parse(fs.readFileSync(packageJSONFile, { encoding: 'utf-8' })); - const hasDirectDependency = dependencies && dependencies['@edx/paragon'] !== undefined; - const hasPeerDependency = peerDependencies && peerDependencies['@edx/paragon'] !== undefined + + const hasDependency = (depsObject, orgs) => { + return orgs.some(org => depsObject && depsObject[`${org}/paragon`] !== undefined); + }; + + const hasDirectDependency = hasDependency(dependencies, ['@edx', '@openedx']); + const hasPeerDependency = hasDependency(peerDependencies, ['@edx', '@openedx']); + return hasDirectDependency || hasPeerDependency; }); From 86fa2a9268ed89da47217c28e601243b16ce694e Mon Sep 17 00:00:00 2001 From: PKulkoRaccoonGang Date: Thu, 28 Dec 2023 15:35:54 +0200 Subject: [PATCH 4/5] refactor: defactoring deps usage analizer --- dependent-usage-analyzer/index.js | 240 +----------------- .../tools/analyzeProject.js | 26 ++ .../tools/findProjectsToAnalyze.js | 33 +++ dependent-usage-analyzer/tools/index.js | 4 + .../utils/getComponentUsagesInFiles.js | 115 +++++++++ .../utils/getDependencyVersion.js | 70 +++++ .../utils/getPackageInfo.js | 28 ++ .../utils/getProjectFiles.js | 22 ++ dependent-usage-analyzer/utils/index.js | 5 + 9 files changed, 305 insertions(+), 238 deletions(-) create mode 100644 dependent-usage-analyzer/tools/analyzeProject.js create mode 100644 dependent-usage-analyzer/tools/findProjectsToAnalyze.js create mode 100644 dependent-usage-analyzer/tools/index.js create mode 100644 dependent-usage-analyzer/utils/getComponentUsagesInFiles.js create mode 100644 dependent-usage-analyzer/utils/getDependencyVersion.js create mode 100644 dependent-usage-analyzer/utils/getPackageInfo.js create mode 100644 dependent-usage-analyzer/utils/getProjectFiles.js create mode 100644 dependent-usage-analyzer/utils/index.js diff --git a/dependent-usage-analyzer/index.js b/dependent-usage-analyzer/index.js index 76c62ad4cf..a2628dddfb 100644 --- a/dependent-usage-analyzer/index.js +++ b/dependent-usage-analyzer/index.js @@ -1,250 +1,14 @@ -/* eslint-disable no-param-reassign */ /* eslint-disable no-console */ -const parser = require('@babel/parser'); const fs = require('fs'); -const walk = require('babel-walk'); -const glob = require('glob'); const { Command } = require('commander'); -const path = require('path'); - -function getProjectFiles(dir) { - // Common project directories to ignore - const ignore = [ - `${dir}/**/node_modules/**`, - `${dir}/dist/**`, - `${dir}/public/**`, - `${dir}/coverage/**`, - `${dir}/**/*.config.*`, - ]; - // Gather all js and jsx source files - return glob.sync(`${dir}/**/*.{js,jsx}`, { ignore }); -} - -/** - * Attempts to extract the Paragon version for a given package directory. - * When no package-lock.json file is found in the given directory path or when - * no Paragon version can be retrieved, recursively traverse up the directory tree - * until we reach the top-level projects directory. This approach is necessary in - * order to account for potential projects that are technically monorepos containing - * multiple packages, where dependencies are hoisted to a parent directory. - * - * @param {string} dir Path to directory - * @param {object} options Optional options - * @param {string} options.projectsDir Path to top-level projects directory - * @returns String representing direct or peer Paragon dependency version - */ -function getDependencyVersion(dir, options = {}) { - // package-lock.json contains the actual Paragon version - // rather than a range in package.json. - const packageFilename = 'package-lock.json'; - const { projectsDir } = options; - if (dir === projectsDir) { - // At the top-level directory containing all projects; Paragon version not found. - return ""; - } - const parentDir = dir.split('/').slice(0, -1).join('/'); - if (!fs.existsSync(`${dir}/${packageFilename}`)) { - // No package-lock.json file exists, so try traversing up the tree until - // reaching the top-level ``projectsDir``. - return getDependencyVersion(parentDir, options); - } - const { - packages, - dependencies, - peerDependencies - } = JSON.parse(fs.readFileSync(`${dir}/${packageFilename}`, { encoding: 'utf-8' })); - - const getVersion = (depsObjectName, org = '@edx') => { - switch (depsObjectName) { - case 'packages': - return packages && packages[`node_modules/${org}/paragon`]?.version; - case 'dependencies': - return dependencies && dependencies[`${org}/paragon`]?.version; - case 'peerDependencies': - return peerDependencies && peerDependencies[`${org}/paragon`]?.version; - default: - console.error(`Unexpected organization: ${org} or dependence object name: ${depsObjectName}`); - return undefined; - } - }; - - // first handle lockfileVersion 3 that contains all dependencies data in 'packages' key - const packagesDependencyVersion = getVersion('packages') || getVersion('packages', '@openedx'); - const directDependencyVersion = getVersion('dependencies') || getVersion('dependencies', '@openedx'); - const peerDependencyVersion = getVersion('peerDependencies') || getVersion('peerDependencies', '@openedx'); - const resolvedVersion = packagesDependencyVersion || directDependencyVersion || peerDependencyVersion; - if (resolvedVersion) { - return resolvedVersion; - } - // No Paragon dependency exists, so try traversing up the tree until - // reaching the top-level ``projectsDir``. - return getDependencyVersion(parentDir, options) -} - -function getPackageInfo(dir, options = {}) { - const version = getDependencyVersion(dir, options); - try { - const { name, repository } = JSON.parse(fs.readFileSync(`${dir}/package.json`, { encoding: 'utf-8' })); - - return { - version, - name, - repository, - folderName: dir.split('/').pop(), - }; - } catch (e) { - console.error('Unable to read package.json in ', dir); - return {}; - } -} - -function getComponentUsagesInFiles(files, rootDir) { - // Save the file and line location of components for all files - return files.reduce((usagesAccumulator, filePath) => { - const sourceCode = fs.readFileSync(filePath, { encoding: 'utf-8' }); - let ast; - try { - ast = parser.parse(sourceCode, { sourceType: 'module', plugins: ['jsx', 'classProperties'] }); - } catch (e) { - console.error(`There was an error parsing a file into an abstract syntax tree. Skipping file: ${filePath}`); - return usagesAccumulator; - } - - // Track the local names of imported paragon components - const paragonImportsInFile = {}; - const addParagonImport = (specifierNode) => { - const { local, imported } = specifierNode; - paragonImportsInFile[local.name] = imported ? imported.name : local.name; - }; - - const addComponentUsage = (fullComponentName, startLocation) => { - if (!usagesAccumulator[fullComponentName]) { - usagesAccumulator[fullComponentName] = []; - } - usagesAccumulator[fullComponentName].push({ - filePath: filePath.substring(rootDir.length + 1), - ...startLocation, - }); - }; - - // Walk the abstract syntax tree of the file looking for paragon imports and component usages - walk.simple({ - // ImportDeclaration nodes contains data about imports in the files - ImportDeclaration(node) { - const allowedPackages = ['@edx/paragon', '@edx/paragon/icons', '@openedx/paragon', '@openedx/paragon/icons']; - // Ignore direct imports for now - if (allowedPackages.includes(node.source.value)) { - node.specifiers.forEach(addParagonImport); - } - }, - // JSXOpeningElement nodes contains data about each JSX element in the file. - // where Paragon component can be found through node.name.object and node.name.property.name for subcomponents - // Example: `Some alert` - JSXOpeningElement(node) { - const componentName = node.name.object ? node.name.object.name : node.name.name; - const isParagonComponent = componentName in paragonImportsInFile; - - if (isParagonComponent) { - const paragonName = paragonImportsInFile[componentName]; - const subComponentName = node.name.object ? node.name.property.name : null; - const fullComponentName = subComponentName ? `${paragonName}.${subComponentName}` : paragonName; - addComponentUsage(fullComponentName, node.loc.start); - } - }, - // JSXExpressionContainer nodes contains data about each JSX props expressions in the file. - // where Paragon component can be found through node.expression.name - // Example: `` - JSXExpressionContainer(node) { - const componentName = node.expression.name; - const isParagonComponent = paragonImportsInFile.hasOwnProperty(componentName); - - if (isParagonComponent) { - addComponentUsage(componentName, node.expression.loc.start); - } - }, - // AssignmentExpression contains data about each assignment in the file, - // where Paragon components, hooks and utils can be found through node.name.object - // Example: `const alert = Alert;` will go here - AssignmentExpression(node) { - const componentName = node.right.name; - const isParagonComponent = paragonImportsInFile.hasOwnProperty(componentName); - - if (isParagonComponent) { - addComponentUsage(componentName, node.loc.start); - } - }, - // CallExpression contains data about each function call in the file, - // where Paragon hooks and functions can be found usage through node.callee. - // Example: `const myVar = useWindowSize();` will go here - CallExpression(node) { - const componentName = node.callee.name; - const isParagonComponent = paragonImportsInFile.hasOwnProperty(componentName); - - if (isParagonComponent) { - addComponentUsage(componentName, node.loc.start); - } - }, - // MemberExpression contains data about complex expressions, - // where Paragon components, hooks and utils can be found node.object. - // Example: `const myVar = isVertical ? Button : ActionRow;` will go here - MemberExpression(node) { - const componentName = node.object.name; - const isParagonComponent = paragonImportsInFile.hasOwnProperty(componentName); - - if (isParagonComponent) { - addComponentUsage(componentName, node.loc.start); - } - } - })(ast); - - return usagesAccumulator; - }, {}); -} - -function analyzeProject(dir, options = {}) { - const packageInfo = getPackageInfo(dir, options); - const files = getProjectFiles(dir); - const usages = getComponentUsagesInFiles(files, dir); - - // Add Paragon version to each component usage - Object.keys(usages).forEach(componentName => { - usages[componentName].usages = usages[componentName].map(usage => ({ - ...usage, - version: packageInfo.version, - })); - }); - - return { ...packageInfo, usages }; -} - -function findProjectsToAnalyze(dir) { - // Find all directories containing a package.json file. - const packageJSONFiles = glob.sync(`${dir}/**/package.json`, { ignore: [`${dir}/**/node_modules/**`] }); - - // If paragon isn't included in the package.json file then skip analyzing it - const packageJSONFilesWithParagon = packageJSONFiles.filter(packageJSONFile => { - const { dependencies, peerDependencies } = JSON.parse(fs.readFileSync(packageJSONFile, { encoding: 'utf-8' })); - - const hasDependency = (depsObject, orgs) => { - return orgs.some(org => depsObject && depsObject[`${org}/paragon`] !== undefined); - }; - - const hasDirectDependency = hasDependency(dependencies, ['@edx', '@openedx']); - const hasPeerDependency = hasDependency(peerDependencies, ['@edx', '@openedx']); - - return hasDirectDependency || hasPeerDependency; - }); - - console.log(packageJSONFilesWithParagon) - - return packageJSONFilesWithParagon.map(packageJSONFile => path.dirname(packageJSONFile)); -} +const { findProjectsToAnalyze, analyzeProject } = require('./tools'); const program = new Command(); program .version('1.0.0') .arguments('') + .description('Analyze projects that include Paragon as a dependency.') .option('-o, --out ', 'output filepath') .action((projectsDir, options) => { const outputFilePath = options.out || 'out.json'; diff --git a/dependent-usage-analyzer/tools/analyzeProject.js b/dependent-usage-analyzer/tools/analyzeProject.js new file mode 100644 index 0000000000..f0e6fa605b --- /dev/null +++ b/dependent-usage-analyzer/tools/analyzeProject.js @@ -0,0 +1,26 @@ +const { getPackageInfo, getProjectFiles, getComponentUsagesInFiles } = require('../utils'); + +/** + * Analyzes a project by retrieving package information, project files, and component usages. + * @param {string} dir - The path to the project directory. + * @param {Object} [options={}] - Additional options for fetching package information. + * @returns {Object} - An object containing information about the analyzed project, + * including package details, component usages, and Paragon version associated with each usage. + */ +function analyzeProject(dir, options = {}) { + const packageInfo = getPackageInfo(dir, options); + const files = getProjectFiles(dir); + const usages = getComponentUsagesInFiles(files, dir); + + // Add Paragon version to each component usage + Object.keys(usages).forEach(componentName => { + usages[componentName].usages = usages[componentName].map(usage => ({ + ...usage, + version: packageInfo.version, + })); + }); + + return { ...packageInfo, usages }; +} + +module.exports = { analyzeProject }; diff --git a/dependent-usage-analyzer/tools/findProjectsToAnalyze.js b/dependent-usage-analyzer/tools/findProjectsToAnalyze.js new file mode 100644 index 0000000000..8e26b8b1d9 --- /dev/null +++ b/dependent-usage-analyzer/tools/findProjectsToAnalyze.js @@ -0,0 +1,33 @@ +const fs = require('fs'); +const path = require('path'); +const glob = require('glob'); + +/** + * Finds projects that include Paragon as a dependency. + * @param {string} dir - The path to the directory to search for projects. + * @returns {Array.} - An array of directory paths containing projects with Paragon as a dependency. + */ +function findProjectsToAnalyze(dir) { + // Find all directories containing a package.json file. + const packageJSONFiles = glob.sync(`${dir}/**/package.json`, { ignore: [`${dir}/**/node_modules/**`] }); + + // If paragon isn't included in the package.json file then skip analyzing it + const packageJSONFilesWithParagon = packageJSONFiles.filter(packageJSONFile => { + const { dependencies, peerDependencies } = JSON.parse(fs.readFileSync(packageJSONFile, { encoding: 'utf-8' })); + + const hasDependency = (depsObject, orgs) => { + return orgs.some(org => depsObject && depsObject[`${org}/paragon`] !== undefined); + }; + + const hasDirectDependency = hasDependency(dependencies, ['@edx', '@openedx']); + const hasPeerDependency = hasDependency(peerDependencies, ['@edx', '@openedx']); + + return hasDirectDependency || hasPeerDependency; + }); + + console.log(packageJSONFilesWithParagon) + + return packageJSONFilesWithParagon.map(packageJSONFile => path.dirname(packageJSONFile)); +} + +module.exports = { findProjectsToAnalyze }; diff --git a/dependent-usage-analyzer/tools/index.js b/dependent-usage-analyzer/tools/index.js new file mode 100644 index 0000000000..dd2f493185 --- /dev/null +++ b/dependent-usage-analyzer/tools/index.js @@ -0,0 +1,4 @@ +const { analyzeProject } = require('./analyzeProject'); +const { findProjectsToAnalyze } = require('./findProjectsToAnalyze'); + +module.exports = { analyzeProject, findProjectsToAnalyze }; diff --git a/dependent-usage-analyzer/utils/getComponentUsagesInFiles.js b/dependent-usage-analyzer/utils/getComponentUsagesInFiles.js new file mode 100644 index 0000000000..7233f3e3b1 --- /dev/null +++ b/dependent-usage-analyzer/utils/getComponentUsagesInFiles.js @@ -0,0 +1,115 @@ +const fs = require('fs'); +const walk = require('babel-walk'); +const parser = require('@babel/parser'); + +/** + * Retrieves information about Paragon component usages in project. + * @param {Array.} files - An array of file paths to analyze. + * @param {string} rootDir - The root directory of the project. + * @returns {Object} - An object containing component usage information, organized by component name. + */ +function getComponentUsagesInFiles(files, rootDir) { + // Save the file and line location of components for all files + return files.reduce((usagesAccumulator, filePath) => { + const sourceCode = fs.readFileSync(filePath, { encoding: 'utf-8' }); + let ast; + + try { + ast = parser.parse(sourceCode, { sourceType: 'module', plugins: ['jsx', 'classProperties'] }); + } catch (e) { + console.error(`There was an error parsing a file into an abstract syntax tree. Skipping file: ${filePath}`); + return usagesAccumulator; + } + + // Track the local names of imported paragon components + const paragonImportsInFile = {}; + const addParagonImport = (specifierNode) => { + const { local, imported } = specifierNode; + paragonImportsInFile[local.name] = imported ? imported.name : local.name; + }; + + const addComponentUsage = (fullComponentName, startLocation) => { + if (!usagesAccumulator[fullComponentName]) { + usagesAccumulator[fullComponentName] = []; + } + usagesAccumulator[fullComponentName].push({ + filePath: filePath.substring(rootDir.length + 1), + ...startLocation, + }); + }; + + // Walk the abstract syntax tree of the file looking for paragon imports and component usages + walk.simple({ + // ImportDeclaration nodes contains data about imports in the files + ImportDeclaration(node) { + const allowedPackages = ['@edx/paragon', '@edx/paragon/icons', '@openedx/paragon', '@openedx/paragon/icons']; + // Ignore direct imports for now + if (allowedPackages.includes(node.source.value)) { + node.specifiers.forEach(addParagonImport); + } + }, + // JSXOpeningElement nodes contains data about each JSX element in the file. + // where Paragon component can be found through node.name.object and node.name.property.name for subcomponents + // Example: `Some alert` + JSXOpeningElement(node) { + const componentName = node.name.object ? node.name.object.name : node.name.name; + const isParagonComponent = componentName in paragonImportsInFile; + + if (isParagonComponent) { + const paragonName = paragonImportsInFile[componentName]; + const subComponentName = node.name.object ? node.name.property.name : null; + const fullComponentName = subComponentName ? `${paragonName}.${subComponentName}` : paragonName; + addComponentUsage(fullComponentName, node.loc.start); + } + }, + // JSXExpressionContainer nodes contains data about each JSX props expressions in the file. + // where Paragon component can be found through node.expression.name + // Example: `` + JSXExpressionContainer(node) { + const componentName = node.expression.name; + const isParagonComponent = paragonImportsInFile.hasOwnProperty(componentName); + + if (isParagonComponent) { + addComponentUsage(componentName, node.expression.loc.start); + } + }, + // AssignmentExpression contains data about each assignment in the file, + // where Paragon components, hooks and utils can be found through node.name.object + // Example: `const alert = Alert;` will go here + AssignmentExpression(node) { + const componentName = node.right.name; + const isParagonComponent = paragonImportsInFile.hasOwnProperty(componentName); + + if (isParagonComponent) { + addComponentUsage(componentName, node.loc.start); + } + }, + // CallExpression contains data about each function call in the file, + // where Paragon hooks and functions can be found usage through node.callee. + // Example: `const myVar = useWindowSize();` will go here + CallExpression(node) { + const componentName = node.callee.name; + const isParagonComponent = paragonImportsInFile.hasOwnProperty(componentName); + + if (isParagonComponent) { + addComponentUsage(componentName, node.loc.start); + } + }, + // MemberExpression contains data about complex expressions, + // where Paragon components, hooks and utils can be found node.object. + // Example: `const myVar = isVertical ? Button : ActionRow;` will go here + MemberExpression(node) { + const componentName = node.object.name; + const isParagonComponent = paragonImportsInFile.hasOwnProperty(componentName); + + if (isParagonComponent) { + addComponentUsage(componentName, node.loc.start); + } + } + })(ast); + + return usagesAccumulator; + }, {}); +} + +module.exports = { getComponentUsagesInFiles }; diff --git a/dependent-usage-analyzer/utils/getDependencyVersion.js b/dependent-usage-analyzer/utils/getDependencyVersion.js new file mode 100644 index 0000000000..ae9d0daddf --- /dev/null +++ b/dependent-usage-analyzer/utils/getDependencyVersion.js @@ -0,0 +1,70 @@ +const fs = require('fs'); + +/** + * Attempts to extract the Paragon version for a given package directory. + * When no package-lock.json file is found in the given directory path or when + * no Paragon version can be retrieved, recursively traverse up the directory tree + * until we reach the top-level projects directory. This approach is necessary in + * order to account for potential projects that are technically monorepos containing + * multiple packages, where dependencies are hoisted to a parent directory. + * + * @param {string} dir Path to directory + * @param {object} options Optional options + * @param {string} options.projectsDir Path to top-level projects directory + * @returns String representing direct or peer Paragon dependency version + */ +function getDependencyVersion(dir, options = {}) { + // package-lock.json contains the actual Paragon version + // rather than a range in package.json. + const packageFilename = 'package-lock.json'; + const { projectsDir } = options; + + if (dir === projectsDir) { + // At the top-level directory containing all projects; Paragon version not found. + return ""; + } + + const parentDir = dir.split('/').slice(0, -1).join('/'); + + if (!fs.existsSync(`${dir}/${packageFilename}`)) { + // No package-lock.json file exists, so try traversing up the tree until + // reaching the top-level ``projectsDir``. + return getDependencyVersion(parentDir, options); + } + + const { + packages, + dependencies, + peerDependencies + } = JSON.parse(fs.readFileSync(`${dir}/${packageFilename}`, { encoding: 'utf-8' })); + + const getVersion = (depsObjectName, org = '@edx') => { + switch (depsObjectName) { + case 'packages': + return packages && packages[`node_modules/${org}/paragon`]?.version; + case 'dependencies': + return dependencies && dependencies[`${org}/paragon`]?.version; + case 'peerDependencies': + return peerDependencies && peerDependencies[`${org}/paragon`]?.version; + default: + console.error(`Unexpected organization: ${org} or dependence object name: ${depsObjectName}`); + return undefined; + } + }; + + // first handle lockfileVersion 3 that contains all dependencies data in 'packages' key + const packagesDependencyVersion = getVersion('packages') || getVersion('packages', '@openedx'); + const directDependencyVersion = getVersion('dependencies') || getVersion('dependencies', '@openedx'); + const peerDependencyVersion = getVersion('peerDependencies') || getVersion('peerDependencies', '@openedx'); + const resolvedVersion = packagesDependencyVersion || directDependencyVersion || peerDependencyVersion; + + if (resolvedVersion) { + return resolvedVersion; + } + + // No Paragon dependency exists, so try traversing up the tree until + // reaching the top-level ``projectsDir``. + return getDependencyVersion(parentDir, options) +} + +module.exports = { getDependencyVersion }; diff --git a/dependent-usage-analyzer/utils/getPackageInfo.js b/dependent-usage-analyzer/utils/getPackageInfo.js new file mode 100644 index 0000000000..72cb234382 --- /dev/null +++ b/dependent-usage-analyzer/utils/getPackageInfo.js @@ -0,0 +1,28 @@ +const fs = require('fs'); +const { getDependencyVersion } = require('./getDependencyVersion'); + +/** + * Retrieves package information from the package.json file in the specified directory. + * @param {string} dir - The path to the project directory. + * @param {Object} [options={}] - Additional options for fetching dependency version. + * @returns {Object} - An object containing package information, including version, name, repository, and folder name. + */ +function getPackageInfo(dir, options = {}) { + const version = getDependencyVersion(dir, options); + + try { + const { name, repository } = JSON.parse(fs.readFileSync(`${dir}/package.json`, { encoding: 'utf-8' })); + + return { + version, + name, + repository, + folderName: dir.split('/').pop(), + }; + } catch (e) { + console.error('Unable to read package.json in ', dir); + return {}; + } +} + +module.exports = { getPackageInfo }; diff --git a/dependent-usage-analyzer/utils/getProjectFiles.js b/dependent-usage-analyzer/utils/getProjectFiles.js new file mode 100644 index 0000000000..2101c271c8 --- /dev/null +++ b/dependent-usage-analyzer/utils/getProjectFiles.js @@ -0,0 +1,22 @@ +const glob = require('glob'); + +/** + * Retrieves an array of JavaScript and JSX source files within the specified directory. + * @param {string} dir - The path to the project directory. + * @returns {Array.} - An array of file paths to JavaScript and JSX source files. + */ +function getProjectFiles(dir) { + // Common project directories to ignore + const ignore = [ + `${dir}/**/node_modules/**`, + `${dir}/dist/**`, + `${dir}/public/**`, + `${dir}/coverage/**`, + `${dir}/**/*.config.*`, + ]; + + // Gather all js and jsx source files + return glob.sync(`${dir}/**/*.{js,jsx}`, { ignore }); +} + +module.exports = { getProjectFiles }; diff --git a/dependent-usage-analyzer/utils/index.js b/dependent-usage-analyzer/utils/index.js new file mode 100644 index 0000000000..2ad3b37730 --- /dev/null +++ b/dependent-usage-analyzer/utils/index.js @@ -0,0 +1,5 @@ +const { getProjectFiles } = require('./getProjectFiles'); +const { getPackageInfo } = require('./getPackageInfo'); +const { getComponentUsagesInFiles } = require('./getComponentUsagesInFiles'); + +module.exports = { getProjectFiles, getPackageInfo, getComponentUsagesInFiles }; From 9cc3611a168f47e199e7270fca8f6826954291b9 Mon Sep 17 00:00:00 2001 From: PKulkoRaccoonGang Date: Fri, 29 Dec 2023 16:25:39 +0200 Subject: [PATCH 5/5] refactor: removed dependent-usage-analyzer from eslintignore --- .eslintignore | 1 - dependent-usage-analyzer/index.js | 2 +- .../tools/analyzeProject.js | 22 +-- .../tools/findProjectsToAnalyze.js | 27 ++- .../utils/getComponentUsagesInFiles.js | 184 +++++++++--------- .../utils/getDependencyVersion.js | 87 +++++---- .../utils/getPackageInfo.js | 27 +-- .../utils/getProjectFiles.js | 20 +- 8 files changed, 186 insertions(+), 184 deletions(-) diff --git a/.eslintignore b/.eslintignore index a693875c8a..3f0d3c4ccc 100644 --- a/.eslintignore +++ b/.eslintignore @@ -3,7 +3,6 @@ dist/ node_modules/ www/ icons/ -dependent-usage-analyzer/ build-scss.js component-generator/ example/ diff --git a/dependent-usage-analyzer/index.js b/dependent-usage-analyzer/index.js index a2628dddfb..a9d89cbee2 100644 --- a/dependent-usage-analyzer/index.js +++ b/dependent-usage-analyzer/index.js @@ -18,7 +18,7 @@ program const analysis = { lastModified: Date.now(), projectUsages: analyzedProjects, - } + }; fs.writeFileSync(outputFilePath, JSON.stringify(analysis, null, 2)); console.log(`Analyzed ${projectDirectories.length} projects:`); console.log(analysis); diff --git a/dependent-usage-analyzer/tools/analyzeProject.js b/dependent-usage-analyzer/tools/analyzeProject.js index f0e6fa605b..eee942a9e0 100644 --- a/dependent-usage-analyzer/tools/analyzeProject.js +++ b/dependent-usage-analyzer/tools/analyzeProject.js @@ -8,19 +8,19 @@ const { getPackageInfo, getProjectFiles, getComponentUsagesInFiles } = require(' * including package details, component usages, and Paragon version associated with each usage. */ function analyzeProject(dir, options = {}) { - const packageInfo = getPackageInfo(dir, options); - const files = getProjectFiles(dir); - const usages = getComponentUsagesInFiles(files, dir); + const packageInfo = getPackageInfo(dir, options); + const files = getProjectFiles(dir); + const usages = getComponentUsagesInFiles(files, dir); - // Add Paragon version to each component usage - Object.keys(usages).forEach(componentName => { - usages[componentName].usages = usages[componentName].map(usage => ({ - ...usage, - version: packageInfo.version, - })); - }); + // Add Paragon version to each component usage + Object.keys(usages).forEach(componentName => { + usages[componentName].usages = usages[componentName].map(usage => ({ + ...usage, + version: packageInfo.version, + })); + }); - return { ...packageInfo, usages }; + return { ...packageInfo, usages }; } module.exports = { analyzeProject }; diff --git a/dependent-usage-analyzer/tools/findProjectsToAnalyze.js b/dependent-usage-analyzer/tools/findProjectsToAnalyze.js index 8e26b8b1d9..2c692f3b29 100644 --- a/dependent-usage-analyzer/tools/findProjectsToAnalyze.js +++ b/dependent-usage-analyzer/tools/findProjectsToAnalyze.js @@ -8,26 +8,25 @@ const glob = require('glob'); * @returns {Array.} - An array of directory paths containing projects with Paragon as a dependency. */ function findProjectsToAnalyze(dir) { - // Find all directories containing a package.json file. - const packageJSONFiles = glob.sync(`${dir}/**/package.json`, { ignore: [`${dir}/**/node_modules/**`] }); + // Find all directories containing a package.json file. + const packageJSONFiles = glob.sync(`${dir}/**/package.json`, { ignore: [`${dir}/**/node_modules/**`] }); - // If paragon isn't included in the package.json file then skip analyzing it - const packageJSONFilesWithParagon = packageJSONFiles.filter(packageJSONFile => { - const { dependencies, peerDependencies } = JSON.parse(fs.readFileSync(packageJSONFile, { encoding: 'utf-8' })); + // If paragon isn't included in the package.json file then skip analyzing it + const packageJSONFilesWithParagon = packageJSONFiles.filter(packageJSONFile => { + const { dependencies, peerDependencies } = JSON.parse(fs.readFileSync(packageJSONFile, { encoding: 'utf-8' })); - const hasDependency = (depsObject, orgs) => { - return orgs.some(org => depsObject && depsObject[`${org}/paragon`] !== undefined); - }; + const hasDependency = (depsObject, orgs) => orgs.some(org => depsObject && depsObject[`${org}/paragon`] !== undefined); - const hasDirectDependency = hasDependency(dependencies, ['@edx', '@openedx']); - const hasPeerDependency = hasDependency(peerDependencies, ['@edx', '@openedx']); + const hasDirectDependency = hasDependency(dependencies, ['@edx', '@openedx']); + const hasPeerDependency = hasDependency(peerDependencies, ['@edx', '@openedx']); - return hasDirectDependency || hasPeerDependency; - }); + return hasDirectDependency || hasPeerDependency; + }); - console.log(packageJSONFilesWithParagon) + // eslint-disable-next-line no-console + console.log(packageJSONFilesWithParagon); - return packageJSONFilesWithParagon.map(packageJSONFile => path.dirname(packageJSONFile)); + return packageJSONFilesWithParagon.map(packageJSONFile => path.dirname(packageJSONFile)); } module.exports = { findProjectsToAnalyze }; diff --git a/dependent-usage-analyzer/utils/getComponentUsagesInFiles.js b/dependent-usage-analyzer/utils/getComponentUsagesInFiles.js index 7233f3e3b1..60996c60ee 100644 --- a/dependent-usage-analyzer/utils/getComponentUsagesInFiles.js +++ b/dependent-usage-analyzer/utils/getComponentUsagesInFiles.js @@ -1,3 +1,4 @@ +/* eslint-disable no-prototype-builtins */ const fs = require('fs'); const walk = require('babel-walk'); const parser = require('@babel/parser'); @@ -9,107 +10,108 @@ const parser = require('@babel/parser'); * @returns {Object} - An object containing component usage information, organized by component name. */ function getComponentUsagesInFiles(files, rootDir) { - // Save the file and line location of components for all files - return files.reduce((usagesAccumulator, filePath) => { - const sourceCode = fs.readFileSync(filePath, { encoding: 'utf-8' }); - let ast; + // Save the file and line location of components for all files + return files.reduce((usagesAccumulator, filePath) => { + const sourceCode = fs.readFileSync(filePath, { encoding: 'utf-8' }); + let ast; - try { - ast = parser.parse(sourceCode, { sourceType: 'module', plugins: ['jsx', 'classProperties'] }); - } catch (e) { - console.error(`There was an error parsing a file into an abstract syntax tree. Skipping file: ${filePath}`); - return usagesAccumulator; - } + try { + ast = parser.parse(sourceCode, { sourceType: 'module', plugins: ['jsx', 'classProperties'] }); + } catch (e) { + // eslint-disable-next-line no-console + console.error(`There was an error parsing a file into an abstract syntax tree. Skipping file: ${filePath}`); + return usagesAccumulator; + } - // Track the local names of imported paragon components - const paragonImportsInFile = {}; - const addParagonImport = (specifierNode) => { - const { local, imported } = specifierNode; - paragonImportsInFile[local.name] = imported ? imported.name : local.name; - }; + // Track the local names of imported paragon components + const paragonImportsInFile = {}; + const addParagonImport = (specifierNode) => { + const { local, imported } = specifierNode; + paragonImportsInFile[local.name] = imported ? imported.name : local.name; + }; - const addComponentUsage = (fullComponentName, startLocation) => { - if (!usagesAccumulator[fullComponentName]) { - usagesAccumulator[fullComponentName] = []; - } - usagesAccumulator[fullComponentName].push({ - filePath: filePath.substring(rootDir.length + 1), - ...startLocation, - }); - }; + const addComponentUsage = (fullComponentName, startLocation) => { + if (!usagesAccumulator[fullComponentName]) { + usagesAccumulator[fullComponentName] = []; + } + usagesAccumulator[fullComponentName].push({ + filePath: filePath.substring(rootDir.length + 1), + ...startLocation, + }); + }; - // Walk the abstract syntax tree of the file looking for paragon imports and component usages - walk.simple({ - // ImportDeclaration nodes contains data about imports in the files - ImportDeclaration(node) { - const allowedPackages = ['@edx/paragon', '@edx/paragon/icons', '@openedx/paragon', '@openedx/paragon/icons']; - // Ignore direct imports for now - if (allowedPackages.includes(node.source.value)) { - node.specifiers.forEach(addParagonImport); - } - }, - // JSXOpeningElement nodes contains data about each JSX element in the file. - // where Paragon component can be found through node.name.object and node.name.property.name for subcomponents - // Example: `Some alert` - JSXOpeningElement(node) { - const componentName = node.name.object ? node.name.object.name : node.name.name; - const isParagonComponent = componentName in paragonImportsInFile; + // Walk the abstract syntax tree of the file looking for paragon imports and component usages + walk.simple({ + // ImportDeclaration nodes contains data about imports in the files + ImportDeclaration(node) { + const allowedPackages = ['@edx/paragon', '@edx/paragon/icons', '@openedx/paragon', '@openedx/paragon/icons']; + // Ignore direct imports for now + if (allowedPackages.includes(node.source.value)) { + node.specifiers.forEach(addParagonImport); + } + }, + // JSXOpeningElement nodes contains data about each JSX element in the file. + // where Paragon component can be found through node.name.object and node.name.property.name for subcomponents + // Example: `Some alert` + JSXOpeningElement(node) { + const componentName = node.name.object ? node.name.object.name : node.name.name; + const isParagonComponent = componentName in paragonImportsInFile; - if (isParagonComponent) { - const paragonName = paragonImportsInFile[componentName]; - const subComponentName = node.name.object ? node.name.property.name : null; - const fullComponentName = subComponentName ? `${paragonName}.${subComponentName}` : paragonName; - addComponentUsage(fullComponentName, node.loc.start); - } - }, - // JSXExpressionContainer nodes contains data about each JSX props expressions in the file. - // where Paragon component can be found through node.expression.name - // Example: `` - JSXExpressionContainer(node) { - const componentName = node.expression.name; - const isParagonComponent = paragonImportsInFile.hasOwnProperty(componentName); + if (isParagonComponent) { + const paragonName = paragonImportsInFile[componentName]; + const subComponentName = node.name.object ? node.name.property.name : null; + const fullComponentName = subComponentName ? `${paragonName}.${subComponentName}` : paragonName; + addComponentUsage(fullComponentName, node.loc.start); + } + }, + // JSXExpressionContainer nodes contains data about each JSX props expressions in the file. + // where Paragon component can be found through node.expression.name + // Example: `` + JSXExpressionContainer(node) { + const componentName = node.expression.name; + const isParagonComponent = paragonImportsInFile.hasOwnProperty(componentName); - if (isParagonComponent) { - addComponentUsage(componentName, node.expression.loc.start); - } - }, - // AssignmentExpression contains data about each assignment in the file, - // where Paragon components, hooks and utils can be found through node.name.object - // Example: `const alert = Alert;` will go here - AssignmentExpression(node) { - const componentName = node.right.name; - const isParagonComponent = paragonImportsInFile.hasOwnProperty(componentName); + if (isParagonComponent) { + addComponentUsage(componentName, node.expression.loc.start); + } + }, + // AssignmentExpression contains data about each assignment in the file, + // where Paragon components, hooks and utils can be found through node.name.object + // Example: `const alert = Alert;` will go here + AssignmentExpression(node) { + const componentName = node.right.name; + const isParagonComponent = paragonImportsInFile.hasOwnProperty(componentName); - if (isParagonComponent) { - addComponentUsage(componentName, node.loc.start); - } - }, - // CallExpression contains data about each function call in the file, - // where Paragon hooks and functions can be found usage through node.callee. - // Example: `const myVar = useWindowSize();` will go here - CallExpression(node) { - const componentName = node.callee.name; - const isParagonComponent = paragonImportsInFile.hasOwnProperty(componentName); + if (isParagonComponent) { + addComponentUsage(componentName, node.loc.start); + } + }, + // CallExpression contains data about each function call in the file, + // where Paragon hooks and functions can be found usage through node.callee. + // Example: `const myVar = useWindowSize();` will go here + CallExpression(node) { + const componentName = node.callee.name; + const isParagonComponent = paragonImportsInFile.hasOwnProperty(componentName); - if (isParagonComponent) { - addComponentUsage(componentName, node.loc.start); - } - }, - // MemberExpression contains data about complex expressions, - // where Paragon components, hooks and utils can be found node.object. - // Example: `const myVar = isVertical ? Button : ActionRow;` will go here - MemberExpression(node) { - const componentName = node.object.name; - const isParagonComponent = paragonImportsInFile.hasOwnProperty(componentName); + if (isParagonComponent) { + addComponentUsage(componentName, node.loc.start); + } + }, + // MemberExpression contains data about complex expressions, + // where Paragon components, hooks and utils can be found node.object. + // Example: `const myVar = isVertical ? Button : ActionRow;` will go here + MemberExpression(node) { + const componentName = node.object.name; + const isParagonComponent = paragonImportsInFile.hasOwnProperty(componentName); - if (isParagonComponent) { - addComponentUsage(componentName, node.loc.start); - } - } - })(ast); + if (isParagonComponent) { + addComponentUsage(componentName, node.loc.start); + } + }, + })(ast); - return usagesAccumulator; - }, {}); + return usagesAccumulator; + }, {}); } module.exports = { getComponentUsagesInFiles }; diff --git a/dependent-usage-analyzer/utils/getDependencyVersion.js b/dependent-usage-analyzer/utils/getDependencyVersion.js index ae9d0daddf..a83d4ea898 100644 --- a/dependent-usage-analyzer/utils/getDependencyVersion.js +++ b/dependent-usage-analyzer/utils/getDependencyVersion.js @@ -14,57 +14,58 @@ const fs = require('fs'); * @returns String representing direct or peer Paragon dependency version */ function getDependencyVersion(dir, options = {}) { - // package-lock.json contains the actual Paragon version - // rather than a range in package.json. - const packageFilename = 'package-lock.json'; - const { projectsDir } = options; + // package-lock.json contains the actual Paragon version + // rather than a range in package.json. + const packageFilename = 'package-lock.json'; + const { projectsDir } = options; - if (dir === projectsDir) { - // At the top-level directory containing all projects; Paragon version not found. - return ""; - } + if (dir === projectsDir) { + // At the top-level directory containing all projects; Paragon version not found. + return ''; + } - const parentDir = dir.split('/').slice(0, -1).join('/'); + const parentDir = dir.split('/').slice(0, -1).join('/'); - if (!fs.existsSync(`${dir}/${packageFilename}`)) { - // No package-lock.json file exists, so try traversing up the tree until - // reaching the top-level ``projectsDir``. - return getDependencyVersion(parentDir, options); - } + if (!fs.existsSync(`${dir}/${packageFilename}`)) { + // No package-lock.json file exists, so try traversing up the tree until + // reaching the top-level ``projectsDir``. + return getDependencyVersion(parentDir, options); + } - const { - packages, - dependencies, - peerDependencies - } = JSON.parse(fs.readFileSync(`${dir}/${packageFilename}`, { encoding: 'utf-8' })); + const { + packages, + dependencies, + peerDependencies, + } = JSON.parse(fs.readFileSync(`${dir}/${packageFilename}`, { encoding: 'utf-8' })); - const getVersion = (depsObjectName, org = '@edx') => { - switch (depsObjectName) { - case 'packages': - return packages && packages[`node_modules/${org}/paragon`]?.version; - case 'dependencies': - return dependencies && dependencies[`${org}/paragon`]?.version; - case 'peerDependencies': - return peerDependencies && peerDependencies[`${org}/paragon`]?.version; - default: - console.error(`Unexpected organization: ${org} or dependence object name: ${depsObjectName}`); - return undefined; - } - }; + const getVersion = (depsObjectName, org = '@edx') => { + switch (depsObjectName) { + case 'packages': + return packages && packages[`node_modules/${org}/paragon`]?.version; + case 'dependencies': + return dependencies && dependencies[`${org}/paragon`]?.version; + case 'peerDependencies': + return peerDependencies && peerDependencies[`${org}/paragon`]?.version; + default: + // eslint-disable-next-line no-console + console.error(`Unexpected organization: ${org} or dependence object name: ${depsObjectName}`); + return undefined; + } + }; - // first handle lockfileVersion 3 that contains all dependencies data in 'packages' key - const packagesDependencyVersion = getVersion('packages') || getVersion('packages', '@openedx'); - const directDependencyVersion = getVersion('dependencies') || getVersion('dependencies', '@openedx'); - const peerDependencyVersion = getVersion('peerDependencies') || getVersion('peerDependencies', '@openedx'); - const resolvedVersion = packagesDependencyVersion || directDependencyVersion || peerDependencyVersion; + // first handle lockfileVersion 3 that contains all dependencies data in 'packages' key + const packagesDependencyVersion = getVersion('packages') || getVersion('packages', '@openedx'); + const directDependencyVersion = getVersion('dependencies') || getVersion('dependencies', '@openedx'); + const peerDependencyVersion = getVersion('peerDependencies') || getVersion('peerDependencies', '@openedx'); + const resolvedVersion = packagesDependencyVersion || directDependencyVersion || peerDependencyVersion; - if (resolvedVersion) { - return resolvedVersion; - } + if (resolvedVersion) { + return resolvedVersion; + } - // No Paragon dependency exists, so try traversing up the tree until - // reaching the top-level ``projectsDir``. - return getDependencyVersion(parentDir, options) + // No Paragon dependency exists, so try traversing up the tree until + // reaching the top-level ``projectsDir``. + return getDependencyVersion(parentDir, options); } module.exports = { getDependencyVersion }; diff --git a/dependent-usage-analyzer/utils/getPackageInfo.js b/dependent-usage-analyzer/utils/getPackageInfo.js index 72cb234382..d63adc5958 100644 --- a/dependent-usage-analyzer/utils/getPackageInfo.js +++ b/dependent-usage-analyzer/utils/getPackageInfo.js @@ -8,21 +8,22 @@ const { getDependencyVersion } = require('./getDependencyVersion'); * @returns {Object} - An object containing package information, including version, name, repository, and folder name. */ function getPackageInfo(dir, options = {}) { - const version = getDependencyVersion(dir, options); + const version = getDependencyVersion(dir, options); - try { - const { name, repository } = JSON.parse(fs.readFileSync(`${dir}/package.json`, { encoding: 'utf-8' })); + try { + const { name, repository } = JSON.parse(fs.readFileSync(`${dir}/package.json`, { encoding: 'utf-8' })); - return { - version, - name, - repository, - folderName: dir.split('/').pop(), - }; - } catch (e) { - console.error('Unable to read package.json in ', dir); - return {}; - } + return { + version, + name, + repository, + folderName: dir.split('/').pop(), + }; + } catch (e) { + // eslint-disable-next-line no-console + console.error('Unable to read package.json in ', dir); + return {}; + } } module.exports = { getPackageInfo }; diff --git a/dependent-usage-analyzer/utils/getProjectFiles.js b/dependent-usage-analyzer/utils/getProjectFiles.js index 2101c271c8..731f0a47b8 100644 --- a/dependent-usage-analyzer/utils/getProjectFiles.js +++ b/dependent-usage-analyzer/utils/getProjectFiles.js @@ -6,17 +6,17 @@ const glob = require('glob'); * @returns {Array.} - An array of file paths to JavaScript and JSX source files. */ function getProjectFiles(dir) { - // Common project directories to ignore - const ignore = [ - `${dir}/**/node_modules/**`, - `${dir}/dist/**`, - `${dir}/public/**`, - `${dir}/coverage/**`, - `${dir}/**/*.config.*`, - ]; + // Common project directories to ignore + const ignore = [ + `${dir}/**/node_modules/**`, + `${dir}/dist/**`, + `${dir}/public/**`, + `${dir}/coverage/**`, + `${dir}/**/*.config.*`, + ]; - // Gather all js and jsx source files - return glob.sync(`${dir}/**/*.{js,jsx}`, { ignore }); + // Gather all js and jsx source files + return glob.sync(`${dir}/**/*.{js,jsx}`, { ignore }); } module.exports = { getProjectFiles };