@searchspring/snap-preact 0.28.0 → 0.30.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.
Files changed (46) hide show
  1. package/README.md +2 -1
  2. package/dist/cjs/Instantiators/RecommendationInstantiator.d.ts +6 -5
  3. package/dist/cjs/Instantiators/RecommendationInstantiator.d.ts.map +1 -1
  4. package/dist/cjs/Instantiators/RecommendationInstantiator.js +19 -5
  5. package/dist/cjs/Snap.d.ts +67 -13
  6. package/dist/cjs/Snap.d.ts.map +1 -1
  7. package/dist/cjs/Snap.js +220 -79
  8. package/dist/cjs/create/createAutocompleteController.d.ts +1 -1
  9. package/dist/cjs/create/createAutocompleteController.d.ts.map +1 -1
  10. package/dist/cjs/create/createAutocompleteController.js +6 -1
  11. package/dist/cjs/create/createFinderController.d.ts +1 -1
  12. package/dist/cjs/create/createFinderController.d.ts.map +1 -1
  13. package/dist/cjs/create/createFinderController.js +6 -1
  14. package/dist/cjs/create/createRecommendationController.d.ts +1 -1
  15. package/dist/cjs/create/createRecommendationController.d.ts.map +1 -1
  16. package/dist/cjs/create/createRecommendationController.js +6 -1
  17. package/dist/cjs/create/createSearchController.d.ts +1 -1
  18. package/dist/cjs/create/createSearchController.d.ts.map +1 -1
  19. package/dist/cjs/create/createSearchController.js +6 -1
  20. package/dist/cjs/getBundleDetails/getBundleDetails.d.ts.map +1 -1
  21. package/dist/cjs/getBundleDetails/getBundleDetails.js +3 -2
  22. package/dist/cjs/types.d.ts +11 -5
  23. package/dist/cjs/types.d.ts.map +1 -1
  24. package/dist/esm/Instantiators/RecommendationInstantiator.d.ts +6 -5
  25. package/dist/esm/Instantiators/RecommendationInstantiator.d.ts.map +1 -1
  26. package/dist/esm/Instantiators/RecommendationInstantiator.js +21 -6
  27. package/dist/esm/Snap.d.ts +67 -13
  28. package/dist/esm/Snap.d.ts.map +1 -1
  29. package/dist/esm/Snap.js +184 -61
  30. package/dist/esm/create/createAutocompleteController.d.ts +1 -1
  31. package/dist/esm/create/createAutocompleteController.d.ts.map +1 -1
  32. package/dist/esm/create/createAutocompleteController.js +6 -1
  33. package/dist/esm/create/createFinderController.d.ts +1 -1
  34. package/dist/esm/create/createFinderController.d.ts.map +1 -1
  35. package/dist/esm/create/createFinderController.js +6 -1
  36. package/dist/esm/create/createRecommendationController.d.ts +1 -1
  37. package/dist/esm/create/createRecommendationController.d.ts.map +1 -1
  38. package/dist/esm/create/createRecommendationController.js +6 -1
  39. package/dist/esm/create/createSearchController.d.ts +1 -1
  40. package/dist/esm/create/createSearchController.d.ts.map +1 -1
  41. package/dist/esm/create/createSearchController.js +6 -1
  42. package/dist/esm/getBundleDetails/getBundleDetails.d.ts.map +1 -1
  43. package/dist/esm/getBundleDetails/getBundleDetails.js +3 -2
  44. package/dist/esm/types.d.ts +11 -5
  45. package/dist/esm/types.d.ts.map +1 -1
  46. package/package.json +13 -13
package/dist/esm/Snap.js CHANGED
@@ -3,14 +3,14 @@ import deepmerge from 'deepmerge';
3
3
  import { isPlainObject } from 'is-plain-object';
4
4
  import { render } from 'preact';
5
5
  import { Client } from '@searchspring/snap-client';
6
- import { Logger, LogMode } from '@searchspring/snap-logger';
6
+ import { Logger } from '@searchspring/snap-logger';
7
7
  import { Tracker } from '@searchspring/snap-tracker';
8
- import { version, DomTargeter, url, cookies, featureFlags } from '@searchspring/snap-toolbox';
9
- import { getContext } from '@searchspring/snap-toolbox';
8
+ import { AppMode, version, getContext, DomTargeter, url, cookies, featureFlags } from '@searchspring/snap-toolbox';
10
9
  import { ControllerTypes } from '@searchspring/snap-controller';
11
10
  import { default as createSearchController } from './create/createSearchController';
12
11
  export const BRANCH_COOKIE = 'ssBranch';
