@salesforce/storefront-next-runtime 0.4.2 → 1.0.0-alpha.1
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/README.md +9 -3
- package/dist/ComponentContext.js +199 -4
- package/dist/ComponentContext.js.map +1 -1
- package/dist/DesignComponent.js +2 -2
- package/dist/DesignRegion.js +2 -2
- package/dist/RegionContext.js +9 -0
- package/dist/RegionContext.js.map +1 -0
- package/dist/component.types.d.ts +1 -1
- package/dist/config.d.ts +34 -221
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +35 -116
- package/dist/config.js.map +1 -1
- package/dist/data-store.d.ts +185 -15
- package/dist/data-store.d.ts.map +1 -1
- package/dist/data-store.js +412 -10
- package/dist/data-store.js.map +1 -1
- package/dist/defaults.d.ts +106 -0
- package/dist/defaults.d.ts.map +1 -0
- package/dist/defaults.js +67 -0
- package/dist/defaults.js.map +1 -0
- package/dist/design-data.d.ts +238 -356
- package/dist/design-data.d.ts.map +1 -1
- package/dist/design-data.js +459 -30
- package/dist/design-data.js.map +1 -1
- package/dist/design-mode.d.ts +3 -2
- package/dist/design-mode.d.ts.map +1 -1
- package/dist/design-react-core.d.ts +5 -15
- package/dist/design-react-core.d.ts.map +1 -1
- package/dist/design-react-core.js +2 -2
- package/dist/design-react.d.ts +2 -2
- package/dist/design.d.ts +2 -2
- package/dist/events.d.ts +32 -6
- package/dist/events.d.ts.map +1 -1
- package/dist/i18n-client.d.ts.map +1 -1
- package/dist/i18n-client.js.map +1 -1
- package/dist/i18n.d.ts +1 -2
- package/dist/i18n.d.ts.map +1 -1
- package/dist/modeDetection.js +0 -18
- package/dist/modeDetection.js.map +1 -1
- package/dist/scapi.d.ts +2185 -466
- package/dist/scapi.d.ts.map +1 -1
- package/dist/scapi.js +1 -1
- package/dist/scapi.js.map +1 -1
- package/dist/schema.d.ts +17 -15
- package/dist/schema.d.ts.map +1 -1
- package/dist/security-react.d.ts +34 -0
- package/dist/security-react.d.ts.map +1 -0
- package/dist/security-react.js +21 -0
- package/dist/security-react.js.map +1 -0
- package/dist/security.d.ts +61 -0
- package/dist/security.d.ts.map +1 -0
- package/dist/security.js +304 -0
- package/dist/security.js.map +1 -0
- package/dist/site-context.d.ts +43 -27
- package/dist/site-context.d.ts.map +1 -1
- package/dist/site-context.js +2 -2
- package/dist/site-context2.js +41 -31
- package/dist/site-context2.js.map +1 -1
- package/dist/types.d.ts +19 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/types2.d.ts +89 -63
- package/dist/types2.d.ts.map +1 -1
- package/dist/types3.d.ts +1 -35
- package/dist/types3.d.ts.map +1 -1
- package/package.json +15 -20
- package/dist/DesignFrame.js +0 -204
- package/dist/DesignFrame.js.map +0 -1
- package/dist/custom-global-preferences.d.ts +0 -20
- package/dist/custom-global-preferences.d.ts.map +0 -1
- package/dist/custom-global-preferences.js +0 -31
- package/dist/custom-global-preferences.js.map +0 -1
- package/dist/custom-site-preferences.d.ts +0 -20
- package/dist/custom-site-preferences.d.ts.map +0 -1
- package/dist/custom-site-preferences.js +0 -31
- package/dist/custom-site-preferences.js.map +0 -1
- package/dist/data-store-custom-global-preferences.d.ts +0 -2
- package/dist/data-store-custom-global-preferences.js +0 -6
- package/dist/data-store-custom-site-preferences.d.ts +0 -2
- package/dist/data-store-custom-site-preferences.js +0 -6
- package/dist/data-store-gcp-preferences.d.ts +0 -2
- package/dist/data-store-gcp-preferences.js +0 -6
- package/dist/gcp-preferences.d.ts +0 -52
- package/dist/gcp-preferences.d.ts.map +0 -1
- package/dist/gcp-preferences.js +0 -64
- package/dist/gcp-preferences.js.map +0 -1
- package/dist/utils.js +0 -90
- package/dist/utils.js.map +0 -1
package/dist/config.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { n as defaultSecurityHeaders } from "./defaults.js";
|
|
1
2
|
import { createContext, useContext } from "react";
|
|
2
3
|
import { jsx } from "react/jsx-runtime";
|
|
3
4
|
import { createContext as createContext$1 } from "react-router";
|
|
@@ -182,18 +183,20 @@ const extractValidPaths = (obj, prefix = "") => {
|
|
|
182
183
|
* @returns Object with overrides to merge into base config
|
|
183
184
|
*
|
|
184
185
|
* @example
|
|
185
|
-
* // Environment variables:
|
|
186
|
-
* //
|
|
187
|
-
* //
|
|
188
|
-
* //
|
|
186
|
+
* // Environment variables (template-specific paths shown):
|
|
187
|
+
* // PUBLIC__app__some__nested__value=abc123
|
|
188
|
+
* // PUBLIC__app__some__numericKnob=1000
|
|
189
|
+
* // PUBLIC__app__some__listKnob=["A","B"]
|
|
189
190
|
*
|
|
190
191
|
* mergeEnvConfig()
|
|
191
192
|
* // Returns:
|
|
192
193
|
* // {
|
|
193
194
|
* // app: {
|
|
194
|
-
* //
|
|
195
|
-
* //
|
|
196
|
-
* //
|
|
195
|
+
* // some: {
|
|
196
|
+
* // nested: { value: 'abc123' },
|
|
197
|
+
* // numericKnob: 1000,
|
|
198
|
+
* // listKnob: ['A', 'B']
|
|
199
|
+
* // }
|
|
197
200
|
* // }
|
|
198
201
|
* // }
|
|
199
202
|
*/
|
|
@@ -214,7 +217,7 @@ const mergeEnvConfig = (env = typeof process !== "undefined" ? process.env : {},
|
|
|
214
217
|
const depth = path.split("__").length;
|
|
215
218
|
if (depth > MAX_DEPTH) throw new Error(`Environment variable "${varName}" exceeds maximum path depth of ${MAX_DEPTH}. Current depth: ${depth}. Consider consolidating with JSON values or reducing nesting levels.`);
|
|
216
219
|
const normalizedPath = path.toLowerCase();
|
|
217
|
-
if (protectedPaths.some((protectedPath) => normalizedPath === protectedPath || normalizedPath.startsWith(`${protectedPath}__`))) throw new Error(`Environment variable "${varName}" attempts to override protected config path "${path}".\n\
|
|
220
|
+
if (protectedPaths.some((protectedPath) => normalizedPath === protectedPath || normalizedPath.startsWith(`${protectedPath}__`))) throw new Error(`Environment variable "${varName}" attempts to override protected config path "${path}".\n\nProtected paths cannot be overridden via environment variables. Update config.server.ts directly, or remove the path from \`protectedPaths\` if env override is intended.`);
|
|
218
221
|
if (baseConfig && validPaths.length > 0) {
|
|
219
222
|
if (!validPaths.includes(normalizedPath)) {
|
|
220
223
|
console.warn(`[Config Warning] Ignoring environment variable "${varName}": Config path "${path}" does not exist in config.server.ts.`);
|
|
@@ -256,15 +259,18 @@ const mergeEnvConfig = (env = typeof process !== "undefined" ? process.env : {},
|
|
|
256
259
|
/**
|
|
257
260
|
* Define a type-safe storefront configuration with IDE autocomplete.
|
|
258
261
|
*
|
|
259
|
-
*
|
|
260
|
-
*
|
|
261
|
-
*
|
|
262
|
+
* Reads `process.env` at call time and merges any `PUBLIC__`-prefixed
|
|
263
|
+
* variables into the config (validated against the base config structure —
|
|
264
|
+
* env vars targeting paths that don't exist in the base config are ignored
|
|
265
|
+
* with a warning). This is a server-only side effect by design; calling
|
|
266
|
+
* `defineConfig` from a browser bundle silently no-ops because `PUBLIC__`
|
|
267
|
+
* vars are not present in the client environment.
|
|
262
268
|
*
|
|
263
269
|
* Environment variables:
|
|
264
270
|
* - `PUBLIC__<path>` (optional): Override any config path using double underscore separators.
|
|
265
|
-
* e.g. `
|
|
266
|
-
* -
|
|
267
|
-
*
|
|
271
|
+
* e.g. `PUBLIC__app__some__nested__value=abc123` maps to `config.app.some.nested.value`
|
|
272
|
+
* - JSON values are parsed optimistically: numbers, booleans, arrays, and objects all work.
|
|
273
|
+
* `PUBLIC__app__features__providers=["A","B"]` parses to an array.
|
|
268
274
|
*
|
|
269
275
|
* @param config - The base configuration object with all defaults
|
|
270
276
|
* @param options - Optional settings (e.g., protectedPaths to prevent env var overrides)
|
|
@@ -277,10 +283,9 @@ const mergeEnvConfig = (env = typeof process !== "undefined" ? process.env : {},
|
|
|
277
283
|
* export default defineConfig({
|
|
278
284
|
* metadata: { projectName: 'My Store', projectSlug: 'my-store' },
|
|
279
285
|
* app: {
|
|
280
|
-
*
|
|
281
|
-
* defaultSiteId: 'RefArch',
|
|
286
|
+
* // template-specific shape
|
|
282
287
|
* },
|
|
283
|
-
* }, { protectedPaths: ['
|
|
288
|
+
* }, { protectedPaths: ['app__analytics'] });
|
|
284
289
|
*/
|
|
285
290
|
function defineConfig(config, options) {
|
|
286
291
|
return deepMerge(config, mergeEnvConfig(process.env, config, { protectedPaths: options?.protectedPaths }));
|
|
@@ -289,29 +294,19 @@ function defineConfig(config, options) {
|
|
|
289
294
|
//#endregion
|
|
290
295
|
//#region src/config/context.tsx
|
|
291
296
|
/**
|
|
292
|
-
* Router context for application configuration.
|
|
293
|
-
*
|
|
294
|
-
*
|
|
295
|
-
* Accessible in loaders, actions, and middleware via `context.get(appConfigContext)`.
|
|
297
|
+
* Router context for application configuration. Populated by the template's
|
|
298
|
+
* app-config middleware; read via `context.get(appConfigContext)` in loaders,
|
|
299
|
+
* actions, and other middleware. Returns the augmented `AppConfigShape`.
|
|
296
300
|
*/
|
|
297
301
|
const appConfigContext = createContext$1();
|
|
298
302
|
/**
|
|
299
|
-
* React context
|
|
303
|
+
* Internal React context backing `useConfig()`.
|
|
300
304
|
*
|
|
301
|
-
*
|
|
302
|
-
*
|
|
305
|
+
* Not exported from the public barrel — components must read config via
|
|
306
|
+
* `useConfig()` so the React tree has a single source of truth.
|
|
303
307
|
*/
|
|
304
308
|
const ConfigContext = createContext(null);
|
|
305
309
|
/**
|
|
306
|
-
* Extract the `app` section from a full config object.
|
|
307
|
-
*
|
|
308
|
-
* @param staticConfig - The full config object (output of `defineConfig()`)
|
|
309
|
-
* @returns The `app` section of the config
|
|
310
|
-
*/
|
|
311
|
-
function createAppConfig(staticConfig) {
|
|
312
|
-
return staticConfig.app;
|
|
313
|
-
}
|
|
314
|
-
/**
|
|
315
310
|
* React context provider for application configuration.
|
|
316
311
|
*
|
|
317
312
|
* Wrap your component tree with this to enable `useConfig()` in child components.
|
|
@@ -327,13 +322,10 @@ function ConfigProvider({ config, children }) {
|
|
|
327
322
|
//#endregion
|
|
328
323
|
//#region src/config/get-config.ts
|
|
329
324
|
/**
|
|
330
|
-
* Get configuration in loaders, actions, and utilities.
|
|
331
|
-
*
|
|
332
|
-
*
|
|
333
|
-
*
|
|
334
|
-
*
|
|
335
|
-
* @param context - Router context for server loaders/actions
|
|
336
|
-
* @returns App configuration
|
|
325
|
+
* Get configuration in loaders, actions, and utilities. Pass `context` on the
|
|
326
|
+
* server; omit it on the client (reads `window.__APP_CONFIG__`). Returns the
|
|
327
|
+
* augmented `AppConfigShape` — pass an explicit generic only for narrower or
|
|
328
|
+
* unrelated shapes (rare).
|
|
337
329
|
*/
|
|
338
330
|
function getConfig(context) {
|
|
339
331
|
if (context) {
|
|
@@ -345,11 +337,8 @@ function getConfig(context) {
|
|
|
345
337
|
throw new Error("Configuration not available. This can happen if:\n1. Server: Pass context parameter: getConfig(context)\n2. Client: Ensure window.__APP_CONFIG__ was injected during SSR\n3. React component: Use useConfig() hook instead of getConfig()");
|
|
346
338
|
}
|
|
347
339
|
/**
|
|
348
|
-
* Get configuration in React components
|
|
349
|
-
*
|
|
350
|
-
* Must use this hook (not getConfig) because React Context requires useContext().
|
|
351
|
-
*
|
|
352
|
-
* @returns App configuration
|
|
340
|
+
* Get configuration in React components (use this instead of `getConfig` —
|
|
341
|
+
* React Context requires `useContext`). Returns the augmented `AppConfigShape`.
|
|
353
342
|
*/
|
|
354
343
|
function useConfig() {
|
|
355
344
|
const config = useContext(ConfigContext);
|
|
@@ -358,75 +347,5 @@ function useConfig() {
|
|
|
358
347
|
}
|
|
359
348
|
|
|
360
349
|
//#endregion
|
|
361
|
-
|
|
362
|
-
/**
|
|
363
|
-
* Create app config middleware for both server and client.
|
|
364
|
-
*
|
|
365
|
-
* Follows the same factory pattern as `createSiteContextMiddleware`.
|
|
366
|
-
*
|
|
367
|
-
* The server middleware:
|
|
368
|
-
* - Validates required Commerce API fields on first request (one-time)
|
|
369
|
-
* - Sets `appConfigContext` in router context with `config.app`
|
|
370
|
-
*
|
|
371
|
-
* The client middleware:
|
|
372
|
-
* - Reads `window.__APP_CONFIG__` (injected during SSR)
|
|
373
|
-
* - Sets `appConfigContext` in router context
|
|
374
|
-
*
|
|
375
|
-
* Environment variables:
|
|
376
|
-
* - `SCAPI_PROXY_HOST` (optional): When set, skips `shortCode` validation
|
|
377
|
-
* (workspace environments route through a proxy that doesn't require shortCode)
|
|
378
|
-
* - `NODE_ENV` (optional): When set to 'test', skips validation entirely
|
|
379
|
-
*
|
|
380
|
-
* @param config - The full config object (output of `defineConfig()`)
|
|
381
|
-
* @returns Object with `server` and `client` middleware functions
|
|
382
|
-
*
|
|
383
|
-
* @example
|
|
384
|
-
* import { createAppConfigMiddleware } from '@salesforce/storefront-next-runtime/config';
|
|
385
|
-
* import config from '@/config/server';
|
|
386
|
-
*
|
|
387
|
-
* const appConfigMiddleware = createAppConfigMiddleware(config);
|
|
388
|
-
*
|
|
389
|
-
* export const middleware = [appConfigMiddleware.server, ...otherMiddleware];
|
|
390
|
-
* export const clientMiddleware = [appConfigMiddleware.client, ...otherClientMiddleware];
|
|
391
|
-
*/
|
|
392
|
-
function createAppConfigMiddleware(config) {
|
|
393
|
-
let validationRun = false;
|
|
394
|
-
function validateConfig() {
|
|
395
|
-
if (validationRun || process.env.NODE_ENV === "test") return;
|
|
396
|
-
const api = config.app.commerce?.api;
|
|
397
|
-
const required = {
|
|
398
|
-
clientId: api?.clientId ?? "",
|
|
399
|
-
organizationId: api?.organizationId ?? ""
|
|
400
|
-
};
|
|
401
|
-
if (!process.env.SCAPI_PROXY_HOST) required.shortCode = api?.shortCode ?? "";
|
|
402
|
-
const missing = Object.entries(required).filter(([_, value]) => !value).map(([key]) => key);
|
|
403
|
-
if (missing.length > 0) {
|
|
404
|
-
const envVarMap = {
|
|
405
|
-
clientId: "PUBLIC__app__commerce__api__clientId",
|
|
406
|
-
organizationId: "PUBLIC__app__commerce__api__organizationId",
|
|
407
|
-
shortCode: "PUBLIC__app__commerce__api__shortCode"
|
|
408
|
-
};
|
|
409
|
-
throw new Error(`Missing required Commerce API configuration: ${missing.join(", ")}\n\nSet these environment variables in your MRT deployment or .env file:\n${missing.map((key) => ` ${envVarMap[key]}=your-value`).join("\n")}\n\nExample .env file:\nPUBLIC__app__commerce__api__clientId=your-client-id\nPUBLIC__app__commerce__api__organizationId=your-org-id\nPUBLIC__app__commerce__api__shortCode=your-short-code\n\nSee docs/README-CONFIG.md for complete configuration documentation.`);
|
|
410
|
-
}
|
|
411
|
-
validationRun = true;
|
|
412
|
-
}
|
|
413
|
-
const server = ({ context }, next) => {
|
|
414
|
-
validateConfig();
|
|
415
|
-
context.set(appConfigContext, config.app);
|
|
416
|
-
return next();
|
|
417
|
-
};
|
|
418
|
-
const client = async ({ context }, next) => {
|
|
419
|
-
const appConfig = typeof window !== "undefined" ? window.__APP_CONFIG__ : void 0;
|
|
420
|
-
if (!appConfig) throw new Error("window.__APP_CONFIG__ not available. Check that server loader is injecting config into HTML via Layout component.");
|
|
421
|
-
context.set(appConfigContext, appConfig);
|
|
422
|
-
return next();
|
|
423
|
-
};
|
|
424
|
-
return {
|
|
425
|
-
server,
|
|
426
|
-
client
|
|
427
|
-
};
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
//#endregion
|
|
431
|
-
export { ConfigContext, ConfigProvider, appConfigContext, createAppConfig, createAppConfigMiddleware, deepMerge, defineConfig, extractValidPaths, getConfig, mergeEnvConfig, parseEnvValue, pathToObject, useConfig };
|
|
350
|
+
export { ConfigProvider, appConfigContext, defaultSecurityHeaders, defineConfig, getConfig, useConfig };
|
|
432
351
|
//# sourceMappingURL=config.js.map
|
package/dist/config.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.js","names":["result: Record<string, unknown>","configCurrent: unknown","paths: string[]","envVars: EnvVar[]","conflicts: Array<{ parent: string; child: string }>","merged: Record<string, unknown>","createRouterContext","required: Record<string, string>","envVarMap: Record<string, string>","server: MiddlewareFunction<Response>","client: MiddlewareFunction<Record<string, unknown>>"],"sources":["../src/config/utils.ts","../src/config/schema.ts","../src/config/context.tsx","../src/config/get-config.ts","../src/config/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 */\n\n/**\n * Type guard to check if value is a plain object (not array, null, or other types)\n */\nconst isPlainObject = (value: unknown): value is Record<string, unknown> => {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n};\n\n/**\n * Deep merge two objects, with source values overriding target values\n * Arrays are replaced, not merged\n *\n * @param target - The base object\n * @param source - The object with values to merge in\n * @returns A new merged object\n *\n * @example\n * deepMerge(\n * { a: { b: 1, c: 2 } },\n * { a: { b: 3, d: 4 } }\n * )\n * // Returns: { a: { b: 3, c: 2, d: 4 } }\n */\nexport const deepMerge = <T extends Record<string, unknown>>(target: T, source: Record<string, unknown>): T => {\n const result: Record<string, unknown> = { ...target };\n\n for (const key in source) {\n const sourceValue = source[key];\n const targetValue = result[key];\n\n if (isPlainObject(sourceValue) && isPlainObject(targetValue)) {\n result[key] = deepMerge(targetValue, sourceValue);\n } else {\n result[key] = sourceValue;\n }\n }\n\n return result as T;\n};\n\n/**\n * Convert a path string with double underscore separators to a nested object\n * Normalizes keys to match baseConfig casing (case-insensitive lookup, preserves baseConfig case)\n *\n * @param path - The path string (e.g., 'app__pages__cart__quantityUpdateDebounce')\n * @param value - The value to set at the path\n * @param baseConfig - Optional base config for case normalization\n * @returns A nested object\n *\n * @example\n * pathToObject('app__pages__cart__maxQuantity', 999)\n * // Returns: { app: { pages: { cart: { maxQuantity: 999 } } } }\n *\n * @example\n * // With baseConfig normalization:\n * pathToObject('APP__SITE__LOCALE', 'en-GB', { app: { site: { locale: 'en-GB' } } })\n * // Returns: { app: { site: { locale: 'en-GB' } } } (normalized to baseConfig casing)\n */\nexport const pathToObject = (\n path: string,\n value: unknown,\n baseConfig?: Record<string, unknown>\n): Record<string, unknown> => {\n const keys = path.split('__');\n const result: Record<string, unknown> = {};\n\n let current = result;\n let configCurrent: unknown = baseConfig;\n\n for (let i = 0; i < keys.length - 1; i++) {\n const key = keys[i];\n\n let normalizedKey = key;\n if (configCurrent && typeof configCurrent === 'object' && !Array.isArray(configCurrent)) {\n const actualKey = Object.keys(configCurrent).find((k) => k.toLowerCase() === key.toLowerCase());\n if (actualKey) {\n normalizedKey = actualKey;\n configCurrent = (configCurrent as Record<string, unknown>)[actualKey];\n } else {\n configCurrent = null;\n }\n }\n\n current[normalizedKey] = {};\n current = current[normalizedKey] as Record<string, unknown>;\n }\n\n const lastKey = keys[keys.length - 1];\n let normalizedLastKey = lastKey;\n if (configCurrent && typeof configCurrent === 'object' && !Array.isArray(configCurrent)) {\n const actualKey = Object.keys(configCurrent).find((k) => k.toLowerCase() === lastKey.toLowerCase());\n if (actualKey) {\n normalizedLastKey = actualKey;\n }\n }\n\n current[normalizedLastKey] = value;\n return result;\n};\n\n/**\n * Parse environment variable value with optimistic JSON parsing\n * Tries to parse as JSON first, falls back to string if invalid\n * Supports multi-line formatted JSON by normalizing whitespace before parsing\n *\n * @param varValue - The environment variable value\n * @param varName - Optional variable name for better error messages\n * @returns The parsed value (JSON type if valid JSON, otherwise string)\n *\n * @example\n * // Primitives\n * parseEnvValue('42') // → 42 (number)\n * parseEnvValue('true') // → true (boolean)\n * parseEnvValue('hello') // → 'hello' (string)\n *\n * @example\n * // Single-line JSON\n * parseEnvValue('[\"Apple\",\"Google\"]') // → ['Apple', 'Google'] (array)\n * parseEnvValue('{\"key\":\"value\"}') // → {key: 'value'} (object)\n *\n * @example\n * // Multi-line formatted JSON (whitespace normalized automatically)\n * parseEnvValue('[\n * {\"id\": \"en-GB\"},\n * {\"id\": \"fr-FR\"}\n * ]') // → [{id: 'en-GB'}, {id: 'fr-FR'}] (array)\n */\nexport const parseEnvValue = (varValue: string, varName?: string): unknown => {\n try {\n return JSON.parse(varValue);\n } catch {\n const trimmed = varValue.trim();\n if (trimmed.startsWith('{') || trimmed.startsWith('[')) {\n try {\n const normalized = varValue.replace(/\\s+/g, ' ').trim();\n return JSON.parse(normalized);\n } catch {\n if (process.env.NODE_ENV === 'development') {\n const preview = varValue.length > 50 ? `${varValue.substring(0, 50)}...` : varValue;\n const varInfo = varName ? ` in \"${varName}\"` : '';\n // eslint-disable-next-line no-console\n console.warn(\n `[Config Warning] Value${varInfo} looks like JSON but failed to parse: \"${preview}\". ` +\n `Using as string instead. Check for syntax errors if this was meant to be JSON.`\n );\n }\n }\n }\n return varValue;\n }\n};\n\n/**\n * Extract all valid paths from a config object (recursively traverses the object structure)\n * Returns paths in lowercase with double underscore separators\n *\n * @param obj - The config object to extract paths from\n * @param prefix - Current path prefix (used for recursion)\n * @returns Array of valid config paths\n *\n * @example\n * extractValidPaths({ app: { site: { locale: 'en-GB' } } })\n * // Returns: ['app__site__locale']\n */\nexport const extractValidPaths = (obj: unknown, prefix = ''): string[] => {\n if (!isPlainObject(obj)) {\n return prefix ? [prefix] : [];\n }\n\n const paths: string[] = [];\n for (const [key, value] of Object.entries(obj)) {\n const normalizedKey = key.toLowerCase();\n const currentPath = prefix ? `${prefix}__${normalizedKey}` : normalizedKey;\n\n if (isPlainObject(value)) {\n paths.push(currentPath); // Allow setting whole object (e.g. PUBLIC__app__commerceAgent)\n // Recursively extract paths from nested objects\n paths.push(...extractValidPaths(value, currentPath));\n } else {\n // Leaf node - this is a valid config path\n paths.push(currentPath);\n }\n }\n\n return paths;\n};\n\ninterface EnvVar {\n name: string;\n path: string;\n value: string;\n depth: number;\n}\n\n/**\n * Options for mergeEnvConfig\n */\nexport interface MergeEnvConfigOptions {\n /**\n * Config paths that cannot be overridden by environment variables.\n * Paths are matched case-insensitively with double underscore separators.\n * Any env var targeting a protected path or a sub-path of it will throw an error.\n *\n * @example ['app__engagement'] — prevents PUBLIC__app__engagement__* from being set via env\n */\n protectedPaths?: string[];\n}\n\n/**\n * Merge environment variables with PUBLIC__ prefix into config.\n *\n * Uses double underscore (__) to target nested config paths.\n * All PUBLIC__ prefixed variables are exposed to the client (bundled into window.__APP_CONFIG__).\n *\n * Server-only secrets should NEVER use this — read them directly from process.env in server code.\n *\n * Environment variables:\n * - `PUBLIC__<path>` (optional): Override any config path. e.g. `PUBLIC__app__commerce__api__clientId=abc123`\n * - `NODE_ENV` (optional): When set to 'development', enables conflict warnings for overlapping paths\n *\n * @param env - Environment variables object (defaults to process.env)\n * @param baseConfig - Optional base config for strict path validation and case normalization\n * @param options - Optional configuration including protected paths\n * @returns Object with overrides to merge into base config\n *\n * @example\n * // Environment variables:\n * // PUBLIC__app__commerce__api__clientId=abc123\n * // PUBLIC__app__pages__cart__quantityUpdateDebounce=1000\n * // PUBLIC__app__features__socialLogin__providers=[\"Apple\",\"Google\"]\n *\n * mergeEnvConfig()\n * // Returns:\n * // {\n * // app: {\n * // commerce: { api: { clientId: 'abc123' } },\n * // pages: { cart: { quantityUpdateDebounce: 1000 } },\n * // features: { socialLogin: { providers: ['Apple', 'Google'] } }\n * // }\n * // }\n */\nexport const mergeEnvConfig = (\n env: Record<string, string | undefined> = typeof process !== 'undefined' ? process.env : {},\n baseConfig?: Record<string, unknown>,\n options?: MergeEnvConfigOptions\n): Record<string, unknown> => {\n const PUBLIC_PREFIX = 'PUBLIC__';\n const MAX_VAR_NAME_LENGTH = 512; // MRT limit: 512 characters\n const MAX_TOTAL_VALUE_SIZE = 32 * 1024; // MRT limit: 32 KB\n const MAX_DEPTH = 10;\n\n const protectedPaths = options?.protectedPaths ?? [];\n const validPaths = baseConfig ? extractValidPaths(baseConfig) : [];\n\n const envVars: EnvVar[] = [];\n let totalValueSize = 0;\n\n for (const [varName, varValue] of Object.entries(env)) {\n if (varValue === undefined || varValue === null || !varName.startsWith(PUBLIC_PREFIX)) continue;\n\n if (varName.length > MAX_VAR_NAME_LENGTH) {\n throw new Error(\n `Environment variable name \"${varName}\" exceeds MRT limit of ${MAX_VAR_NAME_LENGTH} characters. ` +\n `Current length: ${varName.length} characters. ` +\n `Consider using shorter paths or consolidating configuration using JSON values.`\n );\n }\n\n const path = varName.substring(PUBLIC_PREFIX.length);\n\n if (!path) {\n throw new Error(\n `Invalid environment variable \"${varName}\": Path cannot be empty after PUBLIC__ prefix. ` +\n `Expected format: PUBLIC__path__to__value (e.g., PUBLIC__app__site__locale)`\n );\n }\n\n const depth = path.split('__').length;\n if (depth > MAX_DEPTH) {\n throw new Error(\n `Environment variable \"${varName}\" exceeds maximum path depth of ${MAX_DEPTH}. ` +\n `Current depth: ${depth}. ` +\n `Consider consolidating with JSON values or reducing nesting levels.`\n );\n }\n\n const normalizedPath = path.toLowerCase();\n const isProtected = protectedPaths.some(\n (protectedPath) => normalizedPath === protectedPath || normalizedPath.startsWith(`${protectedPath}__`)\n );\n\n if (isProtected) {\n throw new Error(\n `Environment variable \"${varName}\" attempts to override protected config path \"${path}\".\\n\\n` +\n `The engagement configuration cannot be overridden via environment variables. ` +\n `Update config.server.ts directly to change engagement settings.`\n );\n }\n\n if (baseConfig && validPaths.length > 0) {\n if (!validPaths.includes(normalizedPath)) {\n // eslint-disable-next-line no-console\n console.warn(\n `[Config Warning] Ignoring environment variable \"${varName}\": Config path \"${path}\" does not exist in config.server.ts.`\n );\n continue;\n }\n }\n\n totalValueSize += varValue.length;\n\n envVars.push({\n name: varName,\n path,\n value: varValue,\n depth: path.split('__').length,\n });\n }\n\n if (totalValueSize > MAX_TOTAL_VALUE_SIZE) {\n throw new Error(\n `Total size of PUBLIC__ environment variable values exceeds MRT limit of ${MAX_TOTAL_VALUE_SIZE} bytes (32 KB). ` +\n `Current size: ${totalValueSize} bytes. ` +\n `Consider consolidating configuration using JSON values to reduce the number of variables, ` +\n `or move non-essential configuration to defaults in config.server.ts.`\n );\n }\n\n envVars.sort((a, b) => a.depth - b.depth);\n\n const conflicts: Array<{ parent: string; child: string }> = [];\n for (let i = 0; i < envVars.length; i++) {\n for (let j = i + 1; j < envVars.length; j++) {\n const shorter = envVars[i].path;\n const longer = envVars[j].path;\n if (longer.startsWith(`${shorter}__`)) {\n conflicts.push({\n parent: envVars[i].name,\n child: envVars[j].name,\n });\n }\n }\n }\n\n if (conflicts.length > 0 && process.env.NODE_ENV === 'development') {\n // eslint-disable-next-line no-console\n console.warn(\n `[Config Warning] Conflicting environment variables detected. More specific paths will override parent paths:\\n${conflicts\n .map((c) => ` ${c.parent} ← overridden by → ${c.child}`)\n .join('\\n')}`\n );\n }\n\n let merged: Record<string, unknown> = {};\n\n for (const envVar of envVars) {\n try {\n const parsedValue = parseEnvValue(envVar.value, envVar.name);\n const pathObject = pathToObject(envVar.path, parsedValue, baseConfig);\n merged = deepMerge(merged, pathObject);\n } catch (error) {\n throw new Error(\n `Failed to process environment variable \"${envVar.name}\" with value \"${envVar.value}\": ` +\n `${error instanceof Error ? error.message : String(error)}`\n );\n }\n }\n\n return merged;\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 { deepMerge, mergeEnvConfig } from './utils';\n\n/**\n * Base configuration type for storefront-next projects.\n *\n * Generic parameter `App` represents the template's application config shape.\n * The SDK does not prescribe what fields `app` must contain — templates define\n * their own `AppConfig` type with SCAPI credentials, pages, features, etc.\n * and pass it as `BaseConfig<AppConfig>`.\n *\n * The SDK accesses specific `app` fields (e.g., `commerce.api.clientId`) at\n * runtime via the middleware validation, not via compile-time type constraints.\n *\n * @typeParam App - The template's application config shape (defaults to `Record<string, unknown>`)\n *\n * @example\n * // In the template's types file:\n * type AppConfig = { commerce: { api: {...} }; pages: {...}; features: {...} };\n * type Config = BaseConfig<AppConfig>;\n *\n * // In config.server.ts:\n * export default defineConfig<Config>({ metadata: {...}, app: {...} });\n */\nexport type BaseConfig<App extends Record<string, unknown> = Record<string, unknown>> = {\n metadata: {\n projectName: string;\n projectSlug: string;\n };\n runtime?: {\n defaultMrtProject?: string;\n defaultMrtTarget?: string;\n ssrOnly?: string[];\n ssrShared?: string[];\n ssrParameters?: Record<string, string | number | boolean>;\n };\n app: App;\n};\n\nexport interface DefineConfigOptions {\n /**\n * Config paths that cannot be overridden by environment variables.\n * Paths use double underscore separators and are matched case-insensitively.\n *\n * @example ['app__engagement'] — prevents PUBLIC__app__engagement__* from being set via env\n */\n protectedPaths?: string[];\n}\n\n/**\n * Define a type-safe storefront configuration with IDE autocomplete.\n *\n * Automatically merges `PUBLIC__` prefixed environment variables into the config\n * at load time. Validates env vars against the base config structure (strict mode —\n * only allows overriding existing paths).\n *\n * Environment variables:\n * - `PUBLIC__<path>` (optional): Override any config path using double underscore separators.\n * e.g. `PUBLIC__app__commerce__api__clientId=abc123` maps to `config.app.commerce.api.clientId`\n * - `PUBLIC__app__pages__cart__quantityUpdateDebounce=1000` maps to a number (optimistic JSON parsing)\n * - `PUBLIC__app__features__socialLogin__providers=[\"Apple\",\"Google\"]` maps to an array\n *\n * @param config - The base configuration object with all defaults\n * @param options - Optional settings (e.g., protectedPaths to prevent env var overrides)\n * @returns The config with environment variable overrides merged in\n *\n * @example\n * // In config.server.ts:\n * import { defineConfig } from '@salesforce/storefront-next-runtime/config';\n *\n * export default defineConfig({\n * metadata: { projectName: 'My Store', projectSlug: 'my-store' },\n * app: {\n * commerce: { api: { clientId: '', organizationId: '', shortCode: '' }, sites: [] },\n * defaultSiteId: 'RefArch',\n * },\n * }, { protectedPaths: ['app__engagement'] });\n */\nexport function defineConfig<T extends BaseConfig>(config: T, options?: DefineConfigOptions): T {\n const envOverrides = mergeEnvConfig(process.env, config as unknown as Record<string, unknown>, {\n protectedPaths: options?.protectedPaths,\n });\n return deepMerge(config, envOverrides);\n}\n","/**\n * Copyright 2026 Salesforce, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Configuration Context and Provider\n *\n * Provides configuration access throughout the application using React Router's\n * context system. Supports both server and client rendering with proper hydration.\n */\n\nimport { createContext, type ReactNode } from 'react';\nimport { createContext as createRouterContext } from 'react-router';\nimport type { BaseConfig } from './schema';\n\n/**\n * Router context for application configuration.\n *\n * Populated by `createAppConfigMiddleware` with the `app` section of config.\n * Accessible in loaders, actions, and middleware via `context.get(appConfigContext)`.\n */\n// eslint-disable-next-line react-refresh/only-export-components\nexport const appConfigContext = createRouterContext<Record<string, unknown>>();\n\n/**\n * React context for application configuration.\n *\n * Used by the `useConfig()` hook in React components.\n * Populated by `ConfigProvider` in the component tree.\n */\n// eslint-disable-next-line react-refresh/only-export-components\nexport const ConfigContext = createContext<Record<string, unknown> | null>(null);\n\n/**\n * Extract the `app` section from a full config object.\n *\n * @param staticConfig - The full config object (output of `defineConfig()`)\n * @returns The `app` section of the config\n */\n// eslint-disable-next-line react-refresh/only-export-components\nexport function createAppConfig<T extends BaseConfig>(staticConfig: T): T['app'] {\n return staticConfig.app;\n}\n\ninterface ConfigProviderProps {\n config: Record<string, unknown>;\n children: ReactNode;\n}\n\n/**\n * React context provider for application configuration.\n *\n * Wrap your component tree with this to enable `useConfig()` in child components.\n * Typically placed in the root layout component.\n */\nexport function ConfigProvider({ config, children }: ConfigProviderProps) {\n return <ConfigContext.Provider value={config}>{children}</ConfigContext.Provider>;\n}\n","/**\n * Copyright 2026 Salesforce, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Configuration access for loaders, actions, utilities, and React components.\n *\n * Two functions:\n * - `getConfig()` — For loaders, actions, and utilities\n * - `useConfig()` — For React components (hook required for React Context)\n */\n\nimport { useContext } from 'react';\nimport type { RouterContextProvider } from 'react-router';\nimport { ConfigContext, appConfigContext } from './context';\n\ndeclare global {\n interface Window {\n __APP_CONFIG__?: Record<string, unknown>;\n }\n}\n\n/**\n * Get configuration in loaders, actions, and utilities.\n *\n * Pass context parameter in server loaders/actions.\n * Omit context parameter in client loaders (uses window.__APP_CONFIG__).\n *\n * @param context - Router context for server loaders/actions\n * @returns App configuration\n */\nexport function getConfig<T extends Record<string, unknown> = Record<string, unknown>>(\n context?: Readonly<RouterContextProvider>\n): T {\n if (context) {\n const config = context.get(appConfigContext);\n if (!config) {\n throw new Error(\n 'Configuration not available in router context. ' +\n 'Ensure appConfigMiddleware.server runs before other middleware.'\n );\n }\n return config as T;\n }\n\n if (typeof window !== 'undefined' && window.__APP_CONFIG__) {\n return window.__APP_CONFIG__ as T;\n }\n\n throw new Error(\n 'Configuration not available. This can happen if:\\n' +\n '1. Server: Pass context parameter: getConfig(context)\\n' +\n '2. Client: Ensure window.__APP_CONFIG__ was injected during SSR\\n' +\n '3. React component: Use useConfig() hook instead of getConfig()'\n );\n}\n\n/**\n * Get configuration in React components.\n *\n * Must use this hook (not getConfig) because React Context requires useContext().\n *\n * @returns App configuration\n */\nexport function useConfig<T extends Record<string, unknown> = Record<string, unknown>>(): T {\n const config = useContext(ConfigContext);\n if (!config) {\n throw new Error(\n 'useConfig must be used within ConfigProvider. ' +\n 'Ensure ConfigProvider wraps your component tree in root.tsx'\n );\n }\n return config as T;\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 { MiddlewareFunction } from 'react-router';\nimport { appConfigContext } from './context';\nimport type { BaseConfig } from './schema';\n\n/**\n * Create app config middleware for both server and client.\n *\n * Follows the same factory pattern as `createSiteContextMiddleware`.\n *\n * The server middleware:\n * - Validates required Commerce API fields on first request (one-time)\n * - Sets `appConfigContext` in router context with `config.app`\n *\n * The client middleware:\n * - Reads `window.__APP_CONFIG__` (injected during SSR)\n * - Sets `appConfigContext` in router context\n *\n * Environment variables:\n * - `SCAPI_PROXY_HOST` (optional): When set, skips `shortCode` validation\n * (workspace environments route through a proxy that doesn't require shortCode)\n * - `NODE_ENV` (optional): When set to 'test', skips validation entirely\n *\n * @param config - The full config object (output of `defineConfig()`)\n * @returns Object with `server` and `client` middleware functions\n *\n * @example\n * import { createAppConfigMiddleware } from '@salesforce/storefront-next-runtime/config';\n * import config from '@/config/server';\n *\n * const appConfigMiddleware = createAppConfigMiddleware(config);\n *\n * export const middleware = [appConfigMiddleware.server, ...otherMiddleware];\n * export const clientMiddleware = [appConfigMiddleware.client, ...otherClientMiddleware];\n */\nexport function createAppConfigMiddleware<T extends BaseConfig>(\n config: T\n): {\n server: MiddlewareFunction<Response>;\n client: MiddlewareFunction<Record<string, unknown>>;\n} {\n let validationRun = false;\n\n function validateConfig(): void {\n if (validationRun || process.env.NODE_ENV === 'test') {\n return;\n }\n\n const api = (config.app as Record<string, unknown> & { commerce?: { api?: Record<string, string> } }).commerce\n ?.api;\n\n const required: Record<string, string> = {\n clientId: api?.clientId ?? '',\n organizationId: api?.organizationId ?? '',\n };\n\n if (!process.env.SCAPI_PROXY_HOST) {\n required.shortCode = api?.shortCode ?? '';\n }\n\n const missing = Object.entries(required)\n .filter(([_, value]) => !value)\n .map(([key]) => key);\n\n if (missing.length > 0) {\n const envVarMap: Record<string, string> = {\n clientId: 'PUBLIC__app__commerce__api__clientId',\n organizationId: 'PUBLIC__app__commerce__api__organizationId',\n shortCode: 'PUBLIC__app__commerce__api__shortCode',\n };\n\n throw new Error(\n `Missing required Commerce API configuration: ${missing.join(', ')}\\n\\n` +\n `Set these environment variables in your MRT deployment or .env file:\\n${missing\n .map((key) => ` ${envVarMap[key]}=your-value`)\n .join('\\n')}\\n\\n` +\n `Example .env file:\\n` +\n `PUBLIC__app__commerce__api__clientId=your-client-id\\n` +\n `PUBLIC__app__commerce__api__organizationId=your-org-id\\n` +\n `PUBLIC__app__commerce__api__shortCode=your-short-code\\n\\n` +\n `See docs/README-CONFIG.md for complete configuration documentation.`\n );\n }\n\n validationRun = true;\n }\n\n const server: MiddlewareFunction<Response> = ({ context }, next) => {\n validateConfig();\n context.set(appConfigContext, config.app);\n return next();\n };\n\n const client: MiddlewareFunction<Record<string, unknown>> = async ({ context }, next) => {\n const appConfig = typeof window !== 'undefined' ? window.__APP_CONFIG__ : undefined;\n\n if (!appConfig) {\n throw new Error(\n 'window.__APP_CONFIG__ not available. ' +\n 'Check that server loader is injecting config into HTML via Layout component.'\n );\n }\n\n context.set(appConfigContext, appConfig);\n\n return next();\n };\n\n return { server, client };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"config.js","names":["result: Record<string, unknown>","configCurrent: unknown","paths: string[]","envVars: EnvVar[]","conflicts: Array<{ parent: string; child: string }>","merged: Record<string, unknown>","createRouterContext"],"sources":["../src/config/utils.ts","../src/config/schema.ts","../src/config/context.tsx","../src/config/get-config.ts"],"sourcesContent":["/**\n * Copyright 2026 Salesforce, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Type guard to check if value is a plain object (not array, null, or other types)\n */\nconst isPlainObject = (value: unknown): value is Record<string, unknown> => {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n};\n\n/**\n * Deep merge two objects, with source values overriding target values\n * Arrays are replaced, not merged\n *\n * @param target - The base object\n * @param source - The object with values to merge in\n * @returns A new merged object\n *\n * @example\n * deepMerge(\n * { a: { b: 1, c: 2 } },\n * { a: { b: 3, d: 4 } }\n * )\n * // Returns: { a: { b: 3, c: 2, d: 4 } }\n */\nexport const deepMerge = <T extends Record<string, unknown>>(target: T, source: Record<string, unknown>): T => {\n const result: Record<string, unknown> = { ...target };\n\n for (const key in source) {\n const sourceValue = source[key];\n const targetValue = result[key];\n\n if (isPlainObject(sourceValue) && isPlainObject(targetValue)) {\n result[key] = deepMerge(targetValue, sourceValue);\n } else {\n result[key] = sourceValue;\n }\n }\n\n return result as T;\n};\n\n/**\n * Convert a path string with double underscore separators to a nested object\n * Normalizes keys to match baseConfig casing (case-insensitive lookup, preserves baseConfig case)\n *\n * @param path - The path string (e.g., 'app__pages__cart__quantityUpdateDebounce')\n * @param value - The value to set at the path\n * @param baseConfig - Optional base config for case normalization\n * @returns A nested object\n *\n * @example\n * pathToObject('app__pages__cart__maxQuantity', 999)\n * // Returns: { app: { pages: { cart: { maxQuantity: 999 } } } }\n *\n * @example\n * // With baseConfig normalization:\n * pathToObject('APP__SITE__LOCALE', 'en-GB', { app: { site: { locale: 'en-GB' } } })\n * // Returns: { app: { site: { locale: 'en-GB' } } } (normalized to baseConfig casing)\n */\nexport const pathToObject = (\n path: string,\n value: unknown,\n baseConfig?: Record<string, unknown>\n): Record<string, unknown> => {\n const keys = path.split('__');\n const result: Record<string, unknown> = {};\n\n let current = result;\n let configCurrent: unknown = baseConfig;\n\n for (let i = 0; i < keys.length - 1; i++) {\n const key = keys[i];\n\n let normalizedKey = key;\n if (configCurrent && typeof configCurrent === 'object' && !Array.isArray(configCurrent)) {\n const actualKey = Object.keys(configCurrent).find((k) => k.toLowerCase() === key.toLowerCase());\n if (actualKey) {\n normalizedKey = actualKey;\n configCurrent = (configCurrent as Record<string, unknown>)[actualKey];\n } else {\n configCurrent = null;\n }\n }\n\n current[normalizedKey] = {};\n current = current[normalizedKey] as Record<string, unknown>;\n }\n\n const lastKey = keys[keys.length - 1];\n let normalizedLastKey = lastKey;\n if (configCurrent && typeof configCurrent === 'object' && !Array.isArray(configCurrent)) {\n const actualKey = Object.keys(configCurrent).find((k) => k.toLowerCase() === lastKey.toLowerCase());\n if (actualKey) {\n normalizedLastKey = actualKey;\n }\n }\n\n current[normalizedLastKey] = value;\n return result;\n};\n\n/**\n * Parse environment variable value with optimistic JSON parsing\n * Tries to parse as JSON first, falls back to string if invalid\n * Supports multi-line formatted JSON by normalizing whitespace before parsing\n *\n * @param varValue - The environment variable value\n * @param varName - Optional variable name for better error messages\n * @returns The parsed value (JSON type if valid JSON, otherwise string)\n *\n * @example\n * // Primitives\n * parseEnvValue('42') // → 42 (number)\n * parseEnvValue('true') // → true (boolean)\n * parseEnvValue('hello') // → 'hello' (string)\n *\n * @example\n * // Single-line JSON\n * parseEnvValue('[\"Apple\",\"Google\"]') // → ['Apple', 'Google'] (array)\n * parseEnvValue('{\"key\":\"value\"}') // → {key: 'value'} (object)\n *\n * @example\n * // Multi-line formatted JSON (whitespace normalized automatically)\n * parseEnvValue('[\n * {\"id\": \"en-GB\"},\n * {\"id\": \"fr-FR\"}\n * ]') // → [{id: 'en-GB'}, {id: 'fr-FR'}] (array)\n */\nexport const parseEnvValue = (varValue: string, varName?: string): unknown => {\n try {\n return JSON.parse(varValue);\n } catch {\n const trimmed = varValue.trim();\n if (trimmed.startsWith('{') || trimmed.startsWith('[')) {\n try {\n const normalized = varValue.replace(/\\s+/g, ' ').trim();\n return JSON.parse(normalized);\n } catch {\n if (process.env.NODE_ENV === 'development') {\n const preview = varValue.length > 50 ? `${varValue.substring(0, 50)}...` : varValue;\n const varInfo = varName ? ` in \"${varName}\"` : '';\n // eslint-disable-next-line no-console\n console.warn(\n `[Config Warning] Value${varInfo} looks like JSON but failed to parse: \"${preview}\". ` +\n `Using as string instead. Check for syntax errors if this was meant to be JSON.`\n );\n }\n }\n }\n return varValue;\n }\n};\n\n/**\n * Extract all valid paths from a config object (recursively traverses the object structure)\n * Returns paths in lowercase with double underscore separators\n *\n * @param obj - The config object to extract paths from\n * @param prefix - Current path prefix (used for recursion)\n * @returns Array of valid config paths\n *\n * @example\n * extractValidPaths({ app: { site: { locale: 'en-GB' } } })\n * // Returns: ['app__site__locale']\n */\nexport const extractValidPaths = (obj: unknown, prefix = ''): string[] => {\n if (!isPlainObject(obj)) {\n return prefix ? [prefix] : [];\n }\n\n const paths: string[] = [];\n for (const [key, value] of Object.entries(obj)) {\n const normalizedKey = key.toLowerCase();\n const currentPath = prefix ? `${prefix}__${normalizedKey}` : normalizedKey;\n\n if (isPlainObject(value)) {\n paths.push(currentPath); // Allow setting whole nested object via JSON env var\n // Recursively extract paths from nested objects\n paths.push(...extractValidPaths(value, currentPath));\n } else {\n // Leaf node - this is a valid config path\n paths.push(currentPath);\n }\n }\n\n return paths;\n};\n\ninterface EnvVar {\n name: string;\n path: string;\n value: string;\n depth: number;\n}\n\n/**\n * Options for mergeEnvConfig\n */\nexport interface MergeEnvConfigOptions {\n /**\n * Config paths that cannot be overridden by environment variables.\n * Paths are matched case-insensitively with double underscore separators.\n * Any env var targeting a protected path or a sub-path of it will throw an error.\n *\n * @example ['app__engagement'] — prevents PUBLIC__app__engagement__* from being set via env\n */\n protectedPaths?: string[];\n}\n\n/**\n * Merge environment variables with PUBLIC__ prefix into config.\n *\n * Uses double underscore (__) to target nested config paths.\n * All PUBLIC__ prefixed variables are exposed to the client (bundled into window.__APP_CONFIG__).\n *\n * Server-only secrets should NEVER use this — read them directly from process.env in server code.\n *\n * Environment variables:\n * - `PUBLIC__<path>` (optional): Override any config path. e.g. `PUBLIC__app__commerce__api__clientId=abc123`\n * - `NODE_ENV` (optional): When set to 'development', enables conflict warnings for overlapping paths\n *\n * @param env - Environment variables object (defaults to process.env)\n * @param baseConfig - Optional base config for strict path validation and case normalization\n * @param options - Optional configuration including protected paths\n * @returns Object with overrides to merge into base config\n *\n * @example\n * // Environment variables (template-specific paths shown):\n * // PUBLIC__app__some__nested__value=abc123\n * // PUBLIC__app__some__numericKnob=1000\n * // PUBLIC__app__some__listKnob=[\"A\",\"B\"]\n *\n * mergeEnvConfig()\n * // Returns:\n * // {\n * // app: {\n * // some: {\n * // nested: { value: 'abc123' },\n * // numericKnob: 1000,\n * // listKnob: ['A', 'B']\n * // }\n * // }\n * // }\n */\nexport const mergeEnvConfig = (\n env: Record<string, string | undefined> = typeof process !== 'undefined' ? process.env : {},\n baseConfig?: Record<string, unknown>,\n options?: MergeEnvConfigOptions\n): Record<string, unknown> => {\n const PUBLIC_PREFIX = 'PUBLIC__';\n const MAX_VAR_NAME_LENGTH = 512; // MRT limit: 512 characters\n const MAX_TOTAL_VALUE_SIZE = 32 * 1024; // MRT limit: 32 KB\n const MAX_DEPTH = 10;\n\n const protectedPaths = options?.protectedPaths ?? [];\n const validPaths = baseConfig ? extractValidPaths(baseConfig) : [];\n\n const envVars: EnvVar[] = [];\n let totalValueSize = 0;\n\n for (const [varName, varValue] of Object.entries(env)) {\n if (varValue === undefined || varValue === null || !varName.startsWith(PUBLIC_PREFIX)) continue;\n\n if (varName.length > MAX_VAR_NAME_LENGTH) {\n throw new Error(\n `Environment variable name \"${varName}\" exceeds MRT limit of ${MAX_VAR_NAME_LENGTH} characters. ` +\n `Current length: ${varName.length} characters. ` +\n `Consider using shorter paths or consolidating configuration using JSON values.`\n );\n }\n\n const path = varName.substring(PUBLIC_PREFIX.length);\n\n if (!path) {\n throw new Error(\n `Invalid environment variable \"${varName}\": Path cannot be empty after PUBLIC__ prefix. ` +\n `Expected format: PUBLIC__path__to__value (e.g., PUBLIC__app__site__locale)`\n );\n }\n\n const depth = path.split('__').length;\n if (depth > MAX_DEPTH) {\n throw new Error(\n `Environment variable \"${varName}\" exceeds maximum path depth of ${MAX_DEPTH}. ` +\n `Current depth: ${depth}. ` +\n `Consider consolidating with JSON values or reducing nesting levels.`\n );\n }\n\n const normalizedPath = path.toLowerCase();\n const isProtected = protectedPaths.some(\n (protectedPath) => normalizedPath === protectedPath || normalizedPath.startsWith(`${protectedPath}__`)\n );\n\n if (isProtected) {\n throw new Error(\n `Environment variable \"${varName}\" attempts to override protected config path \"${path}\".\\n\\n` +\n `Protected paths cannot be overridden via environment variables. ` +\n `Update config.server.ts directly, or remove the path from \\`protectedPaths\\` if env override is intended.`\n );\n }\n\n if (baseConfig && validPaths.length > 0) {\n if (!validPaths.includes(normalizedPath)) {\n // eslint-disable-next-line no-console\n console.warn(\n `[Config Warning] Ignoring environment variable \"${varName}\": Config path \"${path}\" does not exist in config.server.ts.`\n );\n continue;\n }\n }\n\n totalValueSize += varValue.length;\n\n envVars.push({\n name: varName,\n path,\n value: varValue,\n depth: path.split('__').length,\n });\n }\n\n if (totalValueSize > MAX_TOTAL_VALUE_SIZE) {\n throw new Error(\n `Total size of PUBLIC__ environment variable values exceeds MRT limit of ${MAX_TOTAL_VALUE_SIZE} bytes (32 KB). ` +\n `Current size: ${totalValueSize} bytes. ` +\n `Consider consolidating configuration using JSON values to reduce the number of variables, ` +\n `or move non-essential configuration to defaults in config.server.ts.`\n );\n }\n\n envVars.sort((a, b) => a.depth - b.depth);\n\n const conflicts: Array<{ parent: string; child: string }> = [];\n for (let i = 0; i < envVars.length; i++) {\n for (let j = i + 1; j < envVars.length; j++) {\n const shorter = envVars[i].path;\n const longer = envVars[j].path;\n if (longer.startsWith(`${shorter}__`)) {\n conflicts.push({\n parent: envVars[i].name,\n child: envVars[j].name,\n });\n }\n }\n }\n\n if (conflicts.length > 0 && process.env.NODE_ENV === 'development') {\n // eslint-disable-next-line no-console\n console.warn(\n `[Config Warning] Conflicting environment variables detected. More specific paths will override parent paths:\\n${conflicts\n .map((c) => ` ${c.parent} ← overridden by → ${c.child}`)\n .join('\\n')}`\n );\n }\n\n let merged: Record<string, unknown> = {};\n\n for (const envVar of envVars) {\n try {\n const parsedValue = parseEnvValue(envVar.value, envVar.name);\n const pathObject = pathToObject(envVar.path, parsedValue, baseConfig);\n merged = deepMerge(merged, pathObject);\n } catch (error) {\n throw new Error(\n `Failed to process environment variable \"${envVar.name}\" with value \"${envVar.value}\": ` +\n `${error instanceof Error ? error.message : String(error)}`\n );\n }\n }\n\n return merged;\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 { deepMerge, mergeEnvConfig } from './utils';\n\n/**\n * Base configuration type for storefront-next projects.\n *\n * Generic parameter `App` represents the template's application config shape.\n * The SDK does not prescribe what fields `app` must contain — templates define\n * their own `AppConfig` type and pass it as `BaseConfig<AppConfig>`.\n *\n * Validation of `app` (e.g., required credentials, required collections) is\n * the template's responsibility, typically handled in its server middleware.\n *\n * @typeParam App - The template's application config shape (defaults to `Record<string, unknown>`)\n *\n * @example\n * // In the template's types file:\n * type AppConfig = { ... };\n * type Config = BaseConfig<AppConfig>;\n *\n * // In config.server.ts:\n * export default defineConfig<Config>({ metadata: {...}, app: {...} });\n */\nexport type BaseConfig<App extends Record<string, unknown> = Record<string, unknown>> = {\n metadata: {\n projectName: string;\n projectSlug: string;\n };\n runtime?: {\n defaultMrtProject?: string;\n defaultMrtTarget?: string;\n ssrOnly?: string[];\n ssrShared?: string[];\n ssrParameters?: Record<string, string | number | boolean>;\n };\n app: App;\n};\n\nexport interface DefineConfigOptions {\n /**\n * Config paths that cannot be overridden by environment variables.\n * Paths use double underscore separators and are matched case-insensitively.\n * Any env var targeting a protected path or a sub-path of it will throw.\n *\n * @example ['app__analytics'] — prevents PUBLIC__app__analytics__* from being set via env\n */\n protectedPaths?: string[];\n}\n\n/**\n * Define a type-safe storefront configuration with IDE autocomplete.\n *\n * Reads `process.env` at call time and merges any `PUBLIC__`-prefixed\n * variables into the config (validated against the base config structure —\n * env vars targeting paths that don't exist in the base config are ignored\n * with a warning). This is a server-only side effect by design; calling\n * `defineConfig` from a browser bundle silently no-ops because `PUBLIC__`\n * vars are not present in the client environment.\n *\n * Environment variables:\n * - `PUBLIC__<path>` (optional): Override any config path using double underscore separators.\n * e.g. `PUBLIC__app__some__nested__value=abc123` maps to `config.app.some.nested.value`\n * - JSON values are parsed optimistically: numbers, booleans, arrays, and objects all work.\n * `PUBLIC__app__features__providers=[\"A\",\"B\"]` parses to an array.\n *\n * @param config - The base configuration object with all defaults\n * @param options - Optional settings (e.g., protectedPaths to prevent env var overrides)\n * @returns The config with environment variable overrides merged in\n *\n * @example\n * // In config.server.ts:\n * import { defineConfig } from '@salesforce/storefront-next-runtime/config';\n *\n * export default defineConfig({\n * metadata: { projectName: 'My Store', projectSlug: 'my-store' },\n * app: {\n * // template-specific shape\n * },\n * }, { protectedPaths: ['app__analytics'] });\n */\nexport function defineConfig<T extends BaseConfig>(config: T, options?: DefineConfigOptions): T {\n const envOverrides = mergeEnvConfig(process.env, config as unknown as Record<string, unknown>, {\n protectedPaths: options?.protectedPaths,\n });\n return deepMerge(config, envOverrides);\n}\n","/**\n * Copyright 2026 Salesforce, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Configuration Context and Provider\n *\n * Provides configuration access throughout the application using React Router's\n * context system. Supports both server and client rendering with proper hydration.\n *\n * `ConfigContext` is intentionally not part of the public surface — consumers\n * must read configuration through `useConfig()` so a single React context owns\n * the value. `appConfigContext` is the public router-context handle for\n * loaders/actions/middleware.\n */\n\nimport { createContext, type ReactNode } from 'react';\nimport { createContext as createRouterContext } from 'react-router';\n\n/**\n * Augmentation hook for typing `getConfig()` / `useConfig()` /\n * `appConfigContext`. Templates augment once via `declare module` so call\n * sites don't need a per-call generic. Without augmentation, property\n * accesses type to `unknown`. See README-CONFIG.md for the augmentation\n * snippet and the multi-template caveat.\n */\nexport interface AppConfigShape {\n [key: string]: unknown;\n}\n\n/**\n * Router context for application configuration. Populated by the template's\n * app-config middleware; read via `context.get(appConfigContext)` in loaders,\n * actions, and other middleware. Returns the augmented `AppConfigShape`.\n */\n// eslint-disable-next-line react-refresh/only-export-components\nexport const appConfigContext = createRouterContext<AppConfigShape>();\n\n/**\n * Internal React context backing `useConfig()`.\n *\n * Not exported from the public barrel — components must read config via\n * `useConfig()` so the React tree has a single source of truth.\n */\n// eslint-disable-next-line react-refresh/only-export-components\nexport const ConfigContext = createContext<AppConfigShape | null>(null);\n\ninterface ConfigProviderProps {\n config: AppConfigShape;\n children: ReactNode;\n}\n\n/**\n * React context provider for application configuration.\n *\n * Wrap your component tree with this to enable `useConfig()` in child components.\n * Typically placed in the root layout component.\n */\nexport function ConfigProvider({ config, children }: ConfigProviderProps) {\n return <ConfigContext.Provider value={config}>{children}</ConfigContext.Provider>;\n}\n","/**\n * Copyright 2026 Salesforce, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Configuration access for loaders, actions, utilities, and React components.\n *\n * - `getConfig()` — for loaders, actions, and utilities (pass `context` on the server)\n * - `useConfig()` — for React components (hook required for React Context)\n *\n * Both return the resolved `app` slice typed as `AppConfigShape`. Templates\n * augment `AppConfigShape` once to get typed access without a per-call generic.\n */\n\nimport { useContext } from 'react';\nimport type { RouterContextProvider } from 'react-router';\nimport { ConfigContext, appConfigContext, type AppConfigShape } from './context';\n\n// Re-export so consumers can `import type { AppConfigShape } from '.../config'`\n// from the package barrel without reaching into `./context`.\nexport type { AppConfigShape };\n\ndeclare global {\n interface Window {\n __APP_CONFIG__?: Record<string, unknown>;\n }\n}\n\n/**\n * Get configuration in loaders, actions, and utilities. Pass `context` on the\n * server; omit it on the client (reads `window.__APP_CONFIG__`). Returns the\n * augmented `AppConfigShape` — pass an explicit generic only for narrower or\n * unrelated shapes (rare).\n */\nexport function getConfig<T extends Record<string, unknown> = AppConfigShape>(\n context?: Readonly<RouterContextProvider>\n): T {\n if (context) {\n const config = context.get(appConfigContext);\n if (!config) {\n throw new Error(\n 'Configuration not available in router context. ' +\n 'Ensure appConfigMiddleware.server runs before other middleware.'\n );\n }\n return config as T;\n }\n\n if (typeof window !== 'undefined' && window.__APP_CONFIG__) {\n return window.__APP_CONFIG__ as T;\n }\n\n throw new Error(\n 'Configuration not available. This can happen if:\\n' +\n '1. Server: Pass context parameter: getConfig(context)\\n' +\n '2. Client: Ensure window.__APP_CONFIG__ was injected during SSR\\n' +\n '3. React component: Use useConfig() hook instead of getConfig()'\n );\n}\n\n/**\n * Get configuration in React components (use this instead of `getConfig` —\n * React Context requires `useContext`). Returns the augmented `AppConfigShape`.\n */\nexport function useConfig<T extends Record<string, unknown> = AppConfigShape>(): T {\n const config = useContext(ConfigContext);\n if (!config) {\n throw new Error(\n 'useConfig must be used within ConfigProvider. ' +\n 'Ensure ConfigProvider wraps your component tree in root.tsx'\n );\n }\n return config as T;\n}\n"],"mappings":"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"}
|
package/dist/data-store.d.ts
CHANGED
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { a as getCustomGlobalPreferences, n as DEFAULT_CUSTOM_GLOBAL_PREFERENCES_KEY, r as customGlobalPreferencesContext, t as CustomGlobalPreferences } from "./custom-global-preferences.js";
|
|
3
|
-
import { a as getGcpApiKey, n as GcpPreferences, o as getGcpPreferences, r as gcpPreferencesContext, t as DEFAULT_GCP_PREFERENCES_KEY } from "./gcp-preferences.js";
|
|
4
|
-
import * as react_router12 from "react-router";
|
|
1
|
+
import * as react_router3 from "react-router";
|
|
5
2
|
import { MiddlewareFunction, RouterContextProvider, createContext } from "react-router";
|
|
6
3
|
import { DataStore, DataStoreNotFoundError, DataStoreServiceError, DataStoreUnavailableError } from "@salesforce/mrt-utilities/data-store";
|
|
7
4
|
|
|
@@ -12,11 +9,52 @@ type DataStoreEntryKey = string | ((context: Readonly<RouterContextProvider>) =>
|
|
|
12
9
|
type DataStoreEntry<TValue = unknown> = {
|
|
13
10
|
value?: TValue;
|
|
14
11
|
};
|
|
12
|
+
/**
|
|
13
|
+
* Options for {@link createDataStoreMiddleware} and {@link createLazyDataStoreMiddleware}.
|
|
14
|
+
*
|
|
15
|
+
* @typeParam T - The shape stored in `context` after the entry is fetched and transformed.
|
|
16
|
+
*/
|
|
15
17
|
type DataStoreMiddlewareOptions<T> = {
|
|
18
|
+
/**
|
|
19
|
+
* The data store entry key, or a function that derives it from request context (used
|
|
20
|
+
* for site-scoped keys — see {@link prefixWithSiteId}).
|
|
21
|
+
*/
|
|
16
22
|
entryKey: DataStoreEntryKey;
|
|
23
|
+
/**
|
|
24
|
+
* The React Router context the resolved value is written to. Create one with
|
|
25
|
+
* {@link createDataStoreContext}.
|
|
26
|
+
*/
|
|
17
27
|
context: DataStoreContextKey<T>;
|
|
28
|
+
/**
|
|
29
|
+
* Optional projection from the raw entry value to the typed shape stored in context.
|
|
30
|
+
* Defaults to the identity function (the raw value cast to `T`). Throws from this
|
|
31
|
+
* function propagate to the caller; do not use it as a place to handle data-store
|
|
32
|
+
* errors.
|
|
33
|
+
*/
|
|
18
34
|
transform?: (value: Record<string, unknown>) => T;
|
|
35
|
+
/**
|
|
36
|
+
* How the middleware reacts when the data store cannot serve the entry
|
|
37
|
+
* (`DataStoreUnavailableError` or `DataStoreServiceError`). `DataStoreNotFoundError`
|
|
38
|
+
* is always handled gracefully and ignores this setting.
|
|
39
|
+
*
|
|
40
|
+
* - `'throw'` *(factory default)*: rethrow with a stable error message. Use when the
|
|
41
|
+
* entry is required and a failure should surface as a 5xx.
|
|
42
|
+
* - `'fallback'`: warn and resolve to {@link DataStoreMiddlewareOptions.fallbackValue}
|
|
43
|
+
* (or the missing state if no fallback is configured). Use for optional preferences
|
|
44
|
+
* where graceful degradation is preferred.
|
|
45
|
+
*
|
|
46
|
+
* The four built-in middlewares (`customSitePreferencesMiddleware`, etc.) override
|
|
47
|
+
* this default to `'fallback'` so the storefront stays up during transient outages,
|
|
48
|
+
* and expose `SFNEXT_DATA_STORE_UNAVAILABLE_MODE=throw` as an opt-in escape hatch.
|
|
49
|
+
*/
|
|
19
50
|
onUnavailable?: 'throw' | 'fallback';
|
|
51
|
+
/**
|
|
52
|
+
* Value to populate the context with when `onUnavailable === 'fallback'` and the data
|
|
53
|
+
* store fetch fails. Either a constant or a function that derives the value from the
|
|
54
|
+
* router context (useful for fallbacks that need request-scoped information). When
|
|
55
|
+
* omitted, the middleware leaves the context unset on failure (downstream consumers
|
|
56
|
+
* see the context's default value, typically `null`).
|
|
57
|
+
*/
|
|
20
58
|
fallbackValue?: T | ((context: Readonly<RouterContextProvider>) => T);
|
|
21
59
|
};
|
|
22
60
|
/**
|
|
@@ -28,18 +66,59 @@ type DataStoreMiddlewareOptions<T> = {
|
|
|
28
66
|
*/
|
|
29
67
|
declare function createDataStoreContext<T>(): DataStoreContextKey<T>;
|
|
30
68
|
/**
|
|
31
|
-
* Creates a
|
|
32
|
-
* and stores
|
|
69
|
+
* Creates a React Router middleware that fetches a single MRT data store entry on every
|
|
70
|
+
* request and stores the resulting value in the supplied router context.
|
|
71
|
+
*
|
|
72
|
+
* Failure handling is controlled by `options.onUnavailable`:
|
|
73
|
+
* - `'throw'` (default for the factory): rethrow `DataStoreUnavailableError` and
|
|
74
|
+
* `DataStoreServiceError` with a stable error message. Fail-fast — the request errors out.
|
|
75
|
+
* - `'fallback'`: log a warning and resolve to `options.fallbackValue` when configured, or
|
|
76
|
+
* to the missing state (context not populated) when no `fallbackValue` is provided. The
|
|
77
|
+
* request continues without crashing the middleware chain.
|
|
33
78
|
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* - `DEPLOY_TARGET` (required): MRT deploy target (e.g., "production")
|
|
79
|
+
* `DataStoreNotFoundError` is always treated as "missing" (warn, do not populate context),
|
|
80
|
+
* regardless of `onUnavailable` — a not-found entry is an expected steady-state for
|
|
81
|
+
* features that haven't been published yet, not a service failure.
|
|
38
82
|
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
83
|
+
* Errors thrown from `options.transform` propagate to the caller — they indicate a
|
|
84
|
+
* programmer error in the middleware definition, not data-store unavailability.
|
|
85
|
+
*
|
|
86
|
+
* @param options - See {@link DataStoreMiddlewareOptions}.
|
|
87
|
+
* @returns React Router middleware for server requests.
|
|
88
|
+
*
|
|
89
|
+
* @env AWS_REGION (required): AWS region for the data store table (e.g., `us-east-1`).
|
|
90
|
+
* @env MOBIFY_PROPERTY_ID (required): MRT property identifier.
|
|
91
|
+
* @env DEPLOY_TARGET (required): MRT deploy target (e.g., `production`).
|
|
41
92
|
*/
|
|
42
93
|
declare function createDataStoreMiddleware<T>(options: DataStoreMiddlewareOptions<T>): MiddlewareFunction<Response>;
|
|
94
|
+
/**
|
|
95
|
+
* Lazy variant of {@link createDataStoreMiddleware}. Instead of fetching the
|
|
96
|
+
* entry up-front during middleware execution, this stores a memoized loader
|
|
97
|
+
* in the router context. Consumers call {@link readLazyDataStoreEntry} to
|
|
98
|
+
* trigger the fetch on demand — pages that never read the value never pay
|
|
99
|
+
* for the data-store call.
|
|
100
|
+
*
|
|
101
|
+
* Repeated reads within the same request share the in-flight promise so
|
|
102
|
+
* the entry is fetched at most once per request.
|
|
103
|
+
*
|
|
104
|
+
* Failure handling matches the eager variant: `onUnavailable` and
|
|
105
|
+
* `fallbackValue` are honored when the underlying fetch fails. The fallback
|
|
106
|
+
* value (or `null` for the missing state) surfaces through
|
|
107
|
+
* {@link readLazyDataStoreEntry}.
|
|
108
|
+
*
|
|
109
|
+
* Use this for entries that only a subset of routes consume (e.g. config
|
|
110
|
+
* read by a single feature) rather than entries needed on every request.
|
|
111
|
+
*/
|
|
112
|
+
declare function createLazyDataStoreMiddleware<T>(options: DataStoreMiddlewareOptions<T>): MiddlewareFunction<Response>;
|
|
113
|
+
/**
|
|
114
|
+
* Reads a value populated by {@link createLazyDataStoreMiddleware}. Triggers
|
|
115
|
+
* the underlying data-store fetch on first call and reuses the cached
|
|
116
|
+
* promise on subsequent calls within the same request.
|
|
117
|
+
*
|
|
118
|
+
* Returns `null` when the lazy middleware did not run (no loader in
|
|
119
|
+
* context) or when the entry is missing/invalid.
|
|
120
|
+
*/
|
|
121
|
+
declare function readLazyDataStoreEntry<T>(context: Readonly<RouterContextProvider>, contextKey: DataStoreContextKey<T>): Promise<T | null>;
|
|
43
122
|
/**
|
|
44
123
|
* Read a data-store entry through the singleton MRT utilities API.
|
|
45
124
|
* The underlying implementation (production DynamoDB vs development pseudo store)
|
|
@@ -50,11 +129,102 @@ declare function createDataStoreMiddleware<T>(options: DataStoreMiddlewareOption
|
|
|
50
129
|
*/
|
|
51
130
|
declare function getDataStoreEntry<TValue = unknown>(key: string): Promise<DataStoreEntry<TValue> | null>;
|
|
52
131
|
//#endregion
|
|
132
|
+
//#region src/data-store/logger-context.d.ts
|
|
133
|
+
/**
|
|
134
|
+
* Minimal structured-logger interface the data-store middleware depends on.
|
|
135
|
+
*
|
|
136
|
+
* Matches the shape of the host application's `Logger` (see the storefront
|
|
137
|
+
* template's `src/lib/logger.ts`) so a host can pass through its own logger
|
|
138
|
+
* object via {@link dataStoreLoggerContext} without an adapter.
|
|
139
|
+
*
|
|
140
|
+
* The data-store middleware emits at `warn` level today; the full interface
|
|
141
|
+
* is exposed so future SDK middlewares that need richer levels stay
|
|
142
|
+
* consistent with this contract.
|
|
143
|
+
*/
|
|
144
|
+
interface DataStoreLogger {
|
|
145
|
+
error(message: string, metadata?: Record<string, unknown>): void;
|
|
146
|
+
warn(message: string, metadata?: Record<string, unknown>): void;
|
|
147
|
+
info(message: string, metadata?: Record<string, unknown>): void;
|
|
148
|
+
debug(message: string, metadata?: Record<string, unknown>): void;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Router context the SDK reads to obtain a request-scoped structured logger.
|
|
152
|
+
*
|
|
153
|
+
* Hosts (e.g. the storefront template) populate this from their own logging
|
|
154
|
+
* middleware. When unset, {@link getDataStoreLogger} falls back to a
|
|
155
|
+
* console-based logger so warnings remain visible.
|
|
156
|
+
*
|
|
157
|
+
* Defaults to `null` (not `undefined`) because React Router's
|
|
158
|
+
* `context.get()` throws when `defaultValue === undefined`.
|
|
159
|
+
*/
|
|
160
|
+
declare const dataStoreLoggerContext: react_router3.RouterContext<DataStoreLogger | null>;
|
|
161
|
+
/**
|
|
162
|
+
* Read the data-store logger from router context, falling back to a
|
|
163
|
+
* console-based default when nothing has been injected.
|
|
164
|
+
*
|
|
165
|
+
* Use this from inside SDK middleware/loaders that have access to a
|
|
166
|
+
* {@link RouterContextProvider}.
|
|
167
|
+
*/
|
|
168
|
+
declare function getDataStoreLogger(context: Readonly<RouterContextProvider>): DataStoreLogger;
|
|
169
|
+
//#endregion
|
|
170
|
+
//#region src/data-store/middleware/custom-site-preferences.d.ts
|
|
171
|
+
type SitePreferences = Record<string, unknown>;
|
|
172
|
+
/**
|
|
173
|
+
* Read site preferences from router context.
|
|
174
|
+
*
|
|
175
|
+
* @param context - Router context provider
|
|
176
|
+
* @returns Site preferences data stored by data-store middleware
|
|
177
|
+
*/
|
|
178
|
+
declare function getSitePreferences(context: Readonly<RouterContextProvider>): SitePreferences;
|
|
179
|
+
//#endregion
|
|
180
|
+
//#region src/data-store/middleware/custom-global-preferences.d.ts
|
|
181
|
+
type CustomGlobalPreferences = Record<string, unknown>;
|
|
182
|
+
/**
|
|
183
|
+
* Read custom global preferences from router context.
|
|
184
|
+
*
|
|
185
|
+
* @param context - Router context provider
|
|
186
|
+
* @returns Custom global preferences data stored by data-store middleware
|
|
187
|
+
*/
|
|
188
|
+
declare function getCustomGlobalPreferences(context: Readonly<RouterContextProvider>): CustomGlobalPreferences;
|
|
189
|
+
//#endregion
|
|
190
|
+
//#region src/data-store/middleware/gcp-preferences.d.ts
|
|
191
|
+
/**
|
|
192
|
+
* OOTB Google Cloud Platform preferences sourced from the MRT data store.
|
|
193
|
+
*
|
|
194
|
+
* Additional fields (e.g. `projectId`, `region`) may be added here as the
|
|
195
|
+
* ECOM MRT sync job expands the `gcp` entry. Consumers should read the
|
|
196
|
+
* object as a whole via `getGcpPreferences`, or use a specific convenience
|
|
197
|
+
* getter like `getGcpApiKey` for a single field.
|
|
198
|
+
*/
|
|
199
|
+
type GcpPreferences = {
|
|
200
|
+
apiKey: string;
|
|
201
|
+
};
|
|
202
|
+
/**
|
|
203
|
+
* Read the GCP (Google Cloud Platform) preferences object from router context.
|
|
204
|
+
*
|
|
205
|
+
* The preferences are sourced from the MRT data store entry `gcp`, which is
|
|
206
|
+
* populated only for storefronts connecting to production ECOM instances.
|
|
207
|
+
* In non-production environments, or when the entry is missing, returns an
|
|
208
|
+
* object whose fields are all empty/default.
|
|
209
|
+
*
|
|
210
|
+
* @param context - Router context provider
|
|
211
|
+
* @returns GCP preferences object; fields are empty/default when the entry is unavailable
|
|
212
|
+
*/
|
|
213
|
+
declare function getGcpPreferences(context: Readonly<RouterContextProvider>): GcpPreferences;
|
|
214
|
+
/**
|
|
215
|
+
* Convenience getter for the Google Cloud API key alone.
|
|
216
|
+
*
|
|
217
|
+
* Equivalent to `getGcpPreferences(context).apiKey`.
|
|
218
|
+
*
|
|
219
|
+
* @param context - Router context provider
|
|
220
|
+
* @returns The GCP API key, or an empty string when unavailable
|
|
221
|
+
*/
|
|
222
|
+
declare function getGcpApiKey(context: Readonly<RouterContextProvider>): string;
|
|
223
|
+
//#endregion
|
|
53
224
|
//#region src/data-store/middleware/login-preferences.d.ts
|
|
54
225
|
type LoginPreferences = {
|
|
55
226
|
emailVerificationEnabled?: boolean;
|
|
56
227
|
};
|
|
57
|
-
declare const loginPreferencesContext: react_router12.RouterContext<LoginPreferences | null>;
|
|
58
228
|
/**
|
|
59
229
|
* Read login preferences from router context.
|
|
60
230
|
*
|
|
@@ -64,7 +234,7 @@ declare const loginPreferencesContext: react_router12.RouterContext<LoginPrefere
|
|
|
64
234
|
declare function getLoginPreferences(context: Readonly<RouterContextProvider>): LoginPreferences;
|
|
65
235
|
//#endregion
|
|
66
236
|
//#region src/data-store/index.d.ts
|
|
67
|
-
declare const dataStoreMiddleware:
|
|
237
|
+
declare const dataStoreMiddleware: react_router3.MiddlewareFunction<Response>[];
|
|
68
238
|
//#endregion
|
|
69
|
-
export { type CustomGlobalPreferences,
|
|
239
|
+
export { type CustomGlobalPreferences, DataStore, type DataStoreContextKey, type DataStoreEntry, type DataStoreEntryKey, type DataStoreLogger, type DataStoreMiddlewareOptions, DataStoreNotFoundError, DataStoreServiceError, DataStoreUnavailableError, type GcpPreferences, type LoginPreferences, type SitePreferences, createDataStoreContext, createDataStoreMiddleware, createLazyDataStoreMiddleware, dataStoreLoggerContext, dataStoreMiddleware, getCustomGlobalPreferences, getDataStoreEntry, getDataStoreLogger, getGcpApiKey, getGcpPreferences, getLoginPreferences, getSitePreferences, readLazyDataStoreEntry };
|
|
70
240
|
//# sourceMappingURL=data-store.d.ts.map
|