@salesforce/storefront-next-dev 1.1.0-alpha.0 → 1.1.0-alpha.2

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
@@ -5,7 +5,7 @@ import { createRequire } from "module";
5
5
  import path$1, { dirname, join, relative, resolve as resolve$1 } from "path";
6
6
  import { fileURLToPath } from "url";
7
7
  import { parse } from "@babel/parser";
8
- 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";
9
9
  import { generate } from "@babel/generator";
10
10
  import traverseModule from "@babel/traverse";
11
11
  import { existsSync, readFileSync, writeFileSync } from "fs";
@@ -15,6 +15,7 @@ import fs$1, { existsSync as existsSync$1, readFileSync as readFileSync$1 } from
15
15
  import { deadCodeElimination, findReferencedIdentifiers } from "babel-dead-code-elimination";
16
16
  import httpProxy from "http-proxy";
17
17
  import { brotliDecompressSync, gunzipSync, inflateSync } from "zlib";
18
+ import { extractPrefixParamValues, stripPathPrefix } from "@salesforce/storefront-next-runtime/site-context";
18
19
 
19
20
  //#region src/utils/logger.ts
20
21
  const LEVEL_PRIORITY = {
@@ -368,7 +369,7 @@ const patchReactRouterPlugin = () => {
368
369
 
369
370
  //#endregion
370
371
  //#region src/extensibility/target-utils.ts
371
- const traverse$2 = traverseModule.default || traverseModule;
372
+ const traverse$1 = traverseModule.default || traverseModule;
372
373
  const TARGET_COMPONENT_TAG = "UITarget";
373
374
  const TARGET_PROVIDERS_TAG = "UITargetProviders";
374
375
  const TARGET_ID_ATTRIBUTE = "targetId";
@@ -443,7 +444,7 @@ function runReplacementPass(ast, tagName, targetRegistry = null, contextProvider
443
444
  if (replacedId) targetIdsReplaced.add(replacedId);
444
445
  } else if (contextProviders) findAndReplaceProviders(pathToReplace, contextProviders);
445
446
  };
446
- traverse$2(ast, {
447
+ traverse$1(ast, {
447
448
  VariableDeclaration(nodePath) {
448
449
  const declarationPaths = nodePath.get("declarations");
449
450
  const declarationsArray = Array.isArray(declarationPaths) ? declarationPaths : [declarationPaths];
@@ -496,7 +497,7 @@ function transformTargets(code, targetRegistry, contextProviders) {
496
497
  });
497
498
  if (TARGET_COMPONENT_JSX_RE.test(code)) {
498
499
  const replacementImportStatements = buildReplacementImportStatements(runReplacementPass(ast, TARGET_COMPONENT_TAG, targetRegistry, null), targetRegistry);
499
- traverse$2(ast, { ImportDeclaration(nodePath) {
500
+ traverse$1(ast, { ImportDeclaration(nodePath) {
500
501
  if (nodePath.node.source.value === "@/targets/ui-target") nodePath.replaceWith(jsxText(replacementImportStatements));
501
502
  } });
502
503
  }
@@ -504,7 +505,7 @@ function transformTargets(code, targetRegistry, contextProviders) {
504
505
  const importStatements = /* @__PURE__ */ new Set();
505
506
  for (const contextProvider of contextProviders) importStatements.add(`import ${contextProvider.componentName} from '@/${contextProvider.path.replace(".tsx", "")}';`);
506
507
  const replacementImportStatements = Array.from(importStatements).join("\n");
507
- traverse$2(ast, { ImportDeclaration(nodePath) {
508
+ traverse$1(ast, { ImportDeclaration(nodePath) {
508
509
  if (nodePath.node.source.value === "@/targets/ui-target-providers") nodePath.replaceWith(jsxText(replacementImportStatements));
509
510
  } });
510
511
  runReplacementPass(ast, TARGET_PROVIDERS_TAG, null, contextProviders);
@@ -607,7 +608,6 @@ function buildTargetRegistry(rootDir, options = {}) {
607
608
  };
608
609
  }
609
610
  const TARGET_ID_PATTERN = /<UITarget[\s][^>]*targetId=["']([^"']+)["']/g;
610
- const EXCLUDED_DIRS = new Set(["ui-target-dev-mode", "ui-target-smoke-test"]);
611
611
  /**
612
612
  * Recursively collect all UITarget IDs declared in template source files.
613
613
  * Excludes extension directories so only "real" UITarget placements are counted.
@@ -619,7 +619,7 @@ function collectUITargetIds(sourceDir) {
619
619
  for (const entry of entries) {
620
620
  const fullPath = path$1.join(dir, entry.name);
621
621
  if (entry.isDirectory()) {
622
- if (entry.name === "extensions" || EXCLUDED_DIRS.has(entry.name)) continue;
622
+ if (entry.name === "extensions") continue;
623
623
  walk(fullPath);
624
624
  } else if (entry.isFile() && /\.(tsx?|jsx?)$/.test(entry.name) && !/\.test\.(tsx?|jsx?)$/.test(entry.name)) {
625
625
  const content = fs.readFileSync(fullPath, "utf-8");
@@ -670,7 +670,6 @@ function transformTargetPlaceholderPlugin() {
670
670
  }
671
671
  },
672
672
  transform(code, id) {
673
- if (process.env.VITE_UI_TARGET_DEV_MODE === "true") return null;
674
673
  try {
675
674
  const transformedCode = transformTargets(code, componentRegistry, contextProviders);
676
675
  if (transformedCode) return {
@@ -1438,7 +1437,7 @@ export * from ${importPath};
1438
1437
  export default _composed.default;
1439
1438
  export const handleDataRequest = _composed.handleDataRequest;
1440
1439
  export const handleError = _composed.handleError;
1441
- export const unstable_instrumentations = _composed.unstable_instrumentations;
1440
+ export const instrumentations = _composed.instrumentations;
1442
1441
  export const streamTimeout = _composed.streamTimeout;
1443
1442
  `.trim();
1444
1443
  }
@@ -1583,7 +1582,7 @@ const workspacePlugin = () => {
1583
1582
 
1584
1583
  //#endregion
1585
1584
  //#region src/plugins/componentLoaders.ts
1586
- const traverse$1 = traverseModule.default || traverseModule;
1585
+ const traverse = traverseModule.default || traverseModule;
1587
1586
  const generate$1 = generate.default || generate;
1588
1587
  /**
1589
1588
  * Names of exports to strip per environment.
@@ -1613,7 +1612,7 @@ function hasExportCandidate(code, names) {
1613
1612
  */
1614
1613
  function hasComponentDecorator(ast) {
1615
1614
  let found = false;
1616
- traverse$1(ast, { ClassDeclaration(path$2) {
1615
+ traverse(ast, { ClassDeclaration(path$2) {
1617
1616
  const decorators = path$2.node.decorators;
1618
1617
  if (!decorators) return;
1619
1618
  for (const decorator of decorators) if (decorator.expression.type === "CallExpression" && isIdentifier(decorator.expression.callee) && decorator.expression.callee.name === "Component") {
@@ -1660,7 +1659,7 @@ function stripExports(code, exportsToStrip, preParsedAst) {
1660
1659
  let changed = false;
1661
1660
  const previouslyReferencedIdentifiers = findReferencedIdentifiers(ast);
1662
1661
  const removedExportLocalNames = /* @__PURE__ */ new Set();
1663
- traverse$1(ast, { ExportNamedDeclaration(path$2) {
1662
+ traverse(ast, { ExportNamedDeclaration(path$2) {
1664
1663
  const { declaration, specifiers } = path$2.node;
1665
1664
  if (declaration && isVariableDeclaration(declaration)) {
1666
1665
  const remaining = declaration.declarations.filter((decl) => {
@@ -1718,7 +1717,7 @@ function stripExports(code, exportsToStrip, preParsedAst) {
1718
1717
  }
1719
1718
  }
1720
1719
  } });
1721
- if (changed) traverse$1(ast, { ExpressionStatement(path$2) {
1720
+ if (changed) traverse(ast, { ExpressionStatement(path$2) {
1722
1721
  if (!path$2.parentPath?.isProgram()) return;
1723
1722
  if (path$2.node.expression.type === "AssignmentExpression") {
1724
1723
  const left = path$2.node.expression.left;
@@ -2037,93 +2036,19 @@ function storefrontNextTargets(config = {}) {
2037
2036
 
2038
2037
  //#endregion
2039
2038
  //#region src/plugins/uiTargetDevMode.ts
2040
- const traverse = traverseModule.default || traverseModule;
2041
- const UI_TARGET_JSX_RE = /<UITarget[\s/>]/;
2042
2039
  /**
2043
- * 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.
2044
2043
  *
2045
- * PRODUCTION: This plugin is completely inactive - zero overhead.
2046
- * 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.
2047
2047
  *
2048
- * @example
2049
- * // Source code:
2050
- * <UITarget targetId="pdp.loyalty.badge">
2051
- * <Widget />
2052
- * </UITarget>
2053
- *
2054
- * // Transformed in DEV mode:
2055
- * <UITargetDevMarker
2056
- * targetId="pdp.loyalty.badge"
2057
- * __file__="/src/components/product.tsx"
2058
- * __hasChildren__={true}
2059
- * >
2060
- * <Widget />
2061
- * </UITargetDevMarker>
2062
- */
2063
- function uiTargetDevModePlugin(config = {}) {
2064
- if (process.env.NODE_ENV === "production") return { name: "storefront-next:ui-target-dev-mode-noop" };
2065
- if (!(config.enabled ?? process.env.VITE_UI_TARGET_DEV_MODE === "true")) return { name: "storefront-next:ui-target-dev-mode-disabled" };
2066
- logger.info("🎯 UITarget Dev Mode enabled");
2067
- if (config.filterCategory) logger.info(` Filtering to category: ${config.filterCategory} (build-time)`);
2068
- return {
2069
- name: "storefront-next:ui-target-dev-mode",
2070
- enforce: "pre",
2071
- transform(code, id) {
2072
- if (!id.match(/\.(tsx|jsx)$/)) return null;
2073
- if (id.includes("node_modules")) return null;
2074
- if (id.includes("/targets/ui-target.tsx")) return null;
2075
- if (!UI_TARGET_JSX_RE.test(code) || !code.includes("from '@/targets/ui-target'")) return null;
2076
- try {
2077
- const ast = parse(code, {
2078
- sourceType: "module",
2079
- plugins: [
2080
- "typescript",
2081
- "jsx",
2082
- "decorators-legacy"
2083
- ]
2084
- });
2085
- let hasTransforms = false;
2086
- const targetIds = [];
2087
- traverse(ast, { JSXElement(path$2) {
2088
- const openingElement = path$2.node.openingElement;
2089
- if (!isJSXIdentifier(openingElement.name) || openingElement.name.name !== "UITarget") return;
2090
- const targetIdAttr = openingElement.attributes.find((attr) => isJSXAttribute(attr) && isJSXIdentifier(attr.name) && attr.name.name === "targetId");
2091
- if (!targetIdAttr) {
2092
- logger.warn(`UITarget without targetId in ${id}`);
2093
- return;
2094
- }
2095
- const targetId = isStringLiteral(targetIdAttr.value) ? targetIdAttr.value.value : null;
2096
- if (!targetId) {
2097
- logger.warn(`UITarget with non-string targetId in ${id}`);
2098
- return;
2099
- }
2100
- if (config.filterCategory && !targetId.startsWith(`${config.filterCategory}.`)) return;
2101
- const hasChildren = path$2.node.children.length > 0;
2102
- openingElement.name = jsxIdentifier("UITargetDevMarker");
2103
- if (path$2.node.closingElement) path$2.node.closingElement.name = jsxIdentifier("UITargetDevMarker");
2104
- const hint = config.hintMap?.[targetId];
2105
- openingElement.attributes.push(jsxAttribute(jsxIdentifier("__file__"), stringLiteral(id)), jsxAttribute(jsxIdentifier("__hasChildren__"), jsxExpressionContainer(booleanLiteral(hasChildren))), ...hint ? [jsxAttribute(jsxIdentifier("__hint__"), stringLiteral(hint))] : []);
2106
- hasTransforms = true;
2107
- targetIds.push(targetId);
2108
- } });
2109
- if (!hasTransforms) return null;
2110
- const devMarkerImport = importDeclaration([importSpecifier(identifier("UITargetDevMarker"), identifier("UITargetDevMarker"))], stringLiteral("@/lib/ui-target-dev-mode/marker"));
2111
- ast.program.body.unshift(devMarkerImport);
2112
- const output = generate(ast, {
2113
- retainLines: true,
2114
- compact: false
2115
- }, code);
2116
- logger.debug(`Transformed ${targetIds.length} targets in ${id.split("/").pop()}`);
2117
- return {
2118
- code: output.code,
2119
- map: output.map
2120
- };
2121
- } catch (error) {
2122
- logger.error(`Failed to transform UITarget in ${id}:`, error);
2123
- return null;
2124
- }
2125
- }
2126
- };
2048
+ * @returns A no-op Vite plugin
2049
+ */
2050
+ function uiTargetDevModePlugin(_config) {
2051
+ return { name: "storefront-next:ui-target-dev-mode-deprecated-noop" };
2127
2052
  }
2128
2053
 
2129
2054
  //#endregion
@@ -2228,6 +2153,82 @@ function escapeRegExp(str) {
2228
2153
  return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2229
2154
  }
2230
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
+ /**
2231
2232
  * Rewrite an SFCC Location header so the user stays on the proxy origin AND so
2232
2233
  * any query params from the original request survive an SFCC redirect.
2233
2234
  *
@@ -2303,7 +2304,8 @@ function hybridProxyPlugin(options) {
2303
2304
  logger.debug(`Hybrid proxy routing rules: ${options.routingRules.slice(0, 100)}...`);
2304
2305
  const locale = options.locale || "default";
2305
2306
  const defaultSiteId = options.defaultSiteId;
2306
- 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`);
2307
2309
  const targetOriginPattern = new RegExp(escapeRegExp(options.targetOrigin), "g");
2308
2310
  const proxy = httpProxy.createProxyServer({
2309
2311
  changeOrigin: true,
@@ -2315,13 +2317,13 @@ function hybridProxyPlugin(options) {
2315
2317
  const pathname = url.pathname;
2316
2318
  if (!pathname.startsWith("/s/") && !pathname.startsWith("/on/demandware.")) {
2317
2319
  const originalPath = proxyReq.path;
2318
- /**
2319
- * "/" maps to the SFRA/SiteGenesis site root — no locale in the path
2320
- * 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.
2321
- * We need to rewrite the path to /s/{siteId} so that it can be proxied to the correct SFCC URL.
2322
- */
2323
- if (pathname === "/") proxyReq.path = `/s/${defaultSiteId}${url.search}`;
2324
- 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}`;
2325
2327
  logger.debug(`Hybrid proxy path rewrite: ${originalPath} → ${proxyReq.path}`);
2326
2328
  }
2327
2329
  });