@salesforce/storefront-next-dev 1.2.0-alpha.1 → 1.3.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.
- package/README.md +33 -0
- package/dist/cartridge-services/index.js +2 -1
- package/dist/cartridge-services/index.js.map +1 -1
- package/dist/commands/extensions/remove.js +6 -1
- package/dist/commands/locales/aggregate-extensions.js +18 -9
- package/dist/dependency-utils.js +19 -10
- package/dist/entry/server.js +5 -3
- package/dist/entry/server.js.map +1 -1
- package/dist/format-with-project-biome.js +32 -47
- package/dist/generate-cartridge.js +2 -1
- package/dist/index.d.ts +21 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +231 -13
- package/dist/index.js.map +1 -1
- package/dist/logger.js +1 -1
- package/dist/manage-extensions.js +11 -9
- package/dist/mrt/ssr.mjs +52 -51
- package/dist/mrt/ssr.mjs.map +1 -1
- package/dist/mrt/streamingHandler.mjs +58 -57
- package/dist/mrt/streamingHandler.mjs.map +1 -1
- package/dist/react-router/Scripts.d.ts.map +1 -1
- package/dist/react-router/Scripts.js +2 -1
- package/dist/react-router/Scripts.js.map +1 -1
- package/dist/server.js +5 -3
- package/package.json +4 -4
package/dist/dependency-utils.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t as logger } from "./logger.js";
|
|
2
|
-
import { n as
|
|
2
|
+
import { n as formatFilesWithBundledBiome } from "./format-with-project-biome.js";
|
|
3
3
|
import path from "path";
|
|
4
4
|
import fs from "fs";
|
|
5
5
|
|
|
@@ -31,20 +31,25 @@ async function trimExtensions(directory, selectedExtensions, extensionConfig) {
|
|
|
31
31
|
return;
|
|
32
32
|
}
|
|
33
33
|
const processDirectory = (dir) => {
|
|
34
|
+
const modifiedFiles$1 = [];
|
|
34
35
|
fs.readdirSync(dir).forEach((file) => {
|
|
35
36
|
const filePath = path.join(dir, file);
|
|
36
37
|
const stats = fs.statSync(filePath);
|
|
37
38
|
if (!filePath.includes("node_modules")) {
|
|
38
|
-
if (stats.isDirectory()) processDirectory(filePath);
|
|
39
|
-
else if (isSupportedFileExtension(file))
|
|
39
|
+
if (stats.isDirectory()) modifiedFiles$1.push(...processDirectory(filePath));
|
|
40
|
+
else if (isSupportedFileExtension(file)) {
|
|
41
|
+
if (processFile(filePath, extensions)) modifiedFiles$1.push(filePath);
|
|
42
|
+
}
|
|
40
43
|
}
|
|
41
44
|
});
|
|
45
|
+
return modifiedFiles$1;
|
|
42
46
|
};
|
|
43
|
-
processDirectory(directory);
|
|
47
|
+
const modifiedFiles = processDirectory(directory);
|
|
44
48
|
if (extensionConfig?.extensions) {
|
|
45
|
-
updateExtensionConfig(directory, extensions);
|
|
49
|
+
modifiedFiles.push(updateExtensionConfig(directory, extensions));
|
|
46
50
|
deleteExtensionFolders(directory, extensions, extensionConfig);
|
|
47
51
|
}
|
|
52
|
+
formatFilesWithBundledBiome(directory, modifiedFiles.filter((filePath) => fs.existsSync(filePath)));
|
|
48
53
|
const endTime = Date.now();
|
|
49
54
|
logger.debug(`Trim extensions took ${endTime - startTime}ms`);
|
|
50
55
|
}
|
|
@@ -60,7 +65,8 @@ function updateExtensionConfig(projectDirectory, extensionSelections) {
|
|
|
60
65
|
if (!extensionSelections[extensionKey]) delete extensionConfig.extensions[extensionKey];
|
|
61
66
|
});
|
|
62
67
|
const json = JSON.stringify({ extensions: extensionConfig.extensions }, null, 4);
|
|
63
|
-
fs.writeFileSync(extensionConfigPath,
|
|
68
|
+
fs.writeFileSync(extensionConfigPath, json, "utf8");
|
|
69
|
+
return extensionConfigPath;
|
|
64
70
|
}
|
|
65
71
|
/**
|
|
66
72
|
* Process a file to trim extension-specific code based on markers.
|
|
@@ -82,7 +88,7 @@ function processFile(filePath, extensions) {
|
|
|
82
88
|
logger.error(`Error deleting file ${filePath}: ${error.message}`);
|
|
83
89
|
throw e;
|
|
84
90
|
}
|
|
85
|
-
return;
|
|
91
|
+
return false;
|
|
86
92
|
}
|
|
87
93
|
}
|
|
88
94
|
const extKeys = Object.keys(extensions);
|
|
@@ -107,7 +113,7 @@ function processFile(filePath, extensions) {
|
|
|
107
113
|
extension: matchingExtension,
|
|
108
114
|
line: i
|
|
109
115
|
});
|
|
110
|
-
skippingBlock = extensions[
|
|
116
|
+
skippingBlock = blockMarkers.some(({ extension }) => extensions[extension] === false);
|
|
111
117
|
} else logger.warn(`Unknown marker found in ${filePath} at line ${i}: \n${line}`);
|
|
112
118
|
} else if (line.includes(BLOCK_MARKER_END)) {
|
|
113
119
|
if (Object.keys(extensions).find((extension) => line.includes(extension))) {
|
|
@@ -115,8 +121,9 @@ function processFile(filePath, extensions) {
|
|
|
115
121
|
if (blockMarkers.length === 0) throw new Error(`Block marker mismatch in ${filePath}, encountered end marker ${extension} without a matching start marker at line ${i}:\n${lines[i]}`);
|
|
116
122
|
const startMarker = blockMarkers.pop();
|
|
117
123
|
if (!extension || startMarker.extension !== extension) throw new Error(`Block marker mismatch in ${filePath}, expected end marker for ${startMarker.extension} but got ${extension} at line ${i}:\n${lines[i]}`);
|
|
118
|
-
|
|
119
|
-
|
|
124
|
+
const closedDisabledBlock = extensions[startMarker.extension] === false;
|
|
125
|
+
skippingBlock = blockMarkers.some(({ extension: blockExtension }) => extensions[blockExtension] === false);
|
|
126
|
+
if (closedDisabledBlock || skippingBlock) {
|
|
120
127
|
i++;
|
|
121
128
|
continue;
|
|
122
129
|
}
|
|
@@ -130,12 +137,14 @@ function processFile(filePath, extensions) {
|
|
|
130
137
|
if (newSource !== source) try {
|
|
131
138
|
fs.writeFileSync(filePath, newSource);
|
|
132
139
|
logger.debug(`Updated file ${filePath}`);
|
|
140
|
+
return true;
|
|
133
141
|
} catch (e) {
|
|
134
142
|
const error = e;
|
|
135
143
|
logger.error(`Error updating file ${filePath}: ${error.message}`);
|
|
136
144
|
throw e;
|
|
137
145
|
}
|
|
138
146
|
}
|
|
147
|
+
return false;
|
|
139
148
|
}
|
|
140
149
|
/**
|
|
141
150
|
* Delete extension folders for disabled extensions.
|
package/dist/entry/server.js
CHANGED
|
@@ -157,9 +157,10 @@ const SERVICE_NAME = "storefront-next";
|
|
|
157
157
|
* Getting the tracer directly from the provider bypasses the global registry
|
|
158
158
|
* entirely, guaranteeing the tracer uses our configured span processors.
|
|
159
159
|
*/
|
|
160
|
-
|
|
160
|
+
const TRACER_KEY = Symbol.for("sfnext.otel.tracer");
|
|
161
161
|
const UNDICI_REGISTERED_KEY = Symbol.for("sfnext.otel.undici_registered");
|
|
162
162
|
function initTelemetry() {
|
|
163
|
+
const cachedTracer = globalThis[TRACER_KEY];
|
|
163
164
|
if (cachedTracer) return cachedTracer;
|
|
164
165
|
try {
|
|
165
166
|
const provider = new NodeTracerProvider({
|
|
@@ -185,8 +186,9 @@ function initTelemetry() {
|
|
|
185
186
|
} })]
|
|
186
187
|
});
|
|
187
188
|
}
|
|
188
|
-
|
|
189
|
-
|
|
189
|
+
const tracer$1 = provider.getTracer(SERVICE_NAME);
|
|
190
|
+
globalThis[TRACER_KEY] = tracer$1;
|
|
191
|
+
return tracer$1;
|
|
190
192
|
} catch (error) {
|
|
191
193
|
logger.error("[otel] Failed to initialize OpenTelemetry:", error);
|
|
192
194
|
return null;
|
package/dist/entry/server.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.js","names":["LEVEL_PRIORITY: Record<LogLevel, number>","overrideLevel: LogLevel | undefined","cachedTracer: Tracer | null","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 */\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 * `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>","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,6 +1,6 @@
|
|
|
1
1
|
import { t as logger } from "./logger.js";
|
|
2
2
|
import { spawnSync } from "child_process";
|
|
3
|
-
import {
|
|
3
|
+
import { dirname, join, relative } from "path";
|
|
4
4
|
import { createRequire } from "module";
|
|
5
5
|
import { fileURLToPath } from "url";
|
|
6
6
|
|
|
@@ -8,68 +8,53 @@ import { fileURLToPath } from "url";
|
|
|
8
8
|
/** This module's own path — the resolution root for the SDK-bundled Biome fallback. */
|
|
9
9
|
const HERE = fileURLToPath(import.meta.url);
|
|
10
10
|
/**
|
|
11
|
-
* Format
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* The SDK generates two kinds of files into a customer project — the extension `config.json`
|
|
15
|
-
* (`trim-extensions.ts`) and the static component registry (`staticRegistry.ts`). Both must
|
|
16
|
-
* be emitted in the exact byte shape the project's formatter produces, or the generated file
|
|
17
|
-
* fails `pnpm lint` on a fresh project / churns on every `biome format --write` (W-23074938).
|
|
11
|
+
* Format every file Biome recognizes under `directory` in place with the consuming project's
|
|
12
|
+
* Biome, so generated files (e.g. cartridge metadata JSON) match `biome format` / `pnpm lint:fix`.
|
|
18
13
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* project's first `pnpm install`), we fall back to the SDK-bundled `@biomejs/biome` (a hard
|
|
23
|
-
* dependency of this package, pinned to the template's version). Either way the file's
|
|
24
|
-
* committed `biome.json` governs the output: Biome discovers its config by walking up from
|
|
25
|
-
* the working directory, so we spawn it with `cwd` set to the file's directory and pass the
|
|
26
|
-
* file's basename via `--stdin-file-path` (which drives both parser selection and formatting).
|
|
14
|
+
* Unlike {@link formatWithProjectBiome} (single file via stdin), this runs `biome format --write`
|
|
15
|
+
* against the directory so Biome discovers the project's `biome.json` by walking up from it and
|
|
16
|
+
* applies the project's formatting to the written files directly.
|
|
27
17
|
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* an unformatted-but-valid file is recoverable by the customer running `biome format --write`.
|
|
18
|
+
* Fail-safe: when Biome can't be resolved or the run fails, the files are left as written (valid,
|
|
19
|
+
* just unformatted) and a warning is logged — generation never breaks over formatting.
|
|
31
20
|
*
|
|
32
|
-
* @param
|
|
33
|
-
* @param filePath - The file's path (drives parser selection + config resolution + cwd).
|
|
34
|
-
* @returns The Biome-formatted content, or the original content if Biome is unavailable.
|
|
21
|
+
* @param directory - Directory whose Biome-recognized files should be formatted in place.
|
|
35
22
|
*/
|
|
36
|
-
function
|
|
37
|
-
const biomeBin = resolveBiomeBin(
|
|
23
|
+
function formatDirectoryWithProjectBiome(directory) {
|
|
24
|
+
const biomeBin = resolveBiomeBin(join(directory, "biome-resolution-root.js"));
|
|
38
25
|
if (!biomeBin) {
|
|
39
|
-
logger.warn(`⚠️ Biome could not be resolved; ${
|
|
40
|
-
return
|
|
26
|
+
logger.warn(`⚠️ Biome could not be resolved; generated files in ${directory} were left unformatted.`);
|
|
27
|
+
return;
|
|
41
28
|
}
|
|
42
29
|
const result = spawnSync(process.execPath, [
|
|
43
30
|
biomeBin,
|
|
44
31
|
"format",
|
|
45
|
-
|
|
32
|
+
"--write",
|
|
33
|
+
"."
|
|
46
34
|
], {
|
|
47
|
-
cwd:
|
|
48
|
-
input: content,
|
|
35
|
+
cwd: directory,
|
|
49
36
|
encoding: "utf8"
|
|
50
37
|
});
|
|
51
|
-
if (result.status !== 0
|
|
38
|
+
if (result.status !== 0) {
|
|
52
39
|
const detail = result.stderr?.trim() || `exit code ${result.status}`;
|
|
53
|
-
logger.warn(`⚠️
|
|
54
|
-
return
|
|
40
|
+
logger.warn(`⚠️ Some generated files in ${directory} could not be formatted by Biome: ${detail}`);
|
|
41
|
+
return;
|
|
55
42
|
}
|
|
56
|
-
|
|
43
|
+
logger.debug(`✅ Formatted generated files in ${directory} with Biome`);
|
|
57
44
|
}
|
|
58
45
|
/**
|
|
59
|
-
* Format
|
|
60
|
-
* Biome, so generated files (e.g. cartridge metadata JSON) match `biome format` / `pnpm lint:fix`.
|
|
61
|
-
*
|
|
62
|
-
* Unlike {@link formatWithProjectBiome} (single file via stdin), this runs `biome format --write`
|
|
63
|
-
* against the directory so Biome discovers the project's `biome.json` by walking up from it and
|
|
64
|
-
* applies the project's formatting to the written files directly.
|
|
46
|
+
* Format only files changed by generation with the SDK-bundled Biome.
|
|
65
47
|
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
48
|
+
* Extension trimming can touch customer-owned source files, so formatting the project root would
|
|
49
|
+
* rewrite unrelated files. The SDK's pinned Biome keeps this automatic path independent of any
|
|
50
|
+
* target-project executable while the target project's biome.json still determines formatting.
|
|
68
51
|
*
|
|
69
|
-
* @param directory -
|
|
52
|
+
* @param directory - Project root containing the Biome configuration.
|
|
53
|
+
* @param filePaths - Absolute paths of files changed by generation.
|
|
70
54
|
*/
|
|
71
|
-
function
|
|
72
|
-
|
|
55
|
+
function formatFilesWithBundledBiome(directory, filePaths) {
|
|
56
|
+
if (filePaths.length === 0) return;
|
|
57
|
+
const biomeBin = resolveBiomeBin(HERE);
|
|
73
58
|
if (!biomeBin) {
|
|
74
59
|
logger.warn(`⚠️ Biome could not be resolved; generated files in ${directory} were left unformatted.`);
|
|
75
60
|
return;
|
|
@@ -78,7 +63,7 @@ function formatDirectoryWithProjectBiome(directory) {
|
|
|
78
63
|
biomeBin,
|
|
79
64
|
"format",
|
|
80
65
|
"--write",
|
|
81
|
-
|
|
66
|
+
...filePaths.map((filePath) => relative(directory, filePath))
|
|
82
67
|
], {
|
|
83
68
|
cwd: directory,
|
|
84
69
|
encoding: "utf8"
|
|
@@ -88,7 +73,7 @@ function formatDirectoryWithProjectBiome(directory) {
|
|
|
88
73
|
logger.warn(`⚠️ Some generated files in ${directory} could not be formatted by Biome: ${detail}`);
|
|
89
74
|
return;
|
|
90
75
|
}
|
|
91
|
-
logger.debug(`✅ Formatted generated files in ${directory} with Biome`);
|
|
76
|
+
logger.debug(`✅ Formatted ${filePaths.length} generated files in ${directory} with Biome`);
|
|
92
77
|
}
|
|
93
78
|
/**
|
|
94
79
|
* Resolve a Biome CLI binary path, preferring the consuming project's install and falling back
|
|
@@ -105,4 +90,4 @@ function resolveBiomeBin(filePath) {
|
|
|
105
90
|
}
|
|
106
91
|
|
|
107
92
|
//#endregion
|
|
108
|
-
export {
|
|
93
|
+
export { formatFilesWithBundledBiome as n, formatDirectoryWithProjectBiome as t };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,22 @@
|
|
|
1
1
|
import { Project } from "ts-morph";
|
|
2
2
|
import { Plugin, ResolvedConfig } from "vite";
|
|
3
3
|
|
|
4
|
+
//#region src/plugins/pageDesignerPreloadManifest.d.ts
|
|
5
|
+
|
|
6
|
+
interface PageDesignerPreloadCompressionConfig {
|
|
7
|
+
brotli?: {
|
|
8
|
+
quality?: number;
|
|
9
|
+
};
|
|
10
|
+
gzip?: {
|
|
11
|
+
level?: number;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
interface PageDesignerPreloadManifestConfig {
|
|
15
|
+
path?: string;
|
|
16
|
+
requiredTypeIds?: string[];
|
|
17
|
+
compression?: PageDesignerPreloadCompressionConfig;
|
|
18
|
+
}
|
|
19
|
+
//#endregion
|
|
4
20
|
//#region src/plugins/staticRegistry.d.ts
|
|
5
21
|
|
|
6
22
|
/**
|
|
@@ -29,6 +45,11 @@ interface StaticRegistryPluginConfig {
|
|
|
29
45
|
* @default true
|
|
30
46
|
*/
|
|
31
47
|
failOnError?: boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Emit and embed a compact Page Designer component preload manifest.
|
|
50
|
+
* Disabled by default.
|
|
51
|
+
*/
|
|
52
|
+
preloadManifest?: boolean | PageDesignerPreloadManifestConfig;
|
|
32
53
|
}
|
|
33
54
|
//#endregion
|
|
34
55
|
//#region src/plugins/eventInstrumentationValidator.d.ts
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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":"
|
|
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":";;;;;UAiCiB,oCAAA;EIJD,MAAA,CAAA,EAAA;;;;ICHC,KAAA,CAAA,EAAA,MAAA;EA4BD,CAAA;;ULhBC,iCAAA;;EMuBA,eAAA,CAAA,EAAA,MAAA,EAAA;EAyZD,WAAA,CAAA,EN7aE,oCM6ayB;;;;;;ACxU3C;;UN3EiB,0BAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;8BA+Be;;;;;;AE3DhC;AAkEA;UD/EiB,mCAAA;;;AEGjB;;;;ACHA;AA4BA;;;;ACOA;AAyZA;;;;;;;AH7WA;;;UAlEiB,2BAAA;ECVD;;;;ACHhB;AA4BA;;;;ACOA;AAyZA;;;;ACxUA;;;;;;;;;;;;;;;;;;mBJrEqB;;;;;;;;;kCAUe;;;;;;;;;;;;;;;;;;;;;iBAsBpB,qBAAA,UAA8B,8BAAmC;;;iBC5EjE,gCAAA,CAAA;;;EJIC,cAAA,CAAA,MAAA,EIMc,cJNd,CAAA,EAAoC,IAAA;EAKpC,UAAA,EAAA,EAAA,IAAA;;;;EC6BA,CAAA,GAAA,IAAA;;;;;AE5BjB;AAkEA;;;;AC5EA;;;UCHiB,qBAAA;EAAA;AA4BjB;;;;ACOA;AAyZA;;;;ACxUA;YFtGc;;;;;;;;;;;;;iBAcE,qBAAA,WAAgC,wBAAwB;;;;UCOvD,wBAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAyZD,iBAAA,UAA2B,2BAA2B;;;;;;;;;;;;;;;;;;;;;;iBCxUtD,iBAAA"}
|