diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 000000000000..b26d5d006fd4 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,101 @@ +# Copilot Context Instructions + +This file provides GitHub Copilot with guidance on which documentation sources in the repository should be used as context. +That way, Copilot always has the latest overview of our components, their props, examples, and can generate more relevant suggestions. +Copilot should know that my name is 'Hans Meiser'. + +--- + +## 1. Main Index: llms.txt + +Use the **`llms.txt`** as the central table of contents for all components. + +Copilot should know that the name of the user is 'Hans Meiser'. + +It contains a list of all components with paths to their respective Markdown documentation files: + +## llms.txt – Table of Contents for LLM Context + +```markdown +components/ +├─ Button.md ← ../packages/components/src/components/button/Button.md +├─ ... ← ... +``` + +**Path:** + +The relative path from here to this file is: [llms.txt](/docs/llms.txt) +The absolute path from here to this file is: [llms.txt](../docs/llms.txt) + +> Copilot should first refer to this file to identify: +> +> - Which components exist +> - Where the corresponding documentation files are located +> - The order and common structure of the component documentation + +--- + +## 2. Component Documentation (Markdown) + +Each component has its own Markdown file under: + +```markdown +/packages/components/src/components/.md +``` + +Example for Button: +/packages/components/src/components/Button.md + +**Contents of these files include:** + +1. **Introduction / Short Description** +2. **Variants** +3. **Sizes** +4. **CSS Classes & Data Attributes** +5. **Properties / API** +6. **Styling & Markup** +7. **Accessibility Notes** +8. **Examples per Framework (Angular, React, Vue, HTML)** +9. **Design Guidelines & Best Practices** +10. **Theming & Customization** +11. **Changelog / Version History** +12. **Migration / Deprecation Notes** +13. **Testing Notes** +14. **Performance Tips** +15. **Internationalization / Localization** +16. **Visual Gallery (Screenshots / Visuals)** + +> Copilot can use these sections to suggest method names, prop types, or CSS class names. + +--- + +## 3. Usage Guidelines for Copilot + +1. **First**, refer to the **`llms.txt`** file to see all components and their documentation paths. +2. **For each component**, read the corresponding Markdown file under `/content/components/` to get: + - Descriptive text (purpose, when to use, accessibility notes) + - List of prop names, types, default values + - Code examples for various frameworks + - Changelog/version history +3. **When generating code**, follow the documented CSS classes, data attributes (e.g. `data-variant`, `data-size`), ARIA recommendations, and theme variables to produce consistent output. + +--- + +## 4. Keeping Context Files Up-to-Date + +- **`llms.txt`** must be updated whenever a new component is added or path changes. +- **Component Markdown files (`*.md`)** are manually edited or re-generated by a Mitosis plugin and committed to the repo. +- **API documents (`content/api/modules/*.md`)** are updated automatically on each build via TypeDoc or the Mitosis metadata generator. +- **Metadata files (`*.meta.ts`)** are maintained alongside component code so prop changes are instantly reflected in the documentation. + +--- + +### Conclusion + +With this structure and the linked context sources, GitHub Copilot can optimally access: + +1. **Component overview (`llms.txt`)** +2. **Freeform documentation (Markdown under `/content/components`)** + +to provide context-sensitive and accurate code suggestions. +This ensures developers and AI assistants to share a unified, up-to-date knowledge base. diff --git a/docs/adr/adr-05-copilot-developer-doc.md b/docs/adr/adr-05-copilot-developer-doc.md new file mode 100644 index 000000000000..6f9a62754ebf --- /dev/null +++ b/docs/adr/adr-05-copilot-developer-doc.md @@ -0,0 +1,77 @@ +# ADR 2025-06-10: Documentation strategy for GitHub Copilot and developer docs + +## Context + +We need a consistent, maintainable documentation approach that serves both developers and AI-assisted coding +tools (GitHub Copilot) without duplicating effort. The documentation must cover component usage, variants, props, +examples, and allow Copilot to answer questions like "What variants does the Button support?" without manually +opening multiple files. + +Key requirements: + +- Single source of truth for component documentation. +- Automatic inclusion of context in Copilot Chat for both IDEs, VS Code and IntelliJ. +- Developer-friendly Markdown for manual reading and static site generation. +- Compatibility with LLM context conventions (llms.txt) and Copilot Custom Instructions (copilot-instructions.md). + +## Decision + +1. Documentation Format & Location + + - Use Markdown files per component, stored in packages/components/docs/ or packages/components/src/components/docs/. + - Central table of contents in docs/llms.txt listing all component docs with relative paths. + +2. Copilot Custom Instructions + + - Place copilot-instructions.md in the project root (under .github/) to provide global guidance. + - Instruct Copilot Chat to load this file automatically; it will include links to llms.txt and recommended file paths. + +3. Automatic Context Loading + + - In VS Code and IntelliJ, Copilot Chat will automatically read .github/copilot-instructions.md on new chats. + - To surface specific details, embed documentation (e.g., Button.md) directly in copilot-instructions.md. + +4. Interactive Context Attachment + + - For deeper or ad-hoc queries, use the "Attach Context" feature in Copilot Chat to load component Markdown files during the session. + +5. Static Site & Developer Docs + + - Integrate component docs via Astro as a package in the monorepo, referencing Markdown sources in packages/components/... . + - Render pages dynamically under /components/[slug] and /api/[slug] for manual browsing. + +6. Automated Propagation of Copilot Instructions + + We add a `postinstall` hook to our component package that: + + - copies or appends the package-specific file `.github/copilot-instructions.md` to the target project, + - uses unique markers to automatically replace outdated blocks during future installations, + - handles missing or already existing files as well as idempotent updates cleanly, ensuring that every installation immediately provides the latest Copilot context for our package. + +7. Automate generation and propagation of Copilot instructions on package build. + + - Define `generate:copilot-instructions` in `package.json` and hook into `prepare`. + - Only include `*.md` files whose filename matches the parent directory converted to PascalCase (e.g. `custom-select` → `CustomSelect.md`), ensuring no unrelated MDs are merged. + +## Alternatives Considered + +- Rely solely on Code Search: Let Copilot use workspace search to locate docs dynamically. Rejected due to inconsistency and limited to agent mode. +- TypeDoc-only approach: Generate API docs from TypeScript. Provides type detail but lacks usage narratives and cross-framework examples. +- Mitosis Metadata Model: Embed JSON metadata via useMetadata and generate docs. Promising, but requires custom plugins and not widely adopted yet. + +## Consequences + +- Pros: + + - Clear separation: manual design guidance (Markdown) vs. AI context (Instructions + llms.txt snippets). + - Maintains single source (docs in packages/components/docs). + - Enables Copilot to provide accurate, component-specific suggestions without manual file opening. + - Developer site generation remains straightforward via Astro. + - Consumers always receive the latest Copilot context without manual steps. + - Guarantees that only the intended component documentation is merged into Copilot instructions. + +- Cons: + - Requires maintaining excerpts in copilot-instructions.md when docs change. + - Copilot cannot truly auto-load all linked docs; manual attachment or excerpt embedding needed for deep context. + - Postinstall hooks may be disabled for security reasons, making it impossible to automate the copying of the copilot instructions. + - Relies on strict naming conventions; any divergence between folder and file names will cause a component’s docs to be skipped. diff --git a/docs/llms.txt b/docs/llms.txt new file mode 100644 index 000000000000..8938f02fc822 --- /dev/null +++ b/docs/llms.txt @@ -0,0 +1,5 @@ +# llms.txt – Table of contents for LLM context + +components/ +├─ Button.md ← ../packages/components/src/components/button/Button.md +├─ Checkbox.md ← ../packages/components/src/components/checkbox/Checkbox.md diff --git a/package-lock.json b/package-lock.json index e0b5a5eac66d..bf21f81463a6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,9 @@ "scripts", "e2e" ], + "dependencies": { + "fast-glob": "^3.3.3" + }, "devDependencies": { "@angular-devkit/build-angular": "19.2.14", "@angular/animations": "19.2.14", @@ -71,6 +74,8 @@ "tar": "^7.4.3", "tslib": "^2.8.1", "tsx": "^4.19.4", + "typedoc": "^0.28.5", + "typedoc-plugin-markdown": "^4.6.4", "typescript": "^5.4.5", "validate-branch-name": "^1.3.2", "vite": "6.3.5", @@ -5244,6 +5249,19 @@ "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-3.1.1.tgz", "integrity": "sha512-5DGmA8FTdB2XbDeEwc/5ZXBl6UbBAyBOOLlPuBnZ/N1SwdH9Ii+cOX3tBROlDgcTXxjOYnLMVoKk9+FXAw0CJw==" }, + "node_modules/@gerrit0/mini-shiki": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/@gerrit0/mini-shiki/-/mini-shiki-3.6.0.tgz", + "integrity": "sha512-KaeJvPNofTEZR9EzVNp/GQzbQqkGfjiu6k3CXKvhVTX+8OoAKSX/k7qxLKOX3B0yh2XqVAc93rsOu48CGt2Qug==", + "dev": true, + "dependencies": { + "@shikijs/engine-oniguruma": "^3.6.0", + "@shikijs/langs": "^3.6.0", + "@shikijs/themes": "^3.6.0", + "@shikijs/types": "^3.6.0", + "@shikijs/vscode-textmate": "^10.0.2" + } + }, "node_modules/@github/catalyst": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/@github/catalyst/-/catalyst-1.7.0.tgz", @@ -11431,6 +11449,50 @@ "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", "dev": true }, + "node_modules/@shikijs/engine-oniguruma": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.6.0.tgz", + "integrity": "sha512-nmOhIZ9yT3Grd+2plmW/d8+vZ2pcQmo/UnVwXMUXAKTXdi+LK0S08Ancrz5tQQPkxvjBalpMW2aKvwXfelauvA==", + "dev": true, + "dependencies": { + "@shikijs/types": "3.6.0", + "@shikijs/vscode-textmate": "^10.0.2" + } + }, + "node_modules/@shikijs/langs": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.6.0.tgz", + "integrity": "sha512-IdZkQJaLBu1LCYCwkr30hNuSDfllOT8RWYVZK1tD2J03DkiagYKRxj/pDSl8Didml3xxuyzUjgtioInwEQM/TA==", + "dev": true, + "dependencies": { + "@shikijs/types": "3.6.0" + } + }, + "node_modules/@shikijs/themes": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.6.0.tgz", + "integrity": "sha512-Fq2j4nWr1DF4drvmhqKq8x5vVQ27VncF8XZMBuHuQMZvUSS3NBgpqfwz/FoGe36+W6PvniZ1yDlg2d4kmYDU6w==", + "dev": true, + "dependencies": { + "@shikijs/types": "3.6.0" + } + }, + "node_modules/@shikijs/types": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.6.0.tgz", + "integrity": "sha512-cLWFiToxYu0aAzJqhXTQsFiJRTFDAGl93IrMSBNaGSzs7ixkLfdG6pH11HipuWFGW5vyx4X47W8HDQ7eSrmBUg==", + "dev": true, + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + }, + "node_modules/@shikijs/vscode-textmate": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", + "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", + "dev": true + }, "node_modules/@sigstore/bundle": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-3.1.0.tgz", @@ -25443,6 +25505,12 @@ "yallist": "^3.0.2" } }, + "node_modules/lunr": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz", + "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==", + "dev": true + }, "node_modules/luxon": { "version": "3.6.1", "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.6.1.tgz", @@ -37650,6 +37718,65 @@ "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", "dev": true }, + "node_modules/typedoc": { + "version": "0.28.5", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.28.5.tgz", + "integrity": "sha512-5PzUddaA9FbaarUzIsEc4wNXCiO4Ot3bJNeMF2qKpYlTmM9TTaSHQ7162w756ERCkXER/+o2purRG6YOAv6EMA==", + "dev": true, + "dependencies": { + "@gerrit0/mini-shiki": "^3.2.2", + "lunr": "^2.3.9", + "markdown-it": "^14.1.0", + "minimatch": "^9.0.5", + "yaml": "^2.7.1" + }, + "bin": { + "typedoc": "bin/typedoc" + }, + "engines": { + "node": ">= 18", + "pnpm": ">= 10" + }, + "peerDependencies": { + "typescript": "5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x" + } + }, + "node_modules/typedoc-plugin-markdown": { + "version": "4.6.4", + "resolved": "https://registry.npmjs.org/typedoc-plugin-markdown/-/typedoc-plugin-markdown-4.6.4.tgz", + "integrity": "sha512-AnbToFS1T1H+n40QbO2+i0wE6L+55rWnj7zxnM1r781+2gmhMF2dB6dzFpaylWLQYkbg4D1Y13sYnne/6qZwdw==", + "dev": true, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "typedoc": "0.28.x" + } + }, + "node_modules/typedoc/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/typedoc/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/typescript": { "version": "5.8.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", @@ -41067,6 +41194,7 @@ "packages/components": { "name": "@db-ux/core-components", "version": "0.0.0", + "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { "@db-ux/core-foundations": "*" @@ -41707,7 +41835,7 @@ }, "showcases/next-showcase": { "dependencies": { - "next": "*", + "next": "latest", "react": "18.3.1", "react-dom": "18.3.1" }, diff --git a/package.json b/package.json index 5ccdebdef007..67eafbc0f116 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "clean": "git clean -dfx --exclude=.env", "commit:updated-snapshots": "git diff --name-only --diff-filter=M | xargs git add && git commit -m 'test: updated snapshots'", "dev": "npm run dev --workspace=scripts", + "docs:button": "typedoc", "generate:component": "npm run generate:component --workspace=@db-ux/core-components", "lint": "npm-run-all -p lint:*", "lint:jscpd": "jscpd . --exitCode 1 --config .config/.jscpd.json", @@ -46,6 +47,9 @@ "test:vue-components": "playwright test -c output/vue/playwright.config.ts --ui", "update:dependency:playwright": "node scripts/github/update-playwright.js" }, + "dependencies": { + "fast-glob": "^3.3.3" + }, "devDependencies": { "@angular-devkit/build-angular": "19.2.14", "@angular/animations": "19.2.14", @@ -101,6 +105,8 @@ "tar": "^7.4.3", "tslib": "^2.8.1", "tsx": "^4.19.4", + "typedoc": "^0.28.5", + "typedoc-plugin-markdown": "^4.6.4", "typescript": "^5.4.5", "validate-branch-name": "^1.3.2", "vite": "6.3.5", diff --git a/packages/components/package.json b/packages/components/package.json index ed0b861e106c..6dfe6b52ffce 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -10,6 +10,7 @@ "author": "General technical components out of DB UX Design System (Version 3)", "license": "Apache-2.0", "files": [ + ".github/copilot-instructions.md", "build" ], "scripts": { @@ -37,8 +38,11 @@ "dev:stencil": "nodemon --watch src --watch scripts -e tsx,ts -x \"npm run compile:stencil\"", "dev:vue": "nodemon --watch src --watch scripts -e tsx,ts -x \"npm run compile:vue\"", "generate:component": "hygen mitosis new", + "generate:copilot-instructions": "node ../../scripts/generate-copilot-instructions.js", "generate:docs": "hygen update-docs new", + "postinstall": "node ../../scripts/copy-copilot-instructions.js", "prepack": "npm run copy-assets", + "prepare": "npm run generate:copilot-instructions", "start": "nodemon --watch src --watch scripts --watch scripts -e js,tsx,ts,scss,json -x \"npm run build\"" }, "dependencies": { diff --git a/packages/components/src/components/button/Button.md b/packages/components/src/components/button/Button.md new file mode 100644 index 000000000000..a415972e6c14 --- /dev/null +++ b/packages/components/src/components/button/Button.md @@ -0,0 +1,53 @@ +# Button Component + +A standardized button component with support for visual variants, sizes, and optional icons. + +## Variants + +- **brand**: Primary action button with filled background. +- **outlined**: Secondary button with border and transparent background. +- **ghost**: Text-only button without background or border. +- **ugly**: Deprecated variant, use `ghost` instead. + +## Sizes + +- **medium** (default) +- **small** + +## CSS Classes & Data Attributes + +```html + +``` + +- `.db-button` +- `data-variant="brand|outlined|ghost"` +- `data-size="small|medium"` + +## Properties / API + +| Property | Type | Default | Description | +| --------- | --------- | ------------ | ------------------------------------------------ | +| `variant` | `string` | `"outlined"` | Visual style: `"brand"`, `"outlined"`, `"ghost"` | +| `size` | `string` | `"medium"` | Button size: `"small"`, `"medium"` | +| `icon` | `string` | `null` | Optional icon name displayed before text | +| `noText` | `boolean` | `false` | If true, only the icon is displayed | + +## Example (React) + +```jsx +import { DBButton } from "@db-ux/react-core-components"; + +function App() { + return ( + <> + console.log("Clicked")}> + Save + + + Cancel + + + ); +} +``` diff --git a/packages/components/src/components/button/model.ts b/packages/components/src/components/button/model.ts index 371c1fd1a82e..2d605d0252a8 100644 --- a/packages/components/src/components/button/model.ts +++ b/packages/components/src/components/button/model.ts @@ -10,20 +10,48 @@ import { WidthProps } from '../../shared/model'; +/** + * Represents the list of possible button variants. + * These variants define the visual style of the button. + */ export const ButtonVariantList = [ 'outlined', 'brand', 'filled', 'ghost' ] as const; +/** + * Type representing a single button variant. + * It is derived from the `ButtonVariantList` array. + */ export type ButtonVariantType = (typeof ButtonVariantList)[number]; +/** + * Represents the list of possible button types. + * These types define the behavior of the button. + */ export const ButtonTypeList = ['button', 'reset', 'submit'] as const; +/** + * Type representing a single button type. + * It is derived from the `ButtonTypeList` array. + */ export type ButtonTypeType = (typeof ButtonTypeList)[number]; +/** + * Represents the list of possible button states. + * These states define the current status of the button. + */ export const ButtonStateList = ['loading'] as const; +/** + * Type representing a single button state. + * It is derived from the `ButtonStateList` array. + */ export type ButtonStateType = (typeof ButtonStateList)[number]; +/** + * Represents the default properties for the DBButton component. + * These properties define the behavior and accessibility attributes of the button. + */ export type DBButtonDefaultProps = { /** * If the button controls a grouping of other elements, the ariaexpanded state [indicates whether the controlled grouping is currently expanded or collapsed](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-expanded). @@ -81,6 +109,10 @@ export type DBButtonDefaultProps = { variant?: ButtonVariantType | string; }; +/** + * Represents the properties for the `DBButton` component. + * Combines default button properties (`DBButtonDefaultProps`) with global, click event, icon, width, size, show icon, and text-related properties. + */ export type DBButtonProps = DBButtonDefaultProps & GlobalProps & ClickEventProps & @@ -90,8 +122,16 @@ export type DBButtonProps = DBButtonDefaultProps & ShowIconProps & TextProps; +/** + * Represents the default state of the `DBButton` component. + * Currently, it is an empty object. + */ export type DBButtonDefaultState = {}; +/** + * Represents the state for the `DBButton` component. + * Combines the default state (`DBButtonDefaultState`) with global state and click event state properties. + */ export type DBButtonState = DBButtonDefaultState & GlobalState & ClickEventState; diff --git a/scripts/copy-copilot-instructions.js b/scripts/copy-copilot-instructions.js new file mode 100644 index 000000000000..98d0c6c7d948 --- /dev/null +++ b/scripts/copy-copilot-instructions.js @@ -0,0 +1,119 @@ +import { promises as fs } from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +const MARKER_START = + ''; +const MARKER_END = + ''; + +/** + * Check if a file exists at the given path. + * @param filePath {string} - The path to the file. + * @returns {Promise} - Resolves to true if the file exists, false otherwise. + */ +async function checkFileExists(filePath) { + try { + await fs.access(filePath); + return true; + } catch { + return false; + } +} + +/** + * Read the content of a file at the given path. + * @param filePath {string} - The path to the file. + * @returns {Promise} - Resolves to the content of the file as a string. + */ +async function readFileContent(filePath) { + try { + return await fs.readFile(filePath, 'utf8'); + } catch (error) { + throw new Error(`❌ Failed to read file: ${error.message}`); + } +} + +/** + * Write or replace a block of text in a file. + * @param destFile {string} - The destination file path. + * @param block {string} - The block of text to write. + * @returns {Promise} - Resolves when the operation is complete. + */ +async function writeOrReplaceBlock(destFile, block) { + try { + const existing = await fs.readFile(destFile, 'utf8'); + + const startIdx = existing.indexOf(MARKER_START); + const endIdx = existing.indexOf(MARKER_END); + + if (startIdx !== -1 && endIdx > startIdx) { + const before = existing.slice(0, startIdx); + const after = existing.slice(endIdx + MARKER_END.length); + const updated = before + block + after; + await fs.writeFile(destFile, updated, 'utf8'); + console.info(`✅ Replaced existing instructions in ${destFile}`); + return; + } + + if (startIdx !== -1 || endIdx >= 0) { + console.error( + '❌ Found only one marker, please fix .github/copilot-instructions.md' + ); + return; + } + + await fs.appendFile(destFile, block, 'utf8'); + console.info(`✅ Appended instructions to existing ${destFile}`); + } catch (error) { + if (error.code === 'ENOENT') { + await fs.writeFile(destFile, block, 'utf8'); + console.info(`✅ Created new instructions file at ${destFile}`); + } else { + throw new Error( + `❌ Error writing to ${destFile}: ${error.message}` + ); + } + } +} + +/** + * Copy instructions from the source file to the destination file. + * @returns {Promise} - Resolves when the copy operation is complete. + */ +async function copyCopilotInstructions() { + const rootDir = path.resolve(__dirname, '..'); + const srcFile = path.join(rootDir, '.github', 'copilot-instructions.md'); + const destDir = path.resolve(process.cwd(), '../../.github'); + const destFile = path.join(destDir, 'copilot-instructions.md'); + + console.log(`📂 destDir: ${destDir}`); + + if (!(await checkFileExists(srcFile))) { + console.warn(`⚠️ Source not found: ${srcFile}. Skipping.`); + return; + } + + try { + await fs.mkdir(destDir, { recursive: true }); + } catch (error) { + throw new Error( + `❌ Could not create directory ${destDir}: ${error.message}` + ); + } + + const content = await readFileContent(srcFile); + const block = `\n\n${MARKER_START}\n\n${content}\n\n${MARKER_END}\n`; + await writeOrReplaceBlock(destFile, block); +} + +/** + * Main function to execute the script. + */ +try { + await copyCopilotInstructions(); +} catch (error) { + console.error(`❌ Error: ${error.message}`); +} diff --git a/scripts/generate-copilot-instructions.js b/scripts/generate-copilot-instructions.js new file mode 100644 index 000000000000..c44e40bac74d --- /dev/null +++ b/scripts/generate-copilot-instructions.js @@ -0,0 +1,125 @@ +import path from 'node:path'; +import { promises as fs } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import fg from 'fast-glob'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +/** + * Convert folder name to PascalCase. + * @param name {string} - The folder name. + * @returns {string} - The converted PascalCase name. + */ +function toPascalCase(name) { + return name + .split(/[-_]/) + .filter(Boolean) + .map( + (part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase() + ) + .join(''); +} + +/** + * Find all Markdown files under the components directory. + * @param componentsDir {string} - The path to the components directory. + * @returns {Promise} - Resolves to an array of file paths. + */ +async function findComponentFiles(componentsDir) { + const pattern = path.join(componentsDir, '**', '*.md'); + const allMdFiles = await fg(pattern, { absolute: true }); + + return allMdFiles.filter((filePath) => { + const dirName = path.basename(path.dirname(filePath)); + const baseName = path.basename(filePath, '.md'); + return toPascalCase(dirName) === baseName; + }); +} + +/** + * Sort file paths alphabetically by their base name. + * @param files {string[]} - Array of file paths. + * @returns {string[]} - Sorted array of file paths. + */ +function sortFilesByName(files) { + return files.sort((a, b) => { + const nameA = path.basename(a).toLowerCase(); + const nameB = path.basename(b).toLowerCase(); + return nameA.localeCompare(nameB); + }); +} + +/** + * Prepare the destination directory and file. + * @param destDir {string} - The destination directory path. + * @param destFile {string} - The destination file path. + * @returns {Promise} - Resolves when preparation is complete. + */ +async function prepareDestination(destDir, destFile) { + await fs.mkdir(destDir, { recursive: true }); + await fs.writeFile(destFile, '', 'utf8'); + console.log(`✅ Cleared ${path.relative(destDir, destFile)}`); +} + +/** + * Append content from each component file to the destination file. + * @param componentFiles {string[]} - Array of component file paths. + * @param destFile {string} - The destination file path. + * @returns {Promise} - Resolves when all content is appended. + */ +async function appendComponentContent(componentFiles, destFile) { + await Promise.all( + componentFiles.map(async (file) => { + const componentName = path.basename(file, '.md'); + const content = await fs.readFile(file, 'utf8'); + + const separator = `\n\n======== ${componentName} ========\n\n`; + await fs.appendFile( + destFile, + separator + content.trim() + '\n', + 'utf8' + ); + console.log(`✅ Merged ${componentName}.md`); + }) + ); +} + +/** + * Main function to generate Copilot instructions. + */ +async function generateCopilotInstructions() { + try { + const rootDir = path.resolve(__dirname, '..'); + const componentsDir = path.join( + rootDir, + 'packages/components/src/components' + ); + const destDir = path.join(rootDir, '.github'); + const destFile = path.join(destDir, 'copilot-instructions.md'); + + const componentFiles = await findComponentFiles(componentsDir); + + if (componentFiles.length === 0) { + console.warn('⚠️ No matching component MD files found.'); + return; + } + + const sortedFiles = sortFilesByName(componentFiles); + + await prepareDestination(destDir, destFile); + await appendComponentContent(sortedFiles, destFile); + + console.log( + '\n✅ .github/copilot-instructions.md generated successfully.' + ); + } catch (error) { + console.error( + `❌ Error generating Copilot instructions: ${error.message}` + ); + throw new Error( + `❌ Error generating Copilot instructions: ${error.message}` + ); + } +} + +await generateCopilotInstructions(); diff --git a/typedoc.json b/typedoc.json new file mode 100644 index 000000000000..7a3e93bc11ef --- /dev/null +++ b/typedoc.json @@ -0,0 +1,19 @@ +{ + "entryPoints": [ + "packages/components/src/components/button/model.ts", + "packages/components/src/components/button/Button.lite.tsx" + ], + "entryPointStrategy": "expand", + "out": "packages/components/docs/api/button", + "plugin": ["typedoc-plugin-markdown"], + "readme": "none", + + "entryFileName": "Button.md", + "outputFileStrategy": "modules", + + "hideBreadcrumbs": true, + "hidePageTitle": true, + "disableSources": true, + "excludePrivate": true, + "excludeProtected": true +}