UNPKG

igniteui-theming

Version:

A set of Sass variables, mixins, and functions for generating palettes, typography, and elevations used by Ignite UI components.

313 lines (312 loc) 12.8 kB
import { PLATFORM_METADATA, detectPlatformFromDependencies, isLicensedPackage } from "../../knowledge/platforms/index.js"; import { z } from "zod"; import { dirname, resolve } from "node:path"; import { readFile } from "node:fs/promises"; //#region src/tools/handlers/platform.ts /** * Handler for detect_platform tool. * * Detects the target platform (Angular, Web Components, React, Blazor, or Generic) * from package.json dependencies and project config files. * * Uses a multi-signal detection approach: * 1. Ignite UI packages (HIGH confidence) * 2. Config files like angular.json, vite.config.ts, etc. (MEDIUM-HIGH confidence) * 3. Framework packages as fallback (LOW confidence) * 4. Generic (standalone) mode when no Ignite UI product is detected * * When multiple platforms are detected with significant confidence, * returns an ambiguous result prompting user to specify explicitly. * * When no Ignite UI product is found, returns "generic" platform * with tool eligibility guidance and Sass load path configuration help. */ /** * Zod schema for validating package.json structure. * Only validates the fields we need for platform detection. */ var packageJsonSchema = z.object({ dependencies: z.record(z.string()).optional(), devDependencies: z.record(z.string()).optional() }); /** * Format a detection signal for human-readable output. */ function formatSignal(signal) { switch (signal.type) { case "ignite_package": return `package: ${signal.package}`; case "config_file": return `config: ${signal.file}`; case "framework_package": return `framework: ${signal.package}`; default: return "unknown"; } } /** * Lookup table mapping config file patterns to Sass load path guidance. * Add new entries here when supporting additional build tools. */ var SASS_CONFIG_GUIDANCE = [ { match: "exact", key: "angular.json", description: "An `angular.json` config file was detected. To use Sass output from this MCP, ensure your Angular project includes `node_modules` in the Sass load paths:", lang: "json", code: [ "// In angular.json → architect → build → options:", "\"stylePreprocessorOptions\": {", " \"includePaths\": [\"node_modules\"]", "}" ].join("\n") }, { match: "prefix", key: "vite.config", description: "A Vite config file was detected. To use Sass output from this MCP, ensure your Vite config includes `node_modules` in the Sass load paths:", lang: "js", code: [ "// In vite.config.ts/js:", "css: {", " preprocessorOptions: {", " scss: {", " loadPaths: ['node_modules']", " }", " }", "}" ].join("\n") }, { match: "prefix", key: "next.config", description: "A Next.js config file was detected. To use Sass output from this MCP, ensure your Next.js config includes `node_modules` in the Sass load paths:", lang: "js", code: [ "// In next.config.js/mjs/ts:", "sassOptions: {", " loadPaths: ['node_modules']", "}" ].join("\n") } ]; var SASS_CONFIG_FALLBACK = "To use Sass output from this MCP, ensure your project's Sass compiler has `node_modules` in its `loadPaths`. The exact configuration depends on your build tool — Angular CLI uses `includePaths` in `angular.json`, while most other tools (Vite, Next.js, sass CLI) use `loadPaths`. Investigate the project's build configuration to find the right place to add this."; /** * Find Sass configuration guidance for a given config file name. */ function findSassGuidance(fileName) { return SASS_CONFIG_GUIDANCE.find((entry) => entry.match === "exact" ? fileName === entry.key : fileName.startsWith(entry.key)); } /** * Build the "Available Tools" and "Not Available" sections for generic mode. */ function buildToolEligibilitySection() { return [ "### Available Tools", "", "The following tools work in generic (standalone) mode:", "", "- `create_palette` — Generate color palettes", "- `create_custom_palette` — Generate fully custom palettes", "- `create_typography` — Set up typography/type scales", "- `create_elevations` — Configure shadow/elevation system", "- `create_theme` — Generate a complete theme", "- `set_size` / `set_spacing` / `set_roundness` — Layout tokens (use `scope` with a custom CSS selector or omit for `:root`; do **not** use `component` as it targets Ignite UI component selectors)", "- `get_color` — Get CSS variable references for palette colors", "- `read_resource` — Read theming reference data", "", "### Not Available in Generic Mode", "", "- `create_component_theme` — Requires a specific Ignite UI product platform (angular, webcomponents, react, or blazor) for component selectors and variable prefixes", "- `get_component_design_tokens` — Returns tokens for Ignite UI framework components which are not present in generic mode", "" ]; } /** * Build the "Sass Configuration" section based on detected config file signals. */ function buildSassConfigSection(signals) { const lines = ["### Sass Configuration", ""]; const configFileSignals = signals.filter((s) => s.type === "config_file"); if (configFileSignals.length === 0) { lines.push(SASS_CONFIG_FALLBACK, ""); return lines; } for (const signal of configFileSignals) { if (signal.type !== "config_file") continue; const guidance = findSassGuidance(signal.file); if (guidance) lines.push(guidance.description, "", `\`\`\`${guidance.lang}`, guidance.code, "```", ""); } return lines; } /** * Build the "Output Format Notes" section based on whether igniteui-theming is installed. */ function buildOutputFormatNotes(hasThemingPackage) { const lines = ["### Output Format Notes", ""]; if (!hasThemingPackage) lines.push("The `igniteui-theming` package was **not found** in this project's dependencies.", "", "- **CSS output** works without any local installation — the MCP compiles Sass to CSS server-side.", "- **Sass output** requires `igniteui-theming` to be resolvable in your project. Run `npm install igniteui-theming` to install it, then configure `loadPaths` as described above."); else lines.push("The `igniteui-theming` package is installed in this project.", "", "- **CSS output** is compiled server-side by the MCP — no Sass toolchain needed.", "- **Sass output** uses `@use 'igniteui-theming' as *;` and requires the `loadPaths` configuration described above."); return lines; } /** * Build response text for ambiguous detection (multiple Ignite UI platforms found). */ function buildAmbiguousResponse(result) { const alternatives = result.alternatives; const lines = [ "## Platform Detection Result", "", "**Status:** Ambiguous - Multiple platforms detected", "", "The project appears to contain dependencies for multiple Ignite UI platforms. This might be a monorepo or a project transitioning between frameworks.", "", "### Detected Platforms", "" ]; for (const alt of alternatives) { const metadata = PLATFORM_METADATA[alt.platform]; lines.push(`#### ${metadata.name}`, `- **Confidence:** ${alt.confidence}%`, `- **Signals:** ${alt.signals.map(formatSignal).join(", ")}`, `- **Theming module:** \`${metadata.themingModule}\``, ""); } lines.push("### Action Required", "", "Please specify the platform explicitly when calling theme generation tools:", ""); for (const alt of alternatives) { const metadata = PLATFORM_METADATA[alt.platform]; lines.push(`- Use \`platform: '${alt.platform}'\` for ${metadata.name}`); } return lines; } /** * Build response text for generic (standalone) mode. */ function buildGenericResponse(result, hasThemingPackage) { const metadata = PLATFORM_METADATA.generic; const lines = [ "## Platform Detection Result", "", `**Detected Platform:** ${metadata.name}`, `**Confidence:** ${result.confidence}`, `**Theming Module:** \`${metadata.themingModule}\``, "", metadata.description, "" ]; if (result.signals && result.signals.length > 0) lines.push(`**Detection Signals:** ${result.signals.map(formatSignal).join(", ")}`, ""); lines.push(...buildToolEligibilitySection(), ...buildSassConfigSection(result.signals ?? []), ...buildOutputFormatNotes(hasThemingPackage)); return lines; } /** * Build response text for a single detected Ignite UI platform. */ function buildPlatformResponse(result, licensed) { const platform = result.platform; const metadata = PLATFORM_METADATA[platform]; const lines = [ "## Platform Detection Result", "", `**Detected Platform:** ${metadata.name}`, `**Confidence:** ${result.confidence}` ]; if (result.detectedPackage) { lines.push(`**Detected Package:** ${result.detectedPackage}`); if (platform === "angular") { const isLicensed = isLicensedPackage(result.detectedPackage); lines.push(`**Package Type:** ${isLicensed ? "Licensed (@infragistics)" : "Open Source (npm)"}`); } } if (result.signals && result.signals.length > 0) lines.push(`**Detection Signals:** ${result.signals.map(formatSignal).join(", ")}`); const themingModule = platform === "angular" && licensed ? metadata.licensedThemingModule : metadata.themingModule; lines.push(`**Theming Module:** \`${themingModule}\``, ""); let usageLine = `When generating theme code, use \`platform: '${platform}'\``; if (platform === "angular" && licensed) usageLine += " and `licensed: true`"; usageLine += " to ensure the correct Sass syntax is generated for this platform."; lines.push("### Usage", "", usageLine, "", metadata.description); if (result.confidence === "low") lines.push("", "### Note", "", "Detection confidence is **low**. This means no Ignite UI package was found, only framework packages. Please verify this is the correct platform before generating themes."); else if (result.confidence === "medium") lines.push("", "### Note", "", "Detection confidence is **medium**. Consider verifying the platform if the generated code doesn't work as expected."); return lines; } /** * Build response text when no platform could be determined (error/null state). */ function buildNullPlatformResponse(result) { return [ "## Platform Detection Result", "", "**Platform:** Not detected", `**Reason:** ${result.reason}`, "", "### Recommendation", "", "Please specify the platform explicitly when calling theme generation tools:", "", "- Use `platform: 'angular'` for Ignite UI for Angular", "- Use `platform: 'webcomponents'` for Ignite UI for Web Components", "- Use `platform: 'react'` for Ignite UI for React", "- Use `platform: 'blazor'` for Ignite UI for Blazor", "- Use `platform: 'generic'` for platform-agnostic output" ]; } /** * Handle the detect_platform tool invocation. */ async function handleDetectPlatform(params) { const packageJsonPath = params.packageJsonPath ?? "./package.json"; const resolvedPath = resolve(process.cwd(), packageJsonPath); const projectRoot = dirname(resolvedPath); let result; let parsedDeps = {}; let parsedDevDeps = {}; try { const packageJsonContent = await readFile(resolvedPath, "utf-8"); const parseResult = packageJsonSchema.safeParse(JSON.parse(packageJsonContent)); if (!parseResult.success) result = { platform: null, confidence: "none", signals: [], reason: `Invalid package.json structure: ${parseResult.error.message}` }; else { const packageJson = parseResult.data; parsedDeps = packageJson.dependencies ?? {}; parsedDevDeps = packageJson.devDependencies ?? {}; result = detectPlatformFromDependencies(parsedDeps, parsedDevDeps, projectRoot); } } catch (error) { result = { platform: null, confidence: "none", signals: [], reason: `Could not read package.json: ${error instanceof Error ? error.message : "Unknown error"}` }; } const hasThemingPackage = "igniteui-theming" in { ...parsedDeps, ...parsedDevDeps }; const response = { platform: result.platform, confidence: result.confidence, reason: result.reason, signals: result.signals }; if (result.ambiguous && result.alternatives) { response.ambiguous = true; response.alternatives = result.alternatives; } if (result.detectedPackage) { response.detectedPackage = result.detectedPackage; response.licensed = isLicensedPackage(result.detectedPackage); } if (result.platform) { const metadata = PLATFORM_METADATA[result.platform]; response.platformInfo = { name: metadata.name, packageName: metadata.packageName, themingModule: metadata.themingModule, description: metadata.description }; } return { content: [{ type: "text", text: (result.ambiguous && result.alternatives ? buildAmbiguousResponse(result) : result.platform === "generic" ? buildGenericResponse(result, hasThemingPackage) : result.platform ? buildPlatformResponse(result, response.licensed) : buildNullPlatformResponse(result)).join("\n") }], structuredData: response }; } //#endregion export { handleDetectPlatform };