@salesforce/pwa-kit-mcp 0.1.0-preview.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "@salesforce/pwa-kit-mcp",
3
+ "version": "0.1.0-preview.0",
4
+ "private": false,
5
+ "description": "MCP server that helps you build Salesforce Commerce Cloud PWA Kit Composable Storefront",
6
+ "main": "dist/server/server.js",
7
+ "files": [
8
+ "CHANGELOG.md",
9
+ "LICENSE",
10
+ "dist/**/*.{js,d.ts,json}",
11
+ "!dist/CHANGELOG.md",
12
+ "!dist/README.md",
13
+ "!**/*.test.{ts,js}"
14
+ ],
15
+ "scripts": {
16
+ "build": "cross-env NODE_ENV=production internal-lib-build build",
17
+ "build:watch": "nodemon --watch 'src/**' --ext 'js,ts' --exec 'npm run build'",
18
+ "format": "pwa-kit-dev format \"**/*.{js,jsx}\"",
19
+ "lint": "npm run lint:js",
20
+ "lint:fix": "npm run lint:js -- --fix",
21
+ "lint:js": "pwa-kit-dev lint \"**/*.{js,ts}\"",
22
+ "prepare": "npm run build",
23
+ "test": "internal-lib-build test",
24
+ "test:inspect": "node --inspect-brk jest --runInBand",
25
+ "test:watch": "npm test -- --watch",
26
+ "start": "node dist/server/server.js"
27
+ },
28
+ "keywords": [
29
+ "mcp",
30
+ "server",
31
+ "pwa",
32
+ "salesforce",
33
+ "commerce",
34
+ "commerce cloud",
35
+ "pwa kit",
36
+ "composable storefront"
37
+ ],
38
+ "author": "Spark Team",
39
+ "bin": {
40
+ "pwa-kit-mcp": "dist/server/server.js"
41
+ },
42
+ "license": "ISC",
43
+ "dependencies": {
44
+ "@axe-core/playwright": "^4.10.1",
45
+ "@babel/runtime": "^7.21.0",
46
+ "@modelcontextprotocol/sdk": "^1.13.2",
47
+ "axe-core": "^4.10.3",
48
+ "cross-spawn": "^7.0.6",
49
+ "playwright": "^1.49.0",
50
+ "zod": "^3.25.56"
51
+ },
52
+ "devDependencies": {
53
+ "@babel/node": "^7.22.5",
54
+ "@playwright/test": "^1.49.0",
55
+ "@salesforce/pwa-kit-dev": "3.11.0-dev.0",
56
+ "cross-env": "^5.2.1",
57
+ "internal-lib-build": "3.11.0-dev.0",
58
+ "nodemon": "^2.0.22"
59
+ },
60
+ "engines": {
61
+ "node": "^16.11.0 || ^18.0.0 || ^20.0.0 || ^22.0.0",
62
+ "npm": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0"
63
+ },
64
+ "publishConfig": {
65
+ "directory": "dist"
66
+ }
67
+ }
@@ -0,0 +1,210 @@
1
+ #!/usr/bin/env node
2
+ /*
3
+ * Copyright (c) 2025, Salesforce, Inc.
4
+ * All rights reserved.
5
+ * SPDX-License-Identifier: BSD-3-Clause
6
+ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
7
+ */
8
+ "use strict";
9
+
10
+ var _mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
11
+ var _stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
12
+ var _zod = require("zod");
13
+ var _utils = require("../utils");
14
+ var _runSiteTestTool = require("../utils/run-site-test-tool");
15
+ 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); }
16
+ 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); }); }; }
17
+ // NOTE: This is a workaround to import JSON files as ES modules.
18
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
19
+ const productDocument = require('../data/ProductDocument.json');
20
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
21
+ const categoryDocument = require('../data/CategoryDocument.json');
22
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
23
+ const documentList = require('../data/DocumentList.json');
24
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
25
+ const packageJson = require('../package.json');
26
+ const FALLBACK_VERSION = '0.1.0';
27
+ class PwaStorefrontMCPServerHighLevel {
28
+ constructor() {
29
+ // Using McpServer instead of Server
30
+ this.server = new _mcp.McpServer({
31
+ name: 'pwa-kit-mcp',
32
+ version: (packageJson === null || packageJson === void 0 ? void 0 : packageJson.version) || FALLBACK_VERSION
33
+ }, {
34
+ capabilities: {
35
+ tools: {}
36
+ }
37
+ });
38
+ this.CreateNewComponentTool = new _utils.CreateNewComponentTool();
39
+ this.testWithPlaywrightTool = new _runSiteTestTool.TestWithPlaywrightTool();
40
+ this.setupTools();
41
+
42
+ // 1. Add in-memory session management
43
+ this.sessions = {};
44
+ this.sessionCounter = 1;
45
+ }
46
+ setupTools() {
47
+ // Register CreateProjectTool
48
+ this.server.tool(_utils.CreateAppGuidelinesTool.name, _utils.CreateAppGuidelinesTool.description, _utils.CreateAppGuidelinesTool.inputSchema, _utils.CreateAppGuidelinesTool.fn);
49
+
50
+ // Register DeveloperGuidelinesTool
51
+ this.server.tool(_utils.DeveloperGuidelinesTool.name, _utils.DeveloperGuidelinesTool.description, _utils.DeveloperGuidelinesTool.inputSchema, _utils.DeveloperGuidelinesTool.fn);
52
+ this.server.tool('run_site_test', 'Run site performance or accessibility test for a given site URL (e.g. https://pwa-kit.mobify-storefront.com)', {
53
+ testType: _zod.z.enum(['performance', 'accessibility']).describe('Type of test to run'),
54
+ siteUrl: _zod.z.string().optional().describe('Site URL to test (optional)')
55
+ }, ({
56
+ testType,
57
+ siteUrl
58
+ }) => this.testWithPlaywrightTool.run(testType, siteUrl));
59
+ this.server.tool('create_new_sample_component', 'Conversationally collect parameters and create a new sample React component.', {
60
+ sessionId: _zod.z.string().optional().describe('Session ID for the conversational flow'),
61
+ answer: _zod.z.string().optional().describe('User answer to the current question')
62
+ }, args => this.handleCreateNewSampleComponent(args));
63
+ }
64
+
65
+ /**
66
+ * Helper to handle the conversational flow for create_new_sample_component
67
+ */
68
+ handleCreateNewSampleComponent(args) {
69
+ var _this = this;
70
+ return _asyncToGenerator(function* () {
71
+ var _args$answer;
72
+ let sessionId = args.sessionId;
73
+ if (!sessionId) {
74
+ sessionId = `session-interactive-${_this.sessionCounter++}`;
75
+ _this.sessions[sessionId] = {
76
+ step: 1,
77
+ answers: {}
78
+ };
79
+ }
80
+ const session = _this.sessions[sessionId];
81
+ const {
82
+ step
83
+ } = session;
84
+ const answer = (_args$answer = args.answer) === null || _args$answer === void 0 ? void 0 : _args$answer.trim();
85
+ switch (step) {
86
+ case 1:
87
+ return _this._handleComponentNameStep(session, answer, sessionId);
88
+ case 2:
89
+ return _this._handleDirectoryStep(session, answer, sessionId);
90
+ case 3:
91
+ return _this._handleSingleOrListStep(session, answer, sessionId);
92
+ default:
93
+ return _this._handleDoneStep(sessionId);
94
+ }
95
+ })();
96
+ }
97
+ _next(sessionId, question) {
98
+ return {
99
+ content: [{
100
+ type: 'text',
101
+ text: JSON.stringify({
102
+ sessionId,
103
+ question
104
+ })
105
+ }]
106
+ };
107
+ }
108
+ _done(sessionId, message) {
109
+ return {
110
+ content: [{
111
+ type: 'text',
112
+ text: JSON.stringify({
113
+ sessionId,
114
+ message
115
+ })
116
+ }]
117
+ };
118
+ }
119
+ _handleComponentNameStep(session, answer, sessionId) {
120
+ if (answer) {
121
+ session.answers.name = answer;
122
+ session.step = 2;
123
+ const defaultDir = process.env.PWA_STOREFRONT_APP_PATH ? process.env.PWA_STOREFRONT_APP_PATH + '/components' : '/components';
124
+ return this._next(sessionId, `Answer yes to use the default components directory (${defaultDir}), or no if you want to specify the full absolute path to use a different directory:`);
125
+ }
126
+ return this._next(sessionId, 'What would you like to name your new React component?');
127
+ }
128
+ _handleDirectoryStep(session, answer, sessionId) {
129
+ const defaultDir = process.env.PWA_STOREFRONT_APP_PATH ? process.env.PWA_STOREFRONT_APP_PATH + '/components' : '/components';
130
+ if (answer) {
131
+ if (/^(yes|y|true|1)$/i.test(answer)) {
132
+ session.answers.location = defaultDir;
133
+ } else {
134
+ session.answers.location = answer;
135
+ }
136
+ session.step = 3;
137
+ return this._next(sessionId, 'Should this component display a single product or a list of products? Reply with "single" or "list".');
138
+ }
139
+ return this._next(sessionId, `Answer yes to use the default components directory (${defaultDir}), or no if you want to specify the full absolute path to use a different directory:`);
140
+ }
141
+ _handleSingleOrListStep(session, answer, sessionId) {
142
+ var _this2 = this;
143
+ return _asyncToGenerator(function* () {
144
+ if (answer && /list/i.test(answer)) {
145
+ // List of products
146
+ const tool = new _utils.CreateNewComponentTool();
147
+ tool.componentData = {
148
+ name: session.answers.name,
149
+ location: session.answers.location,
150
+ createTestFile: false,
151
+ customCode: '',
152
+ entityType: 'product'
153
+ };
154
+ const dataModel = _this2.getDataModel('product');
155
+ let schemaObj = dataModel && dataModel.properties ? dataModel.properties : {};
156
+ let presentationalResult = yield tool.updateComponentToPresentational('product', session.answers.name, session.answers.location, schemaObj, {
157
+ list: true
158
+ });
159
+ session.step = 99;
160
+ return _this2._done(sessionId, (session.basicComponentResult || '') + `\n\n${presentationalResult}\nComponent creation flow complete.`);
161
+ } else if (answer && /single/i.test(answer)) {
162
+ // Single product
163
+ const tool = new _utils.CreateNewComponentTool();
164
+ tool.componentData = {
165
+ name: session.answers.name,
166
+ location: session.answers.location,
167
+ createTestFile: false,
168
+ customCode: '',
169
+ entityType: 'product'
170
+ };
171
+ const dataModel = _this2.getDataModel('product');
172
+ let schemaObj = dataModel && dataModel.properties ? dataModel.properties : {};
173
+ let presentationalResult = yield tool.updateComponentToPresentational('product', session.answers.name, session.answers.location, schemaObj, {
174
+ list: false
175
+ });
176
+ session.step = 99;
177
+ return _this2._done(sessionId, (session.basicComponentResult || '') + `\n\n${presentationalResult}\nComponent creation flow complete.`);
178
+ } else {
179
+ return _this2._next(sessionId, 'Please reply with "single" or "list".');
180
+ }
181
+ })();
182
+ }
183
+ _handleDoneStep(sessionId) {
184
+ return this._done(sessionId, 'Component creation flow complete.');
185
+ }
186
+
187
+ /**
188
+ * Simple method to get data models directly from imports
189
+ * @param {string} modelName - Name of the model (e.g., 'product', 'category')
190
+ * @returns {object|null} The data model object or null if not found
191
+ */
192
+ getDataModel(modelName) {
193
+ const models = {
194
+ product: productDocument,
195
+ category: categoryDocument,
196
+ documentList: documentList
197
+ };
198
+ return models[modelName.toLowerCase()] || null;
199
+ }
200
+ run() {
201
+ var _this3 = this;
202
+ return _asyncToGenerator(function* () {
203
+ const transport = new _stdio.StdioServerTransport();
204
+ yield _this3.server.connect(transport);
205
+ console.error('PWA Storefront MCP server (McpServer version) running on stdio');
206
+ })();
207
+ }
208
+ }
209
+ const server = new PwaStorefrontMCPServerHighLevel();
210
+ server.run().catch(console.error);
@@ -0,0 +1,340 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.getCopyrightHeader = exports.default = void 0;
7
+ var _promises = _interopRequireDefault(require("fs/promises"));
8
+ var _path = _interopRequireDefault(require("path"));
9
+ var _utils = require("./utils");
10
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
11
+ 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); }
12
+ 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); }); }; } /*
13
+ * Copyright (c) 2025, Salesforce, Inc.
14
+ * All rights reserved.
15
+ * SPDX-License-Identifier: BSD-3-Clause
16
+ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
17
+ */
18
+ const getCopyrightHeader = () => {
19
+ const year = new Date().getFullYear();
20
+ return `/*
21
+ * Copyright (c) ${year}, Salesforce, Inc.
22
+ * All rights reserved.
23
+ * SPDX-License-Identifier: BSD-3-Clause
24
+ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
25
+ */`;
26
+ };
27
+
28
+ // Utility to infer entity from component name
29
+ exports.getCopyrightHeader = getCopyrightHeader;
30
+ function inferEntityFromComponentName(componentName) {
31
+ const name = componentName.toLowerCase();
32
+ if (name.includes('customer')) return 'customer';
33
+ if (name.includes('product')) return 'product';
34
+ if (name.includes('basket')) return 'basket';
35
+ if (name.includes('category')) return 'category';
36
+ return null;
37
+ }
38
+ class CreateNewComponentTool {
39
+ constructor() {
40
+ this.currentStep = 0;
41
+ this.componentData = {
42
+ name: null,
43
+ location: null,
44
+ entityType: null
45
+ };
46
+ }
47
+
48
+ /**
49
+ * Creates the component based on all collected data
50
+ * @returns {Promise<string>} The result of component creation
51
+ */
52
+ createComponent() {
53
+ var _this = this;
54
+ return _asyncToGenerator(function* () {
55
+ const messages = [];
56
+
57
+ // Use the provided absolute path directly if available
58
+ const location = _this.componentData.location;
59
+ const componentMessage = yield _this.createComponentFile(_this.componentData.name, location);
60
+ messages.push(componentMessage);
61
+
62
+ // Handle entity type information
63
+ if (_this.componentData.entityType) {
64
+ messages.push(`\nℹ️ Entity type '${_this.componentData.entityType}' ${inferEntityFromComponentName(_this.componentData.name) ? 'was inferred' : 'was specified'} for component '${_this.componentData.name}'.`);
65
+ } else {
66
+ messages.push(`\nℹ️ No entity type was specified or could be inferred for component '${_this.componentData.name}'.`);
67
+ }
68
+
69
+ // Always append lint reminder
70
+ messages.push("\n💡 After creating or modifying a component, run 'npm run lint -- --fix' to automatically fix formatting and linter issues.");
71
+
72
+ // Reset for next use
73
+ _this.reset();
74
+ return messages.join('\n');
75
+ })();
76
+ }
77
+
78
+ /**
79
+ * Resets the tool state for the next component creation
80
+ */
81
+ reset() {
82
+ this.currentStep = 0;
83
+ this.componentData = {
84
+ name: null,
85
+ location: null,
86
+ entityType: null
87
+ };
88
+ }
89
+
90
+ /**
91
+ * Creates a new React component file.
92
+ * @param {string} componentName - Name for the new component.
93
+ * @param {string} projectDir - The absolute path to the project directory for the new component.
94
+ */
95
+ createComponentFile(componentName, projectDir) {
96
+ return _asyncToGenerator(function* () {
97
+ const kebabDirName = (0, _utils.toKebabCase)(componentName);
98
+ const pascalComponentName = (0, _utils.toPascalCase)(componentName);
99
+ const componentDir = _path.default.join(projectDir, kebabDirName);
100
+ try {
101
+ yield _promises.default.mkdir(componentDir, {
102
+ recursive: true
103
+ });
104
+ // Create component file
105
+ const componentFilePath = _path.default.join(componentDir, 'index.jsx');
106
+ const codeToWrite = `${getCopyrightHeader()}
107
+ import React from 'react';
108
+
109
+ const ${pascalComponentName} = () => {
110
+ return (
111
+ <div>${pascalComponentName} component</div>
112
+ );
113
+ };
114
+
115
+ export default ${pascalComponentName};
116
+ `;
117
+ yield _promises.default.writeFile(componentFilePath, codeToWrite, 'utf-8');
118
+ return `✅ Created ${componentFilePath}`;
119
+ } catch (err) {
120
+ console.error('Error during file creation:', err);
121
+ return `❌ Error creating component file at ${componentDir}: ${err.message}`;
122
+ }
123
+ })();
124
+ }
125
+
126
+ /**
127
+ * Updates the component file to be a presentational component for the given data model.
128
+ * @param {string} entityType - The entity type (e.g., 'product').
129
+ * @param {string} componentName - The component name.
130
+ * @param {string} location - The absolute path to the component's parent directory.
131
+ * @param {object} dataModel - The data model schema (properties object).
132
+ */
133
+ updateComponentToPresentational(entityType, componentName, location, dataModel, options = {}) {
134
+ return _asyncToGenerator(function* () {
135
+ const kebabDirName = (0, _utils.toKebabCase)(componentName);
136
+ const pascalComponentName = (0, _utils.toPascalCase)(componentName);
137
+ const componentDir = _path.default.join(location, kebabDirName);
138
+ yield _promises.default.mkdir(componentDir, {
139
+ recursive: true
140
+ });
141
+ const componentFilePath = _path.default.join(componentDir, 'index.jsx');
142
+ let code = '';
143
+
144
+ // Special logic for product entity
145
+ if (entityType === 'product') {
146
+ // If options.list is true, generate a list-of-products component
147
+ if (options.list) {
148
+ code = `${getCopyrightHeader()}
149
+ import React from 'react';
150
+ import PropTypes from 'prop-types';
151
+ import { Box, Text, Image, Stack } from '@chakra-ui/react';
152
+
153
+ const ${pascalComponentName} = ({ products }) => (
154
+ <Stack spacing={4}>
155
+ {products.map(product => (
156
+ <Box key={product.productId} borderWidth="1px" borderRadius="md" p={4}>
157
+ <Text fontSize="xl" fontWeight="bold">{product.name}</Text>
158
+ {product.imageGroups && product.imageGroups[0]?.images[0]?.link && (
159
+ <Image
160
+ src={product.imageGroups[0].images[0].link}
161
+ alt={product.name}
162
+ maxW="150px"
163
+ mb={2}
164
+ />
165
+ )}
166
+ <Text>assigned_categories: {product.assigned_categories?.toString?.() ?? ''}</Text>
167
+ <Text>price: {product.price?.toString?.() ?? ''}</Text>
168
+ </Box>
169
+ ))}
170
+ </Stack>
171
+ );
172
+
173
+ ${pascalComponentName}.propTypes = {
174
+ products: PropTypes.arrayOf(PropTypes.shape({
175
+ productId: PropTypes.string,
176
+ name: PropTypes.string,
177
+ assigned_categories: PropTypes.any,
178
+ price: PropTypes.any,
179
+ imageGroups: PropTypes.array
180
+ })).isRequired
181
+ };
182
+
183
+ export default ${pascalComponentName};
184
+ `;
185
+ } else {
186
+ // Single product component (with selectors, image, etc.)
187
+ code = `${getCopyrightHeader()}
188
+ import React, { useState } from 'react';
189
+ import PropTypes from 'prop-types';
190
+ import { Box, Text, Image, Button, HStack, Stack } from '@chakra-ui/react';
191
+
192
+ // Helper to filter variants by selected attribute values
193
+ const filterVariants = (variants, selected) => {
194
+ return variants.filter(variant =>
195
+ Object.entries(selected).every(
196
+ ([attr, value]) => !value || variant.variationValues?.[attr] === value
197
+ )
198
+ );
199
+ };
200
+
201
+ // Helper to get the image for the selected color
202
+ const getImageForSelection = (imageGroups, selected) => {
203
+ if (selected.color) {
204
+ const group = imageGroups.find(
205
+ g =>
206
+ g.variationAttributes &&
207
+ g.variationAttributes.some(
208
+ va =>
209
+ va.id === 'color' &&
210
+ va.values.some(v => v.value === selected.color)
211
+ )
212
+ );
213
+ if (group && group.images.length > 0) {
214
+ return group.images[0].link;
215
+ }
216
+ }
217
+ if (imageGroups.length > 0 && imageGroups[0].images.length > 0) {
218
+ return imageGroups[0].images[0].link;
219
+ }
220
+ return null;
221
+ };
222
+
223
+ const ${pascalComponentName} = ({ product }) => {
224
+ const { variationAttributes = [], variants = [], imageGroups = [] } = product;
225
+ const [selected, setSelected] = useState(() => {
226
+ const initial = {};
227
+ variationAttributes.forEach(attr => {
228
+ initial[attr.id] = '';
229
+ });
230
+ return initial;
231
+ });
232
+
233
+ // Build a color code to swatch image URL map
234
+ const swatchMap = {};
235
+ imageGroups
236
+ .filter(group => group.viewType === 'swatch')
237
+ .forEach(group => {
238
+ const colorCode = group.variationAttributes?.[0]?.values?.[0]?.value;
239
+ if (colorCode && group.images[0]?.link) {
240
+ swatchMap[colorCode] = group.images[0].link;
241
+ }
242
+ });
243
+
244
+ const filteredVariants = filterVariants(variants, selected);
245
+ const getAvailableValues = (attrId) => {
246
+ const otherSelected = { ...selected };
247
+ delete otherSelected[attrId];
248
+ const possibleVariants = filterVariants(variants, otherSelected);
249
+ const values = new Set();
250
+ possibleVariants.forEach(v => {
251
+ if (v.variationValues?.[attrId]) values.add(v.variationValues[attrId]);
252
+ });
253
+ return Array.from(values);
254
+ };
255
+
256
+ const imageUrl = getImageForSelection(imageGroups, selected);
257
+
258
+ return (
259
+ <Box>
260
+ <Text fontSize="2xl" fontWeight="bold" mb={2}>{product.name}</Text>
261
+ {imageUrl && (
262
+ <Image src={imageUrl} alt={product.name} maxW="300px" mb={4} />
263
+ )}
264
+ <Text>assigned_categories: {product.assigned_categories?.toString?.() ?? ''}</Text>
265
+ <Text>price: {product.price?.toString?.() ?? ''}</Text>
266
+ {/* Dynamic variant attribute selectors */}
267
+ {variationAttributes.map(attr => (
268
+ <Box key={attr.id} my={2}>
269
+ <Text as="span" fontWeight="semibold">{attr.name}:</Text>
270
+ <HStack spacing={2} mt={1}>
271
+ {getAvailableValues(attr.id).map(val =>
272
+ attr.id === 'color' ? (
273
+ <Button
274
+ key={val}
275
+ onClick={() => setSelected(sel => ({ ...sel, [attr.id]: val }))}
276
+ variant={selected[attr.id] === val ? 'solid' : 'outline'}
277
+ borderRadius="full"
278
+ minW="32px"
279
+ h="32px"
280
+ p={0}
281
+ borderColor={
282
+ selected[attr.id] === val ? 'blue.500' : 'gray.200'
283
+ }
284
+ _hover={{opacity: 0.8}}
285
+ aria-label={val}
286
+ >
287
+ {swatchMap[val] ? (
288
+ <Image
289
+ src={swatchMap[val]}
290
+ alt={val}
291
+ borderRadius="full"
292
+ boxSize="28px"
293
+ />
294
+ ) : (
295
+ val
296
+ )}
297
+ </Button>
298
+ ) : (
299
+ <Button
300
+ key={val}
301
+ onClick={() => setSelected(sel => ({ ...sel, [attr.id]: val }))}
302
+ variant={selected[attr.id] === val ? 'solid' : 'outline'}
303
+ colorScheme={selected[attr.id] === val ? 'blue' : 'gray'}
304
+ borderRadius="md"
305
+ size="sm"
306
+ >
307
+ {val}
308
+ </Button>
309
+ )
310
+ )}
311
+ </HStack>
312
+ </Box>
313
+ ))}
314
+ </Box>
315
+ );
316
+ };
317
+
318
+ ${pascalComponentName}.propTypes = {
319
+ product: PropTypes.shape({
320
+ name: PropTypes.string,
321
+ assigned_categories: PropTypes.any,
322
+ price: PropTypes.any,
323
+ variationAttributes: PropTypes.array,
324
+ variants: PropTypes.array,
325
+ imageGroups: PropTypes.array
326
+ }).isRequired
327
+ };
328
+
329
+ export default ${pascalComponentName};
330
+ `;
331
+ }
332
+ } else {
333
+ throw new Error(`Entity type '${entityType}' is not supported.`);
334
+ }
335
+ yield _promises.default.writeFile(componentFilePath, code, 'utf-8');
336
+ return `✅ Updated ${componentFilePath} to presentational component for ${entityType}`;
337
+ })();
338
+ }
339
+ }
340
+ var _default = exports.default = CreateNewComponentTool;
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ Object.defineProperty(exports, "CreateAppGuidelinesTool", {
7
+ enumerable: true,
8
+ get: function () {
9
+ return _pwaCreateAppGuidelineTool.default;
10
+ }
11
+ });
12
+ Object.defineProperty(exports, "CreateNewComponentTool", {
13
+ enumerable: true,
14
+ get: function () {
15
+ return _createNewComponentTool.default;
16
+ }
17
+ });
18
+ Object.defineProperty(exports, "DeveloperGuidelinesTool", {
19
+ enumerable: true,
20
+ get: function () {
21
+ return _pwaDeveloperGuidelineTool.default;
22
+ }
23
+ });
24
+ var _pwaCreateAppGuidelineTool = _interopRequireDefault(require("./pwa-create-app-guideline-tool"));
25
+ var _createNewComponentTool = _interopRequireDefault(require("./create-new-component-tool"));
26
+ var _pwaDeveloperGuidelineTool = _interopRequireDefault(require("./pwa-developer-guideline-tool"));
27
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }