@salesforce/storefront-next-dev 1.0.1 → 1.1.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.
@@ -0,0 +1,279 @@
1
+ import { t as logger } from "../../logger.js";
2
+ import "../../logger2.js";
3
+ import { r as commonFlags } from "../../flags.js";
4
+ import { t as GENERATED_EXTENSION_DIRS } from "../../constants.js";
5
+ import { Command, Flags } from "@oclif/core";
6
+ import { existsSync } from "node:fs";
7
+ import { join } from "node:path";
8
+ import { mkdir, readFile, readdir, writeFile } from "fs/promises";
9
+ import { parse } from "@babel/parser";
10
+
11
+ //#region src/config/aggregate-extension-config.ts
12
+ /** Apache License 2.0 header text for generated files. Inlined to avoid path resolution issues in standalone projects. */
13
+ const APACHE_LICENSE_HEADER = [
14
+ `Copyright ${(/* @__PURE__ */ new Date()).getFullYear()} Salesforce, Inc.`,
15
+ "",
16
+ "Licensed under the Apache License, Version 2.0 (the \"License\");",
17
+ "you may not use this file except in compliance with the License.",
18
+ "You may obtain a copy of the License at",
19
+ "",
20
+ " http://www.apache.org/licenses/LICENSE-2.0",
21
+ "",
22
+ "Unless required by applicable law or agreed to in writing, software",
23
+ "distributed under the License is distributed on an \"AS IS\" BASIS,",
24
+ "WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.",
25
+ "See the License for the specific language governing permissions and",
26
+ "limitations under the License."
27
+ ].join("\n");
28
+ const OUTPUT_DIR_NAME = GENERATED_EXTENSION_DIRS.config;
29
+ const LOCALES_DIR_NAME = GENERATED_EXTENSION_DIRS.locales;
30
+ /**
31
+ * A camelCased folder name is only usable as a config key if it is also a plain JS identifier:
32
+ * a leading letter followed by letters or digits. Folder names that camelCase to anything else
33
+ * (a dot, a leading digit, an empty string) would emit a barrel that fails to parse.
34
+ */
35
+ const VALID_CONFIG_KEY = /^[a-zA-Z][a-zA-Z0-9]*$/;
36
+ const CLIENT_MODE = {
37
+ sourceFileName: "config.ts",
38
+ outputFileName: "index.ts",
39
+ barrelFooterComment: [
40
+ "// Each extension's config.ts default export is namespaced by the camelCase of its folder",
41
+ "// name (e.g. loqate-address-verification -> loqateAddressVerification), making it available",
42
+ "// at config.app.extension.<key> and overridable via PUBLIC__app__extension__<key>__<setting>."
43
+ ].join("\n")
44
+ };
45
+ const SERVER_MODE = {
46
+ sourceFileName: "server-config.ts",
47
+ outputFileName: "server.ts",
48
+ barrelFooterComment: [
49
+ "// Each extension's server-config.ts default export is namespaced by the camelCase of its",
50
+ "// folder name (e.g. loqate-address-verification -> loqateAddressVerification), making it",
51
+ "// available at config.app.serverExtension.<key> on the server only. Values never reach the",
52
+ "// browser: the client extractor strips app.serverExtension before window.__APP_CONFIG__,",
53
+ "// and a Vite plugin fails the build if any client chunk imports this barrel. There is no",
54
+ "// PUBLIC__ override path by design — the AST validator forbids process.env, so server-only",
55
+ "// secrets must be read from process.env in a route handler instead."
56
+ ].join("\n")
57
+ };
58
+ function getDefaultDirs(projectDirectory) {
59
+ const extensionsDir = join(projectDirectory, "src", "extensions");
60
+ return {
61
+ EXTENSIONS_DIR: extensionsDir,
62
+ OUTPUT_DIR: join(extensionsDir, OUTPUT_DIR_NAME)
63
+ };
64
+ }
65
+ /** Convert kebab-case to PascalCase (e.g. `store-locator` → `StoreLocator`). */
66
+ function toPascalCase(str) {
67
+ return str.split("-").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
68
+ }
69
+ /**
70
+ * Convert a kebab-case extension folder name to the camelCase key it occupies under
71
+ * `app.extension` (e.g. `loqate-address-verification` → `loqateAddressVerification`). This
72
+ * is the key merchants reference as `PUBLIC__app__extension__<key>__<setting>`.
73
+ */
74
+ function toCamelCase(str) {
75
+ const pascal = toPascalCase(str);
76
+ return pascal.charAt(0).toLowerCase() + pascal.slice(1);
77
+ }
78
+ /** Value node types allowed inside a config object: static data only, no computed expressions. */
79
+ const STATIC_VALUE_TYPES = new Set([
80
+ "StringLiteral",
81
+ "NumericLiteral",
82
+ "BooleanLiteral",
83
+ "NullLiteral",
84
+ "ArrayExpression",
85
+ "ObjectExpression",
86
+ "TSAsExpression"
87
+ ]);
88
+ /**
89
+ * Assert an extension's config module is a static, default-exported object literal — and nothing
90
+ * more. Generated barrels import each module as a live ES module, evaluated at build, typegen,
91
+ * `pnpm dev`, and server startup; since extensions are third-party and distributable, any
92
+ * top-level statement (an import, a `fetch`, a `process.env` read) would run arbitrary vendor
93
+ * code in those contexts and could leak secrets into the client bundle. Validating the shape
94
+ * at discovery time turns that supply-chain risk into a clear build-time error. The same rule
95
+ * applies to both client `config.ts` and server `server-config.ts` — the server barrel is
96
+ * structurally fenced from the client, but a vendor calling out to a network or reading env
97
+ * at module-eval time is still a build-time hazard we don't want to inherit.
98
+ */
99
+ function assertStaticConfigModule(filePath, source) {
100
+ const reject = (reason) => {
101
+ throw new Error(`Extension config "${filePath}" must default-export a plain object of static values. ${reason} Config files are imported as live modules at build and server startup, so they may not import, call functions, or read process.env. Move dynamic/server-only values to a route handler.`);
102
+ };
103
+ const ast = parse(source, {
104
+ sourceType: "module",
105
+ plugins: ["typescript"]
106
+ });
107
+ let declaration;
108
+ for (const node of ast.program.body) if (node.type === "ExportDefaultDeclaration") declaration = node.declaration;
109
+ else if (node.type !== "EmptyStatement") reject(`Found a top-level \`${node.type}\` (only \`export default { … }\` is allowed).`);
110
+ if (!declaration) {
111
+ reject("No default export found.");
112
+ return;
113
+ }
114
+ const unwrap = (n) => n.type === "TSAsExpression" && n.expression ? n.expression : n;
115
+ const assertStaticValue = (node) => {
116
+ const v = unwrap(node);
117
+ if (!STATIC_VALUE_TYPES.has(v.type)) reject(`Found a non-static \`${v.type}\` value.`);
118
+ if (v.type === "ObjectExpression") for (const prop of v.properties ?? []) {
119
+ if (prop.type !== "ObjectProperty") reject(`Found a \`${prop.type}\` (spreads and methods are not allowed).`);
120
+ if (prop.value) assertStaticValue(prop.value);
121
+ }
122
+ else if (v.type === "ArrayExpression") {
123
+ for (const el of v.elements ?? []) if (el) assertStaticValue(el);
124
+ }
125
+ };
126
+ assertStaticValue(unwrap(declaration));
127
+ }
128
+ /**
129
+ * Find every extension folder that ships the mode's source file. The output `config/`
130
+ * directory and the locale aggregator's `locales/` directory are siblings of real
131
+ * extensions, so both are skipped. Each discovered file is validated as a static object
132
+ * literal before it joins the list (see `assertStaticConfigModule`). Results are sorted
133
+ * by folder name for stable output.
134
+ *
135
+ * Imports in the generated barrel target the source file's basename without extension —
136
+ * `../<ext>/config` for client, `../<ext>/server-config` for server. The server barrel
137
+ * sits alongside the client barrel in `src/extensions/config/`, so both barrels resolve
138
+ * extension paths through the same `../` parent.
139
+ */
140
+ async function findExtensionsWithConfigByMode(extensionsDir, mode) {
141
+ const extensions = [];
142
+ const importBasename = mode.sourceFileName.replace(/\.ts$/, "");
143
+ try {
144
+ const entries = await readdir(extensionsDir, { withFileTypes: true });
145
+ for (const entry of entries) {
146
+ if (!entry.isDirectory() || entry.name === OUTPUT_DIR_NAME || entry.name === LOCALES_DIR_NAME) continue;
147
+ const sourcePath = join(extensionsDir, entry.name, mode.sourceFileName);
148
+ if (existsSync(sourcePath)) {
149
+ assertStaticConfigModule(`${entry.name}/${mode.sourceFileName}`, await readFile(sourcePath, "utf8"));
150
+ extensions.push({
151
+ name: entry.name,
152
+ path: `../${entry.name}/${importBasename}`
153
+ });
154
+ }
155
+ }
156
+ } catch (error) {
157
+ if (error.code !== "ENOENT") throw error;
158
+ }
159
+ return extensions.sort((a, b) => a.name.localeCompare(b.name));
160
+ }
161
+ /**
162
+ * Build the barrel that re-exports each extension's config default under its camelCase key.
163
+ * Imports are widened (no `as const`) so the merged `app.extension` / `app.serverExtension`
164
+ * type stays mutable and a value like `apiKey: ''` types as `string`, not the literal `''`.
165
+ *
166
+ * Throws if two folders collapse to the same camelCase key — config feeds runtime values
167
+ * like API keys, so a silent last-wins could route a setting to the wrong extension.
168
+ */
169
+ function generateBarrelByMode(extensions, mode) {
170
+ const header = `${`/**\n${APACHE_LICENSE_HEADER.split("\n").map((line) => line ? ` * ${line}` : " *").join("\n")}\n */`}
171
+
172
+ // NOTE: This file is auto-generated. Do not edit manually.
173
+ // Run 'pnpm config:aggregate-extensions' to regenerate this file.
174
+
175
+ `;
176
+ if (extensions.length === 0) return `${header}// No extension config files found\nexport default {};\n`;
177
+ const keyByExtension = /* @__PURE__ */ new Map();
178
+ for (const ext of extensions) {
179
+ const key = toCamelCase(ext.name);
180
+ if (ext.name.includes("_")) throw new Error(`Extension folder "${ext.name}" uses underscores. Use hyphens so the name camelCases to the expected config key (e.g. "store-locator" → "storeLocator", not "store_locator").`);
181
+ if (!VALID_CONFIG_KEY.test(key)) throw new Error(`Extension folder "${ext.name}" maps to an invalid config key "${key}". Use a folder name of letters, digits, and hyphens that starts with a letter (e.g. "loqate-address-verification" → "loqateAddressVerification").`);
182
+ const existing = keyByExtension.get(key);
183
+ if (existing) throw new Error(`Extension config key collision: "${existing}" and "${ext.name}" both map to "${key}". Rename one extension folder so each resolves to a distinct config namespace.`);
184
+ keyByExtension.set(key, ext.name);
185
+ }
186
+ const imports = extensions.map((ext) => `import ${toCamelCase(ext.name)}Config from '${ext.path}';`).join("\n");
187
+ const exports = extensions.map((ext) => {
188
+ const key = toCamelCase(ext.name);
189
+ return ` ${key}: ${key}Config,`;
190
+ }).join("\n");
191
+ return `${header}${imports}
192
+
193
+ ${mode.barrelFooterComment}
194
+ export default {
195
+ ${exports}
196
+ };
197
+ `;
198
+ }
199
+ /**
200
+ * Discover every extension's source file under the given mode and write the matching barrel
201
+ * if its content has changed. Skipping the write when content is unchanged keeps the Vite
202
+ * watcher quiet — a no-op rewrite would touch the file's mtime and trigger a self-induced
203
+ * reload loop on every `pnpm dev` prestep.
204
+ */
205
+ async function aggregateByMode(options, mode) {
206
+ const { projectDirectory = process.cwd(), silent = false } = options;
207
+ const { OUTPUT_DIR, EXTENSIONS_DIR } = options.dirs ?? getDefaultDirs(projectDirectory);
208
+ const log = (message, ...args) => {
209
+ if (!silent) logger.debug(message, ...args);
210
+ };
211
+ try {
212
+ log(`🔍 Scanning for extension ${mode.sourceFileName} files...`);
213
+ const extensions = await findExtensionsWithConfigByMode(EXTENSIONS_DIR, mode);
214
+ const content = generateBarrelByMode(extensions, mode);
215
+ await mkdir(OUTPUT_DIR, { recursive: true });
216
+ const filePath = join(OUTPUT_DIR, mode.outputFileName);
217
+ if (await readFile(filePath, "utf8").catch(() => null) !== content) await writeFile(filePath, content, "utf8");
218
+ log(`✅ Generated: src/extensions/config/${mode.outputFileName} (${extensions.length} extension(s))`);
219
+ return {
220
+ extensions: extensions.map((ext) => toCamelCase(ext.name)),
221
+ filePath
222
+ };
223
+ } catch (error) {
224
+ if (!silent) logger.error(`❌ Error generating extension ${mode.outputFileName}: ${String(error)}`);
225
+ throw error;
226
+ }
227
+ }
228
+ /**
229
+ * Discover every extension's `config.ts` AND `server-config.ts` and regenerate both barrels:
230
+ * `src/extensions/config/index.ts` (merged into `app.extension`, reaches the client) and
231
+ * `src/extensions/config/server.ts` (merged into `app.serverExtension`, server-only — the
232
+ * loader strips it before React Router serializes the hydration payload, and a Vite plugin
233
+ * fails the build if any client chunk imports the server barrel).
234
+ *
235
+ * Runs both modes in parallel: the source files are distinct (`config.ts` vs
236
+ * `server-config.ts`), the output files are distinct (`index.ts` vs `server.ts`), and each
237
+ * mode does its own mtime-aware write — there's no shared state between them. Folding both
238
+ * into one CLI invocation closes a class of footgun where a contributor regenerates one
239
+ * barrel but not the other and ships an inconsistent `app.extension` / `app.serverExtension`
240
+ * pair (see PR #1964 review).
241
+ *
242
+ * Used as the `sfnext config aggregate-extensions` build prestep (mirrors `sfnext locales
243
+ * aggregate-extensions`).
244
+ */
245
+ async function aggregateExtensionConfig(options = {}) {
246
+ const [client, server] = await Promise.all([aggregateByMode(options, CLIENT_MODE), aggregateByMode(options, SERVER_MODE)]);
247
+ return {
248
+ client,
249
+ server
250
+ };
251
+ }
252
+
253
+ //#endregion
254
+ //#region src/commands/config/aggregate-extensions.ts
255
+ var AggregateExtensions = class AggregateExtensions extends Command {
256
+ static description = "Aggregate extension config.ts and server-config.ts files into the app.extension and app.serverExtension barrels";
257
+ static examples = [
258
+ "<%= config.bin %> <%= command.id %>",
259
+ "<%= config.bin %> <%= command.id %> -d ./my-project",
260
+ "<%= config.bin %> <%= command.id %> --silent"
261
+ ];
262
+ static flags = {
263
+ ...commonFlags,
264
+ silent: Flags.boolean({
265
+ description: "Suppress output",
266
+ default: false
267
+ })
268
+ };
269
+ async run() {
270
+ const { flags } = await this.parse(AggregateExtensions);
271
+ await aggregateExtensionConfig({
272
+ projectDirectory: flags["project-directory"],
273
+ silent: flags.silent
274
+ });
275
+ }
276
+ };
277
+
278
+ //#endregion
279
+ export { AggregateExtensions as default };
@@ -1,6 +1,7 @@
1
1
  import { t as logger } from "../../logger.js";
