From 72801adf953a2c204c419595b25cddac8a6f66ac Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 8 Jul 2025 17:11:30 +0000 Subject: [PATCH 1/4] Initial plan From 4886c3c90a6ab3b0abe39bbe229991607a0732a8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 8 Jul 2025 17:20:35 +0000 Subject: [PATCH 2/4] Fix critical missing template files bug Co-authored-by: edrouhardmicrosoft <128614151+edrouhardmicrosoft@users.noreply.github.com> --- vindsmidi-ui/cli/dist/index.js | 2 +- .../cli/src/utils/template-manager.ts | 5 +- .../components/button/button.tsx.template | 41 +++++++---- .../button/button.types.ts.template | 35 +++++++++ .../components/button/index.ts.template | 3 + .../components/button/variants.ts.template | 32 +++++++++ .../components/card/card.tsx.template | 71 +++++++++++++++++++ .../components/card/card.types.ts.template | 22 ++++++ .../components/card/index.ts.template | 2 + .../cli/templates/utils/cn.ts.template | 10 +++ 10 files changed, 206 insertions(+), 17 deletions(-) create mode 100644 vindsmidi-ui/cli/templates/components/button/button.types.ts.template create mode 100644 vindsmidi-ui/cli/templates/components/button/index.ts.template create mode 100644 vindsmidi-ui/cli/templates/components/button/variants.ts.template create mode 100644 vindsmidi-ui/cli/templates/components/card/card.tsx.template create mode 100644 vindsmidi-ui/cli/templates/components/card/card.types.ts.template create mode 100644 vindsmidi-ui/cli/templates/components/card/index.ts.template create mode 100644 vindsmidi-ui/cli/templates/utils/cn.ts.template diff --git a/vindsmidi-ui/cli/dist/index.js b/vindsmidi-ui/cli/dist/index.js index 368578b..cd36064 100644 --- a/vindsmidi-ui/cli/dist/index.js +++ b/vindsmidi-ui/cli/dist/index.js @@ -335,7 +335,7 @@ function resolveDependencies(components2) { var import_path3 = __toESM(require("path")); var import_fs_extra4 = __toESM(require("fs-extra")); function getTemplatePath(templateName) { - return import_path3.default.resolve(__dirname, "..", "..", "templates", templateName); + return import_path3.default.resolve(__dirname, "..", "templates", templateName); } async function installComponent(component, targetDir, options = {}) { logger.info(`Installing component: ${component.name}`); diff --git a/vindsmidi-ui/cli/src/utils/template-manager.ts b/vindsmidi-ui/cli/src/utils/template-manager.ts index be08d6e..c416f92 100644 --- a/vindsmidi-ui/cli/src/utils/template-manager.ts +++ b/vindsmidi-ui/cli/src/utils/template-manager.ts @@ -8,9 +8,8 @@ import { Component } from "../registry/schema"; * Gets the absolute path to a template */ function getTemplatePath(templateName: string): string { - // In a real implementation, this would resolve from the CLI's templates directory - // For now, we'll use a relative path for demonstration - return path.resolve(__dirname, "..", "..", "templates", templateName); + // Templates are located relative to the built CLI package in dist/ + return path.resolve(__dirname, "..", "templates", templateName); } /** diff --git a/vindsmidi-ui/cli/templates/components/button/button.tsx.template b/vindsmidi-ui/cli/templates/components/button/button.tsx.template index 3c7ccf0..13c3c6f 100644 --- a/vindsmidi-ui/cli/templates/components/button/button.tsx.template +++ b/vindsmidi-ui/cli/templates/components/button/button.tsx.template @@ -1,28 +1,43 @@ -import * as React from 'react'; +import React from "react"; +import { cn } from "../utils/cn"; +import { buttonVariants } from "./variants"; +import type { ButtonProps } from "./button.types"; /** - * Button component using the Component Shell Pattern + * Button component using Fluent UI styling with Tailwind CSS */ -export const Button = React.forwardRef( - ({ - variant = 'primary', - size = 'md', - className, - disabled = false, - children, - ...props - }, ref) => { +export const Button = React.forwardRef( + ( + { + children, + className, + variant = "primary", + size = "medium", + iconBefore, + iconAfter, + fullWidth = false, + disabled = false, + ...props + }, + ref + ) => { return ( ); } ); -Button.displayName = 'Button'; \ No newline at end of file +Button.displayName = "Button"; \ No newline at end of file diff --git a/vindsmidi-ui/cli/templates/components/button/button.types.ts.template b/vindsmidi-ui/cli/templates/components/button/button.types.ts.template new file mode 100644 index 0000000..08a2156 --- /dev/null +++ b/vindsmidi-ui/cli/templates/components/button/button.types.ts.template @@ -0,0 +1,35 @@ +import React from "react"; + +/** + * Variants for the Button component + */ +export type ButtonVariant = + | "primary" + | "secondary" + | "outline" + | "subtle" + | "transparent"; + +/** + * Sizes for the Button component + */ +export type ButtonSize = "small" | "medium" | "large"; + +/** + * Button component props + */ +export interface ButtonProps + extends Omit, "ref"> { + /** Content to be rendered inside the button */ + children: React.ReactNode; + /** Visual variant of the button */ + variant?: ButtonVariant; + /** Size of the button */ + size?: ButtonSize; + /** Optional icon to display before the button text */ + iconBefore?: React.ReactNode; + /** Optional icon to display after the button text */ + iconAfter?: React.ReactNode; + /** Makes the button take the full width of its container */ + fullWidth?: boolean; +} \ No newline at end of file diff --git a/vindsmidi-ui/cli/templates/components/button/index.ts.template b/vindsmidi-ui/cli/templates/components/button/index.ts.template new file mode 100644 index 0000000..5f00a2b --- /dev/null +++ b/vindsmidi-ui/cli/templates/components/button/index.ts.template @@ -0,0 +1,3 @@ +export { Button } from "./button"; +export type { ButtonProps, ButtonVariant, ButtonSize } from "./button.types"; +export { buttonVariants } from "./variants"; \ No newline at end of file diff --git a/vindsmidi-ui/cli/templates/components/button/variants.ts.template b/vindsmidi-ui/cli/templates/components/button/variants.ts.template new file mode 100644 index 0000000..7782d0a --- /dev/null +++ b/vindsmidi-ui/cli/templates/components/button/variants.ts.template @@ -0,0 +1,32 @@ +import { cva } from "class-variance-authority"; + +/** + * Button variants using class-variance-authority + */ +export const buttonVariants = cva( + // Base classes + "inline-flex items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", + { + variants: { + variant: { + primary: "bg-blue-600 text-white hover:bg-blue-700 focus-visible:ring-blue-600", + secondary: "bg-gray-100 text-gray-900 hover:bg-gray-200 focus-visible:ring-gray-600", + outline: "border border-gray-300 bg-transparent text-gray-900 hover:bg-gray-50 focus-visible:ring-gray-600", + subtle: "bg-gray-50 text-gray-900 hover:bg-gray-100 focus-visible:ring-gray-600", + transparent: "bg-transparent text-gray-900 hover:bg-gray-50 focus-visible:ring-gray-600", + }, + size: { + small: "h-8 px-3 text-xs", + medium: "h-10 px-4 text-sm", + large: "h-12 px-6 text-base", + }, + fullWidth: { + true: "w-full", + }, + }, + defaultVariants: { + variant: "primary", + size: "medium", + }, + } +); \ No newline at end of file diff --git a/vindsmidi-ui/cli/templates/components/card/card.tsx.template b/vindsmidi-ui/cli/templates/components/card/card.tsx.template new file mode 100644 index 0000000..eb23be2 --- /dev/null +++ b/vindsmidi-ui/cli/templates/components/card/card.tsx.template @@ -0,0 +1,71 @@ +import React from "react"; +import { cn } from "../utils/cn"; + +/** + * Card appearance variants + */ +export type CardVariant = "filled" | "outline" | "subtle" | "elevated"; + +/** + * Card component props + */ +export interface CardProps extends React.HTMLAttributes { + /** Content to be rendered inside the card */ + children: React.ReactNode; + /** Visual variant of the card */ + variant?: CardVariant; + /** Enables interactive hover and focus styles */ + interactive?: boolean; + /** Adds a header section to the card */ + header?: React.ReactNode; + /** Adds a footer section to the card */ + footer?: React.ReactNode; +} + +/** + * Card component using Fluent UI styling with Tailwind CSS + */ +export const Card: React.FC = ({ + children, + className, + variant = "filled", + interactive = false, + header, + footer, + ...props +}) => { + const baseClasses = cn( + "rounded-lg overflow-hidden transition-all duration-200", + { + "bg-white": variant === "filled", + "bg-white border border-gray-200": variant === "outline", + "bg-gray-50": variant === "subtle", + "bg-white shadow-md": variant === "elevated", + "cursor-pointer hover:shadow-lg focus:outline-none focus:ring-2 focus:ring-blue-500": interactive, + }, + className + ); + + return ( +
+ {header && ( +
+ {header} +
+ )} +
{children}
+ {footer && ( +
+ {footer} +
+ )} +
+ ); +}; + +Card.displayName = "Card"; \ No newline at end of file diff --git a/vindsmidi-ui/cli/templates/components/card/card.types.ts.template b/vindsmidi-ui/cli/templates/components/card/card.types.ts.template new file mode 100644 index 0000000..33fc625 --- /dev/null +++ b/vindsmidi-ui/cli/templates/components/card/card.types.ts.template @@ -0,0 +1,22 @@ +import React from "react"; + +/** + * Card appearance variants + */ +export type CardVariant = "filled" | "outline" | "subtle" | "elevated"; + +/** + * Card component props + */ +export interface CardProps extends React.HTMLAttributes { + /** Content to be rendered inside the card */ + children: React.ReactNode; + /** Visual variant of the card */ + variant?: CardVariant; + /** Enables interactive hover and focus styles */ + interactive?: boolean; + /** Adds a header section to the card */ + header?: React.ReactNode; + /** Adds a footer section to the card */ + footer?: React.ReactNode; +} \ No newline at end of file diff --git a/vindsmidi-ui/cli/templates/components/card/index.ts.template b/vindsmidi-ui/cli/templates/components/card/index.ts.template new file mode 100644 index 0000000..a616a41 --- /dev/null +++ b/vindsmidi-ui/cli/templates/components/card/index.ts.template @@ -0,0 +1,2 @@ +export { Card } from "./card"; +export type { CardProps, CardVariant } from "./card.types"; \ No newline at end of file diff --git a/vindsmidi-ui/cli/templates/utils/cn.ts.template b/vindsmidi-ui/cli/templates/utils/cn.ts.template new file mode 100644 index 0000000..8ddb848 --- /dev/null +++ b/vindsmidi-ui/cli/templates/utils/cn.ts.template @@ -0,0 +1,10 @@ +import { clsx, type ClassValue } from "clsx" +import { twMerge } from "tailwind-merge" + +/** + * Utility function to merge class names conditionally + * Combines clsx and tailwind-merge for optimal class name handling + */ +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} \ No newline at end of file From f5c74398313d3e438d05e3757da0dacc2840049c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 8 Jul 2025 17:25:34 +0000 Subject: [PATCH 3/4] Fix dependency resolver to handle utilities and hooks Co-authored-by: edrouhardmicrosoft <128614151+edrouhardmicrosoft@users.noreply.github.com> --- vindsmidi-ui/cli/dist/index.js | 127 +++++++++++++++++- vindsmidi-ui/cli/src/commands/add.ts | 11 +- vindsmidi-ui/cli/src/registry/hooks.ts | 56 ++++++++ vindsmidi-ui/cli/src/registry/utilities.ts | 56 ++++++++ .../cli/src/utils/dependency-resolver.ts | 50 ++++++- .../cli/src/utils/template-manager.ts | 80 ++++++++++- .../hooks/useFluentButton.ts.template | 28 ++++ 7 files changed, 389 insertions(+), 19 deletions(-) create mode 100644 vindsmidi-ui/cli/src/registry/hooks.ts create mode 100644 vindsmidi-ui/cli/src/registry/utilities.ts create mode 100644 vindsmidi-ui/cli/templates/hooks/useFluentButton.ts.template diff --git a/vindsmidi-ui/cli/dist/index.js b/vindsmidi-ui/cli/dist/index.js index cd36064..fd85a33 100644 --- a/vindsmidi-ui/cli/dist/index.js +++ b/vindsmidi-ui/cli/dist/index.js @@ -286,9 +286,45 @@ function getComponentNames() { return components.map((c) => c.name); } +// src/registry/utilities.ts +var utilities = [ + { + name: "cn", + description: "Utility function to merge class names conditionally", + dependencies: ["clsx", "tailwind-merge"], + file: { + name: "cn.ts", + path: "utils/cn.ts", + template: "utils/cn.ts.template" + } + } +]; +function getUtility(name) { + return utilities.find((u) => u.name === name); +} + +// src/registry/hooks.ts +var hooks = [ + { + name: "useFluentButton", + description: "Enhanced button hook with Fluent UI integration", + dependencies: [], + file: { + name: "useFluentButton.ts", + path: "hooks/useFluentButton.ts", + template: "hooks/useFluentButton.ts.template" + } + } +]; +function getHook(name) { + return hooks.find((h) => h.name === name); +} + // src/utils/dependency-resolver.ts function resolveDependencies(components2) { const resolvedComponents = /* @__PURE__ */ new Map(); + const resolvedUtilities = /* @__PURE__ */ new Map(); + const resolvedHooks = /* @__PURE__ */ new Map(); const unresolvedDependencies = /* @__PURE__ */ new Map(); components2.forEach((component) => { resolvedComponents.set(component.name, component); @@ -297,6 +333,20 @@ function resolveDependencies(components2) { component.dependencies.forEach((dep) => { if (dep.type === "component" && !resolvedComponents.has(dep.name)) { unresolvedDependencies.set(dep.name, dep); + } else if (dep.type === "utility" && !resolvedUtilities.has(dep.name)) { + const utility = getUtility(dep.name); + if (utility) { + resolvedUtilities.set(dep.name, utility); + } else if (!dep.optional) { + logger.warn(`Required utility not found: ${dep.name}`); + } + } else if (dep.type === "hook" && !resolvedHooks.has(dep.name)) { + const hook = getHook(dep.name); + if (hook) { + resolvedHooks.set(dep.name, hook); + } else if (!dep.optional) { + logger.warn(`Required hook not found: ${dep.name}`); + } } }); }); @@ -324,11 +374,29 @@ function resolveDependencies(components2) { component.dependencies.forEach((newDep) => { if (newDep.type === "component" && !resolvedComponents.has(newDep.name)) { unresolvedDependencies.set(newDep.name, newDep); + } else if (newDep.type === "utility" && !resolvedUtilities.has(newDep.name)) { + const utility = getUtility(newDep.name); + if (utility) { + resolvedUtilities.set(newDep.name, utility); + } else if (!newDep.optional) { + logger.warn(`Required utility not found: ${newDep.name}`); + } + } else if (newDep.type === "hook" && !resolvedHooks.has(newDep.name)) { + const hook = getHook(newDep.name); + if (hook) { + resolvedHooks.set(newDep.name, hook); + } else if (!newDep.optional) { + logger.warn(`Required hook not found: ${newDep.name}`); + } } }); } } - return Array.from(resolvedComponents.values()); + return { + components: Array.from(resolvedComponents.values()), + utilities: Array.from(resolvedUtilities.values()), + hooks: Array.from(resolvedHooks.values()) + }; } // src/utils/template-manager.ts @@ -359,8 +427,52 @@ async function installComponent(component, targetDir, options = {}) { } logger.success(`Component ${component.name} installed successfully`); } -async function installComponents(components2, targetDir, options = {}) { - for (const component of components2) { +async function installUtility(utility, targetDir, options = {}) { + logger.info(`Installing utility: ${utility.name}`); + const templatePath = getTemplatePath(utility.file.template); + const outputPath = import_path3.default.join(targetDir, utility.file.path); + if (!await import_fs_extra4.default.pathExists(templatePath)) { + logger.warn(`Template not found: ${utility.file.template}`); + return; + } + await renderTemplate( + templatePath, + outputPath, + { + utilityName: utility.name + }, + { overwrite: options.overwrite } + ); + logger.success(`Installed utility: ${utility.name}`); +} +async function installHook(hook, targetDir, options = {}) { + logger.info(`Installing hook: ${hook.name}`); + const templatePath = getTemplatePath(hook.file.template); + const outputPath = import_path3.default.join(targetDir, hook.file.path); + if (!await import_fs_extra4.default.pathExists(templatePath)) { + logger.warn(`Template not found: ${hook.file.template}`); + return; + } + await renderTemplate( + templatePath, + outputPath, + { + hookName: hook.name + }, + { overwrite: options.overwrite } + ); + logger.success(`Installed hook: ${hook.name}`); +} +async function installComponents(resolved, targetDir, options = {}) { + if (options.installDependencies) { + for (const utility of resolved.utilities) { + await installUtility(utility, targetDir, options); + } + for (const hook of resolved.hooks) { + await installHook(hook, targetDir, options); + } + } + for (const component of resolved.components) { await installComponent(component, targetDir, options); } } @@ -497,7 +609,7 @@ async function detectProject(dir) { // src/commands/add.ts function registerAddCommand(program) { - program.command("add").description("Add components to your project").argument("[components...]", "Component names to add").option("-d, --dir ", "Target directory", process.cwd()).option("-f, --force", "Overwrite existing files", false).option("--no-deps", "Skip installing dependencies", false).action(async (componentNames, options) => { + program.command("add").description("Add components to your project").argument("[components...]", "Component names to add").option("-d, --dir ", "Target directory", process.cwd()).option("-f, --force", "Overwrite existing files", false).option("--no-deps", "Skip installing dependencies").action(async (componentNames, options) => { try { if (!componentNames.length) { logger.title("Available Components"); @@ -518,12 +630,13 @@ function registerAddCommand(program) { spinner.text = "Resolving components..."; spinner.start(); const components2 = getComponents(componentNames); - const allComponents = options.deps ? resolveDependencies(components2) : components2; - spinner.succeed(`Resolved ${allComponents.length} components`); + const resolved = options.deps ? resolveDependencies(components2) : { components: components2, utilities: [], hooks: [] }; + const totalItems = resolved.components.length + resolved.utilities.length + resolved.hooks.length; + spinner.succeed(`Resolved ${totalItems} items (${resolved.components.length} components, ${resolved.utilities.length} utilities, ${resolved.hooks.length} hooks)`); spinner.text = "Installing components..."; spinner.start(); const targetDir = import_path6.default.join(options.dir, projectInfo.sourceDir); - await installComponents(allComponents, targetDir, { + await installComponents(resolved, targetDir, { overwrite: options.force, installDependencies: options.deps }); diff --git a/vindsmidi-ui/cli/src/commands/add.ts b/vindsmidi-ui/cli/src/commands/add.ts index 388fea6..c096cae 100644 --- a/vindsmidi-ui/cli/src/commands/add.ts +++ b/vindsmidi-ui/cli/src/commands/add.ts @@ -14,7 +14,7 @@ export function registerAddCommand(program: Command): void { .argument("[components...]", "Component names to add") .option("-d, --dir ", "Target directory", process.cwd()) .option("-f, --force", "Overwrite existing files", false) - .option("--no-deps", "Skip installing dependencies", false) + .option("--no-deps", "Skip installing dependencies") .action(async (componentNames, options) => { try { // If no components specified, list available components @@ -43,18 +43,19 @@ export function registerAddCommand(program: Command): void { spinner.start(); const components = getComponents(componentNames); - const allComponents = options.deps + const resolved = options.deps ? resolveDependencies(components) - : components; + : { components, utilities: [], hooks: [] }; - spinner.succeed(`Resolved ${allComponents.length} components`); + const totalItems = resolved.components.length + resolved.utilities.length + resolved.hooks.length; + spinner.succeed(`Resolved ${totalItems} items (${resolved.components.length} components, ${resolved.utilities.length} utilities, ${resolved.hooks.length} hooks)`); // Install components spinner.text = "Installing components..."; spinner.start(); const targetDir = path.join(options.dir, projectInfo.sourceDir); - await installComponents(allComponents, targetDir, { + await installComponents(resolved, targetDir, { overwrite: options.force, installDependencies: options.deps, }); diff --git a/vindsmidi-ui/cli/src/registry/hooks.ts b/vindsmidi-ui/cli/src/registry/hooks.ts new file mode 100644 index 0000000..ee9d564 --- /dev/null +++ b/vindsmidi-ui/cli/src/registry/hooks.ts @@ -0,0 +1,56 @@ +/** + * Represents a hook dependency + */ +export interface Hook { + name: string; + description: string; + dependencies: string[]; + file: { + name: string; + path: string; + template: string; + }; +} + +/** + * Registry of available hooks + */ +export const hooks: Hook[] = [ + { + name: "useFluentButton", + description: "Enhanced button hook with Fluent UI integration", + dependencies: [], + file: { + name: "useFluentButton.ts", + path: "hooks/useFluentButton.ts", + template: "hooks/useFluentButton.ts.template", + }, + }, +]; + +/** + * Get a hook by name + */ +export function getHook(name: string): Hook | undefined { + return hooks.find((h) => h.name === name); +} + +/** + * Get multiple hooks by name + */ +export function getHooks(names: string[]): Hook[] { + return names.map((name) => { + const hook = getHook(name); + if (!hook) { + throw new Error(`Hook not found: ${name}`); + } + return hook; + }); +} + +/** + * Get all available hook names + */ +export function getHookNames(): string[] { + return hooks.map((h) => h.name); +} \ No newline at end of file diff --git a/vindsmidi-ui/cli/src/registry/utilities.ts b/vindsmidi-ui/cli/src/registry/utilities.ts new file mode 100644 index 0000000..2fca67c --- /dev/null +++ b/vindsmidi-ui/cli/src/registry/utilities.ts @@ -0,0 +1,56 @@ +/** + * Represents a utility dependency + */ +export interface Utility { + name: string; + description: string; + dependencies: string[]; + file: { + name: string; + path: string; + template: string; + }; +} + +/** + * Registry of available utilities + */ +export const utilities: Utility[] = [ + { + name: "cn", + description: "Utility function to merge class names conditionally", + dependencies: ["clsx", "tailwind-merge"], + file: { + name: "cn.ts", + path: "utils/cn.ts", + template: "utils/cn.ts.template", + }, + }, +]; + +/** + * Get a utility by name + */ +export function getUtility(name: string): Utility | undefined { + return utilities.find((u) => u.name === name); +} + +/** + * Get multiple utilities by name + */ +export function getUtilities(names: string[]): Utility[] { + return names.map((name) => { + const utility = getUtility(name); + if (!utility) { + throw new Error(`Utility not found: ${name}`); + } + return utility; + }); +} + +/** + * Get all available utility names + */ +export function getUtilityNames(): string[] { + return utilities.map((u) => u.name); +} \ No newline at end of file diff --git a/vindsmidi-ui/cli/src/utils/dependency-resolver.ts b/vindsmidi-ui/cli/src/utils/dependency-resolver.ts index a07e2d7..f94c359 100644 --- a/vindsmidi-ui/cli/src/utils/dependency-resolver.ts +++ b/vindsmidi-ui/cli/src/utils/dependency-resolver.ts @@ -1,12 +1,22 @@ import { Component, ComponentDependency } from "../registry/schema"; import { getComponent } from "../registry/components"; +import { getUtility } from "../registry/utilities"; +import { getHook } from "../registry/hooks"; import { logger } from "./logger"; +export interface ResolvedDependencies { + components: Component[]; + utilities: Array<{ name: string; file: any }>; + hooks: Array<{ name: string; file: any }>; +} + /** * Resolves all dependencies for a set of components */ -export function resolveDependencies(components: Component[]): Component[] { +export function resolveDependencies(components: Component[]): ResolvedDependencies { const resolvedComponents = new Map(); + const resolvedUtilities = new Map(); + const resolvedHooks = new Map(); const unresolvedDependencies = new Map(); // Add initial components @@ -14,16 +24,30 @@ export function resolveDependencies(components: Component[]): Component[] { resolvedComponents.set(component.name, component); }); - // Collect unresolved dependencies + // Collect unresolved dependencies from initial components components.forEach((component) => { component.dependencies.forEach((dep) => { if (dep.type === "component" && !resolvedComponents.has(dep.name)) { unresolvedDependencies.set(dep.name, dep); + } else if (dep.type === "utility" && !resolvedUtilities.has(dep.name)) { + const utility = getUtility(dep.name); + if (utility) { + resolvedUtilities.set(dep.name, utility); + } else if (!dep.optional) { + logger.warn(`Required utility not found: ${dep.name}`); + } + } else if (dep.type === "hook" && !resolvedHooks.has(dep.name)) { + const hook = getHook(dep.name); + if (hook) { + resolvedHooks.set(dep.name, hook); + } else if (!dep.optional) { + logger.warn(`Required hook not found: ${dep.name}`); + } } }); }); - // Resolve dependencies recursively + // Resolve component dependencies recursively let hasNewDependencies = true; while (hasNewDependencies) { hasNewDependencies = false; @@ -56,10 +80,28 @@ export function resolveDependencies(components: Component[]): Component[] { !resolvedComponents.has(newDep.name) ) { unresolvedDependencies.set(newDep.name, newDep); + } else if (newDep.type === "utility" && !resolvedUtilities.has(newDep.name)) { + const utility = getUtility(newDep.name); + if (utility) { + resolvedUtilities.set(newDep.name, utility); + } else if (!newDep.optional) { + logger.warn(`Required utility not found: ${newDep.name}`); + } + } else if (newDep.type === "hook" && !resolvedHooks.has(newDep.name)) { + const hook = getHook(newDep.name); + if (hook) { + resolvedHooks.set(newDep.name, hook); + } else if (!newDep.optional) { + logger.warn(`Required hook not found: ${newDep.name}`); + } } }); } } - return Array.from(resolvedComponents.values()); + return { + components: Array.from(resolvedComponents.values()), + utilities: Array.from(resolvedUtilities.values()), + hooks: Array.from(resolvedHooks.values()), + }; } diff --git a/vindsmidi-ui/cli/src/utils/template-manager.ts b/vindsmidi-ui/cli/src/utils/template-manager.ts index c416f92..3a46b4e 100644 --- a/vindsmidi-ui/cli/src/utils/template-manager.ts +++ b/vindsmidi-ui/cli/src/utils/template-manager.ts @@ -3,6 +3,7 @@ import fs from "fs-extra"; import { logger } from "./logger"; import { renderTemplate } from "./template"; import { Component } from "../registry/schema"; +import { ResolvedDependencies } from "./dependency-resolver"; /** * Gets the absolute path to a template @@ -50,14 +51,87 @@ export async function installComponent( } /** - * Installs multiple components + * Installs a utility by copying its template file + */ +export async function installUtility( + utility: any, + targetDir: string, + options: { overwrite?: boolean } = {} +): Promise { + logger.info(`Installing utility: ${utility.name}`); + + const templatePath = getTemplatePath(utility.file.template); + const outputPath = path.join(targetDir, utility.file.path); + + if (!(await fs.pathExists(templatePath))) { + logger.warn(`Template not found: ${utility.file.template}`); + return; + } + + await renderTemplate( + templatePath, + outputPath, + { + utilityName: utility.name, + }, + { overwrite: options.overwrite } + ); + + logger.success(`Installed utility: ${utility.name}`); +} + +/** + * Installs a hook by copying its template file + */ +export async function installHook( + hook: any, + targetDir: string, + options: { overwrite?: boolean } = {} +): Promise { + logger.info(`Installing hook: ${hook.name}`); + + const templatePath = getTemplatePath(hook.file.template); + const outputPath = path.join(targetDir, hook.file.path); + + if (!(await fs.pathExists(templatePath))) { + logger.warn(`Template not found: ${hook.file.template}`); + return; + } + + await renderTemplate( + templatePath, + outputPath, + { + hookName: hook.name, + }, + { overwrite: options.overwrite } + ); + + logger.success(`Installed hook: ${hook.name}`); +} + +/** + * Installs components with their dependencies (utilities and hooks) */ export async function installComponents( - components: Component[], + resolved: ResolvedDependencies, targetDir: string, options: { overwrite?: boolean; installDependencies?: boolean } = {} ): Promise { - for (const component of components) { + // Install utilities first + if (options.installDependencies) { + for (const utility of resolved.utilities) { + await installUtility(utility, targetDir, options); + } + + // Install hooks + for (const hook of resolved.hooks) { + await installHook(hook, targetDir, options); + } + } + + // Install components + for (const component of resolved.components) { await installComponent(component, targetDir, options); } } diff --git a/vindsmidi-ui/cli/templates/hooks/useFluentButton.ts.template b/vindsmidi-ui/cli/templates/hooks/useFluentButton.ts.template new file mode 100644 index 0000000..96ceff2 --- /dev/null +++ b/vindsmidi-ui/cli/templates/hooks/useFluentButton.ts.template @@ -0,0 +1,28 @@ +import { useRef } from "react"; + +/** + * Enhanced button hook with Fluent UI integration + */ +export function useFluentButton() { + const ref = useRef(null); + + // Enhanced button props with accessibility + const buttonProps = { + role: "button", + tabIndex: 0, + onKeyDown: (event: React.KeyboardEvent) => { + // Handle Enter and Space key activation + if (event.key === "Enter" || event.key === " ") { + event.preventDefault(); + if (ref.current) { + ref.current.click(); + } + } + }, + }; + + return { + ref, + buttonProps, + }; +} \ No newline at end of file From c4ecb815ca80eff97969c6bed75ccb83fedee0ce Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 8 Jul 2025 17:26:48 +0000 Subject: [PATCH 4/4] Add safeguards against infinite loops and fix logger import inconsistency Co-authored-by: edrouhardmicrosoft <128614151+edrouhardmicrosoft@users.noreply.github.com> --- vindsmidi-ui/cli/dist/index.js | 13 +++++++++---- vindsmidi-ui/cli/src/utils/dependency-resolver.ts | 11 ++++++++++- vindsmidi-ui/cli/src/utils/logger.ts | 5 ++--- 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/vindsmidi-ui/cli/dist/index.js b/vindsmidi-ui/cli/dist/index.js index fd85a33..2efacfa 100644 --- a/vindsmidi-ui/cli/dist/index.js +++ b/vindsmidi-ui/cli/dist/index.js @@ -31,7 +31,6 @@ var import_ora = __toESM(require("ora")); var import_execa = require("execa"); // src/utils/logger.ts -var import_chalk = __toESM(require("chalk")); var import_picocolors = __toESM(require("picocolors")); var logger = { info: (message) => { @@ -55,11 +54,11 @@ var logger = { console.log(message); }, title: (message) => { - console.log(import_chalk.default.bold(` + console.log(import_picocolors.default.bold(` ${message}`)); }, divider: () => { - console.log(import_chalk.default.dim("\u2500".repeat(40))); + console.log(import_picocolors.default.dim("\u2500".repeat(40))); }, newLine: () => { console.log(); @@ -351,8 +350,11 @@ function resolveDependencies(components2) { }); }); let hasNewDependencies = true; - while (hasNewDependencies) { + let iterationCount = 0; + const maxIterations = 100; + while (hasNewDependencies && iterationCount < maxIterations) { hasNewDependencies = false; + iterationCount++; for (const [name, dep] of unresolvedDependencies.entries()) { if (resolvedComponents.has(name)) { unresolvedDependencies.delete(name); @@ -392,6 +394,9 @@ function resolveDependencies(components2) { }); } } + if (iterationCount >= maxIterations) { + logger.warn("Dependency resolution stopped after maximum iterations. Possible circular dependencies detected."); + } return { components: Array.from(resolvedComponents.values()), utilities: Array.from(resolvedUtilities.values()), diff --git a/vindsmidi-ui/cli/src/utils/dependency-resolver.ts b/vindsmidi-ui/cli/src/utils/dependency-resolver.ts index f94c359..39d9cdf 100644 --- a/vindsmidi-ui/cli/src/utils/dependency-resolver.ts +++ b/vindsmidi-ui/cli/src/utils/dependency-resolver.ts @@ -49,8 +49,12 @@ export function resolveDependencies(components: Component[]): ResolvedDependenci // Resolve component dependencies recursively let hasNewDependencies = true; - while (hasNewDependencies) { + let iterationCount = 0; + const maxIterations = 100; // Prevent infinite loops + + while (hasNewDependencies && iterationCount < maxIterations) { hasNewDependencies = false; + iterationCount++; for (const [name, dep] of unresolvedDependencies.entries()) { if (resolvedComponents.has(name)) { @@ -99,6 +103,11 @@ export function resolveDependencies(components: Component[]): ResolvedDependenci } } + // Check if we hit the iteration limit (potential circular dependency) + if (iterationCount >= maxIterations) { + logger.warn("Dependency resolution stopped after maximum iterations. Possible circular dependencies detected."); + } + return { components: Array.from(resolvedComponents.values()), utilities: Array.from(resolvedUtilities.values()), diff --git a/vindsmidi-ui/cli/src/utils/logger.ts b/vindsmidi-ui/cli/src/utils/logger.ts index 87a8d33..0043ca1 100644 --- a/vindsmidi-ui/cli/src/utils/logger.ts +++ b/vindsmidi-ui/cli/src/utils/logger.ts @@ -1,4 +1,3 @@ -import chalk from "chalk"; import picocolors from "picocolors"; export const logger = { @@ -23,10 +22,10 @@ export const logger = { console.log(message); }, title: (message: string): void => { - console.log(chalk.bold(`\n${message}`)); + console.log(picocolors.bold(`\n${message}`)); }, divider: (): void => { - console.log(chalk.dim("─".repeat(40))); + console.log(picocolors.dim("─".repeat(40))); }, newLine: (): void => { console.log();