diff --git a/.github/workflows/mrc-usage-report.yml b/.github/workflows/mrc-usage-report.yml new file mode 100644 index 000000000..b45e6528c --- /dev/null +++ b/.github/workflows/mrc-usage-report.yml @@ -0,0 +1,253 @@ +name: MRC Usage Report Generation + +on: + schedule: + - cron: "0 8 * * *" # Daily at 8 AM UTC + workflow_dispatch: # Manual trigger + push: + branches: + - iphadte/DATAGO-103052 # Trigger on pushes to the main branch + +jobs: + generate-and-publish-report: + runs-on: ubuntu-latest + permissions: + contents: write # For checking out repositories + pages: write # For publishing to GitHub Pages + id-token: write # For OIDC authentication with GitHub Pages + + steps: + - name: Checkout maas-react-components (current repo) + uses: actions/checkout@v4 + with: + ref: feature/mrc-usage-report-data + path: maas-react-components-repo + + - name: Clean up old report data + run: rm -rf maas-react-components-repo/mrc-usage-report-data + + - name: Print directory structure (maas-react-components) + run: | + echo "==== DIRECTORY STRUCTURE INFORMATION ====" + echo "Current working directory: $(pwd)" + echo "maas-react-components-repo location: $(pwd)/maas-react-components-repo" + ls -la maas-react-components-repo + + - name: Checkout maas-ui + uses: actions/checkout@v4 + with: + repository: SolaceDev/maas-ui + ref: sthomas/mrc-usage-report + path: maas-ui-repo + token: ${{ secrets.PACKAGES_ADMIN_TOKEN }} + + - name: Print directory structure (maas-ui) + run: | + echo "==== MAAS-UI DIRECTORY STRUCTURE ====" + echo "maas-ui-repo location: $(pwd)/maas-ui-repo" + ls -la maas-ui-repo + echo "maas-ui-repo/tools location: $(pwd)/maas-ui-repo/tools" + ls -la maas-ui-repo/tools || echo "tools directory not found" + echo "maas-ui-repo/tools/mrc-usage-report location: $(pwd)/maas-ui-repo/tools/mrc-usage-report" + ls -la maas-ui-repo/tools/mrc-usage-report || echo "mrc-usage-report directory not found" + + - name: Checkout maas-ops-ui + uses: actions/checkout@v4 + with: + repository: SolaceDev/maas-ops-ui + ref: iphadte/DATAGO-103044 + path: maas-ops-ui-repo + token: ${{ secrets.PACKAGES_ADMIN_TOKEN }} + + - name: Print directory structure (maas-ops-ui) + run: | + echo "==== MAAS-OPS-UI DIRECTORY STRUCTURE ====" + echo "maas-ops-ui-repo location: $(pwd)/maas-ops-ui-repo" + ls -la maas-ops-ui-repo + echo "maas-ops-ui-repo/tools location: $(pwd)/maas-ops-ui-repo/tools" + ls -la maas-ops-ui-repo/tools || echo "tools directory not found" + echo "maas-ops-ui-repo/tools/mrc-usage-report location: $(pwd)/maas-ops-ui-repo/tools/mrc-usage-report" + ls -la maas-ops-ui-repo/tools/mrc-usage-report || echo "mrc-usage-report directory not found" + echo "maas-ops-ui-repo/tools/mrc-report-merger location: $(pwd)/maas-ops-ui-repo/tools/mrc-report-merger" + ls -la maas-ops-ui-repo/tools/mrc-report-merger || echo "mrc-report-merger directory not found" + echo "maas-ops-ui-repo/tools/json-splitter location: $(pwd)/maas-ops-ui-repo/tools/json-splitter" + ls -la maas-ops-ui-repo/tools/json-splitter || echo "json-splitter directory not found" + + - name: Checkout broker manager + uses: actions/checkout@v4 + with: + repository: SolaceDev/broker-manager + ref: mrc-usage-report + path: broker-manager-repo + token: ${{ secrets.PACKAGES_ADMIN_TOKEN }} + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: "22" + + - name: Step 1. Generate maas-ui report + run: | + echo "==== STEP 1: GENERATE MAAS-UI REPORT ====" + echo "Current working directory: $(pwd)" + ls -la + echo "Checking for package.json:" + cat package.json || echo "package.json not found" + echo "Creating reports directory if it doesn't exist..." + mkdir -p reports + echo "Installing dependencies..." + npm install + npm run build + + + echo "Generating JSON report..." + npm start -- -b ../../../maas-ui-repo -r ../maas-react-components-repo -f json + + echo "Generating HTML report..." + npm start -- -b ../../../maas-ui-repo -r ../maas-react-components-repo -f html + + working-directory: maas-react-components-repo/tools/mrc-usage-report-maas-ui + + - name: Step 2. Generate maas-ops-ui report + run: | + echo "==== STEP 2: GENERATE MAAS-OPS-UI REPORTS ====" + echo "Current working directory: $(pwd)" + pwd + ls + echo "Checking for package.json:" + cat package.json || echo "package.json not found" + echo "Creating reports directory if it doesn't exist..." + mkdir -p reports + echo "Installing dependencies..." + npm install @types/babel__traverse + npm run build + echo "Checking for dist directory after npm install:" + ls -la dist || echo "dist directory not found after npm install" + + echo "Generating JSON report..." + npm start -- -b ../../../maas-ops-ui-repo -r ../maas-react-components-repo -f json + + echo "Generating HTML report..." + npm start -- -b ../../../maas-ops-ui-repo -r ../maas-react-components-repo -f html + + working-directory: maas-react-components-repo/tools/mrc-usage-report-maas-ops-ui + + - name: Step 3. Generate broker report + run: | + echo "==== STEP 3: GENERATE BROKER REPORT ====" + echo "Current working directory: $(pwd)" + mkdir -p reports + npm install + npm run build + + echo "Generating JSON report..." + npm start -- -b ../../../broker-manager-repo -r ../maas-react-components-repo -f json + + echo "Generating HTML report..." + npm start -- -b ../../../broker-manager-repo -r ../maas-react-components-repo -f html + + working-directory: maas-react-components-repo/tools/mrc-usage-report-broker-manager + + - name: Step 4. Merge reports + run: | + echo "==== STEP 4: MERGE REPORTS ====" + echo "Current working directory: $(pwd)" + ls -la + + echo "Changing permissions for report files..." + chmod -R 777 ../../../maas-react-components-repo/tools/mrc-usage-report-maas-ui/reports/ + chmod -R 777 ../../../maas-react-components-repo/tools/mrc-usage-report-maas-ops-ui/reports/ + chmod -R 777 ../../../maas-react-components-repo/tools/mrc-usage-report-broker-manager/reports/ + + + echo "Checking for input files:" + REPORTS_TO_MERGE="" + MAAS_OPS_UI_REPORT_PATH="../../../maas-react-components-repo/tools/mrc-usage-report-maas-ops-ui/reports/mrc-maas-ops-ui-usage-report.json" + MAAS_UI_REPORT_PATH="../../../maas-react-components-repo/tools/mrc-usage-report-maas-ui/reports/mrc-maas-ui-usage-report.json" + BROKER_MANAGER_REPORT_PATH="../../../maas-react-components-repo/tools/mrc-usage-report-broker-manager/reports/mrc-broker-manager-usage-report.json" + + if [ -f "$MAAS_OPS_UI_REPORT_PATH" ]; then + REPORTS_TO_MERGE="$REPORTS_TO_MERGE $MAAS_OPS_UI_REPORT_PATH" + else + echo "$MAAS_OPS_UI_REPORT_PATH not found." + fi + + if [ -f "$MAAS_UI_REPORT_PATH" ]; then + REPORTS_TO_MERGE="$REPORTS_TO_MERGE $MAAS_UI_REPORT_PATH" + else + echo "$MAAS_UI_REPORT_PATH not found." + fi + + if [ -f "$BROKER_MANAGER_REPORT_PATH" ]; then + REPORTS_TO_MERGE="$REPORTS_TO_MERGE $BROKER_MANAGER_REPORT_PATH" + else + echo "$BROKER_MANAGER_REPORT_PATH not found." + fi + + echo "Installing dependencies..." + npm install + + if [ -n "$REPORTS_TO_MERGE" ]; then + mkdir -p merged-reports + echo "Running npm start with parameters: $REPORTS_TO_MERGE" + npm start -- --output-json merged-reports/merged-mrc-usage-report.json --output-html merged-reports/merged-mrc-usage-report.html $REPORTS_TO_MERGE + chmod 777 merged-reports/merged-mrc-usage-report.json + chmod 777 merged-reports/merged-mrc-usage-report.html + else + echo "No reports found to merge." + fi + + working-directory: maas-react-components-repo/tools/mrc-usage-report-merger + + - name: Step 5. Split merged JSON + run: | + echo "==== STEP 5: SPLIT MERGED JSON ====" + echo "Current working directory: $(pwd)" + ls -la + echo "Checking for input file:" + ls -la ../../../maas-react-components-repo/tools/mrc-usage-report-merger/merged-reports/merged-mrc-usage-report.json || echo "merged-report.json not found" + echo "Creating reports directory if it doesn't exist..." + echo "Installing dependencies..." + npm install + + npm install + npm run build + + echo "Running npm start with parameters..." + npm start -- -i ../../../maas-react-components-repo/tools/mrc-usage-report-merger/merged-reports/merged-mrc-usage-report.json -o ../../../maas-react-components-repo/mrc-usage-report-data + + echo "Listing generated files:" + find . -type f -name "*.json" | sort + + echo "Contents of first split file (if any):" + find . -type f -name "*.json" | sort | head -n 1 | xargs cat || echo "No split files found" + + working-directory: maas-react-components-repo/tools/mrc-usage-report-json-splitter + + - name: Step 6. Upload merged reports + uses: actions/upload-artifact@v4 + with: + name: merged-reports + path: | + maas-react-components-repo/tools/mrc-usage-report-merger/merged-reports/ + maas-react-components-repo/mrc-usage-report-data + + - name: Step 7. Commit and push report data + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + cd maas-react-components-repo + + git config --global user.name "github-actions[bot]" + git config --global user.email "github-actions[bot]@users.noreply.github.com" + + git add mrc-usage-report-data/ + + if git diff --staged --quiet; then + echo "No changes to commit." + else + set -e + COMMIT_MESSAGE="feat: add mrc usage report data for $(date +'%Y-%m-%d %H:%M')" + git commit --no-verify -m "$COMMIT_MESSAGE" + git push + fi diff --git a/.gitignore b/.gitignore index 84eac835e..f0171e75f 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,9 @@ storybook/coverage storybook/.nyc_output *.iml .DS_Store + +tools/mrc-usage-report-json-splitter/output +tools/mrc-usage-report-broker-manager/reports +tools/mrc-usage-report-maas-ops-ui/reports +tools/mrc-usage-report-maas-ui/reports +tools/mrc-usage-report-merger/merged-reports \ No newline at end of file diff --git a/src/components/form/SolaceRadio.tsx b/src/components/form/SolaceRadio.tsx index 021f85245..b02b47ccc 100644 --- a/src/components/form/SolaceRadio.tsx +++ b/src/components/form/SolaceRadio.tsx @@ -2,7 +2,6 @@ import { Box, FormLabel, InputLabel, Radio, useRadioGroup, useTheme } from "@mui import React, { useEffect, useState } from "react"; import SolaceComponentProps from "../SolaceComponentProps"; import SolaceHTMLAttributeProps from "../SolaceHTMLAttributesProps"; -import { RestingRadioIcon, SelectedRadioIcon } from "../../resources/icons/RadioIcons"; import clsx from "clsx"; export interface SolaceRadioChangeEvent { name: string; @@ -143,8 +142,6 @@ function SolaceRadio({ id={`${id}-radio`} name={name} value={value} - icon={RestingRadioIcon} - checkedIcon={SelectedRadioIcon} inputProps={ { "aria-labelledby": label ? `${id}-label` : "", diff --git a/src/resources/theme.ts b/src/resources/theme.ts index 8a5386b0e..73c0c9756 100644 --- a/src/resources/theme.ts +++ b/src/resources/theme.ts @@ -905,13 +905,60 @@ const getThemeOptions = (themeName: SupportedThemes) => { ".MuiFormHelperText-root": { marginLeft: "0" }, - ".MuiSvgIcon-root .SolaceRadioContainer": { - fill: themeMapping.palette.background.w10, - stroke: themeMapping.palette.secondary.w40 - }, - "&:hover": { - ".MuiSvgIcon-root .SolaceRadioContainer": { - stroke: themeMapping.palette.deprecated.secondary.wMain + "&.MuiRadio-root": { + // Enabled state - Outer circle + color: themeMapping.palette.secondary.w40, // Stroke (border) + "& .MuiSvgIcon-root:first-of-type circle": { + fill: themeMapping.palette.background.w10, // Fill (background) + strokeWidth: "1px" // Set border width to 1px + }, + + // Hover state - Outer circle + "&:hover": { + color: themeMapping.palette.secondary.wMain // Stroke (border) on hover + }, + + // Read-only state - must come before checked state to establish base styles + "&.readOnly": { + color: themeMapping.palette.secondary.w40, // Outer circle stroke + "& .MuiSvgIcon-root:first-of-type circle": { + fill: themeMapping.palette.background.w20 // Outer circle fill + } + }, + + // Checked state - Inner circle (indicator) + "&.Mui-checked": { + // Keep outer circle styling + color: themeMapping.palette.secondary.w40, + + // Inner circle styling for enabled/hover + "& .MuiSvgIcon-root:last-of-type": { + color: themeMapping.palette.accent.n2.wMain // Indicator color + }, + + // Hover when checked - outer circle + "&:hover": { + color: themeMapping.palette.secondary.wMain + }, + + // Read-only checked - ensure inner circle is secondary.wMain + // This must be inside the checked state to override the default checked color + "&.readOnly .MuiSvgIcon-root:last-of-type": { + color: `${themeMapping.palette.secondary.wMain} !important` // Force grey indicator for read-only + } + }, + + // Disabled state + "&.Mui-disabled": { + color: themeMapping.palette.secondary.w20, // Outer circle stroke + "& .MuiSvgIcon-root:first-of-type circle": { + fill: themeMapping.palette.background.w10 // Outer circle fill + }, + + // Disabled and checked + "&.Mui-checked .MuiSvgIcon-root:last-of-type": { + color: themeMapping.palette.accent.n2.w30 // Indicator color for disabled + } } }, "&.Mui-focusVisible": { @@ -919,28 +966,6 @@ const getThemeOptions = (themeName: SupportedThemes) => { outlineOffset: "-1px", borderRadius: "50%" }, - "&.Mui-checked": { - ".SolaceRadioSelection": { - fill: themeMapping.palette.deprecated.accent.n2.wMain - } - }, - "&.Mui-disabled .MuiSvgIcon-root": { - ".SolaceRadioContainer": { - stroke: themeMapping.palette.secondary.w20 - }, - ".SolaceRadioSelection": { - fill: themeMapping.palette.accent.n2.w30 - } - }, - "&.readOnly .MuiSvgIcon-root": { - ".SolaceRadioContainer": { - fill: themeMapping.palette.background.w20, - stroke: themeMapping.palette.secondary.w40 - }, - ".SolaceRadioSelection": { - fill: themeMapping.palette.deprecated.secondary.wMain - } - }, // Style added to overide Aurelia custom styling, to be removed once Aurelia code is phased out. input: { left: "auto !important" diff --git a/tools/mrc-usage-report-broker-manager/README.md b/tools/mrc-usage-report-broker-manager/README.md new file mode 100644 index 000000000..28b562f28 --- /dev/null +++ b/tools/mrc-usage-report-broker-manager/README.md @@ -0,0 +1,212 @@ +# MRC Usage Report + +A tool to analyze and report on the usage of MRC (maas-react-components) components across different micro-frontends. + +## Features + +- Scans TypeScript/JavaScript files for MRC component usage +- Analyzes how components are used (props, customization, etc.) +- Identifies unused components (globally and per MFE) +- Detects components that are imported but not directly used as JSX elements +- Generates detailed HTML reports with interactive charts +- Configurable to analyze specific MFEs +- Supports different output formats (HTML, JSON, YAML) +- Includes trend analysis to track component usage changes over time +- Can be run as a GitHub Action with automatic GitHub Pages deployment + +## Installation + +1. Navigate to the tool directory: + +```bash +cd tools/mrc-usage-report +``` + +2. Install dependencies: + +```bash +npm install +``` + +3. Build the tool: + +```bash +npm run build +``` + +## Usage + +Run the tool with default settings: + +```bash +npm start +``` + +This will analyze all MFEs (except api-products) and generate an HTML report in the `./reports` directory. + +### Command Line Options + +You can customize the behavior with the following options: + +``` +Options: + -o, --output Output directory for the report (default: "./reports") + -f, --format Output format (html, json, yaml, csv) (default: "html") + -m, --mfes Comma-separated list of MFEs to analyze (default: "ep,intg,mc,saas") + --mfe-paths JSON string mapping MFE names to their repository paths (default: "{}") + -r, --mrc-path Path to the MRC repository (default: "../../maas-react-components") + -b, --base-path Base path for the project (default: current working directory) + -s, --source Source type for MRC components (local or github) (default: "local") + -g, --github Use GitHub as the source for MRC components (shorthand for -s github) + --github-url GitHub repository URL for MRC components (default: "https://github.com/SolaceDev/maas-react-components") + --github-branch Branch name for GitHub repository (default: "main") + -h, --help Display help for command + -V, --version Output the version number +``` + +### Examples + +Analyze only the 'ep' and 'saas' MFEs: + +```bash +npm start -- -m ep,saas +``` + +Generate a JSON or YAML report: + +```bash +# JSON format +npm start -- -f json + +# YAML format +npm start -- -f yaml +``` + +Specify custom paths: + +```bash +npm start -- -b /path/to/project -r /path/to/mrc -o /path/to/output +``` + +Use GitHub as the source for MRC components: + +```bash +# Using the -g flag (shorthand) +GITHUB_TOKEN=your_github_token npm start -- -g + +# Or using the --source option +GITHUB_TOKEN=your_github_token npm start -- -s github + +# Optionally specify a different GitHub repository URL +GITHUB_TOKEN=your_github_token npm start -- -g --github-url https://github.com/your-org/your-repo + +# Optionally specify a different branch name +GITHUB_TOKEN=your_github_token npm start -- -g --github-branch develop +``` + +This is particularly useful in CI/CD environments or GitHub Actions where you don't want to clone the repository manually. + +**Note:** If the MRC repository is private, you need to provide a GitHub personal access token with the `repo` scope via the `GITHUB_TOKEN` environment variable. This token is used to authenticate with the GitHub API. + +## Report Structure + +The HTML report includes: + +- Summary statistics (total usages, MFEs analyzed, unique components, unused components) +- Interactive charts showing component usage distribution +- Detailed breakdown of each component's usage +- Analysis of props used with each component +- Information about customization and styling overrides +- File references where components are used +- List of unused components (not used in any MFE) +- Per-MFE analysis of unused components (components used in some MFEs but not others) +- MRC version information for each MFE + +## Component Usage Detection + +The tool detects component usage in two ways: + +1. **Direct JSX Usage:** When a component is used directly in JSX elements within a file. +2. **Import-Only Usage:** When a component is imported from the MRC library but not directly used as a JSX element in the same file. This accounts for components that might be: + - Used conditionally in code paths + - Passed as props to other components + - Imported for future use or as a precaution + - Used in ways other than direct JSX elements + +## Development + +### Project Structure + +- `src/index.ts` - Main entry point +- `src/types.ts` - TypeScript interfaces +- `src/scanner/` - File scanning functionality +- `src/parser/` - Code parsing and analysis +- `src/aggregator/` - Data aggregation and statistics +- `src/reporter/` - Report generation + +### Adding New Features + +To add support for a new output format: + +1. Update the `outputFormat` type in `src/types.ts` +2. Add a new reporter class in `src/reporter/` +3. Update the report generation logic in `src/index.ts` + +### GitHub Action Integration + +This tool can be run automatically as a GitHub Action. A workflow file is included at `.github/workflows/mrc-usage-report.yml` that: + +1. Runs on every push to the main branch (and can be triggered manually) +2. Generates both HTML and JSON reports +3. Creates a trend analysis comparing the current report with previous ones +4. Publishes the reports to GitHub Pages + +### Setting Up GitHub Pages Deployment + +To enable the GitHub Pages deployment: + +1. Go to your repository settings +2. Navigate to "Pages" in the sidebar +3. Under "Build and deployment", select "GitHub Actions" as the source +4. The reports will be available at `https://[username].github.io/[repo-name]/mrc-usage-report/` + +### Testing Locally + +To test the report generation and trend analysis locally: + +```bash +# Navigate to the tool directory +cd tools/mrc-usage-report + +# Build the tool +npm run build + +# Generate the HTML report with correct base path +npm start -- -g -f html -o ./reports -b /path/to/repository/root + +# Generate the JSON report with correct base path +npm start -- -g -f json -o ./reports -b /path/to/repository/root + +# Run the trend analysis script +node ./scripts/trend-analyzer.js +``` + +Note: The `-b` parameter is crucial as it tells the tool where to look for the MFEs to analyze. Without it, the tool will use the current directory as the base path, which may not contain any MFEs to analyze. + +### Trend Analysis + +The trend analysis feature tracks changes in component usage over time: + +- On the first run, it creates a baseline report +- On subsequent runs, it compares the current report with the previous one +- The analysis shows: + - New components added + - Components removed + - Components with significant usage changes + - Overall statistics changes + +The trend report is set as the landing page, with a link to the detailed component usage report. + +## License + +ISC diff --git a/tools/mrc-usage-report-broker-manager/build/aggregator/dataAggregator.js b/tools/mrc-usage-report-broker-manager/build/aggregator/dataAggregator.js new file mode 100644 index 000000000..add5c1ea2 --- /dev/null +++ b/tools/mrc-usage-report-broker-manager/build/aggregator/dataAggregator.js @@ -0,0 +1,156 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.DataAggregator = void 0; +/** + * Aggregates component usage data into statistics + */ +class DataAggregator { + /** + * Aggregates component usage data into statistics + * @param usages Array of component usages + * @param config Analysis configuration + * @param allComponents All available MRC components + * @param mrcVersions MRC version information by MFE + * @returns Report data + */ + aggregate(usages, config, allComponents, mrcVersions) { + var _a, _b, _c; + // Group usages by component name + const usagesByComponent = new Map(); + for (const usage of usages) { + const { componentName } = usage; + if (!usagesByComponent.has(componentName)) { + usagesByComponent.set(componentName, []); + } + usagesByComponent.get(componentName).push(usage); + } + // Generate component stats + const componentStats = []; + for (const [componentName, componentUsages,] of usagesByComponent.entries()) { + // Count usages by MFE + const usagesByMfe = {}; + for (const usage of componentUsages) { + usagesByMfe[usage.mfe] = (usagesByMfe[usage.mfe] || 0) + 1; + } + // Count prop usage + const propCounts = new Map(); + for (const usage of componentUsages) { + for (const prop of usage.props) { + propCounts.set(prop.name, (propCounts.get(prop.name) || 0) + 1); + } + } + // Get most common props + const commonProps = Array.from(propCounts.entries()) + .map(([name, count]) => ({ name, count })) + .sort((a, b) => b.count - a.count) + .slice(0, 10); + // Get files where the component is used + const files = Array.from(new Set(componentUsages.map((usage) => usage.filePath))); + // Count customization stats + let styledComponentCount = 0; + let customStylesCount = 0; + const overriddenPropertiesCounts = {}; + for (const usage of componentUsages) { + if ((_a = usage.customization) === null || _a === void 0 ? void 0 : _a.styledComponent) { + styledComponentCount++; + } + if ((_b = usage.customization) === null || _b === void 0 ? void 0 : _b.customStyles) { + customStylesCount++; + } + if ((_c = usage.customization) === null || _c === void 0 ? void 0 : _c.overriddenProperties) { + for (const prop of usage.customization.overriddenProperties) { + overriddenPropertiesCounts[prop] = + (overriddenPropertiesCounts[prop] || 0) + 1; + } + } + } + // Add component stats + componentStats.push({ + componentName, + totalUsages: componentUsages.length, + usagesByMfe, + commonProps, + files, + customization: { + styledComponentCount, + customStylesCount, + overriddenPropertiesCounts, + }, + }); + } + // Sort component stats by total usages + componentStats.sort((a, b) => b.totalUsages - a.totalUsages); + // Generate overall stats + const totalUsages = usages.length; + // Most used components + const mostUsedComponents = componentStats.slice(0, 10).map((stats) => ({ + name: stats.componentName, + count: stats.totalUsages, + })); + // Most used props + const allPropCounts = new Map(); + for (const usage of usages) { + for (const prop of usage.props) { + allPropCounts.set(prop.name, (allPropCounts.get(prop.name) || 0) + 1); + } + } + const mostUsedProps = Array.from(allPropCounts.entries()) + .map(([name, count]) => ({ name, count })) + .sort((a, b) => b.count - a.count) + .slice(0, 10); + // MFE usage counts + const mfeUsages = {}; + for (const usage of usages) { + mfeUsages[usage.mfe] = (mfeUsages[usage.mfe] || 0) + 1; + } + // Find unused components + const usedComponentNames = new Set(componentStats.map((s) => s.componentName)); + const unusedComponents = allComponents.filter((comp) => !usedComponentNames.has(comp.name)); + // Find unused components by MFE + const unusedComponentsByMfe = {}; + // Initialize with all MFEs + const mfeList = config.mfes.length > 0 ? config.mfes : ["broker-manager"]; + for (const mfe of mfeList) { + unusedComponentsByMfe[mfe] = []; + } + // For each component, check which MFEs don't use it + for (const component of allComponents) { + const stat = componentStats.find((s) => s.componentName === component.name); + if (!stat) { + // If component is not used at all, add to all MFEs + for (const mfe of mfeList) { + unusedComponentsByMfe[mfe].push(component.name); + } + } + else { + // If component is used in some MFEs but not others + for (const mfe of mfeList) { + if (!stat.usagesByMfe[mfe]) { + unusedComponentsByMfe[mfe].push(component.name); + } + } + } + } + // Generate report data + const reportData = { + generatedAt: new Date().toISOString(), + config, + mrcVersions, + componentStats, + unusedComponents, + unusedComponentsByMfe, + overallStats: { + totalUsages, + mostUsedComponents, + mostUsedProps, + mfeUsages, + totalUnusedComponents: unusedComponents.length, + }, + rawData: { + componentUsages: usages, + }, + }; + return reportData; + } +} +exports.DataAggregator = DataAggregator; diff --git a/tools/mrc-usage-report-broker-manager/build/index.js b/tools/mrc-usage-report-broker-manager/build/index.js new file mode 100644 index 000000000..2581f371d --- /dev/null +++ b/tools/mrc-usage-report-broker-manager/build/index.js @@ -0,0 +1,227 @@ +#!/usr/bin/env node +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const commander_1 = require("commander"); +const path_1 = __importDefault(require("path")); +const chalk_1 = __importDefault(require("chalk")); +const fs_1 = __importDefault(require("fs")); +const fileScanner_1 = require("./scanner/fileScanner"); +const componentParser_1 = require("./parser/componentParser"); +const dataAggregator_1 = require("./aggregator/dataAggregator"); +const htmlReporter_1 = require("./reporter/htmlReporter"); +// Define the program +const program = new commander_1.Command(); +program + .name("mrc-usage-report") + .description("Generate a report on MRC component usage across MFEs") + .version("1.0.0") + .option("-o, --output ", "Output directory for the report", "./reports") + .option("-f, --format ", "Output format (html, json, csv)", "html") + .option("-r, --mrc-path ", "Path to the MRC repository", "/Users/ishanphadte/Desktop/maas-react-components") + .option("-b, --base-path ", "Base path for the project", process.cwd()) + .option("-s, --source ", "Source type for MRC components (local or github)", "local") + .option("-g, --github", "Use GitHub as the source for MRC components (shorthand for -s github)") + .option("--github-url ", "GitHub repository URL for MRC components", "https://github.com/SolaceDev/maas-react-components") + .option("--github-branch ", "Branch name for GitHub repository", "main"); +program.parse(process.argv); +const options = program.opts(); +// If -g flag is used, set source type to github +if (options.github) { + options.source = "github"; +} +// Main function +function main() { + return __awaiter(this, void 0, void 0, function* () { + try { + console.log(chalk_1.default.blue("MRC Component Usage Report Generator")); + console.log(chalk_1.default.gray("------------------------------------")); + // Parse options + const mfes = []; + const basePath = path_1.default.resolve(options.basePath); + const mrcPath = path_1.default.resolve(basePath, options.mrcPath); + const outputDir = path_1.default.resolve(options.output); + const outputFormats = options.format + .split(",") + .map((f) => f.trim()); + const mrcSourceType = options.source; + const mrcGithubUrl = options.githubUrl; + const mrcGithubBranch = options.githubBranch; + // If no MFEs are specified, default to "broker-manager" + if (mfes.length === 0) { + mfes.push("broker-manager"); + } + // Create config + const config = { + mfes, + mrcPath, + outputDir, + outputFormats, + mrcSourceType, + mrcGithubUrl, + mrcGithubBranch, + }; + console.log(chalk_1.default.yellow("Configuration:")); + console.log(` Base Path: ${basePath}`); + console.log(` MRC Path: ${mrcPath}`); + console.log(` MFEs: ${mfes.length > 0 ? mfes.join(", ") : "broker-manager"}`); + console.log(` Output Directory: ${outputDir}`); + console.log(` Output Format: ${outputFormats.join(", ")}`); + console.log(` MRC Source Type: ${mrcSourceType}`); + if (mrcSourceType === "github") { + console.log(` MRC GitHub URL: ${mrcGithubUrl}`); + console.log(` MRC GitHub Branch: ${mrcGithubBranch}`); + } + console.log(""); + // Step 1: Scan for files + console.log(chalk_1.default.yellow("Step 1: Scanning for files...")); + const fileScanner = new fileScanner_1.FileScanner(basePath, mfes, mrcSourceType, mrcGithubUrl, mrcGithubBranch); + const files = yield fileScanner.scanForFiles(); + console.log(`Found ${files.length} files to analyze`); + // Step 2: Scan for MRC components + console.log(chalk_1.default.yellow("Step 2: Scanning for MRC components...")); + const allComponents = yield fileScanner.scanForMrcComponents(mrcPath); + console.log(`Found ${allComponents.length} MRC component files`); + // Step 3: Parse files for component usage + console.log(chalk_1.default.yellow("Step 3: Parsing files for component usage...")); + const componentParser = new componentParser_1.ComponentParser(mrcPath, mrcSourceType); + yield componentParser.initialize(allComponents); + let totalUsages = 0; + const allUsages = []; + for (let i = 0; i < files.length; i++) { + const file = files[i]; + const mfe = mfes.length > 0 + ? file.split(path_1.default.sep).find((part) => mfes.includes(part)) || "" + : "broker-manager"; + try { + const usages = yield componentParser.parseFile(file, mfe); + totalUsages += usages.length; + allUsages.push(...usages); + // Log progress every 100 files + if ((i + 1) % 100 === 0 || i === files.length - 1) { + console.log(` Processed ${i + 1}/${files.length} files, found ${totalUsages} component usages so far`); + } + } + catch (error) { + console.error(`Error parsing file ${file}:`, error); + } + } + console.log(`Found ${totalUsages} total component usages`); + // Step 4: Detect MRC versions for each MFE + console.log(chalk_1.default.yellow("Step 4: Detecting MRC versions...")); + const mrcVersions = {}; + if (mfes.length > 0) { + for (const mfe of mfes) { + try { + const mfePath = path_1.default.join(basePath, "micro-frontends", mfe); + const packageJsonPath = path_1.default.join(mfePath, "package.json"); + if (fs_1.default.existsSync(packageJsonPath)) { + const packageJson = JSON.parse(fs_1.default.readFileSync(packageJsonPath, "utf8")); + // Check dependencies and devDependencies + const dependencies = packageJson.dependencies || {}; + const devDependencies = packageJson.devDependencies || {}; + const mrcPackageName = "@SolaceDev/maas-react-components"; + if (dependencies[mrcPackageName]) { + mrcVersions[mfe] = dependencies[mrcPackageName]; + console.log(` ${mfe}: MRC version ${dependencies[mrcPackageName]}`); + } + else if (devDependencies[mrcPackageName]) { + mrcVersions[mfe] = devDependencies[mrcPackageName]; + console.log(` ${mfe}: MRC version ${devDependencies[mrcPackageName]}`); + } + else { + mrcVersions[mfe] = "not found"; + console.log(` ${mfe}: MRC version not found`); + } + } + else { + mrcVersions[mfe] = "package.json not found"; + console.log(` ${mfe}: package.json not found`); + } + } + catch (error) { + mrcVersions[mfe] = "error"; + console.error(` Error getting MRC version for ${mfe}:`, error); + } + } + } + else { + // Handle the case where no MFEs are specified + const mfe = "broker-manager"; + try { + const packageJsonPath = path_1.default.join(basePath, "package.json"); + if (fs_1.default.existsSync(packageJsonPath)) { + const packageJson = JSON.parse(fs_1.default.readFileSync(packageJsonPath, "utf8")); + const dependencies = packageJson.dependencies || {}; + const devDependencies = packageJson.devDependencies || {}; + const mrcPackageName = "@SolaceDev/maas-react-components"; + if (dependencies[mrcPackageName]) { + mrcVersions[mfe] = dependencies[mrcPackageName]; + console.log(` ${mfe}: MRC version ${dependencies[mrcPackageName]}`); + } + else if (devDependencies[mrcPackageName]) { + mrcVersions[mfe] = devDependencies[mrcPackageName]; + console.log(` ${mfe}: MRC version ${devDependencies[mrcPackageName]}`); + } + else { + mrcVersions[mfe] = "not found"; + console.log(` ${mfe}: MRC version not found`); + } + } + else { + mrcVersions[mfe] = "package.json not found"; + console.log(` ${mfe}: package.json not found`); + } + } + catch (error) { + mrcVersions[mfe] = "error"; + console.error(` Error getting MRC version for ${mfe}:`, error); + } + } + // Step 5: Aggregate data + console.log(chalk_1.default.yellow("Step 5: Aggregating data...")); + const dataAggregator = new dataAggregator_1.DataAggregator(); + const reportData = dataAggregator.aggregate(allUsages, config, allComponents, mrcVersions); + console.log(`Generated report data with ${reportData.componentStats.length} component statistics`); + console.log(`Found ${reportData.unusedComponents.length} unused components`); + // Step 5: Generate report + console.log(chalk_1.default.yellow("Step 5: Generating report...")); + for (const format of outputFormats) { + const outputPath = path_1.default.join(outputDir, `mrc-broker-manager-usage-report.${format}`); + if (format === "html") { + console.log(chalk_1.default.blue("Generating HTML report...")); + const htmlReporter = new htmlReporter_1.HtmlReporter(); + yield htmlReporter.generateReport(reportData, outputPath); + console.log(chalk_1.default.green("HTML report generation complete.")); + } + else if (format === "json") { + console.log(chalk_1.default.blue("Generating JSON report...")); + const jsonOutput = JSON.stringify(reportData, null, 2); + fs_1.default.writeFileSync(outputPath, jsonOutput); + console.log(chalk_1.default.green(`JSON report generated at ${outputPath}`)); + } + else if (format === "csv") { + console.error("CSV format not yet implemented"); + } + } + console.log(chalk_1.default.green("Report generation completed successfully!")); + } + catch (error) { + console.error(chalk_1.default.red("Error generating report:"), error); + process.exit(1); + } + }); +} +// Run the main function +main(); diff --git a/tools/mrc-usage-report-broker-manager/build/parser/componentParser.js b/tools/mrc-usage-report-broker-manager/build/parser/componentParser.js new file mode 100644 index 000000000..ca288ff9c --- /dev/null +++ b/tools/mrc-usage-report-broker-manager/build/parser/componentParser.js @@ -0,0 +1,331 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.ComponentParser = void 0; +const parser = __importStar(require("@babel/parser")); +const traverse_1 = __importDefault(require("@babel/traverse")); +const t = __importStar(require("@babel/types")); +const fs_1 = __importDefault(require("fs")); +const path_1 = __importDefault(require("path")); +/** + * Parses files for MRC component usage + */ +class ComponentParser { + constructor(mrcPath, mrcSourceType = "local") { + this.mrcPath = mrcPath; + this.mrcSourceType = mrcSourceType; + this.mrcComponentNames = new Set(); + this.mrcFileNames = new Set(); + this.exportNameToFileNameToExportName = new Map(); + this.fileNameToExportName = new Map(); + } + /** + * Initializes the parser by loading all MRC component names + * @param componentInfo Array of MRC component information with exported names + */ + initialize(componentInfo) { + return __awaiter(this, void 0, void 0, function* () { + for (const component of componentInfo) { + // Get both the exported name and the file name + const exportedName = component.name; + const fileName = path_1.default.basename(component.path); + const fileNameWithoutExt = path_1.default.parse(fileName).name; + // Add both to our sets + this.mrcComponentNames.add(exportedName); + this.mrcFileNames.add(fileNameWithoutExt); + // Create mappings between them + this.exportNameToFileNameToExportName.set(exportedName, fileNameWithoutExt); + this.fileNameToExportName.set(fileNameWithoutExt, exportedName); + } + console.log(`Loaded ${this.mrcComponentNames.size} MRC component names and ${this.mrcFileNames.size} file names`); + // Log some examples of the mappings for debugging + let count = 0; + for (const [exportedName, fileName] of this.exportNameToFileNameToExportName.entries()) { + if (exportedName !== fileName) { + console.log(`Export mapping: ${exportedName} -> ${fileName}`); + count++; + if (count >= 5) + break; // Just log a few examples + } + } + }); + } + /** + * Parses a file for MRC component usage + * @param filePath Path to the file to parse + * @param mfe The MFE the file belongs to + * @returns Array of component usages found in the file + */ + parseFile(filePath, mfe) { + return __awaiter(this, void 0, void 0, function* () { + try { + const content = fs_1.default.readFileSync(filePath, "utf8"); + const usages = []; + // Parse the file + const ast = parser.parse(content, { + sourceType: "module", + plugins: ["jsx", "typescript", "decorators-legacy"] + }); + // Track imported MRC components + const importedComponents = new Map(); // Map + // Track imported components that are considered used just by being imported + const importedComponentUsages = new Map(); + // Traverse the AST + (0, traverse_1.default)(ast, { + // Find imports from @SolaceDev/maas-react-components + ImportDeclaration: (path) => { + var _a; + const source = path.node.source.value; + if (source === "@SolaceDev/maas-react-components") { + // Get the line number of the import declaration + const lineNumber = ((_a = path.node.loc) === null || _a === void 0 ? void 0 : _a.start.line) || 0; + path.node.specifiers.forEach((specifier) => { + if (t.isImportSpecifier(specifier) && t.isIdentifier(specifier.imported)) { + const importedName = specifier.imported.name; + const localName = specifier.local.name; + let componentName = importedName; + let isComponent = false; + // First check if it's a direct match with an exported name + if (this.mrcComponentNames.has(importedName)) { + isComponent = true; + } + // Then check if it matches a file name and get the corresponding exported name + else if (this.mrcFileNames.has(importedName) && + this.fileNameToExportName.has(importedName)) { + componentName = this.fileNameToExportName.get(importedName); + isComponent = true; + // console.log(`Found component by file name: ${importedName} -> ${componentName}`); + } + if (isComponent) { + importedComponents.set(localName, componentName); + // Consider the component as used just by being imported + // This handles cases where components are imported but not used as JSX elements + importedComponentUsages.set(componentName, { + componentName: componentName, + filePath: filePath, + mfe: mfe, + lineNumber: lineNumber, + props: [], + customization: { + styledComponent: false, + customStyles: false, + overriddenProperties: [] + } + }); + } + } + }); + } + }, + // Find JSX elements that use MRC components + JSXOpeningElement: (path) => { + var _a; + let elementName = null; + if (t.isJSXIdentifier(path.node.name)) { + elementName = path.node.name.name; + } + else if (t.isJSXMemberExpression(path.node.name)) { + // Handle cases like + elementName = `${path.node.name.object.name}.${path.node.name.property.name}`; + } + if (elementName) { + let componentName = null; + // Check if the element name is an imported MRC component + if (importedComponents.has(elementName)) { + componentName = importedComponents.get(elementName); + } + else if (this.mrcComponentNames.has(elementName)) { + // Direct usage of an MRC component not necessarily imported with a local name + componentName = elementName; + } + else if (this.mrcFileNames.has(elementName) && + this.fileNameToExportName.has(elementName)) { + // Usage by file name + componentName = this.fileNameToExportName.get(elementName); + } + if (componentName) { + const props = []; + // Extract props + path.node.attributes.forEach((attr) => { + if (t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name)) { + const propName = attr.name.name; + let propType = "unknown"; + let propValue = undefined; + let isFunction = false; + let isJSX = false; + // Extract prop value and type + if (attr.value) { + if (t.isStringLiteral(attr.value)) { + propType = "string"; + propValue = attr.value.value; + } + else if (t.isJSXExpressionContainer(attr.value)) { + const expression = attr.value.expression; + if (t.isNumericLiteral(expression)) { + propType = "number"; + propValue = expression.value; + } + else if (t.isBooleanLiteral(expression)) { + propType = "boolean"; + propValue = expression.value; + } + else if (t.isNullLiteral(expression)) { + propType = "null"; + propValue = null; + } + else if (t.isObjectExpression(expression)) { + propType = "object"; + } + else if (t.isArrayExpression(expression)) { + propType = "array"; + } + else if (t.isArrowFunctionExpression(expression) || + t.isFunctionExpression(expression)) { + propType = "function"; + isFunction = true; + } + else if (t.isJSXElement(expression) || t.isJSXFragment(expression)) { + propType = "jsx"; + isJSX = true; + } + else if (t.isIdentifier(expression)) { + propType = "variable"; + propValue = expression.name; + } + } + } + props.push({ + name: propName, + type: propType, + value: propValue, + isFunction: isFunction, + isJSX: isJSX + }); + } + else if (t.isJSXSpreadAttribute(attr)) { + props.push({ + name: "...", + type: "spread", + isSpread: true + }); + } + }); + // Get line number + const lineNumber = ((_a = path.node.loc) === null || _a === void 0 ? void 0 : _a.start.line) || 0; + // Check for customization + const customization = this.detectCustomization(path); + usages.push({ + componentName, + filePath, + mfe, + lineNumber, + props, + customization + }); + // Remove from importedComponentUsages since it's directly used as JSX + importedComponentUsages.delete(componentName); + } + } + } + }); + // Add all imported components that weren't used as JSX elements + usages.push(...Array.from(importedComponentUsages.values())); + return usages; + } + catch (error) { + console.error(`Error parsing file ${filePath}:`, error); + return []; + } + }); + } + /** + * Detects if a component has custom styling or is a styled-component + * @param path The JSX element path + * @returns Customization information + */ + detectCustomization(path) { + const customization = { + styledComponent: false, + customStyles: false, + overriddenProperties: [] + }; + // Check if the component is wrapped in a styled-component + let parent = path.parentPath; + while (parent) { + if (parent.node.type === "VariableDeclarator" && + parent.node.init && + parent.node.init.type === "CallExpression" && + parent.node.init.callee && + parent.node.init.callee.type === "MemberExpression" && + parent.node.init.callee.object.name === "styled") { + customization.styledComponent = true; + break; + } + parent = parent.parentPath; + } + // Check for style props + path.node.attributes.forEach((attr) => { + if (t.isJSXAttribute(attr) && + t.isJSXIdentifier(attr.name) && + (attr.name.name === "style" || attr.name.name === "sx" || attr.name.name === "css")) { + customization.customStyles = true; + // Try to extract overridden properties + if (attr.value && + t.isJSXExpressionContainer(attr.value) && + t.isObjectExpression(attr.value.expression)) { + attr.value.expression.properties.forEach((prop) => { + if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) { + customization.overriddenProperties.push(prop.key.name); + } + }); + } + } + }); + return customization; + } +} +exports.ComponentParser = ComponentParser; diff --git a/tools/mrc-usage-report-broker-manager/build/reporter/htmlReporter.js b/tools/mrc-usage-report-broker-manager/build/reporter/htmlReporter.js new file mode 100644 index 000000000..370b8b523 --- /dev/null +++ b/tools/mrc-usage-report-broker-manager/build/reporter/htmlReporter.js @@ -0,0 +1,649 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.HtmlReporter = void 0; +const fs_1 = __importDefault(require("fs")); +const path_1 = __importDefault(require("path")); +/** + * Generates an HTML report from the component usage data + */ +class HtmlReporter { + /** + * Generates an HTML report from the component usage data + * @param reportData The report data + * @param outputPath The path to write the report to + */ + generateReport(reportData, outputPath) { + return __awaiter(this, void 0, void 0, function* () { + const html = this.generateHtml(reportData); + // Create the output directory if it doesn't exist + const outputDir = path_1.default.dirname(outputPath); + if (!fs_1.default.existsSync(outputDir)) { + fs_1.default.mkdirSync(outputDir, { recursive: true }); + } + // Write the HTML to the output file + fs_1.default.writeFileSync(outputPath, html); + console.log(`HTML report generated at ${outputPath}`); + }); + } + /** + * Generates the HTML for the report + * @param reportData The report data + * @returns The HTML string + */ + generateHtml(reportData) { + const { componentStats, overallStats, generatedAt, config } = reportData; + // Format date + const formattedDate = new Date(generatedAt).toLocaleString(); + // Generate HTML + return ` + + + + + + MRC Component Usage Report + + + + +
+
+

