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