@salesforce/storefront-next-dev 0.2.0-alpha.2 → 0.3.0-alpha.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 (49) hide show
  1. package/dist/cartridge-services/index.d.ts.map +1 -1
  2. package/dist/cartridge-services/index.js +171 -50
  3. package/dist/cartridge-services/index.js.map +1 -1
  4. package/dist/commands/create-bundle.js +12 -11
  5. package/dist/commands/create-instructions.js +7 -5
  6. package/dist/commands/create-storefront.js +18 -22
  7. package/dist/commands/deploy-cartridge.js +67 -26
  8. package/dist/commands/dev.js +6 -4
  9. package/dist/commands/extensions/create.js +2 -0
  10. package/dist/commands/extensions/install.js +3 -7
  11. package/dist/commands/extensions/list.js +2 -0
  12. package/dist/commands/extensions/remove.js +3 -7
  13. package/dist/commands/generate-cartridge.js +23 -2
  14. package/dist/commands/preview.js +15 -10
  15. package/dist/commands/push.js +25 -19
  16. package/dist/commands/validate-cartridge.js +51 -0
  17. package/dist/config.js +74 -47
  18. package/dist/configs/react-router.config.d.ts.map +1 -1
  19. package/dist/configs/react-router.config.js +36 -0
  20. package/dist/configs/react-router.config.js.map +1 -1
  21. package/dist/dependency-utils.js +14 -16
  22. package/dist/entry/server.d.ts.map +1 -1
  23. package/dist/entry/server.js +221 -11
  24. package/dist/entry/server.js.map +1 -1
  25. package/dist/generate-cartridge.js +106 -50
  26. package/dist/index.d.ts +127 -13
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +1147 -167
  29. package/dist/index.js.map +1 -1
  30. package/dist/local-dev-setup.js +13 -13
  31. package/dist/logger/index.d.ts +20 -0
  32. package/dist/logger/index.d.ts.map +1 -0
  33. package/dist/logger/index.js +69 -0
  34. package/dist/logger/index.js.map +1 -0
  35. package/dist/logger.js +79 -33
  36. package/dist/logger2.js +1 -0
  37. package/dist/manage-extensions.js +7 -13
  38. package/dist/mrt/ssr.mjs +60 -72
  39. package/dist/mrt/ssr.mjs.map +1 -1
  40. package/dist/mrt/streamingHandler.mjs +66 -78
  41. package/dist/mrt/streamingHandler.mjs.map +1 -1
  42. package/dist/react-router/Scripts.d.ts +1 -1
  43. package/dist/react-router/Scripts.d.ts.map +1 -1
  44. package/dist/react-router/Scripts.js +38 -2
  45. package/dist/react-router/Scripts.js.map +1 -1
  46. package/dist/server.js +296 -16
  47. package/dist/utils.js +4 -4
  48. package/dist/validate-cartridge.js +45 -0
  49. package/package.json +22 -5
package/dist/index.js CHANGED
@@ -1,30 +1,106 @@
1
1
  import path, { basename, extname, join, resolve } from "node:path";
2
2
  import fs from "fs-extra";
3
+ import chalk from "chalk";
3
4
  import path$1, { dirname, join as join$1, relative, resolve as resolve$1 } from "path";
4
5
  import { fileURLToPath, pathToFileURL } from "url";
5
6
  import { parse } from "@babel/parser";
6
- import { isJSXAttribute, isJSXElement, isJSXFragment, isJSXIdentifier, jsxClosingElement, jsxClosingFragment, jsxElement, jsxFragment, jsxIdentifier, jsxOpeningElement, jsxOpeningFragment, jsxText } from "@babel/types";
7
+ import { isArrayPattern, isClassDeclaration, isExportSpecifier, isFunctionDeclaration, isIdentifier, isJSXAttribute, isJSXElement, isJSXFragment, isJSXIdentifier, isMemberExpression, isObjectPattern, isObjectProperty, isRestElement, isVariableDeclaration, jsxClosingElement, jsxClosingFragment, jsxElement, jsxFragment, jsxIdentifier, jsxOpeningElement, jsxOpeningFragment, jsxText } from "@babel/types";
7
8
  import { generate } from "@babel/generator";
8
9
  import traverseModule from "@babel/traverse";
9
10
  import fs$1, { existsSync, readFileSync, writeFileSync } from "fs";
10
11
  import { glob } from "glob";
11
12
  import { Node, Project, ts } from "ts-morph";
12
13
  import fs$2, { existsSync as existsSync$1, readFileSync as readFileSync$1, unlinkSync } from "node:fs";
14
+ import { deadCodeElimination, findReferencedIdentifiers } from "babel-dead-code-elimination";
15
+ import httpProxy from "http-proxy";
16
+ import { brotliDecompressSync, gunzipSync, inflateSync } from "zlib";
13
17
  import express from "express";
14
18
  import { createRequestHandler } from "@react-router/express";
15
19
  import { pathToFileURL as pathToFileURL$1 } from "node:url";
16
20
  import { createProxyMiddleware } from "http-proxy-middleware";
17
- import chalk from "chalk";
18
21
  import compression from "compression";
19
22
  import zlib from "node:zlib";
20
23
  import morgan from "morgan";
21
24
  import { minimatch } from "minimatch";
