@salesforce/storefront-next-dev 1.0.1 → 1.1.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,19 +1,21 @@
1
- import path from "node:path";
1
+ import path, { resolve } from "node:path";
2
2
  import fs from "fs-extra";
3
3
  import chalk from "chalk";
4
- import path$1, { dirname, join, relative, resolve } from "path";
5
- import { fileURLToPath, pathToFileURL } from "url";
4
+ import { createRequire } from "module";
5
+ import path$1, { dirname, join, relative, resolve as resolve$1 } from "path";
6
+ import { fileURLToPath } from "url";
6
7
  import { parse } from "@babel/parser";
7
- import { booleanLiteral, identifier, importDeclaration, importSpecifier, isArrayPattern, isClassDeclaration, isExportSpecifier, isFunctionDeclaration, isIdentifier, isJSXAttribute, isJSXElement, isJSXFragment, isJSXIdentifier, isMemberExpression, isObjectPattern, isObjectProperty, isRestElement, isStringLiteral, isVariableDeclaration, jsxAttribute, jsxClosingElement, jsxClosingFragment, jsxElement, jsxExpressionContainer, jsxFragment, jsxIdentifier, jsxOpeningElement, jsxOpeningFragment, jsxText, stringLiteral } from "@babel/types";
8
+ 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";
8
9
  import { generate } from "@babel/generator";
9
10
  import traverseModule from "@babel/traverse";
10
11
  import { existsSync, readFileSync, writeFileSync } from "fs";
11
12
  import { glob } from "glob";
12
13
  import { Node, Project, ts } from "ts-morph";
13
- import fs$1 from "node:fs";
14
+ import fs$1, { existsSync as existsSync$1, readFileSync as readFileSync$1 } from "node:fs";
14
15
  import { deadCodeElimination, findReferencedIdentifiers } from "babel-dead-code-elimination";
15
16
  import httpProxy from "http-proxy";
16
17
  import { brotliDecompressSync, gunzipSync, inflateSync } from "zlib";
18
+ import { extractPrefixParamValues, stripPathPrefix } from "@salesforce/storefront-next-runtime/site-context";
17
19
 
18
20
  //#region src/utils/logger.ts
