@salesforce/storefront-next-dev 1.1.0 → 1.2.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.
@@ -1 +1 @@
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 *\n * Environment variables:\n * - `SFW_FALCON_INSTANCE` — (Optional) The Falcon instance identifier (e.g., `aws-dev2-uswest2`).\n * When set together with `SFW_FUNCTIONAL_DOMAIN`, adds workspace proxy domains to\n * `allowedActionOrigins` for CSRF protection on form actions.\n * - `SFW_FUNCTIONAL_DOMAIN` — (Optional) The functional domain name (e.g., `cvw-dataplane-test`).\n * Required alongside `SFW_FALCON_INSTANCE` to construct workspace origin patterns.\n */\nexport function storefrontNextPreset(): Preset {\n const sfwFalconInstance = process.env.SFW_FALCON_INSTANCE;\n const sfwFunctionalDomain = process.env.SFW_FUNCTIONAL_DOMAIN;\n\n if (sfwFalconInstance && !sfwFunctionalDomain) {\n console.warn(\n '[storefront-next] SFW_FALCON_INSTANCE is set but SFW_FUNCTIONAL_DOMAIN is not. ' +\n 'allowedActionOrigins will not include workspace domains. ' +\n 'Set both env vars to enable CSRF protection for workspace proxy origins.'\n );\n }\n\n if (sfwFunctionalDomain && !sfwFalconInstance) {\n console.warn(\n '[storefront-next] SFW_FUNCTIONAL_DOMAIN is set but SFW_FALCON_INSTANCE is not. ' +\n 'allowedActionOrigins will not include workspace domains. ' +\n 'Set both env vars to enable CSRF protection for workspace proxy origins.'\n );\n }\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 unstable_optimizeDeps: 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 domains for CSRF protection on form actions\n // Both the legacy port-based dataplane format and the Pomerium reverse proxy format are supported\n ...(sfwFalconInstance &&\n sfwFunctionalDomain && {\n allowedActionOrigins: [\n `*.dataplane.${sfwFunctionalDomain}.${sfwFalconInstance}.aws.sfdc.cl`,\n `*.platform.a.${sfwFunctionalDomain}.${sfwFalconInstance}.aws.sfdc.cl`,\n ],\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 // Only validate allowedActionOrigins when the preset sets it (workspace env).\n // In prod builds (no workspace env vars), customers are free to configure their own origins.\n if (\n presetConfig.allowedActionOrigins &&\n JSON.stringify(reactRouterConfig.allowedActionOrigins) !==\n JSON.stringify(presetConfig.allowedActionOrigins)\n ) {\n errors.push(\n `allowedActionOrigins: expected ${JSON.stringify(presetConfig.allowedActionOrigins)}, got ${JSON.stringify(reactRouterConfig.allowedActionOrigins)}`\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":";;;;;;;;;;;;;;;;;;;;;;;;;;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;;;;;;;;;;;;;;;;;AC7CX,SAAgB,uBAA+B;CAC3C,MAAM,oBAAoB,QAAQ,IAAI;CACtC,MAAM,sBAAsB,QAAQ,IAAI;AAExC,KAAI,qBAAqB,CAAC,oBACtB,SAAQ,KACJ,mNAGH;AAGL,KAAI,uBAAuB,CAAC,kBACxB,SAAQ,KACJ,mNAGH;CASL,MAAM,eAAe;EACjB,cAAc;EACd,gBAAgB;EAChB,gBAAgB,EAAE,MAAM,WAAoB;EAC5C,oBAAoB;EACpB,KAAK;EACL,QAAQ;GACJ,eAAe;GACf,uBAAuB;GACvB,uBAAuB;GAC1B;EAID,UAhBa,aAAa,IAgBJ;EAGtB,GAAI,qBACA,uBAAuB,EACnB,sBAAsB,CAClB,eAAe,oBAAoB,GAAG,kBAAkB,eACxD,gBAAgB,oBAAoB,GAAG,kBAAkB,cAC5D,EACJ;EACR;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;AAKjG,OACI,aAAa,wBACb,KAAK,UAAU,kBAAkB,qBAAqB,KAClD,KAAK,UAAU,aAAa,qBAAqB,CAErD,QAAO,KACH,kCAAkC,KAAK,UAAU,aAAa,qBAAqB,CAAC,QAAQ,KAAK,UAAU,kBAAkB,qBAAqB,GACrJ;AAGL,OAAI,OAAO,SAAS,EAChB,OAAM,IAAI,MACN,oGAAoG,OAAO,KAAK,MAAM,OAAO,IAAI,CAAC,KAAK,KAAK,GAC/I;;EAGZ"}
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 * Most pinned values (`serverModuleFormat`, `ssr`, the `v8` future flags, `basename`, and — in\n * workspace environments — `allowedActionOrigins`) cannot be overridden: they are validated and an\n * error is thrown if modified. `routeDiscovery.mode` is the one exception — it defaults to\n * `'initial'` but may be overridden (e.g. to `'lazy'`); overriding it emits a warning rather than\n * throwing, so customers can opt into other modes at their own risk.\n *\n * Environment variables:\n * - `SFW_FALCON_INSTANCE` — (Optional) The Falcon instance identifier (e.g., `aws-dev2-uswest2`).\n * When set together with `SFW_FUNCTIONAL_DOMAIN`, adds workspace proxy domains to\n * `allowedActionOrigins` for CSRF protection on form actions.\n * - `SFW_FUNCTIONAL_DOMAIN` — (Optional) The functional domain name (e.g., `cvw-dataplane-test`).\n * Required alongside `SFW_FALCON_INSTANCE` to construct workspace origin patterns.\n */\nexport function storefrontNextPreset(): Preset {\n const sfwFalconInstance = process.env.SFW_FALCON_INSTANCE;\n const sfwFunctionalDomain = process.env.SFW_FUNCTIONAL_DOMAIN;\n\n if (sfwFalconInstance && !sfwFunctionalDomain) {\n console.warn(\n '[storefront-next] SFW_FALCON_INSTANCE is set but SFW_FUNCTIONAL_DOMAIN is not. ' +\n 'allowedActionOrigins will not include workspace domains. ' +\n 'Set both env vars to enable CSRF protection for workspace proxy origins.'\n );\n }\n\n if (sfwFunctionalDomain && !sfwFalconInstance) {\n console.warn(\n '[storefront-next] SFW_FUNCTIONAL_DOMAIN is set but SFW_FALCON_INSTANCE is not. ' +\n 'allowedActionOrigins will not include workspace domains. ' +\n 'Set both env vars to enable CSRF protection for workspace proxy origins.'\n );\n }\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 unstable_optimizeDeps: 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 domains for CSRF protection on form actions\n // Both the legacy port-based dataplane format and the Pomerium reverse proxy format are supported\n ...(sfwFalconInstance &&\n sfwFunctionalDomain && {\n allowedActionOrigins: [\n `*.dataplane.${sfwFunctionalDomain}.${sfwFalconInstance}.aws.sfdc.cl`,\n `*.platform.a.${sfwFunctionalDomain}.${sfwFalconInstance}.aws.sfdc.cl`,\n ],\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 // routeDiscovery.mode defaults to 'initial' but, unlike the values below, overriding it\n // is NOT a hard error. Customers may opt into another mode (e.g. 'lazy' route discovery)\n // to reduce the initial route-manifest payload on large route trees. This is unsupported\n // and may behave unexpectedly, so we warn instead of throwing.\n if (reactRouterConfig.routeDiscovery?.mode !== presetConfig.routeDiscovery.mode) {\n console.warn(\n `[storefront-next] routeDiscovery.mode is \"${reactRouterConfig.routeDiscovery?.mode}\", but Storefront Next pins it to \"${presetConfig.routeDiscovery.mode}\". ` +\n 'Overriding it is unsupported and may cause unexpected behavior — test thoroughly before deploying.'\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 // Only validate allowedActionOrigins when the preset sets it (workspace env).\n // In prod builds (no workspace env vars), customers are free to configure their own origins.\n if (\n presetConfig.allowedActionOrigins &&\n JSON.stringify(reactRouterConfig.allowedActionOrigins) !==\n JSON.stringify(presetConfig.allowedActionOrigins)\n ) {\n errors.push(\n `allowedActionOrigins: expected ${JSON.stringify(presetConfig.allowedActionOrigins)}, got ${JSON.stringify(reactRouterConfig.allowedActionOrigins)}`\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":";;;;;;;;;;;;;;;;;;;;;;;;;;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;;;;;;;;;;;;;;;;;;;;;ACzCX,SAAgB,uBAA+B;CAC3C,MAAM,oBAAoB,QAAQ,IAAI;CACtC,MAAM,sBAAsB,QAAQ,IAAI;AAExC,KAAI,qBAAqB,CAAC,oBACtB,SAAQ,KACJ,mNAGH;AAGL,KAAI,uBAAuB,CAAC,kBACxB,SAAQ,KACJ,mNAGH;CASL,MAAM,eAAe;EACjB,cAAc;EACd,gBAAgB;EAChB,gBAAgB,EAAE,MAAM,WAAoB;EAC5C,oBAAoB;EACpB,KAAK;EACL,QAAQ;GACJ,eAAe;GACf,uBAAuB;GACvB,uBAAuB;GAC1B;EAID,UAhBa,aAAa,IAgBJ;EAGtB,GAAI,qBACA,uBAAuB,EACnB,sBAAsB,CAClB,eAAe,oBAAoB,GAAG,kBAAkB,eACxD,gBAAgB,oBAAoB,GAAG,kBAAkB,cAC5D,EACJ;EACR;AAED,QAAO;EACH,MAAM;EACN,yBAAyB;EACzB,4BAA4B,EAAE,wBAAwB;GAIlD,MAAMA,SAAmB,EAAE;AAM3B,OAAI,kBAAkB,gBAAgB,SAAS,aAAa,eAAe,KACvE,SAAQ,KACJ,6CAA6C,kBAAkB,gBAAgB,KAAK,qCAAqC,aAAa,eAAe,KAAK,uGAE7J;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;AAKjG,OACI,aAAa,wBACb,KAAK,UAAU,kBAAkB,qBAAqB,KAClD,KAAK,UAAU,aAAa,qBAAqB,CAErD,QAAO,KACH,kCAAkC,KAAK,UAAU,aAAa,qBAAqB,CAAC,QAAQ,KAAK,UAAU,kBAAkB,qBAAqB,GACrJ;AAGL,OAAI,OAAO,SAAS,EAChB,OAAM,IAAI,MACN,oGAAoG,OAAO,KAAK,MAAM,OAAO,IAAI,CAAC,KAAK,KAAK,GAC/I;;EAGZ"}
@@ -1,6 +1,6 @@
1
1
  import { t as logger } from "./logger.js";
2
+ import { n as formatWithProjectBiome } from "./format-with-project-biome.js";
2
3
  import path from "path";
3
- import { createRequire } from "module";
4
4
  import fs from "fs";
5
5
 
6
6
  //#region src/extensibility/path-util.ts
@@ -42,7 +42,7 @@ async function trimExtensions(directory, selectedExtensions, extensionConfig) {
42
42
  };
43
43
  processDirectory(directory);
44
44
  if (extensionConfig?.extensions) {
45
- await updateExtensionConfig(directory, extensions);
45
+ updateExtensionConfig(directory, extensions);
46
46
  deleteExtensionFolders(directory, extensions, extensionConfig);
47
47
  }
48
48
  const endTime = Date.now();
@@ -53,64 +53,14 @@ async function trimExtensions(directory, selectedExtensions, extensionConfig) {
53
53
  * @param projectDirectory - The project directory
54
54
  * @param extensionSelections - The selected extensions
55
55
  */
56
- async function updateExtensionConfig(projectDirectory, extensionSelections) {
56
+ function updateExtensionConfig(projectDirectory, extensionSelections) {
57
57
  const extensionConfigPath = path.join(projectDirectory, "src", "extensions", "config.json");
58
58
  const extensionConfig = JSON.parse(fs.readFileSync(extensionConfigPath, "utf8"));
59
59
  Object.keys(extensionConfig.extensions).forEach((extensionKey) => {
60
60
  if (!extensionSelections[extensionKey]) delete extensionConfig.extensions[extensionKey];
61
61
  });
62
62
  const json = JSON.stringify({ extensions: extensionConfig.extensions }, null, 4);
63
- fs.writeFileSync(extensionConfigPath, await formatWithProjectPrettier(json, extensionConfigPath), "utf8");
64
- }
65
- /**
66
- * Format generated JSON/JS so the written file matches what the project's
67
- * `prettier --write` / `pnpm lint` would produce.
68
- *
69
- * `trimExtensions` runs during `create-storefront`, BEFORE the generated project's
70
- * `pnpm install` — so the project's own Prettier is usually not on disk yet. We therefore
71
- * prefer the consumer's Prettier when it happens to be present (re-runs, manage-extensions
72
- * after install), but fall back to the SDK-bundled `prettier` (a hard dependency of this
73
- * package, pinned to the template's version) so formatting is deterministic and available
74
- * at generate time. Relying on the consumer's copy alone resolved only by accident in the
75
- * monorepo harness, where the generated dir is nested under the monorepo and `createRequire`
76
- * walks up to the monorepo's Prettier; a customer in a clean directory would get unformatted
77
- * output and fail lint on first run (W-23074938).
78
- *
79
- * NOTE: mirror of the helper in
80
- * packages/template-retail-rsc-app/scripts/generate-eslint-config.js — kept separate because
81
- * the two live in different packages/module systems. Keep the parser/config-resolution
82
- * behavior in sync; the fallback chains INTENTIONALLY differ — this copy has a two-level
83
- * fallback (consumer Prettier → SDK-bundled `import('prettier')` → unformatted) because it
84
- * runs pre-install, while the generator copy has a single fallback (consumer Prettier →
85
- * unformatted). Don't "fix" that asymmetry or you reintroduce the pre-install bug (W-23074938).
86
- *
87
- * @param content - The serialized file content to format.
88
- * @param filePath - The file's path (drives parser selection + config resolution).
89
- * @returns The Prettier-formatted content. Returns the content unchanged only if no Prettier
90
- * can be resolved at all; a genuine format/config error throws rather than silently shipping
91
- * unformatted output.
92
- */
93
- async function formatWithProjectPrettier(content, filePath) {
94
- let prettier;
95
- try {
96
- prettier = createRequire(filePath)("prettier");
97
- } catch {
98
- try {
99
- prettier = (await import("prettier")).default;
100
- } catch {
101
- logger.warn("⚠️ Prettier could not be resolved; extension config.json will be written unformatted.");
102
- return content;
103
- }
104
- }
105
- try {
106
- const config = await prettier.resolveConfig(filePath, { editorconfig: true });
107
- return await prettier.format(content, {
108
- ...config,
109
- filepath: filePath
110
- });
111
- } catch (error) {
112
- throw new Error(`Prettier formatting failed for ${path.basename(filePath)}: ${error.message}`);
113
- }
63
+ fs.writeFileSync(extensionConfigPath, formatWithProjectBiome(json, extensionConfigPath), "utf8");
114
64
  }
115
65
  /**
116
66
  * Process a file to trim extension-specific code based on markers.
@@ -1,5 +1,6 @@
1
1
  import { SpanStatusCode, context, trace } from "@opentelemetry/api";
2
2
  import { ATTR_HTTP_REQUEST_METHOD, ATTR_HTTP_ROUTE, ATTR_SERVICE_NAME, ATTR_URL_PATH } from "@opentelemetry/semantic-conventions";
3
+ import { siteContext } from "@salesforce/storefront-next-runtime/site-context";
3
4
  import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
4
5
  import { AlwaysOnSampler, ConsoleSpanExporter, ParentBasedSampler, SimpleSpanProcessor } from "@opentelemetry/sdk-trace-base";
5
6
  import { ExportResultCode, W3CTraceContextPropagator, hrTimeToTimeStamp } from "@opentelemetry/core";
@@ -196,10 +197,46 @@ function initTelemetry() {
196
197
  //#region src/otel/react-router/instrumentation.ts
197
198
  const tracer = process.env.SFNEXT_OTEL_ENABLED === "true" ? initTelemetry() : null;
198
199
  /**
200
+ * Reads the resolved site from the router context, or `undefined` if the site
201
+ * has not been resolved yet (or the context is unavailable).
202
+ *
203
+ * `siteContext` is set by the site-context middleware (which runs early in the
204
+ * middleware chain). We resolve it from the same `@salesforce/storefront-next-runtime`
205
+ * module the template's middleware writes to, so both share the one `createContext`
206
+ * singleton and this read observes the value that middleware set. Wrapped in a
207
+ * try/catch because OTel instrumentation must never break the request pipeline.
208
+ */
209
+ function readSiteId(context$1) {
210
+ try {
211
+ if (typeof context$1?.get !== "function") return void 0;
212
+ return context$1.get(siteContext)?.site?.id;
213
+ } catch {
214
+ return;
215
+ }
216
+ }
217
+ /**
199
218
  * Runs `handle` inside an active OTel span, recording errors and ending the span.
200
219
  * When `tracer` is null (OTel disabled), calls `handle` directly with no overhead.
220
+ *
221
+ * The resolved site id is read from `context` and stamped just before the span
222
+ * ends. It is read at span end (not span start) because the site-context
223
+ * middleware runs as part of the wrapped work — at span start the site may not be
224
+ * resolved yet; by span end it is. The site id is a bounded, low-cardinality value.
225
+ *
226
+ * It is emitted under the key `site_name` (not `sfnext.site_id`): the MRT log→trace
227
+ * bridge filters span attributes against an allow list and only keeps matching
228
+ * keys, and `site_name` is on that list (confirmed with the MRT team) while
229
+ * `site_id`/`sfnext.*` keys are not. The site config carries no separate display
230
+ * name, so the site id is the value we have. See ../README.md ("MRT log→trace bridge").
231
+ *
232
+ * Route-level spans (`sfnext.loader`/`.action`/`.middleware`) always receive the
233
+ * router `context`, so they carry `site_name`. The handler-level `sfnext.ssr` span
234
+ * does NOT: React Router passes `context: undefined` to the request-handler
235
+ * instrumentation (confirmed at runtime), so `readSiteId` returns `undefined`
236
+ * there and the attribute is simply omitted. No branching needed — the
237
+ * `if (siteId)` guard handles both cases uniformly.
201
238
  */
202
- async function traced(spanName, attributes, handle) {
239
+ async function traced(spanName, attributes, context$1, handle) {
203
240
  if (!tracer) {
204
241
  await handle();
205
242
  return;
@@ -218,6 +255,8 @@ async function traced(spanName, attributes, handle) {
218
255
  span.recordException(result.error);
219
256
  }
220
257
  } finally {
258
+ const siteId = readSiteId(context$1);
259
+ if (siteId) span.setAttribute("site_name", siteId);
221
260
  span.end();
222
261
  }
223
262
  });
@@ -240,26 +279,23 @@ function httpAttributes(request) {
240
279
  }
241
280
  const platformInstrumentation = {
242
281
  handler(handler) {
243
- handler.instrument({ async request(handleRequest, { request }) {
244
- await traced("sfnext.ssr", httpAttributes(request), handleRequest);
282
+ handler.instrument({ async request(handleRequest, { request, context: context$1 }) {
283
+ await traced("sfnext.ssr", httpAttributes(request), context$1, handleRequest);
245
284
  } });
246
285
  },
247
286
  route(route) {
248
287
  function routeAttributes(pattern) {
249
- return {
250
- "rr.route.id": route.id,
251
- [ATTR_HTTP_ROUTE]: pattern
252
- };
288
+ return { [ATTR_HTTP_ROUTE]: pattern };
253
289
  }
254
290
  route.instrument({
255
- async loader(handleLoader, { pattern }) {
256
- await traced("sfnext.loader", routeAttributes(pattern), handleLoader);
291
+ async loader(handleLoader, { pattern, context: context$1 }) {
292
+ await traced("sfnext.loader", routeAttributes(pattern), context$1, handleLoader);
257
293
  },
258
- async action(handleAction, { pattern }) {
259
- await traced("sfnext.action", routeAttributes(pattern), handleAction);
294
+ async action(handleAction, { pattern, context: context$1 }) {
295
+ await traced("sfnext.action", routeAttributes(pattern), context$1, handleAction);
260
296
  },
261
- async middleware(handleMiddleware, { pattern }) {
262
- await traced("sfnext.middleware", routeAttributes(pattern), handleMiddleware);
297
+ async middleware(handleMiddleware, { pattern, context: context$1 }) {
298
+ await traced("sfnext.middleware", routeAttributes(pattern), context$1, handleMiddleware);
263
299
  }
264
300
  });
265
301
  }
@@ -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: ServerInstrumentation","context"],"sources":["../../src/otel/mrt-console-span-exporter.ts","../../src/utils/logger.ts","../../src/otel/setup.ts","../../src/otel/react-router/instrumentation.ts","../../src/entry/server.ts"],"sourcesContent":["/**\n * Copyright 2026 Salesforce, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * MRT-compatible console span exporter.\n *\n * Extends the default ConsoleSpanExporter to output structured JSON that\n * Managed Runtime's log infrastructure can parse. The default exporter uses\n * `console.dir` with a human-readable format; this override uses\n * `console.info(JSON.stringify(...))` to produce machine-parseable JSON\n * matching the format MRT expects.\n *\n * Inherits `shutdown()` and `forceFlush()` from ConsoleSpanExporter.\n */\n\nimport { ConsoleSpanExporter, type ReadableSpan } from '@opentelemetry/sdk-trace-base';\nimport { ExportResultCode, type ExportResult, hrTimeToTimeStamp } from '@opentelemetry/core';\n\nexport class MrtConsoleSpanExporter extends ConsoleSpanExporter {\n export(spans: ReadableSpan[], resultCallback: (result: ExportResult) => void): void {\n for (const span of spans) {\n try {\n const ctx = span.spanContext();\n const spanData = {\n traceId: ctx.traceId,\n parentId: span.parentSpanId,\n name: span.name,\n id: ctx.spanId,\n kind: span.kind,\n timestamp: hrTimeToTimeStamp(span.startTime),\n duration: span.duration,\n attributes: span.attributes,\n status: span.status,\n events: span.events,\n links: span.links,\n start_time: span.startTime,\n end_time: span.endTime,\n forwardTrace: process.env.SFNEXT_OTEL_ENABLED === 'true',\n };\n // 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 parses the\n * inbound `traceparent` into the parent context).\n * - It forwards `traceparent` on outbound fetches (the UndiciInstrumentation\n * `requestHook` below injects it via a privately-held W3C propagator — see the\n * `outboundPropagator` doc comment for why the global registry is bypassed).\n * - It honors MRT's sampling decision via a ParentBasedSampler — it never forces\n * a sampled trace and implements no sampling policy of its own. MRT owns the\n * rate via MRT_DISTRIBUTED_TRACING_ENABLED / MRT_DISTRIBUTED_TRACING_SAMPLING_RATE.\n *\n * @env SFNEXT_OTEL_ENABLED — set to `\"true\"` to enable (e.g. `SFNEXT_OTEL_ENABLED=true pnpm dev`)\n */\n\nimport { context, trace, type Tracer } from '@opentelemetry/api';\nimport { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';\nimport { AlwaysOnSampler, ParentBasedSampler, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base';\nimport { W3CTraceContextPropagator } from '@opentelemetry/core';\nimport { MrtConsoleSpanExporter } from './mrt-console-span-exporter';\nimport { Resource } from '@opentelemetry/resources';\nimport { ATTR_SERVICE_NAME } from '@opentelemetry/semantic-conventions';\nimport { registerInstrumentations } from '@opentelemetry/instrumentation';\nimport { UndiciInstrumentation, type UndiciRequest } from '@opentelemetry/instrumentation-undici';\nimport { logger } from '../logger';\n\n/**\n * A privately-held W3C Trace Context propagator, used to inject `traceparent` onto\n * outbound fetches WITHOUT going through OpenTelemetry's global propagator registry.\n *\n * ## Why not the global propagator?\n *\n * `UndiciInstrumentation` injects outbound headers by calling the *global* registered\n * propagator (`propagation.inject()`). Registering one with `setGlobalPropagator()`\n * works locally, but is a silent no-op on Managed Runtime (MRT):\n *\n * - The MRT Lambda wrapper bundles its own copy of `@opentelemetry/api` (declared\n * `^1.9.1`, with core/sdk at 2.x) and boots first, creating the version-keyed\n * global registry object (`globalThis[Symbol.for('opentelemetry.js.api.1')]`,\n * stamped `version: \"1.9.1\"`).\n * - Our SDK bundle ships a *different* copy of `@opentelemetry/api` (`1.9.0`). When\n * our code calls `setGlobalPropagator()`, OTel's `registerGlobal()` compares the\n * registry's recorded version against ours with an EXACT string match — `\"1.9.1\"\n * !== \"1.9.0\"` — refuses the registration, and reports the error only to OTel's\n * internal diag channel (which we do not wire up). The propagation slot stays\n * empty, so undici's `propagation.inject()` injects nothing and no `traceparent`\n * reaches SCAPI. Proven on soak: `propagation.fields() === []`, 0/198 outbound\n * requests carried a header.\n *\n * This is the same multi-instance split that makes inbound `propagation.extract()`\n * fail (see `express/middleware.ts`, which bypasses it with `parseTraceParent` +\n * `trace.setSpanContext`). MRT's own data-plane tracer solves it the same way: it\n * holds a private `new W3CTraceContextPropagator()` and calls `inject()`/`extract()`\n * on it directly, never touching the global registry.\n *\n * Holding our own instance and injecting it ourselves (in the undici `requestHook`)\n * makes outbound propagation deterministic and independent of MRT's bundle, its OTel\n * version, and its boot order. We deliberately do NOT call `setGlobalPropagator()` —\n * leaving the global propagator as the default no-op guarantees undici's own\n * `propagation.inject()` (which runs right after our hook) adds nothing, so the\n * `traceparent` is written exactly once in every environment.\n */\nconst outboundPropagator = new W3CTraceContextPropagator();\n\nexport const SERVICE_NAME = 'storefront-next';\n\n/**\n * Initializes OpenTelemetry and returns a Tracer from the provider directly.\n *\n * Returns the tracer via `provider.getTracer()` instead of the global\n * `trace.getTracer()` API. In the Vite SSR module runner, the built\n * dist/entry/server.js and the externalized @opentelemetry/sdk-trace-node\n * resolve @opentelemetry/api to different module instances (different paths\n * through pnpm's strict node_modules). Each instance has its own\n * ProxyTracerProvider singleton, so `provider.register()` sets the delegate\n * on sdk-trace-node's API instance while our code's `trace.getTracer()`\n * reads from a separate API instance with no delegate — returning a tracer\n * backed by a bare BasicTracerProvider with NoopSpanProcessor.\n *\n * Getting the tracer directly from the provider bypasses the global registry\n * entirely, guaranteeing the tracer uses our configured span processors.\n */\nlet cachedTracer: Tracer | null = null;\n\n// In the Vite SSR module runner, setup.ts may be loaded by two different module\n// instances (the Express server and the SSR bundle) — each with its own\n// `cachedTracer`. UndiciInstrumentation hooks into process-global\n// diagnostics_channel events, so registering it twice causes duplicate spans\n// for every fetch. This process-global flag ensures it is registered only once.\nconst UNDICI_REGISTERED_KEY = Symbol.for('sfnext.otel.undici_registered');\n\nexport function initTelemetry(): Tracer | null {\n if (cachedTracer) return cachedTracer;\n try {\n const provider = new NodeTracerProvider({\n resource: new Resource({ [ATTR_SERVICE_NAME]: SERVICE_NAME }),\n // Honor the upstream (MRT) sampling decision rather than implementing our\n // own. ParentBasedSampler: when an inbound traceparent is present, defer\n // to its sampled flag (continue + export iff sampled); when there is no\n // inbound parent, sample the fresh root ourselves (AlwaysOnSampler). This\n // is exactly the standard \"downstream participant\" behavior — MRT owns the\n // rate via MRT_DISTRIBUTED_TRACING_* env vars.\n sampler: new ParentBasedSampler({ root: new AlwaysOnSampler() }),\n });\n\n provider.addSpanProcessor(new SimpleSpanProcessor(new MrtConsoleSpanExporter()));\n // Register with `propagator: null` so the provider sets up the global tracer\n // provider and the AsyncLocalStorage context manager, but does NOT install a\n // global propagator. (`register()` otherwise defaults to a W3C propagator built\n // from OTEL_PROPAGATORS.) We deliberately own propagation ourselves:\n // - outbound: the undici requestHook below injects via our `outboundPropagator`.\n // - inbound: the Express middleware parses `traceparent` by hand\n // (parseTraceParent + trace.setSpanContext).\n // Both bypass the global registry, which is unreliable on MRT (see the\n // `outboundPropagator` doc comment). Leaving the global propagator as the\n // default no-op also means undici's own propagation.inject() — which runs right\n // after our hook — adds nothing, so `traceparent` is written exactly once.\n provider.register({ propagator: null });\n\n // Guard against double-registration across Vite module boundaries.\n // See comment above UNDICI_REGISTERED_KEY.\n if (!(globalThis as Record<symbol, boolean>)[UNDICI_REGISTERED_KEY]) {\n (globalThis as Record<symbol, boolean>)[UNDICI_REGISTERED_KEY] = true;\n registerInstrumentations({\n tracerProvider: provider,\n instrumentations: [\n new UndiciInstrumentation({\n requestHook(span, request) {\n try {\n // Static, low-cardinality client span name shared by every\n // outbound fetch (SCAPI, SLAS, …), matching the dotted\n // `sfnext.*` operation namespace used by our other spans.\n // The HTTP detail (method, full url, host, status) is already\n // set on the span as attributes by the instrumentation —\n // `http.request.method`, `url.full`, `url.path`, `url.query`,\n // `server.address`, `http.response.status_code` — so traces\n // aggregate by operation and the high-cardinality values\n // (paths/queries that can carry ids, tokens, or PII) stay out\n // of the span name.\n span.updateName('sfnext.fetch');\n } catch {\n // Non-fatal — default span name is acceptable\n }\n\n // Inject `traceparent` ourselves, via our private propagator, so the\n // header reaches SCAPI even when undici's own global-propagator\n // inject() is a no-op on MRT (see `outboundPropagator` above). The\n // hook runs just before undici's inject(), and because we never\n // register a global propagator that inject() adds nothing — so the\n // header is written exactly once. We build the context from THIS\n // CLIENT span (a pure realm-global-symbol write, no registry lookup),\n // matching what undici would have injected.\n try {\n const ctx = trace.setSpan(context.active(), span);\n outboundPropagator.inject(ctx, request, {\n set(req: UndiciRequest, key: string, value: string) {\n req.addHeader?.(key, value);\n },\n });\n } catch {\n // Non-fatal — propagation must never break the request\n }\n },\n }),\n ],\n });\n }\n\n cachedTracer = provider.getTracer(SERVICE_NAME);\n return cachedTracer;\n } catch (error) {\n logger.error('[otel] Failed to initialize OpenTelemetry:', error);\n return null;\n }\n}\n","/**\n * Copyright 2026 Salesforce, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Platform-level handler instrumentation.\n *\n * Uses React Router's ServerInstrumentation API to observe the\n * request lifecycle at the handler and route levels. This runs around ALL\n * requests (document + data) and provides OpenTelemetry spans for:\n * - `sfnext.ssr` — SSR render span per incoming HTTP request (handler level)\n * - `sfnext.loader` — child span per route loader\n * - `sfnext.action` — child span per route action\n * - `sfnext.middleware` — child span per route middleware\n *\n * Span names are low-cardinality operation names; the route is carried in the\n * `rr.route.id` and `http.route` attributes rather than the name, so traces\n * aggregate by operation and filter by route.\n *\n * `tracer` is `null` when OTel is disabled. Each handler checks explicitly\n * and calls through immediately, so the disabled path is always obvious.\n *\n * @env SFNEXT_OTEL_ENABLED — set to `\"true\"` to enable OpenTelemetry tracing.\n * Spans are written synchronously to stdout via ConsoleSpanExporter, which is\n * compatible with both local development and the Managed Runtime serverless\n * environment (see otel/setup.ts for the full design rationale).\n * Example: `SFNEXT_OTEL_ENABLED=true pnpm dev`\n *\n * @see https://reactrouter.com/how-to/instrumentation\n */\n\nimport { type Tracer, type Attributes, SpanStatusCode } from '@opentelemetry/api';\nimport type { ServerInstrumentation } from 'react-router';\nimport { ATTR_HTTP_REQUEST_METHOD, ATTR_HTTP_ROUTE, ATTR_URL_PATH } from '@opentelemetry/semantic-conventions';\nimport { 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 * InstrumentationHandlerResult.\n */\nfunction httpAttributes(request: { method: string; url: string }): Attributes {\n const attrs: Attributes = { [ATTR_HTTP_REQUEST_METHOD]: request.method };\n try {\n attrs[ATTR_URL_PATH] = new URL(request.url).pathname;\n } catch {\n // Malformed URL — skip url.path rather than throwing\n }\n return attrs;\n}\n\nexport const platformInstrumentation: ServerInstrumentation = {\n handler(handler) {\n handler.instrument({\n async request(handleRequest, { request }) {\n await traced('sfnext.ssr', httpAttributes(request), handleRequest);\n },\n });\n },\n route(route) {\n // The request method/url.path live on the parent server span. At the route\n // level the meaningful identifiers are the framework route id (`rr.route.id`)\n // and the URL route template (`http.route`, the standard semantic-convention\n // key — a bounded, low-cardinality value, never the raw path with ids).\n function routeAttributes(pattern: string): Attributes {\n return {\n 'rr.route.id': route.id,\n [ATTR_HTTP_ROUTE]: pattern,\n };\n }\n\n route.instrument({\n async loader(handleLoader, { pattern }) {\n await traced('sfnext.loader', routeAttributes(pattern), handleLoader);\n },\n async action(handleAction, { pattern }) {\n await traced('sfnext.action', routeAttributes(pattern), handleAction);\n },\n async middleware(handleMiddleware, { pattern }) {\n await traced('sfnext.middleware', routeAttributes(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 instrumentations\n */\nexport function composeServerEntry(appModule: ServerEntryModule): ServerEntryModule {\n // React Router 7.18 reads the server-entry `instrumentations` export by name,\n // so an ejected entry still exporting the pre-stabilization `unstable_instrumentations`\n // compiles fine but is never registered. Surface that in dev so it doesn't ship silently.\n if (\n process.env.NODE_ENV !== 'production' &&\n 'unstable_instrumentations' in appModule &&\n !('instrumentations' in appModule)\n ) {\n // eslint-disable-next-line no-console\n console.warn(\n '[storefront-next] entry.server exports `unstable_instrumentations`, which React Router 7.18 ' +\n 'no longer reads. Rename the export to `instrumentations` or it will not register.'\n );\n }\n return {\n // Spread all properties first so unknown/future exports pass through\n ...appModule,\n // Override the exports the platform layer enhances\n default(request, statusCode, headers, context, loadContext) {\n return appModule.default(request, statusCode, headers, context, loadContext);\n },\n instrumentations: [platformInstrumentation, ...(appModule.instrumentations ?? [])],\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"server.js","names":["LEVEL_PRIORITY: Record<LogLevel, number>","overrideLevel: LogLevel | undefined","cachedTracer: Tracer | null","tracer: Tracer | null","context","attrs: Attributes","platformInstrumentation: ServerInstrumentation","context"],"sources":["../../src/otel/mrt-console-span-exporter.ts","../../src/utils/logger.ts","../../src/otel/setup.ts","../../src/otel/react-router/instrumentation.ts","../../src/entry/server.ts"],"sourcesContent":["/**\n * Copyright 2026 Salesforce, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * MRT-compatible console span exporter.\n *\n * Extends the default ConsoleSpanExporter to output structured JSON that\n * Managed Runtime's log infrastructure can parse. The default exporter uses\n * `console.dir` with a human-readable format; this override uses\n * `console.info(JSON.stringify(...))` to produce machine-parseable JSON\n * matching the format MRT expects.\n *\n * Inherits `shutdown()` and `forceFlush()` from ConsoleSpanExporter.\n */\n\nimport { ConsoleSpanExporter, type ReadableSpan } from '@opentelemetry/sdk-trace-base';\nimport { ExportResultCode, type ExportResult, hrTimeToTimeStamp } from '@opentelemetry/core';\n\nexport class MrtConsoleSpanExporter extends ConsoleSpanExporter {\n export(spans: ReadableSpan[], resultCallback: (result: ExportResult) => void): void {\n for (const span of spans) {\n try {\n const ctx = span.spanContext();\n const spanData = {\n traceId: ctx.traceId,\n parentId: span.parentSpanId,\n name: span.name,\n id: ctx.spanId,\n kind: span.kind,\n timestamp: hrTimeToTimeStamp(span.startTime),\n duration: span.duration,\n attributes: span.attributes,\n status: span.status,\n events: span.events,\n links: span.links,\n start_time: span.startTime,\n end_time: span.endTime,\n forwardTrace: process.env.SFNEXT_OTEL_ENABLED === 'true',\n };\n // oxlint-disable-next-line no-console -- intentional: MRT collects stdout as the telemetry transport\n console.info(JSON.stringify(spanData));\n } catch {\n // Skip malformed spans — never let a serialization failure propagate\n }\n }\n resultCallback({ code: ExportResultCode.SUCCESS });\n }\n}\n","/**\n * Copyright 2026 Salesforce, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n/* oxlint-disable no-console */\nimport os from 'os';\nimport chalk from 'chalk';\nimport { createRequire } from 'module';\nimport type { ServerMode } from '../server/modes';\nimport pkg from '../../package.json' with { type: 'json' };\n\n/**\n * Get the local network IPv4 address\n */\nexport function getNetworkAddress(): string | undefined {\n const interfaces = os.networkInterfaces();\n for (const name of Object.keys(interfaces)) {\n const iface = interfaces[name];\n if (!iface) continue;\n for (const alias of iface) {\n if (alias.family === 'IPv4' && !alias.internal) {\n return alias.address;\n }\n }\n }\n return undefined;\n}\n\n/**\n * Get the version of a package from the project's package.json\n */\nexport function getPackageVersion(packageName: string, projectDir: string): string {\n try {\n const require = createRequire(import.meta.url);\n const pkgPath = require.resolve(`${packageName}/package.json`, { paths: [projectDir] });\n const pkgJson = require(pkgPath) as { version: string };\n return pkgJson.version;\n } catch {\n return 'unknown';\n }\n}\n\n/**\n * Centralized, level-gated logger for the SDK.\n *\n * Log level is controlled by `SFCC_LOG_LEVEL` env var (`error` | `warn` | `info` | `debug`).\n * Falls back to: `DEBUG` targeting sfnext -> `debug`, `NODE_ENV=production` -> `warn`, otherwise `info`.\n */\n\nexport type LogLevel = 'error' | 'warn' | 'info' | 'debug';\n\nconst LEVEL_PRIORITY: Record<LogLevel, number> = {\n error: 0,\n warn: 1,\n info: 2,\n debug: 3,\n};\n\nlet overrideLevel: LogLevel | undefined;\n\n/**\n * Returns true when the `DEBUG` env var targets sfnext or is a general enable flag.\n * Avoids accidentally enabling debug mode when DEBUG is set for unrelated libraries\n * (e.g. `DEBUG=express:*`).\n */\nfunction debugEnablesSfnext(): boolean {\n const raw = process.env.DEBUG?.trim();\n if (!raw) return false;\n const normalized = raw.toLowerCase();\n if (['1', 'true', 'yes', 'on'].includes(normalized)) return true;\n return raw.split(',').some((token) => {\n const value = token.trim();\n return value === '*' || value === 'sfnext' || value === 'sfnext:*';\n });\n}\n\nfunction resolveLevel(): LogLevel {\n if (overrideLevel) return overrideLevel;\n const envLevel = process.env.MRT_LOG_LEVEL ?? process.env.SFCC_LOG_LEVEL;\n if (envLevel && envLevel in LEVEL_PRIORITY) return envLevel as LogLevel;\n if (debugEnablesSfnext()) return 'debug';\n if (process.env.NODE_ENV === 'production') return 'warn';\n return 'info';\n}\n\nfunction shouldLog(level: LogLevel): boolean {\n return LEVEL_PRIORITY[level] <= LEVEL_PRIORITY[resolveLevel()];\n}\n\nexport const logger = {\n error(msg: string, ...args: unknown[]): void {\n if (!shouldLog('error')) return;\n console.error(chalk.red('[sfnext:error]'), msg, ...args);\n },\n warn(msg: string, ...args: unknown[]): void {\n if (!shouldLog('warn')) return;\n console.warn(chalk.yellow('[sfnext:warn]'), msg, ...args);\n },\n info(msg: string, ...args: unknown[]): void {\n if (!shouldLog('info')) return;\n console.log(chalk.cyan('[sfnext:info]'), msg, ...args);\n },\n debug(msg: string, ...args: unknown[]): void {\n if (!shouldLog('debug')) return;\n console.log(chalk.gray('[sfnext:debug]'), msg, ...args);\n },\n setLevel(level: LogLevel | undefined): void {\n overrideLevel = level;\n },\n getLevel(): LogLevel {\n return resolveLevel();\n },\n};\n\n/**\n * Print the server information banner with URLs and versions\n */\nexport function printServerInfo(mode: ServerMode, port: number, startTime: number, projectDir: string): void {\n const elapsed = Date.now() - startTime;\n const sfnextVersion = pkg.version;\n const reactVersion = getPackageVersion('react', projectDir);\n const reactRouterVersion = getPackageVersion('react-router', projectDir);\n const viteVersion = getPackageVersion('vite', projectDir);\n\n const modeLabel = mode === 'development' ? 'Development Mode' : 'Preview Mode';\n\n console.log();\n console.log(` ${chalk.cyan.bold('⚡ SFCC Storefront Next')} ${chalk.dim(`v${sfnextVersion}`)}`);\n console.log(` ${chalk.green.bold(modeLabel)}`);\n console.log();\n const logLevel = resolveLevel();\n const logLevelColors: Record<LogLevel, (s: string) => string> = {\n error: chalk.red,\n warn: chalk.yellow,\n info: chalk.cyan,\n debug: chalk.gray,\n };\n\n console.log(\n ` ${chalk.dim('react')} ${chalk.green(`v${reactVersion}`)} ${chalk.dim('|')} ` +\n `${chalk.dim('react-router')} ${chalk.green(`v${reactRouterVersion}`)} ${chalk.dim('|')} ` +\n `${chalk.dim('vite')} ${chalk.green(`v${viteVersion}`)}`\n );\n console.log(\n ` ${chalk.dim('log level')} ${logLevelColors[logLevel](logLevel)} ${chalk.dim('|')} ` +\n `${chalk.green(`ready in ${elapsed}ms`)}`\n );\n console.log();\n}\n\n/**\n * Print server configuration details (proxy, static, etc.)\n */\nexport function printServerConfig(config: {\n mode: ServerMode;\n port: number;\n enableProxy?: boolean;\n enableStaticServing?: boolean;\n enableCompression?: boolean;\n proxyPath?: string;\n proxyHost?: string;\n shortCode?: string;\n organizationId?: string;\n clientId?: string;\n}): void {\n const {\n port,\n enableProxy,\n enableStaticServing,\n enableCompression,\n proxyPath,\n proxyHost,\n shortCode,\n organizationId,\n clientId,\n } = config;\n\n console.log(` ${chalk.bold('Environment Configuration:')}`);\n\n if (enableProxy && proxyPath && proxyHost && shortCode) {\n console.log(\n ` ${chalk.green('✓')} ${chalk.bold('Proxy:')} ${chalk.cyan(`localhost:${port}${proxyPath}`)} ${chalk.dim('→')} ${chalk.cyan(proxyHost)}`\n );\n console.log(` ${chalk.dim('Short Code: ')}${chalk.dim(shortCode)}`);\n if (organizationId) {\n console.log(` ${chalk.dim('Organization ID: ')}${chalk.dim(organizationId)}`);\n }\n if (clientId) {\n console.log(` ${chalk.dim('Client ID: ')}${chalk.dim(clientId)}`);\n }\n } else {\n console.log(` ${chalk.bold('Proxy: ')} ${chalk.dim('disabled')}`);\n }\n\n if (enableStaticServing) {\n console.log(` ${chalk.bold('Static: ')} ${chalk.dim('enabled')}`);\n }\n\n if (enableCompression) {\n console.log(` ${chalk.bold('Compression: ')} ${chalk.dim('enabled')}`);\n }\n\n // URLs\n const localUrl = `http://localhost:${port}`;\n const showNetwork = process.env.SHOW_NETWORK === 'true';\n const networkAddress = showNetwork ? getNetworkAddress() : undefined;\n const networkUrl = networkAddress ? `http://${networkAddress}:${port}` : undefined;\n\n console.log();\n console.log(` ${chalk.bold('Local: ')} ${chalk.cyan(localUrl)}`);\n if (networkUrl) {\n console.log(` ${chalk.bold('Network:')} ${chalk.cyan(networkUrl)}`);\n }\n\n console.log();\n console.log(` ${chalk.dim('Press')} ${chalk.bold('Ctrl+C')} ${chalk.dim('to stop the server')}`);\n console.log();\n}\n\n/**\n * Print shutdown message\n */\nexport function printShutdownMessage(): void {\n console.log(`\\n ${chalk.yellow('⚡')} ${chalk.dim('Server shutting down...')}\\n`);\n}\n","/**\n * Copyright 2026 Salesforce, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * OpenTelemetry tracer provider initialization.\n *\n * ## Exporter design: why ConsoleSpanExporter?\n *\n * This storefront runs in two environments:\n * - Local development on a developer's machine (Vite dev server)\n * - Salesforce Managed Runtime (MRT), a serverless environment backed by AWS Lambda\n *\n * Standard OpenTelemetry exporters (OTLP, Jaeger, Zipkin) are designed for\n * long-running processes: they maintain background agents, batch spans in memory,\n * and flush asynchronously. That model does not work in a serverless environment.\n * Lambda invocations must complete quickly — a background agent or async flush\n * after the response is sent would hold the Lambda open unnecessarily, increasing\n * cost and cold-start latency.\n *\n * Instead, we write spans synchronously to stdout via MrtConsoleSpanExporter — a\n * subclass of ConsoleSpanExporter that outputs structured JSON matching the format\n * MRT's log infrastructure expects. MRT collects stdout from every invocation and\n * forwards the log stream to downstream analytics systems. Telemetry reaches those\n * systems without any in-process agent or network connection from the Lambda\n * function itself.\n *\n * SimpleSpanProcessor is used (instead of BatchSpanProcessor) for the same reason:\n * immediate, synchronous export with no in-memory queue that could be lost if the\n * process exits.\n *\n * Uses NodeTracerProvider directly (instead of NodeSDK) to avoid async resource\n * detection. NodeSDK's autoDetectResources creates a Resource with\n * asyncAttributesPending=true, which causes SimpleSpanProcessor.onEnd() to\n * defer export via waitForAsyncAttributes(). In the Vite SSR module runner\n * context, that deferred microtask never executes. A synchronous Resource\n * ensures the immediate export path is always taken.\n *\n * ## Distributed tracing: W3C Trace Context propagation\n *\n * The storefront runs below Managed Runtime (MRT) in the call tree. MRT is\n * trace-aware: it makes the sampling decision and stamps an inbound W3C\n * `traceparent` header on the request the storefront receives. To appear on the\n * same end-to-end trace as the surrounding services, the storefront behaves as a\n * standard downstream participant:\n * - It continues the inbound trace (see the Express middleware, which parses the\n * inbound `traceparent` into the parent context).\n * - It forwards `traceparent` on outbound fetches (the UndiciInstrumentation\n * `requestHook` below injects it via a privately-held W3C propagator — see the\n * `outboundPropagator` doc comment for why the global registry is bypassed).\n * - It honors MRT's sampling decision via a ParentBasedSampler — it never forces\n * a sampled trace and implements no sampling policy of its own. MRT owns the\n * rate via MRT_DISTRIBUTED_TRACING_ENABLED / MRT_DISTRIBUTED_TRACING_SAMPLING_RATE.\n *\n * @env SFNEXT_OTEL_ENABLED — set to `\"true\"` to enable (e.g. `SFNEXT_OTEL_ENABLED=true pnpm dev`)\n */\n\nimport { context, trace, type Tracer } from '@opentelemetry/api';\nimport { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';\nimport { AlwaysOnSampler, ParentBasedSampler, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base';\nimport { W3CTraceContextPropagator } from '@opentelemetry/core';\nimport { MrtConsoleSpanExporter } from './mrt-console-span-exporter';\nimport { Resource } from '@opentelemetry/resources';\nimport { ATTR_SERVICE_NAME } from '@opentelemetry/semantic-conventions';\nimport { registerInstrumentations } from '@opentelemetry/instrumentation';\nimport { UndiciInstrumentation, type UndiciRequest } from '@opentelemetry/instrumentation-undici';\nimport { logger } from '../logger';\n\n/**\n * A privately-held W3C Trace Context propagator, used to inject `traceparent` onto\n * outbound fetches WITHOUT going through OpenTelemetry's global propagator registry.\n *\n * ## Why not the global propagator?\n *\n * `UndiciInstrumentation` injects outbound headers by calling the *global* registered\n * propagator (`propagation.inject()`). Registering one with `setGlobalPropagator()`\n * works locally, but is a silent no-op on Managed Runtime (MRT):\n *\n * - The MRT Lambda wrapper bundles its own copy of `@opentelemetry/api` (declared\n * `^1.9.1`, with core/sdk at 2.x) and boots first, creating the version-keyed\n * global registry object (`globalThis[Symbol.for('opentelemetry.js.api.1')]`,\n * stamped `version: \"1.9.1\"`).\n * - Our SDK bundle ships a *different* copy of `@opentelemetry/api` (`1.9.0`). When\n * our code calls `setGlobalPropagator()`, OTel's `registerGlobal()` compares the\n * registry's recorded version against ours with an EXACT string match — `\"1.9.1\"\n * !== \"1.9.0\"` — refuses the registration, and reports the error only to OTel's\n * internal diag channel (which we do not wire up). The propagation slot stays\n * empty, so undici's `propagation.inject()` injects nothing and no `traceparent`\n * reaches SCAPI. Proven on soak: `propagation.fields() === []`, 0/198 outbound\n * requests carried a header.\n *\n * This is the same multi-instance split that makes inbound `propagation.extract()`\n * fail (see `express/middleware.ts`, which bypasses it with `parseTraceParent` +\n * `trace.setSpanContext`). MRT's own data-plane tracer solves it the same way: it\n * holds a private `new W3CTraceContextPropagator()` and calls `inject()`/`extract()`\n * on it directly, never touching the global registry.\n *\n * Holding our own instance and injecting it ourselves (in the undici `requestHook`)\n * makes outbound propagation deterministic and independent of MRT's bundle, its OTel\n * version, and its boot order. We deliberately do NOT call `setGlobalPropagator()` —\n * leaving the global propagator as the default no-op guarantees undici's own\n * `propagation.inject()` (which runs right after our hook) adds nothing, so the\n * `traceparent` is written exactly once in every environment.\n */\nconst outboundPropagator = new W3CTraceContextPropagator();\n\nexport const SERVICE_NAME = 'storefront-next';\n\n/**\n * Initializes OpenTelemetry and returns a Tracer from the provider directly.\n *\n * Returns the tracer via `provider.getTracer()` instead of the global\n * `trace.getTracer()` API. In the Vite SSR module runner, the built\n * dist/entry/server.js and the externalized @opentelemetry/sdk-trace-node\n * resolve @opentelemetry/api to different module instances (different paths\n * through pnpm's strict node_modules). Each instance has its own\n * ProxyTracerProvider singleton, so `provider.register()` sets the delegate\n * on sdk-trace-node's API instance while our code's `trace.getTracer()`\n * reads from a separate API instance with no delegate — returning a tracer\n * backed by a bare BasicTracerProvider with NoopSpanProcessor.\n *\n * Getting the tracer directly from the provider bypasses the global registry\n * entirely, guaranteeing the tracer uses our configured span processors.\n */\nlet cachedTracer: Tracer | null = null;\n\n// In the Vite SSR module runner, setup.ts may be loaded by two different module\n// instances (the Express server and the SSR bundle) — each with its own\n// `cachedTracer`. UndiciInstrumentation hooks into process-global\n// diagnostics_channel events, so registering it twice causes duplicate spans\n// for every fetch. This process-global flag ensures it is registered only once.\nconst UNDICI_REGISTERED_KEY = Symbol.for('sfnext.otel.undici_registered');\n\nexport function initTelemetry(): Tracer | null {\n if (cachedTracer) return cachedTracer;\n try {\n const provider = new NodeTracerProvider({\n resource: new Resource({ [ATTR_SERVICE_NAME]: SERVICE_NAME }),\n // Honor the upstream (MRT) sampling decision rather than implementing our\n // own. ParentBasedSampler: when an inbound traceparent is present, defer\n // to its sampled flag (continue + export iff sampled); when there is no\n // inbound parent, sample the fresh root ourselves (AlwaysOnSampler). This\n // is exactly the standard \"downstream participant\" behavior — MRT owns the\n // rate via MRT_DISTRIBUTED_TRACING_* env vars.\n sampler: new ParentBasedSampler({ root: new AlwaysOnSampler() }),\n });\n\n provider.addSpanProcessor(new SimpleSpanProcessor(new MrtConsoleSpanExporter()));\n // Register with `propagator: null` so the provider sets up the global tracer\n // provider and the AsyncLocalStorage context manager, but does NOT install a\n // global propagator. (`register()` otherwise defaults to a W3C propagator built\n // from OTEL_PROPAGATORS.) We deliberately own propagation ourselves:\n // - outbound: the undici requestHook below injects via our `outboundPropagator`.\n // - inbound: the Express middleware parses `traceparent` by hand\n // (parseTraceParent + trace.setSpanContext).\n // Both bypass the global registry, which is unreliable on MRT (see the\n // `outboundPropagator` doc comment). Leaving the global propagator as the\n // default no-op also means undici's own propagation.inject() — which runs right\n // after our hook — adds nothing, so `traceparent` is written exactly once.\n provider.register({ propagator: null });\n\n // Guard against double-registration across Vite module boundaries.\n // See comment above UNDICI_REGISTERED_KEY.\n if (!(globalThis as Record<symbol, boolean>)[UNDICI_REGISTERED_KEY]) {\n (globalThis as Record<symbol, boolean>)[UNDICI_REGISTERED_KEY] = true;\n registerInstrumentations({\n tracerProvider: provider,\n instrumentations: [\n new UndiciInstrumentation({\n requestHook(span, request) {\n try {\n // Static, low-cardinality client span name shared by every\n // outbound fetch (SCAPI, SLAS, …), matching the dotted\n // `sfnext.*` operation namespace used by our other spans.\n // The HTTP detail (method, full url, host, status) is already\n // set on the span as attributes by the instrumentation —\n // `http.request.method`, `url.full`, `url.path`, `url.query`,\n // `server.address`, `http.response.status_code` — so traces\n // aggregate by operation and the high-cardinality values\n // (paths/queries that can carry ids, tokens, or PII) stay out\n // of the span name.\n span.updateName('sfnext.fetch');\n } catch {\n // Non-fatal — default span name is acceptable\n }\n\n // Inject `traceparent` ourselves, via our private propagator, so the\n // header reaches SCAPI even when undici's own global-propagator\n // inject() is a no-op on MRT (see `outboundPropagator` above). The\n // hook runs just before undici's inject(), and because we never\n // register a global propagator that inject() adds nothing — so the\n // header is written exactly once. We build the context from THIS\n // CLIENT span (a pure realm-global-symbol write, no registry lookup),\n // matching what undici would have injected.\n try {\n const ctx = trace.setSpan(context.active(), span);\n outboundPropagator.inject(ctx, request, {\n set(req: UndiciRequest, key: string, value: string) {\n req.addHeader?.(key, value);\n },\n });\n } catch {\n // Non-fatal — propagation must never break the request\n }\n },\n }),\n ],\n });\n }\n\n cachedTracer = provider.getTracer(SERVICE_NAME);\n return cachedTracer;\n } catch (error) {\n logger.error('[otel] Failed to initialize OpenTelemetry:', error);\n return null;\n }\n}\n","/**\n * Copyright 2026 Salesforce, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Platform-level handler instrumentation.\n *\n * Uses React Router's ServerInstrumentation API to observe the\n * request lifecycle at the handler and route levels. This runs around ALL\n * requests (document + data) and provides OpenTelemetry spans for:\n * - `sfnext.ssr` — SSR render span per incoming HTTP request (handler level)\n * - `sfnext.loader` — child span per route loader\n * - `sfnext.action` — child span per route action\n * - `sfnext.middleware` — child span per route middleware\n *\n * Span names are low-cardinality operation names; the route is carried in the\n * `http.route` attribute rather than the name, so traces aggregate by operation\n * and filter by route.\n *\n * `tracer` is `null` when OTel is disabled. Each handler checks explicitly\n * and calls through immediately, so the disabled path is always obvious.\n *\n * @env SFNEXT_OTEL_ENABLED — set to `\"true\"` to enable OpenTelemetry tracing.\n * Spans are written synchronously to stdout via ConsoleSpanExporter, which is\n * compatible with both local development and the Managed Runtime serverless\n * environment (see otel/setup.ts for the full design rationale).\n * Example: `SFNEXT_OTEL_ENABLED=true pnpm dev`\n *\n * @see https://reactrouter.com/how-to/instrumentation\n */\n\nimport { type Tracer, type Attributes, SpanStatusCode } from '@opentelemetry/api';\nimport type { ServerInstrumentation } from 'react-router';\nimport { ATTR_HTTP_REQUEST_METHOD, ATTR_HTTP_ROUTE, ATTR_URL_PATH } from '@opentelemetry/semantic-conventions';\nimport { siteContext } from '@salesforce/storefront-next-runtime/site-context';\nimport { initTelemetry } from '../setup';\n\nconst tracer: Tracer | null = process.env.SFNEXT_OTEL_ENABLED === 'true' ? initTelemetry() : null;\n\n/**\n * The read-only router context passed to each instrumentation callback. React\n * Router types it as `ReadonlyContext`, which is a `.get`-bearing provider only\n * when the app has enabled middleware (`MiddlewareEnabled`); the SDK's own\n * tsconfig doesn't resolve that flag, so we accept an optionally-`get`-bearing\n * value and narrow structurally at runtime.\n */\ntype MaybeReadonlyContext = { get?: (context: typeof siteContext) => unknown } | undefined;\n\n/**\n * Reads the resolved site from the router context, or `undefined` if the site\n * has not been resolved yet (or the context is unavailable).\n *\n * `siteContext` is set by the site-context middleware (which runs early in the\n * middleware chain). We resolve it from the same `@salesforce/storefront-next-runtime`\n * module the template's middleware writes to, so both share the one `createContext`\n * singleton and this read observes the value that middleware set. Wrapped in a\n * try/catch because OTel instrumentation must never break the request pipeline.\n */\nfunction readSiteId(context: MaybeReadonlyContext): string | undefined {\n try {\n if (typeof context?.get !== 'function') return undefined;\n const resolved = context.get(siteContext) as { site?: { id?: string } } | null | undefined;\n return resolved?.site?.id;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Runs `handle` inside an active OTel span, recording errors and ending the span.\n * When `tracer` is null (OTel disabled), calls `handle` directly with no overhead.\n *\n * The resolved site id is read from `context` and stamped just before the span\n * ends. It is read at span end (not span start) because the site-context\n * middleware runs as part of the wrapped work — at span start the site may not be\n * resolved yet; by span end it is. The site id is a bounded, low-cardinality value.\n *\n * It is emitted under the key `site_name` (not `sfnext.site_id`): the MRT log→trace\n * bridge filters span attributes against an allow list and only keeps matching\n * keys, and `site_name` is on that list (confirmed with the MRT team) while\n * `site_id`/`sfnext.*` keys are not. The site config carries no separate display\n * name, so the site id is the value we have. See ../README.md (\"MRT log→trace bridge\").\n *\n * Route-level spans (`sfnext.loader`/`.action`/`.middleware`) always receive the\n * router `context`, so they carry `site_name`. The handler-level `sfnext.ssr` span\n * does NOT: React Router passes `context: undefined` to the request-handler\n * instrumentation (confirmed at runtime), so `readSiteId` returns `undefined`\n * there and the attribute is simply omitted. No branching needed — the\n * `if (siteId)` guard handles both cases uniformly.\n */\nasync function traced(\n spanName: string,\n attributes: Attributes,\n context: MaybeReadonlyContext,\n handle: () => Promise<{ status: string; error?: Error }>\n): Promise<void> {\n if (!tracer) {\n await handle();\n return;\n }\n let handled = false;\n try {\n await tracer.startActiveSpan(spanName, { attributes }, async (span) => {\n try {\n handled = true;\n const result = await handle();\n if (result.status === 'error' && result.error) {\n span.setStatus({ code: SpanStatusCode.ERROR, message: result.error.message });\n span.recordException(result.error);\n }\n } finally {\n const siteId = readSiteId(context);\n if (siteId) span.setAttribute('site_name', siteId);\n span.end();\n }\n });\n } catch {\n // OTel failure must never break the request pipeline.\n // If handle() was already called inside the span callback, don't call it again.\n if (!handled) await handle();\n }\n}\n\n/**\n * HTTP attributes common to all spans.\n * url.path only — url.full would expose query params which may contain auth\n * tokens or PII. http.response.status_code is not available from\n * InstrumentationHandlerResult.\n */\nfunction httpAttributes(request: { method: string; url: string }): Attributes {\n const attrs: Attributes = { [ATTR_HTTP_REQUEST_METHOD]: request.method };\n try {\n attrs[ATTR_URL_PATH] = new URL(request.url).pathname;\n } catch {\n // Malformed URL — skip url.path rather than throwing\n }\n return attrs;\n}\n\nexport const platformInstrumentation: ServerInstrumentation = {\n handler(handler) {\n handler.instrument({\n async request(handleRequest, { request, context }) {\n await traced('sfnext.ssr', httpAttributes(request), context, handleRequest);\n },\n });\n },\n route(route) {\n // The request method/url.path live on the parent server span. At the route\n // level the meaningful identifier is the URL route template (`http.route`,\n // the standard semantic-convention key — a bounded, low-cardinality value,\n // never the raw path with ids). The MRT log→trace bridge filters span\n // attributes against an allow list and drops keys not on it, so we only emit\n // attributes known to survive (`http.route` is a semantic-convention key the\n // bridge keeps; the framework's `rr.route.id` is not on the list and was\n // silently dropped, so it is no longer recorded).\n function routeAttributes(pattern: string): Attributes {\n return {\n [ATTR_HTTP_ROUTE]: pattern,\n };\n }\n\n route.instrument({\n async loader(handleLoader, { pattern, context }) {\n await traced('sfnext.loader', routeAttributes(pattern), context, handleLoader);\n },\n async action(handleAction, { pattern, context }) {\n await traced('sfnext.action', routeAttributes(pattern), context, handleAction);\n },\n async middleware(handleMiddleware, { pattern, context }) {\n await traced('sfnext.middleware', routeAttributes(pattern), context, handleMiddleware);\n },\n });\n },\n};\n","/**\n * Copyright 2026 Salesforce, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Server entry composition function.\n *\n * Wraps the full ServerEntryModule (the customer's or SDK default entry.server)\n * with platform-level features. The Vite plugin (platformEntryPlugin) generates\n * a virtual module that imports this function and calls it with the app's entry\n * module. New platform features ship via `npm update` without changes to the\n * plugin or customer code.\n *\n * Current platform behaviors:\n * - OpenTelemetry instrumentation for request lifecycle tracing\n *\n * Future additions:\n * - loadContext enrichment (correlation IDs, platform metadata)\n * - Response header injection (tracing, cache directives)\n * - Default error handling with platform error reporting\n */\n\nimport type { ServerEntryModule } from 'react-router';\nimport { platformInstrumentation } from '../otel/react-router/instrumentation';\n\n/**\n * Composes a server entry module with platform-level features.\n *\n * - Spreads all app module properties to forward unknown/future exports\n * - Wraps the default handler for platform-level processing\n * - Prepends a platform instrumentation to instrumentations\n */\nexport function composeServerEntry(appModule: ServerEntryModule): ServerEntryModule {\n // React Router 7.18 reads the server-entry `instrumentations` export by name,\n // so an ejected entry still exporting the pre-stabilization `unstable_instrumentations`\n // compiles fine but is never registered. Surface that in dev so it doesn't ship silently.\n if (\n process.env.NODE_ENV !== 'production' &&\n 'unstable_instrumentations' in appModule &&\n !('instrumentations' in appModule)\n ) {\n // oxlint-disable-next-line no-console\n console.warn(\n '[storefront-next] entry.server exports `unstable_instrumentations`, which React Router 7.18 ' +\n 'no longer reads. Rename the export to `instrumentations` or it will not register.'\n );\n }\n return {\n // Spread all properties first so unknown/future exports pass through\n ...appModule,\n // Override the exports the platform layer enhances\n default(request, statusCode, headers, context, loadContext) {\n return appModule.default(request, statusCode, headers, context, loadContext);\n },\n instrumentations: [platformInstrumentation, ...(appModule.instrumentations ?? [])],\n };\n}\n"],"mappings":"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"}
@@ -0,0 +1,108 @@
1
+ import { t as logger } from "./logger.js";
2
+ import { spawnSync } from "child_process";
3
+ import { basename, dirname, join } from "path";
4
+ import { createRequire } from "module";
5
+ import { fileURLToPath } from "url";
6
+
7
+ //#region src/utils/format-with-project-biome.ts
8
+ /** This module's own path — the resolution root for the SDK-bundled Biome fallback. */
9
+ const HERE = fileURLToPath(import.meta.url);
10
+ /**
11
+ * Format generated file content with the *consuming project's* Biome so the written file
12
+ * matches what the project's own `biome format` / `pnpm lint:fix` would produce.
13
+ *
14
+ * The SDK generates two kinds of files into a customer project — the extension `config.json`
15
+ * (`trim-extensions.ts`) and the static component registry (`staticRegistry.ts`). Both must
16
+ * be emitted in the exact byte shape the project's formatter produces, or the generated file
17
+ * fails `pnpm lint` on a fresh project / churns on every `biome format --write` (W-23074938).
18
+ *
19
+ * Biome is preferred from the target file's own location — i.e. the consuming project's
20
+ * `node_modules` — so it's the project's Biome version whose output is matched. When that
21
+ * isn't installed yet (e.g. `create-storefront` runs `trim-extensions` BEFORE the generated
22
+ * project's first `pnpm install`), we fall back to the SDK-bundled `@biomejs/biome` (a hard
23
+ * dependency of this package, pinned to the template's version). Either way the file's
24
+ * committed `biome.json` governs the output: Biome discovers its config by walking up from
25
+ * the working directory, so we spawn it with `cwd` set to the file's directory and pass the
26
+ * file's basename via `--stdin-file-path` (which drives both parser selection and formatting).
27
+ *
28
+ * Returns the content unchanged when no Biome can be resolved at all or the format fails, so
29
+ * generation never breaks over formatting. A genuine format error is logged but non-fatal —
30
+ * an unformatted-but-valid file is recoverable by the customer running `biome format --write`.
31
+ *
32
+ * @param content - The serialized file content to format.
33
+ * @param filePath - The file's path (drives parser selection + config resolution + cwd).
34
+ * @returns The Biome-formatted content, or the original content if Biome is unavailable.
35
+ */
36
+ function formatWithProjectBiome(content, filePath) {
37
+ const biomeBin = resolveBiomeBin(filePath);
38
+ if (!biomeBin) {
39
+ logger.warn(`⚠️ Biome could not be resolved; ${basename(filePath)} will be written unformatted.`);
40
+ return content;
41
+ }
42
+ const result = spawnSync(process.execPath, [
43
+ biomeBin,
44
+ "format",
45
+ `--stdin-file-path=${basename(filePath)}`
46
+ ], {
47
+ cwd: dirname(filePath),
48
+ input: content,
49
+ encoding: "utf8"
50
+ });
51
+ if (result.status !== 0 || typeof result.stdout !== "string") {
52
+ const detail = result.stderr?.trim() || `exit code ${result.status}`;
53
+ logger.warn(`⚠️ Skipping Biome formatting for ${basename(filePath)}: ${detail}`);
54
+ return content;
55
+ }
56
+ return result.stdout;
57
+ }
58
+ /**
59
+ * Format every file Biome recognizes under `directory` in place with the consuming project's
60
+ * Biome, so generated files (e.g. cartridge metadata JSON) match `biome format` / `pnpm lint:fix`.
61
+ *
62
+ * Unlike {@link formatWithProjectBiome} (single file via stdin), this runs `biome format --write`
63
+ * against the directory so Biome discovers the project's `biome.json` by walking up from it and
64
+ * applies the project's formatting to the written files directly.
65
+ *
66
+ * Fail-safe: when Biome can't be resolved or the run fails, the files are left as written (valid,
67
+ * just unformatted) and a warning is logged — generation never breaks over formatting.
68
+ *
69
+ * @param directory - Directory whose Biome-recognized files should be formatted in place.
70
+ */
71
+ function formatDirectoryWithProjectBiome(directory) {
72
+ const biomeBin = resolveBiomeBin(join(directory, "biome-resolution-root.js"));
73
+ if (!biomeBin) {
74
+ logger.warn(`⚠️ Biome could not be resolved; generated files in ${directory} were left unformatted.`);
75
+ return;
76
+ }
77
+ const result = spawnSync(process.execPath, [
78
+ biomeBin,
79
+ "format",
80
+ "--write",
81
+ "."
82
+ ], {
83
+ cwd: directory,
84
+ encoding: "utf8"
85
+ });
86
+ if (result.status !== 0) {
87
+ const detail = result.stderr?.trim() || `exit code ${result.status}`;
88
+ logger.warn(`⚠️ Some generated files in ${directory} could not be formatted by Biome: ${detail}`);
89
+ return;
90
+ }
91
+ logger.debug(`✅ Formatted generated files in ${directory} with Biome`);
92
+ }
93
+ /**
94
+ * Resolve a Biome CLI binary path, preferring the consuming project's install and falling back
95
+ * to the SDK-bundled copy (available pre-install). Returns null when neither resolves.
96
+ */
97
+ function resolveBiomeBin(filePath) {
98
+ for (const fromPath of [filePath, HERE]) try {
99
+ const req = createRequire(fromPath);
100
+ const biomePkgJson = req.resolve("@biomejs/biome/package.json");
101
+ const { bin } = req(biomePkgJson);
102
+ return join(dirname(biomePkgJson), bin.biome);
103
+ } catch {}
104
+ return null;
105
+ }
106
+
107
+ //#endregion
108
+ export { formatWithProjectBiome as n, formatDirectoryWithProjectBiome as t };