@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.
@@ -12,7 +12,23 @@ import { parseEnv } from "node:util";
12
12
 
13
13
  //#region src/create-storefront.ts
14
14
  const DEFAULT_STOREFRONT = "sfcc-storefront";
15
- const STOREFRONT_NEXT_GITHUB_URL = "https://github.com/SalesforceCommerceCloud/storefront-next-template";
15
+ /**
16
+ * Available storefront verticals, keyed by the value accepted by the `--vertical` flag.
17
+ * `label` is the human-facing choice shown in the interactive prompt; `url` is the
18
+ * published template repository cloned for that vertical. Extend this map to surface a
19
+ * new vertical in both the prompt and the flag.
20
+ */
21
+ const VERTICALS = {
22
+ fashion: {
23
+ label: "Salesforce B2C Commerce Retail Storefront (Fashion)",
24
+ url: "https://github.com/SalesforceCommerceCloud/storefront-next-template"
25
+ },
26
+ cosmetic: {
27
+ label: "Salesforce B2C Commerce Beauty Storefront (Cosmetic)",
28
+ url: "https://github.com/SalesforceCommerceCloud/storefront-next-beauty"
29
+ }
30
+ };
31
+ const DEFAULT_VERTICAL = "fashion";
16
32
  const isLocalPath = (template) => template.startsWith("file://") || template.startsWith("/") || template.startsWith("./") || template.startsWith("../");
