Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions tools/cli/.swcrc
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"sourceMaps": true,
"exclude": [
"jest.config.ts",
"__fixtures__",
".*\\.spec.tsx?$",
".*\\.test.tsx?$",
"./src/jest-setup.ts$",
Expand Down
2 changes: 1 addition & 1 deletion tools/cli/eslint.config.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const nodeConfig = fluentPlugin.configs['flat/node'];
/** @type {import("eslint").Linter.Config[]} */
module.exports = [
{
ignores: ['src/commands/migrate/v8-to-v9/__tests__/fixtures/**'],
ignores: ['src/commands/migrate/v8-to-v9/__tests__/fixtures/**', 'src/**/__fixtures__/**'],
},
...(Array.isArray(nodeConfig) ? nodeConfig : [nodeConfig]),
];
105 changes: 105 additions & 0 deletions tools/cli/src/commands/report/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# `report` command

The `report` command generates reports about Fluent UI package usage in a codebase. It has two subcommands targeting different audiences and use cases.

## Subcommands

### `report info`

Quick package & environment summary intended for **end-users** reporting issues.

```bash
fluentui report info
```

Outputs a copy-paste-friendly block with:

- System info (Node version, OS, package manager)
- Installed Fluent UI and related packages with versions
- Duplicate package warnings (multiple resolved versions)

No flags — runs against the current project and prints to stdout.

#### Tracked packages

| Scope | Packages |
| -------------- | ------------------------------------------------------- |
| Fluent scoped | `@fluentui/*`, `@fluentui-contrib/*` |
| Fluent related | `tabster`, `keyborg`, `@griffel/*` |
| 3rd party | `react`, `@types/react`, `typescript`, `@floating-ui/*` |

### `report usage`

Deep codebase analysis of Fluent UI API usage intended for the **core team** to understand how consumers use the library.

```bash
fluentui report usage [--path <dir>] [--reporter json|markdown|html] [--include <glob>...] [--exclude <glob>...]
```

| Flag | Alias | Default | Description |
| ------------ | ----- | --------- | -------------------------------------------- |
| `--path` | `-p` | git root | Root directory for file traversal |
| `--reporter` | `-r` | `json` | Output format: `json`, `markdown`, or `html` |
| `--include` | — | all files | Glob patterns to include |
| `--exclude` | — | none | Glob patterns to exclude |

Traverses `.ts` and `.tsx` files (skipping gitignored files), resolves imports from tracked packages, and classifies every imported symbol into one of five categories.

#### Categories

| Category | What it captures | Tracked details |
| -------------- | ---------------------------------------------------- | ------------------------------------------------ |
| **Components** | React components (JSX elements) | Per-component prop usage with values |
| **Hooks** | React hooks (`use*` naming) | Call-site argument usage with values |
| **Types** | TypeScript interfaces, type aliases, enums | `typeof` reference count, generic type arguments |
| **Others** | Value exports (constants, utility functions, themes) | Call-site argument usage when invoked |
| **Unknowns** | Symbols whose `.d.ts` could not be resolved | Naming-convention-based description |

#### Output formats

- **JSON** — machine-readable metadata with a `legend`, `fileMap`, and per-package `packages` map. Default.
- **Markdown** — summary tables and per-package breakdowns; concise, no prop details.
- **HTML** — self-contained report with collapsible prop/argument details and dark mode support.

## Architecture

```
report/
├── index.ts # Parent command — registers subcommands
├── commands/
│ ├── info.ts # `report info` subcommand
│ └── usage.ts # `report usage` subcommand
├── impl/
│ ├── types.ts # All type definitions
│ ├── ast-parser.ts # ts-morph AST parser (symbol classification, JSX/call/type-ref extraction)
│ ├── file-discovery.ts # Source file traversal (respects .gitignore)
│ ├── package-resolver.ts # Package version resolution and reportable-package filtering
│ ├── usage-report.ts # Core analysis engine — collects metadata from AST parser
│ ├── info-report.ts # Package/system info collection and formatting
│ ├── markdown-reporter.ts # Markdown output formatter
│ ├── html-reporter.ts # HTML output formatter
│ └── index.ts # Barrel exports
├── __fixtures__/ # Test fixtures (sample-app with mock node_modules)
├── SPEC.md # Original specification
└── README.md # This file
```

### How symbol classification works

1. **Import scanning** — all `import` declarations from tracked packages are collected
2. **Type resolution** — each symbol is resolved through its `.d.ts` declaration:
- Functions returning JSX → `component`
- `use*` naming or hook signatures → `hook`
- Interfaces, type aliases, enums → `type`
- Everything else with a resolved `.d.ts` → `other`
- Unresolvable `.d.ts` → `unknown`
3. **Usage enrichment** — JSX props, call arguments, `typeof` references, and generic type arguments are captured
4. **Deduplication** — symbols appearing in multiple categories are reconciled (e.g., a component found via both JSX and value reference)

### Testing

```bash
yarn nx run cli:test
```

Tests use a fixture-based approach with a `__fixtures__/sample-app/` containing mock `_mock_node_modules` with `.d.ts` declarations. The `usage-report.spec.ts` uses a mock `AstParser` for isolated unit testing.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"name": "sample-app",
"version": "1.0.0",
"dependencies": {
"@proj/react-components": "^9.50.0",
"@proj/react-icons": "^2.0.200",
"react": "^18.2.0",
"@types/react": "^18.2.0",
"typescript": "^5.3.0",
"@griffel/react": "^1.5.0"
},
"devDependencies": {
"@proj/eslint-plugin": "^1.0.0"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const AzureLightTheme: Record<string, string> = {
colorBrandBackground: '#0078d4',
colorBrandForeground1: '#0078d4',
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { AzureLightTheme } from './AzureLightTheme';
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { AzureLightTheme } from './components/AzureLightTheme';
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import React from 'react';
import { Button, Input, makeStyles, tokens } from '@proj/react-components';
import { useId } from '@proj/react-components';
import { SearchRegular } from '@proj/react-icons';

const useStyles = makeStyles({
root: {
display: 'flex',
gap: tokens.spacingHorizontalM,
},
});

export const SearchForm = () => {
const inputId = useId('search-input');
const styles = useStyles();

return (
<div className={styles.root}>
<Input id={inputId} placeholder="Search..." appearance="outline" contentBefore={<SearchRegular />} />
<Button appearance="primary" size="medium" icon={<SearchRegular />}>
Search
</Button>
<Button appearance="secondary">Cancel</Button>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React from 'react';
import { useArrowNavigationGroup, useId } from '@proj/react-components';

const circular = true;

export const NavGroup = () => {
const id = useId('nav');

// Explicit property assignments — values should be captured as literals
const attrs = useArrowNavigationGroup({
axis: 'vertical',
memorizeCurrent: true,
unstable_hasDefault: true,
});

// Mix of explicit and shorthand — `circular` is shorthand (value = variable ref)
const attrs2 = useArrowNavigationGroup({
axis: 'horizontal',
circular,
});

return (
<div id={id} {...attrs} {...attrs2}>
<span>Navigation group</span>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import React from 'react';
import { FluentProvider, webLightTheme, Tooltip, useToastController } from '@proj/react-components';
import { makeStyles } from '@griffel/react';

const useStyles = makeStyles({
wrapper: {
padding: '20px',
},
});

export const App: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const styles = useStyles();
const { dispatchToast } = useToastController();

return (
<FluentProvider theme={webLightTheme}>
<div className={styles.wrapper}>
<Tooltip content="App wrapper" relationship="description">
<span>{children}</span>
</Tooltip>
</div>
</FluentProvider>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import React from 'react';
import { AzureLightTheme } from '@sample/azure-theme';
import { FluentProvider } from '@proj/react-components';

export const ThemedApp = () => (
<FluentProvider theme={AzureLightTheme}>
<span>Hello</span>
</FluentProvider>
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { ButtonProps, InputProps } from '@proj/react-components';
import type { FluentIcon } from '@proj/react-icons';

export type CustomButtonProps = ButtonProps & {
tooltip?: string;
};

export type SearchInputProps = InputProps & {
onSearch?: (value: string) => void;
};

export type IconType = FluentIcon;
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Button, FluentProvider } from '@proj/react-components';
import type { ButtonProps, ColumnDef } from '@proj/react-components';

type MyButtonProps = ButtonProps & {
tooltip?: string;
};

const meta: { component: typeof Button } = {
component: Button,
};

// Generic type usage — ColumnDef with type argument
type UserColumn = ColumnDef<{ name: string; age: number }>;
type ProductColumn = ColumnDef<{ id: number; price: number }>;

export const App = () => (
<FluentProvider>
<Button>Click</Button>
</FluentProvider>
);
Loading
Loading