@salesforce/storefront-next-dev 1.3.2 → 1.4.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,28 +3,42 @@ import "../logger2.js";
3
3
  import { n as getDefaultBuildDir, r as getDefaultMessage } from "../utils.js";
4
4
  import "../format-with-project-biome.js";
5
5
  import { a as buildMrtConfig, i as SFNEXT_BASE_CARTRIDGE_OUTPUT_DIR, n as GENERATE_AND_DEPLOY_CARTRIDGE_ON_MRT_PUSH, r as SFNEXT_BASE_CARTRIDGE_NAME, t as CARTRIDGES_BASE_DIR } from "../config.js";
6
- import { t as createBundle } from "../bundle.js";
6
+ import { i as withLegacyMrtShortFlags } from "../flags.js";
7
+ import { n as getBundleDependencies, t as createBundle } from "../bundle.js";
7
8
  import { t as generateMetadata } from "../generate-cartridge.js";
8
9
  import { Flags } from "@oclif/core";
9
10
  import path from "path";
10
11
  import fs from "fs-extra";
11
12
  import { MrtCommand } from "@salesforce/b2c-tooling-sdk/cli";
12
13
  import { uploadCartridges } from "@salesforce/b2c-tooling-sdk/operations/code";
13
- import { uploadBundle, waitForEnv } from "@salesforce/b2c-tooling-sdk/operations/mrt";
14
+ import { createBundleV2, createDeploymentScapi, mrtScapiUnavailableMessage, runMrtWithFallback, uploadBundle, uploadBundleScapi, waitForDeploymentScapi, waitForEnv } from "@salesforce/b2c-tooling-sdk/operations/mrt";
14
15
  import { DEFAULT_MRT_ORIGIN, createMrtClient } from "@salesforce/b2c-tooling-sdk/clients";
15
16
 
16
17
  //#region src/commands/push.ts
18
+ /** config.server.ts SSR parameter keys and their v2 bundle (PascalCase) equivalents. */
19
+ const V2_SSR_PARAMETER_KEYS = {
20
+ ssrFunctionNodeVersion: "SSRFunctionNodeVersion",
21
+ envBasePath: "EnvBasePath"
22
+ };
23
+ /** Rename known camelCase SSR parameters to the PascalCase keys the v2 bundle config expects. */
24
+ function toV2SsrParameters(params) {
25
+ return Object.fromEntries(Object.entries(params).map(([key, value]) => [V2_SSR_PARAMETER_KEYS[key] ?? key, value]));
26
+ }
17
27
  /**
18
28
  * MRT Push command - builds and pushes bundle to Managed Runtime.
19
29
  *
20
30
  * Inherits MRT flags from MrtCommand:
21
31
  * - --api-key: MRT API key (env: MRT_API_KEY, fallback: SFCC_MRT_API_KEY)
22
- * - --project/-p: MRT project slug (env: MRT_PROJECT, fallback: SFCC_MRT_PROJECT)
23
- * - --environment/-e: MRT target environment (env: MRT_TARGET, fallback: SFCC_MRT_ENVIRONMENT)
24
- * - --cloud-origin: MRT cloud origin URL (env: MRT_CLOUD_ORIGIN, fallback: SFCC_MRT_CLOUD_ORIGIN)
25
- * - --credentials-file: Path to MRT credentials file (env: MRT_CREDENTIALS_FILE)
32
+ * - --project/-p (alias --storefront/-s): MRT project slug (env: MRT_PROJECT, fallback: SFCC_MRT_PROJECT)
33
+ * - --environment/-e (aliases --target, -t): MRT target environment (env: MRT_ENVIRONMENT, fallbacks: SFCC_MRT_ENVIRONMENT, MRT_TARGET)
34
+ * - --cloud-origin/-u (alias -o): MRT cloud origin URL (env: MRT_CLOUD_ORIGIN, fallback: SFCC_MRT_CLOUD_ORIGIN)
35
+ * - --credentials-file/-c: Path to MRT credentials file (env: MRT_CREDENTIALS_FILE)
36
+ * - --mrt-backend: legacy (default), scapi, or auto (env: MRT_BACKEND, fallback: SFCC_MRT_BACKEND)
26
37
  * - --config: Path to dw.json config file (env: SFCC_CONFIG)
27
38
  * - --instance/-i: Named instance from config (env: SFCC_INSTANCE)
39
+ *
40
+ * The SCAPI backend (Storefront Deployments API) additionally reads OAuth flags/env:
41
+ * SFCC_CLIENT_ID, SFCC_CLIENT_SECRET (client credentials), SFCC_SHORTCODE, SFCC_TENANT_ID.
28
42
  */