17
33
  const createStorefront = async (options = {}) => {
18
34
  try {
@@ -35,15 +51,23 @@ const createStorefront = async (options = {}) => {
35
51
  logger.info("\n");
36
52
  const outputPath = options.outputDir ? path.join(options.outputDir, storefront) : storefront;
37
53
  let template = options.template;
54
+ if (!template && options.vertical) {
55
+ const vertical = VERTICALS[options.vertical];
56
+ if (!vertical) {
57
+ logger.error(`Unknown vertical "${options.vertical}". Available verticals: ${Object.keys(VERTICALS).join(", ")}.`);
58
+ process.exit(1);
59
+ }
60
+ template = vertical.url;
61
+ } else if (!template && options.defaults) template = VERTICALS[DEFAULT_VERTICAL].url;
38
62
  if (!template) {
39
63
  template = (await prompts({
40
64
  type: "select",
41
65
  name: "template",
42
66
  message: "📄 Which template would you like to use for your storefront?\n",
43
- choices: [{
44
- title: "Salesforce B2C Commerce Retail Storefront",
45
- value: STOREFRONT_NEXT_GITHUB_URL
46
- }, {
67
+ choices: [...Object.values(VERTICALS).map((vertical) => ({
68
+ title: vertical.label,
69
+ value: vertical.url
70
+ })), {
47
71
  title: "A different template (I will provide the Github URL)",
48
72
  value: "custom"
49
73
  }]
@@ -153,7 +177,7 @@ const createStorefront = async (options = {}) => {
153
177
  logger.warn(`${dependentNames} requires ${addedName}. ${addedName} has been automatically added.`);
154
178
  }
155
179
  }
156
- trimExtensions(outputPath, Object.fromEntries(resolvedExtensions.map((ext) => [ext, true])), { extensions: extensionConfig.extensions });
180
+ await trimExtensions(outputPath, Object.fromEntries(resolvedExtensions.map((ext) => [ext, true])), { extensions: extensionConfig.extensions });
157
181
  }
158
182
  }
159
183
  const configMetaPath = fs.existsSync(path.join(outputPath, "config-meta.json")) ? path.join(outputPath, "config-meta.json") : path.join(outputPath, "src", "config", "config-meta.json");
@@ -198,6 +222,7 @@ var CreateStorefront = class CreateStorefront extends Command {
198
222
  static description = "Create a storefront project";
199
223
  static examples = [
200
224
  "<%= config.bin %> <%= command.id %>",
225
+ "<%= config.bin %> <%= command.id %> -n my-storefront -V cosmetic",
201
226
  "<%= config.bin %> <%= command.id %> -n my-storefront -t https://github.com/org/template -b release-0.2.x",
202
227
  "<%= config.bin %> <%= command.id %> -n my-storefront -t /path/to/local/template",
203
228
  "<%= config.bin %> <%= command.id %> -l /path/to/monorepo/packages"
@@ -211,6 +236,11 @@ var CreateStorefront = class CreateStorefront extends Command {
211
236
  char: "t",
212
237
  description: "Template repository to use for the storefront (GitHub URL or local path)"
213
238
  }),
239
+ vertical: Flags.string({
240
+ char: "V",
241
+ description: "Vertical template to generate from. Selects the matching published template repository. Ignored if --template is provided.",
242
+ options: ["fashion", "cosmetic"]
243
+ }),
214
244
  "template-branch": Flags.string({
215
245
  char: "b",
216
246
  description: "Branch or tag to clone from the template repository"
@@ -234,6 +264,7 @@ var CreateStorefront = class CreateStorefront extends Command {
234
264
  await createStorefront({
235
265
  name: flags.name,
236
266
  template: flags.template,
267
+ vertical: flags.vertical,
237
268
  templateBranch: flags["template-branch"],
238
269
  localPackagesDir: flags["local-packages-dir"],
239
270
  defaults: flags.defaults,
@@ -1,5 +1,6 @@
1
1
  import { t as logger } from "./logger.js";
2
2
  import path from "path";
3
+ import { createRequire } from "module";
3
4
  import fs from "fs";
4
5
 
5
6
  //#region src/extensibility/path-util.ts
@@ -18,7 +19,7 @@ const SINGLE_LINE_MARKER = "@sfdc-extension-line";
18
19
  const BLOCK_MARKER_START = "@sfdc-extension-block-start";
19
20
  const BLOCK_MARKER_END = "@sfdc-extension-block-end";
20
21
  const FILE_MARKER = "@sfdc-extension-file";
21
- function trimExtensions(directory, selectedExtensions, extensionConfig) {
22
+ async function trimExtensions(directory, selectedExtensions, extensionConfig) {
22
23
  const startTime = Date.now();
23
24
  const configuredExtensions = extensionConfig?.extensions || {};
24
25
  const extensions = {};
@@ -41,8 +42,8 @@ function trimExtensions(directory, selectedExtensions, extensionConfig) {
41
42
  };
42
43
  processDirectory(directory);
43
44
  if (extensionConfig?.extensions) {
45
+ await updateExtensionConfig(directory, extensions);
44
46
  deleteExtensionFolders(directory, extensions, extensionConfig);
45
- updateExtensionConfig(directory, extensions);
46
47
  }
47
48
  const endTime = Date.now();
48
49
  logger.debug(`Trim extensions took ${endTime - startTime}ms`);
@@ -52,13 +53,64 @@ function trimExtensions(directory, selectedExtensions, extensionConfig) {
52
53
  * @param projectDirectory - The project directory
53
54
  * @param extensionSelections - The selected extensions
54
55
  */
55
- function updateExtensionConfig(projectDirectory, extensionSelections) {
56
+ async function updateExtensionConfig(projectDirectory, extensionSelections) {
56
57
  const extensionConfigPath = path.join(projectDirectory, "src", "extensions", "config.json");
57
58
  const extensionConfig = JSON.parse(fs.readFileSync(extensionConfigPath, "utf8"));
58
59
  Object.keys(extensionConfig.extensions).forEach((extensionKey) => {
59
60
  if (!extensionSelections[extensionKey]) delete extensionConfig.extensions[extensionKey];
60
61
  });
61
- fs.writeFileSync(extensionConfigPath, JSON.stringify({ extensions: extensionConfig.extensions }, null, 4), "utf8");
62
+ const json = JSON.stringify({ extensions: extensionConfig.extensions }, null, 4);
63
+ fs.writeFileSync(extensionConfigPath, await formatWithProjectPrettier(json, extensionConfigPath), "utf8");
64
+ }
65
+ /**
66
+ * Format generated JSON/JS so the written file matches what the project's
67
+ * `prettier --write` / `pnpm lint` would produce.
68
+ *
69
+ * `trimExtensions` runs during `create-storefront`, BEFORE the generated project's
70
+ * `pnpm install` — so the project's own Prettier is usually not on disk yet. We therefore
71
+ * prefer the consumer's Prettier when it happens to be present (re-runs, manage-extensions
72
+ * after install), but fall back to the SDK-bundled `prettier` (a hard dependency of this
73
+ * package, pinned to the template's version) so formatting is deterministic and available
74
+ * at generate time. Relying on the consumer's copy alone resolved only by accident in the
75
+ * monorepo harness, where the generated dir is nested under the monorepo and `createRequire`
76
+ * walks up to the monorepo's Prettier; a customer in a clean directory would get unformatted
77
+ * output and fail lint on first run (W-23074938).
78
+ *
79
+ * NOTE: mirror of the helper in
80
+ * packages/template-retail-rsc-app/scripts/generate-eslint-config.js — kept separate because
81
+ * the two live in different packages/module systems. Keep the parser/config-resolution
82
+ * behavior in sync; the fallback chains INTENTIONALLY differ — this copy has a two-level
83
+ * fallback (consumer Prettier → SDK-bundled `import('prettier')` → unformatted) because it
84
+ * runs pre-install, while the generator copy has a single fallback (consumer Prettier →
85
+ * unformatted). Don't "fix" that asymmetry or you reintroduce the pre-install bug (W-23074938).
86
+ *
87
+ * @param content - The serialized file content to format.
88
+ * @param filePath - The file's path (drives parser selection + config resolution).
89
+ * @returns The Prettier-formatted content. Returns the content unchanged only if no Prettier
90
+ * can be resolved at all; a genuine format/config error throws rather than silently shipping
91
+ * unformatted output.
92
+ */
93
+ async function formatWithProjectPrettier(content, filePath) {
94
+ let prettier;
95
+ try {
96
+ prettier = createRequire(filePath)("prettier");
97
+ } catch {
98
+ try {
99
+ prettier = (await import("prettier")).default;
100
+ } catch {
101
+ logger.warn("⚠️ Prettier could not be resolved; extension config.json will be written unformatted.");
102
+ return content;
103
+ }
104
+ }
105
+ try {
106
+ const config = await prettier.resolveConfig(filePath, { editorconfig: true });
107
+ return await prettier.format(content, {
108
+ ...config,
109
+ filepath: filePath
110
+ });
111
+ } catch (error) {
112
+ throw new Error(`Prettier formatting failed for ${path.basename(filePath)}: ${error.message}`);
113
+ }
62
114
  }
63
115
  /**
64
116
  * Process a file to trim extension-specific code based on markers.
@@ -7,7 +7,7 @@ import { ServerEntryModule } from "react-router";
7
7
  *
8
8
  * - Spreads all app module properties to forward unknown/future exports
9
9
  * - Wraps the default handler for platform-level processing
10
- * - Prepends a platform instrumentation to unstable_instrumentations
10
+ * - Prepends a platform instrumentation to instrumentations
11
11
  */
12
12
  declare function composeServerEntry(appModule: ServerEntryModule): ServerEntryModule;
13
13
  //#endregion
@@ -1,5 +1,5 @@
1
- import { SpanStatusCode, propagation } from "@opentelemetry/api";
2
- import { ATTR_HTTP_REQUEST_METHOD, ATTR_SERVICE_NAME, ATTR_URL_PATH } from "@opentelemetry/semantic-conventions";
1
+ import { SpanStatusCode, context, trace } from "@opentelemetry/api";
2
+ import { ATTR_HTTP_REQUEST_METHOD, ATTR_HTTP_ROUTE, ATTR_SERVICE_NAME, ATTR_URL_PATH } from "@opentelemetry/semantic-conventions";
3
3
  import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
4
4
  import { AlwaysOnSampler, ConsoleSpanExporter, ParentBasedSampler, SimpleSpanProcessor } from "@opentelemetry/sdk-trace-base";
5
5
  import { ExportResultCode, W3CTraceContextPropagator, hrTimeToTimeStamp } from "@opentelemetry/core";
@@ -102,6 +102,43 @@ const logger = {
102
102
 
103
103
  //#endregion
104
104
  //#region src/otel/setup.ts
105
+ /**
106
+ * A privately-held W3C Trace Context propagator, used to inject `traceparent` onto
107
+ * outbound fetches WITHOUT going through OpenTelemetry's global propagator registry.
108
+ *
109
+ * ## Why not the global propagator?
110
+ *
111
+ * `UndiciInstrumentation` injects outbound headers by calling the *global* registered
112
+ * propagator (`propagation.inject()`). Registering one with `setGlobalPropagator()`
113
+ * works locally, but is a silent no-op on Managed Runtime (MRT):
114
+ *
115
+ * - The MRT Lambda wrapper bundles its own copy of `@opentelemetry/api` (declared
116
+ * `^1.9.1`, with core/sdk at 2.x) and boots first, creating the version-keyed
117
+ * global registry object (`globalThis[Symbol.for('opentelemetry.js.api.1')]`,
118
+ * stamped `version: "1.9.1"`).
119
+ * - Our SDK bundle ships a *different* copy of `@opentelemetry/api` (`1.9.0`). When
120
+ * our code calls `setGlobalPropagator()`, OTel's `registerGlobal()` compares the
121
+ * registry's recorded version against ours with an EXACT string match — `"1.9.1"
122
+ * !== "1.9.0"` — refuses the registration, and reports the error only to OTel's
123
+ * internal diag channel (which we do not wire up). The propagation slot stays
124
+ * empty, so undici's `propagation.inject()` injects nothing and no `traceparent`
125
+ * reaches SCAPI. Proven on soak: `propagation.fields() === []`, 0/198 outbound
126
+ * requests carried a header.
127
+ *
128
+ * This is the same multi-instance split that makes inbound `propagation.extract()`
129
+ * fail (see `express/middleware.ts`, which bypasses it with `parseTraceParent` +
130
+ * `trace.setSpanContext`). MRT's own data-plane tracer solves it the same way: it
131
+ * holds a private `new W3CTraceContextPropagator()` and calls `inject()`/`extract()`
132
+ * on it directly, never touching the global registry.
133
+ *
134
+ * Holding our own instance and injecting it ourselves (in the undici `requestHook`)
135
+ * makes outbound propagation deterministic and independent of MRT's bundle, its OTel
136
+ * version, and its boot order. We deliberately do NOT call `setGlobalPropagator()` —
137
+ * leaving the global propagator as the default no-op guarantees undici's own
138
+ * `propagation.inject()` (which runs right after our hook) adds nothing, so the
139
+ * `traceparent` is written exactly once in every environment.
140
+ */
141
+ const outboundPropagator = new W3CTraceContextPropagator();
105
142
  const SERVICE_NAME = "storefront-next";
106
143
  /**
107
144
  * Initializes OpenTelemetry and returns a Tracer from the provider directly.
@@ -129,17 +166,20 @@ function initTelemetry() {
129
166
  sampler: new ParentBasedSampler({ root: new AlwaysOnSampler() })
130
167
  });
131
168
  provider.addSpanProcessor(new SimpleSpanProcessor(new MrtConsoleSpanExporter()));
132
- provider.register();
133
- propagation.setGlobalPropagator(new W3CTraceContextPropagator());
169
+ provider.register({ propagator: null });
134
170
  if (!globalThis[UNDICI_REGISTERED_KEY]) {
135
171
  globalThis[UNDICI_REGISTERED_KEY] = true;
136
172
  registerInstrumentations({
137
173
  tracerProvider: provider,
138
174
  instrumentations: [new UndiciInstrumentation({ requestHook(span, request) {
139
175
  try {
140
- const method = request.method.toUpperCase();
141
- const url = `${request.origin}${request.path}`;
142
- span.updateName(`${method} ${url}`);
176
+ span.updateName("sfnext.fetch");
177
+ } catch {}
178
+ try {
179
+ const ctx = trace.setSpan(context.active(), span);
180
+ outboundPropagator.inject(ctx, request, { set(req, key, value) {
181
+ req.addHeader?.(key, value);
182
+ } });
143
183
  } catch {}
144
184
  } })]
145
185
  });
@@ -189,7 +229,7 @@ async function traced(spanName, attributes, handle) {
189
229
  * HTTP attributes common to all spans.
190
230
  * url.path only — url.full would expose query params which may contain auth
191
231
  * tokens or PII. http.response.status_code is not available from
192
- * unstable_InstrumentationHandlerResult.
232
+ * InstrumentationHandlerResult.
193
233
  */
194
234
  function httpAttributes(request) {
195
235
  const attrs = { [ATTR_HTTP_REQUEST_METHOD]: request.method };
@@ -201,25 +241,25 @@ function httpAttributes(request) {
201
241
  const platformInstrumentation = {
202
242
  handler(handler) {
203
243
  handler.instrument({ async request(handleRequest, { request }) {
204
- await traced("react-router ssr", httpAttributes(request), handleRequest);
244
+ await traced("sfnext.ssr", httpAttributes(request), handleRequest);
205
245
  } });
206
246
  },
207
247
  route(route) {
208
- function routeAttributes(unstable_pattern) {
248
+ function routeAttributes(pattern) {
209
249
  return {
210
250
  "rr.route.id": route.id,
211
- "rr.route.pattern": unstable_pattern
251
+ [ATTR_HTTP_ROUTE]: pattern
212
252
  };
213
253
  }
214
254
  route.instrument({
215
- async loader(handleLoader, { unstable_pattern }) {
216
- await traced(`loader (${route.id})`, routeAttributes(unstable_pattern), handleLoader);
255
+ async loader(handleLoader, { pattern }) {
256
+ await traced("sfnext.loader", routeAttributes(pattern), handleLoader);
217
257
  },
218
- async action(handleAction, { unstable_pattern }) {
219
- await traced(`action (${route.id})`, routeAttributes(unstable_pattern), handleAction);
258
+ async action(handleAction, { pattern }) {
259
+ await traced("sfnext.action", routeAttributes(pattern), handleAction);
220
260
  },
221
- async middleware(handleMiddleware, { unstable_pattern }) {
222
- await traced(`middleware (${route.id})`, routeAttributes(unstable_pattern), handleMiddleware);
261
+ async middleware(handleMiddleware, { pattern }) {
262
+ await traced("sfnext.middleware", routeAttributes(pattern), handleMiddleware);
223
263
  }
224
264
  });
225
265
  }
@@ -232,15 +272,16 @@ const platformInstrumentation = {
232
272
  *
233
273
  * - Spreads all app module properties to forward unknown/future exports
234
274
  * - Wraps the default handler for platform-level processing
235
- * - Prepends a platform instrumentation to unstable_instrumentations
275
+ * - Prepends a platform instrumentation to instrumentations
236
276
  */
237
277
  function composeServerEntry(appModule) {
278
+ if (process.env.NODE_ENV !== "production" && "unstable_instrumentations" in appModule && !("instrumentations" in appModule)) console.warn("[storefront-next] entry.server exports `unstable_instrumentations`, which React Router 7.18 no longer reads. Rename the export to `instrumentations` or it will not register.");
238
279
  return {
239
280
  ...appModule,
240
- default(request, statusCode, headers, context, loadContext) {
241
- return appModule.default(request, statusCode, headers, context, loadContext);
281
+ default(request, statusCode, headers, context$1, loadContext) {
282
+ return appModule.default(request, statusCode, headers, context$1, loadContext);
242
283
  },
243
- unstable_instrumentations: [platformInstrumentation, ...appModule.unstable_instrumentations ?? []]
284
+ instrumentations: [platformInstrumentation, ...appModule.instrumentations ?? []]
244
285
  };
245
286
  }
246
287
 
@@ -1 +1 @@
1
- {"version":3,"file":"server.js","names":["LEVEL_PRIORITY: Record<LogLevel, number>","overrideLevel: LogLevel | undefined","cachedTracer: Tracer | null","tracer: Tracer | null","attrs: Attributes","platformInstrumentation: unstable_ServerInstrumentation"],"sources":["../../src/otel/mrt-console-span-exporter.ts","../../src/utils/logger.ts","../../src/otel/setup.ts","../../src/otel/react-router/instrumentation.ts","../../src/entry/server.ts"],"sourcesContent":["/**\n * Copyright 2026 Salesforce, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * MRT-compatible console span exporter.\n *\n * Extends the default ConsoleSpanExporter to output structured JSON that\n * Managed Runtime's log infrastructure can parse. The default exporter uses\n * `console.dir` with a human-readable format; this override uses\n * `console.info(JSON.stringify(...))` to produce machine-parseable JSON\n * matching the format MRT expects.\n *\n * Inherits `shutdown()` and `forceFlush()` from ConsoleSpanExporter.\n */\n\nimport { ConsoleSpanExporter, type ReadableSpan } from '@opentelemetry/sdk-trace-base';\nimport { ExportResultCode, type ExportResult, hrTimeToTimeStamp } from '@opentelemetry/core';\n\nexport class MrtConsoleSpanExporter extends ConsoleSpanExporter {\n export(spans: ReadableSpan[], resultCallback: (result: ExportResult) => void): void {\n for (const span of spans) {\n try {\n const ctx = span.spanContext();\n const spanData = {\n traceId: ctx.traceId,\n parentId: span.parentSpanId,\n name: span.name,\n id: ctx.spanId,\n kind: span.kind,\n timestamp: hrTimeToTimeStamp(span.startTime),\n duration: span.duration,\n attributes: span.attributes,\n status: span.status,\n events: span.events,\n links: span.links,\n start_time: span.startTime,\n end_time: span.endTime,\n forwardTrace: process.env.SFNEXT_OTEL_ENABLED === 'true',\n };\n // eslint-disable-next-line no-console -- intentional: MRT collects stdout as the telemetry transport\n console.info(JSON.stringify(spanData));\n } catch {\n // Skip malformed spans — never let a serialization failure propagate\n }\n }\n resultCallback({ code: ExportResultCode.SUCCESS });\n }\n}\n","/**\n * Copyright 2026 Salesforce, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n/* eslint-disable no-console */\nimport os from 'os';\nimport chalk from 'chalk';\nimport { createRequire } from 'module';\nimport type { ServerMode } from '../server/modes';\nimport pkg from '../../package.json' with { type: 'json' };\n\n/**\n * Get the local network IPv4 address\n */\nexport function getNetworkAddress(): string | undefined {\n const interfaces = os.networkInterfaces();\n for (const name of Object.keys(interfaces)) {\n const iface = interfaces[name];\n if (!iface) continue;\n for (const alias of iface) {\n if (alias.family === 'IPv4' && !alias.internal) {\n return alias.address;\n }\n }\n }\n return undefined;\n}\n\n/**\n * Get the version of a package from the project's package.json\n */\nexport function getPackageVersion(packageName: string, projectDir: string): string {\n try {\n const require = createRequire(import.meta.url);\n const pkgPath = require.resolve(`${packageName}/package.json`, { paths: [projectDir] });\n const pkgJson = require(pkgPath) as { version: string };\n return pkgJson.version;\n } catch {\n return 'unknown';\n }\n}\n\n/**\n * Centralized, level-gated logger for the SDK.\n *\n * Log level is controlled by `SFCC_LOG_LEVEL` env var (`error` | `warn` | `info` | `debug`).\n * Falls back to: `DEBUG` targeting sfnext -> `debug`, `NODE_ENV=production` -> `warn`, otherwise `info`.\n */\n\nexport type LogLevel = 'error' | 'warn' | 'info' | 'debug';\n\nconst LEVEL_PRIORITY: Record<LogLevel, number> = {\n error: 0,\n warn: 1,\n info: 2,\n debug: 3,\n};\n\nlet overrideLevel: LogLevel | undefined;\n\n/**\n * Returns true when the `DEBUG` env var targets sfnext or is a general enable flag.\n * Avoids accidentally enabling debug mode when DEBUG is set for unrelated libraries\n * (e.g. `DEBUG=express:*`).\n */\nfunction debugEnablesSfnext(): boolean {\n const raw = process.env.DEBUG?.trim();\n if (!raw) return false;\n const normalized = raw.toLowerCase();\n if (['1', 'true', 'yes', 'on'].includes(normalized)) return true;\n return raw.split(',').some((token) => {\n const value = token.trim();\n return value === '*' || value === 'sfnext' || value === 'sfnext:*';\n });\n}\n\nfunction resolveLevel(): LogLevel {\n if (overrideLevel) return overrideLevel;\n const envLevel = process.env.MRT_LOG_LEVEL ?? process.env.SFCC_LOG_LEVEL;\n if (envLevel && envLevel in LEVEL_PRIORITY) return envLevel as LogLevel;\n if (debugEnablesSfnext()) return 'debug';\n if (process.env.NODE_ENV === 'production') return 'warn';\n return 'info';\n}\n\nfunction shouldLog(level: LogLevel): boolean {\n return LEVEL_PRIORITY[level] <= LEVEL_PRIORITY[resolveLevel()];\n}\n\nexport const logger = {\n error(msg: string, ...args: unknown[]): void {\n if (!shouldLog('error')) return;\n console.error(chalk.red('[sfnext:error]'), msg, ...args);\n },\n warn(msg: string, ...args: unknown[]): void {\n if (!shouldLog('warn')) return;\n console.warn(chalk.yellow('[sfnext:warn]'), msg, ...args);\n },\n info(msg: string, ...args: unknown[]): void {\n if (!shouldLog('info')) return;\n console.log(chalk.cyan('[sfnext:info]'), msg, ...args);\n },\n debug(msg: string, ...args: unknown[]): void {\n if (!shouldLog('debug')) return;\n console.log(chalk.gray('[sfnext:debug]'), msg, ...args);\n },\n setLevel(level: LogLevel | undefined): void {\n overrideLevel = level;\n },\n getLevel(): LogLevel {\n return resolveLevel();\n },\n};\n\n/**\n * Print the server information banner with URLs and versions\n */\nexport function printServerInfo(mode: ServerMode, port: number, startTime: number, projectDir: string): void {\n const elapsed = Date.now() - startTime;\n const sfnextVersion = pkg.version;\n const reactVersion = getPackageVersion('react', projectDir);\n const reactRouterVersion = getPackageVersion('react-router', projectDir);\n const viteVersion = getPackageVersion('vite', projectDir);\n\n const modeLabel = mode === 'development' ? 'Development Mode' : 'Preview Mode';\n\n console.log();\n console.log(` ${chalk.cyan.bold('⚡ SFCC Storefront Next')} ${chalk.dim(`v${sfnextVersion}`)}`);\n console.log(` ${chalk.green.bold(modeLabel)}`);\n console.log();\n const logLevel = resolveLevel();\n const logLevelColors: Record<LogLevel, (s: string) => string> = {\n error: chalk.red,\n warn: chalk.yellow,\n info: chalk.cyan,\n debug: chalk.gray,\n };\n\n console.log(\n ` ${chalk.dim('react')} ${chalk.green(`v${reactVersion}`)} ${chalk.dim('|')} ` +\n `${chalk.dim('react-router')} ${chalk.green(`v${reactRouterVersion}`)} ${chalk.dim('|')} ` +\n `${chalk.dim('vite')} ${chalk.green(`v${viteVersion}`)}`\n );\n console.log(\n ` ${chalk.dim('log level')} ${logLevelColors[logLevel](logLevel)} ${chalk.dim('|')} ` +\n `${chalk.green(`ready in ${elapsed}ms`)}`\n );\n console.log();\n}\n\n/**\n * Print server configuration details (proxy, static, etc.)\n */\nexport function printServerConfig(config: {\n mode: ServerMode;\n port: number;\n enableProxy?: boolean;\n enableStaticServing?: boolean;\n enableCompression?: boolean;\n proxyPath?: string;\n proxyHost?: string;\n shortCode?: string;\n organizationId?: string;\n clientId?: string;\n}): void {\n const {\n port,\n enableProxy,\n enableStaticServing,\n enableCompression,\n proxyPath,\n proxyHost,\n shortCode,\n organizationId,\n clientId,\n } = config;\n\n console.log(` ${chalk.bold('Environment Configuration:')}`);\n\n if (enableProxy && proxyPath && proxyHost && shortCode) {\n console.log(\n ` ${chalk.green('✓')} ${chalk.bold('Proxy:')} ${chalk.cyan(`localhost:${port}${proxyPath}`)} ${chalk.dim('→')} ${chalk.cyan(proxyHost)}`\n );\n console.log(` ${chalk.dim('Short Code: ')}${chalk.dim(shortCode)}`);\n if (organizationId) {\n console.log(` ${chalk.dim('Organization ID: ')}${chalk.dim(organizationId)}`);\n }\n if (clientId) {\n console.log(` ${chalk.dim('Client ID: ')}${chalk.dim(clientId)}`);\n }\n } else {\n console.log(` ${chalk.bold('Proxy: ')} ${chalk.dim('disabled')}`);\n }\n\n if (enableStaticServing) {\n console.log(` ${chalk.bold('Static: ')} ${chalk.dim('enabled')}`);\n }\n\n if (enableCompression) {\n console.log(` ${chalk.bold('Compression: ')} ${chalk.dim('enabled')}`);\n }\n\n // URLs\n const localUrl = `http://localhost:${port}`;\n const showNetwork = process.env.SHOW_NETWORK === 'true';\n const networkAddress = showNetwork ? getNetworkAddress() : undefined;\n const networkUrl = networkAddress ? `http://${networkAddress}:${port}` : undefined;\n\n console.log();\n console.log(` ${chalk.bold('Local: ')} ${chalk.cyan(localUrl)}`);\n if (networkUrl) {\n console.log(` ${chalk.bold('Network:')} ${chalk.cyan(networkUrl)}`);\n }\n\n console.log();\n console.log(` ${chalk.dim('Press')} ${chalk.bold('Ctrl+C')} ${chalk.dim('to stop the server')}`);\n console.log();\n}\n\n/**\n * Print shutdown message\n */\nexport function printShutdownMessage(): void {\n console.log(`\\n ${chalk.yellow('⚡')} ${chalk.dim('Server shutting down...')}\\n`);\n}\n","/**\n * Copyright 2026 Salesforce, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * OpenTelemetry tracer provider initialization.\n *\n * ## Exporter design: why ConsoleSpanExporter?\n *\n * This storefront runs in two environments:\n * - Local development on a developer's machine (Vite dev server)\n * - Salesforce Managed Runtime (MRT), a serverless environment backed by AWS Lambda\n *\n * Standard OpenTelemetry exporters (OTLP, Jaeger, Zipkin) are designed for\n * long-running processes: they maintain background agents, batch spans in memory,\n * and flush asynchronously. That model does not work in a serverless environment.\n * Lambda invocations must complete quickly — a background agent or async flush\n * after the response is sent would hold the Lambda open unnecessarily, increasing\n * cost and cold-start latency.\n *\n * Instead, we write spans synchronously to stdout via MrtConsoleSpanExporter — a\n * subclass of ConsoleSpanExporter that outputs structured JSON matching the format\n * MRT's log infrastructure expects. MRT collects stdout from every invocation and\n * forwards the log stream to downstream analytics systems. Telemetry reaches those\n * systems without any in-process agent or network connection from the Lambda\n * function itself.\n *\n * SimpleSpanProcessor is used (instead of BatchSpanProcessor) for the same reason:\n * immediate, synchronous export with no in-memory queue that could be lost if the\n * process exits.\n *\n * Uses NodeTracerProvider directly (instead of NodeSDK) to avoid async resource\n * detection. NodeSDK's autoDetectResources creates a Resource with\n * asyncAttributesPending=true, which causes SimpleSpanProcessor.onEnd() to\n * defer export via waitForAsyncAttributes(). In the Vite SSR module runner\n * context, that deferred microtask never executes. A synchronous Resource\n * ensures the immediate export path is always taken.\n *\n * ## Distributed tracing: W3C Trace Context propagation\n *\n * The storefront runs below Managed Runtime (MRT) in the call tree. MRT is\n * trace-aware: it makes the sampling decision and stamps an inbound W3C\n * `traceparent` header on the request the storefront receives. To appear on the\n * same end-to-end trace as the surrounding services, the storefront behaves as a\n * standard downstream participant:\n * - It continues the inbound trace (see the Express middleware, which extracts\n * `traceparent` into the parent context).\n * - It forwards `traceparent` on outbound fetches (UndiciInstrumentation injects\n * it automatically via the globally-registered propagator below).\n * - It honors MRT's sampling decision via a ParentBasedSampler — it never forces\n * a sampled trace and implements no sampling policy of its own. MRT owns the\n * rate via MRT_DISTRIBUTED_TRACING_ENABLED / MRT_DISTRIBUTED_TRACING_SAMPLING_RATE.\n *\n * @env SFNEXT_OTEL_ENABLED — set to `\"true\"` to enable (e.g. `SFNEXT_OTEL_ENABLED=true pnpm dev`)\n */\n\nimport { propagation, type Tracer } from '@opentelemetry/api';\nimport { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';\nimport { AlwaysOnSampler, ParentBasedSampler, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base';\nimport { W3CTraceContextPropagator } from '@opentelemetry/core';\nimport { MrtConsoleSpanExporter } from './mrt-console-span-exporter';\nimport { Resource } from '@opentelemetry/resources';\nimport { ATTR_SERVICE_NAME } from '@opentelemetry/semantic-conventions';\nimport { registerInstrumentations } from '@opentelemetry/instrumentation';\nimport { UndiciInstrumentation } from '@opentelemetry/instrumentation-undici';\nimport { logger } from '../logger';\n\nexport const SERVICE_NAME = 'storefront-next';\n\n/**\n * Initializes OpenTelemetry and returns a Tracer from the provider directly.\n *\n * Returns the tracer via `provider.getTracer()` instead of the global\n * `trace.getTracer()` API. In the Vite SSR module runner, the built\n * dist/entry/server.js and the externalized @opentelemetry/sdk-trace-node\n * resolve @opentelemetry/api to different module instances (different paths\n * through pnpm's strict node_modules). Each instance has its own\n * ProxyTracerProvider singleton, so `provider.register()` sets the delegate\n * on sdk-trace-node's API instance while our code's `trace.getTracer()`\n * reads from a separate API instance with no delegate — returning a tracer\n * backed by a bare BasicTracerProvider with NoopSpanProcessor.\n *\n * Getting the tracer directly from the provider bypasses the global registry\n * entirely, guaranteeing the tracer uses our configured span processors.\n */\nlet cachedTracer: Tracer | null = null;\n\n// In the Vite SSR module runner, setup.ts may be loaded by two different module\n// instances (the Express server and the SSR bundle) — each with its own\n// `cachedTracer`. UndiciInstrumentation hooks into process-global\n// diagnostics_channel events, so registering it twice causes duplicate spans\n// for every fetch. This process-global flag ensures it is registered only once.\nconst UNDICI_REGISTERED_KEY = Symbol.for('sfnext.otel.undici_registered');\n\nexport function initTelemetry(): Tracer | null {\n if (cachedTracer) return cachedTracer;\n try {\n const provider = new NodeTracerProvider({\n resource: new Resource({ [ATTR_SERVICE_NAME]: SERVICE_NAME }),\n // Honor the upstream (MRT) sampling decision rather than implementing our\n // own. ParentBasedSampler: when an inbound traceparent is present, defer\n // to its sampled flag (continue + export iff sampled); when there is no\n // inbound parent, sample the fresh root ourselves (AlwaysOnSampler). This\n // is exactly the standard \"downstream participant\" behavior — MRT owns the\n // rate via MRT_DISTRIBUTED_TRACING_* env vars.\n sampler: new ParentBasedSampler({ root: new AlwaysOnSampler() }),\n });\n\n provider.addSpanProcessor(new SimpleSpanProcessor(new MrtConsoleSpanExporter()));\n provider.register();\n\n // Register the standard W3C Trace Context propagator. This single\n // registration drives propagation in both directions:\n // - inbound: the Express middleware calls propagation.extract() to continue\n // the trace described by the incoming `traceparent`.\n // - outbound: UndiciInstrumentation calls propagation.inject() to add\n // `traceparent` to every outgoing fetch (SCAPI, etc.).\n // Registration lives here, inside the SFNEXT_OTEL_ENABLED-gated init path, so\n // that when tracing is disabled no propagator is installed and no trace\n // headers are ever added.\n propagation.setGlobalPropagator(new W3CTraceContextPropagator());\n\n // Guard against double-registration across Vite module boundaries.\n // See comment above UNDICI_REGISTERED_KEY.\n if (!(globalThis as Record<symbol, boolean>)[UNDICI_REGISTERED_KEY]) {\n (globalThis as Record<symbol, boolean>)[UNDICI_REGISTERED_KEY] = true;\n registerInstrumentations({\n tracerProvider: provider,\n instrumentations: [\n new UndiciInstrumentation({\n requestHook(span, request) {\n try {\n const method = request.method.toUpperCase();\n const url = `${request.origin}${request.path}`;\n span.updateName(`${method} ${url}`);\n } catch {\n // Non-fatal — default span name is acceptable\n }\n },\n }),\n ],\n });\n }\n\n cachedTracer = provider.getTracer(SERVICE_NAME);\n return cachedTracer;\n } catch (error) {\n logger.error('[otel] Failed to initialize OpenTelemetry:', error);\n return null;\n }\n}\n","/**\n * Copyright 2026 Salesforce, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Platform-level handler instrumentation.\n *\n * Uses React Router's unstable_ServerInstrumentation API to observe the\n * request lifecycle at the handler and route levels. This runs around ALL\n * requests (document + data) and provides OpenTelemetry spans for:\n * - `request` — root span per incoming HTTP request\n * - `loader` — child span per route loader\n * - `action` — child span per route action\n * - `middleware` — child span per route middleware\n *\n * `tracer` is `null` when OTel is disabled. Each handler checks explicitly\n * and calls through immediately, so the disabled path is always obvious.\n *\n * @env SFNEXT_OTEL_ENABLED — set to `\"true\"` to enable OpenTelemetry tracing.\n * Spans are written synchronously to stdout via ConsoleSpanExporter, which is\n * compatible with both local development and the Managed Runtime serverless\n * environment (see otel/setup.ts for the full design rationale).\n * Example: `SFNEXT_OTEL_ENABLED=true pnpm dev`\n *\n * @see https://reactrouter.com/how-to/instrumentation\n */\n\nimport { type Tracer, type Attributes, SpanStatusCode } from '@opentelemetry/api';\nimport type { unstable_ServerInstrumentation } from 'react-router';\nimport { ATTR_HTTP_REQUEST_METHOD, ATTR_URL_PATH } from '@opentelemetry/semantic-conventions';\nimport { initTelemetry } from '../setup';\n\nconst tracer: Tracer | null = process.env.SFNEXT_OTEL_ENABLED === 'true' ? initTelemetry() : null;\n\n/**\n * Runs `handle` inside an active OTel span, recording errors and ending the span.\n * When `tracer` is null (OTel disabled), calls `handle` directly with no overhead.\n */\nasync function traced(\n spanName: string,\n attributes: Attributes,\n handle: () => Promise<{ status: string; error?: Error }>\n): Promise<void> {\n if (!tracer) {\n await handle();\n return;\n }\n let handled = false;\n try {\n await tracer.startActiveSpan(spanName, { attributes }, async (span) => {\n try {\n handled = true;\n const result = await handle();\n if (result.status === 'error' && result.error) {\n span.setStatus({ code: SpanStatusCode.ERROR, message: result.error.message });\n span.recordException(result.error);\n }\n } finally {\n span.end();\n }\n });\n } catch {\n // OTel failure must never break the request pipeline.\n // If handle() was already called inside the span callback, don't call it again.\n if (!handled) await handle();\n }\n}\n\n/**\n * HTTP attributes common to all spans.\n * url.path only — url.full would expose query params which may contain auth\n * tokens or PII. http.response.status_code is not available from\n * unstable_InstrumentationHandlerResult.\n */\nfunction httpAttributes(request: { method: string; url: string }): Attributes {\n const attrs: Attributes = { [ATTR_HTTP_REQUEST_METHOD]: request.method };\n try {\n attrs[ATTR_URL_PATH] = new URL(request.url).pathname;\n } catch {\n // Malformed URL — skip url.path rather than throwing\n }\n return attrs;\n}\n\nexport const platformInstrumentation: unstable_ServerInstrumentation = {\n handler(handler) {\n handler.instrument({\n async request(handleRequest, { request }) {\n await traced('react-router ssr', httpAttributes(request), handleRequest);\n },\n });\n },\n route(route) {\n // HTTP attributes (method, url.path) are intentionally omitted here.\n // These spans are children of the request span which already carries\n // them, and rr.route.id / rr.route.pattern are the meaningful\n // identifiers at the route level.\n function routeAttributes(unstable_pattern: string): Attributes {\n return {\n 'rr.route.id': route.id,\n 'rr.route.pattern': unstable_pattern,\n };\n }\n\n route.instrument({\n async loader(handleLoader, { unstable_pattern }) {\n await traced(`loader (${route.id})`, routeAttributes(unstable_pattern), handleLoader);\n },\n async action(handleAction, { unstable_pattern }) {\n await traced(`action (${route.id})`, routeAttributes(unstable_pattern), handleAction);\n },\n async middleware(handleMiddleware, { unstable_pattern }) {\n await traced(`middleware (${route.id})`, routeAttributes(unstable_pattern), handleMiddleware);\n },\n });\n },\n};\n","/**\n * Copyright 2026 Salesforce, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Server entry composition function.\n *\n * Wraps the full ServerEntryModule (the customer's or SDK default entry.server)\n * with platform-level features. The Vite plugin (platformEntryPlugin) generates\n * a virtual module that imports this function and calls it with the app's entry\n * module. New platform features ship via `npm update` without changes to the\n * plugin or customer code.\n *\n * Current platform behaviors:\n * - OpenTelemetry instrumentation for request lifecycle tracing\n *\n * Future additions:\n * - loadContext enrichment (correlation IDs, platform metadata)\n * - Response header injection (tracing, cache directives)\n * - Default error handling with platform error reporting\n */\n\nimport type { ServerEntryModule } from 'react-router';\nimport { platformInstrumentation } from '../otel/react-router/instrumentation';\n\n/**\n * Composes a server entry module with platform-level features.\n *\n * - Spreads all app module properties to forward unknown/future exports\n * - Wraps the default handler for platform-level processing\n * - Prepends a platform instrumentation to unstable_instrumentations\n */\nexport function composeServerEntry(appModule: ServerEntryModule): ServerEntryModule {\n return {\n // Spread all properties first so unknown/future exports pass through\n ...appModule,\n // Override the exports the platform layer enhances\n default(request, statusCode, headers, context, loadContext) {\n return appModule.default(request, statusCode, headers, context, loadContext);\n },\n unstable_instrumentations: [platformInstrumentation, ...(appModule.unstable_instrumentations ?? [])],\n };\n}\n"],"mappings":";;;;;;;;;;;AA+BA,IAAa,yBAAb,cAA4C,oBAAoB;CAC5D,OAAO,OAAuB,gBAAsD;AAChF,OAAK,MAAM,QAAQ,MACf,KAAI;GACA,MAAM,MAAM,KAAK,aAAa;GAC9B,MAAM,WAAW;IACb,SAAS,IAAI;IACb,UAAU,KAAK;IACf,MAAM,KAAK;IACX,IAAI,IAAI;IACR,MAAM,KAAK;IACX,WAAW,kBAAkB,KAAK,UAAU;IAC5C,UAAU,KAAK;IACf,YAAY,KAAK;IACjB,QAAQ,KAAK;IACb,QAAQ,KAAK;IACb,OAAO,KAAK;IACZ,YAAY,KAAK;IACjB,UAAU,KAAK;IACf,cAAc,QAAQ,IAAI,wBAAwB;IACrD;AAED,WAAQ,KAAK,KAAK,UAAU,SAAS,CAAC;UAClC;AAIZ,iBAAe,EAAE,MAAM,iBAAiB,SAAS,CAAC;;;;;;ACI1D,MAAMA,iBAA2C;CAC7C,OAAO;CACP,MAAM;CACN,MAAM;CACN,OAAO;CACV;AAED,IAAIC;;;;;;AAOJ,SAAS,qBAA8B;CACnC,MAAM,MAAM,QAAQ,IAAI,OAAO,MAAM;AACrC,KAAI,CAAC,IAAK,QAAO;CACjB,MAAM,aAAa,IAAI,aAAa;AACpC,KAAI;EAAC;EAAK;EAAQ;EAAO;EAAK,CAAC,SAAS,WAAW,CAAE,QAAO;AAC5D,QAAO,IAAI,MAAM,IAAI,CAAC,MAAM,UAAU;EAClC,MAAM,QAAQ,MAAM,MAAM;AAC1B,SAAO,UAAU,OAAO,UAAU,YAAY,UAAU;GAC1D;;AAGN,SAAS,eAAyB;AAC9B,KAAI,cAAe,QAAO;CAC1B,MAAM,WAAW,QAAQ,IAAI,iBAAiB,QAAQ,IAAI;AAC1D,KAAI,YAAY,YAAY,eAAgB,QAAO;AACnD,KAAI,oBAAoB,CAAE,QAAO;AACjC,KAAI,QAAQ,IAAI,aAAa,aAAc,QAAO;AAClD,QAAO;;AAGX,SAAS,UAAU,OAA0B;AACzC,QAAO,eAAe,UAAU,eAAe,cAAc;;AAGjE,MAAa,SAAS;CAClB,MAAM,KAAa,GAAG,MAAuB;AACzC,MAAI,CAAC,UAAU,QAAQ,CAAE;AACzB,UAAQ,MAAM,MAAM,IAAI,iBAAiB,EAAE,KAAK,GAAG,KAAK;;CAE5D,KAAK,KAAa,GAAG,MAAuB;AACxC,MAAI,CAAC,UAAU,OAAO,CAAE;AACxB,UAAQ,KAAK,MAAM,OAAO,gBAAgB,EAAE,KAAK,GAAG,KAAK;;CAE7D,KAAK,KAAa,GAAG,MAAuB;AACxC,MAAI,CAAC,UAAU,OAAO,CAAE;AACxB,UAAQ,IAAI,MAAM,KAAK,gBAAgB,EAAE,KAAK,GAAG,KAAK;;CAE1D,MAAM,KAAa,GAAG,MAAuB;AACzC,MAAI,CAAC,UAAU,QAAQ,CAAE;AACzB,UAAQ,IAAI,MAAM,KAAK,iBAAiB,EAAE,KAAK,GAAG,KAAK;;CAE3D,SAAS,OAAmC;AACxC,kBAAgB;;CAEpB,WAAqB;AACjB,SAAO,cAAc;;CAE5B;;;;AC5CD,MAAa,eAAe;;;;;;;;;;;;;;;;;AAkB5B,IAAIC,eAA8B;AAOlC,MAAM,wBAAwB,OAAO,IAAI,gCAAgC;AAEzE,SAAgB,gBAA+B;AAC3C,KAAI,aAAc,QAAO;AACzB,KAAI;EACA,MAAM,WAAW,IAAI,mBAAmB;GACpC,UAAU,IAAI,SAAS,GAAG,oBAAoB,cAAc,CAAC;GAO7D,SAAS,IAAI,mBAAmB,EAAE,MAAM,IAAI,iBAAiB,EAAE,CAAC;GACnE,CAAC;AAEF,WAAS,iBAAiB,IAAI,oBAAoB,IAAI,wBAAwB,CAAC,CAAC;AAChF,WAAS,UAAU;AAWnB,cAAY,oBAAoB,IAAI,2BAA2B,CAAC;AAIhE,MAAI,CAAE,WAAuC,wBAAwB;AACjE,GAAC,WAAuC,yBAAyB;AACjE,4BAAyB;IACrB,gBAAgB;IAChB,kBAAkB,CACd,IAAI,sBAAsB,EACtB,YAAY,MAAM,SAAS;AACvB,SAAI;MACA,MAAM,SAAS,QAAQ,OAAO,aAAa;MAC3C,MAAM,MAAM,GAAG,QAAQ,SAAS,QAAQ;AACxC,WAAK,WAAW,GAAG,OAAO,GAAG,MAAM;aAC/B;OAIf,CAAC,CACL;IACJ,CAAC;;AAGN,iBAAe,SAAS,UAAU,aAAa;AAC/C,SAAO;UACF,OAAO;AACZ,SAAO,MAAM,8CAA8C,MAAM;AACjE,SAAO;;;;;;ACpHf,MAAMC,SAAwB,QAAQ,IAAI,wBAAwB,SAAS,eAAe,GAAG;;;;;AAM7F,eAAe,OACX,UACA,YACA,QACa;AACb,KAAI,CAAC,QAAQ;AACT,QAAM,QAAQ;AACd;;CAEJ,IAAI,UAAU;AACd,KAAI;AACA,QAAM,OAAO,gBAAgB,UAAU,EAAE,YAAY,EAAE,OAAO,SAAS;AACnE,OAAI;AACA,cAAU;IACV,MAAM,SAAS,MAAM,QAAQ;AAC7B,QAAI,OAAO,WAAW,WAAW,OAAO,OAAO;AAC3C,UAAK,UAAU;MAAE,MAAM,eAAe;MAAO,SAAS,OAAO,MAAM;MAAS,CAAC;AAC7E,UAAK,gBAAgB,OAAO,MAAM;;aAEhC;AACN,SAAK,KAAK;;IAEhB;SACE;AAGJ,MAAI,CAAC,QAAS,OAAM,QAAQ;;;;;;;;;AAUpC,SAAS,eAAe,SAAsD;CAC1E,MAAMC,QAAoB,GAAG,2BAA2B,QAAQ,QAAQ;AACxE,KAAI;AACA,QAAM,iBAAiB,IAAI,IAAI,QAAQ,IAAI,CAAC;SACxC;AAGR,QAAO;;AAGX,MAAaC,0BAA0D;CACnE,QAAQ,SAAS;AACb,UAAQ,WAAW,EACf,MAAM,QAAQ,eAAe,EAAE,WAAW;AACtC,SAAM,OAAO,oBAAoB,eAAe,QAAQ,EAAE,cAAc;KAE/E,CAAC;;CAEN,MAAM,OAAO;EAKT,SAAS,gBAAgB,kBAAsC;AAC3D,UAAO;IACH,eAAe,MAAM;IACrB,oBAAoB;IACvB;;AAGL,QAAM,WAAW;GACb,MAAM,OAAO,cAAc,EAAE,oBAAoB;AAC7C,UAAM,OAAO,WAAW,MAAM,GAAG,IAAI,gBAAgB,iBAAiB,EAAE,aAAa;;GAEzF,MAAM,OAAO,cAAc,EAAE,oBAAoB;AAC7C,UAAM,OAAO,WAAW,MAAM,GAAG,IAAI,gBAAgB,iBAAiB,EAAE,aAAa;;GAEzF,MAAM,WAAW,kBAAkB,EAAE,oBAAoB;AACrD,UAAM,OAAO,eAAe,MAAM,GAAG,IAAI,gBAAgB,iBAAiB,EAAE,iBAAiB;;GAEpG,CAAC;;CAET;;;;;;;;;;;ACpFD,SAAgB,mBAAmB,WAAiD;AAChF,QAAO;EAEH,GAAG;EAEH,QAAQ,SAAS,YAAY,SAAS,SAAS,aAAa;AACxD,UAAO,UAAU,QAAQ,SAAS,YAAY,SAAS,SAAS,YAAY;;EAEhF,2BAA2B,CAAC,yBAAyB,GAAI,UAAU,6BAA6B,EAAE,CAAE;EACvG"}
1
+ {"version":3,"file":"server.js","names":["LEVEL_PRIORITY: Record<LogLevel, number>","overrideLevel: LogLevel | undefined","cachedTracer: Tracer | null","tracer: Tracer | null","attrs: Attributes","platformInstrumentation: ServerInstrumentation","context"],"sources":["../../src/otel/mrt-console-span-exporter.ts","../../src/utils/logger.ts","../../src/otel/setup.ts","../../src/otel/react-router/instrumentation.ts","../../src/entry/server.ts"],"sourcesContent":["/**\n * Copyright 2026 Salesforce, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * MRT-compatible console span exporter.\n *\n * Extends the default ConsoleSpanExporter to output structured JSON that\n * Managed Runtime's log infrastructure can parse. The default exporter uses\n * `console.dir` with a human-readable format; this override uses\n * `console.info(JSON.stringify(...))` to produce machine-parseable JSON\n * matching the format MRT expects.\n *\n * Inherits `shutdown()` and `forceFlush()` from ConsoleSpanExporter.\n */\n\nimport { ConsoleSpanExporter, type ReadableSpan } from '@opentelemetry/sdk-trace-base';\nimport { ExportResultCode, type ExportResult, hrTimeToTimeStamp } from '@opentelemetry/core';\n\nexport class MrtConsoleSpanExporter extends ConsoleSpanExporter {\n export(spans: ReadableSpan[], resultCallback: (result: ExportResult) => void): void {\n for (const span of spans) {\n try {\n const ctx = span.spanContext();\n const spanData = {\n traceId: ctx.traceId,\n parentId: span.parentSpanId,\n name: span.name,\n id: ctx.spanId,\n kind: span.kind,\n timestamp: hrTimeToTimeStamp(span.startTime),\n duration: span.duration,\n attributes: span.attributes,\n status: span.status,\n events: span.events,\n links: span.links,\n start_time: span.startTime,\n end_time: span.endTime,\n forwardTrace: process.env.SFNEXT_OTEL_ENABLED === 'true',\n };\n // eslint-disable-next-line no-console -- intentional: MRT collects stdout as the telemetry transport\n console.info(JSON.stringify(spanData));\n } catch {\n // Skip malformed spans — never let a serialization failure propagate\n }\n }\n resultCallback({ code: ExportResultCode.SUCCESS });\n }\n}\n","/**\n * Copyright 2026 Salesforce, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n/* eslint-disable no-console */\nimport os from 'os';\nimport chalk from 'chalk';\nimport { createRequire } from 'module';\nimport type { ServerMode } from '../server/modes';\nimport pkg from '../../package.json' with { type: 'json' };\n\n/**\n * Get the local network IPv4 address\n */\nexport function getNetworkAddress(): string | undefined {\n const interfaces = os.networkInterfaces();\n for (const name of Object.keys(interfaces)) {\n const iface = interfaces[name];\n if (!iface) continue;\n for (const alias of iface) {\n if (alias.family === 'IPv4' && !alias.internal) {\n return alias.address;\n }\n }\n }\n return undefined;\n}\n\n/**\n * Get the version of a package from the project's package.json\n */\nexport function getPackageVersion(packageName: string, projectDir: string): string {\n try {\n const require = createRequire(import.meta.url);\n const pkgPath = require.resolve(`${packageName}/package.json`, { paths: [projectDir] });\n const pkgJson = require(pkgPath) as { version: string };\n return pkgJson.version;\n } catch {\n return 'unknown';\n }\n}\n\n/**\n * Centralized, level-gated logger for the SDK.\n *\n * Log level is controlled by `SFCC_LOG_LEVEL` env var (`error` | `warn` | `info` | `debug`).\n * Falls back to: `DEBUG` targeting sfnext -> `debug`, `NODE_ENV=production` -> `warn`, otherwise `info`.\n */\n\nexport type LogLevel = 'error' | 'warn' | 'info' | 'debug';\n\nconst LEVEL_PRIORITY: Record<LogLevel, number> = {\n error: 0,\n warn: 1,\n info: 2,\n debug: 3,\n};\n\nlet overrideLevel: LogLevel | undefined;\n\n/**\n * Returns true when the `DEBUG` env var targets sfnext or is a general enable flag.\n * Avoids accidentally enabling debug mode when DEBUG is set for unrelated libraries\n * (e.g. `DEBUG=express:*`).\n */\nfunction debugEnablesSfnext(): boolean {\n const raw = process.env.DEBUG?.trim();\n if (!raw) return false;\n const normalized = raw.toLowerCase();\n if (['1', 'true', 'yes', 'on'].includes(normalized)) return true;\n return raw.split(',').some((token) => {\n const value = token.trim();\n return value === '*' || value === 'sfnext' || value === 'sfnext:*';\n });\n}\n\nfunction resolveLevel(): LogLevel {\n if (overrideLevel) return overrideLevel;\n const envLevel = process.env.MRT_LOG_LEVEL ?? process.env.SFCC_LOG_LEVEL;\n if (envLevel && envLevel in LEVEL_PRIORITY) return envLevel as LogLevel;\n if (debugEnablesSfnext()) return 'debug';\n if (process.env.NODE_ENV === 'production') return 'warn';\n return 'info';\n}\n\nfunction shouldLog(level: LogLevel): boolean {\n return LEVEL_PRIORITY[level] <= LEVEL_PRIORITY[resolveLevel()];\n}\n\nexport const logger = {\n error(msg: string, ...args: unknown[]): void {\n if (!shouldLog('error')) return;\n console.error(chalk.red('[sfnext:error]'), msg, ...args);\n },\n warn(msg: string, ...args: unknown[]): void {\n if (!shouldLog('warn')) return;\n console.warn(chalk.yellow('[sfnext:warn]'), msg, ...args);\n },\n info(msg: string, ...args: unknown[]): void {\n if (!shouldLog('info')) return;\n console.log(chalk.cyan('[sfnext:info]'), msg, ...args);\n },\n debug(msg: string, ...args: unknown[]): void {\n if (!shouldLog('debug')) return;\n console.log(chalk.gray('[sfnext:debug]'), msg, ...args);\n },\n setLevel(level: LogLevel | undefined): void {\n overrideLevel = level;\n },\n getLevel(): LogLevel {\n return resolveLevel();\n },\n};\n\n/**\n * Print the server information banner with URLs and versions\n */\nexport function printServerInfo(mode: ServerMode, port: number, startTime: number, projectDir: string): void {\n const elapsed = Date.now() - startTime;\n const sfnextVersion = pkg.version;\n const reactVersion = getPackageVersion('react', projectDir);\n const reactRouterVersion = getPackageVersion('react-router', projectDir);\n const viteVersion = getPackageVersion('vite', projectDir);\n\n const modeLabel = mode === 'development' ? 'Development Mode' : 'Preview Mode';\n\n console.log();\n console.log(` ${chalk.cyan.bold('⚡ SFCC Storefront Next')} ${chalk.dim(`v${sfnextVersion}`)}`);\n console.log(` ${chalk.green.bold(modeLabel)}`);\n console.log();\n const logLevel = resolveLevel();\n const logLevelColors: Record<LogLevel, (s: string) => string> = {\n error: chalk.red,\n warn: chalk.yellow,\n info: chalk.cyan,\n debug: chalk.gray,\n };\n\n console.log(\n ` ${chalk.dim('react')} ${chalk.green(`v${reactVersion}`)} ${chalk.dim('|')} ` +\n `${chalk.dim('react-router')} ${chalk.green(`v${reactRouterVersion}`)} ${chalk.dim('|')} ` +\n `${chalk.dim('vite')} ${chalk.green(`v${viteVersion}`)}`\n );\n console.log(\n ` ${chalk.dim('log level')} ${logLevelColors[logLevel](logLevel)} ${chalk.dim('|')} ` +\n `${chalk.green(`ready in ${elapsed}ms`)}`\n );\n console.log();\n}\n\n/**\n * Print server configuration details (proxy, static, etc.)\n */\nexport function printServerConfig(config: {\n mode: ServerMode;\n port: number;\n enableProxy?: boolean;\n enableStaticServing?: boolean;\n enableCompression?: boolean;\n proxyPath?: string;\n proxyHost?: string;\n shortCode?: string;\n organizationId?: string;\n clientId?: string;\n}): void {\n const {\n port,\n enableProxy,\n enableStaticServing,\n enableCompression,\n proxyPath,\n proxyHost,\n shortCode,\n organizationId,\n clientId,\n } = config;\n\n console.log(` ${chalk.bold('Environment Configuration:')}`);\n\n if (enableProxy && proxyPath && proxyHost && shortCode) {\n console.log(\n ` ${chalk.green('✓')} ${chalk.bold('Proxy:')} ${chalk.cyan(`localhost:${port}${proxyPath}`)} ${chalk.dim('→')} ${chalk.cyan(proxyHost)}`\n );\n console.log(` ${chalk.dim('Short Code: ')}${chalk.dim(shortCode)}`);\n if (organizationId) {\n console.log(` ${chalk.dim('Organization ID: ')}${chalk.dim(organizationId)}`);\n }\n if (clientId) {\n console.log(` ${chalk.dim('Client ID: ')}${chalk.dim(clientId)}`);\n }\n } else {\n console.log(` ${chalk.bold('Proxy: ')} ${chalk.dim('disabled')}`);\n }\n\n if (enableStaticServing) {\n console.log(` ${chalk.bold('Static: ')} ${chalk.dim('enabled')}`);\n }\n\n if (enableCompression) {\n console.log(` ${chalk.bold('Compression: ')} ${chalk.dim('enabled')}`);\n }\n\n // URLs\n const localUrl = `http://localhost:${port}`;\n const showNetwork = process.env.SHOW_NETWORK === 'true';\n const networkAddress = showNetwork ? getNetworkAddress() : undefined;\n const networkUrl = networkAddress ? `http://${networkAddress}:${port}` : undefined;\n\n console.log();\n console.log(` ${chalk.bold('Local: ')} ${chalk.cyan(localUrl)}`);\n if (networkUrl) {\n console.log(` ${chalk.bold('Network:')} ${chalk.cyan(networkUrl)}`);\n }\n\n console.log();\n console.log(` ${chalk.dim('Press')} ${chalk.bold('Ctrl+C')} ${chalk.dim('to stop the server')}`);\n console.log();\n}\n\n/**\n * Print shutdown message\n */\nexport function printShutdownMessage(): void {\n console.log(`\\n ${chalk.yellow('⚡')} ${chalk.dim('Server shutting down...')}\\n`);\n}\n","/**\n * Copyright 2026 Salesforce, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * OpenTelemetry tracer provider initialization.\n *\n * ## Exporter design: why ConsoleSpanExporter?\n *\n * This storefront runs in two environments:\n * - Local development on a developer's machine (Vite dev server)\n * - Salesforce Managed Runtime (MRT), a serverless environment backed by AWS Lambda\n *\n * Standard OpenTelemetry exporters (OTLP, Jaeger, Zipkin) are designed for\n * long-running processes: they maintain background agents, batch spans in memory,\n * and flush asynchronously. That model does not work in a serverless environment.\n * Lambda invocations must complete quickly — a background agent or async flush\n * after the response is sent would hold the Lambda open unnecessarily, increasing\n * cost and cold-start latency.\n *\n * Instead, we write spans synchronously to stdout via MrtConsoleSpanExporter — a\n * subclass of ConsoleSpanExporter that outputs structured JSON matching the format\n * MRT's log infrastructure expects. MRT collects stdout from every invocation and\n * forwards the log stream to downstream analytics systems. Telemetry reaches those\n * systems without any in-process agent or network connection from the Lambda\n * function itself.\n *\n * SimpleSpanProcessor is used (instead of BatchSpanProcessor) for the same reason:\n * immediate, synchronous export with no in-memory queue that could be lost if the\n * process exits.\n *\n * Uses NodeTracerProvider directly (instead of NodeSDK) to avoid async resource\n * detection. NodeSDK's autoDetectResources creates a Resource with\n * asyncAttributesPending=true, which causes SimpleSpanProcessor.onEnd() to\n * defer export via waitForAsyncAttributes(). In the Vite SSR module runner\n * context, that deferred microtask never executes. A synchronous Resource\n * ensures the immediate export path is always taken.\n *\n * ## Distributed tracing: W3C Trace Context propagation\n *\n * The storefront runs below Managed Runtime (MRT) in the call tree. MRT is\n * trace-aware: it makes the sampling decision and stamps an inbound W3C\n * `traceparent` header on the request the storefront receives. To appear on the\n * same end-to-end trace as the surrounding services, the storefront behaves as a\n * standard downstream participant:\n * - It continues the inbound trace (see the Express middleware, which parses the\n * inbound `traceparent` into the parent context).\n * - It forwards `traceparent` on outbound fetches (the UndiciInstrumentation\n * `requestHook` below injects it via a privately-held W3C propagator — see the\n * `outboundPropagator` doc comment for why the global registry is bypassed).\n * - It honors MRT's sampling decision via a ParentBasedSampler — it never forces\n * a sampled trace and implements no sampling policy of its own. MRT owns the\n * rate via MRT_DISTRIBUTED_TRACING_ENABLED / MRT_DISTRIBUTED_TRACING_SAMPLING_RATE.\n *\n * @env SFNEXT_OTEL_ENABLED — set to `\"true\"` to enable (e.g. `SFNEXT_OTEL_ENABLED=true pnpm dev`)\n */\n\nimport { context, trace, type Tracer } from '@opentelemetry/api';\nimport { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';\nimport { AlwaysOnSampler, ParentBasedSampler, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base';\nimport { W3CTraceContextPropagator } from '@opentelemetry/core';\nimport { MrtConsoleSpanExporter } from './mrt-console-span-exporter';\nimport { Resource } from '@opentelemetry/resources';\nimport { ATTR_SERVICE_NAME } from '@opentelemetry/semantic-conventions';\nimport { registerInstrumentations } from '@opentelemetry/instrumentation';\nimport { UndiciInstrumentation, type UndiciRequest } from '@opentelemetry/instrumentation-undici';\nimport { logger } from '../logger';\n\n/**\n * A privately-held W3C Trace Context propagator, used to inject `traceparent` onto\n * outbound fetches WITHOUT going through OpenTelemetry's global propagator registry.\n *\n * ## Why not the global propagator?\n *\n * `UndiciInstrumentation` injects outbound headers by calling the *global* registered\n * propagator (`propagation.inject()`). Registering one with `setGlobalPropagator()`\n * works locally, but is a silent no-op on Managed Runtime (MRT):\n *\n * - The MRT Lambda wrapper bundles its own copy of `@opentelemetry/api` (declared\n * `^1.9.1`, with core/sdk at 2.x) and boots first, creating the version-keyed\n * global registry object (`globalThis[Symbol.for('opentelemetry.js.api.1')]`,\n * stamped `version: \"1.9.1\"`).\n * - Our SDK bundle ships a *different* copy of `@opentelemetry/api` (`1.9.0`). When\n * our code calls `setGlobalPropagator()`, OTel's `registerGlobal()` compares the\n * registry's recorded version against ours with an EXACT string match — `\"1.9.1\"\n * !== \"1.9.0\"` — refuses the registration, and reports the error only to OTel's\n * internal diag channel (which we do not wire up). The propagation slot stays\n * empty, so undici's `propagation.inject()` injects nothing and no `traceparent`\n * reaches SCAPI. Proven on soak: `propagation.fields() === []`, 0/198 outbound\n * requests carried a header.\n *\n * This is the same multi-instance split that makes inbound `propagation.extract()`\n * fail (see `express/middleware.ts`, which bypasses it with `parseTraceParent` +\n * `trace.setSpanContext`). MRT's own data-plane tracer solves it the same way: it\n * holds a private `new W3CTraceContextPropagator()` and calls `inject()`/`extract()`\n * on it directly, never touching the global registry.\n *\n * Holding our own instance and injecting it ourselves (in the undici `requestHook`)\n * makes outbound propagation deterministic and independent of MRT's bundle, its OTel\n * version, and its boot order. We deliberately do NOT call `setGlobalPropagator()` —\n * leaving the global propagator as the default no-op guarantees undici's own\n * `propagation.inject()` (which runs right after our hook) adds nothing, so the\n * `traceparent` is written exactly once in every environment.\n */\nconst outboundPropagator = new W3CTraceContextPropagator();\n\nexport const SERVICE_NAME = 'storefront-next';\n\n/**\n * Initializes OpenTelemetry and returns a Tracer from the provider directly.\n *\n * Returns the tracer via `provider.getTracer()` instead of the global\n * `trace.getTracer()` API. In the Vite SSR module runner, the built\n * dist/entry/server.js and the externalized @opentelemetry/sdk-trace-node\n * resolve @opentelemetry/api to different module instances (different paths\n * through pnpm's strict node_modules). Each instance has its own\n * ProxyTracerProvider singleton, so `provider.register()` sets the delegate\n * on sdk-trace-node's API instance while our code's `trace.getTracer()`\n * reads from a separate API instance with no delegate — returning a tracer\n * backed by a bare BasicTracerProvider with NoopSpanProcessor.\n *\n * Getting the tracer directly from the provider bypasses the global registry\n * entirely, guaranteeing the tracer uses our configured span processors.\n */\nlet cachedTracer: Tracer | null = null;\n\n// In the Vite SSR module runner, setup.ts may be loaded by two different module\n// instances (the Express server and the SSR bundle) — each with its own\n// `cachedTracer`. UndiciInstrumentation hooks into process-global\n// diagnostics_channel events, so registering it twice causes duplicate spans\n// for every fetch. This process-global flag ensures it is registered only once.\nconst UNDICI_REGISTERED_KEY = Symbol.for('sfnext.otel.undici_registered');\n\nexport function initTelemetry(): Tracer | null {\n if (cachedTracer) return cachedTracer;\n try {\n const provider = new NodeTracerProvider({\n resource: new Resource({ [ATTR_SERVICE_NAME]: SERVICE_NAME }),\n // Honor the upstream (MRT) sampling decision rather than implementing our\n // own. ParentBasedSampler: when an inbound traceparent is present, defer\n // to its sampled flag (continue + export iff sampled); when there is no\n // inbound parent, sample the fresh root ourselves (AlwaysOnSampler). This\n // is exactly the standard \"downstream participant\" behavior — MRT owns the\n // rate via MRT_DISTRIBUTED_TRACING_* env vars.\n sampler: new ParentBasedSampler({ root: new AlwaysOnSampler() }),\n });\n\n provider.addSpanProcessor(new SimpleSpanProcessor(new MrtConsoleSpanExporter()));\n // Register with `propagator: null` so the provider sets up the global tracer\n // provider and the AsyncLocalStorage context manager, but does NOT install a\n // global propagator. (`register()` otherwise defaults to a W3C propagator built\n // from OTEL_PROPAGATORS.) We deliberately own propagation ourselves:\n // - outbound: the undici requestHook below injects via our `outboundPropagator`.\n // - inbound: the Express middleware parses `traceparent` by hand\n // (parseTraceParent + trace.setSpanContext).\n // Both bypass the global registry, which is unreliable on MRT (see the\n // `outboundPropagator` doc comment). Leaving the global propagator as the\n // default no-op also means undici's own propagation.inject() — which runs right\n // after our hook — adds nothing, so `traceparent` is written exactly once.\n provider.register({ propagator: null });\n\n // Guard against double-registration across Vite module boundaries.\n // See comment above UNDICI_REGISTERED_KEY.\n if (!(globalThis as Record<symbol, boolean>)[UNDICI_REGISTERED_KEY]) {\n (globalThis as Record<symbol, boolean>)[UNDICI_REGISTERED_KEY] = true;\n registerInstrumentations({\n tracerProvider: provider,\n instrumentations: [\n new UndiciInstrumentation({\n requestHook(span, request) {\n try {\n // Static, low-cardinality client span name shared by every\n // outbound fetch (SCAPI, SLAS, …), matching the dotted\n // `sfnext.*` operation namespace used by our other spans.\n // The HTTP detail (method, full url, host, status) is already\n // set on the span as attributes by the instrumentation —\n // `http.request.method`, `url.full`, `url.path`, `url.query`,\n // `server.address`, `http.response.status_code` — so traces\n // aggregate by operation and the high-cardinality values\n // (paths/queries that can carry ids, tokens, or PII) stay out\n // of the span name.\n span.updateName('sfnext.fetch');\n } catch {\n // Non-fatal — default span name is acceptable\n }\n\n // Inject `traceparent` ourselves, via our private propagator, so the\n // header reaches SCAPI even when undici's own global-propagator\n // inject() is a no-op on MRT (see `outboundPropagator` above). The\n // hook runs just before undici's inject(), and because we never\n // register a global propagator that inject() adds nothing — so the\n // header is written exactly once. We build the context from THIS\n // CLIENT span (a pure realm-global-symbol write, no registry lookup),\n // matching what undici would have injected.\n try {\n const ctx = trace.setSpan(context.active(), span);\n outboundPropagator.inject(ctx, request, {\n set(req: UndiciRequest, key: string, value: string) {\n req.addHeader?.(key, value);\n },\n });\n } catch {\n // Non-fatal — propagation must never break the request\n }\n },\n }),\n ],\n });\n }\n\n cachedTracer = provider.getTracer(SERVICE_NAME);\n return cachedTracer;\n } catch (error) {\n logger.error('[otel] Failed to initialize OpenTelemetry:', error);\n return null;\n }\n}\n","/**\n * Copyright 2026 Salesforce, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Platform-level handler instrumentation.\n *\n * Uses React Router's ServerInstrumentation API to observe the\n * request lifecycle at the handler and route levels. This runs around ALL\n * requests (document + data) and provides OpenTelemetry spans for:\n * - `sfnext.ssr` — SSR render span per incoming HTTP request (handler level)\n * - `sfnext.loader` — child span per route loader\n * - `sfnext.action` — child span per route action\n * - `sfnext.middleware` — child span per route middleware\n *\n * Span names are low-cardinality operation names; the route is carried in the\n * `rr.route.id` and `http.route` attributes rather than the name, so traces\n * aggregate by operation and filter by route.\n *\n * `tracer` is `null` when OTel is disabled. Each handler checks explicitly\n * and calls through immediately, so the disabled path is always obvious.\n *\n * @env SFNEXT_OTEL_ENABLED — set to `\"true\"` to enable OpenTelemetry tracing.\n * Spans are written synchronously to stdout via ConsoleSpanExporter, which is\n * compatible with both local development and the Managed Runtime serverless\n * environment (see otel/setup.ts for the full design rationale).\n * Example: `SFNEXT_OTEL_ENABLED=true pnpm dev`\n *\n * @see https://reactrouter.com/how-to/instrumentation\n */\n\nimport { type Tracer, type Attributes, SpanStatusCode } from '@opentelemetry/api';\nimport type { ServerInstrumentation } from 'react-router';\nimport { ATTR_HTTP_REQUEST_METHOD, ATTR_HTTP_ROUTE, ATTR_URL_PATH } from '@opentelemetry/semantic-conventions';\nimport { initTelemetry } from '../setup';\n\nconst tracer: Tracer | null = process.env.SFNEXT_OTEL_ENABLED === 'true' ? initTelemetry() : null;\n\n/**\n * Runs `handle` inside an active OTel span, recording errors and ending the span.\n * When `tracer` is null (OTel disabled), calls `handle` directly with no overhead.\n */\nasync function traced(\n spanName: string,\n attributes: Attributes,\n handle: () => Promise<{ status: string; error?: Error }>\n): Promise<void> {\n if (!tracer) {\n await handle();\n return;\n }\n let handled = false;\n try {\n await tracer.startActiveSpan(spanName, { attributes }, async (span) => {\n try {\n handled = true;\n const result = await handle();\n if (result.status === 'error' && result.error) {\n span.setStatus({ code: SpanStatusCode.ERROR, message: result.error.message });\n span.recordException(result.error);\n }\n } finally {\n span.end();\n }\n });\n } catch {\n // OTel failure must never break the request pipeline.\n // If handle() was already called inside the span callback, don't call it again.\n if (!handled) await handle();\n }\n}\n\n/**\n * HTTP attributes common to all spans.\n * url.path only — url.full would expose query params which may contain auth\n * tokens or PII. http.response.status_code is not available from\n * InstrumentationHandlerResult.\n */\nfunction httpAttributes(request: { method: string; url: string }): Attributes {\n const attrs: Attributes = { [ATTR_HTTP_REQUEST_METHOD]: request.method };\n try {\n attrs[ATTR_URL_PATH] = new URL(request.url).pathname;\n } catch {\n // Malformed URL — skip url.path rather than throwing\n }\n return attrs;\n}\n\nexport const platformInstrumentation: ServerInstrumentation = {\n handler(handler) {\n handler.instrument({\n async request(handleRequest, { request }) {\n await traced('sfnext.ssr', httpAttributes(request), handleRequest);\n },\n });\n },\n route(route) {\n // The request method/url.path live on the parent server span. At the route\n // level the meaningful identifiers are the framework route id (`rr.route.id`)\n // and the URL route template (`http.route`, the standard semantic-convention\n // key — a bounded, low-cardinality value, never the raw path with ids).\n function routeAttributes(pattern: string): Attributes {\n return {\n 'rr.route.id': route.id,\n [ATTR_HTTP_ROUTE]: pattern,\n };\n }\n\n route.instrument({\n async loader(handleLoader, { pattern }) {\n await traced('sfnext.loader', routeAttributes(pattern), handleLoader);\n },\n async action(handleAction, { pattern }) {\n await traced('sfnext.action', routeAttributes(pattern), handleAction);\n },\n async middleware(handleMiddleware, { pattern }) {\n await traced('sfnext.middleware', routeAttributes(pattern), handleMiddleware);\n },\n });\n },\n};\n","/**\n * Copyright 2026 Salesforce, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Server entry composition function.\n *\n * Wraps the full ServerEntryModule (the customer's or SDK default entry.server)\n * with platform-level features. The Vite plugin (platformEntryPlugin) generates\n * a virtual module that imports this function and calls it with the app's entry\n * module. New platform features ship via `npm update` without changes to the\n * plugin or customer code.\n *\n * Current platform behaviors:\n * - OpenTelemetry instrumentation for request lifecycle tracing\n *\n * Future additions:\n * - loadContext enrichment (correlation IDs, platform metadata)\n * - Response header injection (tracing, cache directives)\n * - Default error handling with platform error reporting\n */\n\nimport type { ServerEntryModule } from 'react-router';\nimport { platformInstrumentation } from '../otel/react-router/instrumentation';\n\n/**\n * Composes a server entry module with platform-level features.\n *\n * - Spreads all app module properties to forward unknown/future exports\n * - Wraps the default handler for platform-level processing\n * - Prepends a platform instrumentation to instrumentations\n */\nexport function composeServerEntry(appModule: ServerEntryModule): ServerEntryModule {\n // React Router 7.18 reads the server-entry `instrumentations` export by name,\n // so an ejected entry still exporting the pre-stabilization `unstable_instrumentations`\n // compiles fine but is never registered. Surface that in dev so it doesn't ship silently.\n if (\n process.env.NODE_ENV !== 'production' &&\n 'unstable_instrumentations' in appModule &&\n !('instrumentations' in appModule)\n ) {\n // eslint-disable-next-line no-console\n console.warn(\n '[storefront-next] entry.server exports `unstable_instrumentations`, which React Router 7.18 ' +\n 'no longer reads. Rename the export to `instrumentations` or it will not register.'\n );\n }\n return {\n // Spread all properties first so unknown/future exports pass through\n ...appModule,\n // Override the exports the platform layer enhances\n default(request, statusCode, headers, context, loadContext) {\n return appModule.default(request, statusCode, headers, context, loadContext);\n },\n instrumentations: [platformInstrumentation, ...(appModule.instrumentations ?? [])],\n };\n}\n"],"mappings":";;;;;;;;;;;AA+BA,IAAa,yBAAb,cAA4C,oBAAoB;CAC5D,OAAO,OAAuB,gBAAsD;AAChF,OAAK,MAAM,QAAQ,MACf,KAAI;GACA,MAAM,MAAM,KAAK,aAAa;GAC9B,MAAM,WAAW;IACb,SAAS,IAAI;IACb,UAAU,KAAK;IACf,MAAM,KAAK;IACX,IAAI,IAAI;IACR,MAAM,KAAK;IACX,WAAW,kBAAkB,KAAK,UAAU;IAC5C,UAAU,KAAK;IACf,YAAY,KAAK;IACjB,QAAQ,KAAK;IACb,QAAQ,KAAK;IACb,OAAO,KAAK;IACZ,YAAY,KAAK;IACjB,UAAU,KAAK;IACf,cAAc,QAAQ,IAAI,wBAAwB;IACrD;AAED,WAAQ,KAAK,KAAK,UAAU,SAAS,CAAC;UAClC;AAIZ,iBAAe,EAAE,MAAM,iBAAiB,SAAS,CAAC;;;;;;ACI1D,MAAMA,iBAA2C;CAC7C,OAAO;CACP,MAAM;CACN,MAAM;CACN,OAAO;CACV;AAED,IAAIC;;;;;;AAOJ,SAAS,qBAA8B;CACnC,MAAM,MAAM,QAAQ,IAAI,OAAO,MAAM;AACrC,KAAI,CAAC,IAAK,QAAO;CACjB,MAAM,aAAa,IAAI,aAAa;AACpC,KAAI;EAAC;EAAK;EAAQ;EAAO;EAAK,CAAC,SAAS,WAAW,CAAE,QAAO;AAC5D,QAAO,IAAI,MAAM,IAAI,CAAC,MAAM,UAAU;EAClC,MAAM,QAAQ,MAAM,MAAM;AAC1B,SAAO,UAAU,OAAO,UAAU,YAAY,UAAU;GAC1D;;AAGN,SAAS,eAAyB;AAC9B,KAAI,cAAe,QAAO;CAC1B,MAAM,WAAW,QAAQ,IAAI,iBAAiB,QAAQ,IAAI;AAC1D,KAAI,YAAY,YAAY,eAAgB,QAAO;AACnD,KAAI,oBAAoB,CAAE,QAAO;AACjC,KAAI,QAAQ,IAAI,aAAa,aAAc,QAAO;AAClD,QAAO;;AAGX,SAAS,UAAU,OAA0B;AACzC,QAAO,eAAe,UAAU,eAAe,cAAc;;AAGjE,MAAa,SAAS;CAClB,MAAM,KAAa,GAAG,MAAuB;AACzC,MAAI,CAAC,UAAU,QAAQ,CAAE;AACzB,UAAQ,MAAM,MAAM,IAAI,iBAAiB,EAAE,KAAK,GAAG,KAAK;;CAE5D,KAAK,KAAa,GAAG,MAAuB;AACxC,MAAI,CAAC,UAAU,OAAO,CAAE;AACxB,UAAQ,KAAK,MAAM,OAAO,gBAAgB,EAAE,KAAK,GAAG,KAAK;;CAE7D,KAAK,KAAa,GAAG,MAAuB;AACxC,MAAI,CAAC,UAAU,OAAO,CAAE;AACxB,UAAQ,IAAI,MAAM,KAAK,gBAAgB,EAAE,KAAK,GAAG,KAAK;;CAE1D,MAAM,KAAa,GAAG,MAAuB;AACzC,MAAI,CAAC,UAAU,QAAQ,CAAE;AACzB,UAAQ,IAAI,MAAM,KAAK,iBAAiB,EAAE,KAAK,GAAG,KAAK;;CAE3D,SAAS,OAAmC;AACxC,kBAAgB;;CAEpB,WAAqB;AACjB,SAAO,cAAc;;CAE5B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACPD,MAAM,qBAAqB,IAAI,2BAA2B;AAE1D,MAAa,eAAe;;;;;;;;;;;;;;;;;AAkB5B,IAAIC,eAA8B;AAOlC,MAAM,wBAAwB,OAAO,IAAI,gCAAgC;AAEzE,SAAgB,gBAA+B;AAC3C,KAAI,aAAc,QAAO;AACzB,KAAI;EACA,MAAM,WAAW,IAAI,mBAAmB;GACpC,UAAU,IAAI,SAAS,GAAG,oBAAoB,cAAc,CAAC;GAO7D,SAAS,IAAI,mBAAmB,EAAE,MAAM,IAAI,iBAAiB,EAAE,CAAC;GACnE,CAAC;AAEF,WAAS,iBAAiB,IAAI,oBAAoB,IAAI,wBAAwB,CAAC,CAAC;AAYhF,WAAS,SAAS,EAAE,YAAY,MAAM,CAAC;AAIvC,MAAI,CAAE,WAAuC,wBAAwB;AACjE,GAAC,WAAuC,yBAAyB;AACjE,4BAAyB;IACrB,gBAAgB;IAChB,kBAAkB,CACd,IAAI,sBAAsB,EACtB,YAAY,MAAM,SAAS;AACvB,SAAI;AAWA,WAAK,WAAW,eAAe;aAC3B;AAYR,SAAI;MACA,MAAM,MAAM,MAAM,QAAQ,QAAQ,QAAQ,EAAE,KAAK;AACjD,yBAAmB,OAAO,KAAK,SAAS,EACpC,IAAI,KAAoB,KAAa,OAAe;AAChD,WAAI,YAAY,KAAK,MAAM;SAElC,CAAC;aACE;OAIf,CAAC,CACL;IACJ,CAAC;;AAGN,iBAAe,SAAS,UAAU,aAAa;AAC/C,SAAO;UACF,OAAO;AACZ,SAAO,MAAM,8CAA8C,MAAM;AACjE,SAAO;;;;;;AClLf,MAAMC,SAAwB,QAAQ,IAAI,wBAAwB,SAAS,eAAe,GAAG;;;;;AAM7F,eAAe,OACX,UACA,YACA,QACa;AACb,KAAI,CAAC,QAAQ;AACT,QAAM,QAAQ;AACd;;CAEJ,IAAI,UAAU;AACd,KAAI;AACA,QAAM,OAAO,gBAAgB,UAAU,EAAE,YAAY,EAAE,OAAO,SAAS;AACnE,OAAI;AACA,cAAU;IACV,MAAM,SAAS,MAAM,QAAQ;AAC7B,QAAI,OAAO,WAAW,WAAW,OAAO,OAAO;AAC3C,UAAK,UAAU;MAAE,MAAM,eAAe;MAAO,SAAS,OAAO,MAAM;MAAS,CAAC;AAC7E,UAAK,gBAAgB,OAAO,MAAM;;aAEhC;AACN,SAAK,KAAK;;IAEhB;SACE;AAGJ,MAAI,CAAC,QAAS,OAAM,QAAQ;;;;;;;;;AAUpC,SAAS,eAAe,SAAsD;CAC1E,MAAMC,QAAoB,GAAG,2BAA2B,QAAQ,QAAQ;AACxE,KAAI;AACA,QAAM,iBAAiB,IAAI,IAAI,QAAQ,IAAI,CAAC;SACxC;AAGR,QAAO;;AAGX,MAAaC,0BAAiD;CAC1D,QAAQ,SAAS;AACb,UAAQ,WAAW,EACf,MAAM,QAAQ,eAAe,EAAE,WAAW;AACtC,SAAM,OAAO,cAAc,eAAe,QAAQ,EAAE,cAAc;KAEzE,CAAC;;CAEN,MAAM,OAAO;EAKT,SAAS,gBAAgB,SAA6B;AAClD,UAAO;IACH,eAAe,MAAM;KACpB,kBAAkB;IACtB;;AAGL,QAAM,WAAW;GACb,MAAM,OAAO,cAAc,EAAE,WAAW;AACpC,UAAM,OAAO,iBAAiB,gBAAgB,QAAQ,EAAE,aAAa;;GAEzE,MAAM,OAAO,cAAc,EAAE,WAAW;AACpC,UAAM,OAAO,iBAAiB,gBAAgB,QAAQ,EAAE,aAAa;;GAEzE,MAAM,WAAW,kBAAkB,EAAE,WAAW;AAC5C,UAAM,OAAO,qBAAqB,gBAAgB,QAAQ,EAAE,iBAAiB;;GAEpF,CAAC;;CAET;;;;;;;;;;;ACxFD,SAAgB,mBAAmB,WAAiD;AAIhF,KACI,QAAQ,IAAI,aAAa,gBACzB,+BAA+B,aAC/B,EAAE,sBAAsB,WAGxB,SAAQ,KACJ,gLAEH;AAEL,QAAO;EAEH,GAAG;EAEH,QAAQ,SAAS,YAAY,SAAS,WAAS,aAAa;AACxD,UAAO,UAAU,QAAQ,SAAS,YAAY,SAASC,WAAS,YAAY;;EAEhF,kBAAkB,CAAC,yBAAyB,GAAI,UAAU,oBAAoB,EAAE,CAAE;EACrF"}
package/dist/index.d.ts CHANGED
@@ -137,48 +137,41 @@ declare function transformTargetPlaceholderPlugin(): {
137
137
  };
138
138
  //#endregion
139
139
  //#region src/plugins/uiTargetDevMode.d.ts
140
+ /**
141
+ * @deprecated This plugin has been removed. It was an internal-only debugging aid
142
+ * that could cause build failures when extensions were trimmed. The export is kept
143
+ * as a no-op for backward compatibility and will be removed in v2.
144
+ *
145
+ * **Migration:** Remove any `uiTargetDevMode()` plugin from your `vite.config.ts`,
146
+ * delete `vite-plugins/ui-target-dev-mode.ts` if it exists, and remove the plugin
147
+ * from the Vite plugins array.
148
+ */
140
149
  interface UITargetDevModeConfig {
141
150
  /**
142
- * Enable dev mode visual markers. Defaults to false.
143
- * Set via VITE_UI_TARGET_DEV_MODE=true environment variable.
151
+ * @deprecated No longer used
144
152
  */
145
153
  enabled?: boolean;
146
154
  /**
147
- * Build-time filter: only show targets matching a category prefix.
148
- * Set via VITE_TARGET_FILTER_CATEGORY=pdp environment variable.
149
- * @example filterCategory: 'pdp' — only shows targets whose id starts with "pdp."
155
+ * @deprecated No longer used
150
156
  */
151
157
  filterCategory?: string;
152
158
  /**
153
- * Map of targetId → hint label for overlay grouping/filtering.
154
- * Built from target-config.json at config time and injected into each dev marker
155
- * as a __hint__ prop, which is emitted as a data-ui-target-hint DOM attribute.
156
- * Example: { 'emailSignUp.consent.marketing': 't/my-branch/W-123', 'checkout.contactInfo': 'pr:1384' }
159
+ * @deprecated No longer used
157
160
  */
158
161
  hintMap?: Record<string, string>;
159
162
  }
160
163
  /**
161
- * Vite plugin that adds visual markers to UITarget components in development.
162
- *
163
- * PRODUCTION: This plugin is completely inactive - zero overhead.
164
- * DEVELOPMENT: Transforms UITarget JSX to add visual debugging markers.
164
+ * @deprecated This plugin has been removed. It was an internal-only debugging aid
165
+ * that could cause build failures when extensions were trimmed. The export is kept
166
+ * as a no-op for backward compatibility and will be removed in v2.
165
167
  *
166
- * @example
167
- * // Source code:
168
- * <UITarget targetId="pdp.loyalty.badge">
169
- * <Widget />
170
- * </UITarget>
168
+ * **Migration:** Remove any `uiTargetDevMode()` plugin from your `vite.config.ts`,
169
+ * delete `vite-plugins/ui-target-dev-mode.ts` if it exists, and remove the plugin
170
+ * from the Vite plugins array.
171
171
  *
172
- * // Transformed in DEV mode:
173
- * <UITargetDevMarker
174
- * targetId="pdp.loyalty.badge"
175
- * __file__="/src/components/product.tsx"
176
- * __hasChildren__={true}
177
- * >
178
- * <Widget />
179
- * </UITargetDevMarker>
172
+ * @returns A no-op Vite plugin
180
173
  */
181
- declare function uiTargetDevModePlugin(config?: UITargetDevModeConfig): Plugin;
174
+ declare function uiTargetDevModePlugin(_config?: UITargetDevModeConfig): Plugin;
182
175
  //#endregion
183
176
  //#region src/plugins/hybridProxy.d.ts
184
177
 
@@ -204,6 +197,44 @@ interface HybridProxyPluginOptions {
204
197
  defaultSiteId?: string;
205
198
  /** Locale for SFRA paths (e.g., 'en-GB'). Defaults to 'default' if not provided. */
206
199
  locale?: string;
200
+ /**
201
+ * The storefront's `url.prefix` (from `config.server.ts`), e.g. `'/:siteId/:localeId'`
202
+ * or `'/:localeId'`. When set, the proxy strips this prefix from the incoming path
203
+ * before decorating to SFRA's `/s/{siteId}/{locale}/…` form, and reuses the
204
+ * site/locale the path already carries instead of the `defaultSiteId`/`locale`
205
+ * fallbacks. This is what keeps a prefixed request like `/uk/cart` from being
206
+ * double-stacked into `/s/{siteId}/{locale}/uk/cart`.
207
+ *
208
+ * Prefix support is OOTB: with `url.prefix` configured, no further opt-in is needed.
209
+ * Leave undefined (or `'/'`) for storefronts that emit bare functional paths.
210
+ */
211
+ urlPrefix?: string;
212
+ /**
213
+ * Known locale identifiers — every locale `id` AND `alias` the storefront serves
214
+ * (e.g. `['en-GB', 'uk', 'de-DE']`). Used to confirm that a captured prefix segment
215
+ * is really a locale before treating it as one, so a bare path like `/cart` under a
216
+ * `/:localeId` prefix isn't mistaken for the locale `cart`. The matched value is
217
+ * passed straight through as the SFRA locale path segment (SFRA resolves both the
218
+ * BM site-path alias `uk` and the canonical `en-GB`).
219
+ */
220
+ localeAliases?: string[];
221
+ /**
222
+ * Known site identifiers — every site `id` AND `alias` the storefront serves
223
+ * (e.g. `['RefArchGlobal', 'global']`). Same role as {@link localeAliases} for the
224
+ * `:siteId` segment of a `/:siteId/:localeId`-style prefix.
225
+ */
226
+ siteAliases?: string[];
227
+ /**
228
+ * Last-resort escape hatch: fully override how an incoming proxy path is rewritten
229
+ * to the SFRA path. Receives the bare pathname (no query) and returns the SFRA path
230
+ * (no query — the proxy re-appends the original query string). Return `null` to fall
231
+ * back to the built-in OOTB rewrite.
232
+ *
233
+ * Prefer leaving this undefined: with `urlPrefix` set the built-in rewrite already
234
+ * handles all standard prefix shapes. Use this only for a non-standard URL model the
235
+ * prefix machinery can't express.
236
+ */
237
+ rewritePath?: (pathname: string) => string | null;
207
238
  }
208
239
  /**
209
240
  * Vite plugin for hybrid proxying between Storefront Next and legacy SFRA.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../src/plugins/staticRegistry.ts","../src/plugins/eventInstrumentationValidator.ts","../src/storefront-next-targets.ts","../src/plugins/transformTargets.ts","../src/plugins/uiTargetDevMode.ts","../src/plugins/hybridProxy.ts","../src/plugins/ecdnMatcher.ts"],"sourcesContent":[],"mappings":";;;;;;;;UAwDiB,0BAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;AG3BjB;;UFHiB,mCAAA;;AGgBjB;AA4CA;;;;AC1BA;AA+PA;;;;AC7KA;;;;;;;;;;;AFpGiB,UFHA,2BAAA,CEuBH;EAwBE;;;;AC1BhB;AA+PA;;;;AC7KA;;;;;;;;;;;;;;;;;;;;;;;mBJrEqB;;;;;;;;;kCAUe;;;;;;;;;;;;;;;;;;;;;iBAsBpB,qBAAA,UAA8B,8BAAmC;;;iBC5EjE,gCAAA,CAAA;;;EH2BC,cAAA,CAAA,MAAA,EGjBc,cHiBY,CAAA,EAAA,IAAA;;;;IC9B1B,GAAA,EAAA,IAAA;;;;;UGgBA,qBAAA;EDbD;;;;ECaC,OAAA,CAAA,EAAA,OAAA;EA4CD;;;;AC1BhB;EA+PgB,cAAA,CAAA,EAAA,MAAiB;;;;AC7KjC;;;YFhFc;;;;;;;;;;;;;;;;;;;;;;;iBAwBE,qBAAA,UAA8B,wBAA6B;;;;UC1B1D,wBAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA+PD,iBAAA,UAA2B,2BAA2B;;;;;;;;;;;;;;;;;;;;;;iBC7KtD,iBAAA"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/plugins/staticRegistry.ts","../src/plugins/eventInstrumentationValidator.ts","../src/storefront-next-targets.ts","../src/plugins/transformTargets.ts","../src/plugins/uiTargetDevMode.ts","../src/plugins/hybridProxy.ts","../src/plugins/ecdnMatcher.ts"],"sourcesContent":[],"mappings":";;;;;;;;UAwDiB,0BAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;AG3BjB;;UFHiB,mCAAA;;AGAjB;AA4BA;;;;ACOA;AAyZA;;;;ACxUA;;;;;;;;;;;AFpHiB,UFaA,2BAAA,CECH;EAcE;;;;ACOhB;AAyZA;;;;ACxUA;;;;;;;;;;;;;;;;;;;;;;;mBJrEqB;;;;;;;;;kCAUe;;;;;;;;;;;;;;;;;;;;;iBAsBpB,qBAAA,UAA8B,8BAAmC;;;iBC5EjE,gCAAA,CAAA;;;EH2BC,cAAA,CAAA,MAAA,EGjBc,cHiBY,CAAA,EAAA,IAAA;;;;IC9B1B,GAAA,EAAA,IAAA;;;;;;AEGjB;;;;ACHA;AA4BA;;;UA5BiB,qBAAA;ECmCA;AAyZjB;;;;ACxUA;;;;;;YFtGc;;;;;;;;;;;;;iBAcE,qBAAA,WAAgC,wBAAwB;;;;UCOvD,wBAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAyZD,iBAAA,UAA2B,2BAA2B;;;;;;;;;;;;;;;;;;;;;;iBCxUtD,iBAAA"}