@salesforce/storefront-next-dev 0.2.0-alpha.2 → 0.3.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cartridge-services/index.d.ts.map +1 -1
- package/dist/cartridge-services/index.js +171 -50
- package/dist/cartridge-services/index.js.map +1 -1
- package/dist/commands/create-bundle.js +12 -11
- package/dist/commands/create-instructions.js +7 -5
- package/dist/commands/create-storefront.js +18 -22
- package/dist/commands/deploy-cartridge.js +67 -26
- package/dist/commands/dev.js +6 -4
- package/dist/commands/extensions/create.js +2 -0
- package/dist/commands/extensions/install.js +3 -7
- package/dist/commands/extensions/list.js +2 -0
- package/dist/commands/extensions/remove.js +3 -7
- package/dist/commands/generate-cartridge.js +23 -2
- package/dist/commands/preview.js +15 -10
- package/dist/commands/push.js +25 -19
- package/dist/commands/validate-cartridge.js +51 -0
- package/dist/config.js +74 -47
- package/dist/configs/react-router.config.d.ts.map +1 -1
- package/dist/configs/react-router.config.js +36 -0
- package/dist/configs/react-router.config.js.map +1 -1
- package/dist/dependency-utils.js +14 -16
- package/dist/entry/server.d.ts.map +1 -1
- package/dist/entry/server.js +221 -11
- package/dist/entry/server.js.map +1 -1
- package/dist/generate-cartridge.js +106 -50
- package/dist/index.d.ts +127 -13
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1147 -167
- package/dist/index.js.map +1 -1
- package/dist/local-dev-setup.js +13 -13
- package/dist/logger/index.d.ts +20 -0
- package/dist/logger/index.d.ts.map +1 -0
- package/dist/logger/index.js +69 -0
- package/dist/logger/index.js.map +1 -0
- package/dist/logger.js +79 -33
- package/dist/logger2.js +1 -0
- package/dist/manage-extensions.js +7 -13
- package/dist/mrt/ssr.mjs +60 -72
- package/dist/mrt/ssr.mjs.map +1 -1
- package/dist/mrt/streamingHandler.mjs +66 -78
- package/dist/mrt/streamingHandler.mjs.map +1 -1
- package/dist/react-router/Scripts.d.ts +1 -1
- package/dist/react-router/Scripts.d.ts.map +1 -1
- package/dist/react-router/Scripts.js +38 -2
- package/dist/react-router/Scripts.js.map +1 -1
- package/dist/server.js +296 -16
- package/dist/utils.js +4 -4
- package/dist/validate-cartridge.js +45 -0
- package/package.json +22 -5
|
@@ -1,3 +1,37 @@
|
|
|
1
|
+
//#region src/utils/paths.ts
|
|
2
|
+
/**
|
|
3
|
+
* Get the configurable base path for the application.
|
|
4
|
+
* Reads from MRT_ENV_BASE_PATH environment variable.
|
|
5
|
+
*
|
|
6
|
+
* The base path is used for CDN routing to the correct MRT environment.
|
|
7
|
+
* It is prepended to all URLs: page routes, /mobify/bundle/ assets, and /mobify/proxy/api.
|
|
8
|
+
*
|
|
9
|
+
* Validation rules:
|
|
10
|
+
* - Must be a single path segment starting with '/'
|
|
11
|
+
* - Max 63 characters after the leading slash
|
|
12
|
+
* - Only URL-safe characters allowed
|
|
13
|
+
* - Returns empty string if not set
|
|
14
|
+
*
|
|
15
|
+
* @returns The sanitized base path (e.g., '/site-a' or '')
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* // No base path configured
|
|
19
|
+
* getBasePath() // → ''
|
|
20
|
+
*
|
|
21
|
+
* // With base path '/storefront'
|
|
22
|
+
* getBasePath() // → '/storefront'
|
|
23
|
+
*
|
|
24
|
+
* // Automatically sanitizes
|
|
25
|
+
* // MRT_ENV_BASE_PATH='storefront/' → '/storefront'
|
|
26
|
+
*/
|
|
27
|
+
function getBasePath() {
|
|
28
|
+
const basePath = process.env.MRT_ENV_BASE_PATH?.trim();
|
|
29
|
+
if (!basePath) return "";
|
|
30
|
+
if (!/^\/[a-zA-Z0-9_.+$~"'@:-]{1,63}$/.test(basePath)) throw new Error(`Invalid base path: "${basePath}". Base path must be a single segment starting with '/' (e.g., '/site-a'), contain only URL-safe characters, and be at most 63 characters after the leading slash.`);
|
|
31
|
+
return basePath;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
//#endregion
|
|
1
35
|
//#region src/configs/react-router.config.ts
|
|
2
36
|
/**
|
|
3
37
|
* Storefront Next preset for React Router configuration.
|
|
@@ -16,6 +50,7 @@ function storefrontNextPreset() {
|
|
|
16
50
|
v8_middleware: true,
|
|
17
51
|
v8_viteEnvironmentApi: true
|
|
18
52
|
},
|
|
53
|
+
basename: getBasePath() || "/",
|
|
19
54
|
...sfwFalconInstance && { allowedActionOrigins: [`*.dataplane.cvw-dataplane-test.${sfwFalconInstance}.aws.sfdc.cl`] }
|
|
20
55
|
};
|
|
21
56
|
return {
|
|
@@ -28,6 +63,7 @@ function storefrontNextPreset() {
|
|
|
28
63
|
if (reactRouterConfig.ssr !== presetConfig.ssr) errors.push(`ssr: expected ${presetConfig.ssr}, got ${reactRouterConfig.ssr}`);
|
|
29
64
|
if (reactRouterConfig.future?.v8_middleware !== presetConfig.future.v8_middleware) errors.push(`future.v8_middleware: expected ${presetConfig.future.v8_middleware}, got ${reactRouterConfig.future?.v8_middleware}`);
|
|
30
65
|
if (reactRouterConfig.future?.v8_viteEnvironmentApi !== presetConfig.future.v8_viteEnvironmentApi) errors.push(`future.v8_viteEnvironmentApi: expected ${presetConfig.future.v8_viteEnvironmentApi}, got ${reactRouterConfig.future?.v8_viteEnvironmentApi}`);
|
|
66
|
+
if (reactRouterConfig.basename !== presetConfig.basename) errors.push(`basename: expected ${presetConfig.basename}, got ${reactRouterConfig.basename}`);
|
|
31
67
|
if (errors.length > 0) throw new Error(`Storefront Next preset configuration was overridden. The following values must not be modified:\n${errors.map((e) => ` - ${e}`).join("\n")}`);
|
|
32
68
|
}
|
|
33
69
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"react-router.config.js","names":["errors: string[]"],"sources":["../../src/configs/react-router.config.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 */\nimport type { Preset } from '@react-router/dev/config';\n\n/**\n * Storefront Next preset for React Router configuration.\n * This preset enforces standard configuration for SFCC Storefront Next applications.\n * Users cannot override these values - they will be validated and an error will be thrown if modified.\n */\nexport function storefrontNextPreset(): Preset {\n const sfwFalconInstance = process.env.SFW_FALCON_INSTANCE;\n\n const presetConfig = {\n appDirectory: './src',\n buildDirectory: 'build',\n routeDiscovery: { mode: 'initial' as const },\n serverModuleFormat: 'cjs' as const,\n ssr: true,\n future: {\n v8_middleware: true,\n v8_viteEnvironmentApi: true,\n },\n // Allow workspace proxy domain for CSRF protection on form actions\n ...(sfwFalconInstance && {\n allowedActionOrigins: [`*.dataplane.cvw-dataplane-test.${sfwFalconInstance}.aws.sfdc.cl`],\n }),\n };\n\n return {\n name: 'storefront-next-preset',\n reactRouterConfig: () => presetConfig,\n reactRouterConfigResolved: ({ reactRouterConfig }) => {\n // Validate that critical config values have not been overridden\n // Note: We don't validate appDirectory and buildDirectory because they get resolved\n // to absolute paths and we can't reliably determine the correct absolute path\n const errors: string[] = [];\n\n if (reactRouterConfig.routeDiscovery?.mode !== presetConfig.routeDiscovery.mode) {\n errors.push(\n `routeDiscovery.mode: expected \"${presetConfig.routeDiscovery.mode}\", got \"${reactRouterConfig.routeDiscovery?.mode}\"`\n );\n }\n\n if (reactRouterConfig.serverModuleFormat !== presetConfig.serverModuleFormat) {\n errors.push(\n `serverModuleFormat: expected \"${presetConfig.serverModuleFormat}\", got \"${reactRouterConfig.serverModuleFormat}\"`\n );\n }\n\n if (reactRouterConfig.ssr !== presetConfig.ssr) {\n errors.push(`ssr: expected ${presetConfig.ssr}, got ${reactRouterConfig.ssr}`);\n }\n\n if (reactRouterConfig.future?.v8_middleware !== presetConfig.future.v8_middleware) {\n errors.push(\n `future.v8_middleware: expected ${presetConfig.future.v8_middleware}, got ${reactRouterConfig.future?.v8_middleware}`\n );\n }\n\n if (reactRouterConfig.future?.v8_viteEnvironmentApi !== presetConfig.future.v8_viteEnvironmentApi) {\n errors.push(\n `future.v8_viteEnvironmentApi: expected ${presetConfig.future.v8_viteEnvironmentApi}, got ${reactRouterConfig.future?.v8_viteEnvironmentApi}`\n );\n }\n\n if (errors.length > 0) {\n throw new Error(\n `Storefront Next preset configuration was overridden. The following values must not be modified:\\n${errors.map((e) => ` - ${e}`).join('\\n')}`\n );\n }\n },\n };\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"react-router.config.js","names":["errors: string[]"],"sources":["../../src/utils/paths.ts","../../src/configs/react-router.config.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 * Normalize a file path to use forward slashes.\n * On Windows, Node APIs return backslash-separated paths, but ESM import\n * specifiers and Vite module IDs require forward slashes.\n */\nexport function toPosixPath(filePath: string): string {\n return filePath.replace(/\\\\/g, '/');\n}\n\n/**\n * Get the Commerce Cloud API URL from a short code\n */\nexport function getCommerceCloudApiUrl(shortCode: string, proxyHost?: string): string {\n return proxyHost || `https://${shortCode}.api.commercecloud.salesforce.com`;\n}\n\n/**\n * Get the configurable base path for the application.\n * Reads from MRT_ENV_BASE_PATH environment variable.\n *\n * The base path is used for CDN routing to the correct MRT environment.\n * It is prepended to all URLs: page routes, /mobify/bundle/ assets, and /mobify/proxy/api.\n *\n * Validation rules:\n * - Must be a single path segment starting with '/'\n * - Max 63 characters after the leading slash\n * - Only URL-safe characters allowed\n * - Returns empty string if not set\n *\n * @returns The sanitized base path (e.g., '/site-a' or '')\n *\n * @example\n * // No base path configured\n * getBasePath() // → ''\n *\n * // With base path '/storefront'\n * getBasePath() // → '/storefront'\n *\n * // Automatically sanitizes\n * // MRT_ENV_BASE_PATH='storefront/' → '/storefront'\n */\nexport function getBasePath(): string {\n const basePath = process.env.MRT_ENV_BASE_PATH?.trim();\n\n // Return empty string if not set or empty\n if (!basePath) {\n return '';\n }\n\n // Base path prefix must be a single path segment starting with '/', max 63 chars,\n // using only URL-safe characters (alphanumeric, hyphens, underscores, dots, and other safe symbols)\n // This aligns with the regex used by MRT\n if (!/^\\/[a-zA-Z0-9_.+$~\"'@:-]{1,63}$/.test(basePath)) {\n throw new Error(\n `Invalid base path: \"${basePath}\". ` +\n \"Base path must be a single segment starting with '/' (e.g., '/site-a'), \" +\n 'contain only URL-safe characters, and be at most 63 characters after the leading slash.'\n );\n }\n\n return basePath;\n}\n\n/**\n * Get the bundle path for static assets\n */\nexport function getBundlePath(bundleId: string): string {\n const basePath = getBasePath();\n return `${basePath}/mobify/bundle/${bundleId}/client/`;\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 */\nimport type { Preset } from '@react-router/dev/config';\nimport { getBasePath } from '../utils/paths';\n\n/**\n * Storefront Next preset for React Router configuration.\n * This preset enforces standard configuration for SFCC Storefront Next applications.\n * Users cannot override these values - they will be validated and an error will be thrown if modified.\n */\nexport function storefrontNextPreset(): Preset {\n const sfwFalconInstance = process.env.SFW_FALCON_INSTANCE;\n\n // Read base path from env var for basename configuration\n // This sets the base path for all React Router routes (e.g., '/site-a')\n // In dev: reads from .env at build time\n // In production: baked into the build, but can be overridden at runtime via patchReactRouterBuild\n const basePath = getBasePath();\n\n const presetConfig = {\n appDirectory: './src',\n buildDirectory: 'build',\n routeDiscovery: { mode: 'initial' as const },\n serverModuleFormat: 'cjs' as const,\n ssr: true,\n future: {\n v8_middleware: true,\n v8_viteEnvironmentApi: true,\n },\n // Set basename from base path for CDN routing\n // When set, all routes are served under this base path (e.g., /site-a/category/womens)\n // React Router automatically handles Link, navigate, .data requests, and redirects\n basename: basePath || '/',\n // Allow workspace proxy domain for CSRF protection on form actions\n ...(sfwFalconInstance && {\n allowedActionOrigins: [`*.dataplane.cvw-dataplane-test.${sfwFalconInstance}.aws.sfdc.cl`],\n }),\n };\n\n return {\n name: 'storefront-next-preset',\n reactRouterConfig: () => presetConfig,\n reactRouterConfigResolved: ({ reactRouterConfig }) => {\n // Validate that critical config values have not been overridden\n // Note: We don't validate appDirectory and buildDirectory because they get resolved\n // to absolute paths and we can't reliably determine the correct absolute path\n const errors: string[] = [];\n\n if (reactRouterConfig.routeDiscovery?.mode !== presetConfig.routeDiscovery.mode) {\n errors.push(\n `routeDiscovery.mode: expected \"${presetConfig.routeDiscovery.mode}\", got \"${reactRouterConfig.routeDiscovery?.mode}\"`\n );\n }\n\n if (reactRouterConfig.serverModuleFormat !== presetConfig.serverModuleFormat) {\n errors.push(\n `serverModuleFormat: expected \"${presetConfig.serverModuleFormat}\", got \"${reactRouterConfig.serverModuleFormat}\"`\n );\n }\n\n if (reactRouterConfig.ssr !== presetConfig.ssr) {\n errors.push(`ssr: expected ${presetConfig.ssr}, got ${reactRouterConfig.ssr}`);\n }\n\n if (reactRouterConfig.future?.v8_middleware !== presetConfig.future.v8_middleware) {\n errors.push(\n `future.v8_middleware: expected ${presetConfig.future.v8_middleware}, got ${reactRouterConfig.future?.v8_middleware}`\n );\n }\n\n if (reactRouterConfig.future?.v8_viteEnvironmentApi !== presetConfig.future.v8_viteEnvironmentApi) {\n errors.push(\n `future.v8_viteEnvironmentApi: expected ${presetConfig.future.v8_viteEnvironmentApi}, got ${reactRouterConfig.future?.v8_viteEnvironmentApi}`\n );\n }\n\n if (reactRouterConfig.basename !== presetConfig.basename) {\n errors.push(`basename: expected ${presetConfig.basename}, got ${reactRouterConfig.basename}`);\n }\n\n if (errors.length > 0) {\n throw new Error(\n `Storefront Next preset configuration was overridden. The following values must not be modified:\\n${errors.map((e) => ` - ${e}`).join('\\n')}`\n );\n }\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AAwDA,SAAgB,cAAsB;CAClC,MAAM,WAAW,QAAQ,IAAI,mBAAmB,MAAM;AAGtD,KAAI,CAAC,SACD,QAAO;AAMX,KAAI,CAAC,kCAAkC,KAAK,SAAS,CACjD,OAAM,IAAI,MACN,uBAAuB,SAAS,oKAGnC;AAGL,QAAO;;;;;;;;;;ACpDX,SAAgB,uBAA+B;CAC3C,MAAM,oBAAoB,QAAQ,IAAI;CAQtC,MAAM,eAAe;EACjB,cAAc;EACd,gBAAgB;EAChB,gBAAgB,EAAE,MAAM,WAAoB;EAC5C,oBAAoB;EACpB,KAAK;EACL,QAAQ;GACJ,eAAe;GACf,uBAAuB;GAC1B;EAID,UAfa,aAAa,IAeJ;EAEtB,GAAI,qBAAqB,EACrB,sBAAsB,CAAC,kCAAkC,kBAAkB,cAAc,EAC5F;EACJ;AAED,QAAO;EACH,MAAM;EACN,yBAAyB;EACzB,4BAA4B,EAAE,wBAAwB;GAIlD,MAAMA,SAAmB,EAAE;AAE3B,OAAI,kBAAkB,gBAAgB,SAAS,aAAa,eAAe,KACvE,QAAO,KACH,kCAAkC,aAAa,eAAe,KAAK,UAAU,kBAAkB,gBAAgB,KAAK,GACvH;AAGL,OAAI,kBAAkB,uBAAuB,aAAa,mBACtD,QAAO,KACH,iCAAiC,aAAa,mBAAmB,UAAU,kBAAkB,mBAAmB,GACnH;AAGL,OAAI,kBAAkB,QAAQ,aAAa,IACvC,QAAO,KAAK,iBAAiB,aAAa,IAAI,QAAQ,kBAAkB,MAAM;AAGlF,OAAI,kBAAkB,QAAQ,kBAAkB,aAAa,OAAO,cAChE,QAAO,KACH,kCAAkC,aAAa,OAAO,cAAc,QAAQ,kBAAkB,QAAQ,gBACzG;AAGL,OAAI,kBAAkB,QAAQ,0BAA0B,aAAa,OAAO,sBACxE,QAAO,KACH,0CAA0C,aAAa,OAAO,sBAAsB,QAAQ,kBAAkB,QAAQ,wBACzH;AAGL,OAAI,kBAAkB,aAAa,aAAa,SAC5C,QAAO,KAAK,sBAAsB,aAAa,SAAS,QAAQ,kBAAkB,WAAW;AAGjG,OAAI,OAAO,SAAS,EAChB,OAAM,IAAI,MACN,oGAAoG,OAAO,KAAK,MAAM,OAAO,IAAI,CAAC,KAAK,KAAK,GAC/I;;EAGZ"}
|
package/dist/dependency-utils.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { t as logger } from "./logger.js";
|
|
1
2
|
import path from "path";
|
|
2
3
|
import fs from "fs";
|
|
3
4
|
|
|
@@ -17,17 +18,15 @@ const SINGLE_LINE_MARKER = "@sfdc-extension-line";
|
|
|
17
18
|
const BLOCK_MARKER_START = "@sfdc-extension-block-start";
|
|
18
19
|
const BLOCK_MARKER_END = "@sfdc-extension-block-end";
|
|
19
20
|
const FILE_MARKER = "@sfdc-extension-file";
|
|
20
|
-
|
|
21
|
-
function trimExtensions(directory, selectedExtensions, extensionConfig, verboseOverride = false) {
|
|
21
|
+
function trimExtensions(directory, selectedExtensions, extensionConfig) {
|
|
22
22
|
const startTime = Date.now();
|
|
23
|
-
verbose = verboseOverride ?? false;
|
|
24
23
|
const configuredExtensions = extensionConfig?.extensions || {};
|
|
25
24
|
const extensions = {};
|
|
26
25
|
Object.keys(configuredExtensions).forEach((targetKey) => {
|
|
27
26
|
extensions[targetKey] = Boolean(selectedExtensions?.[targetKey]) || false;
|
|
28
27
|
});
|
|
29
28
|
if (Object.keys(extensions).length === 0) {
|
|
30
|
-
|
|
29
|
+
logger.debug("No targets found, skipping trim");
|
|
31
30
|
return;
|
|
32
31
|
}
|
|
33
32
|
const processDirectory = (dir) => {
|
|
@@ -46,7 +45,7 @@ function trimExtensions(directory, selectedExtensions, extensionConfig, verboseO
|
|
|
46
45
|
updateExtensionConfig(directory, extensions);
|
|
47
46
|
}
|
|
48
47
|
const endTime = Date.now();
|
|
49
|
-
|
|
48
|
+
logger.debug(`Trim extensions took ${endTime - startTime}ms`);
|
|
50
49
|
}
|
|
51
50
|
/**
|
|
52
51
|
* Update the extension config file to only include the selected extensions.
|
|
@@ -71,15 +70,14 @@ function processFile(filePath, extensions) {
|
|
|
71
70
|
if (source.includes(FILE_MARKER)) {
|
|
72
71
|
const markerLine = source.split("\n").find((line) => line.includes(FILE_MARKER));
|
|
73
72
|
const extMatch = Object.keys(extensions).find((ext) => markerLine.includes(ext));
|
|
74
|
-
if (!extMatch) {
|
|
75
|
-
|
|
76
|
-
} else if (extensions[extMatch] === false) {
|
|
73
|
+
if (!extMatch) logger.warn(`File ${filePath} is marked with ${markerLine} but it does not match any known extensions`);
|
|
74
|
+
else if (extensions[extMatch] === false) {
|
|
77
75
|
try {
|
|
78
76
|
fs.unlinkSync(filePath);
|
|
79
|
-
|
|
77
|
+
logger.debug(`Deleted file ${filePath}`);
|
|
80
78
|
} catch (e) {
|
|
81
79
|
const error = e;
|
|
82
|
-
|
|
80
|
+
logger.error(`Error deleting file ${filePath}: ${error.message}`);
|
|
83
81
|
throw e;
|
|
84
82
|
}
|
|
85
83
|
return;
|
|
@@ -108,7 +106,7 @@ function processFile(filePath, extensions) {
|
|
|
108
106
|
line: i
|
|
109
107
|
});
|
|
110
108
|
skippingBlock = extensions[matchingExtension] === false;
|
|
111
|
-
} else
|
|
109
|
+
} else logger.warn(`Unknown marker found in ${filePath} at line ${i}: \n${line}`);
|
|
112
110
|
} else if (line.includes(BLOCK_MARKER_END)) {
|
|
113
111
|
if (Object.keys(extensions).find((extension) => line.includes(extension))) {
|
|
114
112
|
const extension = Object.keys(extensions).find((p) => line.includes(p));
|
|
@@ -129,10 +127,10 @@ function processFile(filePath, extensions) {
|
|
|
129
127
|
const newSource = newLines.join("\n");
|
|
130
128
|
if (newSource !== source) try {
|
|
131
129
|
fs.writeFileSync(filePath, newSource);
|
|
132
|
-
|
|
130
|
+
logger.debug(`Updated file ${filePath}`);
|
|
133
131
|
} catch (e) {
|
|
134
132
|
const error = e;
|
|
135
|
-
|
|
133
|
+
logger.error(`Error updating file ${filePath}: ${error.message}`);
|
|
136
134
|
throw e;
|
|
137
135
|
}
|
|
138
136
|
}
|
|
@@ -156,11 +154,11 @@ function deleteExtensionFolders(projectRoot, extensions, extensionConfig) {
|
|
|
156
154
|
recursive: true,
|
|
157
155
|
force: true
|
|
158
156
|
});
|
|
159
|
-
|
|
157
|
+
logger.debug(`Deleted extension folder: ${extensionFolderPath}`);
|
|
160
158
|
} catch (err) {
|
|
161
159
|
const error = err;
|
|
162
|
-
if (error.code === "EPERM")
|
|
163
|
-
else
|
|
160
|
+
if (error.code === "EPERM") logger.error(`Permission denied - cannot delete ${extensionFolderPath}. You may need to run with sudo or check permissions.`);
|
|
161
|
+
else logger.error(`Error deleting ${extensionFolderPath}: ${error.message}`);
|
|
164
162
|
}
|
|
165
163
|
}
|
|
166
164
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","names":[],"sources":["../../src/entry/server.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"server.d.ts","names":[],"sources":["../../src/entry/server.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;iBA4CgB,kBAAA,YAA8B,oBAAoB"}
|
package/dist/entry/server.js
CHANGED
|
@@ -1,18 +1,228 @@
|
|
|
1
|
-
|
|
1
|
+
import { SpanStatusCode } from "@opentelemetry/api";
|
|
2
|
+
import { ATTR_HTTP_REQUEST_METHOD, ATTR_SERVICE_NAME, ATTR_URL_PATH } from "@opentelemetry/semantic-conventions";
|
|
3
|
+
import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
|
|
4
|
+
import { ConsoleSpanExporter, SimpleSpanProcessor } from "@opentelemetry/sdk-trace-base";
|
|
5
|
+
import { ExportResultCode, hrTimeToTimeStamp } from "@opentelemetry/core";
|
|
6
|
+
import { Resource } from "@opentelemetry/resources";
|
|
7
|
+
import { registerInstrumentations } from "@opentelemetry/instrumentation";
|
|
8
|
+
import { UndiciInstrumentation } from "@opentelemetry/instrumentation-undici";
|
|
9
|
+
import chalk from "chalk";
|
|
10
|
+
|
|
11
|
+
//#region src/otel/mrt-console-span-exporter.ts
|
|
12
|
+
var MrtConsoleSpanExporter = class extends ConsoleSpanExporter {
|
|
13
|
+
export(spans, resultCallback) {
|
|
14
|
+
for (const span of spans) try {
|
|
15
|
+
const ctx = span.spanContext();
|
|
16
|
+
const spanData = {
|
|
17
|
+
traceId: ctx.traceId,
|
|
18
|
+
parentId: span.parentSpanId,
|
|
19
|
+
name: span.name,
|
|
20
|
+
id: ctx.spanId,
|
|
21
|
+
kind: span.kind,
|
|
22
|
+
timestamp: hrTimeToTimeStamp(span.startTime),
|
|
23
|
+
duration: span.duration,
|
|
24
|
+
attributes: span.attributes,
|
|
25
|
+
status: span.status,
|
|
26
|
+
events: span.events,
|
|
27
|
+
links: span.links,
|
|
28
|
+
start_time: span.startTime,
|
|
29
|
+
end_time: span.endTime,
|
|
30
|
+
forwardTrace: process.env.SFNEXT_OTEL_ENABLED === "true"
|
|
31
|
+
};
|
|
32
|
+
console.info(JSON.stringify(spanData));
|
|
33
|
+
} catch {}
|
|
34
|
+
resultCallback({ code: ExportResultCode.SUCCESS });
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
//#endregion
|
|
39
|
+
//#region src/utils/logger.ts
|
|
40
|
+
const LEVEL_PRIORITY = {
|
|
41
|
+
error: 0,
|
|
42
|
+
warn: 1,
|
|
43
|
+
info: 2,
|
|
44
|
+
debug: 3
|
|
45
|
+
};
|
|
46
|
+
let overrideLevel;
|
|
47
|
+
/**
|
|
48
|
+
* Returns true when the `DEBUG` env var targets sfnext or is a general enable flag.
|
|
49
|
+
* Avoids accidentally enabling debug mode when DEBUG is set for unrelated libraries
|
|
50
|
+
* (e.g. `DEBUG=express:*`).
|
|
51
|
+
*/
|
|
52
|
+
function debugEnablesSfnext() {
|
|
53
|
+
const raw = process.env.DEBUG?.trim();
|
|
54
|
+
if (!raw) return false;
|
|
55
|
+
const normalized = raw.toLowerCase();
|
|
56
|
+
if ([
|
|
57
|
+
"1",
|
|
58
|
+
"true",
|
|
59
|
+
"yes",
|
|
60
|
+
"on"
|
|
61
|
+
].includes(normalized)) return true;
|
|
62
|
+
return raw.split(",").some((token) => {
|
|
63
|
+
const value = token.trim();
|
|
64
|
+
return value === "*" || value === "sfnext" || value === "sfnext:*";
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
function resolveLevel() {
|
|
68
|
+
if (overrideLevel) return overrideLevel;
|
|
69
|
+
const envLevel = process.env.MRT_LOG_LEVEL ?? process.env.SFCC_LOG_LEVEL;
|
|
70
|
+
if (envLevel && envLevel in LEVEL_PRIORITY) return envLevel;
|
|
71
|
+
if (debugEnablesSfnext()) return "debug";
|
|
72
|
+
if (process.env.NODE_ENV === "production") return "warn";
|
|
73
|
+
return "info";
|
|
74
|
+
}
|
|
75
|
+
function shouldLog(level) {
|
|
76
|
+
return LEVEL_PRIORITY[level] <= LEVEL_PRIORITY[resolveLevel()];
|
|
77
|
+
}
|
|
78
|
+
const logger = {
|
|
79
|
+
error(msg, ...args) {
|
|
80
|
+
if (!shouldLog("error")) return;
|
|
81
|
+
console.error(chalk.red("[sfnext:error]"), msg, ...args);
|
|
82
|
+
},
|
|
83
|
+
warn(msg, ...args) {
|
|
84
|
+
if (!shouldLog("warn")) return;
|
|
85
|
+
console.warn(chalk.yellow("[sfnext:warn]"), msg, ...args);
|
|
86
|
+
},
|
|
87
|
+
info(msg, ...args) {
|
|
88
|
+
if (!shouldLog("info")) return;
|
|
89
|
+
console.log(chalk.cyan("[sfnext:info]"), msg, ...args);
|
|
90
|
+
},
|
|
91
|
+
debug(msg, ...args) {
|
|
92
|
+
if (!shouldLog("debug")) return;
|
|
93
|
+
console.log(chalk.gray("[sfnext:debug]"), msg, ...args);
|
|
94
|
+
},
|
|
95
|
+
setLevel(level) {
|
|
96
|
+
overrideLevel = level;
|
|
97
|
+
},
|
|
98
|
+
getLevel() {
|
|
99
|
+
return resolveLevel();
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
//#endregion
|
|
104
|
+
//#region src/otel/setup.ts
|
|
105
|
+
const SERVICE_NAME = "storefront-next";
|
|
2
106
|
/**
|
|
3
|
-
*
|
|
107
|
+
* Initializes OpenTelemetry and returns a Tracer from the provider directly.
|
|
4
108
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
109
|
+
* Returns the tracer via `provider.getTracer()` instead of the global
|
|
110
|
+
* `trace.getTracer()` API. In the Vite SSR module runner, the built
|
|
111
|
+
* dist/entry/server.js and the externalized @opentelemetry/sdk-trace-node
|
|
112
|
+
* resolve @opentelemetry/api to different module instances (different paths
|
|
113
|
+
* through pnpm's strict node_modules). Each instance has its own
|
|
114
|
+
* ProxyTracerProvider singleton, so `provider.register()` sets the delegate
|
|
115
|
+
* on sdk-trace-node's API instance while our code's `trace.getTracer()`
|
|
116
|
+
* reads from a separate API instance with no delegate — returning a tracer
|
|
117
|
+
* backed by a bare BasicTracerProvider with NoopSpanProcessor.
|
|
8
118
|
*
|
|
9
|
-
*
|
|
119
|
+
* Getting the tracer directly from the provider bypasses the global registry
|
|
120
|
+
* entirely, guaranteeing the tracer uses our configured span processors.
|
|
10
121
|
*/
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
122
|
+
let cachedTracer = null;
|
|
123
|
+
const UNDICI_REGISTERED_KEY = Symbol.for("sfnext.otel.undici_registered");
|
|
124
|
+
function initTelemetry() {
|
|
125
|
+
if (cachedTracer) return cachedTracer;
|
|
126
|
+
try {
|
|
127
|
+
const provider = new NodeTracerProvider({ resource: new Resource({ [ATTR_SERVICE_NAME]: SERVICE_NAME }) });
|
|
128
|
+
provider.addSpanProcessor(new SimpleSpanProcessor(new MrtConsoleSpanExporter()));
|
|
129
|
+
provider.register();
|
|
130
|
+
if (!globalThis[UNDICI_REGISTERED_KEY]) {
|
|
131
|
+
globalThis[UNDICI_REGISTERED_KEY] = true;
|
|
132
|
+
registerInstrumentations({
|
|
133
|
+
tracerProvider: provider,
|
|
134
|
+
instrumentations: [new UndiciInstrumentation({ requestHook(span, request) {
|
|
135
|
+
try {
|
|
136
|
+
const method = request.method.toUpperCase();
|
|
137
|
+
const url = `${request.origin}${request.path}`;
|
|
138
|
+
span.updateName(`${method} ${url}`);
|
|
139
|
+
} catch {}
|
|
140
|
+
} })]
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
cachedTracer = provider.getTracer(SERVICE_NAME);
|
|
144
|
+
return cachedTracer;
|
|
145
|
+
} catch (error) {
|
|
146
|
+
logger.error("[otel] Failed to initialize OpenTelemetry:", error);
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
//#endregion
|
|
152
|
+
//#region src/otel/react-router/instrumentation.ts
|
|
153
|
+
const tracer = process.env.SFNEXT_OTEL_ENABLED === "true" ? initTelemetry() : null;
|
|
154
|
+
/**
|
|
155
|
+
* Runs `handle` inside an active OTel span, recording errors and ending the span.
|
|
156
|
+
* When `tracer` is null (OTel disabled), calls `handle` directly with no overhead.
|
|
157
|
+
*/
|
|
158
|
+
async function traced(spanName, attributes, handle) {
|
|
159
|
+
if (!tracer) {
|
|
160
|
+
await handle();
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
let handled = false;
|
|
164
|
+
try {
|
|
165
|
+
await tracer.startActiveSpan(spanName, { attributes }, async (span) => {
|
|
166
|
+
try {
|
|
167
|
+
handled = true;
|
|
168
|
+
const result = await handle();
|
|
169
|
+
if (result.status === "error" && result.error) {
|
|
170
|
+
span.setStatus({
|
|
171
|
+
code: SpanStatusCode.ERROR,
|
|
172
|
+
message: result.error.message
|
|
173
|
+
});
|
|
174
|
+
span.recordException(result.error);
|
|
175
|
+
}
|
|
176
|
+
} finally {
|
|
177
|
+
span.end();
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
} catch {
|
|
181
|
+
if (!handled) await handle();
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* HTTP attributes common to all spans.
|
|
186
|
+
* url.path only — url.full would expose query params which may contain auth
|
|
187
|
+
* tokens or PII. http.response.status_code is not available from
|
|
188
|
+
* unstable_InstrumentationHandlerResult.
|
|
189
|
+
*/
|
|
190
|
+
function httpAttributes(request) {
|
|
191
|
+
const attrs = { [ATTR_HTTP_REQUEST_METHOD]: request.method };
|
|
192
|
+
try {
|
|
193
|
+
attrs[ATTR_URL_PATH] = new URL(request.url).pathname;
|
|
194
|
+
} catch {}
|
|
195
|
+
return attrs;
|
|
196
|
+
}
|
|
197
|
+
const platformInstrumentation = {
|
|
198
|
+
handler(handler) {
|
|
199
|
+
handler.instrument({ async request(handleRequest, { request }) {
|
|
200
|
+
await traced("react-router ssr", httpAttributes(request), handleRequest);
|
|
201
|
+
} });
|
|
202
|
+
},
|
|
203
|
+
route(route) {
|
|
204
|
+
function routeAttributes(unstable_pattern) {
|
|
205
|
+
return {
|
|
206
|
+
"rr.route.id": route.id,
|
|
207
|
+
"rr.route.pattern": unstable_pattern
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
route.instrument({
|
|
211
|
+
async loader(handleLoader, { unstable_pattern }) {
|
|
212
|
+
await traced(`loader (${route.id})`, routeAttributes(unstable_pattern), handleLoader);
|
|
213
|
+
},
|
|
214
|
+
async action(handleAction, { unstable_pattern }) {
|
|
215
|
+
await traced(`action (${route.id})`, routeAttributes(unstable_pattern), handleAction);
|
|
216
|
+
},
|
|
217
|
+
async middleware(handleMiddleware, { unstable_pattern }) {
|
|
218
|
+
await traced(`middleware (${route.id})`, routeAttributes(unstable_pattern), handleMiddleware);
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
//#endregion
|
|
225
|
+
//#region src/entry/server.ts
|
|
16
226
|
/**
|
|
17
227
|
* Composes a server entry module with platform-level features.
|
|
18
228
|
*
|
package/dist/entry/server.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.js","names":["platformInstrumentation: unstable_ServerInstrumentation"],"sources":["../../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 * 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 * - Adds a handler-level 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, unstable_ServerInstrumentation } from 'react-router';\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 level. This runs around ALL requests\n * (document + data) and provides a hook for platform-level tracing.\n *\n * @see https://reactrouter.com/how-to/instrumentation\n */\nconst platformInstrumentation: unstable_ServerInstrumentation = {\n handler(handler) {\n handler.instrument({\n async request(handleRequest) {\n await handleRequest();\n },\n });\n },\n};\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":";;;;;;;;;;AA6CA,MAAMA,0BAA0D,EAC5D,QAAQ,SAAS;AACb,SAAQ,WAAW,EACf,MAAM,QAAQ,eAAe;AACzB,QAAM,eAAe;IAE5B,CAAC;GAET;;;;;;;;AASD,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"}
|
|
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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"}
|