25
+ import { SpanStatusCode, context, trace } from "@opentelemetry/api";
26
+ import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
27
+ import { ConsoleSpanExporter, SimpleSpanProcessor } from "@opentelemetry/sdk-trace-base";
28
+ import { ExportResultCode, hrTimeToTimeStamp } from "@opentelemetry/core";
29
+ import { Resource } from "@opentelemetry/resources";
30
+ import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions";
31
+ import { registerInstrumentations } from "@opentelemetry/instrumentation";
32
+ import { UndiciInstrumentation } from "@opentelemetry/instrumentation-undici";
22
33
  import { access, mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
23
34
  import { execSync } from "node:child_process";
24
35
  import { tmpdir } from "node:os";
25
36
  import { randomUUID } from "node:crypto";
26
37
  import { npmRunPathEnv } from "npm-run-path";
27
38
 
39
+ //#region src/utils/logger.ts
40
+ const LEVEL_PRIORITY = {
41
+ error: 0,
42
+ warn: 1,
43
+ info: 2,
44
+ debug: 3
45
+ };
46
+ let overrideLevel;
47
+ /**
48
+ * Returns true when the `DEBUG` env var targets sfnext or is a general enable flag.
49
+ * Avoids accidentally enabling debug mode when DEBUG is set for unrelated libraries
50
+ * (e.g. `DEBUG=express:*`).
51
+ */
52
+ function debugEnablesSfnext() {
53
+ const raw = process.env.DEBUG?.trim();
54
+ if (!raw) return false;
55
+ const normalized = raw.toLowerCase();
56
+ if ([
57
+ "1",
58
+ "true",
59
+ "yes",
60
+ "on"
61
+ ].includes(normalized)) return true;
62
+ return raw.split(",").some((token) => {
63
+ const value = token.trim();
64
+ return value === "*" || value === "sfnext" || value === "sfnext:*";
65
+ });
66
+ }
67
+ function resolveLevel() {
68
+ if (overrideLevel) return overrideLevel;
69
+ const envLevel = process.env.MRT_LOG_LEVEL ?? process.env.SFCC_LOG_LEVEL;
70
+ if (envLevel && envLevel in LEVEL_PRIORITY) return envLevel;
71
+ if (debugEnablesSfnext()) return "debug";
72
+ if (process.env.NODE_ENV === "production") return "warn";
73
+ return "info";
74
+ }
75
+ function shouldLog(level) {
76
+ return LEVEL_PRIORITY[level] <= LEVEL_PRIORITY[resolveLevel()];
77
+ }
78
+ const logger = {
79
+ error(msg, ...args) {
80
+ if (!shouldLog("error")) return;
81
+ console.error(chalk.red("[sfnext:error]"), msg, ...args);
82
+ },
83
+ warn(msg, ...args) {
84
+ if (!shouldLog("warn")) return;
85
+ console.warn(chalk.yellow("[sfnext:warn]"), msg, ...args);
86
+ },
87
+ info(msg, ...args) {
88
+ if (!shouldLog("info")) return;
89
+ console.log(chalk.cyan("[sfnext:info]"), msg, ...args);
90
+ },
91
+ debug(msg, ...args) {
92
+ if (!shouldLog("debug")) return;
93
+ console.log(chalk.gray("[sfnext:debug]"), msg, ...args);
94
+ },
95
+ setLevel(level) {
96
+ overrideLevel = level;
97
+ },
98
+ getLevel() {
99
+ return resolveLevel();
100
+ }
101
+ };
102
+
103
+ //#endregion
28
104
  //#region src/plugins/fixReactRouterManifestUrls.ts
29
105
  function patchAssetsPaths(dir) {
30
106
  const entries = fs.readdirSync(dir, { withFileTypes: true });
@@ -35,7 +111,7 @@ function patchAssetsPaths(dir) {
35
111
  const content = fs.readFileSync(fullPath, "utf-8");
36
112
  if (content.includes("\"/assets/") || content.includes("'/assets/")) {
37
113
  fs.writeFileSync(fullPath, content.replace(/["']\/assets\//g, "(window._BUNDLE_PATH || \"/\") + \"assets/"));
38
- console.log(`patched /assets/ references in ${fullPath}`);
114
+ logger.debug(`patched /assets/ references in ${fullPath}`);
39
115
  }
40
116
  }
41
117
  }
@@ -90,10 +166,41 @@ function getCommerceCloudApiUrl(shortCode, proxyHost) {
90
166
  return proxyHost || `https://${shortCode}.api.commercecloud.salesforce.com`;
91
167
  }
92
168
  /**
169
+ * Get the configurable base path for the application.
170
+ * Reads from MRT_ENV_BASE_PATH environment variable.
171
+ *
172
+ * The base path is used for CDN routing to the correct MRT environment.
173
+ * It is prepended to all URLs: page routes, /mobify/bundle/ assets, and /mobify/proxy/api.
174
+ *
175
+ * Validation rules:
176
+ * - Must be a single path segment starting with '/'
177
+ * - Max 63 characters after the leading slash
178
+ * - Only URL-safe characters allowed
179
+ * - Returns empty string if not set
180
+ *
181
+ * @returns The sanitized base path (e.g., '/site-a' or '')
182
+ *
183
+ * @example
184
+ * // No base path configured
185
+ * getBasePath() // → ''
186
+ *
187
+ * // With base path '/storefront'
188
+ * getBasePath() // → '/storefront'
189
+ *
190
+ * // Automatically sanitizes
191
+ * // MRT_ENV_BASE_PATH='storefront/' → '/storefront'
192
+ */
193
+ function getBasePath() {
194
+ const basePath = process.env.MRT_ENV_BASE_PATH?.trim();
195
+ if (!basePath) return "";
196
+ if (!/^\/[a-zA-Z0-9_.+$~"'@:-]{1,63}$/.test(basePath)) throw new Error(`Invalid base path: "${basePath}". Base path must be a single segment starting with '/' (e.g., '/site-a'), contain only URL-safe characters, and be at most 63 characters after the leading slash.`);
197
+ return basePath;
198
+ }
199
+ /**
93
200
  * Get the bundle path for static assets
94
201
  */
95
202
  function getBundlePath(bundleId) {
96
- return `/mobify/bundle/${bundleId}/client/`;
203
+ return `${getBasePath()}/mobify/bundle/${bundleId}/client/`;
97
204
  }
98
205
 
99
206
  //#endregion
@@ -230,10 +337,16 @@ const managedRuntimeBundlePlugin = () => {
230
337
  await fs.outputFile(loaderPath, "// This file is intentionally empty");
231
338
  const prebuiltMrtEntryPath = path$1.resolve(__dirname$1, `./mrt/${mrtEntryFile}`);
232
339
  await fs.copy(prebuiltMrtEntryPath, mrtEntryPath);
340
+ const prebuiltMrtEntryMapPath = `${prebuiltMrtEntryPath}.map`;
341
+ if (await fs.pathExists(prebuiltMrtEntryMapPath)) await fs.copy(prebuiltMrtEntryMapPath, `${mrtEntryPath}.map`);
233
342
  const mrtDir = path$1.resolve(__dirname$1, "./mrt");
234
343
  if (await fs.pathExists(mrtDir)) {
235
344
  const files = await fs.readdir(mrtDir);
236
- 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));
345
+ for (const file of files) if (file.startsWith("sfnext-server-") && file.endsWith(".mjs")) {
346
+ await fs.copy(path$1.join(mrtDir, file), path$1.resolve(buildDirectory, file));
347
+ const mapFile = `${file}.map`;
348
+ if (files.includes(mapFile)) await fs.copy(path$1.join(mrtDir, mapFile), path$1.resolve(buildDirectory, mapFile));
349
+ }
237
350
  }
238
351
  const packageJsonPath = path$1.resolve(resolvedConfig.root, "package.json");
239
352
  const buildPackageJsonPath = path$1.resolve(buildDirectory, "package.json");
@@ -246,9 +359,13 @@ const managedRuntimeBundlePlugin = () => {
246
359
  apply: "build",
247
360
  config({ mode }) {
248
361
  return {
362
+ build: { rollupOptions: { onLog(level, log, defaultHandler) {
363
+ if (log.code === "SOURCEMAP_ERROR" && log.message.includes("resolve original location")) return;
364
+ defaultHandler(level, log);
365
+ } } },
249
366
  environments: { ssr: { resolve: { noExternal: true } } },
250
367
  experimental: { renderBuiltUrl(filename, { type }) {
251
- 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)}` };
368
+ if (mode !== "preview" && (type === "asset" || type === "public")) return { runtime: `(typeof window !== 'undefined' ? window._BUNDLE_PATH : ((process.env.MRT_ENV_BASE_PATH??'')+'/mobify/bundle/'+(process.env.BUNDLE_ID??'local')+'/client/')) + ${JSON.stringify(filename)}` };
252
369
  } }
253
370
  };
254
371
  },
@@ -311,7 +428,7 @@ const patchReactRouterPlugin = () => {
311
428
 
312
429
  //#endregion
313
430
  //#region src/extensibility/target-utils.ts
314
- const traverse = traverseModule.default || traverseModule;
431
+ const traverse$1 = traverseModule.default || traverseModule;
315
432
  const TARGET_COMPONENT_TAG = "UITarget";
316
433
  const TARGET_PROVIDERS_TAG = "TargetProviders";
317
434
  const TARGET_ID_ATTRIBUTE = "targetId";
@@ -376,7 +493,7 @@ function runReplacementPass(ast, tagName, targetRegistry = null, contextProvider
376
493
  if (replacedId) targetIdsReplaced.add(replacedId);
377
494
  } else if (contextProviders) findAndReplaceProviders(pathToReplace, contextProviders);
378
495
  };
379
- traverse(ast, {
496
+ traverse$1(ast, {
380
497
  VariableDeclaration(nodePath) {
381
498
  const declarationPaths = nodePath.get("declarations");
382
499
  const declarationsArray = Array.isArray(declarationPaths) ? declarationPaths : [declarationPaths];
@@ -429,7 +546,7 @@ function transformTargets(code, targetRegistry, contextProviders) {
429
546
  });
430
547
  if (code.includes(TARGET_COMPONENT_TAG)) {
431
548
  const replacementImportStatements = buildReplacementImportStatements(runReplacementPass(ast, TARGET_COMPONENT_TAG, targetRegistry, null), targetRegistry);
432
- traverse(ast, { ImportDeclaration(nodePath) {
549
+ traverse$1(ast, { ImportDeclaration(nodePath) {
433
550
  if (nodePath.node.source.value.includes("@/targets/ui-target")) nodePath.replaceWith(jsxText(replacementImportStatements));
434
551
  } });
435
552
  }
@@ -437,7 +554,7 @@ function transformTargets(code, targetRegistry, contextProviders) {
437
554
  const importStatements = /* @__PURE__ */ new Set();
438
555
  for (const contextProvider of contextProviders) importStatements.add(`import ${contextProvider.componentName} from '@/${contextProvider.path.replace(".tsx", "")}';`);
439
556
  const replacementImportStatements = Array.from(importStatements).join("\n");
440
- traverse(ast, { ImportDeclaration(nodePath) {
557
+ traverse$1(ast, { ImportDeclaration(nodePath) {
441
558
  if (nodePath.node.source.value.includes("@/targets/target-providers")) nodePath.replaceWith(jsxText(replacementImportStatements));
442
559
  } });
443
560
  runReplacementPass(ast, TARGET_PROVIDERS_TAG, null, contextProviders);
@@ -527,7 +644,7 @@ function transformTargetPlaceholderPlugin() {
527
644
  };
528
645
  return null;
529
646
  } catch (err) {
530
- console.error(`UITarget replace ERROR in ${id}: ${err instanceof Error ? err.stack : String(err)}`);
647
+ logger.error(`UITarget replace ERROR in ${id}: ${err instanceof Error ? err.stack : String(err)}`);
531
648
  throw err;
532
649
  }
533
650
  }
@@ -550,7 +667,7 @@ const watchConfigFilesPlugin = () => {
550
667
  server.watcher.add(glob$1);
551
668
  const onChange = (file) => {
552
669
  if (file.endsWith("target-config.json")) {
553
- console.log(`🔁 target-config.json changed: ${file}`);
670
+ logger.debug(`🔁 target-config.json changed: ${file}`);
554
671
  server.restart();
555
672
  }
556
673
  };
@@ -630,12 +747,12 @@ function hasFallbackExport(sourceFile) {
630
747
  /**
631
748
  * Scans all files in the component directory for @Component decorators and extracts metadata using ts-morph
632
749
  */
633
- async function scanComponents(project, projectRoot, componentPath, registryPath, verbose$1) {
750
+ async function scanComponents(project, projectRoot, componentPath, registryPath) {
634
751
  const componentFiles = await glob(`${componentPath}/**/*.{ts,tsx}`, {
635
752
  cwd: projectRoot,
636
753
  absolute: true
637
754
  });
638
- if (verbose$1) console.log(`🔍 Scanning ${componentFiles.length} files in ${componentPath}...`);
755
+ logger.debug(`🔍 Scanning ${componentFiles.length} files in ${componentPath}...`);
639
756
  const components = [];
640
757
  const registryDir = dirname(resolve$1(projectRoot, registryPath));
641
758
  for (const filePath of componentFiles) try {
@@ -660,19 +777,17 @@ async function scanComponents(project, projectRoot, componentPath, registryPath,
660
777
  hasClientLoader: hasClientLoaderExport,
661
778
  hasFallback
662
779
  });
663
- if (verbose$1) {
664
- const exports = [];
665
- if (hasLoaderExport) exports.push("loader");
666
- if (hasClientLoaderExport) exports.push("clientLoader");
667
- if (hasFallback) exports.push("fallback");
668
- const exportsText = exports.length > 0 ? ` (with ${exports.join(", ")})` : "";
669
- console.log(` ✅ Found component: ${componentInfo.id} → ${relativePath}${exportsText}`);
670
- }
780
+ const exports = [];
781
+ if (hasLoaderExport) exports.push("loader");
782
+ if (hasClientLoaderExport) exports.push("clientLoader");
783
+ if (hasFallback) exports.push("fallback");
784
+ const exportsText = exports.length > 0 ? ` (with ${exports.join(", ")})` : "";
785
+ logger.debug(` ✅ Found component: ${componentInfo.id} ${relativePath}${exportsText}`);
671
786
  }
672
787
  }
673
788
  }
674
789
  } catch (error) {
675
- if (verbose$1) console.warn(`⚠️ Could not process ${filePath}:`, error.message);
790
+ logger.warn(`⚠️ Could not process ${filePath}: ${error.message}`);
676
791
  }
677
792
  return components;
678
793
  }
@@ -720,10 +835,10 @@ ${registrations}
720
835
  /**
721
836
  * Updates the registry.ts file with the generated code
722
837
  */
723
- function updateRegistryFile(registryFilePath, generatedCode, verbose$1) {
838
+ function updateRegistryFile(registryFilePath, generatedCode) {
724
839
  let existingContent;
725
840
  if (!existsSync(registryFilePath)) {
726
- if (verbose$1) console.log(`📝 Creating new registry file...`);
841
+ logger.debug("📝 Creating new registry file...");
727
842
  const basicRegistryContent = `import { ComponentRegistry } from '@/lib/component-registry';
728
843
 
729
844
  // Create the component registry instance
@@ -748,7 +863,7 @@ export const registry = new ComponentRegistry();
748
863
  const updatedContent = `${existingContent.slice(0, startIndex + 24)}\n${generatedCode}\n${existingContent.slice(endIndex)}`;
749
864
  try {
750
865
  writeFileSync(registryFilePath, updatedContent, "utf-8");
751
- if (verbose$1) console.log(`💾 Updated registry file: ${registryFilePath}`);
866
+ logger.debug(`💾 Updated registry file: ${registryFilePath}`);
752
867
  } catch (error) {
753
868
  throw new Error(`Failed to write registry file: ${error.message}`);
754
869
  }
@@ -777,10 +892,10 @@ export const registry = new ComponentRegistry();
777
892
  * })
778
893
  */
779
894
  const staticRegistryPlugin = (config = {}) => {
780
- const { componentPath = "src/components", registryPath = "src/lib/static-registry.ts", registryIdentifier = "registry", failOnError = true, verbose: verbose$1 = false } = config;
895
+ const { componentPath = "src/components", registryPath = "src/lib/static-registry.ts", registryIdentifier = "registry", failOnError = true } = config;
781
896
  let projectRoot;
782
897
  const runRegistryGeneration = async () => {
783
- if (verbose$1) console.log("🚀 Starting static registry generation...");
898
+ logger.debug("🚀 Starting static registry generation...");
784
899
  const components = await scanComponents(new Project({ compilerOptions: {
785
900
  target: ts.ScriptTarget.Latest,
786
901
  module: ts.ModuleKind.ESNext,
@@ -788,12 +903,12 @@ const staticRegistryPlugin = (config = {}) => {
788
903
  allowJs: true,
789
904
  skipLibCheck: true,
790
905
  noEmit: true
791
- } }), projectRoot, componentPath, registryPath, verbose$1);
792
- if (verbose$1) console.log(`📦 Found ${components.length} components with @Component decorators`);
906
+ } }), projectRoot, componentPath, registryPath);
907
+ logger.debug(`📦 Found ${components.length} components with @Component decorators`);
793
908
  const generatedCode = generateRegistryCode(components, registryIdentifier);
794
909
  const registryFilePath = resolve$1(projectRoot, registryPath);
795
- updateRegistryFile(registryFilePath, generatedCode, verbose$1);
796
- if (verbose$1) console.log("✅ Static registry generation complete!");
910
+ updateRegistryFile(registryFilePath, generatedCode);
911
+ logger.debug("✅ Static registry generation complete!");
797
912
  return registryFilePath;
798
913
  };
799
914
  return {
@@ -805,23 +920,23 @@ const staticRegistryPlugin = (config = {}) => {
805
920
  try {
806
921
  await runRegistryGeneration();
807
922
  } catch (error) {
808
- console.error(`❌ Static registry generation failed: ${error.message}`);
923
+ logger.error(`❌ Static registry generation failed: ${error.message}`);
809
924
  if (failOnError) throw error;
810
- console.warn("⚠️ Continuing build without static registry...");
925
+ logger.warn("⚠️ Continuing build without static registry...");
811
926
  }
812
927
  },
813
928
  async handleHotUpdate({ file, server }) {
814
929
  const normalizedComponentPath = componentPath.replace(/\\/g, "/");
815
930
  const normalizedFile = file.replace(/\\/g, "/");
816
931
  if (normalizedFile.includes(`/${normalizedComponentPath}/`) && (normalizedFile.endsWith(".ts") || normalizedFile.endsWith(".tsx"))) {
817
- if (verbose$1) console.log(`🔄 Component file changed: ${file}, regenerating registry...`);
932
+ logger.debug(`🔄 Component file changed: ${file}, regenerating registry...`);
818
933
  try {
819
934
  const registryFilePath = await runRegistryGeneration();
820
935
  const registryModule = server.moduleGraph.getModuleById(registryFilePath);
821
936
  if (registryModule) await server.reloadModule(registryModule);
822
- if (verbose$1) console.log("✅ Registry regenerated successfully!");
937
+ logger.debug("✅ Registry regenerated successfully!");
823
938
  } catch (error) {
824
- console.error(`❌ Failed to regenerate registry: ${error.message}`);
939
+ logger.error(`❌ Failed to regenerate registry: ${error.message}`);
825
940
  }
826
941
  return [];
827
942
  }
@@ -834,19 +949,19 @@ const staticRegistryPlugin = (config = {}) => {
834
949
  /**
835
950
  * Load the engagement config from config.server.ts
836
951
  */
837
- async function loadEngagementConfig(projectRoot, configPath, verbose$1) {
952
+ async function loadEngagementConfig(projectRoot, configPath) {
838
953
  const absoluteConfigPath = resolve$1(projectRoot, configPath);
839
954
  try {
840
955
  const config = (await import(pathToFileURL(absoluteConfigPath).href)).default;
841
- if (verbose$1) console.log(` 📄 Loaded config from ${configPath}`);
956
+ logger.debug(`📄 Loaded config from ${configPath}`);
842
957
  const engagement = config?.app?.engagement;
843
958
  if (!engagement) {
844
- if (verbose$1) console.log(` ⚠️ No engagement config found in ${configPath}`);
959
+ logger.debug(`⚠️ No engagement config found in ${configPath}`);
845
960
  return null;
846
961
  }
847
962
  return engagement;
848
963
  } catch (error) {
849
- if (verbose$1) console.warn(` ⚠️ Could not load config from ${configPath}: ${error.message}`);
964
+ logger.warn(`⚠️ Could not load config from ${configPath}: ${error.message}`);
850
965
  return null;
851
966
  }
852
967
  }
@@ -856,7 +971,7 @@ async function loadEngagementConfig(projectRoot, configPath, verbose$1) {
856
971
  /**
857
972
  * Extract all trackEvent calls from source files and return the event types found
858
973
  */
859
- async function scanForInstrumentedEvents(projectRoot, scanPaths, verbose$1) {
974
+ async function scanForInstrumentedEvents(projectRoot, scanPaths) {
860
975
  const instrumentedEvents = /* @__PURE__ */ new Set();
861
976
  const trackEventPattern = /trackEvent\s*\([^,]+,[^,]+,[^,]+,\s*['"]([^'"]+)['"]/g;
862
977
  const sendViewPagePattern = /sendViewPageEvent\s*\(/g;
@@ -869,29 +984,29 @@ async function scanForInstrumentedEvents(projectRoot, scanPaths, verbose$1) {
869
984
  "**/*.spec.tsx",
870
985
  "**/node_modules/**"
871
986
  ] });
872
- if (verbose$1) console.log(` 📂 Scanning ${files.length} files in ${scanPath}...`);
987
+ logger.debug(`📂 Scanning ${files.length} files in ${scanPath}...`);
873
988
  for (const file of files) try {
874
989
  const content = readFileSync(file, "utf-8");
875
990
  let match;
876
991
  while ((match = trackEventPattern.exec(content)) !== null) {
877
992
  const eventType = match[1];
878
993
  instrumentedEvents.add(eventType);
879
- if (verbose$1) console.log(` ✓ Found trackEvent('${eventType}') in ${file}`);
994
+ logger.debug(` ✓ Found trackEvent('${eventType}') in ${file}`);
880
995
  }
881
996
  if (sendViewPagePattern.test(content)) {
882
997
  instrumentedEvents.add("view_page");
883
- if (verbose$1) console.log(` ✓ Found sendViewPageEvent() in ${file}`);
998
+ logger.debug(` ✓ Found sendViewPageEvent() in ${file}`);
884
999
  }
885
1000
  while ((match = createEventPattern.exec(content)) !== null) {
886
1001
  const eventType = match[1];
887
1002
  instrumentedEvents.add(eventType);
888
- if (verbose$1) console.log(` ✓ Found createEvent('${eventType}') in ${file}`);
1003
+ logger.debug(` ✓ Found createEvent('${eventType}') in ${file}`);
889
1004
  }
890
1005
  trackEventPattern.lastIndex = 0;
891
1006
  sendViewPagePattern.lastIndex = 0;
892
1007
  createEventPattern.lastIndex = 0;
893
1008
  } catch (error) {
894
- if (verbose$1) console.warn(` ⚠️ Could not read ${file}: ${error.message}`);
1009
+ logger.warn(`⚠️ Could not read ${file}: ${error.message}`);
895
1010
  }
896
1011
  }
897
1012
  return instrumentedEvents;
@@ -935,7 +1050,7 @@ function extractEnabledEvents(engagement) {
935
1050
  * })
936
1051
  */
937
1052
  const eventInstrumentationValidatorPlugin = (config = {}) => {
938
- const { configPath = "config.server.ts", scanPaths = ["src"], failOnMissing = false, verbose: verbose$1 = false } = config;
1053
+ const { configPath = "config.server.ts", scanPaths = ["src"], failOnMissing = false } = config;
939
1054
  let resolvedConfig;
940
1055
  return {
941
1056
  name: "storefrontnext:event-instrumentation-validator",
@@ -945,34 +1060,29 @@ const eventInstrumentationValidatorPlugin = (config = {}) => {
945
1060
  },
946
1061
  async buildStart() {
947
1062
  const projectRoot = resolvedConfig.root;
948
- if (verbose$1) console.log("\n🔍 [event-instrumentation] Validating event instrumentation...");
949
- const engagement = await loadEngagementConfig(projectRoot, configPath, verbose$1);
1063
+ logger.debug("🔍 Validating event instrumentation...");
1064
+ const engagement = await loadEngagementConfig(projectRoot, configPath);
950
1065
  if (!engagement) {
951
- if (verbose$1) console.log(" ℹ️ Skipping validation - no engagement config found\n");
1066
+ logger.debug("ℹ️ Skipping validation - no engagement config found");
952
1067
  return;
953
1068
  }
954
1069
  const adapterEvents = extractEnabledEvents(engagement);
955
1070
  if (adapterEvents.size === 0) {
956
- if (verbose$1) console.log(" ℹ️ No enabled adapters with event toggles found\n");
1071
+ logger.debug("ℹ️ No enabled adapters with event toggles found");
957
1072
  return;
958
1073
  }
959
- const instrumentedEvents = await scanForInstrumentedEvents(projectRoot, scanPaths, verbose$1);
960
- if (verbose$1) {
961
- console.log(`\n 🔎 Found ${instrumentedEvents.size} instrumented event types:`);
962
- for (const event of instrumentedEvents) console.log(` - ${event}`);
963
- }
1074
+ const instrumentedEvents = await scanForInstrumentedEvents(projectRoot, scanPaths);
1075
+ logger.debug(`🔎 Found ${instrumentedEvents.size} instrumented event types: ${[...instrumentedEvents].join(", ")}`);
964
1076
  const missingInstrumentation = [];
965
1077
  for (const [adapterName, enabledEvents] of adapterEvents) for (const eventType of enabledEvents) if (!instrumentedEvents.has(eventType)) missingInstrumentation.push({
966
1078
  adapter: adapterName,
967
1079
  event: eventType
968
1080
  });
969
1081
  if (missingInstrumentation.length === 0) {
970
- if (verbose$1) console.log("\n ✅ All enabled events are instrumented\n");
1082
+ logger.debug("✅ All enabled events are instrumented");
971
1083
  return;
972
1084
  }
973
- console.log("\n");
974
- for (const { adapter, event } of missingInstrumentation) console.warn(` ⚠️ [event-instrumentation] ${adapter}.${event} is enabled but '${event}' is never instrumented`);
975
- console.log("\n");
1085
+ for (const { adapter, event } of missingInstrumentation) logger.warn(`⚠️ ${adapter}.${event} is enabled but '${event}' is never instrumented`);
976
1086
  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.`);
977
1087
  }
978
1088
  };
@@ -1252,6 +1362,242 @@ const workspacePlugin = () => {
1252
1362
  };
1253
1363
  };
1254
1364
 
1365
+ //#endregion
1366
+ //#region src/plugins/componentLoaders.ts
1367
+ const traverse = traverseModule.default || traverseModule;
1368
+ const generate$1 = generate.default || generate;
1369
+ /**
1370
+ * Names of exports to strip per environment.
1371
+ *
1372
+ * - `loader` is server-only → strip from the **client** build
1373
+ * - `clientLoader` is client-only → strip from the **server** build
1374
+ */
1375
+ const STRIP_FROM_CLIENT = ["loader"];
1376
+ const STRIP_FROM_SERVER = ["clientLoader"];
1377
+ /**
1378
+ * Determines which export names should be stripped for a given Vite environment.
1379
+ */
1380
+ function getExportsToStrip(environmentName) {
1381
+ if (environmentName === "client") return STRIP_FROM_CLIENT;
1382
+ if (environmentName === "ssr") return STRIP_FROM_SERVER;
1383
+ return [];
1384
+ }
1385
+ /**
1386
+ * Returns `true` when the source code contains at least one of the given export names as a quick pre-check before
1387
+ * running the full AST transform.
1388
+ */
1389
+ function hasExportCandidate(code, names) {
1390
+ return names.some((name) => code.includes(name));
1391
+ }
1392
+ /**
1393
+ * Checks whether the AST contains at least one class declaration decorated with `@Component(…)`.
1394
+ */
1395
+ function hasComponentDecorator(ast) {
1396
+ let found = false;
1397
+ traverse(ast, { ClassDeclaration(path$2) {
1398
+ const decorators = path$2.node.decorators;
1399
+ if (!decorators) return;
1400
+ for (const decorator of decorators) if (decorator.expression.type === "CallExpression" && isIdentifier(decorator.expression.callee) && decorator.expression.callee.name === "Component") {
1401
+ found = true;
1402
+ path$2.stop();
1403
+ return;
1404
+ }
1405
+ } });
1406
+ return found;
1407
+ }
1408
+ /**
1409
+ * Strips the specified named exports from the given source code using a
1410
+ * Babel AST transform.
1411
+ *
1412
+ * The transform handles the following patterns:
1413
+ *
1414
+ * 1. `export const loader = …;`
1415
+ * 2. `export function loader(…) {…}`
1416
+ * 3. `export class Loader {…}`
1417
+ * 4. `export { loader }` / `export { foo as loader }`
1418
+ * 5. `export { loader } from './loaders'`
1419
+ *
1420
+ * Destructured exports (`export const { loader } = …` or
1421
+ * `export const [loader] = …`) cannot be safely removed and will
1422
+ * throw an error if encountered (matching React Router behaviour).
1423
+ *
1424
+ * After removing an export, the transform also:
1425
+ * - Removes top-level property assignments to the stripped export
1426
+ * (e.g. `clientLoader.hydrate = true`)
1427
+ * - Removes any import declarations that become unused as a result
1428
+ *
1429
+ * @see {@link https://github.com/remix-run/react-router/blob/main/packages/react-router-dev/vite/remove-exports.ts React Router remove-exports}
1430
+ * @returns The transformed source code, or `null` if nothing was changed.
1431
+ */
1432
+ function stripExports(code, exportsToStrip, preParsedAst) {
1433
+ const ast = preParsedAst ?? parse(code, {
1434
+ sourceType: "module",
1435
+ plugins: [
1436
+ "typescript",
1437
+ "jsx",
1438
+ "decorators"
1439
+ ]
1440
+ });
1441
+ let changed = false;
1442
+ const previouslyReferencedIdentifiers = findReferencedIdentifiers(ast);
1443
+ const removedExportLocalNames = /* @__PURE__ */ new Set();
1444
+ traverse(ast, { ExportNamedDeclaration(path$2) {
1445
+ const { declaration, specifiers } = path$2.node;
1446
+ if (declaration && isVariableDeclaration(declaration)) {
1447
+ const remaining = declaration.declarations.filter((decl) => {
1448
+ if (isIdentifier(decl.id) && exportsToStrip.includes(decl.id.name)) {
1449
+ removedExportLocalNames.add(decl.id.name);
1450
+ return false;
1451
+ }
1452
+ if (isArrayPattern(decl.id) || isObjectPattern(decl.id)) validateDestructuredExports(decl.id, exportsToStrip);
1453
+ return true;
1454
+ });
1455
+ if (remaining.length < declaration.declarations.length) {
1456
+ changed = true;
1457
+ if (remaining.length === 0) {
1458
+ removeLeadingEslintDisableComment(path$2);
1459
+ path$2.remove();
1460
+ } else declaration.declarations = remaining;
1461
+ }
1462
+ return;
1463
+ }
1464
+ if (declaration && isFunctionDeclaration(declaration)) {
1465
+ if (declaration.id && exportsToStrip.includes(declaration.id.name)) {
1466
+ changed = true;
1467
+ removedExportLocalNames.add(declaration.id.name);
1468
+ removeLeadingEslintDisableComment(path$2);
1469
+ path$2.remove();
1470
+ return;
1471
+ }
1472
+ }
1473
+ if (declaration && isClassDeclaration(declaration)) {
1474
+ if (declaration.id && exportsToStrip.includes(declaration.id.name)) {
1475
+ changed = true;
1476
+ removedExportLocalNames.add(declaration.id.name);
1477
+ removeLeadingEslintDisableComment(path$2);
1478
+ path$2.remove();
1479
+ return;
1480
+ }
1481
+ }
1482
+ if (specifiers.length > 0) {
1483
+ const remaining = specifiers.filter((spec) => {
1484
+ if (isExportSpecifier(spec)) {
1485
+ const exportedName = isIdentifier(spec.exported) ? spec.exported.name : spec.exported.value;
1486
+ if (exportsToStrip.includes(exportedName)) {
1487
+ removedExportLocalNames.add(spec.local.name);
1488
+ return false;
1489
+ }
1490
+ }
1491
+ return true;
1492
+ });
1493
+ if (remaining.length < specifiers.length) {
1494
+ changed = true;
1495
+ if (remaining.length === 0) {
1496
+ removeLeadingEslintDisableComment(path$2);
1497
+ path$2.remove();
1498
+ } else path$2.node.specifiers = remaining;
1499
+ }
1500
+ }
1501
+ } });
1502
+ if (changed) traverse(ast, { ExpressionStatement(path$2) {
1503
+ if (!path$2.parentPath?.isProgram()) return;
1504
+ if (path$2.node.expression.type === "AssignmentExpression") {
1505
+ const left = path$2.node.expression.left;
1506
+ if (isMemberExpression(left) && isIdentifier(left.object) && (exportsToStrip.includes(left.object.name) || removedExportLocalNames.has(left.object.name))) {
1507
+ removeLeadingEslintDisableComment(path$2);
1508
+ path$2.remove();
1509
+ }
1510
+ }
1511
+ } });
1512
+ if (changed) deadCodeElimination(ast, previouslyReferencedIdentifiers);
1513
+ if (!changed) return null;
1514
+ return generate$1(ast, { retainLines: true }, code).code;
1515
+ }
1516
+ /**
1517
+ * Validates that no destructured export patterns contain names that should
1518
+ * be stripped. Destructured exports cannot be safely removed, so we throw
1519
+ * an error instead (matching React Router behaviour).
1520
+ */
1521
+ function validateDestructuredExports(id, exportsToStrip) {
1522
+ if (isArrayPattern(id)) for (const element of id.elements) {
1523
+ if (!element) continue;
1524
+ if (isIdentifier(element) && exportsToStrip.includes(element.name)) throw new Error(`Cannot remove destructured export "${element.name}"`);
1525
+ if (isRestElement(element) && isIdentifier(element.argument) && exportsToStrip.includes(element.argument.name)) throw new Error(`Cannot remove destructured export "${element.argument.name}"`);
1526
+ if (isArrayPattern(element) || isObjectPattern(element)) validateDestructuredExports(element, exportsToStrip);
1527
+ }
1528
+ if (isObjectPattern(id)) for (const property of id.properties) {
1529
+ if (!property) continue;
1530
+ if (isObjectProperty(property) && isIdentifier(property.key)) {
1531
+ if (isIdentifier(property.value) && exportsToStrip.includes(property.value.name)) throw new Error(`Cannot remove destructured export "${property.value.name}"`);
1532
+ if (isArrayPattern(property.value) || isObjectPattern(property.value)) validateDestructuredExports(property.value, exportsToStrip);
1533
+ }
1534
+ if (isRestElement(property) && isIdentifier(property.argument) && exportsToStrip.includes(property.argument.name)) throw new Error(`Cannot remove destructured export "${property.argument.name}"`);
1535
+ }
1536
+ }
1537
+ /**
1538
+ * Removes a leading `// eslint-disable-next-line …` comment that sits on
1539
+ * the line immediately before the given path.
1540
+ */
1541
+ function removeLeadingEslintDisableComment(path$2) {
1542
+ const leadingComments = path$2.node.leadingComments;
1543
+ if (!leadingComments || leadingComments.length === 0) return;
1544
+ const last = leadingComments[leadingComments.length - 1];
1545
+ if (last.type === "CommentLine" && last.value.includes("eslint-disable")) leadingComments.pop();
1546
+ }
1547
+ /**
1548
+ * Vite plugin that strips environment-specific loader exports from
1549
+ * component modules.
1550
+ *
1551
+ * Following the React Router convention:
1552
+ * - `export const loader` → server-only, stripped from the **client** bundle
1553
+ * - `export const clientLoader` → client-only, stripped from the **server** bundle
1554
+ *
1555
+ * This ensures that server-only code (e.g. API calls, database access) is
1556
+ * never included in the client bundle, and vice versa.
1557
+ *
1558
+ * The plugin only processes files that:
1559
+ * 1. Are under the configured `componentPath` directory
1560
+ * 2. Contain a `@Component` decorator (i.e. are Page Designer components)
1561
+ * 3. Are not test or story files
1562
+ */
1563
+ function componentLoadersPlugin(config = {}) {
1564
+ const { componentPath = "src/components" } = config;
1565
+ let isTestMode = false;
1566
+ return {
1567
+ name: "storefrontnext:component-loaders",
1568
+ enforce: "pre",
1569
+ configResolved(resolvedConfig) {
1570
+ isTestMode = resolvedConfig.mode === "test";
1571
+ },
1572
+ transform(code, id) {
1573
+ if (isTestMode) return null;
1574
+ if (!id.includes(componentPath)) return null;
1575
+ if (!/\.[mc]?[jt]sx?$/.test(id)) return null;
1576
+ if (/\.(test|spec|stories)\.[jt]sx?$/.test(id)) return null;
1577
+ const environmentName = this.environment?.name;
1578
+ if (!environmentName) return null;
1579
+ const exportsToStrip = getExportsToStrip(environmentName);
1580
+ if (exportsToStrip.length === 0) return null;
1581
+ if (!hasExportCandidate(code, exportsToStrip)) return null;
1582
+ const ast = parse(code, {
1583
+ sourceType: "module",
1584
+ plugins: [
1585
+ "typescript",
1586
+ "jsx",
1587
+ "decorators"
1588
+ ]
1589
+ });
1590
+ if (!hasComponentDecorator(ast)) return null;
1591
+ const transformed = stripExports(code, exportsToStrip, ast);
1592
+ if (!transformed) return null;
1593
+ return {
1594
+ code: transformed,
1595
+ map: null
1596
+ };
1597
+ }
1598
+ };
1599
+ }
1600
+
1255
1601
  //#endregion
1256
1602
  //#region src/storefront-next-targets.ts
1257
1603
  /**
@@ -1276,13 +1622,11 @@ const workspacePlugin = () => {
1276
1622
  function storefrontNextTargets(config = {}) {
1277
1623
  const { readableChunkNames = false, staticRegistry = {
1278
1624
  componentPath: "",
1279
- registryPath: "",
1280
- verbose: false
1625
+ registryPath: ""
1281
1626
  }, eventInstrumentationValidator = {
1282
1627
  configPath: "config.server.ts",
1283
1628
  scanPaths: ["src"],
1284
- failOnMissing: false,
1285
- verbose: false
1629
+ failOnMissing: false
1286
1630
  } } = config;
1287
1631
  const plugins = [
1288
1632
  ...process.env.SCAPI_PROXY_HOST ? [workspacePlugin()] : [],
@@ -1294,12 +1638,383 @@ function storefrontNextTargets(config = {}) {
1294
1638
  watchConfigFilesPlugin(),
1295
1639
  buildMiddlewareRegistryPlugin()
1296
1640
  ];
1297
- if (staticRegistry?.componentPath && staticRegistry?.registryPath) plugins.push(staticRegistryPlugin(staticRegistry));
1641
+ if (staticRegistry?.componentPath && staticRegistry?.registryPath) {
1642
+ plugins.push(staticRegistryPlugin(staticRegistry));
1643
+ plugins.push(componentLoadersPlugin({ componentPath: staticRegistry.componentPath }));
1644
+ }
1298
1645
  if (eventInstrumentationValidator !== false) plugins.push(eventInstrumentationValidatorPlugin(eventInstrumentationValidator));
1299
1646
  if (readableChunkNames) plugins.push(readableChunkFileNamesPlugin());
1300
1647
  return plugins;
1301
1648
  }
1302
1649
 
1650
+ //#endregion
1651
+ //#region src/plugins/hybridProxy.ts
1652
+ /**
1653
+ * Check if a request path should skip proxying (Vite internals, assets, etc.)
1654
+ *
1655
+ * @param pathname - URL pathname to check
1656
+ * @returns true if the request should NOT be proxied
1657
+ */
1658
+ function shouldSkipProxy(pathname) {
1659
+ if (pathname.startsWith("/@")) return true;
1660
+ if (pathname.startsWith("/__")) return true;
1661
+ if (pathname.startsWith("/src/")) return true;
1662
+ if (pathname.startsWith("/node_modules/")) return true;
1663
+ if (pathname.endsWith(".data")) return true;
1664
+ if (pathname.startsWith("/mobify/")) return true;
1665
+ if (pathname.startsWith("/on/demandware.")) return false;
1666
+ if (/\.(js|jsx|ts|tsx|css|json|map|woff2?|ttf|svg|png|jpe?g|gif|webp|ico|mp4)$/i.test(pathname)) return true;
1667
+ return false;
1668
+ }
1669
+ /**
1670
+ * Rewrite Set-Cookie header for localhost development.
1671
+ *
1672
+ * Rewrites SFCC Set-Cookie headers so they work on localhost during local development.
1673
+ *
1674
+ * **LOCAL DEVELOPMENT ONLY** — This function is part of the hybrid proxy Vite plugin
1675
+ * which only runs during `pnpm dev`. In production (MRT deployments), SFCC cookies
1676
+ * flow through the eCDN unmodified.
1677
+ *
1678
+ * Rewrites applied:
1679
+ * - **Domain**: `.salesforce.com` → `localhost` (browsers reject cross-domain cookies)
1680
+ *
1681
+ * Attributes intentionally preserved:
1682
+ * - **Secure**: Kept. Localhost is a secure context — browsers accept `Secure` cookies
1683
+ * on `http://localhost` (see https://w3c.github.io/webappsec-secure-contexts/).
1684
+ * - **SameSite**: Kept. `SameSite=None; Secure` is valid on localhost since `Secure`
1685
+ * is accepted. This keeps SFCC cookies transparent and in sync with Storefront Next
1686
+ * cookies, which is critical for hybrid auth session bridging.
1687
+ *
1688
+ * @param cookie - Original Set-Cookie header value from SFCC
1689
+ * @returns Rewritten cookie suitable for localhost
1690
+ *
1691
+ * @example
1692
+ * Input: "dwsid=abc123; Domain=.salesforce.com; Path=/; Secure; SameSite=None; HttpOnly"
1693
+ * Output: "dwsid=abc123; Domain=localhost; Path=/; Secure; SameSite=None; HttpOnly"
1694
+ */
1695
+ function rewriteCookieForLocalhost(cookie) {
1696
+ let rewritten = cookie;
1697
+ rewritten = rewritten.replace(/Domain=[^;]+/gi, "Domain=localhost");
1698
+ if (!/Domain=/i.test(cookie)) rewritten = rewritten.replace(/^([^;]+)/, "$1; Domain=localhost");
1699
+ return rewritten.trim();
1700
+ }
1701
+ /**
1702
+ * Inline script injected into proxied HTML responses to intercept `document.cookie` writes.
1703
+ *
1704
+ * **Why this is needed (Layer 3 cookie rewriting):**
1705
+ *
1706
+ * The hybrid proxy rewrites Set-Cookie headers from SFCC responses (Layer 1), but after
1707
+ * the SFRA page fully loads, client-side JavaScript sets cookies via `document.cookie`.
1708
+ * These writes bypass the proxy entirely.
1709
+ *
1710
+ * SFRA's JS typically checks `window.location.protocol` to decide whether to add `Secure`.
1711
+ * On `http://localhost`, it sees `http:` and omits `Secure`, producing cookies like:
1712
+ *
1713
+ * document.cookie = "dwsid=abc; SameSite=None" // No Secure → browser rejects
1714
+ *
1715
+ * This interceptor patches `document.cookie` to:
1716
+ * 1. Rewrite `Domain=...` → `Domain=localhost`
1717
+ * 2. Ensure `Secure` is present (localhost is a secure context)
1718
+ * 3. If `SameSite=None` is present without `Secure`, add `Secure`
1719
+ *
1720
+ * This keeps client-side cookie writes consistent with the proxy's Layer 1 rewrites
1721
+ * and ensures hybrid auth cookies (dwsid, cc-*) stay in sync between Storefront Next
1722
+ * and SFRA.
1723
+ *
1724
+ * **LOCAL DEVELOPMENT ONLY** — This script is only injected by the Vite dev server proxy.
1725
+ */
1726
+ const COOKIE_INTERCEPTOR_SCRIPT = `<script data-hybrid-proxy="cookie-interceptor">
1727
+ (function() {
1728
+ var desc = Object.getOwnPropertyDescriptor(Document.prototype, 'cookie');
1729
+ if (!desc || !desc.set) return;
1730
+ Object.defineProperty(document, 'cookie', {
1731
+ get: function() { return desc.get.call(this); },
1732
+ set: function(val) {
1733
+ // Rewrite Domain to localhost
1734
+ val = val.replace(/Domain=[^;]+/gi, 'Domain=localhost');
1735
+ // Ensure Secure is present if SameSite=None (localhost is a secure context)
1736
+ if (/SameSite=None/i.test(val) && !/;\\s*Secure\\b/i.test(val)) {
1737
+ val += '; Secure';
1738
+ }
1739
+ desc.set.call(this, val);
1740
+ },
1741
+ configurable: true
1742
+ });
1743
+ })();
1744
+ <\/script>`;
1745
+ /**
1746
+ * Escape special regex characters in a string for use in `new RegExp()`.
1747
+ */
1748
+ function escapeRegExp(str) {
1749
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1750
+ }
1751
+ /**
1752
+ * Vite plugin for hybrid proxying between Storefront Next and legacy SFRA.
1753
+ *
1754
+ * Uses http-proxy to silently forward non-matching requests to SFCC without visible
1755
+ * redirects. Rewrites Set-Cookie headers, Location headers, and HTML/JSON response
1756
+ * bodies to keep all navigation within the localhost proxy.
1757
+ *
1758
+ * Routing decisions are delegated to the `routeMatcher` callback injected via options,
1759
+ * keeping the SDK free of template-specific routing logic.
1760
+ *
1761
+ * @param options - Plugin configuration
1762
+ * @returns Vite plugin
1763
+ */
1764
+ function hybridProxyPlugin(options) {
1765
+ if (!options.enabled) {
1766
+ logger.debug("Hybrid proxy disabled (HYBRID_PROXY_ENABLED is not true)");
1767
+ return { name: "hybrid-proxy" };
1768
+ }
1769
+ if (!options.targetOrigin) {
1770
+ logger.warn("Hybrid proxy: no target origin configured (SFCC_ORIGIN required)");
1771
+ return { name: "hybrid-proxy" };
1772
+ }
1773
+ logger.info(`Hybrid proxy enabled → ${options.targetOrigin}`);
1774
+ logger.debug(`Hybrid proxy routing rules: ${options.routingRules.slice(0, 100)}...`);
1775
+ const locale = options.locale || "default";
1776
+ logger.debug(`Hybrid proxy path transformation: / → /s/${options.siteId}, /path → /s/${options.siteId}/${locale}/path`);
1777
+ const targetOriginPattern = new RegExp(escapeRegExp(options.targetOrigin), "g");
1778
+ const proxy = httpProxy.createProxyServer({
1779
+ changeOrigin: true,
1780
+ followRedirects: false,
1781
+ selfHandleResponse: true
1782
+ });
1783
+ proxy.on("proxyReq", (proxyReq, req) => {
1784
+ const url = new URL(req.url || "", `http://${req.headers.host}`);
1785
+ const pathname = url.pathname;
1786
+ if (!pathname.startsWith("/s/") && !pathname.startsWith("/on/demandware.")) {
1787
+ const originalPath = proxyReq.path;
1788
+ /**
1789
+ * "/" maps to the SFRA/SiteGenesis site root — no locale in the path
1790
+ * This would simply proxy to SFCC hostname (eg.: https://zzrf-001.dx.commercecloud.salesforce.com/s/{siteId}/{locale}/) which is not a valid storefront URL.
1791
+ * We need to rewrite the path to /s/{siteId} so that it can be proxied to the correct SFCC URL.
1792
+ */
1793
+ if (pathname === "/") proxyReq.path = `/s/${options.siteId}${url.search}`;
1794
+ else proxyReq.path = `/s/${options.siteId}/${locale}${pathname}${url.search}`;
1795
+ logger.debug(`Hybrid proxy path rewrite: ${originalPath} → ${proxyReq.path}`);
1796
+ }
1797
+ });
1798
+ proxy.on("proxyRes", (proxyRes, req, res) => {
1799
+ const clientRes = res;
1800
+ const locationHeader = proxyRes.headers.location;
1801
+ const statusCode = proxyRes.statusCode || 200;
1802
+ if (statusCode >= 300 && statusCode < 400 && typeof locationHeader === "string" && /\/404\b/.test(locationHeader)) {
1803
+ logger.warn(`⚠️ SFCC returned a redirect to 404 for ${req.url}. This usually means your HYBRID_ROUTING_RULES are missing a pattern for this path. Stripping Set-Cookie headers to prevent session cookie corruption. Fix: add a matching pattern to HYBRID_ROUTING_RULES (e.g., "^${req.url?.split("?")[0]}.*")`);
1804
+ delete proxyRes.headers["set-cookie"];
1805
+ }
1806
+ const setCookieHeaders = proxyRes.headers["set-cookie"];
1807
+ if (setCookieHeaders && Array.isArray(setCookieHeaders)) proxyRes.headers["set-cookie"] = setCookieHeaders.map((cookie) => {
1808
+ const rewritten = rewriteCookieForLocalhost(cookie);
1809
+ logger.debug(`Hybrid proxy cookie rewrite: ${cookie.slice(0, 50)}... → ${rewritten.slice(0, 50)}...`);
1810
+ return rewritten;
1811
+ });
1812
+ if (locationHeader && typeof locationHeader === "string") try {
1813
+ const locationUrl = new URL(locationHeader, options.targetOrigin);
1814
+ if (locationUrl.origin === options.targetOrigin) {
1815
+ const localUrl = `http://${req.headers.host}${locationUrl.pathname}${locationUrl.search}${locationUrl.hash}`;
1816
+ proxyRes.headers.location = localUrl;
1817
+ logger.debug(`Hybrid proxy location rewrite: ${locationHeader} → ${localUrl}`);
1818
+ }
1819
+ } catch {
1820
+ logger.warn(`Hybrid proxy: invalid Location header: ${locationHeader}`);
1821
+ }
1822
+ const contentType = (proxyRes.headers["content-type"] || "").split(";")[0].trim();
1823
+ if (!(contentType === "text/html" || contentType === "application/json")) {
1824
+ clientRes.writeHead(proxyRes.statusCode || 200, proxyRes.headers);
1825
+ proxyRes.pipe(clientRes);
1826
+ return;
1827
+ }
1828
+ const chunks = [];
1829
+ proxyRes.on("data", (chunk) => chunks.push(chunk));
1830
+ proxyRes.on("end", () => {
1831
+ let body = Buffer.concat(chunks);
1832
+ const encoding = proxyRes.headers["content-encoding"];
1833
+ if (encoding === "gzip") body = gunzipSync(body);
1834
+ else if (encoding === "br") body = brotliDecompressSync(body);
1835
+ else if (encoding === "deflate") body = inflateSync(body);
1836
+ const proxyOrigin = `http://${req.headers.host}`;
1837
+ let text = body.toString("utf8");
1838
+ targetOriginPattern.lastIndex = 0;
1839
+ text = text.replace(targetOriginPattern, proxyOrigin);
1840
+ if (contentType === "text/html") {
1841
+ const headIndex = text.indexOf("<head");
1842
+ if (headIndex !== -1) {
1843
+ const insertAfter = text.indexOf(">", headIndex);
1844
+ if (insertAfter !== -1) text = text.slice(0, insertAfter + 1) + COOKIE_INTERCEPTOR_SCRIPT + text.slice(insertAfter + 1);
1845
+ }
1846
+ }
1847
+ const headers = { ...proxyRes.headers };
1848
+ delete headers["content-encoding"];
1849
+ delete headers["transfer-encoding"];
1850
+ headers["content-length"] = String(Buffer.byteLength(text, "utf8"));
1851
+ clientRes.writeHead(proxyRes.statusCode || 200, headers);
1852
+ clientRes.end(text);
1853
+ logger.debug(`Hybrid proxy rewrote ${contentType} body URLs for ${req.url}`);
1854
+ });
1855
+ });
1856
+ proxy.on("error", (err, req, res) => {
1857
+ logger.error(`Hybrid proxy error: ${err.message} ${req.url}`);
1858
+ if ("writeHead" in res && !res.headersSent) {
1859
+ res.writeHead(502, { "Content-Type": "text/plain" });
1860
+ res.end("Bad Gateway: Failed to proxy to SFCC");
1861
+ }
1862
+ });
1863
+ return {
1864
+ name: "hybrid-proxy",
1865
+ enforce: "pre",
1866
+ configureServer(server) {
1867
+ server.middlewares.use((req, res, next) => {
1868
+ const pathname = req.url?.split("?")[0] || "";
1869
+ if (shouldSkipProxy(pathname)) return next();
1870
+ const isSFCCPath = pathname.startsWith("/on/demandware.");
1871
+ let shouldRouteToNextApp = false;
1872
+ if (!isSFCCPath) {
1873
+ try {
1874
+ shouldRouteToNextApp = options.routeMatcher(pathname, options.routingRules);
1875
+ } catch (error) {
1876
+ logger.error(`Hybrid proxy error checking routing rules: ${String(error)}`);
1877
+ return next();
1878
+ }
1879
+ if (shouldRouteToNextApp) return next();
1880
+ }
1881
+ logger.debug(`Hybrid proxy: ${req.method} ${pathname} → ${options.targetOrigin}`);
1882
+ try {
1883
+ proxy.web(req, res, { target: options.targetOrigin });
1884
+ } catch (error) {
1885
+ logger.error(`Hybrid proxy failed to proxy request: ${String(error)}`);
1886
+ if (!res.headersSent) {
1887
+ res.writeHead(502, { "Content-Type": "text/plain" });
1888
+ res.end("Bad Gateway: Failed to proxy to SFCC");
1889
+ }
1890
+ }
1891
+ });
1892
+ }
1893
+ };
1894
+ }
1895
+
1896
+ //#endregion
1897
+ //#region src/plugins/ecdnMatcher.ts
1898
+ /**
1899
+ * Cloudflare eCDN Routing Rule Matcher
1900
+ *
1901
+ * Parses Cloudflare-style routing expressions and tests pathnames against them.
1902
+ * This utility is environment-agnostic and works in both Node.js and browser contexts.
1903
+ *
1904
+ * @example
1905
+ * ```typescript
1906
+ * const rules = '(http.request.uri.path matches "^/$" or http.request.uri.path matches "^/search.*")';
1907
+ * shouldRouteToNext('/', rules); // true - route to Storefront Next
1908
+ * shouldRouteToNext('/search', rules); // true - route to Storefront Next
1909
+ * shouldRouteToNext('/checkout', rules); // false - proxy to SFRA/legacy
1910
+ * ```
1911
+ *
1912
+ * Environment variables used:
1913
+ * - HYBRID_PROXY_ENABLED (optional) - Boolean flag to enable/disable hybrid proxy
1914
+ * - HYBRID_ROUTING_RULES (optional) - Cloudflare routing expression string
1915
+ * - SFCC_ORIGIN (optional) - Base URL for SFCC sandbox redirects
1916
+ */
1917
+ const regexCache = /* @__PURE__ */ new Map();
1918
+ /**
1919
+ * Extracts regex patterns from a Cloudflare routing expression.
1920
+ *
1921
+ * Parses Cloudflare "matches" expressions like:
1922
+ * (http.request.uri.path matches "^/$" or http.request.uri.path matches "^/category.*")
1923
+ *
1924
+ * And extracts the regex patterns: ["^/$", "^/category.*"]
1925
+ *
1926
+ * @param expression - Cloudflare expression string
1927
+ * @returns Array of regex pattern strings
1928
+ *
1929
+ * @example
1930
+ * ```typescript
1931
+ * extractPatterns('(http.request.uri.path matches "^/$")');
1932
+ * // Returns: ["^/$"]
1933
+ *
1934
+ * extractPatterns('(http.request.uri.path matches "^/$" or http.request.uri.path matches "^/search.*")');
1935
+ * // Returns: ["^/$", "^/search.*"]
1936
+ * ```
1937
+ */
1938
+ function extractPatterns(expression) {
1939
+ if (!expression || typeof expression !== "string") return [];
1940
+ const regex = /http\.request\.uri\.path\s+matches\s+["']([^"']+)["']/gi;
1941
+ const patterns = [];
1942
+ let match;
1943
+ while ((match = regex.exec(expression)) !== null) patterns.push(match[1]);
1944
+ return patterns;
1945
+ }
1946
+ /**
1947
+ * Tests if a pathname matches any of the provided regex patterns (logical OR).
1948
+ * Uses caching to optimize repeated pattern compilations.
1949
+ *
1950
+ * @param pathname - URL pathname to test (e.g., "/search", "/category/shoes")
1951
+ * @param patterns - Array of regex pattern strings
1952
+ * @returns true if pathname matches any pattern, false otherwise
1953
+ *
1954
+ * @example
1955
+ * ```typescript
1956
+ * testPatterns('/category/shoes', ['^/category.*', '^/search.*']);
1957
+ * // Returns: true (matches first pattern)
1958
+ *
1959
+ * testPatterns('/checkout', ['^/category.*', '^/search.*']);
1960
+ * // Returns: false (matches no patterns)
1961
+ * ```
1962
+ */
1963
+ function testPatterns(pathname, patterns) {
1964
+ if (!pathname || !patterns || patterns.length === 0) return false;
1965
+ for (const pattern of patterns) try {
1966
+ let regex = regexCache.get(pattern);
1967
+ if (!regex) {
1968
+ regex = new RegExp(pattern);
1969
+ regexCache.set(pattern, regex);
1970
+ }
1971
+ if (regex.test(pathname)) return true;
1972
+ } catch (error) {
1973
+ logger.warn(`Invalid regex pattern: ${pattern} ${String(error)}`);
1974
+ continue;
1975
+ }
1976
+ return false;
1977
+ }
1978
+ /**
1979
+ * Main function: Determines if a pathname should route to Storefront Next
1980
+ * or be proxied/redirected to SFRA/legacy backend.
1981
+ *
1982
+ * @param pathname - URL pathname (e.g., "/search", "/checkout")
1983
+ * @param routingRules - Cloudflare routing expression string (optional)
1984
+ * @returns true if should route to Storefront Next, false if should proxy to SFRA
1985
+ *
1986
+ * @example
1987
+ * ```typescript
1988
+ * const rules = '(http.request.uri.path matches "^/$" or http.request.uri.path matches "^/category.*")';
1989
+ *
1990
+ * shouldRouteToNext('/', rules); // true - route to Next
1991
+ * shouldRouteToNext('/category/mens', rules); // true - route to Next
1992
+ * shouldRouteToNext('/checkout', rules); // false - proxy to SFRA
1993
+ * shouldRouteToNext('/any-path', undefined); // true - no rules = default to Next
1994
+ * ```
1995
+ */
1996
+ function shouldRouteToNext(pathname, routingRules) {
1997
+ if (!routingRules) return true;
1998
+ const patterns = extractPatterns(routingRules);
1999
+ if (patterns.length === 0) {
2000
+ logger.warn("No valid patterns found in routing rules");
2001
+ return true;
2002
+ }
2003
+ return testPatterns(pathname, patterns);
2004
+ }
2005
+ /**
2006
+ * Clears the regex cache. Useful for testing or when routing rules change.
2007
+ *
2008
+ * @example
2009
+ * ```typescript
2010
+ * clearCache();
2011
+ * // All cached regex patterns are removed
2012
+ * ```
2013
+ */
2014
+ function clearCache() {
2015
+ regexCache.clear();
2016
+ }
2017
+
1303
2018
  //#endregion
1304
2019
  //#region src/server/ts-import.ts
1305
2020
  /**
@@ -1367,18 +2082,15 @@ function loadConfigFromEnv() {
1367
2082
  const shortCode = process.env.PUBLIC__app__commerce__api__shortCode;
1368
2083
  const organizationId = process.env.PUBLIC__app__commerce__api__organizationId;
1369
2084
  const clientId = process.env.PUBLIC__app__commerce__api__clientId;
1370
- const siteId = process.env.PUBLIC__app__commerce__api__siteId;
1371
2085
  const proxy = process.env.PUBLIC__app__commerce__api__proxy || "/mobify/proxy/api";
1372
2086
  const proxyHost = process.env.SCAPI_PROXY_HOST;
1373
2087
  if (!shortCode && !proxyHost) throw new Error("Missing PUBLIC__app__commerce__api__shortCode environment variable.\nPlease set it in your .env file or environment.");
1374
2088
  if (!organizationId) throw new Error("Missing PUBLIC__app__commerce__api__organizationId environment variable.\nPlease set it in your .env file or environment.");
1375
2089
  if (!clientId) throw new Error("Missing PUBLIC__app__commerce__api__clientId environment variable.\nPlease set it in your .env file or environment.");
1376
- if (!siteId) throw new Error("Missing PUBLIC__app__commerce__api__siteId environment variable.\nPlease set it in your .env file or environment.");
1377
2090
  return { commerce: { api: {
1378
2091
  shortCode: shortCode || "",
1379
2092
  organizationId,
1380
2093
  clientId,
1381
- siteId,
1382
2094
  proxy,
1383
2095
  proxyHost
1384
2096
  } } };
@@ -1404,17 +2116,20 @@ async function loadProjectConfig(projectDirectory) {
1404
2116
  if (!api.shortCode && !proxyHost) throw new Error("Missing shortCode in config.server.ts commerce.api configuration");
1405
2117
  if (!api.organizationId) throw new Error("Missing organizationId in config.server.ts commerce.api configuration");
1406
2118
  if (!api.clientId) throw new Error("Missing clientId in config.server.ts commerce.api configuration");
1407
- if (!api.siteId) throw new Error("Missing siteId in config.server.ts commerce.api configuration");
1408
2119
  return { commerce: { api: {
1409
2120
  shortCode: api.shortCode || "",
1410
2121
  organizationId: api.organizationId,
1411
2122
  clientId: api.clientId,
1412
- siteId: api.siteId,
1413
2123
  proxy: api.proxy || "/mobify/proxy/api",
1414
2124
  proxyHost
1415
2125
  } } };
1416
2126
  }
1417
2127
 
2128
+ //#endregion
2129
+ //#region src/config.ts
2130
+ const SFNEXT_BASE_CARTRIDGE_NAME = "app_storefrontnext_base";
2131
+ const SFNEXT_BASE_CARTRIDGE_OUTPUT_DIR = `${SFNEXT_BASE_CARTRIDGE_NAME}/cartridge/experience`;
2132
+
1418
2133
  //#endregion
1419
2134
  //#region src/server/middleware/proxy.ts
1420
2135
  /**
@@ -1429,25 +2144,6 @@ function createCommerceProxyMiddleware(config) {
1429
2144
  });
1430
2145
  }
1431
2146
 
1432
- //#endregion
1433
- //#region src/utils/logger.ts
1434
- /**
1435
- * Logger utilities
1436
- */
1437
- const colors = {
1438
- warn: "yellow",
1439
- error: "red",
1440
- success: "cyan",
1441
- info: "green",
1442
- debug: "gray"
1443
- };
1444
- const fancyLog = (level, msg) => {
1445
- const colorFn = chalk[colors[level]];
1446
- console.log(`${colorFn(level)}: ${msg}`);
1447
- };
1448
- const info = (msg) => fancyLog("info", msg);
1449
- const warn = (msg) => fancyLog("warn", msg);
1450
-
1451
2147
  //#endregion
1452
2148
  //#region src/server/middleware/static.ts
1453
2149
  /**
@@ -1457,7 +2153,7 @@ const warn = (msg) => fancyLog("warn", msg);
1457
2153
  function createStaticMiddleware(bundleId, projectDirectory) {
1458
2154
  const bundlePath = getBundlePath(bundleId);
1459
2155
  const clientBuildDir = path$1.join(projectDirectory, "build", "client");
1460
- info(`Serving static assets from ${clientBuildDir} at ${bundlePath}`);
2156
+ logger.info(`Serving static assets from ${clientBuildDir} at ${bundlePath}`);
1461
2157
  return express.static(clientBuildDir, { setHeaders: (res) => {
1462
2158
  res.setHeader("Cache-Control", "public, max-age=31536000, immutable");
1463
2159
  res.setHeader("x-local-static-cache-control", "1");
@@ -1476,7 +2172,7 @@ function getCompressionLevel() {
1476
2172
  if (raw == null || raw.trim() === "") return DEFAULT;
1477
2173
  const level = Number(raw);
1478
2174
  if (!(Number.isInteger(level) && level >= 0 && level <= 9)) {
1479
- warn(`[compression] Invalid COMPRESSION_LEVEL="${raw}". Using default (${DEFAULT}).`);
2175
+ logger.warn(`[compression] Invalid COMPRESSION_LEVEL="${raw}". Using default (${DEFAULT}).`);
1480
2176
  return DEFAULT;
1481
2177
  }
1482
2178
  return level;
@@ -1529,20 +2225,18 @@ function createLoggingMiddleware() {
1529
2225
  });
1530
2226
  morgan.token("method-colored", (req) => {
1531
2227
  const method = req.method;
1532
- const colors$1 = {
2228
+ const colors = {
1533
2229
  GET: chalk.green,
1534
2230
  POST: chalk.blue,
1535
2231
  PUT: chalk.yellow,
1536
2232
  DELETE: chalk.red,
1537
2233
  PATCH: chalk.magenta
1538
2234
  };
1539
- return (method && colors$1[method] || chalk.white)(method);
2235
+ return (method && colors[method] || chalk.white)(method);
1540
2236
  });
1541
2237
  return morgan((tokens, req, res) => {
1542
2238
  return [
1543
- chalk.gray("["),
1544
2239
  tokens["method-colored"](req, res),
1545
- chalk.gray("]"),
1546
2240
  tokens.url(req, res),
1547
2241
  "-",
1548
2242
  tokens["status-colored"](req, res),
@@ -1594,11 +2288,13 @@ function createHostHeaderMiddleware() {
1594
2288
  */
1595
2289
  function patchReactRouterBuild(build, bundleId) {
1596
2290
  const bundlePath = getBundlePath(bundleId);
2291
+ const basePath = getBasePath();
1597
2292
  const patchedAssetsJson = JSON.stringify(build.assets).replace(/"\/assets\//g, `"${bundlePath}assets/`);
1598
2293
  const newAssets = JSON.parse(patchedAssetsJson);
1599
2294
  return Object.assign({}, build, {
1600
2295
  publicPath: bundlePath,
1601
- assets: newAssets
2296
+ assets: newAssets,
2297
+ ...basePath && { basename: basePath }
1602
2298
  });
1603
2299
  }
1604
2300
 
@@ -1631,6 +2327,225 @@ const ServerModeFeatureMap = {
1631
2327
  }
1632
2328
  };
1633
2329
 
2330
+ //#endregion
2331
+ //#region src/otel/mrt-console-span-exporter.ts
2332
+ var MrtConsoleSpanExporter = class extends ConsoleSpanExporter {
2333
+ export(spans, resultCallback) {
2334
+ for (const span of spans) try {
2335
+ const ctx = span.spanContext();
2336
+ const spanData = {
2337
+ traceId: ctx.traceId,
2338
+ parentId: span.parentSpanId,
2339
+ name: span.name,
2340
+ id: ctx.spanId,
2341
+ kind: span.kind,
2342
+ timestamp: hrTimeToTimeStamp(span.startTime),
2343
+ duration: span.duration,
2344
+ attributes: span.attributes,
2345
+ status: span.status,
2346
+ events: span.events,
2347
+ links: span.links,
2348
+ start_time: span.startTime,
2349
+ end_time: span.endTime,
2350
+ forwardTrace: process.env.SFNEXT_OTEL_ENABLED === "true"
2351
+ };
2352
+ console.info(JSON.stringify(spanData));
2353
+ } catch {}
2354
+ resultCallback({ code: ExportResultCode.SUCCESS });
2355
+ }
2356
+ };
2357
+
2358
+ //#endregion
2359
+ //#region src/otel/setup.ts
2360
+ const SERVICE_NAME = "storefront-next";
2361
+ /**
2362
+ * Initializes OpenTelemetry and returns a Tracer from the provider directly.
2363
+ *
2364
+ * Returns the tracer via `provider.getTracer()` instead of the global
2365
+ * `trace.getTracer()` API. In the Vite SSR module runner, the built
2366
+ * dist/entry/server.js and the externalized @opentelemetry/sdk-trace-node
2367
+ * resolve @opentelemetry/api to different module instances (different paths
2368
+ * through pnpm's strict node_modules). Each instance has its own
2369
+ * ProxyTracerProvider singleton, so `provider.register()` sets the delegate
2370
+ * on sdk-trace-node's API instance while our code's `trace.getTracer()`
2371
+ * reads from a separate API instance with no delegate — returning a tracer
2372
+ * backed by a bare BasicTracerProvider with NoopSpanProcessor.
2373
+ *
2374
+ * Getting the tracer directly from the provider bypasses the global registry
2375
+ * entirely, guaranteeing the tracer uses our configured span processors.
2376
+ */
2377
+ let cachedTracer = null;
2378
+ const UNDICI_REGISTERED_KEY = Symbol.for("sfnext.otel.undici_registered");
2379
+ function initTelemetry() {
2380
+ if (cachedTracer) return cachedTracer;
2381
+ try {
2382
+ const provider = new NodeTracerProvider({ resource: new Resource({ [ATTR_SERVICE_NAME]: SERVICE_NAME }) });
2383
+ provider.addSpanProcessor(new SimpleSpanProcessor(new MrtConsoleSpanExporter()));
2384
+ provider.register();
2385
+ if (!globalThis[UNDICI_REGISTERED_KEY]) {
2386
+ globalThis[UNDICI_REGISTERED_KEY] = true;
2387
+ registerInstrumentations({
2388
+ tracerProvider: provider,
2389
+ instrumentations: [new UndiciInstrumentation({ requestHook(span, request) {
2390
+ try {
2391
+ const method = request.method.toUpperCase();
2392
+ const url = `${request.origin}${request.path}`;
2393
+ span.updateName(`${method} ${url}`);
2394
+ } catch {}
2395
+ } })]
2396
+ });
2397
+ }
2398
+ cachedTracer = provider.getTracer(SERVICE_NAME);
2399
+ return cachedTracer;
2400
+ } catch (error) {
2401
+ logger.error("[otel] Failed to initialize OpenTelemetry:", error);
2402
+ return null;
2403
+ }
2404
+ }
2405
+
2406
+ //#endregion
2407
+ //#region src/otel/express/middleware.ts
2408
+ function createOtelExpressMiddleware() {
2409
+ const maybeTracer = initTelemetry();
2410
+ if (!maybeTracer) return (_req, _res, next) => next();
2411
+ const tracer = maybeTracer;
2412
+ return (req, res, next) => {
2413
+ try {
2414
+ const url = new URL(req.originalUrl || req.url, "http://localhost").pathname;
2415
+ const method = req.method;
2416
+ tracer.startActiveSpan(`[sfnext] server ${method} ${url}`, { attributes: {
2417
+ "http.request.method": method,
2418
+ "url.path": url
2419
+ } }, (serverSpan) => {
2420
+ try {
2421
+ const spanContext = trace.getSpan(context.active())?.spanContext();
2422
+ if (spanContext) {
2423
+ const flags = spanContext.traceFlags.toString(16).padStart(2, "0");
2424
+ const traceparent = `00-${spanContext.traceId}-${spanContext.spanId}-${flags}`;
2425
+ res.setHeader("traceparent", traceparent);
2426
+ }
2427
+ } catch {}
2428
+ const serverCtx = context.active();
2429
+ const startTime = performance.now();
2430
+ let streamingSpan = null;
2431
+ let ttfbMs = 0;
2432
+ let ended = false;
2433
+ function recordTTFB() {
2434
+ if (streamingSpan) return;
2435
+ try {
2436
+ ttfbMs = Math.round(performance.now() - startTime);
2437
+ serverSpan.setAttribute("sfnext.ttfb_ms", ttfbMs);
2438
+ streamingSpan = tracer.startSpan(`[sfnext] response streaming ${method} ${url}`, { attributes: {
2439
+ "http.request.method": method,
2440
+ "url.path": url,
2441
+ "sfnext.ttfb_ms": ttfbMs
2442
+ } }, serverCtx);
2443
+ } catch {}
2444
+ }
2445
+ const origWriteHead = res.writeHead.bind(res);
2446
+ res.writeHead = ((...args) => {
2447
+ recordTTFB();
2448
+ return origWriteHead(...args);
2449
+ });
2450
+ const origWrite = res.write.bind(res);
2451
+ res.write = ((...args) => {
2452
+ recordTTFB();
2453
+ return origWrite(...args);
2454
+ });
2455
+ function endSpans() {
2456
+ if (ended) return;
2457
+ ended = true;
2458
+ try {
2459
+ const totalMs = Math.round(performance.now() - startTime);
2460
+ const statusCode = res.statusCode;
2461
+ if (streamingSpan) {
2462
+ streamingSpan.setAttribute("http.streaming_duration_ms", totalMs - ttfbMs);
2463
+ streamingSpan.setAttribute("http.response.status_code", statusCode);
2464
+ if (statusCode >= 500) streamingSpan.setStatus({ code: SpanStatusCode.ERROR });
2465
+ streamingSpan.end();
2466
+ }
2467
+ serverSpan.setAttribute("http.response.status_code", statusCode);
2468
+ serverSpan.setAttribute("http.total_duration_ms", totalMs);
2469
+ if (statusCode >= 500) serverSpan.setStatus({ code: SpanStatusCode.ERROR });
2470
+ serverSpan.end();
2471
+ } catch {}
2472
+ }
2473
+ res.once("close", endSpans);
2474
+ res.once("finish", endSpans);
2475
+ next();
2476
+ });
2477
+ } catch {
2478
+ next();
2479
+ }
2480
+ };
2481
+ }
2482
+
2483
+ //#endregion
2484
+ //#region src/server/handlers/health-check.ts
2485
+ const DEFAULT_HEALTH_DESCRIPTION = "storefront-next-dev server health";
2486
+ const PACKAGE_JSON_NAME = "package.json";
2487
+ const RUNTIME_PACKAGE_NAME = "@salesforce/storefront-next-runtime";
2488
+ const DEV_PACKAGE_NAME = "@salesforce/storefront-next-dev";
2489
+ const BUILD_FOLDER_NAME = "build";
2490
+ const LOCAL_BUNDLE_ID = "local";
2491
+ const HEALTH_ENDPOINT_PATH = "/sfdc-health";
2492
+ /**
2493
+ * Reads a package.json file and returns selected metadata.
2494
+ *
2495
+ * @param path - Absolute path to a package.json file
2496
+ * @returns Parsed metadata, or null if missing/unreadable
2497
+ *
2498
+ * @example
2499
+ * ```ts
2500
+ * const metadata = readPackageMetadata('/app/package.json');
2501
+ * console.log(metadata?.version);
2502
+ * ```
2503
+ */
2504
+ function readPackageMetadata(path$2) {
2505
+ if (!existsSync$1(path$2)) return null;
2506
+ try {
2507
+ return JSON.parse(readFileSync$1(path$2, "utf8"));
2508
+ } catch (error) {
2509
+ logger.debug(`Health check: failed to parse package.json at ${path$2}`, error);
2510
+ return null;
2511
+ }
2512
+ }
2513
+ /**
2514
+ * Creates an Express handler that returns Health+JSON for the project.
2515
+ *
2516
+ * @param options - Handler options
2517
+ * @returns Express request handler for the health endpoint
2518
+ *
2519
+ * @example
2520
+ * ```ts
2521
+ * app.get(HEALTH_ENDPOINT_PATH, createHealthCheckHandler({
2522
+ * projectDirectory: process.cwd(),
2523
+ * bundleId: LOCAL_BUNDLE_ID,
2524
+ * }));
2525
+ * ```
2526
+ */
2527
+ function createHealthCheckHandler(options) {
2528
+ const { projectDirectory, bundleId } = options;
2529
+ const projectPackage = readPackageMetadata(bundleId === LOCAL_BUNDLE_ID ? resolve(projectDirectory, PACKAGE_JSON_NAME) : resolve(projectDirectory, BUILD_FOLDER_NAME, PACKAGE_JSON_NAME));
2530
+ const allDependencies = {
2531
+ ...projectPackage?.dependencies,
2532
+ ...projectPackage?.devDependencies
2533
+ };
2534
+ const devVersion = allDependencies?.[DEV_PACKAGE_NAME];
2535
+ const runtimeVersion = allDependencies?.[RUNTIME_PACKAGE_NAME];
2536
+ const notes = [devVersion ? `Built using ${DEV_PACKAGE_NAME}@${devVersion}.` : null, runtimeVersion ? `Running ${RUNTIME_PACKAGE_NAME}@${runtimeVersion}.` : null].filter(Boolean);
2537
+ return (_req, res) => {
2538
+ const healthResponse = {
2539
+ status: "pass",
2540
+ version: projectPackage?.version,
2541
+ bundleId,
2542
+ description: projectPackage?.description ?? DEFAULT_HEALTH_DESCRIPTION,
2543
+ notes: notes.length > 0 ? notes : void 0
2544
+ };
2545
+ res.status(200).type("application/health+json").json(healthResponse);
2546
+ };
2547
+ }
2548
+
1634
2549
  //#endregion
1635
2550
  //#region src/server/index.ts
1636
2551
  /** Relative path to the middleware registry TypeScript source (development). Must match appDirectory + server dir + filename used by buildMiddlewareRegistry plugin. */
@@ -1643,6 +2558,7 @@ const MIDDLEWARE_REGISTRY_BUILT_EXTENSIONS = [
1643
2558
  ];
1644
2559
  /** All paths to try when loading the built middlewares (base + extension). */
1645
2560
  const RELATIVE_MIDDLEWARE_REGISTRY_BUILT_PATHS = ["bld/server/middleware-registry", "build/server/middleware-registry"].flatMap((base) => MIDDLEWARE_REGISTRY_BUILT_EXTENSIONS.map((ext) => `${base}${ext}`));
2561
+ const DEFAULT_BUNDLE_ID = "local";
1646
2562
  /**
1647
2563
  * Create a unified Express server for development, preview, or production mode
1648
2564
  */
@@ -1651,9 +2567,14 @@ async function createServer(options) {
1651
2567
  if (mode === "development" && !vite) throw new Error("Vite dev server instance is required for development mode");
1652
2568
  if ((mode === "preview" || mode === "production") && !build) throw new Error("React Router server build is required for preview/production mode");
1653
2569
  const config = providedConfig ?? loadConfigFromEnv();
1654
- const bundleId = process.env.BUNDLE_ID ?? "local";
2570
+ const bundleId = process.env.BUNDLE_ID ?? DEFAULT_BUNDLE_ID;
1655
2571
  const app = express();
1656
2572
  app.disable("x-powered-by");
2573
+ if (process.env.SFNEXT_OTEL_ENABLED === "true") app.use(createOtelExpressMiddleware());
2574
+ app.get(HEALTH_ENDPOINT_PATH, createHealthCheckHandler({
2575
+ projectDirectory,
2576
+ bundleId
2577
+ }));
1657
2578
  if (enableLogging) app.use(createLoggingMiddleware());
1658
2579
  if (enableCompression && !streaming) app.use(createCompressionMiddleware());
1659
2580
  if (enableStaticServing && build) {
@@ -1678,8 +2599,14 @@ async function createServer(options) {
1678
2599
  });
1679
2600
  if (mode === "development" && vite) app.use(vite.middlewares);
1680
2601
  if (enableProxy) app.use(config.commerce.api.proxy, createCommerceProxyMiddleware(config));
2602
+ const basePath = getBasePath();
2603
+ if (basePath) app.use((req, res, next) => {
2604
+ if (req.path.startsWith(`${basePath}/`) || req.path === basePath) return next();
2605
+ if (req.path.startsWith("/mobify/")) return next();
2606
+ res.redirect(`${basePath}${req.originalUrl}`);
2607
+ });
1681
2608
  app.use(createHostHeaderMiddleware());
1682
- app.all("*", await createSSRHandler(mode, bundleId, vite, build, enableAssetUrlPatching));
2609
+ app.all("*splat", await createSSRHandler(mode, bundleId, vite, build, enableAssetUrlPatching));
1683
2610
  return app;
1684
2611
  }
1685
2612
  /**
@@ -1707,9 +2634,10 @@ async function createSSRHandler(mode, bundleId, vite, build, enableAssetUrlPatch
1707
2634
  } else if (build) {
1708
2635
  let patchedBuild = build;
1709
2636
  if (enableAssetUrlPatching) patchedBuild = patchReactRouterBuild(build, bundleId);
2637
+ const requestHandlerMode = process.env.NODE_OPTIONS?.includes("--enable-source-maps") ? "development" : process.env.NODE_ENV;
1710
2638
  return createRequestHandler({
1711
2639
  build: patchedBuild,
1712
- mode: process.env.NODE_ENV
2640
+ mode: requestHandlerMode
1713
2641
  });
1714
2642
  } else throw new Error("Invalid server configuration: no vite or build provided");
1715
2643
  }
@@ -1731,17 +2659,15 @@ const SINGLE_LINE_MARKER = "@sfdc-extension-line";
1731
2659
  const BLOCK_MARKER_START = "@sfdc-extension-block-start";
1732
2660
  const BLOCK_MARKER_END = "@sfdc-extension-block-end";
1733
2661
  const FILE_MARKER = "@sfdc-extension-file";
1734
- let verbose = false;
1735
- function trimExtensions(directory, selectedExtensions, extensionConfig, verboseOverride = false) {
2662
+ function trimExtensions(directory, selectedExtensions, extensionConfig) {
1736
2663
  const startTime = Date.now();
1737
- verbose = verboseOverride ?? false;
1738
2664
  const configuredExtensions = extensionConfig?.extensions || {};
1739
2665
  const extensions = {};
1740
2666
  Object.keys(configuredExtensions).forEach((targetKey) => {
1741
2667
  extensions[targetKey] = Boolean(selectedExtensions?.[targetKey]) || false;
1742
2668
  });
1743
2669
  if (Object.keys(extensions).length === 0) {
1744
- if (verbose) console.log("No targets found, skipping trim");
2670
+ logger.debug("No targets found, skipping trim");
1745
2671
  return;
1746
2672
  }
1747
2673
  const processDirectory = (dir) => {
@@ -1760,7 +2686,7 @@ function trimExtensions(directory, selectedExtensions, extensionConfig, verboseO
1760
2686
  updateExtensionConfig(directory, extensions);
1761
2687
  }
1762
2688
  const endTime = Date.now();
1763
- if (verbose) console.log(`Trim extensions took ${endTime - startTime}ms`);
2689
+ logger.debug(`Trim extensions took ${endTime - startTime}ms`);
1764
2690
  }
1765
2691
  /**
1766
2692
  * Update the extension config file to only include the selected extensions.
@@ -1785,15 +2711,14 @@ function processFile(filePath, extensions) {
1785
2711
  if (source.includes(FILE_MARKER)) {
1786
2712
  const markerLine = source.split("\n").find((line) => line.includes(FILE_MARKER));
1787
2713
  const extMatch = Object.keys(extensions).find((ext) => markerLine.includes(ext));
1788
- if (!extMatch) {
1789
- if (verbose) console.warn(`File ${filePath} is marked with ${markerLine} but it does not match any known extensions`);
1790
- } else if (extensions[extMatch] === false) {
2714
+ if (!extMatch) logger.warn(`File ${filePath} is marked with ${markerLine} but it does not match any known extensions`);
2715
+ else if (extensions[extMatch] === false) {
1791
2716
  try {
1792
2717
  fs$1.unlinkSync(filePath);
1793
- if (verbose) console.log(`Deleted file ${filePath}`);
2718
+ logger.debug(`Deleted file ${filePath}`);
1794
2719
  } catch (e) {
1795
2720
  const error = e;
1796
- console.error(`Error deleting file ${filePath}: ${error.message}`);
2721
+ logger.error(`Error deleting file ${filePath}: ${error.message}`);
1797
2722
  throw e;
1798
2723
  }
1799
2724
  return;
@@ -1822,7 +2747,7 @@ function processFile(filePath, extensions) {
1822
2747
  line: i
1823
2748
  });
1824
2749
  skippingBlock = extensions[matchingExtension] === false;
1825
- } else if (verbose) console.warn(`Warning: Unknown marker found in ${filePath} at line ${i}: \n${line}`);
2750
+ } else logger.warn(`Unknown marker found in ${filePath} at line ${i}: \n${line}`);
1826
2751
  } else if (line.includes(BLOCK_MARKER_END)) {
1827
2752
  if (Object.keys(extensions).find((extension) => line.includes(extension))) {
1828
2753
  const extension = Object.keys(extensions).find((p) => line.includes(p));
@@ -1843,10 +2768,10 @@ function processFile(filePath, extensions) {
1843
2768
  const newSource = newLines.join("\n");
1844
2769
  if (newSource !== source) try {
1845
2770
  fs$1.writeFileSync(filePath, newSource);
1846
- if (verbose) console.log(`Updated file ${filePath}`);
2771
+ logger.debug(`Updated file ${filePath}`);
1847
2772
  } catch (e) {
1848
2773
  const error = e;
1849
- console.error(`Error updating file ${filePath}: ${error.message}`);
2774
+ logger.error(`Error updating file ${filePath}: ${error.message}`);
1850
2775
  throw e;
1851
2776
  }
1852
2777
  }
@@ -1870,11 +2795,11 @@ function deleteExtensionFolders(projectRoot, extensions, extensionConfig) {
1870
2795
  recursive: true,
1871
2796
  force: true
1872
2797
  });
1873
- if (verbose) console.log(`Deleted extension folder: ${extensionFolderPath}`);
2798
+ logger.debug(`Deleted extension folder: ${extensionFolderPath}`);
1874
2799
  } catch (err) {
1875
2800
  const error = err;
1876
- if (error.code === "EPERM") console.error(`Permission denied - cannot delete ${extensionFolderPath}. You may need to run with sudo or check permissions.`);
1877
- else console.error(`Error deleting ${extensionFolderPath}: ${error.message}`);
2801
+ if (error.code === "EPERM") logger.error(`Permission denied - cannot delete ${extensionFolderPath}. You may need to run with sudo or check permissions.`);
2802
+ else logger.error(`Error deleting ${extensionFolderPath}: ${error.message}`);
1878
2803
  }
1879
2804
  }
1880
2805
  });
@@ -1950,7 +2875,7 @@ function filePathToRoute(filePath, projectRoot) {
1950
2875
  const routeFileNormalized = routeFilePosix.replace(/^\.\//, "");
1951
2876
  if (filePathPosix.endsWith(routeFileNormalized) || filePathPosix.endsWith(`/${routeFileNormalized}`)) return route.path;
1952
2877
  }
1953
- console.warn(`Warning: Could not find route for file: ${filePath}`);
2878
+ logger.warn(`Could not find route for file: ${filePath}`);
1954
2879
  return "/unknown";
1955
2880
  }
1956
2881
  /**
@@ -2025,7 +2950,7 @@ const TYPE_MAPPING = {
2025
2950
  function resolveAttributeType(decoratorType, tsMorphType, fieldName) {
2026
2951
  if (decoratorType) {
2027
2952
  if (!VALID_ATTRIBUTE_TYPES.includes(decoratorType)) {
2028
- console.error(`Error: Invalid attribute type '${decoratorType}' for field '${fieldName || "unknown"}'. Valid types are: ${VALID_ATTRIBUTE_TYPES.join(", ")}`);
2953
+ logger.error(`Invalid attribute type '${decoratorType}' for field '${fieldName || "unknown"}'. Valid types are: ${VALID_ATTRIBUTE_TYPES.join(", ")}`);
2029
2954
  process.exit(1);
2030
2955
  }
2031
2956
  return decoratorType;
@@ -2050,6 +2975,30 @@ function getTypeFromTsMorph(property, _sourceFile) {
2050
2975
  } catch {}
2051
2976
  return "string";
2052
2977
  }
2978
+ /**
2979
+ * Resolve a variable's initializer expression from the same source file,
2980
+ * unwrapping `as const` type assertions.
2981
+ */
2982
+ function resolveVariableInitializer(sourceFile, name) {
2983
+ const varDecl = sourceFile.getVariableDeclaration(name);
2984
+ if (!varDecl) return void 0;
2985
+ let initializer = varDecl.getInitializer();
2986
+ if (initializer && Node.isAsExpression(initializer)) initializer = initializer.getExpression();
2987
+ return initializer;
2988
+ }
2989
+ /**
2990
+ * Check whether an AST node is a type that `parseExpression` can resolve to a
2991
+ * concrete JS value (as opposed to falling through to `getText()`).
2992
+ */
2993
+ function isResolvableLiteral(node) {
2994
+ return Node.isStringLiteral(node) || Node.isNumericLiteral(node) || Node.isTrueLiteral(node) || Node.isFalseLiteral(node) || Node.isObjectLiteralExpression(node) || Node.isArrayLiteralExpression(node);
2995
+ }
2996
+ var UnresolvedConstantReferenceError = class extends Error {
2997
+ constructor(reference) {
2998
+ super(`Cannot resolve constant reference '${reference}'. Ensure the variable is declared in the same file as a literal value.`);
2999
+ this.name = "UnresolvedConstantReferenceError";
3000
+ }
3001
+ };
2053
3002
  function parseExpression(expression) {
2054
3003
  if (Node.isStringLiteral(expression)) return expression.getLiteralValue();
2055
3004
  else if (Node.isNumericLiteral(expression)) return expression.getLiteralValue();
@@ -2057,7 +3006,26 @@ function parseExpression(expression) {
2057
3006
  else if (Node.isFalseLiteral(expression)) return false;
2058
3007
  else if (Node.isObjectLiteralExpression(expression)) return parseNestedObject(expression);
2059
3008
  else if (Node.isArrayLiteralExpression(expression)) return parseArrayLiteral(expression);
2060
- else return expression.getText();
3009
+ else if (Node.isPropertyAccessExpression(expression)) {
3010
+ const obj = expression.getExpression();
3011
+ const propName = expression.getName();
3012
+ if (Node.isIdentifier(obj)) {
3013
+ const resolved = resolveVariableInitializer(expression.getSourceFile(), obj.getText());
3014
+ if (resolved && Node.isObjectLiteralExpression(resolved)) {
3015
+ const prop = resolved.getProperty(propName);
3016
+ if (prop && Node.isPropertyAssignment(prop)) {
3017
+ const propInit = prop.getInitializer();
3018
+ if (propInit) return parseExpression(propInit);
3019
+ }
3020
+ }
3021
+ throw new UnresolvedConstantReferenceError(expression.getText());
3022
+ }
3023
+ return expression.getText();
3024
+ } else if (Node.isIdentifier(expression)) {
3025
+ const resolved = resolveVariableInitializer(expression.getSourceFile(), expression.getText());
3026
+ if (resolved && isResolvableLiteral(resolved)) return parseExpression(resolved);
3027
+ return expression.getText();
3028
+ } else return expression.getText();
2061
3029
  }
2062
3030
  function parseNestedObject(objectLiteral) {
2063
3031
  const result = {};
@@ -2069,7 +3037,7 @@ function parseNestedObject(objectLiteral) {
2069
3037
  if (initializer) result[name] = parseExpression(initializer);
2070
3038
  }
2071
3039
  } catch (error) {
2072
- console.warn(`Warning: Could not parse nested object: ${error.message}`);
3040
+ logger.warn(`Could not parse nested object: ${error.message}`);
2073
3041
  return result;
2074
3042
  }
2075
3043
  return result;
@@ -2080,7 +3048,7 @@ function parseArrayLiteral(arrayLiteral) {
2080
3048
  const elements = arrayLiteral.getElements();
2081
3049
  for (const element of elements) result.push(parseExpression(element));
2082
3050
  } catch (error) {
2083
- console.warn(`Warning: Could not parse array literal: ${error.message}`);
3051
+ logger.warn(`Could not parse array literal: ${error.message}`);
2084
3052
  }
2085
3053
  return result;
2086
3054
  }
@@ -2113,7 +3081,8 @@ function parseDecoratorArgs(decorator) {
2113
3081
  }
2114
3082
  return result;
2115
3083
  } catch (error) {
2116
- console.warn(`Warning: Could not parse decorator arguments: ${error.message}`);
3084
+ if (error instanceof UnresolvedConstantReferenceError) throw error;
3085
+ logger.warn(`Could not parse decorator arguments: ${error.message}`);
2117
3086
  return result;
2118
3087
  }
2119
3088
  }
@@ -2142,11 +3111,15 @@ function extractAttributesFromSource(sourceFile, className) {
2142
3111
  attributes.push(attribute);
2143
3112
  }
2144
3113
  } catch (error) {
2145
- console.warn(`Warning: Could not extract attributes from class ${className}: ${error.message}`);
3114
+ if (error instanceof UnresolvedConstantReferenceError) throw error;
3115
+ logger.warn(`Could not extract attributes from class ${className}: ${error.message}`);
2146
3116
  }
2147
3117
  return attributes;
2148
3118
  }
2149
- function extractRegionDefinitionsFromSource(sourceFile, className) {
3119
+ function normalizeComponentTypeId(typeId, defaultGroup) {
3120
+ return typeId.includes(".") ? typeId : `${defaultGroup}.${typeId}`;
3121
+ }
3122
+ function extractRegionDefinitionsFromSource(sourceFile, className, defaultComponentGroup = DEFAULT_COMPONENT_GROUP) {
2150
3123
  const regionDefinitions = [];
2151
3124
  try {
2152
3125
  const classDeclaration = sourceFile.getClass(className);
@@ -2165,8 +3138,8 @@ function extractRegionDefinitionsFromSource(sourceFile, className) {
2165
3138
  name: regionConfig.name || "Region"
2166
3139
  };
2167
3140
  if (regionConfig.componentTypes) regionDefinition.component_types = regionConfig.componentTypes;
2168
- if (Array.isArray(regionConfig.componentTypeInclusions)) regionDefinition.component_type_inclusions = regionConfig.componentTypeInclusions.map((incl) => ({ type_id: incl }));
2169
- if (Array.isArray(regionConfig.componentTypeExclusions)) regionDefinition.component_type_exclusions = regionConfig.componentTypeExclusions.map((excl) => ({ type_id: excl }));
3141
+ if (Array.isArray(regionConfig.componentTypeInclusions)) regionDefinition.component_type_inclusions = regionConfig.componentTypeInclusions.map((incl) => ({ type_id: normalizeComponentTypeId(String(incl), defaultComponentGroup) }));
3142
+ if (Array.isArray(regionConfig.componentTypeExclusions)) regionDefinition.component_type_exclusions = regionConfig.componentTypeExclusions.map((excl) => ({ type_id: normalizeComponentTypeId(String(excl), defaultComponentGroup) }));
2170
3143
  if (regionConfig.maxComponents !== void 0) regionDefinition.max_components = regionConfig.maxComponents;
2171
3144
  if (regionConfig.minComponents !== void 0) regionDefinition.min_components = regionConfig.minComponents;
2172
3145
  if (regionConfig.allowMultiple !== void 0) regionDefinition.allow_multiple = regionConfig.allowMultiple;
@@ -2177,7 +3150,7 @@ function extractRegionDefinitionsFromSource(sourceFile, className) {
2177
3150
  }
2178
3151
  }
2179
3152
  } catch (error) {
2180
- console.warn(`Warning: Could not extract region definitions from class ${className}: ${error.message}`);
3153
+ logger.warn(`Warning: Could not extract region definitions from class ${className}: ${error.message}`);
2181
3154
  }
2182
3155
  return regionDefinitions;
2183
3156
  }
@@ -2198,12 +3171,13 @@ async function processComponentFile(filePath, _projectRoot) {
2198
3171
  const className = classDeclaration.getName();
2199
3172
  if (!className) continue;
2200
3173
  const componentConfig = parseDecoratorArgs(componentDecorator);
3174
+ const componentGroup = String(componentConfig.group || DEFAULT_COMPONENT_GROUP);
2201
3175
  const attributes = extractAttributesFromSource(sourceFile, className);
2202
- const regionDefinitions = extractRegionDefinitionsFromSource(sourceFile, className);
3176
+ const regionDefinitions = extractRegionDefinitionsFromSource(sourceFile, className, componentGroup);
2203
3177
  const componentMetadata = {
2204
3178
  typeId: componentConfig.id || className.toLowerCase(),
2205
3179
  name: componentConfig.name || toHumanReadableName(className),
2206
- group: componentConfig.group || DEFAULT_COMPONENT_GROUP,
3180
+ group: componentGroup,
2207
3181
  description: componentConfig.description || `Custom component: ${className}`,
2208
3182
  regionDefinitions,
2209
3183
  attributes
@@ -2211,11 +3185,13 @@ async function processComponentFile(filePath, _projectRoot) {
2211
3185
  components.push(componentMetadata);
2212
3186
  }
2213
3187
  } catch (error) {
2214
- console.warn(`Warning: Could not process file ${filePath}:`, error.message);
3188
+ if (error instanceof UnresolvedConstantReferenceError) throw error;
3189
+ logger.warn(`Could not process file ${filePath}:`, error.message);
2215
3190
  }
2216
3191
  return components;
2217
3192
  } catch (error) {
2218
- console.warn(`Warning: Could not read file ${filePath}:`, error.message);
3193
+ if (error instanceof UnresolvedConstantReferenceError) throw error;
3194
+ logger.warn(`Could not read file ${filePath}:`, error.message);
2219
3195
  return [];
2220
3196
  }
2221
3197
  }
@@ -2251,11 +3227,11 @@ async function processPageTypeFile(filePath, projectRoot) {
2251
3227
  pageTypes.push(pageTypeMetadata);
2252
3228
  }
2253
3229
  } catch (error) {
2254
- console.warn(`Warning: Could not process file ${filePath}:`, error.message);
3230
+ logger.warn(`Could not process file ${filePath}:`, error.message);
2255
3231
  }
2256
3232
  return pageTypes;
2257
3233
  } catch (error) {
2258
- console.warn(`Warning: Could not read file ${filePath}:`, error.message);
3234
+ logger.warn(`Could not read file ${filePath}:`, error.message);
2259
3235
  return [];
2260
3236
  }
2261
3237
  }
@@ -2278,11 +3254,11 @@ async function processAspectFile(filePath, _projectRoot) {
2278
3254
  };
2279
3255
  aspects.push(aspectMetadata);
2280
3256
  } catch (parseError) {
2281
- console.warn(`Warning: Could not parse JSON in file ${filePath}:`, parseError.message);
3257
+ logger.warn(`Could not parse JSON in file ${filePath}:`, parseError.message);
2282
3258
  }
2283
3259
  return aspects;
2284
3260
  } catch (error) {
2285
- console.warn(`Warning: Could not read file ${filePath}:`, error.message);
3261
+ logger.warn(`Could not read file ${filePath}:`, error.message);
2286
3262
  return [];
2287
3263
  }
2288
3264
  }
@@ -2311,7 +3287,7 @@ async function generateComponentCartridge(component, outputDir, dryRun = false)
2311
3287
  await writeFile(outputPath, JSON.stringify(cartridgeData, null, 2));
2312
3288
  }
2313
3289
  const prefix = dryRun ? " - [DRY RUN]" : " -";
2314
- console.log(`${prefix} ${String(component.typeId)}: ${String(component.name)} (${String(component.attributes.length)} attributes) → ${fileName}.json`);
3290
+ logger.debug(`${prefix} ${String(component.typeId)}: ${String(component.name)} (${String(component.attributes.length)} attributes) → ${fileName}.json`);
2315
3291
  }
2316
3292
  async function generatePageTypeCartridge(pageType, outputDir, dryRun = false) {
2317
3293
  const fileName = toCamelCaseFileName(pageType.name);
@@ -2334,7 +3310,7 @@ async function generatePageTypeCartridge(pageType, outputDir, dryRun = false) {
2334
3310
  await writeFile(outputPath, JSON.stringify(cartridgeData, null, 2));
2335
3311
  }
2336
3312
  const prefix = dryRun ? " - [DRY RUN]" : " -";
2337
- console.log(`${prefix} ${String(pageType.name)}: ${String(pageType.description)} (${String(pageType.attributes.length)} attributes) → ${fileName}.json`);
3313
+ logger.debug(`${prefix} ${String(pageType.name)}: ${String(pageType.description)} (${String(pageType.attributes.length)} attributes) → ${fileName}.json`);
2338
3314
  }
2339
3315
  async function generateAspectCartridge(aspect, outputDir, dryRun = false) {
2340
3316
  const fileName = toCamelCaseFileName(aspect.id);
@@ -2350,7 +3326,7 @@ async function generateAspectCartridge(aspect, outputDir, dryRun = false) {
2350
3326
  await writeFile(outputPath, JSON.stringify(cartridgeData, null, 2));
2351
3327
  }
2352
3328
  const prefix = dryRun ? " - [DRY RUN]" : " -";
2353
- console.log(`${prefix} ${String(aspect.name)}: ${String(aspect.description)} (${String(aspect.attributeDefinitions.length)} attributes) → ${fileName}.json`);
3329
+ logger.debug(`${prefix} ${String(aspect.name)}: ${String(aspect.description)} (${String(aspect.attributeDefinitions.length)} attributes) → ${fileName}.json`);
2354
3330
  }
2355
3331
  /**
2356
3332
  * Runs ESLint with --fix on the specified directory to format JSON files.
@@ -2358,20 +3334,20 @@ async function generateAspectCartridge(aspect, outputDir, dryRun = false) {
2358
3334
  */
2359
3335
  function lintGeneratedFiles(metadataDir, projectRoot) {
2360
3336
  try {
2361
- console.log("🔧 Running ESLint --fix on generated JSON files...");
3337
+ logger.debug("🔧 Running ESLint --fix on generated JSON files...");
2362
3338
  execSync(`npx eslint "${metadataDir}/**/*.json" --fix --no-error-on-unmatched-pattern`, {
2363
3339
  cwd: projectRoot,
2364
3340
  stdio: "pipe",
2365
3341
  encoding: "utf-8"
2366
3342
  });
2367
- console.log("✅ JSON files formatted successfully");
3343
+ logger.debug("✅ JSON files formatted successfully");
2368
3344
  } catch (error) {
2369
3345
  const execError = error;
2370
3346
  if (execError.status === 2) {
2371
3347
  const errMsg = execError.stderr || execError.stdout || "Unknown error";
2372
- console.warn(`⚠️ Warning: Could not run ESLint --fix: ${errMsg}`);
2373
- } else if (execError.stderr && execError.stderr.includes("error")) console.warn(`⚠️ Warning: Some linting issues could not be auto-fixed. Run ESLint manually to review.`);
2374
- else console.log("✅ JSON files formatted successfully");
3348
+ logger.warn(`⚠️ Could not run ESLint --fix: ${errMsg}`);
3349
+ } else if (execError.stderr && execError.stderr.includes("error")) logger.warn(`⚠️ Some linting issues could not be auto-fixed. Run ESLint manually to review.`);
3350
+ else logger.debug("✅ JSON files formatted successfully");
2375
3351
  }
2376
3352
  }
2377
3353
  async function generateMetadata(projectDirectory, metadataDirectory, options) {
@@ -2379,9 +3355,9 @@ async function generateMetadata(projectDirectory, metadataDirectory, options) {
2379
3355
  const filePaths = options?.filePaths;
2380
3356
  const isIncrementalMode = filePaths && filePaths.length > 0;
2381
3357
  const dryRun = options?.dryRun || false;
2382
- if (dryRun) console.log("🔍 [DRY RUN] Scanning for decorated components and page types...");
2383
- else if (isIncrementalMode) console.log(`🔍 Generating metadata for ${filePaths.length} specified file(s)...`);
2384
- else console.log("🔍 Generating metadata for decorated components and page types...");
3358
+ if (dryRun) logger.debug("🔍 [DRY RUN] Scanning for decorated components and page types...");
3359
+ else if (isIncrementalMode) logger.debug(`🔍 Generating metadata for ${filePaths.length} specified file(s)...`);
3360
+ else logger.debug("🔍 Generating metadata for decorated components and page types...");
2385
3361
  const projectRoot = resolve(projectDirectory);
2386
3362
  const srcDir = join(projectRoot, "src");
2387
3363
  const metadataDir = resolve(metadataDirectory);
@@ -2390,7 +3366,7 @@ async function generateMetadata(projectDirectory, metadataDirectory, options) {
2390
3366
  const aspectsOutputDir = join(metadataDir, "aspects");
2391
3367
  if (!dryRun) {
2392
3368
  if (!isIncrementalMode) {
2393
- console.log("🗑️ Cleaning existing output directories...");
3369
+ logger.debug("🗑️ Cleaning existing output directories...");
2394
3370
  for (const outputDir of [
2395
3371
  componentsOutputDir,
2396
3372
  pagesOutputDir,
@@ -2400,12 +3376,12 @@ async function generateMetadata(projectDirectory, metadataDirectory, options) {
2400
3376
  recursive: true,
2401
3377
  force: true
2402
3378
  });
2403
- console.log(` - Deleted: ${outputDir}`);
3379
+ logger.debug(` - Deleted: ${outputDir}`);
2404
3380
  } catch {
2405
- console.log(` - Directory not found (skipping): ${outputDir}`);
3381
+ logger.debug(` - Directory not found (skipping): ${outputDir}`);
2406
3382
  }
2407
- } else console.log("📝 Incremental mode: existing cartridge files will be preserved/overwritten");
2408
- console.log("📁 Creating output directories...");
3383
+ } else logger.debug("📝 Incremental mode: existing cartridge files will be preserved/overwritten");
3384
+ logger.debug("Creating output directories...");
2409
3385
  for (const outputDir of [
2410
3386
  componentsOutputDir,
2411
3387
  pagesOutputDir,
@@ -2416,16 +3392,18 @@ async function generateMetadata(projectDirectory, metadataDirectory, options) {
2416
3392
  try {
2417
3393
  await access(outputDir);
2418
3394
  } catch {
2419
- console.error(`❌ Error: Failed to create output directory ${outputDir}: ${error.message}`);
3395
+ const err = error;
3396
+ logger.error(`❌ Failed to create output directory ${outputDir}: ${err.message}`);
2420
3397
  process.exit(1);
3398
+ throw err;
2421
3399
  }
2422
3400
  }
2423
- } else if (isIncrementalMode) console.log(`📝 [DRY RUN] Would process ${filePaths.length} specific file(s)`);
2424
- else console.log("📝 [DRY RUN] Would clean and regenerate all metadata files");
3401
+ } else if (isIncrementalMode) logger.debug(`📝 [DRY RUN] Would process ${filePaths.length} specific file(s)`);
3402
+ else logger.debug("📝 [DRY RUN] Would clean and regenerate all metadata files");
2425
3403
  let files = [];
2426
3404
  if (isIncrementalMode && filePaths) {
2427
3405
  files = filePaths.map((fp) => resolve(projectRoot, fp));
2428
- console.log(`📂 Processing ${files.length} specified file(s)...`);
3406
+ logger.debug(`📂 Processing ${files.length} specified file(s)...`);
2429
3407
  } else {
2430
3408
  const scanDirectory = async (dir) => {
2431
3409
  const entries = await readdir(dir, { withFileTypes: true });
@@ -2450,7 +3428,7 @@ async function generateMetadata(projectDirectory, metadataDirectory, options) {
2450
3428
  allAspects.push(...aspects);
2451
3429
  }
2452
3430
  if (allComponents.length === 0 && allPageTypes.length === 0 && allAspects.length === 0) {
2453
- console.log("⚠️ No decorated components, page types, or aspect files found.");
3431
+ logger.info("⚠️ No decorated components, page types, or aspect files found.");
2454
3432
  return {
2455
3433
  componentsGenerated: 0,
2456
3434
  pageTypesGenerated: 0,
@@ -2459,22 +3437,22 @@ async function generateMetadata(projectDirectory, metadataDirectory, options) {
2459
3437
  };
2460
3438
  }
2461
3439
  if (allComponents.length > 0) {
2462
- console.log(`✅ Found ${allComponents.length} decorated component(s):`);
3440
+ logger.debug(`✅ Found ${allComponents.length} decorated component(s)`);
2463
3441
  for (const component of allComponents) await generateComponentCartridge(component, componentsOutputDir, dryRun);
2464
- if (dryRun) console.log(`📄 [DRY RUN] Would generate ${allComponents.length} component metadata file(s) in: ${componentsOutputDir}`);
2465
- else console.log(`📄 Generated ${allComponents.length} component metadata file(s) in: ${componentsOutputDir}`);
3442
+ if (dryRun) logger.info(`[DRY RUN] Would generate ${allComponents.length} component metadata file(s)`);
3443
+ else logger.info(`Generated ${allComponents.length} component metadata file(s)`);
2466
3444
  }
2467
3445
  if (allPageTypes.length > 0) {
2468
- console.log(`✅ Found ${allPageTypes.length} decorated page type(s):`);
3446
+ logger.debug(`✅ Found ${allPageTypes.length} decorated page type(s)`);
2469
3447
  for (const pageType of allPageTypes) await generatePageTypeCartridge(pageType, pagesOutputDir, dryRun);
2470
- if (dryRun) console.log(`📄 [DRY RUN] Would generate ${allPageTypes.length} page type metadata file(s) in: ${pagesOutputDir}`);
2471
- else console.log(`📄 Generated ${allPageTypes.length} page type metadata file(s) in: ${pagesOutputDir}`);
3448
+ if (dryRun) logger.info(`[DRY RUN] Would generate ${allPageTypes.length} page type metadata file(s)`);
3449
+ else logger.info(`Generated ${allPageTypes.length} page type metadata file(s)`);
2472
3450
  }
2473
3451
  if (allAspects.length > 0) {
2474
- console.log(`✅ Found ${allAspects.length} decorated aspect(s):`);
3452
+ logger.debug(`✅ Found ${allAspects.length} decorated aspect(s)`);
2475
3453
  for (const aspect of allAspects) await generateAspectCartridge(aspect, aspectsOutputDir, dryRun);
2476
- if (dryRun) console.log(`📄 [DRY RUN] Would generate ${allAspects.length} aspect metadata file(s) in: ${aspectsOutputDir}`);
2477
- else console.log(`📄 Generated ${allAspects.length} aspect metadata file(s) in: ${aspectsOutputDir}`);
3454
+ if (dryRun) logger.info(`[DRY RUN] Would generate ${allAspects.length} aspect metadata file(s)`);
3455
+ else logger.info(`Generated ${allAspects.length} aspect metadata file(s)`);
2478
3456
  }
2479
3457
  const shouldLintFix = options?.lintFix !== false;
2480
3458
  if (!dryRun && shouldLintFix && (allComponents.length > 0 || allPageTypes.length > 0 || allAspects.length > 0)) lintGeneratedFiles(metadataDir, projectRoot);
@@ -2485,11 +3463,13 @@ async function generateMetadata(projectDirectory, metadataDirectory, options) {
2485
3463
  totalFiles: allComponents.length + allPageTypes.length + allAspects.length
2486
3464
  };
2487
3465
  } catch (error) {
2488
- console.error("❌ Error:", error.message);
3466
+ const err = error;
3467
+ logger.error("❌ Error:", err.message);
2489
3468
  process.exit(1);
3469
+ throw err;
2490
3470
  }
2491
3471
  }
2492
3472
 
2493
3473
  //#endregion
2494
- export { createServer, storefrontNextTargets as default, generateMetadata, loadConfigFromEnv, loadProjectConfig, transformTargetPlaceholderPlugin, trimExtensions };
3474
+ export { clearCache, createServer, storefrontNextTargets as default, extractPatterns, generateMetadata, hybridProxyPlugin, loadConfigFromEnv, loadProjectConfig, shouldRouteToNext, testPatterns, transformTargetPlaceholderPlugin, trimExtensions };
2495
3475
  //# sourceMappingURL=index.js.map