@salesforce/storefront-next-runtime 0.2.0-alpha.2 → 0.3.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/DesignFrame.js +6 -2
  2. package/dist/DesignFrame.js.map +1 -1
  3. package/dist/DesignRegion.js +2 -1
  4. package/dist/DesignRegion.js.map +1 -1
  5. package/dist/component.types.d.ts +6 -0
  6. package/dist/component.types.d.ts.map +1 -1
  7. package/dist/config-load.d.ts +27 -0
  8. package/dist/config-load.d.ts.map +1 -0
  9. package/dist/config-load.js +3 -0
  10. package/dist/config.d.ts +248 -1
  11. package/dist/config.d.ts.map +1 -0
  12. package/dist/config.js +429 -0
  13. package/dist/config.js.map +1 -0
  14. package/dist/design-data.d.ts +40 -27
  15. package/dist/design-data.d.ts.map +1 -1
  16. package/dist/design-data.js +50 -26
  17. package/dist/design-data.js.map +1 -1
  18. package/dist/design-react-core.d.ts +2 -2
  19. package/dist/design-react-core.js +3 -1
  20. package/dist/design-react-core.js.map +1 -1
  21. package/dist/events.d.ts +9 -4
  22. package/dist/events.d.ts.map +1 -1
  23. package/dist/events.js +6 -6
  24. package/dist/events.js.map +1 -1
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/load-config.js +41 -0
  27. package/dist/load-config.js.map +1 -0
  28. package/dist/multi-site.d.ts +68 -43
  29. package/dist/multi-site.d.ts.map +1 -1
  30. package/dist/multi-site.js +36 -10
  31. package/dist/multi-site.js.map +1 -1
  32. package/dist/routing.d.ts.map +1 -1
  33. package/dist/routing.js +4 -37
  34. package/dist/routing.js.map +1 -1
  35. package/dist/scapi.d.ts +8 -0
  36. package/dist/scapi.d.ts.map +1 -1
  37. package/dist/scapi.js +1 -1
  38. package/dist/scapi.js.map +1 -1
  39. package/dist/schema.d.ts +78 -0
  40. package/dist/schema.d.ts.map +1 -0
  41. package/dist/types.d.ts.map +1 -1
  42. package/package.json +7 -1
@@ -0,0 +1,41 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ //#region src/config/load-config.ts
5
+ /**
6
+ * Dynamically imports `config.server.ts` from the project root (CWD) and returns
7
+ * the full configuration object. This runs at route discovery time under vite-node
8
+ * (typegen, dev, build), which handles the TS transformation.
9
+ *
10
+ * Uses jiti to transpile TypeScript on the fly, which works regardless of whether
11
+ * the caller runs under vite-node, a plain Node process, or any other runtime.
12
+ * This avoids the fragile assumption that vite-node will intercept dynamic imports
13
+ * from pre-compiled npm packages (it won't — Vite externalizes node_modules).
14
+ *
15
+ * Returns the full config including `metadata`, `runtime`, and `app` sections.
16
+ * Callers that only need `app` can destructure: `const { app } = await loadConfig()`.
17
+ *
18
+ * - If the config file is missing, throws with a clear message.
19
+ * - If the config file exists but fails to import, throws with the original error as cause.
20
+ *
21
+ * @returns The full configuration object.
22
+ * @throws If `config.server.ts` is not found or fails to import.
23
+ */
24
+ async function loadConfig() {
25
+ const configPath = path.resolve(process.cwd(), "config.server.ts");
26
+ if (!fs.existsSync(configPath)) throw new Error(`[storefront-next-runtime] config.server.ts is required but not found at ${configPath}. Create this file with defineConfig() to configure your storefront application.`);
27
+ try {
28
+ const { createJiti } = await import("jiti");
29
+ const mod = await createJiti(import.meta.url, {
30
+ fsCache: false,
31
+ interopDefault: true
32
+ }).import(configPath);
33
+ return mod.default ?? mod;
34
+ } catch (error) {
35
+ throw new Error(`[storefront-next-runtime] Found config.server.ts at ${configPath} but failed to import it.`, { cause: error });
36
+ }
37
+ }
38
+
39
+ //#endregion
40
+ export { loadConfig as t };
41
+ //# sourceMappingURL=load-config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"load-config.js","names":[],"sources":["../src/config/load-config.ts"],"sourcesContent":["/**\n * Copyright 2026 Salesforce, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport type { BaseConfig } from './schema';\n\n/**\n * Dynamically imports `config.server.ts` from the project root (CWD) and returns\n * the full configuration object. This runs at route discovery time under vite-node\n * (typegen, dev, build), which handles the TS transformation.\n *\n * Uses jiti to transpile TypeScript on the fly, which works regardless of whether\n * the caller runs under vite-node, a plain Node process, or any other runtime.\n * This avoids the fragile assumption that vite-node will intercept dynamic imports\n * from pre-compiled npm packages (it won't — Vite externalizes node_modules).\n *\n * Returns the full config including `metadata`, `runtime`, and `app` sections.\n * Callers that only need `app` can destructure: `const { app } = await loadConfig()`.\n *\n * - If the config file is missing, throws with a clear message.\n * - If the config file exists but fails to import, throws with the original error as cause.\n *\n * @returns The full configuration object.\n * @throws If `config.server.ts` is not found or fails to import.\n */\nexport async function loadConfig<T extends BaseConfig = BaseConfig>(): Promise<T> {\n const configPath = path.resolve(process.cwd(), 'config.server.ts');\n\n if (!fs.existsSync(configPath)) {\n throw new Error(\n `[storefront-next-runtime] config.server.ts is required but not found at ${configPath}. ` +\n `Create this file with defineConfig() to configure your storefront application.`\n );\n }\n\n try {\n const { createJiti } = await import('jiti');\n\n const jiti = createJiti(import.meta.url, {\n fsCache: false,\n interopDefault: true,\n });\n\n const mod = await jiti.import(configPath);\n const config = (mod as Record<string, unknown>).default ?? mod;\n return config as T;\n } catch (error) {\n throw new Error(`[storefront-next-runtime] Found config.server.ts at ${configPath} but failed to import it.`, {\n cause: error,\n });\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAsCA,eAAsB,aAA4D;CAC9E,MAAM,aAAa,KAAK,QAAQ,QAAQ,KAAK,EAAE,mBAAmB;AAElE,KAAI,CAAC,GAAG,WAAW,WAAW,CAC1B,OAAM,IAAI,MACN,2EAA2E,WAAW,kFAEzF;AAGL,KAAI;EACA,MAAM,EAAE,eAAe,MAAM,OAAO;EAOpC,MAAM,MAAM,MALC,WAAW,OAAO,KAAK,KAAK;GACrC,SAAS;GACT,gBAAgB;GACnB,CAAC,CAEqB,OAAO,WAAW;AAEzC,SADgB,IAAgC,WAAW;UAEtD,OAAO;AACZ,QAAM,IAAI,MAAM,uDAAuD,WAAW,4BAA4B,EAC1G,OAAO,OACV,CAAC"}
@@ -5,8 +5,45 @@ import * as react_router0 from "react-router";
5
5
  import { Cookie, MiddlewareFunction, RouterContextProvider } from "react-router";
6
6
  import { RouteConfigEntry } from "@react-router/dev/routes";
7
7
 
8
- //#region src/multi-site/site-context.d.ts
8
+ //#region src/multi-site/types.d.ts
9
9
 
10
+ type Locale = Locale$1 & {
11
+ alias?: string;
12
+ };
13
+ type Site = Omit<Site$1, 'supportedLocales'> & {
14
+ name?: string;
15
+ alias?: string;
16
+ supportedLocales: Locale[];
17
+ };
18
+ type MultiSiteContext = {
19
+ site: Site;
20
+ locale: Locale;
21
+ siteCookie: Cookie;
22
+ localeCookie: Cookie;
23
+ };
24
+ /**
25
+ * Configuration passed into the multi-site middleware
26
+ * Configured by the consumer
27
+ */
28
+ type MultiSiteConfig = {
29
+ sites: Site[];
30
+ defaultSiteId: string;
31
+ defaultLocale: string;
32
+ siteDetectionConfig?: DetectionConfig;
33
+ localeDetectionConfig?: DetectionConfig;
34
+ };
35
+ /** Detection method identifier (used for both site and locale detection) */
36
+ type DetectionMethod = 'path' | 'querystring' | 'cookie' | 'header';
37
+ type DetectionConfig = {
38
+ order: DetectionMethod[];
39
+ lookupFromPathIndex?: number;
40
+ lookupQuerystring?: string;
41
+ lookupCookie?: string;
42
+ lookupHeader?: string;
43
+ caches?: Array<'cookie'>;
44
+ };
45
+ //#endregion
46
+ //#region src/multi-site/site-context.d.ts
10
47
  /**
11
48
  * Provides the current site to the component tree.
12
49
  * Follows the same pattern as CurrencyProvider.
@@ -18,16 +55,15 @@ declare function SiteProvider({
18
55
  value,
19
56
  children
20
57
  }: PropsWithChildren<{
21
- value: Site$1;
58
+ value: Site;
22
59
  }>): react_jsx_runtime0.JSX.Element;
23
60
  /**
24
61
  * React hook to get the current site.
25
62
  * Returns undefined when no SiteProvider is mounted.
26
63
  */
27
- declare function useSite(): Site$1 | undefined;
64
+ declare function useSite(): Site | undefined;
28
65
  //#endregion
29
66
  //#region src/multi-site/apply-url-config.d.ts
30
-
31
67
  /**
32
68
  * Applies multi-site URL configuration to a set of route entries.
33
69
  *
@@ -49,7 +85,33 @@ declare function applyUrlConfig(options: {
49
85
  }): RouteConfigEntry[];
50
86
  //#endregion
51
87
  //#region src/multi-site/build-url.d.ts
52
-
88
+ /**
89
+ * Resolves a prefix template by replacing parameter placeholders with values.
90
+ * ('/:siteId/:localeId', { siteId: 'global', localeId: 'en-GB' }) → '/global/en-GB'
91
+ */
92
+ declare function resolvePrefix(prefix: string, params: Record<string, string>): string;
93
+ /**
94
+ * Strips the URL prefix segments from a pathname based on a prefix pattern.
95
+ * Since all routes are configured with the prefix baked in, segment counting is sufficient.
96
+ *
97
+ * @param pathname - Full pathname (e.g. '/global/en-GB/checkout')
98
+ * @param prefixPattern - URL prefix pattern from config (e.g. '/:siteId/:localeId')
99
+ * @returns Pathname with prefix stripped (e.g. '/checkout'), or original if
100
+ * the pathname has fewer segments than the prefix
101
+ *
102
+ * @example
103
+ * stripPathPrefix('/global/en-GB/checkout', '/:siteId/:localeId') // → '/checkout'
104
+ * stripPathPrefix('/checkout', '/:siteId/:localeId') // → '/checkout' (fewer segments → unchanged)
105
+ * stripPathPrefix('/checkout', '') // → '/checkout' (no prefix configured)
106
+ * stripPathPrefix('/', '/:siteId/:localeId') // → '/'
107
+ */
108
+ declare function stripPathPrefix(pathname: string, prefixPattern: string): string;
109
+ /**
110
+ * Sanitize a resolved prefix from a pathname if present.
111
+ * sanitizePrefix('/global/en-GB/product/123', '/global/en-GB') → '/product/123'
112
+ * sanitizePrefix('/product/123', '/global/en-GB') → '/product/123' (no-op)
113
+ */
114
+ declare function sanitizePrefix(pathname: string, pathPrefix: string): string;
53
115
  /**
54
116
  * Builds a fully-qualified URL with multi-site prefix and search params.
55
117
  *
@@ -72,43 +134,6 @@ declare function buildUrl({
72
134
  params: Record<string, string>;
73
135
  }): string;
74
136
  //#endregion
75
- //#region src/multi-site/types.d.ts
76
- type Locale = Locale$1 & {
77
- alias?: string;
78
- };
79
- type Site = Omit<Site$1, 'supportedLocales'> & {
80
- name?: string;
81
- alias?: string;
82
- supportedLocales: Locale[];
83
- };
84
- type MultiSiteContext = {
85
- site: Site;
86
- locale: Locale;
87
- siteCookie: Cookie;
88
- localeCookie: Cookie;
89
- };
90
- /**
91
- * Configuration passed into the multi-site middleware
92
- * Configured by the consumer
93
- */
94
- type MultiSiteConfig = {
95
- sites: Site[];
96
- defaultSiteId: string;
97
- defaultLocale: string;
98
- siteDetectionConfig?: DetectionConfig;
99
- localeDetectionConfig?: DetectionConfig;
100
- };
101
- /** Detection method identifier (used for both site and locale detection) */
102
- type DetectionMethod = 'path' | 'querystring' | 'cookie' | 'header';
103
- type DetectionConfig = {
104
- order: DetectionMethod[];
105
- lookupFromPathIndex?: number;
106
- lookupQuerystring?: string;
107
- lookupCookie?: string;
108
- lookupHeader?: string;
109
- caches?: Array<'cookie'>;
110
- };
111
- //#endregion
112
137
  //#region src/multi-site/middleware.d.ts
113
138
  declare const multiSiteContext: react_router0.RouterContext<MultiSiteContext | null>;
114
139
  /**
@@ -150,5 +175,5 @@ declare function createMultiSiteMiddleware(config: MultiSiteConfig): MiddlewareF
150
175
  */
151
176
  declare const requestToLocaleMap: WeakMap<Request, string>;
152
177
  //#endregion