MRC Component Usage Report

+

Generated on ${formattedDate}

+
+ +
+
+

Total Component Usages

+

${overallStats.totalUsages}

+
+
+

MFEs Analyzed

+

1

+

broker-manager

+
+
+

Unique Components Used

+

${componentStats.length}

+
+
+

Unused Components

+

${reportData.unusedComponents.length}

+

Components not used in any MFE

+
+
+ +

Overview

+ +
+
Components
+
MFEs
+
Unused Components
+
+ +
+

Most Used Components

+
+ +
+ +
+ +

Component Details

+ ${componentStats + .map((stats) => ` +
+
+
+ ${stats.componentName} + ${stats.totalUsages} usages +
+ +
+
+

Usage by MFE

+ + + + + + + + + ${Object.entries(stats.usagesByMfe) + .map(([mfe, count]) => ` + + + + + `) + .join("")} + +
MFEUsages
${mfe}${count}
+ +

Common Props

+ + + + + + + + + ${stats.commonProps + .map((prop) => ` + + + + + `) + .join("")} + +
Prop NameOccurrences
${prop.name}${prop.count}
+ +

Customization

+

+ Styled Components: ${stats.customization.styledComponentCount}
+ Custom Styles: ${stats.customization.customStylesCount} +

+ + ${Object.keys(stats.customization.overriddenPropertiesCounts) + .length > 0 + ? ` +
Overridden Properties
+ + + + + + + + + ${Object.entries(stats.customization.overriddenPropertiesCounts) + .map(([prop, count]) => ` + + + + + `) + .join("")} + +
PropertyOccurrences
${prop}${count}
+ ` + : ""} + +

Files (${stats.files.length})

+
    + ${stats.files.map((file) => `
  • ${file}
  • `).join("")} +
+
+
+ `) + .join("")} +
+ +
+

Component Usage by MFE

+
+ + + + + + + + + + + ${Object.entries(overallStats.mfeUsages) + .map(([mfe, count]) => ` + + + + + + `) + .join("")} + +
MFEComponent UsagesMRC Version
${mfe}${count}${reportData.mrcVersions[mfe] || "N/A"}
+
+ + +
+

Unused Components (${reportData.unusedComponents.length})

+

These components are not used in any of the analyzed MFEs. Consider reviewing them for potential removal or promotion.

+ + + + + + + + + + ${reportData.unusedComponents + .map((comp) => ` + + + + + `) + .join("")} + +
Component NamePath
${comp.name}${comp.path}
+ +

Unused Components by MFE

+

These components are used in some MFEs but not in others. Consider standardizing component usage across MFEs.

+ + ${Object.entries(reportData.unusedComponentsByMfe) + .map(([mfe, components]) => ` +
+
+
+ ${mfe} + ${components.length} unused components +
+ +
+
+ + + + + + + + ${components + .map((comp) => ` + + + + `) + .join("")} + +
Component Name
${comp}
+
+
+ `) + .join("")} +
+ + +
+ + + + + `; + } +} +exports.HtmlReporter = HtmlReporter; diff --git a/tools/mrc-usage-report-broker-manager/build/scanner/fileScanner.js b/tools/mrc-usage-report-broker-manager/build/scanner/fileScanner.js new file mode 100644 index 000000000..0e705ed7f --- /dev/null +++ b/tools/mrc-usage-report-broker-manager/build/scanner/fileScanner.js @@ -0,0 +1,280 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.FileScanner = void 0; +const path_1 = __importDefault(require("path")); +const fs_1 = __importDefault(require("fs")); +const util_1 = require("util"); +const child_process_1 = require("child_process"); +const axios_1 = __importDefault(require("axios")); +const execPromise = (0, util_1.promisify)(child_process_1.exec); +// Regular expression to match export statements like: +// export { default as ComponentName } from "./path/to/Component"; +const EXPORT_REGEX = /export\s*{\s*default\s*as\s*([A-Za-z0-9_]+)\s*}\s*from\s*["'](.*?)["'];?/g; +/** + * Scans for files in the specified MFEs + */ +class FileScanner { + constructor(basePath, mfes, mrcSourceType = "local", mrcGithubUrl, mrcGithubBranch = "main") { + this.basePath = basePath; + this.mfes = mfes; + this.mrcSourceType = mrcSourceType; + this.mrcGithubUrl = mrcGithubUrl; + this.mrcGithubBranch = mrcGithubBranch; + } + /** + * Scans for all TypeScript and JavaScript files in the specified MFEs + * @returns Array of file paths + */ + scanForFiles() { + return __awaiter(this, void 0, void 0, function* () { + const allFiles = []; + if (this.mfes.length === 0) { + // No specific MFEs, scan the entire base path + const mfePath = this.basePath; + if (!fs_1.default.existsSync(mfePath)) { + console.warn(`Base path not found: ${mfePath}`); + return []; + } + try { + const { stdout } = yield execPromise(`find ${mfePath} -type f \\( -name "*.ts" -o -name "*.tsx" -o -name "*.js" -o -name "*.jsx" \\) -not -path "*/node_modules/*" -not -path "*/dist/*" -not -path "*/build/*" -not -name "*.d.ts" -not -name "*.test.*" -not -name "*.spec.*"`); + const files = stdout.trim().split("\n").filter(Boolean); + allFiles.push(...files); + } + catch (error) { + console.error(`Error scanning files in ${mfePath}:`, error); + } + } + else { + // Scan specified MFEs + for (const mfe of this.mfes) { + const mfePath = mfe === "broker-manager" + ? this.basePath + : path_1.default.join(this.basePath, mfe); + if (!fs_1.default.existsSync(mfePath)) { + console.warn(`MFE directory not found: ${mfePath}`); + continue; + } + try { + const { stdout } = yield execPromise(`find ${mfePath}/src -type f \\( -name "*.ts" -o -name "*.tsx" -o -name "*.js" -o -name "*.jsx" \\) -not -path "*/node_modules/*" -not -name "*.d.ts" -not -name "*.test.*" -not -name "*.spec.*"`); + const files = stdout.trim().split("\n").filter(Boolean); + allFiles.push(...files); + } + catch (error) { + console.error(`Error scanning files in ${mfePath}:`, error); + } + } + } + return allFiles; + }); + } + /** + * Extracts owner and repo from GitHub URL + * @param githubUrl GitHub repository URL + * @returns Object containing owner and repo + */ + parseGithubUrl(githubUrl) { + // Handle URLs like https://github.com/owner/repo or git@github.com:owner/repo.git + const urlMatch = githubUrl.match(/github\.com[/:]([^/]+)\/([^/]+?)(\.git)?$/); + if (urlMatch) { + return { + owner: urlMatch[1], + repo: urlMatch[2].replace(/\.git$/, ""), // Remove .git if present + }; + } + throw new Error(`Invalid GitHub URL: ${githubUrl}`); + } + /** + * Gets GitHub authentication token from environment variable + * @returns GitHub authentication token or undefined if not available + */ + getGithubToken() { + return process.env.GITHUB_TOKEN; + } + /** + * Fetches a file from GitHub API + * @param owner Repository owner + * @param repo Repository name + * @param path File path within the repository + * @param branch Branch name (default: main) + * @returns File content as string + */ + fetchFileFromGithub(owner, repo, path, branch) { + return __awaiter(this, void 0, void 0, function* () { + branch = branch || this.mrcGithubBranch; + try { + const url = `https://raw.githubusercontent.com/${owner}/${repo}/${branch}/${path}`; + console.log(`Fetching file from GitHub: ${url}`); + const headers = {}; + const token = this.getGithubToken(); + if (token) { + headers.Authorization = `token ${token}`; + } + const response = yield axios_1.default.get(url, { headers }); + return response.data; + } + catch (error) { + console.error(`Error fetching file from GitHub:`, error); + throw new Error(`Failed to fetch file from GitHub: ${path}`); + } + }); + } + /** + * Fetches directory contents from GitHub API + * @param owner Repository owner + * @param repo Repository name + * @param path Directory path within the repository + * @param branch Branch name (default: main) + * @returns Array of file paths + */ + fetchDirectoryFromGithub(owner, repo, path, branch) { + return __awaiter(this, void 0, void 0, function* () { + branch = branch || this.mrcGithubBranch; + try { + const url = `https://api.github.com/repos/${owner}/${repo}/contents/${path}?ref=${branch}`; + console.log(`Fetching directory from GitHub API: ${url}`); + const headers = { + Accept: "application/vnd.github.v3+json", + }; + const token = this.getGithubToken(); + if (token) { + headers.Authorization = `token ${token}`; + } + const response = yield axios_1.default.get(url, { headers }); + // Process the response to extract file paths + const files = []; + const processItems = (items) => __awaiter(this, void 0, void 0, function* () { + for (const item of items) { + if (item.type === "file") { + files.push(item.path); + } + else if (item.type === "dir") { + // Recursively fetch subdirectory contents + const subDirUrl = `https://api.github.com/repos/${owner}/${repo}/contents/${item.path}?ref=${branch}`; + const subDirResponse = yield axios_1.default.get(subDirUrl, { headers }); + yield processItems(subDirResponse.data); + } + } + }); + yield processItems(response.data); + return files; + } + catch (error) { + console.error(`Error fetching directory from GitHub:`, error); + throw new Error(`Failed to fetch directory from GitHub: ${path}`); + } + }); + } + /** + * Prepares the MRC path based on the source type. + * If source type is local, returns the provided path. + * If source type is github, returns null to indicate GitHub API usage. + */ + prepareMrcPath(mrcPath) { + return __awaiter(this, void 0, void 0, function* () { + // If source type is local, use the provided path + if (this.mrcSourceType === "local") { + return mrcPath; + } + // If source type is github, we'll use the GitHub API directly + if (this.mrcSourceType === "github" && this.mrcGithubUrl) { + console.log(`Using GitHub API for ${this.mrcGithubUrl}`); + return null; // Return null to indicate we're using GitHub API + } + throw new Error("Invalid MRC source configuration"); + }); + } + /** + * Scans for all MRC components in the MRC repository + * @param mrcPath Path to the MRC repository + * @returns Array of MRC component paths + */ + scanForMrcComponents(mrcPath) { + return __awaiter(this, void 0, void 0, function* () { + const allComponents = []; + // If using GitHub API + if (this.mrcSourceType === "github" && this.mrcGithubUrl) { + try { + const { owner, repo } = this.parseGithubUrl(this.mrcGithubUrl); + console.log(`Scanning for MRC components in GitHub repository: ${owner}/${repo}`); + // Fetch all files in the components directory to find index.ts/tsx files + const files = yield this.fetchDirectoryFromGithub(owner, repo, "src/components"); + for (const file of files) { + // Only process index.ts or index.tsx files + if (file.endsWith("index.ts") || file.endsWith("index.tsx")) { + const fileContent = yield this.fetchFileFromGithub(owner, repo, file, this.mrcGithubBranch); + // Reset regex lastIndex for each file + EXPORT_REGEX.lastIndex = 0; + const match = EXPORT_REGEX.exec(fileContent); + if (match && match[1] && match[2]) { + const componentName = match[1]; + const relativePath = match[2]; + // Construct the full path to the actual component file + // The relativePath is relative to the index.ts file's directory + const componentDir = path_1.default.dirname(file); + const fullComponentPath = path_1.default.join(componentDir, relativePath); + // Assuming .tsx or .ts extension for the actual component file + let finalComponentPath = fullComponentPath; + if (!finalComponentPath.endsWith(".ts") && + !finalComponentPath.endsWith(".tsx")) { + // Try .tsx first, then .ts + // Note: For GitHub, we can't check fs.existsSync directly. + // We'll assume .tsx for now or rely on the parser to handle it. + // For a more robust solution, we'd need to list files in the componentDir on GitHub. + finalComponentPath += ".tsx"; // Default to .tsx for GitHub + } + allComponents.push({ + name: componentName, + path: finalComponentPath, + }); + } + } + } + console.log(`Found ${allComponents.length} component files in GitHub repository`); + return allComponents; + } + catch (error) { + console.error(`Error scanning MRC components from GitHub:`, error); + return []; + } + } + // If using local path + const actualMrcPath = yield this.prepareMrcPath(mrcPath); + if (actualMrcPath) { + const componentsPath = path_1.default.join(actualMrcPath, "src", "components"); + // Check if the components directory exists + if (!fs_1.default.existsSync(componentsPath)) { + throw new Error(`MRC components directory not found: ${componentsPath}`); + } + try { + // Use find command to locate all index.ts/tsx files within component subdirectories + const { stdout } = yield execPromise(`find ${componentsPath} -type f \\( -name "*.ts" -o -name "*.tsx" -o -name "*.js" -o -name "*.jsx" \\) -not -path "*/node_modules/*" -not -path "*/dist/*" -not -path "*/build/*" -not -name "*.d.ts" -not -name "*.test.*" -not -name "*.spec.*"`); + const componentFiles = stdout.trim().split("\n").filter(Boolean); + for (const file of componentFiles) { + const componentName = path_1.default.basename(file, path_1.default.extname(file)); + allComponents.push({ name: componentName, path: file }); + } + console.log(`Found ${allComponents.length} MRC components from local path`); + return allComponents; + } + catch (error) { + console.error(`Error scanning MRC components:`, error); + return []; + } + } + throw new Error("Invalid MRC source configuration"); + }); + } +} +exports.FileScanner = FileScanner; diff --git a/tools/mrc-usage-report-broker-manager/build/types.js b/tools/mrc-usage-report-broker-manager/build/types.js new file mode 100644 index 000000000..c8ad2e549 --- /dev/null +++ b/tools/mrc-usage-report-broker-manager/build/types.js @@ -0,0 +1,2 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/tools/mrc-usage-report-broker-manager/package-lock.json b/tools/mrc-usage-report-broker-manager/package-lock.json new file mode 100644 index 000000000..41ae538f7 --- /dev/null +++ b/tools/mrc-usage-report-broker-manager/package-lock.json @@ -0,0 +1,595 @@ +{ + "name": "mrc-usage-report-tool", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "mrc-usage-report-tool", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@babel/parser": "^7.24.7", + "@babel/traverse": "^7.24.7", + "@types/node": "^20.14.9", + "axios": "^1.7.2", + "chalk": "^5.3.0", + "commander": "^11.1.0", + "js-yaml": "^4.1.0" + }, + "devDependencies": { + "@types/babel__traverse": "^7.20.7", + "typescript": "^5.8.3" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/generator": { + "version": "7.27.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.5.tgz", + "integrity": "sha512-ZGhA37l0e/g2s1Cnzdix0O3aLYm66eF8aufiVteOgnwxgnRP8GoyMj7VWsgWnQbVKXyge7hqrFh2K2TQM6t1Hw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.27.5", + "@babel/types": "^7.27.3", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.27.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.7.tgz", + "integrity": "sha512-qnzXzDXdr/po3bOTbTIQZ7+TxNKxpkN5IifVLXS+r7qwynkZfPyjZfE7hCXbo7IoO9TNcSyibgONsf2HauUd3Q==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.27.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.7.tgz", + "integrity": "sha512-X6ZlfR/O/s5EQ/SnUSLzr+6kGnkg8HXGMzpgsMsrJVcfDtH1vIp6ctCN4eZ1LS5c0+te5Cb6Y514fASjMRJ1nw==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.27.5", + "@babel/parser": "^7.27.7", + "@babel/template": "^7.27.2", + "@babel/types": "^7.27.7", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.27.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.7.tgz", + "integrity": "sha512-8OLQgDScAOHXnAz2cV+RfzzNMipuLVBz2biuAJFMV9bfkNf393je3VM8CLkjQodW5+iWsSJdSgSWT6rsZoXHPw==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.7.tgz", + "integrity": "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/node": { + "version": "20.19.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.1.tgz", + "integrity": "sha512-jJD50LtlD2dodAEO653i3YF04NWak6jN3ky+Ri3Em3mGR39/glWiboM/IePaRbgwSfqM1TpGXfAg8ohn/4dTgA==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.10.0.tgz", + "integrity": "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/chalk": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", + "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.3.tgz", + "integrity": "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + } + } +} diff --git a/tools/mrc-usage-report-broker-manager/package.json b/tools/mrc-usage-report-broker-manager/package.json new file mode 100644 index 000000000..199e33405 --- /dev/null +++ b/tools/mrc-usage-report-broker-manager/package.json @@ -0,0 +1,26 @@ +{ + "name": "mrc-usage-report-tool", + "version": "1.0.0", + "description": "A tool to analyze and report on MRC component usage.", + "main": "build/index.js", + "scripts": { + "build": "tsc", + "start": "node build/index.js" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "@babel/parser": "^7.24.7", + "@babel/traverse": "^7.24.7", + "@types/node": "^20.14.9", + "axios": "^1.7.2", + "chalk": "^5.3.0", + "commander": "^11.1.0", + "js-yaml": "^4.1.0" + }, + "devDependencies": { + "@types/babel__traverse": "^7.20.7", + "typescript": "^5.8.3" + } +} diff --git a/tools/mrc-usage-report-broker-manager/src/aggregator/dataAggregator.ts b/tools/mrc-usage-report-broker-manager/src/aggregator/dataAggregator.ts new file mode 100644 index 000000000..05b92b94e --- /dev/null +++ b/tools/mrc-usage-report-broker-manager/src/aggregator/dataAggregator.ts @@ -0,0 +1,194 @@ +import { + ComponentUsage, + ComponentStats, + ReportData, + AnalysisConfig, +} from "../types"; + +/** + * Aggregates component usage data into statistics + */ +export class DataAggregator { + /** + * Aggregates component usage data into statistics + * @param usages Array of component usages + * @param config Analysis configuration + * @param allComponents All available MRC components + * @param mrcVersions MRC version information by MFE + * @returns Report data + */ + aggregate( + usages: ComponentUsage[], + config: AnalysisConfig, + allComponents: { name: string; path: string }[], + mrcVersions: Record + ): ReportData { + // Group usages by component name + const usagesByComponent = new Map(); + + for (const usage of usages) { + const { componentName } = usage; + if (!usagesByComponent.has(componentName)) { + usagesByComponent.set(componentName, []); + } + usagesByComponent.get(componentName)!.push(usage); + } + + // Generate component stats + const componentStats: ComponentStats[] = []; + + for (const [ + componentName, + componentUsages, + ] of usagesByComponent.entries()) { + // Count usages by MFE + const usagesByMfe: Record = {}; + for (const usage of componentUsages) { + usagesByMfe[usage.mfe] = (usagesByMfe[usage.mfe] || 0) + 1; + } + + // Count prop usage + const propCounts = new Map(); + for (const usage of componentUsages) { + for (const prop of usage.props) { + propCounts.set(prop.name, (propCounts.get(prop.name) || 0) + 1); + } + } + + // Get most common props + const commonProps = Array.from(propCounts.entries()) + .map(([name, count]) => ({ name, count })) + .sort((a, b) => b.count - a.count) + .slice(0, 10); + + // Get files where the component is used + const files = Array.from( + new Set(componentUsages.map((usage) => usage.filePath)) + ); + + // Count customization stats + let styledComponentCount = 0; + let customStylesCount = 0; + const overriddenPropertiesCounts: Record = {}; + + for (const usage of componentUsages) { + if (usage.customization?.styledComponent) { + styledComponentCount++; + } + if (usage.customization?.customStyles) { + customStylesCount++; + } + if (usage.customization?.overriddenProperties) { + for (const prop of usage.customization.overriddenProperties) { + overriddenPropertiesCounts[prop] = + (overriddenPropertiesCounts[prop] || 0) + 1; + } + } + } + + // Add component stats + componentStats.push({ + componentName, + totalUsages: componentUsages.length, + usagesByMfe, + commonProps, + files, + customization: { + styledComponentCount, + customStylesCount, + overriddenPropertiesCounts, + }, + }); + } + + // Sort component stats by total usages + componentStats.sort((a, b) => b.totalUsages - a.totalUsages); + + // Generate overall stats + const totalUsages = usages.length; + + // Most used components + const mostUsedComponents = componentStats.slice(0, 10).map((stats) => ({ + name: stats.componentName, + count: stats.totalUsages, + })); + + // Most used props + const allPropCounts = new Map(); + for (const usage of usages) { + for (const prop of usage.props) { + allPropCounts.set(prop.name, (allPropCounts.get(prop.name) || 0) + 1); + } + } + + const mostUsedProps = Array.from(allPropCounts.entries()) + .map(([name, count]) => ({ name, count })) + .sort((a, b) => b.count - a.count) + .slice(0, 10); + + // MFE usage counts + const mfeUsages: Record = {}; + for (const usage of usages) { + mfeUsages[usage.mfe] = (mfeUsages[usage.mfe] || 0) + 1; + } + + // Find unused components + const usedComponentNames = new Set( + componentStats.map((s) => s.componentName) + ); + const unusedComponents = allComponents.filter( + (comp) => !usedComponentNames.has(comp.name) + ); + + // Find unused components by MFE + const unusedComponentsByMfe: Record = {}; + // Initialize with all MFEs + const mfeList = config.mfes.length > 0 ? config.mfes : ["broker-manager"]; + for (const mfe of mfeList) { + unusedComponentsByMfe[mfe] = []; + } + + // For each component, check which MFEs don't use it + for (const component of allComponents) { + const stat = componentStats.find( + (s) => s.componentName === component.name + ); + + if (!stat) { + // If component is not used at all, add to all MFEs + for (const mfe of mfeList) { + unusedComponentsByMfe[mfe].push(component.name); + } + } else { + // If component is used in some MFEs but not others + for (const mfe of mfeList) { + if (!stat.usagesByMfe[mfe]) { + unusedComponentsByMfe[mfe].push(component.name); + } + } + } + } + + // Generate report data + const reportData: ReportData = { + generatedAt: new Date().toISOString(), + config, + mrcVersions, + componentStats, + unusedComponents, + unusedComponentsByMfe, + overallStats: { + totalUsages, + mostUsedComponents, + mostUsedProps, + mfeUsages, + totalUnusedComponents: unusedComponents.length, + }, + rawData: { + componentUsages: usages, + }, + }; + + return reportData; + } +} diff --git a/tools/mrc-usage-report-broker-manager/src/index.ts b/tools/mrc-usage-report-broker-manager/src/index.ts new file mode 100644 index 000000000..0b7fe11a7 --- /dev/null +++ b/tools/mrc-usage-report-broker-manager/src/index.ts @@ -0,0 +1,285 @@ +#!/usr/bin/env node + +import { Command } from "commander"; +import path from "path"; +import chalk from "chalk"; +import fs from "fs"; +import { FileScanner } from "./scanner/fileScanner"; +import { ComponentParser } from "./parser/componentParser"; +import { DataAggregator } from "./aggregator/dataAggregator"; +import { HtmlReporter } from "./reporter/htmlReporter"; +import { AnalysisConfig, MrcSourceType } from "./types"; + +// Define the program +const program = new Command(); + +program + .name("mrc-usage-report") + .description("Generate a report on MRC component usage across MFEs") + .version("1.0.0") + .option("-o, --output ", "Output directory for the report", "./reports") + .option("-f, --format ", "Output format (html, json, csv)", "html") + .option( + "-r, --mrc-path ", + "Path to the MRC repository", + "/Users/ishanphadte/Desktop/maas-react-components" + ) + .option("-b, --base-path ", "Base path for the project", process.cwd()) + .option( + "-s, --source ", + "Source type for MRC components (local or github)", + "local" + ) + .option( + "-g, --github", + "Use GitHub as the source for MRC components (shorthand for -s github)" + ) + .option( + "--github-url ", + "GitHub repository URL for MRC components", + "https://github.com/SolaceDev/maas-react-components" + ) + .option( + "--github-branch ", + "Branch name for GitHub repository", + "main" + ); + +program.parse(process.argv); + +const options = program.opts(); + +// If -g flag is used, set source type to github +if (options.github) { + options.source = "github"; +} + +// Main function +async function main() { + try { + console.log(chalk.blue("MRC Component Usage Report Generator")); + console.log(chalk.gray("------------------------------------")); + + // Parse options + const mfes: string[] = []; + const basePath = path.resolve(options.basePath); + const mrcPath = path.resolve(basePath, options.mrcPath); + const outputDir = path.resolve(options.output); + const outputFormats = options.format + .split(",") + .map((f: string) => f.trim()); + const mrcSourceType = options.source as MrcSourceType; + const mrcGithubUrl = options.githubUrl; + const mrcGithubBranch = options.githubBranch; + + // If no MFEs are specified, default to "broker-manager" + if (mfes.length === 0) { + mfes.push("broker-manager"); + } + + // Create config + const config: AnalysisConfig = { + mfes, + mrcPath, + outputDir, + outputFormats, + mrcSourceType, + mrcGithubUrl, + mrcGithubBranch, + }; + + console.log(chalk.yellow("Configuration:")); + console.log(` Base Path: ${basePath}`); + console.log(` MRC Path: ${mrcPath}`); + console.log( + ` MFEs: ${mfes.length > 0 ? mfes.join(", ") : "broker-manager"}` + ); + console.log(` Output Directory: ${outputDir}`); + console.log(` Output Format: ${outputFormats.join(", ")}`); + console.log(` MRC Source Type: ${mrcSourceType}`); + if (mrcSourceType === "github") { + console.log(` MRC GitHub URL: ${mrcGithubUrl}`); + console.log(` MRC GitHub Branch: ${mrcGithubBranch}`); + } + console.log(""); + + // Step 1: Scan for files + console.log(chalk.yellow("Step 1: Scanning for files...")); + const fileScanner = new FileScanner( + basePath, + mfes, + mrcSourceType, + mrcGithubUrl, + mrcGithubBranch + ); + const files = await fileScanner.scanForFiles(); + console.log(`Found ${files.length} files to analyze`); + + // Step 2: Scan for MRC components + console.log(chalk.yellow("Step 2: Scanning for MRC components...")); + const allComponents = await fileScanner.scanForMrcComponents(mrcPath); + console.log(`Found ${allComponents.length} MRC component files`); + + // Step 3: Parse files for component usage + console.log(chalk.yellow("Step 3: Parsing files for component usage...")); + const componentParser = new ComponentParser(mrcPath, mrcSourceType); + await componentParser.initialize(allComponents); + + let totalUsages = 0; + const allUsages: any[] = []; + + for (let i = 0; i < files.length; i++) { + const file = files[i]; + const mfe = + mfes.length > 0 + ? file.split(path.sep).find((part) => mfes.includes(part)) || "" + : "broker-manager"; + + try { + const usages = await componentParser.parseFile(file, mfe); + totalUsages += usages.length; + allUsages.push(...usages); + + // Log progress every 100 files + if ((i + 1) % 100 === 0 || i === files.length - 1) { + console.log( + ` Processed ${i + 1}/${ + files.length + } files, found ${totalUsages} component usages so far` + ); + } + } catch (error) { + console.error(`Error parsing file ${file}:`, error); + } + } + + console.log(`Found ${totalUsages} total component usages`); + + // Step 4: Detect MRC versions for each MFE + console.log(chalk.yellow("Step 4: Detecting MRC versions...")); + const mrcVersions: Record = {}; + + if (mfes.length > 0) { + for (const mfe of mfes) { + try { + const mfePath = path.join(basePath, "micro-frontends", mfe); + const packageJsonPath = path.join(mfePath, "package.json"); + + if (fs.existsSync(packageJsonPath)) { + const packageJson = JSON.parse( + fs.readFileSync(packageJsonPath, "utf8") + ); + + // Check dependencies and devDependencies + const dependencies = packageJson.dependencies || {}; + const devDependencies = packageJson.devDependencies || {}; + + const mrcPackageName = "@SolaceDev/maas-react-components"; + + if (dependencies[mrcPackageName]) { + mrcVersions[mfe] = dependencies[mrcPackageName]; + console.log( + ` ${mfe}: MRC version ${dependencies[mrcPackageName]}` + ); + } else if (devDependencies[mrcPackageName]) { + mrcVersions[mfe] = devDependencies[mrcPackageName]; + console.log( + ` ${mfe}: MRC version ${devDependencies[mrcPackageName]}` + ); + } else { + mrcVersions[mfe] = "not found"; + console.log(` ${mfe}: MRC version not found`); + } + } else { + mrcVersions[mfe] = "package.json not found"; + console.log(` ${mfe}: package.json not found`); + } + } catch (error) { + mrcVersions[mfe] = "error"; + console.error(` Error getting MRC version for ${mfe}:`, error); + } + } + } else { + // Handle the case where no MFEs are specified + const mfe = "broker-manager"; + try { + const packageJsonPath = path.join(basePath, "package.json"); + if (fs.existsSync(packageJsonPath)) { + const packageJson = JSON.parse( + fs.readFileSync(packageJsonPath, "utf8") + ); + const dependencies = packageJson.dependencies || {}; + const devDependencies = packageJson.devDependencies || {}; + const mrcPackageName = "@SolaceDev/maas-react-components"; + if (dependencies[mrcPackageName]) { + mrcVersions[mfe] = dependencies[mrcPackageName]; + console.log( + ` ${mfe}: MRC version ${dependencies[mrcPackageName]}` + ); + } else if (devDependencies[mrcPackageName]) { + mrcVersions[mfe] = devDependencies[mrcPackageName]; + console.log( + ` ${mfe}: MRC version ${devDependencies[mrcPackageName]}` + ); + } else { + mrcVersions[mfe] = "not found"; + console.log(` ${mfe}: MRC version not found`); + } + } else { + mrcVersions[mfe] = "package.json not found"; + console.log(` ${mfe}: package.json not found`); + } + } catch (error) { + mrcVersions[mfe] = "error"; + console.error(` Error getting MRC version for ${mfe}:`, error); + } + } + + // Step 5: Aggregate data + console.log(chalk.yellow("Step 5: Aggregating data...")); + const dataAggregator = new DataAggregator(); + const reportData = dataAggregator.aggregate( + allUsages, + config, + allComponents, + mrcVersions + ); + console.log( + `Generated report data with ${reportData.componentStats.length} component statistics` + ); + console.log( + `Found ${reportData.unusedComponents.length} unused components` + ); + + // Step 5: Generate report + console.log(chalk.yellow("Step 5: Generating report...")); + for (const format of outputFormats) { + const outputPath = path.join( + outputDir, + `mrc-broker-manager-usage-report.${format}` + ); + + if (format === "html") { + console.log(chalk.blue("Generating HTML report...")); + const htmlReporter = new HtmlReporter(); + await htmlReporter.generateReport(reportData, outputPath); + console.log(chalk.green("HTML report generation complete.")); + } else if (format === "json") { + console.log(chalk.blue("Generating JSON report...")); + const jsonOutput = JSON.stringify(reportData, null, 2); + fs.writeFileSync(outputPath, jsonOutput); + console.log(chalk.green(`JSON report generated at ${outputPath}`)); + } else if (format === "csv") { + console.error("CSV format not yet implemented"); + } + } + + console.log(chalk.green("Report generation completed successfully!")); + } catch (error) { + console.error(chalk.red("Error generating report:"), error); + process.exit(1); + } +} + +// Run the main function +main(); diff --git a/tools/mrc-usage-report-broker-manager/src/parser/componentParser.ts b/tools/mrc-usage-report-broker-manager/src/parser/componentParser.ts new file mode 100644 index 000000000..f0670457e --- /dev/null +++ b/tools/mrc-usage-report-broker-manager/src/parser/componentParser.ts @@ -0,0 +1,320 @@ +import * as parser from "@babel/parser"; +import traverse from "@babel/traverse"; +import * as t from "@babel/types"; +import fs from "fs"; +import path from "path"; +import { ComponentUsage, ComponentProp } from "../types"; +import { MrcSourceType } from "../types"; + +/** + * Parses files for MRC component usage + */ +export class ComponentParser { + private mrcComponentNames: Set; + private mrcFileNames: Set; + private mrcPath: string; + private mrcSourceType: MrcSourceType; + private exportNameToFileNameToExportName: Map; // Map + private fileNameToExportName: Map; // Map + + constructor(mrcPath: string, mrcSourceType: MrcSourceType = "local") { + this.mrcPath = mrcPath; + this.mrcSourceType = mrcSourceType; + this.mrcComponentNames = new Set(); + this.mrcFileNames = new Set(); + this.exportNameToFileNameToExportName = new Map(); + this.fileNameToExportName = new Map(); + } + + /** + * Initializes the parser by loading all MRC component names + * @param componentInfo Array of MRC component information with exported names + */ + async initialize(componentInfo: { name: string; path: string }[]): Promise { + for (const component of componentInfo) { + // Get both the exported name and the file name + const exportedName = component.name; + const fileName = path.basename(component.path); + const fileNameWithoutExt = path.parse(fileName).name; + + // Add both to our sets + this.mrcComponentNames.add(exportedName); + this.mrcFileNames.add(fileNameWithoutExt); + + // Create mappings between them + this.exportNameToFileNameToExportName.set(exportedName, fileNameWithoutExt); + this.fileNameToExportName.set(fileNameWithoutExt, exportedName); + } + + console.log( + `Loaded ${this.mrcComponentNames.size} MRC component names and ${this.mrcFileNames.size} file names` + ); + + // Log some examples of the mappings for debugging + let count = 0; + for (const [exportedName, fileName] of this.exportNameToFileNameToExportName.entries()) { + if (exportedName !== fileName) { + console.log(`Export mapping: ${exportedName} -> ${fileName}`); + count++; + if (count >= 5) break; // Just log a few examples + } + } + } + + /** + * Parses a file for MRC component usage + * @param filePath Path to the file to parse + * @param mfe The MFE the file belongs to + * @returns Array of component usages found in the file + */ + async parseFile(filePath: string, mfe: string): Promise { + try { + const content = fs.readFileSync(filePath, "utf8"); + const usages: ComponentUsage[] = []; + + // Parse the file + const ast = parser.parse(content, { + sourceType: "module", + plugins: ["jsx", "typescript", "decorators-legacy"] + }); + + // Track imported MRC components + const importedComponents = new Map(); // Map + // Track imported components that are considered used just by being imported + const importedComponentUsages = new Map(); + + // Traverse the AST + traverse(ast, { + // Find imports from @SolaceDev/maas-react-components + ImportDeclaration: (path) => { + const source = path.node.source.value; + if (source === "@SolaceDev/maas-react-components") { + // Get the line number of the import declaration + const lineNumber = path.node.loc?.start.line || 0; + + path.node.specifiers.forEach((specifier) => { + if (t.isImportSpecifier(specifier) && t.isIdentifier(specifier.imported)) { + const importedName = specifier.imported.name; + const localName = specifier.local.name; + + let componentName = importedName; + let isComponent = false; + + // First check if it's a direct match with an exported name + if (this.mrcComponentNames.has(importedName)) { + isComponent = true; + } + // Then check if it matches a file name and get the corresponding exported name + else if ( + this.mrcFileNames.has(importedName) && + this.fileNameToExportName.has(importedName) + ) { + componentName = this.fileNameToExportName.get(importedName)!; + isComponent = true; + // console.log(`Found component by file name: ${importedName} -> ${componentName}`); + } + + if (isComponent) { + importedComponents.set(localName, componentName); + + // Consider the component as used just by being imported + // This handles cases where components are imported but not used as JSX elements + importedComponentUsages.set(componentName, { + componentName: componentName, + filePath: filePath, + mfe: mfe, + lineNumber: lineNumber, + props: [], + customization: { + styledComponent: false, + customStyles: false, + overriddenProperties: [] + } + }); + } + } + }); + } + }, + + // Find JSX elements that use MRC components + JSXOpeningElement: (path) => { + let elementName: string | null = null; + + if (t.isJSXIdentifier(path.node.name)) { + elementName = path.node.name.name; + } else if (t.isJSXMemberExpression(path.node.name)) { + // Handle cases like + elementName = `${(path.node.name.object as t.JSXIdentifier).name}.${ + (path.node.name.property as t.JSXIdentifier).name + }`; + } + + if (elementName) { + let componentName: string | null = null; + // Check if the element name is an imported MRC component + if (importedComponents.has(elementName)) { + componentName = importedComponents.get(elementName)!; + } else if (this.mrcComponentNames.has(elementName)) { + // Direct usage of an MRC component not necessarily imported with a local name + componentName = elementName; + } else if ( + this.mrcFileNames.has(elementName) && + this.fileNameToExportName.has(elementName) + ) { + // Usage by file name + componentName = this.fileNameToExportName.get(elementName)!; + } + + if (componentName) { + const props: ComponentProp[] = []; + + // Extract props + path.node.attributes.forEach((attr) => { + if (t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name)) { + const propName = attr.name.name; + let propType = "unknown"; + let propValue: any = undefined; + let isFunction = false; + let isJSX = false; + + // Extract prop value and type + if (attr.value) { + if (t.isStringLiteral(attr.value)) { + propType = "string"; + propValue = attr.value.value; + } else if (t.isJSXExpressionContainer(attr.value)) { + const expression = attr.value.expression; + if (t.isNumericLiteral(expression)) { + propType = "number"; + propValue = expression.value; + } else if (t.isBooleanLiteral(expression)) { + propType = "boolean"; + propValue = expression.value; + } else if (t.isNullLiteral(expression)) { + propType = "null"; + propValue = null; + } else if (t.isObjectExpression(expression)) { + propType = "object"; + } else if (t.isArrayExpression(expression)) { + propType = "array"; + } else if ( + t.isArrowFunctionExpression(expression) || + t.isFunctionExpression(expression) + ) { + propType = "function"; + isFunction = true; + } else if (t.isJSXElement(expression) || t.isJSXFragment(expression)) { + propType = "jsx"; + isJSX = true; + } else if (t.isIdentifier(expression)) { + propType = "variable"; + propValue = expression.name; + } + } + } + + props.push({ + name: propName, + type: propType, + value: propValue, + isFunction: isFunction, + isJSX: isJSX + }); + } else if (t.isJSXSpreadAttribute(attr)) { + props.push({ + name: "...", + type: "spread", + isSpread: true + }); + } + }); + + // Get line number + const lineNumber = path.node.loc?.start.line || 0; + + // Check for customization + const customization = this.detectCustomization(path); + + usages.push({ + componentName, + filePath, + mfe, + lineNumber, + props, + customization + }); + + // Remove from importedComponentUsages since it's directly used as JSX + importedComponentUsages.delete(componentName); + } + } + } + }); + + // Add all imported components that weren't used as JSX elements + usages.push(...Array.from(importedComponentUsages.values())); + + return usages; + } catch (error) { + console.error(`Error parsing file ${filePath}:`, error); + return []; + } + } + + /** + * Detects if a component has custom styling or is a styled-component + * @param path The JSX element path + * @returns Customization information + */ + private detectCustomization(path: any): ComponentUsage["customization"] { + const customization: ComponentUsage["customization"] = { + styledComponent: false, + customStyles: false, + overriddenProperties: [] + }; + + // Check if the component is wrapped in a styled-component + let parent = path.parentPath; + while (parent) { + if ( + parent.node.type === "VariableDeclarator" && + parent.node.init && + parent.node.init.type === "CallExpression" && + parent.node.init.callee && + parent.node.init.callee.type === "MemberExpression" && + (parent.node.init.callee.object as t.Identifier).name === "styled" + ) { + customization.styledComponent = true; + break; + } + parent = parent.parentPath; + } + + // Check for style props + path.node.attributes.forEach((attr: any) => { + if ( + t.isJSXAttribute(attr) && + t.isJSXIdentifier(attr.name) && + (attr.name.name === "style" || attr.name.name === "sx" || attr.name.name === "css") + ) { + customization.customStyles = true; + + // Try to extract overridden properties + if ( + attr.value && + t.isJSXExpressionContainer(attr.value) && + t.isObjectExpression(attr.value.expression) + ) { + attr.value.expression.properties.forEach((prop: any) => { + if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) { + customization.overriddenProperties.push(prop.key.name); + } + }); + } + } + }); + + return customization; + } +} diff --git a/tools/mrc-usage-report-broker-manager/src/reporter/htmlReporter.ts b/tools/mrc-usage-report-broker-manager/src/reporter/htmlReporter.ts new file mode 100644 index 000000000..2f96d5632 --- /dev/null +++ b/tools/mrc-usage-report-broker-manager/src/reporter/htmlReporter.ts @@ -0,0 +1,664 @@ +import fs from "fs"; +import path from "path"; +import { ReportData } from "../types"; + +/** + * Generates an HTML report from the component usage data + */ +export class HtmlReporter { + /** + * Generates an HTML report from the component usage data + * @param reportData The report data + * @param outputPath The path to write the report to + */ + async generateReport( + reportData: ReportData, + outputPath: string + ): Promise { + const html = this.generateHtml(reportData); + + // Create the output directory if it doesn't exist + const outputDir = path.dirname(outputPath); + if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir, { recursive: true }); + } + + // Write the HTML to the output file + fs.writeFileSync(outputPath, html); + + console.log(`HTML report generated at ${outputPath}`); + } + + /** + * Generates the HTML for the report + * @param reportData The report data + * @returns The HTML string + */ + private generateHtml(reportData: ReportData): string { + const { componentStats, overallStats, generatedAt, config } = reportData; + + // Format date + const formattedDate = new Date(generatedAt).toLocaleString(); + + // Generate HTML + return ` + + + + + + MRC Component Usage Report + + + + +
+
+

