@dropins/mcp 0.1.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/LICENSE.md +127 -0
- package/README.md +314 -0
- package/dist/common/project-reader.d.ts +55 -0
- package/dist/common/project-reader.js +173 -0
- package/dist/common/registry-loader.d.ts +101 -0
- package/dist/common/registry-loader.js +386 -0
- package/dist/common/response-handling.d.ts +12 -0
- package/dist/common/response-handling.js +21 -0
- package/dist/common/sanitize.d.ts +8 -0
- package/dist/common/sanitize.js +45 -0
- package/dist/common/synonyms.d.ts +9 -0
- package/dist/common/synonyms.js +127 -0
- package/dist/common/telemetry.d.ts +14 -0
- package/dist/common/telemetry.js +54 -0
- package/dist/common/types.d.ts +308 -0
- package/dist/common/types.js +1 -0
- package/dist/common/version.d.ts +2 -0
- package/dist/common/version.js +14 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +136 -0
- package/dist/operations/analyze-project.d.ts +13 -0
- package/dist/operations/analyze-project.js +125 -0
- package/dist/operations/check-block-health.d.ts +19 -0
- package/dist/operations/check-block-health.js +1149 -0
- package/dist/operations/check-config.d.ts +13 -0
- package/dist/operations/check-config.js +228 -0
- package/dist/operations/explain-event-flow.d.ts +16 -0
- package/dist/operations/explain-event-flow.js +218 -0
- package/dist/operations/get-upgrade-diff.d.ts +13 -0
- package/dist/operations/get-upgrade-diff.js +144 -0
- package/dist/operations/list-api-functions.d.ts +13 -0
- package/dist/operations/list-api-functions.js +53 -0
- package/dist/operations/list-containers.d.ts +13 -0
- package/dist/operations/list-containers.js +44 -0
- package/dist/operations/list-design-tokens.d.ts +13 -0
- package/dist/operations/list-design-tokens.js +47 -0
- package/dist/operations/list-events.d.ts +16 -0
- package/dist/operations/list-events.js +39 -0
- package/dist/operations/list-graphql-queries.d.ts +19 -0
- package/dist/operations/list-graphql-queries.js +84 -0
- package/dist/operations/list-i18n-keys.d.ts +19 -0
- package/dist/operations/list-i18n-keys.js +105 -0
- package/dist/operations/list-models.d.ts +16 -0
- package/dist/operations/list-models.js +80 -0
- package/dist/operations/list-slots.d.ts +16 -0
- package/dist/operations/list-slots.js +81 -0
- package/dist/operations/scaffold-block.d.ts +31 -0
- package/dist/operations/scaffold-block.js +331 -0
- package/dist/operations/scaffold-extension.d.ts +28 -0
- package/dist/operations/scaffold-extension.js +346 -0
- package/dist/operations/scaffold-slot.d.ts +22 -0
- package/dist/operations/scaffold-slot.js +189 -0
- package/dist/operations/search-commerce-docs.d.ts +16 -0
- package/dist/operations/search-commerce-docs.js +101 -0
- package/dist/operations/search-docs.d.ts +23 -0
- package/dist/operations/search-docs.js +298 -0
- package/dist/operations/suggest-event-handler.d.ts +16 -0
- package/dist/operations/suggest-event-handler.js +175 -0
- package/dist/operations/suggest-slot-implementation.d.ts +19 -0
- package/dist/operations/suggest-slot-implementation.js +183 -0
- package/dist/registry/api-functions.json +3045 -0
- package/dist/registry/block-patterns.json +78 -0
- package/dist/registry/containers.json +2003 -0
- package/dist/registry/design-tokens.json +577 -0
- package/dist/registry/docs/boilerplate.json +55 -0
- package/dist/registry/docs/dropins-all.json +97 -0
- package/dist/registry/docs/dropins-b2b.json +607 -0
- package/dist/registry/docs/dropins-cart.json +163 -0
- package/dist/registry/docs/dropins-checkout.json +193 -0
- package/dist/registry/docs/dropins-order.json +139 -0
- package/dist/registry/docs/dropins-payment-services.json +73 -0
- package/dist/registry/docs/dropins-personalization.json +67 -0
- package/dist/registry/docs/dropins-product-details.json +139 -0
- package/dist/registry/docs/dropins-product-discovery.json +85 -0
- package/dist/registry/docs/dropins-recommendations.json +67 -0
- package/dist/registry/docs/dropins-user-account.json +121 -0
- package/dist/registry/docs/dropins-user-auth.json +103 -0
- package/dist/registry/docs/dropins-wishlist.json +85 -0
- package/dist/registry/docs/get-started.json +85 -0
- package/dist/registry/docs/how-tos.json +19 -0
- package/dist/registry/docs/index.json +139 -0
- package/dist/registry/docs/licensing.json +19 -0
- package/dist/registry/docs/merchants.json +523 -0
- package/dist/registry/docs/resources.json +13 -0
- package/dist/registry/docs/sdk.json +139 -0
- package/dist/registry/docs/setup.json +145 -0
- package/dist/registry/docs/troubleshooting.json +19 -0
- package/dist/registry/events.json +2200 -0
- package/dist/registry/examples/index.json +19 -0
- package/dist/registry/examples/storefront-checkout.json +377 -0
- package/dist/registry/examples/storefront-quote-management.json +49 -0
- package/dist/registry/extensions.json +272 -0
- package/dist/registry/graphql.json +3469 -0
- package/dist/registry/i18n.json +1873 -0
- package/dist/registry/models.json +1001 -0
- package/dist/registry/sdk.json +2357 -0
- package/dist/registry/slots.json +2270 -0
- package/dist/registry/tools-components.json +595 -0
- package/dist/resources/guides.d.ts +7 -0
- package/dist/resources/guides.js +625 -0
- package/dist/resources/handlers.d.ts +31 -0
- package/dist/resources/handlers.js +322 -0
- package/package.json +47 -0
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { formatSuccessResponse, formatExceptionResponse, } from "../common/response-handling.js";
|
|
3
|
+
import { getExtensionRegistry } from "../common/registry-loader.js";
|
|
4
|
+
import { projectDirGuard } from "../common/project-reader.js";
|
|
5
|
+
import { validateExtensionId, validateUrl, escapeJsString, sanitizeForComment, } from "../common/sanitize.js";
|
|
6
|
+
export const ScaffoldExtensionSchema = z.object({
|
|
7
|
+
extensionName: z
|
|
8
|
+
.string()
|
|
9
|
+
.describe('Human-readable name (e.g. "Stripe Payment")'),
|
|
10
|
+
extensionId: z
|
|
11
|
+
.string()
|
|
12
|
+
.describe('Unique ID (e.g. "stripe-payment"). Used as folder name and id field.'),
|
|
13
|
+
hooks: z
|
|
14
|
+
.array(z.string())
|
|
15
|
+
.describe('Hook names to implement (e.g. ["checkout/payment-methods", "checkout/validate", "checkout/place-order"])'),
|
|
16
|
+
externalScripts: z
|
|
17
|
+
.array(z.string())
|
|
18
|
+
.optional()
|
|
19
|
+
.describe('External script URLs to load (e.g. ["https://js.stripe.com/v3/"])'),
|
|
20
|
+
externalStyles: z
|
|
21
|
+
.array(z.string())
|
|
22
|
+
.optional()
|
|
23
|
+
.describe("External stylesheet URLs to load"),
|
|
24
|
+
projectDir: z
|
|
25
|
+
.string()
|
|
26
|
+
.describe("Absolute path to the merchant storefront project root"),
|
|
27
|
+
});
|
|
28
|
+
function generateHookStub(hook) {
|
|
29
|
+
const contextFields = Object.entries(hook.contextShape ?? {})
|
|
30
|
+
.map(([key, type]) => ` * ${key}: ${type}`)
|
|
31
|
+
.join("\n");
|
|
32
|
+
const contextDoc = contextFields ? `\n * Context:\n${contextFields}` : "";
|
|
33
|
+
switch (hook.name) {
|
|
34
|
+
case "checkout/validate":
|
|
35
|
+
return ` /**
|
|
36
|
+
* Validation hook — set context.isValid = false to block order placement${contextDoc}
|
|
37
|
+
*/
|
|
38
|
+
'checkout/validate': async ({ context }) => {
|
|
39
|
+
const { code } = context;
|
|
40
|
+
|
|
41
|
+
// Only handle your payment method
|
|
42
|
+
if (code !== 'YOUR_PAYMENT_CODE') return;
|
|
43
|
+
|
|
44
|
+
// Perform custom validation
|
|
45
|
+
const isValid = true; // Replace with actual validation
|
|
46
|
+
if (!isValid) {
|
|
47
|
+
context.isValid = false;
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
},`;
|
|
51
|
+
case "checkout/payment-methods":
|
|
52
|
+
return ` /**
|
|
53
|
+
* Payment methods hook — add your payment method to context.paymentMethods${contextDoc}
|
|
54
|
+
*/
|
|
55
|
+
'checkout/payment-methods': async ({ context }) => {
|
|
56
|
+
context.paymentMethods.YOUR_PAYMENT_CODE = {
|
|
57
|
+
autoSync: false,
|
|
58
|
+
render: (ctx) => {
|
|
59
|
+
const container = document.createElement('div');
|
|
60
|
+
container.className = 'custom-payment-method';
|
|
61
|
+
|
|
62
|
+
const title = document.createElement('h3');
|
|
63
|
+
title.textContent = 'Your Payment Method';
|
|
64
|
+
|
|
65
|
+
const description = document.createElement('p');
|
|
66
|
+
description.textContent = 'Payment method description here.';
|
|
67
|
+
|
|
68
|
+
container.appendChild(title);
|
|
69
|
+
container.appendChild(description);
|
|
70
|
+
ctx.replaceHTML(container);
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
},`;
|
|
74
|
+
case "checkout/place-order":
|
|
75
|
+
return ` /**
|
|
76
|
+
* Place order hook — handle payment processing${contextDoc}
|
|
77
|
+
*/
|
|
78
|
+
'checkout/place-order': async ({ context }) => {
|
|
79
|
+
const { cartId, code } = context;
|
|
80
|
+
|
|
81
|
+
// Only handle your payment method
|
|
82
|
+
if (code !== 'YOUR_PAYMENT_CODE') return;
|
|
83
|
+
|
|
84
|
+
// Prevent default order placement
|
|
85
|
+
context.preventDefault = true;
|
|
86
|
+
|
|
87
|
+
// Process payment with your provider
|
|
88
|
+
// const paymentResult = await processPayment(cartId);
|
|
89
|
+
|
|
90
|
+
// Place order via API
|
|
91
|
+
// const { placeOrder } = await import('@dropins/storefront-order/api.js');
|
|
92
|
+
// await placeOrder(cartId);
|
|
93
|
+
},`;
|
|
94
|
+
case "checkout/payment-response":
|
|
95
|
+
return ` /**
|
|
96
|
+
* Payment response hook — runs before checkout renders, detect redirect returns here${contextDoc}
|
|
97
|
+
*/
|
|
98
|
+
'checkout/payment-response': async ({ context }) => {
|
|
99
|
+
const { block, shouldExit } = context;
|
|
100
|
+
|
|
101
|
+
// Detect a returning payment redirect (e.g. check URL params or session state)
|
|
102
|
+
const isReturningFromRedirect = false; // replace with your detection logic
|
|
103
|
+
|
|
104
|
+
if (isReturningFromRedirect) {
|
|
105
|
+
context.shouldExit = true; // stop normal checkout flow
|
|
106
|
+
// Handle the gateway response here
|
|
107
|
+
}
|
|
108
|
+
},`;
|
|
109
|
+
case "checkout/address-form-render":
|
|
110
|
+
return ` /**
|
|
111
|
+
* Address form render hook — wrap context.render() to augment address form behavior${contextDoc}
|
|
112
|
+
*/
|
|
113
|
+
'checkout/address-form-render': async ({ context }) => {
|
|
114
|
+
const { container, addressType, render } = context;
|
|
115
|
+
|
|
116
|
+
// addressType is 'shipping' or 'billing'
|
|
117
|
+
// Wrap the default render to inject custom behavior before/after
|
|
118
|
+
context.render = (props) => {
|
|
119
|
+
render(props); // call the original render first
|
|
120
|
+
// Add custom fields or post-render logic here
|
|
121
|
+
// const customField = document.createElement('input');
|
|
122
|
+
// container.appendChild(customField);
|
|
123
|
+
};
|
|
124
|
+
},`;
|
|
125
|
+
default:
|
|
126
|
+
return ` /**
|
|
127
|
+
* ${hook.name} hook
|
|
128
|
+
* ${hook.description ?? ""}${contextDoc}
|
|
129
|
+
*/
|
|
130
|
+
'${hook.name}': async ({ context }) => {
|
|
131
|
+
// Implement hook logic here
|
|
132
|
+
},`;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function generateExtensionFile(extensionName, extensionId, hookDefs, externalScripts, externalStyles) {
|
|
136
|
+
const hookStubs = hookDefs.map(generateHookStub).join("\n\n");
|
|
137
|
+
const scriptsProp = externalScripts?.length
|
|
138
|
+
? `\n externalScripts: [\n ${externalScripts.map((s) => `'${escapeJsString(s)}'`).join(",\n ")},\n ],`
|
|
139
|
+
: "";
|
|
140
|
+
const stylesProp = externalStyles?.length
|
|
141
|
+
? `\n externalStyles: [\n ${externalStyles.map((s) => `'${escapeJsString(s)}'`).join(",\n ")},\n ],`
|
|
142
|
+
: "";
|
|
143
|
+
return `/**
|
|
144
|
+
* ${sanitizeForComment(extensionName)}
|
|
145
|
+
*
|
|
146
|
+
* Checkout extension that implements: ${hookDefs.map((h) => h.name).join(", ")}
|
|
147
|
+
*/
|
|
148
|
+
|
|
149
|
+
export default {
|
|
150
|
+
id: '${escapeJsString(extensionId)}',
|
|
151
|
+
name: '${escapeJsString(extensionName)}',${scriptsProp}${stylesProp}
|
|
152
|
+
|
|
153
|
+
hooks: {
|
|
154
|
+
${hookStubs}
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
`;
|
|
158
|
+
}
|
|
159
|
+
function generateExtensionsIndex(extensionId) {
|
|
160
|
+
return `/**
|
|
161
|
+
* Commerce Checkout Extensions
|
|
162
|
+
*
|
|
163
|
+
* Import and register all extensions here.
|
|
164
|
+
* ADD new extensions — do not replace this file.
|
|
165
|
+
*/
|
|
166
|
+
|
|
167
|
+
import ${toCamelCase(extensionId)} from './${extensionId}/${extensionId}.js';
|
|
168
|
+
|
|
169
|
+
export default [
|
|
170
|
+
${toCamelCase(extensionId)},
|
|
171
|
+
];
|
|
172
|
+
`;
|
|
173
|
+
}
|
|
174
|
+
function generateIndexMergeSnippet(extensionId) {
|
|
175
|
+
return {
|
|
176
|
+
importLine: `import ${toCamelCase(extensionId)} from './${extensionId}/${extensionId}.js';`,
|
|
177
|
+
exportEntry: ` ${toCamelCase(extensionId)},`,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
function generateExtensionManager() {
|
|
181
|
+
return `/**
|
|
182
|
+
* Extension Manager
|
|
183
|
+
*
|
|
184
|
+
* Singleton manager that loads extensions and executes hooks.
|
|
185
|
+
*/
|
|
186
|
+
|
|
187
|
+
function loadScript(src) {
|
|
188
|
+
return new Promise((resolve, reject) => {
|
|
189
|
+
if (document.querySelector(\`script[src="\${src}"]\`)) {
|
|
190
|
+
resolve();
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const script = document.createElement('script');
|
|
194
|
+
script.src = src;
|
|
195
|
+
script.onload = resolve;
|
|
196
|
+
script.onerror = () => reject(new Error(\`Failed to load script: \${src}\`));
|
|
197
|
+
document.head.appendChild(script);
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function loadStyle(href) {
|
|
202
|
+
return new Promise((resolve) => {
|
|
203
|
+
if (document.querySelector(\`link[href="\${href}"]\`)) {
|
|
204
|
+
resolve();
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
const link = document.createElement('link');
|
|
208
|
+
link.rel = 'stylesheet';
|
|
209
|
+
link.href = href;
|
|
210
|
+
link.onload = resolve;
|
|
211
|
+
document.head.appendChild(link);
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
let extensionManagerInstance = null;
|
|
216
|
+
let initializationPromise = null;
|
|
217
|
+
|
|
218
|
+
async function createExtensionManager() {
|
|
219
|
+
const extensions = [];
|
|
220
|
+
|
|
221
|
+
try {
|
|
222
|
+
const extensionsModule = await import('./extensions/index.js');
|
|
223
|
+
extensions.push(...(extensionsModule.default || []));
|
|
224
|
+
|
|
225
|
+
const scripts = extensions.flatMap((ext) => ext.externalScripts || []);
|
|
226
|
+
const styles = extensions.flatMap((ext) => ext.externalStyles || []);
|
|
227
|
+
|
|
228
|
+
await Promise.all([
|
|
229
|
+
...scripts.map(loadScript),
|
|
230
|
+
...styles.map(loadStyle),
|
|
231
|
+
]);
|
|
232
|
+
} catch (e) {
|
|
233
|
+
console.error('[ExtensionManager] Failed to load extensions:', e.message);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
async executeHook(hookName, context = {}) {
|
|
238
|
+
for (const ext of extensions) {
|
|
239
|
+
if (ext.hooks?.[hookName]) {
|
|
240
|
+
try {
|
|
241
|
+
await ext.hooks[hookName]({ context });
|
|
242
|
+
} catch (error) {
|
|
243
|
+
console.error(\`[ExtensionManager] Error in \${hookName} for \${ext.name}:\`, error);
|
|
244
|
+
throw error;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** Returns the extension manager boilerplate code for loading and executing hooks. */
|
|
253
|
+
export async function getExtensionManager() {
|
|
254
|
+
if (extensionManagerInstance) return extensionManagerInstance;
|
|
255
|
+
if (!initializationPromise) initializationPromise = createExtensionManager();
|
|
256
|
+
extensionManagerInstance = await initializationPromise;
|
|
257
|
+
return extensionManagerInstance;
|
|
258
|
+
}
|
|
259
|
+
`;
|
|
260
|
+
}
|
|
261
|
+
function toCamelCase(str) {
|
|
262
|
+
return str.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
263
|
+
}
|
|
264
|
+
export async function scaffoldExtension(params) {
|
|
265
|
+
try {
|
|
266
|
+
const extIdError = validateExtensionId(params.extensionId);
|
|
267
|
+
if (extIdError) {
|
|
268
|
+
return formatSuccessResponse("Invalid extension ID", {
|
|
269
|
+
error: extIdError,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
if (params.externalScripts) {
|
|
273
|
+
for (const url of params.externalScripts) {
|
|
274
|
+
const urlError = validateUrl(url, "external script URL");
|
|
275
|
+
if (urlError) {
|
|
276
|
+
return formatSuccessResponse("Invalid external script URL", {
|
|
277
|
+
error: urlError,
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
if (params.externalStyles) {
|
|
283
|
+
for (const url of params.externalStyles) {
|
|
284
|
+
const urlError = validateUrl(url, "external stylesheet URL");
|
|
285
|
+
if (urlError) {
|
|
286
|
+
return formatSuccessResponse("Invalid external stylesheet URL", {
|
|
287
|
+
error: urlError,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
const guard = projectDirGuard(params.projectDir);
|
|
293
|
+
if (guard)
|
|
294
|
+
return guard;
|
|
295
|
+
const registry = getExtensionRegistry();
|
|
296
|
+
const availableHooks = registry.hooks.map((h) => h.name);
|
|
297
|
+
const invalidHooks = params.hooks.filter((h) => !availableHooks.includes(h));
|
|
298
|
+
if (invalidHooks.length > 0) {
|
|
299
|
+
return formatSuccessResponse("Invalid hook names", {
|
|
300
|
+
invalidHooks,
|
|
301
|
+
availableHooks,
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
const hookDefs = params.hooks
|
|
305
|
+
.map((hookName) => registry.hooks.find((h) => h.name === hookName))
|
|
306
|
+
.filter((h) => h !== undefined);
|
|
307
|
+
const extensionFile = generateExtensionFile(params.extensionName, params.extensionId, hookDefs, params.externalScripts, params.externalStyles);
|
|
308
|
+
const indexFile = generateExtensionsIndex(params.extensionId);
|
|
309
|
+
const indexMergeSnippet = generateIndexMergeSnippet(params.extensionId);
|
|
310
|
+
const managerFile = generateExtensionManager();
|
|
311
|
+
const blockBase = "blocks/commerce-checkout";
|
|
312
|
+
const extDir = `${blockBase}/extensions/${params.extensionId}`;
|
|
313
|
+
const extFile = `${extDir}/${params.extensionId}.js`;
|
|
314
|
+
const extIndex = `${blockBase}/extensions/index.js`;
|
|
315
|
+
const extManager = `${blockBase}/extensions.js`;
|
|
316
|
+
return formatSuccessResponse(`Extension "${params.extensionName}" scaffolded successfully`, {
|
|
317
|
+
files: {
|
|
318
|
+
[extFile]: extensionFile,
|
|
319
|
+
[extIndex]: indexFile,
|
|
320
|
+
[extManager]: managerFile,
|
|
321
|
+
},
|
|
322
|
+
indexMergeSnippet,
|
|
323
|
+
instructions: [
|
|
324
|
+
`Create directory: ${extDir}/`,
|
|
325
|
+
`Write extension file: ${extFile}`,
|
|
326
|
+
`If ${extIndex} does NOT exist: write the full indexFile content above`,
|
|
327
|
+
`If ${extIndex} already EXISTS: add only indexMergeSnippet.importLine near the top and indexMergeSnippet.exportEntry inside the export default array — do NOT replace the file`,
|
|
328
|
+
`Write extension manager (if not exists): ${extManager}`,
|
|
329
|
+
"Replace YOUR_PAYMENT_CODE with your actual payment method code",
|
|
330
|
+
params.externalScripts?.length
|
|
331
|
+
? `External scripts will be auto-loaded: ${params.externalScripts.join(", ")}`
|
|
332
|
+
: "No external scripts configured",
|
|
333
|
+
"Import getExtensionManager in your checkout block to use extensions",
|
|
334
|
+
'Call extensionManager.executeHook("hookName", context) at the appropriate lifecycle points',
|
|
335
|
+
],
|
|
336
|
+
hooksSummary: hookDefs.map((h) => ({
|
|
337
|
+
name: h?.name,
|
|
338
|
+
description: h?.description,
|
|
339
|
+
whenFired: h?.whenFired,
|
|
340
|
+
})),
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
catch (error) {
|
|
344
|
+
return formatExceptionResponse(error, "scaffolding extension");
|
|
345
|
+
}
|
|
346
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const ScaffoldSlotSchema: z.ZodObject<{
|
|
3
|
+
containerName: z.ZodString;
|
|
4
|
+
slotName: z.ZodString;
|
|
5
|
+
dropin: z.ZodOptional<z.ZodString>;
|
|
6
|
+
projectDir: z.ZodString;
|
|
7
|
+
}, "strip", z.ZodTypeAny, {
|
|
8
|
+
projectDir: string;
|
|
9
|
+
containerName: string;
|
|
10
|
+
slotName: string;
|
|
11
|
+
dropin?: string | undefined;
|
|
12
|
+
}, {
|
|
13
|
+
projectDir: string;
|
|
14
|
+
containerName: string;
|
|
15
|
+
slotName: string;
|
|
16
|
+
dropin?: string | undefined;
|
|
17
|
+
}>;
|
|
18
|
+
export declare function scaffoldSlot(params: z.infer<typeof ScaffoldSlotSchema>): Promise<{
|
|
19
|
+
success: boolean;
|
|
20
|
+
message: string;
|
|
21
|
+
data: unknown;
|
|
22
|
+
}>;
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { formatSuccessResponse, formatExceptionResponse, } from "../common/response-handling.js";
|
|
3
|
+
import { getSlotRegistry, getContainerRegistry, } from "../common/registry-loader.js";
|
|
4
|
+
import { projectDirGuard } from "../common/project-reader.js";
|
|
5
|
+
export const ScaffoldSlotSchema = z.object({
|
|
6
|
+
containerName: z
|
|
7
|
+
.string()
|
|
8
|
+
.describe('Container name (e.g. "MiniCart", "ShippingMethods")'),
|
|
9
|
+
slotName: z
|
|
10
|
+
.string()
|
|
11
|
+
.describe('Slot name to scaffold (e.g. "Thumbnail", "ShippingMethodItem")'),
|
|
12
|
+
dropin: z
|
|
13
|
+
.string()
|
|
14
|
+
.optional()
|
|
15
|
+
.describe('Dropin package name (e.g. "storefront-cart"). Provide to resolve ambiguity when the same slot name exists in multiple dropins.'),
|
|
16
|
+
projectDir: z
|
|
17
|
+
.string()
|
|
18
|
+
.describe("Absolute path to the merchant storefront project root"),
|
|
19
|
+
});
|
|
20
|
+
function extractContextProps(contextType) {
|
|
21
|
+
if (!contextType || contextType === "DefaultSlotContext")
|
|
22
|
+
return [];
|
|
23
|
+
if (!contextType.includes("{"))
|
|
24
|
+
return [];
|
|
25
|
+
const matches = [...contextType.matchAll(/^\s*(\w+)\s*:/gm)];
|
|
26
|
+
return matches.map((m) => m[1]);
|
|
27
|
+
}
|
|
28
|
+
function toKebabCase(str) {
|
|
29
|
+
return str.replace(/([A-Z])/g, (_, c, i) => (i > 0 ? "-" : "") + c.toLowerCase());
|
|
30
|
+
}
|
|
31
|
+
function buildSlotCodeParts(slot, registry) {
|
|
32
|
+
const contextType = slot.contextType ?? "DefaultSlotContext";
|
|
33
|
+
const customMethods = slot.customMethods ?? [];
|
|
34
|
+
const defaultMethods = registry.defaultSlotMethods ?? {};
|
|
35
|
+
const contextProps = extractContextProps(contextType);
|
|
36
|
+
const allMethods = [
|
|
37
|
+
...Object.entries(defaultMethods).map(([name, desc]) => ({ name, desc })),
|
|
38
|
+
...customMethods
|
|
39
|
+
.filter((m) => !(m in defaultMethods))
|
|
40
|
+
.map((m) => ({ name: m, desc: "" })),
|
|
41
|
+
];
|
|
42
|
+
const allMethodNames = allMethods.map(({ name }) => name);
|
|
43
|
+
const methodComments = allMethods
|
|
44
|
+
.map(({ name, desc }) => desc ? ` // ctx.${name}(...) - ${desc}` : ` // ctx.${name}(...)`)
|
|
45
|
+
.join("\n");
|
|
46
|
+
const contextDestructure = contextProps.length > 0
|
|
47
|
+
? ` const { ${contextProps.join(", ")} } = ctx;\n`
|
|
48
|
+
: "";
|
|
49
|
+
if (customMethods.includes("appendAgreement")) {
|
|
50
|
+
return {
|
|
51
|
+
contextProps,
|
|
52
|
+
allMethodNames,
|
|
53
|
+
slotCode: `${slot.name}: (ctx) => {
|
|
54
|
+
ctx.appendAgreement(() => ({
|
|
55
|
+
name: 'custom-agreement',
|
|
56
|
+
mode: 'manual',
|
|
57
|
+
translationId: 'Custom.Agreement.Label',
|
|
58
|
+
}));
|
|
59
|
+
},`,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
contextProps,
|
|
64
|
+
allMethodNames,
|
|
65
|
+
slotCode: `${slot.name}: (ctx) => {
|
|
66
|
+
// Available methods:
|
|
67
|
+
${methodComments}
|
|
68
|
+
${contextDestructure}
|
|
69
|
+
const element = document.createElement('div');
|
|
70
|
+
element.className = 'custom-${toKebabCase(slot.name)}';
|
|
71
|
+
|
|
72
|
+
// Build your custom content here
|
|
73
|
+
element.textContent = 'Custom content';
|
|
74
|
+
|
|
75
|
+
ctx.appendChild(element);
|
|
76
|
+
},`,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function generateScaffold(slotCode, match) {
|
|
80
|
+
return `// 1. Imports
|
|
81
|
+
import { render as provider } from '${match.renderImport}';
|
|
82
|
+
import ${match.containerName} from '${match.containerImportPath}';
|
|
83
|
+
|
|
84
|
+
// 2. Render the container with the slot
|
|
85
|
+
const container = document.querySelector('.block-placeholder');
|
|
86
|
+
if (container) {
|
|
87
|
+
provider.render(${match.containerName}, {
|
|
88
|
+
slots: {
|
|
89
|
+
${slotCode}
|
|
90
|
+
},
|
|
91
|
+
})(container);
|
|
92
|
+
}`;
|
|
93
|
+
}
|
|
94
|
+
export async function scaffoldSlot(params) {
|
|
95
|
+
try {
|
|
96
|
+
const guard = projectDirGuard(params.projectDir);
|
|
97
|
+
if (guard)
|
|
98
|
+
return guard;
|
|
99
|
+
const slotRegistry = getSlotRegistry();
|
|
100
|
+
const containerRegistry = getContainerRegistry();
|
|
101
|
+
const matches = [];
|
|
102
|
+
const searchDropins = params.dropin
|
|
103
|
+
? [params.dropin]
|
|
104
|
+
: Object.keys(slotRegistry.dropins);
|
|
105
|
+
const containerLower = params.containerName.toLowerCase();
|
|
106
|
+
const slotLower = params.slotName.toLowerCase();
|
|
107
|
+
for (const dropinName of searchDropins) {
|
|
108
|
+
const dropinSlotData = slotRegistry.dropins[dropinName];
|
|
109
|
+
if (!dropinSlotData)
|
|
110
|
+
continue;
|
|
111
|
+
for (const [cName, cData] of Object.entries(dropinSlotData.containers)) {
|
|
112
|
+
if (cName.toLowerCase() !== containerLower)
|
|
113
|
+
continue;
|
|
114
|
+
const slot = cData.slots.find((s) => s.name.toLowerCase() === slotLower);
|
|
115
|
+
if (!slot)
|
|
116
|
+
continue;
|
|
117
|
+
const dropinContainerData = containerRegistry.dropins[dropinName];
|
|
118
|
+
const containerEntry = dropinContainerData?.containers.find((c) => c.name.toLowerCase() === containerLower);
|
|
119
|
+
matches.push({
|
|
120
|
+
dropin: dropinName,
|
|
121
|
+
containerName: cName,
|
|
122
|
+
slot,
|
|
123
|
+
renderImport: dropinContainerData?.renderImport ??
|
|
124
|
+
`@dropins/${dropinName}/render.js`,
|
|
125
|
+
containerImportPath: containerEntry?.importPath ??
|
|
126
|
+
`@dropins/${dropinName}/containers/${cName}.js`,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (matches.length === 0) {
|
|
131
|
+
const knownContainers = Object.entries(slotRegistry.dropins)
|
|
132
|
+
.flatMap(([d, data]) => Object.keys(data.containers).map((c) => `${d} → ${c}`))
|
|
133
|
+
.slice(0, 12);
|
|
134
|
+
return formatSuccessResponse(`Slot "${params.slotName}" not found in container "${params.containerName}"`, {
|
|
135
|
+
error: "Slot not found",
|
|
136
|
+
searched: params.dropin ?? "all dropins",
|
|
137
|
+
hint: `Use list_slots to browse available slots. Sample known containers: ${knownContainers.join(", ")}`,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
if (matches.length > 1) {
|
|
141
|
+
return formatSuccessResponse(`Slot "${params.slotName}" exists in multiple dropins - specify the dropin parameter`, {
|
|
142
|
+
ambiguousMatches: matches.map((m) => ({
|
|
143
|
+
dropin: m.dropin,
|
|
144
|
+
containerName: m.containerName,
|
|
145
|
+
slotName: m.slot.name,
|
|
146
|
+
})),
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
const match = matches[0];
|
|
150
|
+
const { contextProps, allMethodNames, slotCode } = buildSlotCodeParts(match.slot, slotRegistry);
|
|
151
|
+
const codeScaffold = generateScaffold(slotCode, match);
|
|
152
|
+
return formatSuccessResponse(`Slot "${match.slot.name}" scaffolded for ${match.containerName} (${match.dropin})`, {
|
|
153
|
+
dropin: match.dropin,
|
|
154
|
+
containerName: match.containerName,
|
|
155
|
+
slotName: match.slot.name,
|
|
156
|
+
description: match.slot.description || "(no description)",
|
|
157
|
+
contextType: match.slot.contextType,
|
|
158
|
+
contextProps: contextProps.length > 0
|
|
159
|
+
? contextProps
|
|
160
|
+
: match.slot.contextType &&
|
|
161
|
+
match.slot.contextType !== "DefaultSlotContext"
|
|
162
|
+
? [
|
|
163
|
+
`named type: ${match.slot.contextType} - use list_slots to inspect its shape`,
|
|
164
|
+
]
|
|
165
|
+
: ["no typed props - default slot context"],
|
|
166
|
+
availableMethods: allMethodNames,
|
|
167
|
+
customMethods: match.slot.customMethods ?? [],
|
|
168
|
+
isDynamic: match.slot.dynamic,
|
|
169
|
+
codeScaffold,
|
|
170
|
+
instructions: [
|
|
171
|
+
`Import render from: ${match.renderImport} (aliased as provider)`,
|
|
172
|
+
`Import container from: ${match.containerImportPath}`,
|
|
173
|
+
"Pass the slot function inside the slots prop of provider.render(Container, { slots: {...} })(element)",
|
|
174
|
+
contextProps.length > 0
|
|
175
|
+
? `Destructure context props from ctx: { ${contextProps.join(", ")} }`
|
|
176
|
+
: "Use ctx.appendChild(element) to inject content into the slot",
|
|
177
|
+
match.slot.customMethods?.length
|
|
178
|
+
? `Extra methods on ctx: ${match.slot.customMethods.join(", ")}`
|
|
179
|
+
: "Only default slot methods are available on ctx",
|
|
180
|
+
match.slot.dynamic
|
|
181
|
+
? "Dynamic slot: this function is called once per item in a list (e.g. per product, per shipping method)"
|
|
182
|
+
: "Static slot: this function is called once per container instance",
|
|
183
|
+
],
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
catch (error) {
|
|
187
|
+
return formatExceptionResponse(error, "scaffolding slot");
|
|
188
|
+
}
|
|
189
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const SearchCommerceDocsSchema: z.ZodObject<{
|
|
3
|
+
query: z.ZodString;
|
|
4
|
+
maxResults: z.ZodOptional<z.ZodNumber>;
|
|
5
|
+
}, "strip", z.ZodTypeAny, {
|
|
6
|
+
query: string;
|
|
7
|
+
maxResults?: number | undefined;
|
|
8
|
+
}, {
|
|
9
|
+
query: string;
|
|
10
|
+
maxResults?: number | undefined;
|
|
11
|
+
}>;
|
|
12
|
+
export declare function searchCommerceDocs(params: z.infer<typeof SearchCommerceDocsSchema>): Promise<{
|
|
13
|
+
success: boolean;
|
|
14
|
+
message: string;
|
|
15
|
+
data: unknown;
|
|
16
|
+
}>;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { execFile } from "node:child_process";
|
|
3
|
+
import { promisify } from "node:util";
|
|
4
|
+
import { formatSuccessResponse, formatExceptionResponse, } from "../common/response-handling.js";
|
|
5
|
+
const execFileAsync = promisify(execFile);
|
|
6
|
+
const RAG_ENDPOINTS = {
|
|
7
|
+
prod: "https://commerce-docs-prod-endpoint-d0ctgyebe7bec8e6.a02.azurefd.net/api/query",
|
|
8
|
+
stage: "https://commerce-docs-dev-endpoint-hbe8ceethxh9d2g3.a02.azurefd.net/api/query",
|
|
9
|
+
};
|
|
10
|
+
export const SearchCommerceDocsSchema = z.object({
|
|
11
|
+
query: z
|
|
12
|
+
.string()
|
|
13
|
+
.describe('Natural language search query (e.g. "how to add a custom payment method", "CORS configuration for storefront")'),
|
|
14
|
+
maxResults: z
|
|
15
|
+
.number()
|
|
16
|
+
.optional()
|
|
17
|
+
.describe("Maximum number of results to return (default: 5)"),
|
|
18
|
+
});
|
|
19
|
+
async function getImsToken() {
|
|
20
|
+
const { stdout } = await execFileAsync("aio", ["config", "get", "ims.contexts.cli.access_token.token"], { timeout: 10000 });
|
|
21
|
+
const token = stdout.trim();
|
|
22
|
+
if (!token) {
|
|
23
|
+
throw new Error("No IMS token returned. Run: aio auth login");
|
|
24
|
+
}
|
|
25
|
+
return token;
|
|
26
|
+
}
|
|
27
|
+
async function getAioEnvironment() {
|
|
28
|
+
try {
|
|
29
|
+
const { stdout } = await execFileAsync("aio", ["config", "get", "cli.env"], { timeout: 5000 });
|
|
30
|
+
if (stdout.trim() === "stage")
|
|
31
|
+
return "stage";
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
}
|
|
35
|
+
return "prod";
|
|
36
|
+
}
|
|
37
|
+
export async function searchCommerceDocs(params) {
|
|
38
|
+
try {
|
|
39
|
+
const [token, env] = await Promise.all([
|
|
40
|
+
getImsToken(),
|
|
41
|
+
getAioEnvironment(),
|
|
42
|
+
]);
|
|
43
|
+
const endpoint = RAG_ENDPOINTS[env];
|
|
44
|
+
const maxResults = params.maxResults ?? 5;
|
|
45
|
+
const controller = new AbortController();
|
|
46
|
+
const timeout = setTimeout(() => controller.abort(), 30000);
|
|
47
|
+
let response;
|
|
48
|
+
try {
|
|
49
|
+
response = await fetch(endpoint, {
|
|
50
|
+
method: "POST",
|
|
51
|
+
headers: {
|
|
52
|
+
"Content-Type": "application/json",
|
|
53
|
+
Authorization: `Bearer ${token}`,
|
|
54
|
+
},
|
|
55
|
+
body: JSON.stringify({
|
|
56
|
+
query: params.query,
|
|
57
|
+
count: maxResults,
|
|
58
|
+
}),
|
|
59
|
+
signal: controller.signal,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
finally {
|
|
63
|
+
clearTimeout(timeout);
|
|
64
|
+
}
|
|
65
|
+
if (!response.ok) {
|
|
66
|
+
if (response.status === 401) {
|
|
67
|
+
throw new Error("IMS token is expired or invalid. Run: aio auth login");
|
|
68
|
+
}
|
|
69
|
+
if (response.status === 429) {
|
|
70
|
+
throw new Error("Rate limit exceeded. Try again later.");
|
|
71
|
+
}
|
|
72
|
+
throw new Error(`Documentation service returned HTTP ${response.status}`);
|
|
73
|
+
}
|
|
74
|
+
const payload = (await response.json());
|
|
75
|
+
if (!payload.success || !payload.results) {
|
|
76
|
+
throw new Error("Invalid response from documentation service");
|
|
77
|
+
}
|
|
78
|
+
const results = payload.results.map((doc, i) => ({
|
|
79
|
+
rank: i + 1,
|
|
80
|
+
source: doc.metadata?.source ?? doc.source ?? "Unknown",
|
|
81
|
+
content: doc.content ?? "",
|
|
82
|
+
score: doc.score,
|
|
83
|
+
metadata: doc.metadata ?? {},
|
|
84
|
+
}));
|
|
85
|
+
return formatSuccessResponse(`Found ${results.length} result(s) for: "${params.query}"`, {
|
|
86
|
+
query: params.query,
|
|
87
|
+
index: payload.index,
|
|
88
|
+
indexSelection: payload.indexSelection ?? "backend-selected",
|
|
89
|
+
confidence: payload.confidence,
|
|
90
|
+
results,
|
|
91
|
+
note: "For structured dropin API data (slots, events, containers, GraphQL), use the dedicated registry tools instead.",
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
96
|
+
if (message.includes("ENOENT") || message.includes("not found")) {
|
|
97
|
+
return formatExceptionResponse(new Error("The aio CLI is required for documentation search. Install it with: npm install -g @adobe/aio-cli && aio auth login"), "searching commerce documentation");
|
|
98
|
+
}
|
|
99
|
+
return formatExceptionResponse(error, "searching commerce documentation");
|
|
100
|
+
}
|
|
101
|
+
}
|