13
- export const SS_DEV_COOKIE = 'ssDev';
12
+ export const DEV_COOKIE = 'ssDev';
13
+ const SESSION_ATTRIBUTION = 'ssAttribution';
14
14
  const COMPONENT_ERROR = `Uncaught Error - Invalid value passed as the component.
15
15
  This usually happens when you pass a JSX Element, and not a function that returns the component, in the snap config.
16
16
 
@@ -47,6 +47,10 @@ This usually happens when you pass a JSX Element, and not a function that return
47
47
  The error above happened in the following targeter in the Snap Config`;
48
48
  export class Snap {
49
49
  constructor(config, services) {
50
+ this.mode = AppMode.production;
51
+ this._instantiatorPromises = {};
52
+ this._controllerPromises = {};
53
+ this.controllers = {};
50
54
  this.getInstantiator = (id) => {
51
55
  return this._instantiatorPromises[id] || Promise.reject(`getInstantiator could not find instantiator with id: ${id}`);
52
56
  };
@@ -58,12 +62,22 @@ export class Snap {
58
62
  controllerIds.forEach((id) => getControllerPromises.push(this.getController(id)));
59
63
  return Promise.all(getControllerPromises);
60
64
  };
65
+ // exposed method used for creating controllers dynamically - calls _createController()
61
66
  this.createController = async (type, config, services, urlConfig, context, callback) => {
67
+ if (typeof this._controllerPromises[config.id] != 'undefined') {
68
+ throw new Error(`Controller with id '${config.id}' is already defined`);
69
+ }
70
+ this._controllerPromises[config.id] = new Promise((resolve) => this._createController(type, config, services, urlConfig, context, async (cntrlr) => {
71
+ if (typeof callback == 'function')
72
+ await callback(cntrlr);
73
+ resolve(cntrlr);
74
+ }));
75
+ return this._controllerPromises[config.id];
76
+ };
77
+ // internal use method that creates controllers without verifying if id is in use first
78
+ this._createController = async (type, config, services, urlConfig, context, callback) => {
62
79
  let importPromise;
63
80
  switch (type) {
64
- case ControllerTypes.search:
65
- importPromise = import('./create/createSearchController');
66
- break;
67
81
  case ControllerTypes.autocomplete:
68
82
  importPromise = import('./create/createAutocompleteController');
69
83
  break;
@@ -73,10 +87,17 @@ export class Snap {
73
87
  case ControllerTypes.recommendation:
74
88
  importPromise = import('./create/createRecommendationController');
75
89
  break;
90
+ case ControllerTypes.search:
91
+ default:
92
+ importPromise = import('./create/createSearchController');
93
+ break;
76
94
  }
95
+ // @ts-ignore - we know the config is correct, but complicated typing
77
96
  const creationFunc = (await importPromise).default;
78
97
  if (!this.controllers[config.id]) {
79
- this.controllers[config.id] = creationFunc({
98
+ window.searchspring.controller = window.searchspring.controller || {};
99
+ window.searchspring.controller[config.id] = this.controllers[config.id] = creationFunc({
100
+ mode: this.mode,
80
101
  url: deepmerge(this.config.url || {}, urlConfig || {}),
81
102
  controller: config,
82
103
  context: deepmerge(this.context || {}, context || {}),
@@ -95,15 +116,42 @@ export class Snap {
95
116
  }
96
117
  return this.controllers[config.id];
97
118
  };
119
+ this.handlers = {
120
+ error: (event) => {
121
+ try {
122
+ const { filename } = event;
123
+ if (filename.includes('snapui.searchspring.io') && this.tracker.track.error) {
124
+ const { colno, lineno, error: { stack }, message, timeStamp, } = event;
125
+ const userAgent = navigator.userAgent;
126
+ const href = window.location.href;
127
+ const beaconPayload = {
128
+ userAgent,
129
+ href,
130
+ filename,
131
+ stack,
132
+ message,
133
+ colno,
134
+ lineno,
135
+ timeStamp,
136
+ };
137
+ this.tracker.track.error(beaconPayload);
138
+ }
139
+ }
140
+ catch (e) {
141
+ // prevent error metrics from breaking the app
142
+ }
143
+ },
144
+ };
145
+ window.removeEventListener('error', this.handlers.error);
146
+ window.addEventListener('error', this.handlers.error);
98
147
  this.config = config;
99
- this.logger = services?.logger || new Logger('Snap Preact ');
100
148
  let globalContext = {};
101
149
  try {
102
150
  // get global context
103
151
  globalContext = getContext(['shopper', 'config', 'merchandising']);
104
152
  }
105
153
  catch (err) {
106
- this.logger.error('failed to find global context');
154
+ console.error('Snap failed to find global context');
107
155
  }
108
156
  // merge configs - but only merge plain objects
109
157
  this.config = deepmerge(this.config || {}, globalContext.config || {}, {
@@ -115,8 +163,9 @@ export class Snap {
115
163
  if ((!services?.client || !services?.tracker) && !this.config?.client?.globals?.siteId) {
116
164
  throw new Error(`Snap: config provided must contain a valid config.client.globals.siteId value`);
117
165
  }
118
- if (this.context.merchandising?.segments) {
119
- if (this.config.client.globals.merchandising) {
166
+ // segmented merchandising context -> client globals
167
+ if (this.config.client?.globals && this.context.merchandising?.segments) {
168
+ if (this.config.client.globals?.merchandising) {
120
169
  this.config.client.globals.merchandising.segments = deepmerge(this.config.client.globals.merchandising.segments, this.context.merchandising.segments);
121
170
  }
122
171
  else {
@@ -125,33 +174,73 @@ export class Snap {
125
174
  };
126
175
  }
127
176
  }
128
- this.client = services?.client || new Client(this.config.client.globals, this.config.client.config);
129
- this.tracker = services?.tracker || new Tracker(this.config.client.globals);
130
- this._controllerPromises = {};
131
- this._instantiatorPromises = {};
132
- this.controllers = {};
133
- // TODO environment switch using URL?
134
- this.logger.setMode(process.env.NODE_ENV);
135
- // log version
136
- this.logger.imageText({
137
- url: 'https://snapui.searchspring.io/favicon.svg',
138
- text: `[${version}]`,
139
- style: `color: ${this.logger.colors.indigo}; font-weight: bold;`,
140
- });
141
177
  try {
142
178
  const urlParams = url(window.location.href);
143
- const branchParam = urlParams.params?.query?.branch || cookies.get(BRANCH_COOKIE);
144
- if (branchParam && !document.querySelector(`script[${BRANCH_COOKIE}]`)) {
145
- // set a cookie or localstorage with branch
179
+ const branchOverride = urlParams?.params?.query?.branch || cookies.get(BRANCH_COOKIE);
180
+ /* app mode priority:
181
+ 1. node env
182
+ 2. config
183
+ 3. override via query param / cookie
184
+ */
185
+ // node env
186
+ if (process.env.NODE_ENV && Object.values(AppMode).includes(process.env.NODE_ENV)) {
187
+ this.mode = process.env.NODE_ENV;
188
+ }
189
+ // config
190
+ if (this.config.mode && Object.values(AppMode).includes(this.config.mode)) {
191
+ this.mode = this.config.mode;
192
+ }
193
+ // query param / cookiev override
194
+ if ((urlParams?.params?.query && 'dev' in urlParams.params.query) || !!cookies.get(DEV_COOKIE)) {
195
+ if (urlParams?.params.query?.dev == 'false' || urlParams?.params.query?.dev == '0') {
196
+ cookies.unset(DEV_COOKIE);
197
+ this.mode = AppMode.production;
198
+ }
199
+ else {
200
+ cookies.set(DEV_COOKIE, '1', 'Lax', 0);
201
+ this.mode = AppMode.development;
202
+ }
203
+ }
204
+ // client mode uses client config over snap config
205
+ if (this.config.client) {
206
+ this.config.client.config = this.config.client.config || {};
207
+ this.config.client.config.mode = this.config.client.config.mode || this.mode;
208
+ }
209
+ this.client = services?.client || new Client(this.config.client.globals, this.config.client.config);
210
+ this.tracker = services?.tracker || new Tracker(this.config.client.globals, { framework: 'preact' });
211
+ this.logger = services?.logger || new Logger({ prefix: 'Snap Preact ', mode: this.mode });
212
+ // check for tracking attribution in URL ?ss_attribution=type:id
213
+ const sessionAttribution = window.sessionStorage?.getItem(SESSION_ATTRIBUTION);
214
+ if (urlParams?.params?.query?.ss_attribution) {
215
+ const attribution = urlParams.params.query.ss_attribution.split(':');
216
+ const [type, id] = attribution;
217
+ if (type && id) {
218
+ this.tracker.updateContext('attribution', { type, id });
219
+ }
220
+ // save to session storage
221
+ window.sessionStorage?.setItem(SESSION_ATTRIBUTION, urlParams.params.query.ss_attribution);
222
+ }
223
+ else if (sessionAttribution) {
224
+ const [type, id] = sessionAttribution.split(':');
225
+ if (type && id) {
226
+ this.tracker.updateContext('attribution', { type, id });
227
+ }
228
+ }
229
+ // log version
230
+ this.logger.imageText({
231
+ url: 'https://snapui.searchspring.io/favicon.svg',
232
+ text: `[${version}]`,
233
+ style: `color: ${this.logger.colors.indigo}; font-weight: bold;`,
234
+ });
235
+ if (branchOverride && !document.querySelector(`script[${BRANCH_COOKIE}]`)) {
236
+ this.logger.warn(`...loading build... '${branchOverride}'`);
237
+ // set a cookie with branch
146
238
  if (featureFlags.cookies) {
147
- cookies.set(BRANCH_COOKIE, branchParam, 'Lax', 3600000); // 1 hour
148
- cookies.set(SS_DEV_COOKIE, '1', 'Lax', 0);
239
+ cookies.set(BRANCH_COOKIE, branchOverride, 'Lax', 3600000); // 1 hour
149
240
  }
150
241
  else {
151
242
  this.logger.warn('Cookies are not supported/enabled by this browser, branch overrides will not persist!');
152
243
  }
153
- this.logger.setMode(LogMode.DEVELOPMENT);
154
- this.logger.warn(`...loading build... '${branchParam}'`);
155
244
  // get the path and siteId from the current bundle script in case its not the same as the client config
156
245
  let path = `https://snapui.searchspring.io/${this.config.client.globals.siteId}/`;
157
246
  const script = document.querySelector('script[src*="//snapui.searchspring.io"]');
@@ -163,9 +252,9 @@ export class Snap {
163
252
  }
164
253
  // append script with new branch in path
165
254
  const branchScript = document.createElement('script');
166
- const src = `${path}${branchParam}/bundle.js`;
255
+ const src = `${path}${branchOverride}/bundle.js`;
167
256
  branchScript.src = src;
168
- branchScript.setAttribute(BRANCH_COOKIE, branchParam);
257
+ branchScript.setAttribute(BRANCH_COOKIE, branchOverride);
169
258
  new DomTargeter([
170
259
  {
171
260
  selector: 'body',
@@ -179,34 +268,43 @@ export class Snap {
179
268
  },
180
269
  },
181
270
  ], async (target, elem) => {
182
- let bundleDetails, error;
271
+ const props = {};
183
272
  try {
184
273
  const getBundleDetails = (await import('./getBundleDetails/getBundleDetails')).getBundleDetails;
185
- bundleDetails = await getBundleDetails(src);
274
+ props.details = await getBundleDetails(src);
186
275
  }
187
276
  catch (err) {
188
- error = err;
277
+ props.error = err;
189
278
  }
190
279
  const BranchOverride = (await import('./components/BranchOverride')).BranchOverride;
191
- render(_jsx(BranchOverride, { name: branchParam, details: bundleDetails, error: error, onRemoveClick: () => {
280
+ render(_jsx(BranchOverride, { ...props, name: branchOverride, onRemoveClick: () => {
192
281
  cookies.unset(BRANCH_COOKIE);
193
282
  const urlState = url(window.location.href);
194
- delete urlState.params.query['branch'];
195
- window.location.href = urlState.url();
283
+ delete urlState?.params.query['branch'];
284
+ const newUrl = urlState?.url();
285
+ if (newUrl && newUrl != window.location.href) {
286
+ window.location.href = newUrl;
287
+ }
288
+ else {
289
+ window.location.reload();
290
+ }
196
291
  } }), elem);
197
- window.searchspring = undefined;
292
+ // reset the global searchspring object
293
+ delete window.searchspring;
198
294
  document.head.appendChild(branchScript);
199
295
  });
200
296
  // prevent further instantiation of config
201
297
  return;
202
298
  }
203
299
  }
204
- catch (e) { }
205
- if (window.searchspring) {
206
- window.searchspring.context = this.context;
207
- if (this.client)
208
- window.searchspring.client = this.client;
300
+ catch (e) {
301
+ this.logger.error(e);
209
302
  }
303
+ // bind to window global
304
+ window.searchspring = window.searchspring || {};
305
+ window.searchspring.context = this.context;
306
+ if (this.client)
307
+ window.searchspring.client = this.client;
210
308
  // autotrack shopper id from the context
211
309
  if (this.context?.shopper?.id) {
212
310
  this.tracker.track.shopper.login({
@@ -217,7 +315,7 @@ export class Snap {
217
315
  if (this.context?.shopper?.cart) {
218
316
  const cart = this.context.shopper.cart;
219
317
  if (Array.isArray(cart)) {
220
- const cartItems = cart.filter((item) => item?.sku || item?.childSku).map((item) => (item?.sku || item?.childSku).trim());
318
+ const cartItems = cart.filter((item) => item?.sku || item?.childSku).map((item) => (item?.sku || item?.childSku || '').trim());
221
319
  this.tracker.cookies.cart.set(cartItems);
222
320
  }
223
321
  }
@@ -226,7 +324,12 @@ export class Snap {
226
324
  case 'search': {
227
325
  this.config.controllers[type].forEach((controller, index) => {
228
326
  try {
327
+ if (typeof this._controllerPromises[controller.config.id] != 'undefined') {
328
+ this.logger.error(`Controller with id '${controller.config.id}' is already defined`);
329
+ return;
330
+ }
229
331
  const cntrlr = createSearchController({
332
+ mode: this.mode,
230
333
  url: deepmerge(this.config.url || {}, controller.url || {}),
231
334
  controller: controller.config,
232
335
  context: deepmerge(this.context || {}, controller.context || {}),
@@ -239,7 +342,8 @@ export class Snap {
239
342
  logger: controller.services?.logger,
240
343
  tracker: controller.services?.tracker || this.tracker,
241
344
  });
242
- this.controllers[cntrlr.config.id] = cntrlr;
345
+ window.searchspring.controller = window.searchspring.controller || {};
346
+ window.searchspring.controller[cntrlr.config.id] = this.controllers[cntrlr.config.id] = cntrlr;
243
347
  this._controllerPromises[cntrlr.config.id] = new Promise((resolve) => resolve(cntrlr));
244
348
  let searched = false;
245
349
  const runSearch = () => {
@@ -273,7 +377,7 @@ export class Snap {
273
377
  runSearch();
274
378
  }
275
379
  cntrlr.createTargeter({ controller: cntrlr, ...target }, async (target, elem, originalElem) => {
276
- if (target.skeleton) {
380
+ if (target && target.skeleton && elem) {
277
381
  const Skeleton = await target.skeleton();
278
382
  setTimeout(() => {
279
383
  render(_jsx(Skeleton, {}), elem);
@@ -291,6 +395,10 @@ export class Snap {
291
395
  }
292
396
  case 'autocomplete': {
293
397
  this.config.controllers[type].forEach((controller, index) => {
398
+ if (typeof this._controllerPromises[controller.config.id] != 'undefined') {
399
+ this.logger.error(`Controller with id '${controller.config.id}' is already defined`);
400
+ return;
401
+ }
294
402
  this._controllerPromises[controller.config.id] = new Promise((resolve) => {
295
403
  try {
296
404
  let bound = false;
@@ -316,8 +424,9 @@ export class Snap {
316
424
  }
317
425
  };
318
426
  if (!controller?.targeters || controller?.targeters.length === 0) {
319
- this.createController(ControllerTypes.autocomplete, controller.config, controller.services, controller.url, controller.context, (cntrlr) => {
320
- resolve(cntrlr);
427
+ this._createController(ControllerTypes.autocomplete, controller.config, controller.services, controller.url, controller.context, (cntrlr) => {
428
+ if (cntrlr)
429
+ resolve(cntrlr);
321
430
  });
322
431
  }
323
432
  controller?.targeters?.forEach((target, target_index) => {
@@ -343,8 +452,9 @@ export class Snap {
343
452
  ...target,
344
453
  },
345
454
  ], async (target, elem, originalElem) => {
346
- const cntrlr = await this.createController(ControllerTypes.autocomplete, controller.config, controller.services, controller.url, controller.context, (cntrlr) => {
347
- resolve(cntrlr);
455
+ const cntrlr = await this._createController(ControllerTypes.autocomplete, controller.config, controller.services, controller.url, controller.context, (cntrlr) => {
456
+ if (cntrlr)
457
+ resolve(cntrlr);
348
458
  });
349
459
  runBind();
350
460
  targetFunction({ controller: cntrlr, ...target }, elem, originalElem);
@@ -361,6 +471,10 @@ export class Snap {
361
471
  }
362
472
  case 'finder': {
363
473
  this.config.controllers[type].forEach((controller, index) => {
474
+ if (typeof this._controllerPromises[controller.config.id] != 'undefined') {
475
+ this.logger.error(`Controller with id '${controller.config.id}' is already defined`);
476
+ return;
477
+ }
364
478
  this._controllerPromises[controller.config.id] = new Promise((resolve) => {
365
479
  try {
366
480
  let searched = false;
@@ -384,8 +498,9 @@ export class Snap {
384
498
  }
385
499
  };
386
500
  if (!controller?.targeters || controller?.targeters.length === 0) {
387
- this.createController(ControllerTypes.finder, controller.config, controller.services, controller.url, controller.context, (cntrlr) => {
388
- resolve(cntrlr);
501
+ this._createController(ControllerTypes.finder, controller.config, controller.services, controller.url, controller.context, (cntrlr) => {
502
+ if (cntrlr)
503
+ resolve(cntrlr);
389
504
  });
390
505
  }
391
506
  controller?.targeters?.forEach((target, target_index) => {
@@ -396,8 +511,9 @@ export class Snap {
396
511
  throw new Error(`Targets at index ${target_index} missing component value (Component).`);
397
512
  }
398
513
  const targeter = new DomTargeter([{ ...target }], async (target, elem, originalElem) => {
399
- const cntrlr = await this.createController(ControllerTypes.finder, controller.config, controller.services, controller.url, controller.context, (cntrlr) => {
400
- resolve(cntrlr);
514
+ const cntrlr = await this._createController(ControllerTypes.finder, controller.config, controller.services, controller.url, controller.context, (cntrlr) => {
515
+ if (cntrlr)
516
+ resolve(cntrlr);
401
517
  });
402
518
  runSearch();
403
519
  targetFunction({ controller: cntrlr, ...target }, elem, originalElem);
@@ -414,6 +530,10 @@ export class Snap {
414
530
  }
415
531
  case 'recommendation': {
416
532
  this.config.controllers[type].forEach((controller, index) => {
533
+ if (typeof this._controllerPromises[controller.config.id] != 'undefined') {
534
+ this.logger.error(`Controller with id '${controller.config.id}' is already defined`);
535
+ return;
536
+ }
417
537
  this._controllerPromises[controller.config.id] = new Promise((resolve) => {
418
538
  try {
419
539
  let searched = false;
@@ -437,8 +557,9 @@ export class Snap {
437
557
  }
438
558
  };
439
559
  if (!controller?.targeters || controller?.targeters.length === 0) {
440
- this.createController(ControllerTypes.recommendation, controller.config, controller.services, controller.url, controller.context, (cntrlr) => {
441
- resolve(cntrlr);
560
+ this._createController(ControllerTypes.recommendation, controller.config, controller.services, controller.url, controller.context, (cntrlr) => {
561
+ if (cntrlr)
562
+ resolve(cntrlr);
442
563
  });
443
564
  }
444
565
  controller?.targeters?.forEach((target, target_index) => {
@@ -449,8 +570,9 @@ export class Snap {
449
570
  throw new Error(`Targets at index ${target_index} missing component value (Component).`);
450
571
  }
451
572
  const targeter = new DomTargeter([{ ...target }], async (target, elem, originalElem) => {
452
- const cntrlr = await this.createController(ControllerTypes.recommendation, controller.config, controller.services, controller.url, controller.context, (cntrlr) => {
453
- resolve(cntrlr);
573
+ const cntrlr = await this._createController(ControllerTypes.recommendation, controller.config, controller.services, controller.url, controller.context, (cntrlr) => {
574
+ if (cntrlr)
575
+ resolve(cntrlr);
454
576
  });
455
577
  runSearch();
456
578
  targetFunction({ controller: cntrlr, ...target }, elem, originalElem);
@@ -470,6 +592,7 @@ export class Snap {
470
592
  if (this.config?.instantiators?.recommendation) {
471
593
  try {
472
594
  this._instantiatorPromises.recommendation = import('./Instantiators/RecommendationInstantiator').then(({ RecommendationInstantiator }) => {
595
+ this.config.instantiators.recommendation.mode = this.config.instantiators.recommendation.mode || this.mode;
473
596
  return new RecommendationInstantiator(this.config.instantiators.recommendation, {
474
597
  client: this.client,
475
598
  tracker: this.tracker,
@@ -1,5 +1,5 @@
1
1
  import { AutocompleteController } from '@searchspring/snap-controller';
2
2
  import type { SnapControllerServices, SnapAutocompleteControllerConfig } from '../types';
3
- declare const _default: (config: SnapAutocompleteControllerConfig, services?: SnapControllerServices) => AutocompleteController;
3
+ declare const _default: (config: SnapAutocompleteControllerConfig, services?: SnapControllerServices | undefined) => AutocompleteController;
4
4
  export default _default;
5
5
  //# sourceMappingURL=createAutocompleteController.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"createAutocompleteController.d.ts","sourceRoot":"","sources":["../../../src/create/createAutocompleteController.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,sBAAsB,EAAE,MAAM,+BAA+B,CAAC;AASvE,OAAO,KAAK,EAAE,sBAAsB,EAAE,gCAAgC,EAAE,MAAM,UAAU,CAAC;iCAIjE,gCAAgC,aAAa,sBAAsB,KAAG,sBAAsB;AAApH,wBAkBE"}
1
+ {"version":3,"file":"createAutocompleteController.d.ts","sourceRoot":"","sources":["../../../src/create/createAutocompleteController.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,sBAAsB,EAAE,MAAM,+BAA+B,CAAC;AASvE,OAAO,KAAK,EAAE,sBAAsB,EAAE,gCAAgC,EAAE,MAAM,UAAU,CAAC;iCAIjE,gCAAgC,oDAAsC,sBAAsB;AAApH,wBAwBE"}
@@ -10,13 +10,18 @@ import { Tracker } from '@searchspring/snap-tracker';
10
10
  configureMobx({ useProxies: 'never' });
11
11
  export default (config, services) => {
12
12
  const urlManager = (services?.urlManager || new UrlManager(new UrlTranslator(config.url), reactLinker)).detach();
13
+ // set client mode
14
+ if (config.mode && config.client) {
15
+ config.client.config = config.client.config || {};
16
+ config.client.config.mode = config.mode;
17
+ }
13
18
  const cntrlr = new AutocompleteController(config.controller, {
14
19
  client: services?.client || new Client(config.client.globals, config.client.config),
15
20
  store: services?.store || new AutocompleteStore(config.controller, { urlManager }),
16
21
  urlManager,
17
22
  eventManager: services?.eventManager || new EventManager(),
18
23
  profiler: services?.profiler || new Profiler(),
19
- logger: services?.logger || new Logger(),
24
+ logger: services?.logger || new Logger({ mode: config.mode }),
20
25
  tracker: services?.tracker || new Tracker(config.client.globals),
21
26
  }, config.context);
22
27
  return cntrlr;
@@ -1,5 +1,5 @@
1
1
  import { FinderController } from '@searchspring/snap-controller';
2
2
  import type { SnapControllerServices, SnapFinderControllerConfig } from '../types';
3
- declare const _default: (config: SnapFinderControllerConfig, services?: SnapControllerServices) => FinderController;
3
+ declare const _default: (config: SnapFinderControllerConfig, services?: SnapControllerServices | undefined) => FinderController;
4
4
  export default _default;
5
5
  //# sourceMappingURL=createFinderController.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"createFinderController.d.ts","sourceRoot":"","sources":["../../../src/create/createFinderController.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AASjE,OAAO,KAAK,EAAE,sBAAsB,EAAE,0BAA0B,EAAE,MAAM,UAAU,CAAC;iCAI3D,0BAA0B,aAAa,sBAAsB,KAAG,gBAAgB;AAAxG,wBAkBE"}
1
+ {"version":3,"file":"createFinderController.d.ts","sourceRoot":"","sources":["../../../src/create/createFinderController.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AASjE,OAAO,KAAK,EAAE,sBAAsB,EAAE,0BAA0B,EAAE,MAAM,UAAU,CAAC;iCAI3D,0BAA0B,oDAAsC,gBAAgB;AAAxG,wBAwBE"}
@@ -10,13 +10,18 @@ import { Tracker } from '@searchspring/snap-tracker';
10
10
  configureMobx({ useProxies: 'never' });
11
11
  export default (config, services) => {
12
12
  const urlManager = (services?.urlManager || new UrlManager(new UrlTranslator(config.url), reactLinker)).detach(true);
13
+ // set client mode
14
+ if (config.mode && config.client) {
15
+ config.client.config = config.client.config || {};
16
+ config.client.config.mode = config.mode;
17
+ }
13
18
  const cntrlr = new FinderController(config.controller, {
14
19
  client: services?.client || new Client(config.client.globals, config.client.config),
15
20
  store: services?.store || new FinderStore(config.controller, { urlManager }),
16
21
  urlManager,
17
22
  eventManager: services?.eventManager || new EventManager(),
18
23
  profiler: services?.profiler || new Profiler(),
19
- logger: services?.logger || new Logger(),
24
+ logger: services?.logger || new Logger({ mode: config.mode }),
20
25
  tracker: services?.tracker || new Tracker(config.client.globals),
21
26
  }, config.context);
22
27
  return cntrlr;
@@ -1,5 +1,5 @@
1
1
  import { RecommendationController } from '@searchspring/snap-controller';
2
2
  import type { SnapControllerServices, SnapRecommendationControllerConfig } from '../types';
3
- declare const _default: (config: SnapRecommendationControllerConfig, services?: SnapControllerServices) => RecommendationController;
3
+ declare const _default: (config: SnapRecommendationControllerConfig, services?: SnapControllerServices | undefined) => RecommendationController;
4
4
  export default _default;
5
5
  //# sourceMappingURL=createRecommendationController.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"createRecommendationController.d.ts","sourceRoot":"","sources":["../../../src/create/createRecommendationController.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAC;AASzE,OAAO,KAAK,EAAE,sBAAsB,EAAE,kCAAkC,EAAE,MAAM,UAAU,CAAC;iCAInE,kCAAkC,aAAa,sBAAsB,KAAG,wBAAwB;AAAxH,wBAiBE"}
1
+ {"version":3,"file":"createRecommendationController.d.ts","sourceRoot":"","sources":["../../../src/create/createRecommendationController.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAC;AASzE,OAAO,KAAK,EAAE,sBAAsB,EAAE,kCAAkC,EAAE,MAAM,UAAU,CAAC;iCAInE,kCAAkC,oDAAsC,wBAAwB;AAAxH,wBAwBE"}
@@ -10,13 +10,18 @@ import { Tracker } from '@searchspring/snap-tracker';
10
10
  configureMobx({ useProxies: 'never' });
11
11
  export default (config, services) => {
12
12
  const urlManager = (services?.urlManager || new UrlManager(new UrlTranslator(config.url), reactLinker)).detach(true);
13
+ // set client mode
14
+ if (config.mode && config.client) {
15
+ config.client.config = config.client.config || {};
16
+ config.client.config.mode = config.mode;
17
+ }
13
18
  const cntrlr = new RecommendationController(config.controller, {
14
19
  client: services?.client || new Client(config.client.globals, config.client.config),
15
20
  store: services?.store || new RecommendationStore(config.controller, { urlManager }),
16
21
  urlManager,
17
22
  eventManager: services?.eventManager || new EventManager(),
18
23
  profiler: services?.profiler || new Profiler(),
19
- logger: services?.logger || new Logger(),
24
+ logger: services?.logger || new Logger({ mode: config.mode }),
20
25
  tracker: services?.tracker || new Tracker(config.client.globals),
21
26
  }, config.context);
22
27
  return cntrlr;
@@ -1,5 +1,5 @@
1
1
  import { SearchController } from '@searchspring/snap-controller';
2
2
  import type { SnapControllerServices, SnapSearchControllerConfig } from '../types';
3
- declare const _default: (config: SnapSearchControllerConfig, services?: SnapControllerServices) => SearchController;
3
+ declare const _default: (config: SnapSearchControllerConfig, services?: SnapControllerServices | undefined) => SearchController;
4
4
  export default _default;
5
5
  //# sourceMappingURL=createSearchController.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"createSearchController.d.ts","sourceRoot":"","sources":["../../../src/create/createSearchController.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AAQjE,OAAO,KAAK,EAAE,sBAAsB,EAAE,0BAA0B,EAAE,MAAM,UAAU,CAAC;iCAI3D,0BAA0B,aAAa,sBAAsB,KAAG,gBAAgB;AAAxG,wBAkBE"}
1
+ {"version":3,"file":"createSearchController.d.ts","sourceRoot":"","sources":["../../../src/create/createSearchController.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AAQjE,OAAO,KAAK,EAAE,sBAAsB,EAAE,0BAA0B,EAAE,MAAM,UAAU,CAAC;iCAI3D,0BAA0B,oDAAsC,gBAAgB;AAAxG,wBAwBE"}
@@ -10,13 +10,18 @@ import { Tracker } from '@searchspring/snap-tracker';
10
10
  configureMobx({ useProxies: 'never' });
11
11
  export default (config, services) => {
12
12
  const urlManager = services?.urlManager || new UrlManager(new UrlTranslator(config.url), reactLinker);
13
+ // set client mode
14
+ if (config.mode && config.client) {
15
+ config.client.config = config.client.config || {};
16
+ config.client.config.mode = config.mode;
17
+ }
13
18
  const cntrlr = new SearchController(config.controller, {
14
19
  client: services?.client || new Client(config.client.globals, config.client.config),
15
20
  store: services?.store || new SearchStore(config.controller, { urlManager }),
16
21
  urlManager,
17
22
  eventManager: services?.eventManager || new EventManager(),
18
23
  profiler: services?.profiler || new Profiler(),
19
- logger: services?.logger || new Logger(),
24
+ logger: services?.logger || new Logger({ mode: config.mode }),
20
25
  tracker: services?.tracker || new Tracker(config.client.globals),
21
26
  }, config.context);
22
27
  return cntrlr;
@@ -1 +1 @@
1
- {"version":3,"file":"getBundleDetails.d.ts","sourceRoot":"","sources":["../../../src/getBundleDetails/getBundleDetails.ts"],"names":[],"mappings":"AAAA,aAAK,aAAa,GAAG;IACpB,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,eAAO,MAAM,gBAAgB,QAAe,MAAM,KAAG,QAAQ,aAAa,CAuBzE,CAAC"}
1
+ {"version":3,"file":"getBundleDetails.d.ts","sourceRoot":"","sources":["../../../src/getBundleDetails/getBundleDetails.ts"],"names":[],"mappings":"AAAA,aAAK,aAAa,GAAG;IACpB,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,eAAO,MAAM,gBAAgB,QAAe,MAAM,KAAG,QAAQ,aAAa,CAyBzE,CAAC"}
@@ -5,10 +5,11 @@ export const getBundleDetails = async (url) => {
5
5
  request.onreadystatechange = () => {
6
6
  if (request.readyState === request.DONE) {
7
7
  const status = request.status;
8
- if (status === 0 || (status >= 200 && status < 400)) {
8
+ const lastModified = request.getResponseHeader('Last-Modified');
9
+ if ((lastModified && status === 0) || (status >= 200 && status < 400)) {
9
10
  resolve({
10
11
  url,
11
- lastModified: request.getResponseHeader('Last-Modified').split(',')[1].trim(),
12
+ lastModified: lastModified.split(',')[1].trim(),
12
13
  });
13
14
  }
14
15
  else {