153
- export { type DetectionConfig, type Locale, type MultiSiteConfig, type MultiSiteContext, type Site, SiteProvider, applyUrlConfig, buildUrl, createMultiSiteMiddleware, getMultiSiteCookies, multiSiteContext, requestToLocaleMap, useSite };
178
+ export { type DetectionConfig, type Locale, type MultiSiteConfig, type MultiSiteContext, type Site, SiteProvider, applyUrlConfig, buildUrl, createMultiSiteMiddleware, getMultiSiteCookies, multiSiteContext, requestToLocaleMap, resolvePrefix, sanitizePrefix, stripPathPrefix, useSite };
154
179
  //# sourceMappingURL=multi-site.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"multi-site.d.ts","names":[],"sources":["../src/multi-site/site-context.tsx","../src/multi-site/apply-url-config.ts","../src/multi-site/build-url.ts","../src/multi-site/types.ts","../src/multi-site/middleware.ts","../src/multi-site/cookies.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;ACwHA;;;;;iBD7FgB,YAAA;;;GAAkC;SAA2B;KAAO,kBAAA,CAAA,GAAA,CAAA;AEiEpF;;;;AAMgB,iBF9DA,OAAA,CAAA,CE8DA,EF9DW,ME8DX,GAAA,SAAA;;;;;;;ACpEhB;;;;;;AAWA;;;;;AAmBY,iBF4DI,cAAA,CE5DW,OAAA,EAAA;EAGf,MAAA,EF0DA,gBE1De,EAAA;cF2DX;;IAEZ;;;;;;;AEnFJ;;;;;AAmBA;AAGA;;;iBD6BgB,QAAA;;;;;EErEH,EAAA,EAAA,MAAA;EAsBG,SAAA,CAAA,EFqDA,GErDA;EAAsC,MAAA,EFsD1C,MEtD0C,CAAA,MAAA,EAAA,MAAA,CAAA;CAAT,CAAA,EAAA,MAAA;;;KDzBjC,MAAA,GAAS;;AFoGrB,CAAA;AACY,KEjGA,IAAA,GAAO,IFiGP,CEjGY,MFiGZ,EAAA,kBAAA,CAAA,GAAA;EACI,IAAA,CAAA,EAAA,MAAA;EAEZ,KAAA,CAAA,EAAA,MAAA;EAAgB,gBAAA,EEjGE,MFiGF,EAAA;;KE9FR,gBAAA;QACF;ED6DM,MAAA,EC5DJ,MD4DY;EACpB,UAAA,EC5DY,MD4DZ;EACA,YAAA,EC5Dc,MD4Dd;CACA;;;;;KCtDQ,eAAA;SACD;EAtBC,aAAM,EAAA,MAAG;EAIT,aAAI,EAAA,MAAA;EAAQ,mBAAA,CAAA,EAqBE,eArBF;EAAL,qBAAA,CAAA,EAsBS,eAtBT;CAGG;;AAmBM,KAchB,eAAA,GAdgB,MAAA,GAAA,aAAA,GAAA,QAAA,GAAA,QAAA;AAAe,KAiB/B,eAAA,GAjB+B;EAc/B,KAAA,EAID,eAJgB,EAAA;EAGf,mBAAe,CAAA,EAAA,MAAA;;;;ECxCd,MAAA,CAAA,ED8CA,KC9CA,CAAA,QAA+D,CAAA;AAsB5E,CAAA;;;cAtBa,kBAAgB,aAAA,CAAA,cAAA;;AHiG7B;;;;;;;;AC5BA;;;;;;;;;iBE/CgB,mBAAA,UAA6B,SAAS;EDzB1C,UAAM,ECyBmC,aAAA,CAAA,MDzBtB;EAInB,YAAI,sBAAA;CAAQ,GAAA,IAAA;;;;AAMxB;;;;AAIkB,iBC8EF,yBAAA,CD9EE,MAAA,EC8EgC,eD9EhC,CAAA,EC8EkD,kBD9ElD,CC8EqE,QD9ErE,CAAA;;;;;;;;AAdN,cEuBC,kBFvBkB,EEuBA,OFvBA,CEuBA,OFvBA,EAAA,MAAA,CAAA"}
1
+ {"version":3,"file":"multi-site.d.ts","names":[],"sources":["../src/multi-site/types.ts","../src/multi-site/site-context.tsx","../src/multi-site/apply-url-config.ts","../src/multi-site/build-url.ts","../src/multi-site/middleware.ts","../src/multi-site/cookies.ts"],"sourcesContent":[],"mappings":";;;;;;;;;AAiCgB,KAbJ,MAAA,GAAS,QAaL,GAAA;EACE,KAAA,CAAA,EAAA,MAAA;CAAM;AAOZ,KAjBA,IAAA,GAAO,IAiBQ,CAjBH,MAiBG,EAAA,kBAAA,CAAA,GAAA;EAChB,IAAA,CAAA,EAAA,MAAA;EAGe,KAAA,CAAA,EAAA,MAAA;EACE,gBAAA,EAnBN,MAmBM,EAAA;CAAe;AAc/B,KA9BA,gBAAA,GA8Be;EAGf,IAAA,EAhCF,IAgCE;UA/BA;cACI;gBACE;ACPlB,CAAA;;;;;AAAoF,KDcxE,eAAA,GCdwE;EAAA,KAAA,EDezE,ICfyE,EAAA;EASpE,aAAO,EAAA,MAAA;;wBDSG;0BACE;AE0E5B,CAAA;ACnBA;AAmBgB,KH5DJ,eAAA,GG4DY,MAAA,GAAA,aAAA,GAAA,QAAA,GAAA,QAAA;AACpB,KH1DQ,eAAA,GG0DR;EACA,KAAA,EH1DO,eG0DP,EAAA;EACA,mBAAA,CAAA,EAAA,MAAA;EAGY,iBAAA,CAAA,EAAA,MAAA;EACJ,YAAA,CAAA,EAAA,MAAA;EAAM,YAAA,CAAA,EAAA,MAAA;WH1DL;;;;;;;AA5Bb;;;;AAK2C,iBCnB3B,YAAA,CDmB2B;EAAA,KAAA;EAAA;AAiB3C,CAjB2C,ECnBO,iBDmBP,CAAA;EAc/B,KAAA,ECjCiE,IDiCjE;AAGZ,CAAA,CAAA,CAAA,ECpCoF,kBAAA,CAAA,GAAA,CAAA,ODqCzE;;;;ACrCX;AAA+B,iBASf,OAAA,CAAA,CATe,EASJ,IATI,GAAA,SAAA;;;;AC6F/B;;;;;;;;AC/DA;AA0BA;AAkBA;AAmBA;;AAEI,iBDFY,cAAA,CCEZ,OAAA,EAAA;EACA,MAAA,EDFQ,gBCER,EAAA;EAGY,SAAA,CAAA,EDJA,GCIA;EACJ,WAAA,EAAA,MAAA;CAAM,CAAA,EDHd,gBCGc,EAAA;;;;;AF3FlB;;iBEqBgB,aAAA,yBAAsC;;AD+DtD;;;;;;;;AC/DA;AA0BA;AAkBA;AAmBA;;;AAGI,iBAxCY,eAAA,CAwCZ,QAAA,EAAA,MAAA,EAAA,aAAA,EAAA,MAAA,CAAA,EAAA,MAAA;;;;;;iBAtBY,cAAA;AC9EhB;AAsBA;;;;;;AAoEA;;;;;iBDOgB,QAAA;;;;;;cAMA;EEnFH,MAAA,EFoFD,MEpFC,CAAA,MAAmD,EAAA,MAAA,CAAA;;;;ALVhD,cIVH,gBJUG,EIVa,aAAA,CAAA,aJUb,CIVa,gBJUb,GAAA,IAAA,CAAA;;;AAQhB;;;;;AAmBA;AAGA;;;;ACpCA;;;;;;AAAoF,iBGkBpE,mBAAA,CHlBoE,OAAA,EGkBvC,QHlBuC,CGkB9B,qBHlB8B,CAAA,CAAA,EAAA;EASpE,UAAO,EGS8B,aAAA,CAAA,MHTtB;;;;ACoF/B;;;;;;iBEPgB,yBAAA,SAAkC,kBAAkB,mBAAmB;;;;;;;;AH7EvE,cIOH,kBJPkB,EIOA,OJPA,CIOA,OJPA,EAAA,MAAA,CAAA"}
@@ -75,6 +75,28 @@ function resolvePrefix(prefix, params) {
75
75
  return resolved;
76
76
  }
77
77
  /**
78
+ * Strips the URL prefix segments from a pathname based on a prefix pattern.
79
+ * Since all routes are configured with the prefix baked in, segment counting is sufficient.
80
+ *
81
+ * @param pathname - Full pathname (e.g. '/global/en-GB/checkout')
82
+ * @param prefixPattern - URL prefix pattern from config (e.g. '/:siteId/:localeId')
83
+ * @returns Pathname with prefix stripped (e.g. '/checkout'), or original if
84
+ * the pathname has fewer segments than the prefix
85
+ *
86
+ * @example
87
+ * stripPathPrefix('/global/en-GB/checkout', '/:siteId/:localeId') // → '/checkout'
88
+ * stripPathPrefix('/checkout', '/:siteId/:localeId') // → '/checkout' (fewer segments → unchanged)
89
+ * stripPathPrefix('/checkout', '') // → '/checkout' (no prefix configured)
90
+ * stripPathPrefix('/', '/:siteId/:localeId') // → '/'
91
+ */
92
+ function stripPathPrefix(pathname, prefixPattern) {
93
+ if (!prefixPattern) return pathname;
94
+ const prefixSegmentCount = prefixPattern.split("/").filter(Boolean).length;
95
+ const pathSegments = pathname.split("/").filter(Boolean);
96
+ if (pathSegments.length <= prefixSegmentCount) return pathSegments.length === prefixSegmentCount ? "/" : pathname;
97
+ return `/${pathSegments.slice(prefixSegmentCount).join("/")}`;
98
+ }
99
+ /**
78
100
  * Sanitize a resolved prefix from a pathname if present.
79
101
  * sanitizePrefix('/global/en-GB/product/123', '/global/en-GB') → '/product/123'
80
102
  * sanitizePrefix('/product/123', '/global/en-GB') → '/product/123' (no-op)
@@ -161,8 +183,9 @@ function getSiteFromIdOrAlias(siteIdentifier, sites) {
161
183
  async function resolveSite(request, settings) {
162
184
  const { sites, defaultSiteId, siteDetectionConfig, siteCookie } = settings;
163
185
  const requestUrl = new URL(request.url);
186
+ const basePathOffset = process.env.MRT_ENV_BASE_PATH ? process.env.MRT_ENV_BASE_PATH.split("/").filter(Boolean).length : 0;
164
187
  const resolvers = {
165
- path: () => Promise.resolve(lookupFromPath(requestUrl.pathname, siteDetectionConfig.lookupFromPathIndex)),
188
+ path: () => Promise.resolve(lookupFromPath(requestUrl.pathname, siteDetectionConfig.lookupFromPathIndex + basePathOffset)),
166
189
  querystring: () => Promise.resolve(requestUrl.searchParams.get(siteDetectionConfig.lookupQuerystring)),
167
190
  header: () => Promise.resolve(request.headers.get(siteDetectionConfig.lookupHeader)),
168
191
  cookie: async () => readSiteFromCookie(request, siteCookie)
@@ -270,8 +293,9 @@ async function resolveLocale(request, settings, site) {
270
293
  const { supportedLocales } = site;
271
294
  let locale = null;
272
295
  const requestUrl = new URL(request.url);
296
+ const basePathOffset = process.env.MRT_ENV_BASE_PATH ? process.env.MRT_ENV_BASE_PATH.split("/").filter(Boolean).length : 0;
273
297
  const resolvers = {
274
- path: () => Promise.resolve(lookupFromPath(requestUrl.pathname, localeDetectionConfig.lookupFromPathIndex)),
298
+ path: () => Promise.resolve(lookupFromPath(requestUrl.pathname, localeDetectionConfig.lookupFromPathIndex + basePathOffset)),
275
299
  querystring: () => Promise.resolve(requestUrl.searchParams.get(localeDetectionConfig.lookupQuerystring)),
276
300
  header: () => Promise.resolve(request.headers.get(localeDetectionConfig.lookupHeader)),
277
301
  cookie: async () => readLocaleFromCookie(request, localeCookie)
@@ -317,29 +341,31 @@ function getMultiSiteCookies(context) {
317
341
  /**
318
342
  * Helper function to determine if cookies should be set based on:
319
343
  * 1. Whether caching is enabled for each cookie type
320
- * 2. Whether cookies already exist in the incoming request
344
+ * 2. Whether the resolved value differs from the existing cookie
321
345
  * 3. Whether cookies were already set by actions/loaders in the response
322
346
  *
323
347
  * @param request - Incoming request
324
348
  * @param response - Response from next()
325
349
  * @param settings - Multi-site settings with cookie instances and detection config
350
+ * @param site - Resolved site for this request
351
+ * @param locale - Resolved locale for this request
326
352
  * @returns Object with shouldSetSiteCookie and shouldSetLocaleCookie booleans
327
353
  */
