diff --git a/packages/manager-tools/manager-muk-cli/README.md b/packages/manager-tools/manager-muk-cli/README.md index 521b2102a7b1..06ee1a3aa8d7 100644 --- a/packages/manager-tools/manager-muk-cli/README.md +++ b/packages/manager-tools/manager-muk-cli/README.md @@ -1,7 +1,8 @@ # 🧩 manager-muk-cli -A Node.js CLI designed to **automate maintenance and synchronization** of the `@ovh-ux/manager-ui-kit` package with the **OVHcloud Design System (ODS)**. -It checks for new ODS releases, ensures component parity, and automatically generates missing component structures, hooks, constants, and type passthroughs while preserving test coverage and export integrity. +A Node.js CLI designed to **automate maintenance, synchronization, and documentation** of the `@ovh-ux/manager-ui-kit` with the **OVHcloud Design System (ODS)**. + +It checks ODS versions, detects missing components, generates passthroughs (hooks, constants, types), and **synchronizes ODS component documentation** directly from GitHub, using a fully-streamed, cache-aware architecture. --- @@ -9,14 +10,13 @@ It checks for new ODS releases, ensures component parity, and automatically gene ### 1.1 `--check-versions` -Checks npm for new ODS package versions and compares them with those declared in `manager-ui-kit/package.json`. +Checks npm for new ODS package releases and compares them with the versions declared in `manager-ui-kit/package.json`. ```bash yarn muk-cli --check-versions ``` **Example Output** - ``` β„Ή πŸ” Checking ODS package versions... ⚠ Updates available: @@ -29,21 +29,19 @@ yarn muk-cli --check-versions ### 1.2 `--check-components` -Compares components between `@ovhcloud/ods-react` and `manager-ui-kit/src/components`, identifying missing or outdated ones. +Compares ODS React components with those in `manager-ui-kit/src/components`, identifying missing or outdated ones. ```bash yarn muk-cli --check-components ``` **Example Output** - ``` β„Ή πŸ“ Found 34 local components β„Ή πŸ“¦ Fetching ODS React v19.2.0 tarball... ⚠ Missing 8 ODS components: β„Ή β€’ form-field β„Ή β€’ form-field-label -β„Ή β€’ form-field-error β„Ή β€’ range β„Ή β€’ range-thumb β„Ή β€’ range-track @@ -53,14 +51,13 @@ yarn muk-cli --check-components ### 1.3 `--update-versions` -Updates all ODS dependencies in `package.json` to their latest versions, validates linting, and runs unit tests. +Automatically updates all ODS dependencies in `package.json` to their latest versions, validates, and runs post-update checks. ```bash yarn muk-cli --update-versions ``` **Example Output** - ``` βœ” Updated 3 ODS dependencies βœ” @ovhcloud/ods-components: 18.6.2 β†’ 18.6.4 @@ -69,8 +66,7 @@ yarn muk-cli --update-versions βœ” package.json successfully updated. ``` -If all versions are up-to-date: - +If all are current: ``` βœ… All ODS versions are already up to date! ✨ Done in 1.78s. @@ -80,184 +76,168 @@ If all versions are up-to-date: ### 1.4 `--add-components` -Generates **missing ODS components** and subcomponents directly from the ODS React source tarball. +Generates **missing ODS components** from the ODS React source tarball, preserving hooks, constants, and external types. ```bash yarn muk-cli --add-components ``` -Supports: - -* Simple components (without children, e.g. `badge`, `progress-bar`) -* Nested components (with children, e.g. `form-field`, `combobox`, `range`, `datepicker`) -* Hook passthroughs (e.g. `useFormField`) -* Constants passthroughs (e.g. `DatepickerConstants`) -* External type re-exports (from contexts or shared ODS types) +#### Supported Scenarios +* Simple components (no children, e.g. `badge`, `progress-bar`) +* Nested components (with subcomponents, e.g. `form-field`, `datepicker`, `range`) +* Hook passthroughs (`useFormField`) +* Constants passthroughs (`DatepickerConstants`) +* External type re-exports --- -## 🧱 2. Simple Components (Without Children) - -A *simple* ODS component has no subcomponents or nested structure. - -**Generated Structure** +### 1.5 `--add-components-documentation` -``` -progress-bar/ -β”œβ”€β”€ __tests__/ -β”‚ └── ProgressBar.snapshot.test.tsx -β”œβ”€β”€ ProgressBar.component.tsx -β”œβ”€β”€ ProgressBar.props.ts -└── index.ts -``` +Fetches and synchronizes **official ODS component documentation** (`.mdx` files) from the [ovh/design-system](https://github.com/ovh/design-system) repository directly into `manager-wiki`. -**Component** - -```tsx -import { ProgressBar as OdsProgressBar } from '@ovhcloud/ods-react'; -import { ProgressBarProps } from './ProgressBar.props'; +```bash +yarn muk-cli --add-components-documentation +``` + +#### 🧠 What It Does +1. Detects the latest `@ovhcloud/ods-react` version from npm. +2. Downloads (or reuses) the GitHub tarball for that version. +3. Streams documentation files under `/storybook/stories/components/`. +4. Extracts per-component documentation and writes it to: + ``` + packages/manager-wiki/stories/manager-ui-kit/components//base-component-doc/ + ``` +5. Caches the tarball for **7 days** to avoid redundant downloads. +6. Synchronizes Storybook base-documents: + ``` + packages/manager-wiki/stories/manager-ui-kit/base-documents/ + ``` +7. Rewrites imports: + - `../../../src/` β†’ `../../../base-documents/` + - `ods-react/src/` β†’ `@ovhcloud/ods-react` -export const ProgressBar = (props: ProgressBarProps) => ; +**Example Output** ``` - -**Index** - -```ts -export { ProgressBar } from './ProgressBar.component'; -export type { ProgressBarProps } from './ProgressBar.props'; +β„Ή πŸ“¦ Starting Design System documentation sync… +β„Ή ℹ️ ODS React latest version: 19.2.1 +βœ” πŸ’Ύ Served 85 documentation files from cache. +β„Ή πŸ“ Found existing component: 'accordion' +βœ” βœ… Sync complete β€” 45 new, 42 updated, 171 files streamed. ``` --- -## πŸͺœ 3. Nested Components (With Children) - -Nested components (e.g. `form-field`, `combobox`, `datepicker`, `range`) contain child components such as `form-field-label` or `datepicker-control`. - -The CLI automatically: - -1. Detects parent–child relationships -2. Generates base and subcomponent folders -3. Determines prop inheritance (own vs parent type) -4. Detects if components **have or lack children** -5. Creates passthroughs for hooks, constants, and external types -6. Consolidates all exports into the parent `index.ts` - -**Example Structure** +## βš™οΈ 2. Streaming Architecture +### 2.1 High-Level Flow ``` -form-field/ -β”œβ”€β”€ __tests__/ -β”‚ └── FormField.snapshot.test.tsx -β”œβ”€β”€ form-field-label/ -β”‚ └── FormFieldLabel.component.tsx -β”œβ”€β”€ form-field-helper/ -β”‚ └── FormFieldHelper.component.tsx -β”œβ”€β”€ form-field-error/ -β”‚ └── FormFieldError.component.tsx -β”œβ”€β”€ constants/ -β”‚ └── FormFieldConstants.ts -β”œβ”€β”€ hooks/ -β”‚ └── useFormField.ts -β”œβ”€β”€ FormField.component.tsx -β”œβ”€β”€ FormField.props.ts -└── index.ts +GitHub tarball (.tar.gz) + β”‚ + β”œβ”€β–Ά streamTarGz() + β”œβ”€β–Ά extractDesignSystemDocs() + β”œβ”€β–Ά createAsyncQueue() + └─▢ streamComponentDocs() ``` -**Parent Index** - -```ts -export { FormField, type FormFieldProps } from './FormField.component'; -export { FormFieldError } from './form-field-error/FormFieldError.component'; -export { FormFieldHelper } from './form-field-helper/FormFieldHelper.component'; -export { FormFieldLabel } from './form-field-label/FormFieldLabel.component'; -export * from './hooks/useFormField'; -export * from './constants/FormFieldConstants'; +### 2.2 Core Streaming Functions +#### Stream Extraction +```js +pipeline( + https.get(url), + zlib.createGunzip(), + tar.extract({ onentry(entry) { ... } }) +); ``` +Each `entry` is processed **as it’s read** β€” no full buffering. ---- - -### 3.1 Detection Logic - -| Detection Type | Logic | Examples | -| ------------------ | ------------------------------------------------------------------------- | ------------------------------------------------------- | -| **Children** | Detects `PropsWithChildren`, `children:` props, or `props.children` usage | Differentiates with vs without children | -| **Subcomponent** | Scans ODS index exports to build parent–child tree | `form-field`, `datepicker` | -| **Hooks** | Detects any export containing `use` prefix | Generates `hooks/use.ts` passthrough | -| **Constants** | Extracts all non-type exports from `constants` paths | Creates `constants/Constants.ts` passthrough | -| **External Types** | Detects `type` or `interface` exports from non-component paths | Appends imports/exports in `.props.ts` | - ---- +#### Stream Bridge +```js +const queue = createAsyncQueue(); +await extractDesignSystemDocs({ onFileStream: q.push }); +await streamComponentDocs(queue); +``` +Manages concurrency and backpressure. -### 3.2 Behavior Summary +#### Stream Consumer +```js +await pipeline(fileStream, fs.createWriteStream(destFile)); +``` +Backpressure-safe, cleans up on error. -| Type | Structure | Children | Hooks | Constants | Types | Index Linking | Test Coverage | -| --------------------- | -------------------- | -------- | -------- | --------- | ------------- | ------------- | --------------- | -| **Simple Component** | Single folder | No | Optional | Optional | Own | Root index | Snapshot | -| **Nested Component** | Parent + children | Yes/No | Auto | Auto | Parent or Own | Parent index | Snapshot + Spec | -| **Invalid Component** | Not found in tarball | β€” | β€” | β€” | β€” | Skipped | None | +#### Cache Layer +``` +target/.cache/ods-docs/ +β”œβ”€β”€ ods-docs-meta.json +└── ods-docs-files.json +``` +TTL: 7 days β€” fully reusable offline. --- -## βš™οΈ 4. Architecture Overview - +## 🧱 3. Codebase Layout ``` manager-muk-cli/ β”œβ”€ src/ β”‚ β”œβ”€ commands/ -β”‚ β”‚ β”œβ”€ check-versions.js -β”‚ β”‚ β”œβ”€ check-components.js -β”‚ β”‚ β”œβ”€ update-version.js -β”‚ β”‚ └─ add-components.js β”‚ β”œβ”€ core/ -β”‚ β”‚ β”œβ”€ component-utils.js -β”‚ β”‚ β”œβ”€ ods-tarball-utils.js -β”‚ β”‚ β”œβ”€ file-utils.js -β”‚ β”‚ └─ tasks-utils.js β”‚ β”œβ”€ config/ -β”‚ β”‚ └─ muk-config.js -β”‚ └─ utils/ -β”‚ β”œβ”€ log-manager.js -β”‚ └─ json-utils.js +└─ target/.cache/ods-docs/ ``` --- -## 🧠 5. Design Principles +## 🧠 4. Design Principles -| Principle | Description | -| ----------------------- | ---------------------------------------------------- | -| **Modular CLI** | Each command is standalone and composable | -| **Granular Heuristics** | Detects children, hooks, constants, and type exports | -| **Idempotent Safety** | Prevents overwriting or duplication | -| **Verbose Logging** | Colorized emoji logs for transparency | +| Principle | Description | +|------------|--------------| +| **Streaming-first** | All I/O ops use Node streams | +| **Memory-safe** | Constant memory footprint | +| **Composable** | Modular, small functions | +| **Idempotent** | Deterministic results | +| **Offline-safe** | Cache-first retry | +| **Verbose logging** | Emoji logs for visibility | +| **Configurable** | Rewrite rules in `muk-config.js` | --- -## βœ… 6. Advantages +## βœ… 5. Advantages +* πŸ” Auto-synced ODS documentation and base-docs +* ⚑ Cached and resumable (7-day TTL) +* 🧩 Full parity with ODS React components +* 🧠 Low-memory async pipeline +* 🧱 Modular, testable, CI-ready +* πŸ”§ Configurable rewriting rules + +--- -* Detects components **with and without children** -* Automatically generates **hooks**, **constants**, and **external types** passthroughs -* Builds fully linked exports for parent and subcomponents -* Modularized, reusable, and testable architecture -* Caches and reuses ODS tarball during execution +## 🧩 6. Cache Troubleshooting +```bash +rm -rf packages/manager-tools/manager-muk-cli/target/.cache/ods-docs +``` +Rebuilds clean cache on rerun. --- -## 🧩 7. Example Output (Range + FormField) +## 🧩 7. Configuration Extraction -``` -β„Ή πŸ“¦ Fetching ODS React v19.2.0 tarball... -πŸ‘Ά form-field supports children -🚫 range has no children -🧩 form-field-error exports its own Prop type -πŸͺ Created hook passthrough for FormField (1 identifier) -βš™οΈ Created constants passthrough for Datepicker (4 identifiers) -βœ” Component structure ready for FormField -βœ” Component structure ready for Range +`muk-config.js` centralizes regex, rewrite, and Storybook folder logic. + +```js +export const MUK_IMPORT_REWRITE_RULES = [ + { + name: 'base-documents', + pattern: /((?:\..\/){2,3})src\//g, + replacer: (_, prefix) => `${prefix}base-documents/`, + }, + { + name: 'ods-react', + pattern: /(['"])[^'"]*ods-react\/src[^'"]*/g, + replacer: (_, quote) => `${quote}@ovhcloud/ods-react`, + }, +]; ``` --- ## πŸͺͺ 8. License - BSD-3-Clause Β© OVH SAS diff --git a/packages/manager-tools/manager-muk-cli/src/commands/add-components-documentation.js b/packages/manager-tools/manager-muk-cli/src/commands/add-components-documentation.js new file mode 100644 index 000000000000..b7894972e578 --- /dev/null +++ b/packages/manager-tools/manager-muk-cli/src/commands/add-components-documentation.js @@ -0,0 +1,54 @@ +import { EMOJIS, MUK_WIKI_COMPONENTS } from '../config/muk-config.js'; +import { + rewriteWikiComponentImports, + syncComponentDocs, + syncStorybookBaseDocuments, +} from '../core/component-documentation-utils.js'; +import { aggregateOperationsStats, runPostUpdateChecks, safeSync } from '../core/tasks-utils.js'; +import { logger } from '../utils/log-manager.js'; + +/** + * CLI Command: addComponentsDocumentation + * + * Synchronizes all Design System documentation artifacts into the Manager Wiki. + * + * This process performs two major synchronization operations: + * + * 1. **Component Base Documentation** β€” retrieves and updates + * each component’s `base-component-doc` folder in the Wiki. + * + * 2. **Storybook Source Documentation** β€” downloads and mirrors all + * Storybook `components`, `constants`, and `helpers` under: + * `packages/manager-wiki/stories/manager-ui-kit/base-documents/` + * + * Both steps use streaming extraction from the ODS tarball, + * ensuring minimal memory usage and safe incremental updates. + * + * @async + * @returns {Promise} Completes when both synchronizations finish. + */ +export async function addComponentsDocumentation() { + logger.info(`${EMOJIS.package} Starting Design System documentation sync…`); + + // 1 Components + const componentResult = await safeSync('component base-docs', syncComponentDocs); + + // 2 Storybook base-documents + const storybookResult = await safeSync('storybook base-documents', syncStorybookBaseDocuments); + + // 3 Rewrite imports + rewriteWikiComponentImports(MUK_WIKI_COMPONENTS); + + // 4 Aggregate + const stats = aggregateOperationsStats([componentResult, storybookResult]); + logger.success( + `${EMOJIS.check} Sync complete β€” ${stats.created} new, ${stats.updated} updated, ${stats.total} files streamed.`, + ); + + // Run validation tasks (install, build, tests) after workspace updates + runPostUpdateChecks(); + + logger.info( + `${EMOJIS.rocket} Components updated and imports normalized under 'stories/manager-ui-kit'.`, + ); +} diff --git a/packages/manager-tools/manager-muk-cli/src/commands/add-components.js b/packages/manager-tools/manager-muk-cli/src/commands/add-components.js index d9c56ab8cad1..53a69bbee504 100644 --- a/packages/manager-tools/manager-muk-cli/src/commands/add-components.js +++ b/packages/manager-tools/manager-muk-cli/src/commands/add-components.js @@ -21,8 +21,8 @@ import { import { detectHasChildrenFromTarball, detectHasTypeExportFromIndex, - extractOdsExportsByCategory, -} from '../core/ods-tarball-utils.js'; + extractOdsComponentsExportsByCategory, +} from '../core/ods-components-tarball-utils.js'; import { runPostUpdateChecks } from '../core/tasks-utils.js'; import { logger } from '../utils/log-manager.js'; import { checkComponents } from './check-components.js'; @@ -152,7 +152,7 @@ export type { ${externalTypes.join(', ')} }; * @returns {Promise<{ hasHooks: boolean, hasConstants: boolean }>} Whether hooks/constants were created. */ async function createHooksAndConstants(componentName, baseDir, odsName, propsPath) { - const { hooks, constants, externalTypes } = await extractOdsExportsByCategory(odsName); + const { hooks, constants, externalTypes } = await extractOdsComponentsExportsByCategory(odsName); // πŸͺ Hooks passthrough if (hooks.length) { diff --git a/packages/manager-tools/manager-muk-cli/src/commands/check-components.js b/packages/manager-tools/manager-muk-cli/src/commands/check-components.js index 19d50c71a74b..53cdf0b8453d 100644 --- a/packages/manager-tools/manager-muk-cli/src/commands/check-components.js +++ b/packages/manager-tools/manager-muk-cli/src/commands/check-components.js @@ -2,7 +2,10 @@ import { promises as fs } from 'node:fs'; import { EMOJIS, MUK_COMPONENTS_SRC } from '../config/muk-config.js'; -import { extractOdsTarball, getOdsPackageMetadata } from '../core/ods-tarball-utils.js'; +import { + extractOdsComponentsTarball, + getOdsComponentsPackageMetadata, +} from '../core/ods-components-tarball-utils.js'; import { logger } from '../utils/log-manager.js'; /** @@ -29,10 +32,10 @@ async function getLocalComponents() { * - Root component fallback */ export async function getRemoteOdsComponents() { - const { version, tarball } = await getOdsPackageMetadata(); + const { version, tarball } = await getOdsComponentsPackageMetadata(); logger.info(`${EMOJIS.package} Fetching ODS React v${version} tarball: ${tarball}`); - const files = await extractOdsTarball(); + const files = await extractOdsComponentsTarball(); const components = new Set(); // Locate index.ts files under src/components//src/index.ts @@ -47,7 +50,7 @@ export async function getRemoteOdsComponents() { const fileBuffer = files.get(filePath); if (!fileBuffer) { - logger.warn(`${EMOJIS.warning} Skipping ${filePath} (not found in tarball)`); + logger.warn(`${EMOJIS.warn} Skipping ${filePath} (not found in tarball)`); continue; } diff --git a/packages/manager-tools/manager-muk-cli/src/commands/check-versions.js b/packages/manager-tools/manager-muk-cli/src/commands/check-versions.js index 16d4bf66cb72..6c10033ed49d 100644 --- a/packages/manager-tools/manager-muk-cli/src/commands/check-versions.js +++ b/packages/manager-tools/manager-muk-cli/src/commands/check-versions.js @@ -7,7 +7,7 @@ import { loadJson } from '../utils/json-utils.js'; import { logger } from '../utils/log-manager.js'; /** - * πŸš€ Compare local and remote ODS package versions. + * Compare local and remote ODS package versions. * * Uses semantic versioning to detect: * - ⚠️ Outdated packages (local < npm) diff --git a/packages/manager-tools/manager-muk-cli/src/config/muk-config.js b/packages/manager-tools/manager-muk-cli/src/config/muk-config.js index b9b0ce59a250..6513b66c8523 100644 --- a/packages/manager-tools/manager-muk-cli/src/config/muk-config.js +++ b/packages/manager-tools/manager-muk-cli/src/config/muk-config.js @@ -1,39 +1,175 @@ +import path from 'node:path'; + /** - * CLI configuration constants for manager-muk-cli. - * - * Keeping all shared constants and paths in one place ensures that the - * different commands (checkVersions, checkComponents, update) remain aligned. + * Enable / Disable caching components + * @type {boolean} */ -import path from 'node:path'; +export const DISABLE_ODS_COMPONENTS_CACHE = false; /** - * Directory for caching extracted ODS tarball files and metadata. + * Absolute directory path for caching extracted ODS tarball files and metadata. + * Used to persist tarball contents between CLI runs. + * * @constant {string} */ -export const CACHE_DIR = path.resolve( +export const ODS_COMPONENTS_CACHE_DIR = path.resolve( 'packages/manager-tools/manager-muk-cli/target/.cache/ods-tarball', ); /** - * Path to the cached tarball contents (JSON representation of all files). + * Absolute path to the cached JSON file representing extracted tarball contents. + * This file stores the list of ODS files and their relative paths. + * * @constant {string} */ -export const TAR_CACHE_FILE = path.join(CACHE_DIR, 'ods-tarball-files.json'); +export const ODS_COMPONENTS_TAR_CACHE_FILE = path.join( + ODS_COMPONENTS_CACHE_DIR, + 'ods-tarball-files.json', +); /** - * Path to the cached metadata file (includes ODS version, checksum, timestamp). + * Absolute path to the metadata JSON file for the cached ODS tarball. + * Metadata includes version number, checksum, and timestamp. + * * @constant {string} */ -export const META_CACHE_FILE = path.join(CACHE_DIR, 'ods-tarball-meta.json'); +export const ODS_COMPONENTS_META_CACHE_FILE = path.join( + ODS_COMPONENTS_CACHE_DIR, + 'ods-tarball-meta.json', +); + +/** + * Cache settings for ODS documentation tarball + */ +export const ODS_DOCS_CACHE_DIR = path.resolve( + 'packages/manager-tools/manager-muk-cli/target/.cache/ods-docs', +); + +/** + * Path to the cached ODS documentation file map. + * + * Stores serialized tarball extraction results as `{ [entryPath]: Buffer }`. + * Used to avoid re-downloading or re-extracting ODS tarballs when fresh. + * + * @constant + * @type {string} + * @example + * "/packages/manager-tools/manager-muk-cli/target/.cache/ods-docs/ods-docs-files.json" + */ +export const ODS_DOCS_TAR_CACHE_FILE = path.join(ODS_DOCS_CACHE_DIR, 'ods-docs-files.json'); + +/** + * Path to the ODS documentation cache metadata file. + * + * Contains metadata describing the cache state: + * ``` + * { + * "version": "19.2.1", + * "checksum": "abc123...", + * "timestamp": 1728201000000 + * } + * ``` + * + * Used by `tarball-cache-utils.js` to validate cache freshness and version. + * + * @constant + * @type {string} + * @example + * "/packages/manager-tools/manager-muk-cli/target/.cache/ods-docs/ods-docs-meta.json" + */ +export const ODS_DOCS_META_CACHE_FILE = path.join(ODS_DOCS_CACHE_DIR, 'ods-docs-meta.json'); + +/** + * Global flag controlling whether ODS documentation caching is disabled. + * + * When `true`, the system skips all cache reads/writes and always downloads + * the latest ODS tarball. Useful for debugging or CI environments. + * + * @constant + * @type {boolean} + * @default false + * @example + * // Force re-fetch documentation on each run + * export const DISABLE_ODS_DOCS_CACHE = true; + */ +export const DISABLE_ODS_DOCS_CACHE = false; /** - * Base directories + * Absolute path to the Manager UI Kit (MUK) base package. + * + * @constant {string} */ export const MUK_COMPONENTS_PATH = path.resolve('packages/manager-ui-kit'); + +/** + * Absolute path to the source components directory within the Manager UI Kit. + * + * @constant {string} + */ export const MUK_COMPONENTS_SRC = path.join(MUK_COMPONENTS_PATH, 'src', 'components'); /** - * Target packages to check and potentially update. + * Absolute path to the Manager Wiki base package. + * + * @constant {string} + */ +export const MUK_WIKI_PATH = path.resolve('packages/manager-wiki'); + +/** + * Absolute path to the Manager Wiki components directory. + * This is where base component documentation (e.g. `base-component-doc`) is stored. + * + * @constant {string} + */ +export const MUK_WIKI_COMPONENTS = path.join( + MUK_WIKI_PATH, + 'stories', + 'manager-ui-kit', + 'components', +); + +/** + * Absolute path to the Manager Wiki components directory. + * This is where base component documentation (e.g. `base-component-doc`) is stored. + * + * @constant {string} + */ +export const MUK_WIKI_BASED_DOCUMENT = path.join( + MUK_WIKI_PATH, + 'stories', + 'manager-ui-kit', + 'base-documents', +); + +/** + * Wiki import-rewrite configuration + */ +export const MUK_IMPORT_REWRITE_RULES = [ + { + name: 'base-documents', + pattern: /((?:\.\.\/){2,3})src\//g, + replacer: (_, prefix) => `${prefix}base-documents/`, + }, + { + name: 'ods-react', + pattern: /(['"])[^'"]*ods-react\/src[^'"]*/g, + replacer: (_, quote) => `${quote}@ovhcloud/ods-react`, + }, +]; + +/** + * Storybook folder and path configuration + */ +export const MUK_STORYBOOK_FOLDERS = ['components', 'constants', 'helpers']; + +export const MUK_STORYBOOK_ENTRY_REGEX = + /packages\/storybook\/src\/(components|constants|helpers)\//; + +/** + * NPM package names that are validated and potentially updated + * during version synchronization and documentation refresh. + * + * @constant {string[]} */ export const TARGET_PACKAGES = [ '@ovhcloud/ods-components', @@ -42,17 +178,72 @@ export const TARGET_PACKAGES = [ ]; /** - * NPM registry base URL for package metadata. + * ODS React Package Name + * @type {string} + */ +export const ODS_REACT_PACKAGE_NAME = '@ovhcloud/ods-react'; + +/** + * Base URL for NPM registry metadata queries. + * + * @constant {string} */ export const NPM_REGISTRY_BASE = 'https://registry.npmjs.org'; /** - * NPM endpoints for latest ODS React tarball and metadata. + * Endpoint for retrieving the latest metadata of the ODS React package. + * Includes version, tarball URL, and dependency list. + * + * @constant {string} + */ +export const ODS_COMPONENTS_LATEST_URL = `${NPM_REGISTRY_BASE}/@ovhcloud%2Fods-react/latest`; + +/** + * Subpath (within the GitHub tarball) that contains the ODS component stories. + * Used to filter relevant entries during extraction. + * + * @constant {string} + */ +export const ODS_TAR_COMPONENTS_PATH = '/packages/storybook/stories/components/'; + +/** + * Absolute path (within the ODS tarball) to Storybook base source files. + * These include components, constants, and helpers inside `packages/storybook/src`. + * + * @constant {string} */ -export const ODS_REACT_LATEST_URL = `${NPM_REGISTRY_BASE}/@ovhcloud%2Fods-react/latest`; +export const ODS_TAR_STORYBOOK_PATH = 'packages/storybook/src/'; /** - * Log formatting constants + * GitHub repository slug (organization/name) where the ODS tarball is hosted. + * + * @constant {string} + */ +export const ODS_GITHUB_REPO_NAME = 'ovh/design-system'; + +/** + * ODS Repository Base Url where the ODS tarball is hosted. + * + * @constant {string} + */ +export const ODS_GITHUB_REPO_BASE_URL = `https://github.com/${ODS_GITHUB_REPO_NAME}/archive/refs/tags`; + +/** + * Emoji constants used for consistent CLI log formatting. + * These provide visual cues in console output. + * + * @typedef {Object} Emojis + * @property {string} info - Informational messages. + * @property {string} check - Validation success. + * @property {string} folder - Directory operations. + * @property {string} package - Package operations. + * @property {string} warn - Warnings or recoverable issues. + * @property {string} error - Fatal or critical errors. + * @property {string} success - Successful completion. + * @property {string} disk - File system operations. + * @property {string} rocket - Final success or completion. + * + * @constant {Emojis} */ export const EMOJIS = { info: 'ℹ️', @@ -67,6 +258,10 @@ export const EMOJIS = { }; /** - * Excluded ODS components + * List of ODS component names excluded from synchronization. + * These components are either deprecated, handled manually, + * or incompatible with automated documentation sync. + * + * @constant {string[]} */ export const ODS_EXCLUDED_COMPONENTS = ['pagination']; diff --git a/packages/manager-tools/manager-muk-cli/src/core/component-documentation-utils.js b/packages/manager-tools/manager-muk-cli/src/core/component-documentation-utils.js new file mode 100644 index 000000000000..8bafa58e0375 --- /dev/null +++ b/packages/manager-tools/manager-muk-cli/src/core/component-documentation-utils.js @@ -0,0 +1,397 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import process from 'node:process'; +import { pipeline } from 'node:stream/promises'; + +import { + EMOJIS, + MUK_IMPORT_REWRITE_RULES, + MUK_STORYBOOK_ENTRY_REGEX, + MUK_STORYBOOK_FOLDERS, + MUK_WIKI_BASED_DOCUMENT, + MUK_WIKI_COMPONENTS, + ODS_REACT_PACKAGE_NAME, + ODS_TAR_COMPONENTS_PATH, + ODS_TAR_STORYBOOK_PATH, +} from '../config/muk-config.js'; +import { logger } from '../utils/log-manager.js'; +import { ensureDir } from './file-utils.js'; +import { fetchLatestVersion } from './npm-utils.js'; +import { + extractComponentDocumentationInfos, + extractDesignSystemDocs, +} from './ods-documentation-tarball-utils.js'; +import { createAsyncQueue } from './tasks-utils.js'; + +/** + * Prepares target directories for a component’s base documentation. + * + * **Behavior:** + * - If the component folder does not exist β†’ create it. + * - If the `base-component-doc` folder already exists β†’ delete its contents. + * - Always ensure the final directory structure exists before streaming files. + * + * **Why:** + * Each synchronization cycle must start from a clean baseline + * to avoid stale or conflicting documentation files. + * + * @param {string} componentDir - Absolute path to the component directory. + * @param {string} baseDocDir - Absolute path to the `base-component-doc` folder. + */ +function prepareComponentDocumentationDir(componentDir, baseDocDir) { + if (fs.existsSync(baseDocDir)) { + fs.rmSync(baseDocDir, { recursive: true, force: true }); + } else if (!fs.existsSync(componentDir)) { + ensureDir(componentDir); + } + ensureDir(baseDocDir); +} + +/** + * Initialize or refresh a component documentation directory. + * + * Handles three cases: + * - Component folder does not exist β†’ create new folder and base-doc. + * - Component folder exists β†’ clean and reinitialize base-doc. + * - Always ensures base-doc directory exists before file writes. + * + * @param {string} component - Component name. + * @returns {{componentDir: string, baseDocDir: string, isNew: boolean, isUpdated: boolean}} + */ +function initializeComponentDocs(component) { + const componentDir = path.join(MUK_WIKI_COMPONENTS, component); + const baseDocDir = path.join(componentDir, 'base-component-doc'); + const exists = fs.existsSync(componentDir); + + if (exists) { + logger.info(`${EMOJIS.folder} Found existing component: '${component}'`); + } else { + logger.info(`${EMOJIS.folder} Creating new component directory: '${component}'`); + } + + prepareComponentDocumentationDir(componentDir, baseDocDir); + + if (exists) { + logger.info(`${EMOJIS.disk} Cleared and ready: ${path.relative(process.cwd(), baseDocDir)}`); + } else { + logger.info(`${EMOJIS.rocket} Initialized new base-doc folder for '${component}'`); + } + + return { + componentDir, + baseDocDir, + isNew: !exists, + isUpdated: exists, + }; +} + +/** + * Stream a single documentation file to disk. + * + * Handles subdirectory creation, error handling, and logging. + * + * @param {import('node:stream').Readable} stream - The file stream. + * @param {string} baseDocDir - Base documentation directory for the component. + * @param {string} relPath - Relative path of the file within the component. + */ +async function writeComponentDocFile(stream, baseDocDir, relPath) { + const destFile = path.join(baseDocDir, relPath); + const subDir = path.dirname(destFile); + + ensureDir(subDir); + + const relativeTarget = path.relative(process.cwd(), destFile); + logger.info(`${EMOJIS.disk} Writing file β†’ ${relativeTarget}`); + + try { + await pipeline(stream, fs.createWriteStream(destFile)); + } catch (err) { + logger.error(`${EMOJIS.error} Failed to write ${relativeTarget}: ${err.message}`); + } +} + +/** + * Stream extracted documentation files to disk directly as they are read + * from the ODS Design System tarball. + * + * This function orchestrates: + * 1️⃣ Parsing tar paths β†’ (component, relPath) + * 2️⃣ Initializing per-component folders (once) + * 3️⃣ Writing documentation files via streaming + * + * @param {AsyncGenerator<{tarPath: string, stream: import('node:stream').Readable}>} fileStreamGenerator + * @returns {Promise<{created: number, updated: number, total: number}>} + */ +async function streamComponentDocs(fileStreamGenerator) { + const initializedComponents = new Set(); + let created = 0; + let updated = 0; + let total = 0; + + logger.info(`${EMOJIS.info} Starting component documentation sync (streaming mode)…`); + + for await (const { tarPath, stream } of fileStreamGenerator) { + const { component, relPath } = extractComponentDocumentationInfos(tarPath); + if (!component || !relPath) { + logger.debug?.(`Skipping unrelated entry: ${tarPath}`); + continue; + } + + let baseDocDir; + + // Initialize directories once per component + if (!initializedComponents.has(component)) { + const { baseDocDir: docDir, isNew, isUpdated } = initializeComponentDocs(component); + initializedComponents.add(component); + baseDocDir = docDir; + + if (isNew) created++; + if (isUpdated) updated++; + } else { + baseDocDir = path.join(MUK_WIKI_COMPONENTS, component, 'base-component-doc'); + } + + // Stream file into local folder + await writeComponentDocFile(stream, baseDocDir, relPath); + total++; + } + + logger.success( + `${EMOJIS.check} Completed streaming sync β€” created: ${created}, updated: ${updated}, files written: ${total}`, + ); + + return { created, updated, total }; +} + +/** + * Filter ODS tarball entries to include only component documentation files. + * + * Matches files under the Design System's component documentation path, such as: + * - documentation.mdx + * - technical-information.mdx + * - .stories.tsx (storybook files) + * + * @param {string} filePath - Full path of a tarball entry. + * @returns {boolean} True if the entry should be processed. + */ +function isOdsComponentDocEntry(filePath) { + return ( + filePath.includes(ODS_TAR_COMPONENTS_PATH) && + (filePath.endsWith('.mdx') || filePath.endsWith('.md') || filePath.endsWith('.stories.tsx')) + ); +} + +/** + * Synchronize all ODS component documentation files. + * Streams entries from GitHub tarball (or cache) into wiki component directories. + * + * Uses a streaming producer–consumer pipeline: + * - Producer β†’ extractDesignSystemDocs (streams tar entries) + * - Queue β†’ createAsyncQueue (handles backpressure) + * - Consumer β†’ streamComponentDocs (writes files to disk) + * + * @async + * @returns {Promise<{created: number, updated: number, total: number}>} + */ +export async function syncComponentDocs() { + const queue = createAsyncQueue(); + + // 🧩 Producer: fetch latest ODS React version + const latestVersion = await fetchLatestVersion(ODS_REACT_PACKAGE_NAME); + logger.info(`${EMOJIS.info} ODS React latest version: ${latestVersion}`); + + await (async () => { + await extractDesignSystemDocs({ + tag: latestVersion, + filter: isOdsComponentDocEntry, + onFileStream: async (tarPath, fileStream) => { + await queue.push({ tarPath, stream: fileStream }); + }, + }); + queue.end(); // Signal the end of production + })(); + + // πŸ’Ύ Consumer: write streamed documentation files + return streamComponentDocs(queue); +} + +/** + * Determines whether a tar entry corresponds to Storybook source files + * under `packages/storybook/src/{components,constants,helpers}`. + * + * @param {string} tarPath + * @returns {boolean} + */ +function isStorybookBaseDocEntry(tarPath) { + const normalized = tarPath.replaceAll('\\', '/'); + return MUK_STORYBOOK_ENTRY_REGEX.test(normalized); +} + +/** + * Maps a tar entry path under `packages/storybook/src/` + * to the Manager Wiki base-documents directory. + * + * Example: + * design-system-19.2.1/packages/storybook/src/helpers/date/formatDate.ts + * β†’ packages/manager-wiki/stories/manager-ui-kit/base-documents/helpers/date/formatDate.ts + */ +function mapStorybookPathToWiki(tarPath) { + const normalized = tarPath.replaceAll('\\', '/'); + const marker = ODS_TAR_STORYBOOK_PATH; + const idx = normalized.indexOf(marker); + + if (idx === -1) { + logger.warn(`${EMOJIS.warn} Unexpected tar path: ${tarPath}`); + return path.join(MUK_WIKI_BASED_DOCUMENT, path.basename(tarPath)); + } + + const rel = normalized.substring(idx + marker.length); + return path.join(MUK_WIKI_BASED_DOCUMENT, rel); +} + +/** + * Writes a streamed Storybook file from the tarball to disk. + * Creates all required directories if they do not exist. + * + * @async + * @param {string} tarPath - Path of the file in the tar archive. + * @param {ReadableStream} fileStream - The stream for the tar entry. + * @returns {Promise<{created:number, updated:number}>} Statistics on the write operation. + */ +async function writeStorybookFile(tarPath, fileStream) { + const target = mapStorybookPathToWiki(tarPath); + await ensureDir(path.dirname(target)); + try { + await pipeline(fileStream, fs.createWriteStream(target)); + logger.debug(`${EMOJIS.disk} ${path.relative(process.cwd(), target)}`); + return { created: 1, updated: 0 }; + } catch (err) { + logger.error(`${EMOJIS.error} Failed to write ${target}: ${err.message}`); + return { created: 0, updated: 0 }; + } +} + +/** + * Ensures that all base Storybook folders exist in the wiki output, + * even if the tarball doesn’t contain any file for them. + * + * This prevents missing directories like "helpers" or "constants" + * when they have no eligible files or only subfolders. + * + * @param {string} baseDir - The base wiki directory (MUK_WIKI_BASED_DOCUMENT). + */ +function ensureBaseStorybookFolders(baseDir) { + for (const directory of MUK_STORYBOOK_FOLDERS) { + const target = path.join(baseDir, directory); + if (!fs.existsSync(target)) { + fs.mkdirSync(target, { recursive: true }); + logger.info( + `${EMOJIS.folder} Created base Storybook folder: ${path.relative(process.cwd(), target)}`, + ); + } + } +} + +/** + * Synchronizes all Storybook base documents from the ODS tarball into + * the Manager Wiki. This includes every file located under: + * + * - packages/storybook/src/components/ + * - packages/storybook/src/constants/ + * - packages/storybook/src/helpers/ + * + * @async + * @param {Object} [options] - Optional configuration. + * @param {string} [options.tag] - Specific ODS release tag to download. Defaults to latest. + * @returns {Promise<{created:number, updated:number, total:number}>} + * Count of streamed and written files. + * + * @example + * await syncStorybookBaseDocuments({ tag: '19.2.1' }) + * // β†’ { created: 53, updated: 0, total: 53 } + */ +export async function syncStorybookBaseDocuments({ tag } = {}) { + let created = 0; + let updated = 0; + let total = 0; + + // πŸ—‚ Ensure base folders exist even if empty + ensureBaseStorybookFolders(MUK_WIKI_BASED_DOCUMENT); + + await extractDesignSystemDocs({ + tag, + filter: isStorybookBaseDocEntry, + onFileStream: async (tarPath, fileStream) => { + const res = await writeStorybookFile(tarPath, fileStream); + created += res.created; + updated += res.updated; + total += 1; + }, + }); + + return { created, updated, total }; +} + +/** + * Recursively collect all source files inside a directory. + * @param {string} dir - The root directory to scan. + * @param {string[]} exts - File extensions to include (e.g. ['.ts', '.tsx', '.mdx']) + * @returns {string[]} Absolute paths of matching files. + */ +function collectSourceFiles(dir, exts = ['.ts', '.tsx', '.mdx']) { + const entries = fs.readdirSync(dir, { withFileTypes: true }); + const files = []; + + for (const entry of entries) { + const fullPath = path.join(dir, entry.name); + if (entry.isDirectory()) { + files.push(...collectSourceFiles(fullPath, exts)); + } else if (exts.includes(path.extname(entry.name))) { + files.push(fullPath); + } + } + + return files; +} + +/** + * Apply import path rewrites for Manager Wiki component documentation. + * + * Rewrites two patterns: + * 1) `../../../src/` β†’ `../../../base-documents/` + * 2) `/ods-react/src/` β†’ `@ovhcloud/ods-react` + * + * Example: + * ```diff + * - import { CONTROL_CATEGORY } from '../../../src/constants/controls'; + * + import { CONTROL_CATEGORY } from '../../../base-documents/constants/controls'; + * + * - import { Accordion } from '../../../../ods-react/src/components/accordion/src'; + * + import { Accordion } from '@ovhcloud/ods-react'; + * ``` + * + * @param {string} componentsRoot - Absolute path to wiki components root (MUK_WIKI_COMPONENTS) + */ +export function rewriteWikiComponentImports(componentsRoot) { + const files = collectSourceFiles(componentsRoot); + let updatedCount = 0; + + logger.info(`${EMOJIS.info} Rewriting import paths inside wiki component documentation…`); + + for (const file of files) { + let content = fs.readFileSync(file, 'utf8'); + let modified = content; + + for (const rule of MUK_IMPORT_REWRITE_RULES) { + modified = modified.replace(rule.pattern, rule.replacer); + } + + if (modified !== content) { + fs.writeFileSync(file, modified, 'utf8'); + updatedCount++; + logger.info(`${EMOJIS.disk} Updated imports β†’ ${path.relative(process.cwd(), file)}`); + } + } + + logger.success(`${EMOJIS.check} Rewrote imports in ${updatedCount} files.`); +} diff --git a/packages/manager-tools/manager-muk-cli/src/core/component-utils.js b/packages/manager-tools/manager-muk-cli/src/core/component-utils.js index 98de554842d7..0e109ea0554a 100644 --- a/packages/manager-tools/manager-muk-cli/src/core/component-utils.js +++ b/packages/manager-tools/manager-muk-cli/src/core/component-utils.js @@ -1,13 +1,32 @@ +import { EMOJIS } from '../config/muk-config.js'; import { logger } from '../utils/log-manager.js'; -import { detectHasChildrenFromTarball } from './ods-tarball-utils.js'; +import { detectHasChildrenFromTarball } from './ods-components-tarball-utils.js'; /** - * Groups ODS components dynamically based on naming and source analysis. - * - Simple components (combobox, datepicker, range) β†’ flat grouping - * - Nested families (form-field-error, form-field-helper) β†’ dynamic detection + * Dynamically groups ODS components based on their naming structure. * - * @param {string[]} components - All ODS component names (kebab-case) - * @returns {Promise>} - Mapping of { parent β†’ [children] } + * Example: + * ``` + * ['button', 'form-field', 'form-field-error', 'form-field-helper'] + * β†’ { 'button': [], 'form-field': ['form-field-error', 'form-field-helper'] } + * ``` + * + * The algorithm: + * 1. Splits each kebab-case component name into parts. + * 2. For single-part names, marks them as standalone. + * 3. For multi-part names, progressively checks whether intermediate + * prefixes (e.g., `form-field`) represent actual component families + * that have subcomponents in the tarball. + * 4. Logs a summary of detected parent-child relationships. + * + * Why: + * - Some ODS components (like `form-field-error`) are logically children + * of a higher-level component (`form-field`). Grouping these helps + * generate structured documentation and avoid duplication. + * + * @async + * @param {string[]} components - All ODS component names in kebab-case. + * @returns {Promise>} - Map of `{ parent β†’ [children] }`. */ export async function groupComponentsDynamically(components) { const grouped = {}; @@ -21,7 +40,7 @@ export async function groupComponentsDynamically(components) { continue; } - // Try dynamic detection only for multi-part names + // Multi-part components β€” progressively detect if any prefix is a parent let parent = parts[0]; let candidate = parts[0]; @@ -29,23 +48,19 @@ export async function groupComponentsDynamically(components) { candidate = `${candidate}-${parts[i]}`; const hasChildren = await detectHasChildrenFromTarball(candidate); - if (hasChildren) { - parent = candidate; - } + if (hasChildren) parent = candidate; } - // fallback to flat grouping if parent not found + // Fallback: treat as flat component if no hierarchy detected if (!grouped[parent]) grouped[parent] = []; - if (parent !== name) grouped[parent].push(name); } - // Log grouping summary + // Log grouping summary for debugging and visibility const summary = Object.entries(grouped) .map(([p, c]) => `${p} β†’ ${c.length ? c.join(', ') : '(no children)'}`) .join('\n'); - - logger.info(`πŸ“¦ Dynamic grouping summary:\n${summary}`); + logger.info(`${EMOJIS.package} Dynamic grouping summary:\n${summary}`); return grouped; } diff --git a/packages/manager-tools/manager-muk-cli/src/core/github-tarball-utils.js b/packages/manager-tools/manager-muk-cli/src/core/github-tarball-utils.js deleted file mode 100644 index 8ffd2fe23dd5..000000000000 --- a/packages/manager-tools/manager-muk-cli/src/core/github-tarball-utils.js +++ /dev/null @@ -1,103 +0,0 @@ -import https from 'node:https'; - -import { logger } from '../utils/log-manager.js'; -import { streamTarGz } from './tarball-cache-utils.js'; - -/** - * Internal utility β€” fetch JSON data with redirect and error handling. - * @param {string} url - The API URL to fetch. - * @param {number} [redirects=5] - Remaining redirect limit. - * @returns {Promise} Parsed JSON response. - */ -async function fetchJson(url, redirects = 5) { - return new Promise((resolve, reject) => { - https - .get( - url, - { - headers: { - 'User-Agent': 'manager-muk-cli', - Accept: 'application/vnd.github+json', - }, - }, - (res) => { - const { statusCode, headers } = res; - - // Follow redirects - if (statusCode >= 300 && statusCode < 400 && headers.location && redirects > 0) { - res.resume(); - return resolve(fetchJson(headers.location, redirects - 1)); - } - - if (statusCode !== 200) { - return reject(new Error(`Failed to fetch ${url} (status ${statusCode})`)); - } - - let data = ''; - res.setEncoding('utf8'); - res.on('data', (chunk) => (data += chunk)); - res.on('end', () => { - try { - resolve(JSON.parse(data)); - } catch (err) { - reject(new Error(`Invalid JSON from ${url}: ${err.message}`)); - } - }); - }, - ) - .on('error', reject); - }); -} - -/** - * Fetch the latest release tag for a given GitHub repository. - * @param {string} owner - GitHub organization or user. - * @param {string} repo - Repository name. - * @returns {Promise} Latest tag name (e.g., "v19.3.1"). - */ -export async function getLatestTag(owner, repo) { - const url = `https://api.github.com/repos/${owner}/${repo}/tags?per_page=1`; - const tags = await fetchJson(url); - const tag = tags?.[0]?.name; - if (!tag) throw new Error(`No tag found for ${owner}/${repo}`); - logger.info(`🏷️ Latest tag for ${owner}/${repo}: ${tag}`); - return tag; -} - -/** - * @typedef {object} ExtractDocsOptions - * @property {(entryPath: string) => boolean} filter - Function to select which files to extract. - * @property {(entryPath: string, content: Buffer) => Promise} onFile - Async handler for extracted files. - * @property {string} [tag] - Optional tag to use (defaults to latest). - */ - -/** - * Download and extract the OVH Design System documentation files from GitHub. - * - * Automatically fetches the latest version unless a tag is provided. - * Streams files directly without writing temporary archives. - * - * @param {ExtractDocsOptions} options - Extraction configuration. - * @returns {Promise} Resolves when extraction is complete. - * - * @example - * await extractDesignSystemDocs({ - * filter: (p) => p.includes('/stories/components/'), - * onFile: async (p, buf) => fs.writeFileSync(`local/${p}`, buf), - * }); - */ -export async function extractDesignSystemDocs({ filter, onFile, tag }) { - const owner = 'ovh'; - const repo = 'design-system'; - const version = tag || (await getLatestTag(owner, repo)); - const url = `https://github.com/${owner}/${repo}/archive/refs/tags/${version}.tar.gz`; - - logger.info(`πŸ“¦ Downloading OVH Design System ${version} from GitHub`); - try { - await streamTarGz(url, filter, onFile); - logger.success(`βœ… Successfully extracted files from ${repo}@${version}`); - } catch (err) { - logger.error(`❌ Failed to extract ${repo}@${version}: ${err.message}`); - throw err; - } -} diff --git a/packages/manager-tools/manager-muk-cli/src/core/ods-tarball-utils.js b/packages/manager-tools/manager-muk-cli/src/core/ods-components-tarball-utils.js similarity index 57% rename from packages/manager-tools/manager-muk-cli/src/core/ods-tarball-utils.js rename to packages/manager-tools/manager-muk-cli/src/core/ods-components-tarball-utils.js index 2c2d2c546833..01e8c6bc2bc4 100644 --- a/packages/manager-tools/manager-muk-cli/src/core/ods-tarball-utils.js +++ b/packages/manager-tools/manager-muk-cli/src/core/ods-components-tarball-utils.js @@ -1,25 +1,32 @@ import https from 'node:https'; -import process from 'node:process'; import { - CACHE_DIR, + DISABLE_ODS_COMPONENTS_CACHE, EMOJIS, - META_CACHE_FILE, - ODS_REACT_LATEST_URL, - TAR_CACHE_FILE, + ODS_COMPONENTS_CACHE_DIR, + ODS_COMPONENTS_LATEST_URL, + ODS_COMPONENTS_META_CACHE_FILE, + ODS_COMPONENTS_TAR_CACHE_FILE, } from '../config/muk-config.js'; import { logger } from '../utils/log-manager.js'; import { toPascalCase } from './file-utils.js'; -import { createTarballCache, streamTarGz } from './tarball-cache-utils.js'; +import { createTarballCache } from './tarball-cache-utils.js'; +import { streamTarGz } from './tarball-utils.js'; /** - * Fetch metadata for the latest ODS React package from npm. - * @returns {Promise<{ version: string, tarball: string }>} Latest version and tarball URL. + * Fetch the latest metadata for the ODS Components NPM package. + * + * This function retrieves the `version` and `dist.tarball` URL from the NPM registry. + * It is used by `extractOdsComponentsTarball()` to determine which version of the + * tarball to download. + * + * @async + * @returns {Promise<{ version: string, tarball: string }>} Package version and tarball URL. */ -export async function getOdsPackageMetadata() { +export async function getOdsComponentsPackageMetadata() { return new Promise((resolve, reject) => { https - .get(ODS_REACT_LATEST_URL, (res) => { + .get(ODS_COMPONENTS_LATEST_URL, (res) => { let data = ''; res.on('data', (chunk) => (data += chunk)); res.on('end', () => { @@ -36,26 +43,30 @@ export async function getOdsPackageMetadata() { } /** - * Download, extract, and cache the ODS React tarball. - * If a valid cache exists, it is used instead of re-downloading. + * Extracts and caches the contents of the latest ODS Components tarball. + * + * The tarball is downloaded from the NPM registry, decompressed, and parsed. + * Extracted file contents are stored in a `Map` keyed by their relative paths. * - * @param {RegExp} [pattern] - Optional regex filter to extract only matching entries. - * @returns {Promise>} Map of file paths β†’ contents. + * To avoid redundant network calls, the result is cached locally using + * `createTarballCache()`. Cache can be disabled with the environment variable: + * `ADD_COMPONENTS_NO_CACHE=1` + * + * @async + * @param {RegExp} [pattern] - Optional RegExp to filter which paths to include. + * @returns {Promise>} Map of file paths β†’ file contents (UTF-8). */ -export async function extractOdsTarball(pattern) { - const { version, tarball } = await getOdsPackageMetadata(); +export async function extractOdsComponentsTarball(pattern) { + const { version, tarball } = await getOdsComponentsPackageMetadata(); - // Use functional cache version const cache = createTarballCache({ - cacheDir: CACHE_DIR, - metaFile: META_CACHE_FILE, - dataFile: TAR_CACHE_FILE, + cacheDir: ODS_COMPONENTS_CACHE_DIR, + metaFile: ODS_COMPONENTS_META_CACHE_FILE, + dataFile: ODS_COMPONENTS_TAR_CACHE_FILE, }); - const disableCache = !!process.env.ADD_COMPONENTS_NO_CACHE; - - // Load from cache if available and not disabled - if (!disableCache) { + // Try to load from cache unless disabled + if (!DISABLE_ODS_COMPONENTS_CACHE) { const cached = cache.load(version); if (cached) { if (pattern) { @@ -82,15 +93,11 @@ export async function extractOdsTarball(pattern) { } /** - * @typedef {Object} OdsPathContext - * @property {string} parent - Kebab-case parent component name. - * @property {string} target - Subcomponent or parent name. - * @property {string} pascalParent - PascalCase parent name. - * @property {string} pascalSub - PascalCase subcomponent name. - */ - -/** - * Centralized declarative pattern definitions for ODS component paths. + * Component file path templates used to locate `.tsx` source files + * across various ODS directory structures. + * + * Some components live under `src/components`, others under + * `package/src/components`, and subcomponents often use PascalCase folders. */ const ODS_PATH_PATTERNS = { withSub: { @@ -106,10 +113,10 @@ const ODS_PATH_PATTERNS = { }; /** - * Expand a path template using contextual replacements. - * @param {string} template - Template string with placeholders. - * @param {OdsPathContext} context - Replacement context. - * @returns {string} Expanded path string. + * Expand a template string with provided path parameters. + * @param {string} template - Path template with placeholders. + * @param {object} context - Replacement values for placeholders. + * @returns {string} Expanded file path. */ function expandTemplate(template, context) { return template @@ -120,16 +127,17 @@ function expandTemplate(template, context) { } /** - * Factory that builds possible ODS source file paths for a given component. + * Factory function that returns path builders for ODS component files. + * * @param {string} parent - Parent component name (kebab-case). - * @param {string} [subcomponent] - Optional subcomponent name (kebab-case). + * @param {string} [subcomponent] - Optional subcomponent name. * @returns {{ - * buildAll: () => string[], - * build: (filter?: string) => string[], - * buildByKey: (key: string) => string[] + * buildAll(): string[], + * build(filter?: string): string[], + * buildByKey(key: string): string[] * }} */ -function createOdsPath(parent, subcomponent) { +function createOdsComponentsPath(parent, subcomponent) { const pascalParent = toPascalCase(parent); const pascalSub = subcomponent ? toPascalCase(subcomponent) : pascalParent; const target = subcomponent ?? parent; @@ -161,13 +169,14 @@ function createOdsPath(parent, subcomponent) { } /** - * Find and return the source file content for a given component path list. - * @param {Map} files - Tarball-extracted file map. - * @param {string[]} possiblePaths - Candidate relative paths. - * @param {string} name - Component name for logging. - * @returns {string|null} UTF-8 content or null. + * Attempt to find and return a matching component source file from the tarball. + * + * @param {Map} files - Map of tarball entries. + * @param {string[]} possiblePaths - List of possible candidate paths. + * @param {string} name - Component name (for logging). + * @returns {string|null} File content as UTF-8 string, or null if not found. */ -function findOdsSourceFile(files, possiblePaths, name) { +function findOdsComponentsSourceFile(files, possiblePaths, name) { const fileEntry = possiblePaths.map((p) => files.get(p)).find(Boolean); if (!fileEntry) { logger.warn(`⚠ Could not find source file for ${name}`); @@ -177,9 +186,15 @@ function findOdsSourceFile(files, possiblePaths, name) { } /** - * Heuristic detection of `children` support in ODS component source. - * @param {string} content - Component source code. - * @returns {boolean} True if children detected. + * Simple heuristic-based detection for `children` support in React components. + * Checks for patterns like: + * - `PropsWithChildren` + * - `children:` in prop definitions + * - `props.children` usage + * - JSX child placeholders + * + * @param {string} content - TypeScript source file content. + * @returns {boolean} Whether the component likely accepts `children`. */ function detectChildrenHeuristics(content) { return [ @@ -191,16 +206,21 @@ function detectChildrenHeuristics(content) { } /** - * Detect if an ODS component supports children based on source analysis. - * @param {string} parent - Kebab-case parent name. + * Detect whether an ODS component (or subcomponent) supports React `children`. + * + * Downloads (or loads from cache) the ODS tarball, locates the relevant `.tsx` file, + * and applies heuristic-based analysis. + * + * @async + * @param {string} parent - Component name (kebab-case). * @param {string} [subcomponent] - Optional subcomponent. - * @returns {Promise} true = supports children, false = stateless, null = not found. + * @returns {Promise} True if supports children, false otherwise, null if not found. */ export async function detectHasChildrenFromTarball(parent, subcomponent) { - const files = await extractOdsTarball(); - const factory = createOdsPath(parent, subcomponent); + const files = await extractOdsComponentsTarball(); + const factory = createOdsComponentsPath(parent, subcomponent); const possiblePaths = factory.buildAll(); - const content = findOdsSourceFile(files, possiblePaths, subcomponent ?? parent); + const content = findOdsComponentsSourceFile(files, possiblePaths, subcomponent ?? parent); if (!content) return null; const hasChildren = detectChildrenHeuristics(content); @@ -215,13 +235,17 @@ export async function detectHasChildrenFromTarball(parent, subcomponent) { } /** - * Detect whether a subcomponent has its own exported Prop type in the parent index.ts. - * @param {string} parent - Parent component (e.g., "tooltip"). - * @param {string} subcomponent - Subcomponent (e.g., "tooltip-trigger"). - * @returns {Promise} True if type export exists. + * Detect whether a subcomponent exports its own prop type (e.g., `type MySubProp`). + * + * Used to infer type granularity for documentation generation. + * + * @async + * @param {string} parent - Parent component name. + * @param {string} subcomponent - Subcomponent name. + * @returns {Promise} Whether a prop type is exported from its index. */ export async function detectHasTypeExportFromIndex(parent, subcomponent) { - const files = await extractOdsTarball(); + const files = await extractOdsComponentsTarball(); const possiblePaths = [ `src/components/${parent}/src/index.ts`, `package/src/components/${parent}/src/index.ts`, @@ -247,12 +271,14 @@ export async function detectHasTypeExportFromIndex(parent, subcomponent) { } /** - * Extract hooks, constants, and external types from an ODS component index.ts. - * @param {string} parent - ODS component name (e.g., "datepicker"). - * @returns {Promise<{ hooks: string[], constants: string[], externalTypes: string[] }>} + * Extract categorized exports (hooks, constants, external types) from a component index file. + * + * @async + * @param {string} parent - Component name. + * @returns {Promise<{hooks: string[], constants: string[], externalTypes: string[]}>} */ -export async function extractOdsExportsByCategory(parent) { - const files = await extractOdsTarball(/src\/components\/.*\/src\/index\.ts$/); +export async function extractOdsComponentsExportsByCategory(parent) { + const files = await extractOdsComponentsTarball(/src\/components\/.*\/src\/index\.ts$/); const entry = [...files.entries()].find(([p]) => p.endsWith(`src/components/${parent}/src/index.ts`), ); @@ -275,6 +301,7 @@ export async function extractOdsExportsByCategory(parent) { .map((i) => i.trim()) .filter(Boolean); + // Ignore re-exports from subcomponents if (fromPath.includes('components')) continue; identifiers diff --git a/packages/manager-tools/manager-muk-cli/src/core/ods-documentation-tarball-utils.js b/packages/manager-tools/manager-muk-cli/src/core/ods-documentation-tarball-utils.js new file mode 100644 index 000000000000..09e6ea5ff03c --- /dev/null +++ b/packages/manager-tools/manager-muk-cli/src/core/ods-documentation-tarball-utils.js @@ -0,0 +1,167 @@ +import { Buffer } from 'node:buffer'; +import { Readable } from 'node:stream'; + +import { + DISABLE_ODS_DOCS_CACHE, + EMOJIS, + ODS_DOCS_CACHE_DIR, + ODS_DOCS_META_CACHE_FILE, + ODS_DOCS_TAR_CACHE_FILE, + ODS_GITHUB_REPO_BASE_URL, + ODS_TAR_COMPONENTS_PATH, +} from '../config/muk-config.js'; +import { logger } from '../utils/log-manager.js'; +import { getOdsComponentsPackageMetadata } from './ods-components-tarball-utils.js'; +import { createTarballCache } from './tarball-cache-utils.js'; +import { streamTarGz } from './tarball-utils.js'; + +/** + * Normalize cached data to an iterable [path, buffer] format. + * @param {Map|Object} cached - Cached data structure. + * @returns {[string, Buffer][]} + */ +function normalizeCacheEntries(cached) { + if (cached instanceof Map) return [...cached.entries()]; + if (cached && typeof cached === 'object') return Object.entries(cached); + return []; +} + +/** + * Safely load cached ODS documentation, handling both Map and plain objects. + * @param {ReturnType} cache + * @param {string} version + * @returns {[string, Buffer][]|null} + */ +function getOdsDocsCache(cache, version) { + const cached = cache.load(version); + if (!cached) return null; + + const entries = normalizeCacheEntries(cached); + if (entries.length === 0) { + logger.warn(`⚠️ Cache is valid but empty, skipping stream.`); + return null; + } + + // 🧠 Validation: ensure cache includes storybook/src files + const hasStorybookSrc = entries.some(([p]) => p.includes('packages/storybook/src/')); + if (!hasStorybookSrc) { + logger.warn(`${EMOJIS.warn} Cache missing Storybook sources β€” forcing re-download.`); + return null; + } + + logger.info(`${EMOJIS.check} Using cached ODS documentation (v${version})`); + return entries; +} + +/** + * Stream documentation files from cache after applying filter. + */ +async function streamCachedDocs(entries, filter, onFile, onFileStream) { + let streamed = 0; + + for (const [entryPath, buffer] of entries) { + if (!filter(entryPath)) continue; + + const buf = Buffer.isBuffer(buffer) ? buffer : Buffer.from(buffer); + if (onFileStream) { + const stream = Readable.from(buf); + await onFileStream(entryPath, stream); + } else { + await onFile(entryPath, buf); + } + streamed++; + } + + logger.success(`${EMOJIS.disk} Streamed ${streamed} documentation files from cache.`); +} + +/** + * Download ODS docs tarball, stream files, and save cache. + * The filter is applied *after* caching, so the cache always contains the full tarball. + */ +async function downloadAndCacheDocs({ url, version, filter, onFile, onFileStream, cache }) { + logger.info(`${EMOJIS.package} Fetching ODS Design System tarball from ${url}`); + const files = new Map(); + + await streamTarGz( + url, + () => true, + async (entryPath, content) => { + const buffer = Buffer.isBuffer(content) ? content : Buffer.from(content); + files.set(entryPath, buffer); + + // Apply filter only for streamed consumer events + if (!filter(entryPath)) return; + + if (onFileStream) { + const stream = Readable.from(buffer); + await onFileStream(entryPath, stream); + } else { + await onFile(entryPath, buffer); + } + }, + ); + + cache.save(version, files); + logger.success( + `${EMOJIS.check} Cached ODS Design System documentation (v${version}) for future runs.`, + ); +} + +/** + * High-level orchestrator for ODS Design System documentation extraction. + * Uses cache when available, otherwise streams from tarball. + */ +export async function extractDesignSystemDocs({ + filter = () => true, + onFile = async () => {}, + onFileStream = null, + tag = null, +}) { + const { version } = await getOdsComponentsPackageMetadata(); + const resolvedTag = tag ?? version; + const url = `${ODS_GITHUB_REPO_BASE_URL}/v${resolvedTag}.tar.gz`; + + logger.info(`${EMOJIS.package} Preparing to extract ODS docs (v${resolvedTag})…`); + + const cache = createTarballCache({ + cacheDir: ODS_DOCS_CACHE_DIR, + metaFile: ODS_DOCS_META_CACHE_FILE, + dataFile: ODS_DOCS_TAR_CACHE_FILE, + }); + + if (!DISABLE_ODS_DOCS_CACHE) { + const entries = getOdsDocsCache(cache, version); + if (entries) { + await streamCachedDocs(entries, filter, onFile, onFileStream); + return; + } + } + + await downloadAndCacheDocs({ + url, + version, + filter, + onFile, + onFileStream, + cache, + }); +} + +/** + * Extract component-level info from tarball entry path. + */ +export function extractComponentDocumentationInfos(tarPath) { + const idx = tarPath.indexOf(ODS_TAR_COMPONENTS_PATH); + if (idx < 0) return { component: null, relPath: null }; + + const relFromComponents = tarPath.slice(idx + ODS_TAR_COMPONENTS_PATH.length); + const parts = relFromComponents.split('/').filter(Boolean); + if (parts.length < 2) return { component: null, relPath: null }; + + const [component, ...rest] = parts; + const relPath = rest.join('/'); + if (!relPath || relPath.endsWith('/')) return { component: null, relPath: null }; + + return { component, relPath }; +} diff --git a/packages/manager-tools/manager-muk-cli/src/core/tarball-cache-utils.js b/packages/manager-tools/manager-muk-cli/src/core/tarball-cache-utils.js index e4d0e7c9e966..1db785686b4b 100644 --- a/packages/manager-tools/manager-muk-cli/src/core/tarball-cache-utils.js +++ b/packages/manager-tools/manager-muk-cli/src/core/tarball-cache-utils.js @@ -1,85 +1,56 @@ -import { Buffer } from 'node:buffer'; import crypto from 'node:crypto'; import fs from 'node:fs'; -import https from 'node:https'; -import { createGunzip } from 'node:zlib'; -import tar from 'tar-stream'; import { logger } from '../utils/log-manager.js'; -import { loadJson, saveJson } from './file-utils.js'; +import { ensureDir, loadJson, saveJson } from './file-utils.js'; /** - * Ensure that a directory exists, creating it recursively if missing. - * @param {string} dir - Directory path to create. + * Validate cache metadata consistency with expected version. + * @param {object} meta - Metadata object read from cache. + * @param {string} version - Expected version string. + * @returns {string|null} Error message if invalid, otherwise null. */ -export function ensureDir(dir) { - if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); +function validateMeta(meta, version) { + if (!meta || typeof meta !== 'object') return 'invalid metadata'; + if (meta.version !== version) return `version mismatch (${meta.version} β‰  ${version})`; + return null; } /** - * Compute a SHA256 checksum for an object or string. - * @param {object|string} obj - Data to hash. + * Validate cache freshness based on timestamp and TTL. + * @param {number} timestamp - Unix epoch of cache creation. + * @param {number} ttlMs - Time-to-live in milliseconds. + * @returns {{ expired: boolean, age?: number, message?: string }} + * `expired` is true if TTL exceeded, otherwise contains age in ms. + */ +function validateTTL(timestamp, ttlMs) { + const age = Date.now() - timestamp; + if (age > ttlMs) { + const daysOld = (age / 86_400_000).toFixed(1); + return { expired: true, message: `${daysOld} days old` }; + } + return { expired: false, age }; +} + +/** + * Compute SHA256 checksum for any serializable data. + * @param {object|string} obj - Object or string to hash. * @returns {string} SHA256 hex digest. */ -export function computeChecksum(obj) { - return crypto.createHash('sha256').update(JSON.stringify(obj)).digest('hex'); +function computeChecksum(obj) { + const data = typeof obj === 'string' ? obj : JSON.stringify(obj); + return crypto.createHash('sha256').update(data).digest('hex'); } /** - * Stream and extract a remote `.tar.gz` file, calling a handler for each matched file. - * Handles redirects transparently. - * - * @param {string} url - Remote tarball URL. - * @param {(entryPath: string) => boolean} filter - Predicate selecting entries to extract. - * @param {(entryPath: string, content: Buffer) => Promise} onFile - Async handler for file contents. - * @returns {Promise} Resolves when extraction completes. + * Verify that a file map matches the checksum stored in metadata. + * @param {object|Map} files - Cached file mapping. + * @param {{ checksum: string }} meta - Metadata object containing checksum. + * @returns {boolean} True if checksum matches. */ -export async function streamTarGz(url, filter, onFile) { - await new Promise((resolve, reject) => { - https - .get(url, { headers: { 'User-Agent': 'manager-muk-cli' } }, (res) => { - // Handle HTTP redirects - if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) { - res.resume(); - streamTarGz(res.headers.location, filter, onFile).then(resolve).catch(reject); - return; - } - - if (res.statusCode !== 200) { - reject(new Error(`Download failed: ${res.statusCode}`)); - return; - } - - const gunzip = createGunzip(); - const extract = tar.extract(); - - extract.on('entry', (header, stream, next) => { - const entryPath = header.name; - if (header.type === 'file' && filter(entryPath)) { - const chunks = []; - stream.on('data', (c) => chunks.push(c)); - stream.on('end', async () => { - try { - await onFile(entryPath, Buffer.concat(chunks)); - next(); - } catch (e) { - reject(e); - } - }); - } else { - stream.resume(); - stream.on('end', next); - } - }); - - extract.on('finish', resolve); - extract.on('error', reject); - gunzip.on('error', reject); - - res.pipe(gunzip).pipe(extract); - }) - .on('error', reject); - }); +function verifyChecksum(files, meta) { + const obj = files instanceof Map ? Object.fromEntries(files) : files; + return computeChecksum(obj) === meta.checksum; } /** @@ -87,76 +58,93 @@ export async function streamTarGz(url, filter, onFile) { * @property {string} cacheDir - Directory to store cache files. * @property {string} metaFile - Path to metadata JSON file. * @property {string} dataFile - Path to cached data JSON file. + * @property {number} [ttlMs=604800000] - Time-to-live in milliseconds (default: 7 days). */ /** - * Create a functional tarball cache handler. - * Provides save/load/clear operations for extracted tarball data. + * Create a TTL-aware, checksum-validated tarball cache handler. * - * @param {TarballCacheConfig} config - Cache directory and file paths. + * Provides: + * - `save(version, filesMap)` β†’ persist cache + * - `load(version)` β†’ load cache if valid, unexpired, and consistent + * - `clear()` β†’ delete cache directory + * + * @param {TarballCacheConfig} config - Cache configuration. * @returns {{ - * save: (version: string, filesMap: Map) => void, + * save: (version: string, filesMap: Map|object) => void, * load: (version: string) => Map|null, * clear: () => void * }} - * - * @example - * const cache = createTarballCache({ cacheDir, metaFile, dataFile }); - * cache.save('19.3.1', filesMap); - * const files = cache.load('19.3.1'); */ -export function createTarballCache({ cacheDir, metaFile, dataFile }) { +export function createTarballCache({ + cacheDir, + metaFile, + dataFile, + ttlMs = 7 * 24 * 60 * 60 * 1000, +}) { /** - * Save versioned tarball cache to disk. - * @param {string} version - Package version (e.g., "19.3.1"). - * @param {Map} filesMap - Extracted file map. + * Remove all cache files from the cache directory. */ - function save(version, filesMap) { - ensureDir(cacheDir); - const filesObject = Object.fromEntries(filesMap); - const checksum = computeChecksum(filesObject); - - saveJson(dataFile, filesObject); - saveJson(metaFile, { version, checksum, timestamp: Date.now() }); - logger.info(`πŸ’Ύ Saved cache for v${version}`); - } + const clear = () => { + fs.rmSync(cacheDir, { recursive: true, force: true }); + logger.info(`πŸ—‘οΈ Cleared cache directory: ${cacheDir}`); + }; /** - * Load cache if valid for the given version. - * @param {string} version - Package version to validate. - * @returns {Map|null} Cached data, or null if invalid/missing. + * Persist current files and metadata to disk. + * @param {string} version - Version identifier. + * @param {Map|object} filesMap - Files to cache. */ - function load(version) { + const save = (version, filesMap) => { + try { + ensureDir(cacheDir); + const files = filesMap instanceof Map ? Object.fromEntries(filesMap) : filesMap || {}; + const checksum = computeChecksum(files); + const meta = { version, checksum, timestamp: Date.now() }; + + saveJson(dataFile, files); + saveJson(metaFile, meta); + logger.info(`πŸ’Ύ Saved cache for v${version} (TTL: ${(ttlMs / 86_400_000).toFixed(1)} days)`); + } catch (err) { + logger.error(`❌ Failed to save cache: ${err.message}`); + } + }; + + /** + * Attempt to load a valid cache from disk. + * @param {string} version - Version identifier. + * @returns {Map|null} Cached files map or null if invalid. + */ + const load = (version) => { if (!fs.existsSync(metaFile) || !fs.existsSync(dataFile)) return null; try { const meta = loadJson(metaFile); - if (meta.version !== version) return null; + const invalid = validateMeta(meta, version); + if (invalid) return (logger.warn(`⚠️ Invalid cache meta: ${invalid}`), clear(), null); + + const ttl = validateTTL(meta.timestamp, ttlMs); + if (ttl.expired) return (logger.warn(`⚠️ Cache expired (${ttl.message})`), clear(), null); const files = loadJson(dataFile); - const map = new Map(Object.entries(files)); - if (map.size < 5) { - logger.warn(`⚠️ Cache incomplete for v${version}, regenerating...`); - clear(); - return null; + if (!files || typeof files !== 'object') { + return (logger.warn(`⚠️ Corrupted cache data`), clear(), null); } - logger.info(`πŸ“¦ Using cached v${version}`); + if (!verifyChecksum(files, meta)) { + return (logger.warn(`⚠️ Checksum mismatch`), clear(), null); + } + + const map = new Map(Object.entries(files)); + logger.info(`πŸ“¦ Using cached v${version} (age ${(ttl.age / 86_400_000).toFixed(1)} days)`); return map; } catch (err) { logger.warn(`⚠️ Failed to load cache: ${err.message}`); + clear(); return null; } - } - - /** - * Remove the entire cache directory recursively. - */ - function clear() { - fs.rmSync(cacheDir, { recursive: true, force: true }); - logger.info(`πŸ—‘οΈ Cleared cache directory: ${cacheDir}`); - } + }; return { save, load, clear }; } diff --git a/packages/manager-tools/manager-muk-cli/src/core/tarball-utils.js b/packages/manager-tools/manager-muk-cli/src/core/tarball-utils.js new file mode 100644 index 000000000000..8afb79f7481e --- /dev/null +++ b/packages/manager-tools/manager-muk-cli/src/core/tarball-utils.js @@ -0,0 +1,61 @@ +import { Buffer } from 'node:buffer'; +import https from 'node:https'; +import { createGunzip } from 'node:zlib'; +import tar from 'tar-stream'; + +/** + * Stream and extract a remote `.tar.gz` file, calling a handler for each matched file. + * Handles redirects transparently. + * + * @param {string} url - Remote tarball URL. + * @param {(entryPath: string) => boolean} filter - Predicate selecting entries to extract. + * @param {(entryPath: string, content: Buffer) => Promise} onFile - Async handler for file contents. + * @returns {Promise} Resolves when extraction completes. + */ +export async function streamTarGz(url, filter, onFile) { + await new Promise((resolve, reject) => { + https + .get(url, { headers: { 'User-Agent': 'manager-muk-cli' } }, (res) => { + // Handle HTTP redirects + if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) { + res.resume(); + streamTarGz(res.headers.location, filter, onFile).then(resolve).catch(reject); + return; + } + + if (res.statusCode !== 200) { + reject(new Error(`Download failed: ${res.statusCode}`)); + return; + } + + const gunzip = createGunzip(); + const extract = tar.extract(); + + extract.on('entry', (header, stream, next) => { + const entryPath = header.name; + if (header.type === 'file' && filter(entryPath)) { + const chunks = []; + stream.on('data', (c) => chunks.push(c)); + stream.on('end', async () => { + try { + await onFile(entryPath, Buffer.concat(chunks)); + next(); + } catch (e) { + reject(e); + } + }); + } else { + stream.resume(); + stream.on('end', next); + } + }); + + extract.on('finish', resolve); + extract.on('error', reject); + gunzip.on('error', reject); + + res.pipe(gunzip).pipe(extract); + }) + .on('error', reject); + }); +} diff --git a/packages/manager-tools/manager-muk-cli/src/core/tasks-utils.js b/packages/manager-tools/manager-muk-cli/src/core/tasks-utils.js index 76409e9b2100..94589f7a6d75 100644 --- a/packages/manager-tools/manager-muk-cli/src/core/tasks-utils.js +++ b/packages/manager-tools/manager-muk-cli/src/core/tasks-utils.js @@ -1,14 +1,21 @@ import { execSync } from 'node:child_process'; import path from 'node:path'; -import { MUK_COMPONENTS_PATH } from '../config/muk-config.js'; +import { EMOJIS, MUK_COMPONENTS_PATH } from '../config/muk-config.js'; import { logger } from '../utils/log-manager.js'; /** - * Run a command safely with logging. - * @param {string} cmd - Command to execute. - * @param {string} cwd - Working directory. - * @param {string} desc - Human-readable description. + * Executes a shell command synchronously and logs progress. + * + * Wraps Node’s `execSync()` to provide: + * - Contextual logging (start/success/error). + * - Project-aware working directory control. + * - Clean CLI output inherited from the child process. + * + * @private + * @param {string} cmd - The command to execute (e.g., `yarn lint:modern:fix`). + * @param {string} cwd - Working directory for the command. + * @param {string} desc - Human-readable description for logs. */ function runCommand(cmd, cwd, desc) { try { @@ -21,23 +28,18 @@ function runCommand(cmd, cwd, desc) { } /** - * Run post-update validation tasks in `manager-react-components`. + * Run all post-update validation steps. * - * This function executes three sequential steps: - * 1. Installs dependencies from the **monorepo root** using `yarn install`. - * 2. Runs the modern lint command (`yarn lint:modern`) inside the `manager-react-components` package. - * 3. Runs unit tests (`yarn test`) inside the same package. + * Used after automated updates (e.g., component documentation sync) + * to ensure the repository remains consistent and buildable. * - * Each command is executed through {@link runCommand}, which handles logging, error capture, and I/O output. + * Steps performed: + * 1. **Install dependencies** from the project root. + * 2. **Run ESLint (modern)** to auto-fix lint issues. + * 3. **Run unit tests** to validate component behavior. * * @example - * ```bash - * yarn muk-cli --update-version - * ``` - * - * @remarks - * - This function does **not** throw if one of the commands fails β€” it logs errors instead. - * - Intended for use after dependency updates to detect regressions (lint/test failures). + * await runPostUpdateChecks(); * * @returns {void} */ @@ -45,7 +47,133 @@ export function runPostUpdateChecks() { const componentDir = MUK_COMPONENTS_PATH; const rootDir = path.resolve('.'); - runCommand('yarn install', rootDir, 'yarn install from project root'); - runCommand('yarn lint:modern:fix', componentDir, 'Lint (modern)'); + runCommand('yarn install', rootDir, 'Dependency installation (root)'); + runCommand('yarn lint:modern:fix', componentDir, 'ESLint (modern mode)'); runCommand('yarn test', componentDir, 'Unit tests'); } + +/** + * Creates an asynchronous, iterable queue that enables + * communication between producers and consumers in streaming workflows. + * + * This is a core utility for bridging **callback-based producers** + * (e.g., tarball extraction events) with **`for await...of` consumers** + * (e.g., file writers or loggers). + * + * It ensures: + * - βœ… Backpressure-safe processing (consumer controls flow). + * - βœ… Constant memory footprint (only holds unconsumed items). + * - βœ… Simple API: `push()`, `end()`, and async iteration. + * + * --- + * + * ## Example Usage + * + * ```js + * const queue = createAsyncQueue(); + * + * // Producer + * (async () => { + * for (const item of data) await queue.push(item); + * queue.end(); + * })(); + * + * // Consumer + * for await (const item of queue) { + * console.log('Processing', item); + * } + * ``` + * + * @returns {{ + * push(item: any): Promise, + * end(): void, + * [Symbol.asyncIterator](): AsyncGenerator + * }} + * Queue interface with three operations: + * - `push(item)` β†’ adds a new item. + * - `end()` β†’ signals that no more items will be added. + * - async iteration (`for await`) β†’ consumes items as they arrive. + */ +export function createAsyncQueue() { + const items = []; + let resolve; + let done = false; + + return { + /** + * Push an item into the queue. + * If a consumer is waiting, it resolves immediately. + * @param {*} item - Any data or object to enqueue. + */ + async push(item) { + if (done) return; + if (resolve) { + resolve({ value: item, done: false }); + resolve = null; + } else { + items.push(item); + } + }, + + /** + * Mark the queue as complete. + * Signals to the consumer that iteration should end. + */ + end() { + done = true; + if (resolve) resolve({ value: undefined, done: true }); + }, + + /** + * Async iterator interface implementation. + * Enables `for await...of` consumption. + */ + [Symbol.asyncIterator]() { + return { + next() { + if (items.length) return Promise.resolve({ value: items.shift(), done: false }); + if (done) return Promise.resolve({ value: undefined, done: true }); + return new Promise((res) => (resolve = res)); + }, + }; + }, + }; +} + +/** + * Aggregates statistics from multiple synchronization operations. + * + * @param {Array<{created:number, updated:number, total:number}>} results + * @returns {{created:number, updated:number, total:number}} Combined totals. + */ +export function aggregateOperationsStats(results) { + return results.reduce( + (acc, curr) => ({ + created: acc.created + (curr.created || 0), + updated: acc.updated + (curr.updated || 0), + total: acc.total + (curr.total || 0), + }), + { created: 0, updated: 0, total: 0 }, + ); +} + +/** + * Executes a sync operation safely, with clear contextual logging. + * + * @async + * @param {string} label - Descriptive name for the operation (e.g., "component base-docs"). + * @param {Function} syncFn - The synchronization function to execute. + * @returns {Promise<{created:number, updated:number, total:number}>} + * Returns counts even if the operation fails. + */ +export async function safeSync(label, syncFn) { + try { + logger.info(`${EMOJIS.info} Syncing ${label}...`); + const result = (await syncFn()) || { created: 0, updated: 0, total: 0 }; + logger.info(`${EMOJIS.disk} ${label} β†’ ${result.total} files processed.`); + return result; + } catch (error) { + logger.error(`${EMOJIS.error} Failed to sync ${label}: ${error.message}`); + return { created: 0, updated: 0, total: 0 }; + } +} diff --git a/packages/manager-tools/manager-muk-cli/src/index.js b/packages/manager-tools/manager-muk-cli/src/index.js index 7e325e3b42e5..658a18f29bac 100755 --- a/packages/manager-tools/manager-muk-cli/src/index.js +++ b/packages/manager-tools/manager-muk-cli/src/index.js @@ -1,6 +1,7 @@ #!/usr/bin/env node import process from 'node:process'; +import { addComponentsDocumentation } from './commands/add-components-documentation.js'; import { addComponents } from './commands/add-components.js'; import { checkComponents } from './commands/check-components.js'; import { checkVersions } from './commands/check-versions.js'; @@ -18,9 +19,11 @@ async function main() { await updateOdsVersions(); } else if (args.includes('--add-components')) { await addComponents(); + } else if (args.includes('--add-components-documentation')) { + await addComponentsDocumentation(); } else { logger.warn( - 'Usage: manager-muk-cli --check-versions | --update-versions | --check-components | --add-components', + 'Usage: manager-muk-cli --check-versions | --update-versions | --check-components | --add-components | --add-components-documentation', ); } } diff --git a/packages/manager-wiki/package.json b/packages/manager-wiki/package.json index ae9ed47b4fc5..c2771077c5df 100644 --- a/packages/manager-wiki/package.json +++ b/packages/manager-wiki/package.json @@ -17,10 +17,13 @@ "@ovhcloud/ods-components": "^18.6.2", "@tanstack/react-table": "^8.10.0", "clsx": "^2.1.1", + "lz-string": "^1.5.0", "react": "^18.2.0", + "react-docgen": "^8.0.2", "react-router-dom": "^6.3.0", "sass": "1.56.1", - "tailwindcss": "^3.4.4" + "tailwindcss": "^3.4.4", + "typedoc": "^0.28.14" }, "devDependencies": { "@etchteam/storybook-addon-status": "^5.0.0", diff --git a/yarn.lock b/yarn.lock index cf7af7fa3041..6d9691163785 100644 --- a/yarn.lock +++ b/yarn.lock @@ -476,6 +476,27 @@ json5 "^2.2.3" semver "^6.3.1" +"@babel/core@^7.28.0": + version "7.28.4" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.28.4.tgz#12a550b8794452df4c8b084f95003bce1742d496" + integrity sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA== + dependencies: + "@babel/code-frame" "^7.27.1" + "@babel/generator" "^7.28.3" + "@babel/helper-compilation-targets" "^7.27.2" + "@babel/helper-module-transforms" "^7.28.3" + "@babel/helpers" "^7.28.4" + "@babel/parser" "^7.28.4" + "@babel/template" "^7.27.2" + "@babel/traverse" "^7.28.4" + "@babel/types" "^7.28.4" + "@jridgewell/remapping" "^2.3.5" + convert-source-map "^2.0.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.3" + semver "^6.3.1" + "@babel/core@^7.8.0": version "7.26.9" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.26.9.tgz#71838542a4b1e49dfed353d7acbc6eb89f4a76f2" @@ -592,6 +613,17 @@ "@jridgewell/trace-mapping" "^0.3.25" jsesc "^3.0.2" +"@babel/generator@^7.28.3": + version "7.28.3" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.28.3.tgz#9626c1741c650cbac39121694a0f2d7451b8ef3e" + integrity sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw== + dependencies: + "@babel/parser" "^7.28.3" + "@babel/types" "^7.28.2" + "@jridgewell/gen-mapping" "^0.3.12" + "@jridgewell/trace-mapping" "^0.3.28" + jsesc "^3.0.2" + "@babel/helper-annotate-as-pure@^7.18.6", "@babel/helper-annotate-as-pure@^7.22.5": version "7.22.5" resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz#e7f06737b197d580a01edf75d97e2c8be99d3882" @@ -958,6 +990,15 @@ "@babel/helper-validator-identifier" "^7.27.1" "@babel/traverse" "^7.27.1" +"@babel/helper-module-transforms@^7.28.3": + version "7.28.3" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz#a2b37d3da3b2344fe085dab234426f2b9a2fa5f6" + integrity sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw== + dependencies: + "@babel/helper-module-imports" "^7.27.1" + "@babel/helper-validator-identifier" "^7.27.1" + "@babel/traverse" "^7.28.3" + "@babel/helper-optimise-call-expression@^7.22.5": version "7.22.5" resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz#f21531a9ccbff644fdd156b4077c16ff0c3f609e" @@ -1215,6 +1256,14 @@ "@babel/template" "^7.27.1" "@babel/types" "^7.27.1" +"@babel/helpers@^7.28.4": + version "7.28.4" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.28.4.tgz#fe07274742e95bdf7cf1443593eeb8926ab63827" + integrity sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w== + dependencies: + "@babel/template" "^7.27.2" + "@babel/types" "^7.28.4" + "@babel/highlight@^7.24.2", "@babel/highlight@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.24.7.tgz#a05ab1df134b286558aae0ed41e6c5f731bf409d" @@ -1314,7 +1363,7 @@ dependencies: "@babel/types" "^7.27.7" -"@babel/parser@^7.6.0", "@babel/parser@^7.9.6": +"@babel/parser@^7.28.3", "@babel/parser@^7.28.4", "@babel/parser@^7.6.0", "@babel/parser@^7.9.6": version "7.28.4" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.28.4.tgz#da25d4643532890932cc03f7705fe19637e03fa8" integrity sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg== @@ -2991,6 +3040,19 @@ debug "^4.3.1" globals "^11.1.0" +"@babel/traverse@^7.28.0", "@babel/traverse@^7.28.3", "@babel/traverse@^7.28.4": + version "7.28.4" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.28.4.tgz#8d456101b96ab175d487249f60680221692b958b" + integrity sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ== + dependencies: + "@babel/code-frame" "^7.27.1" + "@babel/generator" "^7.28.3" + "@babel/helper-globals" "^7.28.0" + "@babel/parser" "^7.28.4" + "@babel/template" "^7.27.2" + "@babel/types" "^7.28.4" + debug "^4.3.1" + "@babel/traverse@^7.7.0": version "7.22.20" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.22.20.tgz#db572d9cb5c79e02d83e5618b82f6991c07584c9" @@ -3126,7 +3188,7 @@ "@babel/helper-string-parser" "^7.27.1" "@babel/helper-validator-identifier" "^7.27.1" -"@babel/types@^7.28.4", "@babel/types@^7.6.1", "@babel/types@^7.9.6": +"@babel/types@^7.28.2", "@babel/types@^7.28.4", "@babel/types@^7.6.1", "@babel/types@^7.9.6": version "7.28.4" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.28.4.tgz#0a4e618f4c60a7cd6c11cb2d48060e4dbe38ac3a" integrity sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q== @@ -4951,6 +5013,17 @@ resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.3.tgz#555193ab2e3bb3b6adc3d551c9c030d9e860daf6" integrity sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw== +"@gerrit0/mini-shiki@^3.12.0": + version "3.13.1" + resolved "https://registry.yarnpkg.com/@gerrit0/mini-shiki/-/mini-shiki-3.13.1.tgz#e35cfa2762a4a6f5763c3e6f01ddb8bc9f2e3995" + integrity sha512-fDWM5QQc70jwBIt/WYMybdyXdyBmoJe7r1hpM+V/bHnyla79sygVDK2/LlVxIPc4n5FA3B5Wzt7AQH2+psNphg== + dependencies: + "@shikijs/engine-oniguruma" "^3.13.0" + "@shikijs/langs" "^3.13.0" + "@shikijs/themes" "^3.13.0" + "@shikijs/types" "^3.13.0" + "@shikijs/vscode-textmate" "^10.0.2" + "@hookform/resolvers@5.1.1": version "5.1.1" resolved "https://registry.yarnpkg.com/@hookform/resolvers/-/resolvers-5.1.1.tgz#91074ba4fb749cc74e6465e75d38256146b0c4ab" @@ -5554,6 +5627,14 @@ "@jridgewell/sourcemap-codec" "^1.4.10" "@jridgewell/trace-mapping" "^0.3.24" +"@jridgewell/remapping@^2.3.5": + version "2.3.5" + resolved "https://registry.yarnpkg.com/@jridgewell/remapping/-/remapping-2.3.5.tgz#375c476d1972947851ba1e15ae8f123047445aa1" + integrity sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ== + dependencies: + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.24" + "@jridgewell/resolve-uri@^3.0.3", "@jridgewell/resolve-uri@^3.1.0": version "3.1.1" resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz#c08679063f279615a3326583ba3a90d1d82cc721" @@ -9363,6 +9444,14 @@ "@shikijs/types" "3.3.0" "@shikijs/vscode-textmate" "^10.0.2" +"@shikijs/engine-oniguruma@^3.13.0": + version "3.13.0" + resolved "https://registry.yarnpkg.com/@shikijs/engine-oniguruma/-/engine-oniguruma-3.13.0.tgz#ae8efa90c30e2b66c7fd5549ee747f693fbd60df" + integrity sha512-O42rBGr4UDSlhT2ZFMxqM7QzIU+IcpoTMzb3W7AlziI1ZF7R8eS2M0yt5Ry35nnnTX/LTLXFPUjRFCIW+Operg== + dependencies: + "@shikijs/types" "3.13.0" + "@shikijs/vscode-textmate" "^10.0.2" + "@shikijs/langs@3.3.0": version "3.3.0" resolved "https://registry.yarnpkg.com/@shikijs/langs/-/langs-3.3.0.tgz#016b8360b4d220064a701c6bab0925898dc70a76" @@ -9370,6 +9459,13 @@ dependencies: "@shikijs/types" "3.3.0" +"@shikijs/langs@^3.13.0": + version "3.13.0" + resolved "https://registry.yarnpkg.com/@shikijs/langs/-/langs-3.13.0.tgz#51a927c8089dffb2560ac8d7549297de9d081b91" + integrity sha512-672c3WAETDYHwrRP0yLy3W1QYB89Hbpj+pO4KhxK6FzIrDI2FoEXNiNCut6BQmEApYLfuYfpgOZaqbY+E9b8wQ== + dependencies: + "@shikijs/types" "3.13.0" + "@shikijs/themes@3.3.0": version "3.3.0" resolved "https://registry.yarnpkg.com/@shikijs/themes/-/themes-3.3.0.tgz#200213a37c7e80d39f9814c38291c360c4c42cf1" @@ -9377,6 +9473,13 @@ dependencies: "@shikijs/types" "3.3.0" +"@shikijs/themes@^3.13.0": + version "3.13.0" + resolved "https://registry.yarnpkg.com/@shikijs/themes/-/themes-3.13.0.tgz#ee92780f0580d4ffa8ed619b52c5eb4a95d012a3" + integrity sha512-Vxw1Nm1/Od8jyA7QuAenaV78BG2nSr3/gCGdBkLpfLscddCkzkL36Q5b67SrLLfvAJTOUzW39x4FHVCFriPVgg== + dependencies: + "@shikijs/types" "3.13.0" + "@shikijs/transformers@^1.2.0": version "1.2.4" resolved "https://registry.yarnpkg.com/@shikijs/transformers/-/transformers-1.2.4.tgz#d72215cd5d0f010004696385e1cc54020499b906" @@ -9384,6 +9487,14 @@ dependencies: shiki "1.2.4" +"@shikijs/types@3.13.0", "@shikijs/types@^3.13.0": + version "3.13.0" + resolved "https://registry.yarnpkg.com/@shikijs/types/-/types-3.13.0.tgz#d223c6e28796914fbb105a3ee63bc3af5483852e" + integrity sha512-oM9P+NCFri/mmQ8LoFGVfVyemm5Hi27330zuOBp0annwJdKH1kOLndw3zCtAVDehPLg9fKqoEx3Ht/wNZxolfw== + dependencies: + "@shikijs/vscode-textmate" "^10.0.2" + "@types/hast" "^3.0.4" + "@shikijs/types@3.3.0": version "3.3.0" resolved "https://registry.yarnpkg.com/@shikijs/types/-/types-3.3.0.tgz#2787aac662ef0cf286abc0ab65595eab67c27c0f" @@ -10737,6 +10848,13 @@ dependencies: "@babel/types" "^7.20.7" +"@types/babel__traverse@^7.20.7": + version "7.28.0" + resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.28.0.tgz#07d713d6cce0d265c9849db0cbe62d3f61f36f74" + integrity sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q== + dependencies: + "@babel/types" "^7.28.2" + "@types/body-parser@*": version "1.19.5" resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.5.tgz#04ce9a3b677dc8bd681a17da1ab9835dc9d3ede4" @@ -23421,6 +23539,13 @@ linkify-it@^4.0.1: dependencies: uc.micro "^1.0.1" +linkify-it@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-5.0.0.tgz#9ef238bfa6dc70bd8e7f9572b52d369af569b421" + integrity sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ== + dependencies: + uc.micro "^2.0.0" + lint-staged@13.2.3: version "13.2.3" resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-13.2.3.tgz#f899aad6c093473467e9c9e316e3c2d8a28f87a7" @@ -23891,6 +24016,11 @@ lucide-react@^0.544.0: resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-0.544.0.tgz#4719953c10fd53a64dd8343bb0ed16ec79f3eeef" integrity sha512-t5tS44bqd825zAW45UQxpG2CvcC4urOwn2TrwSH8u+MjeE+1NnWl6QqeQ/6NdjMqdOygyiT9p3Ev0p1NJykxjw== +lunr@^2.3.9: + version "2.3.9" + resolved "https://registry.yarnpkg.com/lunr/-/lunr-2.3.9.tgz#18b123142832337dd6e964df1a5a7707b25d35e1" + integrity sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow== + luxon@3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/luxon/-/luxon-3.2.1.tgz#14f1af209188ad61212578ea7e3d518d18cee45f" @@ -24083,6 +24213,18 @@ markdown-it@^13.0.2: mdurl "^1.0.1" uc.micro "^1.0.5" +markdown-it@^14.1.0: + version "14.1.0" + resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-14.1.0.tgz#3c3c5992883c633db4714ccb4d7b5935d98b7d45" + integrity sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg== + dependencies: + argparse "^2.0.1" + entities "^4.4.0" + linkify-it "^5.0.0" + mdurl "^2.0.0" + punycode.js "^2.3.1" + uc.micro "^2.1.0" + markdown-table@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/markdown-table/-/markdown-table-2.0.0.tgz#194a90ced26d31fe753d8b9434430214c011865b" @@ -24212,6 +24354,11 @@ mdurl@^1.0.1: resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e" integrity sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g== +mdurl@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-2.0.0.tgz#80676ec0433025dd3e17ee983d0fe8de5a2237e0" + integrity sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w== + media-typer@0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" @@ -27190,6 +27337,11 @@ pump@^3.0.0: end-of-stream "^1.1.0" once "^1.3.1" +punycode.js@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/punycode.js/-/punycode.js-2.3.1.tgz#6b53e56ad75588234e79f4affa90972c7dd8cdb7" + integrity sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA== + punycode@2.3.1, punycode@^2.1.0, punycode@^2.1.1, punycode@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" @@ -27374,6 +27526,22 @@ react-docgen@^7.0.0: resolve "^1.22.1" strip-indent "^4.0.0" +react-docgen@^8.0.2: + version "8.0.2" + resolved "https://registry.yarnpkg.com/react-docgen/-/react-docgen-8.0.2.tgz#450efcac75813e3d614d7bd15eb4066e2e7bcbf5" + integrity sha512-+NRMYs2DyTP4/tqWz371Oo50JqmWltR1h2gcdgUMAWZJIAvrd0/SqlCfx7tpzpl/s36rzw6qH2MjoNrxtRNYhA== + dependencies: + "@babel/core" "^7.28.0" + "@babel/traverse" "^7.28.0" + "@babel/types" "^7.28.2" + "@types/babel__core" "^7.20.5" + "@types/babel__traverse" "^7.20.7" + "@types/doctrine" "^0.0.9" + "@types/resolve" "^1.20.2" + doctrine "^3.0.0" + resolve "^1.22.1" + strip-indent "^4.0.0" + react-dom@18.2.0, react-dom@^18.2.0: version "18.2.0" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d" @@ -31666,6 +31834,17 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA== +typedoc@^0.28.14: + version "0.28.14" + resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.28.14.tgz#f48d650efc983b5cb3034b3b0e986b1702074326" + integrity sha512-ftJYPvpVfQvFzpkoSfHLkJybdA/geDJ8BGQt/ZnkkhnBYoYW6lBgPQXu6vqLxO4X75dA55hX8Af847H5KXlEFA== + dependencies: + "@gerrit0/mini-shiki" "^3.12.0" + lunr "^2.3.9" + markdown-it "^14.1.0" + minimatch "^9.0.5" + yaml "^2.8.1" + typescript-coverage-report@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/typescript-coverage-report/-/typescript-coverage-report-1.1.1.tgz#b61ccb496a58784d220c45c37c5c8607ac3addfb" @@ -31742,6 +31921,11 @@ uc.micro@^1.0.1, uc.micro@^1.0.5: resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac" integrity sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA== +uc.micro@^2.0.0, uc.micro@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-2.1.0.tgz#f8d3f7d0ec4c3dea35a7e3c8efa4cb8b45c9e7ee" + integrity sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A== + ufo@^1.5.4: version "1.5.4" resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.5.4.tgz#16d6949674ca0c9e0fbbae1fa20a71d7b1ded754" @@ -33636,6 +33820,11 @@ yaml@^2.2.2, yaml@^2.3.4: resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.4.1.tgz#2e57e0b5e995292c25c75d2658f0664765210eed" integrity sha512-pIXzoImaqmfOrL7teGUBt/T7ZDnyeGBWyXQBvOVhLkWLN37GXv8NMLK406UY6dS51JfcQHsmcW5cJ441bHg6Lg== +yaml@^2.8.1: + version "2.8.1" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.8.1.tgz#1870aa02b631f7e8328b93f8bc574fac5d6c4d79" + integrity sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw== + yargs-parser@20.2.4: version "20.2.4" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.4.tgz#b42890f14566796f85ae8e3a25290d205f154a54"