MRC Component Usage Report

+

Generated on ${formattedDate}

+
+ +
+
+

Total Component Usages

+

${overallStats.totalUsages}

+
+
+

MFEs Analyzed

+

1

+

broker-manager

+
+
+

Unique Components Used

+

${componentStats.length}

+
+
+

Unused Components

+

${reportData.unusedComponents.length}

+

Components not used in any MFE

+
+
+ +

Overview

+ +
+
Components
+
MFEs
+
Unused Components
+
+ +
+

Most Used Components

+
+ +
+ +
+ +

Component Details

+ ${componentStats + .map( + (stats) => ` +
+
+
+ ${stats.componentName} + ${stats.totalUsages} usages +
+ +
+
+

Usage by MFE

+ + + + + + + + + ${Object.entries(stats.usagesByMfe) + .map( + ([mfe, count]) => ` + + + + + ` + ) + .join("")} + +
MFEUsages
${mfe}${count}
+ +

Common Props

+ + + + + + + + + ${stats.commonProps + .map( + (prop) => ` + + + + + ` + ) + .join("")} + +
Prop NameOccurrences
${prop.name}${prop.count}
+ +

Customization

+

+ Styled Components: ${stats.customization.styledComponentCount}
+ Custom Styles: ${stats.customization.customStylesCount} +

+ + ${ + Object.keys(stats.customization.overriddenPropertiesCounts) + .length > 0 + ? ` +
Overridden Properties
+ + + + + + + + + ${Object.entries( + stats.customization.overriddenPropertiesCounts + ) + .map( + ([prop, count]) => ` + + + + + ` + ) + .join("")} + +
PropertyOccurrences
${prop}${count}
+ ` + : "" + } + +

Files (${stats.files.length})

+
    + ${stats.files.map((file) => `
  • ${file}
  • `).join("")} +
+
+
+ ` + ) + .join("")} +
+ +
+

Component Usage by MFE

+
+ + + + + + + + + + + ${Object.entries(overallStats.mfeUsages) + .map( + ([mfe, count]) => ` + + + + + + ` + ) + .join("")} + +
MFEComponent UsagesMRC Version
${mfe}${count}${reportData.mrcVersions[mfe] || "N/A"}
+
+ + +
+

Unused Components (${reportData.unusedComponents.length})

+

These components are not used in any of the analyzed MFEs. Consider reviewing them for potential removal or promotion.

+ + + + + + + + + + ${reportData.unusedComponents + .map( + (comp) => ` + + + + + ` + ) + .join("")} + +
Component NamePath
${comp.name}${comp.path}
+ +

Unused Components by MFE

+

These components are used in some MFEs but not in others. Consider standardizing component usage across MFEs.

