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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,8 +1,8 @@
1
- import { SpanStatusCode } 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
- import { ConsoleSpanExporter, SimpleSpanProcessor } from "@opentelemetry/sdk-trace-base";
5
- import { ExportResultCode, hrTimeToTimeStamp } from "@opentelemetry/core";
4
+ import { AlwaysOnSampler, ConsoleSpanExporter, ParentBasedSampler, SimpleSpanProcessor } from "@opentelemetry/sdk-trace-base";
5
+ import { ExportResultCode, W3CTraceContextPropagator, hrTimeToTimeStamp } from "@opentelemetry/core";
6
6
  import { Resource } from "@opentelemetry/resources";
7
7
  import { registerInstrumentations } from "@opentelemetry/instrumentation";
8
8
  import { UndiciInstrumentation } from "@opentelemetry/instrumentation-undici";
@@ -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.
@@ -124,18 +161,25 @@ const UNDICI_REGISTERED_KEY = Symbol.for("sfnext.otel.undici_registered");
124
161
  function initTelemetry() {
125
162
  if (cachedTracer) return cachedTracer;
126
163
  try {
127
- const provider = new NodeTracerProvider({ resource: new Resource({ [ATTR_SERVICE_NAME]: SERVICE_NAME }) });
164
+ const provider = new NodeTracerProvider({
165
+ resource: new Resource({ [ATTR_SERVICE_NAME]: SERVICE_NAME }),
166
+ sampler: new ParentBasedSampler({ root: new AlwaysOnSampler() })
167
+ });
128
168
  provider.addSpanProcessor(new SimpleSpanProcessor(new MrtConsoleSpanExporter()));
129
- provider.register();
169
+ provider.register({ propagator: null });
130
170
  if (!globalThis[UNDICI_REGISTERED_KEY]) {
131
171
  globalThis[UNDICI_REGISTERED_KEY] = true;
132
172
  registerInstrumentations({
133
173
  tracerProvider: provider,
134
174
  instrumentations: [new UndiciInstrumentation({ requestHook(span, request) {
135
175
  try {
136
- const method = request.method.toUpperCase();
137
- const url = `${request.origin}${request.path}`;
138
- 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
+ } });
139
183
  } catch {}
140
184
  } })]
141
185
  });
@@ -185,7 +229,7 @@ async function traced(spanName, attributes, handle) {
185
229
  * HTTP attributes common to all spans.
186
230
  * url.path only — url.full would expose query params which may contain auth
187
231
  * tokens or PII. http.response.status_code is not available from
188
- * unstable_InstrumentationHandlerResult.
232
+ * InstrumentationHandlerResult.
189
233
  */
190
234
  function httpAttributes(request) {
191
235
  const attrs = { [ATTR_HTTP_REQUEST_METHOD]: request.method };
@@ -197,25 +241,25 @@ function httpAttributes(request) {
197
241
  const platformInstrumentation = {
198
242
  handler(handler) {
199
243
  handler.instrument({ async request(handleRequest, { request }) {
200
- await traced("react-router ssr", httpAttributes(request), handleRequest);
244
+ await traced("sfnext.ssr", httpAttributes(request), handleRequest);
201
245
  } });
202
246
  },
203
247
  route(route) {
204
- function routeAttributes(unstable_pattern) {
248
+ function routeAttributes(pattern) {
205
249
  return {
206
250
  "rr.route.id": route.id,
207
- "rr.route.pattern": unstable_pattern
251
+ [ATTR_HTTP_ROUTE]: pattern
208
252
  };
209
253
  }
210
254
  route.instrument({
211
- async loader(handleLoader, { unstable_pattern }) {
212
- await traced(`loader (${route.id})`, routeAttributes(unstable_pattern), handleLoader);
255
+ async loader(handleLoader, { pattern }) {
256
+ await traced("sfnext.loader", routeAttributes(pattern), handleLoader);
213
257
  },
214
- async action(handleAction, { unstable_pattern }) {
215
- await traced(`action (${route.id})`, routeAttributes(unstable_pattern), handleAction);
258
+ async action(handleAction, { pattern }) {
259
+ await traced("sfnext.action", routeAttributes(pattern), handleAction);
216
260
  },
217
- async middleware(handleMiddleware, { unstable_pattern }) {
218
- await traced(`middleware (${route.id})`, routeAttributes(unstable_pattern), handleMiddleware);
261
+ async middleware(handleMiddleware, { pattern }) {
262
+ await traced("sfnext.middleware", routeAttributes(pattern), handleMiddleware);
219
263
  }
220
264
  });
221
265
  }
@@ -228,15 +272,16 @@ const platformInstrumentation = {
228
272
  *
229
273
  * - Spreads all app module properties to forward unknown/future exports
230
274
  * - Wraps the default handler for platform-level processing
231
- * - Prepends a platform instrumentation to unstable_instrumentations
275
+ * - Prepends a platform instrumentation to instrumentations
232
276
  */
233
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.");
234
279
  return {
235
280
  ...appModule,
236
- default(request, statusCode, headers, context, loadContext) {
237
- 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);
238
283
  },
239
- unstable_instrumentations: [platformInstrumentation, ...appModule.unstable_instrumentations ?? []]
284
+ instrumentations: [platformInstrumentation, ...appModule.instrumentations ?? []]
240
285
  };
241
286
  }
242
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 * @env SFNEXT_OTEL_ENABLED — set to `\"true\"` to enable (e.g. `SFNEXT_OTEL_ENABLED=true pnpm dev`)\n */\n\nimport type { Tracer } from '@opentelemetry/api';\nimport { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';\nimport { SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base';\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 });\n\n provider.addSpanProcessor(new SimpleSpanProcessor(new MrtConsoleSpanExporter()));\n provider.register();\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":"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"}
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":"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"}
@@ -384,6 +384,8 @@ async function processComponentFile(filePath, _projectRoot) {
384
384
  regionDefinitions,
385
385
  attributes
386
386
  };
387
+ if (typeof componentConfig.embedded === "boolean") componentMetadata.embedded = componentConfig.embedded;
388
+ if (componentConfig.component_id !== void 0) componentMetadata.component_id = String(componentConfig.component_id);
387
389
  components.push(componentMetadata);
388
390
  }
389
391
  } catch (error) {
@@ -483,6 +485,8 @@ async function generateComponentCartridge(component, outputDir, dryRun = false)
483
485
  description: component.description,
484
486
  group: component.group,
485
487
  arch_type: ARCH_TYPE_HEADLESS,
488
+ ...typeof component.embedded === "boolean" && { embedded: component.embedded },
489
+ component_id: component.component_id,
486
490
  region_definitions: component.regionDefinitions || [],
487
491
  attribute_definition_groups: attributeDefinitionGroups
488
492
  };
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":";;;;;;;;UAuDiB,0BAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;AG5BjB;;UFDiB,mCAAA;;AGgBjB;AA4CA;;;;AC1BA;AA+PA;;;;AC7KA;;;;;;;;;;;AFpGiB,UFHA,2BAAA,CEuBH;EAwBE;;;;AC1BhB;AA+PA;;;;AC7KA;;;;;;;;;;;;;;;;;;;;;;;mBJrEqB;;;;;;;;;kCAUe;;;;;;;;;;;;;;;;;;;;;iBAsBpB,qBAAA,UAA8B,8BAAmC;;;iBC9EjE,gCAAA,CAAA;;;EH4BC,cAAA,CAAA,MAAA,EGlBc,cHkBY,CAAA,EAAA,IAAA;;;;IC7B1B,GAAA,EAAA,IAAA;;;;;UGgBA,qBAAA;EDfD;;;;ECeC,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"}