UNPKG

@salesforce/pwa-kit-mcp

Version:

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

493 lines (462 loc) 22.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.EmptyJsonSchema = void 0; exports.autoDetectCommerceSDKTypesPath = autoDetectCommerceSDKTypesPath; exports.autoDetectNodeModulesPath = autoDetectNodeModulesPath; exports.callCustomApiDxEndpoint = callCustomApiDxEndpoint; exports.detectWorkspacePaths = detectWorkspacePaths; exports.findDwJsonPath = void 0; exports.generateComponentImportStatement = generateComponentImportStatement; exports.getCreateAppCommand = void 0; exports.getOAuthToken = getOAuthToken; exports.isLocalSharedUIComponent = exports.isLocalComponent = exports.isBaseComponent = void 0; exports.isMonoRepo = isMonoRepo; exports.isSharedUIBaseComponent = void 0; exports.loadConfig = loadConfig; exports.logMCPMessage = logMCPMessage; exports.runCommand = void 0; exports.toKebabCase = toKebabCase; exports.toPascalCase = void 0; var _fs = _interopRequireDefault(require("fs")); var _promises = _interopRequireDefault(require("fs/promises")); var _path = _interopRequireDefault(require("path")); var _crossSpawn = require("cross-spawn"); var _zodToJsonSchema = require("zod-to-json-schema"); var _zod = require("zod"); function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; } 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); } 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 */ // CONSTANTS const CREATE_APP_VERSION = 'latest'; // Private schema used to generate the JSON schema const emptySchema = _zod.z.object({}).strict(); const EmptyJsonSchema = exports.EmptyJsonSchema = (0, _zodToJsonSchema.zodToJsonSchema)(emptySchema); /** * Converts a string to PascalCase (e.g., product-card -> ProductCard) */ const toPascalCase = str => str.replace(/(^\w|[-_\s]\w)/g, match => match.replace(/[-_\s]/, '').toUpperCase()); /** * Runs a shell command and captures its stdout/stderr as a string. * * @param {string} command - The executable to run (e.g. "node", "npx", "ls"). * @param {string[]} args - Arguments to pass to the command. * @param {Object} [options] - Optional spawn options (e.g. cwd). * @returns {Promise<string>} - Resolves with combined stdout and stderr. */ exports.toPascalCase = toPascalCase; const runCommand = exports.runCommand = /*#__PURE__*/function () { var _ref = _asyncToGenerator(function* (command, args = [], options = {}) { return new Promise((resolve, reject) => { const child = (0, _crossSpawn.spawn)(command, args, _objectSpread(_objectSpread({}, options), {}, { stdio: ['ignore', 'pipe', 'pipe'], // ignore stdin, pipe out/err shell: false // be explicit — set to true if you want shell features })); let output = ''; child.stdout.on('data', chunk => { output += chunk.toString(); }); child.stderr.on('data', chunk => { output += chunk.toString(); // combine stderr into output }); child.on('error', err => { reject(err); }); child.on('close', code => { if (code === 0) { resolve(output); } else { const error = new Error(`Command failed with exit code ${code}`); error.output = output; error.code = code; reject(error); } }); }); }); return function runCommand(_x) { return _ref.apply(this, arguments); }; }(); /** * Checks if the project is a monorepo by verifying the existence of lerna.json in the root directory. * * @returns {boolean} True if lerna.json exists in the current workspace, false otherwise. */ function isMonoRepo() { const lernaPath = _path.default.resolve(...(process.env.WORKSPACE_FOLDER_PATHS ? [process.env.WORKSPACE_FOLDER_PATHS] : []), 'lerna.json'); return _fs.default.existsSync(lernaPath); } /** * Check if the component is the base component under node_modules/@salesforce/retail-react-app/app/components * * @param {string} componentName - The name of the component to check. * @param {string} nodeModulesPath - The absolute path to the node_modules directory. * @returns {boolean} True if the component is the base component, false otherwise. */ const isBaseComponent = (componentName, nodeModulesPath) => { const baseComponentPath = _path.default.join(nodeModulesPath, '@salesforce/retail-react-app/app/components', componentName); return _fs.default.existsSync(baseComponentPath); }; /** * Check if the component is the shared UI base component under node_modules/@salesforce/retail-react-app/app/components/shared/ui * * @param {string} componentName - The name of the component to check. * @param {string} nodeModulesPath - The absolute path to the node_modules directory. * @returns {boolean} True if the component is the shared UI base component, false otherwise. */ exports.isBaseComponent = isBaseComponent; const isSharedUIBaseComponent = (componentName, nodeModulesPath) => { const baseComponentPath = _path.default.join(nodeModulesPath, '@salesforce/retail-react-app/app/components/shared/ui', componentName); return _fs.default.existsSync(baseComponentPath); }; /** * Check if the component is the local component under components folder * * @param {string} componentName - The name of the component to check. * @param {string} componentsPath - The absolute path to the components directory. * @returns {boolean} True if the component is the local component, false otherwise. */ exports.isSharedUIBaseComponent = isSharedUIBaseComponent; const isLocalComponent = (componentName, componentsPath) => { const localComponentPath = _path.default.join(componentsPath, componentName); return _fs.default.existsSync(localComponentPath); }; /** * Check if the component is a local shared UI component under components/shared/ui folder * * @param {string} componentName - The name of the component to check. * @param {string} componentsPath - The absolute path to the components directory. * @returns {boolean} True if the component is a local shared UI component, false otherwise. */ exports.isLocalComponent = isLocalComponent; const isLocalSharedUIComponent = (componentName, componentsPath) => { const localSharedUIComponentPath = _path.default.join(componentsPath, 'shared', 'ui', componentName); return _fs.default.existsSync(localSharedUIComponentPath); }; /** * Returns the command or path to use for creating a new PWA Kit app. * * If the project is a monorepo (detected by the presence of lerna.json), * it returns the absolute path to the local create-mobify-app.js script. * Otherwise, it returns the npm package name with a specific version. * * @returns {string} The command or path to use for app creation. */ exports.isLocalSharedUIComponent = isLocalSharedUIComponent; const getCreateAppCommand = () => { return isMonoRepo() ? _path.default.resolve(`${process.env.WORKSPACE_FOLDER_PATHS}/packages/pwa-kit-create-app/scripts/create-mobify-app.js`) : `@salesforce/pwa-kit-create-app@${CREATE_APP_VERSION}`; }; /** * Converts a string to kebab-case (e.g., ProductCard -> product-card) */ exports.getCreateAppCommand = getCreateAppCommand; function toKebabCase(str) { return str.replace(/([a-z])([A-Z])/g, '$1-$2').replace(/\s+/g, '-').toLowerCase(); } /** * Logs a message to the mcp-debug.log file in the current directory. * @param {string} message - The message to log. */ function logMCPMessage(_x2) { return _logMCPMessage.apply(this, arguments); } function _logMCPMessage() { _logMCPMessage = _asyncToGenerator(function* (message) { if (process.env.DEBUG) { // Check if DEBUG mode is enabled const logFilePath = _path.default.join(__dirname, 'mcp-debug.log'); const timestamp = new Date().toLocaleString('en-US', { timeZone: 'GMT' }); const logMessage = `[${timestamp}] ${message}\n`; try { // Ensure the log file exists, create it if it doesn't yield _promises.default.access(logFilePath).catch(/*#__PURE__*/_asyncToGenerator(function* () { yield _promises.default.writeFile(logFilePath, '', 'utf8'); })); yield _promises.default.appendFile(logFilePath, logMessage, 'utf8'); } catch (error) { console.error(`Failed to write to log file: ${error.message}`); } } }); return _logMCPMessage.apply(this, arguments); } function detectWorkspacePaths() { return _detectWorkspacePaths.apply(this, arguments); } /** * Returns the import statement for a component * @param {string} componentName - The name of the component to import. * @param {string} componentDir - The directory of the component to import. * @param {boolean} isLocal - Whether the component is a local component. * @param {boolean} isBase - Whether the component is a base component. * @param {Object} absolutePaths - Object containing absolute paths for components and pages. * @param {string} absolutePaths.componentsPath - The absolute path to the components directory. * @param {string} absolutePaths.pagesPath - The absolute path to the pages directory. * @param {boolean} hasOverridesDir - Whether ccExtensibility.overridesDir is set in package.json. * @returns {string} The import statement for the component. */ function _detectWorkspacePaths() { _detectWorkspacePaths = _asyncToGenerator(function* () { let appPath = process.env.PWA_STOREFRONT_APP_PATH; if (appPath) { try { yield _promises.default.access(appPath); } catch (error) { // no env path variable appPath = null; } } // Prompt user if detection failed if (!appPath) { throw new Error("Could not detect PWA Kit project directory. Please either:\n1. Navigate to your PWA Kit project directory, or\n2. Set PWA_STOREFRONT_APP_PATH environment variable to your project's app directory path."); } // Build paths relative to the detected app directory const pagesPath = _path.default.join(appPath, 'pages'); const componentsPath = _path.default.join(appPath, 'components'); const routesPath = _path.default.join(appPath, 'routes.jsx'); const nodeModulesPath = _path.default.join(appPath, '../../', 'node_modules'); const hasOverridesDir = _fs.default.existsSync(_path.default.join(appPath, '../../', 'overrides')); // Verify essential directories exist if (!_fs.default.existsSync(pagesPath)) { throw new Error(`Pages directory not found at: ${pagesPath}`); } if (!_fs.default.existsSync(componentsPath)) { throw new Error(`Components directory not found at: ${componentsPath}`); } if (!_fs.default.existsSync(routesPath)) { throw new Error(`Routes file not found at: ${routesPath}`); } return { pagesPath, componentsPath, routesPath, nodeModulesPath, hasOverridesDir }; }); return _detectWorkspacePaths.apply(this, arguments); } function generateComponentImportStatement(componentName, componentDir, isLocal, isBase, absolutePaths, hasOverridesDir) { const relativePath = _path.default.relative(_path.default.join(absolutePaths.pagesPath, 'dummy'), // dummy file to get parent directory _path.default.join(absolutePaths.componentsPath, componentDir)); if (!hasOverridesDir && isLocal || isBase) { return `import ${componentName} from '@salesforce/retail-react-app/app/components/${componentDir}'`; } // Use local relative path for other cases // Normalize path separators to forward slashes for ES6 imports const normalizedPath = relativePath.replace(/\\/g, '/'); return `import ${componentName} from '${normalizedPath}'`; } /** * Finds the dw.json configuration file in the following priority order: * 1. Global DW_JSON_PATH (if set) * 2. PWA_STOREFRONT_APP_PATH/dw.json (if PWA_STOREFRONT_APP_PATH exists) * 3. PWA_STOREFRONT_APP_PATH/../dw.json (parent directory) * 4. PWA_STOREFRONT_APP_PATH/../../dw.json (grandparent directory) * 5. Current working directory/dw.json * * @returns {string|null} The path to the dw.json file, or null if not found */ const findDwJsonPath = () => { // Check global path const configFromGlobalPath = global.DW_JSON_PATH; if (configFromGlobalPath && _fs.default.existsSync(configFromGlobalPath)) { return configFromGlobalPath; } // Check PWA_STOREFRONT_APP_PATH and its parent directories if (process.env.PWA_STOREFRONT_APP_PATH) { const storefrontPath = process.env.PWA_STOREFRONT_APP_PATH; // Check PWA_STOREFRONT_APP_PATH/dw.json const configFromStorefrontPath = _path.default.join(storefrontPath, 'dw.json'); if (_fs.default.existsSync(configFromStorefrontPath)) { return configFromStorefrontPath; } // Check PWA_STOREFRONT_APP_PATH/../dw.json const configFromStorefrontParentPath = _path.default.join(storefrontPath, '..', 'dw.json'); if (_fs.default.existsSync(configFromStorefrontParentPath)) { return configFromStorefrontParentPath; } // Check PWA_STOREFRONT_APP_PATH/../../dw.json const configFromStorefrontGrandparentPath = _path.default.join(storefrontPath, '..', '..', 'dw.json'); if (_fs.default.existsSync(configFromStorefrontGrandparentPath)) { return configFromStorefrontGrandparentPath; } } // Check current working directory const configFromCwdPath = _path.default.join(process.cwd(), 'dw.json'); if (_fs.default.existsSync(configFromCwdPath)) { return configFromCwdPath; } return null; }; /** * Loads configuration from environment variables or dw.json file if it exists * Priority: Environment variables > dw.json file * * @returns {Object} Configuration object with SFCC settings */ exports.findDwJsonPath = findDwJsonPath; function loadConfig() { let dwConfig = {}; // Attempt to load dw.json try { const configPath = findDwJsonPath(); if (configPath) { const fileContent = _fs.default.readFileSync(configPath, 'utf-8'); dwConfig = JSON.parse(fileContent); } } catch (error) { logMCPMessage(`Failed to parse dw.json: ${error.message}`); } // Get hostname first to derive a fallback organizationId const hostname = process.env.SFCC_HOSTNAME || dwConfig['hostname']; // Extract instance ID from hostname pattern: https://zzrf-001.dx.commercecloud.salesforce.com const hostnameMatch = hostname === null || hostname === void 0 ? void 0 : hostname.match(/https?:\/\/([a-z0-9-]+)\.dx\.commercecloud\.salesforce\.com/); const derivedInstanceId = hostnameMatch ? hostnameMatch[1].replace(/-/g, '_') : null; const derivedOrganizationId = derivedInstanceId ? `f_ecom_${derivedInstanceId}` : null; // Merge with environment variables (environment variables take precedence if both exist) return { hostname: hostname, instanceId: process.env.SFCC_INSTANCE_ID || dwConfig['instance-id'] || derivedInstanceId, organizationId: process.env.SFCC_ORG_ID || dwConfig['org-id'] || derivedOrganizationId, clientId: process.env.SFCC_CLIENT_ID || dwConfig['client-id'], clientSecret: process.env.SFCC_CLIENT_SECRET || dwConfig['client-secret'], shortCode: process.env.SFCC_SHORT_CODE || dwConfig['short-code'] }; } /** * Obtains OAuth access token from Salesforce Commerce Cloud * @param {string} clientId - The OAuth client ID * @param {string} clientSecret - The OAuth client secret * @param {string} oauthScope - The OAuth scope for the token * @returns {Promise<Response>} The fetch response containing the OAuth token */ function getOAuthToken(_x3, _x4, _x5) { return _getOAuthToken.apply(this, arguments); } /** * Calls the custom API DX endpoint * @param {string} accessToken - The OAuth access token for authentication * @param {string} customApiHost - The hostname for the custom API DX endpoint * @param {string} organizationId - The organization ID for the API request * @returns {Promise<Response>} The fetch response containing custom API data */ function _getOAuthToken() { _getOAuthToken = _asyncToGenerator(function* (clientId, clientSecret, oauthScope) { const accountManagerHost = process.env.SFCC_LOGIN_URL || 'account.demandware.com'; const oauthTokenUrl = `https://${accountManagerHost}/dwsso/oauth2/access_token`; const response = yield fetch(oauthTokenUrl, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', Authorization: `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString('base64')}` }, body: `grant_type=client_credentials&scope=${encodeURIComponent(oauthScope)}` }); return response; }); return _getOAuthToken.apply(this, arguments); } function callCustomApiDxEndpoint(_x6, _x7, _x8) { return _callCustomApiDxEndpoint.apply(this, arguments); } /** * Auto-detects the node_modules directory path * @param {string} [startPath] - Optional starting path for detection * @returns {string|null} The absolute path to node_modules or null if not found */ function _callCustomApiDxEndpoint() { _callCustomApiDxEndpoint = _asyncToGenerator(function* (accessToken, customApiHost, organizationId) { const customApiBase = `https://${customApiHost}/dx/custom-apis/v1/organizations/${organizationId}/endpoints`; const response = yield fetch(customApiBase, { method: 'GET', headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json' } }); return response; }); return _callCustomApiDxEndpoint.apply(this, arguments); } function autoDetectNodeModulesPath(startPath = process.cwd()) { // Check for explicit environment variable (and its parents) const storefrontAppPath = process.env.PWA_STOREFRONT_APP_PATH; if (storefrontAppPath) { let envPath = _path.default.resolve(storefrontAppPath); while (envPath !== _path.default.dirname(envPath)) { const nodeModulesPath = _path.default.join(envPath, 'node_modules'); if (_fs.default.existsSync(nodeModulesPath)) { return nodeModulesPath; } envPath = _path.default.dirname(envPath); } } // Check for node_modules in cwd and its parents let currentPath = _path.default.resolve(startPath); while (currentPath !== _path.default.dirname(currentPath)) { const nodeModulesPath = _path.default.join(currentPath, 'node_modules'); if (_fs.default.existsSync(nodeModulesPath)) { return nodeModulesPath; } currentPath = _path.default.dirname(currentPath); } // Check for node_modules in common PWA Kit app subfolders (fallback) const resolvedStartPath = _path.default.resolve(startPath); const appSpecificPaths = [_path.default.join(resolvedStartPath, 'retail-react-app/node_modules'), _path.default.join(resolvedStartPath, 'app/node_modules'), _path.default.join(resolvedStartPath, 'node_modules')]; for (const appPath of appSpecificPaths) { if (_fs.default.existsSync(appPath)) { return appPath; } } return null; } /** * Auto-detects the commerce-sdk-isomorphic type definitions path * @param {string} [nodeModulesPath] - Optional node_modules path * @returns {string|null} The absolute path to index.cjs.d.ts or null if not found */ function autoDetectCommerceSDKTypesPath(nodeModulesPath = null) { // Try the provided node_modules path first if (nodeModulesPath) { const result = checkCommerceSDKInNodeModules(nodeModulesPath); if (result) return result; } // Try auto-detected node_modules const nmPath = autoDetectNodeModulesPath(); if (nmPath) { const result = checkCommerceSDKInNodeModules(nmPath); if (result) return result; } return null; } /** * Helper function to check for commerce-sdk-isomorphic in a specific node_modules directory * @param {string} nodeModulesPath - Path to node_modules directory * @returns {string|null} Path to type definitions or null if not found */ function checkCommerceSDKInNodeModules(nodeModulesPath) { const possiblePaths = [_path.default.join(nodeModulesPath, 'commerce-sdk-isomorphic/lib/index.cjs.d.ts'), _path.default.join(nodeModulesPath, '@salesforce/commerce-sdk-isomorphic/lib/index.cjs.d.ts'), _path.default.join(nodeModulesPath, 'commerce-sdk-isomorphic/dist/index.cjs.d.ts'), _path.default.join(nodeModulesPath, '@salesforce/commerce-sdk-isomorphic/dist/index.cjs.d.ts'), _path.default.join(nodeModulesPath, 'commerce-sdk-isomorphic/index.cjs.d.ts'), _path.default.join(nodeModulesPath, '@salesforce/commerce-sdk-isomorphic/index.cjs.d.ts')]; for (const possiblePath of possiblePaths) { if (_fs.default.existsSync(possiblePath)) { return possiblePath; } } return null; }