@salesforce/storefront-next-dev 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.
Files changed (33) hide show
  1. package/LICENSE.txt +181 -0
  2. package/README.md +302 -0
  3. package/dist/cartridge-services/index.d.ts +60 -0
  4. package/dist/cartridge-services/index.d.ts.map +1 -0
  5. package/dist/cartridge-services/index.js +954 -0
  6. package/dist/cartridge-services/index.js.map +1 -0
  7. package/dist/cli.js +3373 -0
  8. package/dist/configs/react-router.config.d.ts +13 -0
  9. package/dist/configs/react-router.config.d.ts.map +1 -0
  10. package/dist/configs/react-router.config.js +36 -0
  11. package/dist/configs/react-router.config.js.map +1 -0
  12. package/dist/extensibility/templates/install-instructions.mdc.hbs +192 -0
  13. package/dist/extensibility/templates/uninstall-instructions.mdc.hbs +137 -0
  14. package/dist/index.d.ts +327 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +2606 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/mrt/sfnext-server-chunk-DUt5XHAg.mjs +1 -0
  19. package/dist/mrt/sfnext-server-jiti-DjnmHo-6.mjs +10 -0
  20. package/dist/mrt/sfnext-server-jiti-DjnmHo-6.mjs.map +1 -0
  21. package/dist/mrt/ssr.d.ts +19 -0
  22. package/dist/mrt/ssr.d.ts.map +1 -0
  23. package/dist/mrt/ssr.mjs +246 -0
  24. package/dist/mrt/ssr.mjs.map +1 -0
  25. package/dist/mrt/streamingHandler.d.ts +11 -0
  26. package/dist/mrt/streamingHandler.d.ts.map +1 -0
  27. package/dist/mrt/streamingHandler.mjs +255 -0
  28. package/dist/mrt/streamingHandler.mjs.map +1 -0
  29. package/dist/react-router/Scripts.d.ts +36 -0
  30. package/dist/react-router/Scripts.d.ts.map +1 -0
  31. package/dist/react-router/Scripts.js +68 -0
  32. package/dist/react-router/Scripts.js.map +1 -0
  33. package/package.json +157 -0
