@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.
- package/CHANGELOG.md +5 -0
- package/README.md +165 -0
- package/dist/CHANGELOG.md +5 -0
- package/dist/data/CategoryDocument.json +239 -0
- package/dist/data/DocumentList.json +920 -0
- package/dist/data/ProductDocument.json +1458 -0
- package/dist/package.json +67 -0
- package/dist/server/server.js +210 -0
- package/dist/utils/create-new-component-tool.js +340 -0
- package/dist/utils/index.js +27 -0
- package/dist/utils/pwa-create-app-guideline-tool.js +99 -0
- package/dist/utils/pwa-developer-guideline-tool.js +131 -0
- package/dist/utils/run-site-test-accessibility.js +33 -0
- package/dist/utils/run-site-test-performance.js +107 -0
- package/dist/utils/run-site-test-tool.js +47 -0
- package/dist/utils/utils.js +115 -0
- package/package.json +67 -0
|
@@ -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 }; }
|