sfcc-dev-mcp
Version:
MCP server for Salesforce B2C Commerce Cloud development assistance including logs, debugging, and development tools
384 lines (382 loc) • 15.7 kB
JavaScript
/**
* SFCC Cartridge Generation Client
*
* This client handles the generation of SFCC cartridge directory structures
* with all necessary files and configurations, replacing the outdated sgmf-scripts
* with a modern, integrated approach.
*/
import { Logger } from '../utils/logger.js';
export class CartridgeGenerationClient {
logger;
templates;
fileSystem;
pathService;
constructor(fileSystem, pathService) {
this.logger = Logger.getChildLogger('CartridgeGenerationClient');
this.fileSystem = fileSystem;
this.pathService = pathService;
this.templates = this.initializeTemplates();
}
/**
* Normalize the target path by removing /cartridges or /cartridges/ from the end
* The cartridge creation always happens from the root folder
*/
normalizeTargetPath(targetPath) {
// Remove trailing slashes first
let normalized = targetPath.replace(/\/+$/, '');
// Remove /cartridges from the end if present
if (normalized.endsWith('/cartridges')) {
normalized = normalized.slice(0, -11); // Remove '/cartridges' (11 characters)
}
this.logger.debug(`Normalized target path from '${targetPath}' to '${normalized}'`);
return normalized;
}
/**
* Generate a complete cartridge structure
*/
async generateCartridgeStructure(options) {
const { cartridgeName, targetPath, fullProjectSetup = true } = options;
const createdFiles = [];
const createdDirectories = [];
const skippedFiles = [];
try {
this.logger.info(`Starting cartridge generation for: ${cartridgeName}`);
// Determine the working directory and normalize path
let workingDir = targetPath ?? process.cwd();
workingDir = this.normalizeTargetPath(workingDir);
if (fullProjectSetup) {
// Full project setup - create everything directly in the working directory
this.logger.info(`Creating full project setup directly in: ${workingDir}`);
// Ensure the working directory exists
await this.ensureDirectory(workingDir);
if (!createdDirectories.includes(workingDir)) {
createdDirectories.push(workingDir);
}
// Create root files directly in working directory
await this.createRootFiles(workingDir, cartridgeName, createdFiles, skippedFiles);
// Create cartridge structure directly in working directory
await this.createCartridgeStructure(workingDir, cartridgeName, createdFiles, createdDirectories, skippedFiles);
return {
success: true,
message: `Successfully created full project setup for cartridge '${cartridgeName}' in '${workingDir}'`,
createdFiles,
createdDirectories,
skippedFiles,
};
}
else {
// Cartridge-only setup - add to existing project
const cartridgesDir = this.pathService.join(workingDir, 'cartridges');
// Ensure cartridges directory exists
await this.ensureDirectory(cartridgesDir);
if (!createdDirectories.includes(cartridgesDir)) {
createdDirectories.push(cartridgesDir);
}
// Create cartridge structure
await this.createCartridgeStructure(workingDir, cartridgeName, createdFiles, createdDirectories, skippedFiles);
return {
success: true,
message: `Successfully created cartridge '${cartridgeName}' in existing project at '${workingDir}'`,
createdFiles,
createdDirectories,
skippedFiles,
};
}
}
catch (error) {
this.logger.error('Error generating cartridge structure:', error);
return {
success: false,
message: `Failed to generate cartridge structure: ${error instanceof Error ? error.message : 'Unknown error'}`,
createdFiles,
createdDirectories,
skippedFiles,
};
}
}
/**
* Create root project files (package.json, webpack, etc.)
*/
async createRootFiles(projectDir, cartridgeName, createdFiles, skippedFiles) {
// Create package.json
const packageJsonPath = this.pathService.join(projectDir, 'package.json');
await this.safeWriteFile(packageJsonPath, JSON.stringify(this.templates.packageJson(cartridgeName), null, 2), createdFiles, skippedFiles);
// Create dw.json
const dwJsonPath = this.pathService.join(projectDir, 'dw.json');
await this.safeWriteFile(dwJsonPath, JSON.stringify(this.templates.dwJson(), null, 2), createdFiles, skippedFiles);
// Create webpack.config.js
const webpackPath = this.pathService.join(projectDir, 'webpack.config.js');
await this.safeWriteFile(webpackPath, this.templates.webpackConfig(cartridgeName), createdFiles, skippedFiles);
// Create .eslintrc.json
const eslintrcPath = this.pathService.join(projectDir, '.eslintrc.json');
await this.safeWriteFile(eslintrcPath, JSON.stringify(this.templates.eslintrc(), null, 2), createdFiles, skippedFiles);
// Create .stylelintrc.json
const stylelintrcPath = this.pathService.join(projectDir, '.stylelintrc.json');
await this.safeWriteFile(stylelintrcPath, JSON.stringify(this.templates.stylelintrc(), null, 2), createdFiles, skippedFiles);
// Create .eslintignore
const eslintignorePath = this.pathService.join(projectDir, '.eslintignore');
await this.safeWriteFile(eslintignorePath, this.templates.eslintignore(), createdFiles, skippedFiles);
// Create .gitignore
const gitignorePath = this.pathService.join(projectDir, '.gitignore');
await this.safeWriteFile(gitignorePath, this.templates.gitignore(), createdFiles, skippedFiles);
}
/**
* Create the cartridge directory structure
*/
async createCartridgeStructure(baseDir, cartridgeName, createdFiles, createdDirectories, skippedFiles) {
// Create cartridges directory
const cartridgesDir = this.pathService.join(baseDir, 'cartridges');
await this.ensureDirectory(cartridgesDir);
createdDirectories.push(cartridgesDir);
// Create specific cartridge directory
const cartridgeDir = this.pathService.join(cartridgesDir, cartridgeName);
await this.ensureDirectory(cartridgeDir);
createdDirectories.push(cartridgeDir);
// Create .project file
const projectPath = this.pathService.join(cartridgeDir, '.project');
await this.safeWriteFile(projectPath, this.templates.dotProject(cartridgeName), createdFiles, skippedFiles);
// Create cartridge subdirectory
const cartridgeSubDir = this.pathService.join(cartridgeDir, 'cartridge');
await this.ensureDirectory(cartridgeSubDir);
createdDirectories.push(cartridgeSubDir);
// Create cartridge properties file
const propertiesPath = this.pathService.join(cartridgeSubDir, `${cartridgeName}.properties`);
await this.safeWriteFile(propertiesPath, this.templates.projectProperties(cartridgeName), createdFiles, skippedFiles);
// Create directory structure
const directories = [
'controllers',
'models',
'templates',
'templates/default',
'templates/resources',
'client',
'client/default',
'client/default/js',
'client/default/scss',
];
for (const dir of directories) {
const fullPath = this.pathService.join(cartridgeSubDir, dir);
await this.ensureDirectory(fullPath);
createdDirectories.push(fullPath);
}
}
/**
* Ensure a directory exists, create if it doesn't
*/
async ensureDirectory(dirPath) {
try {
await this.fileSystem.access(dirPath);
}
catch {
await this.fileSystem.mkdir(dirPath, { recursive: true });
this.logger.info(`Created directory: ${dirPath}`);
}
}
/**
* Safely write a file, skipping if it already exists
*/
async safeWriteFile(filePath, content, createdFiles, skippedFiles) {
try {
await this.fileSystem.access(filePath);
// File exists, skip it
skippedFiles.push(filePath);
this.logger.info(`Skipped existing file: ${filePath}`);
}
catch {
// File doesn't exist, create it
await this.fileSystem.writeFile(filePath, content);
createdFiles.push(filePath);
this.logger.info(`Created file: ${filePath}`);
}
}
/**
* Initialize all file templates
*/
initializeTemplates() {
return {
packageJson: (cartridgeName) => ({
name: cartridgeName,
version: '0.0.1',
description: 'New overlay cartridge',
main: 'index.js',
scripts: {
'lint': 'npm run lint:css && npm run lint:js',
'lint:css': 'sgmf-scripts --lint css',
'lint:js': 'sgmf-scripts --lint js',
'lint:fix': 'eslint cartridges --fix',
upload: 'sgmf-scripts --upload -- ',
uploadCartridge: `sgmf-scripts --uploadCartridge ${cartridgeName}`,
'compile:js': 'sgmf-scripts --compile js',
'compile:scss': 'sgmf-scripts --compile css',
},
devDependencies: {
autoprefixer: '^10.4.14',
bestzip: '^2.2.1',
'css-loader': '^6.0.0',
'css-minimizer-webpack-plugin': '^5.0.1',
eslint: '^8.56.0',
'eslint-config-airbnb-base': '^15.0.0',
'eslint-config-prettier': '^9.1.0',
'eslint-plugin-import': '^2.29.0',
'mini-css-extract-plugin': '^2.7.6',
'postcss-loader': '^7.0.0',
sass: '^1.69.7',
'sass-loader': '^13.3.2',
'sgmf-scripts': '^3.0.0',
shx: '^0.3.4',
stylelint: '^15.4.0',
'stylelint-config-standard-scss': '^11.0.0',
'webpack-remove-empty-scripts': '^1.0.4',
},
browserslist: [
'last 2 versions',
'ie >= 10',
],
}),
dwJson: () => ({
hostname: '',
username: '',
password: '',
'code-version': '',
}),
webpackConfig: (cartridgeName) => `'use strict';
var path = require('path');
var MiniCssExtractPlugin = require('mini-css-extract-plugin');
var CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
var sgmfScripts = require('sgmf-scripts');
var RemoveEmptyScriptsPlugin = require('webpack-remove-empty-scripts');
module.exports = [{
mode: 'development',
name: 'js',
entry: sgmfScripts.createJsPath(),
output: {
path: path.resolve('./cartridges/${cartridgeName}/cartridge/static'),
filename: '[name].js'
}
}, {
mode: 'none',
name: 'scss',
entry: sgmfScripts.createScssPath(),
output: {
path: path.resolve('./cartridges/${cartridgeName}/cartridge/static')
},
module: {
rules: [{
test: /\\.scss$/,
use: [{
loader: MiniCssExtractPlugin.loader,
options: {
esModule: false
}
},
{
loader: 'css-loader',
options: {
url: false
}
}, {
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: [require('autoprefixer')]
}
}
}, {
loader: 'sass-loader',
options: {
implementation: require('sass'),
sassOptions: {
includePaths: [
path.resolve(path.resolve(process.cwd(), '../storefront-reference-architecture/node_modules/')),
path.resolve(process.cwd(), '../storefront-reference-architecture/node_modules/flag-icons/sass')
]
}
}
}]
}]
},
plugins: [
new RemoveEmptyScriptsPlugin(),
new MiniCssExtractPlugin({
filename: '[name].css',
chunkFilename: '[name].css'
})
],
optimization: {
minimizer: ['...', new CssMinimizerPlugin()]
}
}];`,
dotProject: (cartridgeName) => `<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>${cartridgeName}</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>com.demandware.studio.core.beehiveElementBuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>com.demandware.studio.core.beehiveNature</nature>
</natures>
</projectDescription>`,
projectProperties: (cartridgeName) => `## cartridge.properties for cartridge ${cartridgeName}
#demandware.cartridges.${cartridgeName}.multipleLanguageStorefront=true`,
eslintrc: () => ({
root: true,
extends: 'airbnb-base/legacy',
globals: {
session: 'readonly',
request: 'readonly',
},
rules: {
'import/no-unresolved': 'off',
indent: ['error', 4, { SwitchCase: 1, VariableDeclarator: 1 }],
'func-names': 'off',
'require-jsdoc': 'error',
'valid-jsdoc': ['error', {
preferType: {
Boolean: 'boolean',
Number: 'number',
object: 'Object',
String: 'string',
},
requireReturn: false,
}],
'vars-on-top': 'off',
'global-require': 'off',
'no-shadow': ['error', { allow: ['err', 'callback'] }],
'max-len': 'off',
'no-plusplus': 'off',
},
}),
stylelintrc: () => ({
extends: 'stylelint-config-standard-scss',
plugins: [
'stylelint-scss',
],
}),
eslintignore: () => `node_modules/
cartridges/**/cartridge/static/
coverage/
doc/
bin/
codecept.conf.js`,
gitignore: () => `node_modules/
cartridges/*/cartridge/static/
.DS_Store
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
coverage/
.nyc_output/
.env
dw.json`,
};
}
}
//# sourceMappingURL=cartridge-generation-client.js.map