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
JavaScript
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 };