+ + ${Object.entries(reportData.unusedComponentsByMfe) + .map( + ([mfe, components]) => ` +
+
+
+ ${mfe} + ${components.length} unused components +
+ +
+
+ + + + + + + + ${components + .map( + (comp) => ` + + + + ` + ) + .join("")} + +
Component Name
${comp}
+
+
+ ` + ) + .join("")} +
+ + +
+ + + + + `; + } +} diff --git a/tools/mrc-usage-report-broker-manager/src/scanner/fileScanner.ts b/tools/mrc-usage-report-broker-manager/src/scanner/fileScanner.ts new file mode 100644 index 000000000..39d77ae56 --- /dev/null +++ b/tools/mrc-usage-report-broker-manager/src/scanner/fileScanner.ts @@ -0,0 +1,339 @@ +import path from "path"; +import fs from "fs"; +import { promisify } from "util"; +import { exec } from "child_process"; +import os from "os"; +import axios from "axios"; +import { MrcSourceType } from "../types"; + +const execPromise = promisify(exec); + +// Regular expression to match export statements like: +// export { default as ComponentName } from "./path/to/Component"; +const EXPORT_REGEX = + /export\s*{\s*default\s*as\s*([A-Za-z0-9_]+)\s*}\s*from\s*["'](.*?)["'];?/g; + +/** + * Scans for files in the specified MFEs + */ +export class FileScanner { + private basePath: string; + private mfes: string[]; + private mrcSourceType: MrcSourceType; + private mrcGithubUrl?: string; + private mrcGithubBranch: string; + private tempDir?: string; + + constructor( + basePath: string, + mfes: string[], + mrcSourceType: MrcSourceType = "local", + mrcGithubUrl?: string, + mrcGithubBranch: string = "main" + ) { + this.basePath = basePath; + this.mfes = mfes; + this.mrcSourceType = mrcSourceType; + this.mrcGithubUrl = mrcGithubUrl; + this.mrcGithubBranch = mrcGithubBranch; + } + + /** + * Scans for all TypeScript and JavaScript files in the specified MFEs + * @returns Array of file paths + */ + async scanForFiles(): Promise { + const allFiles: string[] = []; + + if (this.mfes.length === 0) { + // No specific MFEs, scan the entire base path + const mfePath = this.basePath; + if (!fs.existsSync(mfePath)) { + console.warn(`Base path not found: ${mfePath}`); + return []; + } + try { + const { stdout } = await execPromise( + `find ${mfePath} -type f \\( -name "*.ts" -o -name "*.tsx" -o -name "*.js" -o -name "*.jsx" \\) -not -path "*/node_modules/*" -not -path "*/dist/*" -not -path "*/build/*" -not -name "*.d.ts" -not -name "*.test.*" -not -name "*.spec.*"` + ); + const files = stdout.trim().split("\n").filter(Boolean); + allFiles.push(...files); + } catch (error) { + console.error(`Error scanning files in ${mfePath}:`, error); + } + } else { + // Scan specified MFEs + for (const mfe of this.mfes) { + const mfePath = + mfe === "broker-manager" + ? this.basePath + : path.join(this.basePath, mfe); + + if (!fs.existsSync(mfePath)) { + console.warn(`MFE directory not found: ${mfePath}`); + continue; + } + + try { + const { stdout } = await execPromise( + `find ${mfePath}/src -type f \\( -name "*.ts" -o -name "*.tsx" -o -name "*.js" -o -name "*.jsx" \\) -not -path "*/node_modules/*" -not -name "*.d.ts" -not -name "*.test.*" -not -name "*.spec.*"` + ); + + const files = stdout.trim().split("\n").filter(Boolean); + allFiles.push(...files); + } catch (error) { + console.error(`Error scanning files in ${mfePath}:`, error); + } + } + } + + return allFiles; + } + + /** + * Extracts owner and repo from GitHub URL + * @param githubUrl GitHub repository URL + * @returns Object containing owner and repo + */ + private parseGithubUrl(githubUrl: string): { owner: string; repo: string } { + // Handle URLs like https://github.com/owner/repo or git@github.com:owner/repo.git + const urlMatch = githubUrl.match( + /github\.com[/:]([^/]+)\/([^/]+?)(\.git)?$/ + ); + if (urlMatch) { + return { + owner: urlMatch[1], + repo: urlMatch[2].replace(/\.git$/, ""), // Remove .git if present + }; + } + throw new Error(`Invalid GitHub URL: ${githubUrl}`); + } + + /** + * Gets GitHub authentication token from environment variable + * @returns GitHub authentication token or undefined if not available + */ + private getGithubToken(): string | undefined { + return process.env.GITHUB_TOKEN; + } + + /** + * Fetches a file from GitHub API + * @param owner Repository owner + * @param repo Repository name + * @param path File path within the repository + * @param branch Branch name (default: main) + * @returns File content as string + */ + private async fetchFileFromGithub( + owner: string, + repo: string, + path: string, + branch?: string + ): Promise { + branch = branch || this.mrcGithubBranch; + try { + const url = `https://raw.githubusercontent.com/${owner}/${repo}/${branch}/${path}`; + console.log(`Fetching file from GitHub: ${url}`); + + const headers: Record = {}; + const token = this.getGithubToken(); + if (token) { + headers.Authorization = `token ${token}`; + } + + const response = await axios.get(url, { headers }); + return response.data; + } catch (error) { + console.error(`Error fetching file from GitHub:`, error); + throw new Error(`Failed to fetch file from GitHub: ${path}`); + } + } + + /** + * Fetches directory contents from GitHub API + * @param owner Repository owner + * @param repo Repository name + * @param path Directory path within the repository + * @param branch Branch name (default: main) + * @returns Array of file paths + */ + private async fetchDirectoryFromGithub( + owner: string, + repo: string, + path: string, + branch?: string + ): Promise { + branch = branch || this.mrcGithubBranch; + try { + const url = `https://api.github.com/repos/${owner}/${repo}/contents/${path}?ref=${branch}`; + console.log(`Fetching directory from GitHub API: ${url}`); + + const headers: Record = { + Accept: "application/vnd.github.v3+json", + }; + const token = this.getGithubToken(); + if (token) { + headers.Authorization = `token ${token}`; + } + + const response = await axios.get(url, { headers }); + + // Process the response to extract file paths + const files: string[] = []; + const processItems = async (items: any[]) => { + for (const item of items) { + if (item.type === "file") { + files.push(item.path); + } else if (item.type === "dir") { + // Recursively fetch subdirectory contents + const subDirUrl = `https://api.github.com/repos/${owner}/${repo}/contents/${item.path}?ref=${branch}`; + const subDirResponse = await axios.get(subDirUrl, { headers }); + await processItems(subDirResponse.data); + } + } + }; + + await processItems(response.data); + return files; + } catch (error) { + console.error(`Error fetching directory from GitHub:`, error); + throw new Error(`Failed to fetch directory from GitHub: ${path}`); + } + } + + /** + * Prepares the MRC path based on the source type. + * If source type is local, returns the provided path. + * If source type is github, returns null to indicate GitHub API usage. + */ + async prepareMrcPath(mrcPath: string): Promise { + // If source type is local, use the provided path + if (this.mrcSourceType === "local") { + return mrcPath; + } + + // If source type is github, we'll use the GitHub API directly + if (this.mrcSourceType === "github" && this.mrcGithubUrl) { + console.log(`Using GitHub API for ${this.mrcGithubUrl}`); + return null; // Return null to indicate we're using GitHub API + } + + throw new Error("Invalid MRC source configuration"); + } + + /** + * Scans for all MRC components in the MRC repository + * @param mrcPath Path to the MRC repository + * @returns Array of MRC component paths + */ + async scanForMrcComponents( + mrcPath: string + ): Promise<{ name: string; path: string }[]> { + const allComponents: { name: string; path: string }[] = []; + + // If using GitHub API + if (this.mrcSourceType === "github" && this.mrcGithubUrl) { + try { + const { owner, repo } = this.parseGithubUrl(this.mrcGithubUrl); + console.log( + `Scanning for MRC components in GitHub repository: ${owner}/${repo}` + ); + + // Fetch all files in the components directory to find index.ts/tsx files + const files = await this.fetchDirectoryFromGithub( + owner, + repo, + "src/components" + ); + + for (const file of files) { + // Only process index.ts or index.tsx files + if (file.endsWith("index.ts") || file.endsWith("index.tsx")) { + const fileContent = await this.fetchFileFromGithub( + owner, + repo, + file, + this.mrcGithubBranch + ); + // Reset regex lastIndex for each file + EXPORT_REGEX.lastIndex = 0; + const match = EXPORT_REGEX.exec(fileContent); + if (match && match[1] && match[2]) { + const componentName = match[1]; + const relativePath = match[2]; + // Construct the full path to the actual component file + // The relativePath is relative to the index.ts file's directory + const componentDir = path.dirname(file); + const fullComponentPath = path.join(componentDir, relativePath); + + // Assuming .tsx or .ts extension for the actual component file + let finalComponentPath = fullComponentPath; + if ( + !finalComponentPath.endsWith(".ts") && + !finalComponentPath.endsWith(".tsx") + ) { + // Try .tsx first, then .ts + // Note: For GitHub, we can't check fs.existsSync directly. + // We'll assume .tsx for now or rely on the parser to handle it. + // For a more robust solution, we'd need to list files in the componentDir on GitHub. + finalComponentPath += ".tsx"; // Default to .tsx for GitHub + } + + allComponents.push({ + name: componentName, + path: finalComponentPath, + }); + } + } + } + + console.log( + `Found ${allComponents.length} component files in GitHub repository` + ); + return allComponents; + } catch (error) { + console.error(`Error scanning MRC components from GitHub:`, error); + return []; + } + } + + // If using local path + const actualMrcPath = await this.prepareMrcPath(mrcPath); + + if (actualMrcPath) { + const componentsPath = path.join(actualMrcPath, "src", "components"); + + // Check if the components directory exists + if (!fs.existsSync(componentsPath)) { + throw new Error( + `MRC components directory not found: ${componentsPath}` + ); + } + + try { + // Use find command to locate all index.ts/tsx files within component subdirectories + const { stdout } = await execPromise( + `find ${componentsPath} -type f \\( -name "*.ts" -o -name "*.tsx" -o -name "*.js" -o -name "*.jsx" \\) -not -path "*/node_modules/*" -not -path "*/dist/*" -not -path "*/build/*" -not -name "*.d.ts" -not -name "*.test.*" -not -name "*.spec.*"` + ); + + const componentFiles = stdout.trim().split("\n").filter(Boolean); + + for (const file of componentFiles) { + const componentName = path.basename(file, path.extname(file)); + allComponents.push({ name: componentName, path: file }); + } + + console.log( + `Found ${allComponents.length} MRC components from local path` + ); + return allComponents; + } catch (error) { + console.error(`Error scanning MRC components:`, error); + return []; + } + } + + throw new Error("Invalid MRC source configuration"); + } +} diff --git a/tools/mrc-usage-report-broker-manager/src/types.ts b/tools/mrc-usage-report-broker-manager/src/types.ts new file mode 100644 index 000000000..82c90f2e5 --- /dev/null +++ b/tools/mrc-usage-report-broker-manager/src/types.ts @@ -0,0 +1,72 @@ +export interface AnalysisConfig { + mfes: string[]; + mrcPath: string; + outputDir: string; + outputFormats: string[]; + mrcSourceType: MrcSourceType; + mrcGithubUrl?: string; + mrcGithubBranch?: string; +} + +export type MrcSourceType = "local" | "github"; + +export interface ComponentUsage { + componentName: string; + filePath: string; + mfe: string; + lineNumber: number; + props: ComponentProp[]; + customization: { + styledComponent: boolean; + customStyles: boolean; + overriddenProperties: string[]; + }; +} + +export interface ComponentProp { + name: string; + type: string; + value?: any; + isFunction?: boolean; + isJSX?: boolean; + isSpread?: boolean; +} + +export interface ComponentStats { + componentName: string; + totalUsages: number; + usagesByMfe: Record; + commonProps: { name: string; count: number }[]; + files: string[]; + customization: { + styledComponentCount: number; + customStylesCount: number; + overriddenPropertiesCounts: Record; + }; +} + +export interface ReportData { + generatedAt: string; + config: AnalysisConfig; + mrcVersions: Record; + componentStats: ComponentStats[]; + unusedComponents: { name: string; path: string }[]; + unusedComponentsByMfe: Record; + overallStats: OverallStats; + rawData: { + componentUsages: ComponentUsage[]; + }; +} + +export interface OverallStats { + totalUsages: number; + mostUsedComponents: { name: string; count: number }[]; + mostUsedProps: { name: string; count: number }[]; + mfeUsages: Record; + totalUnusedComponents: number; +} + +export interface MrcComponentInfo { + name: string; + path: string; +} diff --git a/tools/mrc-usage-report-broker-manager/tsconfig.json b/tools/mrc-usage-report-broker-manager/tsconfig.json new file mode 100644 index 000000000..ef068a05b --- /dev/null +++ b/tools/mrc-usage-report-broker-manager/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "es2016", + "module": "commonjs", + "outDir": "./build", + "rootDir": "./src", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true + }, + "include": ["src/**/*.ts", "src/jsonMerger.ts"], + "exclude": ["node_modules"] +} diff --git a/tools/mrc-usage-report-json-splitter/package-lock.json b/tools/mrc-usage-report-json-splitter/package-lock.json new file mode 100644 index 000000000..e63377815 --- /dev/null +++ b/tools/mrc-usage-report-json-splitter/package-lock.json @@ -0,0 +1,48 @@ +{ + "name": "json-splitter", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "json-splitter", + "version": "1.0.0", + "license": "ISC", + "devDependencies": { + "@types/node": "^20.14.10", + "typescript": "^5.5.3" + } + }, + "node_modules/@types/node": { + "version": "20.19.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.1.tgz", + "integrity": "sha512-jJD50LtlD2dodAEO653i3YF04NWak6jN3ky+Ri3Em3mGR39/glWiboM/IePaRbgwSfqM1TpGXfAg8ohn/4dTgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/tools/mrc-usage-report-json-splitter/package.json b/tools/mrc-usage-report-json-splitter/package.json new file mode 100644 index 000000000..4bfc88ee3 --- /dev/null +++ b/tools/mrc-usage-report-json-splitter/package.json @@ -0,0 +1,21 @@ +{ + "name": "json-splitter", + "version": "1.0.0", + "description": "A tool to split the merged MRC usage report JSON into individual component files.", + "main": "dist/index.js", + "scripts": { + "build": "tsc", + "start": "node dist/index.js" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "chalk": "^5.3.0", + "commander": "^11.1.0" + }, + "devDependencies": { + "@types/node": "^20.14.10", + "typescript": "^5.5.3" + } +} diff --git a/tools/mrc-usage-report-json-splitter/src/index.ts b/tools/mrc-usage-report-json-splitter/src/index.ts new file mode 100644 index 000000000..68c85b883 --- /dev/null +++ b/tools/mrc-usage-report-json-splitter/src/index.ts @@ -0,0 +1,83 @@ +import * as fs from "fs"; +import * as path from "path"; +import { Command } from "commander"; +import chalk from "chalk"; + +interface ComponentStat { + componentName: string; + totalUsages: number; + usagesByMfe: { [key: string]: number }; + commonProps: any[]; + files: string[]; + customization: any; +} + +interface MergedReport { + generatedAt: string; + config: any; + mrcVersions: any; + componentStats: ComponentStat[]; +} + +const program = new Command(); + +program + .name("json-splitter") + .description("A tool to split the merged MRC usage report JSON into individual component files.") + .version("1.0.0") + .option( + "-i, --input ", + "Input path for the merged JSON report", + "../../merged-mrc-usage-report.json" + ) + .option( + "-o, --output ", + "Output directory for the component JSON files", + "output/SolaceComponent" + ); + +program.parse(process.argv); +const options = program.opts(); + +async function splitJsonFile() { + try { + const inputFilePath = path.resolve(options.input); + const outputDirPath = path.resolve(options.output); + + console.log(chalk.blue("JSON Splitter Tool")); + console.log(chalk.gray("------------------")); + console.log(chalk.yellow("Configuration:")); + console.log(` Input File: ${inputFilePath}`); + console.log(` Output Directory: ${outputDirPath}`); + console.log(""); + + const rawData = fs.readFileSync(inputFilePath, "utf-8"); + const report: MergedReport = JSON.parse(rawData); + + // Ensure output directory exists + if (!fs.existsSync(outputDirPath)) { + fs.mkdirSync(outputDirPath, { recursive: true }); + } + + for (const component of report.componentStats) { + const componentName = component.componentName; + const componentOutputDir = path.join(outputDirPath, componentName); + + if (!fs.existsSync(componentOutputDir)) { + fs.mkdirSync(componentOutputDir, { recursive: true }); + } + + const outputFileName = `${componentName}.json`; + const outputFilePath = path.join(componentOutputDir, outputFileName); + + fs.writeFileSync(outputFilePath, JSON.stringify(component, null, 2), "utf-8"); + console.log(chalk.green(`Successfully wrote ${outputFilePath}`)); + } + + console.log(chalk.green("JSON splitting complete.")); + } catch (error) { + console.error(chalk.red("Error splitting JSON file:"), error); + } +} + +splitJsonFile(); diff --git a/tools/mrc-usage-report-json-splitter/tsconfig.json b/tools/mrc-usage-report-json-splitter/tsconfig.json new file mode 100644 index 000000000..7d9efe288 --- /dev/null +++ b/tools/mrc-usage-report-json-splitter/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "es2016", + "module": "commonjs", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true, + "outDir": "./dist" + }, + "include": ["src/**/*"] +} diff --git a/tools/mrc-usage-report-maas-ops-ui/README.md b/tools/mrc-usage-report-maas-ops-ui/README.md new file mode 100644 index 000000000..c6177a96b --- /dev/null +++ b/tools/mrc-usage-report-maas-ops-ui/README.md @@ -0,0 +1,212 @@ +# MRC Usage Report + +A tool to analyze and report on the usage of MRC (maas-react-components) components across different micro-frontends. + +## Features + +- Scans TypeScript/JavaScript files for MRC component usage +- Analyzes how components are used (props, customization, etc.) +- Identifies unused components (globally and per MFE) +- Detects components that are imported but not directly used as JSX elements +- Generates detailed HTML reports with interactive charts +- Configurable to analyze specific MFEs +- Supports different output formats (HTML, JSON, YAML) +- Includes trend analysis to track component usage changes over time +- Can be run as a GitHub Action with automatic GitHub Pages deployment + +## Installation + +1. Navigate to the tool directory: + +```bash +cd tools/mrc-usage-report +``` + +2. Install dependencies: + +```bash +npm install +``` + +3. Build the tool: + +```bash +npm run build +``` + +## Usage + +Run the tool with default settings: + +```bash +npm start +``` + +This will analyze the `maas-ops-react` and `infra` MFEs and generate an HTML report in the `./reports` directory. + +### Command Line Options + +You can customize the behavior with the following options: + +``` +Options: + -o, --output Output directory for the report (default: "./reports") + -f, --format Output format (html, json, yaml, csv) (default: "html") + -m, --mfes Comma-separated list of MFEs to analyze (default: "maas-ops-react,infra") + --mfe-paths JSON string mapping MFE names to their repository paths (default: "{}") + -r, --mrc-path Path to the MRC repository (default: "../../maas-react-components") + -b, --base-path Base path for the project (default: current working directory) + -s, --source Source type for MRC components (local or github) (default: "local") + -g, --github Use GitHub as the source for MRC components (shorthand for -s github) + --github-url GitHub repository URL for MRC components (default: "https://github.com/SolaceDev/maas-react-components") + --github-branch Branch name for GitHub repository (default: "main") + -h, --help Display help for command + -V, --version Output the version number +``` + +### Examples + +Analyze only the 'ep' and 'saas' MFEs: + +```bash +npm start -- -m ep,saas +``` + +Generate a JSON or YAML report: + +```bash +# JSON format +npm start -- -f json + +# YAML format +npm start -- -f yaml +``` + +Specify custom paths: + +```bash +npm start -- -b /path/to/project -r /path/to/mrc -o /path/to/output +``` + +Use GitHub as the source for MRC components: + +```bash +# Using the -g flag (shorthand) +GITHUB_TOKEN=your_github_token npm start -- -g + +# Or using the --source option +GITHUB_TOKEN=your_github_token npm start -- -s github + +# Optionally specify a different GitHub repository URL +GITHUB_TOKEN=your_github_token npm start -- -g --github-url https://github.com/your-org/your-repo + +# Optionally specify a different branch name +GITHUB_TOKEN=your_github_token npm start -- -g --github-branch develop +``` + +This is particularly useful in CI/CD environments or GitHub Actions where you don't want to clone the repository manually. + +**Note:** If the MRC repository is private, you need to provide a GitHub personal access token with the `repo` scope via the `GITHUB_TOKEN` environment variable. This token is used to authenticate with the GitHub API. + +## Report Structure + +The HTML report includes: + +- Summary statistics (total usages, MFEs analyzed, unique components, unused components) +- Interactive charts showing component usage distribution +- Detailed breakdown of each component's usage +- Analysis of props used with each component +- Information about customization and styling overrides +- File references where components are used +- List of unused components (not used in any MFE) +- Per-MFE analysis of unused components (components used in some MFEs but not others) +- MRC version information for each MFE + +## Component Usage Detection + +The tool detects component usage in two ways: + +1. **Direct JSX Usage:** When a component is used directly in JSX elements within a file. +2. **Import-Only Usage:** When a component is imported from the MRC library but not directly used as a JSX element in the same file. This accounts for components that might be: + - Used conditionally in code paths + - Passed as props to other components + - Imported for future use or as a precaution + - Used in ways other than direct JSX elements + +## Development + +### Project Structure + +- `src/index.ts` - Main entry point +- `src/types.ts` - TypeScript interfaces +- `src/scanner/` - File scanning functionality +- `src/parser/` - Code parsing and analysis +- `src/aggregator/` - Data aggregation and statistics +- `src/reporter/` - Report generation + +### Adding New Features + +To add support for a new output format: + +1. Update the `outputFormat` type in `src/types.ts` +2. Add a new reporter class in `src/reporter/` +3. Update the report generation logic in `src/index.ts` + +### GitHub Action Integration + +This tool can be run automatically as a GitHub Action. A workflow file is included at `.github/workflows/mrc-usage-report.yml` that: + +1. Runs on every push to the main branch (and can be triggered manually) +2. Generates both HTML and JSON reports +3. Creates a trend analysis comparing the current report with previous ones +4. Publishes the reports to GitHub Pages + +### Setting Up GitHub Pages Deployment + +To enable the GitHub Pages deployment: + +1. Go to your repository settings +2. Navigate to "Pages" in the sidebar +3. Under "Build and deployment", select "GitHub Actions" as the source +4. The reports will be available at `https://[username].github.io/[repo-name]/mrc-usage-report/` + +### Testing Locally + +To test the report generation and trend analysis locally: + +```bash +# Navigate to the tool directory +cd tools/mrc-usage-report + +# Build the tool +npm run build + +# Generate the HTML report with correct base path +npm start -- -g -f html -o ./reports -b /path/to/repository/root + +# Generate the JSON report with correct base path +npm start -- -g -f json -o ./reports -b /path/to/repository/root + +# Run the trend analysis script +node ./scripts/trend-analyzer.js +``` + +Note: The `-b` parameter is crucial as it tells the tool where to look for the MFEs to analyze. Without it, the tool will use the current directory as the base path, which may not contain any MFEs to analyze. + +### Trend Analysis + +The trend analysis feature tracks changes in component usage over time: + +- On the first run, it creates a baseline report +- On subsequent runs, it compares the current report with the previous one +- The analysis shows: + - New components added + - Components removed + - Components with significant usage changes + - Overall statistics changes + +The trend report is set as the landing page, with a link to the detailed component usage report. + +## License + +ISC diff --git a/tools/mrc-usage-report-maas-ops-ui/build/aggregator/dataAggregator.js b/tools/mrc-usage-report-maas-ops-ui/build/aggregator/dataAggregator.js new file mode 100644 index 000000000..c52d220e6 --- /dev/null +++ b/tools/mrc-usage-report-maas-ops-ui/build/aggregator/dataAggregator.js @@ -0,0 +1,154 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.DataAggregator = void 0; +/** + * Aggregates component usage data into statistics + */ +class DataAggregator { + /** + * Aggregates component usage data into statistics + * @param usages Array of component usages + * @param config Analysis configuration + * @param allComponents All available MRC components + * @param mrcVersions MRC version information by MFE + * @returns Report data + */ + aggregate(usages, config, allComponents, mrcVersions) { + var _a, _b, _c; + // Group usages by component name + const usagesByComponent = new Map(); + for (const usage of usages) { + const { componentName } = usage; + if (!usagesByComponent.has(componentName)) { + usagesByComponent.set(componentName, []); + } + usagesByComponent.get(componentName).push(usage); + } + // Generate component stats + const componentStats = []; + for (const [componentName, componentUsages] of usagesByComponent.entries()) { + // Count usages by MFE + const usagesByMfe = {}; + for (const usage of componentUsages) { + usagesByMfe[usage.mfe] = (usagesByMfe[usage.mfe] || 0) + 1; + } + // Count prop usage + const propCounts = new Map(); + for (const usage of componentUsages) { + for (const prop of usage.props) { + propCounts.set(prop.name, (propCounts.get(prop.name) || 0) + 1); + } + } + // Get most common props + const commonProps = Array.from(propCounts.entries()) + .map(([name, count]) => ({ name, count })) + .sort((a, b) => b.count - a.count) + .slice(0, 10); + // Get files where the component is used + const files = Array.from(new Set(componentUsages.map((usage) => usage.filePath))); + // Count customization stats + let styledComponentCount = 0; + let customStylesCount = 0; + const overriddenPropertiesCounts = {}; + for (const usage of componentUsages) { + if ((_a = usage.customization) === null || _a === void 0 ? void 0 : _a.styledComponent) { + styledComponentCount++; + } + if ((_b = usage.customization) === null || _b === void 0 ? void 0 : _b.customStyles) { + customStylesCount++; + } + if ((_c = usage.customization) === null || _c === void 0 ? void 0 : _c.overriddenProperties) { + for (const prop of usage.customization.overriddenProperties) { + overriddenPropertiesCounts[prop] = (overriddenPropertiesCounts[prop] || 0) + 1; + } + } + } + // Add component stats + componentStats.push({ + componentName, + totalUsages: componentUsages.length, + usagesByMfe, + commonProps, + files, + customization: { + styledComponentCount, + customStylesCount, + overriddenPropertiesCounts + } + }); + } + // Sort component stats by total usages + componentStats.sort((a, b) => b.totalUsages - a.totalUsages); + // Generate overall stats + const totalUsages = usages.length; + // Most used components + const mostUsedComponents = componentStats.slice(0, 10).map((stats) => ({ + name: stats.componentName, + count: stats.totalUsages + })); + // Most used props + const allPropCounts = new Map(); + for (const usage of usages) { + for (const prop of usage.props) { + allPropCounts.set(prop.name, (allPropCounts.get(prop.name) || 0) + 1); + } + } + const mostUsedProps = Array.from(allPropCounts.entries()) + .map(([name, count]) => ({ name, count })) + .sort((a, b) => b.count - a.count) + .slice(0, 10); + // MFE usage counts + const mfeUsages = {}; + for (const usage of usages) { + mfeUsages[usage.mfe] = (mfeUsages[usage.mfe] || 0) + 1; + } + // Find unused components + const usedComponentNames = new Set(componentStats.map((s) => s.componentName)); + const unusedComponents = allComponents.filter((comp) => !usedComponentNames.has(comp.name)); + // Find unused components by MFE + const unusedComponentsByMfe = {}; + // Initialize with all MFEs + for (const mfe of config.mfes) { + unusedComponentsByMfe[mfe] = []; + } + // For each component, check which MFEs don't use it + for (const component of allComponents) { + const stat = componentStats.find((s) => s.componentName === component.name); + if (!stat) { + // If component is not used at all, add to all MFEs + for (const mfe of config.mfes) { + unusedComponentsByMfe[mfe].push(component.name); + } + } + else { + // If component is used in some MFEs but not others + for (const mfe of config.mfes) { + if (!stat.usagesByMfe[mfe]) { + unusedComponentsByMfe[mfe].push(component.name); + } + } + } + } + // Generate report data + const reportData = { + generatedAt: new Date().toISOString(), + config, + mrcVersions, + componentStats, + unusedComponents, + unusedComponentsByMfe, + overallStats: { + totalUsages, + mostUsedComponents, + mostUsedProps, + mfeUsages, + totalUnusedComponents: unusedComponents.length + }, + rawData: { + componentUsages: usages + } + }; + return reportData; + } +} +exports.DataAggregator = DataAggregator; diff --git a/tools/mrc-usage-report-maas-ops-ui/build/index.js b/tools/mrc-usage-report-maas-ops-ui/build/index.js new file mode 100644 index 000000000..371f12f45 --- /dev/null +++ b/tools/mrc-usage-report-maas-ops-ui/build/index.js @@ -0,0 +1,194 @@ +#!/usr/bin/env node +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const commander_1 = require("commander"); +const path_1 = __importDefault(require("path")); +const chalk_1 = __importDefault(require("chalk")); +const fs_1 = __importDefault(require("fs")); +const fileScanner_1 = require("./scanner/fileScanner"); +const componentParser_1 = require("./parser/componentParser"); +const dataAggregator_1 = require("./aggregator/dataAggregator"); +const htmlReporter_1 = require("./reporter/htmlReporter"); +// Define the program +const program = new commander_1.Command(); +program + .name("mrc-usage-report") + .description("Generate a report on MRC component usage across MFEs") + .version("1.0.0") + .option("-o, --output ", "Output directory for the report", "./reports") + .option("-f, --formats ", "Comma-separated list of output formats (html, json, csv)", "html") + .option("-m, --mfes ", "Comma-separated list of MFEs to analyze", "maas-ops-react,infra") + .option("-r, --mrc-path ", "Path to the MRC repository", "../../maas-react-components") + .option("-b, --base-path ", "Base path for the project", process.cwd()) + .option("-s, --source ", "Source type for MRC components (local or github)", "local") + .option("-g, --github", "Use GitHub as the source for MRC components (shorthand for -s github)") + .option("--github-url ", "GitHub repository URL for MRC components", "https://github.com/SolaceDev/maas-react-components") + .option("--github-branch ", "Branch name for GitHub repository", "main"); +program.parse(process.argv); +const options = program.opts(); +// If -g flag is used, set source type to github +if (options.github) { + options.source = "github"; +} +// Main function +function main() { + return __awaiter(this, void 0, void 0, function* () { + try { + console.log(chalk_1.default.blue("MRC Component Usage Report Generator")); + console.log(chalk_1.default.gray("------------------------------------")); + // Parse options + const mfes = options.mfes + .split(",") + .map((mfe) => mfe.trim()) + .filter(Boolean); + const basePath = path_1.default.resolve(options.basePath); + const mrcPath = path_1.default.resolve(basePath, options.mrcPath); + const outputDir = path_1.default.resolve(options.output); + const outputFormats = options.formats.split(",").map((f) => f.trim()); + const mrcSourceType = options.source; + const mrcGithubUrl = options.githubUrl; + const mrcGithubBranch = options.githubBranch; + // Create config + const config = { + mfes, + mrcPath, + outputDir, + outputFormats, + mrcSourceType, + mrcGithubUrl, + mrcGithubBranch + }; + console.log(chalk_1.default.yellow("Configuration:")); + console.log(` Base Path: ${basePath}`); + console.log(` MRC Path: ${mrcPath}`); + console.log(` MFEs: ${mfes.join(", ")}`); + console.log(` Output Directory: ${outputDir}`); + console.log(` Output Formats: ${outputFormats.join(", ")}`); + console.log(` MRC Source Type: ${mrcSourceType}`); + if (mrcSourceType === "github") { + console.log(` MRC GitHub URL: ${mrcGithubUrl}`); + console.log(` MRC GitHub Branch: ${mrcGithubBranch}`); + } + console.log(""); + // Step 1: Scan for files + console.log(chalk_1.default.yellow("Step 1: Scanning for files...")); + const fileScanner = new fileScanner_1.FileScanner(basePath, mfes, mrcSourceType, mrcGithubUrl, mrcGithubBranch); + const files = yield fileScanner.scanForFiles(); + console.log(`Found ${files.length} files to analyze`); + // Step 2: Scan for MRC components + console.log(chalk_1.default.yellow("Step 2: Scanning for MRC components...")); + const allComponents = yield fileScanner.scanForMrcComponents(mrcPath); + console.log(`Found ${allComponents.length} MRC component files`); + // Step 3: Parse files for component usage + console.log(chalk_1.default.yellow("Step 3: Parsing files for component usage...")); + const componentParser = new componentParser_1.ComponentParser(mrcPath, mrcSourceType); + yield componentParser.initialize(allComponents); + let totalUsages = 0; + const allUsages = []; + for (let i = 0; i < files.length; i++) { + const file = files[i]; + const mfe = file.split(path_1.default.sep).find((part) => mfes.includes(part)) || ""; + try { + const usages = yield componentParser.parseFile(file, mfe); + totalUsages += usages.length; + allUsages.push(...usages); + // Log progress every 100 files + if ((i + 1) % 100 === 0 || i === files.length - 1) { + console.log(` Processed ${i + 1}/${files.length} files, found ${totalUsages} component usages so far`); + } + } + catch (error) { + console.error(`Error parsing file ${file}:`, error); + } + } + console.log(`Found ${totalUsages} total component usages`); + // Step 4: Detect MRC versions for each MFE + console.log(chalk_1.default.yellow("Step 4: Detecting MRC versions...")); + const mrcVersions = {}; + for (const mfe of mfes) { + try { + const mfePath = path_1.default.join(basePath, "micro-frontends", mfe); + const packageJsonPath = path_1.default.join(mfePath, "package.json"); + if (fs_1.default.existsSync(packageJsonPath)) { + const packageJson = JSON.parse(fs_1.default.readFileSync(packageJsonPath, "utf8")); + // Check dependencies and devDependencies + const dependencies = packageJson.dependencies || {}; + const devDependencies = packageJson.devDependencies || {}; + const mrcPackageName = "@SolaceDev/maas-react-components"; + if (dependencies[mrcPackageName]) { + mrcVersions[mfe] = dependencies[mrcPackageName]; + console.log(` ${mfe}: MRC version ${dependencies[mrcPackageName]}`); + } + else if (devDependencies[mrcPackageName]) { + mrcVersions[mfe] = devDependencies[mrcPackageName]; + console.log(` ${mfe}: MRC version ${devDependencies[mrcPackageName]}`); + } + else { + mrcVersions[mfe] = "not found"; + console.log(` ${mfe}: MRC version not found`); + } + } + else { + mrcVersions[mfe] = "package.json not found"; + console.log(` ${mfe}: package.json not found`); + } + } + catch (error) { + mrcVersions[mfe] = "error"; + console.error(` Error getting MRC version for ${mfe}:`, error); + } + } + // Step 5: Aggregate data + console.log(chalk_1.default.yellow("Step 5: Aggregating data...")); + const dataAggregator = new dataAggregator_1.DataAggregator(); + const reportData = dataAggregator.aggregate(allUsages, config, allComponents, mrcVersions); + console.log(`Generated report data with ${reportData.componentStats.length} component statistics`); + console.log(`Found ${reportData.unusedComponents.length} unused components`); + // Step 5: Generate report + console.log(chalk_1.default.yellow("Step 5: Generating report...")); + // Ensure output directory exists + if (!fs_1.default.existsSync(outputDir)) { + fs_1.default.mkdirSync(outputDir, { recursive: true }); + } + for (const format of outputFormats) { + const outputPath = path_1.default.join(outputDir, `mrc-maas-ops-ui-usage-report.${format}`); + if (format === "html") { + const htmlReporter = new htmlReporter_1.HtmlReporter(); + yield htmlReporter.generateReport(reportData, outputPath); + console.log(`HTML report generated at ${outputPath}`); + } + else if (format === "json") { + const jsonOutput = JSON.stringify(reportData, null, 2); + fs_1.default.writeFileSync(outputPath, jsonOutput); + console.log(`JSON report generated at ${outputPath}`); + } + else if (format === "csv") { + console.error("CSV format not yet implemented"); + // Do not exit, continue with other formats if any + } + else { + console.warn(`Unsupported output format: ${format}. Skipping.`); + } + } + console.log(chalk_1.default.green("Report generation completed successfully!")); + } + catch (error) { + console.error(chalk_1.default.red("Error generating report:"), error); + process.exit(1); + } + }); +} +// Run the main function +main(); diff --git a/tools/mrc-usage-report-maas-ops-ui/build/jsonMerger.js b/tools/mrc-usage-report-maas-ops-ui/build/jsonMerger.js new file mode 100644 index 000000000..ec22c3601 --- /dev/null +++ b/tools/mrc-usage-report-maas-ops-ui/build/jsonMerger.js @@ -0,0 +1,99 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.JsonMerger = void 0; +class JsonMerger { + /** + * Merges multiple ReportData objects into a single consolidated ReportData object. + * @param reports An array of ReportData objects to merge. + * @returns A new ReportData object containing the merged data. + */ + mergeReports(reports) { + if (reports.length === 0) { + throw new Error("No reports provided for merging."); + } + // Initialize with the first report's structure + const mergedReport = JSON.parse(JSON.stringify(reports[0])); + // Clear dynamic data for aggregation + mergedReport.componentStats = []; + mergedReport.overallStats.totalUsages = 0; + mergedReport.overallStats.mfeUsages = {}; + mergedReport.unusedComponents = []; + mergedReport.unusedComponentsByMfe = {}; + mergedReport.mrcVersions = {}; + const componentMap = new Map(); + const allUnusedComponents = []; + const allUnusedComponentsByMfe = {}; + for (const report of reports) { + // Aggregate componentStats + for (const compStat of report.componentStats) { + if (componentMap.has(compStat.componentName)) { + const existingStat = componentMap.get(compStat.componentName); + existingStat.totalUsages += compStat.totalUsages; + for (const mfe in compStat.usagesByMfe) { + existingStat.usagesByMfe[mfe] = + (existingStat.usagesByMfe[mfe] || 0) + compStat.usagesByMfe[mfe]; + } + existingStat.files.push(...compStat.files); + // Simple concatenation for commonProps, might need more sophisticated merging if props have complex structures + existingStat.commonProps.push(...compStat.commonProps); + existingStat.customization.styledComponentCount += + compStat.customization.styledComponentCount; + existingStat.customization.customStylesCount += compStat.customization.customStylesCount; + for (const prop in compStat.customization.overriddenPropertiesCounts) { + existingStat.customization.overriddenPropertiesCounts[prop] = + (existingStat.customization.overriddenPropertiesCounts[prop] || 0) + + compStat.customization.overriddenPropertiesCounts[prop]; + } + } + else { + componentMap.set(compStat.componentName, JSON.parse(JSON.stringify(compStat))); + } + } + // Aggregate overallStats + mergedReport.overallStats.totalUsages += report.overallStats.totalUsages; + for (const mfe in report.overallStats.mfeUsages) { + mergedReport.overallStats.mfeUsages[mfe] = + (mergedReport.overallStats.mfeUsages[mfe] || 0) + report.overallStats.mfeUsages[mfe]; + } + // Aggregate unusedComponents + allUnusedComponents.push(...report.unusedComponents); + // Aggregate unusedComponentsByMfe + for (const mfe in report.unusedComponentsByMfe) { + if (!allUnusedComponentsByMfe[mfe]) { + allUnusedComponentsByMfe[mfe] = []; + } + allUnusedComponentsByMfe[mfe].push(...report.unusedComponentsByMfe[mfe]); + } + // Aggregate mrcVersions (take the latest or first encountered for each MFE) + for (const mfe in report.mrcVersions) { + mergedReport.mrcVersions[mfe] = report.mrcVersions[mfe]; + } + } + mergedReport.componentStats = Array.from(componentMap.values()); + mergedReport.unusedComponents = this.deduplicateComponents(allUnusedComponents); + mergedReport.unusedComponentsByMfe = + this.deduplicateUnusedComponentsByMfe(allUnusedComponentsByMfe); + // Update generatedAt to current time + mergedReport.generatedAt = new Date().toISOString(); + return mergedReport; + } + deduplicateComponents(components) { + const seen = new Set(); + return components.filter((comp) => { + const key = `${comp.name}-${comp.path}`; + if (seen.has(key)) { + return false; + } + seen.add(key); + return true; + }); + } + deduplicateUnusedComponentsByMfe(unusedByMfe) { + const result = {}; + for (const mfe in unusedByMfe) { + result[mfe] = Array.from(new Set(unusedByMfe[mfe])); + } + return result; + } +} +exports.JsonMerger = JsonMerger; diff --git a/tools/mrc-usage-report-maas-ops-ui/build/merger/reportMerger.js b/tools/mrc-usage-report-maas-ops-ui/build/merger/reportMerger.js new file mode 100644 index 000000000..620360fd9 --- /dev/null +++ b/tools/mrc-usage-report-maas-ops-ui/build/merger/reportMerger.js @@ -0,0 +1,125 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.ReportMerger = void 0; +class ReportMerger { + mergeReports(report1, report2) { + const mergedReport = { + generatedAt: new Date().toISOString(), // Use current time for merged report + config: this.mergeConfigs(report1.config, report2.config), + mrcVersions: Object.assign(Object.assign({}, report1.mrcVersions), report2.mrcVersions), // Desktop versions overwrite maas-ops-ui + componentStats: this.mergeComponentStats(report1.componentStats, report2.componentStats), + unusedComponents: this.mergeUnusedComponents(report1.unusedComponents, report2.unusedComponents), + unusedComponentsByMfe: this.mergeUnusedComponentsByMfe(report1.unusedComponentsByMfe, report2.unusedComponentsByMfe), + overallStats: {}, // Will be recalculated + rawData: { + componentUsages: [...report1.rawData.componentUsages, ...report2.rawData.componentUsages] + } + }; + // Recalculate overallStats based on merged data + mergedReport.overallStats = this.recalculateOverallStats(mergedReport); + return mergedReport; + } + mergeConfigs(config1, config2) { + const mergedMfes = Array.from(new Set([...config1.mfes, ...config2.mfes])); + return Object.assign(Object.assign({}, config1), { mfes: mergedMfes }); + } + mergeComponentStats(stats1, stats2) { + const mergedStatsMap = new Map(); + [...stats1, ...stats2].forEach((stats) => { + if (mergedStatsMap.has(stats.componentName)) { + const existingStats = mergedStatsMap.get(stats.componentName); + existingStats.totalUsages += stats.totalUsages; + // Merge usagesByMfe + for (const mfe in stats.usagesByMfe) { + existingStats.usagesByMfe[mfe] = + (existingStats.usagesByMfe[mfe] || 0) + stats.usagesByMfe[mfe]; + } + // Merge commonProps + stats.commonProps.forEach((prop) => { + const existingProp = existingStats.commonProps.find((p) => p.name === prop.name); + if (existingProp) { + existingProp.count += prop.count; + } + else { + existingStats.commonProps.push(Object.assign({}, prop)); + } + }); + // Merge files and deduplicate + existingStats.files = Array.from(new Set([...existingStats.files, ...stats.files])); + // Merge customization + existingStats.customization.styledComponentCount += + stats.customization.styledComponentCount; + existingStats.customization.customStylesCount += stats.customization.customStylesCount; + for (const prop in stats.customization.overriddenPropertiesCounts) { + existingStats.customization.overriddenPropertiesCounts[prop] = + (existingStats.customization.overriddenPropertiesCounts[prop] || 0) + + stats.customization.overriddenPropertiesCounts[prop]; + } + } + else { + mergedStatsMap.set(stats.componentName, Object.assign({}, stats)); + } + }); + return Array.from(mergedStatsMap.values()); + } + mergeUnusedComponents(unused1, unused2) { + const allUnused = [...unused1, ...unused2]; + const uniqueUnused = new Map(); + allUnused.forEach((comp) => { + const key = `${comp.name}-${comp.path}`; + if (!uniqueUnused.has(key)) { + uniqueUnused.set(key, comp); + } + }); + return Array.from(uniqueUnused.values()); + } + mergeUnusedComponentsByMfe(unusedByMfe1, unusedByMfe2) { + const merged = {}; + // Add all from unusedByMfe1 + for (const mfe in unusedByMfe1) { + merged[mfe] = [...unusedByMfe1[mfe]]; + } + // Merge with unusedByMfe2 + for (const mfe in unusedByMfe2) { + if (merged[mfe]) { + merged[mfe] = Array.from(new Set([...merged[mfe], ...unusedByMfe2[mfe]])); + } + else { + merged[mfe] = [...unusedByMfe2[mfe]]; + } + } + return merged; + } + recalculateOverallStats(reportData) { + let totalUsages = 0; + const mfeUsages = {}; + const componentUsageMap = new Map(); + const propUsageMap = new Map(); + reportData.componentStats.forEach((compStat) => { + totalUsages += compStat.totalUsages; + componentUsageMap.set(compStat.componentName, compStat.totalUsages); + for (const mfe in compStat.usagesByMfe) { + mfeUsages[mfe] = (mfeUsages[mfe] || 0) + compStat.usagesByMfe[mfe]; + } + compStat.commonProps.forEach((prop) => { + propUsageMap.set(prop.name, (propUsageMap.get(prop.name) || 0) + prop.count); + }); + }); + const mostUsedComponents = Array.from(componentUsageMap.entries()) + .map(([name, count]) => ({ name, count })) + .sort((a, b) => b.count - a.count) + .slice(0, 10); // Top 10 + const mostUsedProps = Array.from(propUsageMap.entries()) + .map(([name, count]) => ({ name, count })) + .sort((a, b) => b.count - a.count) + .slice(0, 10); // Top 10 + return { + totalUsages: totalUsages, + mostUsedComponents: mostUsedComponents, + mostUsedProps: mostUsedProps, + mfeUsages: mfeUsages, + totalUnusedComponents: reportData.unusedComponents.length + }; + } +} +exports.ReportMerger = ReportMerger; diff --git a/tools/mrc-usage-report-maas-ops-ui/build/parser/componentParser.js b/tools/mrc-usage-report-maas-ops-ui/build/parser/componentParser.js new file mode 100644 index 000000000..ca288ff9c --- /dev/null +++ b/tools/mrc-usage-report-maas-ops-ui/build/parser/componentParser.js @@ -0,0 +1,331 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.ComponentParser = void 0; +const parser = __importStar(require("@babel/parser")); +const traverse_1 = __importDefault(require("@babel/traverse")); +const t = __importStar(require("@babel/types")); +const fs_1 = __importDefault(require("fs")); +const path_1 = __importDefault(require("path")); +/** + * Parses files for MRC component usage + */ +class ComponentParser { + constructor(mrcPath, mrcSourceType = "local") { + this.mrcPath = mrcPath; + this.mrcSourceType = mrcSourceType; + this.mrcComponentNames = new Set(); + this.mrcFileNames = new Set(); + this.exportNameToFileNameToExportName = new Map(); + this.fileNameToExportName = new Map(); + } + /** + * Initializes the parser by loading all MRC component names + * @param componentInfo Array of MRC component information with exported names + */ + initialize(componentInfo) { + return __awaiter(this, void 0, void 0, function* () { + for (const component of componentInfo) { + // Get both the exported name and the file name + const exportedName = component.name; + const fileName = path_1.default.basename(component.path); + const fileNameWithoutExt = path_1.default.parse(fileName).name; + // Add both to our sets + this.mrcComponentNames.add(exportedName); + this.mrcFileNames.add(fileNameWithoutExt); + // Create mappings between them + this.exportNameToFileNameToExportName.set(exportedName, fileNameWithoutExt); + this.fileNameToExportName.set(fileNameWithoutExt, exportedName); + } + console.log(`Loaded ${this.mrcComponentNames.size} MRC component names and ${this.mrcFileNames.size} file names`); + // Log some examples of the mappings for debugging + let count = 0; + for (const [exportedName, fileName] of this.exportNameToFileNameToExportName.entries()) { + if (exportedName !== fileName) { + console.log(`Export mapping: ${exportedName} -> ${fileName}`); + count++; + if (count >= 5) + break; // Just log a few examples + } + } + }); + } + /** + * Parses a file for MRC component usage + * @param filePath Path to the file to parse + * @param mfe The MFE the file belongs to + * @returns Array of component usages found in the file + */ + parseFile(filePath, mfe) { + return __awaiter(this, void 0, void 0, function* () { + try { + const content = fs_1.default.readFileSync(filePath, "utf8"); + const usages = []; + // Parse the file + const ast = parser.parse(content, { + sourceType: "module", + plugins: ["jsx", "typescript", "decorators-legacy"] + }); + // Track imported MRC components + const importedComponents = new Map(); // Map + // Track imported components that are considered used just by being imported + const importedComponentUsages = new Map(); + // Traverse the AST + (0, traverse_1.default)(ast, { + // Find imports from @SolaceDev/maas-react-components + ImportDeclaration: (path) => { + var _a; + const source = path.node.source.value; + if (source === "@SolaceDev/maas-react-components") { + // Get the line number of the import declaration + const lineNumber = ((_a = path.node.loc) === null || _a === void 0 ? void 0 : _a.start.line) || 0; + path.node.specifiers.forEach((specifier) => { + if (t.isImportSpecifier(specifier) && t.isIdentifier(specifier.imported)) { + const importedName = specifier.imported.name; + const localName = specifier.local.name; + let componentName = importedName; + let isComponent = false; + // First check if it's a direct match with an exported name + if (this.mrcComponentNames.has(importedName)) { + isComponent = true; + } + // Then check if it matches a file name and get the corresponding exported name + else if (this.mrcFileNames.has(importedName) && + this.fileNameToExportName.has(importedName)) { + componentName = this.fileNameToExportName.get(importedName); + isComponent = true; + // console.log(`Found component by file name: ${importedName} -> ${componentName}`); + } + if (isComponent) { + importedComponents.set(localName, componentName); + // Consider the component as used just by being imported + // This handles cases where components are imported but not used as JSX elements + importedComponentUsages.set(componentName, { + componentName: componentName, + filePath: filePath, + mfe: mfe, + lineNumber: lineNumber, + props: [], + customization: { + styledComponent: false, + customStyles: false, + overriddenProperties: [] + } + }); + } + } + }); + } + }, + // Find JSX elements that use MRC components + JSXOpeningElement: (path) => { + var _a; + let elementName = null; + if (t.isJSXIdentifier(path.node.name)) { + elementName = path.node.name.name; + } + else if (t.isJSXMemberExpression(path.node.name)) { + // Handle cases like + elementName = `${path.node.name.object.name}.${path.node.name.property.name}`; + } + if (elementName) { + let componentName = null; + // Check if the element name is an imported MRC component + if (importedComponents.has(elementName)) { + componentName = importedComponents.get(elementName); + } + else if (this.mrcComponentNames.has(elementName)) { + // Direct usage of an MRC component not necessarily imported with a local name + componentName = elementName; + } + else if (this.mrcFileNames.has(elementName) && + this.fileNameToExportName.has(elementName)) { + // Usage by file name + componentName = this.fileNameToExportName.get(elementName); + } + if (componentName) { + const props = []; + // Extract props + path.node.attributes.forEach((attr) => { + if (t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name)) { + const propName = attr.name.name; + let propType = "unknown"; + let propValue = undefined; + let isFunction = false; + let isJSX = false; + // Extract prop value and type + if (attr.value) { + if (t.isStringLiteral(attr.value)) { + propType = "string"; + propValue = attr.value.value; + } + else if (t.isJSXExpressionContainer(attr.value)) { + const expression = attr.value.expression; + if (t.isNumericLiteral(expression)) { + propType = "number"; + propValue = expression.value; + } + else if (t.isBooleanLiteral(expression)) { + propType = "boolean"; + propValue = expression.value; + } + else if (t.isNullLiteral(expression)) { + propType = "null"; + propValue = null; + } + else if (t.isObjectExpression(expression)) { + propType = "object"; + } + else if (t.isArrayExpression(expression)) { + propType = "array"; + } + else if (t.isArrowFunctionExpression(expression) || + t.isFunctionExpression(expression)) { + propType = "function"; + isFunction = true; + } + else if (t.isJSXElement(expression) || t.isJSXFragment(expression)) { + propType = "jsx"; + isJSX = true; + } + else if (t.isIdentifier(expression)) { + propType = "variable"; + propValue = expression.name; + } + } + } + props.push({ + name: propName, + type: propType, + value: propValue, + isFunction: isFunction, + isJSX: isJSX + }); + } + else if (t.isJSXSpreadAttribute(attr)) { + props.push({ + name: "...", + type: "spread", + isSpread: true + }); + } + }); + // Get line number + const lineNumber = ((_a = path.node.loc) === null || _a === void 0 ? void 0 : _a.start.line) || 0; + // Check for customization + const customization = this.detectCustomization(path); + usages.push({ + componentName, + filePath, + mfe, + lineNumber, + props, + customization + }); + // Remove from importedComponentUsages since it's directly used as JSX + importedComponentUsages.delete(componentName); + } + } + } + }); + // Add all imported components that weren't used as JSX elements + usages.push(...Array.from(importedComponentUsages.values())); + return usages; + } + catch (error) { + console.error(`Error parsing file ${filePath}:`, error); + return []; + } + }); + } + /** + * Detects if a component has custom styling or is a styled-component + * @param path The JSX element path + * @returns Customization information + */ + detectCustomization(path) { + const customization = { + styledComponent: false, + customStyles: false, + overriddenProperties: [] + }; + // Check if the component is wrapped in a styled-component + let parent = path.parentPath; + while (parent) { + if (parent.node.type === "VariableDeclarator" && + parent.node.init && + parent.node.init.type === "CallExpression" && + parent.node.init.callee && + parent.node.init.callee.type === "MemberExpression" && + parent.node.init.callee.object.name === "styled") { + customization.styledComponent = true; + break; + } + parent = parent.parentPath; + } + // Check for style props + path.node.attributes.forEach((attr) => { + if (t.isJSXAttribute(attr) && + t.isJSXIdentifier(attr.name) && + (attr.name.name === "style" || attr.name.name === "sx" || attr.name.name === "css")) { + customization.customStyles = true; + // Try to extract overridden properties + if (attr.value && + t.isJSXExpressionContainer(attr.value) && + t.isObjectExpression(attr.value.expression)) { + attr.value.expression.properties.forEach((prop) => { + if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) { + customization.overriddenProperties.push(prop.key.name); + } + }); + } + } + }); + return customization; + } +} +exports.ComponentParser = ComponentParser; diff --git a/tools/mrc-usage-report-maas-ops-ui/build/reporter/htmlReporter.js b/tools/mrc-usage-report-maas-ops-ui/build/reporter/htmlReporter.js new file mode 100644 index 000000000..6f2f687cd --- /dev/null +++ b/tools/mrc-usage-report-maas-ops-ui/build/reporter/htmlReporter.js @@ -0,0 +1,648 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.HtmlReporter = void 0; +const fs_1 = __importDefault(require("fs")); +const path_1 = __importDefault(require("path")); +/** + * Generates an HTML report from the component usage data + */ +class HtmlReporter { + /** + * Generates an HTML report from the component usage data + * @param reportData The report data + * @param outputPath The path to write the report to + */ + generateReport(reportData, outputPath) { + return __awaiter(this, void 0, void 0, function* () { + const html = this.generateHtml(reportData); + // Create the output directory if it doesn't exist + const outputDir = path_1.default.dirname(outputPath); + if (!fs_1.default.existsSync(outputDir)) { + fs_1.default.mkdirSync(outputDir, { recursive: true }); + } + // Write the HTML to the output file + fs_1.default.writeFileSync(outputPath, html); + console.log(`HTML report generated at ${outputPath}`); + }); + } + /** + * Generates the HTML for the report + * @param reportData The report data + * @returns The HTML string + */ + generateHtml(reportData) { + const { componentStats, overallStats, generatedAt, config } = reportData; + // Format date + const formattedDate = new Date(generatedAt).toLocaleString(); + // Generate HTML + return ` + + + + + + MRC Component Usage Report + + + + +
+
+

MRC Component Usage Report

+

Generated on ${formattedDate}

+
+ +
+
+

Total Component Usages

+

${overallStats.totalUsages}

+
+
+

MFEs Analyzed

+

${config.mfes.length}

+

${config.mfes.join(", ")}

+
+
+

Unique Components Used

+

${componentStats.length}

+
+
+

Unused Components

+

${reportData.unusedComponents.length}

+

Components not used in any MFE

+
+
+ +

Overview

+ +
+
Components
+
MFEs
+
Unused Components
+
+ +
+

Most Used Components

+
+ +
+ +
+ +

Component Details

+ ${componentStats + .map((stats) => ` +
+
+
+ ${stats.componentName} + ${stats.totalUsages} usages +
+ +
+
+

Usage by MFE

+ + + + + + + + + ${Object.entries(stats.usagesByMfe) + .map(([mfe, count]) => ` + + + + + `) + .join("")} + +
MFEUsages
${mfe}${count}
+ +

Common Props

+ + + + + + + + + ${stats.commonProps + .map((prop) => ` + + + + + `) + .join("")} + +
Prop NameOccurrences
${prop.name}${prop.count}
+ +

Customization

+

+ Styled Components: ${stats.customization.styledComponentCount}
+ Custom Styles: ${stats.customization.customStylesCount} +

+ + ${Object.keys(stats.customization.overriddenPropertiesCounts).length > 0 + ? ` +
Overridden Properties
+ + + + + + + + + ${Object.entries(stats.customization.overriddenPropertiesCounts) + .map(([prop, count]) => ` + + + + + `) + .join("")} + +
PropertyOccurrences
${prop}${count}
+ ` + : ""} + +

Files (${stats.files.length})

+
    + ${stats.files.map((file) => `
  • ${file}
  • `).join("")} +
+
+
+ `) + .join("")} +
+ +
+

Component Usage by MFE

+
+ + + + + + + + + + + ${Object.entries(overallStats.mfeUsages) + .map(([mfe, count]) => ` + + + + + + `) + .join("")} + +
MFEComponent UsagesMRC Version
${mfe}${count}${reportData.mrcVersions[mfe] || "N/A"}
+
+ + +
+

