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