@salesforce/pwa-kit-mcp
Version:
MCP server that helps you build Salesforce Commerce Cloud PWA Kit Composable Storefront
424 lines (409 loc) • 17.9 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
exports.extractAllClassNames = extractAllClassNames;
exports.extractClassDocs = extractClassDocs;
exports.extractNamedParameterTypes = extractNamedParameterTypes;
exports.extractReturnTypeStructure = extractReturnTypeStructure;
exports.getTypeDefinitionMarkdown = getTypeDefinitionMarkdown;
exports.parseInterfaceProperties = parseInterfaceProperties;
exports.parseJSDoc = parseJSDoc;
exports.parseOptionsParameter = parseOptionsParameter;
exports.parseReturnType = parseReturnType;
exports.parseTypeScriptParameters = parseTypeScriptParameters;
var _zod = require("zod");
var _promises = _interopRequireDefault(require("fs/promises"));
var _path = _interopRequireDefault(require("path"));
var _index = require("../utils/index.js");
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); }); }; }
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; }
function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : i + ""; }
function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); } /*
* 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
*/
function getCommerceSDKTypesFromNodeModulesPath(nodeModulesPath) {
return _path.default.join(nodeModulesPath, 'commerce-sdk-isomorphic/lib/index.cjs.d.ts');
}
function extractAllClassNames(fileContent) {
// Match all lines like: declare class ShopperProducts<...> { or declare class ShopperProducts {
// Allow for any amount of whitespace before 'declare'
const classRegex = /\bdeclare class (\w+)(?:<[^{]+>)?\s*{/g;
const classNames = [];
let match;
while (match = classRegex.exec(fileContent)) {
classNames.push(match[1]);
}
return classNames;
}
function extractClassDocs(fileContent, className) {
// Find the class block, allowing for generics, whitespace, and comments (multi-line)
const classRegex = new RegExp(`declare class ${className}(?:<(.+?)>)?.*?{(.*?)}\\s*(?:export|declare|$)`, 'ms');
const classMatch = fileContent.match(classRegex);
if (!classMatch) {
return {
error: `${className} class not found.`
};
}
const classBody = classMatch[2];
return extractMethodsFromClassBody(classBody, fileContent);
}
function extractMethodsFromClassBody(classBody, fileContent) {
// Find all methods with JSDoc comments
const methodRegex = /\/\*\*([\s\S]*?)\*\/\s*(\w+)\s*\(([^)]*)\):\s*([^;]+);?/g;
const docs = {};
let match;
while (match = methodRegex.exec(classBody)) {
const jsdoc = match[1];
const methodName = match[2];
const parameters = match[3];
const returnType = match[4];
// Parse JSDoc
const parsedJSDoc = parseJSDoc(jsdoc);
// Parse TypeScript parameters
const parameterStructure = parseTypeScriptParameters(parameters);
// Parse return type
const parsedReturnType = parseReturnType(returnType, fileContent);
docs[methodName] = _objectSpread(_objectSpread({}, parsedJSDoc), {}, {
fullSignature: `${methodName}(${parameters}): ${returnType}`,
parameterStructure,
returnType: parsedReturnType
});
}
return docs;
}
function parseJSDoc(jsdoc) {
// Extract description (first non-@ line)
const descMatch = jsdoc.match(/\*\s*([^@\n*]+)/);
const description = descMatch ? descMatch[1].trim() : '';
// Extract @param lines
const paramLines = [...jsdoc.matchAll(/@param\s+([^\n]+)/g)].map(m => m[1].trim());
// Extract @returns
const returnsMatch = jsdoc.match(/@returns?\s+([^\n]+)/);
const returns = returnsMatch ? returnsMatch[1].trim() : '';
// Extract @example (if present)
const exampleMatch = jsdoc.match(/@example\s+([\s\S]*?)(?=\n\s*\*@|\n\s*\*\/|$)/);
const example = exampleMatch ? exampleMatch[1].replace(/^\s*\*\s?/gm, '').trim() : '';
return {
description,
params: paramLines,
returns,
example
};
}
function parseTypeScriptParameters(parametersString) {
if (!parametersString.trim()) {
return {
parameters: {},
headers: {}
};
}
// Handle the common pattern: options: { parameters?: {...}, headers?: {...} }
const optionsMatch = parametersString.match(/options\s*:\s*{([^}]+)}/);
if (optionsMatch) {
return parseOptionsParameter(optionsMatch[1]);
}
// For other parameter patterns, return basic structure
return {
raw: parametersString,
parameters: {},
headers: {}
};
}
function parseOptionsParameter(optionsContent) {
const result = {
parameters: {},
headers: {}
};
// Extract parameters object
const parametersMatch = optionsContent.match(/parameters\?\s*:\s*{([^}]+)}/);
if (parametersMatch) {
const paramContent = parametersMatch[1];
const paramFields = paramContent.split(',').map(p => p.trim());
for (const field of paramFields) {
const fieldMatch = field.match(/(\w+)\??\s*:\s*(.+)/);
if (fieldMatch) {
result.parameters[fieldMatch[1]] = fieldMatch[2].trim();
}
}
}
// Extract headers object
const headersMatch = optionsContent.match(/headers\?\s*:\s*{([^}]+)}/);
if (headersMatch) {
const headerContent = headersMatch[1];
const headerFields = headerContent.split(',').map(h => h.trim());
for (const field of headerFields) {
const fieldMatch = field.match(/(\w+)\??\s*:\s*(.+)/);
if (fieldMatch) {
result.headers[fieldMatch[1]] = fieldMatch[2].trim();
}
}
}
return result;
}
function parseReturnType(returnTypeString, fileContent) {
let cleanType = returnTypeString.trim();
// Handle Promise types
const promiseMatch = cleanType.match(/Promise<(.+)>/);
if (promiseMatch) {
cleanType = promiseMatch[1];
}
const result = {
type: returnTypeString.trim(),
structure: {
properties: []
}
};
// Extract detailed structure for known Commerce SDK types
result.structure = extractReturnTypeStructure(cleanType, fileContent);
return result;
}
function extractReturnTypeStructure(typeName, fileContent) {
if (!typeName) return {
properties: []
};
// Try to find interface/type definition in the file content
const interfaceRegex = new RegExp(`interface\\s+${typeName}\\s*{([^}]*)}`, 'ms');
const typeRegex = new RegExp(`type\\s+${typeName}\\s*=\\s*{([^}]*)}`, 'ms');
const interfaceMatch = fileContent.match(interfaceRegex) || fileContent.match(typeRegex);
if (interfaceMatch) {
return parseInterfaceProperties(interfaceMatch[1]);
}
// fallback for type = ...; forms
const fallback = fileContent.match(new RegExp(`type\\s+${typeName}\\s*=\\s*([^;]+);`, 'm'));
if (fallback) {
return {
properties: [{
name: '(raw)',
type: fallback[1].trim()
}]
};
}
return {
properties: []
};
}
function parseInterfaceProperties(interfaceBody) {
if (!interfaceBody) {
return {
properties: []
};
}
const properties = [];
const lines = interfaceBody.split('\n');
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('//') || trimmed.startsWith('*')) continue;
const propMatch = trimmed.match(/(\w+)\??\s*:\s*([^;,]+)/);
if (propMatch) {
properties.push({
name: propMatch[1],
type: propMatch[2].trim()
});
}
}
return {
properties
};
}
function extractNamedParameterTypes(parametersString) {
// Match param: <name>: <TypeName>
// Only matches top-level non-primitive type references
const regex = /(?:[\w\d_]+)\s*:\s*([A-Z][A-Za-z0-9_]+)/g;
const types = new Set();
let match;
while (match = regex.exec(parametersString)) {
// Exclude primitives
if (!['string', 'number', 'boolean', 'void', 'any', 'unknown', 'object'].includes(match[1])) {
types.add(match[1]);
}
}
return Array.from(types);
}
function getTypeDefinitionMarkdown(typeName, fileContent) {
// Find interface or type definition block
const interfaceRegex = new RegExp(`interface\\s+${typeName}\\s*{([\\s\\S]*?)}\\s`, 'm');
const typeRegex = new RegExp(`type\\s+${typeName}\\s*=\\s*{([\\s\\S]*?)}\\s`, 'm');
let match = fileContent.match(interfaceRegex) || fileContent.match(typeRegex);
if (!match) {
// fallback for type = ...; forms
const fallback = fileContent.match(new RegExp(`type\\s+${typeName}\\s*=\\s*([^;]+);`, 'm'));
if (fallback) return `\n**Type ${typeName}:** \n\n\`${fallback[1].trim()}\``;
return `\n_No type definition found for ${typeName}_\n`;
}
const block = match[1].trim();
// Format as markdown properties
const lines = block.split('\n').filter(Boolean).map(l => l.trim().replace(/;?$/, ''));
let out = `\n**Parameters for \`${typeName}\`**\n\n| Name | Type |\n|---|---|\n`;
for (const line of lines) {
// Parse as: name?: type
const propMatch = line.match(/^(\w+)\??\s*:\s*([^,{}]+)/);
if (propMatch) {
out += `| \`${propMatch[1]}\` | \`${propMatch[2].trim()}\` |\n`;
}
}
return out;
}
function printTypeWithNestedReferences(typeName, fileContent, seenTypes = new Set()) {
if (seenTypes.has(typeName)) return '';
seenTypes.add(typeName);
// Find interface or type definition block
const interfaceRegex = new RegExp(`interface\\s+${typeName}\\s*{([\\s\\S]*?)}\\s`, 'm');
const typeRegex = new RegExp(`type\\s+${typeName}\\s*=\\s*{([\\s\\S]*?)}\\s`, 'm');
let match = fileContent.match(interfaceRegex) || fileContent.match(typeRegex);
if (!match) {
// fallback for type = ...; forms
const fallback = fileContent.match(new RegExp(`type\\s+${typeName}\\s*=\\s*([^;]+);`, 'm'));
if (fallback) return `\n**Type ${typeName}:** \n\n\`${fallback[1].trim()}\``;
return `\n_No type definition found for ${typeName}_\n`;
}
const block = match[1].trim();
// Format as markdown property table
const lines = block.split('\n').filter(Boolean).map(l => l.trim().replace(/;?$/, ''));
let out = `\n**Parameters for \`${typeName}\`**\n\n| Name | Type |\n|---|---|\n`;
const nestedTypes = [];
for (const line of lines) {
// Parse as: name?: type
const propMatch = line.match(/^(\w+)\??\s*:\s*([^,{}]+)/);
if (propMatch) {
out += `| \`${propMatch[1]}\` | \`${propMatch[2].trim()}\` |\n`;
// If this property is a capitalized type reference, queue it for expansion if not seen yet
const subType = propMatch[2].trim();
if (/^[A-Z][A-Za-z0-9_]+$/.test(subType) && !seenTypes.has(subType) && !['String', 'Number', 'Boolean', 'Object', 'Date'].includes(subType)) {
nestedTypes.push(subType);
}
}
}
// Add 1-level nested types inline below this table
for (const nested of nestedTypes) {
out += printTypeWithNestedReferences(nested, fileContent, seenTypes);
}
return out;
}
class ExploreCommerceAPITool {
constructor() {
this.name = 'pwakit_explore_scapi_shop_api';
this.description = 'Explore and document any commerce-sdk-isomorphic class API endpoints, parameters, and usage examples. Reads from the commerce-sdk-isomorphic type definitions.';
this.inputSchema = {
prompt: _zod.z.string().describe('Natural language question or method query (e.g., "How do I get a product?", "ShopperProducts.getProduct", "search products")')
};
this.handler = this.handler.bind(this);
}
/**
* Returns a markdown snippet for just the most relevant class(es)/method(s) for the user's query, plus referenced parameter types.
*/
getRelevantAPIContext(fileContent, userQuery) {
const classNames = extractAllClassNames(fileContent);
const normalizedQuery = userQuery.toLowerCase();
let context = '# Commerce SDK API Reference\n\n';
// Try to detect explicit class.method: e.g., ShopperProducts.getProduct
let matchClass = null;
let matchMethod = null;
const dotMatch = userQuery.match(/(\w+)\.(\w+)/);
if (dotMatch) {
matchClass = dotMatch[1];
matchMethod = dotMatch[2];
} else {
// Try keyword match: see if query includes a class name
matchClass = classNames.find(cn => normalizedQuery.includes(cn.toLowerCase())) || null;
}
if (matchClass) {
const classDocs = extractClassDocs(fileContent, matchClass);
if (classDocs.error) return `Class ${matchClass} not found.`;
context += `## ${matchClass}\n`;
if (matchMethod && classDocs[matchMethod]) {
const method = classDocs[matchMethod];
context += `- **${matchMethod}**: ${method.description || ''}\n`;
context += ` - Full signature: ${method.fullSignature}\n`;
// Look for referenced parameter types and print those
const refTypes = extractNamedParameterTypes(method.fullSignature);
for (const refType of refTypes) {
context += printTypeWithNestedReferences(refType, fileContent) + '\n';
}
if (method.parameterStructure && method.parameterStructure.parameters && Object.keys(method.parameterStructure.parameters).length > 0) {
context += ` - Parameters: ${JSON.stringify(method.parameterStructure.parameters)}\n`;
}
if (method.returnType && method.returnType.type) {
context += ` - Returns: ${method.returnType.type}\n`;
}
if (method.example) {
context += ` - Example: ${method.example}\n`;
}
} else {
// No explicit method, show all methods in class
for (const key of Object.keys(classDocs)) {
const method = classDocs[key];
context += `- **${key}**: ${method.description || ''}\n`;
}
}
} else {
// Ambiguous/keyword: show candidate classes with a sample method for each
context += '## Top classes in Commerce SDK\n';
for (const cn of classNames.slice(0, 3)) {
const classDocs = extractClassDocs(fileContent, cn);
if (!classDocs.error) {
context += `### ${cn}\n`;
const keys = Object.keys(classDocs).slice(0, 1);
for (const key of keys) {
const method = classDocs[key];
context += `- **${key}**: ${method.description || ''}\n`;
}
}
}
}
return context;
}
handler(args) {
var _this = this;
return _asyncToGenerator(function* () {
// Auto-detect Commerce SDK path
let describePath = (0, _index.autoDetectCommerceSDKTypesPath)();
if (!describePath) {
const nodeModulesPath = (0, _index.autoDetectNodeModulesPath)();
if (nodeModulesPath) {
describePath = getCommerceSDKTypesFromNodeModulesPath(nodeModulesPath);
}
}
if (!describePath) {
return {
role: 'system',
content: [{
type: 'text',
text: 'Could not auto-detect the commerce-sdk-isomorphic type definitions. Please ensure you have commerce-sdk-isomorphic installed in your node_modules.'
}]
};
}
// Read Commerce SDK TypeScript definitions
let fileContent;
try {
fileContent = yield _promises.default.readFile(describePath, 'utf-8');
} catch (e) {
return {
role: 'system',
content: [{
type: 'text',
text: `Could not read Commerce SDK type definitions at ${describePath}: ${e.message}`
}]
};
}
// Only build context for relevant API area
const apiContext = _this.getRelevantAPIContext(fileContent, args.prompt);
return {
role: 'system',
content: [{
type: 'text',
text: `${apiContext}\n---\n` + `**User Query:** "${args.prompt}"\n` + 'Please analyze this query and provide:\n' + '1. The most relevant Commerce SDK API method(s) for this query\n' + '2. Complete documentation including:\n' + ' - Method description and purpose\n' + ' - Full method signature\n' + " - Detailed parameter structure (especially the 'options' object)\n" + ' - Return type structure with key properties\n' + ' - Usage examples if available\n' + 'If the query is ambiguous, suggest 2-3 relevant options with brief explanations.\n' + 'Focus on being concise, comprehensive, and practical for developers using the Commerce SDK.'
}]
};
})();
}
}
var _default = exports.default = ExploreCommerceAPITool;