Unused Components (${reportData.unusedComponents.length})

+

These components are not used in any of the analyzed MFEs. Consider reviewing them for potential removal or promotion.

+ + + + + + + + + + ${reportData.unusedComponents + .map((comp) => ` + + + + + `) + .join("")} + +
Component NamePath
${comp.name}${comp.path}
+ +

Unused Components by MFE

+

These components are used in some MFEs but not in others. Consider standardizing component usage across MFEs.

+ + ${Object.entries(reportData.unusedComponentsByMfe) + .map(([mfe, components]) => ` +
+
+
+ ${mfe} + ${components.length} unused components +
+ +
+
+ + + + + + + + ${components + .map((comp) => ` + + + + `) + .join("")} + +
Component Name
${comp}
+
+
+ `) + .join("")} +
+ + +
+ + + + + `; + } +} +exports.HtmlReporter = HtmlReporter; diff --git a/tools/mrc-usage-report-maas-ops-ui/build/scanner/fileScanner.js b/tools/mrc-usage-report-maas-ops-ui/build/scanner/fileScanner.js new file mode 100644 index 000000000..3ab9c1fde --- /dev/null +++ b/tools/mrc-usage-report-maas-ops-ui/build/scanner/fileScanner.js @@ -0,0 +1,274 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.FileScanner = void 0; +const path_1 = __importDefault(require("path")); +const fs_1 = __importDefault(require("fs")); +const util_1 = require("util"); +const child_process_1 = require("child_process"); +const axios_1 = __importDefault(require("axios")); +const execPromise = (0, util_1.promisify)(child_process_1.exec); +// Regular expression to match export statements like: +// export { default as ComponentName } from "./path/to/Component"; +const EXPORT_REGEX = /export\s*{\s*default\s*as\s*([A-Za-z0-9_]+)\s*}\s*from\s*["'](.*?)["'];?/g; +/** + * Scans for files in the specified MFEs + */ +class FileScanner { + constructor(basePath, mfes, mrcSourceType = "local", mrcGithubUrl, mrcGithubBranch = "main") { + this.basePath = basePath; + this.mfes = mfes; + this.mrcSourceType = mrcSourceType; + this.mrcGithubUrl = mrcGithubUrl; + this.mrcGithubBranch = mrcGithubBranch; + } + /** + * Scans for all TypeScript and JavaScript files in the specified MFEs + * @returns Array of file paths + */ + scanForFiles() { + return __awaiter(this, void 0, void 0, function* () { + const allFiles = []; + for (const mfe of this.mfes) { + let mfePath; + if (mfe === "maas-ops-ui") { + mfePath = this.basePath; + } + else { + mfePath = path_1.default.join(this.basePath, "micro-frontends", mfe); + } + // Check if the MFE directory exists + if (!fs_1.default.existsSync(mfePath)) { + console.warn(`MFE directory not found: ${mfePath}`); + continue; + } + try { + // Use find command to locate all TypeScript and JavaScript files + const { stdout } = yield execPromise(`find ${mfePath}/src -type f \\( -name "*.ts" -o -name "*.tsx" -o -name "*.js" -o -name "*.jsx" \\) -not -path "*/node_modules/*" -not -name "*.d.ts" -not -name "*.test.*" -not -name "*.spec.*"`); + const files = stdout.trim().split("\n").filter(Boolean); + allFiles.push(...files); + } + catch (error) { + console.error(`Error scanning files in ${mfePath}:`, error); + } + } + return allFiles; + }); + } + /** + * Extracts owner and repo from GitHub URL + * @param githubUrl GitHub repository URL + * @returns Object containing owner and repo + */ + parseGithubUrl(githubUrl) { + // Handle URLs like https://github.com/owner/repo or git@github.com:owner/repo.git + const urlMatch = githubUrl.match(/github\.com[/:]([^/]+)\/([^/]+?)(\.git)?$/); + if (urlMatch) { + return { + owner: urlMatch[1], + repo: urlMatch[2].replace(/\.git$/, "") // Remove .git if present + }; + } + throw new Error(`Invalid GitHub URL: ${githubUrl}`); + } + /** + * Gets GitHub authentication token from environment variable + * @returns GitHub authentication token or undefined if not available + */ + getGithubToken() { + return process.env.GITHUB_TOKEN; + } + /** + * Fetches a file from GitHub API + * @param owner Repository owner + * @param repo Repository name + * @param path File path within the repository + * @param branch Branch name (default: main) + * @returns File content as string + */ + fetchFileFromGithub(owner, repo, path, branch) { + return __awaiter(this, void 0, void 0, function* () { + branch = branch || this.mrcGithubBranch; + try { + const url = `https://raw.githubusercontent.com/${owner}/${repo}/${branch}/${path}`; + console.log(`Fetching file from GitHub: ${url}`); + const headers = {}; + const token = this.getGithubToken(); + if (token) { + headers.Authorization = `token ${token}`; + } + const response = yield axios_1.default.get(url, { headers }); + return response.data; + } + catch (error) { + console.error(`Error fetching file from GitHub:`, error); + throw new Error(`Failed to fetch file from GitHub: ${path}`); + } + }); + } + /** + * Fetches directory contents from GitHub API + * @param owner Repository owner + * @param repo Repository name + * @param path Directory path within the repository + * @param branch Branch name (default: main) + * @returns Array of file paths + */ + fetchDirectoryFromGithub(owner, repo, path, branch) { + return __awaiter(this, void 0, void 0, function* () { + branch = branch || this.mrcGithubBranch; + try { + const url = `https://api.github.com/repos/${owner}/${repo}/contents/${path}?ref=${branch}`; + console.log(`Fetching directory from GitHub API: ${url}`); + const headers = { + Accept: "application/vnd.github.v3+json" + }; + const token = this.getGithubToken(); + if (token) { + headers.Authorization = `token ${token}`; + } + const response = yield axios_1.default.get(url, { headers }); + // Process the response to extract file paths + const files = []; + const processItems = (items) => __awaiter(this, void 0, void 0, function* () { + for (const item of items) { + if (item.type === "file") { + files.push(item.path); + } + else if (item.type === "dir") { + // Recursively fetch subdirectory contents + const subDirUrl = `https://api.github.com/repos/${owner}/${repo}/contents/${item.path}?ref=${branch}`; + const subDirResponse = yield axios_1.default.get(subDirUrl, { headers }); + yield processItems(subDirResponse.data); + } + } + }); + yield processItems(response.data); + return files; + } + catch (error) { + console.error(`Error fetching directory from GitHub:`, error); + throw new Error(`Failed to fetch directory from GitHub: ${path}`); + } + }); + } + /** + * Prepares the MRC path based on the source type. + * If source type is local, returns the provided path. + * If source type is github, returns null to indicate GitHub API usage. + */ + prepareMrcPath(mrcPath) { + return __awaiter(this, void 0, void 0, function* () { + // If source type is local, use the provided path + if (this.mrcSourceType === "local") { + return mrcPath; + } + // If source type is github, we'll use the GitHub API directly + if (this.mrcSourceType === "github" && this.mrcGithubUrl) { + console.log(`Using GitHub API for ${this.mrcGithubUrl}`); + return null; // Return null to indicate we're using GitHub API + } + throw new Error("Invalid MRC source configuration"); + }); + } + /** + * Scans for all MRC components in the MRC repository + * @param mrcPath Path to the MRC repository + * @returns Array of MRC component paths + */ + scanForMrcComponents(mrcPath) { + return __awaiter(this, void 0, void 0, function* () { + const allComponents = []; + // If using GitHub API + if (this.mrcSourceType === "github" && this.mrcGithubUrl) { + try { + const { owner, repo } = this.parseGithubUrl(this.mrcGithubUrl); + console.log(`Scanning for MRC components in GitHub repository: ${owner}/${repo}`); + // Fetch all files in the components directory to find index.ts/tsx files + const files = yield this.fetchDirectoryFromGithub(owner, repo, "src/components"); + for (const file of files) { + // Only process index.ts or index.tsx files + if (file.endsWith("index.ts") || file.endsWith("index.tsx")) { + const fileContent = yield this.fetchFileFromGithub(owner, repo, file, this.mrcGithubBranch); + // Reset regex lastIndex for each file + EXPORT_REGEX.lastIndex = 0; + const match = EXPORT_REGEX.exec(fileContent); + if (match && match[1] && match[2]) { + const componentName = match[1]; + const relativePath = match[2]; + // Construct the full path to the actual component file + // The relativePath is relative to the index.ts file's directory + const componentDir = path_1.default.dirname(file); + const fullComponentPath = path_1.default.join(componentDir, relativePath); + // Assuming .tsx or .ts extension for the actual component file + let finalComponentPath = fullComponentPath; + if (!finalComponentPath.endsWith(".ts") && !finalComponentPath.endsWith(".tsx")) { + // Try .tsx first, then .ts + // Note: For GitHub, we can't check fs.existsSync directly. + // We'll assume .tsx for now or rely on the parser to handle it. + // For a more robust solution, we'd need to list files in the componentDir on GitHub. + finalComponentPath += ".tsx"; // Default to .tsx for GitHub + } + allComponents.push({ name: componentName, path: finalComponentPath }); + } + } + } + console.log(`Found ${allComponents.length} component files in GitHub repository`); + return allComponents; + } + catch (error) { + console.error(`Error scanning MRC components from GitHub:`, error); + return []; + } + } + // If using local path + const actualMrcPath = yield this.prepareMrcPath(mrcPath); + if (actualMrcPath) { + const componentsPath = path_1.default.join(actualMrcPath, "src", "components"); + // Check if the components directory exists + if (!fs_1.default.existsSync(componentsPath)) { + throw new Error(`MRC components directory not found: ${componentsPath}`); + } + try { + // Use find command to locate all index.ts/tsx files within component subdirectories + const { stdout } = yield execPromise(`find ${componentsPath} -type f \\( -name "*.ts" -o -name "*.tsx" -o -name "*.js" -o -name "*.jsx" \\) -not -name "*.d.ts" -not -name "*.test.*" -not -name "*.spec.*"`); + const componentFiles = stdout.trim().split("\n").filter(Boolean); + for (const file of componentFiles) { + const fileName = path_1.default.basename(file); + const componentName = path_1.default.parse(fileName).name; + // Exclude components with "Props", "Utils", "Icon", "use" in their name, + // React hooks (starting with "use"), and files in table/components subdirectory + const shouldExclude = componentName.includes("Props") || + componentName.includes("Utils") || + componentName.toLowerCase().includes("utils") || + componentName.includes("Icon") || + componentName.startsWith("use") || + file.includes("/table/components/"); + if (!shouldExclude) { + allComponents.push({ name: componentName, path: file }); + } + } + console.log(`Found ${allComponents.length} MRC components from local path`); + return allComponents; + } + catch (error) { + console.error(`Error scanning MRC components:`, error); + return []; + } + } + throw new Error("Invalid MRC source configuration"); + }); + } +} +exports.FileScanner = FileScanner; diff --git a/tools/mrc-usage-report-maas-ops-ui/build/types.js b/tools/mrc-usage-report-maas-ops-ui/build/types.js new file mode 100644 index 000000000..c8ad2e549 --- /dev/null +++ b/tools/mrc-usage-report-maas-ops-ui/build/types.js @@ -0,0 +1,2 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/tools/mrc-usage-report-maas-ops-ui/package-lock.json b/tools/mrc-usage-report-maas-ops-ui/package-lock.json new file mode 100644 index 000000000..42e91c882 --- /dev/null +++ b/tools/mrc-usage-report-maas-ops-ui/package-lock.json @@ -0,0 +1,584 @@ +{ + "name": "mrc-usage-report-tool", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "mrc-usage-report-tool", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@babel/parser": "^7.24.7", + "@babel/traverse": "^7.24.7", + "@types/node": "^20.14.9", + "axios": "^1.7.2", + "chalk": "^5.3.0", + "commander": "^11.1.0", + "js-yaml": "^4.1.0" + }, + "devDependencies": { + "typescript": "^5.8.3" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/generator": { + "version": "7.27.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.5.tgz", + "integrity": "sha512-ZGhA37l0e/g2s1Cnzdix0O3aLYm66eF8aufiVteOgnwxgnRP8GoyMj7VWsgWnQbVKXyge7hqrFh2K2TQM6t1Hw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.27.5", + "@babel/types": "^7.27.3", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.27.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.7.tgz", + "integrity": "sha512-qnzXzDXdr/po3bOTbTIQZ7+TxNKxpkN5IifVLXS+r7qwynkZfPyjZfE7hCXbo7IoO9TNcSyibgONsf2HauUd3Q==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.27.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.7.tgz", + "integrity": "sha512-X6ZlfR/O/s5EQ/SnUSLzr+6kGnkg8HXGMzpgsMsrJVcfDtH1vIp6ctCN4eZ1LS5c0+te5Cb6Y514fASjMRJ1nw==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.27.5", + "@babel/parser": "^7.27.7", + "@babel/template": "^7.27.2", + "@babel/types": "^7.27.7", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.27.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.7.tgz", + "integrity": "sha512-8OLQgDScAOHXnAz2cV+RfzzNMipuLVBz2biuAJFMV9bfkNf393je3VM8CLkjQodW5+iWsSJdSgSWT6rsZoXHPw==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@types/node": { + "version": "20.19.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.1.tgz", + "integrity": "sha512-jJD50LtlD2dodAEO653i3YF04NWak6jN3ky+Ri3Em3mGR39/glWiboM/IePaRbgwSfqM1TpGXfAg8ohn/4dTgA==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.10.0.tgz", + "integrity": "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/chalk": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", + "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.3.tgz", + "integrity": "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + } + } +} diff --git a/tools/mrc-usage-report-maas-ops-ui/package.json b/tools/mrc-usage-report-maas-ops-ui/package.json new file mode 100644 index 000000000..3cd09fa8e --- /dev/null +++ b/tools/mrc-usage-report-maas-ops-ui/package.json @@ -0,0 +1,25 @@ +{ + "name": "mrc-usage-report-tool", + "version": "1.0.0", + "description": "A tool to analyze and report on MRC component usage.", + "main": "build/index.js", + "scripts": { + "build": "tsc", + "start": "node build/index.js -m maas-ops-react,infra" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "@babel/parser": "^7.24.7", + "@babel/traverse": "^7.24.7", + "@types/node": "^20.14.9", + "axios": "^1.7.2", + "chalk": "^5.3.0", + "commander": "^11.1.0", + "js-yaml": "^4.1.0" + }, + "devDependencies": { + "typescript": "^5.8.3" + } +} diff --git a/tools/mrc-usage-report-maas-ops-ui/src/aggregator/dataAggregator.ts b/tools/mrc-usage-report-maas-ops-ui/src/aggregator/dataAggregator.ts new file mode 100644 index 000000000..c17d71d91 --- /dev/null +++ b/tools/mrc-usage-report-maas-ops-ui/src/aggregator/dataAggregator.ts @@ -0,0 +1,176 @@ +import { ComponentUsage, ComponentStats, ReportData, AnalysisConfig } from "../types"; + +/** + * Aggregates component usage data into statistics + */ +export class DataAggregator { + /** + * Aggregates component usage data into statistics + * @param usages Array of component usages + * @param config Analysis configuration + * @param allComponents All available MRC components + * @param mrcVersions MRC version information by MFE + * @returns Report data + */ + aggregate( + usages: ComponentUsage[], + config: AnalysisConfig, + allComponents: { name: string; path: string }[], + mrcVersions: Record + ): ReportData { + // Group usages by component name + const usagesByComponent = new Map(); + + for (const usage of usages) { + const { componentName } = usage; + if (!usagesByComponent.has(componentName)) { + usagesByComponent.set(componentName, []); + } + usagesByComponent.get(componentName)!.push(usage); + } + + // Generate component stats + const componentStats: ComponentStats[] = []; + + for (const [componentName, componentUsages] of usagesByComponent.entries()) { + // Count usages by MFE + const usagesByMfe: Record = {}; + for (const usage of componentUsages) { + usagesByMfe[usage.mfe] = (usagesByMfe[usage.mfe] || 0) + 1; + } + + // Count prop usage + const propCounts = new Map(); + for (const usage of componentUsages) { + for (const prop of usage.props) { + propCounts.set(prop.name, (propCounts.get(prop.name) || 0) + 1); + } + } + + // Get most common props + const commonProps = Array.from(propCounts.entries()) + .map(([name, count]) => ({ name, count })) + .sort((a, b) => b.count - a.count) + .slice(0, 10); + + // Get files where the component is used + const files = Array.from(new Set(componentUsages.map((usage) => usage.filePath))); + + // Count customization stats + let styledComponentCount = 0; + let customStylesCount = 0; + const overriddenPropertiesCounts: Record = {}; + + for (const usage of componentUsages) { + if (usage.customization?.styledComponent) { + styledComponentCount++; + } + if (usage.customization?.customStyles) { + customStylesCount++; + } + if (usage.customization?.overriddenProperties) { + for (const prop of usage.customization.overriddenProperties) { + overriddenPropertiesCounts[prop] = (overriddenPropertiesCounts[prop] || 0) + 1; + } + } + } + + // Add component stats + componentStats.push({ + componentName, + totalUsages: componentUsages.length, + usagesByMfe, + commonProps, + files, + customization: { + styledComponentCount, + customStylesCount, + overriddenPropertiesCounts + } + }); + } + + // Sort component stats by total usages + componentStats.sort((a, b) => b.totalUsages - a.totalUsages); + + // Generate overall stats + const totalUsages = usages.length; + + // Most used components + const mostUsedComponents = componentStats.slice(0, 10).map((stats) => ({ + name: stats.componentName, + count: stats.totalUsages + })); + + // Most used props + const allPropCounts = new Map(); + for (const usage of usages) { + for (const prop of usage.props) { + allPropCounts.set(prop.name, (allPropCounts.get(prop.name) || 0) + 1); + } + } + + const mostUsedProps = Array.from(allPropCounts.entries()) + .map(([name, count]) => ({ name, count })) + .sort((a, b) => b.count - a.count) + .slice(0, 10); + + // MFE usage counts + const mfeUsages: Record = {}; + for (const usage of usages) { + mfeUsages[usage.mfe] = (mfeUsages[usage.mfe] || 0) + 1; + } + + // Find unused components + const usedComponentNames = new Set(componentStats.map((s) => s.componentName)); + const unusedComponents = allComponents.filter((comp) => !usedComponentNames.has(comp.name)); + + // Find unused components by MFE + const unusedComponentsByMfe: Record = {}; + // Initialize with all MFEs + for (const mfe of config.mfes) { + unusedComponentsByMfe[mfe] = []; + } + + // For each component, check which MFEs don't use it + for (const component of allComponents) { + const stat = componentStats.find((s) => s.componentName === component.name); + + if (!stat) { + // If component is not used at all, add to all MFEs + for (const mfe of config.mfes) { + unusedComponentsByMfe[mfe].push(component.name); + } + } else { + // If component is used in some MFEs but not others + for (const mfe of config.mfes) { + if (!stat.usagesByMfe[mfe]) { + unusedComponentsByMfe[mfe].push(component.name); + } + } + } + } + + // Generate report data + const reportData: ReportData = { + generatedAt: new Date().toISOString(), + config, + mrcVersions, + componentStats, + unusedComponents, + unusedComponentsByMfe, + overallStats: { + totalUsages, + mostUsedComponents, + mostUsedProps, + mfeUsages, + totalUnusedComponents: unusedComponents.length + }, + rawData: { + componentUsages: usages + } + }; + + return reportData; + } +} diff --git a/tools/mrc-usage-report-maas-ops-ui/src/index.ts b/tools/mrc-usage-report-maas-ops-ui/src/index.ts new file mode 100644 index 000000000..99db19c02 --- /dev/null +++ b/tools/mrc-usage-report-maas-ops-ui/src/index.ts @@ -0,0 +1,224 @@ +#!/usr/bin/env node + +import { Command } from "commander"; +import path from "path"; +import chalk from "chalk"; +import fs from "fs"; +import { FileScanner } from "./scanner/fileScanner"; +import { ComponentParser } from "./parser/componentParser"; +import { DataAggregator } from "./aggregator/dataAggregator"; +import { HtmlReporter } from "./reporter/htmlReporter"; +import { AnalysisConfig, MrcSourceType } from "./types"; + +// Define the program +const program = new Command(); + +program + .name("mrc-usage-report") + .description("Generate a report on MRC component usage across MFEs") + .version("1.0.0") + .option("-o, --output ", "Output directory for the report", "./reports") + .option( + "-f, --formats ", + "Comma-separated list of output formats (html, json, csv)", + "html" + ) + .option("-m, --mfes ", "Comma-separated list of MFEs to analyze", "maas-ops-react,infra") + .option("-r, --mrc-path ", "Path to the MRC repository", "../../maas-react-components") + .option("-b, --base-path ", "Base path for the project", process.cwd()) + .option("-s, --source ", "Source type for MRC components (local or github)", "local") + .option("-g, --github", "Use GitHub as the source for MRC components (shorthand for -s github)") + .option( + "--github-url ", + "GitHub repository URL for MRC components", + "https://github.com/SolaceDev/maas-react-components" + ) + .option("--github-branch ", "Branch name for GitHub repository", "main"); + +program.parse(process.argv); + +const options = program.opts(); + +// If -g flag is used, set source type to github +if (options.github) { + options.source = "github"; +} + +// Main function +async function main() { + try { + console.log(chalk.blue("MRC Component Usage Report Generator")); + console.log(chalk.gray("------------------------------------")); + + // Parse options + const mfes = options.mfes + .split(",") + .map((mfe: string) => mfe.trim()) + .filter(Boolean); + const basePath = path.resolve(options.basePath); + const mrcPath = path.resolve(basePath, options.mrcPath); + const outputDir = path.resolve(options.output); + const outputFormats = (options.formats as string).split(",").map((f) => f.trim()) as ( + | "html" + | "json" + | "yaml" + | "csv" + )[]; + const mrcSourceType = options.source as MrcSourceType; + const mrcGithubUrl = options.githubUrl; + const mrcGithubBranch = options.githubBranch; + + // Create config + const config: AnalysisConfig = { + mfes, + mrcPath, + outputDir, + outputFormats, + mrcSourceType, + mrcGithubUrl, + mrcGithubBranch + }; + + console.log(chalk.yellow("Configuration:")); + console.log(` Base Path: ${basePath}`); + console.log(` MRC Path: ${mrcPath}`); + console.log(` MFEs: ${mfes.join(", ")}`); + console.log(` Output Directory: ${outputDir}`); + console.log(` Output Formats: ${outputFormats.join(", ")}`); + console.log(` MRC Source Type: ${mrcSourceType}`); + if (mrcSourceType === "github") { + console.log(` MRC GitHub URL: ${mrcGithubUrl}`); + console.log(` MRC GitHub Branch: ${mrcGithubBranch}`); + } + console.log(""); + + // Step 1: Scan for files + console.log(chalk.yellow("Step 1: Scanning for files...")); + const fileScanner = new FileScanner( + basePath, + mfes, + mrcSourceType, + mrcGithubUrl, + mrcGithubBranch + ); + const files = await fileScanner.scanForFiles(); + console.log(`Found ${files.length} files to analyze`); + + // Step 2: Scan for MRC components + console.log(chalk.yellow("Step 2: Scanning for MRC components...")); + const allComponents = await fileScanner.scanForMrcComponents(mrcPath); + console.log(`Found ${allComponents.length} MRC component files`); + + // Step 3: Parse files for component usage + console.log(chalk.yellow("Step 3: Parsing files for component usage...")); + const componentParser = new ComponentParser(mrcPath, mrcSourceType); + await componentParser.initialize(allComponents); + + let totalUsages = 0; + const allUsages: any[] = []; + + for (let i = 0; i < files.length; i++) { + const file = files[i]; + const mfe = file.split(path.sep).find((part) => mfes.includes(part)) || ""; + + try { + const usages = await componentParser.parseFile(file, mfe); + totalUsages += usages.length; + allUsages.push(...usages); + + // Log progress every 100 files + if ((i + 1) % 100 === 0 || i === files.length - 1) { + console.log( + ` Processed ${i + 1}/${ + files.length + } files, found ${totalUsages} component usages so far` + ); + } + } catch (error) { + console.error(`Error parsing file ${file}:`, error); + } + } + + console.log(`Found ${totalUsages} total component usages`); + + // Step 4: Detect MRC versions for each MFE + console.log(chalk.yellow("Step 4: Detecting MRC versions...")); + const mrcVersions: Record = {}; + + for (const mfe of mfes) { + try { + const mfePath = path.join(basePath, "micro-frontends", mfe); + const packageJsonPath = path.join(mfePath, "package.json"); + + if (fs.existsSync(packageJsonPath)) { + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")); + + // Check dependencies and devDependencies + const dependencies = packageJson.dependencies || {}; + const devDependencies = packageJson.devDependencies || {}; + + const mrcPackageName = "@SolaceDev/maas-react-components"; + + if (dependencies[mrcPackageName]) { + mrcVersions[mfe] = dependencies[mrcPackageName]; + console.log(` ${mfe}: MRC version ${dependencies[mrcPackageName]}`); + } else if (devDependencies[mrcPackageName]) { + mrcVersions[mfe] = devDependencies[mrcPackageName]; + console.log(` ${mfe}: MRC version ${devDependencies[mrcPackageName]}`); + } else { + mrcVersions[mfe] = "not found"; + console.log(` ${mfe}: MRC version not found`); + } + } else { + mrcVersions[mfe] = "package.json not found"; + console.log(` ${mfe}: package.json not found`); + } + } catch (error) { + mrcVersions[mfe] = "error"; + console.error(` Error getting MRC version for ${mfe}:`, error); + } + } + + // Step 5: Aggregate data + console.log(chalk.yellow("Step 5: Aggregating data...")); + const dataAggregator = new DataAggregator(); + const reportData = dataAggregator.aggregate(allUsages, config, allComponents, mrcVersions); + console.log( + `Generated report data with ${reportData.componentStats.length} component statistics` + ); + console.log(`Found ${reportData.unusedComponents.length} unused components`); + + // Step 5: Generate report + console.log(chalk.yellow("Step 5: Generating report...")); + // Ensure output directory exists + if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir, { recursive: true }); + } + for (const format of outputFormats) { + const outputPath = path.join(outputDir, `mrc-maas-ops-ui-usage-report.${format}`); + + if (format === "html") { + const htmlReporter = new HtmlReporter(); + await htmlReporter.generateReport(reportData, outputPath); + console.log(`HTML report generated at ${outputPath}`); + } else if (format === "json") { + const jsonOutput = JSON.stringify(reportData, null, 2); + fs.writeFileSync(outputPath, jsonOutput); + console.log(`JSON report generated at ${outputPath}`); + } else if (format === "csv") { + console.error("CSV format not yet implemented"); + // Do not exit, continue with other formats if any + } else { + console.warn(`Unsupported output format: ${format}. Skipping.`); + } + } + + console.log(chalk.green("Report generation completed successfully!")); + } catch (error) { + console.error(chalk.red("Error generating report:"), error); + process.exit(1); + } +} + +// Run the main function +main(); diff --git a/tools/mrc-usage-report-maas-ops-ui/src/parser/componentParser.ts b/tools/mrc-usage-report-maas-ops-ui/src/parser/componentParser.ts new file mode 100644 index 000000000..f0670457e --- /dev/null +++ b/tools/mrc-usage-report-maas-ops-ui/src/parser/componentParser.ts @@ -0,0 +1,320 @@ +import * as parser from "@babel/parser"; +import traverse from "@babel/traverse"; +import * as t from "@babel/types"; +import fs from "fs"; +import path from "path"; +import { ComponentUsage, ComponentProp } from "../types"; +import { MrcSourceType } from "../types"; + +/** + * Parses files for MRC component usage + */ +export class ComponentParser { + private mrcComponentNames: Set; + private mrcFileNames: Set; + private mrcPath: string; + private mrcSourceType: MrcSourceType; + private exportNameToFileNameToExportName: Map; // Map + private fileNameToExportName: Map; // Map + + constructor(mrcPath: string, mrcSourceType: MrcSourceType = "local") { + this.mrcPath = mrcPath; + this.mrcSourceType = mrcSourceType; + this.mrcComponentNames = new Set(); + this.mrcFileNames = new Set(); + this.exportNameToFileNameToExportName = new Map(); + this.fileNameToExportName = new Map(); + } + + /** + * Initializes the parser by loading all MRC component names + * @param componentInfo Array of MRC component information with exported names + */ + async initialize(componentInfo: { name: string; path: string }[]): Promise { + for (const component of componentInfo) { + // Get both the exported name and the file name + const exportedName = component.name; + const fileName = path.basename(component.path); + const fileNameWithoutExt = path.parse(fileName).name; + + // Add both to our sets + this.mrcComponentNames.add(exportedName); + this.mrcFileNames.add(fileNameWithoutExt); + + // Create mappings between them + this.exportNameToFileNameToExportName.set(exportedName, fileNameWithoutExt); + this.fileNameToExportName.set(fileNameWithoutExt, exportedName); + } + + console.log( + `Loaded ${this.mrcComponentNames.size} MRC component names and ${this.mrcFileNames.size} file names` + ); + + // Log some examples of the mappings for debugging + let count = 0; + for (const [exportedName, fileName] of this.exportNameToFileNameToExportName.entries()) { + if (exportedName !== fileName) { + console.log(`Export mapping: ${exportedName} -> ${fileName}`); + count++; + if (count >= 5) break; // Just log a few examples + } + } + } + + /** + * Parses a file for MRC component usage + * @param filePath Path to the file to parse + * @param mfe The MFE the file belongs to + * @returns Array of component usages found in the file + */ + async parseFile(filePath: string, mfe: string): Promise { + try { + const content = fs.readFileSync(filePath, "utf8"); + const usages: ComponentUsage[] = []; + + // Parse the file + const ast = parser.parse(content, { + sourceType: "module", + plugins: ["jsx", "typescript", "decorators-legacy"] + }); + + // Track imported MRC components + const importedComponents = new Map(); // Map + // Track imported components that are considered used just by being imported + const importedComponentUsages = new Map(); + + // Traverse the AST + traverse(ast, { + // Find imports from @SolaceDev/maas-react-components + ImportDeclaration: (path) => { + const source = path.node.source.value; + if (source === "@SolaceDev/maas-react-components") { + // Get the line number of the import declaration + const lineNumber = path.node.loc?.start.line || 0; + + path.node.specifiers.forEach((specifier) => { + if (t.isImportSpecifier(specifier) && t.isIdentifier(specifier.imported)) { + const importedName = specifier.imported.name; + const localName = specifier.local.name; + + let componentName = importedName; + let isComponent = false; + + // First check if it's a direct match with an exported name + if (this.mrcComponentNames.has(importedName)) { + isComponent = true; + } + // Then check if it matches a file name and get the corresponding exported name + else if ( + this.mrcFileNames.has(importedName) && + this.fileNameToExportName.has(importedName) + ) { + componentName = this.fileNameToExportName.get(importedName)!; + isComponent = true; + // console.log(`Found component by file name: ${importedName} -> ${componentName}`); + } + + if (isComponent) { + importedComponents.set(localName, componentName); + + // Consider the component as used just by being imported + // This handles cases where components are imported but not used as JSX elements + importedComponentUsages.set(componentName, { + componentName: componentName, + filePath: filePath, + mfe: mfe, + lineNumber: lineNumber, + props: [], + customization: { + styledComponent: false, + customStyles: false, + overriddenProperties: [] + } + }); + } + } + }); + } + }, + + // Find JSX elements that use MRC components + JSXOpeningElement: (path) => { + let elementName: string | null = null; + + if (t.isJSXIdentifier(path.node.name)) { + elementName = path.node.name.name; + } else if (t.isJSXMemberExpression(path.node.name)) { + // Handle cases like + elementName = `${(path.node.name.object as t.JSXIdentifier).name}.${ + (path.node.name.property as t.JSXIdentifier).name + }`; + } + + if (elementName) { + let componentName: string | null = null; + // Check if the element name is an imported MRC component + if (importedComponents.has(elementName)) { + componentName = importedComponents.get(elementName)!; + } else if (this.mrcComponentNames.has(elementName)) { + // Direct usage of an MRC component not necessarily imported with a local name + componentName = elementName; + } else if ( + this.mrcFileNames.has(elementName) && + this.fileNameToExportName.has(elementName) + ) { + // Usage by file name + componentName = this.fileNameToExportName.get(elementName)!; + } + + if (componentName) { + const props: ComponentProp[] = []; + + // Extract props + path.node.attributes.forEach((attr) => { + if (t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name)) { + const propName = attr.name.name; + let propType = "unknown"; + let propValue: any = undefined; + let isFunction = false; + let isJSX = false; + + // Extract prop value and type + if (attr.value) { + if (t.isStringLiteral(attr.value)) { + propType = "string"; + propValue = attr.value.value; + } else if (t.isJSXExpressionContainer(attr.value)) { + const expression = attr.value.expression; + if (t.isNumericLiteral(expression)) { + propType = "number"; + propValue = expression.value; + } else if (t.isBooleanLiteral(expression)) { + propType = "boolean"; + propValue = expression.value; + } else if (t.isNullLiteral(expression)) { + propType = "null"; + propValue = null; + } else if (t.isObjectExpression(expression)) { + propType = "object"; + } else if (t.isArrayExpression(expression)) { + propType = "array"; + } else if ( + t.isArrowFunctionExpression(expression) || + t.isFunctionExpression(expression) + ) { + propType = "function"; + isFunction = true; + } else if (t.isJSXElement(expression) || t.isJSXFragment(expression)) { + propType = "jsx"; + isJSX = true; + } else if (t.isIdentifier(expression)) { + propType = "variable"; + propValue = expression.name; + } + } + } + + props.push({ + name: propName, + type: propType, + value: propValue, + isFunction: isFunction, + isJSX: isJSX + }); + } else if (t.isJSXSpreadAttribute(attr)) { + props.push({ + name: "...", + type: "spread", + isSpread: true + }); + } + }); + + // Get line number + const lineNumber = path.node.loc?.start.line || 0; + + // Check for customization + const customization = this.detectCustomization(path); + + usages.push({ + componentName, + filePath, + mfe, + lineNumber, + props, + customization + }); + + // Remove from importedComponentUsages since it's directly used as JSX + importedComponentUsages.delete(componentName); + } + } + } + }); + + // Add all imported components that weren't used as JSX elements + usages.push(...Array.from(importedComponentUsages.values())); + + return usages; + } catch (error) { + console.error(`Error parsing file ${filePath}:`, error); + return []; + } + } + + /** + * Detects if a component has custom styling or is a styled-component + * @param path The JSX element path + * @returns Customization information + */ + private detectCustomization(path: any): ComponentUsage["customization"] { + const customization: ComponentUsage["customization"] = { + styledComponent: false, + customStyles: false, + overriddenProperties: [] + }; + + // Check if the component is wrapped in a styled-component + let parent = path.parentPath; + while (parent) { + if ( + parent.node.type === "VariableDeclarator" && + parent.node.init && + parent.node.init.type === "CallExpression" && + parent.node.init.callee && + parent.node.init.callee.type === "MemberExpression" && + (parent.node.init.callee.object as t.Identifier).name === "styled" + ) { + customization.styledComponent = true; + break; + } + parent = parent.parentPath; + } + + // Check for style props + path.node.attributes.forEach((attr: any) => { + if ( + t.isJSXAttribute(attr) && + t.isJSXIdentifier(attr.name) && + (attr.name.name === "style" || attr.name.name === "sx" || attr.name.name === "css") + ) { + customization.customStyles = true; + + // Try to extract overridden properties + if ( + attr.value && + t.isJSXExpressionContainer(attr.value) && + t.isObjectExpression(attr.value.expression) + ) { + attr.value.expression.properties.forEach((prop: any) => { + if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) { + customization.overriddenProperties.push(prop.key.name); + } + }); + } + } + }); + + return customization; + } +} diff --git a/tools/mrc-usage-report-maas-ops-ui/src/reporter/htmlReporter.ts b/tools/mrc-usage-report-maas-ops-ui/src/reporter/htmlReporter.ts new file mode 100644 index 000000000..7fc8fa876 --- /dev/null +++ b/tools/mrc-usage-report-maas-ops-ui/src/reporter/htmlReporter.ts @@ -0,0 +1,658 @@ +import fs from "fs"; +import path from "path"; +import { ReportData } from "../types"; + +/** + * Generates an HTML report from the component usage data + */ +export class HtmlReporter { + /** + * Generates an HTML report from the component usage data + * @param reportData The report data + * @param outputPath The path to write the report to + */ + async generateReport(reportData: ReportData, outputPath: string): Promise { + const html = this.generateHtml(reportData); + + // Create the output directory if it doesn't exist + const outputDir = path.dirname(outputPath); + if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir, { recursive: true }); + } + + // Write the HTML to the output file + fs.writeFileSync(outputPath, html); + + console.log(`HTML report generated at ${outputPath}`); + } + + /** + * Generates the HTML for the report + * @param reportData The report data + * @returns The HTML string + */ + private generateHtml(reportData: ReportData): string { + const { componentStats, overallStats, generatedAt, config } = reportData; + + // Format date + const formattedDate = new Date(generatedAt).toLocaleString(); + + // Generate HTML + return ` + + + + + + MRC Component Usage Report + + + + +
+
+

