@salesforce/storefront-next-runtime 0.1.1-alpha.0 → 0.2.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/DesignComponent.js +150 -0
- package/dist/DesignComponent.js.map +1 -0
- package/dist/DesignFrame.js +196 -0
- package/dist/DesignFrame.js.map +1 -0
- package/dist/DesignRegion.js +83 -0
- package/dist/DesignRegion.js.map +1 -0
- package/dist/apply-url-config.js +130 -0
- package/dist/apply-url-config.js.map +1 -0
- package/dist/component.types.d.ts +87 -0
- package/dist/component.types.d.ts.map +1 -0
- package/dist/config.d.ts +2 -0
- package/dist/config.js +0 -0
- package/dist/design-data.d.ts +983 -0
- package/dist/design-data.d.ts.map +1 -0
- package/dist/design-data.js +908 -0
- package/dist/design-data.js.map +1 -0
- package/dist/design-messaging.d.ts +2 -2
- package/dist/design-react-core.d.ts +50 -5
- package/dist/design-react-core.d.ts.map +1 -1
- package/dist/design-react-core.js +81 -2
- package/dist/design-react-core.js.map +1 -1
- package/dist/design-react.d.ts +20 -95
- package/dist/design-react.d.ts.map +1 -1
- package/dist/design-react.js +3 -485
- package/dist/design-styles.css +2 -1
- package/dist/design.d.ts +110 -2
- package/dist/design.d.ts.map +1 -0
- package/dist/events.d.ts +1 -1
- package/dist/index.d.ts +1110 -154
- package/dist/index.d.ts.map +1 -1
- package/dist/multi-site.d.ts +154 -0
- package/dist/multi-site.d.ts.map +1 -0
- package/dist/multi-site.js +393 -0
- package/dist/multi-site.js.map +1 -0
- package/dist/routing-app-wrapper.d.ts +18 -0
- package/dist/routing-app-wrapper.d.ts.map +1 -0
- package/dist/routing-app-wrapper.js +21 -0
- package/dist/routing-app-wrapper.js.map +1 -0
- package/dist/routing.d.ts +42 -0
- package/dist/routing.d.ts.map +1 -0
- package/dist/routing.js +171 -0
- package/dist/routing.js.map +1 -0
- package/dist/scapi.d.ts +69 -5
- package/dist/scapi.d.ts.map +1 -1
- package/dist/scapi.js +1 -1
- package/dist/scapi.js.map +1 -1
- package/dist/types.d.ts +40 -13289
- package/dist/types.d.ts.map +1 -1
- package/dist/types2.d.ts +13293 -0
- package/dist/types2.d.ts.map +1 -0
- package/dist/types3.d.ts +110 -0
- package/dist/types3.d.ts.map +1 -0
- package/dist/workspace.d.ts +46 -0
- package/dist/workspace.d.ts.map +1 -0
- package/dist/workspace.js +52 -0
- package/dist/workspace.js.map +1 -0
- package/package.json +44 -2
- package/dist/design-react.js.map +0 -1
- package/dist/index2.d.ts +0 -1171
- package/dist/index2.d.ts.map +0 -1
- /package/{LICENSE.txt → LICENSE} +0 -0
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/design/messaging-api/domain-types.ts","../src/design/messaging-api/api-types.ts","../src/design/messaging-api/client.ts","../src/design/messaging-api/host.ts"],"sourcesContent":[],"mappings":"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"}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { n as Site$1, r as Url, t as Locale$1 } from "./types.js";
|
|
2
|
+
import { PropsWithChildren } from "react";
|
|
3
|
+
import * as react_jsx_runtime0 from "react/jsx-runtime";
|
|
4
|
+
import * as react_router0 from "react-router";
|
|
5
|
+
import { Cookie, MiddlewareFunction, RouterContextProvider } from "react-router";
|
|
6
|
+
import { RouteConfigEntry } from "@react-router/dev/routes";
|
|
7
|
+
|
|
8
|
+
//#region src/multi-site/site-context.d.ts
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Provides the current site to the component tree.
|
|
12
|
+
* Follows the same pattern as CurrencyProvider.
|
|
13
|
+
*
|
|
14
|
+
* Mounted in the template (e.g., app-wrapper.tsx or root.tsx) with the resolved
|
|
15
|
+
* site value from the loader/middleware.
|
|
16
|
+
*/
|
|
17
|
+
declare function SiteProvider({
|
|
18
|
+
value,
|
|
19
|
+
children
|
|
20
|
+
}: PropsWithChildren<{
|
|
21
|
+
value: Site$1;
|
|
22
|
+
}>): react_jsx_runtime0.JSX.Element;
|
|
23
|
+
/**
|
|
24
|
+
* React hook to get the current site.
|
|
25
|
+
* Returns undefined when no SiteProvider is mounted.
|
|
26
|
+
*/
|
|
27
|
+
declare function useSite(): Site$1 | undefined;
|
|
28
|
+
//#endregion
|
|
29
|
+
//#region src/multi-site/apply-url-config.d.ts
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Applies multi-site URL configuration to a set of route entries.
|
|
33
|
+
*
|
|
34
|
+
* Wraps non-excluded routes under a parent route with the configured URL prefix
|
|
35
|
+
* (e.g. `/:siteId/:localeId`), while keeping excluded routes (action/resource by default)
|
|
36
|
+
* at the root level. The homepage index route (and its parent layout) is always
|
|
37
|
+
* duplicated at `/` so the root URL still serves content.
|
|
38
|
+
*
|
|
39
|
+
* @param options - Configuration for URL customisation.
|
|
40
|
+
* @param options.routes - The flat route entries discovered from the filesystem.
|
|
41
|
+
* @param options.urlConfig - URL customisation configuration (prefix, excludeRoutes).
|
|
42
|
+
* @param options.wrapperFile - Path to the wrapper component file, relative to appDirectory.
|
|
43
|
+
* @returns The transformed route entries with prefix wrapping applied.
|
|
44
|
+
*/
|
|
45
|
+
declare function applyUrlConfig(options: {
|
|
46
|
+
routes: RouteConfigEntry[];
|
|
47
|
+
urlConfig?: Url;
|
|
48
|
+
wrapperFile: string;
|
|
49
|
+
}): RouteConfigEntry[];
|
|
50
|
+
//#endregion
|
|
51
|
+
//#region src/multi-site/build-url.d.ts
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Builds a fully-qualified URL with multi-site prefix and search params.
|
|
55
|
+
*
|
|
56
|
+
* Only keys defined in urlConfig.search are set by multi-site. Any other query params
|
|
57
|
+
* already present on the `to` URL (including duplicate keys) are preserved as-is.
|
|
58
|
+
* e.g. to='/api/search?refine=color:blue&refine=size:M', search='?lng=:localeId'
|
|
59
|
+
* → '/api/search?refine=color:blue&refine=size:M&lng=en-GB'
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* buildUrl({ to: '/product/123', urlConfig: { prefix: '/:siteId', search: '?lng=:localeId' }, params: { siteId: 'global', localeId: 'en-GB' } })
|
|
63
|
+
* // → '/global/product/123?lng=en-GB'
|
|
64
|
+
*/
|
|
65
|
+
declare function buildUrl({
|
|
66
|
+
to,
|
|
67
|
+
urlConfig,
|
|
68
|
+
params
|
|
69
|
+
}: {
|
|
70
|
+
to: string;
|
|
71
|
+
urlConfig?: Url;
|
|
72
|
+
params: Record<string, string>;
|
|
73
|
+
}): string;
|
|
74
|
+
//#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
|
+
//#region src/multi-site/middleware.d.ts
|
|
113
|
+
declare const multiSiteContext: react_router0.RouterContext<MultiSiteContext | null>;
|
|
114
|
+
/**
|
|
115
|
+
* Helper function to get multi-site cookies from router context.
|
|
116
|
+
* Useful in server actions and loaders that need to read/set cookies.
|
|
117
|
+
*
|
|
118
|
+
* @param context - Router context provider
|
|
119
|
+
* @returns Object with siteCookie and localeCookie instances, or null if context not set
|
|
120
|
+
*
|
|
121
|
+
* @example
|
|
122
|
+
* ```typescript
|
|
123
|
+
* export const action: ActionFunction = async ({ request, context }) => {
|
|
124
|
+
* const cookies = getMultiSiteCookies(context);
|
|
125
|
+
* if (cookies) {
|
|
126
|
+
* const cookieHeader = await cookies.localeCookie.serialize(locale);
|
|
127
|
+
* // ... use cookieHeader
|
|
128
|
+
* }
|
|
129
|
+
* };
|
|
130
|
+
* ```
|
|
131
|
+
*/
|
|
132
|
+
declare function getMultiSiteCookies(context: Readonly<RouterContextProvider>): {
|
|
133
|
+
siteCookie: react_router0.Cookie;
|
|
134
|
+
localeCookie: react_router0.Cookie;
|
|
135
|
+
} | null;
|
|
136
|
+
/**
|
|
137
|
+
* Creates a multi-site middleware that resolves the current site from
|
|
138
|
+
* the request (path, cookie, header, query, or default) and stores the
|
|
139
|
+
* result in the router context.
|
|
140
|
+
*
|
|
141
|
+
* Does not import or read from app config context; the consumer supplies config.
|
|
142
|
+
*/
|
|
143
|
+
declare function createMultiSiteMiddleware(config: MultiSiteConfig): MiddlewareFunction<Response>;
|
|
144
|
+
//#endregion
|
|
145
|
+
//#region src/multi-site/cookies.d.ts
|
|
146
|
+
/**
|
|
147
|
+
* WeakMap to pass resolved locale from multi-site middleware to i18next's findLocale.
|
|
148
|
+
* WeakMap allows garbage collection when requests are done.
|
|
149
|
+
* This is necessary because findLocale() only receives the Request object, not the router context.
|
|
150
|
+
*/
|
|
151
|
+
declare const requestToLocaleMap: WeakMap<Request, string>;
|
|
152
|
+
//#endregion
|
|
153
|
+
export { type DetectionConfig, type Locale, type MultiSiteConfig, type MultiSiteContext, type Site, SiteProvider, applyUrlConfig, buildUrl, createMultiSiteMiddleware, getMultiSiteCookies, multiSiteContext, requestToLocaleMap, useSite };
|
|
154
|
+
//# sourceMappingURL=multi-site.d.ts.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
import { t as applyUrlConfig } from "./apply-url-config.js";
|
|
2
|
+
import { createContext, useContext } from "react";
|
|
3
|
+
import { jsx } from "react/jsx-runtime";
|
|
4
|
+
import { createContext as createContext$1, createCookie } from "react-router";
|
|
5
|
+
|
|
6
|
+
//#region src/multi-site/site-context.tsx
|
|
7
|
+
const SiteContext = createContext(void 0);
|
|
8
|
+
/**
|
|
9
|
+
* Provides the current site to the component tree.
|
|
10
|
+
* Follows the same pattern as CurrencyProvider.
|
|
11
|
+
*
|
|
12
|
+
* Mounted in the template (e.g., app-wrapper.tsx or root.tsx) with the resolved
|
|
13
|
+
* site value from the loader/middleware.
|
|
14
|
+
*/
|
|
15
|
+
function SiteProvider({ value, children }) {
|
|
16
|
+
return /* @__PURE__ */ jsx(SiteContext.Provider, {
|
|
17
|
+
value,
|
|
18
|
+
children
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* React hook to get the current site.
|
|
23
|
+
* Returns undefined when no SiteProvider is mounted.
|
|
24
|
+
*/
|
|
25
|
+
function useSite() {
|
|
26
|
+
return useContext(SiteContext);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
//#endregion
|
|
30
|
+
//#region src/multi-site/build-url.ts
|
|
31
|
+
/**
|
|
32
|
+
* Parses search config string into key-value pairs, preserving ':param' placeholders.
|
|
33
|
+
* '?lng=:localeId&site=:siteId' → { lng: ':localeId', site: ':siteId' }
|
|
34
|
+
*/
|
|
35
|
+
function parseSearchConfig(search) {
|
|
36
|
+
const searchParams = new URLSearchParams(search);
|
|
37
|
+
const result = {};
|
|
38
|
+
for (const [key, value] of searchParams) result[key] = value;
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Extracts parameter names from a prefix string.
|
|
43
|
+
* '/:siteId/:localeId' → ['siteId', 'localeId']
|
|
44
|
+
*/
|
|
45
|
+
function extractPrefixParams(prefix) {
|
|
46
|
+
const matches = prefix.match(/:(\w+)/g);
|
|
47
|
+
return matches ? matches.map((m) => m.slice(1)) : [];
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Splits a URL string into its component parts.
|
|
51
|
+
* '/product/123?color=red#details' → { pathname: '/product/123', search: 'color=red', hash: '#details' }
|
|
52
|
+
*/
|
|
53
|
+
function decomposeUrl(url) {
|
|
54
|
+
const hashIdx = url.indexOf("#");
|
|
55
|
+
const hash = hashIdx >= 0 ? url.slice(hashIdx) : "";
|
|
56
|
+
const withoutHash = hashIdx >= 0 ? url.slice(0, hashIdx) : url;
|
|
57
|
+
const searchIdx = withoutHash.indexOf("?");
|
|
58
|
+
const search = searchIdx >= 0 ? withoutHash.slice(searchIdx + 1) : "";
|
|
59
|
+
return {
|
|
60
|
+
pathname: searchIdx >= 0 ? withoutHash.slice(0, searchIdx) : withoutHash,
|
|
61
|
+
search,
|
|
62
|
+
hash
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Resolves a prefix template by replacing parameter placeholders with values.
|
|
67
|
+
* ('/:siteId/:localeId', { siteId: 'global', localeId: 'en-GB' }) → '/global/en-GB'
|
|
68
|
+
*/
|
|
69
|
+
function resolvePrefix(prefix, params) {
|
|
70
|
+
let resolved = prefix;
|
|
71
|
+
for (const paramName of extractPrefixParams(prefix)) {
|
|
72
|
+
const value = params[paramName];
|
|
73
|
+
if (value) resolved = resolved.replace(`:${paramName}`, value);
|
|
74
|
+
}
|
|
75
|
+
return resolved;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Sanitize a resolved prefix from a pathname if present.
|
|
79
|
+
* sanitizePrefix('/global/en-GB/product/123', '/global/en-GB') → '/product/123'
|
|
80
|
+
* sanitizePrefix('/product/123', '/global/en-GB') → '/product/123' (no-op)
|
|
81
|
+
*/
|
|
82
|
+
function sanitizePrefix(pathname, pathPrefix) {
|
|
83
|
+
if (!pathPrefix) return pathname;
|
|
84
|
+
if (pathname === pathPrefix) return "";
|
|
85
|
+
if (pathname.startsWith(`${pathPrefix}/`)) return pathname.slice(pathPrefix.length);
|
|
86
|
+
return pathname;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Builds a fully-qualified URL with multi-site prefix and search params.
|
|
90
|
+
*
|
|
91
|
+
* Only keys defined in urlConfig.search are set by multi-site. Any other query params
|
|
92
|
+
* already present on the `to` URL (including duplicate keys) are preserved as-is.
|
|
93
|
+
* e.g. to='/api/search?refine=color:blue&refine=size:M', search='?lng=:localeId'
|
|
94
|
+
* → '/api/search?refine=color:blue&refine=size:M&lng=en-GB'
|
|
95
|
+
*
|
|
96
|
+
* @example
|
|
97
|
+
* buildUrl({ to: '/product/123', urlConfig: { prefix: '/:siteId', search: '?lng=:localeId' }, params: { siteId: 'global', localeId: 'en-GB' } })
|
|
98
|
+
* // → '/global/product/123?lng=en-GB'
|
|
99
|
+
*/
|
|
100
|
+
function buildUrl({ to, urlConfig, params }) {
|
|
101
|
+
if (!urlConfig) return to;
|
|
102
|
+
if (!to || to === "#" || to.startsWith("http") || to.startsWith("//")) return to;
|
|
103
|
+
const { pathname, search: existingSearch, hash } = decomposeUrl(to);
|
|
104
|
+
const pathPrefix = urlConfig.prefix && urlConfig.prefix !== "/" ? resolvePrefix(urlConfig.prefix, params) : "";
|
|
105
|
+
const path = pathPrefix ? `${pathPrefix}${sanitizePrefix(pathname, pathPrefix)}` : pathname;
|
|
106
|
+
const searchParams = new URLSearchParams(existingSearch);
|
|
107
|
+
if (urlConfig.search) {
|
|
108
|
+
const searchConfig = parseSearchConfig(urlConfig.search);
|
|
109
|
+
for (const [queryKey, value] of Object.entries(searchConfig)) if (value.startsWith(":")) {
|
|
110
|
+
const paramValue = params[value.slice(1)];
|
|
111
|
+
if (paramValue) searchParams.set(queryKey, paramValue);
|
|
112
|
+
} else searchParams.set(queryKey, value);
|
|
113
|
+
}
|
|
114
|
+
const search = searchParams.toString();
|
|
115
|
+
return `${path}${search ? `?${search}` : ""}${hash}`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
//#endregion
|
|
119
|
+
//#region src/multi-site/utils.ts
|
|
120
|
+
/**
|
|
121
|
+
* Extract a string value from the URL path segment at the given index.
|
|
122
|
+
*/
|
|
123
|
+
function lookupFromPath(pathname, pathIndex) {
|
|
124
|
+
const pathSegments = pathname.split("/").filter(Boolean);
|
|
125
|
+
if (pathSegments.length <= pathIndex) return null;
|
|
126
|
+
return pathSegments[pathIndex];
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Detect a string value from cookie using the given cookie parser.
|
|
130
|
+
*
|
|
131
|
+
* Returns a promise that resolves to the cookie value.
|
|
132
|
+
*/
|
|
133
|
+
async function readCookieFromRequest(request, cookie) {
|
|
134
|
+
const cookies = request.headers.get("Cookie");
|
|
135
|
+
if (!cookies) return null;
|
|
136
|
+
return await cookie.parse(cookies);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
//#endregion
|
|
140
|
+
//#region src/multi-site/site-detection.ts
|
|
141
|
+
/**
|
|
142
|
+
* Detect site reference from cookie.
|
|
143
|
+
*/
|
|
144
|
+
async function readSiteFromCookie(request, cookie) {
|
|
145
|
+
return readCookieFromRequest(request, cookie);
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Get site object using the site id or alias
|
|
149
|
+
* 1. Check siteIdentifier against each site's alias; if matched, return that site.
|
|
150
|
+
* 2. Else check against each site's id; if matched, return that site.
|
|
151
|
+
* 3. If no match, return null.
|
|
152
|
+
*/
|
|
153
|
+
function getSiteFromIdOrAlias(siteIdentifier, sites) {
|
|
154
|
+
if (!siteIdentifier) return null;
|
|
155
|
+
return sites.find((site) => site.alias === siteIdentifier || site.id === siteIdentifier) ?? null;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Resolve site using the configured detection order.
|
|
159
|
+
* Returns the first valid site from the first source that yields a valid value.
|
|
160
|
+
*/
|
|
161
|
+
async function resolveSite(request, settings) {
|
|
162
|
+
const { sites, defaultSiteId, siteDetectionConfig, siteCookie } = settings;
|
|
163
|
+
const requestUrl = new URL(request.url);
|
|
164
|
+
const resolvers = {
|
|
165
|
+
path: () => Promise.resolve(lookupFromPath(requestUrl.pathname, siteDetectionConfig.lookupFromPathIndex)),
|
|
166
|
+
querystring: () => Promise.resolve(requestUrl.searchParams.get(siteDetectionConfig.lookupQuerystring)),
|
|
167
|
+
header: () => Promise.resolve(request.headers.get(siteDetectionConfig.lookupHeader)),
|
|
168
|
+
cookie: async () => readSiteFromCookie(request, siteCookie)
|
|
169
|
+
};
|
|
170
|
+
for (const method of siteDetectionConfig.order) {
|
|
171
|
+
const resolvedSite = getSiteFromIdOrAlias(await resolvers[method]?.(), sites);
|
|
172
|
+
if (resolvedSite) return resolvedSite;
|
|
173
|
+
}
|
|
174
|
+
const site = getSiteFromIdOrAlias(defaultSiteId, sites);
|
|
175
|
+
if (!site) throw new Error(`Default site ${defaultSiteId} not found.`);
|
|
176
|
+
return site;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
//#endregion
|
|
180
|
+
//#region src/multi-site/configs.ts
|
|
181
|
+
/**
|
|
182
|
+
* Default site detection configuration
|
|
183
|
+
*/
|
|
184
|
+
const DEFAULT_SITE_DETECTION = {
|
|
185
|
+
order: [
|
|
186
|
+
"path",
|
|
187
|
+
"querystring",
|
|
188
|
+
"cookie",
|
|
189
|
+
"header"
|
|
190
|
+
],
|
|
191
|
+
lookupFromPathIndex: 0,
|
|
192
|
+
lookupQuerystring: "site",
|
|
193
|
+
lookupCookie: "site_id",
|
|
194
|
+
lookupHeader: "X-Site-Id",
|
|
195
|
+
caches: ["cookie"]
|
|
196
|
+
};
|
|
197
|
+
/**
|
|
198
|
+
* Default locale detection configuration
|
|
199
|
+
*/
|
|
200
|
+
const DEFAULT_LOCALE_DETECTION = {
|
|
201
|
+
order: [
|
|
202
|
+
"path",
|
|
203
|
+
"querystring",
|
|
204
|
+
"cookie",
|
|
205
|
+
"header"
|
|
206
|
+
],
|
|
207
|
+
lookupFromPathIndex: 1,
|
|
208
|
+
lookupQuerystring: "lng",
|
|
209
|
+
lookupCookie: "lng",
|
|
210
|
+
lookupHeader: "Accept-Language",
|
|
211
|
+
caches: ["cookie"]
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
//#endregion
|
|
215
|
+
//#region src/multi-site/cookies.ts
|
|
216
|
+
/**
|
|
217
|
+
* Cookie options for multi-site cookies
|
|
218
|
+
*/
|
|
219
|
+
const COOKIE_OPTIONS = {
|
|
220
|
+
path: "/",
|
|
221
|
+
sameSite: "lax",
|
|
222
|
+
secure: process.env.NODE_ENV === "production",
|
|
223
|
+
httpOnly: true
|
|
224
|
+
};
|
|
225
|
+
/**
|
|
226
|
+
* Creates a cookie instance with the given name.
|
|
227
|
+
*
|
|
228
|
+
* @param name - Cookie name
|
|
229
|
+
* @returns Cookie instance configured with multi-site options
|
|
230
|
+
*/
|
|
231
|
+
function createMultiSiteCookie(name) {
|
|
232
|
+
return createCookie(name, COOKIE_OPTIONS);
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* WeakMap to pass resolved locale from multi-site middleware to i18next's findLocale.
|
|
236
|
+
* WeakMap allows garbage collection when requests are done.
|
|
237
|
+
* This is necessary because findLocale() only receives the Request object, not the router context.
|
|
238
|
+
*/
|
|
239
|
+
const requestToLocaleMap = /* @__PURE__ */ new WeakMap();
|
|
240
|
+
|
|
241
|
+
//#endregion
|
|
242
|
+
//#region src/multi-site/locale-detection.ts
|
|
243
|
+
/**
|
|
244
|
+
* Read locale from cookie.
|
|
245
|
+
*/
|
|
246
|
+
async function readLocaleFromCookie(request, cookie) {
|
|
247
|
+
return readCookieFromRequest(request, cookie);
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Get locale object using the locale id or alias.
|
|
251
|
+
* 1. Check localeIdOrAlias against each locale's alias; if matched, return that locale.
|
|
252
|
+
* 2. Else check against each locale's id; if matched, return that locale.
|
|
253
|
+
* 3. If no match, return null (caller should use defaultLocale).
|
|
254
|
+
*
|
|
255
|
+
* @param localeIdentifier - The locale id or alias to get the locale from. Null is allowed because this may come from
|
|
256
|
+
* extrenal sources such as cookies, headers, or query parameters.
|
|
257
|
+
* @param locales - The list of locales to search through.
|
|
258
|
+
* @returns The locale object if found, otherwise null.
|
|
259
|
+
*/
|
|
260
|
+
function getLocaleFromIdOrAlias(localeIdentifier, locales) {
|
|
261
|
+
if (!localeIdentifier) return null;
|
|
262
|
+
return locales.find((locale) => locale.alias === localeIdentifier || locale.id === localeIdentifier) ?? null;
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Resolve locale using the configured detection order.
|
|
266
|
+
* Returns the first valid locale from the first source that yields a valid value.
|
|
267
|
+
*/
|
|
268
|
+
async function resolveLocale(request, settings, site) {
|
|
269
|
+
const { defaultLocale, localeDetectionConfig, localeCookie } = settings;
|
|
270
|
+
const { supportedLocales } = site;
|
|
271
|
+
let locale = null;
|
|
272
|
+
const requestUrl = new URL(request.url);
|
|
273
|
+
const resolvers = {
|
|
274
|
+
path: () => Promise.resolve(lookupFromPath(requestUrl.pathname, localeDetectionConfig.lookupFromPathIndex)),
|
|
275
|
+
querystring: () => Promise.resolve(requestUrl.searchParams.get(localeDetectionConfig.lookupQuerystring)),
|
|
276
|
+
header: () => Promise.resolve(request.headers.get(localeDetectionConfig.lookupHeader)),
|
|
277
|
+
cookie: async () => readLocaleFromCookie(request, localeCookie)
|
|
278
|
+
};
|
|
279
|
+
for (const method of localeDetectionConfig.order) {
|
|
280
|
+
const resolvedLocale = getLocaleFromIdOrAlias(await resolvers[method]?.(), supportedLocales);
|
|
281
|
+
if (resolvedLocale) return resolvedLocale;
|
|
282
|
+
}
|
|
283
|
+
if (!locale) locale = getLocaleFromIdOrAlias(defaultLocale, supportedLocales);
|
|
284
|
+
if (!locale) throw new Error(`Default locale ${defaultLocale} not found in the list of supported locales for site ${site.id}.`);
|
|
285
|
+
return locale;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
//#endregion
|
|
289
|
+
//#region src/multi-site/middleware.ts
|
|
290
|
+
const multiSiteContext = createContext$1(null);
|
|
291
|
+
/**
|
|
292
|
+
* Helper function to get multi-site cookies from router context.
|
|
293
|
+
* Useful in server actions and loaders that need to read/set cookies.
|
|
294
|
+
*
|
|
295
|
+
* @param context - Router context provider
|
|
296
|
+
* @returns Object with siteCookie and localeCookie instances, or null if context not set
|
|
297
|
+
*
|
|
298
|
+
* @example
|
|
299
|
+
* ```typescript
|
|
300
|
+
* export const action: ActionFunction = async ({ request, context }) => {
|
|
301
|
+
* const cookies = getMultiSiteCookies(context);
|
|
302
|
+
* if (cookies) {
|
|
303
|
+
* const cookieHeader = await cookies.localeCookie.serialize(locale);
|
|
304
|
+
* // ... use cookieHeader
|
|
305
|
+
* }
|
|
306
|
+
* };
|
|
307
|
+
* ```
|
|
308
|
+
*/
|
|
309
|
+
function getMultiSiteCookies(context) {
|
|
310
|
+
const multiSite = context.get(multiSiteContext);
|
|
311
|
+
if (!multiSite) return null;
|
|
312
|
+
return {
|
|
313
|
+
siteCookie: multiSite.siteCookie,
|
|
314
|
+
localeCookie: multiSite.localeCookie
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Helper function to determine if cookies should be set based on:
|
|
319
|
+
* 1. Whether caching is enabled for each cookie type
|
|
320
|
+
* 2. Whether cookies already exist in the incoming request
|
|
321
|
+
* 3. Whether cookies were already set by actions/loaders in the response
|
|
322
|
+
*
|
|
323
|
+
* @param request - Incoming request
|
|
324
|
+
* @param response - Response from next()
|
|
325
|
+
* @param settings - Multi-site settings with cookie instances and detection config
|
|
326
|
+
* @returns Object with shouldSetSiteCookie and shouldSetLocaleCookie booleans
|
|
327
|
+
*/
|
|
328
|
+
async function shouldSetCookies(request, response, settings) {
|
|
329
|
+
const cacheSite = settings.siteDetectionConfig.caches?.includes("cookie");
|
|
330
|
+
const cacheLocale = settings.localeDetectionConfig.caches?.includes("cookie");
|
|
331
|
+
if (!cacheSite && !cacheLocale) return {
|
|
332
|
+
shouldSetSiteCookie: false,
|
|
333
|
+
shouldSetLocaleCookie: false
|
|
334
|
+
};
|
|
335
|
+
const requestCookieHeader = request.headers.get("Cookie");
|
|
336
|
+
const [existingSiteCookie, existingLocaleCookie] = await Promise.all([settings.siteCookie.parse(requestCookieHeader), settings.localeCookie.parse(requestCookieHeader)]);
|
|
337
|
+
const responseSetCookies = response.headers.getSetCookie?.() || [];
|
|
338
|
+
const isSettingSiteCookieInResponse = responseSetCookies.some((cookie) => cookie.startsWith(`${settings.siteCookie.name}=`));
|
|
339
|
+
const isSettingLocaleCookieInResponse = responseSetCookies.some((cookie) => cookie.startsWith(`${settings.localeCookie.name}=`));
|
|
340
|
+
return {
|
|
341
|
+
shouldSetSiteCookie: cacheSite && !existingSiteCookie && !isSettingSiteCookieInResponse,
|
|
342
|
+
shouldSetLocaleCookie: cacheLocale && !existingLocaleCookie && !isSettingLocaleCookieInResponse
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Creates a multi-site middleware that resolves the current site from
|
|
347
|
+
* the request (path, cookie, header, query, or default) and stores the
|
|
348
|
+
* result in the router context.
|
|
349
|
+
*
|
|
350
|
+
* Does not import or read from app config context; the consumer supplies config.
|
|
351
|
+
*/
|
|
352
|
+
function createMultiSiteMiddleware(config) {
|
|
353
|
+
const siteDetectionConfig = {
|
|
354
|
+
...DEFAULT_SITE_DETECTION,
|
|
355
|
+
...config.siteDetectionConfig
|
|
356
|
+
};
|
|
357
|
+
const localeDetectionConfig = {
|
|
358
|
+
...DEFAULT_LOCALE_DETECTION,
|
|
359
|
+
...config.localeDetectionConfig
|
|
360
|
+
};
|
|
361
|
+
const siteCookie = createMultiSiteCookie(siteDetectionConfig.lookupCookie);
|
|
362
|
+
const localeCookie = createMultiSiteCookie(localeDetectionConfig.lookupCookie);
|
|
363
|
+
const settings = {
|
|
364
|
+
...config,
|
|
365
|
+
siteDetectionConfig,
|
|
366
|
+
localeDetectionConfig,
|
|
367
|
+
siteCookie,
|
|
368
|
+
localeCookie
|
|
369
|
+
};
|
|
370
|
+
const multiSiteMiddleware = async ({ request, context }, next) => {
|
|
371
|
+
const site = await resolveSite(request, settings);
|
|
372
|
+
const locale = await resolveLocale(request, settings, site);
|
|
373
|
+
context.set(multiSiteContext, {
|
|
374
|
+
site,
|
|
375
|
+
locale,
|
|
376
|
+
siteCookie: settings.siteCookie,
|
|
377
|
+
localeCookie: settings.localeCookie
|
|
378
|
+
});
|
|
379
|
+
requestToLocaleMap.set(request, locale.id);
|
|
380
|
+
const response = await next();
|
|
381
|
+
const { shouldSetSiteCookie, shouldSetLocaleCookie } = await shouldSetCookies(request, response, settings);
|
|
382
|
+
if (!shouldSetSiteCookie && !shouldSetLocaleCookie) return response;
|
|
383
|
+
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
|
+
if (siteSetCookie) response.headers.append("Set-Cookie", siteSetCookie);
|
|
385
|
+
if (localeSetCookie) response.headers.append("Set-Cookie", localeSetCookie);
|
|
386
|
+
return response;
|
|
387
|
+
};
|
|
388
|
+
return multiSiteMiddleware;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
//#endregion
|
|
392
|
+
export { SiteProvider, applyUrlConfig, buildUrl, createMultiSiteMiddleware, getMultiSiteCookies, multiSiteContext, requestToLocaleMap, useSite };
|
|
393
|
+
//# sourceMappingURL=multi-site.js.map
|
|
@@ -0,0 +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":"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"}
|