@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,13 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const CheckConfigSchema: z.ZodObject<{
|
|
3
|
+
projectDir: z.ZodString;
|
|
4
|
+
}, "strip", z.ZodTypeAny, {
|
|
5
|
+
projectDir: string;
|
|
6
|
+
}, {
|
|
7
|
+
projectDir: string;
|
|
8
|
+
}>;
|
|
9
|
+
export declare function checkConfig(params: z.infer<typeof CheckConfigSchema>): Promise<{
|
|
10
|
+
success: boolean;
|
|
11
|
+
message: string;
|
|
12
|
+
data: unknown;
|
|
13
|
+
}>;
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { createHash } from "crypto";
|
|
2
|
+
import { existsSync, readFileSync, readdirSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { formatSuccessResponse, formatExceptionResponse, } from "../common/response-handling.js";
|
|
6
|
+
import { readPackageJson, readConfigJson, listBlocks, readBlockFile, listInitializers, projectDirGuard, extractDropinDependencies, extractImportsFromBlock, } from "../common/project-reader.js";
|
|
7
|
+
const DROPIN_ENTRY_FILES = ["api.js", "render.js", "fragments.js"];
|
|
8
|
+
function fileHash(filePath) {
|
|
9
|
+
if (!existsSync(filePath))
|
|
10
|
+
return null;
|
|
11
|
+
try {
|
|
12
|
+
return createHash("sha256").update(readFileSync(filePath)).digest("hex");
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function checkDropinFileIntegrity(projectDir) {
|
|
19
|
+
const issues = [];
|
|
20
|
+
const dropinsDir = join(projectDir, "scripts", "__dropins__");
|
|
21
|
+
const nodeModulesDropins = join(projectDir, "node_modules", "@dropins");
|
|
22
|
+
if (!existsSync(dropinsDir) || !existsSync(nodeModulesDropins))
|
|
23
|
+
return issues;
|
|
24
|
+
let dropinFolders;
|
|
25
|
+
try {
|
|
26
|
+
dropinFolders = readdirSync(dropinsDir);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return issues;
|
|
30
|
+
}
|
|
31
|
+
for (const dropin of dropinFolders) {
|
|
32
|
+
const installedDir = join(dropinsDir, dropin);
|
|
33
|
+
const sourceDir = join(nodeModulesDropins, dropin);
|
|
34
|
+
if (!existsSync(sourceDir))
|
|
35
|
+
continue;
|
|
36
|
+
for (const file of DROPIN_ENTRY_FILES) {
|
|
37
|
+
const installedPath = join(installedDir, file);
|
|
38
|
+
const sourcePath = join(sourceDir, file);
|
|
39
|
+
const installedHash = fileHash(installedPath);
|
|
40
|
+
const sourceHash = fileHash(sourcePath);
|
|
41
|
+
if (installedHash === null || sourceHash === null)
|
|
42
|
+
continue;
|
|
43
|
+
if (installedHash !== sourceHash) {
|
|
44
|
+
issues.push({
|
|
45
|
+
level: "warning",
|
|
46
|
+
area: "dropin-integrity",
|
|
47
|
+
message: `scripts/__dropins__/${dropin}/${file} has been manually modified and will be silently overwritten on the next npm update`,
|
|
48
|
+
fix: `Revert changes to scripts/__dropins__/${dropin}/${file} and use slots, events, or CSS custom properties to customise dropin behaviour instead`,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return issues;
|
|
54
|
+
}
|
|
55
|
+
export const CheckConfigSchema = z.object({
|
|
56
|
+
projectDir: z
|
|
57
|
+
.string()
|
|
58
|
+
.describe("Absolute path to the merchant storefront project root"),
|
|
59
|
+
});
|
|
60
|
+
export async function checkConfig(params) {
|
|
61
|
+
try {
|
|
62
|
+
const guard = projectDirGuard(params.projectDir);
|
|
63
|
+
if (guard)
|
|
64
|
+
return guard;
|
|
65
|
+
const issues = [];
|
|
66
|
+
const pkg = readPackageJson(params.projectDir);
|
|
67
|
+
if (!pkg) {
|
|
68
|
+
issues.push({
|
|
69
|
+
level: "error",
|
|
70
|
+
area: "package.json",
|
|
71
|
+
message: "No package.json found in project root",
|
|
72
|
+
fix: "Run npm init or ensure you are pointing to the correct directory",
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
const dropinDeps = extractDropinDependencies(pkg);
|
|
77
|
+
const hasTools = dropinDeps.some((d) => d.name === "@dropins/tools");
|
|
78
|
+
if (!hasTools) {
|
|
79
|
+
issues.push({
|
|
80
|
+
level: "error",
|
|
81
|
+
area: "dependencies",
|
|
82
|
+
message: "@dropins/tools is not installed — required by all dropins",
|
|
83
|
+
fix: "Run: npm install @dropins/tools",
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
const prodDropins = dropinDeps.filter((d) => !d.isDev &&
|
|
87
|
+
d.name !== "@dropins/tools" &&
|
|
88
|
+
d.name !== "@dropins/build-tools");
|
|
89
|
+
if (prodDropins.length === 0) {
|
|
90
|
+
issues.push({
|
|
91
|
+
level: "warning",
|
|
92
|
+
area: "dependencies",
|
|
93
|
+
message: "No dropin dependencies found (besides tools)",
|
|
94
|
+
fix: "Install at least one dropin, e.g.: npm install @dropins/storefront-cart",
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
const tildeVersions = dropinDeps.filter((d) => d.version.startsWith("~"));
|
|
98
|
+
const caretVersions = dropinDeps.filter((d) => d.version.startsWith("^"));
|
|
99
|
+
if (caretVersions.length > 0) {
|
|
100
|
+
issues.push({
|
|
101
|
+
level: "info",
|
|
102
|
+
area: "versions",
|
|
103
|
+
message: `${caretVersions.length} dropin(s) use caret (^) versioning which may pull breaking changes`,
|
|
104
|
+
fix: "Consider using tilde (~) for dropin versions to restrict to patch updates",
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
const mixedVersioning = tildeVersions.length > 0 && caretVersions.length > 0;
|
|
108
|
+
if (mixedVersioning) {
|
|
109
|
+
issues.push({
|
|
110
|
+
level: "warning",
|
|
111
|
+
area: "versions",
|
|
112
|
+
message: "Mixed versioning strategies detected across dropin dependencies",
|
|
113
|
+
fix: "Standardize on either tilde (~) or caret (^) for all dropin dependencies",
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
const config = readConfigJson(params.projectDir);
|
|
118
|
+
if (!config) {
|
|
119
|
+
issues.push({
|
|
120
|
+
level: "error",
|
|
121
|
+
area: "config.json",
|
|
122
|
+
message: "No config.json or demo-config.json found",
|
|
123
|
+
fix: "Create a config.json with commerce-endpoint and commerce-core-endpoint",
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
const defaultConfig = config.public?.default;
|
|
128
|
+
if (!defaultConfig) {
|
|
129
|
+
issues.push({
|
|
130
|
+
level: "error",
|
|
131
|
+
area: "config.json",
|
|
132
|
+
message: "Missing public.default configuration block",
|
|
133
|
+
fix: "Add a public.default object with commerce endpoints",
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
if (!defaultConfig["commerce-endpoint"]) {
|
|
138
|
+
issues.push({
|
|
139
|
+
level: "error",
|
|
140
|
+
area: "config.json",
|
|
141
|
+
message: "Missing commerce-endpoint — catalog service GraphQL endpoint",
|
|
142
|
+
fix: 'Add "commerce-endpoint" pointing to your catalog service GraphQL URL',
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
if (!defaultConfig["commerce-core-endpoint"]) {
|
|
146
|
+
issues.push({
|
|
147
|
+
level: "warning",
|
|
148
|
+
area: "config.json",
|
|
149
|
+
message: "Missing commerce-core-endpoint — core Magento GraphQL endpoint",
|
|
150
|
+
fix: 'Add "commerce-core-endpoint" pointing to your core GraphQL URL',
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
if (!defaultConfig.headers?.cs) {
|
|
154
|
+
issues.push({
|
|
155
|
+
level: "warning",
|
|
156
|
+
area: "config.json",
|
|
157
|
+
message: "Missing catalog service headers (headers.cs)",
|
|
158
|
+
fix: "Add headers.cs with required keys like Magento-Store-Code or AC-View-ID",
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
if (!defaultConfig.analytics) {
|
|
162
|
+
issues.push({
|
|
163
|
+
level: "info",
|
|
164
|
+
area: "config.json",
|
|
165
|
+
message: "No analytics configuration found",
|
|
166
|
+
fix: "Add analytics block for commerce event tracking (base-currency-code, environment, store-id, etc.)",
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
const blocks = listBlocks(params.projectDir);
|
|
172
|
+
const commerceBlocks = blocks.filter((b) => {
|
|
173
|
+
if (!b.hasJs)
|
|
174
|
+
return false;
|
|
175
|
+
const js = readBlockFile(params.projectDir, b.name);
|
|
176
|
+
return (js !== null &&
|
|
177
|
+
extractImportsFromBlock(js).dropinImports.some((p) => p.includes("@dropins/storefront-")));
|
|
178
|
+
});
|
|
179
|
+
const blocksWithoutCss = commerceBlocks.filter((b) => !b.hasCss);
|
|
180
|
+
const blocksWithoutJs = blocks.filter((b) => !b.hasJs && (b.hasCss || b.name.startsWith("commerce-")));
|
|
181
|
+
if (blocksWithoutJs.length > 0) {
|
|
182
|
+
issues.push({
|
|
183
|
+
level: "warning",
|
|
184
|
+
area: "blocks",
|
|
185
|
+
message: `Blocks missing JS files: ${blocksWithoutJs.map((b) => b.name).join(", ")}`,
|
|
186
|
+
fix: "Each block directory should have a matching JS file (e.g., product-details/product-details.js)",
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
if (blocksWithoutCss.length > 0) {
|
|
190
|
+
issues.push({
|
|
191
|
+
level: "info",
|
|
192
|
+
area: "blocks",
|
|
193
|
+
message: `Dropin blocks missing CSS files: ${blocksWithoutCss.map((b) => b.name).join(", ")}`,
|
|
194
|
+
fix: "Consider adding CSS files for custom styling",
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
const initializers = listInitializers(params.projectDir);
|
|
198
|
+
if (initializers.length === 0 && commerceBlocks.length > 0) {
|
|
199
|
+
issues.push({
|
|
200
|
+
level: "warning",
|
|
201
|
+
area: "initializers",
|
|
202
|
+
message: "Dropin blocks exist but no dropin initializers found",
|
|
203
|
+
fix: "Create initializer files in scripts/initializers/ for each dropin (e.g., cart.js, checkout.js)",
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
issues.push(...checkDropinFileIntegrity(params.projectDir));
|
|
207
|
+
const errors = issues.filter((i) => i.level === "error");
|
|
208
|
+
const warnings = issues.filter((i) => i.level === "warning");
|
|
209
|
+
const infos = issues.filter((i) => i.level === "info");
|
|
210
|
+
const statusMessage = errors.length > 0
|
|
211
|
+
? `Configuration has ${errors.length} error(s), ${warnings.length} warning(s), ${infos.length} info(s)`
|
|
212
|
+
: warnings.length > 0
|
|
213
|
+
? `Configuration has ${warnings.length} warning(s) and ${infos.length} info(s)`
|
|
214
|
+
: "Configuration looks good";
|
|
215
|
+
return formatSuccessResponse(statusMessage, {
|
|
216
|
+
valid: errors.length === 0,
|
|
217
|
+
summary: {
|
|
218
|
+
errors: errors.length,
|
|
219
|
+
warnings: warnings.length,
|
|
220
|
+
info: infos.length,
|
|
221
|
+
},
|
|
222
|
+
issues,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
catch (error) {
|
|
226
|
+
return formatExceptionResponse(error, "checking configuration");
|
|
227
|
+
}
|
|
228
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const ExplainEventFlowSchema: z.ZodObject<{
|
|
3
|
+
eventName: z.ZodString;
|
|
4
|
+
dropin: z.ZodOptional<z.ZodString>;
|
|
5
|
+
}, "strip", z.ZodTypeAny, {
|
|
6
|
+
eventName: string;
|
|
7
|
+
dropin?: string | undefined;
|
|
8
|
+
}, {
|
|
9
|
+
eventName: string;
|
|
10
|
+
dropin?: string | undefined;
|
|
11
|
+
}>;
|
|
12
|
+
export declare function explainEventFlow(params: z.infer<typeof ExplainEventFlowSchema>): Promise<{
|
|
13
|
+
success: boolean;
|
|
14
|
+
message: string;
|
|
15
|
+
data: unknown;
|
|
16
|
+
}>;
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { formatSuccessResponse, formatExceptionResponse, } from "../common/response-handling.js";
|
|
3
|
+
import { getEventCatalog } from "../common/registry-loader.js";
|
|
4
|
+
import { sanitizeForComment } from "../common/sanitize.js";
|
|
5
|
+
export const ExplainEventFlowSchema = z.object({
|
|
6
|
+
eventName: z
|
|
7
|
+
.string()
|
|
8
|
+
.describe('Exact or partial event name to look up (e.g. "cart/data", "cart", "order/placed"). Prefix matching is supported: "cart" matches all cart/* events.'),
|
|
9
|
+
dropin: z
|
|
10
|
+
.string()
|
|
11
|
+
.optional()
|
|
12
|
+
.describe('Filter results to events emitted or consumed by a specific dropin (e.g. "storefront-cart").'),
|
|
13
|
+
});
|
|
14
|
+
const RESERVED_VAR_NAMES = new Set([
|
|
15
|
+
"string",
|
|
16
|
+
"number",
|
|
17
|
+
"boolean",
|
|
18
|
+
"object",
|
|
19
|
+
"symbol",
|
|
20
|
+
"bigint",
|
|
21
|
+
"null",
|
|
22
|
+
"undefined",
|
|
23
|
+
"void",
|
|
24
|
+
"any",
|
|
25
|
+
"never",
|
|
26
|
+
"unknown",
|
|
27
|
+
]);
|
|
28
|
+
function payloadVarName(payloadType) {
|
|
29
|
+
if (!payloadType)
|
|
30
|
+
return "payload";
|
|
31
|
+
if (payloadType.includes("{"))
|
|
32
|
+
return "data";
|
|
33
|
+
const stripped = payloadType.replace(/\[\]$/, "").replace(/\s+/g, "");
|
|
34
|
+
if (stripped.endsWith("Model")) {
|
|
35
|
+
return stripped.replace("Model", "").toLowerCase() || "data";
|
|
36
|
+
}
|
|
37
|
+
const candidate = stripped.toLowerCase().replace(/[^a-z]/g, "") || "payload";
|
|
38
|
+
return RESERVED_VAR_NAMES.has(candidate) ? "payload" : candidate;
|
|
39
|
+
}
|
|
40
|
+
function refLabel(ref) {
|
|
41
|
+
return ref.dropin.startsWith("boilerplate/")
|
|
42
|
+
? ref.dropin.replace("boilerplate/", "block:")
|
|
43
|
+
: ref.dropin;
|
|
44
|
+
}
|
|
45
|
+
function uniqueLabels(refs) {
|
|
46
|
+
return [...new Set(refs.map(refLabel))];
|
|
47
|
+
}
|
|
48
|
+
function flowDirection(emittedBy, consumedBy) {
|
|
49
|
+
const isDropin = (r) => !r.dropin.startsWith("boilerplate/");
|
|
50
|
+
const emitDropin = emittedBy.some(isDropin);
|
|
51
|
+
const consumeDropin = consumedBy.some(isDropin);
|
|
52
|
+
const emitBlock = emittedBy.some((r) => !isDropin(r));
|
|
53
|
+
const consumeBlock = consumedBy.some((r) => !isDropin(r));
|
|
54
|
+
if (emittedBy.length === 0 && consumedBy.length === 0)
|
|
55
|
+
return "unknown";
|
|
56
|
+
if (emittedBy.length === 0)
|
|
57
|
+
return "consumed-only";
|
|
58
|
+
if (consumedBy.length === 0)
|
|
59
|
+
return "emitted-only";
|
|
60
|
+
if (emitDropin && consumeDropin && consumeBlock)
|
|
61
|
+
return "dropin-to-many";
|
|
62
|
+
if (emitDropin && consumeDropin)
|
|
63
|
+
return "dropin-to-dropin";
|
|
64
|
+
if (emitDropin && consumeBlock)
|
|
65
|
+
return "dropin-to-block";
|
|
66
|
+
if (emitBlock && consumeDropin)
|
|
67
|
+
return "block-to-dropin";
|
|
68
|
+
return "block-to-block";
|
|
69
|
+
}
|
|
70
|
+
function buildFlowSummary(name, emittedBy, consumedBy) {
|
|
71
|
+
const emitterLabels = emittedBy.length > 0
|
|
72
|
+
? uniqueLabels(emittedBy).join(", ")
|
|
73
|
+
: "unknown emitter";
|
|
74
|
+
const consumerLabels = consumedBy.length > 0
|
|
75
|
+
? uniqueLabels(consumedBy).join(", ")
|
|
76
|
+
: "no known consumers";
|
|
77
|
+
return `${name}: emitted by ${emitterLabels} -> consumed by ${consumerLabels}`;
|
|
78
|
+
}
|
|
79
|
+
function buildListenCode(event) {
|
|
80
|
+
const varName = payloadVarName(event.payloadType);
|
|
81
|
+
const payloadComment = event.payloadType
|
|
82
|
+
? ` // Payload type: ${sanitizeForComment(event.payloadType)}\n`
|
|
83
|
+
: "";
|
|
84
|
+
const descComment = event.description
|
|
85
|
+
? ` // ${sanitizeForComment(event.description)}\n`
|
|
86
|
+
: "";
|
|
87
|
+
return `import { events } from '@dropins/tools/event-bus.js';
|
|
88
|
+
|
|
89
|
+
events.on('${event.name}', (${varName}) => {
|
|
90
|
+
${payloadComment}${descComment}
|
|
91
|
+
// Handle the event here
|
|
92
|
+
console.log('DEBUG ${event.name}:', ${varName});
|
|
93
|
+
});`;
|
|
94
|
+
}
|
|
95
|
+
function buildEmitCode(event) {
|
|
96
|
+
if (event.emittedBy.length === 0)
|
|
97
|
+
return null;
|
|
98
|
+
if (!event.payloadType) {
|
|
99
|
+
return `import { events } from '@dropins/tools/event-bus.js';
|
|
100
|
+
|
|
101
|
+
events.emit('${event.name}');`;
|
|
102
|
+
}
|
|
103
|
+
const varName = payloadVarName(event.payloadType);
|
|
104
|
+
return `import { events } from '@dropins/tools/event-bus.js';
|
|
105
|
+
|
|
106
|
+
const ${varName} = { /* ${event.payloadType} */ };
|
|
107
|
+
events.emit('${event.name}', ${varName});`;
|
|
108
|
+
}
|
|
109
|
+
function tieBreakScore(event) {
|
|
110
|
+
let score = 0;
|
|
111
|
+
if (event.emittedBy.length > 0)
|
|
112
|
+
score += 2;
|
|
113
|
+
if (event.consumedBy.length > 0)
|
|
114
|
+
score += 2;
|
|
115
|
+
if (event.payloadType)
|
|
116
|
+
score += 1;
|
|
117
|
+
if (event.description)
|
|
118
|
+
score += 1;
|
|
119
|
+
score -= event.name.length * 0.01;
|
|
120
|
+
return score;
|
|
121
|
+
}
|
|
122
|
+
function matchEvents(events, query, dropinFilter, maxResults) {
|
|
123
|
+
const lowerQuery = query.toLowerCase().trim();
|
|
124
|
+
const byTier = {
|
|
125
|
+
exact: [],
|
|
126
|
+
prefix: [],
|
|
127
|
+
substring: [],
|
|
128
|
+
};
|
|
129
|
+
for (const event of events) {
|
|
130
|
+
const lowerName = event.name.toLowerCase();
|
|
131
|
+
if (dropinFilter) {
|
|
132
|
+
const involvedDropins = [
|
|
133
|
+
...event.emittedBy.map((r) => r.dropin),
|
|
134
|
+
...event.consumedBy.map((r) => r.dropin),
|
|
135
|
+
];
|
|
136
|
+
if (!involvedDropins.includes(dropinFilter))
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (lowerName === lowerQuery) {
|
|
140
|
+
byTier.exact.push(event);
|
|
141
|
+
}
|
|
142
|
+
else if (lowerName.startsWith(lowerQuery + "/")) {
|
|
143
|
+
byTier.prefix.push(event);
|
|
144
|
+
}
|
|
145
|
+
else if (lowerName.includes(lowerQuery)) {
|
|
146
|
+
byTier.substring.push(event);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
const sortByScore = (a, b) => tieBreakScore(b) - tieBreakScore(a);
|
|
150
|
+
const orderedTiers = [
|
|
151
|
+
[byTier.exact.sort(sortByScore), "exact"],
|
|
152
|
+
[byTier.prefix.sort(sortByScore), "prefix"],
|
|
153
|
+
[byTier.substring.sort(sortByScore), "substring"],
|
|
154
|
+
];
|
|
155
|
+
const results = [];
|
|
156
|
+
for (const [bucket, tier] of orderedTiers) {
|
|
157
|
+
for (const event of bucket) {
|
|
158
|
+
if (results.length >= maxResults)
|
|
159
|
+
break;
|
|
160
|
+
const emittedBy = [
|
|
161
|
+
...new Map((event.emittedBy ?? []).map((r) => [`${r.dropin}|${r.file}`, r])).values(),
|
|
162
|
+
];
|
|
163
|
+
const consumedBy = [
|
|
164
|
+
...new Map((event.consumedBy ?? []).map((r) => [`${r.dropin}|${r.file}`, r])).values(),
|
|
165
|
+
];
|
|
166
|
+
results.push({
|
|
167
|
+
name: event.name,
|
|
168
|
+
matchTier: tier,
|
|
169
|
+
payloadType: event.payloadType || "(unknown)",
|
|
170
|
+
description: event.description || "(no description)",
|
|
171
|
+
emittedBy,
|
|
172
|
+
consumedBy,
|
|
173
|
+
direction: flowDirection(emittedBy, consumedBy),
|
|
174
|
+
flowSummary: buildFlowSummary(event.name, emittedBy, consumedBy),
|
|
175
|
+
listenCode: buildListenCode(event),
|
|
176
|
+
emitCode: buildEmitCode(event),
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
if (results.length >= maxResults)
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
return results;
|
|
183
|
+
}
|
|
184
|
+
export async function explainEventFlow(params) {
|
|
185
|
+
try {
|
|
186
|
+
const catalog = getEventCatalog();
|
|
187
|
+
const allEvents = catalog.events ?? [];
|
|
188
|
+
const matched = matchEvents(allEvents, params.eventName, params.dropin, 10);
|
|
189
|
+
if (matched.length === 0) {
|
|
190
|
+
const namespaces = [
|
|
191
|
+
...new Set(allEvents.map((e) => e.name.split("/")[0]).filter(Boolean)),
|
|
192
|
+
].sort();
|
|
193
|
+
return formatSuccessResponse(`No events found matching "${params.eventName}"`, {
|
|
194
|
+
searched: params.eventName,
|
|
195
|
+
dropinFilter: params.dropin ?? null,
|
|
196
|
+
hint: `Use list_events to browse all events. Known namespaces: ${namespaces.join(", ")}`,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
const tierCounts = matched.reduce((acc, e) => {
|
|
200
|
+
acc[e.matchTier] = (acc[e.matchTier] ?? 0) + 1;
|
|
201
|
+
return acc;
|
|
202
|
+
}, {});
|
|
203
|
+
const summary = matched.length === 1
|
|
204
|
+
? `Found 1 event matching "${params.eventName}"`
|
|
205
|
+
: `Found ${matched.length} events matching "${params.eventName}" (${Object.entries(tierCounts)
|
|
206
|
+
.map(([t, c]) => `${c} ${t}`)
|
|
207
|
+
.join(", ")})`;
|
|
208
|
+
return formatSuccessResponse(summary, {
|
|
209
|
+
query: params.eventName,
|
|
210
|
+
dropinFilter: params.dropin ?? null,
|
|
211
|
+
totalMatched: matched.length,
|
|
212
|
+
events: matched,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
catch (error) {
|
|
216
|
+
return formatExceptionResponse(error, "explaining event flow");
|
|
217
|
+
}
|
|
218
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const GetUpgradeDiffSchema: z.ZodObject<{
|
|
3
|
+
projectDir: z.ZodString;
|
|
4
|
+
}, "strip", z.ZodTypeAny, {
|
|
5
|
+
projectDir: string;
|
|
6
|
+
}, {
|
|
7
|
+
projectDir: string;
|
|
8
|
+
}>;
|
|
9
|
+
export declare function getUpgradeDiff(params: z.infer<typeof GetUpgradeDiffSchema>): Promise<{
|
|
10
|
+
success: boolean;
|
|
11
|
+
message: string;
|
|
12
|
+
data: unknown;
|
|
13
|
+
}>;
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { formatSuccessResponse, formatExceptionResponse, } from "../common/response-handling.js";
|
|
3
|
+
import { getContainerRegistry, getApiFunctionRegistry, } from "../common/registry-loader.js";
|
|
4
|
+
import { projectDirGuard, readPackageJson, extractDropinDependencies, listBlocks, readBlockFile, extractImportsFromBlock, } from "../common/project-reader.js";
|
|
5
|
+
export const GetUpgradeDiffSchema = z.object({
|
|
6
|
+
projectDir: z
|
|
7
|
+
.string()
|
|
8
|
+
.describe("Absolute path to the merchant storefront project root"),
|
|
9
|
+
});
|
|
10
|
+
function parseVersion(versionStr) {
|
|
11
|
+
return versionStr.replace(/^[~^>=<]+/, "");
|
|
12
|
+
}
|
|
13
|
+
function compareVersions(a, b) {
|
|
14
|
+
const pa = a.split(".").map(Number);
|
|
15
|
+
const pb = b.split(".").map(Number);
|
|
16
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
17
|
+
const na = pa[i] ?? 0;
|
|
18
|
+
const nb = pb[i] ?? 0;
|
|
19
|
+
if (na !== nb)
|
|
20
|
+
return na - nb;
|
|
21
|
+
}
|
|
22
|
+
return 0;
|
|
23
|
+
}
|
|
24
|
+
export async function getUpgradeDiff(params) {
|
|
25
|
+
try {
|
|
26
|
+
const guard = projectDirGuard(params.projectDir);
|
|
27
|
+
if (guard)
|
|
28
|
+
return guard;
|
|
29
|
+
const pkg = readPackageJson(params.projectDir);
|
|
30
|
+
if (!pkg) {
|
|
31
|
+
return formatSuccessResponse("No package.json found", {
|
|
32
|
+
error: "Cannot analyze versions without package.json",
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
const dropinDeps = extractDropinDependencies(pkg);
|
|
36
|
+
const containerRegistry = getContainerRegistry();
|
|
37
|
+
const apiRegistry = getApiFunctionRegistry();
|
|
38
|
+
const blocks = listBlocks(params.projectDir);
|
|
39
|
+
const blockUsage = {};
|
|
40
|
+
for (const block of blocks) {
|
|
41
|
+
const content = readBlockFile(params.projectDir, block.name);
|
|
42
|
+
if (!content)
|
|
43
|
+
continue;
|
|
44
|
+
const imports = extractImportsFromBlock(content);
|
|
45
|
+
const dropins = [];
|
|
46
|
+
const scopedContainers = [];
|
|
47
|
+
for (const imp of imports.dropinImports) {
|
|
48
|
+
const dropinMatch = imp.match(/@dropins\/(storefront-[^/]+)/);
|
|
49
|
+
if (!dropinMatch)
|
|
50
|
+
continue;
|
|
51
|
+
const d = dropinMatch[1];
|
|
52
|
+
if (!dropins.includes(d))
|
|
53
|
+
dropins.push(d);
|
|
54
|
+
if (imp.includes("/containers/")) {
|
|
55
|
+
const name = imp.split("/containers/").pop()?.replace(".js", "");
|
|
56
|
+
if (name)
|
|
57
|
+
scopedContainers.push({ dropin: d, name });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
blockUsage[block.name] = {
|
|
61
|
+
scopedContainers,
|
|
62
|
+
dropins,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
const diffs = [];
|
|
66
|
+
for (const dep of dropinDeps) {
|
|
67
|
+
const dropinName = dep.name.replace("@dropins/", "");
|
|
68
|
+
if (dropinName === "tools" || dropinName === "build-tools")
|
|
69
|
+
continue;
|
|
70
|
+
const containerData = containerRegistry.dropins[dropinName];
|
|
71
|
+
const apiData = apiRegistry.dropins?.[dropinName];
|
|
72
|
+
if (!containerData)
|
|
73
|
+
continue;
|
|
74
|
+
const installedVersion = parseVersion(dep.version);
|
|
75
|
+
const registryVersion = containerData.version ?? "unknown";
|
|
76
|
+
const blocksUsingDropin = Object.entries(blockUsage)
|
|
77
|
+
.filter(([_, usage]) => usage.dropins.includes(dropinName))
|
|
78
|
+
.map(([blockName]) => blockName);
|
|
79
|
+
const usedContainerNames = Object.values(blockUsage)
|
|
80
|
+
.flatMap((usage) => usage.scopedContainers)
|
|
81
|
+
.filter((sc) => sc.dropin === dropinName)
|
|
82
|
+
.map((sc) => sc.name);
|
|
83
|
+
const containers = containerData.containers.map((c) => {
|
|
84
|
+
return {
|
|
85
|
+
name: c.name,
|
|
86
|
+
availableSlots: c.slotNames ?? [],
|
|
87
|
+
usedInBlocks: Object.entries(blockUsage)
|
|
88
|
+
.filter(([_, usage]) => usage.scopedContainers.some((sc) => sc.dropin === dropinName && sc.name === c.name))
|
|
89
|
+
.map(([blockName]) => blockName),
|
|
90
|
+
};
|
|
91
|
+
});
|
|
92
|
+
const unusedContainers = containerData.containers
|
|
93
|
+
.map((c) => c.name)
|
|
94
|
+
.filter((name) => !usedContainerNames.includes(name));
|
|
95
|
+
const apiFunctions = (apiData?.functions ?? []).map((f) => ({
|
|
96
|
+
name: f.name,
|
|
97
|
+
signature: f.signature,
|
|
98
|
+
}));
|
|
99
|
+
const migrationNotes = [];
|
|
100
|
+
if (installedVersion !== registryVersion &&
|
|
101
|
+
registryVersion !== "unknown") {
|
|
102
|
+
const installedMajor = parseInt(installedVersion.split(".")[0], 10);
|
|
103
|
+
const registryMajor = parseInt(registryVersion.split(".")[0], 10);
|
|
104
|
+
if (registryMajor > installedMajor) {
|
|
105
|
+
migrationNotes.push(`Major version upgrade available (${installedVersion} → ${registryVersion}). Check for breaking changes.`);
|
|
106
|
+
}
|
|
107
|
+
else if (compareVersions(registryVersion, installedVersion) > 0) {
|
|
108
|
+
migrationNotes.push(`Minor/patch update available (${installedVersion} → ${registryVersion}).`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (unusedContainers.length > 0) {
|
|
112
|
+
migrationNotes.push(`${unusedContainers.length} container(s) available but not used: ${unusedContainers.join(", ")}`);
|
|
113
|
+
}
|
|
114
|
+
if (blocksUsingDropin.length === 0) {
|
|
115
|
+
migrationNotes.push("Dropin is installed but not used in any block");
|
|
116
|
+
}
|
|
117
|
+
diffs.push({
|
|
118
|
+
dropin: dropinName,
|
|
119
|
+
installedVersion,
|
|
120
|
+
registryVersion,
|
|
121
|
+
versionMatch: installedVersion === registryVersion,
|
|
122
|
+
blocksUsingDropin,
|
|
123
|
+
containers,
|
|
124
|
+
unusedContainers,
|
|
125
|
+
apiFunctions,
|
|
126
|
+
migrationNotes,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
const outdatedCount = diffs.filter((d) => !d.versionMatch).length;
|
|
130
|
+
const totalNotes = diffs.reduce((sum, d) => sum + d.migrationNotes.length, 0);
|
|
131
|
+
return formatSuccessResponse(`Analyzed ${diffs.length} dropins: ${outdatedCount} version mismatch(es), ${totalNotes} migration note(s)`, {
|
|
132
|
+
summary: {
|
|
133
|
+
totalDropins: diffs.length,
|
|
134
|
+
outdated: outdatedCount,
|
|
135
|
+
upToDate: diffs.length - outdatedCount,
|
|
136
|
+
totalMigrationNotes: totalNotes,
|
|
137
|
+
},
|
|
138
|
+
dropins: diffs,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
return formatExceptionResponse(error, "generating upgrade diff");
|
|
143
|
+
}
|
|
144
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const ListApiFunctionsSchema: z.ZodObject<{
|
|
3
|
+
dropin: z.ZodOptional<z.ZodString>;
|
|
4
|
+
}, "strip", z.ZodTypeAny, {
|
|
5
|
+
dropin?: string | undefined;
|
|
6
|
+
}, {
|
|
7
|
+
dropin?: string | undefined;
|
|
8
|
+
}>;
|
|
9
|
+
export declare function listApiFunctions(params: z.infer<typeof ListApiFunctionsSchema>): Promise<{
|
|
10
|
+
success: boolean;
|
|
11
|
+
message: string;
|
|
12
|
+
data: unknown;
|
|
13
|
+
}>;
|