@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.
- package/dist/cartridge-services/index.d.ts.map +1 -1
- package/dist/cartridge-services/index.js +4 -0
- package/dist/cartridge-services/index.js.map +1 -1
- package/dist/commands/config/aggregate-extensions.js +279 -0
- package/dist/commands/locales/aggregate-extensions.js +3 -2
- package/dist/commands/push.js +2 -0
- package/dist/constants.js +29 -0
- package/dist/entry/server.js +8 -4
- package/dist/entry/server.js.map +1 -1
- package/dist/generate-cartridge.js +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +150 -20
- package/dist/index.js.map +1 -1
- package/dist/logger.js +1 -1
- package/dist/mrt/ssr.mjs +51 -51
- package/dist/mrt/ssr.mjs.map +1 -1
- package/dist/mrt/streamingHandler.mjs +57 -57
- package/dist/mrt/streamingHandler.mjs.map +1 -1
- package/dist/server.js +10 -5
- package/package.json +4 -4
|
@@ -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 ===
|
|
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 ===
|
|
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`
|
package/dist/commands/push.js
CHANGED
|
@@ -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 };
|
package/dist/entry/server.js
CHANGED
|
@@ -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({
|
|
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({
|
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","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":"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"}
|
|
@@ -384,6 +384,8 @@ async function processComponentFile(filePath, _projectRoot) {
|
|
|
384
384
|
regionDefinitions,
|
|
385
385
|
attributes
|
|
386
386
|
};
|
|
387
|
+
if (typeof componentConfig.embedded === "boolean") componentMetadata.embedded = componentConfig.embedded;
|
|
388
|
+
if (componentConfig.component_id !== void 0) componentMetadata.component_id = String(componentConfig.component_id);
|
|
387
389
|
components.push(componentMetadata);
|
|
388
390
|
}
|
|
389
391
|
} catch (error) {
|
|
@@ -483,6 +485,8 @@ async function generateComponentCartridge(component, outputDir, dryRun = false)
|
|
|
483
485
|
description: component.description,
|
|
484
486
|
group: component.group,
|
|
485
487
|
arch_type: ARCH_TYPE_HEADLESS,
|
|
488
|
+
...typeof component.embedded === "boolean" && { embedded: component.embedded },
|
|
489
|
+
component_id: component.component_id,
|
|
486
490
|
region_definitions: component.regionDefinitions || [],
|
|
487
491
|
attribute_definition_groups: attributeDefinitionGroups
|
|
488
492
|
};
|
package/dist/index.d.ts.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/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"}
|