UNPKG

@salesforce/pwa-kit-mcp

Version:

MCP server that helps you build Salesforce Commerce Cloud PWA Kit Composable Storefront

350 lines (333 loc) 14.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var _zod = require("zod"); var _path = _interopRequireDefault(require("path")); var _utils = require("../utils"); var _constants = require("../utils/constants"); function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; } function asyncGeneratorStep(n, t, e, r, o, a, c) { try { var i = n[a](c), u = i.value; } catch (n) { return void e(n); } i.done ? t(u) : Promise.resolve(u).then(r, o); } function _asyncToGenerator(n) { return function () { var t = this, e = arguments; return new Promise(function (r, o) { var a = n.apply(t, e); function _next(n) { asyncGeneratorStep(a, r, o, _next, _throw, "next", n); } function _throw(n) { asyncGeneratorStep(a, r, o, _next, _throw, "throw", n); } _next(void 0); }); }; } /* * Copyright (c) 2025, Salesforce, Inc. * All rights reserved. * SPDX-License-Identifier: BSD-3-Clause * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ const systemPrompt = ` You are a smart assistant that helps create new React components. Please ask the user for the following, one at a time: 1. What is the name of the new component? 2. What is the main purpose of this component? Please reply with exactly one of the following options: - Display a single Product - Display a list of Products - Other (please specify) **Do not** assume answers. Collect all answers before proceeding. Once the answers are provided, execute the createComponent tool with the collected information as input parameters. `; const systemPromptForCustomComponent = ` You have chosen a custom purpose for your component. Please provide the following details: - What is the main purpose of this component? - What are the requirements? - What type of component is this? (presentational, container, form, etc.) **Component Generation Guidelines:** - Use functional components with hooks - Use PascalCase for component names - Use kebab-case for directories - Start simple, expand only if requested - One main purpose per component - Components should be created in the components folder under PWA_STOREFRONT_APP_PATH, at: [PWA_STOREFRONT_APP_PATH]/components/[component-name]/index.jsx `; const systemPromptForComponentPurpose = ` What is the main purpose of this component? Reply with exactly one of the following options: "Display a single Product", "Display a list of Products", or "Other (please specify)".`; class CreateNewComponentTool { constructor() { var _this = this; this.name = 'pwakit_create_component'; this.description = `Create a new ${_constants.PWA_KIT_DESCRIPTIVE_NAME} component. Gather information from user for the MCP tool parameters **one at a time**, in a natural and conversational way. Do **not** ask all the questions at once.`; this.inputSchema = { componentName: _zod.z.string().min(1, 'The name of the new component to create?'), purpose: _zod.z.string().min(1, 'The purpose of the new component (e.g., display a single Product, display a list of products or something else)').describe(systemPromptForComponentPurpose) }; this.handler = /*#__PURE__*/function () { var _ref = _asyncToGenerator(function* (args) { if (!args || !args.componentName || !args.purpose) { return { content: [{ type: 'text', text: systemPrompt }] }; } let absolutePaths; try { absolutePaths = yield (0, _utils.detectWorkspacePaths)(); (0, _utils.logMCPMessage)(`Detected workspace paths: ${JSON.stringify(absolutePaths)}`); } catch (error) { (0, _utils.logMCPMessage)(`Error detecting workspace paths: ${error.message}`); // if this is a user prompt error (project path not detected) if (error.message.includes('Could not detect PWA Kit project directory')) { return { content: [{ type: 'text', text: `I need to know where your PWA Kit project is located to create the component. ${error.message}\n\nPlease provide the path to your PWA Kit project's app directory.` }] }; } return { content: [{ type: 'text', text: `Error detecting workspace configuration: ${error.message}` }] }; } const normalizedPurpose = args.purpose.trim().toLowerCase(); const isSingleProduct = normalizedPurpose === 'display a single product'; const isProductList = normalizedPurpose === 'display a list of products'; if (isSingleProduct) { // Proceed with standard component creation return _this.createComponent(args.componentName, absolutePaths.componentsPath, 'singleProduct'); } else if (isProductList) { return _this.createComponent(args.componentName, absolutePaths.componentsPath, 'productList'); } else { // Custom purpose: let Cursor take over and ask clarifying questions return { content: [{ type: 'text', text: systemPromptForCustomComponent }] }; } }); return function (_x) { return _ref.apply(this, arguments); }; }(); } createComponent(componentName, location, entityType) { var _this2 = this; return _asyncToGenerator(function* () { try { return yield _this2.generateComponentFiles(componentName, location, entityType); } catch (error) { return { content: [{ type: 'text', text: `Error creating component: ${error.message}` }] }; } })(); } generateComponentFiles(componentName, location, entityType) { var _this3 = this; return _asyncToGenerator(function* () { if (entityType === 'singleProduct' || entityType === 'productList') { // Call updateComponentToPresentational for product-based components return yield _this3.updateComponentToPresentational('product', componentName, location, { list: entityType === 'productList' }); } })(); } updateComponentToPresentational(entityType, componentName, location, options = {}) { return _asyncToGenerator(function* () { const kebabDirName = (0, _utils.toKebabCase)(componentName); const pascalComponentName = (0, _utils.toPascalCase)(componentName); const componentDir = _path.default.join(location, kebabDirName); const componentFilePath = _path.default.join(componentDir, 'index.jsx'); let code = ''; // Special logic for product entity if (entityType === 'product') { // If options.list is true, generate a list-of-products component if (options.list) { code = ` import React from 'react'; import PropTypes from 'prop-types'; import { Box, Text, Image, Stack } from '@chakra-ui/react'; const ${pascalComponentName} = ({ products }) => ( <Stack spacing={4}> {products.map(product => ( <Box key={product.productId} borderWidth="1px" borderRadius="md" p={4}> <Text fontSize="xl" fontWeight="bold">{product.name}</Text> {product.imageGroups && product.imageGroups[0]?.images[0]?.link && ( <Image src={product.imageGroups[0].images[0].link} alt={product.name} maxW="150px" mb={2} /> )} <Text>assigned_categories: {product.assigned_categories?.toString?.() ?? ''}</Text> <Text>price: {product.price?.toString?.() ?? ''}</Text> </Box> ))} </Stack> ); ${pascalComponentName}.propTypes = { products: PropTypes.arrayOf(PropTypes.shape({ productId: PropTypes.string, name: PropTypes.string, assigned_categories: PropTypes.any, price: PropTypes.any, imageGroups: PropTypes.array })).isRequired }; export default ${pascalComponentName}; `; } else { // Single product component (with selectors, image, etc.) code = ` import React, { useState } from 'react'; import PropTypes from 'prop-types'; import { Box, Text, Image, Button, HStack, Stack } from '@chakra-ui/react'; // Helper to filter variants by selected attribute values const filterVariants = (variants, selected) => { return variants.filter(variant => Object.entries(selected).every( ([attr, value]) => !value || variant.variationValues?.[attr] === value ) ); }; // Helper to get the image for the selected color const getImageForSelection = (imageGroups, selected) => { if (selected.color) { const group = imageGroups.find( g => g.variationAttributes && g.variationAttributes.some( va => va.id === 'color' && va.values.some(v => v.value === selected.color) ) ); if (group && group.images.length > 0) { return group.images[0].link; } } if (imageGroups.length > 0 && imageGroups[0].images.length > 0) { return imageGroups[0].images[0].link; } return null; }; const ${pascalComponentName} = ({ product }) => { const { variationAttributes = [], variants = [], imageGroups = [] } = product; const [selected, setSelected] = useState(() => { const initial = {}; variationAttributes.forEach(attr => { initial[attr.id] = ''; }); return initial; }); // Build a color code to swatch image URL map const swatchMap = {}; imageGroups .filter(group => group.viewType === 'swatch') .forEach(group => { const colorCode = group.variationAttributes?.[0]?.values?.[0]?.value; if (colorCode && group.images[0]?.link) { swatchMap[colorCode] = group.images[0].link; } }); const filteredVariants = filterVariants(variants, selected); const getAvailableValues = (attrId) => { const otherSelected = { ...selected }; delete otherSelected[attrId]; const possibleVariants = filterVariants(variants, otherSelected); const values = new Set(); possibleVariants.forEach(v => { if (v.variationValues?.[attrId]) values.add(v.variationValues[attrId]); }); return Array.from(values); }; const imageUrl = getImageForSelection(imageGroups, selected); return ( <Box> <Text fontSize="2xl" fontWeight="bold" mb={2}>{product.name}</Text> {imageUrl && ( <Image src={imageUrl} alt={product.name} maxW="300px" mb={4} /> )} <Text>assigned_categories: {product.assigned_categories?.toString?.() ?? ''}</Text> <Text>price: {product.price?.toString?.() ?? ''}</Text> {/* Dynamic variant attribute selectors */} {variationAttributes.map(attr => ( <Box key={attr.id} my={2}> <Text as="span" fontWeight="semibold">{attr.name}:</Text> <HStack spacing={2} mt={1}> {getAvailableValues(attr.id).map(val => attr.id === 'color' ? ( <Button key={val} onClick={() => setSelected(sel => ({ ...sel, [attr.id]: val }))} variant={selected[attr.id] === val ? 'solid' : 'outline'} borderRadius="full" minW="32px" h="32px" p={0} borderColor={ selected[attr.id] === val ? 'blue.500' : 'gray.200' } _hover={{opacity: 0.8}} aria-label={val} > {swatchMap[val] ? ( <Image src={swatchMap[val]} alt={val} borderRadius="full" boxSize="28px" /> ) : ( val )} </Button> ) : ( <Button key={val} onClick={() => setSelected(sel => ({ ...sel, [attr.id]: val }))} variant={selected[attr.id] === val ? 'solid' : 'outline'} colorScheme={selected[attr.id] === val ? 'blue' : 'gray'} borderRadius="md" size="sm" > {val} </Button> ) )} </HStack> </Box> ))} </Box> ); }; ${pascalComponentName}.propTypes = { product: PropTypes.shape({ name: PropTypes.string, assigned_categories: PropTypes.any, price: PropTypes.any, variationAttributes: PropTypes.array, variants: PropTypes.array, imageGroups: PropTypes.array }).isRequired }; export default ${pascalComponentName}; `; } } else { throw new Error(`Entity type '${entityType}' is not supported.`); } const messages = []; messages.push((0, _constants.systemPromptForFileGeneration)(componentFilePath, code)); messages.push(_constants.SYSTEM_PROMPT_FOR_LINT_INSTRUCTIONS); return { content: [{ type: 'text', text: (0, _constants.systemPromptForOrderedFileChanges)(messages) }] }; })(); } } var _default = exports.default = CreateNewComponentTool;