Skip to main content
Glama
nrwl

Nx MCP Server

Official
by nrwl
generate-ui-webview.ts9.29 kB
import { FormValues, GenerateUiBannerInputMessage, GenerateUiConfigurationInputMessage, GenerateUiGeneratorSchemaInputMessage, GenerateUiInputMessage, GenerateUiOutputMessage, GenerateUiValidationResultsInputMessage, GeneratorSchema, ValidationResults, } from '@nx-console/shared-generate-ui-types'; import { getNxWorkspacePath, GlobalConfigurationStore, } from '@nx-console/vscode-configuration'; import { getStartupMessage, getTransformedGeneratorSchema, } from '@nx-console/vscode-nx-workspace'; import { CliTaskProvider, NodeTask } from '@nx-console/vscode-tasks'; import { existsSync } from 'node:fs'; import { join } from 'node:path'; import { commands, ExtensionContext, tasks, Uri, ViewColumn, WebviewPanel, window, EventEmitter, } from 'vscode'; export class GenerateUiWebview { private webviewPanel: WebviewPanel | undefined; private _webviewSourceUri: Uri; private generatorToDisplay: GeneratorSchema | undefined; private plugins: | { schemaProcessors?: any[]; validators?: any[]; startupMessages?: any[] } | undefined; private openedFromAI = false; private readonly _onDispose = new EventEmitter<void>(); constructor(private context: ExtensionContext) { this._webviewSourceUri = Uri.joinPath( this.context.extensionUri, 'generate-ui-v2', ); } get onDispose() { return this._onDispose.event; } async openGenerateUi(generator: GeneratorSchema, openedFromAI = false) { if (this.webviewPanel !== undefined) { this.webviewPanel.dispose(); } this.openedFromAI = openedFromAI; this.generatorToDisplay = generator; this.webviewPanel = window.createWebviewPanel( 'nx-console', // Identifies the type of the webview. Used internally 'Generate UI', // Title of the panel displayed to the user ViewColumn.Active, // Editor column to show the new webview panel in. { retainContextWhenHidden: true, enableScripts: true, localResourceRoots: [this.context.extensionUri], }, ); const scriptUri = this.webviewPanel.webview.asWebviewUri( Uri.joinPath(this._webviewSourceUri, 'main.js'), ); const stylesUri = this.webviewPanel.webview.asWebviewUri( Uri.joinPath(this._webviewSourceUri, 'output.css'), ); const codiconsUri = this.webviewPanel.webview.asWebviewUri( Uri.joinPath( this._webviewSourceUri, '@vscode', 'codicons', 'dist', 'codicon.css', ), ); const vscodeElementsUri = this.webviewPanel.webview.asWebviewUri( Uri.joinPath( this.context.extensionUri, 'node_modules', '@vscode-elements', 'elements', 'dist', 'bundled.js', ), ); this.webviewPanel.webview.html = ` <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Generate UI</title> <link href="${stylesUri}" rel="stylesheet"> <link href="${codiconsUri}" rel="stylesheet" id="vscode-codicon-stylesheet" > <style> :root { ${this.getVscodeStyleMappings()} font-size: var(--vscode-font-size); } body { padding: 0; } </style> <script type="module" src="${vscodeElementsUri}"></script> </head> <body> <script type="module" src="${scriptUri}"></script> <root-element></root-element> </body> </html> `; this.webviewPanel.webview.onDidReceiveMessage( (message: GenerateUiOutputMessage) => { this.handleMessageFromWebview(message); }, ); this.webviewPanel.onDidDispose(() => { this.webviewPanel = undefined; this.generatorToDisplay = undefined; this._onDispose.fire(); }); this.plugins = await this.loadPlugins(); this.webviewPanel.reveal(); } async updateFormValues(formValues: FormValues) { this.postMessageToWebview({ payloadType: 'update-form-values', payload: formValues, }); } private async postMessageToWebview(message: GenerateUiInputMessage) { if (message.payloadType === 'generator') { const transformed = await this.transformMessage(message); this.webviewPanel?.webview.postMessage(transformed); } else { this.webviewPanel?.webview.postMessage(message); } } private async handleMessageFromWebview(message: GenerateUiOutputMessage) { switch (message.payloadType) { case 'run-generator': { if (message.payload.flags.includes('--dry-run') || !this.openedFromAI) { CliTaskProvider.instance.executeTask({ command: 'generate', ...message.payload, }); } else { const scriptLocation = join( this.context.extensionUri.fsPath, 'wrap-generator.js', ); const task = await NodeTask.create({ script: scriptLocation, args: [ 'npx', 'nx', 'generate', message.payload.positional, ...message.payload.flags, ], }); await tasks.executeTask(task); } break; } case 'output-init': { if (!this.generatorToDisplay) { return; } commands.executeCommand('workbench.action.focusActiveEditorGroup'); this.postMessageToWebview( new GenerateUiConfigurationInputMessage({ enableTaskExecutionDryRunOnChange: !!GlobalConfigurationStore.instance.get( 'enableTaskExecutionDryRunOnChange', ), // Temporarily disabled copilot feature - to be replaced with new feature hasCopilot: false, }), ); this.postMessageToWebview( new GenerateUiGeneratorSchemaInputMessage(this.generatorToDisplay), ); getStartupMessage(this.generatorToDisplay).then((startupMessage) => { if (startupMessage) { this.postMessageToWebview( new GenerateUiBannerInputMessage(startupMessage), ); } }); break; } case 'request-validation': { const validators = this.plugins?.validators; let validationErrors: ValidationResults = {}; if (validators) { validators.forEach((validator) => { const result = validator( message.payload.formValues, message.payload.schema, ); if (result) { validationErrors = { ...validationErrors, ...result }; } }); } this.postMessageToWebview( new GenerateUiValidationResultsInputMessage(validationErrors), ); break; } // Temporarily commented out - to be replaced with new feature // case 'fill-with-copilot': { // await fillWithGenerateUi( // message.payload.generatorName, // message.payload.formValues, // ); // break; // } } } private async loadPlugins(): Promise< | { schemaProcessors?: any[]; validators?: any[]; startupMessages?: any[] } | undefined > { try { const pluginFile = `${getNxWorkspacePath()}/.nx/console/plugins.mjs`; if (!existsSync(pluginFile)) { return undefined; } return await import(pluginFile).then((module) => module.default); } catch (_) { return undefined; } } private async transformMessage( message: GenerateUiGeneratorSchemaInputMessage, ): Promise<GenerateUiGeneratorSchemaInputMessage> { const transformedSchema = await getTransformedGeneratorSchema( message.payload, ); return { ...message, payload: transformedSchema ?? message.payload, }; } private getVscodeStyleMappings(): string { // note that --vscode-settings-dropdownListBorder is the color used for the webview ui toolkit divider // refer to https://github.com/microsoft/vscode-webview-ui-toolkit/blob/main/src/design-tokens.ts return ` --foreground-color: var(--vscode-editor-foreground); --muted-foreground-color: var(--vscode-input-placeholderForeground); --background-color: var(--vscode-editor-background); --primary-color: var(--button-primary-background, var(--vscode-button-background)); --secondary-color: var(--button-secondary-background); --error-color: var(--vscode-inputValidation-errorBorder); --field-border-color: var(--panel-view-border); --focus-border-color: var(--vscode-focusBorder); --badge-background-color: var(--vscode-badge-background); --badge-foreground-color: var(--vscode-badge-foreground); --banner-warning-color: var(--vscode-statusBarItem-warningBackground); --banner-error-color: var(--vscode-statusBarItem-errorBackground); --banner-text-color: var(--vscode-statusBarItem-warningForeground); --separator-color: var(--vscode-settings-dropdownListBorder); --field-nav-hover-color: var(--vscode-list-hoverBackground); `; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/nrwl/nx-console'

If you have feedback or need assistance with the MCP directory API, please join our Discord server