Skip to main content
Glama
nrwl

Nx MCP Server

Official
by nrwl
form-values.service.ts8.26 kB
import { ContextProvider, createContext } from '@lit-labs/context'; import { IdeCommunicationController } from './ide-communication.controller'; import { compareWithDefaultValue, debounce, extractDefaultValue, getGeneratorIdentifier, } from './utils/generator-schema-utils'; import { FormValues, GenerateUiCopyToClipboardOutputMessage, GeneratorSchema, ValidationResults, } from '@nx-console/shared-generate-ui-types'; import { OptionChangedDetails } from './components/fields/mixins/field-mixin'; import { Root } from './main'; import { submittedContext } from './contexts/submitted-context'; export const formValuesServiceContext = createContext<FormValuesService>( Symbol('form-values'), ); export class FormValuesService { private cwdValue: string | undefined = undefined; private formValues: FormValues = {}; private validationResults: ValidationResults = {}; private icc: IdeCommunicationController; private submittedContextProvider: ContextProvider<{ __context__: boolean }>; constructor(rootElement: Root) { this.icc = rootElement.icc; this.submittedContextProvider = new ContextProvider(rootElement, { context: submittedContext, initialValue: false, }); new ContextProvider(rootElement, { context: formValuesServiceContext, initialValue: this, }); window.addEventListener( 'option-changed', (e: CustomEventInit<OptionChangedDetails>) => { if (!e.detail) return; this.handleOptionChange(e.detail); }, ); window.addEventListener( 'cwd-changed', async (e: CustomEventInit<string>) => { if (e.detail === undefined) return; const firstChange = this.cwdValue === undefined; this.cwdValue = e.detail; if ( !firstChange && this.icc.configuration?.enableTaskExecutionDryRunOnChange ) { this.validationResults = await this.validate( this.formValues, this.icc.generatorSchema, ); if (Object.keys(this.validationResults).length === 0) { this.debouncedRunGenerator(true); } } }, ); } public getFormValues() { return this.formValues; } private async handleOptionChange(details: OptionChangedDetails) { this.formValues = { ...this.formValues, [details.name]: details.value, }; this.validationResults = await this.validate( this.formValues, this.icc.generatorSchema, ); // notify consumers of changes Object.entries(this.validationListeners).forEach(([key, callbacks]) => { callbacks?.forEach((callback) => callback(this.validationResults[key])); }); if (!details.isDefaultValue) { if (Object.keys(this.validationResults).length === 0) { if (this.icc.configuration?.enableTaskExecutionDryRunOnChange) { this.debouncedRunGenerator(true); } } this.touchedListeners[details.name]?.forEach((callback) => callback(true), ); } if (this.defaultValueListeners[details.name]) { this.defaultValueListeners[details.name]?.forEach((callback) => callback(details.isDefaultValue), ); } } private async validate( formValues: FormValues, schema: GeneratorSchema | undefined, ): Promise<ValidationResults> { if (!schema) return {}; const options = schema.options; const errors: Record<string, boolean | string> = {}; Object.entries(formValues).forEach(([key, value]) => { const option = options.find((option) => option.name === key); if (!option) return; if (option.pattern) { const regex = new RegExp(option.pattern); if (!regex.test(String(value))) { errors[key] = `Value should match the pattern '${option.pattern}'`; } } if ( option.isRequired && (!value || (Array.isArray(value) && value.length === 0)) ) { errors[key] = 'This field is required'; } }); const pluginValidationResults = await this.icc.getValidationResults( formValues, schema, ); return { ...errors, ...pluginValidationResults }; } runGenerator(dryRun = false) { const args = this.getSerializedFormValues(); args.push('--no-interactive'); if (dryRun) { args.push('--dry-run'); } this.submittedContextProvider.setValue(true); this.icc.postMessageToIde({ payloadType: 'run-generator', payload: { positional: getGeneratorIdentifier(this.icc.generatorSchema), flags: args, cwd: this.cwdValue?.toString(), }, }); } private debouncedRunGenerator = debounce( (dryRun: boolean) => this.runGenerator(dryRun), 500, ); copyCommandToClipboard() { const args = this.getSerializedFormValues(); const positional = getGeneratorIdentifier(this.icc.generatorSchema); const command = `nx g ${positional} ${args.join(' ')}`; if (this.icc.editor === 'vscode') { navigator.clipboard.writeText(command); } else { this.icc.postMessageToIde( new GenerateUiCopyToClipboardOutputMessage(command), ); } } private getSerializedFormValues(): string[] { const args: string[] = []; const formValues = { ...this.formValues, ...(this.icc.generatorSchema?.context?.fixedFormValues ?? {}), }; Object.entries(formValues).forEach(([key, value]) => { const option = this.icc.generatorSchema?.options.find( (option) => option.name === key, ); const defaultValue = extractDefaultValue(option); if (compareWithDefaultValue(value, defaultValue)) return; const valueString = value?.toString() ?? ''; if (valueString.includes(' ')) { if (valueString.includes('"')) { args.push(`--${key}='${value}'`); } else { args.push(`--${key}="${value}"`); } } else { args.push(`--${key}=${value}`); } }); return args; } /** listeners **/ private validationListeners: Record< string, ((value: string | boolean | undefined) => void)[] > = {}; private defaultValueListeners: Record< string, ((isDefault: boolean) => void)[] > = {}; private touchedListeners: Record<string, ((isTouched: boolean) => void)[]> = {}; private valueChangeListeners: Record<string, ((value: any) => void)[]> = {}; private formValueListeners: ((formValues: FormValues) => void)[] = []; registerValidationListener( key: string, listener: (value: string | boolean | undefined) => void, ) { if (!this.validationListeners[key]) this.validationListeners[key] = []; this.validationListeners[key].push(listener); } registerDefaultValueListener( key: string, listener: (isDefault: boolean) => void, ) { if (!this.defaultValueListeners[key]) this.defaultValueListeners[key] = []; this.defaultValueListeners[key].push(listener); } registerTouchedListener(key: string, listener: (isTouched: boolean) => void) { if (!this.touchedListeners[key]) this.touchedListeners[key] = []; this.touchedListeners[key].push(listener); } registerValueChangeListener(key: string, listener: (value: any) => void) { if (!this.valueChangeListeners[key]) this.valueChangeListeners[key] = []; this.valueChangeListeners[key].push(listener); } registerFormValueListener(listener: (formValues: FormValues) => void) { this.formValueListeners.push(listener); } /** * Update form values from IDE and notify components */ updateFormValuesFromIde(updatedValues: FormValues) { this.formValues = { ...this.formValues, ...updatedValues }; // Notify all affected field components Object.entries(updatedValues).forEach(([key, value]) => { // Update cwd if included in updated values if (key === 'cwd' && typeof value === 'string') { this.cwdValue = value; } // Notify any listeners for this field this.valueChangeListeners[key]?.forEach((listener) => listener(value)); }); // Notify form value listeners about the complete form state this.formValueListeners.forEach((listener) => listener(this.formValues)); } }

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