MRC Component Usage Report

+

Generated on ${formattedDate}

+
+ +
+
+

Total Component Usages

+

${overallStats.totalUsages}

+
+
+

MFEs Analyzed

+

${config.mfes.length}

+

${config.mfes.join(", ")}

+
+
+

Unique Components Used

+

${componentStats.length}

+
+
+

Unused Components

+

${reportData.unusedComponents.length}

+

Components not used in any MFE

+
+
+ +

Overview

+ +
+
Components
+
MFEs
+
Unused Components
+
+ +
+

Most Used Components

+
+ +
+ +
+ +

Component Details

+ ${componentStats + .map( + (stats) => ` +
+
+
+ ${stats.componentName} + ${stats.totalUsages} usages +
+ +
+
+

Usage by MFE

+ + + + + + + + + ${Object.entries(stats.usagesByMfe) + .map( + ([mfe, count]) => ` + + + + + ` + ) + .join("")} + +
MFEUsages
${mfe}${count}
+ +

Common Props

+ + + + + + + + + ${stats.commonProps + .map( + (prop) => ` + + + + + ` + ) + .join("")} + +
Prop NameOccurrences
${prop.name}${prop.count}
+ +

Customization

+

+ Styled Components: ${stats.customization.styledComponentCount}
+ Custom Styles: ${stats.customization.customStylesCount} +

+ + ${ + Object.keys(stats.customization.overriddenPropertiesCounts).length > 0 + ? ` +
Overridden Properties
+ + + + + + + + + ${Object.entries(stats.customization.overriddenPropertiesCounts) + .map( + ([prop, count]) => ` + + + + + ` + ) + .join("")} + +
PropertyOccurrences
${prop}${count}
+ ` + : "" + } + +

Files (${stats.files.length})

+
    + ${stats.files.map((file) => `
  • ${file}
  • `).join("")} +
+
+
+ ` + ) + .join("")} +
+ +
+

Component Usage by MFE

+
+ + + + + + + + + + + ${Object.entries(overallStats.mfeUsages) + .map( + ([mfe, count]) => ` + + + + + + ` + ) + .join("")} + +
MFEComponent UsagesMRC Version
${mfe}${count}${reportData.mrcVersions[mfe] || "N/A"}
+
+ + +
+

Unused Components (${reportData.unusedComponents.length})

+

These components are not used in any of the analyzed MFEs. Consider reviewing them for potential removal or promotion.

+ + + + + + + + + + ${reportData.unusedComponents + .map( + (comp) => ` + + + + + ` + ) + .join("")} + +
Component NamePath
${comp.name}${comp.path}
+ +

Unused Components by MFE

+

These components are used in some MFEs but not in others. Consider standardizing component usage across MFEs.

+ + ${Object.entries(reportData.unusedComponentsByMfe) + .map( + ([mfe, components]) => ` +
+
+
+ ${mfe} + ${components.length} unused components +
+ +
+
+ + + + + + + + ${components + .map( + (comp) => ` + + + + ` + ) + .join("")} + +
Component Name
${comp}
+
+
+ ` + ) + .join("")} +
+ + +
+ + + + + `; + } +} diff --git a/tools/mrc-usage-report-maas-ops-ui/src/scanner/fileScanner.ts b/tools/mrc-usage-report-maas-ops-ui/src/scanner/fileScanner.ts new file mode 100644 index 000000000..a66169a46 --- /dev/null +++ b/tools/mrc-usage-report-maas-ops-ui/src/scanner/fileScanner.ts @@ -0,0 +1,315 @@ +import path from "path"; +import fs from "fs"; +import { promisify } from "util"; +import { exec } from "child_process"; +import os from "os"; +import axios from "axios"; +import { MrcSourceType } from "../types"; + +const execPromise = promisify(exec); + +// Regular expression to match export statements like: +// export { default as ComponentName } from "./path/to/Component"; +const EXPORT_REGEX = /export\s*{\s*default\s*as\s*([A-Za-z0-9_]+)\s*}\s*from\s*["'](.*?)["'];?/g; + +/** + * Scans for files in the specified MFEs + */ +export class FileScanner { + private basePath: string; + private mfes: string[]; + private mrcSourceType: MrcSourceType; + private mrcGithubUrl?: string; + private mrcGithubBranch: string; + private tempDir?: string; + + constructor( + basePath: string, + mfes: string[], + mrcSourceType: MrcSourceType = "local", + mrcGithubUrl?: string, + mrcGithubBranch: string = "main" + ) { + this.basePath = basePath; + this.mfes = mfes; + this.mrcSourceType = mrcSourceType; + this.mrcGithubUrl = mrcGithubUrl; + this.mrcGithubBranch = mrcGithubBranch; + } + + /** + * Scans for all TypeScript and JavaScript files in the specified MFEs + * @returns Array of file paths + */ + async scanForFiles(): Promise { + const allFiles: string[] = []; + + for (const mfe of this.mfes) { + let mfePath: string; + if (mfe === "maas-ops-ui") { + mfePath = this.basePath; + } else { + mfePath = path.join(this.basePath, "micro-frontends", mfe); + } + + // Check if the MFE directory exists + if (!fs.existsSync(mfePath)) { + console.warn(`MFE directory not found: ${mfePath}`); + continue; + } + + try { + // Use find command to locate all TypeScript and JavaScript files + const { stdout } = await execPromise( + `find ${mfePath}/src -type f \\( -name "*.ts" -o -name "*.tsx" -o -name "*.js" -o -name "*.jsx" \\) -not -path "*/node_modules/*" -not -name "*.d.ts" -not -name "*.test.*" -not -name "*.spec.*"` + ); + + const files = stdout.trim().split("\n").filter(Boolean); + allFiles.push(...files); + } catch (error) { + console.error(`Error scanning files in ${mfePath}:`, error); + } + } + + return allFiles; + } + + /** + * Extracts owner and repo from GitHub URL + * @param githubUrl GitHub repository URL + * @returns Object containing owner and repo + */ + private parseGithubUrl(githubUrl: string): { owner: string; repo: string } { + // Handle URLs like https://github.com/owner/repo or git@github.com:owner/repo.git + const urlMatch = githubUrl.match(/github\.com[/:]([^/]+)\/([^/]+?)(\.git)?$/); + if (urlMatch) { + return { + owner: urlMatch[1], + repo: urlMatch[2].replace(/\.git$/, "") // Remove .git if present + }; + } + throw new Error(`Invalid GitHub URL: ${githubUrl}`); + } + + /** + * Gets GitHub authentication token from environment variable + * @returns GitHub authentication token or undefined if not available + */ + private getGithubToken(): string | undefined { + return process.env.GITHUB_TOKEN; + } + + /** + * Fetches a file from GitHub API + * @param owner Repository owner + * @param repo Repository name + * @param path File path within the repository + * @param branch Branch name (default: main) + * @returns File content as string + */ + private async fetchFileFromGithub( + owner: string, + repo: string, + path: string, + branch?: string + ): Promise { + branch = branch || this.mrcGithubBranch; + try { + const url = `https://raw.githubusercontent.com/${owner}/${repo}/${branch}/${path}`; + console.log(`Fetching file from GitHub: ${url}`); + + const headers: Record = {}; + const token = this.getGithubToken(); + if (token) { + headers.Authorization = `token ${token}`; + } + + const response = await axios.get(url, { headers }); + return response.data; + } catch (error) { + console.error(`Error fetching file from GitHub:`, error); + throw new Error(`Failed to fetch file from GitHub: ${path}`); + } + } + + /** + * Fetches directory contents from GitHub API + * @param owner Repository owner + * @param repo Repository name + * @param path Directory path within the repository + * @param branch Branch name (default: main) + * @returns Array of file paths + */ + private async fetchDirectoryFromGithub( + owner: string, + repo: string, + path: string, + branch?: string + ): Promise { + branch = branch || this.mrcGithubBranch; + try { + const url = `https://api.github.com/repos/${owner}/${repo}/contents/${path}?ref=${branch}`; + console.log(`Fetching directory from GitHub API: ${url}`); + + const headers: Record = { + Accept: "application/vnd.github.v3+json" + }; + const token = this.getGithubToken(); + if (token) { + headers.Authorization = `token ${token}`; + } + + const response = await axios.get(url, { headers }); + + // Process the response to extract file paths + const files: string[] = []; + const processItems = async (items: any[]) => { + for (const item of items) { + if (item.type === "file") { + files.push(item.path); + } else if (item.type === "dir") { + // Recursively fetch subdirectory contents + const subDirUrl = `https://api.github.com/repos/${owner}/${repo}/contents/${item.path}?ref=${branch}`; + const subDirResponse = await axios.get(subDirUrl, { headers }); + await processItems(subDirResponse.data); + } + } + }; + + await processItems(response.data); + return files; + } catch (error) { + console.error(`Error fetching directory from GitHub:`, error); + throw new Error(`Failed to fetch directory from GitHub: ${path}`); + } + } + + /** + * Prepares the MRC path based on the source type. + * If source type is local, returns the provided path. + * If source type is github, returns null to indicate GitHub API usage. + */ + async prepareMrcPath(mrcPath: string): Promise { + // If source type is local, use the provided path + if (this.mrcSourceType === "local") { + return mrcPath; + } + + // If source type is github, we'll use the GitHub API directly + if (this.mrcSourceType === "github" && this.mrcGithubUrl) { + console.log(`Using GitHub API for ${this.mrcGithubUrl}`); + return null; // Return null to indicate we're using GitHub API + } + + throw new Error("Invalid MRC source configuration"); + } + + /** + * Scans for all MRC components in the MRC repository + * @param mrcPath Path to the MRC repository + * @returns Array of MRC component paths + */ + async scanForMrcComponents(mrcPath: string): Promise<{ name: string; path: string }[]> { + const allComponents: { name: string; path: string }[] = []; + + // If using GitHub API + if (this.mrcSourceType === "github" && this.mrcGithubUrl) { + try { + const { owner, repo } = this.parseGithubUrl(this.mrcGithubUrl); + console.log(`Scanning for MRC components in GitHub repository: ${owner}/${repo}`); + + // Fetch all files in the components directory to find index.ts/tsx files + const files = await this.fetchDirectoryFromGithub(owner, repo, "src/components"); + + for (const file of files) { + // Only process index.ts or index.tsx files + if (file.endsWith("index.ts") || file.endsWith("index.tsx")) { + const fileContent = await this.fetchFileFromGithub( + owner, + repo, + file, + this.mrcGithubBranch + ); + // Reset regex lastIndex for each file + EXPORT_REGEX.lastIndex = 0; + const match = EXPORT_REGEX.exec(fileContent); + if (match && match[1] && match[2]) { + const componentName = match[1]; + const relativePath = match[2]; + // Construct the full path to the actual component file + // The relativePath is relative to the index.ts file's directory + const componentDir = path.dirname(file); + const fullComponentPath = path.join(componentDir, relativePath); + + // Assuming .tsx or .ts extension for the actual component file + let finalComponentPath = fullComponentPath; + if (!finalComponentPath.endsWith(".ts") && !finalComponentPath.endsWith(".tsx")) { + // Try .tsx first, then .ts + // Note: For GitHub, we can't check fs.existsSync directly. + // We'll assume .tsx for now or rely on the parser to handle it. + // For a more robust solution, we'd need to list files in the componentDir on GitHub. + finalComponentPath += ".tsx"; // Default to .tsx for GitHub + } + + allComponents.push({ name: componentName, path: finalComponentPath }); + } + } + } + + console.log(`Found ${allComponents.length} component files in GitHub repository`); + return allComponents; + } catch (error) { + console.error(`Error scanning MRC components from GitHub:`, error); + return []; + } + } + + // If using local path + const actualMrcPath = await this.prepareMrcPath(mrcPath); + + if (actualMrcPath) { + const componentsPath = path.join(actualMrcPath, "src", "components"); + + // Check if the components directory exists + if (!fs.existsSync(componentsPath)) { + throw new Error(`MRC components directory not found: ${componentsPath}`); + } + + try { + // Use find command to locate all index.ts/tsx files within component subdirectories + const { stdout } = await execPromise( + `find ${componentsPath} -type f \\( -name "*.ts" -o -name "*.tsx" -o -name "*.js" -o -name "*.jsx" \\) -not -name "*.d.ts" -not -name "*.test.*" -not -name "*.spec.*"` + ); + + const componentFiles = stdout.trim().split("\n").filter(Boolean); + + for (const file of componentFiles) { + const fileName = path.basename(file); + const componentName = path.parse(fileName).name; + + // Exclude components with "Props", "Utils", "Icon", "use" in their name, + // React hooks (starting with "use"), and files in table/components subdirectory + const shouldExclude = + componentName.includes("Props") || + componentName.includes("Utils") || + componentName.toLowerCase().includes("utils") || + componentName.includes("Icon") || + componentName.startsWith("use") || + file.includes("/table/components/"); + + if (!shouldExclude) { + allComponents.push({ name: componentName, path: file }); + } + } + + console.log(`Found ${allComponents.length} MRC components from local path`); + return allComponents; + } catch (error) { + console.error(`Error scanning MRC components:`, error); + return []; + } + } + + throw new Error("Invalid MRC source configuration"); + } +} diff --git a/tools/mrc-usage-report-maas-ops-ui/src/types.ts b/tools/mrc-usage-report-maas-ops-ui/src/types.ts new file mode 100644 index 000000000..ae1869eaa --- /dev/null +++ b/tools/mrc-usage-report-maas-ops-ui/src/types.ts @@ -0,0 +1,72 @@ +export interface AnalysisConfig { + mfes: string[]; + mrcPath: string; + outputDir: string; + outputFormats: ("html" | "json" | "yaml" | "csv")[]; + mrcSourceType: MrcSourceType; + mrcGithubUrl?: string; + mrcGithubBranch?: string; +} + +export type MrcSourceType = "local" | "github"; + +export interface ComponentUsage { + componentName: string; + filePath: string; + mfe: string; + lineNumber: number; + props: ComponentProp[]; + customization: { + styledComponent: boolean; + customStyles: boolean; + overriddenProperties: string[]; + }; +} + +export interface ComponentProp { + name: string; + type: string; + value?: any; + isFunction?: boolean; + isJSX?: boolean; + isSpread?: boolean; +} + +export interface ComponentStats { + componentName: string; + totalUsages: number; + usagesByMfe: Record; + commonProps: { name: string; count: number }[]; + files: string[]; + customization: { + styledComponentCount: number; + customStylesCount: number; + overriddenPropertiesCounts: Record; + }; +} + +export interface ReportData { + generatedAt: string; + config: AnalysisConfig; + mrcVersions: Record; + componentStats: ComponentStats[]; + unusedComponents: { name: string; path: string }[]; + unusedComponentsByMfe: Record; + overallStats: OverallStats; + rawData: { + componentUsages: ComponentUsage[]; + }; +} + +export interface OverallStats { + totalUsages: number; + mostUsedComponents: { name: string; count: number }[]; + mostUsedProps: { name: string; count: number }[]; + mfeUsages: Record; + totalUnusedComponents: number; +} + +export interface MrcComponentInfo { + name: string; + path: string; +} diff --git a/tools/mrc-usage-report-maas-ops-ui/tsconfig.json b/tools/mrc-usage-report-maas-ops-ui/tsconfig.json new file mode 100644 index 000000000..ef068a05b --- /dev/null +++ b/tools/mrc-usage-report-maas-ops-ui/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "es2016", + "module": "commonjs", + "outDir": "./build", + "rootDir": "./src", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true + }, + "include": ["src/**/*.ts", "src/jsonMerger.ts"], + "exclude": ["node_modules"] +} diff --git a/tools/mrc-usage-report-maas-ui/README.md b/tools/mrc-usage-report-maas-ui/README.md new file mode 100644 index 000000000..e0a4e4456 --- /dev/null +++ b/tools/mrc-usage-report-maas-ui/README.md @@ -0,0 +1,211 @@ +# MRC Usage Report + +A tool to analyze and report on the usage of MRC (maas-react-components) components across different micro-frontends. + +## Features + +- Scans TypeScript/JavaScript files for MRC component usage +- Analyzes how components are used (props, customization, etc.) +- Identifies unused components (globally and per MFE) +- Detects components that are imported but not directly used as JSX elements +- Generates detailed HTML reports with interactive charts +- Configurable to analyze specific MFEs +- Supports different output formats (HTML, JSON, YAML) +- Includes trend analysis to track component usage changes over time +- Can be run as a GitHub Action with automatic GitHub Pages deployment + +## Installation + +1. Navigate to the tool directory: + +```bash +cd tools/mrc-usage-report +``` + +2. Install dependencies: + +```bash +npm install +``` + +3. Build the tool: + +```bash +npm run build +``` + +## Usage + +Run the tool with default settings: + +```bash +npm start +``` + +This will analyze all MFEs (except api-products) and generate an HTML report in the `./reports` directory. + +### Command Line Options + +You can customize the behavior with the following options: + +``` +Options: + -o, --output Output directory for the report (default: "./reports") + -f, --format Output format (html, json, yaml, csv) (default: "html") + -m, --mfes Comma-separated list of MFEs to analyze (default: "ep,intg,mc,saas") + -r, --mrc-path Path to the MRC repository (default: "../maas-react-components") + -b, --base-path Base path for the project (default: current working directory) + -s, --source Source type for MRC components (local or github) (default: "local") + -g, --github Use GitHub as the source for MRC components (shorthand for -s github) + --github-url GitHub repository URL for MRC components (default: "https://github.com/SolaceDev/maas-react-components") + --github-branch Branch name for GitHub repository (default: "main") + -h, --help Display help for command + -V, --version Output the version number +``` + +### Examples + +Analyze only the 'ep' and 'saas' MFEs: + +```bash +npm start -- -m ep,saas +``` + +Generate a JSON or YAML report: + +```bash +# JSON format +npm start -- -f json + +# YAML format +npm start -- -f yaml +``` + +Specify custom paths: + +```bash +npm start -- -b /path/to/project -r /path/to/mrc -o /path/to/output +``` + +Use GitHub as the source for MRC components: + +```bash +# Using the -g flag (shorthand) +GITHUB_TOKEN=your_github_token npm start -- -g + +# Or using the --source option +GITHUB_TOKEN=your_github_token npm start -- -s github + +# Optionally specify a different GitHub repository URL +GITHUB_TOKEN=your_github_token npm start -- -g --github-url https://github.com/your-org/your-repo + +# Optionally specify a different branch name +GITHUB_TOKEN=your_github_token npm start -- -g --github-branch develop +``` + +This is particularly useful in CI/CD environments or GitHub Actions where you don't want to clone the repository manually. + +> **Note:** If the MRC repository is private, you need to provide a GitHub personal access token with the `repo` scope via the `GITHUB_TOKEN` environment variable. This token is used to authenticate with the GitHub API. + +## Report Structure + +The HTML report includes: + +- Summary statistics (total usages, MFEs analyzed, unique components, unused components) +- Interactive charts showing component usage distribution +- Detailed breakdown of each component's usage +- Analysis of props used with each component +- Information about customization and styling overrides +- File references where components are used +- List of unused components (not used in any MFE) +- Per-MFE analysis of unused components (components used in some MFEs but not others) +- MRC version information for each MFE + +## Component Usage Detection + +The tool detects component usage in two ways: + +1. **Direct JSX Usage**: When a component is used directly in JSX elements within a file. +2. **Import-Only Usage**: When a component is imported from the MRC library but not directly used as a JSX element in the same file. This accounts for components that might be: + - Used conditionally in code paths + - Passed as props to other components + - Imported for future use or as a precaution + - Used in ways other than direct JSX elements + +## Development + +### Project Structure + +- `src/index.ts` - Main entry point +- `src/types.ts` - TypeScript interfaces +- `src/scanner/` - File scanning functionality +- `src/parser/` - Code parsing and analysis +- `src/aggregator/` - Data aggregation and statistics +- `src/reporter/` - Report generation + +### Adding New Features + +To add support for a new output format: + +1. Update the `outputFormat` type in `src/types.ts` +2. Add a new reporter class in `src/reporter/` +3. Update the report generation logic in `src/index.ts` + +## GitHub Action Integration + +This tool can be run automatically as a GitHub Action. A workflow file is included at `.github/workflows/mrc-usage-report.yml` that: + +1. Runs on every push to the main branch (and can be triggered manually) +2. Generates both HTML and JSON reports +3. Creates a trend analysis comparing the current report with previous reports +4. Publishes the reports to GitHub Pages + +### Setting Up GitHub Pages Deployment + +To enable the GitHub Pages deployment: + +1. Go to your repository settings +2. Navigate to "Pages" in the sidebar +3. Under "Build and deployment", select "GitHub Actions" as the source +4. The reports will be available at `https://[username].github.io/[repo-name]/mrc-usage-report/` + +### Testing Locally + +To test the report generation and trend analysis locally: + +```bash +# Navigate to the tool directory +cd tools/mrc-usage-report + +# Build the tool +npm run build + +# Generate the HTML report with correct base path +npm start -- -g -f html -o ./reports -b /path/to/repository/root + +# Generate the JSON report with correct base path +npm start -- -g -f json -o ./reports -b /path/to/repository/root + +# Run the trend analysis script +node ./scripts/trend-analyzer.js +``` + +Note: The `-b` parameter is crucial as it tells the tool where to look for the MFEs to analyze. Without it, the tool will use the current directory as the base path, which may not contain any MFEs to analyze. + +### Trend Analysis + +The trend analysis feature tracks changes in component usage over time: + +- On the first run, it creates a baseline report +- On subsequent runs, it compares the current report with the previous one +- The analysis shows: + - New components added + - Components removed + - Components with significant usage changes + - Overall statistics changes + +The trend report is set as the landing page, with a link to the detailed component usage report. + +## License + +ISC diff --git a/tools/mrc-usage-report-maas-ui/package-lock.json b/tools/mrc-usage-report-maas-ui/package-lock.json new file mode 100644 index 000000000..94db41625 --- /dev/null +++ b/tools/mrc-usage-report-maas-ui/package-lock.json @@ -0,0 +1,1273 @@ +{ + "name": "mrc-usage-report", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "mrc-usage-report", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@babel/parser": "^7.23.0", + "@babel/traverse": "^7.23.0", + "@babel/types": "^7.23.0", + "@types/js-yaml": "^4.0.9", + "axios": "^1.6.0", + "chalk": "^4.1.2", + "commander": "^11.0.0", + "glob": "^10.3.10", + "js-yaml": "^4.1.0" + }, + "devDependencies": { + "@types/babel__traverse": "^7.20.4", + "@types/node": "^20.8.2", + "ts-node": "^10.9.1", + "typescript": "^5.2.2" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.25.9", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/generator": { + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.10.tgz", + "integrity": "sha512-rRHT8siFIXQrAYOYqZQVsAr8vJ+cBNqcVAY6m5V8/4QqzaPl+zDBe6cLEPRDuNOUf3ww8RfJVlOyQMoSI+5Ang==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.26.10", + "@babel/types": "^7.26.10", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.10.tgz", + "integrity": "sha512-6aQR2zGE/QFi8JpDLjUZEPYOs7+mhKXm86VaKFiLP35JQwQb6bwUE+XbvkH0EptsYhbNBSUGaUBLKqxH1xSgsA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.26.10" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.9.tgz", + "integrity": "sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "@babel/parser": "^7.26.9", + "@babel/types": "^7.26.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.10.tgz", + "integrity": "sha512-k8NuDrxr0WrPH5Aupqb2LCVURP/S0vBEn5mK6iH+GIYob66U5EtoZvcdudR2jQ4cmTwhEwW1DLB+Yyas9zjF6A==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.10", + "@babel/parser": "^7.26.10", + "@babel/template": "^7.26.9", + "@babel/types": "^7.26.10", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.10.tgz", + "integrity": "sha512-emqcG3vHrpxUKTrxcblR36dcrcoRDvKmnL/dCL6ZsHaShW80qxCAcNhzQZrpeM765VzEos+xOi4s+r4IXzTwdQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.6", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", + "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/js-yaml": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", + "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.17.24", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.24.tgz", + "integrity": "sha512-d7fGCyB96w9BnWQrOsJtpyiSaBcAYYr75bnK6ZRjDbql2cGLj/3GsL5OYmLPNq76l7Gf2q4Rv9J2o6h5CrD9sA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/acorn": { + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", + "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.3.tgz", + "integrity": "sha512-iP4DebzoNlP/YN2dpwCgb8zoCmhtkajzS48JvwmkSkXvPI3DHc7m+XYL5tGnSlJtR6nImXZmdCuN5aP8dh1d8A==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", + "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/typescript": { + "version": "5.8.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz", + "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, + "license": "MIT" + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + } + } +} diff --git a/tools/mrc-usage-report-maas-ui/package.json b/tools/mrc-usage-report-maas-ui/package.json new file mode 100644 index 000000000..7ba687037 --- /dev/null +++ b/tools/mrc-usage-report-maas-ui/package.json @@ -0,0 +1,36 @@ +{ + "name": "mrc-usage-report", + "version": "1.0.0", + "description": "Tool to analyze MRC component usage across MFEs", + "main": "index.js", + "scripts": { + "build": "tsc", + "start": "node dist/index.js", + "dev": "ts-node src/index.ts", + "test": "jest" + }, + "keywords": [ + "component", + "analysis", + "report" + ], + "author": "", + "license": "ISC", + "dependencies": { + "@babel/parser": "^7.23.0", + "@babel/traverse": "^7.23.0", + "@babel/types": "^7.23.0", + "@types/js-yaml": "^4.0.9", + "axios": "^1.6.0", + "chalk": "^4.1.2", + "commander": "^11.0.0", + "glob": "^10.3.10", + "js-yaml": "^4.1.0" + }, + "devDependencies": { + "@types/babel__traverse": "^7.20.4", + "@types/node": "^20.8.2", + "ts-node": "^10.9.1", + "typescript": "^5.2.2" + } +} diff --git a/tools/mrc-usage-report-maas-ui/scripts/trend-analyzer.js b/tools/mrc-usage-report-maas-ui/scripts/trend-analyzer.js new file mode 100644 index 000000000..5b97fbc6e --- /dev/null +++ b/tools/mrc-usage-report-maas-ui/scripts/trend-analyzer.js @@ -0,0 +1,166 @@ +/** + * MRC Usage Report Trend Analyzer + * + * Analyzes trends between current and previous reports + * Generates a simple HTML report showing changes over time + */ + +const fs = require("fs"); +const path = require("path"); +const { generateTrendReport } = require("./trend-reporter"); + +// Paths +const REPORTS_DIR = path.join(__dirname, "../reports"); +const ARCHIVE_DIR = path.join(REPORTS_DIR, "archive"); +const CURRENT_REPORT_PATH = path.join(REPORTS_DIR, "mrc-usage-report.json"); +const INDEX_PATH = path.join(REPORTS_DIR, "index.html"); + +// Ensure reports directory exists +if (!fs.existsSync(REPORTS_DIR)) { + fs.mkdirSync(REPORTS_DIR, { recursive: true }); +} + +// Ensure archive directory exists +if (!fs.existsSync(ARCHIVE_DIR)) { + fs.mkdirSync(ARCHIVE_DIR, { recursive: true }); +} + +// Load current report +let currentReport; +try { + currentReport = require(CURRENT_REPORT_PATH); + + // Validate that the report has the expected structure + if (!currentReport.componentStats || !currentReport.overallStats || !currentReport.unusedComponents) { + throw new Error("Invalid report format: missing required fields"); + } +} catch (error) { + console.error(`Error loading current report: ${error.message}`); + console.error("Make sure the JSON report was generated successfully before running trend analysis"); + process.exit(1); +} + +// Find previous reports +const archiveFiles = fs + .readdirSync(ARCHIVE_DIR) + .filter((file) => file.startsWith("mrc-usage-report-") && file.endsWith(".json")) + .sort(); + +// First run - no previous data +if (archiveFiles.length <= 1) { + console.log("First run - creating initial report"); + + const simpleReport = { + generatedAt: new Date().toISOString(), + message: "First report - no trend data available yet", + summary: { + totalComponents: currentReport.componentStats.length, + totalUsages: currentReport.overallStats.totalUsages, + unusedComponents: currentReport.unusedComponents.length + }, + topComponents: currentReport.componentStats + .sort((a, b) => b.totalUsages - a.totalUsages) + .slice(0, 10) + .map((c) => ({ name: c.componentName, usages: c.totalUsages })) + }; + + // Generate HTML + const html = generateTrendReport(simpleReport); + fs.writeFileSync(INDEX_PATH, html); + console.log("Created initial trend report"); + process.exit(0); +} + +// Get previous report (most recent before current) +const previousReportFile = archiveFiles[archiveFiles.length - 2]; +let previousReport; +try { + previousReport = require(path.join(ARCHIVE_DIR, previousReportFile)); + + // Validate that the report has the expected structure + if (!previousReport.componentStats || !previousReport.overallStats || !previousReport.unusedComponents) { + throw new Error("Invalid previous report format: missing required fields"); + } +} catch (error) { + console.error(`Error loading previous report: ${error.message}`); + console.error("Creating a new baseline report instead"); + + // Create a simple report as if this is the first run + const simpleReport = { + generatedAt: new Date().toISOString(), + message: "First report - previous report was invalid or corrupted", + summary: { + totalComponents: currentReport.componentStats.length, + totalUsages: currentReport.overallStats.totalUsages, + unusedComponents: currentReport.unusedComponents.length + }, + topComponents: currentReport.componentStats + .sort((a, b) => b.totalUsages - a.totalUsages) + .slice(0, 10) + .map((c) => ({ name: c.componentName, usages: c.totalUsages })) + }; + + // Generate HTML + const html = generateTrendReport(simpleReport); + fs.writeFileSync(INDEX_PATH, html); + console.log("Created baseline report due to issues with previous report"); + process.exit(0); +} + +// Simple trend analysis +const trends = { + currentDate: new Date().toISOString(), + previousDate: previousReport.generatedAt, + summary: { + totalComponents: { + current: currentReport.componentStats.length, + previous: previousReport.componentStats.length, + change: currentReport.componentStats.length - previousReport.componentStats.length + }, + totalUsages: { + current: currentReport.overallStats.totalUsages, + previous: previousReport.overallStats.totalUsages, + change: currentReport.overallStats.totalUsages - previousReport.overallStats.totalUsages + }, + unusedComponents: { + current: currentReport.unusedComponents.length, + previous: previousReport.unusedComponents.length, + change: currentReport.unusedComponents.length - previousReport.unusedComponents.length + } + } +}; + +// Find new and removed components +const currentComponentNames = new Set(currentReport.componentStats.map((c) => c.componentName)); +const previousComponentNames = new Set(previousReport.componentStats.map((c) => c.componentName)); + +trends.newComponents = [...currentComponentNames].filter((name) => !previousComponentNames.has(name)); +trends.removedComponents = [...previousComponentNames].filter((name) => !currentComponentNames.has(name)); + +// Find components with significant usage changes +trends.changedComponents = []; + +for (const component of currentReport.componentStats) { + if (previousComponentNames.has(component.componentName)) { + const previousComponent = previousReport.componentStats.find((c) => c.componentName === component.componentName); + const change = component.totalUsages - previousComponent.totalUsages; + + if (Math.abs(change) > 0) { + trends.changedComponents.push({ + name: component.componentName, + current: component.totalUsages, + previous: previousComponent.totalUsages, + change: change, + percentChange: previousComponent.totalUsages > 0 ? ((change / previousComponent.totalUsages) * 100).toFixed(1) : "N/A" + }); + } + } +} + +// Sort by absolute change +trends.changedComponents.sort((a, b) => Math.abs(b.change) - Math.abs(a.change)); + +// Generate HTML report +const html = generateTrendReport(trends); +fs.writeFileSync(INDEX_PATH, html); +console.log("Trend analysis complete"); diff --git a/tools/mrc-usage-report-maas-ui/scripts/trend-reporter.js b/tools/mrc-usage-report-maas-ui/scripts/trend-reporter.js new file mode 100644 index 000000000..8453c3c75 --- /dev/null +++ b/tools/mrc-usage-report-maas-ui/scripts/trend-reporter.js @@ -0,0 +1,187 @@ +/** + * MRC Usage Report Trend Reporter + * + * Generates HTML reports for trend analysis + */ + +/** + * Generate HTML report for first run (no trend data) + * @param {Object} data Initial report data + * @returns {string} HTML content + */ +function generateInitialReport(data) { + return ` + + + + MRC Usage Report - Trends + + + +

MRC Component Usage Report

+

Generated on: ${new Date().toLocaleString()}

+ +
+

Summary

+

Total Components: ${data.summary.totalComponents}

+

Total Usages: ${data.summary.totalUsages}

+

Unused Components: ${data.summary.unusedComponents}

+

${data.message}

+
+ +
+

Top 10 Most Used Components

+ + + + + + ${data.topComponents + .map( + (c) => ` + + + + + ` + ) + .join("")} +
ComponentUsages
${c.name}${c.usages}
+
+ +
+

View Full Report

+

View detailed component usage report

+
+ + + `; +} + +/** + * Generate HTML report for trend analysis + * @param {Object} trends Trend analysis data + * @returns {string} HTML content + */ +function generateTrendReport(trends) { + // First run case + if (trends.message && trends.message.includes("First report")) { + return generateInitialReport(trends); + } + + // Regular trend report + return ` + + + + MRC Usage Report - Trends + + + +

MRC Component Usage Trends

+

Comparing reports from ${new Date(trends.previousDate).toLocaleDateString()} to ${new Date(trends.currentDate).toLocaleDateString()}

+ +
+

Summary Changes

+

Total Components: ${trends.summary.totalComponents.current} + + (${trends.summary.totalComponents.change >= 0 ? "+" : ""}${trends.summary.totalComponents.change}) + +

+

Total Usages: ${trends.summary.totalUsages.current} + + (${trends.summary.totalUsages.change >= 0 ? "+" : ""}${trends.summary.totalUsages.change}) + +

+

Unused Components: ${trends.summary.unusedComponents.current} + + (${trends.summary.unusedComponents.change >= 0 ? "+" : ""}${trends.summary.unusedComponents.change}) + +

+
+ +
+

New Components (${trends.newComponents.length})