19
21
  const LEVEL_PRIORITY = {
@@ -367,7 +369,7 @@ const patchReactRouterPlugin = () => {
367
369
 
368
370
  //#endregion
369
371
  //#region src/extensibility/target-utils.ts
370
- const traverse$2 = traverseModule.default || traverseModule;
372
+ const traverse$1 = traverseModule.default || traverseModule;
371
373
  const TARGET_COMPONENT_TAG = "UITarget";
372
374
  const TARGET_PROVIDERS_TAG = "UITargetProviders";
373
375
  const TARGET_ID_ATTRIBUTE = "targetId";
@@ -442,7 +444,7 @@ function runReplacementPass(ast, tagName, targetRegistry = null, contextProvider
442
444
  if (replacedId) targetIdsReplaced.add(replacedId);
443
445
  } else if (contextProviders) findAndReplaceProviders(pathToReplace, contextProviders);
444
446
  };
445
- traverse$2(ast, {
447
+ traverse$1(ast, {
446
448
  VariableDeclaration(nodePath) {
447
449
  const declarationPaths = nodePath.get("declarations");
448
450
  const declarationsArray = Array.isArray(declarationPaths) ? declarationPaths : [declarationPaths];
@@ -495,7 +497,7 @@ function transformTargets(code, targetRegistry, contextProviders) {
495
497
  });
496
498
  if (TARGET_COMPONENT_JSX_RE.test(code)) {
497
499
  const replacementImportStatements = buildReplacementImportStatements(runReplacementPass(ast, TARGET_COMPONENT_TAG, targetRegistry, null), targetRegistry);
498
- traverse$2(ast, { ImportDeclaration(nodePath) {
500
+ traverse$1(ast, { ImportDeclaration(nodePath) {
499
501
  if (nodePath.node.source.value === "@/targets/ui-target") nodePath.replaceWith(jsxText(replacementImportStatements));
500
502
  } });
501
503
  }
@@ -503,7 +505,7 @@ function transformTargets(code, targetRegistry, contextProviders) {
503
505
  const importStatements = /* @__PURE__ */ new Set();
504
506
  for (const contextProvider of contextProviders) importStatements.add(`import ${contextProvider.componentName} from '@/${contextProvider.path.replace(".tsx", "")}';`);
505
507
  const replacementImportStatements = Array.from(importStatements).join("\n");
506
- traverse$2(ast, { ImportDeclaration(nodePath) {
508
+ traverse$1(ast, { ImportDeclaration(nodePath) {
507
509
  if (nodePath.node.source.value === "@/targets/ui-target-providers") nodePath.replaceWith(jsxText(replacementImportStatements));
508
510
  } });
509
511
  runReplacementPass(ast, TARGET_PROVIDERS_TAG, null, contextProviders);
@@ -605,6 +607,44 @@ function buildTargetRegistry(rootDir, options = {}) {
605
607
  actionHookRegistry
606
608
  };
607
609
  }
610
+ const TARGET_ID_PATTERN = /<UITarget[\s][^>]*targetId=["']([^"']+)["']/g;
611
+ /**
612
+ * Recursively collect all UITarget IDs declared in template source files.
613
+ * Excludes extension directories so only "real" UITarget placements are counted.
614
+ */
615
+ function collectUITargetIds(sourceDir) {
616
+ const result = /* @__PURE__ */ new Set();
617
+ function walk(dir) {
618
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
619
+ for (const entry of entries) {
620
+ const fullPath = path$1.join(dir, entry.name);
621
+ if (entry.isDirectory()) {
622
+ if (entry.name === "extensions") continue;
623
+ walk(fullPath);
624
+ } else if (entry.isFile() && /\.(tsx?|jsx?)$/.test(entry.name) && !/\.test\.(tsx?|jsx?)$/.test(entry.name)) {
625
+ const content = fs.readFileSync(fullPath, "utf-8");
626
+ TARGET_ID_PATTERN.lastIndex = 0;
627
+ let match;
628
+ while ((match = TARGET_ID_PATTERN.exec(content)) !== null) result.add(match[1]);
629
+ }
630
+ }
631
+ }
632
+ walk(sourceDir);
633
+ return result;
634
+ }
635
+ /**
636
+ * Validate that all targetIds in the component registry correspond to
637
+ * UITarget declarations in the template source. Returns orphaned entries.
638
+ */
639
+ function validateTargetRegistry(componentRegistry, declaredTargetIds) {
640
+ const orphaned = [];
641
+ for (const targetId in componentRegistry) if (!declaredTargetIds.has(targetId)) for (const entry of componentRegistry[targetId]) orphaned.push({
642
+ targetId,
643
+ extension: entry.namespace,
644
+ componentPath: entry.path
645
+ });
646
+ return orphaned;
647
+ }
608
648
 
609
649
  //#endregion
610
650
  //#region src/plugins/transformTargets.ts
@@ -622,9 +662,14 @@ function transformTargetPlaceholderPlugin() {
622
662
  },
623
663
  buildStart() {
624
664
  ({componentRegistry, contextProviders} = buildTargetRegistry(sourceDir, { isProduction }));
665
+ const declaredTargetIds = collectUITargetIds(sourceDir);
666
+ const orphaned = validateTargetRegistry(componentRegistry, declaredTargetIds);
667
+ if (orphaned.length > 0) {
668
+ const lines = orphaned.map((o) => ` • "${o.targetId}" (extension: ${o.extension}, component: ${o.componentPath})`);
669
+ throw new Error(`[storefront-next] ${orphaned.length} extension component(s) target UITarget IDs that do not exist in the template:\n${lines.join("\n")}\n\nEither add a <UITarget targetId="..."> to the template or remove/disable the component in target-config.json.`);
670
+ }
625
671
  },
626
672
  transform(code, id) {
627
- if (process.env.VITE_UI_TARGET_DEV_MODE === "true") return null;
628
673
  try {
629
674
  const transformedCode = transformTargets(code, componentRegistry, contextProviders);
630
675
  if (transformedCode) return {
@@ -833,7 +878,7 @@ async function scanComponents(project, projectRoot, componentPath, registryPath)
833
878
  });
834
879
  logger.debug(`🔍 Scanning ${componentFiles.length} files in ${componentPath}...`);
835
880
  const components = [];
836
- const registryDir = dirname(resolve(projectRoot, registryPath));
881
+ const registryDir = dirname(resolve$1(projectRoot, registryPath));
837
882
  for (const filePath of componentFiles) try {
838
883
  const content = readFileSync(filePath, "utf-8");
839
884
  const sourceFile = project.createSourceFile(filePath, content, { overwrite: true });
@@ -876,11 +921,10 @@ async function scanComponents(project, projectRoot, componentPath, registryPath)
876
921
  function generateRegistryCode(components, registryIdentifier = "registry") {
877
922
  const sorted = [...components].sort((a, b) => a.id.localeCompare(b.id) || a.relativePath.localeCompare(b.relativePath));
878
923
  if (sorted.length === 0) return `
879
- /* eslint-disable */
880
924
  /**
881
925
  * Initialize the static component registry.
882
926
  * This function is auto-generated by the staticRegistry Vite plugin.
883
- *
927
+ *
884
928
  * DO NOT EDIT THIS FUNCTION MANUALLY - it will be overwritten on next build.
885
929
  */
886
930
  export function initializeRegistry(targetRegistry = ${registryIdentifier}): void {
@@ -897,13 +941,12 @@ export function initializeRegistry(targetRegistry = ${registryIdentifier}): void
897
941
  } else return ` targetRegistry.registerImporter('${id}', () => import('${relativePath}'));`;
898
942
  }).join("\n");
899
943
  return `
900
- /* eslint-disable */
901
944
  /**
902
945
  * Initialize the static component registry.
903
946
  * This function is auto-generated by the staticRegistry Vite plugin.
904
- *
947
+ *
905
948
  * DO NOT EDIT THIS FUNCTION MANUALLY - it will be overwritten on next build.
906
- *
949
+ *
907
950
  * Components registered: ${sorted.map((c) => c.id).join(", ")}
908
951
  */
909
952
  export function initializeRegistry(targetRegistry = ${registryIdentifier}): void {
@@ -912,9 +955,42 @@ ${registrations}
912
955
  `;
913
956
  }
914
957
  /**
958
+ * Formats registry file content with the project's own Prettier so the written file
959
+ * matches what the project's formatter would produce.
960
+ *
961
+ * Without this, a standalone `prettier --write` (pre-commit hook, format-on-save) rewrites
962
+ * the generated file on every commit: the generator emits one `registerImporter` call per
963
+ * line and cannot anticipate an arbitrary `printWidth`, so Prettier re-wraps long lines and
964
+ * the regenerate -> format -> regenerate loop never settles.
965
+ *
966
+ * Prettier is resolved from the registry file's own location — i.e. the consuming project's
967
+ * `node_modules`, not the SDK bundle (which keeps `node_modules` external). It is the project's
968
+ * Prettier version and config whose output must be matched. Returns the content unchanged when
969
+ * Prettier is absent or fails, so registry generation never breaks a build over formatting.
970
+ */
971
+ let warnedNoPrettier = false;
972
+ async function formatWithProjectPrettier(content, registryFilePath) {
973
+ try {
974
+ const prettier = createRequire(registryFilePath)("prettier");
975
+ const config = await prettier.resolveConfig(registryFilePath, { editorconfig: true });
976
+ return await prettier.format(content, {
977
+ ...config,
978
+ filepath: registryFilePath
979
+ });
980
+ } catch (error) {
981
+ if (error.code === "MODULE_NOT_FOUND") {
982
+ if (!warnedNoPrettier) {
983
+ logger.warn("⚠️ Prettier not found in the project; static registry will be written unformatted.");
984
+ warnedNoPrettier = true;
985
+ }
986
+ } else logger.warn(`⚠️ Skipping Prettier formatting for registry file: ${error.message}`);
987
+ return content;
988
+ }
989
+ }
990
+ /**
915
991
  * Updates the registry.ts file with the generated code
916
992
  */
917
- function updateRegistryFile(registryFilePath, generatedCode) {
993
+ async function updateRegistryFile(registryFilePath, generatedCode) {
918
994
  let existingContent;
919
995
  if (!existsSync(registryFilePath)) {
920
996
  logger.debug("📝 Creating new registry file...");
@@ -939,7 +1015,7 @@ export const registry = new ComponentRegistry();
939
1015
  const startIndex = existingContent.indexOf(startMarker);
940
1016
  const endIndex = existingContent.indexOf(endMarker);
941
1017
  if (startIndex === -1 || endIndex === -1) throw new Error(`Registry file ${registryFilePath} is missing static registry markers. Please add "${startMarker}" and "${endMarker}" markers to define the generated content area.`);
942
- const updatedContent = `${existingContent.slice(0, startIndex + 24)}\n${generatedCode}\n${existingContent.slice(endIndex)}`;
1018
+ const updatedContent = await formatWithProjectPrettier(`${existingContent.slice(0, startIndex + 24)}\n${generatedCode}\n${existingContent.slice(endIndex)}`, registryFilePath);
943
1019
  if (updatedContent === existingContent) {
944
1020
  logger.debug(`⏭️ Registry unchanged, skipping write: ${registryFilePath}`);
945
1021
  return false;
@@ -990,8 +1066,8 @@ const staticRegistryPlugin = (config = {}) => {
990
1066
  } }), projectRoot, componentPath, registryPath);
991
1067
  logger.debug(`📦 Found ${components.length} components with @Component decorators`);
992
1068
  const generatedCode = generateRegistryCode(components, registryIdentifier);
993
- const registryFilePath = resolve(projectRoot, registryPath);
994
- const changed = updateRegistryFile(registryFilePath, generatedCode);
1069
+ const registryFilePath = resolve$1(projectRoot, registryPath);
1070
+ const changed = await updateRegistryFile(registryFilePath, generatedCode);
995
1071
  logger.debug("✅ Static registry generation complete!");
996
1072
  return {
997
1073
  registryFilePath,
@@ -1033,15 +1109,68 @@ const staticRegistryPlugin = (config = {}) => {
1033
1109
  };
1034
1110
  };
1035
1111
 
1112
+ //#endregion
1113
+ //#region src/server/ts-import.ts
1114
+ /**
1115
+ * Parse TypeScript paths from tsconfig.json and convert to jiti alias format.
1116
+ *
1117
+ * @param tsconfigPath - Path to tsconfig.json
1118
+ * @param projectDirectory - Project root directory for resolving relative paths
1119
+ * @returns Record of alias mappings for jiti
1120
+ *
1121
+ * @example
1122
+ * // tsconfig.json: { "compilerOptions": { "paths": { "@/*": ["./src/*"] } } }
1123
+ * // Returns: { "@/": "/absolute/path/to/src/" }
1124
+ */
1125
+ function parseTsconfigPaths(tsconfigPath, projectDirectory) {
1126
+ const alias = {};
1127
+ if (!existsSync$1(tsconfigPath)) return alias;
1128
+ try {
1129
+ const tsconfigContent = readFileSync$1(tsconfigPath, "utf-8");
1130
+ const tsconfig = JSON.parse(tsconfigContent);
1131
+ const paths = tsconfig.compilerOptions?.paths;
1132
+ const baseUrl = tsconfig.compilerOptions?.baseUrl || ".";
1133
+ if (paths) {
1134
+ for (const [key, values] of Object.entries(paths)) if (values && values.length > 0) {
1135
+ const aliasKey = key.replace(/\/\*$/, "/");
1136
+ alias[aliasKey] = resolve(projectDirectory, baseUrl, values[0].replace(/\/\*$/, "/").replace(/^\.\//, ""));
1137
+ }
1138
+ }
1139
+ } catch {}
1140
+ const sortedAlias = {};
1141
+ Object.keys(alias).sort((a, b) => b.length - a.length).forEach((key) => {
1142
+ sortedAlias[key] = alias[key];
1143
+ });
1144
+ return sortedAlias;
1145
+ }
1146
+ /**
1147
+ * Import a TypeScript file using jiti with proper path alias resolution.
1148
+ * This is a cross-platform alternative to tsx that works on Windows.
1149
+ *
1150
+ * @param filePath - Absolute path to the TypeScript file to import
1151
+ * @param options - Import options including project directory
1152
+ * @returns The imported module
1153
+ */
1154
+ async function importTypescript(filePath, options) {
1155
+ const { projectDirectory, tsconfigPath = resolve(projectDirectory, "tsconfig.json") } = options;
1156
+ const { createJiti } = await import("jiti");
1157
+ const alias = parseTsconfigPaths(tsconfigPath, projectDirectory);
1158
+ return createJiti(import.meta.url, {
1159
+ fsCache: false,
1160
+ interopDefault: true,
1161
+ alias
1162
+ }).import(filePath);
1163
+ }
1164
+
1036
1165
  //#endregion
1037
1166
  //#region src/plugins/configLoader.ts
1038
1167
  /**
1039
1168
  * Load the engagement config from config.server.ts
1040
1169
  */
1041
1170
  async function loadEngagementConfig(projectRoot, configPath) {
1042
- const absoluteConfigPath = resolve(projectRoot, configPath);
1171
+ const absoluteConfigPath = resolve$1(projectRoot, configPath);
1043
1172
  try {
1044
- const config = (await import(pathToFileURL(absoluteConfigPath).href)).default;
1173
+ const config = (await importTypescript(absoluteConfigPath, { projectDirectory: projectRoot })).default;
1045
1174
  logger.debug(`📄 Loaded config from ${configPath}`);
1046
1175
  const engagement = config?.app?.engagement;
1047
1176
  if (!engagement) {
@@ -1066,7 +1195,7 @@ async function scanForInstrumentedEvents(projectRoot, scanPaths) {
1066
1195
  const sendViewPagePattern = /sendViewPageEvent\s*\(/g;
1067
1196
  const createEventPattern = /createEvent\s*\(\s*['"]([^'"]+)['"]/g;
1068
1197
  for (const scanPath of scanPaths) {
1069
- const files = await glob(join(resolve(projectRoot, scanPath), "**/*.{ts,tsx}"), { ignore: [
1198
+ const files = await glob(join(resolve$1(projectRoot, scanPath), "**/*.{ts,tsx}"), { ignore: [
1070
1199
  "**/*.test.ts",
1071
1200
  "**/*.test.tsx",
1072
1201
  "**/*.spec.ts",
@@ -1218,17 +1347,17 @@ const buildMiddlewareRegistryPlugin = () => {
1218
1347
  configResolved(config) {
1219
1348
  resolvedConfig = config;
1220
1349
  const rr = config.__reactRouterPluginContext?.reactRouterConfig ?? {};
1221
- buildDirectory = rr.buildDirectory ?? resolve(config.root, "build");
1350
+ buildDirectory = rr.buildDirectory ?? resolve$1(config.root, "build");
1222
1351
  appDirectory = rr.appDirectory ?? "src";
1223
1352
  },
1224
1353
  buildApp: {
1225
1354
  order: "post",
1226
1355
  handler: async () => {
1227
1356
  const projectRoot = resolvedConfig.root;
1228
- const middlewareRegistryPath = resolve(projectRoot, appDirectory, SERVER_OUT_SUBDIR, MIDDLEWARE_REGISTRY_SOURCE_FILE);
1357
+ const middlewareRegistryPath = resolve$1(projectRoot, appDirectory, SERVER_OUT_SUBDIR, MIDDLEWARE_REGISTRY_SOURCE_FILE);
1229
1358
  if (!existsSync(middlewareRegistryPath)) return;
1230
1359
  const { build } = await import("tsdown");
1231
- const serverOutDir = resolve(projectRoot, buildDirectory, SERVER_OUT_SUBDIR);
1360
+ const serverOutDir = resolve$1(projectRoot, buildDirectory, SERVER_OUT_SUBDIR);
1232
1361
  await build({
1233
1362
  cwd: projectRoot,
1234
1363
  entry: { [MIDDLEWARE_REGISTRY_SOURCE_FILE.replace(/\.ts$/, "")]: middlewareRegistryPath },
@@ -1308,7 +1437,7 @@ export * from ${importPath};
1308
1437
  export default _composed.default;
1309
1438
  export const handleDataRequest = _composed.handleDataRequest;
1310
1439
  export const handleError = _composed.handleError;
1311
- export const unstable_instrumentations = _composed.unstable_instrumentations;
1440
+ export const instrumentations = _composed.instrumentations;
1312
1441
  export const streamTimeout = _composed.streamTimeout;
1313
1442
  `.trim();
1314
1443
  }
@@ -1453,7 +1582,7 @@ const workspacePlugin = () => {
1453
1582
 
1454
1583
  //#endregion
1455
1584
  //#region src/plugins/componentLoaders.ts
1456
- const traverse$1 = traverseModule.default || traverseModule;
1585
+ const traverse = traverseModule.default || traverseModule;
1457
1586
  const generate$1 = generate.default || generate;
1458
1587
  /**
1459
1588
  * Names of exports to strip per environment.
@@ -1483,7 +1612,7 @@ function hasExportCandidate(code, names) {
1483
1612
  */
1484
1613
  function hasComponentDecorator(ast) {
1485
1614
  let found = false;
1486
- traverse$1(ast, { ClassDeclaration(path$2) {
1615
+ traverse(ast, { ClassDeclaration(path$2) {
1487
1616
  const decorators = path$2.node.decorators;
1488
1617
  if (!decorators) return;
1489
1618
  for (const decorator of decorators) if (decorator.expression.type === "CallExpression" && isIdentifier(decorator.expression.callee) && decorator.expression.callee.name === "Component") {
@@ -1530,7 +1659,7 @@ function stripExports(code, exportsToStrip, preParsedAst) {
1530
1659
  let changed = false;
1531
1660
  const previouslyReferencedIdentifiers = findReferencedIdentifiers(ast);
1532
1661
  const removedExportLocalNames = /* @__PURE__ */ new Set();
1533
- traverse$1(ast, { ExportNamedDeclaration(path$2) {
1662
+ traverse(ast, { ExportNamedDeclaration(path$2) {
1534
1663
  const { declaration, specifiers } = path$2.node;
1535
1664
  if (declaration && isVariableDeclaration(declaration)) {
1536
1665
  const remaining = declaration.declarations.filter((decl) => {
@@ -1588,7 +1717,7 @@ function stripExports(code, exportsToStrip, preParsedAst) {
1588
1717
  }
1589
1718
  }
1590
1719
  } });
1591
- if (changed) traverse$1(ast, { ExpressionStatement(path$2) {
1720
+ if (changed) traverse(ast, { ExpressionStatement(path$2) {
1592
1721
  if (!path$2.parentPath?.isProgram()) return;
1593
1722
  if (path$2.node.expression.type === "AssignmentExpression") {
1594
1723
  const left = path$2.node.expression.left;
@@ -1907,93 +2036,19 @@ function storefrontNextTargets(config = {}) {
1907
2036
 
1908
2037
  //#endregion
1909
2038
  //#region src/plugins/uiTargetDevMode.ts
1910
- const traverse = traverseModule.default || traverseModule;
1911
- const UI_TARGET_JSX_RE = /<UITarget[\s/>]/;
1912
2039
  /**
1913
- * Vite plugin that adds visual markers to UITarget components in development.
2040
+ * @deprecated This plugin has been removed. It was an internal-only debugging aid
2041
+ * that could cause build failures when extensions were trimmed. The export is kept
2042
+ * as a no-op for backward compatibility and will be removed in v2.
1914
2043
  *
1915
- * PRODUCTION: This plugin is completely inactive - zero overhead.
1916
- * DEVELOPMENT: Transforms UITarget JSX to add visual debugging markers.
2044
+ * **Migration:** Remove any `uiTargetDevMode()` plugin from your `vite.config.ts`,
2045
+ * delete `vite-plugins/ui-target-dev-mode.ts` if it exists, and remove the plugin
2046
+ * from the Vite plugins array.
1917
2047
  *
1918
- * @example
1919
- * // Source code:
1920
- * <UITarget targetId="pdp.loyalty.badge">
1921
- * <Widget />
1922
- * </UITarget>
1923
- *
1924
- * // Transformed in DEV mode:
1925
- * <UITargetDevMarker
1926
- * targetId="pdp.loyalty.badge"
1927
- * __file__="/src/components/product.tsx"
1928
- * __hasChildren__={true}
1929
- * >
1930
- * <Widget />
1931
- * </UITargetDevMarker>
1932
- */
1933
- function uiTargetDevModePlugin(config = {}) {
1934
- if (process.env.NODE_ENV === "production") return { name: "storefront-next:ui-target-dev-mode-noop" };
1935
- if (!(config.enabled ?? process.env.VITE_UI_TARGET_DEV_MODE === "true")) return { name: "storefront-next:ui-target-dev-mode-disabled" };
1936
- logger.info("🎯 UITarget Dev Mode enabled");
1937
- if (config.filterCategory) logger.info(` Filtering to category: ${config.filterCategory} (build-time)`);
1938
- return {
1939
- name: "storefront-next:ui-target-dev-mode",
1940
- enforce: "pre",
1941
- transform(code, id) {
1942
- if (!id.match(/\.(tsx|jsx)$/)) return null;
1943
- if (id.includes("node_modules")) return null;
1944
- if (id.includes("/targets/ui-target.tsx")) return null;
1945
- if (!UI_TARGET_JSX_RE.test(code) || !code.includes("from '@/targets/ui-target'")) return null;
1946
- try {
1947
- const ast = parse(code, {
1948
- sourceType: "module",
1949
- plugins: [
1950
- "typescript",
1951
- "jsx",
1952
- "decorators-legacy"
1953
- ]
1954
- });
1955
- let hasTransforms = false;
1956
- const targetIds = [];
1957
- traverse(ast, { JSXElement(path$2) {
1958
- const openingElement = path$2.node.openingElement;
1959
- if (!isJSXIdentifier(openingElement.name) || openingElement.name.name !== "UITarget") return;
1960
- const targetIdAttr = openingElement.attributes.find((attr) => isJSXAttribute(attr) && isJSXIdentifier(attr.name) && attr.name.name === "targetId");
1961
- if (!targetIdAttr) {
1962
- logger.warn(`UITarget without targetId in ${id}`);
1963
- return;
1964
- }
1965
- const targetId = isStringLiteral(targetIdAttr.value) ? targetIdAttr.value.value : null;
1966
- if (!targetId) {
1967
- logger.warn(`UITarget with non-string targetId in ${id}`);
1968
- return;
1969
- }
1970
- if (config.filterCategory && !targetId.startsWith(`${config.filterCategory}.`)) return;
1971
- const hasChildren = path$2.node.children.length > 0;
1972
- openingElement.name = jsxIdentifier("UITargetDevMarker");
1973
- if (path$2.node.closingElement) path$2.node.closingElement.name = jsxIdentifier("UITargetDevMarker");
1974
- const hint = config.hintMap?.[targetId];
1975
- openingElement.attributes.push(jsxAttribute(jsxIdentifier("__file__"), stringLiteral(id)), jsxAttribute(jsxIdentifier("__hasChildren__"), jsxExpressionContainer(booleanLiteral(hasChildren))), ...hint ? [jsxAttribute(jsxIdentifier("__hint__"), stringLiteral(hint))] : []);
1976
- hasTransforms = true;
1977
- targetIds.push(targetId);
1978
- } });
1979
- if (!hasTransforms) return null;
1980
- const devMarkerImport = importDeclaration([importSpecifier(identifier("UITargetDevMarker"), identifier("UITargetDevMarker"))], stringLiteral("@/lib/ui-target-dev-mode/marker"));
1981
- ast.program.body.unshift(devMarkerImport);
1982
- const output = generate(ast, {
1983
- retainLines: true,
1984
- compact: false
1985
- }, code);
1986
- logger.debug(`Transformed ${targetIds.length} targets in ${id.split("/").pop()}`);
1987
- return {
1988
- code: output.code,
1989
- map: output.map
1990
- };
1991
- } catch (error) {
1992
- logger.error(`Failed to transform UITarget in ${id}:`, error);
1993
- return null;
1994
- }
1995
- }
1996
- };
2048
+ * @returns A no-op Vite plugin
2049
+ */
2050
+ function uiTargetDevModePlugin(_config) {
2051
+ return { name: "storefront-next:ui-target-dev-mode-deprecated-noop" };
1997
2052
  }
1998
2053
 
1999
2054
  //#endregion
@@ -2098,6 +2153,82 @@ function escapeRegExp(str) {
2098
2153
  return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2099
2154
  }
2100
2155
  /**
2156
+ * Resolve a captured prefix segment to its canonical-cased alias, case-insensitively.
2157
+ * Returns the matching entry from `aliases` (preserving the catalog's casing) so the value
2158
+ * forwarded to SFRA is always the expected canonical form, or `undefined` when the segment
2159
+ * isn't a known site/locale identifier.
2160
+ */
2161
+ function matchAlias(segment, aliases) {
2162
+ if (segment === void 0) return void 0;
2163
+ const lower = segment.toLowerCase();
2164
+ return aliases.find((alias) => alias.toLowerCase() === lower);
2165
+ }
2166
+ /**
2167
+ * Rewrite an incoming proxy pathname to the SFRA `/s/{siteId}/{locale}/…` shape.
2168
+ *
2169
+ * This is the OOTB path transform. It is `url.prefix`-aware: when `urlPrefix` is set
2170
+ * it strips the configured prefix to recover the bare functional path AND reuses the
2171
+ * site/locale the path already carried — so a prefixed request like `/uk/cart` becomes
2172
+ * `/s/{siteId}/uk/cart` rather than the double-stacked `/s/{siteId}/{locale}/uk/cart`.
2173
+ *
2174
+ * It handles every standard prefix shape uniformly:
2175
+ * - no prefix (`''`/`/`): `/cart` → `/s/{site}/{locale}/cart`
2176
+ * - `/:localeId`: `/uk/cart` → `/s/{site}/uk/cart`
2177
+ * - `/:siteId/:localeId`: `/global/uk/cart` → `/s/global/uk/cart`
2178
+ *
2179
+ * Disambiguation: a captured prefix segment is only treated as a site/locale when it is
2180
+ * a known identifier (`siteAliases`/`localeAliases`). This stops a bare path like `/cart`
2181
+ * under a `/:localeId` prefix — which is structurally indistinguishable from a locale
2182
+ * home `/uk` — from being mis-stripped (here `cart` isn't a known locale, so the whole
2183
+ * path is treated as the bare functional path and decorated with the `defaultSiteId`/
2184
+ * `defaultLocale` fallbacks). When a prefix segment IS a known site/locale, that value is
2185
+ * passed straight through as the SFRA path segment; SFRA resolves both the BM site-path
2186
+ * alias (`uk`) and the canonical id (`en-GB`).
2187
+ *
2188
+ * Paths already in SFCC shape (`/s/…`, `/on/demandware.…`) are returned unchanged — the
2189
+ * caller is expected to skip them, but this stays defensive.
2190
+ *
2191
+ * Pure function (query string handled by the caller) so it's unit-testable in isolation.
2192
+ *
2193
+ * @param pathname - Incoming pathname on the proxy (no query string).
2194
+ * @param opts.defaultSiteId - Site to use when the path doesn't carry one.
2195
+ * @param opts.defaultLocale - Locale to use when the path doesn't carry one.
2196
+ * @param opts.urlPrefix - The storefront's `url.prefix` (e.g. `'/:siteId/:localeId'`).
2197
+ * @param opts.localeAliases - Known locale ids + aliases.
2198
+ * @param opts.siteAliases - Known site ids + aliases.
2199
+ */
2200
+ function rewriteToSfraPath(pathname, opts) {
2201
+ const { defaultSiteId, defaultLocale, urlPrefix, localeAliases = [], siteAliases = [] } = opts;
2202
+ if (pathname.startsWith("/s/") || pathname.startsWith("/on/demandware.")) return pathname;
2203
+ let site = defaultSiteId;
2204
+ let locale = defaultLocale;
2205
+ let barePath = pathname;
2206
+ let localeFromPath = false;
2207
+ if (urlPrefix && urlPrefix !== "/") {
2208
+ const captured = extractPrefixParamValues({
2209
+ pathname,
2210
+ prefix: urlPrefix
2211
+ });
2212
+ const matchedSite = "siteId" in captured ? matchAlias(captured.siteId, siteAliases) : void 0;
2213
+ const matchedLocale = "localeId" in captured ? matchAlias(captured.localeId, localeAliases) : void 0;
2214
+ const siteOk = !("siteId" in captured) || matchedSite !== void 0;
2215
+ const localeOk = !("localeId" in captured) || matchedLocale !== void 0;
2216
+ if (Object.keys(captured).length > 0 && siteOk && localeOk) {
2217
+ if (matchedSite) site = matchedSite;
2218
+ if (matchedLocale) {
2219
+ locale = matchedLocale;
2220
+ localeFromPath = true;
2221
+ }
2222
+ barePath = stripPathPrefix({
2223
+ pathname,
2224
+ prefix: urlPrefix
2225
+ }) || "/";
2226
+ }
2227
+ }
2228
+ if (barePath === "/") return localeFromPath ? `/s/${site}/${locale}/` : `/s/${site}`;
2229
+ return `/s/${site}/${locale}${barePath}`;
2230
+ }
2231
+ /**
2101
2232
  * Rewrite an SFCC Location header so the user stays on the proxy origin AND so
2102
2233
  * any query params from the original request survive an SFCC redirect.
2103
2234
  *
@@ -2173,7 +2304,8 @@ function hybridProxyPlugin(options) {
2173
2304
  logger.debug(`Hybrid proxy routing rules: ${options.routingRules.slice(0, 100)}...`);
2174
2305
  const locale = options.locale || "default";
2175
2306
  const defaultSiteId = options.defaultSiteId;
2176
- logger.debug(`Hybrid proxy path transformation: / → /s/${defaultSiteId}, /path → /s/${defaultSiteId}/${locale}/path`);
2307
+ const { urlPrefix, localeAliases, siteAliases, rewritePath } = options;
2308
+ logger.debug(urlPrefix ? `Hybrid proxy path transformation: url.prefix=${urlPrefix} → /s/${defaultSiteId}/{locale}/{bare path}` : `Hybrid proxy path transformation: / → /s/${defaultSiteId}, /path → /s/${defaultSiteId}/${locale}/path`);
2177
2309
  const targetOriginPattern = new RegExp(escapeRegExp(options.targetOrigin), "g");
2178
2310
  const proxy = httpProxy.createProxyServer({
2179
2311
  changeOrigin: true,
@@ -2185,13 +2317,13 @@ function hybridProxyPlugin(options) {
2185
2317
  const pathname = url.pathname;
2186
2318
  if (!pathname.startsWith("/s/") && !pathname.startsWith("/on/demandware.")) {
2187
2319
  const originalPath = proxyReq.path;
2188
- /**
2189
- * "/" maps to the SFRA/SiteGenesis site root — no locale in the path
2190
- * 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.
2191
- * We need to rewrite the path to /s/{siteId} so that it can be proxied to the correct SFCC URL.
2192
- */
2193
- if (pathname === "/") proxyReq.path = `/s/${defaultSiteId}${url.search}`;
2194
- else proxyReq.path = `/s/${defaultSiteId}/${locale}${pathname}${url.search}`;
2320
+ proxyReq.path = `${rewritePath?.(pathname) ?? null ?? rewriteToSfraPath(pathname, {
2321
+ defaultSiteId,
2322
+ defaultLocale: locale,
2323
+ urlPrefix,
2324
+ localeAliases,
2325
+ siteAliases
2326
+ })}${url.search}`;
2195
2327
  logger.debug(`Hybrid proxy path rewrite: ${originalPath} → ${proxyReq.path}`);
2196
2328
  }
2197
2329
  });