29
43
  var Push = class Push extends MrtCommand {
30
44
  static description = "Build and push bundle to Managed Runtime";
@@ -32,10 +46,11 @@ var Push = class Push extends MrtCommand {
32
46
  "<%= config.bin %> <%= command.id %>",
33
47
  "<%= config.bin %> <%= command.id %> --project-directory ./my-project",
34
48
  "<%= config.bin %> <%= command.id %> --project my-project --environment staging",
35
- "<%= config.bin %> <%= command.id %> --wait"
49
+ "<%= config.bin %> <%= command.id %> --wait",
50
+ "<%= config.bin %> <%= command.id %> --mrt-backend scapi --environment staging --wait"
36
51
  ];
37
52
  static flags = {
38
- ...MrtCommand.baseFlags,
53
+ ...withLegacyMrtShortFlags(MrtCommand.baseFlags),
39
54
  "build-directory": Flags.string({
40
55
  char: "b",
41
56
  description: "Build directory to push (default: auto-detected)"
@@ -50,23 +65,16 @@ var Push = class Push extends MrtCommand {
50
65
  default: false
51
66
  }),
52
67
  "project-slug": Flags.string({
53
- char: "s",
54
68
  description: "DEPRECATED: Use --project instead",
55
69
  hidden: true
56
- }),
57
- target: Flags.string({
58
- char: "t",
59
- description: "DEPRECATED: Use --environment instead",
60
- hidden: true
61
70
  })
62
71
  };
63
72
  async run() {
64
73
  const { flags } = await this.parse(Push);
65
- const projectDirectory = flags["project-directory"] || process.cwd();
74
+ const projectDirectory = path.resolve(flags["project-directory"] || process.cwd());
66
75
  if (flags["project-slug"]) this.warn("Flag --project-slug is deprecated. Use --project instead.");
67
- if (flags.target) this.warn("Flag --target is deprecated. Use --environment instead.");
68
- const target = flags.environment || flags.target || this.resolvedConfig.values.mrtEnvironment;
69
- if (flags.wait && !target) this.error("You must provide a target environment when using --wait (via --environment flag, MRT_TARGET env var, or dw.json)");
76
+ const target = flags.environment || this.resolvedConfig.values.mrtEnvironment;
77
+ if (flags.wait && !target) this.error("You must provide a target environment when using --wait (via --environment flag, MRT_ENVIRONMENT env var, or dw.json)");
70
78
  if (!fs.existsSync(projectDirectory)) this.error(`Project directory "${projectDirectory}" does not exist!`);
71
79
  const projectSlug = flags.project || flags["project-slug"] || this.resolvedConfig.values.mrtProject;
72
80
  if (!projectSlug || projectSlug.trim() === "") this.error("Project slug is required. Provide --project, set MRT_PROJECT env var, or configure mrtProject in dw.json");
@@ -74,11 +82,54 @@ var Push = class Push extends MrtCommand {
74
82
  if (!fs.existsSync(buildDirectory)) this.error(`Build directory "${buildDirectory}" does not exist!`);
75
83
  if (GENERATE_AND_DEPLOY_CARTRIDGE_ON_MRT_PUSH) await this.generateAndDeployCartridge(projectDirectory);
76
84
  if (target) process.env.DEPLOY_TARGET = target;
77
- this.requireMrtCredentials();
85
+ const { preference, scapiConnection, legacyAuth } = this.getMrtBackendContext();
78
86
  const config = await buildMrtConfig(buildDirectory, projectDirectory);
79
87
  const message = flags.message ?? getDefaultMessage(projectDirectory);
80
88
  this.log(`Creating bundle for project: ${projectSlug}`);
81
89
  if (target) this.log(`Target environment: ${target}`);
90
+ const pushOptions = {
91
+ message,
92
+ config,
93
+ buildDirectory,
94
+ projectDirectory,
95
+ projectSlug,
96
+ target
97
+ };
98
+ const { value: result } = await runMrtWithFallback({
99
+ preference,
100
+ hasScapiConfig: Boolean(scapiConnection),
101
+ canFallbackToLegacy: Boolean(legacyAuth),
102
+ onFallback: (reason) => this.warn(`SCAPI MRT backend unavailable, using legacy MRT API: ${reason}`),
103
+ onResolve: (backend) => this.logger.debug({ backend }, "[MRT] Pushing bundle via backend")
104
+ }, {
105
+ scapi: () => this.pushScapi(scapiConnection, pushOptions),
106
+ legacy: () => this.pushLegacy(pushOptions)
107
+ });
108
+ this.log(`Bundle ${result.bundleId} uploaded`);
109
+ for (const w of result.warnings) this.warn(w);
110
+ if (flags.wait && target) {
111
+ this.log(`Waiting for deployment to ${target}...`);
112
+ if (result.backend === "scapi") {
113
+ if (!scapiConnection || !result.deploymentId) {
114
+ this.warn("SCAPI did not return a deployment ID; cannot wait for the deployment to complete.");
115
+ return;
116
+ }
117
+ await waitForDeploymentScapi(scapiConnection, {
118
+ storefrontId: projectSlug,
119
+ environmentId: target,
120
+ deploymentId: result.deploymentId,
121
+ onPoll: (info) => this.log(` ${target}: ${info.status} (${info.elapsedSeconds}s)`)
122
+ });
123
+ } else await this.waitLegacy(projectSlug, target);
124
+ this.log(`Deployment complete — bundle ${result.bundleId} is live on ${target}`);
125
+ }
126
+ }
127
+ supportsScapiMrt() {
128
+ return true;
129
+ }
130
+ /** Upload a v1 bundle through the legacy MRT Cloud API (API key auth). */
131
+ async pushLegacy(options) {
132
+ const { message, config, buildDirectory, projectDirectory, projectSlug, target } = options;
82
133
  const bundle = await createBundle({
83
134
  message,
84
135
  ssr_parameters: config.ssrParameters,
@@ -92,26 +143,70 @@ var Push = class Push extends MrtCommand {
92
143
  const client = createMrtClient({ origin }, this.getMrtAuth());
93
144
  this.log(`Uploading bundle to ${origin}`);
94
145
  const result = await uploadBundle(client, projectSlug, bundle, target);
95
- this.log(`Bundle ${result.bundleId} uploaded`);
96
- const warnings = result.warnings ?? [];
97
- for (const w of warnings) this.warn(w);
98
- if (flags.wait && target) {
99
- this.log(`Waiting for deployment to ${target}...`);
100
- let lastState = "";
101
- await waitForEnv({
102
- projectSlug,
103
- slug: target,
104
- origin,
105
- onPoll: (info) => {
106
- if (info.state !== lastState) {
107
- lastState = info.state;
108
- this.log(` ${target}: ${info.state} (${info.elapsedSeconds}s)`);
109
- }
110
- }
111
- }, this.getMrtAuth());
112
- this.log(`Deployment complete — bundle ${result.bundleId} is live on ${target}`);
146
+ return {
147
+ backend: "legacy",
148
+ bundleId: result.bundleId,
149
+ warnings: result.warnings ?? []
150
+ };
151
+ }
152
+ /**
153
+ * Upload a v2 bundle through the SCAPI Storefront Deployments API (OAuth), then
154
+ * deploy it when a target is given (the SCAPI upload endpoint is upload-only).
155
+ */
156
+ async pushScapi(conn, options) {
157
+ if (!conn) throw new Error(mrtScapiUnavailableMessage());
158
+ const { message, config, buildDirectory, projectDirectory, projectSlug, target } = options;
159
+ const bundle = await createBundleV2({
160
+ message,
161
+ ssrParameters: toV2SsrParameters(config.ssrParameters),
162
+ ssrOnly: config.ssrOnly,
163
+ ssrShared: config.ssrShared,
164
+ bundleMetadata: { dependencies: getBundleDependencies(projectDirectory) },
165
+ buildDirectory,
166
+ projectDirectory,
167
+ matchMode: "ignore_missing"
168
+ });
169
+ this.log(`Uploading bundle via SCAPI (${conn.shortCode})`);
170
+ const uploaded = await uploadBundleScapi(conn, {
171
+ storefrontId: projectSlug,
172
+ bundle
173
+ });
174
+ if (!target) return {
175
+ backend: "scapi",
176
+ bundleId: uploaded.bundleId,
177
+ warnings: uploaded.warnings
178
+ };
179
+ try {
180
+ const deployment = await createDeploymentScapi(conn, {
181
+ storefrontId: projectSlug,
182
+ environmentId: target,
183
+ bundleId: uploaded.bundleId
184
+ });
185
+ return {
186
+ backend: "scapi",
187
+ bundleId: uploaded.bundleId,
188
+ warnings: uploaded.warnings,
189
+ deploymentId: deployment.deploymentId
190
+ };
191
+ } catch (error) {
192
+ const reason = error instanceof Error ? error.message : String(error);
193
+ throw new Error(`Bundle ${uploaded.bundleId} uploaded but the deployment to ${target} failed: ${reason}`);
113
194
  }
114
195
  }
196
+ async waitLegacy(projectSlug, target) {
197
+ let lastState = "";
198
+ await waitForEnv({
199
+ projectSlug,
200
+ slug: target,
201
+ origin: this.resolvedConfig.values.mrtOrigin || DEFAULT_MRT_ORIGIN,
202
+ onPoll: (info) => {
203
+ if (info.state !== lastState) {
204
+ lastState = info.state;
205
+ this.log(` ${target}: ${info.state} (${info.elapsedSeconds}s)`);
206
+ }
207
+ }
208
+ }, this.getMrtAuth());
209
+ }
115
210
  /**
116
211
  * Generate and deploy cartridge metadata to B2C instance.
117
212
  * This is a pre-MRT-push step that ensures Page Designer metadata is current.
@@ -44,6 +44,27 @@ const LEVEL_PRIORITY = {
44
44
  info: 2,
45
45
  debug: 3
46
46
  };
47
+ /**
48
+ * Type guard for a {@link LogLevel} name. Uses `Object.hasOwn` (not `in`) so
49
+ * inherited properties like `toString` don't falsely validate an env value.
50
+ */
51
+ function isLogLevel(value) {
52
+ return Object.hasOwn(LEVEL_PRIORITY, value);
53
+ }
54
+ /**
55
+ * Managed Runtime injects `MRT_LOG_LEVEL` as a numeric string from its own
56
+ * `LogLevel` enum (`0=TRACE, 1=DEBUG, 2=INFO, 3=WARN, 4=ERROR, 5=FATAL`), not a
57
+ * level name — so it must be mapped, not matched against {@link LEVEL_PRIORITY}'s
58
+ * keys. `TRACE` and `FATAL` clamp to the closest level this logger supports.
59
+ */
60
+ const MRT_NUMERIC_TO_LEVEL = {
61
+ "0": "debug",
62
+ "1": "debug",
63
+ "2": "info",
64
+ "3": "warn",
65
+ "4": "error",
66
+ "5": "error"
67
+ };
47
68
  let overrideLevel;
48
69
  /**
49
70
  * Returns true when the `DEBUG` env var targets sfnext or is a general enable flag.
@@ -67,8 +88,10 @@ function debugEnablesSfnext() {
67
88
  }
68
89
  function resolveLevel() {
69
90
  if (overrideLevel) return overrideLevel;
70
- const envLevel = process.env.MRT_LOG_LEVEL ?? process.env.SFCC_LOG_LEVEL;
71
- if (envLevel && envLevel in LEVEL_PRIORITY) return envLevel;
91
+ const mrtLevel = process.env.MRT_LOG_LEVEL;
92
+ if (mrtLevel && mrtLevel in MRT_NUMERIC_TO_LEVEL) return MRT_NUMERIC_TO_LEVEL[mrtLevel];
93
+ const sfccLevel = process.env.SFCC_LOG_LEVEL;
94
+ if (sfccLevel && isLogLevel(sfccLevel)) return sfccLevel;
72
95
  if (debugEnablesSfnext()) return "debug";
73
96
  if (process.env.NODE_ENV === "production") return "warn";
74
97
  return "info";
@@ -1 +1 @@
1
- {"version":3,"file":"server.js","names":["LEVEL_PRIORITY: Record<LogLevel, number>","overrideLevel: LogLevel | undefined","tracer","tracer: Tracer | null","context","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 // oxlint-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/* oxlint-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 */\n// The tracer is cached on `globalThis`, not in a module-scoped `let`, so it can be\n// shared with code that never imports this module. `initTelemetry()` (called by the\n// platform instrumentation at server bootstrap) builds and registers the single\n// NodeTracerProvider and publishes its tracer here; the runtime package reads the same\n// slot via `@salesforce/storefront-next-runtime/otel` `getPlatformTracer()` to hand the\n// tracer to its data-store span seam. The two packages cannot share a module (the\n// dependency direction is dev → runtime), so they share the tracer through the global\n// symbol registry: `Symbol.for()` resolves to the same symbol everywhere. The runtime\n// side MUST use this exact key. This also collapses any accidental second module\n// instance of setup.ts (e.g. across Vite SSR module-runner boundaries) onto one tracer\n// rather than a competing provider — the same pattern used by UNDICI_REGISTERED_KEY below.\nconst TRACER_KEY = Symbol.for('sfnext.otel.tracer');\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 const cachedTracer = (globalThis as Record<symbol, Tracer | undefined>)[TRACER_KEY];\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 const tracer = provider.getTracer(SERVICE_NAME);\n (globalThis as Record<symbol, Tracer | undefined>)[TRACER_KEY] = tracer;\n return tracer;\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 * `http.route` attribute rather than the name, so traces aggregate by operation\n * 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 { siteContext } from '@salesforce/storefront-next-runtime/site-context';\nimport { initTelemetry } from '../setup';\n\nconst tracer: Tracer | null = process.env.SFNEXT_OTEL_ENABLED === 'true' ? initTelemetry() : null;\n\n/**\n * The read-only router context passed to each instrumentation callback. React\n * Router types it as `ReadonlyContext`, which is a `.get`-bearing provider only\n * when the app has enabled middleware (`MiddlewareEnabled`); the SDK's own\n * tsconfig doesn't resolve that flag, so we accept an optionally-`get`-bearing\n * value and narrow structurally at runtime.\n */\ntype MaybeReadonlyContext = { get?: (context: typeof siteContext) => unknown } | undefined;\n\n/**\n * Reads the resolved site from the router context, or `undefined` if the site\n * has not been resolved yet (or the context is unavailable).\n *\n * `siteContext` is set by the site-context middleware (which runs early in the\n * middleware chain). We resolve it from the same `@salesforce/storefront-next-runtime`\n * module the template's middleware writes to, so both share the one `createContext`\n * singleton and this read observes the value that middleware set. Wrapped in a\n * try/catch because OTel instrumentation must never break the request pipeline.\n */\nfunction readSiteId(context: MaybeReadonlyContext): string | undefined {\n try {\n if (typeof context?.get !== 'function') return undefined;\n const resolved = context.get(siteContext) as { site?: { id?: string } } | null | undefined;\n return resolved?.site?.id;\n } catch {\n return undefined;\n }\n}\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 *\n * The resolved site id is read from `context` and stamped just before the span\n * ends. It is read at span end (not span start) because the site-context\n * middleware runs as part of the wrapped work — at span start the site may not be\n * resolved yet; by span end it is. The site id is a bounded, low-cardinality value.\n *\n * It is emitted under the key `site_name` (not `sfnext.site_id`): the MRT log→trace\n * bridge filters span attributes against an allow list and only keeps matching\n * keys, and `site_name` is on that list (confirmed with the MRT team) while\n * `site_id`/`sfnext.*` keys are not. The site config carries no separate display\n * name, so the site id is the value we have. See ../README.md (\"MRT log→trace bridge\").\n *\n * Route-level spans (`sfnext.loader`/`.action`/`.middleware`) always receive the\n * router `context`, so they carry `site_name`. The handler-level `sfnext.ssr` span\n * does NOT: React Router passes `context: undefined` to the request-handler\n * instrumentation (confirmed at runtime), so `readSiteId` returns `undefined`\n * there and the attribute is simply omitted. No branching needed — the\n * `if (siteId)` guard handles both cases uniformly.\n */\nasync function traced(\n spanName: string,\n attributes: Attributes,\n context: MaybeReadonlyContext,\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 const siteId = readSiteId(context);\n if (siteId) span.setAttribute('site_name', siteId);\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, context }) {\n await traced('sfnext.ssr', httpAttributes(request), context, 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 identifier is the URL route template (`http.route`,\n // the standard semantic-convention key — a bounded, low-cardinality value,\n // never the raw path with ids). The MRT log→trace bridge filters span\n // attributes against an allow list and drops keys not on it, so we only emit\n // attributes known to survive (`http.route` is a semantic-convention key the\n // bridge keeps; the framework's `rr.route.id` is not on the list and was\n // silently dropped, so it is no longer recorded).\n function routeAttributes(pattern: string): Attributes {\n return {\n [ATTR_HTTP_ROUTE]: pattern,\n };\n }\n\n route.instrument({\n async loader(handleLoader, { pattern, context }) {\n await traced('sfnext.loader', routeAttributes(pattern), context, handleLoader);\n },\n async action(handleAction, { pattern, context }) {\n await traced('sfnext.action', routeAttributes(pattern), context, handleAction);\n },\n async middleware(handleMiddleware, { pattern, context }) {\n await traced('sfnext.middleware', routeAttributes(pattern), context, 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 // oxlint-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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"}
1
+ {"version":3,"file":"server.js","names":["LEVEL_PRIORITY: Record<LogLevel, number>","MRT_NUMERIC_TO_LEVEL: Record<string, LogLevel>","overrideLevel: LogLevel | undefined","tracer","tracer: Tracer | null","context","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 // oxlint-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/* oxlint-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 * or on Managed Runtime by the numeric `MRT_LOG_LEVEL` (see {@link MRT_NUMERIC_TO_LEVEL}).\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\n/**\n * Type guard for a {@link LogLevel} name. Uses `Object.hasOwn` (not `in`) so\n * inherited properties like `toString` don't falsely validate an env value.\n */\nfunction isLogLevel(value: string): value is LogLevel {\n return Object.hasOwn(LEVEL_PRIORITY, value);\n}\n\n/**\n * Managed Runtime injects `MRT_LOG_LEVEL` as a numeric string from its own\n * `LogLevel` enum (`0=TRACE, 1=DEBUG, 2=INFO, 3=WARN, 4=ERROR, 5=FATAL`), not a\n * level name — so it must be mapped, not matched against {@link LEVEL_PRIORITY}'s\n * keys. `TRACE` and `FATAL` clamp to the closest level this logger supports.\n */\nconst MRT_NUMERIC_TO_LEVEL: Record<string, LogLevel> = {\n '0': 'debug', // TRACE\n '1': 'debug', // DEBUG\n '2': 'info', // INFO\n '3': 'warn', // WARN\n '4': 'error', // ERROR\n '5': 'error', // FATAL\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\n // MRT sets MRT_LOG_LEVEL as a number; resolve it via the numeric map first.\n const mrtLevel = process.env.MRT_LOG_LEVEL;\n if (mrtLevel && mrtLevel in MRT_NUMERIC_TO_LEVEL) return MRT_NUMERIC_TO_LEVEL[mrtLevel];\n\n // SFCC_LOG_LEVEL is a level name set by the developer.\n const sfccLevel = process.env.SFCC_LOG_LEVEL;\n if (sfccLevel && isLogLevel(sfccLevel)) return sfccLevel;\n\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 */\n// The tracer is cached on `globalThis`, not in a module-scoped `let`, so it can be\n// shared with code that never imports this module. `initTelemetry()` (called by the\n// platform instrumentation at server bootstrap) builds and registers the single\n// NodeTracerProvider and publishes its tracer here; the runtime package reads the same\n// slot via `@salesforce/storefront-next-runtime/otel` `getPlatformTracer()` to hand the\n// tracer to its data-store span seam. The two packages cannot share a module (the\n// dependency direction is dev → runtime), so they share the tracer through the global\n// symbol registry: `Symbol.for()` resolves to the same symbol everywhere. The runtime\n// side MUST use this exact key. This also collapses any accidental second module\n// instance of setup.ts (e.g. across Vite SSR module-runner boundaries) onto one tracer\n// rather than a competing provider — the same pattern used by UNDICI_REGISTERED_KEY below.\nconst TRACER_KEY = Symbol.for('sfnext.otel.tracer');\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 const cachedTracer = (globalThis as Record<symbol, Tracer | undefined>)[TRACER_KEY];\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 const tracer = provider.getTracer(SERVICE_NAME);\n (globalThis as Record<symbol, Tracer | undefined>)[TRACER_KEY] = tracer;\n return tracer;\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 * `http.route` attribute rather than the name, so traces aggregate by operation\n * 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 { siteContext } from '@salesforce/storefront-next-runtime/site-context';\nimport { initTelemetry } from '../setup';\n\nconst tracer: Tracer | null = process.env.SFNEXT_OTEL_ENABLED === 'true' ? initTelemetry() : null;\n\n/**\n * The read-only router context passed to each instrumentation callback. React\n * Router types it as `ReadonlyContext`, which is a `.get`-bearing provider only\n * when the app has enabled middleware (`MiddlewareEnabled`); the SDK's own\n * tsconfig doesn't resolve that flag, so we accept an optionally-`get`-bearing\n * value and narrow structurally at runtime.\n */\ntype MaybeReadonlyContext = { get?: (context: typeof siteContext) => unknown } | undefined;\n\n/**\n * Reads the resolved site from the router context, or `undefined` if the site\n * has not been resolved yet (or the context is unavailable).\n *\n * `siteContext` is set by the site-context middleware (which runs early in the\n * middleware chain). We resolve it from the same `@salesforce/storefront-next-runtime`\n * module the template's middleware writes to, so both share the one `createContext`\n * singleton and this read observes the value that middleware set. Wrapped in a\n * try/catch because OTel instrumentation must never break the request pipeline.\n */\nfunction readSiteId(context: MaybeReadonlyContext): string | undefined {\n try {\n if (typeof context?.get !== 'function') return undefined;\n const resolved = context.get(siteContext) as { site?: { id?: string } } | null | undefined;\n return resolved?.site?.id;\n } catch {\n return undefined;\n }\n}\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 *\n * The resolved site id is read from `context` and stamped just before the span\n * ends. It is read at span end (not span start) because the site-context\n * middleware runs as part of the wrapped work — at span start the site may not be\n * resolved yet; by span end it is. The site id is a bounded, low-cardinality value.\n *\n * It is emitted under the key `site_name` (not `sfnext.site_id`): the MRT log→trace\n * bridge filters span attributes against an allow list and only keeps matching\n * keys, and `site_name` is on that list (confirmed with the MRT team) while\n * `site_id`/`sfnext.*` keys are not. The site config carries no separate display\n * name, so the site id is the value we have. See ../README.md (\"MRT log→trace bridge\").\n *\n * Route-level spans (`sfnext.loader`/`.action`/`.middleware`) always receive the\n * router `context`, so they carry `site_name`. The handler-level `sfnext.ssr` span\n * does NOT: React Router passes `context: undefined` to the request-handler\n * instrumentation (confirmed at runtime), so `readSiteId` returns `undefined`\n * there and the attribute is simply omitted. No branching needed — the\n * `if (siteId)` guard handles both cases uniformly.\n */\nasync function traced(\n spanName: string,\n attributes: Attributes,\n context: MaybeReadonlyContext,\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 const siteId = readSiteId(context);\n if (siteId) span.setAttribute('site_name', siteId);\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, context }) {\n await traced('sfnext.ssr', httpAttributes(request), context, 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 identifier is the URL route template (`http.route`,\n // the standard semantic-convention key — a bounded, low-cardinality value,\n // never the raw path with ids). The MRT log→trace bridge filters span\n // attributes against an allow list and drops keys not on it, so we only emit\n // attributes known to survive (`http.route` is a semantic-convention key the\n // bridge keeps; the framework's `rr.route.id` is not on the list and was\n // silently dropped, so it is no longer recorded).\n function routeAttributes(pattern: string): Attributes {\n return {\n [ATTR_HTTP_ROUTE]: pattern,\n };\n }\n\n route.instrument({\n async loader(handleLoader, { pattern, context }) {\n await traced('sfnext.loader', routeAttributes(pattern), context, handleLoader);\n },\n async action(handleAction, { pattern, context }) {\n await traced('sfnext.action', routeAttributes(pattern), context, handleAction);\n },\n async middleware(handleMiddleware, { pattern, context }) {\n await traced('sfnext.middleware', routeAttributes(pattern), context, 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 // oxlint-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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"}
package/dist/flags.js CHANGED
@@ -8,6 +8,23 @@ const commonFlags = { [PROJECT_DIRECTORY_FLAG]: Flags.string({
8
8
  description: "Project directory",
9
9
  default: process.cwd()
10
10
  }) };
11
+ /**
12
+ * Restore the 1.x short flags on `MrtCommand.baseFlags`: `-t` for `--environment`/`--target`
13
+ * and `-o` for `--cloud-origin` (b2c-tooling-sdk 2.x moved it to `-u`).
14
+ */
15
+ function withLegacyMrtShortFlags(baseFlags) {
16
+ return {
17
+ ...baseFlags,
18
+ environment: {
19
+ ...baseFlags.environment,
20
+ charAliases: ["t"]
21
+ },
22
+ "cloud-origin": {
23
+ ...baseFlags["cloud-origin"],
24
+ charAliases: ["o"]
25
+ }
26
+ };
27
+ }
11
28
 
12
29
  //#endregion
13
- export { PROJECT_DIRECTORY_FLAG as n, commonFlags as r, PROJECT_DIRECTORY_CHAR as t };
30
+ export { withLegacyMrtShortFlags as i, PROJECT_DIRECTORY_FLAG as n, commonFlags as r, PROJECT_DIRECTORY_CHAR as t };
@@ -11,6 +11,23 @@ import { npmRunPathEnv } from "npm-run-path";
11
11
 
12
12
  //#region src/cartridge-services/react-router-config.ts
13
13
  let isCliAvailable = null;
14
+ function filePathEndsWithRoute(filePath, routeFile) {
15
+ const routeFileNormalized = routeFile.replace(/^\.\//, "");
16
+ return filePath === routeFileNormalized || filePath.endsWith(`/${routeFileNormalized}`);
17
+ }
18
+ /**
19
+ * Match the selected vertical route shadow to its canonical route module.
20
+ *
21
+ * `VERTICAL` is optional and selects the overlay directory reported by React Router,
22
+ * for example `furniture` for `verticals/furniture/routes/_app._index.tsx`.
23
+ */
24
+ function routeFileMatches(filePath, routeFile) {
25
+ if (filePathEndsWithRoute(filePath, routeFile)) return true;
26
+ const vertical = process.env.VERTICAL;
27
+ const verticalRoutePrefix = vertical ? `verticals/${vertical}/routes/` : void 0;
28
+ if (!verticalRoutePrefix || !routeFile.startsWith(verticalRoutePrefix)) return false;
29
+ return filePathEndsWithRoute(filePath, `routes/${routeFile.slice(verticalRoutePrefix.length)}`);
30
+ }
14
31
  function checkReactRouterCli(projectDirectory) {
15
32
  if (isCliAvailable !== null) return isCliAvailable;
16
33
  try {
@@ -64,7 +81,7 @@ function getReactRouterRoutes(projectDirectory) {
64
81
  * This ensures we get the exact same route resolution as React Router uses internally.
65
82
  * @param filePath - Absolute path to the route file
66
83
  * @param projectRoot - The project root directory
67
- * @returns The route path (e.g., '/cart', '/product/:productId')
84
+ * @returns The route path (e.g., '/cart', '/p/*')
68
85
  * @example
69
86
  * const route = filePathToRoute('/path/to/project/src/routes/_app.cart.tsx', '/path/to/project');
70
87
  * // Returns: '/cart'
@@ -72,10 +89,13 @@ function getReactRouterRoutes(projectDirectory) {
72
89
  function filePathToRoute(filePath, projectRoot) {
73
90
  const filePathPosix = filePath.replace(/\\/g, "/");
74
91
  const canonicalRoutes = flattenRoutes(getReactRouterRoutes(projectRoot)).filter((route) => !route.id.endsWith("--root-duplicate"));
75
- for (const route of canonicalRoutes) {
76
- const routeFilePosix = route.file.replace(/\\/g, "/");
77
- const routeFileNormalized = routeFilePosix.replace(/^\.\//, "");
78
- if (filePathPosix.endsWith(routeFilePosix) || filePathPosix.endsWith(`/${routeFilePosix}`) || filePathPosix.endsWith(routeFileNormalized) || filePathPosix.endsWith(`/${routeFileNormalized}`)) return route.path;
92
+ for (const route of canonicalRoutes) if (routeFileMatches(filePathPosix, route.file.replace(/\\/g, "/"))) {
93
+ const seoAliases = canonicalRoutes.filter((candidate) => candidate.id.startsWith(`${route.id}--seo-alias--`)).sort((left, right) => left.path.localeCompare(right.path));
94
+ if (seoAliases.length > 0) {
95
+ if (seoAliases.length > 1) logger.warn(`Multiple SEO route aliases match ${filePath}; using ${seoAliases[0].path} for Page Designer metadata.`);
96
+ return seoAliases[0].path;
97
+ }
98
+ return route.path;
79
99
  }
80
100
  logger.warn(`Could not find route for file: ${filePath}`);
81
101
  return "/unknown";
@@ -45,7 +45,7 @@ async function initializePlugins() {
45
45
  * registers their middleware and config sources with the global registries.
46
46
  *
47
47
  * @env {string} [MRT_PROJECT] - Project name for MRT deployments
48
- * @env {string} [MRT_TARGET] - Target environment for MRT deployments
48
+ * @env {string} [MRT_ENVIRONMENT] - Target environment for MRT deployments (MRT_TARGET also accepted)
49
49
  */
50
50
  const hook = async function(opts) {
51
51
  if (!(this.config.bin === "sfnext" || opts.id === "sfnext" || (opts.id?.startsWith("sfnext:") ?? false))) return;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../src/plugins/pageDesignerPreloadManifest.ts","../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":";;;;;AI6BgB,UJGC,iCAAA,CIOc;;;;;;;AGuG/B;;;UN3EiB,0BAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;8BA+Be;;;;;AE3DhC;AAkEA;;UD/EiB,mCAAA;;AEGjB;;;;ECHiB;AA4BjB;;;;ECOiB;AAyZjB;;;;ACxUA;;;;;;;AHjHgB,UDUC,2BAAA,CCV+B;;;;ACHhD;AA4BA;;;;ACOA;AAyZA;;;;ACxUA;;;;;;;;;;;;;;;;;;;mBJrEqB;;;;;;;;;kCAUe;;;;;;;;;;;;;;;;;;;;;iBAsBpB,qBAAA,UAA8B,8BAAmC;;;iBC5EjE,gCAAA,CAAA;;;EJGC,cAAA,CAAA,MAAA,EIOc,cJPmB,CAAA,EAAA,IAAA;;;;ICmCjC,GAAA,EAAA,IAAA;;;;;AE5BjB;AAkEA;;;;AC5EA;;;;ACHiB,UAAA,qBAAA,CAcH;EAcE;;;;ECOC;AAyZjB;;;;ACxUA;;YFtGc;;;;;;;;;;;;;iBAcE,qBAAA,WAAgC,wBAAwB;;;;UCOvD,wBAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAyZD,iBAAA,UAA2B,2BAA2B;;;;;;;;;;;;;;;;;;;;;;iBCxUtD,iBAAA"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/plugins/pageDesignerPreloadManifest.ts","../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":";;;;;AI6BgB,UJGC,iCAAA,CIOc;;;;;;;AGuG/B;;;UN3EiB,0BAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;8BA+Be;;;;;AE3DhC;AAkEA;;UD/EiB,mCAAA;;AEGjB;;;;ECHiB;AA4BjB;;;;ECOiB;AAyZjB;;;;ACxUA;;;;;;;AHjHgB,UDUC,2BAAA,CCV+B;;;;ACHhD;AA4BA;;;;ACOA;AAyZA;;;;ACxUA;;;;;;;;;;;;;;;;;;;mBJrEqB;;;;;;;;;kCAUe;;;;;;;;;;;;;;;;;;;;;iBAsBpB,qBAAA,UAA8B,8BAAmC;;;iBC5EjE,gCAAA,CAAA;;;EJGC,cAAA,CAAA,MAAA,EIOc,cJPmB,CAAA,EAAA,IAAA;;;;ICmCjC,GAAA,EAAA,IAAA;;;;;AE5BjB;AAkEA;;;;AC5EA;;;;ACHiB,UAAA,qBAAA,CAAqB;EA4BtB;;;;ECOC;AAyZjB;;;;ACxUA;;YFtGc;;;;;;;;;;;;;iBAcE,qBAAA,WAAgC,wBAAwB;;;;UCOvD,wBAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAyZD,iBAAA,UAA2B,2BAA2B;;;;;;;;;;;;;;;;;;;;;;iBCxUtD,iBAAA"}