328
- async function shouldSetCookies(request, response, settings) {
354
+ async function shouldSetCookies(request, response, settings, site, locale) {
329
355
  const cacheSite = settings.siteDetectionConfig.caches?.includes("cookie");
330
356
  const cacheLocale = settings.localeDetectionConfig.caches?.includes("cookie");
331
357
  if (!cacheSite && !cacheLocale) return {
332
358
  shouldSetSiteCookie: false,
333
359
  shouldSetLocaleCookie: false
334
360
  };
335
- const requestCookieHeader = request.headers.get("Cookie");
336
- const [existingSiteCookie, existingLocaleCookie] = await Promise.all([settings.siteCookie.parse(requestCookieHeader), settings.localeCookie.parse(requestCookieHeader)]);
337
361
  const responseSetCookies = response.headers.getSetCookie?.() || [];
338
362
  const isSettingSiteCookieInResponse = responseSetCookies.some((cookie) => cookie.startsWith(`${settings.siteCookie.name}=`));
339
363
  const isSettingLocaleCookieInResponse = responseSetCookies.some((cookie) => cookie.startsWith(`${settings.localeCookie.name}=`));
364
+ const requestCookieHeader = request.headers.get("Cookie");
365
+ const [existingSiteCookie, existingLocaleCookie] = await Promise.all([settings.siteCookie.parse(requestCookieHeader), settings.localeCookie.parse(requestCookieHeader)]);
340
366
  return {
341
- shouldSetSiteCookie: cacheSite && !existingSiteCookie && !isSettingSiteCookieInResponse,
342
- shouldSetLocaleCookie: cacheLocale && !existingLocaleCookie && !isSettingLocaleCookieInResponse
367
+ shouldSetSiteCookie: cacheSite && !isSettingSiteCookieInResponse && existingSiteCookie !== site.id,
368
+ shouldSetLocaleCookie: cacheLocale && !isSettingLocaleCookieInResponse && existingLocaleCookie !== locale.id
343
369
  };
344
370
  }
345
371
  /**
@@ -378,7 +404,7 @@ function createMultiSiteMiddleware(config) {
378
404
  });
379
405
  requestToLocaleMap.set(request, locale.id);
380
406
  const response = await next();
381
- const { shouldSetSiteCookie, shouldSetLocaleCookie } = await shouldSetCookies(request, response, settings);
407
+ const { shouldSetSiteCookie, shouldSetLocaleCookie } = await shouldSetCookies(request, response, settings, site, locale);
382
408
  if (!shouldSetSiteCookie && !shouldSetLocaleCookie) return response;
383
409
  const [siteSetCookie, localeSetCookie] = await Promise.all([shouldSetSiteCookie ? settings.siteCookie.serialize(site.id, { path: "/" }) : Promise.resolve(null), shouldSetLocaleCookie ? settings.localeCookie.serialize(locale.id, { path: "/" }) : Promise.resolve(null)]);
384
410
  if (siteSetCookie) response.headers.append("Set-Cookie", siteSetCookie);
@@ -389,5 +415,5 @@ function createMultiSiteMiddleware(config) {
389
415
  }
390
416
 
391
417
  //#endregion
392
- export { SiteProvider, applyUrlConfig, buildUrl, createMultiSiteMiddleware, getMultiSiteCookies, multiSiteContext, requestToLocaleMap, useSite };
418
+ export { SiteProvider, applyUrlConfig, buildUrl, createMultiSiteMiddleware, getMultiSiteCookies, multiSiteContext, requestToLocaleMap, resolvePrefix, sanitizePrefix, stripPathPrefix, useSite };
393
419
  //# sourceMappingURL=multi-site.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"multi-site.js","names":["result: Record<string, string>","resolvers: Record<DetectionMethod, () => Promise<string | null>>","DEFAULT_SITE_DETECTION: Required<DetectionConfig>","DEFAULT_LOCALE_DETECTION: Required<DetectionConfig>","locale: Locale | null","resolvers: Record<DetectionMethod, () => Promise<string | null>>","createContext","siteDetectionConfig: MultiSiteSettings['siteDetectionConfig']","localeDetectionConfig: MultiSiteSettings['localeDetectionConfig']","settings: MultiSiteSettings","multiSiteMiddleware: MiddlewareFunction<Response>"],"sources":["../src/multi-site/site-context.tsx","../src/multi-site/build-url.ts","../src/multi-site/utils.ts","../src/multi-site/site-detection.ts","../src/multi-site/configs.ts","../src/multi-site/cookies.ts","../src/multi-site/locale-detection.ts","../src/multi-site/middleware.ts"],"sourcesContent":["/**\n * Copyright 2026 Salesforce, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport { createContext, useContext, type PropsWithChildren } from 'react';\nimport type { Site } from '../config/types';\n\nconst SiteContext = createContext<Site | undefined>(undefined);\n\n/**\n * Provides the current site to the component tree.\n * Follows the same pattern as CurrencyProvider.\n *\n * Mounted in the template (e.g., app-wrapper.tsx or root.tsx) with the resolved\n * site value from the loader/middleware.\n */\nexport function SiteProvider({ value, children }: PropsWithChildren<{ value: Site }>) {\n return <SiteContext.Provider value={value}>{children}</SiteContext.Provider>;\n}\n\n/**\n * React hook to get the current site.\n * Returns undefined when no SiteProvider is mounted.\n */\n// eslint-disable-next-line react-refresh/only-export-components\nexport function useSite(): Site | undefined {\n return useContext(SiteContext);\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 { Url } from '../config/types';\n\n/**\n * Parses search config string into key-value pairs, preserving ':param' placeholders.\n * '?lng=:localeId&site=:siteId' → { lng: ':localeId', site: ':siteId' }\n */\nexport function parseSearchConfig(search: string): Record<string, string> {\n const searchParams = new URLSearchParams(search);\n const result: Record<string, string> = {};\n for (const [key, value] of searchParams) {\n result[key] = value;\n }\n return result;\n}\n\n/**\n * Extracts parameter names from a prefix string.\n * '/:siteId/:localeId' → ['siteId', 'localeId']\n */\nexport function extractPrefixParams(prefix: string): string[] {\n const matches = prefix.match(/:(\\w+)/g);\n return matches ? matches.map((m) => m.slice(1)) : [];\n}\n\n/**\n * Splits a URL string into its component parts.\n * '/product/123?color=red#details' → { pathname: '/product/123', search: 'color=red', hash: '#details' }\n */\nexport function decomposeUrl(url: string): { pathname: string; search: string; hash: string } {\n const hashIdx = url.indexOf('#');\n const hash = hashIdx >= 0 ? url.slice(hashIdx) : '';\n const withoutHash = hashIdx >= 0 ? url.slice(0, hashIdx) : url;\n const searchIdx = withoutHash.indexOf('?');\n const search = searchIdx >= 0 ? withoutHash.slice(searchIdx + 1) : '';\n const pathname = searchIdx >= 0 ? withoutHash.slice(0, searchIdx) : withoutHash;\n return { pathname, search, hash };\n}\n\n/**\n * Resolves a prefix template by replacing parameter placeholders with values.\n * ('/:siteId/:localeId', { siteId: 'global', localeId: 'en-GB' }) → '/global/en-GB'\n */\nexport function resolvePrefix(prefix: string, params: Record<string, string>): string {\n let resolved = prefix;\n for (const paramName of extractPrefixParams(prefix)) {\n const value = params[paramName];\n if (value) {\n resolved = resolved.replace(`:${paramName}`, value);\n }\n }\n return resolved;\n}\n\n/**\n * Sanitize a resolved prefix from a pathname if present.\n * sanitizePrefix('/global/en-GB/product/123', '/global/en-GB') → '/product/123'\n * sanitizePrefix('/product/123', '/global/en-GB') → '/product/123' (no-op)\n */\nexport function sanitizePrefix(pathname: string, pathPrefix: string): string {\n if (!pathPrefix) return pathname;\n if (pathname === pathPrefix) return '';\n if (pathname.startsWith(`${pathPrefix}/`)) return pathname.slice(pathPrefix.length);\n return pathname;\n}\n\n/**\n * Builds a fully-qualified URL with multi-site prefix and search params.\n *\n * Only keys defined in urlConfig.search are set by multi-site. Any other query params\n * already present on the `to` URL (including duplicate keys) are preserved as-is.\n * e.g. to='/api/search?refine=color:blue&refine=size:M', search='?lng=:localeId'\n * → '/api/search?refine=color:blue&refine=size:M&lng=en-GB'\n *\n * @example\n * buildUrl({ to: '/product/123', urlConfig: { prefix: '/:siteId', search: '?lng=:localeId' }, params: { siteId: 'global', localeId: 'en-GB' } })\n * // → '/global/product/123?lng=en-GB'\n */\nexport function buildUrl({\n to,\n urlConfig,\n params,\n}: {\n to: string;\n urlConfig?: Url;\n params: Record<string, string>;\n}): string {\n if (!urlConfig) return to;\n if (!to || to === '#' || to.startsWith('http') || to.startsWith('//')) return to;\n\n const { pathname, search: existingSearch, hash } = decomposeUrl(to);\n\n const pathPrefix = urlConfig.prefix && urlConfig.prefix !== '/' ? resolvePrefix(urlConfig.prefix, params) : '';\n // sanitize prefix to make sure there is no prefix duplication at any case\n const path = pathPrefix ? `${pathPrefix}${sanitizePrefix(pathname, pathPrefix)}` : pathname;\n\n const searchParams = new URLSearchParams(existingSearch);\n if (urlConfig.search) {\n const searchConfig = parseSearchConfig(urlConfig.search);\n for (const [queryKey, value] of Object.entries(searchConfig)) {\n if (value.startsWith(':')) {\n const paramValue = params[value.slice(1)];\n if (paramValue) {\n searchParams.set(queryKey, paramValue);\n }\n } else {\n searchParams.set(queryKey, value);\n }\n }\n }\n\n const search = searchParams.toString();\n return `${path}${search ? `?${search}` : ''}${hash}`;\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\nimport type { Cookie } from 'react-router';\n\n/**\n * Extract a string value from the URL path segment at the given index.\n */\nexport function lookupFromPath(pathname: string, pathIndex: number): string | null {\n const pathSegments = pathname.split('/').filter(Boolean);\n\n if (pathSegments.length <= pathIndex) return null;\n\n return pathSegments[pathIndex];\n}\n\n/**\n * Detect a string value from cookie using the given cookie parser.\n *\n * Returns a promise that resolves to the cookie value.\n */\nexport async function readCookieFromRequest(request: Request, cookie: Cookie): Promise<string | null> {\n const cookies = request.headers.get('Cookie');\n if (!cookies) return null;\n\n const cookieValue = await cookie.parse(cookies);\n return cookieValue;\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\nimport type { Cookie } from 'react-router';\nimport type { Site, MultiSiteSettings, DetectionMethod } from './types';\nimport { readCookieFromRequest, lookupFromPath } from './utils';\n\n/**\n * Detect site reference from cookie.\n */\nexport async function readSiteFromCookie(request: Request, cookie: Cookie): Promise<string | null> {\n return readCookieFromRequest(request, cookie);\n}\n\n/**\n * Get site object using the site id or alias\n * 1. Check siteIdentifier against each site's alias; if matched, return that site.\n * 2. Else check against each site's id; if matched, return that site.\n * 3. If no match, return null.\n */\nfunction getSiteFromIdOrAlias(siteIdentifier: string | null, sites: Site[]): Site | null {\n if (!siteIdentifier) return null;\n return sites.find((site) => site.alias === siteIdentifier || site.id === siteIdentifier) ?? null;\n}\n\n/**\n * Resolve site using the configured detection order.\n * Returns the first valid site from the first source that yields a valid value.\n */\nexport async function resolveSite(request: Request, settings: MultiSiteSettings): Promise<Site> {\n const { sites, defaultSiteId, siteDetectionConfig, siteCookie } = settings;\n\n const requestUrl = new URL(request.url);\n\n const resolvers: Record<DetectionMethod, () => Promise<string | null>> = {\n path: () => Promise.resolve(lookupFromPath(requestUrl.pathname, siteDetectionConfig.lookupFromPathIndex)),\n querystring: () => Promise.resolve(requestUrl.searchParams.get(siteDetectionConfig.lookupQuerystring)),\n header: () => Promise.resolve(request.headers.get(siteDetectionConfig.lookupHeader)),\n cookie: async () => readSiteFromCookie(request, siteCookie),\n };\n\n for (const method of siteDetectionConfig.order) {\n const siteIdOrAlias = await resolvers[method]?.();\n const resolvedSite = getSiteFromIdOrAlias(siteIdOrAlias, sites);\n if (resolvedSite) return resolvedSite;\n }\n\n // If no site id was found, use the default site id\n const site = getSiteFromIdOrAlias(defaultSiteId, sites);\n\n // If default site id is invalid, throw an error\n if (!site) {\n throw new Error(`Default site ${defaultSiteId} not found.`);\n }\n\n return site;\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\nimport type { DetectionConfig } from './types';\n\n/**\n * Default site detection configuration\n */\nexport const DEFAULT_SITE_DETECTION: Required<DetectionConfig> = {\n order: ['path', 'querystring', 'cookie', 'header'],\n lookupFromPathIndex: 0,\n lookupQuerystring: 'site',\n lookupCookie: 'site_id',\n lookupHeader: 'X-Site-Id',\n caches: ['cookie'],\n};\n\n/**\n * Default locale detection configuration\n */\nexport const DEFAULT_LOCALE_DETECTION: Required<DetectionConfig> = {\n order: ['path', 'querystring', 'cookie', 'header'],\n lookupFromPathIndex: 1,\n lookupQuerystring: 'lng',\n lookupCookie: 'lng',\n lookupHeader: 'Accept-Language',\n caches: ['cookie'],\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\nimport { createCookie, type Cookie } from 'react-router';\n\n/**\n * Cookie options for multi-site cookies\n */\nexport const COOKIE_OPTIONS = {\n path: '/',\n sameSite: 'lax' as const,\n secure: process.env.NODE_ENV === 'production',\n httpOnly: true,\n};\n\n/**\n * Creates a cookie instance with the given name.\n *\n * @param name - Cookie name\n * @returns Cookie instance configured with multi-site options\n */\nexport function createMultiSiteCookie(name: string): Cookie {\n return createCookie(name, COOKIE_OPTIONS);\n}\n\n/**\n * WeakMap to pass resolved locale from multi-site middleware to i18next's findLocale.\n * WeakMap allows garbage collection when requests are done.\n * This is necessary because findLocale() only receives the Request object, not the router context.\n */\nexport const requestToLocaleMap = new WeakMap<Request, string>();\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\nimport type { Cookie } from 'react-router';\nimport type { DetectionMethod, Locale, MultiSiteSettings, Site } from './types';\nimport { readCookieFromRequest, lookupFromPath } from './utils';\n\n/**\n * Read locale from cookie.\n */\nexport async function readLocaleFromCookie(request: Request, cookie: Cookie): Promise<string | null> {\n return readCookieFromRequest(request, cookie);\n}\n\n/**\n * Get locale object using the locale id or alias.\n * 1. Check localeIdOrAlias against each locale's alias; if matched, return that locale.\n * 2. Else check against each locale's id; if matched, return that locale.\n * 3. If no match, return null (caller should use defaultLocale).\n *\n * @param localeIdentifier - The locale id or alias to get the locale from. Null is allowed because this may come from\n * extrenal sources such as cookies, headers, or query parameters.\n * @param locales - The list of locales to search through.\n * @returns The locale object if found, otherwise null.\n */\nfunction getLocaleFromIdOrAlias(localeIdentifier: string | undefined | null, locales: Locale[]): Locale | null {\n if (!localeIdentifier) return null;\n return locales.find((locale) => locale.alias === localeIdentifier || locale.id === localeIdentifier) ?? null;\n}\n\n/**\n * Resolve locale using the configured detection order.\n * Returns the first valid locale from the first source that yields a valid value.\n */\nexport async function resolveLocale(request: Request, settings: MultiSiteSettings, site: Site): Promise<Locale> {\n const { defaultLocale, localeDetectionConfig, localeCookie } = settings;\n const { supportedLocales } = site;\n\n let locale: Locale | null = null;\n const requestUrl = new URL(request.url);\n\n const resolvers: Record<DetectionMethod, () => Promise<string | null>> = {\n path: () => Promise.resolve(lookupFromPath(requestUrl.pathname, localeDetectionConfig.lookupFromPathIndex)),\n querystring: () => Promise.resolve(requestUrl.searchParams.get(localeDetectionConfig.lookupQuerystring)),\n header: () => Promise.resolve(request.headers.get(localeDetectionConfig.lookupHeader)),\n cookie: async () => readLocaleFromCookie(request, localeCookie),\n };\n\n for (const method of localeDetectionConfig.order) {\n const localeIdOrAlias = await resolvers[method]?.();\n const resolvedLocale = getLocaleFromIdOrAlias(localeIdOrAlias, supportedLocales);\n if (resolvedLocale) return resolvedLocale;\n }\n\n // If no locale was found, use the default locale\n if (!locale) {\n locale = getLocaleFromIdOrAlias(defaultLocale, supportedLocales);\n }\n\n // If default locale is invalid, throw an error\n if (!locale) {\n throw new Error(\n `Default locale ${defaultLocale} not found in the list of supported locales for site ${site.id}.`\n );\n }\n\n return locale;\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\nimport { createContext, type MiddlewareFunction, type RouterContextProvider } from 'react-router';\nimport { resolveSite } from './site-detection';\nimport type { MultiSiteConfig, MultiSiteContext, MultiSiteSettings } from './types';\nimport { DEFAULT_SITE_DETECTION, DEFAULT_LOCALE_DETECTION } from './configs';\nimport { createMultiSiteCookie, requestToLocaleMap } from './cookies';\nimport { resolveLocale } from './locale-detection';\n\nexport const multiSiteContext = createContext<MultiSiteContext | null>(null);\n\ntype MiddlewareArgs = { request: Request; context: Readonly<RouterContextProvider> };\n\n/**\n * Helper function to get multi-site cookies from router context.\n * Useful in server actions and loaders that need to read/set cookies.\n *\n * @param context - Router context provider\n * @returns Object with siteCookie and localeCookie instances, or null if context not set\n *\n * @example\n * ```typescript\n * export const action: ActionFunction = async ({ request, context }) => {\n * const cookies = getMultiSiteCookies(context);\n * if (cookies) {\n * const cookieHeader = await cookies.localeCookie.serialize(locale);\n * // ... use cookieHeader\n * }\n * };\n * ```\n */\nexport function getMultiSiteCookies(context: Readonly<RouterContextProvider>) {\n const multiSite = context.get(multiSiteContext);\n if (!multiSite) return null;\n return {\n siteCookie: multiSite.siteCookie,\n localeCookie: multiSite.localeCookie,\n };\n}\n\n/**\n * Helper function to determine if cookies should be set based on:\n * 1. Whether caching is enabled for each cookie type\n * 2. Whether cookies already exist in the incoming request\n * 3. Whether cookies were already set by actions/loaders in the response\n *\n * @param request - Incoming request\n * @param response - Response from next()\n * @param settings - Multi-site settings with cookie instances and detection config\n * @returns Object with shouldSetSiteCookie and shouldSetLocaleCookie booleans\n */\nasync function shouldSetCookies(\n request: Request,\n response: Response,\n settings: MultiSiteSettings\n): Promise<{ shouldSetSiteCookie: boolean; shouldSetLocaleCookie: boolean }> {\n const cacheSite = settings.siteDetectionConfig.caches?.includes('cookie');\n const cacheLocale = settings.localeDetectionConfig.caches?.includes('cookie');\n\n // Early return if no cookie caching is enabled\n if (!cacheSite && !cacheLocale) {\n return { shouldSetSiteCookie: false, shouldSetLocaleCookie: false };\n }\n\n // Check if cookies already exist in the incoming request\n // If cookies already exist, we don't need to set them here\n const requestCookieHeader = request.headers.get('Cookie');\n const [existingSiteCookie, existingLocaleCookie] = await Promise.all([\n settings.siteCookie.parse(requestCookieHeader),\n settings.localeCookie.parse(requestCookieHeader),\n ]);\n\n // Check if cookies were already set by actions/loaders\n // If they were then we don't want to override them\n const responseSetCookies = response.headers.getSetCookie?.() || [];\n const isSettingSiteCookieInResponse = responseSetCookies.some((cookie) =>\n cookie.startsWith(`${settings.siteCookie.name}=`)\n );\n const isSettingLocaleCookieInResponse = responseSetCookies.some((cookie) =>\n cookie.startsWith(`${settings.localeCookie.name}=`)\n );\n\n // Only set cookies if they don't exist in request AND weren't set in response\n // In other words, we create the cookies if they are not initialized\n // Othewise, rely only actions to update the cookies\n return {\n shouldSetSiteCookie: cacheSite && !existingSiteCookie && !isSettingSiteCookieInResponse,\n shouldSetLocaleCookie: cacheLocale && !existingLocaleCookie && !isSettingLocaleCookieInResponse,\n };\n}\n\n/**\n * Creates a multi-site middleware that resolves the current site from\n * the request (path, cookie, header, query, or default) and stores the\n * result in the router context.\n *\n * Does not import or read from app config context; the consumer supplies config.\n */\nexport function createMultiSiteMiddleware(config: MultiSiteConfig): MiddlewareFunction<Response> {\n // Merge config with defaults so every detection option has a value\n const siteDetectionConfig: MultiSiteSettings['siteDetectionConfig'] = {\n ...DEFAULT_SITE_DETECTION,\n ...config.siteDetectionConfig,\n };\n const localeDetectionConfig: MultiSiteSettings['localeDetectionConfig'] = {\n ...DEFAULT_LOCALE_DETECTION,\n ...config.localeDetectionConfig,\n };\n\n // Create cookies based on configured names\n const siteCookie = createMultiSiteCookie(siteDetectionConfig.lookupCookie);\n const localeCookie = createMultiSiteCookie(localeDetectionConfig.lookupCookie);\n\n const settings: MultiSiteSettings = {\n ...config,\n siteDetectionConfig,\n localeDetectionConfig,\n siteCookie,\n localeCookie,\n };\n\n const multiSiteMiddleware: MiddlewareFunction<Response> = async (\n { request, context }: MiddlewareArgs,\n next: () => Promise<Response>\n ): Promise<Response> => {\n const site = await resolveSite(request, settings);\n const locale = await resolveLocale(request, settings, site);\n\n // Store full Site, Locale, and Cookie objects in context for downstream middlewares (currency, loaders, etc.)\n context.set(multiSiteContext, {\n site,\n locale,\n siteCookie: settings.siteCookie,\n localeCookie: settings.localeCookie,\n });\n\n // Store locale in a WeakMap so i18next's findLocale can access it\n // This is necessary because findLocale only receives Request and cannot access the router context\n requestToLocaleMap.set(request, locale.id);\n\n const response = await next();\n\n // Determine if cookies should be set\n const { shouldSetSiteCookie, shouldSetLocaleCookie } = await shouldSetCookies(request, response, settings);\n\n // Early return if no cookies need to be set\n if (!shouldSetSiteCookie && !shouldSetLocaleCookie) {\n return response;\n }\n\n const [siteSetCookie, localeSetCookie] = await Promise.all([\n shouldSetSiteCookie ? settings.siteCookie.serialize(site.id, { path: '/' }) : Promise.resolve(null),\n shouldSetLocaleCookie ? settings.localeCookie.serialize(locale.id, { path: '/' }) : Promise.resolve(null),\n ]);\n\n if (siteSetCookie) response.headers.append('Set-Cookie', siteSetCookie);\n if (localeSetCookie) response.headers.append('Set-Cookie', localeSetCookie);\n\n return response;\n };\n\n return multiSiteMiddleware;\n}\n"],"mappings":";;;;;;AAkBA,MAAM,cAAc,cAAgC,OAAU;;;;;;;;AAS9D,SAAgB,aAAa,EAAE,OAAO,YAAgD;AAClF,QAAO,oBAAC,YAAY;EAAgB;EAAQ;GAAgC;;;;;;AAQhF,SAAgB,UAA4B;AACxC,QAAO,WAAW,YAAY;;;;;;;;;AChBlC,SAAgB,kBAAkB,QAAwC;CACtE,MAAM,eAAe,IAAI,gBAAgB,OAAO;CAChD,MAAMA,SAAiC,EAAE;AACzC,MAAK,MAAM,CAAC,KAAK,UAAU,aACvB,QAAO,OAAO;AAElB,QAAO;;;;;;AAOX,SAAgB,oBAAoB,QAA0B;CAC1D,MAAM,UAAU,OAAO,MAAM,UAAU;AACvC,QAAO,UAAU,QAAQ,KAAK,MAAM,EAAE,MAAM,EAAE,CAAC,GAAG,EAAE;;;;;;AAOxD,SAAgB,aAAa,KAAiE;CAC1F,MAAM,UAAU,IAAI,QAAQ,IAAI;CAChC,MAAM,OAAO,WAAW,IAAI,IAAI,MAAM,QAAQ,GAAG;CACjD,MAAM,cAAc,WAAW,IAAI,IAAI,MAAM,GAAG,QAAQ,GAAG;CAC3D,MAAM,YAAY,YAAY,QAAQ,IAAI;CAC1C,MAAM,SAAS,aAAa,IAAI,YAAY,MAAM,YAAY,EAAE,GAAG;AAEnE,QAAO;EAAE,UADQ,aAAa,IAAI,YAAY,MAAM,GAAG,UAAU,GAAG;EACjD;EAAQ;EAAM;;;;;;AAOrC,SAAgB,cAAc,QAAgB,QAAwC;CAClF,IAAI,WAAW;AACf,MAAK,MAAM,aAAa,oBAAoB,OAAO,EAAE;EACjD,MAAM,QAAQ,OAAO;AACrB,MAAI,MACA,YAAW,SAAS,QAAQ,IAAI,aAAa,MAAM;;AAG3D,QAAO;;;;;;;AAQX,SAAgB,eAAe,UAAkB,YAA4B;AACzE,KAAI,CAAC,WAAY,QAAO;AACxB,KAAI,aAAa,WAAY,QAAO;AACpC,KAAI,SAAS,WAAW,GAAG,WAAW,GAAG,CAAE,QAAO,SAAS,MAAM,WAAW,OAAO;AACnF,QAAO;;;;;;;;;;;;;;AAeX,SAAgB,SAAS,EACrB,IACA,WACA,UAKO;AACP,KAAI,CAAC,UAAW,QAAO;AACvB,KAAI,CAAC,MAAM,OAAO,OAAO,GAAG,WAAW,OAAO,IAAI,GAAG,WAAW,KAAK,CAAE,QAAO;CAE9E,MAAM,EAAE,UAAU,QAAQ,gBAAgB,SAAS,aAAa,GAAG;CAEnE,MAAM,aAAa,UAAU,UAAU,UAAU,WAAW,MAAM,cAAc,UAAU,QAAQ,OAAO,GAAG;CAE5G,MAAM,OAAO,aAAa,GAAG,aAAa,eAAe,UAAU,WAAW,KAAK;CAEnF,MAAM,eAAe,IAAI,gBAAgB,eAAe;AACxD,KAAI,UAAU,QAAQ;EAClB,MAAM,eAAe,kBAAkB,UAAU,OAAO;AACxD,OAAK,MAAM,CAAC,UAAU,UAAU,OAAO,QAAQ,aAAa,CACxD,KAAI,MAAM,WAAW,IAAI,EAAE;GACvB,MAAM,aAAa,OAAO,MAAM,MAAM,EAAE;AACxC,OAAI,WACA,cAAa,IAAI,UAAU,WAAW;QAG1C,cAAa,IAAI,UAAU,MAAM;;CAK7C,MAAM,SAAS,aAAa,UAAU;AACtC,QAAO,GAAG,OAAO,SAAS,IAAI,WAAW,KAAK;;;;;;;;ACzGlD,SAAgB,eAAe,UAAkB,WAAkC;CAC/E,MAAM,eAAe,SAAS,MAAM,IAAI,CAAC,OAAO,QAAQ;AAExD,KAAI,aAAa,UAAU,UAAW,QAAO;AAE7C,QAAO,aAAa;;;;;;;AAQxB,eAAsB,sBAAsB,SAAkB,QAAwC;CAClG,MAAM,UAAU,QAAQ,QAAQ,IAAI,SAAS;AAC7C,KAAI,CAAC,QAAS,QAAO;AAGrB,QADoB,MAAM,OAAO,MAAM,QAAQ;;;;;;;;ACfnD,eAAsB,mBAAmB,SAAkB,QAAwC;AAC/F,QAAO,sBAAsB,SAAS,OAAO;;;;;;;;AASjD,SAAS,qBAAqB,gBAA+B,OAA4B;AACrF,KAAI,CAAC,eAAgB,QAAO;AAC5B,QAAO,MAAM,MAAM,SAAS,KAAK,UAAU,kBAAkB,KAAK,OAAO,eAAe,IAAI;;;;;;AAOhG,eAAsB,YAAY,SAAkB,UAA4C;CAC5F,MAAM,EAAE,OAAO,eAAe,qBAAqB,eAAe;CAElE,MAAM,aAAa,IAAI,IAAI,QAAQ,IAAI;CAEvC,MAAMC,YAAmE;EACrE,YAAY,QAAQ,QAAQ,eAAe,WAAW,UAAU,oBAAoB,oBAAoB,CAAC;EACzG,mBAAmB,QAAQ,QAAQ,WAAW,aAAa,IAAI,oBAAoB,kBAAkB,CAAC;EACtG,cAAc,QAAQ,QAAQ,QAAQ,QAAQ,IAAI,oBAAoB,aAAa,CAAC;EACpF,QAAQ,YAAY,mBAAmB,SAAS,WAAW;EAC9D;AAED,MAAK,MAAM,UAAU,oBAAoB,OAAO;EAE5C,MAAM,eAAe,qBADC,MAAM,UAAU,WAAW,EACQ,MAAM;AAC/D,MAAI,aAAc,QAAO;;CAI7B,MAAM,OAAO,qBAAqB,eAAe,MAAM;AAGvD,KAAI,CAAC,KACD,OAAM,IAAI,MAAM,gBAAgB,cAAc,aAAa;AAG/D,QAAO;;;;;;;;AC/CX,MAAaC,yBAAoD;CAC7D,OAAO;EAAC;EAAQ;EAAe;EAAU;EAAS;CAClD,qBAAqB;CACrB,mBAAmB;CACnB,cAAc;CACd,cAAc;CACd,QAAQ,CAAC,SAAS;CACrB;;;;AAKD,MAAaC,2BAAsD;CAC/D,OAAO;EAAC;EAAQ;EAAe;EAAU;EAAS;CAClD,qBAAqB;CACrB,mBAAmB;CACnB,cAAc;CACd,cAAc;CACd,QAAQ,CAAC,SAAS;CACrB;;;;;;;ACnBD,MAAa,iBAAiB;CAC1B,MAAM;CACN,UAAU;CACV,QAAQ,QAAQ,IAAI,aAAa;CACjC,UAAU;CACb;;;;;;;AAQD,SAAgB,sBAAsB,MAAsB;AACxD,QAAO,aAAa,MAAM,eAAe;;;;;;;AAQ7C,MAAa,qCAAqB,IAAI,SAA0B;;;;;;;ACpBhE,eAAsB,qBAAqB,SAAkB,QAAwC;AACjG,QAAO,sBAAsB,SAAS,OAAO;;;;;;;;;;;;;AAcjD,SAAS,uBAAuB,kBAA6C,SAAkC;AAC3G,KAAI,CAAC,iBAAkB,QAAO;AAC9B,QAAO,QAAQ,MAAM,WAAW,OAAO,UAAU,oBAAoB,OAAO,OAAO,iBAAiB,IAAI;;;;;;AAO5G,eAAsB,cAAc,SAAkB,UAA6B,MAA6B;CAC5G,MAAM,EAAE,eAAe,uBAAuB,iBAAiB;CAC/D,MAAM,EAAE,qBAAqB;CAE7B,IAAIC,SAAwB;CAC5B,MAAM,aAAa,IAAI,IAAI,QAAQ,IAAI;CAEvC,MAAMC,YAAmE;EACrE,YAAY,QAAQ,QAAQ,eAAe,WAAW,UAAU,sBAAsB,oBAAoB,CAAC;EAC3G,mBAAmB,QAAQ,QAAQ,WAAW,aAAa,IAAI,sBAAsB,kBAAkB,CAAC;EACxG,cAAc,QAAQ,QAAQ,QAAQ,QAAQ,IAAI,sBAAsB,aAAa,CAAC;EACtF,QAAQ,YAAY,qBAAqB,SAAS,aAAa;EAClE;AAED,MAAK,MAAM,UAAU,sBAAsB,OAAO;EAE9C,MAAM,iBAAiB,uBADC,MAAM,UAAU,WAAW,EACY,iBAAiB;AAChF,MAAI,eAAgB,QAAO;;AAI/B,KAAI,CAAC,OACD,UAAS,uBAAuB,eAAe,iBAAiB;AAIpE,KAAI,CAAC,OACD,OAAM,IAAI,MACN,kBAAkB,cAAc,uDAAuD,KAAK,GAAG,GAClG;AAGL,QAAO;;;;;ACxDX,MAAa,mBAAmBC,gBAAuC,KAAK;;;;;;;;;;;;;;;;;;;AAsB5E,SAAgB,oBAAoB,SAA0C;CAC1E,MAAM,YAAY,QAAQ,IAAI,iBAAiB;AAC/C,KAAI,CAAC,UAAW,QAAO;AACvB,QAAO;EACH,YAAY,UAAU;EACtB,cAAc,UAAU;EAC3B;;;;;;;;;;;;;AAcL,eAAe,iBACX,SACA,UACA,UACyE;CACzE,MAAM,YAAY,SAAS,oBAAoB,QAAQ,SAAS,SAAS;CACzE,MAAM,cAAc,SAAS,sBAAsB,QAAQ,SAAS,SAAS;AAG7E,KAAI,CAAC,aAAa,CAAC,YACf,QAAO;EAAE,qBAAqB;EAAO,uBAAuB;EAAO;CAKvE,MAAM,sBAAsB,QAAQ,QAAQ,IAAI,SAAS;CACzD,MAAM,CAAC,oBAAoB,wBAAwB,MAAM,QAAQ,IAAI,CACjE,SAAS,WAAW,MAAM,oBAAoB,EAC9C,SAAS,aAAa,MAAM,oBAAoB,CACnD,CAAC;CAIF,MAAM,qBAAqB,SAAS,QAAQ,gBAAgB,IAAI,EAAE;CAClE,MAAM,gCAAgC,mBAAmB,MAAM,WAC3D,OAAO,WAAW,GAAG,SAAS,WAAW,KAAK,GAAG,CACpD;CACD,MAAM,kCAAkC,mBAAmB,MAAM,WAC7D,OAAO,WAAW,GAAG,SAAS,aAAa,KAAK,GAAG,CACtD;AAKD,QAAO;EACH,qBAAqB,aAAa,CAAC,sBAAsB,CAAC;EAC1D,uBAAuB,eAAe,CAAC,wBAAwB,CAAC;EACnE;;;;;;;;;AAUL,SAAgB,0BAA0B,QAAuD;CAE7F,MAAMC,sBAAgE;EAClE,GAAG;EACH,GAAG,OAAO;EACb;CACD,MAAMC,wBAAoE;EACtE,GAAG;EACH,GAAG,OAAO;EACb;CAGD,MAAM,aAAa,sBAAsB,oBAAoB,aAAa;CAC1E,MAAM,eAAe,sBAAsB,sBAAsB,aAAa;CAE9E,MAAMC,WAA8B;EAChC,GAAG;EACH;EACA;EACA;EACA;EACH;CAED,MAAMC,sBAAoD,OACtD,EAAE,SAAS,WACX,SACoB;EACpB,MAAM,OAAO,MAAM,YAAY,SAAS,SAAS;EACjD,MAAM,SAAS,MAAM,cAAc,SAAS,UAAU,KAAK;AAG3D,UAAQ,IAAI,kBAAkB;GAC1B;GACA;GACA,YAAY,SAAS;GACrB,cAAc,SAAS;GAC1B,CAAC;AAIF,qBAAmB,IAAI,SAAS,OAAO,GAAG;EAE1C,MAAM,WAAW,MAAM,MAAM;EAG7B,MAAM,EAAE,qBAAqB,0BAA0B,MAAM,iBAAiB,SAAS,UAAU,SAAS;AAG1G,MAAI,CAAC,uBAAuB,CAAC,sBACzB,QAAO;EAGX,MAAM,CAAC,eAAe,mBAAmB,MAAM,QAAQ,IAAI,CACvD,sBAAsB,SAAS,WAAW,UAAU,KAAK,IAAI,EAAE,MAAM,KAAK,CAAC,GAAG,QAAQ,QAAQ,KAAK,EACnG,wBAAwB,SAAS,aAAa,UAAU,OAAO,IAAI,EAAE,MAAM,KAAK,CAAC,GAAG,QAAQ,QAAQ,KAAK,CAC5G,CAAC;AAEF,MAAI,cAAe,UAAS,QAAQ,OAAO,cAAc,cAAc;AACvE,MAAI,gBAAiB,UAAS,QAAQ,OAAO,cAAc,gBAAgB;AAE3E,SAAO;;AAGX,QAAO"}
1
+ {"version":3,"file":"multi-site.js","names":["result: Record<string, string>","resolvers: Record<DetectionMethod, () => Promise<string | null>>","DEFAULT_SITE_DETECTION: Required<DetectionConfig>","DEFAULT_LOCALE_DETECTION: Required<DetectionConfig>","locale: Locale | null","resolvers: Record<DetectionMethod, () => Promise<string | null>>","createContext","siteDetectionConfig: MultiSiteSettings['siteDetectionConfig']","localeDetectionConfig: MultiSiteSettings['localeDetectionConfig']","settings: MultiSiteSettings","multiSiteMiddleware: MiddlewareFunction<Response>"],"sources":["../src/multi-site/site-context.tsx","../src/multi-site/build-url.ts","../src/multi-site/utils.ts","../src/multi-site/site-detection.ts","../src/multi-site/configs.ts","../src/multi-site/cookies.ts","../src/multi-site/locale-detection.ts","../src/multi-site/middleware.ts"],"sourcesContent":["/**\n * Copyright 2026 Salesforce, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport { createContext, useContext, type PropsWithChildren } from 'react';\nimport type { Site } from './types';\n\nconst SiteContext = createContext<Site | undefined>(undefined);\n\n/**\n * Provides the current site to the component tree.\n * Follows the same pattern as CurrencyProvider.\n *\n * Mounted in the template (e.g., app-wrapper.tsx or root.tsx) with the resolved\n * site value from the loader/middleware.\n */\nexport function SiteProvider({ value, children }: PropsWithChildren<{ value: Site }>) {\n return <SiteContext.Provider value={value}>{children}</SiteContext.Provider>;\n}\n\n/**\n * React hook to get the current site.\n * Returns undefined when no SiteProvider is mounted.\n */\n// eslint-disable-next-line react-refresh/only-export-components\nexport function useSite(): Site | undefined {\n return useContext(SiteContext);\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 { Url } from '../config/types';\n\n/**\n * Parses search config string into key-value pairs, preserving ':param' placeholders.\n * '?lng=:localeId&site=:siteId' → { lng: ':localeId', site: ':siteId' }\n */\nexport function parseSearchConfig(search: string): Record<string, string> {\n const searchParams = new URLSearchParams(search);\n const result: Record<string, string> = {};\n for (const [key, value] of searchParams) {\n result[key] = value;\n }\n return result;\n}\n\n/**\n * Extracts parameter names from a prefix string.\n * '/:siteId/:localeId' → ['siteId', 'localeId']\n */\nexport function extractPrefixParams(prefix: string): string[] {\n const matches = prefix.match(/:(\\w+)/g);\n return matches ? matches.map((m) => m.slice(1)) : [];\n}\n\n/**\n * Splits a URL string into its component parts.\n * '/product/123?color=red#details' → { pathname: '/product/123', search: 'color=red', hash: '#details' }\n */\nexport function decomposeUrl(url: string): { pathname: string; search: string; hash: string } {\n const hashIdx = url.indexOf('#');\n const hash = hashIdx >= 0 ? url.slice(hashIdx) : '';\n const withoutHash = hashIdx >= 0 ? url.slice(0, hashIdx) : url;\n const searchIdx = withoutHash.indexOf('?');\n const search = searchIdx >= 0 ? withoutHash.slice(searchIdx + 1) : '';\n const pathname = searchIdx >= 0 ? withoutHash.slice(0, searchIdx) : withoutHash;\n return { pathname, search, hash };\n}\n\n/**\n * Resolves a prefix template by replacing parameter placeholders with values.\n * ('/:siteId/:localeId', { siteId: 'global', localeId: 'en-GB' }) → '/global/en-GB'\n */\nexport function resolvePrefix(prefix: string, params: Record<string, string>): string {\n let resolved = prefix;\n for (const paramName of extractPrefixParams(prefix)) {\n const value = params[paramName];\n if (value) {\n resolved = resolved.replace(`:${paramName}`, value);\n }\n }\n return resolved;\n}\n\n/**\n * Strips the URL prefix segments from a pathname based on a prefix pattern.\n * Since all routes are configured with the prefix baked in, segment counting is sufficient.\n *\n * @param pathname - Full pathname (e.g. '/global/en-GB/checkout')\n * @param prefixPattern - URL prefix pattern from config (e.g. '/:siteId/:localeId')\n * @returns Pathname with prefix stripped (e.g. '/checkout'), or original if\n * the pathname has fewer segments than the prefix\n *\n * @example\n * stripPathPrefix('/global/en-GB/checkout', '/:siteId/:localeId') // → '/checkout'\n * stripPathPrefix('/checkout', '/:siteId/:localeId') // → '/checkout' (fewer segments → unchanged)\n * stripPathPrefix('/checkout', '') // → '/checkout' (no prefix configured)\n * stripPathPrefix('/', '/:siteId/:localeId') // → '/'\n */\nexport function stripPathPrefix(pathname: string, prefixPattern: string): string {\n if (!prefixPattern) return pathname;\n\n const prefixSegmentCount = prefixPattern.split('/').filter(Boolean).length;\n const pathSegments = pathname.split('/').filter(Boolean);\n\n if (pathSegments.length <= prefixSegmentCount) {\n return pathSegments.length === prefixSegmentCount ? '/' : pathname;\n }\n\n return `/${pathSegments.slice(prefixSegmentCount).join('/')}`;\n}\n\n/**\n * Sanitize a resolved prefix from a pathname if present.\n * sanitizePrefix('/global/en-GB/product/123', '/global/en-GB') → '/product/123'\n * sanitizePrefix('/product/123', '/global/en-GB') → '/product/123' (no-op)\n */\nexport function sanitizePrefix(pathname: string, pathPrefix: string): string {\n if (!pathPrefix) return pathname;\n if (pathname === pathPrefix) return '';\n if (pathname.startsWith(`${pathPrefix}/`)) return pathname.slice(pathPrefix.length);\n return pathname;\n}\n\n/**\n * Builds a fully-qualified URL with multi-site prefix and search params.\n *\n * Only keys defined in urlConfig.search are set by multi-site. Any other query params\n * already present on the `to` URL (including duplicate keys) are preserved as-is.\n * e.g. to='/api/search?refine=color:blue&refine=size:M', search='?lng=:localeId'\n * → '/api/search?refine=color:blue&refine=size:M&lng=en-GB'\n *\n * @example\n * buildUrl({ to: '/product/123', urlConfig: { prefix: '/:siteId', search: '?lng=:localeId' }, params: { siteId: 'global', localeId: 'en-GB' } })\n * // → '/global/product/123?lng=en-GB'\n */\nexport function buildUrl({\n to,\n urlConfig,\n params,\n}: {\n to: string;\n urlConfig?: Url;\n params: Record<string, string>;\n}): string {\n if (!urlConfig) return to;\n if (!to || to === '#' || to.startsWith('http') || to.startsWith('//')) return to;\n\n const { pathname, search: existingSearch, hash } = decomposeUrl(to);\n\n const pathPrefix = urlConfig.prefix && urlConfig.prefix !== '/' ? resolvePrefix(urlConfig.prefix, params) : '';\n // sanitize prefix to make sure there is no prefix duplication at any case\n const path = pathPrefix ? `${pathPrefix}${sanitizePrefix(pathname, pathPrefix)}` : pathname;\n\n const searchParams = new URLSearchParams(existingSearch);\n if (urlConfig.search) {\n const searchConfig = parseSearchConfig(urlConfig.search);\n for (const [queryKey, value] of Object.entries(searchConfig)) {\n if (value.startsWith(':')) {\n const paramValue = params[value.slice(1)];\n if (paramValue) {\n searchParams.set(queryKey, paramValue);\n }\n } else {\n searchParams.set(queryKey, value);\n }\n }\n }\n\n const search = searchParams.toString();\n return `${path}${search ? `?${search}` : ''}${hash}`;\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\nimport type { Cookie } from 'react-router';\n\n/**\n * Extract a string value from the URL path segment at the given index.\n */\nexport function lookupFromPath(pathname: string, pathIndex: number): string | null {\n const pathSegments = pathname.split('/').filter(Boolean);\n\n if (pathSegments.length <= pathIndex) return null;\n\n return pathSegments[pathIndex];\n}\n\n/**\n * Detect a string value from cookie using the given cookie parser.\n *\n * Returns a promise that resolves to the cookie value.\n */\nexport async function readCookieFromRequest(request: Request, cookie: Cookie): Promise<string | null> {\n const cookies = request.headers.get('Cookie');\n if (!cookies) return null;\n\n const cookieValue = await cookie.parse(cookies);\n return cookieValue;\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\nimport type { Cookie } from 'react-router';\nimport type { Site, MultiSiteSettings, DetectionMethod } from './types';\nimport { readCookieFromRequest, lookupFromPath } from './utils';\n\n/**\n * Detect site reference from cookie.\n */\nexport async function readSiteFromCookie(request: Request, cookie: Cookie): Promise<string | null> {\n return readCookieFromRequest(request, cookie);\n}\n\n/**\n * Get site object using the site id or alias\n * 1. Check siteIdentifier against each site's alias; if matched, return that site.\n * 2. Else check against each site's id; if matched, return that site.\n * 3. If no match, return null.\n */\nfunction getSiteFromIdOrAlias(siteIdentifier: string | null, sites: Site[]): Site | null {\n if (!siteIdentifier) return null;\n return sites.find((site) => site.alias === siteIdentifier || site.id === siteIdentifier) ?? null;\n}\n\n/**\n * Resolve site using the configured detection order.\n * Returns the first valid site from the first source that yields a valid value.\n */\nexport async function resolveSite(request: Request, settings: MultiSiteSettings): Promise<Site> {\n const { sites, defaultSiteId, siteDetectionConfig, siteCookie } = settings;\n\n const requestUrl = new URL(request.url);\n\n // When a base path is configured (e.g., '/shop'), we need to skip its path segments.\n // React Router handles the base path internally for hooks like useParams or useLocation,\n // but it does not strip it from request.url. The offset is calculated dynamically from\n // the number of segments in the base path as future-proof in case we support multi-segment\n // base paths in the future.\n const basePathOffset = process.env.MRT_ENV_BASE_PATH\n ? process.env.MRT_ENV_BASE_PATH.split('/').filter(Boolean).length\n : 0;\n\n const resolvers: Record<DetectionMethod, () => Promise<string | null>> = {\n path: () =>\n Promise.resolve(\n lookupFromPath(requestUrl.pathname, siteDetectionConfig.lookupFromPathIndex + basePathOffset)\n ),\n querystring: () => Promise.resolve(requestUrl.searchParams.get(siteDetectionConfig.lookupQuerystring)),\n header: () => Promise.resolve(request.headers.get(siteDetectionConfig.lookupHeader)),\n cookie: async () => readSiteFromCookie(request, siteCookie),\n };\n\n for (const method of siteDetectionConfig.order) {\n const siteIdOrAlias = await resolvers[method]?.();\n const resolvedSite = getSiteFromIdOrAlias(siteIdOrAlias, sites);\n if (resolvedSite) return resolvedSite;\n }\n\n // If no site id was found, use the default site id\n const site = getSiteFromIdOrAlias(defaultSiteId, sites);\n\n // If default site id is invalid, throw an error\n if (!site) {\n throw new Error(`Default site ${defaultSiteId} not found.`);\n }\n\n return site;\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\nimport type { DetectionConfig } from './types';\n\n/**\n * Default site detection configuration\n */\nexport const DEFAULT_SITE_DETECTION: Required<DetectionConfig> = {\n order: ['path', 'querystring', 'cookie', 'header'],\n lookupFromPathIndex: 0,\n lookupQuerystring: 'site',\n lookupCookie: 'site_id',\n lookupHeader: 'X-Site-Id',\n caches: ['cookie'],\n};\n\n/**\n * Default locale detection configuration\n */\nexport const DEFAULT_LOCALE_DETECTION: Required<DetectionConfig> = {\n order: ['path', 'querystring', 'cookie', 'header'],\n lookupFromPathIndex: 1,\n lookupQuerystring: 'lng',\n lookupCookie: 'lng',\n lookupHeader: 'Accept-Language',\n caches: ['cookie'],\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\nimport { createCookie, type Cookie } from 'react-router';\n\n/**\n * Cookie options for multi-site cookies\n */\nexport const COOKIE_OPTIONS = {\n path: '/',\n sameSite: 'lax' as const,\n secure: process.env.NODE_ENV === 'production',\n httpOnly: true,\n};\n\n/**\n * Creates a cookie instance with the given name.\n *\n * @param name - Cookie name\n * @returns Cookie instance configured with multi-site options\n */\nexport function createMultiSiteCookie(name: string): Cookie {\n return createCookie(name, COOKIE_OPTIONS);\n}\n\n/**\n * WeakMap to pass resolved locale from multi-site middleware to i18next's findLocale.\n * WeakMap allows garbage collection when requests are done.\n * This is necessary because findLocale() only receives the Request object, not the router context.\n */\nexport const requestToLocaleMap = new WeakMap<Request, string>();\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\nimport type { Cookie } from 'react-router';\nimport type { DetectionMethod, Locale, MultiSiteSettings, Site } from './types';\nimport { readCookieFromRequest, lookupFromPath } from './utils';\n\n/**\n * Read locale from cookie.\n */\nexport async function readLocaleFromCookie(request: Request, cookie: Cookie): Promise<string | null> {\n return readCookieFromRequest(request, cookie);\n}\n\n/**\n * Get locale object using the locale id or alias.\n * 1. Check localeIdOrAlias against each locale's alias; if matched, return that locale.\n * 2. Else check against each locale's id; if matched, return that locale.\n * 3. If no match, return null (caller should use defaultLocale).\n *\n * @param localeIdentifier - The locale id or alias to get the locale from. Null is allowed because this may come from\n * extrenal sources such as cookies, headers, or query parameters.\n * @param locales - The list of locales to search through.\n * @returns The locale object if found, otherwise null.\n */\nfunction getLocaleFromIdOrAlias(localeIdentifier: string | undefined | null, locales: Locale[]): Locale | null {\n if (!localeIdentifier) return null;\n return locales.find((locale) => locale.alias === localeIdentifier || locale.id === localeIdentifier) ?? null;\n}\n\n/**\n * Resolve locale using the configured detection order.\n * Returns the first valid locale from the first source that yields a valid value.\n */\nexport async function resolveLocale(request: Request, settings: MultiSiteSettings, site: Site): Promise<Locale> {\n const { defaultLocale, localeDetectionConfig, localeCookie } = settings;\n const { supportedLocales } = site;\n\n let locale: Locale | null = null;\n const requestUrl = new URL(request.url);\n\n // When a base path is configured (e.g., '/shop'), we need to skip its path segments.\n // React Router handles the base path internally for hooks like useParams or useLocation,\n // but it does not strip it from request.url. The offset is calculated dynamically from\n // the number of segments in the base path as future-proof in case we support multi-segment\n // base paths in the future.\n const basePathOffset = process.env.MRT_ENV_BASE_PATH\n ? process.env.MRT_ENV_BASE_PATH.split('/').filter(Boolean).length\n : 0;\n\n const resolvers: Record<DetectionMethod, () => Promise<string | null>> = {\n path: () =>\n Promise.resolve(\n lookupFromPath(requestUrl.pathname, localeDetectionConfig.lookupFromPathIndex + basePathOffset)\n ),\n querystring: () => Promise.resolve(requestUrl.searchParams.get(localeDetectionConfig.lookupQuerystring)),\n header: () => Promise.resolve(request.headers.get(localeDetectionConfig.lookupHeader)),\n cookie: async () => readLocaleFromCookie(request, localeCookie),\n };\n\n for (const method of localeDetectionConfig.order) {\n const localeIdOrAlias = await resolvers[method]?.();\n const resolvedLocale = getLocaleFromIdOrAlias(localeIdOrAlias, supportedLocales);\n if (resolvedLocale) return resolvedLocale;\n }\n\n // If no locale was found, use the default locale\n if (!locale) {\n locale = getLocaleFromIdOrAlias(defaultLocale, supportedLocales);\n }\n\n // If default locale is invalid, throw an error\n if (!locale) {\n throw new Error(\n `Default locale ${defaultLocale} not found in the list of supported locales for site ${site.id}.`\n );\n }\n\n return locale;\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\nimport { createContext, type MiddlewareFunction, type RouterContextProvider } from 'react-router';\nimport { resolveSite } from './site-detection';\nimport type { MultiSiteConfig, MultiSiteContext, MultiSiteSettings, Site, Locale } from './types';\nimport { DEFAULT_SITE_DETECTION, DEFAULT_LOCALE_DETECTION } from './configs';\nimport { createMultiSiteCookie, requestToLocaleMap } from './cookies';\nimport { resolveLocale } from './locale-detection';\n\nexport const multiSiteContext = createContext<MultiSiteContext | null>(null);\n\ntype MiddlewareArgs = { request: Request; context: Readonly<RouterContextProvider> };\n\n/**\n * Helper function to get multi-site cookies from router context.\n * Useful in server actions and loaders that need to read/set cookies.\n *\n * @param context - Router context provider\n * @returns Object with siteCookie and localeCookie instances, or null if context not set\n *\n * @example\n * ```typescript\n * export const action: ActionFunction = async ({ request, context }) => {\n * const cookies = getMultiSiteCookies(context);\n * if (cookies) {\n * const cookieHeader = await cookies.localeCookie.serialize(locale);\n * // ... use cookieHeader\n * }\n * };\n * ```\n */\nexport function getMultiSiteCookies(context: Readonly<RouterContextProvider>) {\n const multiSite = context.get(multiSiteContext);\n if (!multiSite) return null;\n return {\n siteCookie: multiSite.siteCookie,\n localeCookie: multiSite.localeCookie,\n };\n}\n\n/**\n * Helper function to determine if cookies should be set based on:\n * 1. Whether caching is enabled for each cookie type\n * 2. Whether the resolved value differs from the existing cookie\n * 3. Whether cookies were already set by actions/loaders in the response\n *\n * @param request - Incoming request\n * @param response - Response from next()\n * @param settings - Multi-site settings with cookie instances and detection config\n * @param site - Resolved site for this request\n * @param locale - Resolved locale for this request\n * @returns Object with shouldSetSiteCookie and shouldSetLocaleCookie booleans\n */\nasync function shouldSetCookies(\n request: Request,\n response: Response,\n settings: MultiSiteSettings,\n site: Site,\n locale: Locale\n): Promise<{ shouldSetSiteCookie: boolean; shouldSetLocaleCookie: boolean }> {\n const cacheSite = settings.siteDetectionConfig.caches?.includes('cookie');\n const cacheLocale = settings.localeDetectionConfig.caches?.includes('cookie');\n\n // Early return if no cookie caching is enabled\n if (!cacheSite && !cacheLocale) {\n return { shouldSetSiteCookie: false, shouldSetLocaleCookie: false };\n }\n\n // Check if cookies were already set by actions/loaders in the response.\n // If they were, we don't want to override them.\n const responseSetCookies = response.headers.getSetCookie?.() || [];\n const isSettingSiteCookieInResponse = responseSetCookies.some((cookie) =>\n cookie.startsWith(`${settings.siteCookie.name}=`)\n );\n const isSettingLocaleCookieInResponse = responseSetCookies.some((cookie) =>\n cookie.startsWith(`${settings.localeCookie.name}=`)\n );\n\n const requestCookieHeader = request.headers.get('Cookie');\n const [existingSiteCookie, existingLocaleCookie] = await Promise.all([\n settings.siteCookie.parse(requestCookieHeader),\n settings.localeCookie.parse(requestCookieHeader),\n ]);\n\n // Set cookie if: doesn't exist yet OR resolved value differs from existing.\n // Skip if an action/loader already set it in the response.\n return {\n shouldSetSiteCookie: cacheSite && !isSettingSiteCookieInResponse && existingSiteCookie !== site.id,\n shouldSetLocaleCookie: cacheLocale && !isSettingLocaleCookieInResponse && existingLocaleCookie !== locale.id,\n };\n}\n\n/**\n * Creates a multi-site middleware that resolves the current site from\n * the request (path, cookie, header, query, or default) and stores the\n * result in the router context.\n *\n * Does not import or read from app config context; the consumer supplies config.\n */\nexport function createMultiSiteMiddleware(config: MultiSiteConfig): MiddlewareFunction<Response> {\n // Merge config with defaults so every detection option has a value\n const siteDetectionConfig: MultiSiteSettings['siteDetectionConfig'] = {\n ...DEFAULT_SITE_DETECTION,\n ...config.siteDetectionConfig,\n };\n const localeDetectionConfig: MultiSiteSettings['localeDetectionConfig'] = {\n ...DEFAULT_LOCALE_DETECTION,\n ...config.localeDetectionConfig,\n };\n\n // Create cookies based on configured names\n const siteCookie = createMultiSiteCookie(siteDetectionConfig.lookupCookie);\n const localeCookie = createMultiSiteCookie(localeDetectionConfig.lookupCookie);\n\n const settings: MultiSiteSettings = {\n ...config,\n siteDetectionConfig,\n localeDetectionConfig,\n siteCookie,\n localeCookie,\n };\n\n const multiSiteMiddleware: MiddlewareFunction<Response> = async (\n { request, context }: MiddlewareArgs,\n next: () => Promise<Response>\n ): Promise<Response> => {\n const site = await resolveSite(request, settings);\n const locale = await resolveLocale(request, settings, site);\n\n // Store full Site, Locale, and Cookie objects in context for downstream middlewares (currency, loaders, etc.)\n context.set(multiSiteContext, {\n site,\n locale,\n siteCookie: settings.siteCookie,\n localeCookie: settings.localeCookie,\n });\n\n // Store locale in a WeakMap so i18next's findLocale can access it\n // This is necessary because findLocale only receives Request and cannot access the router context\n requestToLocaleMap.set(request, locale.id);\n\n const response = await next();\n\n // Determine if cookies should be set\n const { shouldSetSiteCookie, shouldSetLocaleCookie } = await shouldSetCookies(\n request,\n response,\n settings,\n site,\n locale\n );\n\n // Early return if no cookies need to be set\n if (!shouldSetSiteCookie && !shouldSetLocaleCookie) {\n return response;\n }\n\n const [siteSetCookie, localeSetCookie] = await Promise.all([\n shouldSetSiteCookie ? settings.siteCookie.serialize(site.id, { path: '/' }) : Promise.resolve(null),\n shouldSetLocaleCookie ? settings.localeCookie.serialize(locale.id, { path: '/' }) : Promise.resolve(null),\n ]);\n\n if (siteSetCookie) response.headers.append('Set-Cookie', siteSetCookie);\n if (localeSetCookie) response.headers.append('Set-Cookie', localeSetCookie);\n\n return response;\n };\n\n return multiSiteMiddleware;\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"routing.d.ts","names":[],"sources":["../src/routing/flat-routes.ts","../src/routing/merge-routes.ts"],"sourcesContent":[],"mappings":";;;;;;AAwEA;;;;ACTA;;;;;;;iBDSsB,UAAA;;;IAGlB,QAAQ;;;;;;;;;;;;;;;;;;iBCZI,WAAA,SACJ,qCACS"}
1
+ {"version":3,"file":"routing.d.ts","names":[],"sources":["../src/routing/flat-routes.ts","../src/routing/merge-routes.ts"],"sourcesContent":[],"mappings":";;;;;;AAyEA;;;;ACVA;;;;;;;iBDUsB,UAAA;;;IAGlB,QAAQ;;;;;;;;;;;;;;;;;;iBCbI,WAAA,SACJ,qCACS"}
package/dist/routing.js CHANGED
@@ -1,8 +1,8 @@
1
+ import { t as loadConfig } from "./load-config.js";
1
2
  import { t as applyUrlConfig } from "./apply-url-config.js";
3
+ import path from "node:path";
2
4
  import { flatRoutes as flatRoutes$1 } from "@react-router/fs-routes";
3
5
  import fs from "node:fs/promises";
4
- import path from "node:path";
5
- import fs$1 from "node:fs";
6
6
 
7
7
  //#region src/routing/merge-routes.ts
8
8
  /**
@@ -76,40 +76,6 @@ function mergeRoutes(routes, extensionRoutes, extensionIdPrefix) {
76
76
  }
77
77
  }
78
78
 
79
- //#endregion
80
- //#region src/config/load-config.ts
81
- /**
82
- * Dynamically imports `config.server.ts` from the project root (CWD) and returns
83
- * the `app` configuration object.
84
- *
85
- * Uses jiti to transpile TypeScript on the fly, which works regardless of whether
86
- * the caller runs under vite-node, a plain Node process, or any other runtime.
87
- * This avoids the fragile assumption that vite-node will intercept dynamic imports
88
- * from pre-compiled npm packages (it won't — Vite externalizes node_modules).
89
- *
90
- * - If the config file is missing, warns and returns an empty config.
91
- * - If the config file exists but fails to import, throws with the original error as cause.
92
- *
93
- * @returns The `app` configuration object, or an empty object if not available.
94
- */
95
- async function loadConfig() {
96
- const configPath = path.resolve(process.cwd(), "config.server.ts");
97
- if (!fs$1.existsSync(configPath)) {
98
- console.warn(`[storefront-next-runtime] config.server.ts not found at ${configPath}. Returning empty config.`);
99
- return {};
100
- }
101
- try {
102
- const { createJiti } = await import("jiti");
103
- const mod = await createJiti(import.meta.url, {
104
- fsCache: false,
105
- interopDefault: true
106
- }).import(configPath);
107
- return (mod.default ?? mod)?.app ?? {};
108
- } catch (error) {
109
- throw new Error(`[storefront-next-runtime] Found config.server.ts at ${configPath} but failed to import it.`, { cause: error });
110
- }
111
- }
112
-
113
79
  //#endregion
114
80
  //#region src/routing/flat-routes.ts
115
81
  const APP_SRC_DIR = "src";
@@ -154,7 +120,8 @@ async function flatRoutes(options) {
154
120
  rootDirectory
155
121
  });
156
122
  await discoverExtensionRoutes(ignoredRouteFiles, routes);
157
- const { url: urlConfig } = await loadConfig();
123
+ const { app } = await loadConfig();
124
+ const urlConfig = app?.url;
158
125
  if (urlConfig?.prefix) {
159
126
  try {
160
127
  await fs.access(path.join(".", APP_SRC_DIR, APP_WRAPPER_FILE));
@@ -1 +1 @@
1
- {"version":3,"file":"routing.js","names":["path","fs","_flatRoutes"],"sources":["../src/routing/merge-routes.ts","../src/config/load-config.ts","../src/routing/flat-routes.ts"],"sourcesContent":["/**\n * Copyright 2026 Salesforce, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport { type RouteConfigEntry } from '@react-router/dev/routes';\n\n/**\n * Find the nearest route by its ID in the route tree\n * @param routes - The route subtree to search\n * @param layoutId - The route ID to find (e.g., \"routes/_app\" or \"routes/_app.account\")\n * @param rootPath - The full route path from the root to the current route (default: '')\n * @returns An object with routes array, routeIndex, and path, or null if not found. Returns exact match if found, otherwise returns route where route.id is a prefix of layoutId\n */\nfunction findNearestRoute(\n routes: RouteConfigEntry[],\n layoutId: string,\n rootPath: string = ''\n): { routes: RouteConfigEntry[]; routeIndex: number; path: string } | null {\n for (let i = 0; i < routes.length; i++) {\n const route = routes[i];\n const path = route.path ? `${rootPath}/${route.path}` : rootPath;\n if (route.id === layoutId) {\n return { routes, routeIndex: i, path };\n }\n if (route.children) {\n const found = findNearestRoute(route.children, layoutId, path);\n if (found) {\n return found;\n }\n }\n // Check if route.id is a prefix of layoutId, indicating a nested route\n if (route.id && layoutId.startsWith(route.id)) {\n return { routes, routeIndex: i, path };\n }\n }\n return null;\n}\n\n/**\n * Merges extension routes into the main routes array, handling route nesting.\n * Routes without IDs are added directly to the routes array. Routes with IDs are processed\n * to remove the extension prefix and are either:\n * - Added as children of existing routes (if a nearest route is found via prefix matching)\n * - Replace existing routes (if an exact match is found)\n * - Added directly to the routes array (if no matching route exists)\n *\n * When adding as a child, the parent route's path is clipped from the child route's path.\n *\n * @param routes - The main routes array to merge into (mutated in place)\n * @param extensionRoutes - The extension routes to merge\n * @param extensionIdPrefix - The prefix to remove from extension route IDs (e.g., \"extensions/store-locator/\")\n */\nexport function mergeRoutes(\n routes: RouteConfigEntry[],\n extensionRoutes: RouteConfigEntry[],\n extensionIdPrefix: string\n): void {\n for (const route of extensionRoutes) {\n if (!route.id) {\n routes.unshift(route);\n continue;\n }\n const routeId = route.id.replace(extensionIdPrefix, '');\n const nearestRouteResult = findNearestRoute(routes, routeId);\n if (nearestRouteResult) {\n const nearestRoute = nearestRouteResult.routes[nearestRouteResult.routeIndex];\n if (nearestRoute.id === routeId) {\n // Replacing an existing route, we assume we can just swap out the implementation\n nearestRouteResult.routes[nearestRouteResult.routeIndex].file = route.file;\n } else {\n // This is a new child of an existing route, insert it at the beginning of the children array\n // and clip out the parent path from the route path\n let path = route.path?.slice(nearestRouteResult.path.length);\n if (path?.startsWith('/')) {\n path = path.slice(1);\n }\n path = path ? path : undefined;\n if (!nearestRoute.children) {\n nearestRoute.children = [];\n }\n nearestRoute.children.unshift({\n ...route,\n id: routeId,\n path,\n });\n }\n } else {\n // This is a new route, insert it at the beginning of the routes array\n routes.unshift({\n ...route,\n id: routeId,\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 */\nimport fs from 'node:fs';\nimport path from 'node:path';\n\n/**\n * Dynamically imports `config.server.ts` from the project root (CWD) and returns\n * the `app` configuration object.\n *\n * Uses jiti to transpile TypeScript on the fly, which works regardless of whether\n * the caller runs under vite-node, a plain Node process, or any other runtime.\n * This avoids the fragile assumption that vite-node will intercept dynamic imports\n * from pre-compiled npm packages (it won't — Vite externalizes node_modules).\n *\n * - If the config file is missing, warns and returns an empty config.\n * - If the config file exists but fails to import, throws with the original error as cause.\n *\n * @returns The `app` configuration object, or an empty object if not available.\n */\n// TODO: add a proper type when config schema is moved to runtime from the template\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport async function loadConfig(): Promise<Record<string, any>> {\n const configPath = path.resolve(process.cwd(), 'config.server.ts');\n\n if (!fs.existsSync(configPath)) {\n console.warn(\n `[storefront-next-runtime] config.server.ts not found at ${configPath}. ` + `Returning empty config.`\n );\n return {};\n }\n\n try {\n const { createJiti } = await import('jiti');\n\n const jiti = createJiti(import.meta.url, {\n fsCache: false,\n interopDefault: true,\n });\n\n const mod = await jiti.import(configPath);\n const config = (mod as Record<string, unknown>).default ?? mod;\n return (config as Record<string, unknown>)?.app ?? {};\n } catch (error) {\n throw new Error(`[storefront-next-runtime] Found config.server.ts at ${configPath} but failed to import it.`, {\n cause: error,\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 */\nimport { flatRoutes as _flatRoutes } from '@react-router/fs-routes';\nimport type { RouteConfigEntry } from '@react-router/dev/routes';\nimport fs from 'node:fs/promises';\nimport path from 'node:path';\nimport { mergeRoutes } from './merge-routes';\nimport { applyUrlConfig } from '../multi-site/apply-url-config';\nimport { loadConfig } from '../config/load-config';\n\nconst APP_SRC_DIR = 'src';\nconst EXTENSIONS_DIR = 'extensions';\n// This file must live at the root of `appDirectory` (src/app-wrapper.tsx) and must NOT\n// be moved into a subdirectory. React Router's typegen resolves route module types using\n// paths relative to `appDirectory` — placing it elsewhere breaks generated type references.\nconst APP_WRAPPER_FILE = 'app-wrapper.tsx';\n\n/**\n * Scans `src/extensions/` for extension route directories and merges any discovered\n * routes into the base route tree. Mutates `routes` in place via `mergeRoutes`.\n */\nasync function discoverExtensionRoutes(ignoredRouteFiles: string[], routes: RouteConfigEntry[]): Promise<void> {\n const extensionsDir = path.join('.', APP_SRC_DIR, EXTENSIONS_DIR);\n\n // Sort to ensure deterministic route order across platforms (readdir order is filesystem-dependent)\n const extensions = await fs.readdir(extensionsDir).then(\n (entries) => entries.sort(),\n () => []\n );\n for (const ext of extensions) {\n // React Router rootDirectory uses forward slashes regardless of OS\n const routesDir = `${EXTENSIONS_DIR}/${ext}/routes`;\n const routesDirFull = path.join('.', APP_SRC_DIR, EXTENSIONS_DIR, ext, 'routes');\n try {\n await fs.access(routesDirFull);\n const extensionRoutes = await _flatRoutes({\n ignoredRouteFiles,\n rootDirectory: routesDir,\n });\n mergeRoutes(routes, extensionRoutes, `${EXTENSIONS_DIR}/${ext}/`);\n } catch {\n // Extension has no routes directory — skip\n }\n }\n}\n\n/**\n * Discovers all file-based routes, merges extension routes, and applies multi-site\n * URL configuration if defined in the project's `config.server.ts`.\n *\n * 1. Discover routes from the filesystem using React Router's `flatRoutes`.\n * 2. Scans `src/extensions/` for extension routes and merges them into the route tree.\n * 3. Load `config.server.ts` from the project root and, if `app.url` is configured,\n * wraps routes under the URL prefix (e.g. `/:siteId/:localeId`).\n *\n * @param options.ignoredRouteFiles - Glob patterns for files to ignore. Defaults to test files.\n * @param options.rootDirectory - Root directory for route discovery, relative to appDirectory.\n * @returns The final route config entries for React Router.\n */\nexport async function flatRoutes(options?: {\n ignoredRouteFiles?: string[];\n rootDirectory?: string;\n}): Promise<RouteConfigEntry[]> {\n const { ignoredRouteFiles = ['**/*.test.{ts,tsx}'], rootDirectory } = options ?? {};\n\n // 1. Discover base routes from filesystem\n const routes = await _flatRoutes({ ignoredRouteFiles, rootDirectory });\n\n // 2. Discover and merge extension routes\n await discoverExtensionRoutes(ignoredRouteFiles, routes);\n\n // 3. Try to load URL config from template's config file\n const { url: urlConfig } = await loadConfig();\n if (urlConfig?.prefix) {\n try {\n await fs.access(path.join('.', APP_SRC_DIR, APP_WRAPPER_FILE));\n } catch {\n throw new Error(\n `[storefront-next-runtime] URL prefix \"${urlConfig.prefix}\" is configured but ` +\n `\"${APP_SRC_DIR}/${APP_WRAPPER_FILE}\" does not exist. ` +\n `Create this file with: export { default } from '@salesforce/storefront-next-runtime/routing/app-wrapper';`\n );\n }\n\n return applyUrlConfig({\n routes,\n urlConfig,\n wrapperFile: APP_WRAPPER_FILE,\n });\n }\n\n return routes;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"routing.js","names":["path","_flatRoutes"],"sources":["../src/routing/merge-routes.ts","../src/routing/flat-routes.ts"],"sourcesContent":["/**\n * Copyright 2026 Salesforce, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport { type RouteConfigEntry } from '@react-router/dev/routes';\n\n/**\n * Find the nearest route by its ID in the route tree\n * @param routes - The route subtree to search\n * @param layoutId - The route ID to find (e.g., \"routes/_app\" or \"routes/_app.account\")\n * @param rootPath - The full route path from the root to the current route (default: '')\n * @returns An object with routes array, routeIndex, and path, or null if not found. Returns exact match if found, otherwise returns route where route.id is a prefix of layoutId\n */\nfunction findNearestRoute(\n routes: RouteConfigEntry[],\n layoutId: string,\n rootPath: string = ''\n): { routes: RouteConfigEntry[]; routeIndex: number; path: string } | null {\n for (let i = 0; i < routes.length; i++) {\n const route = routes[i];\n const path = route.path ? `${rootPath}/${route.path}` : rootPath;\n if (route.id === layoutId) {\n return { routes, routeIndex: i, path };\n }\n if (route.children) {\n const found = findNearestRoute(route.children, layoutId, path);\n if (found) {\n return found;\n }\n }\n // Check if route.id is a prefix of layoutId, indicating a nested route\n if (route.id && layoutId.startsWith(route.id)) {\n return { routes, routeIndex: i, path };\n }\n }\n return null;\n}\n\n/**\n * Merges extension routes into the main routes array, handling route nesting.\n * Routes without IDs are added directly to the routes array. Routes with IDs are processed\n * to remove the extension prefix and are either:\n * - Added as children of existing routes (if a nearest route is found via prefix matching)\n * - Replace existing routes (if an exact match is found)\n * - Added directly to the routes array (if no matching route exists)\n *\n * When adding as a child, the parent route's path is clipped from the child route's path.\n *\n * @param routes - The main routes array to merge into (mutated in place)\n * @param extensionRoutes - The extension routes to merge\n * @param extensionIdPrefix - The prefix to remove from extension route IDs (e.g., \"extensions/store-locator/\")\n */\nexport function mergeRoutes(\n routes: RouteConfigEntry[],\n extensionRoutes: RouteConfigEntry[],\n extensionIdPrefix: string\n): void {\n for (const route of extensionRoutes) {\n if (!route.id) {\n routes.unshift(route);\n continue;\n }\n const routeId = route.id.replace(extensionIdPrefix, '');\n const nearestRouteResult = findNearestRoute(routes, routeId);\n if (nearestRouteResult) {\n const nearestRoute = nearestRouteResult.routes[nearestRouteResult.routeIndex];\n if (nearestRoute.id === routeId) {\n // Replacing an existing route, we assume we can just swap out the implementation\n nearestRouteResult.routes[nearestRouteResult.routeIndex].file = route.file;\n } else {\n // This is a new child of an existing route, insert it at the beginning of the children array\n // and clip out the parent path from the route path\n let path = route.path?.slice(nearestRouteResult.path.length);\n if (path?.startsWith('/')) {\n path = path.slice(1);\n }\n path = path ? path : undefined;\n if (!nearestRoute.children) {\n nearestRoute.children = [];\n }\n nearestRoute.children.unshift({\n ...route,\n id: routeId,\n path,\n });\n }\n } else {\n // This is a new route, insert it at the beginning of the routes array\n routes.unshift({\n ...route,\n id: routeId,\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 */\nimport { flatRoutes as _flatRoutes } from '@react-router/fs-routes';\nimport type { RouteConfigEntry } from '@react-router/dev/routes';\nimport fs from 'node:fs/promises';\nimport path from 'node:path';\nimport { mergeRoutes } from './merge-routes';\nimport { applyUrlConfig } from '../multi-site/apply-url-config';\nimport { loadConfig } from '../config/load-config';\nimport type { Url } from '../config/types';\n\nconst APP_SRC_DIR = 'src';\nconst EXTENSIONS_DIR = 'extensions';\n// This file must live at the root of `appDirectory` (src/app-wrapper.tsx) and must NOT\n// be moved into a subdirectory. React Router's typegen resolves route module types using\n// paths relative to `appDirectory` — placing it elsewhere breaks generated type references.\nconst APP_WRAPPER_FILE = 'app-wrapper.tsx';\n\n/**\n * Scans `src/extensions/` for extension route directories and merges any discovered\n * routes into the base route tree. Mutates `routes` in place via `mergeRoutes`.\n */\nasync function discoverExtensionRoutes(ignoredRouteFiles: string[], routes: RouteConfigEntry[]): Promise<void> {\n const extensionsDir = path.join('.', APP_SRC_DIR, EXTENSIONS_DIR);\n\n // Sort to ensure deterministic route order across platforms (readdir order is filesystem-dependent)\n const extensions = await fs.readdir(extensionsDir).then(\n (entries) => entries.sort(),\n () => []\n );\n for (const ext of extensions) {\n // React Router rootDirectory uses forward slashes regardless of OS\n const routesDir = `${EXTENSIONS_DIR}/${ext}/routes`;\n const routesDirFull = path.join('.', APP_SRC_DIR, EXTENSIONS_DIR, ext, 'routes');\n try {\n await fs.access(routesDirFull);\n const extensionRoutes = await _flatRoutes({\n ignoredRouteFiles,\n rootDirectory: routesDir,\n });\n mergeRoutes(routes, extensionRoutes, `${EXTENSIONS_DIR}/${ext}/`);\n } catch {\n // Extension has no routes directory — skip\n }\n }\n}\n\n/**\n * Discovers all file-based routes, merges extension routes, and applies multi-site\n * URL configuration if defined in the project's `config.server.ts`.\n *\n * 1. Discover routes from the filesystem using React Router's `flatRoutes`.\n * 2. Scans `src/extensions/` for extension routes and merges them into the route tree.\n * 3. Load `config.server.ts` from the project root and, if `app.url` is configured,\n * wraps routes under the URL prefix (e.g. `/:siteId/:localeId`).\n *\n * @param options.ignoredRouteFiles - Glob patterns for files to ignore. Defaults to test files.\n * @param options.rootDirectory - Root directory for route discovery, relative to appDirectory.\n * @returns The final route config entries for React Router.\n */\nexport async function flatRoutes(options?: {\n ignoredRouteFiles?: string[];\n rootDirectory?: string;\n}): Promise<RouteConfigEntry[]> {\n const { ignoredRouteFiles = ['**/*.test.{ts,tsx}'], rootDirectory } = options ?? {};\n\n // 1. Discover base routes from filesystem\n const routes = await _flatRoutes({ ignoredRouteFiles, rootDirectory });\n\n // 2. Discover and merge extension routes\n await discoverExtensionRoutes(ignoredRouteFiles, routes);\n\n // 3. Try to load URL config from template's config file\n const { app } = await loadConfig();\n const urlConfig = app?.url as Url | undefined;\n if (urlConfig?.prefix) {\n try {\n await fs.access(path.join('.', APP_SRC_DIR, APP_WRAPPER_FILE));\n } catch {\n throw new Error(\n `[storefront-next-runtime] URL prefix \"${urlConfig.prefix}\" is configured but ` +\n `\"${APP_SRC_DIR}/${APP_WRAPPER_FILE}\" does not exist. ` +\n `Create this file with: export { default } from '@salesforce/storefront-next-runtime/routing/app-wrapper';`\n );\n }\n\n return applyUrlConfig({\n routes,\n urlConfig,\n wrapperFile: APP_WRAPPER_FILE,\n });\n }\n\n return routes;\n}\n"],"mappings":";;;;;;;;;;;;;;AAwBA,SAAS,iBACL,QACA,UACA,WAAmB,IACoD;AACvE,MAAK,IAAI,IAAI,GAAG,IAAI,OAAO,QAAQ,KAAK;EACpC,MAAM,QAAQ,OAAO;EACrB,MAAMA,SAAO,MAAM,OAAO,GAAG,SAAS,GAAG,MAAM,SAAS;AACxD,MAAI,MAAM,OAAO,SACb,QAAO;GAAE;GAAQ,YAAY;GAAG;GAAM;AAE1C,MAAI,MAAM,UAAU;GAChB,MAAM,QAAQ,iBAAiB,MAAM,UAAU,UAAUA,OAAK;AAC9D,OAAI,MACA,QAAO;;AAIf,MAAI,MAAM,MAAM,SAAS,WAAW,MAAM,GAAG,CACzC,QAAO;GAAE;GAAQ,YAAY;GAAG;GAAM;;AAG9C,QAAO;;;;;;;;;;;;;;;;AAiBX,SAAgB,YACZ,QACA,iBACA,mBACI;AACJ,MAAK,MAAM,SAAS,iBAAiB;AACjC,MAAI,CAAC,MAAM,IAAI;AACX,UAAO,QAAQ,MAAM;AACrB;;EAEJ,MAAM,UAAU,MAAM,GAAG,QAAQ,mBAAmB,GAAG;EACvD,MAAM,qBAAqB,iBAAiB,QAAQ,QAAQ;AAC5D,MAAI,oBAAoB;GACpB,MAAM,eAAe,mBAAmB,OAAO,mBAAmB;AAClE,OAAI,aAAa,OAAO,QAEpB,oBAAmB,OAAO,mBAAmB,YAAY,OAAO,MAAM;QACnE;IAGH,IAAIA,SAAO,MAAM,MAAM,MAAM,mBAAmB,KAAK,OAAO;AAC5D,QAAIA,QAAM,WAAW,IAAI,CACrB,UAAOA,OAAK,MAAM,EAAE;AAExB,aAAOA,SAAOA,SAAO;AACrB,QAAI,CAAC,aAAa,SACd,cAAa,WAAW,EAAE;AAE9B,iBAAa,SAAS,QAAQ;KAC1B,GAAG;KACH,IAAI;KACJ;KACH,CAAC;;QAIN,QAAO,QAAQ;GACX,GAAG;GACH,IAAI;GACP,CAAC;;;;;;AC9Ed,MAAM,cAAc;AACpB,MAAM,iBAAiB;AAIvB,MAAM,mBAAmB;;;;;AAMzB,eAAe,wBAAwB,mBAA6B,QAA2C;CAC3G,MAAM,gBAAgB,KAAK,KAAK,KAAK,aAAa,eAAe;CAGjE,MAAM,aAAa,MAAM,GAAG,QAAQ,cAAc,CAAC,MAC9C,YAAY,QAAQ,MAAM,QACrB,EAAE,CACX;AACD,MAAK,MAAM,OAAO,YAAY;EAE1B,MAAM,YAAY,GAAG,eAAe,GAAG,IAAI;EAC3C,MAAM,gBAAgB,KAAK,KAAK,KAAK,aAAa,gBAAgB,KAAK,SAAS;AAChF,MAAI;AACA,SAAM,GAAG,OAAO,cAAc;AAK9B,eAAY,QAJY,MAAMC,aAAY;IACtC;IACA,eAAe;IAClB,CAAC,EACmC,GAAG,eAAe,GAAG,IAAI,GAAG;UAC7D;;;;;;;;;;;;;;;;AAmBhB,eAAsB,WAAW,SAGD;CAC5B,MAAM,EAAE,oBAAoB,CAAC,qBAAqB,EAAE,kBAAkB,WAAW,EAAE;CAGnF,MAAM,SAAS,MAAMA,aAAY;EAAE;EAAmB;EAAe,CAAC;AAGtE,OAAM,wBAAwB,mBAAmB,OAAO;CAGxD,MAAM,EAAE,QAAQ,MAAM,YAAY;CAClC,MAAM,YAAY,KAAK;AACvB,KAAI,WAAW,QAAQ;AACnB,MAAI;AACA,SAAM,GAAG,OAAO,KAAK,KAAK,KAAK,aAAa,iBAAiB,CAAC;UAC1D;AACJ,SAAM,IAAI,MACN,yCAAyC,UAAU,OAAO,uBAClD,YAAY,GAAG,iBAAiB,6HAE3C;;AAGL,SAAO,eAAe;GAClB;GACA;GACA,aAAa;GAChB,CAAC;;AAGN,QAAO"}