package/dist/index.js ADDED
@@ -0,0 +1,2606 @@
1
+ import path, { basename, extname, join, resolve } from "node:path";
2
+ import fs from "fs-extra";
3
+ import path$1, { dirname, join as join$1, relative, resolve as resolve$1 } from "path";
4
+ import { URL as URL$1, fileURLToPath, pathToFileURL } from "url";
5
+ import { parse } from "@babel/parser";
6
+ import { isJSXAttribute, isJSXElement, isJSXFragment, isJSXIdentifier, jsxClosingElement, jsxClosingFragment, jsxElement, jsxFragment, jsxIdentifier, jsxOpeningElement, jsxOpeningFragment, jsxText } from "@babel/types";
7
+ import { generate } from "@babel/generator";
8
+ import traverseModule from "@babel/traverse";
9
+ import fs$1, { existsSync, readFileSync, writeFileSync } from "fs";
10
+ import { glob } from "glob";
11
+ import { Node, Project, ts } from "ts-morph";
12
+ import os from "os";
13
+ import archiver from "archiver";
14
+ import { Minimatch, minimatch } from "minimatch";
15
+ import { execSync } from "child_process";
16
+ import dotenv from "dotenv";
17
+ import chalk from "chalk";
18
+ import express from "express";
19
+ import { createRequestHandler } from "@react-router/express";
20
+ import { existsSync as existsSync$1, readFileSync as readFileSync$1, unlinkSync } from "node:fs";
21
+ import { createProxyMiddleware } from "http-proxy-middleware";
22
+ import compression from "compression";
23
+ import zlib from "node:zlib";
24
+ import morgan from "morgan";
25
+ import { access, mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
26
+ import { execSync as execSync$1 } from "node:child_process";
27
+ import { tmpdir } from "node:os";
28
+ import { randomUUID } from "node:crypto";
29
+ import { npmRunPathEnv } from "npm-run-path";
30
+
31
+ //#region src/plugins/fixReactRouterManifestUrls.ts
32
+ function patchAssetsPaths(dir) {
33
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
34
+ for (const entry of entries) {
35
+ const fullPath = path.join(dir, entry.name);
36
+ if (entry.isDirectory()) patchAssetsPaths(fullPath);
37
+ else if (entry.isFile() && entry.name.endsWith(".js")) {
38
+ const content = fs.readFileSync(fullPath, "utf-8");
39
+ if (content.includes("\"/assets/") || content.includes("'/assets/")) {
40
+ fs.writeFileSync(fullPath, content.replace(/["']\/assets\//g, "(window._BUNDLE_PATH || \"/\") + \"assets/"));
41
+ console.log(`patched /assets/ references in ${fullPath}`);
42
+ }
43
+ }
44
+ }
45
+ }
46
+ /**
47
+ * Plugin to transform React Router client manifest URLs to use dynamic bundle paths
48
+ */
49
+ function fixReactRouterManifestUrlsPlugin() {
50
+ let resolvedConfig;
51
+ return {
52
+ name: "odyssey:fix-react-router-manifest-urls",
53
+ enforce: "post",
54
+ configResolved(config) {
55
+ resolvedConfig = config;
56
+ },
57
+ closeBundle() {
58
+ const clientBuildDir = resolvedConfig.environments.client.build.outDir;
59
+ if (fs.existsSync(clientBuildDir)) patchAssetsPaths(clientBuildDir);
60
+ }
61
+ };
62
+ }
63
+
64
+ //#endregion
65
+ //#region src/plugins/readableChunkFileNames.ts
66
+ /**
67
+ * Generates human-readable chunk file names for better debugging in production builds.
68
+ *
69
+ * Transforms Rollup's default hash-based chunk names into structured paths that reflect
70
+ * the original source location, making it easier to identify and debug specific chunks.
71
+ *
72
+ * @param chunkInfo - Rollup's pre-rendered chunk information containing module IDs and metadata
73
+ * @returns A formatted string pattern for the chunk filename with one of these formats:
74
+ * - `assets/(folder1)-(folder2)-filename.[hash].js` for source files in /src/
75
+ * - `assets/(package)-(pkg-name)-(subfolder)-filename.[hash].js` for node_modules
76
+ * - `assets/(chunk)-[name].[hash].js` as fallback for chunks without identifiable paths
77
+ *
78
+ * @example
79
+ * // Source file: /src/components/ui/Button.tsx
80
+ * // Output: assets/(components)-(ui)-Button.[hash].js
81
+ *
82
+ * @example
83
+ * // Node module: /node_modules/@radix-ui/react-dialog/dist/index.js
84
+ * // Output: assets/(package)-(@radix-ui)-(react-dialog)-(dist)-index.[hash].js
85
+ */
86
+ const readableChunkFileNames = (chunkInfo) => {
87
+ const moduleIds = chunkInfo.moduleIds;
88
+ const defaultName = "assets/(chunk)-[name].[hash].js";
89
+ if (!moduleIds || moduleIds.length === 0) return defaultName;
90
+ const lastModuleId = moduleIds[moduleIds.length - 1];
91
+ const toPosixPath = (pathname) => {
92
+ return pathname.replace(/\\/g, "/");
93
+ };
94
+ const getFileName = (pathname) => {
95
+ const posixPath = toPosixPath(pathname);
96
+ return path$1.posix.parse(posixPath).base.split("?")[0].replace(/\.(tsx?|jsx?|mjs|js)$/, "");
97
+ };
98
+ const cleanPath = (pathname) => {
99
+ return pathname?.split("?")[0];
100
+ };
101
+ const normalizedModuleId = toPosixPath(lastModuleId);
102
+ if (normalizedModuleId.includes("/src/")) {
103
+ const match = toPosixPath(cleanPath(lastModuleId)).match(/\/src\/(.+)$/);
104
+ if (match) {
105
+ const parts = match[1].split("/");
106
+ const fileName = getFileName(parts[parts.length - 1]);
107
+ return `assets/${parts.slice(0, -1).map((f) => `(${f})`).join("-")}-${fileName}.[hash].js`;
108
+ }
109
+ }
110
+ if (normalizedModuleId.includes("/node_modules/")) {
111
+ const parts = toPosixPath(cleanPath(lastModuleId)).split("/node_modules/");
112
+ const pathParts = parts[parts.length - 1].split("/");
113
+ let packageName;
114
+ let remainingPath;
115
+ if (pathParts[0].startsWith("@")) {
116
+ packageName = `${pathParts[0]}-${pathParts[1]}`;
117
+ remainingPath = pathParts.slice(2);
118
+ } else {
119
+ packageName = pathParts[0];
120
+ remainingPath = pathParts.slice(1);
121
+ }
122
+ const fileName = getFileName(remainingPath[remainingPath.length - 1]);
123
+ const folders = remainingPath.slice(0, -1);
124
+ return `assets/${[
125
+ "package",
126
+ packageName,
127
+ ...folders
128
+ ].map((s) => `(${s})`).join("-")}-${fileName}.[hash].js`;
129
+ }
130
+ return defaultName;
131
+ };
132
+ /**
133
+ * Vite plugin that configures Rollup to use human-readable chunk file names in production builds.
134
+ *
135
+ * Applies the `readableChunkFileNames` naming strategy to both code-split chunks and entry files,
136
+ * making it easier to identify the source of specific chunks when debugging production builds.
137
+ *
138
+ * @returns A Vite plugin that configures chunk naming for the client build environment
139
+ *
140
+ * @example
141
+ * // In vite.config.ts
142
+ * export default defineConfig({
143
+ * plugins: [readableChunkFileNamesPlugin()]
144
+ * })
145
+ */
146
+ const readableChunkFileNamesPlugin = () => {
147
+ return {
148
+ name: "odyssey:readable-chunk-file-names",
149
+ apply: "build",
150
+ config() {
151
+ return { environments: { client: { build: { rollupOptions: { output: {
152
+ chunkFileNames: readableChunkFileNames,
153
+ entryFileNames: readableChunkFileNames
154
+ } } } } } };
155
+ }
156
+ };
157
+ };
158
+
159
+ //#endregion
160
+ //#region src/mrt/utils.ts
161
+ const MRT_BUNDLE_TYPE_SSR = "ssr";
162
+ const MRT_STREAMING_ENTRY_FILE = "streamingHandler";
163
+ /**
164
+ * Gets the MRT entry file for the given mode
165
+ * @param mode - The mode to get the MRT entry file for
166
+ * @returns The MRT entry file for the given mode
167
+ */
168
+ const getMrtEntryFile = (mode) => {
169
+ return process.env.MRT_BUNDLE_TYPE !== MRT_BUNDLE_TYPE_SSR && mode === "production" ? MRT_STREAMING_ENTRY_FILE : MRT_BUNDLE_TYPE_SSR;
170
+ };
171
+
172
+ //#endregion
173
+ //#region src/plugins/managedRuntimeBundle.ts
174
+ const __dirname$1 = path$1.dirname(fileURLToPath(import.meta.url));
175
+ /**
176
+ * This is a Vite plugin specifically for building the Managed Runtime production bundle.
177
+ * This plugin relies on the @react-router/dev/vite plugin to work.
178
+ * This plugin creates the Managed Runtime production bundle from the build output of the @react-router/dev/vite plugin.
179
+ *
180
+ * @returns {Plugin} A Vite plugin for building the Managed Runtime production react-router bundle
181
+ */
182
+ const managedRuntimeBundlePlugin = () => {
183
+ let resolvedConfig;
184
+ let buildDirectory;
185
+ /**
186
+ * Creates the Managed Runtime production bundle assets
187
+ * - ssr.mjs or streamingHandler.mjs
188
+ * - loader.js
189
+ * - package.json
190
+ *
191
+ * @returns {Promise<void>}
192
+ */
193
+ const createManagedRuntimeBundleAssets = async () => {
194
+ const loaderPath = path$1.resolve(buildDirectory, "loader.js");
195
+ const mrtEntryFile = `${getMrtEntryFile(resolvedConfig?.mode)}.mjs`;
196
+ const mrtEntryPath = path$1.resolve(buildDirectory, mrtEntryFile);
197
+ await fs.ensureDir(buildDirectory);
198
+ await fs.outputFile(loaderPath, "// This file is intentionally empty");
199
+ const prebuiltMrtEntryPath = path$1.resolve(__dirname$1, `./mrt/${mrtEntryFile}`);
200
+ await fs.copy(prebuiltMrtEntryPath, mrtEntryPath);
201
+ const mrtDir = path$1.resolve(__dirname$1, "./mrt");
202
+ if (await fs.pathExists(mrtDir)) {
203
+ const files = await fs.readdir(mrtDir);
204
+ for (const file of files) if (file.startsWith("sfnext-server-") && file.endsWith(".mjs")) await fs.copy(path$1.join(mrtDir, file), path$1.resolve(buildDirectory, file));
205
+ }
206
+ const packageJsonPath = path$1.resolve(resolvedConfig.root, "package.json");
207
+ const buildPackageJsonPath = path$1.resolve(buildDirectory, "package.json");
208
+ const packageJson = await fs.readJson(packageJsonPath);
209
+ delete packageJson.type;
210
+ await fs.writeJson(buildPackageJsonPath, packageJson, { spaces: 2 });
211
+ };
212
+ return {
213
+ name: "odyssey:managed-runtime-bundle",
214
+ apply: "build",
215
+ config({ mode }) {
216
+ return {
217
+ environments: { ssr: { resolve: { noExternal: true } } },
218
+ experimental: { renderBuiltUrl(filename, { type }) {
219
+ if (mode !== "preview" && (type === "asset" || type === "public")) return { runtime: `(typeof window !== 'undefined' ? window._BUNDLE_PATH : ('/mobify/bundle/'+(process.env.BUNDLE_ID??'local')+'/client/')) + ${JSON.stringify(filename)}` };
220
+ } }
221
+ };
222
+ },
223
+ configResolved(config) {
224
+ resolvedConfig = config;
225
+ buildDirectory = config.__reactRouterPluginContext.reactRouterConfig.buildDirectory;
226
+ },
227
+ buildApp: {
228
+ order: "post",
229
+ handler: async () => {
230
+ await createManagedRuntimeBundleAssets();
231
+ }
232
+ }
233
+ };
234
+ };
235
+
236
+ //#endregion
237
+ //#region src/plugins/patchReactRouter.ts
238
+ const VIRTUAL_MODULE_ID = "\0patched-react-router";
239
+ const MODULE_TO_PATCH = "react-router";
240
+ /**
241
+ * This plugin intercepts imports of 'react-router' and provides patched versions
242
+ * of specific components (like Scripts) with custom logic.
243
+ *
244
+ * @returns {Plugin} A Vite plugin for patching react-router components
245
+ */
246
+ const patchReactRouterPlugin = () => {
247
+ let isTestMode = false;
248
+ return {
249
+ name: "odyssey:patch-react-router",
250
+ enforce: "pre",
251
+ config(_config, { mode }) {
252
+ isTestMode = mode === "test";
253
+ },
254
+ configEnvironment(name) {
255
+ if (isTestMode) return;
256
+ if (name === "ssr") return { resolve: { noExternal: ["react-router"] } };
257
+ },
258
+ resolveId(id, importer) {
259
+ if (isTestMode) return null;
260
+ if (id === MODULE_TO_PATCH) {
261
+ if (importer === VIRTUAL_MODULE_ID || importer?.includes("storefront-next-dev")) return null;
262
+ return VIRTUAL_MODULE_ID;
263
+ }
264
+ return null;
265
+ },
266
+ load(id) {
267
+ if (isTestMode) return null;
268
+ if (id === VIRTUAL_MODULE_ID) return `
269
+ export * from 'react-router';
270
+ export { Scripts } from '@salesforce/storefront-next-dev/react-router/Scripts';
271
+ `;
272
+ return null;
273
+ }
274
+ };
275
+ };
276
+
277
+ //#endregion
278
+ //#region src/extensibility/plugin-utils.ts
279
+ const traverse = traverseModule.default || traverseModule;
280
+ const PLUGIN_COMPONENT_TAG = "PluginComponent";
281
+ const PLUGIN_PROVIDERS_TAG = "PluginProviders";
282
+ const PLUGIN_ID_ATTRIBUTE = "pluginId";
283
+ /**
284
+ * Find and replace the PluginProviders tags with the corresponding context providers
285
+ * @param element - the AST element to replace
286
+ * @param contextProviders - the context providers to replace
287
+ */
288
+ function findAndReplaceProviders(element, contextProviders) {
289
+ if (isJSXIdentifier(element.node.openingElement.name, { name: PLUGIN_PROVIDERS_TAG })) if (contextProviders.length > 0) {
290
+ let nested = element.node.children;
291
+ for (let i = contextProviders.length - 1; i >= 0; i--) {
292
+ const componentName = contextProviders[i].componentName;
293
+ nested = [jsxElement(jsxOpeningElement(jsxIdentifier(componentName), [], false), jsxClosingElement(jsxIdentifier(componentName)), nested, false)];
294
+ }
295
+ element.replaceWithMultiple(nested);
296
+ } else element.replaceWith(jsxFragment(jsxOpeningFragment(), jsxClosingFragment(), element.node.children));
297
+ }
298
+ /**
299
+ * Find and replace the plugin component with the replacement code
300
+ * @param componentName - the name of the component to replace
301
+ * @param element - the AST element as the replacement candidate
302
+ * @param pluginRegistry - the plugin registry
303
+ * @returns the pluginId that was replaced, or null if no replacement was found
304
+ */
305
+ function findAndReplaceComponent(componentName, element, pluginRegistry) {
306
+ let pluginIdReplaced = null;
307
+ if (isJSXIdentifier(element.node.openingElement.name, { name: componentName })) {
308
+ let replaced = false;
309
+ if (Array.isArray(element.node.openingElement.attributes)) {
310
+ const attr = element.node.openingElement.attributes.find((a) => isJSXAttribute(a) && isJSXIdentifier(a.name, { name: PLUGIN_ID_ATTRIBUTE }));
311
+ const pluginId = attr && isJSXAttribute(attr) && attr.value && "value" in attr.value ? attr.value.value : void 0;
312
+ if (pluginId == null) throw new Error(`PluginComponent must contain a pluginId attribute`);
313
+ if (pluginRegistry[pluginId] && pluginRegistry[pluginId].length > 0) {
314
+ const components = pluginRegistry[pluginId].map((pluginComponent) => {
315
+ return jsxElement(jsxOpeningElement(jsxIdentifier(pluginComponent.componentName), [], true), null, [], true);
316
+ });
317
+ if (components.length > 1) element.replaceWith(jsxFragment(jsxOpeningFragment(), jsxClosingFragment(), components));
318
+ else element.replaceWith(components[0]);
319
+ pluginIdReplaced = pluginId;
320
+ replaced = true;
321
+ }
322
+ }
323
+ if (!replaced) if (element.node.children && element.node.children.length > 0) element.replaceWithMultiple(element.node.children);
324
+ else element.remove();
325
+ }
326
+ return pluginIdReplaced;
327
+ }
328
+ /**
329
+ * Run a replacement pass on the AST
330
+ * @param ast - the AST to traverse
331
+ * @param tagName - the name of the tag to replace
332
+ * @param pluginRegistry - the plugin registry
333
+ * @param contextProviders - the context providers to replace
334
+ * @returns a set of pluginIds that were replaced
335
+ */
336
+ function runReplacementPass(ast, tagName, pluginRegistry = null, contextProviders = null) {
337
+ const pluginIdsReplaced = /* @__PURE__ */ new Set();
338
+ const applyReplacement = (pathToReplace) => {
339
+ if (pluginRegistry) {
340
+ const replacedId = findAndReplaceComponent(tagName, pathToReplace, pluginRegistry);
341
+ if (replacedId) pluginIdsReplaced.add(replacedId);
342
+ } else if (contextProviders) findAndReplaceProviders(pathToReplace, contextProviders);
343
+ };
344
+ traverse(ast, {
345
+ VariableDeclaration(nodePath) {
346
+ const declarationPaths = nodePath.get("declarations");
347
+ const declarationsArray = Array.isArray(declarationPaths) ? declarationPaths : [declarationPaths];
348
+ for (const declarationPath of declarationsArray) {
349
+ const initPath = declarationPath.get("init");
350
+ if (initPath && isJSXElement(initPath.node)) {
351
+ const content = generate(initPath.node).code;
352
+ if ((/* @__PURE__ */ new RegExp(`<(${tagName})(\\s|\\/|>)`)).test(content)) {
353
+ applyReplacement(initPath);
354
+ initPath.traverse({ JSXElement(inner) {
355
+ applyReplacement(inner);
356
+ } });
357
+ }
358
+ }
359
+ }
360
+ },
361
+ ReturnStatement(nodePath) {
362
+ const arg = nodePath.node.argument;
363
+ if (!isJSXElement(arg) && !isJSXFragment(arg)) return;
364
+ nodePath.traverse({ JSXElement(inner) {
365
+ applyReplacement(inner);
366
+ } });
367
+ }
368
+ });
369
+ return pluginIdsReplaced;
370
+ }
371
+ /**
372
+ * Build the import statements for the plugin components
373
+ * @param pluginIds - the pluginIds that were replaced
374
+ * @param pluginRegistry - the plugin registry
375
+ * @returns the import statements
376
+ */
377
+ function buildReplacementImportStatements(pluginIds, pluginRegistry) {
378
+ const importStatements = /* @__PURE__ */ new Set();
379
+ for (const pluginId of pluginIds) {
380
+ const pluginComponents = pluginRegistry[pluginId];
381
+ for (const pluginComponent of pluginComponents) importStatements.add(`import ${pluginComponent.componentName} from '@/${pluginComponent.path.replace(".tsx", "")}';`);
382
+ }
383
+ return Array.from(importStatements).join("\n");
384
+ }
385
+ function transformPlugins(code, pluginRegistry, contextProviders) {
386
+ if (!code.includes(PLUGIN_COMPONENT_TAG) && !code.includes(PLUGIN_PROVIDERS_TAG)) return null;
387
+ const ast = parse(code, {
388
+ sourceType: "module",
389
+ plugins: [
390
+ "typescript",
391
+ "jsx",
392
+ "decorators-legacy"
393
+ ]
394
+ });
395
+ if (code.includes(PLUGIN_COMPONENT_TAG)) {
396
+ const replacementImportStatements = buildReplacementImportStatements(runReplacementPass(ast, PLUGIN_COMPONENT_TAG, pluginRegistry, null), pluginRegistry);
397
+ traverse(ast, { ImportDeclaration(nodePath) {
398
+ if (nodePath.node.source.value.includes("@/plugins/plugin-component")) nodePath.replaceWith(jsxText(replacementImportStatements));
399
+ } });
400
+ }
401
+ if (code.includes(PLUGIN_PROVIDERS_TAG)) {
402
+ const importStatements = /* @__PURE__ */ new Set();
403
+ for (const contextProvider of contextProviders) importStatements.add(`import ${contextProvider.componentName} from '@/${contextProvider.path.replace(".tsx", "")}';`);
404
+ const replacementImportStatements = Array.from(importStatements).join("\n");
405
+ traverse(ast, { ImportDeclaration(nodePath) {
406
+ if (nodePath.node.source.value.includes("@/plugins/plugin-providers")) nodePath.replaceWith(jsxText(replacementImportStatements));
407
+ } });
408
+ runReplacementPass(ast, PLUGIN_PROVIDERS_TAG, null, contextProviders);
409
+ }
410
+ return generate(ast).code;
411
+ }
412
+ /**
413
+ * Build the plugin registry from the extension directories
414
+ * @param rootDir - the root directory of the project
415
+ * @param sourceDir - the source directory of the project
416
+ * @returns the plugin registry
417
+ */
418
+ function buildPluginRegistry(rootDir) {
419
+ const componentRegistry = {};
420
+ const contextProviders = [];
421
+ const extensionDirPath = path$1.join(rootDir, "extensions");
422
+ const extensionDirs = fs.readdirSync(extensionDirPath, { withFileTypes: true });
423
+ const getNamespaceAndComponentName = (dir, filePath) => {
424
+ const namespace = dir.name.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join("");
425
+ return {
426
+ namespace,
427
+ componentName: `${namespace}_${(filePath.split("/").pop()?.replace(".tsx", ""))?.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join("")}`
428
+ };
429
+ };
430
+ for (const dir of extensionDirs) if (dir.isDirectory()) {
431
+ const configPath = path$1.join(extensionDirPath, dir.name, "plugin-config.json");
432
+ if (fs.existsSync(configPath)) {
433
+ const pluginConfig = fs.readJsonSync(configPath);
434
+ if (pluginConfig && pluginConfig.components) for (const pluginComponent of pluginConfig.components) {
435
+ const { pluginId, path: componentPath, order = 0 } = pluginComponent;
436
+ if (pluginId && componentPath) {
437
+ if (!componentRegistry[pluginId]) componentRegistry[pluginId] = [];
438
+ const { namespace, componentName } = getNamespaceAndComponentName(dir, componentPath);
439
+ componentRegistry[pluginId].push({
440
+ pluginId,
441
+ path: componentPath,
442
+ order,
443
+ namespace,
444
+ componentName
445
+ });
446
+ }
447
+ }
448
+ if (pluginConfig && pluginConfig.contextProviders) for (const contextProvider of pluginConfig.contextProviders) {
449
+ const { path: providerPath, order = 0 } = contextProvider;
450
+ if (providerPath) {
451
+ const { namespace, componentName } = getNamespaceAndComponentName(dir, providerPath);
452
+ contextProviders.push({
453
+ path: providerPath,
454
+ namespace,
455
+ componentName,
456
+ order
457
+ });
458
+ }
459
+ }
460
+ }
461
+ }
462
+ for (const pluginId in componentRegistry) componentRegistry[pluginId].sort((a, b) => a.order - b.order);
463
+ contextProviders.sort((a, b) => a.order - b.order);
464
+ return {
465
+ componentRegistry,
466
+ contextProviders
467
+ };
468
+ }
469
+
470
+ //#endregion
471
+ //#region src/plugins/transformPlugins.ts
472
+ function transformPluginPlaceholderPlugin() {
473
+ let componentRegistry;
474
+ let contextProviders;
475
+ let sourceDir;
476
+ return {
477
+ name: "odyssey:transform-plugin-placeholder",
478
+ enforce: "pre",
479
+ configResolved(config) {
480
+ sourceDir = config.resolve.alias.find((alias) => alias.find === "@")?.replacement || path$1.resolve(__dirname, "./src");
481
+ },
482
+ buildStart() {
483
+ ({componentRegistry, contextProviders} = buildPluginRegistry(sourceDir));
484
+ },
485
+ transform(code, id) {
486
+ try {
487
+ const transformedCode = transformPlugins(code, componentRegistry, contextProviders);
488
+ if (transformedCode) return {
489
+ code: transformedCode,
490
+ map: null
491
+ };
492
+ return null;
493
+ } catch (err) {
494
+ console.error(`PluginComponent replace ERROR in ${id}: ${err instanceof Error ? err.stack : String(err)}`);
495
+ throw err;
496
+ }
497
+ }
498
+ };
499
+ }
500
+
501
+ //#endregion
502
+ //#region src/plugins/watchConfigFiles.ts
503
+ const watchConfigFilesPlugin = () => {
504
+ let viteConfig;
505
+ return {
506
+ name: "odyssey:watch-config-files",
507
+ configResolved(config) {
508
+ viteConfig = config;
509
+ },
510
+ configureServer(server) {
511
+ const aliases = viteConfig.resolve.alias;
512
+ const root = Object.values(aliases).find((alias) => alias.find === "@")?.replacement || "src";
513
+ const glob$1 = path$1.posix.join(root, "extensions", "**", "plugin-config.json");
514
+ server.watcher.add(glob$1);
515
+ const onChange = (file) => {
516
+ if (file.endsWith("plugin-config.json")) {
517
+ console.log(`🔁 plugin-config.json changed: ${file}`);
518
+ server.restart();
519
+ }
520
+ };
521
+ server.watcher.on("add", onChange);
522
+ server.watcher.on("change", onChange);
523
+ server.watcher.on("unlink", onChange);
524
+ }
525
+ };
526
+ };
527
+
528
+ //#endregion
529
+ //#region src/plugins/staticRegistry.ts
530
+ const DEFAULT_COMPONENT_GROUP$1 = "odyssey_base";
531
+ /**
532
+ * Extracts component ID and group from @Component decorator using ts-morph AST parsing
533
+ */
534
+ function extractComponentInfo(decorator) {
535
+ const callExpression = decorator.getCallExpression();
536
+ if (!callExpression) return null;
537
+ const args = callExpression.getArguments();
538
+ if (args.length === 0) return null;
539
+ const firstArg = args[0];
540
+ let baseComponentId;
541
+ if (Node.isStringLiteral(firstArg)) baseComponentId = firstArg.getLiteralValue();
542
+ else if (Node.isNoSubstitutionTemplateLiteral(firstArg)) baseComponentId = firstArg.getText().slice(1, -1);
543
+ else if (Node.isTemplateExpression(firstArg)) throw new Error(`@Component id must be a simple string literal or backtick string without interpolation. Found: ${firstArg.getText()}`);
544
+ else return null;
545
+ let group = DEFAULT_COMPONENT_GROUP$1;
546
+ if (args.length > 1) {
547
+ const secondArg = args[1];
548
+ if (Node.isObjectLiteralExpression(secondArg)) {
549
+ const groupProperty = secondArg.getProperty("group");
550
+ if (groupProperty && Node.isPropertyAssignment(groupProperty)) {
551
+ const initializer = groupProperty.getInitializer();
552
+ if (initializer && Node.isStringLiteral(initializer)) group = initializer.getLiteralValue();
553
+ }
554
+ }
555
+ }
556
+ return {
557
+ id: `${group}.${baseComponentId}`,
558
+ group
559
+ };
560
+ }
561
+ /**
562
+ * Checks if a source file has a specific named export using ts-morph AST parsing
563
+ */
564
+ function hasNamedExport(sourceFile, exportName) {
565
+ if (sourceFile.getFunctions().filter((func) => func.hasExportKeyword() && func.getName() === exportName).length > 0) return true;
566
+ const variableStatements = sourceFile.getVariableStatements().filter((stmt) => stmt.hasExportKeyword());
567
+ for (const stmt of variableStatements) {
568
+ const declarations = stmt.getDeclarations();
569
+ for (const decl of declarations) if (decl.getName() === exportName) return true;
570
+ }
571
+ const exportDeclarations = sourceFile.getExportDeclarations();
572
+ for (const exportDecl of exportDeclarations) {
573
+ const namedExports = exportDecl.getNamedExports();
574
+ for (const namedExport of namedExports) {
575
+ const localName = namedExport.getName();
576
+ const aliasName = namedExport.getAliasNode()?.getText();
577
+ if (localName === exportName || aliasName === exportName) return true;
578
+ }
579
+ }
580
+ return false;
581
+ }
582
+ /**
583
+ * Checks if a source file has a fallback export (including default exports with 'fallback' in name)
584
+ */
585
+ function hasFallbackExport(sourceFile) {
586
+ if (hasNamedExport(sourceFile, "fallback")) return true;
587
+ const functions = sourceFile.getFunctions().filter((func) => func.hasExportKeyword() && func.hasDefaultKeyword());
588
+ for (const func of functions) {
589
+ const name = func.getName();
590
+ if (name && name.toLowerCase().includes("fallback")) return true;
591
+ }
592
+ return false;
593
+ }
594
+ /**
595
+ * Scans all files in the component directory for @Component decorators and extracts metadata using ts-morph
596
+ */
597
+ async function scanComponents(project, projectRoot, componentPath, registryPath, verbose$1) {
598
+ const componentFiles = await glob(`${componentPath}/**/*.{ts,tsx}`, {
599
+ cwd: projectRoot,
600
+ absolute: true
601
+ });
602
+ if (verbose$1) console.log(`🔍 Scanning ${componentFiles.length} files in ${componentPath}...`);
603
+ const components = [];
604
+ const registryDir = dirname(resolve$1(projectRoot, registryPath));
605
+ for (const filePath of componentFiles) try {
606
+ const content = readFileSync(filePath, "utf-8");
607
+ const sourceFile = project.createSourceFile(filePath, content, { overwrite: true });
608
+ const classes = sourceFile.getClasses();
609
+ for (const classDeclaration of classes) {
610
+ const decorators = classDeclaration.getDecorators();
611
+ for (const decorator of decorators) if (decorator.getName() === "Component") {
612
+ const componentInfo = extractComponentInfo(decorator);
613
+ if (componentInfo) {
614
+ let relativePath = relative(registryDir, filePath).replace(/\\/g, "/").replace(/\.(ts|tsx)$/, "");
615
+ if (!relativePath.startsWith(".")) relativePath = `./${relativePath}`;
616
+ const hasLoaderExport = hasNamedExport(sourceFile, "loader");
617
+ const hasClientLoaderExport = hasNamedExport(sourceFile, "clientLoader");
618
+ const hasFallback = hasFallbackExport(sourceFile);
619
+ components.push({
620
+ id: componentInfo.id,
621
+ filePath,
622
+ relativePath,
623
+ hasLoader: hasLoaderExport,
624
+ hasClientLoader: hasClientLoaderExport,
625
+ hasFallback
626
+ });
627
+ if (verbose$1) {
628
+ const exports = [];
629
+ if (hasLoaderExport) exports.push("loader");
630
+ if (hasClientLoaderExport) exports.push("clientLoader");
631
+ if (hasFallback) exports.push("fallback");
632
+ const exportsText = exports.length > 0 ? ` (with ${exports.join(", ")})` : "";
633
+ console.log(` ✅ Found component: ${componentInfo.id} → ${relativePath}${exportsText}`);
634
+ }
635
+ }
636
+ }
637
+ }
638
+ } catch (error$1) {
639
+ if (verbose$1) console.warn(`⚠️ Could not process ${filePath}:`, error$1.message);
640
+ }
641
+ return components;
642
+ }
643
+ /**
644
+ * Generates the initializeRegistry function code
645
+ */
646
+ function generateRegistryCode(components, registryIdentifier = "registry") {
647
+ const sorted = [...components].sort((a, b) => a.id.localeCompare(b.id) || a.relativePath.localeCompare(b.relativePath));
648
+ if (sorted.length === 0) return `
649
+ /* eslint-disable */
650
+ /**
651
+ * Initialize the static component registry.
652
+ * This function is auto-generated by the staticRegistry Vite plugin.
653
+ *
654
+ * DO NOT EDIT THIS FUNCTION MANUALLY - it will be overwritten on next build.
655
+ */
656
+ export function initializeRegistry(targetRegistry = ${registryIdentifier}): void {
657
+ // No components found with @Component decorators
658
+ }
659
+ `;
660
+ const registrations = sorted.map(({ id, relativePath, hasLoader, hasClientLoader, hasFallback }) => {
661
+ if (hasLoader || hasClientLoader || hasFallback) {
662
+ const metadata = [];
663
+ if (hasLoader) metadata.push(`loader: 'loader'`);
664
+ if (hasClientLoader) metadata.push(`clientLoader: 'clientLoader'`);
665
+ if (hasFallback) metadata.push(`fallback: 'fallback'`);
666
+ return ` targetRegistry.registerImporter('${id}', () => import('${relativePath}'), { ${metadata.join(", ")} });`;
667
+ } else return ` targetRegistry.registerImporter('${id}', () => import('${relativePath}'));`;
668
+ }).join("\n");
669
+ return `
670
+ /* eslint-disable */
671
+ /**
672
+ * Initialize the static component registry.
673
+ * This function is auto-generated by the staticRegistry Vite plugin.
674
+ *
675
+ * DO NOT EDIT THIS FUNCTION MANUALLY - it will be overwritten on next build.
676
+ *
677
+ * Components registered: ${sorted.map((c) => c.id).join(", ")}
678
+ */
679
+ export function initializeRegistry(targetRegistry = ${registryIdentifier}): void {
680
+ ${registrations}
681
+ }
682
+ `;
683
+ }
684
+ /**
685
+ * Updates the registry.ts file with the generated code
686
+ */
687
+ function updateRegistryFile(registryFilePath, generatedCode, verbose$1) {
688
+ let existingContent;
689
+ if (!existsSync(registryFilePath)) {
690
+ if (verbose$1) console.log(`📝 Creating new registry file...`);
691
+ const basicRegistryContent = `import { ComponentRegistry } from '@/lib/component-registry';
692
+
693
+ // Create the component registry instance
694
+ export const registry = new ComponentRegistry();
695
+
696
+ // STATIC_REGISTRY_START
697
+ // Generated content will be inserted here by the static registry plugin
698
+ // STATIC_REGISTRY_END
699
+ `;
700
+ writeFileSync(registryFilePath, basicRegistryContent, "utf-8");
701
+ existingContent = basicRegistryContent;
702
+ } else try {
703
+ existingContent = readFileSync(registryFilePath, "utf-8");
704
+ } catch (error$1) {
705
+ throw new Error(`Failed to read registry file: ${error$1.message}`);
706
+ }
707
+ const startMarker = "// STATIC_REGISTRY_START";
708
+ const endMarker = "// STATIC_REGISTRY_END";
709
+ const startIndex = existingContent.indexOf(startMarker);
710
+ const endIndex = existingContent.indexOf(endMarker);
711
+ if (startIndex === -1 || endIndex === -1) throw new Error(`Registry file ${registryFilePath} is missing static registry markers. Please add "${startMarker}" and "${endMarker}" markers to define the generated content area.`);
712
+ const updatedContent = `${existingContent.slice(0, startIndex + 24)}\n${generatedCode}\n${existingContent.slice(endIndex)}`;
713
+ try {
714
+ writeFileSync(registryFilePath, updatedContent, "utf-8");
715
+ if (verbose$1) console.log(`💾 Updated registry file: ${registryFilePath}`);
716
+ } catch (error$1) {
717
+ throw new Error(`Failed to write registry file: ${error$1.message}`);
718
+ }
719
+ }
720
+ /**
721
+ * Vite plugin that generates static component registry based on @Component decorators.
722
+ *
723
+ * This plugin scans component files for @Component decorators and automatically generates
724
+ * a static registry function that pre-registers all components with their import paths.
725
+ * This eliminates the need for manual component registration and provides build-time
726
+ * optimization for component discovery.
727
+ *
728
+ * @param config - Configuration options for the plugin
729
+ * @returns A Vite plugin that generates static component registrations
730
+ *
731
+ * @example
732
+ * // In vite.config.ts
733
+ * export default defineConfig({
734
+ * plugins: [
735
+ * staticRegistryPlugin({
736
+ * componentPath: 'src/components',
737
+ * registryPath: 'src/lib/registry.ts',
738
+ * verbose: true
739
+ * })
740
+ * ]
741
+ * })
742
+ */
743
+ const staticRegistryPlugin = (config = {}) => {
744
+ const { componentPath = "src/components", registryPath = "src/lib/static-registry.ts", registryIdentifier = "registry", failOnError = true, verbose: verbose$1 = false } = config;
745
+ let projectRoot;
746
+ const runRegistryGeneration = async () => {
747
+ if (verbose$1) console.log("🚀 Starting static registry generation...");
748
+ const components = await scanComponents(new Project({ compilerOptions: {
749
+ target: ts.ScriptTarget.Latest,
750
+ module: ts.ModuleKind.ESNext,
751
+ jsx: ts.JsxEmit.ReactJSX,
752
+ allowJs: true,
753
+ skipLibCheck: true,
754
+ noEmit: true
755
+ } }), projectRoot, componentPath, registryPath, verbose$1);
756
+ if (verbose$1) console.log(`📦 Found ${components.length} components with @Component decorators`);
757
+ const generatedCode = generateRegistryCode(components, registryIdentifier);
758
+ const registryFilePath = resolve$1(projectRoot, registryPath);
759
+ updateRegistryFile(registryFilePath, generatedCode, verbose$1);
760
+ if (verbose$1) console.log("✅ Static registry generation complete!");
761
+ return registryFilePath;
762
+ };
763
+ return {
764
+ name: "storefrontnext:static-registry",
765
+ configResolved(resolvedConfig) {
766
+ projectRoot = resolvedConfig.root;
767
+ },
768
+ async buildStart() {
769
+ try {
770
+ await runRegistryGeneration();
771
+ } catch (error$1) {
772
+ console.error(`❌ Static registry generation failed: ${error$1.message}`);
773
+ if (failOnError) throw error$1;
774
+ console.warn("⚠️ Continuing build without static registry...");
775
+ }
776
+ },
777
+ async handleHotUpdate({ file, server }) {
778
+ const normalizedComponentPath = componentPath.replace(/\\/g, "/");
779
+ const normalizedFile = file.replace(/\\/g, "/");
780
+ if (normalizedFile.includes(`/${normalizedComponentPath}/`) && (normalizedFile.endsWith(".ts") || normalizedFile.endsWith(".tsx"))) {
781
+ if (verbose$1) console.log(`🔄 Component file changed: ${file}, regenerating registry...`);
782
+ try {
783
+ const registryFilePath = await runRegistryGeneration();
784
+ const registryModule = server.moduleGraph.getModuleById(registryFilePath);
785
+ if (registryModule) await server.reloadModule(registryModule);
786
+ if (verbose$1) console.log("✅ Registry regenerated successfully!");
787
+ } catch (error$1) {
788
+ console.error(`❌ Failed to regenerate registry: ${error$1.message}`);
789
+ }
790
+ return [];
791
+ }
792
+ }
793
+ };
794
+ };
795
+
796
+ //#endregion
797
+ //#region src/plugins/configLoader.ts
798
+ /**
799
+ * Load the engagement config from config.server.ts
800
+ */
801
+ async function loadEngagementConfig(projectRoot, configPath, verbose$1) {
802
+ const absoluteConfigPath = resolve$1(projectRoot, configPath);
803
+ try {
804
+ const config = (await import(pathToFileURL(absoluteConfigPath).href)).default;
805
+ if (verbose$1) console.log(` 📄 Loaded config from ${configPath}`);
806
+ const engagement = config?.app?.engagement;
807
+ if (!engagement) {
808
+ if (verbose$1) console.log(` ⚠️ No engagement config found in ${configPath}`);
809
+ return null;
810
+ }
811
+ return engagement;
812
+ } catch (error$1) {
813
+ if (verbose$1) console.warn(` ⚠️ Could not load config from ${configPath}: ${error$1.message}`);
814
+ return null;
815
+ }
816
+ }
817
+
818
+ //#endregion
819
+ //#region src/plugins/eventInstrumentationValidator.ts
820
+ /**
821
+ * Extract all trackEvent calls from source files and return the event types found
822
+ */
823
+ async function scanForInstrumentedEvents(projectRoot, scanPaths, verbose$1) {
824
+ const instrumentedEvents = /* @__PURE__ */ new Set();
825
+ const trackEventPattern = /trackEvent\s*\([^,]+,[^,]+,[^,]+,\s*['"]([^'"]+)['"]/g;
826
+ const sendViewPagePattern = /sendViewPageEvent\s*\(/g;
827
+ const createEventPattern = /createEvent\s*\(\s*['"]([^'"]+)['"]/g;
828
+ for (const scanPath of scanPaths) {
829
+ const files = await glob(join$1(resolve$1(projectRoot, scanPath), "**/*.{ts,tsx}"), { ignore: [
830
+ "**/*.test.ts",
831
+ "**/*.test.tsx",
832
+ "**/*.spec.ts",
833
+ "**/*.spec.tsx",
834
+ "**/node_modules/**"
835
+ ] });
836
+ if (verbose$1) console.log(` 📂 Scanning ${files.length} files in ${scanPath}...`);
837
+ for (const file of files) try {
838
+ const content = readFileSync(file, "utf-8");
839
+ let match;
840
+ while ((match = trackEventPattern.exec(content)) !== null) {
841
+ const eventType = match[1];
842
+ instrumentedEvents.add(eventType);
843
+ if (verbose$1) console.log(` ✓ Found trackEvent('${eventType}') in ${file}`);
844
+ }
845
+ if (sendViewPagePattern.test(content)) {
846
+ instrumentedEvents.add("view_page");
847
+ if (verbose$1) console.log(` ✓ Found sendViewPageEvent() in ${file}`);
848
+ }
849
+ while ((match = createEventPattern.exec(content)) !== null) {
850
+ const eventType = match[1];
851
+ instrumentedEvents.add(eventType);
852
+ if (verbose$1) console.log(` ✓ Found createEvent('${eventType}') in ${file}`);
853
+ }
854
+ trackEventPattern.lastIndex = 0;
855
+ sendViewPagePattern.lastIndex = 0;
856
+ createEventPattern.lastIndex = 0;
857
+ } catch (error$1) {
858
+ if (verbose$1) console.warn(` ⚠️ Could not read ${file}: ${error$1.message}`);
859
+ }
860
+ }
861
+ return instrumentedEvents;
862
+ }
863
+ /**
864
+ * Extract enabled event toggles per adapter
865
+ * Dynamically iterates over all keys in eventToggles - supports custom event types
866
+ */
867
+ function extractEnabledEvents(engagement) {
868
+ const adapterEvents = /* @__PURE__ */ new Map();
869
+ if (!engagement.adapters) return adapterEvents;
870
+ for (const [adapterName, adapterConfig] of Object.entries(engagement.adapters)) {
871
+ if (!adapterConfig.enabled) continue;
872
+ const enabledEvents = /* @__PURE__ */ new Set();
873
+ if (adapterConfig.eventToggles) {
874
+ for (const [eventType, isEnabled] of Object.entries(adapterConfig.eventToggles)) if (isEnabled === true) enabledEvents.add(eventType);
875
+ }
876
+ if (enabledEvents.size > 0) adapterEvents.set(adapterName, enabledEvents);
877
+ }
878
+ return adapterEvents;
879
+ }
880
+ /**
881
+ * Vite plugin that validates event instrumentation at build time.
882
+ *
883
+ * This plugin scans source files for trackEvent() calls and validates that
884
+ * all enabled event toggles in config.server.ts have corresponding instrumentation.
885
+ *
886
+ * @param config - Configuration options for the plugin
887
+ * @returns A Vite plugin that validates event instrumentation
888
+ *
889
+ * @example
890
+ * // In vite.config.ts
891
+ * export default defineConfig({
892
+ * plugins: [
893
+ * eventInstrumentationValidatorPlugin({
894
+ * configPath: 'config.server.ts',
895
+ * scanPaths: ['src'],
896
+ * verbose: true
897
+ * })
898
+ * ]
899
+ * })
900
+ */
901
+ const eventInstrumentationValidatorPlugin = (config = {}) => {
902
+ const { configPath = "config.server.ts", scanPaths = ["src"], failOnMissing = false, verbose: verbose$1 = false } = config;
903
+ let resolvedConfig;
904
+ return {
905
+ name: "storefrontnext:event-instrumentation-validator",
906
+ apply: "build",
907
+ configResolved(viteConfig) {
908
+ resolvedConfig = viteConfig;
909
+ },
910
+ async buildStart() {
911
+ const projectRoot = resolvedConfig.root;
912
+ if (verbose$1) console.log("\n🔍 [event-instrumentation] Validating event instrumentation...");
913
+ const engagement = await loadEngagementConfig(projectRoot, configPath, verbose$1);
914
+ if (!engagement) {
915
+ if (verbose$1) console.log(" ℹ️ Skipping validation - no engagement config found\n");
916
+ return;
917
+ }
918
+ const adapterEvents = extractEnabledEvents(engagement);
919
+ if (adapterEvents.size === 0) {
920
+ if (verbose$1) console.log(" ℹ️ No enabled adapters with event toggles found\n");
921
+ return;
922
+ }
923
+ const instrumentedEvents = await scanForInstrumentedEvents(projectRoot, scanPaths, verbose$1);
924
+ if (verbose$1) {
925
+ console.log(`\n 🔎 Found ${instrumentedEvents.size} instrumented event types:`);
926
+ for (const event of instrumentedEvents) console.log(` - ${event}`);
927
+ }
928
+ const missingInstrumentation = [];
929
+ for (const [adapterName, enabledEvents] of adapterEvents) for (const eventType of enabledEvents) if (!instrumentedEvents.has(eventType)) missingInstrumentation.push({
930
+ adapter: adapterName,
931
+ event: eventType
932
+ });
933
+ if (missingInstrumentation.length === 0) {
934
+ if (verbose$1) console.log("\n ✅ All enabled events are instrumented\n");
935
+ return;
936
+ }
937
+ console.log("\n");
938
+ for (const { adapter, event } of missingInstrumentation) console.warn(` ⚠️ [event-instrumentation] ${adapter}.${event} is enabled but '${event}' is never instrumented`);
939
+ console.log("\n");
940
+ if (failOnMissing) throw new Error(`[event-instrumentation] ${missingInstrumentation.length} event(s) are enabled but not instrumented. Either add instrumentation or disable the event toggles in config.server.ts.`);
941
+ }
942
+ };
943
+ };
944
+
945
+ //#endregion
946
+ //#region src/plugin.ts
947
+ /**
948
+ * Storefront Next Vite plugin that powers the React Router RSC app.
949
+ * Supports building and optimizing for the managed runtime environment.
950
+ *
951
+ * @param config - Configuration options for the plugin
952
+ * @returns {Plugin[]} An array of Vite plugins for Storefront Next functionality
953
+ *
954
+ * @example
955
+ * // With default options
956
+ * export default defineConfig({
957
+ * plugins: [storefrontNextPlugins()]
958
+ * })
959
+ *
960
+ * @example
961
+ * // Disable readable chunk names
962
+ * export default defineConfig({
963
+ * plugins: [storefrontNextPlugins({ readableChunkNames: false })]
964
+ * })
965
+ */
966
+ function storefrontNextPlugins(config = {}) {
967
+ const { readableChunkNames = false, staticRegistry = {
968
+ componentPath: "",
969
+ registryPath: "",
970
+ verbose: false
971
+ }, eventInstrumentationValidator = {
972
+ configPath: "config.server.ts",
973
+ scanPaths: ["src"],
974
+ failOnMissing: false,
975
+ verbose: false
976
+ } } = config;
977
+ const plugins = [
978
+ managedRuntimeBundlePlugin(),
979
+ fixReactRouterManifestUrlsPlugin(),
980
+ patchReactRouterPlugin(),
981
+ transformPluginPlaceholderPlugin(),
982
+ watchConfigFilesPlugin()
983
+ ];
984
+ if (staticRegistry?.componentPath && staticRegistry?.registryPath) plugins.push(staticRegistryPlugin(staticRegistry));
985
+ if (eventInstrumentationValidator !== false) plugins.push(eventInstrumentationValidatorPlugin(eventInstrumentationValidator));
986
+ if (readableChunkNames) plugins.push(readableChunkFileNamesPlugin());
987
+ return plugins;
988
+ }
989
+
990
+ //#endregion
991
+ //#region package.json
992
+ var version = "0.1.0";
993
+
994
+ //#endregion
995
+ //#region src/utils/logger.ts
996
+ /**
997
+ * Logger utilities
998
+ */
999
+ const colors = {
1000
+ warn: "yellow",
1001
+ error: "red",
1002
+ success: "cyan",
1003
+ info: "green",
1004
+ debug: "gray"
1005
+ };
1006
+ const fancyLog = (level, msg) => {
1007
+ const colorFn = chalk[colors[level]];
1008
+ console.log(`${colorFn(level)}: ${msg}`);
1009
+ };
1010
+ const info = (msg) => fancyLog("info", msg);
1011
+ const success = (msg) => fancyLog("success", msg);
1012
+ const warn = (msg) => fancyLog("warn", msg);
1013
+ const error = (msg) => fancyLog("error", msg);
1014
+ const debug = (msg, data) => {
1015
+ if (process.env.DEBUG || process.env.NODE_ENV !== "production") {
1016
+ fancyLog("debug", msg);
1017
+ if (data) console.log(data);
1018
+ }
1019
+ };
1020
+
1021
+ //#endregion
1022
+ //#region src/utils.ts
1023
+ const DEFAULT_CLOUD_ORIGIN = "https://cloud.mobify.com";
1024
+ const getDefaultBuildDir = (targetDir) => path$1.join(targetDir, "build");
1025
+ const NODE_ENV = process.env.NODE_ENV || "development";
1026
+ /**
1027
+ * Get credentials file path based on cloud origin
1028
+ */
1029
+ const getCredentialsFile = (cloudOrigin, credentialsFile) => {
1030
+ if (credentialsFile) return credentialsFile;
1031
+ const host = new URL(cloudOrigin).host;
1032
+ const suffix = host === "cloud.mobify.com" ? "" : `--${host}`;
1033
+ return path$1.join(os.homedir(), `.mobify${suffix}`);
1034
+ };
1035
+ /**
1036
+ * Read credentials from file
1037
+ */
1038
+ const readCredentials = async (filepath) => {
1039
+ try {
1040
+ const data = await fs.readJSON(filepath);
1041
+ return {
1042
+ username: data.username,
1043
+ api_key: data.api_key
1044
+ };
1045
+ } catch {
1046
+ throw new Error(`Credentials file "${filepath}" not found.\nVisit https://runtime.commercecloud.com/account/settings for steps on authorizing your computer to push bundles.`);
1047
+ }
1048
+ };
1049
+ /**
1050
+ * Get project package.json
1051
+ */
1052
+ const getProjectPkg = (projectDir) => {
1053
+ const packagePath = path$1.join(projectDir, "package.json");
1054
+ try {
1055
+ return fs.readJSONSync(packagePath);
1056
+ } catch {
1057
+ throw new Error(`Could not read project package at "${packagePath}"`);
1058
+ }
1059
+ };
1060
+ /**
1061
+ * Load .env file from project directory
1062
+ */
1063
+ const loadEnvFile = (projectDir) => {
1064
+ const envPath = path$1.join(projectDir, ".env");
1065
+ if (fs.existsSync(envPath)) dotenv.config({ path: envPath });
1066
+ else warn("No .env file found");
1067
+ };
1068
+ /**
1069
+ * Get MRT configuration with priority logic: .env -> package.json -> defaults
1070
+ */
1071
+ const getMrtConfig = (projectDir) => {
1072
+ loadEnvFile(projectDir);
1073
+ const pkg = getProjectPkg(projectDir);
1074
+ const defaultMrtProject = process.env.MRT_PROJECT ?? pkg.name;
1075
+ if (!defaultMrtProject || defaultMrtProject.trim() === "") throw new Error("Project name couldn't be determined. Do one of these options:\n 1. Set MRT_PROJECT in your .env file, or\n 2. Ensure package.json has a valid \"name\" field.");
1076
+ const defaultMrtTarget = process.env.MRT_TARGET ?? void 0;
1077
+ debug("MRT configuration resolved", {
1078
+ projectDir,
1079
+ envMrtProject: process.env.MRT_PROJECT,
1080
+ envMrtTarget: process.env.MRT_TARGET,
1081
+ packageName: pkg.name,
1082
+ resolvedProject: defaultMrtProject,
1083
+ resolvedTarget: defaultMrtTarget
1084
+ });
1085
+ return {
1086
+ defaultMrtProject,
1087
+ defaultMrtTarget
1088
+ };
1089
+ };
1090
+ /**
1091
+ * Get project dependency tree (simplified version)
1092
+ */
1093
+ const getProjectDependencyTree = (projectDir) => {
1094
+ try {
1095
+ const tmpFile = path$1.join(os.tmpdir(), `npm-ls-${Date.now()}.json`);
1096
+ execSync(`npm ls --all --json > ${tmpFile}`, {
1097
+ stdio: "ignore",
1098
+ cwd: projectDir
1099
+ });
1100
+ const data = fs.readJSONSync(tmpFile);
1101
+ fs.unlinkSync(tmpFile);
1102
+ return data;
1103
+ } catch {
1104
+ return null;
1105
+ }
1106
+ };
1107
+ /**
1108
+ * Get PWA Kit dependencies from dependency tree
1109
+ */
1110
+ const getPwaKitDependencies = (dependencyTree) => {
1111
+ if (!dependencyTree) return {};
1112
+ const pwaKitDependencies = ["@salesforce/storefront-next-dev"];
1113
+ const result = {};
1114
+ const searchDeps = (tree) => {
1115
+ if (tree.dependencies) for (const [name, dep] of Object.entries(tree.dependencies)) {
1116
+ if (pwaKitDependencies.includes(name)) result[name] = dep.version || "unknown";
1117
+ if (dep.dependencies) searchDeps({ dependencies: dep.dependencies });
1118
+ }
1119
+ };
1120
+ searchDeps(dependencyTree);
1121
+ return result;
1122
+ };
1123
+ /**
1124
+ * Get default commit message from git
1125
+ */
1126
+ const getDefaultMessage = (projectDir) => {
1127
+ try {
1128
+ return `${execSync("git rev-parse --abbrev-ref HEAD", {
1129
+ encoding: "utf8",
1130
+ cwd: projectDir
1131
+ }).trim()}: ${execSync("git rev-parse --short HEAD", {
1132
+ encoding: "utf8",
1133
+ cwd: projectDir
1134
+ }).trim()}`;
1135
+ } catch {
1136
+ debug("Using default bundle message as no message was provided and not in a Git repo.");
1137
+ return "PWA Kit Bundle";
1138
+ }
1139
+ };
1140
+
1141
+ //#endregion
1142
+ //#region src/bundle.ts
1143
+ /**
1144
+ * Create a bundle from the build directory
1145
+ */
1146
+ const createBundle = async (options) => {
1147
+ const { message, ssr_parameters, ssr_only, ssr_shared, buildDirectory, projectDirectory, projectSlug } = options;
1148
+ const tmpDir = fs.mkdtempSync(path$1.join(os.tmpdir(), "storefront-next-dev-push-"));
1149
+ const destination = path$1.join(tmpDir, "build.tar");
1150
+ const filesInArchive = [];
1151
+ if (!ssr_only || ssr_only.length === 0 || !ssr_shared || ssr_shared.length === 0) throw new Error("no ssrOnly or ssrShared files are defined");
1152
+ return new Promise((resolve$2, reject) => {
1153
+ const output = fs.createWriteStream(destination);
1154
+ const archive = archiver("tar");
1155
+ archive.pipe(output);
1156
+ const newRoot = path$1.join(projectSlug, "bld", "");
1157
+ const storybookExclusionMatchers = [
1158
+ "**/*.stories.tsx",
1159
+ "**/*.stories.ts",
1160
+ "**/*-snapshot.tsx",
1161
+ ".storybook/**/*",
1162
+ "storybook-static/**/*",
1163
+ "**/__mocks__/**/*",
1164
+ "**/__snapshots__/**/*"
1165
+ ].map((pattern) => new Minimatch(pattern, { nocomment: true }));
1166
+ archive.directory(buildDirectory, "", (entry) => {
1167
+ if (entry.name && storybookExclusionMatchers.some((matcher) => matcher.match(entry.name))) return false;
1168
+ if (entry.stats?.isFile() && entry.name) filesInArchive.push(entry.name);
1169
+ entry.prefix = newRoot;
1170
+ return entry;
1171
+ });
1172
+ archive.on("error", reject);
1173
+ output.on("finish", () => {
1174
+ try {
1175
+ const { dependencies = {}, devDependencies = {} } = getProjectPkg(projectDirectory);
1176
+ const dependencyTree = getProjectDependencyTree(projectDirectory);
1177
+ const pwaKitDeps = dependencyTree ? getPwaKitDependencies(dependencyTree) : {};
1178
+ const bundle_metadata = { dependencies: {
1179
+ ...dependencies,
1180
+ ...devDependencies,
1181
+ ...pwaKitDeps
1182
+ } };
1183
+ const data = fs.readFileSync(destination);
1184
+ const encoding = "base64";
1185
+ fs.rmSync(tmpDir, { recursive: true });
1186
+ const createGlobMatcher = (patterns) => {
1187
+ const allPatterns = patterns.map((pattern) => new Minimatch(pattern, { nocomment: true })).filter((pattern) => !pattern.empty);
1188
+ const positivePatterns = allPatterns.filter((pattern) => !pattern.negate);
1189
+ const negativePatterns = allPatterns.filter((pattern) => pattern.negate);
1190
+ return (filePath) => {
1191
+ if (filePath) {
1192
+ const positive = positivePatterns.some((pattern) => pattern.match(filePath));
1193
+ const negative = negativePatterns.some((pattern) => !pattern.match(filePath));
1194
+ return positive && !negative;
1195
+ }
1196
+ return false;
1197
+ };
1198
+ };
1199
+ resolve$2({
1200
+ message,
1201
+ encoding,
1202
+ data: data.toString(encoding),
1203
+ ssr_parameters,
1204
+ ssr_only: filesInArchive.filter(createGlobMatcher(ssr_only)),
1205
+ ssr_shared: filesInArchive.filter(createGlobMatcher(ssr_shared)),
1206
+ bundle_metadata
1207
+ });
1208
+ } catch (err) {
1209
+ reject(err);
1210
+ }
1211
+ });
1212
+ archive.finalize().catch(reject);
1213
+ });
1214
+ };
1215
+
1216
+ //#endregion
1217
+ //#region src/cloud-api.ts
1218
+ var CloudAPIClient = class {
1219
+ credentials;
1220
+ origin;
1221
+ constructor({ credentials, origin }) {
1222
+ this.credentials = credentials;
1223
+ this.origin = origin;
1224
+ }
1225
+ getAuthHeader() {
1226
+ const { username, api_key } = this.credentials;
1227
+ return { Authorization: `Basic ${Buffer.from(`${username}:${api_key}`, "binary").toString("base64")}` };
1228
+ }
1229
+ getHeaders() {
1230
+ return {
1231
+ "User-Agent": `storefront-next-dev@${version}`,
1232
+ ...this.getAuthHeader()
1233
+ };
1234
+ }
1235
+ /**
1236
+ * Push bundle to Managed Runtime
1237
+ */
1238
+ async push(bundle, projectSlug, target) {
1239
+ const base = `api/projects/${projectSlug}/builds/`;
1240
+ const pathname = target ? `${base}${target}/` : base;
1241
+ const url = new URL$1(this.origin);
1242
+ url.pathname = pathname;
1243
+ const body = Buffer.from(JSON.stringify(bundle));
1244
+ const headers = {
1245
+ ...this.getHeaders(),
1246
+ "Content-Length": body.length.toString()
1247
+ };
1248
+ const res = await fetch(url.toString(), {
1249
+ body,
1250
+ method: "POST",
1251
+ headers
1252
+ });
1253
+ if (res.status >= 400) {
1254
+ const bodyText = await res.text();
1255
+ let errorData;
1256
+ try {
1257
+ errorData = JSON.parse(bodyText);
1258
+ } catch {
1259
+ errorData = { message: bodyText };
1260
+ }
1261
+ throw new Error(`HTTP ${res.status}: ${errorData.message || bodyText}\nFor more information visit https://developer.salesforce.com/docs/commerce/pwa-kit-managed-runtime/guide/pushing-and-deploying-bundles.html`);
1262
+ }
1263
+ return await res.json();
1264
+ }
1265
+ /**
1266
+ * Wait for deployment to complete
1267
+ */
1268
+ async waitForDeploy(project, environment) {
1269
+ return new Promise((resolve$2, reject) => {
1270
+ const delay = 3e4;
1271
+ const check = async () => {
1272
+ const url = new URL$1(`/api/projects/${project}/target/${environment}`, this.origin);
1273
+ const res = await fetch(url, { headers: this.getHeaders() });
1274
+ if (!res.ok) {
1275
+ const text = await res.text();
1276
+ let json;
1277
+ try {
1278
+ if (text) json = JSON.parse(text);
1279
+ } catch {}
1280
+ const message = json?.detail ?? text;
1281
+ const detail = message ? `: ${message}` : "";
1282
+ throw new Error(`${res.status} ${res.statusText}${detail}`);
1283
+ }
1284
+ const data = await res.json();
1285
+ if (typeof data.state !== "string") return reject(/* @__PURE__ */ new Error("An unknown state occurred when polling the deployment."));
1286
+ switch (data.state) {
1287
+ case "CREATE_IN_PROGRESS":
1288
+ case "PUBLISH_IN_PROGRESS":
1289
+ setTimeout(() => {
1290
+ check().catch(reject);
1291
+ }, delay);
1292
+ return;
1293
+ case "CREATE_FAILED":
1294
+ case "PUBLISH_FAILED": return reject(/* @__PURE__ */ new Error("Deployment failed."));
1295
+ case "ACTIVE": return resolve$2();
1296
+ default: return reject(/* @__PURE__ */ new Error(`Unknown deployment state "${data.state}".`));
1297
+ }
1298
+ };
1299
+ setTimeout(() => {
1300
+ check().catch(reject);
1301
+ }, delay);
1302
+ });
1303
+ }
1304
+ };
1305
+
1306
+ //#endregion
1307
+ //#region src/config.ts
1308
+ const SFNEXT_BASE_CARTRIDGE_NAME = "app_storefrontnext_base";
1309
+ const SFNEXT_BASE_CARTRIDGE_OUTPUT_DIR = `${SFNEXT_BASE_CARTRIDGE_NAME}/cartridge/experience`;
1310
+ /**
1311
+ * Build MRT SSR configuration for bundle deployment
1312
+ *
1313
+ * Defines which files should be:
1314
+ * - Server-only (ssrOnly): Deployed only to Lambda functions
1315
+ * - Shared (ssrShared): Deployed to both Lambda and CDN
1316
+ *
1317
+ * @param buildDirectory - Path to the build output directory
1318
+ * @param projectDirectory - Path to the project root (reserved for future use)
1319
+ * @returns MRT SSR configuration with glob patterns
1320
+ */
1321
+ const buildMrtConfig = (_buildDirectory, _projectDirectory) => {
1322
+ const ssrEntryPoint = getMrtEntryFile("production");
1323
+ return {
1324
+ ssrOnly: [
1325
+ "server/**/*",
1326
+ "loader.js",
1327
+ `${ssrEntryPoint}.{js,mjs,cjs}`,
1328
+ `${ssrEntryPoint}.{js,mjs,cjs}.map`,
1329
+ "!static/**/*",
1330
+ "sfnext-server-*.mjs",
1331
+ "!**/*.stories.tsx",
1332
+ "!**/*.stories.ts",
1333
+ "!**/*-snapshot.tsx",
1334
+ "!.storybook/**/*",
1335
+ "!storybook-static/**/*",
1336
+ "!**/__mocks__/**/*",
1337
+ "!**/__snapshots__/**/*"
1338
+ ],
1339
+ ssrShared: [
1340
+ "client/**/*",
1341
+ "static/**/*",
1342
+ "**/*.css",
1343
+ "**/*.png",
1344
+ "**/*.jpg",
1345
+ "**/*.jpeg",
1346
+ "**/*.gif",
1347
+ "**/*.svg",
1348
+ "**/*.ico",
1349
+ "**/*.woff",
1350
+ "**/*.woff2",
1351
+ "**/*.ttf",
1352
+ "**/*.eot",
1353
+ "!**/*.stories.tsx",
1354
+ "!**/*.stories.ts",
1355
+ "!**/*-snapshot.tsx",
1356
+ "!.storybook/**/*",
1357
+ "!storybook-static/**/*",
1358
+ "!**/__mocks__/**/*",
1359
+ "!**/__snapshots__/**/*"
1360
+ ],
1361
+ ssrParameters: { ssrFunctionNodeVersion: "24.x" }
1362
+ };
1363
+ };
1364
+
1365
+ //#endregion
1366
+ //#region src/commands/push.ts
1367
+ /**
1368
+ * Main function to push bundle to Managed Runtime
1369
+ */
1370
+ async function push(options) {
1371
+ const mrtConfig = getMrtConfig(options.projectDirectory);
1372
+ const resolvedTarget = options.target ?? mrtConfig.defaultMrtTarget;
1373
+ if (options.wait && !resolvedTarget) throw new Error("You must provide a target to deploy to when using --wait (via --target flag or .env MRT_TARGET)");
1374
+ if (options.user && !options.key || !options.user && options.key) throw new Error("You must provide both --user and --key together, or neither");
1375
+ if (!fs.existsSync(options.projectDirectory)) throw new Error(`Project directory "${options.projectDirectory}" does not exist!`);
1376
+ const projectSlug = options.projectSlug ?? mrtConfig.defaultMrtProject;
1377
+ if (!projectSlug || projectSlug.trim() === "") throw new Error("Project slug could not be determined from CLI, .env, or package.json");
1378
+ const target = resolvedTarget;
1379
+ const buildDirectory = options.buildDirectory ?? getDefaultBuildDir(options.projectDirectory);
1380
+ if (!fs.existsSync(buildDirectory)) throw new Error(`Build directory "${buildDirectory}" does not exist!`);
1381
+ try {
1382
+ if (target) process.env.DEPLOY_TARGET = target;
1383
+ let credentials;
1384
+ if (options.user && options.key) credentials = {
1385
+ username: options.user,
1386
+ api_key: options.key
1387
+ };
1388
+ else credentials = await readCredentials(getCredentialsFile(options.cloudOrigin ?? DEFAULT_CLOUD_ORIGIN, options.credentialsFile));
1389
+ const config = buildMrtConfig(buildDirectory, options.projectDirectory);
1390
+ const message = options.message ?? getDefaultMessage(options.projectDirectory);
1391
+ info(`Creating bundle for project: ${projectSlug}`);
1392
+ if (options.projectSlug) debug("Using project slug from CLI argument");
1393
+ else if (process.env.MRT_PROJECT) debug("Using project slug from .env MRT_PROJECT");
1394
+ else debug("Using project slug from package.json name");
1395
+ if (target) {
1396
+ info(`Target environment: ${target}`);
1397
+ if (options.target) debug("Using target from CLI argument");
1398
+ else debug("Using target from .env");
1399
+ }
1400
+ debug("SSR shared files", config.ssrShared);
1401
+ debug("SSR only files", config.ssrOnly);
1402
+ const bundle = await createBundle({
1403
+ message,
1404
+ ssr_parameters: config.ssrParameters,
1405
+ ssr_only: config.ssrOnly,
1406
+ ssr_shared: config.ssrShared,
1407
+ buildDirectory,
1408
+ projectDirectory: options.projectDirectory,
1409
+ projectSlug
1410
+ });
1411
+ const client = new CloudAPIClient({
1412
+ credentials,
1413
+ origin: options.cloudOrigin ?? DEFAULT_CLOUD_ORIGIN
1414
+ });
1415
+ info(`Beginning upload to ${options.cloudOrigin ?? DEFAULT_CLOUD_ORIGIN}`);
1416
+ const data = await client.push(bundle, projectSlug, target);
1417
+ debug("API response", data);
1418
+ (data.warnings || []).forEach(warn);
1419
+ if (options.wait && target) {
1420
+ success("Bundle uploaded - waiting for deployment to complete");
1421
+ await client.waitForDeploy(projectSlug, target);
1422
+ success("Deployment complete!");
1423
+ } else success("Bundle uploaded successfully!");
1424
+ if (data.url) info(`Bundle URL: ${data.url}`);
1425
+ } catch (err) {
1426
+ error(err.message || err?.toString() || "Unknown error");
1427
+ throw err;
1428
+ }
1429
+ }
1430
+
1431
+ //#endregion
1432
+ //#region src/server/ts-import.ts
1433
+ /**
1434
+ * Parse TypeScript paths from tsconfig.json and convert to jiti alias format.
1435
+ *
1436
+ * @param tsconfigPath - Path to tsconfig.json
1437
+ * @param projectDirectory - Project root directory for resolving relative paths
1438
+ * @returns Record of alias mappings for jiti
1439
+ *
1440
+ * @example
1441
+ * // tsconfig.json: { "compilerOptions": { "paths": { "@/*": ["./src/*"] } } }
1442
+ * // Returns: { "@/": "/absolute/path/to/src/" }
1443
+ */
1444
+ function parseTsconfigPaths(tsconfigPath, projectDirectory) {
1445
+ const alias = {};
1446
+ if (!existsSync$1(tsconfigPath)) return alias;
1447
+ try {
1448
+ const tsconfigContent = readFileSync$1(tsconfigPath, "utf-8");
1449
+ const tsconfig = JSON.parse(tsconfigContent);
1450
+ const paths = tsconfig.compilerOptions?.paths;
1451
+ const baseUrl = tsconfig.compilerOptions?.baseUrl || ".";
1452
+ if (paths) {
1453
+ for (const [key, values] of Object.entries(paths)) if (values && values.length > 0) {
1454
+ const aliasKey = key.replace(/\/\*$/, "/");
1455
+ alias[aliasKey] = resolve(projectDirectory, baseUrl, values[0].replace(/\/\*$/, "/").replace(/^\.\//, ""));
1456
+ }
1457
+ }
1458
+ } catch {}
1459
+ return alias;
1460
+ }
1461
+ /**
1462
+ * Import a TypeScript file using jiti with proper path alias resolution.
1463
+ * This is a cross-platform alternative to tsx that works on Windows.
1464
+ *
1465
+ * @param filePath - Absolute path to the TypeScript file to import
1466
+ * @param options - Import options including project directory
1467
+ * @returns The imported module
1468
+ */
1469
+ async function importTypescript(filePath, options) {
1470
+ const { projectDirectory, tsconfigPath = resolve(projectDirectory, "tsconfig.json") } = options;
1471
+ const { createJiti } = await import("jiti");
1472
+ const alias = parseTsconfigPaths(tsconfigPath, projectDirectory);
1473
+ return createJiti(import.meta.url, {
1474
+ fsCache: false,
1475
+ interopDefault: true,
1476
+ alias
1477
+ }).import(filePath);
1478
+ }
1479
+
1480
+ //#endregion
1481
+ //#region src/server/config.ts
1482
+ /**
1483
+ * This is a temporary function before we move the config implementation from
1484
+ * template-retail-rsc-app to the SDK.
1485
+ *
1486
+ * @ TODO: Remove this function after we move the config implementation from
1487
+ * template-retail-rsc-app to the SDK.
1488
+ *
1489
+ */
1490
+ function loadConfigFromEnv() {
1491
+ const shortCode = process.env.PUBLIC__app__commerce__api__shortCode;
1492
+ const organizationId = process.env.PUBLIC__app__commerce__api__organizationId;
1493
+ const clientId = process.env.PUBLIC__app__commerce__api__clientId;
1494
+ const siteId = process.env.PUBLIC__app__commerce__api__siteId;
1495
+ const proxy = process.env.PUBLIC__app__commerce__api__proxy || "/mobify/proxy/api";
1496
+ if (!shortCode) throw new Error("Missing PUBLIC__app__commerce__api__shortCode environment variable.\nPlease set it in your .env file or environment.");
1497
+ if (!organizationId) throw new Error("Missing PUBLIC__app__commerce__api__organizationId environment variable.\nPlease set it in your .env file or environment.");
1498
+ if (!clientId) throw new Error("Missing PUBLIC__app__commerce__api__clientId environment variable.\nPlease set it in your .env file or environment.");
1499
+ if (!siteId) throw new Error("Missing PUBLIC__app__commerce__api__siteId environment variable.\nPlease set it in your .env file or environment.");
1500
+ return { commerce: { api: {
1501
+ shortCode,
1502
+ organizationId,
1503
+ clientId,
1504
+ siteId,
1505
+ proxy
1506
+ } } };
1507
+ }
1508
+ /**
1509
+ * Load storefront-next project configuration from config.server.ts.
1510
+ * Requires projectDirectory to be provided.
1511
+ *
1512
+ * @param projectDirectory - Project directory to load config.server.ts from
1513
+ * @throws Error if config.server.ts is not found or invalid
1514
+ */
1515
+ async function loadProjectConfig(projectDirectory) {
1516
+ const configPath = resolve(projectDirectory, "config.server.ts");
1517
+ const tsconfigPath = resolve(projectDirectory, "tsconfig.json");
1518
+ if (!existsSync$1(configPath)) throw new Error(`config.server.ts not found at ${configPath}.\nPlease ensure config.server.ts exists in your project root.`);
1519
+ const config = (await importTypescript(configPath, {
1520
+ projectDirectory,
1521
+ tsconfigPath
1522
+ })).default;
1523
+ if (!config?.app?.commerce?.api) throw new Error("Invalid config.server.ts: missing app.commerce.api configuration.\nPlease ensure your config.server.ts has the commerce API configuration.");
1524
+ const api = config.app.commerce.api;
1525
+ if (!api.shortCode) throw new Error("Missing shortCode in config.server.ts commerce.api configuration");
1526
+ if (!api.organizationId) throw new Error("Missing organizationId in config.server.ts commerce.api configuration");
1527
+ if (!api.clientId) throw new Error("Missing clientId in config.server.ts commerce.api configuration");
1528
+ if (!api.siteId) throw new Error("Missing siteId in config.server.ts commerce.api configuration");
1529
+ return { commerce: { api: {
1530
+ shortCode: api.shortCode,
1531
+ organizationId: api.organizationId,
1532
+ clientId: api.clientId,
1533
+ siteId: api.siteId,
1534
+ proxy: api.proxy || "/mobify/proxy/api"
1535
+ } } };
1536
+ }
1537
+
1538
+ //#endregion
1539
+ //#region src/utils/paths.ts
1540
+ /**
1541
+ * Copyright 2026 Salesforce, Inc.
1542
+ *
1543
+ * Licensed under the Apache License, Version 2.0 (the "License");
1544
+ * you may not use this file except in compliance with the License.
1545
+ * You may obtain a copy of the License at
1546
+ *
1547
+ * http://www.apache.org/licenses/LICENSE-2.0
1548
+ *
1549
+ * Unless required by applicable law or agreed to in writing, software
1550
+ * distributed under the License is distributed on an "AS IS" BASIS,
1551
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1552
+ * See the License for the specific language governing permissions and
1553
+ * limitations under the License.
1554
+ */
1555
+ /**
1556
+ * Get the Commerce Cloud API URL from a short code
1557
+ */
1558
+ function getCommerceCloudApiUrl(shortCode) {
1559
+ return `https://${shortCode}.api.commercecloud.salesforce.com`;
1560
+ }
1561
+ /**
1562
+ * Get the bundle path for static assets
1563
+ */
1564
+ function getBundlePath(bundleId) {
1565
+ return `/mobify/bundle/${bundleId}/client/`;
1566
+ }
1567
+
1568
+ //#endregion
1569
+ //#region src/server/middleware/proxy.ts
1570
+ /**
1571
+ * Create proxy middleware for Commerce Cloud API
1572
+ * Proxies requests from /mobify/proxy/api to the Commerce Cloud API
1573
+ */
1574
+ function createCommerceProxyMiddleware(config) {
1575
+ return createProxyMiddleware({
1576
+ target: getCommerceCloudApiUrl(config.commerce.api.shortCode),
1577
+ changeOrigin: true
1578
+ });
1579
+ }
1580
+
1581
+ //#endregion
1582
+ //#region src/server/middleware/static.ts
1583
+ /**
1584
+ * Create static file serving middleware for client assets
1585
+ * Serves files from build/client at /mobify/bundle/{BUNDLE_ID}/client/
1586
+ */
1587
+ function createStaticMiddleware(bundleId, projectDirectory) {
1588
+ const bundlePath = getBundlePath(bundleId);
1589
+ const clientBuildDir = path$1.join(projectDirectory, "build", "client");
1590
+ info(`Serving static assets from ${clientBuildDir} at ${bundlePath}`);
1591
+ return express.static(clientBuildDir, { setHeaders: (res) => {
1592
+ res.setHeader("Cache-Control", "public, max-age=31536000, immutable");
1593
+ res.setHeader("x-local-static-cache-control", "1");
1594
+ } });
1595
+ }
1596
+
1597
+ //#endregion
1598
+ //#region src/server/middleware/compression.ts
1599
+ /**
1600
+ * Parse and validate COMPRESSION_LEVEL environment variable
1601
+ * @returns Valid compression level (0-9) or default compression level
1602
+ */
1603
+ function getCompressionLevel() {
1604
+ const raw = process.env.COMPRESSION_LEVEL;
1605
+ const DEFAULT = zlib.constants.Z_DEFAULT_COMPRESSION;
1606
+ if (raw == null || raw.trim() === "") return DEFAULT;
1607
+ const level = Number(raw);
1608
+ if (!(Number.isInteger(level) && level >= 0 && level <= 9)) {
1609
+ warn(`[compression] Invalid COMPRESSION_LEVEL="${raw}". Using default (${DEFAULT}).`);
1610
+ return DEFAULT;
1611
+ }
1612
+ return level;
1613
+ }
1614
+ /**
1615
+ * Create compression middleware for gzip/brotli compression
1616
+ * Used in preview mode to optimize response sizes
1617
+ */
1618
+ function createCompressionMiddleware() {
1619
+ return compression({
1620
+ filter: (req, res) => {
1621
+ if (req.headers["x-no-compression"]) return false;
1622
+ return compression.filter(req, res);
1623
+ },
1624
+ level: getCompressionLevel()
1625
+ });
1626
+ }
1627
+
1628
+ //#endregion
1629
+ //#region src/server/middleware/logging.ts
1630
+ /**
1631
+ * Patterns for URLs to skip logging (static assets and Vite internals)
1632
+ */
1633
+ const SKIP_PATTERNS = [
1634
+ "/@vite/**",
1635
+ "/@id/**",
1636
+ "/@fs/**",
1637
+ "/@react-router/**",
1638
+ "/src/**",
1639
+ "/node_modules/**",
1640
+ "**/*.js",
1641
+ "**/*.css",
1642
+ "**/*.ts",
1643
+ "**/*.tsx",
1644
+ "**/*.js.map",
1645
+ "**/*.css.map"
1646
+ ];
1647
+ /**
1648
+ * Create request logging middleware
1649
+ * Used in dev and preview modes for request visibility
1650
+ */
1651
+ function createLoggingMiddleware() {
1652
+ morgan.token("status-colored", (req, res) => {
1653
+ const status = res.statusCode;
1654
+ let color = chalk.green;
1655
+ if (status >= 500) color = chalk.red;
1656
+ else if (status >= 400) color = chalk.yellow;
1657
+ else if (status >= 300) color = chalk.cyan;
1658
+ return color(String(status));
1659
+ });
1660
+ morgan.token("method-colored", (req) => {
1661
+ const method = req.method;
1662
+ const colors$1 = {
1663
+ GET: chalk.green,
1664
+ POST: chalk.blue,
1665
+ PUT: chalk.yellow,
1666
+ DELETE: chalk.red,
1667
+ PATCH: chalk.magenta
1668
+ };
1669
+ return (method && colors$1[method] || chalk.white)(method);
1670
+ });
1671
+ return morgan((tokens, req, res) => {
1672
+ return [
1673
+ chalk.gray("["),
1674
+ tokens["method-colored"](req, res),
1675
+ chalk.gray("]"),
1676
+ tokens.url(req, res),
1677
+ "-",
1678
+ tokens["status-colored"](req, res),
1679
+ chalk.gray(`(${tokens["response-time"](req, res)}ms)`)
1680
+ ].join(" ");
1681
+ }, { skip: (req) => {
1682
+ return SKIP_PATTERNS.some((pattern) => minimatch(req.url, pattern, { dot: true }));
1683
+ } });
1684
+ }
1685
+
1686
+ //#endregion
1687
+ //#region src/server/middleware/host-header.ts
1688
+ /**
1689
+ * Normalizes the X-Forwarded-Host header to support React Router's CSRF validation features.
1690
+ *
1691
+ * NOTE: This middleware performs header manipulation as a temporary, internal
1692
+ * solution for MRT/Lambda environments. It may be updated or removed if React Router
1693
+ * introduces a first-class configuration for validating against forwarded headers.
1694
+ *
1695
+ * React Router v7.12+ uses the X-Forwarded-Host header (preferring it over Host)
1696
+ * to validate request origins for security. In Managed Runtime (MRT) with a vanity
1697
+ * domain, the eCDN automatically sets the X-Forwarded-Host to the vanity domain.
1698
+ * React Router handles cases where this header contains multiple comma-separated
1699
+ * values by prioritizing the first entry.
1700
+ *
1701
+ * This middleware ensures that X-Forwarded-Host is always present by falling back
1702
+ * to a configured public domain if the header is missing (e.g., local development).
1703
+ * By only modifying X-Forwarded-Host, we provide a consistent environment for
1704
+ * React Router's security checks without modifying the internal 'Host' header,
1705
+ * which is required for environment-specific routing logic (e.g., Hybrid Proxy).
1706
+ *
1707
+ * Priority order:
1708
+ * 1. X-Forwarded-Host: Automatically set by eCDN for vanity domains.
1709
+ * 2. EXTERNAL_DOMAIN_NAME: Fallback environment variable for the public domain
1710
+ * used when no forwarded headers are present (e.g., local development).
1711
+ */
1712
+ function createHostHeaderMiddleware() {
1713
+ return (req, _res, next) => {
1714
+ if (!req.get("x-forwarded-host") && process.env.EXTERNAL_DOMAIN_NAME) req.headers["x-forwarded-host"] = process.env.EXTERNAL_DOMAIN_NAME;
1715
+ next();
1716
+ };
1717
+ }
1718
+
1719
+ //#endregion
1720
+ //#region src/server/utils.ts
1721
+ /**
1722
+ * Patch React Router build to rewrite asset URLs with the correct bundle path
1723
+ * This is needed because the build output uses /assets/ but we preview at /mobify/bundle/{BUNDLE_ID}/client/assets/
1724
+ */
1725
+ function patchReactRouterBuild(build, bundleId) {
1726
+ const bundlePath = getBundlePath(bundleId);
1727
+ const patchedAssetsJson = JSON.stringify(build.assets).replace(/"\/assets\//g, `"${bundlePath}assets/`);
1728
+ const newAssets = JSON.parse(patchedAssetsJson);
1729
+ return Object.assign({}, build, {
1730
+ publicPath: bundlePath,
1731
+ assets: newAssets
1732
+ });
1733
+ }
1734
+
1735
+ //#endregion
1736
+ //#region src/server/modes.ts
1737
+ /**
1738
+ * Default feature configuration for each server mode
1739
+ */
1740
+ const ServerModeFeatureMap = {
1741
+ development: {
1742
+ enableProxy: true,
1743
+ enableStaticServing: false,
1744
+ enableCompression: false,
1745
+ enableLogging: true,
1746
+ enableAssetUrlPatching: false
1747
+ },
1748
+ preview: {
1749
+ enableProxy: true,
1750
+ enableStaticServing: true,
1751
+ enableCompression: true,
1752
+ enableLogging: true,
1753
+ enableAssetUrlPatching: true
1754
+ },
1755
+ production: {
1756
+ enableProxy: false,
1757
+ enableStaticServing: false,
1758
+ enableCompression: true,
1759
+ enableLogging: true,
1760
+ enableAssetUrlPatching: true
1761
+ }
1762
+ };
1763
+
1764
+ //#endregion
1765
+ //#region src/server/index.ts
1766
+ /**
1767
+ * Create a unified Express server for development, preview, or production mode
1768
+ */
1769
+ async function createServer(options) {
1770
+ const { mode, projectDirectory = process.cwd(), config: providedConfig, vite, build, streaming = false, enableProxy = ServerModeFeatureMap[mode].enableProxy, enableStaticServing = ServerModeFeatureMap[mode].enableStaticServing, enableCompression = ServerModeFeatureMap[mode].enableCompression, enableLogging = ServerModeFeatureMap[mode].enableLogging, enableAssetUrlPatching = ServerModeFeatureMap[mode].enableAssetUrlPatching } = options;
1771
+ if (mode === "development" && !vite) throw new Error("Vite dev server instance is required for development mode");
1772
+ if ((mode === "preview" || mode === "production") && !build) throw new Error("React Router server build is required for preview/production mode");
1773
+ const config = providedConfig ?? loadConfigFromEnv();
1774
+ const bundleId = process.env.BUNDLE_ID ?? "local";
1775
+ const app = express();
1776
+ app.disable("x-powered-by");
1777
+ if (enableLogging) app.use(createLoggingMiddleware());
1778
+ if (enableCompression && !streaming) app.use(createCompressionMiddleware());
1779
+ if (enableStaticServing && build) {
1780
+ const bundlePath = getBundlePath(bundleId);
1781
+ app.use(bundlePath, createStaticMiddleware(bundleId, projectDirectory));
1782
+ }
1783
+ const middlewareRegistryPath = resolve(projectDirectory, "src/server/middleware-registry.ts");
1784
+ if (existsSync$1(middlewareRegistryPath)) {
1785
+ const registry = await importTypescript(middlewareRegistryPath, { projectDirectory });
1786
+ if (registry.customMiddlewares && Array.isArray(registry.customMiddlewares)) registry.customMiddlewares.forEach((middleware) => {
1787
+ app.use(middleware);
1788
+ });
1789
+ }
1790
+ if (mode === "development" && vite) app.use(vite.middlewares);
1791
+ if (enableProxy) app.use(config.commerce.api.proxy, createCommerceProxyMiddleware(config));
1792
+ app.use(createHostHeaderMiddleware());
1793
+ app.all("*", await createSSRHandler(mode, bundleId, vite, build, enableAssetUrlPatching));
1794
+ return app;
1795
+ }
1796
+ /**
1797
+ * Create the SSR request handler based on mode
1798
+ */
1799
+ async function createSSRHandler(mode, bundleId, vite, build, enableAssetUrlPatching) {
1800
+ if (mode === "development" && vite) {
1801
+ const { isRunnableDevEnvironment } = await import("vite");
1802
+ return async (req, res, next) => {
1803
+ try {
1804
+ const ssrEnvironment = vite.environments.ssr;
1805
+ if (!isRunnableDevEnvironment(ssrEnvironment)) {
1806
+ next(/* @__PURE__ */ new Error("SSR environment is not runnable. Please ensure:\n 1. \"@salesforce/storefront-next-dev\" plugin is added to vite.config.ts\n 2. React Router config uses the Storefront Next preset"));
1807
+ return;
1808
+ }
1809
+ await createRequestHandler({
1810
+ build: await ssrEnvironment.runner.import("virtual:react-router/server-build"),
1811
+ mode: process.env.NODE_ENV
1812
+ })(req, res, next);
1813
+ } catch (error$1) {
1814
+ vite.ssrFixStacktrace(error$1);
1815
+ next(error$1);
1816
+ }
1817
+ };
1818
+ } else if (build) {
1819
+ let patchedBuild = build;
1820
+ if (enableAssetUrlPatching) patchedBuild = patchReactRouterBuild(build, bundleId);
1821
+ return createRequestHandler({
1822
+ build: patchedBuild,
1823
+ mode: process.env.NODE_ENV
1824
+ });
1825
+ } else throw new Error("Invalid server configuration: no vite or build provided");
1826
+ }
1827
+
1828
+ //#endregion
1829
+ //#region src/extensibility/path-util.ts
1830
+ const FILE_EXTENSIONS = [
1831
+ ".tsx",
1832
+ ".ts",
1833
+ ".d.ts"
1834
+ ];
1835
+ function isSupportedFileExtension(fileName) {
1836
+ return FILE_EXTENSIONS.some((ext) => fileName.endsWith(ext));
1837
+ }
1838
+
1839
+ //#endregion
1840
+ //#region src/extensibility/trim-extensions.ts
1841
+ const SINGLE_LINE_MARKER = "@sfdc-extension-line";
1842
+ const BLOCK_MARKER_START = "@sfdc-extension-block-start";
1843
+ const BLOCK_MARKER_END = "@sfdc-extension-block-end";
1844
+ const FILE_MARKER = "@sfdc-extension-file";
1845
+ let verbose = false;
1846
+ function trimExtensions(directory, selectedExtensions, extensionConfig, verboseOverride = false) {
1847
+ const startTime = Date.now();
1848
+ verbose = verboseOverride ?? false;
1849
+ const configuredExtensions = extensionConfig?.extensions || {};
1850
+ const extensions = {};
1851
+ Object.keys(configuredExtensions).forEach((pluginKey) => {
1852
+ extensions[pluginKey] = Boolean(selectedExtensions?.[pluginKey]) || false;
1853
+ });
1854
+ if (Object.keys(extensions).length === 0) {
1855
+ if (verbose) console.log("No plugins found, skipping trim");
1856
+ return;
1857
+ }
1858
+ const processDirectory = (dir) => {
1859
+ fs$1.readdirSync(dir).forEach((file) => {
1860
+ const filePath = path$1.join(dir, file);
1861
+ const stats = fs$1.statSync(filePath);
1862
+ if (!filePath.includes("node_modules")) {
1863
+ if (stats.isDirectory()) processDirectory(filePath);
1864
+ else if (isSupportedFileExtension(file)) processFile(filePath, extensions);
1865
+ }
1866
+ });
1867
+ };
1868
+ processDirectory(directory);
1869
+ if (extensionConfig?.extensions) {
1870
+ deleteExtensionFolders(directory, extensions, extensionConfig);
1871
+ updateExtensionConfig(directory, extensions);
1872
+ }
1873
+ const endTime = Date.now();
1874
+ if (verbose) console.log(`Trim extensions took ${endTime - startTime}ms`);
1875
+ }
1876
+ /**
1877
+ * Update the extension config file to only include the selected extensions.
1878
+ * @param projectDirectory - The project directory
1879
+ * @param extensionSelections - The selected extensions
1880
+ */
1881
+ function updateExtensionConfig(projectDirectory, extensionSelections) {
1882
+ const extensionConfigPath = path$1.join(projectDirectory, "src", "extensions", "config.json");
1883
+ const extensionConfig = JSON.parse(fs$1.readFileSync(extensionConfigPath, "utf8"));
1884
+ Object.keys(extensionConfig.extensions).forEach((extensionKey) => {
1885
+ if (!extensionSelections[extensionKey]) delete extensionConfig.extensions[extensionKey];
1886
+ });
1887
+ fs$1.writeFileSync(extensionConfigPath, JSON.stringify({ extensions: extensionConfig.extensions }, null, 4), "utf8");
1888
+ }
1889
+ /**
1890
+ * Process a file to trim extension-specific code based on markers.
1891
+ * @param filePath - The file path to process
1892
+ * @param extensions - The extension selections
1893
+ */
1894
+ function processFile(filePath, extensions) {
1895
+ const source = fs$1.readFileSync(filePath, "utf-8");
1896
+ if (source.includes(FILE_MARKER)) {
1897
+ const markerLine = source.split("\n").find((line) => line.includes(FILE_MARKER));
1898
+ const extMatch = Object.keys(extensions).find((ext) => markerLine.includes(ext));
1899
+ if (!extMatch) {
1900
+ if (verbose) console.warn(`File ${filePath} is marked with ${markerLine} but it does not match any known extensions`);
1901
+ } else if (extensions[extMatch] === false) {
1902
+ try {
1903
+ fs$1.unlinkSync(filePath);
1904
+ if (verbose) console.log(`Deleted file ${filePath}`);
1905
+ } catch (e) {
1906
+ const error$1 = e;
1907
+ console.error(`Error deleting file ${filePath}: ${error$1.message}`);
1908
+ throw e;
1909
+ }
1910
+ return;
1911
+ }
1912
+ }
1913
+ const extKeys = Object.keys(extensions);
1914
+ if (new RegExp(extKeys.join("|"), "g").test(source)) {
1915
+ const lines = source.split("\n");
1916
+ const newLines = [];
1917
+ const blockMarkers = [];
1918
+ let skippingBlock = false;
1919
+ let i = 0;
1920
+ while (i < lines.length) {
1921
+ const line = lines[i];
1922
+ if (line.includes(SINGLE_LINE_MARKER)) {
1923
+ const matchingExtension = Object.keys(extensions).find((extension) => line.includes(extension));
1924
+ if (matchingExtension && extensions[matchingExtension] === false) {
1925
+ i += 2;
1926
+ continue;
1927
+ }
1928
+ } else if (line.includes(BLOCK_MARKER_START)) {
1929
+ const matchingExtension = Object.keys(extensions).find((extension) => line.includes(extension));
1930
+ if (matchingExtension) {
1931
+ blockMarkers.push({
1932
+ extension: matchingExtension,
1933
+ line: i
1934
+ });
1935
+ skippingBlock = extensions[matchingExtension] === false;
1936
+ } else if (verbose) console.warn(`Warning: Unknown marker found in ${filePath} at line ${i}: \n${line}`);
1937
+ } else if (line.includes(BLOCK_MARKER_END)) {
1938
+ if (Object.keys(extensions).find((extension) => line.includes(extension))) {
1939
+ const extension = Object.keys(extensions).find((p) => line.includes(p));
1940
+ if (blockMarkers.length === 0) throw new Error(`Block marker mismatch in ${filePath}, encountered end marker ${extension} without a matching start marker at line ${i}:\n${lines[i]}`);
1941
+ const startMarker = blockMarkers.pop();
1942
+ if (!extension || startMarker.extension !== extension) throw new Error(`Block marker mismatch in ${filePath}, expected end marker for ${startMarker.extension} but got ${extension} at line ${i}:\n${lines[i]}`);
1943
+ if (extensions[extension] === false) {
1944
+ skippingBlock = false;
1945
+ i++;
1946
+ continue;
1947
+ }
1948
+ }
1949
+ }
1950
+ if (!skippingBlock) newLines.push(line);
1951
+ i++;
1952
+ }
1953
+ if (blockMarkers.length > 0) throw new Error(`Unclosed end marker found in ${filePath}: ${blockMarkers[blockMarkers.length - 1].extension}`);
1954
+ const newSource = newLines.join("\n");
1955
+ if (newSource !== source) try {
1956
+ fs$1.writeFileSync(filePath, newSource);
1957
+ if (verbose) console.log(`Updated file ${filePath}`);
1958
+ } catch (e) {
1959
+ const error$1 = e;
1960
+ console.error(`Error updating file ${filePath}: ${error$1.message}`);
1961
+ throw e;
1962
+ }
1963
+ }
1964
+ }
1965
+ /**
1966
+ * Delete extension folders for disabled extensions.
1967
+ * @param projectRoot - The project root directory
1968
+ * @param extensions - The extension selections
1969
+ * @param extensionConfig - The extension configuration
1970
+ */
1971
+ function deleteExtensionFolders(projectRoot, extensions, extensionConfig) {
1972
+ const extensionsDir = path$1.join(projectRoot, "src", "extensions");
1973
+ if (!fs$1.existsSync(extensionsDir)) return;
1974
+ const configuredExtensions = extensionConfig.extensions;
1975
+ Object.keys(extensions).filter((ext) => extensions[ext] === false).forEach((extKey) => {
1976
+ const extensionMeta = configuredExtensions[extKey];
1977
+ if (extensionMeta?.folder) {
1978
+ const extensionFolderPath = path$1.join(extensionsDir, extensionMeta.folder);
1979
+ if (fs$1.existsSync(extensionFolderPath)) try {
1980
+ fs$1.rmSync(extensionFolderPath, {
1981
+ recursive: true,
1982
+ force: true
1983
+ });
1984
+ if (verbose) console.log(`Deleted extension folder: ${extensionFolderPath}`);
1985
+ } catch (err) {
1986
+ const error$1 = err;
1987
+ if (error$1.code === "EPERM") console.error(`Permission denied - cannot delete ${extensionFolderPath}. You may need to run with sudo or check permissions.`);
1988
+ else console.error(`Error deleting ${extensionFolderPath}: ${error$1.message}`);
1989
+ }
1990
+ }
1991
+ });
1992
+ }
1993
+
1994
+ //#endregion
1995
+ //#region src/cartridge-services/react-router-config.ts
1996
+ let isCliAvailable = null;
1997
+ function checkReactRouterCli(projectDirectory) {
1998
+ if (isCliAvailable !== null) return isCliAvailable;
1999
+ try {
2000
+ execSync$1("react-router --version", {
2001
+ cwd: projectDirectory,
2002
+ env: npmRunPathEnv(),
2003
+ stdio: "pipe"
2004
+ });
2005
+ isCliAvailable = true;
2006
+ } catch {
2007
+ isCliAvailable = false;
2008
+ }
2009
+ return isCliAvailable;
2010
+ }
2011
+ /**
2012
+ * Get the fully resolved routes from React Router by invoking its CLI.
2013
+ * This ensures we get the exact same route resolution as React Router uses internally,
2014
+ * including all presets, file-system routes, and custom route configurations.
2015
+ * @param projectDirectory - The project root directory
2016
+ * @returns Array of resolved route config entries
2017
+ * @example
2018
+ * const routes = getReactRouterRoutes('/path/to/project');
2019
+ * // Returns the same structure as `react-router routes --json`
2020
+ */
2021
+ function getReactRouterRoutes(projectDirectory) {
2022
+ if (!checkReactRouterCli(projectDirectory)) throw new Error("React Router CLI is not available. Please make sure @react-router/dev is installed and accessible.");
2023
+ const tempFile = join(tmpdir(), `react-router-routes-${randomUUID()}.json`);
2024
+ try {
2025
+ execSync$1(`react-router routes --json > "${tempFile}"`, {
2026
+ cwd: projectDirectory,
2027
+ env: npmRunPathEnv(),
2028
+ encoding: "utf-8",
2029
+ stdio: [
2030
+ "pipe",
2031
+ "pipe",
2032
+ "pipe"
2033
+ ]
2034
+ });
2035
+ const output = readFileSync$1(tempFile, "utf-8");
2036
+ return JSON.parse(output);
2037
+ } catch (error$1) {
2038
+ throw new Error(`Failed to get routes from React Router CLI: ${error$1.message}`);
2039
+ } finally {
2040
+ try {
2041
+ if (existsSync$1(tempFile)) unlinkSync(tempFile);
2042
+ } catch {}
2043
+ }
2044
+ }
2045
+ /**
2046
+ * Convert a file path to its corresponding route path using React Router's CLI.
2047
+ * This ensures we get the exact same route resolution as React Router uses internally.
2048
+ * @param filePath - Absolute path to the route file
2049
+ * @param projectRoot - The project root directory
2050
+ * @returns The route path (e.g., '/cart', '/product/:productId')
2051
+ * @example
2052
+ * const route = filePathToRoute('/path/to/project/src/routes/_app.cart.tsx', '/path/to/project');
2053
+ * // Returns: '/cart'
2054
+ */
2055
+ function filePathToRoute(filePath, projectRoot) {
2056
+ const filePathPosix = filePath.replace(/\\/g, "/");
2057
+ const flatRoutes = flattenRoutes(getReactRouterRoutes(projectRoot));
2058
+ for (const route of flatRoutes) {
2059
+ const routeFilePosix = route.file.replace(/\\/g, "/");
2060
+ if (filePathPosix.endsWith(routeFilePosix) || filePathPosix.endsWith(`/${routeFilePosix}`)) return route.path;
2061
+ const routeFileNormalized = routeFilePosix.replace(/^\.\//, "");
2062
+ if (filePathPosix.endsWith(routeFileNormalized) || filePathPosix.endsWith(`/${routeFileNormalized}`)) return route.path;
2063
+ }
2064
+ console.warn(`Warning: Could not find route for file: ${filePath}`);
2065
+ return "/unknown";
2066
+ }
2067
+ /**
2068
+ * Flatten a nested route tree into a flat array with computed paths.
2069
+ * Each route will have its full path computed from parent paths.
2070
+ * @param routes - The nested route config entries
2071
+ * @param parentPath - The parent path prefix (used internally for recursion)
2072
+ * @returns Flat array of routes with their full paths
2073
+ */
2074
+ function flattenRoutes(routes, parentPath = "") {
2075
+ const result = [];
2076
+ for (const route of routes) {
2077
+ let fullPath;
2078
+ if (route.index) fullPath = parentPath || "/";
2079
+ else if (route.path) {
2080
+ const pathSegment = route.path.startsWith("/") ? route.path : `/${route.path}`;
2081
+ fullPath = parentPath ? `${parentPath}${pathSegment}`.replace(/\/+/g, "/") : pathSegment;
2082
+ } else fullPath = parentPath || "/";
2083
+ if (route.id) result.push({
2084
+ id: route.id,
2085
+ path: fullPath,
2086
+ file: route.file,
2087
+ index: route.index
2088
+ });
2089
+ if (route.children && route.children.length > 0) {
2090
+ const childPath = route.path ? fullPath : parentPath;
2091
+ result.push(...flattenRoutes(route.children, childPath));
2092
+ }
2093
+ }
2094
+ return result;
2095
+ }
2096
+
2097
+ //#endregion
2098
+ //#region src/cartridge-services/generate-cartridge.ts
2099
+ const SKIP_DIRECTORIES = [
2100
+ "build",
2101
+ "dist",
2102
+ "node_modules",
2103
+ ".git",
2104
+ ".next",
2105
+ "coverage"
2106
+ ];
2107
+ const DEFAULT_COMPONENT_GROUP = "odyssey_base";
2108
+ const ARCH_TYPE_HEADLESS = "headless";
2109
+ const VALID_ATTRIBUTE_TYPES = [
2110
+ "string",
2111
+ "text",
2112
+ "markup",
2113
+ "integer",
2114
+ "boolean",
2115
+ "product",
2116
+ "category",
2117
+ "file",
2118
+ "page",
2119
+ "image",
2120
+ "url",
2121
+ "enum",
2122
+ "custom",
2123
+ "cms_record"
2124
+ ];
2125
+ const TYPE_MAPPING = {
2126
+ String: "string",
2127
+ string: "string",
2128
+ Number: "integer",
2129
+ number: "integer",
2130
+ Boolean: "boolean",
2131
+ boolean: "boolean",
2132
+ Date: "string",
2133
+ URL: "url",
2134
+ CMSRecord: "cms_record"
2135
+ };
2136
+ function resolveAttributeType(decoratorType, tsMorphType, fieldName) {
2137
+ if (decoratorType) {
2138
+ if (!VALID_ATTRIBUTE_TYPES.includes(decoratorType)) {
2139
+ console.error(`Error: Invalid attribute type '${decoratorType}' for field '${fieldName || "unknown"}'. Valid types are: ${VALID_ATTRIBUTE_TYPES.join(", ")}`);
2140
+ process.exit(1);
2141
+ }
2142
+ return decoratorType;
2143
+ }
2144
+ if (tsMorphType && TYPE_MAPPING[tsMorphType]) return TYPE_MAPPING[tsMorphType];
2145
+ return "string";
2146
+ }
2147
+ function toHumanReadableName(fieldName) {
2148
+ return fieldName.replace(/([A-Z])/g, " $1").replace(/^./, (str) => str.toUpperCase()).trim();
2149
+ }
2150
+ function toCamelCaseFileName(name) {
2151
+ if (!/[\s-]/.test(name)) return name;
2152
+ return name.split(/[\s-]+/).map((word, index) => {
2153
+ if (index === 0) return word.toLowerCase();
2154
+ return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
2155
+ }).join("");
2156
+ }
2157
+ function getTypeFromTsMorph(property, _sourceFile) {
2158
+ try {
2159
+ const typeNode = property.getTypeNode();
2160
+ if (typeNode) return typeNode.getText().split("|")[0].split("&")[0].trim();
2161
+ } catch {}
2162
+ return "string";
2163
+ }
2164
+ function parseExpression(expression) {
2165
+ if (Node.isStringLiteral(expression)) return expression.getLiteralValue();
2166
+ else if (Node.isNumericLiteral(expression)) return expression.getLiteralValue();
2167
+ else if (Node.isTrueLiteral(expression)) return true;
2168
+ else if (Node.isFalseLiteral(expression)) return false;
2169
+ else if (Node.isObjectLiteralExpression(expression)) return parseNestedObject(expression);
2170
+ else if (Node.isArrayLiteralExpression(expression)) return parseArrayLiteral(expression);
2171
+ else return expression.getText();
2172
+ }
2173
+ function parseNestedObject(objectLiteral) {
2174
+ const result = {};
2175
+ try {
2176
+ const properties = objectLiteral.getProperties();
2177
+ for (const property of properties) if (Node.isPropertyAssignment(property)) {
2178
+ const name = property.getName();
2179
+ const initializer = property.getInitializer();
2180
+ if (initializer) result[name] = parseExpression(initializer);
2181
+ }
2182
+ } catch (error$1) {
2183
+ console.warn(`Warning: Could not parse nested object: ${error$1.message}`);
2184
+ return result;
2185
+ }
2186
+ return result;
2187
+ }
2188
+ function parseArrayLiteral(arrayLiteral) {
2189
+ const result = [];
2190
+ try {
2191
+ const elements = arrayLiteral.getElements();
2192
+ for (const element of elements) result.push(parseExpression(element));
2193
+ } catch (error$1) {
2194
+ console.warn(`Warning: Could not parse array literal: ${error$1.message}`);
2195
+ }
2196
+ return result;
2197
+ }
2198
+ function parseDecoratorArgs(decorator) {
2199
+ const result = {};
2200
+ try {
2201
+ const args = decorator.getArguments();
2202
+ if (args.length === 0) return result;
2203
+ const firstArg = args[0];
2204
+ if (Node.isObjectLiteralExpression(firstArg)) {
2205
+ const properties = firstArg.getProperties();
2206
+ for (const property of properties) if (Node.isPropertyAssignment(property)) {
2207
+ const name = property.getName();
2208
+ const initializer = property.getInitializer();
2209
+ if (initializer) result[name] = parseExpression(initializer);
2210
+ }
2211
+ } else if (Node.isStringLiteral(firstArg)) {
2212
+ result.id = parseExpression(firstArg);
2213
+ if (args.length > 1) {
2214
+ const secondArg = args[1];
2215
+ if (Node.isObjectLiteralExpression(secondArg)) {
2216
+ const properties = secondArg.getProperties();
2217
+ for (const property of properties) if (Node.isPropertyAssignment(property)) {
2218
+ const name = property.getName();
2219
+ const initializer = property.getInitializer();
2220
+ if (initializer) result[name] = parseExpression(initializer);
2221
+ }
2222
+ }
2223
+ }
2224
+ }
2225
+ return result;
2226
+ } catch (error$1) {
2227
+ console.warn(`Warning: Could not parse decorator arguments: ${error$1.message}`);
2228
+ return result;
2229
+ }
2230
+ }
2231
+ function extractAttributesFromSource(sourceFile, className) {
2232
+ const attributes = [];
2233
+ try {
2234
+ const classDeclaration = sourceFile.getClass(className);
2235
+ if (!classDeclaration) return attributes;
2236
+ const properties = classDeclaration.getProperties();
2237
+ for (const property of properties) {
2238
+ const attributeDecorator = property.getDecorator("AttributeDefinition");
2239
+ if (!attributeDecorator) continue;
2240
+ const fieldName = property.getName();
2241
+ const config = parseDecoratorArgs(attributeDecorator);
2242
+ const isRequired = !property.hasQuestionToken();
2243
+ const inferredType = config.type || getTypeFromTsMorph(property, sourceFile);
2244
+ const attribute = {
2245
+ id: config.id || fieldName,
2246
+ name: config.name || toHumanReadableName(fieldName),
2247
+ type: resolveAttributeType(config.type, inferredType, fieldName),
2248
+ required: config.required !== void 0 ? config.required : isRequired,
2249
+ description: config.description || `Field: ${fieldName}`
2250
+ };
2251
+ if (config.values) attribute.values = config.values;
2252
+ if (config.defaultValue !== void 0) attribute.default_value = config.defaultValue;
2253
+ attributes.push(attribute);
2254
+ }
2255
+ } catch (error$1) {
2256
+ console.warn(`Warning: Could not extract attributes from class ${className}: ${error$1.message}`);
2257
+ }
2258
+ return attributes;
2259
+ }
2260
+ function extractRegionDefinitionsFromSource(sourceFile, className) {
2261
+ const regionDefinitions = [];
2262
+ try {
2263
+ const classDeclaration = sourceFile.getClass(className);
2264
+ if (!classDeclaration) return regionDefinitions;
2265
+ const classRegionDecorator = classDeclaration.getDecorator("RegionDefinition");
2266
+ if (classRegionDecorator) {
2267
+ const args = classRegionDecorator.getArguments();
2268
+ if (args.length > 0) {
2269
+ const firstArg = args[0];
2270
+ if (Node.isArrayLiteralExpression(firstArg)) {
2271
+ const elements = firstArg.getElements();
2272
+ for (const element of elements) if (Node.isObjectLiteralExpression(element)) {
2273
+ const regionConfig = parseDecoratorArgs({ getArguments: () => [element] });
2274
+ const regionDefinition = {
2275
+ id: regionConfig.id || "region",
2276
+ name: regionConfig.name || "Region"
2277
+ };
2278
+ if (regionConfig.componentTypes) regionDefinition.component_types = regionConfig.componentTypes;
2279
+ if (Array.isArray(regionConfig.componentTypeInclusions)) regionDefinition.component_type_inclusions = regionConfig.componentTypeInclusions.map((incl) => ({ type_id: incl }));
2280
+ if (Array.isArray(regionConfig.componentTypeExclusions)) regionDefinition.component_type_exclusions = regionConfig.componentTypeExclusions.map((excl) => ({ type_id: excl }));
2281
+ if (regionConfig.maxComponents !== void 0) regionDefinition.max_components = regionConfig.maxComponents;
2282
+ if (regionConfig.minComponents !== void 0) regionDefinition.min_components = regionConfig.minComponents;
2283
+ if (regionConfig.allowMultiple !== void 0) regionDefinition.allow_multiple = regionConfig.allowMultiple;
2284
+ if (regionConfig.defaultComponentConstructors) regionDefinition.default_component_constructors = regionConfig.defaultComponentConstructors;
2285
+ regionDefinitions.push(regionDefinition);
2286
+ }
2287
+ }
2288
+ }
2289
+ }
2290
+ } catch (error$1) {
2291
+ console.warn(`Warning: Could not extract region definitions from class ${className}: ${error$1.message}`);
2292
+ }
2293
+ return regionDefinitions;
2294
+ }
2295
+ async function processComponentFile(filePath, _projectRoot) {
2296
+ try {
2297
+ const content = await readFile(filePath, "utf-8");
2298
+ const components = [];
2299
+ if (!content.includes("@Component")) return components;
2300
+ try {
2301
+ const sourceFile = new Project({
2302
+ useInMemoryFileSystem: true,
2303
+ skipAddingFilesFromTsConfig: true
2304
+ }).createSourceFile(filePath, content);
2305
+ const classes = sourceFile.getClasses();
2306
+ for (const classDeclaration of classes) {
2307
+ const componentDecorator = classDeclaration.getDecorator("Component");
2308
+ if (!componentDecorator) continue;
2309
+ const className = classDeclaration.getName();
2310
+ if (!className) continue;
2311
+ const componentConfig = parseDecoratorArgs(componentDecorator);
2312
+ const attributes = extractAttributesFromSource(sourceFile, className);
2313
+ const regionDefinitions = extractRegionDefinitionsFromSource(sourceFile, className);
2314
+ const componentMetadata = {
2315
+ typeId: componentConfig.id || className.toLowerCase(),
2316
+ name: componentConfig.name || toHumanReadableName(className),
2317
+ group: componentConfig.group || DEFAULT_COMPONENT_GROUP,
2318
+ description: componentConfig.description || `Custom component: ${className}`,
2319
+ regionDefinitions,
2320
+ attributes
2321
+ };
2322
+ components.push(componentMetadata);
2323
+ }
2324
+ } catch (error$1) {
2325
+ console.warn(`Warning: Could not process file ${filePath}:`, error$1.message);
2326
+ }
2327
+ return components;
2328
+ } catch (error$1) {
2329
+ console.warn(`Warning: Could not read file ${filePath}:`, error$1.message);
2330
+ return [];
2331
+ }
2332
+ }
2333
+ async function processPageTypeFile(filePath, projectRoot) {
2334
+ try {
2335
+ const content = await readFile(filePath, "utf-8");
2336
+ const pageTypes = [];
2337
+ if (!content.includes("@PageType")) return pageTypes;
2338
+ try {
2339
+ const sourceFile = new Project({
2340
+ useInMemoryFileSystem: true,
2341
+ skipAddingFilesFromTsConfig: true
2342
+ }).createSourceFile(filePath, content);
2343
+ const classes = sourceFile.getClasses();
2344
+ for (const classDeclaration of classes) {
2345
+ const pageTypeDecorator = classDeclaration.getDecorator("PageType");
2346
+ if (!pageTypeDecorator) continue;
2347
+ const className = classDeclaration.getName();
2348
+ if (!className) continue;
2349
+ const pageTypeConfig = parseDecoratorArgs(pageTypeDecorator);
2350
+ const attributes = extractAttributesFromSource(sourceFile, className);
2351
+ const regionDefinitions = extractRegionDefinitionsFromSource(sourceFile, className);
2352
+ const route = filePathToRoute(filePath, projectRoot);
2353
+ const pageTypeMetadata = {
2354
+ typeId: pageTypeConfig.id || className.toLowerCase(),
2355
+ name: pageTypeConfig.name || toHumanReadableName(className),
2356
+ description: pageTypeConfig.description || `Custom page type: ${className}`,
2357
+ regionDefinitions,
2358
+ supportedAspectTypes: pageTypeConfig.supportedAspectTypes || [],
2359
+ attributes,
2360
+ route
2361
+ };
2362
+ pageTypes.push(pageTypeMetadata);
2363
+ }
2364
+ } catch (error$1) {
2365
+ console.warn(`Warning: Could not process file ${filePath}:`, error$1.message);
2366
+ }
2367
+ return pageTypes;
2368
+ } catch (error$1) {
2369
+ console.warn(`Warning: Could not read file ${filePath}:`, error$1.message);
2370
+ return [];
2371
+ }
2372
+ }
2373
+ async function processAspectFile(filePath, _projectRoot) {
2374
+ try {
2375
+ const content = await readFile(filePath, "utf-8");
2376
+ const aspects = [];
2377
+ if (!filePath.endsWith(".json") || !content.trim().startsWith("{")) return aspects;
2378
+ if (!filePath.includes("/aspects/") && !filePath.includes("\\aspects\\")) return aspects;
2379
+ try {
2380
+ const aspectData = JSON.parse(content);
2381
+ const fileName = basename(filePath, ".json");
2382
+ if (!aspectData.name || !aspectData.attribute_definitions) return aspects;
2383
+ const aspectMetadata = {
2384
+ id: fileName,
2385
+ name: aspectData.name,
2386
+ description: aspectData.description || `Aspect type: ${aspectData.name}`,
2387
+ attributeDefinitions: aspectData.attribute_definitions || [],
2388
+ supportedObjectTypes: aspectData.supported_object_types || []
2389
+ };
2390
+ aspects.push(aspectMetadata);
2391
+ } catch (parseError) {
2392
+ console.warn(`Warning: Could not parse JSON in file ${filePath}:`, parseError.message);
2393
+ }
2394
+ return aspects;
2395
+ } catch (error$1) {
2396
+ console.warn(`Warning: Could not read file ${filePath}:`, error$1.message);
2397
+ return [];
2398
+ }
2399
+ }
2400
+ async function generateComponentCartridge(component, outputDir, dryRun = false) {
2401
+ const fileName = toCamelCaseFileName(component.typeId);
2402
+ const groupDir = join(outputDir, component.group);
2403
+ const outputPath = join(groupDir, `${fileName}.json`);
2404
+ if (!dryRun) {
2405
+ try {
2406
+ await mkdir(groupDir, { recursive: true });
2407
+ } catch {}
2408
+ const attributeDefinitionGroups = [{
2409
+ id: component.typeId,
2410
+ name: component.name,
2411
+ description: component.description,
2412
+ attribute_definitions: component.attributes
2413
+ }];
2414
+ const cartridgeData = {
2415
+ name: component.name,
2416
+ description: component.description,
2417
+ group: component.group,
2418
+ arch_type: ARCH_TYPE_HEADLESS,
2419
+ region_definitions: component.regionDefinitions || [],
2420
+ attribute_definition_groups: attributeDefinitionGroups
2421
+ };
2422
+ await writeFile(outputPath, JSON.stringify(cartridgeData, null, 2));
2423
+ }
2424
+ const prefix = dryRun ? " - [DRY RUN]" : " -";
2425
+ console.log(`${prefix} ${String(component.typeId)}: ${String(component.name)} (${String(component.attributes.length)} attributes) → ${fileName}.json`);
2426
+ }
2427
+ async function generatePageTypeCartridge(pageType, outputDir, dryRun = false) {
2428
+ const fileName = toCamelCaseFileName(pageType.name);
2429
+ const outputPath = join(outputDir, `${fileName}.json`);
2430
+ if (!dryRun) {
2431
+ const cartridgeData = {
2432
+ name: pageType.name,
2433
+ description: pageType.description,
2434
+ arch_type: ARCH_TYPE_HEADLESS,
2435
+ region_definitions: pageType.regionDefinitions || []
2436
+ };
2437
+ if (pageType.attributes && pageType.attributes.length > 0) cartridgeData.attribute_definition_groups = [{
2438
+ id: pageType.typeId || fileName,
2439
+ name: pageType.name,
2440
+ description: pageType.description,
2441
+ attribute_definitions: pageType.attributes
2442
+ }];
2443
+ if (pageType.supportedAspectTypes) cartridgeData.supported_aspect_types = pageType.supportedAspectTypes;
2444
+ if (pageType.route) cartridgeData.route = pageType.route;
2445
+ await writeFile(outputPath, JSON.stringify(cartridgeData, null, 2));
2446
+ }
2447
+ const prefix = dryRun ? " - [DRY RUN]" : " -";
2448
+ console.log(`${prefix} ${String(pageType.name)}: ${String(pageType.description)} (${String(pageType.attributes.length)} attributes) → ${fileName}.json`);
2449
+ }
2450
+ async function generateAspectCartridge(aspect, outputDir, dryRun = false) {
2451
+ const fileName = toCamelCaseFileName(aspect.id);
2452
+ const outputPath = join(outputDir, `${fileName}.json`);
2453
+ if (!dryRun) {
2454
+ const cartridgeData = {
2455
+ name: aspect.name,
2456
+ description: aspect.description,
2457
+ arch_type: ARCH_TYPE_HEADLESS,
2458
+ attribute_definitions: aspect.attributeDefinitions || []
2459
+ };
2460
+ if (aspect.supportedObjectTypes) cartridgeData.supported_object_types = aspect.supportedObjectTypes;
2461
+ await writeFile(outputPath, JSON.stringify(cartridgeData, null, 2));
2462
+ }
2463
+ const prefix = dryRun ? " - [DRY RUN]" : " -";
2464
+ console.log(`${prefix} ${String(aspect.name)}: ${String(aspect.description)} (${String(aspect.attributeDefinitions.length)} attributes) → ${fileName}.json`);
2465
+ }
2466
+ /**
2467
+ * Runs ESLint with --fix on the specified directory to format JSON files.
2468
+ * This ensures generated JSON files match the project's Prettier/ESLint configuration.
2469
+ */
2470
+ function lintGeneratedFiles(metadataDir, projectRoot) {
2471
+ try {
2472
+ console.log("🔧 Running ESLint --fix on generated JSON files...");
2473
+ execSync$1(`npx eslint "${metadataDir}/**/*.json" --fix --no-error-on-unmatched-pattern`, {
2474
+ cwd: projectRoot,
2475
+ stdio: "pipe",
2476
+ encoding: "utf-8"
2477
+ });
2478
+ console.log("✅ JSON files formatted successfully");
2479
+ } catch (error$1) {
2480
+ const execError = error$1;
2481
+ if (execError.status === 2) {
2482
+ const errMsg = execError.stderr || execError.stdout || "Unknown error";
2483
+ console.warn(`⚠️ Warning: Could not run ESLint --fix: ${errMsg}`);
2484
+ } else if (execError.stderr && execError.stderr.includes("error")) console.warn(`⚠️ Warning: Some linting issues could not be auto-fixed. Run ESLint manually to review.`);
2485
+ else console.log("✅ JSON files formatted successfully");
2486
+ }
2487
+ }
2488
+ async function generateMetadata(projectDirectory, metadataDirectory, options) {
2489
+ try {
2490
+ const filePaths = options?.filePaths;
2491
+ const isIncrementalMode = filePaths && filePaths.length > 0;
2492
+ const dryRun = options?.dryRun || false;
2493
+ if (dryRun) console.log("🔍 [DRY RUN] Scanning for decorated components and page types...");
2494
+ else if (isIncrementalMode) console.log(`🔍 Generating metadata for ${filePaths.length} specified file(s)...`);
2495
+ else console.log("🔍 Generating metadata for decorated components and page types...");
2496
+ const projectRoot = resolve(projectDirectory);
2497
+ const srcDir = join(projectRoot, "src");
2498
+ const metadataDir = resolve(metadataDirectory);
2499
+ const componentsOutputDir = join(metadataDir, "components");
2500
+ const pagesOutputDir = join(metadataDir, "pages");
2501
+ const aspectsOutputDir = join(metadataDir, "aspects");
2502
+ if (!dryRun) {
2503
+ if (!isIncrementalMode) {
2504
+ console.log("🗑️ Cleaning existing output directories...");
2505
+ for (const outputDir of [
2506
+ componentsOutputDir,
2507
+ pagesOutputDir,
2508
+ aspectsOutputDir
2509
+ ]) try {
2510
+ await rm(outputDir, {
2511
+ recursive: true,
2512
+ force: true
2513
+ });
2514
+ console.log(` - Deleted: ${outputDir}`);
2515
+ } catch {
2516
+ console.log(` - Directory not found (skipping): ${outputDir}`);
2517
+ }
2518
+ } else console.log("📝 Incremental mode: existing cartridge files will be preserved/overwritten");
2519
+ console.log("📁 Creating output directories...");
2520
+ for (const outputDir of [
2521
+ componentsOutputDir,
2522
+ pagesOutputDir,
2523
+ aspectsOutputDir
2524
+ ]) try {
2525
+ await mkdir(outputDir, { recursive: true });
2526
+ } catch (error$1) {
2527
+ try {
2528
+ await access(outputDir);
2529
+ } catch {
2530
+ console.error(`❌ Error: Failed to create output directory ${outputDir}: ${error$1.message}`);
2531
+ process.exit(1);
2532
+ }
2533
+ }
2534
+ } else if (isIncrementalMode) console.log(`📝 [DRY RUN] Would process ${filePaths.length} specific file(s)`);
2535
+ else console.log("📝 [DRY RUN] Would clean and regenerate all metadata files");
2536
+ let files = [];
2537
+ if (isIncrementalMode && filePaths) {
2538
+ files = filePaths.map((fp) => resolve(projectRoot, fp));
2539
+ console.log(`📂 Processing ${files.length} specified file(s)...`);
2540
+ } else {
2541
+ const scanDirectory = async (dir) => {
2542
+ const entries = await readdir(dir, { withFileTypes: true });
2543
+ for (const entry of entries) {
2544
+ const fullPath = join(dir, entry.name);
2545
+ if (entry.isDirectory()) {
2546
+ if (!SKIP_DIRECTORIES.includes(entry.name)) await scanDirectory(fullPath);
2547
+ } else if (entry.isFile() && (extname(entry.name) === ".ts" || extname(entry.name) === ".tsx" || extname(entry.name) === ".json")) files.push(fullPath);
2548
+ }
2549
+ };
2550
+ await scanDirectory(srcDir);
2551
+ }
2552
+ const allComponents = [];
2553
+ const allPageTypes = [];
2554
+ const allAspects = [];
2555
+ for (const file of files) {
2556
+ const components = await processComponentFile(file, projectRoot);
2557
+ allComponents.push(...components);
2558
+ const pageTypes = await processPageTypeFile(file, projectRoot);
2559
+ allPageTypes.push(...pageTypes);
2560
+ const aspects = await processAspectFile(file, projectRoot);
2561
+ allAspects.push(...aspects);
2562
+ }
2563
+ if (allComponents.length === 0 && allPageTypes.length === 0 && allAspects.length === 0) {
2564
+ console.log("⚠️ No decorated components, page types, or aspect files found.");
2565
+ return {
2566
+ componentsGenerated: 0,
2567
+ pageTypesGenerated: 0,
2568
+ aspectsGenerated: 0,
2569
+ totalFiles: 0
2570
+ };
2571
+ }
2572
+ if (allComponents.length > 0) {
2573
+ console.log(`✅ Found ${allComponents.length} decorated component(s):`);
2574
+ for (const component of allComponents) await generateComponentCartridge(component, componentsOutputDir, dryRun);
2575
+ if (dryRun) console.log(`📄 [DRY RUN] Would generate ${allComponents.length} component metadata file(s) in: ${componentsOutputDir}`);
2576
+ else console.log(`📄 Generated ${allComponents.length} component metadata file(s) in: ${componentsOutputDir}`);
2577
+ }
2578
+ if (allPageTypes.length > 0) {
2579
+ console.log(`✅ Found ${allPageTypes.length} decorated page type(s):`);
2580
+ for (const pageType of allPageTypes) await generatePageTypeCartridge(pageType, pagesOutputDir, dryRun);
2581
+ if (dryRun) console.log(`📄 [DRY RUN] Would generate ${allPageTypes.length} page type metadata file(s) in: ${pagesOutputDir}`);
2582
+ else console.log(`📄 Generated ${allPageTypes.length} page type metadata file(s) in: ${pagesOutputDir}`);
2583
+ }
2584
+ if (allAspects.length > 0) {
2585
+ console.log(`✅ Found ${allAspects.length} decorated aspect(s):`);
2586
+ for (const aspect of allAspects) await generateAspectCartridge(aspect, aspectsOutputDir, dryRun);
2587
+ if (dryRun) console.log(`📄 [DRY RUN] Would generate ${allAspects.length} aspect metadata file(s) in: ${aspectsOutputDir}`);
2588
+ else console.log(`📄 Generated ${allAspects.length} aspect metadata file(s) in: ${aspectsOutputDir}`);
2589
+ }
2590
+ const shouldLintFix = options?.lintFix !== false;
2591
+ if (!dryRun && shouldLintFix && (allComponents.length > 0 || allPageTypes.length > 0 || allAspects.length > 0)) lintGeneratedFiles(metadataDir, projectRoot);
2592
+ return {
2593
+ componentsGenerated: allComponents.length,
2594
+ pageTypesGenerated: allPageTypes.length,
2595
+ aspectsGenerated: allAspects.length,
2596
+ totalFiles: allComponents.length + allPageTypes.length + allAspects.length
2597
+ };
2598
+ } catch (error$1) {
2599
+ console.error("❌ Error:", error$1.message);
2600
+ process.exit(1);
2601
+ }
2602
+ }
2603
+
2604
+ //#endregion
2605
+ export { createServer, storefrontNextPlugins as default, generateMetadata, loadConfigFromEnv, loadProjectConfig, push, transformPluginPlaceholderPlugin, trimExtensions };
2606
+ //# sourceMappingURL=index.js.map