2
2
  import "../../logger2.js";
3
3
  import { r as commonFlags } from "../../flags.js";
4
+ import { t as GENERATED_EXTENSION_DIRS } from "../../constants.js";
4
5
  import { Command, Flags } from "@oclif/core";
5
6
  import { existsSync } from "node:fs";
6
7
  import { join } from "node:path";
@@ -54,7 +55,7 @@ async function discoverLocales(dirs) {
54
55
  try {
55
56
  const extensions = await readdir(EXTENSIONS_DIR, { withFileTypes: true });
56
57
  for (const extension of extensions) {
57
- if (!extension.isDirectory() || extension.name === "locales") continue;
58
+ if (!extension.isDirectory() || extension.name === GENERATED_EXTENSION_DIRS.locales || extension.name === GENERATED_EXTENSION_DIRS.config) continue;
58
59
  const localesPath = join(EXTENSIONS_DIR, extension.name, "locales");
59
60
  if (!existsSync(localesPath)) continue;
60
61
  const localeEntries = await readdir(localesPath, { withFileTypes: true });
@@ -71,7 +72,7 @@ async function findExtensionsWithLocale(locale, extensionsDir) {
71
72
  try {
72
73
  const extensionEntries = await readdir(extensionsDir, { withFileTypes: true });
73
74
  for (const entry of extensionEntries) {
74
- if (!entry.isDirectory() || entry.name === "locales") continue;
75
+ if (!entry.isDirectory() || entry.name === GENERATED_EXTENSION_DIRS.locales || entry.name === GENERATED_EXTENSION_DIRS.config) continue;
75
76
  if (existsSync(join(extensionsDir, entry.name, "locales", locale, "translations.json"))) extensions.push({
76
77
  name: entry.name,
77
78
  path: `@/extensions/${entry.name}/locales/${locale}/translations.json`
@@ -92,6 +92,8 @@ var Push = class Push extends MrtCommand {
92
92
  this.log(`Uploading bundle to ${origin}`);
93
93
  const result = await uploadBundle(client, projectSlug, bundle, target);
94
94
  this.log(`Bundle ${result.bundleId} uploaded`);
95
+ const warnings = result.warnings ?? [];
96
+ for (const w of warnings) this.warn(w);
95
97
  if (flags.wait && target) {
96
98
  this.log(`Waiting for deployment to ${target}...`);
97
99
  let lastState = "";
@@ -0,0 +1,29 @@
1
+ //#region src/extensibility/constants.ts
2
+ /**
3
+ * Copyright 2026 Salesforce, Inc.
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ /**
18
+ * Names of the generated barrel directories under `src/extensions/`. Each aggregator writes
19
+ * into one (`locales/`, `config/`) and both must skip both during discovery, so neither
20
+ * treats the other's output — or its own — as an extension. Shared here so the skip lists in
21
+ * the locale and config aggregators can't drift apart.
22
+ */
23
+ const GENERATED_EXTENSION_DIRS = {
24
+ locales: "locales",
25
+ config: "config"
26
+ };
27
+
28
+ //#endregion
29
+ export { GENERATED_EXTENSION_DIRS as t };
@@ -1,8 +1,8 @@
1
- import { SpanStatusCode } from "@opentelemetry/api";
1
+ import { SpanStatusCode, propagation } from "@opentelemetry/api";
2
2
  import { ATTR_HTTP_REQUEST_METHOD, ATTR_SERVICE_NAME, ATTR_URL_PATH } from "@opentelemetry/semantic-conventions";
3
3
  import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
4
- import { ConsoleSpanExporter, SimpleSpanProcessor } from "@opentelemetry/sdk-trace-base";
5
- import { ExportResultCode, hrTimeToTimeStamp } from "@opentelemetry/core";
4
+ import { AlwaysOnSampler, ConsoleSpanExporter, ParentBasedSampler, SimpleSpanProcessor } from "@opentelemetry/sdk-trace-base";
5
+ import { ExportResultCode, W3CTraceContextPropagator, hrTimeToTimeStamp } from "@opentelemetry/core";
6
6
  import { Resource } from "@opentelemetry/resources";
7
7
  import { registerInstrumentations } from "@opentelemetry/instrumentation";
8
8
  import { UndiciInstrumentation } from "@opentelemetry/instrumentation-undici";
@@ -124,9 +124,13 @@ const UNDICI_REGISTERED_KEY = Symbol.for("sfnext.otel.undici_registered");
124
124
  function initTelemetry() {
125
125
  if (cachedTracer) return cachedTracer;
126
126
  try {
127
- const provider = new NodeTracerProvider({ resource: new Resource({ [ATTR_SERVICE_NAME]: SERVICE_NAME }) });
127
+ const provider = new NodeTracerProvider({
128
+ resource: new Resource({ [ATTR_SERVICE_NAME]: SERVICE_NAME }),
129
+ sampler: new ParentBasedSampler({ root: new AlwaysOnSampler() })
130
+ });
128
131
  provider.addSpanProcessor(new SimpleSpanProcessor(new MrtConsoleSpanExporter()));
129
132
  provider.register();
133
+ propagation.setGlobalPropagator(new W3CTraceContextPropagator());
130
134
  if (!globalThis[UNDICI_REGISTERED_KEY]) {
131
135
  globalThis[UNDICI_REGISTERED_KEY] = true;
132
136
  registerInstrumentations({
@@ -1 +1 @@
1
- {"version":3,"file":"server.js","names":["LEVEL_PRIORITY: Record<LogLevel, number>","overrideLevel: LogLevel | undefined","cachedTracer: Tracer | null","tracer: Tracer | null","attrs: Attributes","platformInstrumentation: unstable_ServerInstrumentation"],"sources":["../../src/otel/mrt-console-span-exporter.ts","../../src/utils/logger.ts","../../src/otel/setup.ts","../../src/otel/react-router/instrumentation.ts","../../src/entry/server.ts"],"sourcesContent":["/**\n * Copyright 2026 Salesforce, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * MRT-compatible console span exporter.\n *\n * Extends the default ConsoleSpanExporter to output structured JSON that\n * Managed Runtime's log infrastructure can parse. The default exporter uses\n * `console.dir` with a human-readable format; this override uses\n * `console.info(JSON.stringify(...))` to produce machine-parseable JSON\n * matching the format MRT expects.\n *\n * Inherits `shutdown()` and `forceFlush()` from ConsoleSpanExporter.\n */\n\nimport { ConsoleSpanExporter, type ReadableSpan } from '@opentelemetry/sdk-trace-base';\nimport { ExportResultCode, type ExportResult, hrTimeToTimeStamp } from '@opentelemetry/core';\n\nexport class MrtConsoleSpanExporter extends ConsoleSpanExporter {\n export(spans: ReadableSpan[], resultCallback: (result: ExportResult) => void): void {\n for (const span of spans) {\n try {\n const ctx = span.spanContext();\n const spanData = {\n traceId: ctx.traceId,\n parentId: span.parentSpanId,\n name: span.name,\n id: ctx.spanId,\n kind: span.kind,\n timestamp: hrTimeToTimeStamp(span.startTime),\n duration: span.duration,\n attributes: span.attributes,\n status: span.status,\n events: span.events,\n links: span.links,\n start_time: span.startTime,\n end_time: span.endTime,\n forwardTrace: process.env.SFNEXT_OTEL_ENABLED === 'true',\n };\n // eslint-disable-next-line no-console -- intentional: MRT collects stdout as the telemetry transport\n console.info(JSON.stringify(spanData));\n } catch {\n // Skip malformed spans — never let a serialization failure propagate\n }\n }\n resultCallback({ code: ExportResultCode.SUCCESS });\n }\n}\n","/**\n * Copyright 2026 Salesforce, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n/* eslint-disable no-console */\nimport os from 'os';\nimport chalk from 'chalk';\nimport { createRequire } from 'module';\nimport type { ServerMode } from '../server/modes';\nimport pkg from '../../package.json' with { type: 'json' };\n\n/**\n * Get the local network IPv4 address\n */\nexport function getNetworkAddress(): string | undefined {\n const interfaces = os.networkInterfaces();\n for (const name of Object.keys(interfaces)) {\n const iface = interfaces[name];\n if (!iface) continue;\n for (const alias of iface) {\n if (alias.family === 'IPv4' && !alias.internal) {\n return alias.address;\n }\n }\n }\n return undefined;\n}\n\n/**\n * Get the version of a package from the project's package.json\n */\nexport function getPackageVersion(packageName: string, projectDir: string): string {\n try {\n const require = createRequire(import.meta.url);\n const pkgPath = require.resolve(`${packageName}/package.json`, { paths: [projectDir] });\n const pkgJson = require(pkgPath) as { version: string };\n return pkgJson.version;\n } catch {\n return 'unknown';\n }\n}\n\n/**\n * Centralized, level-gated logger for the SDK.\n *\n * Log level is controlled by `SFCC_LOG_LEVEL` env var (`error` | `warn` | `info` | `debug`).\n * Falls back to: `DEBUG` targeting sfnext -> `debug`, `NODE_ENV=production` -> `warn`, otherwise `info`.\n */\n\nexport type LogLevel = 'error' | 'warn' | 'info' | 'debug';\n\nconst LEVEL_PRIORITY: Record<LogLevel, number> = {\n error: 0,\n warn: 1,\n info: 2,\n debug: 3,\n};\n\nlet overrideLevel: LogLevel | undefined;\n\n/**\n * Returns true when the `DEBUG` env var targets sfnext or is a general enable flag.\n * Avoids accidentally enabling debug mode when DEBUG is set for unrelated libraries\n * (e.g. `DEBUG=express:*`).\n */\nfunction debugEnablesSfnext(): boolean {\n const raw = process.env.DEBUG?.trim();\n if (!raw) return false;\n const normalized = raw.toLowerCase();\n if (['1', 'true', 'yes', 'on'].includes(normalized)) return true;\n return raw.split(',').some((token) => {\n const value = token.trim();\n return value === '*' || value === 'sfnext' || value === 'sfnext:*';\n });\n}\n\nfunction resolveLevel(): LogLevel {\n if (overrideLevel) return overrideLevel;\n const envLevel = process.env.MRT_LOG_LEVEL ?? process.env.SFCC_LOG_LEVEL;\n if (envLevel && envLevel in LEVEL_PRIORITY) return envLevel as LogLevel;\n if (debugEnablesSfnext()) return 'debug';\n if (process.env.NODE_ENV === 'production') return 'warn';\n return 'info';\n}\n\nfunction shouldLog(level: LogLevel): boolean {\n return LEVEL_PRIORITY[level] <= LEVEL_PRIORITY[resolveLevel()];\n}\n\nexport const logger = {\n error(msg: string, ...args: unknown[]): void {\n if (!shouldLog('error')) return;\n console.error(chalk.red('[sfnext:error]'), msg, ...args);\n },\n warn(msg: string, ...args: unknown[]): void {\n if (!shouldLog('warn')) return;\n console.warn(chalk.yellow('[sfnext:warn]'), msg, ...args);\n },\n info(msg: string, ...args: unknown[]): void {\n if (!shouldLog('info')) return;\n console.log(chalk.cyan('[sfnext:info]'), msg, ...args);\n },\n debug(msg: string, ...args: unknown[]): void {\n if (!shouldLog('debug')) return;\n console.log(chalk.gray('[sfnext:debug]'), msg, ...args);\n },\n setLevel(level: LogLevel | undefined): void {\n overrideLevel = level;\n },\n getLevel(): LogLevel {\n return resolveLevel();\n },\n};\n\n/**\n * Print the server information banner with URLs and versions\n */\nexport function printServerInfo(mode: ServerMode, port: number, startTime: number, projectDir: string): void {\n const elapsed = Date.now() - startTime;\n const sfnextVersion = pkg.version;\n const reactVersion = getPackageVersion('react', projectDir);\n const reactRouterVersion = getPackageVersion('react-router', projectDir);\n const viteVersion = getPackageVersion('vite', projectDir);\n\n const modeLabel = mode === 'development' ? 'Development Mode' : 'Preview Mode';\n\n console.log();\n console.log(` ${chalk.cyan.bold('⚡ SFCC Storefront Next')} ${chalk.dim(`v${sfnextVersion}`)}`);\n console.log(` ${chalk.green.bold(modeLabel)}`);\n console.log();\n const logLevel = resolveLevel();\n const logLevelColors: Record<LogLevel, (s: string) => string> = {\n error: chalk.red,\n warn: chalk.yellow,\n info: chalk.cyan,\n debug: chalk.gray,\n };\n\n console.log(\n ` ${chalk.dim('react')} ${chalk.green(`v${reactVersion}`)} ${chalk.dim('|')} ` +\n `${chalk.dim('react-router')} ${chalk.green(`v${reactRouterVersion}`)} ${chalk.dim('|')} ` +\n `${chalk.dim('vite')} ${chalk.green(`v${viteVersion}`)}`\n );\n console.log(\n ` ${chalk.dim('log level')} ${logLevelColors[logLevel](logLevel)} ${chalk.dim('|')} ` +\n `${chalk.green(`ready in ${elapsed}ms`)}`\n );\n console.log();\n}\n\n/**\n * Print server configuration details (proxy, static, etc.)\n */\nexport function printServerConfig(config: {\n mode: ServerMode;\n port: number;\n enableProxy?: boolean;\n enableStaticServing?: boolean;\n enableCompression?: boolean;\n proxyPath?: string;\n proxyHost?: string;\n shortCode?: string;\n organizationId?: string;\n clientId?: string;\n}): void {\n const {\n port,\n enableProxy,\n enableStaticServing,\n enableCompression,\n proxyPath,\n proxyHost,\n shortCode,\n organizationId,\n clientId,\n } = config;\n\n console.log(` ${chalk.bold('Environment Configuration:')}`);\n\n if (enableProxy && proxyPath && proxyHost && shortCode) {\n console.log(\n ` ${chalk.green('✓')} ${chalk.bold('Proxy:')} ${chalk.cyan(`localhost:${port}${proxyPath}`)} ${chalk.dim('→')} ${chalk.cyan(proxyHost)}`\n );\n console.log(` ${chalk.dim('Short Code: ')}${chalk.dim(shortCode)}`);\n if (organizationId) {\n console.log(` ${chalk.dim('Organization ID: ')}${chalk.dim(organizationId)}`);\n }\n if (clientId) {\n console.log(` ${chalk.dim('Client ID: ')}${chalk.dim(clientId)}`);\n }\n } else {\n console.log(` ${chalk.bold('Proxy: ')} ${chalk.dim('disabled')}`);\n }\n\n if (enableStaticServing) {\n console.log(` ${chalk.bold('Static: ')} ${chalk.dim('enabled')}`);\n }\n\n if (enableCompression) {\n console.log(` ${chalk.bold('Compression: ')} ${chalk.dim('enabled')}`);\n }\n\n // URLs\n const localUrl = `http://localhost:${port}`;\n const showNetwork = process.env.SHOW_NETWORK === 'true';\n const networkAddress = showNetwork ? getNetworkAddress() : undefined;\n const networkUrl = networkAddress ? `http://${networkAddress}:${port}` : undefined;\n\n console.log();\n console.log(` ${chalk.bold('Local: ')} ${chalk.cyan(localUrl)}`);\n if (networkUrl) {\n console.log(` ${chalk.bold('Network:')} ${chalk.cyan(networkUrl)}`);\n }\n\n console.log();\n console.log(` ${chalk.dim('Press')} ${chalk.bold('Ctrl+C')} ${chalk.dim('to stop the server')}`);\n console.log();\n}\n\n/**\n * Print shutdown message\n */\nexport function printShutdownMessage(): void {\n console.log(`\\n ${chalk.yellow('⚡')} ${chalk.dim('Server shutting down...')}\\n`);\n}\n","/**\n * Copyright 2026 Salesforce, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * OpenTelemetry tracer provider initialization.\n *\n * ## Exporter design: why ConsoleSpanExporter?\n *\n * This storefront runs in two environments:\n * - Local development on a developer's machine (Vite dev server)\n * - Salesforce Managed Runtime (MRT), a serverless environment backed by AWS Lambda\n *\n * Standard OpenTelemetry exporters (OTLP, Jaeger, Zipkin) are designed for\n * long-running processes: they maintain background agents, batch spans in memory,\n * and flush asynchronously. That model does not work in a serverless environment.\n * Lambda invocations must complete quickly — a background agent or async flush\n * after the response is sent would hold the Lambda open unnecessarily, increasing\n * cost and cold-start latency.\n *\n * Instead, we write spans synchronously to stdout via MrtConsoleSpanExporter — a\n * subclass of ConsoleSpanExporter that outputs structured JSON matching the format\n * MRT's log infrastructure expects. MRT collects stdout from every invocation and\n * forwards the log stream to downstream analytics systems. Telemetry reaches those\n * systems without any in-process agent or network connection from the Lambda\n * function itself.\n *\n * SimpleSpanProcessor is used (instead of BatchSpanProcessor) for the same reason:\n * immediate, synchronous export with no in-memory queue that could be lost if the\n * process exits.\n *\n * Uses NodeTracerProvider directly (instead of NodeSDK) to avoid async resource\n * detection. NodeSDK's autoDetectResources creates a Resource with\n * asyncAttributesPending=true, which causes SimpleSpanProcessor.onEnd() to\n * defer export via waitForAsyncAttributes(). In the Vite SSR module runner\n * context, that deferred microtask never executes. A synchronous Resource\n * ensures the immediate export path is always taken.\n *\n * @env SFNEXT_OTEL_ENABLED — set to `\"true\"` to enable (e.g. `SFNEXT_OTEL_ENABLED=true pnpm dev`)\n */\n\nimport type { Tracer } from '@opentelemetry/api';\nimport { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';\nimport { SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base';\nimport { MrtConsoleSpanExporter } from './mrt-console-span-exporter';\nimport { Resource } from '@opentelemetry/resources';\nimport { ATTR_SERVICE_NAME } from '@opentelemetry/semantic-conventions';\nimport { registerInstrumentations } from '@opentelemetry/instrumentation';\nimport { UndiciInstrumentation } from '@opentelemetry/instrumentation-undici';\nimport { logger } from '../logger';\n\nexport const SERVICE_NAME = 'storefront-next';\n\n/**\n * Initializes OpenTelemetry and returns a Tracer from the provider directly.\n *\n * Returns the tracer via `provider.getTracer()` instead of the global\n * `trace.getTracer()` API. In the Vite SSR module runner, the built\n * dist/entry/server.js and the externalized @opentelemetry/sdk-trace-node\n * resolve @opentelemetry/api to different module instances (different paths\n * through pnpm's strict node_modules). Each instance has its own\n * ProxyTracerProvider singleton, so `provider.register()` sets the delegate\n * on sdk-trace-node's API instance while our code's `trace.getTracer()`\n * reads from a separate API instance with no delegate — returning a tracer\n * backed by a bare BasicTracerProvider with NoopSpanProcessor.\n *\n * Getting the tracer directly from the provider bypasses the global registry\n * entirely, guaranteeing the tracer uses our configured span processors.\n */\nlet cachedTracer: Tracer | null = null;\n\n// In the Vite SSR module runner, setup.ts may be loaded by two different module\n// instances (the Express server and the SSR bundle) — each with its own\n// `cachedTracer`. UndiciInstrumentation hooks into process-global\n// diagnostics_channel events, so registering it twice causes duplicate spans\n// for every fetch. This process-global flag ensures it is registered only once.\nconst UNDICI_REGISTERED_KEY = Symbol.for('sfnext.otel.undici_registered');\n\nexport function initTelemetry(): Tracer | null {\n if (cachedTracer) return cachedTracer;\n try {\n const provider = new NodeTracerProvider({\n resource: new Resource({ [ATTR_SERVICE_NAME]: SERVICE_NAME }),\n });\n\n provider.addSpanProcessor(new SimpleSpanProcessor(new MrtConsoleSpanExporter()));\n provider.register();\n\n // Guard against double-registration across Vite module boundaries.\n // See comment above UNDICI_REGISTERED_KEY.\n if (!(globalThis as Record<symbol, boolean>)[UNDICI_REGISTERED_KEY]) {\n (globalThis as Record<symbol, boolean>)[UNDICI_REGISTERED_KEY] = true;\n registerInstrumentations({\n tracerProvider: provider,\n instrumentations: [\n new UndiciInstrumentation({\n requestHook(span, request) {\n try {\n const method = request.method.toUpperCase();\n const url = `${request.origin}${request.path}`;\n span.updateName(`${method} ${url}`);\n } catch {\n // Non-fatal — default span name is acceptable\n }\n },\n }),\n ],\n });\n }\n\n cachedTracer = provider.getTracer(SERVICE_NAME);\n return cachedTracer;\n } catch (error) {\n logger.error('[otel] Failed to initialize OpenTelemetry:', error);\n return null;\n }\n}\n","/**\n * Copyright 2026 Salesforce, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Platform-level handler instrumentation.\n *\n * Uses React Router's unstable_ServerInstrumentation API to observe the\n * request lifecycle at the handler and route levels. This runs around ALL\n * requests (document + data) and provides OpenTelemetry spans for:\n * - `request` — root span per incoming HTTP request\n * - `loader` — child span per route loader\n * - `action` — child span per route action\n * - `middleware` — child span per route middleware\n *\n * `tracer` is `null` when OTel is disabled. Each handler checks explicitly\n * and calls through immediately, so the disabled path is always obvious.\n *\n * @env SFNEXT_OTEL_ENABLED — set to `\"true\"` to enable OpenTelemetry tracing.\n * Spans are written synchronously to stdout via ConsoleSpanExporter, which is\n * compatible with both local development and the Managed Runtime serverless\n * environment (see otel/setup.ts for the full design rationale).\n * Example: `SFNEXT_OTEL_ENABLED=true pnpm dev`\n *\n * @see https://reactrouter.com/how-to/instrumentation\n */\n\nimport { type Tracer, type Attributes, SpanStatusCode } from '@opentelemetry/api';\nimport type { unstable_ServerInstrumentation } from 'react-router';\nimport { ATTR_HTTP_REQUEST_METHOD, ATTR_URL_PATH } from '@opentelemetry/semantic-conventions';\nimport { initTelemetry } from '../setup';\n\nconst tracer: Tracer | null = process.env.SFNEXT_OTEL_ENABLED === 'true' ? initTelemetry() : null;\n\n/**\n * Runs `handle` inside an active OTel span, recording errors and ending the span.\n * When `tracer` is null (OTel disabled), calls `handle` directly with no overhead.\n */\nasync function traced(\n spanName: string,\n attributes: Attributes,\n handle: () => Promise<{ status: string; error?: Error }>\n): Promise<void> {\n if (!tracer) {\n await handle();\n return;\n }\n let handled = false;\n try {\n await tracer.startActiveSpan(spanName, { attributes }, async (span) => {\n try {\n handled = true;\n const result = await handle();\n if (result.status === 'error' && result.error) {\n span.setStatus({ code: SpanStatusCode.ERROR, message: result.error.message });\n span.recordException(result.error);\n }\n } finally {\n span.end();\n }\n });\n } catch {\n // OTel failure must never break the request pipeline.\n // If handle() was already called inside the span callback, don't call it again.\n if (!handled) await handle();\n }\n}\n\n/**\n * HTTP attributes common to all spans.\n * url.path only — url.full would expose query params which may contain auth\n * tokens or PII. http.response.status_code is not available from\n * unstable_InstrumentationHandlerResult.\n */\nfunction httpAttributes(request: { method: string; url: string }): Attributes {\n const attrs: Attributes = { [ATTR_HTTP_REQUEST_METHOD]: request.method };\n try {\n attrs[ATTR_URL_PATH] = new URL(request.url).pathname;\n } catch {\n // Malformed URL — skip url.path rather than throwing\n }\n return attrs;\n}\n\nexport const platformInstrumentation: unstable_ServerInstrumentation = {\n handler(handler) {\n handler.instrument({\n async request(handleRequest, { request }) {\n await traced('react-router ssr', httpAttributes(request), handleRequest);\n },\n });\n },\n route(route) {\n // HTTP attributes (method, url.path) are intentionally omitted here.\n // These spans are children of the request span which already carries\n // them, and rr.route.id / rr.route.pattern are the meaningful\n // identifiers at the route level.\n function routeAttributes(unstable_pattern: string): Attributes {\n return {\n 'rr.route.id': route.id,\n 'rr.route.pattern': unstable_pattern,\n };\n }\n\n route.instrument({\n async loader(handleLoader, { unstable_pattern }) {\n await traced(`loader (${route.id})`, routeAttributes(unstable_pattern), handleLoader);\n },\n async action(handleAction, { unstable_pattern }) {\n await traced(`action (${route.id})`, routeAttributes(unstable_pattern), handleAction);\n },\n async middleware(handleMiddleware, { unstable_pattern }) {\n await traced(`middleware (${route.id})`, routeAttributes(unstable_pattern), handleMiddleware);\n },\n });\n },\n};\n","/**\n * Copyright 2026 Salesforce, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Server entry composition function.\n *\n * Wraps the full ServerEntryModule (the customer's or SDK default entry.server)\n * with platform-level features. The Vite plugin (platformEntryPlugin) generates\n * a virtual module that imports this function and calls it with the app's entry\n * module. New platform features ship via `npm update` without changes to the\n * plugin or customer code.\n *\n * Current platform behaviors:\n * - OpenTelemetry instrumentation for request lifecycle tracing\n *\n * Future additions:\n * - loadContext enrichment (correlation IDs, platform metadata)\n * - Response header injection (tracing, cache directives)\n * - Default error handling with platform error reporting\n */\n\nimport type { ServerEntryModule } from 'react-router';\nimport { platformInstrumentation } from '../otel/react-router/instrumentation';\n\n/**\n * Composes a server entry module with platform-level features.\n *\n * - Spreads all app module properties to forward unknown/future exports\n * - Wraps the default handler for platform-level processing\n * - Prepends a platform instrumentation to unstable_instrumentations\n */\nexport function composeServerEntry(appModule: ServerEntryModule): ServerEntryModule {\n return {\n // Spread all properties first so unknown/future exports pass through\n ...appModule,\n // Override the exports the platform layer enhances\n default(request, statusCode, headers, context, loadContext) {\n return appModule.default(request, statusCode, headers, context, loadContext);\n },\n unstable_instrumentations: [platformInstrumentation, ...(appModule.unstable_instrumentations ?? [])],\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"server.js","names":["LEVEL_PRIORITY: Record<LogLevel, number>","overrideLevel: LogLevel | undefined","cachedTracer: Tracer | null","tracer: Tracer | null","attrs: Attributes","platformInstrumentation: unstable_ServerInstrumentation"],"sources":["../../src/otel/mrt-console-span-exporter.ts","../../src/utils/logger.ts","../../src/otel/setup.ts","../../src/otel/react-router/instrumentation.ts","../../src/entry/server.ts"],"sourcesContent":["/**\n * Copyright 2026 Salesforce, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * MRT-compatible console span exporter.\n *\n * Extends the default ConsoleSpanExporter to output structured JSON that\n * Managed Runtime's log infrastructure can parse. The default exporter uses\n * `console.dir` with a human-readable format; this override uses\n * `console.info(JSON.stringify(...))` to produce machine-parseable JSON\n * matching the format MRT expects.\n *\n * Inherits `shutdown()` and `forceFlush()` from ConsoleSpanExporter.\n */\n\nimport { ConsoleSpanExporter, type ReadableSpan } from '@opentelemetry/sdk-trace-base';\nimport { ExportResultCode, type ExportResult, hrTimeToTimeStamp } from '@opentelemetry/core';\n\nexport class MrtConsoleSpanExporter extends ConsoleSpanExporter {\n export(spans: ReadableSpan[], resultCallback: (result: ExportResult) => void): void {\n for (const span of spans) {\n try {\n const ctx = span.spanContext();\n const spanData = {\n traceId: ctx.traceId,\n parentId: span.parentSpanId,\n name: span.name,\n id: ctx.spanId,\n kind: span.kind,\n timestamp: hrTimeToTimeStamp(span.startTime),\n duration: span.duration,\n attributes: span.attributes,\n status: span.status,\n events: span.events,\n links: span.links,\n start_time: span.startTime,\n end_time: span.endTime,\n forwardTrace: process.env.SFNEXT_OTEL_ENABLED === 'true',\n };\n // eslint-disable-next-line no-console -- intentional: MRT collects stdout as the telemetry transport\n console.info(JSON.stringify(spanData));\n } catch {\n // Skip malformed spans — never let a serialization failure propagate\n }\n }\n resultCallback({ code: ExportResultCode.SUCCESS });\n }\n}\n","/**\n * Copyright 2026 Salesforce, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n/* eslint-disable no-console */\nimport os from 'os';\nimport chalk from 'chalk';\nimport { createRequire } from 'module';\nimport type { ServerMode } from '../server/modes';\nimport pkg from '../../package.json' with { type: 'json' };\n\n/**\n * Get the local network IPv4 address\n */\nexport function getNetworkAddress(): string | undefined {\n const interfaces = os.networkInterfaces();\n for (const name of Object.keys(interfaces)) {\n const iface = interfaces[name];\n if (!iface) continue;\n for (const alias of iface) {\n if (alias.family === 'IPv4' && !alias.internal) {\n return alias.address;\n }\n }\n }\n return undefined;\n}\n\n/**\n * Get the version of a package from the project's package.json\n */\nexport function getPackageVersion(packageName: string, projectDir: string): string {\n try {\n const require = createRequire(import.meta.url);\n const pkgPath = require.resolve(`${packageName}/package.json`, { paths: [projectDir] });\n const pkgJson = require(pkgPath) as { version: string };\n return pkgJson.version;\n } catch {\n return 'unknown';\n }\n}\n\n/**\n * Centralized, level-gated logger for the SDK.\n *\n * Log level is controlled by `SFCC_LOG_LEVEL` env var (`error` | `warn` | `info` | `debug`).\n * Falls back to: `DEBUG` targeting sfnext -> `debug`, `NODE_ENV=production` -> `warn`, otherwise `info`.\n */\n\nexport type LogLevel = 'error' | 'warn' | 'info' | 'debug';\n\nconst LEVEL_PRIORITY: Record<LogLevel, number> = {\n error: 0,\n warn: 1,\n info: 2,\n debug: 3,\n};\n\nlet overrideLevel: LogLevel | undefined;\n\n/**\n * Returns true when the `DEBUG` env var targets sfnext or is a general enable flag.\n * Avoids accidentally enabling debug mode when DEBUG is set for unrelated libraries\n * (e.g. `DEBUG=express:*`).\n */\nfunction debugEnablesSfnext(): boolean {\n const raw = process.env.DEBUG?.trim();\n if (!raw) return false;\n const normalized = raw.toLowerCase();\n if (['1', 'true', 'yes', 'on'].includes(normalized)) return true;\n return raw.split(',').some((token) => {\n const value = token.trim();\n return value === '*' || value === 'sfnext' || value === 'sfnext:*';\n });\n}\n\nfunction resolveLevel(): LogLevel {\n if (overrideLevel) return overrideLevel;\n const envLevel = process.env.MRT_LOG_LEVEL ?? process.env.SFCC_LOG_LEVEL;\n if (envLevel && envLevel in LEVEL_PRIORITY) return envLevel as LogLevel;\n if (debugEnablesSfnext()) return 'debug';\n if (process.env.NODE_ENV === 'production') return 'warn';\n return 'info';\n}\n\nfunction shouldLog(level: LogLevel): boolean {\n return LEVEL_PRIORITY[level] <= LEVEL_PRIORITY[resolveLevel()];\n}\n\nexport const logger = {\n error(msg: string, ...args: unknown[]): void {\n if (!shouldLog('error')) return;\n console.error(chalk.red('[sfnext:error]'), msg, ...args);\n },\n warn(msg: string, ...args: unknown[]): void {\n if (!shouldLog('warn')) return;\n console.warn(chalk.yellow('[sfnext:warn]'), msg, ...args);\n },\n info(msg: string, ...args: unknown[]): void {\n if (!shouldLog('info')) return;\n console.log(chalk.cyan('[sfnext:info]'), msg, ...args);\n },\n debug(msg: string, ...args: unknown[]): void {\n if (!shouldLog('debug')) return;\n console.log(chalk.gray('[sfnext:debug]'), msg, ...args);\n },\n setLevel(level: LogLevel | undefined): void {\n overrideLevel = level;\n },\n getLevel(): LogLevel {\n return resolveLevel();\n },\n};\n\n/**\n * Print the server information banner with URLs and versions\n */\nexport function printServerInfo(mode: ServerMode, port: number, startTime: number, projectDir: string): void {\n const elapsed = Date.now() - startTime;\n const sfnextVersion = pkg.version;\n const reactVersion = getPackageVersion('react', projectDir);\n const reactRouterVersion = getPackageVersion('react-router', projectDir);\n const viteVersion = getPackageVersion('vite', projectDir);\n\n const modeLabel = mode === 'development' ? 'Development Mode' : 'Preview Mode';\n\n console.log();\n console.log(` ${chalk.cyan.bold('⚡ SFCC Storefront Next')} ${chalk.dim(`v${sfnextVersion}`)}`);\n console.log(` ${chalk.green.bold(modeLabel)}`);\n console.log();\n const logLevel = resolveLevel();\n const logLevelColors: Record<LogLevel, (s: string) => string> = {\n error: chalk.red,\n warn: chalk.yellow,\n info: chalk.cyan,\n debug: chalk.gray,\n };\n\n console.log(\n ` ${chalk.dim('react')} ${chalk.green(`v${reactVersion}`)} ${chalk.dim('|')} ` +\n `${chalk.dim('react-router')} ${chalk.green(`v${reactRouterVersion}`)} ${chalk.dim('|')} ` +\n `${chalk.dim('vite')} ${chalk.green(`v${viteVersion}`)}`\n );\n console.log(\n ` ${chalk.dim('log level')} ${logLevelColors[logLevel](logLevel)} ${chalk.dim('|')} ` +\n `${chalk.green(`ready in ${elapsed}ms`)}`\n );\n console.log();\n}\n\n/**\n * Print server configuration details (proxy, static, etc.)\n */\nexport function printServerConfig(config: {\n mode: ServerMode;\n port: number;\n enableProxy?: boolean;\n enableStaticServing?: boolean;\n enableCompression?: boolean;\n proxyPath?: string;\n proxyHost?: string;\n shortCode?: string;\n organizationId?: string;\n clientId?: string;\n}): void {\n const {\n port,\n enableProxy,\n enableStaticServing,\n enableCompression,\n proxyPath,\n proxyHost,\n shortCode,\n organizationId,\n clientId,\n } = config;\n\n console.log(` ${chalk.bold('Environment Configuration:')}`);\n\n if (enableProxy && proxyPath && proxyHost && shortCode) {\n console.log(\n ` ${chalk.green('✓')} ${chalk.bold('Proxy:')} ${chalk.cyan(`localhost:${port}${proxyPath}`)} ${chalk.dim('→')} ${chalk.cyan(proxyHost)}`\n );\n console.log(` ${chalk.dim('Short Code: ')}${chalk.dim(shortCode)}`);\n if (organizationId) {\n console.log(` ${chalk.dim('Organization ID: ')}${chalk.dim(organizationId)}`);\n }\n if (clientId) {\n console.log(` ${chalk.dim('Client ID: ')}${chalk.dim(clientId)}`);\n }\n } else {\n console.log(` ${chalk.bold('Proxy: ')} ${chalk.dim('disabled')}`);\n }\n\n if (enableStaticServing) {\n console.log(` ${chalk.bold('Static: ')} ${chalk.dim('enabled')}`);\n }\n\n if (enableCompression) {\n console.log(` ${chalk.bold('Compression: ')} ${chalk.dim('enabled')}`);\n }\n\n // URLs\n const localUrl = `http://localhost:${port}`;\n const showNetwork = process.env.SHOW_NETWORK === 'true';\n const networkAddress = showNetwork ? getNetworkAddress() : undefined;\n const networkUrl = networkAddress ? `http://${networkAddress}:${port}` : undefined;\n\n console.log();\n console.log(` ${chalk.bold('Local: ')} ${chalk.cyan(localUrl)}`);\n if (networkUrl) {\n console.log(` ${chalk.bold('Network:')} ${chalk.cyan(networkUrl)}`);\n }\n\n console.log();\n console.log(` ${chalk.dim('Press')} ${chalk.bold('Ctrl+C')} ${chalk.dim('to stop the server')}`);\n console.log();\n}\n\n/**\n * Print shutdown message\n */\nexport function printShutdownMessage(): void {\n console.log(`\\n ${chalk.yellow('⚡')} ${chalk.dim('Server shutting down...')}\\n`);\n}\n","/**\n * Copyright 2026 Salesforce, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * OpenTelemetry tracer provider initialization.\n *\n * ## Exporter design: why ConsoleSpanExporter?\n *\n * This storefront runs in two environments:\n * - Local development on a developer's machine (Vite dev server)\n * - Salesforce Managed Runtime (MRT), a serverless environment backed by AWS Lambda\n *\n * Standard OpenTelemetry exporters (OTLP, Jaeger, Zipkin) are designed for\n * long-running processes: they maintain background agents, batch spans in memory,\n * and flush asynchronously. That model does not work in a serverless environment.\n * Lambda invocations must complete quickly — a background agent or async flush\n * after the response is sent would hold the Lambda open unnecessarily, increasing\n * cost and cold-start latency.\n *\n * Instead, we write spans synchronously to stdout via MrtConsoleSpanExporter — a\n * subclass of ConsoleSpanExporter that outputs structured JSON matching the format\n * MRT's log infrastructure expects. MRT collects stdout from every invocation and\n * forwards the log stream to downstream analytics systems. Telemetry reaches those\n * systems without any in-process agent or network connection from the Lambda\n * function itself.\n *\n * SimpleSpanProcessor is used (instead of BatchSpanProcessor) for the same reason:\n * immediate, synchronous export with no in-memory queue that could be lost if the\n * process exits.\n *\n * Uses NodeTracerProvider directly (instead of NodeSDK) to avoid async resource\n * detection. NodeSDK's autoDetectResources creates a Resource with\n * asyncAttributesPending=true, which causes SimpleSpanProcessor.onEnd() to\n * defer export via waitForAsyncAttributes(). In the Vite SSR module runner\n * context, that deferred microtask never executes. A synchronous Resource\n * ensures the immediate export path is always taken.\n *\n * ## Distributed tracing: W3C Trace Context propagation\n *\n * The storefront runs below Managed Runtime (MRT) in the call tree. MRT is\n * trace-aware: it makes the sampling decision and stamps an inbound W3C\n * `traceparent` header on the request the storefront receives. To appear on the\n * same end-to-end trace as the surrounding services, the storefront behaves as a\n * standard downstream participant:\n * - It continues the inbound trace (see the Express middleware, which extracts\n * `traceparent` into the parent context).\n * - It forwards `traceparent` on outbound fetches (UndiciInstrumentation injects\n * it automatically via the globally-registered propagator below).\n * - It honors MRT's sampling decision via a ParentBasedSampler — it never forces\n * a sampled trace and implements no sampling policy of its own. MRT owns the\n * rate via MRT_DISTRIBUTED_TRACING_ENABLED / MRT_DISTRIBUTED_TRACING_SAMPLING_RATE.\n *\n * @env SFNEXT_OTEL_ENABLED — set to `\"true\"` to enable (e.g. `SFNEXT_OTEL_ENABLED=true pnpm dev`)\n */\n\nimport { propagation, type Tracer } from '@opentelemetry/api';\nimport { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';\nimport { AlwaysOnSampler, ParentBasedSampler, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base';\nimport { W3CTraceContextPropagator } from '@opentelemetry/core';\nimport { MrtConsoleSpanExporter } from './mrt-console-span-exporter';\nimport { Resource } from '@opentelemetry/resources';\nimport { ATTR_SERVICE_NAME } from '@opentelemetry/semantic-conventions';\nimport { registerInstrumentations } from '@opentelemetry/instrumentation';\nimport { UndiciInstrumentation } from '@opentelemetry/instrumentation-undici';\nimport { logger } from '../logger';\n\nexport const SERVICE_NAME = 'storefront-next';\n\n/**\n * Initializes OpenTelemetry and returns a Tracer from the provider directly.\n *\n * Returns the tracer via `provider.getTracer()` instead of the global\n * `trace.getTracer()` API. In the Vite SSR module runner, the built\n * dist/entry/server.js and the externalized @opentelemetry/sdk-trace-node\n * resolve @opentelemetry/api to different module instances (different paths\n * through pnpm's strict node_modules). Each instance has its own\n * ProxyTracerProvider singleton, so `provider.register()` sets the delegate\n * on sdk-trace-node's API instance while our code's `trace.getTracer()`\n * reads from a separate API instance with no delegate — returning a tracer\n * backed by a bare BasicTracerProvider with NoopSpanProcessor.\n *\n * Getting the tracer directly from the provider bypasses the global registry\n * entirely, guaranteeing the tracer uses our configured span processors.\n */\nlet cachedTracer: Tracer | null = null;\n\n// In the Vite SSR module runner, setup.ts may be loaded by two different module\n// instances (the Express server and the SSR bundle) — each with its own\n// `cachedTracer`. UndiciInstrumentation hooks into process-global\n// diagnostics_channel events, so registering it twice causes duplicate spans\n// for every fetch. This process-global flag ensures it is registered only once.\nconst UNDICI_REGISTERED_KEY = Symbol.for('sfnext.otel.undici_registered');\n\nexport function initTelemetry(): Tracer | null {\n if (cachedTracer) return cachedTracer;\n try {\n const provider = new NodeTracerProvider({\n resource: new Resource({ [ATTR_SERVICE_NAME]: SERVICE_NAME }),\n // Honor the upstream (MRT) sampling decision rather than implementing our\n // own. ParentBasedSampler: when an inbound traceparent is present, defer\n // to its sampled flag (continue + export iff sampled); when there is no\n // inbound parent, sample the fresh root ourselves (AlwaysOnSampler). This\n // is exactly the standard \"downstream participant\" behavior — MRT owns the\n // rate via MRT_DISTRIBUTED_TRACING_* env vars.\n sampler: new ParentBasedSampler({ root: new AlwaysOnSampler() }),\n });\n\n provider.addSpanProcessor(new SimpleSpanProcessor(new MrtConsoleSpanExporter()));\n provider.register();\n\n // Register the standard W3C Trace Context propagator. This single\n // registration drives propagation in both directions:\n // - inbound: the Express middleware calls propagation.extract() to continue\n // the trace described by the incoming `traceparent`.\n // - outbound: UndiciInstrumentation calls propagation.inject() to add\n // `traceparent` to every outgoing fetch (SCAPI, etc.).\n // Registration lives here, inside the SFNEXT_OTEL_ENABLED-gated init path, so\n // that when tracing is disabled no propagator is installed and no trace\n // headers are ever added.\n propagation.setGlobalPropagator(new W3CTraceContextPropagator());\n\n // Guard against double-registration across Vite module boundaries.\n // See comment above UNDICI_REGISTERED_KEY.\n if (!(globalThis as Record<symbol, boolean>)[UNDICI_REGISTERED_KEY]) {\n (globalThis as Record<symbol, boolean>)[UNDICI_REGISTERED_KEY] = true;\n registerInstrumentations({\n tracerProvider: provider,\n instrumentations: [\n new UndiciInstrumentation({\n requestHook(span, request) {\n try {\n const method = request.method.toUpperCase();\n const url = `${request.origin}${request.path}`;\n span.updateName(`${method} ${url}`);\n } catch {\n // Non-fatal — default span name is acceptable\n }\n },\n }),\n ],\n });\n }\n\n cachedTracer = provider.getTracer(SERVICE_NAME);\n return cachedTracer;\n } catch (error) {\n logger.error('[otel] Failed to initialize OpenTelemetry:', error);\n return null;\n }\n}\n","/**\n * Copyright 2026 Salesforce, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Platform-level handler instrumentation.\n *\n * Uses React Router's unstable_ServerInstrumentation API to observe the\n * request lifecycle at the handler and route levels. This runs around ALL\n * requests (document + data) and provides OpenTelemetry spans for:\n * - `request` — root span per incoming HTTP request\n * - `loader` — child span per route loader\n * - `action` — child span per route action\n * - `middleware` — child span per route middleware\n *\n * `tracer` is `null` when OTel is disabled. Each handler checks explicitly\n * and calls through immediately, so the disabled path is always obvious.\n *\n * @env SFNEXT_OTEL_ENABLED — set to `\"true\"` to enable OpenTelemetry tracing.\n * Spans are written synchronously to stdout via ConsoleSpanExporter, which is\n * compatible with both local development and the Managed Runtime serverless\n * environment (see otel/setup.ts for the full design rationale).\n * Example: `SFNEXT_OTEL_ENABLED=true pnpm dev`\n *\n * @see https://reactrouter.com/how-to/instrumentation\n */\n\nimport { type Tracer, type Attributes, SpanStatusCode } from '@opentelemetry/api';\nimport type { unstable_ServerInstrumentation } from 'react-router';\nimport { ATTR_HTTP_REQUEST_METHOD, ATTR_URL_PATH } from '@opentelemetry/semantic-conventions';\nimport { initTelemetry } from '../setup';\n\nconst tracer: Tracer | null = process.env.SFNEXT_OTEL_ENABLED === 'true' ? initTelemetry() : null;\n\n/**\n * Runs `handle` inside an active OTel span, recording errors and ending the span.\n * When `tracer` is null (OTel disabled), calls `handle` directly with no overhead.\n */\nasync function traced(\n spanName: string,\n attributes: Attributes,\n handle: () => Promise<{ status: string; error?: Error }>\n): Promise<void> {\n if (!tracer) {\n await handle();\n return;\n }\n let handled = false;\n try {\n await tracer.startActiveSpan(spanName, { attributes }, async (span) => {\n try {\n handled = true;\n const result = await handle();\n if (result.status === 'error' && result.error) {\n span.setStatus({ code: SpanStatusCode.ERROR, message: result.error.message });\n span.recordException(result.error);\n }\n } finally {\n span.end();\n }\n });\n } catch {\n // OTel failure must never break the request pipeline.\n // If handle() was already called inside the span callback, don't call it again.\n if (!handled) await handle();\n }\n}\n\n/**\n * HTTP attributes common to all spans.\n * url.path only — url.full would expose query params which may contain auth\n * tokens or PII. http.response.status_code is not available from\n * unstable_InstrumentationHandlerResult.\n */\nfunction httpAttributes(request: { method: string; url: string }): Attributes {\n const attrs: Attributes = { [ATTR_HTTP_REQUEST_METHOD]: request.method };\n try {\n attrs[ATTR_URL_PATH] = new URL(request.url).pathname;\n } catch {\n // Malformed URL — skip url.path rather than throwing\n }\n return attrs;\n}\n\nexport const platformInstrumentation: unstable_ServerInstrumentation = {\n handler(handler) {\n handler.instrument({\n async request(handleRequest, { request }) {\n await traced('react-router ssr', httpAttributes(request), handleRequest);\n },\n });\n },\n route(route) {\n // HTTP attributes (method, url.path) are intentionally omitted here.\n // These spans are children of the request span which already carries\n // them, and rr.route.id / rr.route.pattern are the meaningful\n // identifiers at the route level.\n function routeAttributes(unstable_pattern: string): Attributes {\n return {\n 'rr.route.id': route.id,\n 'rr.route.pattern': unstable_pattern,\n };\n }\n\n route.instrument({\n async loader(handleLoader, { unstable_pattern }) {\n await traced(`loader (${route.id})`, routeAttributes(unstable_pattern), handleLoader);\n },\n async action(handleAction, { unstable_pattern }) {\n await traced(`action (${route.id})`, routeAttributes(unstable_pattern), handleAction);\n },\n async middleware(handleMiddleware, { unstable_pattern }) {\n await traced(`middleware (${route.id})`, routeAttributes(unstable_pattern), handleMiddleware);\n },\n });\n },\n};\n","/**\n * Copyright 2026 Salesforce, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Server entry composition function.\n *\n * Wraps the full ServerEntryModule (the customer's or SDK default entry.server)\n * with platform-level features. The Vite plugin (platformEntryPlugin) generates\n * a virtual module that imports this function and calls it with the app's entry\n * module. New platform features ship via `npm update` without changes to the\n * plugin or customer code.\n *\n * Current platform behaviors:\n * - OpenTelemetry instrumentation for request lifecycle tracing\n *\n * Future additions:\n * - loadContext enrichment (correlation IDs, platform metadata)\n * - Response header injection (tracing, cache directives)\n * - Default error handling with platform error reporting\n */\n\nimport type { ServerEntryModule } from 'react-router';\nimport { platformInstrumentation } from '../otel/react-router/instrumentation';\n\n/**\n * Composes a server entry module with platform-level features.\n *\n * - Spreads all app module properties to forward unknown/future exports\n * - Wraps the default handler for platform-level processing\n * - Prepends a platform instrumentation to unstable_instrumentations\n */\nexport function composeServerEntry(appModule: ServerEntryModule): ServerEntryModule {\n return {\n // Spread all properties first so unknown/future exports pass through\n ...appModule,\n // Override the exports the platform layer enhances\n default(request, statusCode, headers, context, loadContext) {\n return appModule.default(request, statusCode, headers, context, loadContext);\n },\n unstable_instrumentations: [platformInstrumentation, ...(appModule.unstable_instrumentations ?? [])],\n };\n}\n"],"mappings":";;;;;;;;;;;AA+BA,IAAa,yBAAb,cAA4C,oBAAoB;CAC5D,OAAO,OAAuB,gBAAsD;AAChF,OAAK,MAAM,QAAQ,MACf,KAAI;GACA,MAAM,MAAM,KAAK,aAAa;GAC9B,MAAM,WAAW;IACb,SAAS,IAAI;IACb,UAAU,KAAK;IACf,MAAM,KAAK;IACX,IAAI,IAAI;IACR,MAAM,KAAK;IACX,WAAW,kBAAkB,KAAK,UAAU;IAC5C,UAAU,KAAK;IACf,YAAY,KAAK;IACjB,QAAQ,KAAK;IACb,QAAQ,KAAK;IACb,OAAO,KAAK;IACZ,YAAY,KAAK;IACjB,UAAU,KAAK;IACf,cAAc,QAAQ,IAAI,wBAAwB;IACrD;AAED,WAAQ,KAAK,KAAK,UAAU,SAAS,CAAC;UAClC;AAIZ,iBAAe,EAAE,MAAM,iBAAiB,SAAS,CAAC;;;;;;ACI1D,MAAMA,iBAA2C;CAC7C,OAAO;CACP,MAAM;CACN,MAAM;CACN,OAAO;CACV;AAED,IAAIC;;;;;;AAOJ,SAAS,qBAA8B;CACnC,MAAM,MAAM,QAAQ,IAAI,OAAO,MAAM;AACrC,KAAI,CAAC,IAAK,QAAO;CACjB,MAAM,aAAa,IAAI,aAAa;AACpC,KAAI;EAAC;EAAK;EAAQ;EAAO;EAAK,CAAC,SAAS,WAAW,CAAE,QAAO;AAC5D,QAAO,IAAI,MAAM,IAAI,CAAC,MAAM,UAAU;EAClC,MAAM,QAAQ,MAAM,MAAM;AAC1B,SAAO,UAAU,OAAO,UAAU,YAAY,UAAU;GAC1D;;AAGN,SAAS,eAAyB;AAC9B,KAAI,cAAe,QAAO;CAC1B,MAAM,WAAW,QAAQ,IAAI,iBAAiB,QAAQ,IAAI;AAC1D,KAAI,YAAY,YAAY,eAAgB,QAAO;AACnD,KAAI,oBAAoB,CAAE,QAAO;AACjC,KAAI,QAAQ,IAAI,aAAa,aAAc,QAAO;AAClD,QAAO;;AAGX,SAAS,UAAU,OAA0B;AACzC,QAAO,eAAe,UAAU,eAAe,cAAc;;AAGjE,MAAa,SAAS;CAClB,MAAM,KAAa,GAAG,MAAuB;AACzC,MAAI,CAAC,UAAU,QAAQ,CAAE;AACzB,UAAQ,MAAM,MAAM,IAAI,iBAAiB,EAAE,KAAK,GAAG,KAAK;;CAE5D,KAAK,KAAa,GAAG,MAAuB;AACxC,MAAI,CAAC,UAAU,OAAO,CAAE;AACxB,UAAQ,KAAK,MAAM,OAAO,gBAAgB,EAAE,KAAK,GAAG,KAAK;;CAE7D,KAAK,KAAa,GAAG,MAAuB;AACxC,MAAI,CAAC,UAAU,OAAO,CAAE;AACxB,UAAQ,IAAI,MAAM,KAAK,gBAAgB,EAAE,KAAK,GAAG,KAAK;;CAE1D,MAAM,KAAa,GAAG,MAAuB;AACzC,MAAI,CAAC,UAAU,QAAQ,CAAE;AACzB,UAAQ,IAAI,MAAM,KAAK,iBAAiB,EAAE,KAAK,GAAG,KAAK;;CAE3D,SAAS,OAAmC;AACxC,kBAAgB;;CAEpB,WAAqB;AACjB,SAAO,cAAc;;CAE5B;;;;AC5CD,MAAa,eAAe;;;;;;;;;;;;;;;;;AAkB5B,IAAIC,eAA8B;AAOlC,MAAM,wBAAwB,OAAO,IAAI,gCAAgC;AAEzE,SAAgB,gBAA+B;AAC3C,KAAI,aAAc,QAAO;AACzB,KAAI;EACA,MAAM,WAAW,IAAI,mBAAmB;GACpC,UAAU,IAAI,SAAS,GAAG,oBAAoB,cAAc,CAAC;GAO7D,SAAS,IAAI,mBAAmB,EAAE,MAAM,IAAI,iBAAiB,EAAE,CAAC;GACnE,CAAC;AAEF,WAAS,iBAAiB,IAAI,oBAAoB,IAAI,wBAAwB,CAAC,CAAC;AAChF,WAAS,UAAU;AAWnB,cAAY,oBAAoB,IAAI,2BAA2B,CAAC;AAIhE,MAAI,CAAE,WAAuC,wBAAwB;AACjE,GAAC,WAAuC,yBAAyB;AACjE,4BAAyB;IACrB,gBAAgB;IAChB,kBAAkB,CACd,IAAI,sBAAsB,EACtB,YAAY,MAAM,SAAS;AACvB,SAAI;MACA,MAAM,SAAS,QAAQ,OAAO,aAAa;MAC3C,MAAM,MAAM,GAAG,QAAQ,SAAS,QAAQ;AACxC,WAAK,WAAW,GAAG,OAAO,GAAG,MAAM;aAC/B;OAIf,CAAC,CACL;IACJ,CAAC;;AAGN,iBAAe,SAAS,UAAU,aAAa;AAC/C,SAAO;UACF,OAAO;AACZ,SAAO,MAAM,8CAA8C,MAAM;AACjE,SAAO;;;;;;ACpHf,MAAMC,SAAwB,QAAQ,IAAI,wBAAwB,SAAS,eAAe,GAAG;;;;;AAM7F,eAAe,OACX,UACA,YACA,QACa;AACb,KAAI,CAAC,QAAQ;AACT,QAAM,QAAQ;AACd;;CAEJ,IAAI,UAAU;AACd,KAAI;AACA,QAAM,OAAO,gBAAgB,UAAU,EAAE,YAAY,EAAE,OAAO,SAAS;AACnE,OAAI;AACA,cAAU;IACV,MAAM,SAAS,MAAM,QAAQ;AAC7B,QAAI,OAAO,WAAW,WAAW,OAAO,OAAO;AAC3C,UAAK,UAAU;MAAE,MAAM,eAAe;MAAO,SAAS,OAAO,MAAM;MAAS,CAAC;AAC7E,UAAK,gBAAgB,OAAO,MAAM;;aAEhC;AACN,SAAK,KAAK;;IAEhB;SACE;AAGJ,MAAI,CAAC,QAAS,OAAM,QAAQ;;;;;;;;;AAUpC,SAAS,eAAe,SAAsD;CAC1E,MAAMC,QAAoB,GAAG,2BAA2B,QAAQ,QAAQ;AACxE,KAAI;AACA,QAAM,iBAAiB,IAAI,IAAI,QAAQ,IAAI,CAAC;SACxC;AAGR,QAAO;;AAGX,MAAaC,0BAA0D;CACnE,QAAQ,SAAS;AACb,UAAQ,WAAW,EACf,MAAM,QAAQ,eAAe,EAAE,WAAW;AACtC,SAAM,OAAO,oBAAoB,eAAe,QAAQ,EAAE,cAAc;KAE/E,CAAC;;CAEN,MAAM,OAAO;EAKT,SAAS,gBAAgB,kBAAsC;AAC3D,UAAO;IACH,eAAe,MAAM;IACrB,oBAAoB;IACvB;;AAGL,QAAM,WAAW;GACb,MAAM,OAAO,cAAc,EAAE,oBAAoB;AAC7C,UAAM,OAAO,WAAW,MAAM,GAAG,IAAI,gBAAgB,iBAAiB,EAAE,aAAa;;GAEzF,MAAM,OAAO,cAAc,EAAE,oBAAoB;AAC7C,UAAM,OAAO,WAAW,MAAM,GAAG,IAAI,gBAAgB,iBAAiB,EAAE,aAAa;;GAEzF,MAAM,WAAW,kBAAkB,EAAE,oBAAoB;AACrD,UAAM,OAAO,eAAe,MAAM,GAAG,IAAI,gBAAgB,iBAAiB,EAAE,iBAAiB;;GAEpG,CAAC;;CAET;;;;;;;;;;;ACpFD,SAAgB,mBAAmB,WAAiD;AAChF,QAAO;EAEH,GAAG;EAEH,QAAQ,SAAS,YAAY,SAAS,SAAS,aAAa;AACxD,UAAO,UAAU,QAAQ,SAAS,YAAY,SAAS,SAAS,YAAY;;EAEhF,2BAA2B,CAAC,yBAAyB,GAAI,UAAU,6BAA6B,EAAE,CAAE;EACvG"}
@@ -384,6 +384,8 @@ async function processComponentFile(filePath, _projectRoot) {
384
384
  regionDefinitions,
385
385
  attributes
386
386
  };
387
+ if (typeof componentConfig.embedded === "boolean") componentMetadata.embedded = componentConfig.embedded;
388
+ if (componentConfig.component_id !== void 0) componentMetadata.component_id = String(componentConfig.component_id);
387
389
  components.push(componentMetadata);
388
390
  }
389
391
  } catch (error) {
@@ -483,6 +485,8 @@ async function generateComponentCartridge(component, outputDir, dryRun = false)
483
485
  description: component.description,
484
486
  group: component.group,
485
487
  arch_type: ARCH_TYPE_HEADLESS,
488
+ ...typeof component.embedded === "boolean" && { embedded: component.embedded },
489
+ component_id: component.component_id,
486
490
  region_definitions: component.regionDefinitions || [],
487
491
  attribute_definition_groups: attributeDefinitionGroups
488
492
  };
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../src/plugins/staticRegistry.ts","../src/plugins/eventInstrumentationValidator.ts","../src/storefront-next-targets.ts","../src/plugins/transformTargets.ts","../src/plugins/uiTargetDevMode.ts","../src/plugins/hybridProxy.ts","../src/plugins/ecdnMatcher.ts"],"sourcesContent":[],"mappings":";;;;;;;;UAuDiB,0BAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;AG5BjB;;UFDiB,mCAAA;;AGgBjB;AA4CA;;;;AC1BA;AA+PA;;;;AC7KA;;;;;;;;;;;AFpGiB,UFHA,2BAAA,CEuBH;EAwBE;;;;AC1BhB;AA+PA;;;;AC7KA;;;;;;;;;;;;;;;;;;;;;;;mBJrEqB;;;;;;;;;kCAUe;;;;;;;;;;;;;;;;;;;;;iBAsBpB,qBAAA,UAA8B,8BAAmC;;;iBC9EjE,gCAAA,CAAA;;;EH4BC,cAAA,CAAA,MAAA,EGlBc,cHkBY,CAAA,EAAA,IAAA;;;;IC7B1B,GAAA,EAAA,IAAA;;;;;UGgBA,qBAAA;EDfD;;;;ECeC,OAAA,CAAA,EAAA,OAAA;EA4CD;;;;AC1BhB;EA+PgB,cAAA,CAAA,EAAA,MAAiB;;;;AC7KjC;;;YFhFc;;;;;;;;;;;;;;;;;;;;;;;iBAwBE,qBAAA,UAA8B,wBAA6B;;;;UC1B1D,wBAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA+PD,iBAAA,UAA2B,2BAA2B;;;;;;;;;;;;;;;;;;;;;;iBC7KtD,iBAAA"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/plugins/staticRegistry.ts","../src/plugins/eventInstrumentationValidator.ts","../src/storefront-next-targets.ts","../src/plugins/transformTargets.ts","../src/plugins/uiTargetDevMode.ts","../src/plugins/hybridProxy.ts","../src/plugins/ecdnMatcher.ts"],"sourcesContent":[],"mappings":";;;;;;;;UAwDiB,0BAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;AG3BjB;;UFHiB,mCAAA;;AGgBjB;AA4CA;;;;AC1BA;AA+PA;;;;AC7KA;;;;;;;;;;;AFpGiB,UFHA,2BAAA,CEuBH;EAwBE;;;;AC1BhB;AA+PA;;;;AC7KA;;;;;;;;;;;;;;;;;;;;;;;mBJrEqB;;;;;;;;;kCAUe;;;;;;;;;;;;;;;;;;;;;iBAsBpB,qBAAA,UAA8B,8BAAmC;;;iBC5EjE,gCAAA,CAAA;;;EH2BC,cAAA,CAAA,MAAA,EGjBc,cHiBY,CAAA,EAAA,IAAA;;;;IC9B1B,GAAA,EAAA,IAAA;;;;;UGgBA,qBAAA;EDbD;;;;ECaC,OAAA,CAAA,EAAA,OAAA;EA4CD;;;;AC1BhB;EA+PgB,cAAA,CAAA,EAAA,MAAiB;;;;AC7KjC;;;YFhFc;;;;;;;;;;;;;;;;;;;;;;;iBAwBE,qBAAA,UAA8B,wBAA6B;;;;UC1B1D,wBAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA+PD,iBAAA,UAA2B,2BAA2B;;;;;;;;;;;;;;;;;;;;;;iBC7KtD,iBAAA"}