+ ${ + trends.newComponents.length > 0 + ? ` +
    + ${trends.newComponents.map((name) => `
  • ${name}
  • `).join("")} +
+ ` + : "

No new components added since last report.

" + } +
+ +
+

Removed Components (${trends.removedComponents.length})

+ ${ + trends.removedComponents.length > 0 + ? ` +
    + ${trends.removedComponents.map((name) => `
  • ${name}
  • `).join("")} +
+ ` + : "

No components removed since last report.

" + } +
+ +
+

Components with Significant Usage Changes

+ ${ + trends.changedComponents.length > 0 + ? ` + + + + + + + + + ${trends.changedComponents + .slice(0, 20) + .map( + (c) => ` + + + + + + + + ` + ) + .join("")} +
ComponentPreviousCurrentChange% Change
${c.name}${c.previous}${c.current}${c.change >= 0 ? "+" : ""}${c.change}${c.change >= 0 ? "+" : ""}${c.percentChange}%
+ ` + : "

No significant usage changes detected.

" + } +
+ +
+

View Full Report

+

View detailed component usage report

+
+ + + `; +} + +module.exports = { + generateTrendReport +}; diff --git a/tools/mrc-usage-report-maas-ui/src/aggregator/dataAggregator.ts b/tools/mrc-usage-report-maas-ui/src/aggregator/dataAggregator.ts new file mode 100644 index 000000000..496c81ec4 --- /dev/null +++ b/tools/mrc-usage-report-maas-ui/src/aggregator/dataAggregator.ts @@ -0,0 +1,172 @@ +import { ComponentUsage, ComponentStats, ReportData, AnalysisConfig } from "../types"; + +/** + * Aggregates component usage data into statistics + */ +export class DataAggregator { + /** + * Aggregates component usage data into statistics + * @param usages Array of component usages + * @param config Analysis configuration + * @param allComponents All available MRC components + * @param mrcVersions MRC version information by MFE + * @returns Report data + */ + aggregate(usages: ComponentUsage[], config: AnalysisConfig, allComponents: { name: string; path: string }[], mrcVersions: Record): ReportData { + // Group usages by component name + const usagesByComponent = new Map(); + + for (const usage of usages) { + const { componentName } = usage; + if (!usagesByComponent.has(componentName)) { + usagesByComponent.set(componentName, []); + } + usagesByComponent.get(componentName)!.push(usage); + } + + // Generate component stats + const componentStats: ComponentStats[] = []; + + for (const [componentName, componentUsages] of usagesByComponent.entries()) { + // Count usages by MFE + const usagesByMfe: Record = {}; + for (const usage of componentUsages) { + usagesByMfe[usage.mfe] = (usagesByMfe[usage.mfe] || 0) + 1; + } + + // Count prop usage + const propCounts = new Map(); + for (const usage of componentUsages) { + for (const prop of usage.props) { + propCounts.set(prop.name, (propCounts.get(prop.name) || 0) + 1); + } + } + + // Get most common props + const commonProps = Array.from(propCounts.entries()) + .map(([name, count]) => ({ name, count })) + .sort((a, b) => b.count - a.count) + .slice(0, 10); + + // Get files where the component is used + const files = Array.from(new Set(componentUsages.map((usage) => usage.filePath))); + + // Count customization stats + let styledComponentCount = 0; + let customStylesCount = 0; + const overriddenPropertiesCounts: Record = {}; + + for (const usage of componentUsages) { + if (usage.customization?.styledComponent) { + styledComponentCount++; + } + if (usage.customization?.customStyles) { + customStylesCount++; + } + if (usage.customization?.overriddenProperties) { + for (const prop of usage.customization.overriddenProperties) { + overriddenPropertiesCounts[prop] = (overriddenPropertiesCounts[prop] || 0) + 1; + } + } + } + + // Add component stats + componentStats.push({ + componentName, + totalUsages: componentUsages.length, + usagesByMfe, + commonProps, + files, + customization: { + styledComponentCount, + customStylesCount, + overriddenPropertiesCounts + } + }); + } + + // Sort component stats by total usages + componentStats.sort((a, b) => b.totalUsages - a.totalUsages); + + // Generate overall stats + const totalUsages = usages.length; + + // Most used components + const mostUsedComponents = componentStats.slice(0, 10).map((stats) => ({ + name: stats.componentName, + count: stats.totalUsages + })); + + // Most used props + const propCounts = new Map(); + for (const usage of usages) { + for (const prop of usage.props) { + propCounts.set(prop.name, (propCounts.get(prop.name) || 0) + 1); + } + } + + const mostUsedProps = Array.from(propCounts.entries()) + .map(([name, count]) => ({ name, count })) + .sort((a, b) => b.count - a.count) + .slice(0, 10); + + // MFE usage counts + const mfeUsages: Record = {}; + for (const usage of usages) { + mfeUsages[usage.mfe] = (mfeUsages[usage.mfe] || 0) + 1; + } + + // Find unused components + const usedComponentNames = new Set(componentStats.map((stat) => stat.componentName)); + const unusedComponents = allComponents.filter((comp) => !usedComponentNames.has(comp.name)); + + // Find unused components by MFE + const unusedComponentsByMfe: Record = {}; + + // Initialize with all MFEs + for (const mfe of config.mfes) { + unusedComponentsByMfe[mfe] = []; + } + + // For each component, check which MFEs don't use it + for (const component of allComponents) { + const stat = componentStats.find((s) => s.componentName === component.name); + + if (!stat) { + // If component is not used at all, add to all MFEs + for (const mfe of config.mfes) { + unusedComponentsByMfe[mfe].push(component.name); + } + } else { + // If component is used in some MFEs but not others + for (const mfe of config.mfes) { + if (!stat.usagesByMfe[mfe]) { + unusedComponentsByMfe[mfe].push(component.name); + } + } + } + } + + // Generate report data + const reportData: ReportData = { + generatedAt: new Date().toISOString(), + config, + mrcVersions, + componentStats, + unusedComponents, + unusedComponentsByMfe, + overallStats: { + totalUsages, + mostUsedComponents, + mostUsedProps, + mfeUsages, + totalUnusedComponents: unusedComponents.length + }, + rawData: { + componentUsages: usages + } + }; + + return reportData; + } +} diff --git a/tools/mrc-usage-report-maas-ui/src/index.ts b/tools/mrc-usage-report-maas-ui/src/index.ts new file mode 100644 index 000000000..95fa6b302 --- /dev/null +++ b/tools/mrc-usage-report-maas-ui/src/index.ts @@ -0,0 +1,216 @@ +#!/usr/bin/env node + +import { Command } from "commander"; +import path from "path"; +import chalk from "chalk"; +import yaml from "js-yaml"; +import fs from "fs"; +import { FileScanner } from "./scanner/fileScanner"; +import { ComponentParser } from "./parser/componentParser"; +import { DataAggregator } from "./aggregator/dataAggregator"; +import { HtmlReporter } from "./reporter/htmlReporter"; +import { AnalysisConfig, MrcSourceType } from "./types"; + +// Define the program +const program = new Command(); + +program + .name("mrc-usage-report") + .description("Generate a report on MRC component usage across MFEs") + .version("1.0.0") + .option("-o, --output ", "Output directory for the report", "./reports") + .option("-f, --format ", "Output format (html, json, yaml, csv)", "html") + .option("-m, --mfes ", "Comma-separated list of MFEs to analyze", "ep,intg,mc,saas") + .option("-r, --mrc-path ", "Path to the MRC repository", "../../maas-react-components") + .option("-b, --base-path ", "Base path for the project", process.cwd()) + .option("-s, --source ", "Source type for MRC components (local or github)", "local") + .option("-g, --github", "Use GitHub as the source for MRC components (shorthand for -s github)") + .option("--github-url ", "GitHub repository URL for MRC components", "https://github.com/SolaceDev/maas-react-components") + .option("--github-branch ", "Branch name for GitHub repository", "main") + .parse(process.argv); + +const options = program.opts(); + +// If -g flag is used, set source type to github +if (options.github) { + options.source = "github"; +} + +// Main function +async function main() { + try { + console.log(chalk.blue("MRC Component Usage Report Generator")); + console.log(chalk.gray("----------------------------------------")); + + // Parse options + const mfes = options.mfes.split(",").filter(Boolean); + const basePath = path.resolve(options.basePath); + const mrcPath = path.resolve(basePath, options.mrcPath); + const outputDir = path.resolve(options.output); + const outputFormat = options.format as "html" | "json" | "yaml" | "csv"; + const mrcSourceType = options.source as MrcSourceType; + const mrcGithubUrl = options.githubUrl; + const mrcGithubBranch = options.githubBranch; + + // Create config + const config: AnalysisConfig = { + mfes, + mrcPath, + outputDir, + outputFormat, + mrcSourceType, + mrcGithubUrl, + mrcGithubBranch + }; + + console.log(chalk.yellow("Configuration:")); + console.log(` Base Path: ${basePath}`); + console.log(` MRC Path: ${mrcPath}`); + console.log(` MFEs: ${mfes.join(", ")}`); + console.log(` Output Directory: ${outputDir}`); + console.log(` Output Format: ${outputFormat}`); + console.log(` MRC Source Type: ${mrcSourceType}`); + if (mrcSourceType === "github") { + console.log(` MRC GitHub URL: ${mrcGithubUrl}`); + console.log(` MRC GitHub Branch: ${mrcGithubBranch}`); + } + console.log(""); + + // Step 1: Scan for files + console.log(chalk.yellow("Step 1: Scanning for files...")); + const fileScanner = new FileScanner(basePath, mfes, mrcSourceType, mrcGithubUrl, mrcGithubBranch); + const files = await fileScanner.scanForFiles(); + console.log(`Found ${files.length} files to analyze`); + + // Step 2: Scan for MRC components + console.log(chalk.yellow("Step 2: Scanning for MRC components...")); + const componentFiles = await fileScanner.scanForMrcComponents(mrcPath); + console.log(`Found ${componentFiles.length} MRC component files`); + + // Get component information + const allComponents = await fileScanner.getMrcComponentInfo(componentFiles, mrcPath); + + // Step 3: Parse files for component usage + console.log(chalk.yellow("Step 3: Parsing files for component usage...")); + const componentParser = new ComponentParser(mrcPath, mrcSourceType); + await componentParser.initialize(allComponents); + + let totalUsages = 0; + const allUsages = []; + + for (let i = 0; i < files.length; i++) { + const file = files[i]; + const mfe = file.split(path.sep).find((part) => mfes.includes(part)) || ""; + + try { + const usages = await componentParser.parseFile(file, mfe); + totalUsages += usages.length; + allUsages.push(...usages); + + // Log progress every 100 files + if ((i + 1) % 100 === 0 || i === files.length - 1) { + console.log(` Processed ${i + 1}/${files.length} files, found ${totalUsages} component usages so far`); + } + } catch (error) { + console.error(`Error parsing file ${file}:`, error); + } + } + + console.log(`Found ${totalUsages} total component usages`); + + // Step 4: Detect MRC versions for each MFE + console.log(chalk.yellow("Step 4: Detecting MRC versions...")); + const mrcVersions: Record = {}; + + for (const mfe of mfes) { + try { + const mfePath = path.join(basePath, "micro-frontends", mfe); + const packageJsonPath = path.join(mfePath, "package.json"); + + if (fs.existsSync(packageJsonPath)) { + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")); + + // Check dependencies and devDependencies + const dependencies = packageJson.dependencies || {}; + const devDependencies = packageJson.devDependencies || {}; + + // Look for MRC package + const mrcPackageName = "@SolaceDev/maas-react-components"; + + if (dependencies[mrcPackageName]) { + mrcVersions[mfe] = dependencies[mrcPackageName]; + console.log(` ${mfe}: MRC version ${dependencies[mrcPackageName]}`); + } else if (devDependencies[mrcPackageName]) { + mrcVersions[mfe] = devDependencies[mrcPackageName]; + console.log(` ${mfe}: MRC version ${devDependencies[mrcPackageName]}`); + } else { + mrcVersions[mfe] = "not found"; + console.log(` ${mfe}: MRC version not found`); + } + } else { + mrcVersions[mfe] = "package.json not found"; + console.log(` ${mfe}: package.json not found`); + } + } catch (error) { + mrcVersions[mfe] = "error"; + console.error(` Error getting MRC version for ${mfe}:`, error); + } + } + + // Step 5: Aggregate data + console.log(chalk.yellow("Step 5: Aggregating data...")); + const dataAggregator = new DataAggregator(); + const reportData = dataAggregator.aggregate(allUsages, config, allComponents, mrcVersions); + console.log(`Generated report data with ${reportData.componentStats.length} component statistics`); + console.log(`Found ${reportData.unusedComponents.length} unused components`); + + // Step 5: Generate report + console.log(chalk.yellow("Step 5: Generating report...")); + const outputPath = path.join(outputDir, `mrc-maas-ui-usage-report.${outputFormat}`); + + let outputFormats: string[]; + if (Array.isArray(options.format)) { + outputFormats = options.format; + } else if (typeof options.format === "string") { + // Split by comma and trim whitespace from each part + outputFormats = options.format + .split(",") + .map((format) => format.trim()) + .filter(Boolean); + } else { + // Handle cases where format might be undefined or null, default to ['html'] + outputFormats = ["html"]; + } + + for (const format of outputFormats) { + let currentOutputPath = path.join(outputDir, `mrc-maas-ui-usage-report.${format}`); + + if (format === "html") { + const htmlReporter = new HtmlReporter(); + await htmlReporter.generateReport(reportData, currentOutputPath); + console.log(`HTML report generated at ${currentOutputPath}`); + } else if (format === "json") { + const jsonOutput = JSON.stringify(reportData, null, 2); + require("fs").writeFileSync(currentOutputPath, jsonOutput); + console.log(`JSON report generated at ${currentOutputPath}`); + } else if (format === "yaml") { + const yamlOutput = yaml.dump(reportData, { indent: 2 }); + require("fs").writeFileSync(currentOutputPath, yamlOutput); + console.log(`YAML report generated at ${currentOutputPath}`); + } else if (format === "csv") { + console.error("CSV format not yet implemented"); + // Optionally, you could decide to exit or continue if other formats are requested + } else { + console.error(`Unsupported format: ${format}`); + } + } + + console.log(chalk.green("Report generation completed successfully!")); + } catch (error) { + console.error(chalk.red("Error generating report:"), error); + process.exit(1); + } +} + +// Run the main function +main(); diff --git a/tools/mrc-usage-report-maas-ui/src/parser/componentParser.ts b/tools/mrc-usage-report-maas-ui/src/parser/componentParser.ts new file mode 100644 index 000000000..7a06bf484 --- /dev/null +++ b/tools/mrc-usage-report-maas-ui/src/parser/componentParser.ts @@ -0,0 +1,291 @@ +import * as parser from "@babel/parser"; +import traverse from "@babel/traverse"; +import * as t from "@babel/types"; +import fs from "fs"; +import path from "path"; +import { ComponentUsage, ComponentProp } from "../types"; + +import { MrcSourceType } from "../types"; + +/** + * Parses files for MRC component usage + */ +export class ComponentParser { + private mrcComponentNames: Set; + private mrcFileNames: Set; + private mrcPath: string; + private mrcSourceType: MrcSourceType; + private exportNameToFileName: Map; + private fileNameToExportName: Map; + + constructor(mrcPath: string, mrcSourceType: MrcSourceType = "local") { + this.mrcPath = mrcPath; + this.mrcSourceType = mrcSourceType; + this.mrcComponentNames = new Set(); + this.mrcFileNames = new Set(); + this.exportNameToFileName = new Map(); + this.fileNameToExportName = new Map(); + } + + /** + * Initializes the parser by loading all MRC component names + * @param componentInfo Array of MRC component information with exported names + */ + async initialize(componentInfo: { name: string; path: string }[]): Promise { + for (const component of componentInfo) { + // Get both the exported name and the file name + const exportedName = component.name; + const fileName = path.basename(component.path); + const fileNameWithoutExt = path.parse(fileName).name; + + // Add both to our sets + this.mrcComponentNames.add(exportedName); + this.mrcFileNames.add(fileNameWithoutExt); + + // Create mappings between them + this.exportNameToFileName.set(exportedName, fileNameWithoutExt); + this.fileNameToExportName.set(fileNameWithoutExt, exportedName); + } + + console.log(`Loaded ${this.mrcComponentNames.size} MRC component names and ${this.mrcFileNames.size} file names`); + + // Log some examples of the mappings for debugging + let count = 0; + for (const [exportName, fileName] of this.exportNameToFileName.entries()) { + if (exportName !== fileName) { + console.log(`Export mapping: ${exportName} -> ${fileName}`); + count++; + if (count >= 5) break; // Just log a few examples + } + } + } + + /** + * Parses a file for MRC component usage + * @param filePath Path to the file to parse + * @param mfe The MFE the file belongs to + * @returns Array of component usages found in the file + */ + async parseFile(filePath: string, mfe: string): Promise { + try { + const content = fs.readFileSync(filePath, "utf-8"); + const usages: ComponentUsage[] = []; + + // Parse the file + const ast = parser.parse(content, { + sourceType: "module", + plugins: ["jsx", "typescript", "decorators-legacy"] + }); + + // Track imported MRC components + const importedComponents = new Map(); + // Track imported components that are considered used just by being imported + const importedComponentUsages = new Map(); + + // Traverse the AST + traverse(ast, { + // Find imports from @SolaceDev/maas-react-components + ImportDeclaration: (path) => { + const source = path.node.source.value; + if (source === "@SolaceDev/maas-react-components") { + // Get the line number of the import declaration + const lineNumber = path.node.loc?.start.line || 0; + + path.node.specifiers.forEach((specifier) => { + if (t.isImportSpecifier(specifier) && t.isIdentifier(specifier.imported)) { + const importedName = specifier.imported.name; + const localName = specifier.local.name; + + // Check if this is an MRC component by exported name or file name + let componentName = importedName; + let isComponent = false; + + // First check if it's a direct match with an exported name + if (this.mrcComponentNames.has(importedName)) { + isComponent = true; + } + // Then check if it matches a file name and get the corresponding export name + else if (this.mrcFileNames.has(importedName) && this.fileNameToExportName.has(importedName)) { + componentName = this.fileNameToExportName.get(importedName)!; + isComponent = true; + console.log(`Found component by file name: ${importedName} -> ${componentName}`); + } + + if (isComponent) { + importedComponents.set(localName, componentName); + + // Consider the component as used just by being imported + // This handles cases where components are imported but not used as JSX elements + importedComponentUsages.set(componentName, { + componentName, + filePath, + mfe, + lineNumber, + props: [], + customization: { + styledComponent: false, + customStyles: false, + overriddenProperties: [] + } + }); + } + } + }); + } + }, + + // Find JSX elements that use MRC components + JSXOpeningElement: (path) => { + const elementName = path.node.name; + let componentName: string | null = null; + + // Handle different types of JSX element names + if (t.isJSXIdentifier(elementName)) { + const localName = elementName.name; + componentName = importedComponents.get(localName) || null; + } + + // If this is an MRC component, extract usage information + if (componentName) { + const props: ComponentProp[] = []; + + // Extract props + path.node.attributes.forEach((attr) => { + if (t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name)) { + const propName = attr.name.name; + let propType = "unknown"; + let propValue: any = undefined; + let isFunction = false; + let isJSX = false; + + // Extract prop value and type + if (attr.value) { + if (t.isStringLiteral(attr.value)) { + propType = "string"; + propValue = attr.value.value; + } else if (t.isJSXExpressionContainer(attr.value)) { + const expression = attr.value.expression; + + if (t.isNumericLiteral(expression)) { + propType = "number"; + propValue = expression.value; + } else if (t.isBooleanLiteral(expression)) { + propType = "boolean"; + propValue = expression.value; + } else if (t.isNullLiteral(expression)) { + propType = "null"; + propValue = null; + } else if (t.isObjectExpression(expression)) { + propType = "object"; + } else if (t.isArrayExpression(expression)) { + propType = "array"; + } else if (t.isArrowFunctionExpression(expression) || t.isFunctionExpression(expression)) { + propType = "function"; + isFunction = true; + } else if (t.isJSXElement(expression) || t.isJSXFragment(expression)) { + propType = "jsx"; + isJSX = true; + } else if (t.isIdentifier(expression)) { + propType = "variable"; + propValue = expression.name; + } + } + } + + props.push({ + name: propName, + type: propType, + value: propValue, + isFunction, + isJSX + }); + } else if (t.isJSXSpreadAttribute(attr)) { + props.push({ + name: "...", + type: "spread", + isSpread: true + }); + } + }); + + // Get line number + const lineNumber = path.node.loc?.start.line || 0; + + // Check for customization + const customization = this.detectCustomization(path); + + // Add the usage + usages.push({ + componentName, + filePath, + mfe, + lineNumber, + props, + customization + }); + + // Remove from importedComponentUsages since we've found an actual JSX usage + importedComponentUsages.delete(componentName); + } + } + }); + + // Add all imported components that weren't used as JSX elements + usages.push(...importedComponentUsages.values()); + + return usages; + } catch (error) { + console.error(`Error parsing file ${filePath}:`, error); + return []; + } + } + + /** + * Detects if a component has custom styling or overrides + * @param path The JSX element path + * @returns Customization information + */ + private detectCustomization(path: any): ComponentUsage["customization"] { + const customization: ComponentUsage["customization"] = { + styledComponent: false, + customStyles: false, + overriddenProperties: [] + }; + + // Check if the component is wrapped in a styled component + let parent = path.parentPath; + while (parent) { + if ( + parent.node.type === "VariableDeclarator" && + parent.node.init && + parent.node.init.type === "CallExpression" && + parent.node.init.callee && + parent.node.init.callee.type === "MemberExpression" && + parent.node.init.callee.object && + parent.node.init.callee.object.name === "styled" + ) { + customization.styledComponent = true; + break; + } + parent = parent.parentPath; + } + + // Check for style prop + path.node.attributes.forEach((attr: any) => { + if (t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name) && (attr.name.name === "style" || attr.name.name === "sx" || attr.name.name === "css")) { + customization.customStyles = true; + + // Try to extract overridden properties + if (attr.value && t.isJSXExpressionContainer(attr.value) && t.isObjectExpression(attr.value.expression)) { + attr.value.expression.properties.forEach((prop: any) => { + if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) { + customization.overriddenProperties!.push(prop.key.name); + } + }); + } + } + }); + + return customization; + } +} diff --git a/tools/mrc-usage-report-maas-ui/src/reporter/htmlReporter.ts b/tools/mrc-usage-report-maas-ui/src/reporter/htmlReporter.ts new file mode 100644 index 000000000..ba83c6369 --- /dev/null +++ b/tools/mrc-usage-report-maas-ui/src/reporter/htmlReporter.ts @@ -0,0 +1,656 @@ +import fs from "fs"; +import path from "path"; +import { ReportData } from "../types"; + +/** + * Generates an HTML report from the component usage data + */ +export class HtmlReporter { + /** + * Generates an HTML report from the component usage data + * @param reportData The report data + * @param outputPath The path to write the report to + */ + async generateReport(reportData: ReportData, outputPath: string): Promise { + const html = this.generateHtml(reportData); + + // Create the output directory if it doesn't exist + const outputDir = path.dirname(outputPath); + if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir, { recursive: true }); + } + + // Write the HTML to the output file + fs.writeFileSync(outputPath, html); + + console.log(`HTML report generated at ${outputPath}`); + } + + /** + * Generates the HTML for the report + * @param reportData The report data + * @returns The HTML string + */ + private generateHtml(reportData: ReportData): string { + const { componentStats, overallStats, generatedAt, config } = reportData; + + // Format date + const formattedDate = new Date(generatedAt).toLocaleString(); + + // Generate HTML + return ` + + + + + + MRC Component Usage Report + + + + +
+
+

MRC Component Usage Report

+

Generated on ${formattedDate}

+
+ +
+
+

Total Component Usages

+

${overallStats.totalUsages}

+
+
+

MFEs Analyzed

+

${config.mfes.length}

+

${config.mfes.join(", ")}

+
+
+

Unique Components Used

+

${componentStats.length}

+
+
+

Unused Components

+

${reportData.unusedComponents.length}

+

Components not used in any MFE

+
+
+ +

Overview

+ +
+
Components
+
MFEs
+
Unused Components
+
+ +
+

Most Used Components

+
+ +
+ +
+ +

Component Details

+ ${componentStats + .map( + (stats) => ` +
+
+
+ ${stats.componentName} + ${stats.totalUsages} usages +
+ +
+
+

Usage by MFE

+ + + + + + + + + ${Object.entries(stats.usagesByMfe) + .map( + ([mfe, count]) => ` + + + + + ` + ) + .join("")} + +
MFEUsages
${mfe}${count}
+ +

Common Props

+ + + + + + + + + ${stats.commonProps + .map( + (prop) => ` + + + + + ` + ) + .join("")} + +
Prop NameOccurrences
${prop.name}${prop.count}
+ +

Customization

+

+ Styled Components: ${stats.customization.styledComponentCount}
+ Custom Styles: ${stats.customization.customStylesCount} +

+ + ${ + Object.keys(stats.customization.overriddenPropertiesCounts).length > 0 + ? ` +
Overridden Properties
+ + + + + + + + + ${Object.entries(stats.customization.overriddenPropertiesCounts) + .map( + ([prop, count]) => ` + + + + + ` + ) + .join("")} + +
PropertyOccurrences
${prop}${count}
+ ` + : "" + } + +

Files (${stats.files.length})

+
    + ${stats.files.map((file) => `
  • ${file}
  • `).join("")} +
+
+
+ ` + ) + .join("")} +
+ +
+

Component Usage by MFE

+
+ + + + + + + + + + + ${Object.entries(overallStats.mfeUsages) + .map( + ([mfe, count]) => ` + + + + + + ` + ) + .join("")} + +
MFEComponent UsagesMRC Version
${mfe}${count}${reportData.mrcVersions[mfe] || "N/A"}
+
+ + +
+

Unused Components (${reportData.unusedComponents.length})

+

These components are not used in any of the analyzed MFEs. Consider reviewing them for potential removal or promotion.

+ + + + + + + + + + ${reportData.unusedComponents + .map( + (comp) => ` + + + + + ` + ) + .join("")} + +
Component NamePath
${comp.name}${comp.path}
+ +

Unused Components by MFE

+

These components are used in some MFEs but not in others. Consider standardizing component usage across MFEs.

+ + ${Object.entries(reportData.unusedComponentsByMfe) + .map( + ([mfe, components]) => ` +
+
+
+ ${mfe} + ${components.length} unused components +
+ +
+
+ + + + + + + + ${components + .map( + (comp) => ` + + + + ` + ) + .join("")} + +
Component Name
${comp}
+
+
+ ` + ) + .join("")} +
+ + +
+ + + + + `; + } +} diff --git a/tools/mrc-usage-report-maas-ui/src/scanner/fileScanner.ts b/tools/mrc-usage-report-maas-ui/src/scanner/fileScanner.ts new file mode 100644 index 000000000..ba472369a --- /dev/null +++ b/tools/mrc-usage-report-maas-ui/src/scanner/fileScanner.ts @@ -0,0 +1,460 @@ +import path from "path"; +import fs from "fs"; +import { promisify } from "util"; +import { exec } from "child_process"; +import os from "os"; +import axios from "axios"; +import { MrcSourceType } from "../types"; + +const execPromise = promisify(exec); + +// Regular expression to match export statements like: +// export { default as ComponentName } from "./path/to/Component"; +const EXPORT_REGEX = /export\s*{\s*default\s+as\s+([A-Za-z0-9_]+)\s*}\s*from\s*["'](.+)["'];?/g; + +/** + * Scans for files in the specified MFEs + */ +export class FileScanner { + private basePath: string; + private mfes: string[]; + private mrcSourceType: MrcSourceType; + private mrcGithubUrl?: string; + private mrcGithubBranch: string; + private tempDir?: string; + + constructor(basePath: string, mfes: string[], mrcSourceType: MrcSourceType = "local", mrcGithubUrl?: string, mrcGithubBranch: string = "main") { + this.basePath = basePath; + this.mfes = mfes; + this.mrcSourceType = mrcSourceType; + this.mrcGithubUrl = mrcGithubUrl; + this.mrcGithubBranch = mrcGithubBranch; + } + + /** + * Scans for all TypeScript and JavaScript files in the specified MFEs + * @returns Array of file paths + */ + async scanForFiles(): Promise { + const allFiles: string[] = []; + + for (const mfe of this.mfes) { + const mfePath = path.join(this.basePath, "micro-frontends", mfe); + + // Check if the MFE directory exists + if (!fs.existsSync(mfePath)) { + console.warn(`MFE directory not found: ${mfePath}`); + continue; + } + + try { + // Use find command to locate all TypeScript and JavaScript files + const { stdout } = await execPromise( + `find ${mfePath}/src -type f \\( -name "*.ts" -o -name "*.tsx" -o -name "*.js" -o -name "*.jsx" \\) -not -path "*/node_modules/*" -not -name "*.d.ts" -not -name "*.test.*" -not -name "*.spec.*"` + ); + + const files = stdout.trim().split("\n").filter(Boolean); + allFiles.push(...files); + } catch (error) { + console.error(`Error scanning files in ${mfePath}:`, error); + } + } + + return allFiles; + } + + /** + * Extracts owner and repo from GitHub URL + * @param githubUrl GitHub repository URL + * @returns Object containing owner and repo + */ + private parseGithubUrl(githubUrl: string): { owner: string; repo: string } { + // Handle URLs like https://github.com/owner/repo + const urlMatch = githubUrl.match(/github\.com\/([^\/]+)\/([^\/]+)/); + if (urlMatch) { + return { + owner: urlMatch[1], + repo: urlMatch[2].replace(/\.git$/, "") // Remove .git if present + }; + } + throw new Error(`Invalid GitHub URL: ${githubUrl}`); + } + + /** + * Gets the GitHub authentication token from environment variable + * @returns GitHub authentication token or undefined if not available + */ + private getGithubToken(): string | undefined { + return process.env.GITHUB_TOKEN; + } + + /** + * Fetches a file from GitHub API + * @param owner Repository owner + * @param repo Repository name + * @param path File path within the repository + * @param branch Branch name (default: main) + * @returns File content as string + */ + private async fetchFileFromGithub(owner: string, repo: string, path: string, branch?: string): Promise { + branch = branch || this.mrcGithubBranch; + try { + const url = `https://raw.githubusercontent.com/${owner}/${repo}/${branch}/${path}`; + console.log(`Fetching file from GitHub: ${url}`); + + const headers: Record = {}; + const token = this.getGithubToken(); + if (token) { + headers.Authorization = `token ${token}`; + } + + const response = await axios.get(url, { headers }); + return response.data; + } catch (error) { + console.error(`Error fetching file from GitHub:`, error); + throw new Error(`Failed to fetch file from GitHub: ${path}`); + } + } + + /** + * Fetches directory contents from GitHub API + * @param owner Repository owner + * @param repo Repository name + * @param path Directory path within the repository + * @param branch Branch name (default: main) + * @returns Array of file paths + */ + private async fetchDirectoryFromGithub(owner: string, repo: string, path: string, branch?: string): Promise { + branch = branch || this.mrcGithubBranch; + try { + const url = `https://api.github.com/repos/${owner}/${repo}/contents/${path}?ref=${branch}`; + console.log(`Fetching directory from GitHub API: ${url}`); + + const headers: Record = { + Accept: "application/vnd.github.v3+json" + }; + + const token = this.getGithubToken(); + if (token) { + headers.Authorization = `token ${token}`; + } + + const response = await axios.get(url, { headers }); + + // Process the response to extract file paths + const files: string[] = []; + const processItems = async (items: any[]) => { + for (const item of items) { + if (item.type === "file") { + files.push(item.path); + } else if (item.type === "dir") { + // Recursively fetch subdirectory contents + const subdirUrl = `https://api.github.com/repos/${owner}/${repo}/contents/${item.path}?ref=${branch}`; + const subdirResponse = await axios.get(subdirUrl, { headers }); + await processItems(subdirResponse.data); + } + } + }; + + await processItems(response.data); + return files; + } catch (error) { + console.error(`Error fetching directory from GitHub:`, error); + throw new Error(`Failed to fetch directory from GitHub: ${path}`); + } + } + + /** + * Prepares the MRC path based on the source type + * @param mrcPath Path to the MRC repository (used for local source type) + * @returns The path to the MRC repository or null if using GitHub API + */ + async prepareMrcPath(mrcPath: string): Promise { + // If source type is local, use the provided path + if (this.mrcSourceType === "local") { + return mrcPath; + } + + // If source type is github, we'll use the GitHub API directly + if (this.mrcSourceType === "github" && this.mrcGithubUrl) { + console.log(`Using GitHub API for ${this.mrcGithubUrl}`); + return null; // Return null to indicate we're using GitHub API + } + + throw new Error("Invalid MRC source configuration"); + } + + /** + * Scans for all MRC components in the MRC repository + * @param mrcPath Path to the MRC repository + * @returns Array of MRC component paths + */ + async scanForMrcComponents(mrcPath: string): Promise { + // If using GitHub API, we don't need the local path + if (this.mrcSourceType === "github" && this.mrcGithubUrl) { + try { + const { owner, repo } = this.parseGithubUrl(this.mrcGithubUrl); + console.log(`Scanning for MRC components in GitHub repository: ${owner}/${repo}`); + + // Fetch all files in the components directory + const files = await this.fetchDirectoryFromGithub(owner, repo, "src/components"); + + // Filter out unwanted components + const filteredFiles = files.filter((file) => { + // Only include TypeScript/TSX files + if (!file.endsWith(".ts") && !file.endsWith(".tsx")) return false; + // Exclude test and definition files + if (file.endsWith(".d.ts") || file.includes(".test.") || file.includes(".spec.")) return false; + + const fileName = path.basename(file); + const componentName = path.parse(fileName).name; + + // Check if the file is in the table/components subdirectory + const isInTableComponents = file.includes("/table/components/"); + + // Exclude components with "Props", "Utils", "utils", "Icon" in the name, + // React hooks (starting with "use"), and files in table/components subdirectory + const shouldExclude = + componentName.includes("Props") || + componentName.includes("Utils") || + componentName.toLowerCase().includes("utils") || + componentName.includes("Icon") || + componentName.startsWith("use") || + isInTableComponents; + + return !shouldExclude; + }); + + console.log(`Found ${filteredFiles.length} component files in GitHub repository`); + return filteredFiles; + } catch (error) { + console.error(`Error scanning MRC components from GitHub:`, error); + return []; + } + } + + // If using local path + // Prepare the MRC path based on the source type + const actualMrcPath = await this.prepareMrcPath(mrcPath); + + if (actualMrcPath) { + const componentsPath = path.join(actualMrcPath, "src", "components"); + + // Check if the components directory exists + if (!fs.existsSync(componentsPath)) { + throw new Error(`MRC components directory not found: ${componentsPath}`); + } + + try { + // Use find command to locate all component files + const { stdout } = await execPromise(`find ${componentsPath} -type f \\( -name "*.ts" -o -name "*.tsx" \\) -not -name "*.d.ts" -not -name "*.test.*" -not -name "*.spec.*"`); + + // Filter out unwanted components + const allFiles = stdout.trim().split("\n").filter(Boolean); + const filteredFiles = allFiles.filter((file) => { + const fileName = path.basename(file); + const componentName = path.parse(fileName).name; + + // Check if the file is in the table/components subdirectory + const isInTableComponents = file.includes("/table/components/"); + + // Exclude components with "Props", "Utils", "utils", "Icon" in the name, + // React hooks (starting with "use"), and files in table/components subdirectory + const shouldExclude = + componentName.includes("Props") || + componentName.includes("Utils") || + componentName.toLowerCase().includes("utils") || + componentName.includes("Icon") || + componentName.startsWith("use") || + isInTableComponents; + + return !shouldExclude; + }); + + console.log(`Filtered out ${allFiles.length - filteredFiles.length} components (Props, Utils/utils, Icon, hooks, table/components)`); + + return filteredFiles; + } catch (error) { + console.error(`Error scanning MRC components:`, error); + return []; + } + } + + throw new Error("Invalid MRC source configuration"); + } + + /** + * Gets information about MRC components + * @param componentFiles Array of component file paths + * @param mrcPath Path to the MRC repository + * @returns Array of MRC component information + */ + async getMrcComponentInfo(componentFiles: string[], mrcPath: string): Promise<{ name: string; path: string }[]> { + // Get export mappings from index.tsx + const exportMappings = await this.getExportMappings(mrcPath); + + // If using GitHub API + if (this.mrcSourceType === "github" && this.mrcGithubUrl) { + // For GitHub, we need to match based on the relative path within the repository + return componentFiles.map((file) => { + // Try to find the export name for this file + for (const [exportName, relativePath] of exportMappings) { + // The file path from GitHub API will be something like "src/components/Button/Button.tsx" + // The relativePath from index.tsx will be something like "./components/Button/Button" + // We need to normalize them for comparison + const normalizedFilePath = file.replace(/\.tsx?$/, ""); // Remove extension + const normalizedRelativePath = relativePath.replace(/^\.\//, ""); // Remove leading ./ + + if (normalizedFilePath.endsWith(normalizedRelativePath)) { + return { + name: exportName, + path: file + }; + } + } + + // Fallback to using the file name if no export mapping is found + const fileName = path.basename(file); + const componentName = path.parse(fileName).name; + return { + name: componentName, + path: file + }; + }); + } else { + // For local path, use the original approach + // Create a mapping from file path to exported component name + const filePathToExportName = new Map(); + + for (const [exportName, relativePath] of exportMappings) { + // Convert relative path to absolute path + const absolutePath = path.resolve(path.join(mrcPath, "src"), relativePath); + filePathToExportName.set(absolutePath, exportName); + } + + return componentFiles.map((file) => { + // Check if we have an export mapping for this file + const resolvedPath = path.resolve(file); + + // Try to find the export name for this file + for (const [filePath, exportName] of filePathToExportName.entries()) { + // Need to handle both with and without file extension + const filePathWithoutExt = filePath.replace(/\.[^/.]+$/, ""); + const resolvedPathWithoutExt = resolvedPath.replace(/\.[^/.]+$/, ""); + + if (resolvedPath === filePath || resolvedPathWithoutExt === filePathWithoutExt) { + return { + name: exportName, + path: file + }; + } + } + + // Fallback to using the file name if no export mapping is found + const fileName = path.basename(file); + const componentName = path.parse(fileName).name; + return { + name: componentName, + path: file + }; + }); + } + } + + /** + * Extracts export mappings from the MRC index.tsx file + * @param mrcPath Path to the MRC repository + * @returns Map of export name to relative file path + */ + private async getExportMappings(mrcPath: string): Promise> { + const mappings = new Map(); + let content: string; + + // If using GitHub API + if (this.mrcSourceType === "github" && this.mrcGithubUrl) { + try { + const { owner, repo } = this.parseGithubUrl(this.mrcGithubUrl); + console.log(`Fetching index.tsx from GitHub repository: ${owner}/${repo}`); + content = await this.fetchFileFromGithub(owner, repo, "src/index.tsx"); + console.log(`Successfully fetched index.tsx from GitHub`); + console.log(`First 500 characters of content: ${content.substring(0, 500)}...`); + } catch (error) { + console.error(`Error fetching index.tsx from GitHub:`, error); + return mappings; + } + } else { + // If using local path + const indexPath = path.join(mrcPath, "src", "index.tsx"); + if (!fs.existsSync(indexPath)) { + console.warn(`MRC index.tsx not found at ${indexPath}`); + return mappings; + } + + try { + content = fs.readFileSync(indexPath, "utf-8"); + console.log(`Reading MRC index.tsx from ${indexPath}`); + console.log(`First 500 characters of content: ${content.substring(0, 500)}...`); + } catch (error) { + console.error(`Error reading index.tsx:`, error); + return mappings; + } + } + + try { + let match; + let matchCount = 0; + let excludedCount = 0; + + while ((match = EXPORT_REGEX.exec(content)) !== null) { + const exportName = match[1]; + const relativePath = match[2]; + + // Check if the relative path is in the table/components subdirectory + const isInTableComponents = relativePath.includes("/table/components/"); + + // Exclude components with "Props", "Utils", "utils", "Icon" in the name, + // React hooks (starting with "use"), and components from table/components subdirectory + const shouldExclude = + exportName.includes("Props") || + exportName.includes("Utils") || + exportName.toLowerCase().includes("utils") || + exportName.includes("Icon") || + exportName.startsWith("use") || + isInTableComponents; + + if (shouldExclude) { + excludedCount++; + continue; + } + + mappings.set(exportName, relativePath); + matchCount++; + + // Log the first few matches for debugging + if (matchCount <= 5) { + console.log(`Export mapping found: ${exportName} from ${relativePath}`); + } + } + + console.log(`Found ${mappings.size} export mappings in index.tsx (excluded ${excludedCount} Props/Utils/Icon/hooks)`); + + // Log a few examples of mappings where the export name differs from the file name + let diffCount = 0; + for (const [exportName, relativePath] of mappings.entries()) { + const fileName = path.basename(relativePath); + const fileNameWithoutExt = path.parse(fileName).name; + + if (exportName !== fileNameWithoutExt) { + console.log(`Different naming: Export ${exportName} -> File ${fileNameWithoutExt}`); + diffCount++; + if (diffCount >= 5) break; + } + } + + return mappings; + } catch (error) { + console.error(`Error parsing MRC index.tsx:`, error); + return mappings; + } + } +} diff --git a/tools/mrc-usage-report-maas-ui/src/types.ts b/tools/mrc-usage-report-maas-ui/src/types.ts new file mode 100644 index 000000000..e0c9d685a --- /dev/null +++ b/tools/mrc-usage-report-maas-ui/src/types.ts @@ -0,0 +1,139 @@ +/** + * Types for MRC Usage Report + */ + +// Source type for MRC components +export type MrcSourceType = "local" | "github"; + +// Configuration for the analysis +export interface AnalysisConfig { + // MFEs to analyze (excluding api-products as requested) + mfes: string[]; + // Path to the MRC repository + mrcPath: string; + // Output directory for the report + outputDir: string; + // Output format (html, json, yaml, csv, etc.) + outputFormat: "html" | "json" | "yaml" | "csv"; + // Source type for MRC components + mrcSourceType: MrcSourceType; + // GitHub repository URL for MRC components + mrcGithubUrl?: string; + // GitHub branch name for MRC components + mrcGithubBranch?: string; +} + +// Represents a component from the MRC library +export interface MrcComponent { + name: string; + path: string; + // Additional metadata about the component + metadata?: { + description?: string; + category?: string; + }; +} + +// Represents a usage of an MRC component in a file +export interface ComponentUsage { + // The name of the component (e.g., SolaceButton) + componentName: string; + // The file where the component is used + filePath: string; + // The MFE where the component is used + mfe: string; + // The line number where the component is used + lineNumber: number; + // The props passed to the component + props: ComponentProp[]; + // Any custom styling or overrides applied to the component + customization?: { + // If the component is wrapped in a styled component + styledComponent?: boolean; + // If the component has custom styles applied + customStyles?: boolean; + // The CSS properties that are overridden + overriddenProperties?: string[]; + }; +} + +// Represents a prop passed to a component +export interface ComponentProp { + name: string; + // The type of the prop value (string, number, boolean, object, function, etc.) + type: string; + // The actual value of the prop (if it's a literal) + value?: any; + // If the prop is a spread operator + isSpread?: boolean; + // If the prop is a function + isFunction?: boolean; + // If the prop is a JSX element + isJSX?: boolean; +} + +// Aggregated data for a component +export interface ComponentStats { + componentName: string; + // Total number of usages across all MFEs + totalUsages: number; + // Usages by MFE + usagesByMfe: Record; + // Most common props + commonProps: { + name: string; + count: number; + }[]; + // Files where the component is used + files: string[]; + // Customization stats + customization: { + styledComponentCount: number; + customStylesCount: number; + overriddenPropertiesCounts: Record; + }; +} + +// The final report data +export interface ReportData { + // When the report was generated + generatedAt: string; + // Configuration used for the analysis + config: AnalysisConfig; + // MRC version information by MFE + mrcVersions: Record; + // Stats for each component + componentStats: ComponentStats[]; + // List of unused components + unusedComponents: { + // Component name + name: string; + // Component path + path: string; + }[]; + // Unused components by MFE + unusedComponentsByMfe: Record; + // Overall stats + overallStats: { + // Total number of component usages + totalUsages: number; + // Most used components + mostUsedComponents: { + name: string; + count: number; + }[]; + // Most used props + mostUsedProps: { + name: string; + count: number; + }[]; + // MFEs with the most component usages + mfeUsages: Record; + // Total number of unused components + totalUnusedComponents: number; + }; + // Raw data for debugging or further analysis + rawData?: { + componentUsages: ComponentUsage[]; + }; +} diff --git a/tools/mrc-usage-report-maas-ui/tsconfig.json b/tools/mrc-usage-report-maas-ui/tsconfig.json new file mode 100644 index 000000000..6fdf8480b --- /dev/null +++ b/tools/mrc-usage-report-maas-ui/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "**/*.spec.ts"] +} diff --git a/tools/mrc-usage-report-merger/build/htmlGenerator.js b/tools/mrc-usage-report-merger/build/htmlGenerator.js new file mode 100644 index 000000000..4d2223fb1 --- /dev/null +++ b/tools/mrc-usage-report-merger/build/htmlGenerator.js @@ -0,0 +1,637 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.HtmlGenerator = void 0; +const fs_1 = __importDefault(require("fs")); +const path_1 = __importDefault(require("path")); +/** + * Generates an HTML report from the component usage data + */ +class HtmlGenerator { + /** + * Generates an HTML report from the component usage data + * @param reportData The report data + * @param outputPath The path to write the report to + */ + async generateReport(reportData, outputPath) { + const html = this.generateHtml(reportData); + // Create the output directory if it doesn't exist + const outputDir = path_1.default.dirname(outputPath); + if (!fs_1.default.existsSync(outputDir)) { + fs_1.default.mkdirSync(outputDir, { recursive: true }); + } + // Write the HTML to the output file + fs_1.default.writeFileSync(outputPath, html); + console.log(`HTML report generated at ${outputPath}`); + } + /** + * Generates the HTML for the report + * @param reportData The report data + * @returns The HTML string + */ + generateHtml(reportData) { + const { componentStats, overallStats, generatedAt, config } = reportData; + // Format date + const formattedDate = new Date(generatedAt).toLocaleString(); + // Generate HTML + return ` + + + + + + MRC Component Usage Report + + + + +
+
+

MRC Component Usage Report

+

Generated on ${formattedDate}

+
+ +
+
+

Total Component Usages

+

${overallStats.totalUsages}

+
+
+

MFEs Analyzed

+

${config.mfes.length}

+

${config.mfes.join(", ")}

+
+
+

Unique Components Used

+

${componentStats.length}

+
+
+

Unused Components

+

${reportData.unusedComponents.length}

+

Components not used in any MFE

+
+
+ +

Overview

+ +
+
Components
+
MFEs
+
Unused Components
+
+ +
+

Most Used Components

+
+ +
+ +
+ +

Component Details

+ ${componentStats + .map((stats) => ` +
+
+
+ ${stats.componentName} + ${stats.totalUsages} usages +
+ +
+
+

Usage by MFE

+ + + + + + + + + ${Object.entries(stats.usagesByMfe) + .map(([mfe, count]) => ` + + + + + `) + .join("")} + +
MFEUsages
${mfe}${count}
+ +

Common Props

+ + + + + + + + + ${stats.commonProps + .map((prop) => ` + + + + + `) + .join("")} + +
Prop NameOccurrences
${prop.name}${prop.count}
+ +

Customization

+

+ Styled Components: ${stats.customization.styledComponentCount}
+ Custom Styles: ${stats.customization.customStylesCount} +

+ + ${Object.keys(stats.customization.overriddenPropertiesCounts).length > 0 + ? ` +
Overridden Properties
+ + + + + + + + + ${Object.entries(stats.customization.overriddenPropertiesCounts) + .map(([prop, count]) => ` + + + + + `) + .join("")} + +
PropertyOccurrences
${prop}${count}
+ ` + : ""} + +

Files (${stats.files.length})

+
    + ${stats.files.map((file) => `
  • ${file}
  • `).join("")} +
+
+
+ `) + .join("")} +
+ +
+

Component Usage by MFE

+
+ + + + + + + + + + + ${Object.entries(overallStats.mfeUsages) + .map(([mfe, count]) => ` + + + + + + `) + .join("")} + +
MFEComponent UsagesMRC Version
${mfe}${count}${reportData.mrcVersions[mfe] || "N/A"}
+
+ + +
+

Unused Components (${reportData.unusedComponents.length})

+

These components are not used in any of the analyzed MFEs. Consider reviewing them for potential removal or promotion.

+ + + + + + + + + + ${reportData.unusedComponents + .map((comp) => ` + + + + + `) + .join("")} + +
Component NamePath
${comp.name}${comp.path}
+ +

Unused Components by MFE

+

These components are used in some MFEs but not in others. Consider standardizing component usage across MFEs.

+ + ${Object.entries(reportData.unusedComponentsByMfe) + .map(([mfe, components]) => ` +
+
+
+ ${mfe} + ${components.length} unused components +
+ +
+
+ + + + + + + + ${components + .map((comp) => ` + + + + `) + .join("")} + +
Component Name
${comp}
+
+
+ `) + .join("")} +
+ + +
+ + + + + `; + } +} +exports.HtmlGenerator = HtmlGenerator; diff --git a/tools/mrc-usage-report-merger/build/index.js b/tools/mrc-usage-report-merger/build/index.js new file mode 100644 index 000000000..573c0b023 --- /dev/null +++ b/tools/mrc-usage-report-merger/build/index.js @@ -0,0 +1,68 @@ +#!/usr/bin/env node +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const commander_1 = require("commander"); +const path_1 = __importDefault(require("path")); +const chalk_1 = __importDefault(require("chalk")); +const fs_1 = __importDefault(require("fs")); +const merger_1 = require("./merger"); +const htmlGenerator_1 = require("./htmlGenerator"); +// Define the program +const program = new commander_1.Command(); +program + .name("mrc-report-merger") + .description("Merge multiple MRC usage report JSON files and generate a combined HTML report") + .version("1.0.1") + .argument("", "Paths to the MRC usage report JSON files to merge") + .option("-o, --output-json ", "Output path for the merged JSON report", "merged-mrc-usage-report.json") + .option("-h, --output-html ", "Output path for the merged HTML report", "merged-mrc-usage-report.html"); +program.parse(process.argv); +const options = program.opts(); +const jsonFiles = program.args; +async function main() { + try { + console.log(chalk_1.default.blue("MRC Report Merger Tool")); + console.log(chalk_1.default.gray("------------------------")); + if (jsonFiles.length < 2) { + console.error(chalk_1.default.red("Error: At least two JSON files are required for merging.")); + process.exit(1); + } + const resolvedJsonFiles = jsonFiles.map((file) => path_1.default.resolve(file)); + const outputJsonPath = path_1.default.resolve(options.outputJson); + const outputHtmlPath = path_1.default.resolve(options.outputHtml); + console.log(chalk_1.default.yellow("Configuration:")); + resolvedJsonFiles.forEach((file) => console.log(` Input JSON: ${file}`)); + console.log(` Output JSON: ${outputJsonPath}`); + console.log(` Output HTML: ${outputHtmlPath}`); + console.log(""); + const reports = []; + for (const filePath of resolvedJsonFiles) { + if (!fs_1.default.existsSync(filePath)) { + console.error(chalk_1.default.red(`Error: Report not found at ${filePath}`)); + process.exit(1); + } + reports.push(JSON.parse(fs_1.default.readFileSync(filePath, "utf8"))); + } + // Merge reports + console.log(chalk_1.default.yellow("Merging reports...")); + const reportMerger = new merger_1.ReportMerger(); + const mergedReportData = reportMerger.mergeReports(reports); + console.log(chalk_1.default.green("Reports merged successfully.")); + // Save merged JSON + fs_1.default.writeFileSync(outputJsonPath, JSON.stringify(mergedReportData, null, 2)); + console.log(chalk_1.default.green(`Merged JSON report generated at ${outputJsonPath}`)); + // Generate HTML from merged JSON + const htmlGenerator = new htmlGenerator_1.HtmlGenerator(); + await htmlGenerator.generateReport(mergedReportData, outputHtmlPath); + console.log(chalk_1.default.green(`Merged HTML report generated at ${outputHtmlPath}`)); + console.log(chalk_1.default.green("Tool execution completed successfully!")); + } + catch (error) { + console.error(chalk_1.default.red("Error during tool execution:"), error); + process.exit(1); + } +} +main(); diff --git a/tools/mrc-usage-report-merger/build/merger.js b/tools/mrc-usage-report-merger/build/merger.js new file mode 100644 index 000000000..d96f193ed --- /dev/null +++ b/tools/mrc-usage-report-merger/build/merger.js @@ -0,0 +1,145 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.ReportMerger = void 0; +class ReportMerger { + mergeReports(reports) { + if (reports.length === 0) { + throw new Error("Cannot merge an empty list of reports."); + } + if (reports.length === 1) { + return reports[0]; + } + // Deep copy the initial report to avoid modifying the original object + const initialReport = reports[0]; + let mergedReport = JSON.parse(JSON.stringify(initialReport)); + mergedReport.generatedAt = new Date().toISOString(); + for (let i = 1; i < reports.length; i++) { + const nextReport = reports[i]; + mergedReport = { + generatedAt: new Date().toISOString(), // Use current time for merged report + config: this.mergeConfigs(mergedReport.config, nextReport.config), + mrcVersions: { ...mergedReport.mrcVersions, ...nextReport.mrcVersions }, // Later versions overwrite earlier ones + componentStats: this.mergeComponentStats(mergedReport.componentStats, nextReport.componentStats), + unusedComponents: this.mergeUnusedComponents(mergedReport.unusedComponents, nextReport.unusedComponents), + unusedComponentsByMfe: this.mergeUnusedComponentsByMfe(mergedReport.unusedComponentsByMfe, nextReport.unusedComponentsByMfe), + overallStats: {}, // Will be recalculated + rawData: { + componentUsages: [ + ...mergedReport.rawData.componentUsages, + ...nextReport.rawData.componentUsages + ] + } + }; + } + // Recalculate overallStats based on the final merged data + mergedReport.overallStats = this.recalculateOverallStats(mergedReport); + return mergedReport; + } + mergeConfigs(config1, config2) { + const mergedMfes = Array.from(new Set([...config1.mfes, ...config2.mfes])); + return { + ...config1, // Prefer config1 (maas-ops-ui) for other properties + mfes: mergedMfes + }; + } + mergeComponentStats(stats1, stats2) { + const mergedStatsMap = new Map(); + [...stats1, ...stats2].forEach((stats) => { + if (mergedStatsMap.has(stats.componentName)) { + const existingStats = mergedStatsMap.get(stats.componentName); + existingStats.totalUsages += stats.totalUsages; + // Merge usagesByMfe + for (const mfe in stats.usagesByMfe) { + existingStats.usagesByMfe[mfe] = + (existingStats.usagesByMfe[mfe] || 0) + stats.usagesByMfe[mfe]; + } + // Merge commonProps + stats.commonProps.forEach((prop) => { + const existingProp = existingStats.commonProps.find((p) => p.name === prop.name); + if (existingProp) { + existingProp.count += prop.count; + } + else { + existingStats.commonProps.push({ ...prop }); + } + }); + // Merge files and deduplicate + existingStats.files = Array.from(new Set([...existingStats.files, ...stats.files])); + // Merge customization + existingStats.customization.styledComponentCount += + stats.customization.styledComponentCount; + existingStats.customization.customStylesCount += stats.customization.customStylesCount; + for (const prop in stats.customization.overriddenPropertiesCounts) { + existingStats.customization.overriddenPropertiesCounts[prop] = + (existingStats.customization.overriddenPropertiesCounts[prop] || 0) + + stats.customization.overriddenPropertiesCounts[prop]; + } + } + else { + // Deep copy the stats object to avoid modifying the original + mergedStatsMap.set(stats.componentName, JSON.parse(JSON.stringify(stats))); + } + }); + return Array.from(mergedStatsMap.values()); + } + mergeUnusedComponents(unused1, unused2) { + const allUnused = [...unused1, ...unused2]; + const uniqueUnused = new Map(); + allUnused.forEach((comp) => { + const key = `${comp.name}-${comp.path}`; + if (!uniqueUnused.has(key)) { + uniqueUnused.set(key, comp); + } + }); + return Array.from(uniqueUnused.values()); + } + mergeUnusedComponentsByMfe(unusedByMfe1, unusedByMfe2) { + const merged = {}; + // Add all from unusedByMfe1 + for (const mfe in unusedByMfe1) { + merged[mfe] = [...unusedByMfe1[mfe]]; + } + // Merge with unusedByMfe2 + for (const mfe in unusedByMfe2) { + if (merged[mfe]) { + merged[mfe] = Array.from(new Set([...merged[mfe], ...unusedByMfe2[mfe]])); + } + else { + merged[mfe] = [...unusedByMfe2[mfe]]; + } + } + return merged; + } + recalculateOverallStats(reportData) { + let totalUsages = 0; + const mfeUsages = {}; + const componentUsageMap = new Map(); + const propUsageMap = new Map(); + reportData.componentStats.forEach((compStat) => { + totalUsages += compStat.totalUsages; + componentUsageMap.set(compStat.componentName, compStat.totalUsages); + for (const mfe in compStat.usagesByMfe) { + mfeUsages[mfe] = (mfeUsages[mfe] || 0) + compStat.usagesByMfe[mfe]; + } + compStat.commonProps.forEach((prop) => { + propUsageMap.set(prop.name, (propUsageMap.get(prop.name) || 0) + prop.count); + }); + }); + const mostUsedComponents = Array.from(componentUsageMap.entries()) + .map(([name, count]) => ({ name, count })) + .sort((a, b) => b.count - a.count) + .slice(0, 10); // Top 10 + const mostUsedProps = Array.from(propUsageMap.entries()) + .map(([name, count]) => ({ name, count })) + .sort((a, b) => b.count - a.count) + .slice(0, 10); // Top 10 + return { + totalUsages: totalUsages, + mostUsedComponents: mostUsedComponents, + mostUsedProps: mostUsedProps, + mfeUsages: mfeUsages, + totalUnusedComponents: reportData.unusedComponents.length + }; + } +} +exports.ReportMerger = ReportMerger; diff --git a/tools/mrc-usage-report-merger/build/types.js b/tools/mrc-usage-report-merger/build/types.js new file mode 100644 index 000000000..c8ad2e549 --- /dev/null +++ b/tools/mrc-usage-report-merger/build/types.js @@ -0,0 +1,2 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/tools/mrc-usage-report-merger/package-lock.json b/tools/mrc-usage-report-merger/package-lock.json new file mode 100644 index 000000000..e4c42edb9 --- /dev/null +++ b/tools/mrc-usage-report-merger/package-lock.json @@ -0,0 +1,73 @@ +{ + "name": "mrc-report-merger", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "mrc-report-merger", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "chalk": "^5.3.0", + "commander": "^11.1.0" + }, + "devDependencies": { + "@types/node": "^20.14.9", + "typescript": "^5.8.3" + } + }, + "node_modules/@types/node": { + "version": "20.19.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.1.tgz", + "integrity": "sha512-jJD50LtlD2dodAEO653i3YF04NWak6jN3ky+Ri3Em3mGR39/glWiboM/IePaRbgwSfqM1TpGXfAg8ohn/4dTgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/chalk": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", + "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/tools/mrc-usage-report-merger/package.json b/tools/mrc-usage-report-merger/package.json new file mode 100644 index 000000000..06901d3b7 --- /dev/null +++ b/tools/mrc-usage-report-merger/package.json @@ -0,0 +1,26 @@ +{ + "name": "mrc-report-merger", + "version": "1.0.0", + "description": "A tool to merge MRC usage reports and generate a combined HTML report.", + "main": "build/index.js", + "scripts": { + "build": "tsc", + "start": "node build/index.js" + }, + "keywords": [ + "mrc", + "report", + "merge", + "html" + ], + "author": "", + "license": "ISC", + "dependencies": { + "commander": "^11.1.0", + "chalk": "^5.3.0" + }, + "devDependencies": { + "typescript": "^5.8.3", + "@types/node": "^20.14.9" + } +} diff --git a/tools/mrc-usage-report-merger/src/htmlGenerator.ts b/tools/mrc-usage-report-merger/src/htmlGenerator.ts new file mode 100644 index 000000000..f62578ef1 --- /dev/null +++ b/tools/mrc-usage-report-merger/src/htmlGenerator.ts @@ -0,0 +1,658 @@ +import fs from "fs"; +import path from "path"; +import { ReportData } from "./types"; + +/** + * Generates an HTML report from the component usage data + */ +export class HtmlGenerator { + /** + * Generates an HTML report from the component usage data + * @param reportData The report data + * @param outputPath The path to write the report to + */ + async generateReport(reportData: ReportData, outputPath: string): Promise { + const html = this.generateHtml(reportData); + + // Create the output directory if it doesn't exist + const outputDir = path.dirname(outputPath); + if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir, { recursive: true }); + } + + // Write the HTML to the output file + fs.writeFileSync(outputPath, html); + + console.log(`HTML report generated at ${outputPath}`); + } + + /** + * Generates the HTML for the report + * @param reportData The report data + * @returns The HTML string + */ + private generateHtml(reportData: ReportData): string { + const { componentStats, overallStats, generatedAt, config } = reportData; + + // Format date + const formattedDate = new Date(generatedAt).toLocaleString(); + + // Generate HTML + return ` + + + + + + MRC Component Usage Report + + + + +
+
+

MRC Component Usage Report

+

Generated on ${formattedDate}

+
+ +
+
+

Total Component Usages

+

${overallStats.totalUsages}

+
+
+

MFEs Analyzed

+

${config.mfes.length}

+

${config.mfes.join(", ")}

+
+
+

Unique Components Used

+

${componentStats.length}

+
+
+

Unused Components

+

${reportData.unusedComponents.length}

+

Components not used in any MFE

+
+
+ +

Overview

+ +
+
Components
+
MFEs
+
Unused Components
+
+ +
+

Most Used Components

+
+ +
+ +
+ +

Component Details

+ ${componentStats + .map( + (stats) => ` +
+
+
+ ${stats.componentName} + ${stats.totalUsages} usages +
+ +
+
+

Usage by MFE

+ + + + + + + + + ${Object.entries(stats.usagesByMfe) + .map( + ([mfe, count]) => ` + + + + + ` + ) + .join("")} + +
MFEUsages
${mfe}${count}
+ +

Common Props

+ + + + + + + + + ${stats.commonProps + .map( + (prop) => ` + + + + + ` + ) + .join("")} + +
Prop NameOccurrences
${prop.name}${prop.count}
+ +

Customization

+

+ Styled Components: ${stats.customization.styledComponentCount}
+ Custom Styles: ${stats.customization.customStylesCount} +

+ + ${ + Object.keys(stats.customization.overriddenPropertiesCounts).length > 0 + ? ` +
Overridden Properties
+ + + + + + + + + ${Object.entries(stats.customization.overriddenPropertiesCounts) + .map( + ([prop, count]) => ` + + + + + ` + ) + .join("")} + +
PropertyOccurrences
${prop}${count}
+ ` + : "" + } + +

Files (${stats.files.length})

+
    + ${stats.files.map((file) => `
  • ${file}
  • `).join("")} +
+
+
+ ` + ) + .join("")} +
+ +
+

Component Usage by MFE

+
+ + + + + + + + + + + ${Object.entries(overallStats.mfeUsages) + .map( + ([mfe, count]) => ` + + + + + + ` + ) + .join("")} + +
MFEComponent UsagesMRC Version
${mfe}${count}${reportData.mrcVersions[mfe] || "N/A"}
+
+ + +
+

Unused Components (${reportData.unusedComponents.length})

+

These components are not used in any of the analyzed MFEs. Consider reviewing them for potential removal or promotion.

+ + + + + + + + + + ${reportData.unusedComponents + .map( + (comp) => ` + + + + + ` + ) + .join("")} + +
Component NamePath
${comp.name}${comp.path}
+ +

Unused Components by MFE

+

These components are used in some MFEs but not in others. Consider standardizing component usage across MFEs.

+ + ${Object.entries(reportData.unusedComponentsByMfe) + .map( + ([mfe, components]) => ` +
+
+
+ ${mfe} + ${components.length} unused components +
+ +
+
+ + + + + + + + ${components + .map( + (comp) => ` + + + + ` + ) + .join("")} + +
Component Name
${comp}
+
+
+ ` + ) + .join("")} +
+ + +
+ + + + + `; + } +} diff --git a/tools/mrc-usage-report-merger/src/index.ts b/tools/mrc-usage-report-merger/src/index.ts new file mode 100644 index 000000000..8f1e3186a --- /dev/null +++ b/tools/mrc-usage-report-merger/src/index.ts @@ -0,0 +1,78 @@ +#!/usr/bin/env node + +import { Command } from "commander"; +import path from "path"; +import chalk from "chalk"; +import fs from "fs"; +import { ReportMerger } from "./merger"; +import { HtmlGenerator } from "./htmlGenerator"; +import { ReportData } from "./types"; + +// Define the program +const program = new Command(); + +program + .name("mrc-report-merger") + .description("Merge multiple MRC usage report JSON files and generate a combined HTML report") + .version("1.0.1") + .argument("", "Paths to the MRC usage report JSON files to merge") + .option("-o, --output-json ", "Output path for the merged JSON report", "merged-mrc-usage-report.json") + .option("-h, --output-html ", "Output path for the merged HTML report", "merged-mrc-usage-report.html"); + +program.parse(process.argv); + +const options = program.opts(); +const jsonFiles = program.args; + +async function main() { + try { + console.log(chalk.blue("MRC Report Merger Tool")); + console.log(chalk.gray("------------------------")); + + if (jsonFiles.length < 2) { + console.error(chalk.red("Error: At least two JSON files are required for merging.")); + process.exit(1); + } + + const resolvedJsonFiles = jsonFiles.map((file) => path.resolve(file)); + const outputJsonPath = path.resolve(options.outputJson); + const outputHtmlPath = path.resolve(options.outputHtml); + + console.log(chalk.yellow("Configuration:")); + resolvedJsonFiles.forEach((file) => console.log(` Input JSON: ${file}`)); + console.log(` Output JSON: ${outputJsonPath}`); + console.log(` Output HTML: ${outputHtmlPath}`); + console.log(""); + + const reports: ReportData[] = []; + for (const filePath of resolvedJsonFiles) { + if (!fs.existsSync(filePath)) { + console.error(chalk.red(`Error: Report not found at ${filePath}`)); + process.exit(1); + } + reports.push(JSON.parse(fs.readFileSync(filePath, "utf8"))); + } + + // Merge reports + console.log(chalk.yellow("Merging reports...")); + const reportMerger = new ReportMerger(); + const mergedReportData = reportMerger.mergeReports(reports); + console.log(chalk.green("Reports merged successfully.")); + + // Save merged JSON + fs.writeFileSync(outputJsonPath, JSON.stringify(mergedReportData, null, 2)); + console.log(chalk.green(`Merged JSON report generated at ${outputJsonPath}`)); + + // Generate HTML from merged JSON + const htmlGenerator = new HtmlGenerator(); + await htmlGenerator.generateReport(mergedReportData, outputHtmlPath); + console.log(chalk.green(`Merged HTML report generated at ${outputHtmlPath}`)); + + console.log(chalk.green("Tool execution completed successfully!")); + } catch (error) { + console.error(chalk.red("Error during tool execution:"), error); + process.exit(1); + } +} + +main(); diff --git a/tools/mrc-usage-report-merger/src/merger.ts b/tools/mrc-usage-report-merger/src/merger.ts new file mode 100644 index 000000000..9df8fd1a4 --- /dev/null +++ b/tools/mrc-usage-report-merger/src/merger.ts @@ -0,0 +1,181 @@ +import { ReportData, ComponentStats, OverallStats, AnalysisConfig } from "./types"; + +export class ReportMerger { + public mergeReports(reports: ReportData[]): ReportData { + if (reports.length === 0) { + throw new Error("Cannot merge an empty list of reports."); + } + if (reports.length === 1) { + return reports[0]; + } + + // Deep copy the initial report to avoid modifying the original object + const initialReport = reports[0]; + let mergedReport: ReportData = JSON.parse(JSON.stringify(initialReport)); + mergedReport.generatedAt = new Date().toISOString(); + + for (let i = 1; i < reports.length; i++) { + const nextReport = reports[i]; + mergedReport = { + generatedAt: new Date().toISOString(), // Use current time for merged report + config: this.mergeConfigs(mergedReport.config, nextReport.config), + mrcVersions: { ...mergedReport.mrcVersions, ...nextReport.mrcVersions }, // Later versions overwrite earlier ones + componentStats: this.mergeComponentStats( + mergedReport.componentStats, + nextReport.componentStats + ), + unusedComponents: this.mergeUnusedComponents( + mergedReport.unusedComponents, + nextReport.unusedComponents + ), + unusedComponentsByMfe: this.mergeUnusedComponentsByMfe( + mergedReport.unusedComponentsByMfe, + nextReport.unusedComponentsByMfe + ), + overallStats: {} as OverallStats, // Will be recalculated + rawData: { + componentUsages: [ + ...mergedReport.rawData.componentUsages, + ...nextReport.rawData.componentUsages + ] + } + }; + } + + // Recalculate overallStats based on the final merged data + mergedReport.overallStats = this.recalculateOverallStats(mergedReport); + + return mergedReport; + } + + private mergeConfigs(config1: AnalysisConfig, config2: AnalysisConfig): AnalysisConfig { + const mergedMfes = Array.from(new Set([...config1.mfes, ...config2.mfes])); + return { + ...config1, // Prefer config1 (maas-ops-ui) for other properties + mfes: mergedMfes + }; + } + + private mergeComponentStats( + stats1: ComponentStats[], + stats2: ComponentStats[] + ): ComponentStats[] { + const mergedStatsMap = new Map(); + + [...stats1, ...stats2].forEach((stats) => { + if (mergedStatsMap.has(stats.componentName)) { + const existingStats = mergedStatsMap.get(stats.componentName)!; + existingStats.totalUsages += stats.totalUsages; + + // Merge usagesByMfe + for (const mfe in stats.usagesByMfe) { + existingStats.usagesByMfe[mfe] = + (existingStats.usagesByMfe[mfe] || 0) + stats.usagesByMfe[mfe]; + } + + // Merge commonProps + stats.commonProps.forEach((prop) => { + const existingProp = existingStats.commonProps.find((p) => p.name === prop.name); + if (existingProp) { + existingProp.count += prop.count; + } else { + existingStats.commonProps.push({ ...prop }); + } + }); + + // Merge files and deduplicate + existingStats.files = Array.from(new Set([...existingStats.files, ...stats.files])); + + // Merge customization + existingStats.customization.styledComponentCount += + stats.customization.styledComponentCount; + existingStats.customization.customStylesCount += stats.customization.customStylesCount; + for (const prop in stats.customization.overriddenPropertiesCounts) { + existingStats.customization.overriddenPropertiesCounts[prop] = + (existingStats.customization.overriddenPropertiesCounts[prop] || 0) + + stats.customization.overriddenPropertiesCounts[prop]; + } + } else { + // Deep copy the stats object to avoid modifying the original + mergedStatsMap.set(stats.componentName, JSON.parse(JSON.stringify(stats))); + } + }); + + return Array.from(mergedStatsMap.values()); + } + + private mergeUnusedComponents( + unused1: { name: string; path: string }[], + unused2: { name: string; path: string }[] + ): { name: string; path: string }[] { + const allUnused = [...unused1, ...unused2]; + const uniqueUnused = new Map(); + allUnused.forEach((comp) => { + const key = `${comp.name}-${comp.path}`; + if (!uniqueUnused.has(key)) { + uniqueUnused.set(key, comp); + } + }); + return Array.from(uniqueUnused.values()); + } + + private mergeUnusedComponentsByMfe( + unusedByMfe1: Record, + unusedByMfe2: Record + ): Record { + const merged: Record = {}; + + // Add all from unusedByMfe1 + for (const mfe in unusedByMfe1) { + merged[mfe] = [...unusedByMfe1[mfe]]; + } + + // Merge with unusedByMfe2 + for (const mfe in unusedByMfe2) { + if (merged[mfe]) { + merged[mfe] = Array.from(new Set([...merged[mfe], ...unusedByMfe2[mfe]])); + } else { + merged[mfe] = [...unusedByMfe2[mfe]]; + } + } + return merged; + } + + private recalculateOverallStats(reportData: ReportData): OverallStats { + let totalUsages = 0; + const mfeUsages: Record = {}; + const componentUsageMap = new Map(); + const propUsageMap = new Map(); + + reportData.componentStats.forEach((compStat) => { + totalUsages += compStat.totalUsages; + componentUsageMap.set(compStat.componentName, compStat.totalUsages); + + for (const mfe in compStat.usagesByMfe) { + mfeUsages[mfe] = (mfeUsages[mfe] || 0) + compStat.usagesByMfe[mfe]; + } + + compStat.commonProps.forEach((prop) => { + propUsageMap.set(prop.name, (propUsageMap.get(prop.name) || 0) + prop.count); + }); + }); + + const mostUsedComponents = Array.from(componentUsageMap.entries()) + .map(([name, count]) => ({ name, count })) + .sort((a, b) => b.count - a.count) + .slice(0, 10); // Top 10 + + const mostUsedProps = Array.from(propUsageMap.entries()) + .map(([name, count]) => ({ name, count })) + .sort((a, b) => b.count - a.count) + .slice(0, 10); // Top 10 + + return { + totalUsages: totalUsages, + mostUsedComponents: mostUsedComponents, + mostUsedProps: mostUsedProps, + mfeUsages: mfeUsages, + totalUnusedComponents: reportData.unusedComponents.length + }; + } +} diff --git a/tools/mrc-usage-report-merger/src/types.ts b/tools/mrc-usage-report-merger/src/types.ts new file mode 100644 index 000000000..0fd56c783 --- /dev/null +++ b/tools/mrc-usage-report-merger/src/types.ts @@ -0,0 +1,72 @@ +export interface AnalysisConfig { + mfes: string[]; + mrcPath: string; + outputDir: string; + outputFormat: "html" | "json" | "yaml" | "csv"; + mrcSourceType: MrcSourceType; + mrcGithubUrl?: string; + mrcGithubBranch?: string; +} + +export type MrcSourceType = "local" | "github"; + +export interface ComponentUsage { + componentName: string; + filePath: string; + mfe: string; + lineNumber: number; + props: ComponentProp[]; + customization: { + styledComponent: boolean; + customStyles: boolean; + overriddenProperties: string[]; + }; +} + +export interface ComponentProp { + name: string; + type: string; + value?: any; + isFunction?: boolean; + isJSX?: boolean; + isSpread?: boolean; +} + +export interface ComponentStats { + componentName: string; + totalUsages: number; + usagesByMfe: Record; + commonProps: { name: string; count: number }[]; + files: string[]; + customization: { + styledComponentCount: number; + customStylesCount: number; + overriddenPropertiesCounts: Record; + }; +} + +export interface ReportData { + generatedAt: string; + config: AnalysisConfig; + mrcVersions: Record; + componentStats: ComponentStats[]; + unusedComponents: { name: string; path: string }[]; + unusedComponentsByMfe: Record; + overallStats: OverallStats; + rawData: { + componentUsages: ComponentUsage[]; + }; +} + +export interface OverallStats { + totalUsages: number; + mostUsedComponents: { name: string; count: number }[]; + mostUsedProps: { name: string; count: number }[]; + mfeUsages: Record; + totalUnusedComponents: number; +} + +export interface MrcComponentInfo { + name: string; + path: string; +} diff --git a/tools/mrc-usage-report-merger/tsconfig.json b/tools/mrc-usage-report-merger/tsconfig.json new file mode 100644 index 000000000..8522fcea8 --- /dev/null +++ b/tools/mrc-usage-report-merger/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "es2020", + "module": "commonjs", + "outDir": "./build", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules", "**/*.spec.ts"] +}