Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BD-46] refactor: changed Paragon NPM package name and source #2979

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ dist/
node_modules/
www/
icons/
dependent-usage-analyzer/
build-scss.js
component-generator/
example/
2 changes: 1 addition & 1 deletion .github/pull_request_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
29 changes: 15 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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
Expand All @@ -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`:
Expand All @@ -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';
```

Expand All @@ -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(
<IntlProvider locale={usersLocale} messages={paragonMessages[usersLocale]}>
Expand All @@ -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';
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -227,7 +227,8 @@ When developing a new component you should generally follow three rules:
variant="primary"
/>
)

}

export default MyFunctionComponent;
```

Expand Down Expand Up @@ -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.
2 changes: 1 addition & 1 deletion catalog-info.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
221 changes: 3 additions & 218 deletions dependent-usage-analyzer/index.js
Original file line number Diff line number Diff line change
@@ -1,229 +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' }));

// 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 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) {
// Ignore direct imports for now
if (node.source.value === '@edx/paragon' || node.source.value === '@edx/paragon/icons') {
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: `<Alert variant="danger">Some alert</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: `<Icon src={Add} />`
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 hasDirectDependency = dependencies && dependencies['@edx/paragon'] !== undefined;
const hasPeerDependency = peerDependencies && peerDependencies['@edx/paragon'] !== undefined
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('<projectsDir>')
.description('Analyze projects that include Paragon as a dependency.')
.option('-o, --out <outFilePath>', 'output filepath')
.action((projectsDir, options) => {
const outputFilePath = options.out || 'out.json';
Expand All @@ -233,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);
Expand Down
26 changes: 26 additions & 0 deletions dependent-usage-analyzer/tools/analyzeProject.js
Original file line number Diff line number Diff line change
@@ -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 };
Loading