@luigisbox/luma 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +152 -0
- package/README.md +76 -9
- package/build/baseline.mjs +2 -2
- package/build/vite.mjs +255 -20
- package/docs/ACCESSIBILITY.md +31 -5
- package/package.json +3 -2
- package/src/analytics/chain.ts +1 -1
- package/src/analytics/pageview.ts +1 -1
- package/src/analytics/start.ts +3 -4
- package/src/analytics/wire.ts +1 -1
- package/src/api/ai.ts +32 -20
- package/src/api/token.ts +7 -7
- package/src/core/a11y.ts +6 -35
- package/src/core/context.ts +2 -0
- package/src/core/contrast-toggle.ts +32 -0
- package/src/core/default-button.ts +66 -0
- package/src/core/early-rules.ts +3 -2
- package/src/core/features.ts +109 -0
- package/src/core/frameworks-check.ts +32 -0
- package/src/core/frameworks.ts +35 -0
- package/src/core/image-load.ts +0 -26
- package/src/core/image-preload.ts +29 -0
- package/src/core/image-tags.ts +7 -1
- package/src/core/images.ts +3 -2
- package/src/core/integration.ts +42 -26
- package/src/core/listings.ts +9 -5
- package/src/core/locales.ts +2 -1
- package/src/core/own.ts +7 -3
- package/src/core/recommenders.ts +2 -2
- package/src/core/reconciler.ts +32 -23
- package/src/core/routes.ts +7 -6
- package/src/core/scheduler.ts +39 -5
- package/src/core/speculation.ts +2 -0
- package/src/core/stash.ts +15 -6
- package/src/core/styles.ts +6 -5
- package/src/core/support.ts +1 -1
- package/src/core/takeover.ts +6 -5
- package/src/core/templates.ts +5 -2
- package/src/core/tiles.ts +33 -10
- package/src/core/url.ts +6 -3
- package/src/core/widget-inits.ts +4 -1
- package/src/lit.ts +2 -1
- package/src/locales/cs.json +1 -1
- package/src/locales/de.json +1 -1
- package/src/locales/en.json +1 -1
- package/src/main.ts +70 -53
- package/src/types.ts +48 -14
- package/src/widgets/autocomplete/index.ts +167 -34
- package/src/widgets/autocomplete/style-autocomplete.css +16 -8
- package/src/widgets/autocomplete/template-autocomplete-empty.ts +1 -1
- package/src/widgets/autocomplete/template-autocomplete-sheet.ts +13 -9
- package/src/widgets/listing/applied.ts +87 -0
- package/src/widgets/listing/element.ts +279 -78
- package/src/widgets/listing/intent.ts +15 -0
- package/src/widgets/listing/query.ts +6 -3
- package/src/widgets/listing/stash.ts +33 -16
- package/src/widgets/listing/state.ts +12 -0
- package/src/widgets/listing/view.ts +2 -1
- package/src/widgets/plp/route.ts +3 -2
- package/src/widgets/recommender/index.ts +3 -2
- package/src/widgets/search/direct.ts +39 -0
- package/src/widgets/search/form-url.ts +38 -34
- package/src/widgets/search/in-place.ts +43 -108
- package/src/widgets/search/index.ts +7 -2
- package/src/widgets/search/speculate.ts +48 -27
- package/src/widgets/search/submit.ts +136 -0
package/src/core/a11y.ts
CHANGED
|
@@ -90,17 +90,17 @@ export const CONTRAST_CSS =
|
|
|
90
90
|
`@media (prefers-contrast:more){:root:not([data-lbx-contrast="off"]) ${OURS}{outline:3px solid!important;outline-offset:2px!important}}` +
|
|
91
91
|
`:root[data-lbx-contrast="more"] ${OURS}{outline:3px solid!important;outline-offset:2px!important}\n`;
|
|
92
92
|
|
|
93
|
-
const
|
|
93
|
+
export const CONTRAST_KEY = 'lbx:contrast';
|
|
94
94
|
const stored = () => {
|
|
95
95
|
try {
|
|
96
|
-
const v = localStorage.getItem(
|
|
96
|
+
const v = localStorage.getItem(CONTRAST_KEY);
|
|
97
97
|
return v === 'more' || v === 'off' ? v : null;
|
|
98
98
|
} catch {
|
|
99
99
|
return null;
|
|
100
100
|
}
|
|
101
101
|
};
|
|
102
102
|
const more = () => (typeof matchMedia === 'function' ? matchMedia('(prefers-contrast: more)') : null);
|
|
103
|
-
const prefersMore = () => !!more()?.matches;
|
|
103
|
+
export const prefersMore = () => !!more()?.matches;
|
|
104
104
|
|
|
105
105
|
/** The mode's state: the visitor's toggle, else their OS setting ('more'), else none. */
|
|
106
106
|
const state = () => stored() ?? (prefersMore() ? 'more' : null);
|
|
@@ -112,7 +112,7 @@ export const contrastOn = () => state() === 'more';
|
|
|
112
112
|
* is on (the toggle, or the OS setting: one selector for both), `"off"` when
|
|
113
113
|
* the visitor switched it off against their OS setting.
|
|
114
114
|
*/
|
|
115
|
-
const
|
|
115
|
+
export const applyContrast = () => {
|
|
116
116
|
const v = state();
|
|
117
117
|
const html = document.documentElement;
|
|
118
118
|
if (!v) html.removeAttribute('data-lbx-contrast');
|
|
@@ -125,36 +125,7 @@ export const useContrast = () => {
|
|
|
125
125
|
setCss('lbx:contrast', layered(CONTRAST_CSS));
|
|
126
126
|
if (!watching) {
|
|
127
127
|
watching = true;
|
|
128
|
-
more()?.addEventListener?.('change',
|
|
128
|
+
more()?.addEventListener?.('change', applyContrast);
|
|
129
129
|
}
|
|
130
|
-
|
|
131
|
-
};
|
|
132
|
-
|
|
133
|
-
/** Switch it (our toggle): remembered for this browser; back to "as the OS says" when that matches. */
|
|
134
|
-
export const setContrast = (on: boolean) => {
|
|
135
|
-
const v = on === prefersMore() ? null : on ? 'more' : 'off';
|
|
136
|
-
try {
|
|
137
|
-
if (v) localStorage.setItem(KEY, v);
|
|
138
|
-
else localStorage.removeItem(KEY);
|
|
139
|
-
} catch {}
|
|
140
|
-
apply();
|
|
141
|
-
};
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Modelled on the previous-generation Luma's switch (`_global.contrastToggle`): a button that turns the
|
|
145
|
-
* mode on and off. Visually hidden until focused unless the visitor's OS asks
|
|
146
|
-
* for a contrast preference or the mode is on (as that one was), so a
|
|
147
|
-
* keyboard user who reaches it always sees it.
|
|
148
|
-
*/
|
|
149
|
-
export const contrastToggle = (label: string) => {
|
|
150
|
-
const b = document.createElement('button');
|
|
151
|
-
b.type = 'button';
|
|
152
|
-
b.className = 'lbx-contrast-toggle';
|
|
153
|
-
b.textContent = label;
|
|
154
|
-
b.setAttribute('aria-pressed', String(contrastOn()));
|
|
155
|
-
b.addEventListener('click', (e) => {
|
|
156
|
-
e.stopPropagation();
|
|
157
|
-
setContrast(!contrastOn());
|
|
158
|
-
});
|
|
159
|
-
return b;
|
|
130
|
+
applyContrast();
|
|
160
131
|
};
|
package/src/core/context.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import type { Templates } from './templates';
|
|
7
7
|
import type { LumaConfigType } from '../types';
|
|
8
8
|
import type { WidgetContext } from '../main';
|
|
9
|
+
import { QUERY_URL } from './features';
|
|
9
10
|
import { nameOf, slot } from './integration';
|
|
10
11
|
|
|
11
12
|
type Support = typeof import('./support');
|
|
@@ -104,5 +105,6 @@ export const widgetContext = (
|
|
|
104
105
|
result: s.resultFactory({ ...g.computedAttributes, ...w.app.computedAttributes }),
|
|
105
106
|
// Images: a listing's first row is eager; elsewhere (recommenders, the autocomplete) every image is lazy.
|
|
106
107
|
helpers: { t, price, plain: s.plain, first: s.first, ...s.imageHelpers(g.images, w.id !== 'search' && w.id !== 'plp') },
|
|
108
|
+
queryUrl: QUERY_URL ? config.search?.queryUrl : undefined,
|
|
107
109
|
};
|
|
108
110
|
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// The listing's high-contrast toggle (`contrastToggle` of a listing config):
|
|
2
|
+
// apart from ./a11y.ts (every widget's), so only the listing's chunk has it.
|
|
3
|
+
import { applyContrast, CONTRAST_KEY, contrastOn, prefersMore } from './a11y';
|
|
4
|
+
|
|
5
|
+
/** Switch it (our toggle): remembered for this browser; back to "as the OS says" when that matches. */
|
|
6
|
+
export const setContrast = (on: boolean) => {
|
|
7
|
+
const v = on === prefersMore() ? null : on ? 'more' : 'off';
|
|
8
|
+
try {
|
|
9
|
+
if (v) localStorage.setItem(CONTRAST_KEY, v);
|
|
10
|
+
else localStorage.removeItem(CONTRAST_KEY);
|
|
11
|
+
} catch {}
|
|
12
|
+
applyContrast();
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Modelled on the previous-generation Luma's switch (`_global.contrastToggle`): a button that turns the
|
|
17
|
+
* mode on and off. Visually hidden until focused unless the visitor's OS asks
|
|
18
|
+
* for a contrast preference or the mode is on (as that one was), so a
|
|
19
|
+
* keyboard user who reaches it always sees it.
|
|
20
|
+
*/
|
|
21
|
+
export const contrastToggle = (label: string) => {
|
|
22
|
+
const b = document.createElement('button');
|
|
23
|
+
b.type = 'button';
|
|
24
|
+
b.className = 'lbx-contrast-toggle';
|
|
25
|
+
b.textContent = label;
|
|
26
|
+
b.setAttribute('aria-pressed', String(contrastOn()));
|
|
27
|
+
b.addEventListener('click', (e) => {
|
|
28
|
+
e.stopPropagation();
|
|
29
|
+
setContrast(!contrastOn());
|
|
30
|
+
});
|
|
31
|
+
return b;
|
|
32
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// A form's default button, what an implicit submission (Enter in a field)
|
|
2
|
+
// submits with. In lbx-core: the autocomplete's "show all" link (its href is
|
|
3
|
+
// the URL Enter would load) and the search form's URL (widgets/search/form-url.ts,
|
|
4
|
+
// lbx-nav) read the same button, so the link, the result fetched while typing
|
|
5
|
+
// and the page fetched ahead agree.
|
|
6
|
+
|
|
7
|
+
export type Submitter = HTMLButtonElement | HTMLInputElement;
|
|
8
|
+
|
|
9
|
+
/** FormData(form, submitter) is newer (Chrome 112, Safari 16.4, Firefox 111): before, the second argument is ignored (it does not throw). */
|
|
10
|
+
let withSubmitter: boolean | undefined;
|
|
11
|
+
export const submitterSupported = () => {
|
|
12
|
+
if (withSubmitter === undefined) {
|
|
13
|
+
try {
|
|
14
|
+
const f = document.createElement('form');
|
|
15
|
+
const b = f.appendChild(document.createElement('button'));
|
|
16
|
+
b.name = 'x';
|
|
17
|
+
b.value = '1';
|
|
18
|
+
withSubmitter = new FormData(f, b).get('x') === '1';
|
|
19
|
+
} catch {
|
|
20
|
+
withSubmitter = false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return withSubmitter;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The form's data as a submit by `btn` sends it (null: by no button). Where
|
|
28
|
+
* the engine ignores the submitter argument, its own entry is added by hand:
|
|
29
|
+
* name=value, or an image button's x and y (`name.x`, `name.y`; `x`, `y`
|
|
30
|
+
* without a name), 0 as a submit without a pointer.
|
|
31
|
+
*/
|
|
32
|
+
export const formDataOf = (form: HTMLFormElement, btn: Submitter | null): FormData => {
|
|
33
|
+
const native = submitterSupported();
|
|
34
|
+
let own = !native; // the submitter's entry is ours to add
|
|
35
|
+
let data: FormData;
|
|
36
|
+
try {
|
|
37
|
+
data = new FormData(form, native && btn ? btn : undefined);
|
|
38
|
+
} catch {
|
|
39
|
+
data = new FormData(form); // not a submit button of this form
|
|
40
|
+
own = true;
|
|
41
|
+
}
|
|
42
|
+
if (own && btn) {
|
|
43
|
+
if (btn.type === 'image') {
|
|
44
|
+
data.append(btn.name ? `${btn.name}.x` : 'x', '0');
|
|
45
|
+
data.append(btn.name ? `${btn.name}.y` : 'y', '0');
|
|
46
|
+
} else if (btn.name) data.append(btn.name, btn.value);
|
|
47
|
+
}
|
|
48
|
+
return data;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The form's default button: the first submit button it owns in tree order
|
|
53
|
+
* (inside it, or anywhere in its tree through `form=`), what an implicit
|
|
54
|
+
* submission (Enter in a field) submits with, so its `formaction`,
|
|
55
|
+
* `formmethod`, `formtarget`, `formnovalidate` and name=value count as on a
|
|
56
|
+
* click. null without one.
|
|
57
|
+
*/
|
|
58
|
+
export const defaultSubmitter = (form: HTMLFormElement): Submitter | null => {
|
|
59
|
+
// A button outside the form owns it through form="<id>": the whole tree only when the form has an id.
|
|
60
|
+
for (const el of (form.id ? (form.getRootNode() as ParentNode) : form).querySelectorAll<Submitter>('button, input')) {
|
|
61
|
+
if (el.form !== form) continue;
|
|
62
|
+
const type = (el.getAttribute('type') ?? '').trim().toLowerCase();
|
|
63
|
+
if (el instanceof HTMLButtonElement ? type === '' || type === 'submit' : type === 'submit' || type === 'image') return el;
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
};
|
package/src/core/early-rules.ts
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
import { holdCss } from './hold';
|
|
17
17
|
import { P, TRACKING } from './routes';
|
|
18
18
|
import type { PlpConfigType, SearchConfigType } from '../types';
|
|
19
|
+
import { NAMED_EARLY_HOLD } from './features';
|
|
19
20
|
|
|
20
21
|
const EARLY_KEY = 'lbx:early';
|
|
21
22
|
// Short: when the bundle stops running (switched off, blocked), holds end soon after.
|
|
@@ -36,7 +37,7 @@ const esc = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
|
36
37
|
// reserveSelector: set by loadConfig (default: the first anchor).
|
|
37
38
|
|
|
38
39
|
/** Capture groups in a regex source (its alternation with the empty string always matches). */
|
|
39
|
-
const groups = (re: string) =>
|
|
40
|
+
const groups = (re: string) => RegExp(`${re}|`).exec('')!.length - 1;
|
|
40
41
|
|
|
41
42
|
/**
|
|
42
43
|
* Only what the snippet can express (no custom readQuery/active): a path
|
|
@@ -61,7 +62,7 @@ export const searchRule = (c: SearchConfigType, hostCss: string): EarlyRoute | n
|
|
|
61
62
|
* the unclaimed hold at boot; `earlyHold: false` opts out where it is not.
|
|
62
63
|
*/
|
|
63
64
|
export const plpRule = (c: PlpConfigType, hostCss: string): EarlyRoute | null => {
|
|
64
|
-
if (c.earlyHold === false) return null;
|
|
65
|
+
if ((NAMED_EARLY_HOLD ? c.earlyHold === false : false)) return null;
|
|
65
66
|
const pm = c.params ?? {};
|
|
66
67
|
// Their page param too: their page 1 is ours. A later page of theirs is
|
|
67
68
|
// theirs (plpAccepts), a value the snippet does not check: held until the
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// The widgets an integration has (build/vite.mjs `features`): a widget is its
|
|
2
|
+
// folder (integration/autocomplete/, search/, plp/, recommender <name>/; an
|
|
3
|
+
// _analytics.ts or _widget.ts in any folder); a feature only the
|
|
4
|
+
// integration's own files can ask for (a template's attribute, a hook its
|
|
5
|
+
// _init.ts sets) by that text in them. The build defines these from the folders, so
|
|
6
|
+
// the entry leaves out what a missing widget's triggers need. The dev server
|
|
7
|
+
// (folders come and go), the render service and unit tests: all true.
|
|
8
|
+
// As in core/frameworks.ts: a flag in `if (FLAG)` or `FLAG ? x : y` only.
|
|
9
|
+
|
|
10
|
+
declare const __LBX_AUTOCOMPLETE__: boolean | undefined;
|
|
11
|
+
declare const __LBX_SEARCH__: boolean | undefined;
|
|
12
|
+
declare const __LBX_PLP__: boolean | undefined;
|
|
13
|
+
declare const __LBX_RECOMMENDER__: boolean | undefined;
|
|
14
|
+
declare const __LBX_ANALYTICS__: boolean | undefined;
|
|
15
|
+
declare const __LBX_SSR__: boolean | undefined;
|
|
16
|
+
declare const __LBX_WIDGET_INITS__: boolean | undefined;
|
|
17
|
+
declare const __LBX_CART__: boolean | undefined;
|
|
18
|
+
declare const __LBX_DRAWER__: boolean | undefined;
|
|
19
|
+
declare const __LBX_MENU__: boolean | undefined;
|
|
20
|
+
declare const __LBX_DROPDOWN__: boolean | undefined;
|
|
21
|
+
declare const __LBX_AC_HOOKS__: boolean | undefined;
|
|
22
|
+
declare const __LBX_ON_RENDER__: boolean | undefined;
|
|
23
|
+
declare const __LBX_READ_QUERY__: boolean | undefined;
|
|
24
|
+
declare const __LBX_FACETS_FOR__: boolean | undefined;
|
|
25
|
+
declare const __LBX_QUERY_URL__: boolean | undefined;
|
|
26
|
+
declare const __LBX_HOST_STYLES__: boolean | undefined;
|
|
27
|
+
declare const __LBX_TRANSLATIONS__: boolean | undefined;
|
|
28
|
+
declare const __LBX_INITS__: boolean | undefined;
|
|
29
|
+
declare const __LBX_EMPTY_VALUES__: boolean | undefined;
|
|
30
|
+
declare const __LBX_PRERENDER_TYPING__: boolean | undefined;
|
|
31
|
+
declare const __LBX_CONTENT_VISIBILITY__: boolean | undefined;
|
|
32
|
+
declare const __LBX_NAMED_PRECONNECT_IMAGES__: boolean | undefined;
|
|
33
|
+
declare const __LBX_NAMED_PREFETCH_FILTERS__: boolean | undefined;
|
|
34
|
+
declare const __LBX_NAMED_PRELOAD__: boolean | undefined;
|
|
35
|
+
declare const __LBX_NAMED_FADE__: boolean | undefined;
|
|
36
|
+
declare const __LBX_NAMED_RESTORE_ON_BACK__: boolean | undefined;
|
|
37
|
+
declare const __LBX_NAMED_PREFETCH_NEXT_PAGE__: boolean | undefined;
|
|
38
|
+
declare const __LBX_NAMED_IN_PLACE_QUERY__: boolean | undefined;
|
|
39
|
+
declare const __LBX_NAMED_PREFETCH_WHILE_TYPING__: boolean | undefined;
|
|
40
|
+
declare const __LBX_NAMED_EARLY_HOLD__: boolean | undefined;
|
|
41
|
+
declare const __LBX_NAMED_SPECULATION__: boolean | undefined;
|
|
42
|
+
|
|
43
|
+
/** integration/autocomplete/. */
|
|
44
|
+
export const AUTOCOMPLETE = typeof __LBX_AUTOCOMPLETE__ === 'boolean' ? __LBX_AUTOCOMPLETE__ : true;
|
|
45
|
+
/** integration/search/. */
|
|
46
|
+
export const SEARCH = typeof __LBX_SEARCH__ === 'boolean' ? __LBX_SEARCH__ : true;
|
|
47
|
+
/** integration/plp/. */
|
|
48
|
+
export const PLP = typeof __LBX_PLP__ === 'boolean' ? __LBX_PLP__ : true;
|
|
49
|
+
/** An integration/recommender <name>/. */
|
|
50
|
+
export const RECOMMENDER = typeof __LBX_RECOMMENDER__ === 'boolean' ? __LBX_RECOMMENDER__ : true;
|
|
51
|
+
/** An _analytics.ts in any folder (without one, analytics is off: main.ts). */
|
|
52
|
+
export const ANALYTICS = typeof __LBX_ANALYTICS__ === 'boolean' ? __LBX_ANALYTICS__ : true;
|
|
53
|
+
/** A _widget.ts in any folder (without one, core/widget-inits.ts has nothing to run). */
|
|
54
|
+
export const WIDGET_INITS = typeof __LBX_WIDGET_INITS__ === 'boolean' ? __LBX_WIDGET_INITS__ : true;
|
|
55
|
+
/** `data-lbx-cart` in any file of the integration (its templates): a tile's add-to-cart (core/tiles.ts). */
|
|
56
|
+
export const CART = typeof __LBX_CART__ === 'boolean' ? __LBX_CART__ : true;
|
|
57
|
+
/** `data-lbx-drawer` in any file of the integration: the listing's filter drawer (widgets/listing/element.ts). */
|
|
58
|
+
export const DRAWER = typeof __LBX_DRAWER__ === 'boolean' ? __LBX_DRAWER__ : true;
|
|
59
|
+
/** `data-lbx-menu` in any file of the integration: menus in the listing's markup (widgets/listing/element.ts). */
|
|
60
|
+
export const MENU = typeof __LBX_MENU__ === 'boolean' ? __LBX_MENU__ : true;
|
|
61
|
+
/** `data-lbx-dropdown` in any file of the integration: `<details>` menus in the listing's markup, closed from outside. */
|
|
62
|
+
export const DROPDOWN = typeof __LBX_DROPDOWN__ === 'boolean' ? __LBX_DROPDOWN__ : true;
|
|
63
|
+
/** The autocomplete's `onOpen`, `onClose`, `onSelect`, `onReady` named in the integration's code. */
|
|
64
|
+
export const AC_HOOKS = typeof __LBX_AC_HOOKS__ === 'boolean' ? __LBX_AC_HOOKS__ : true;
|
|
65
|
+
/** A listing's `onRender` named in the integration's code. */
|
|
66
|
+
export const ON_RENDER = typeof __LBX_ON_RENDER__ === 'boolean' ? __LBX_ON_RENDER__ : true;
|
|
67
|
+
/** The search page's `readQuery` named in the integration's code. */
|
|
68
|
+
export const READ_QUERY = typeof __LBX_READ_QUERY__ === 'boolean' ? __LBX_READ_QUERY__ : true;
|
|
69
|
+
/** A category page's `facetsFor` named in the integration's code. */
|
|
70
|
+
export const FACETS_FOR = typeof __LBX_FACETS_FOR__ === 'boolean' ? __LBX_FACETS_FOR__ : true;
|
|
71
|
+
/** The search page's `queryUrl` named in the integration's code: Enter straight to the query's page (widgets/search/direct.ts), the hook's page as every search's. */
|
|
72
|
+
export const QUERY_URL = typeof __LBX_QUERY_URL__ === 'boolean' ? __LBX_QUERY_URL__ : true;
|
|
73
|
+
/** A style-host.css in any folder (core/integration.ts hostStyle: the host's own to hide or hold). */
|
|
74
|
+
export const HOST_STYLES = typeof __LBX_HOST_STYLES__ === 'boolean' ? __LBX_HOST_STYLES__ : true;
|
|
75
|
+
/** A _translations.ts in any folder (core/integration.ts: over Luma's dictionary). */
|
|
76
|
+
export const TRANSLATIONS = typeof __LBX_TRANSLATIONS__ === 'boolean' ? __LBX_TRANSLATIONS__ : true;
|
|
77
|
+
/** An _init.ts in any folder (core/integration.ts loadConfig). */
|
|
78
|
+
export const INITS = typeof __LBX_INITS__ === 'boolean' ? __LBX_INITS__ : true;
|
|
79
|
+
/**
|
|
80
|
+
* Options off by default, named in the integration's code (its _config.ts,
|
|
81
|
+
* or code setting them): a key no file names keeps its default, as only
|
|
82
|
+
* code naming it can change it. `emptyValues`, `prerenderWhileTyping` (or
|
|
83
|
+
* `pageWhileTyping`: prerendering the search page typed for), `contentVisibility`.
|
|
84
|
+
*/
|
|
85
|
+
export const EMPTY_VALUES = typeof __LBX_EMPTY_VALUES__ === 'boolean' ? __LBX_EMPTY_VALUES__ : true;
|
|
86
|
+
export const PRERENDER_TYPING = typeof __LBX_PRERENDER_TYPING__ === 'boolean' ? __LBX_PRERENDER_TYPING__ : true;
|
|
87
|
+
export const CONTENT_VISIBILITY = typeof __LBX_CONTENT_VISIBILITY__ === 'boolean' ? __LBX_CONTENT_VISIBILITY__ : true;
|
|
88
|
+
/**
|
|
89
|
+
* Server-rendered listings (Luigi's Box's server-side includes): adopting
|
|
90
|
+
* and hydrating the server's markup. Declared, not found in the folders:
|
|
91
|
+
* `clientConfig(url, { serverRendering: false })` for a site without them.
|
|
92
|
+
*/
|
|
93
|
+
export const SSR = typeof __LBX_SSR__ === 'boolean' ? __LBX_SSR__ : true;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Options on by default, named in the integration's code: where none names
|
|
97
|
+
* one it is on (only code naming a key sets it), and a read of it is
|
|
98
|
+
* `NAMED_X ? cfg.x !== false : true` (build/vite.mjs ON_BY_DEFAULT).
|
|
99
|
+
*/
|
|
100
|
+
export const NAMED_PRECONNECT_IMAGES = typeof __LBX_NAMED_PRECONNECT_IMAGES__ === 'boolean' ? __LBX_NAMED_PRECONNECT_IMAGES__ : true;
|
|
101
|
+
export const NAMED_PREFETCH_FILTERS = typeof __LBX_NAMED_PREFETCH_FILTERS__ === 'boolean' ? __LBX_NAMED_PREFETCH_FILTERS__ : true;
|
|
102
|
+
export const NAMED_PRELOAD = typeof __LBX_NAMED_PRELOAD__ === 'boolean' ? __LBX_NAMED_PRELOAD__ : true;
|
|
103
|
+
export const NAMED_FADE = typeof __LBX_NAMED_FADE__ === 'boolean' ? __LBX_NAMED_FADE__ : true;
|
|
104
|
+
export const NAMED_RESTORE_ON_BACK = typeof __LBX_NAMED_RESTORE_ON_BACK__ === 'boolean' ? __LBX_NAMED_RESTORE_ON_BACK__ : true;
|
|
105
|
+
export const NAMED_PREFETCH_NEXT_PAGE = typeof __LBX_NAMED_PREFETCH_NEXT_PAGE__ === 'boolean' ? __LBX_NAMED_PREFETCH_NEXT_PAGE__ : true;
|
|
106
|
+
export const NAMED_IN_PLACE_QUERY = typeof __LBX_NAMED_IN_PLACE_QUERY__ === 'boolean' ? __LBX_NAMED_IN_PLACE_QUERY__ : true;
|
|
107
|
+
export const NAMED_PREFETCH_WHILE_TYPING = typeof __LBX_NAMED_PREFETCH_WHILE_TYPING__ === 'boolean' ? __LBX_NAMED_PREFETCH_WHILE_TYPING__ : true;
|
|
108
|
+
export const NAMED_EARLY_HOLD = typeof __LBX_NAMED_EARLY_HOLD__ === 'boolean' ? __LBX_NAMED_EARLY_HOLD__ : true;
|
|
109
|
+
export const NAMED_SPECULATION = typeof __LBX_NAMED_SPECULATION__ === 'boolean' ? __LBX_NAMED_SPECULATION__ : true;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Development only (src/main.ts, import.meta.env.DEV): a framework on the
|
|
2
|
+
// page whose support the build left out (clientConfig `framework`,
|
|
3
|
+
// core/frameworks.ts) is said in window.__lbx.health.errors. Without its
|
|
4
|
+
// support Luma does not wait for its hydration or talk to its router: a
|
|
5
|
+
// re-rendered page, ours dropped, a router putting its URL back.
|
|
6
|
+
import { SSR } from './features';
|
|
7
|
+
import { ANGULAR, ASTRO, DOCUMENT_REACT, NEXT, REACT, SVELTEKIT, TURBO, VUE } from './frameworks';
|
|
8
|
+
import { reportError } from './health';
|
|
9
|
+
import { reactDocumentSigns } from './own';
|
|
10
|
+
|
|
11
|
+
const keyed = (prefixes: string[]) => [...document.querySelectorAll('*')].some((el) => Object.keys(el).some((k) => prefixes.some((p) => k.startsWith(p))));
|
|
12
|
+
|
|
13
|
+
/** Per part of the support (build/vite.mjs SUPPORTS): left out of this build, and the page shows its framework. */
|
|
14
|
+
const CHECKS: [boolean, string, () => boolean][] = [
|
|
15
|
+
[DOCUMENT_REACT, 'React hydrating the whole document (next, remix or react-router)', reactDocumentSigns],
|
|
16
|
+
[REACT, 'React (react)', () => !!document.querySelector('#__next, #___gatsby, [data-reactroot]') || keyed(['__reactContainer$', '__reactFiber$'])],
|
|
17
|
+
[NEXT, 'the Next.js pages router (next)', () => !!(window as any).__NEXT_DATA__ && !(window as any).next?.appDir],
|
|
18
|
+
[VUE, 'Vue (vue or nuxt)', () => !!((window as any).__NUXT__ || (window as any).$nuxt || document.querySelector('[data-v-app], #__nuxt, [data-server-rendered]')) || keyed(['__vue_app__', '__vue__'])],
|
|
19
|
+
[ANGULAR, 'Angular (angular)', () => !!document.querySelector('[ng-version], [ng-server-context]')],
|
|
20
|
+
[SVELTEKIT, 'SvelteKit (sveltekit)', () => [...document.scripts].some((s) => s.text.includes('__sveltekit_')) || !!document.querySelector('link[href*="/immutable/entry/start."]')],
|
|
21
|
+
[ASTRO, 'Astro (astro)', () => !!document.querySelector('astro-island, meta[name="astro-view-transitions-enabled"]')],
|
|
22
|
+
[TURBO, 'Turbo (turbo)', () => !!((window as any).Turbo || (window as any).Turbolinks)],
|
|
23
|
+
[SSR, 'server-rendered listings (serverRendering)', () => !!document.querySelector('[data-lbx-ssr]')],
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
/** The frameworks on the page this build has no support for (reported once). */
|
|
27
|
+
export const checkFrameworks = () => {
|
|
28
|
+
const missing = CHECKS.filter(([has, , seen]) => !has && seen()).map(([, name]) => name);
|
|
29
|
+
if (missing.length)
|
|
30
|
+
reportError('boot', `this page uses ${missing.join(', ')}, which the build leaves out (clientConfig \`framework\`, \`serverRendering\`). Declare every framework the site uses, or leave them out.`);
|
|
31
|
+
return missing;
|
|
32
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// What of each framework's support a build has (build/vite.mjs SUPPORTS):
|
|
2
|
+
// all of it unless the integration declares its framework(s). Constants the
|
|
3
|
+
// build replaces, so code behind a false one is left out of every chunk.
|
|
4
|
+
// Not built by clientConfig (the render service, unit tests): all true.
|
|
5
|
+
//
|
|
6
|
+
// Use them in `if (FLAG)` or `FLAG ? x : y`, not as a value (`FLAG && x`):
|
|
7
|
+
// Rollup keeps such a reference, and a chunk other than the entry then imports
|
|
8
|
+
// the flag from it and keeps the code (test/browser/specs/budgets.spec.ts,
|
|
9
|
+
// .claude/skills/tree-shaking/SKILL.md).
|
|
10
|
+
|
|
11
|
+
declare const __LBX_REACT__: boolean | undefined;
|
|
12
|
+
declare const __LBX_DOCUMENT_REACT__: boolean | undefined;
|
|
13
|
+
declare const __LBX_NEXT__: boolean | undefined;
|
|
14
|
+
declare const __LBX_VUE__: boolean | undefined;
|
|
15
|
+
declare const __LBX_ANGULAR__: boolean | undefined;
|
|
16
|
+
declare const __LBX_SVELTEKIT__: boolean | undefined;
|
|
17
|
+
declare const __LBX_ASTRO__: boolean | undefined;
|
|
18
|
+
declare const __LBX_TURBO__: boolean | undefined;
|
|
19
|
+
|
|
20
|
+
/** React roots and islands: createRoot / hydrateRoot, Suspense boundaries, Gatsby. */
|
|
21
|
+
export const REACT = typeof __LBX_REACT__ === 'boolean' ? __LBX_REACT__ : true;
|
|
22
|
+
/** React hydrating the whole document (Next.js, Remix, React Router 7 / Hydrogen): the entry's checks (core/own.ts, core/styles.ts). */
|
|
23
|
+
export const DOCUMENT_REACT = typeof __LBX_DOCUMENT_REACT__ === 'boolean' ? __LBX_DOCUMENT_REACT__ : true;
|
|
24
|
+
/** Next.js pages router: its router (core/url.ts), its hydration measure (core/reconciler.ts). */
|
|
25
|
+
export const NEXT = typeof __LBX_NEXT__ === 'boolean' ? __LBX_NEXT__ : true;
|
|
26
|
+
/** Vue 2 / 3 and Nuxt: their mount (core/reconciler.ts), their router (core/url.ts). */
|
|
27
|
+
export const VUE = typeof __LBX_VUE__ === 'boolean' ? __LBX_VUE__ : true;
|
|
28
|
+
/** Angular's hydration (core/reconciler.ts). */
|
|
29
|
+
export const ANGULAR = typeof __LBX_ANGULAR__ === 'boolean' ? __LBX_ANGULAR__ : true;
|
|
30
|
+
/** SvelteKit: its root (core/reconciler.ts), its history entries (core/url.ts). */
|
|
31
|
+
export const SVELTEKIT = typeof __LBX_SVELTEKIT__ === 'boolean' ? __LBX_SVELTEKIT__ : true;
|
|
32
|
+
/** Astro: islands (core/reconciler.ts), ClientRouter history entries (core/url.ts). */
|
|
33
|
+
export const ASTRO = typeof __LBX_ASTRO__ === 'boolean' ? __LBX_ASTRO__ : true;
|
|
34
|
+
/** Turbo Drive / Turbolinks: snapshots (core/reconciler.ts), history and prefetch (core/turbo.ts). */
|
|
35
|
+
export const TURBO = typeof __LBX_TURBO__ === 'boolean' ? __LBX_TURBO__ : true;
|
package/src/core/image-load.ts
CHANGED
|
@@ -50,32 +50,6 @@ export const settleImages = (root: Element, fade = true) => {
|
|
|
50
50
|
if (fade && !root.hasAttribute('data-lbx-fade')) root.setAttribute('data-lbx-fade', '');
|
|
51
51
|
};
|
|
52
52
|
|
|
53
|
-
const preloaded = new Set<string>();
|
|
54
|
-
/** <link rel=preload as=image> for these images (their srcset and sizes, so the candidate the <img> picks is the one fetched), at high priority. */
|
|
55
|
-
export const preloadImages = (list: ImageInfo[]) => {
|
|
56
|
-
try {
|
|
57
|
-
for (const im of list) {
|
|
58
|
-
const key = im.srcset || im.src;
|
|
59
|
-
if (!im.src || preloaded.has(key)) continue;
|
|
60
|
-
preloaded.add(key);
|
|
61
|
-
const l = document.createElement('link');
|
|
62
|
-
l.rel = 'preload';
|
|
63
|
-
l.setAttribute('as', 'image');
|
|
64
|
-
l.href = im.src;
|
|
65
|
-
if (im.srcset) {
|
|
66
|
-
l.setAttribute('imagesrcset', im.srcset);
|
|
67
|
-
if (im.sizes) l.setAttribute('imagesizes', im.sizes);
|
|
68
|
-
}
|
|
69
|
-
l.setAttribute('fetchpriority', 'high');
|
|
70
|
-
l.setAttribute('data-lbx', 'preload');
|
|
71
|
-
document.head.append(l);
|
|
72
|
-
}
|
|
73
|
-
// Bounded: the oldest of ours go (a long session of filters and queries).
|
|
74
|
-
const ours = document.head.querySelectorAll('link[data-lbx="preload"]');
|
|
75
|
-
for (let k = 0; k < ours.length - 24; k++) ours[k].remove();
|
|
76
|
-
} catch {}
|
|
77
|
-
};
|
|
78
|
-
|
|
79
53
|
const warmed = new Set<string>();
|
|
80
54
|
/**
|
|
81
55
|
* The first `cap` images of results fetched ahead, at low priority (the
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// The listing's preload of its first row's images: apart from ./image-load.ts
|
|
2
|
+
// (every widget's), so only the listing's chunk has it.
|
|
3
|
+
import type { ImageInfo } from './images';
|
|
4
|
+
|
|
5
|
+
const preloaded = new Set<string>();
|
|
6
|
+
/** <link rel=preload as=image> for these images (their srcset and sizes, so the candidate the <img> picks is the one fetched), at high priority. */
|
|
7
|
+
export const preloadImages = (list: ImageInfo[]) => {
|
|
8
|
+
try {
|
|
9
|
+
for (const im of list) {
|
|
10
|
+
const key = im.srcset || im.src;
|
|
11
|
+
if (!im.src || preloaded.has(key)) continue;
|
|
12
|
+
preloaded.add(key);
|
|
13
|
+
const l = document.createElement('link');
|
|
14
|
+
l.rel = 'preload';
|
|
15
|
+
l.setAttribute('as', 'image');
|
|
16
|
+
l.href = im.src;
|
|
17
|
+
if (im.srcset) {
|
|
18
|
+
l.setAttribute('imagesrcset', im.srcset);
|
|
19
|
+
if (im.sizes) l.setAttribute('imagesizes', im.sizes);
|
|
20
|
+
}
|
|
21
|
+
l.setAttribute('fetchpriority', 'high');
|
|
22
|
+
l.setAttribute('data-lbx', 'preload');
|
|
23
|
+
document.head.append(l);
|
|
24
|
+
}
|
|
25
|
+
// Bounded: the oldest of ours go (a long session of filters and queries).
|
|
26
|
+
const ours = document.head.querySelectorAll('link[data-lbx="preload"]');
|
|
27
|
+
for (let k = 0; k < ours.length - 24; k++) ours[k].remove();
|
|
28
|
+
} catch {}
|
|
29
|
+
};
|
package/src/core/image-tags.ts
CHANGED
|
@@ -10,9 +10,15 @@ import type { ImagesConfig, Result } from '../types';
|
|
|
10
10
|
* A complete `<img>` (data-lbx-img: load marks, the fade-in). The placeholder
|
|
11
11
|
* goes on its box: put `data-lbx-ph` and `style=${image(r, i).ph}` on the
|
|
12
12
|
* element around it (the `img` helper does).
|
|
13
|
+
*
|
|
14
|
+
* `src` last: lit sets the attributes in this order, and WebKit starts an
|
|
15
|
+
* image's fetch the moment `src` is set, with the attributes it has then.
|
|
16
|
+
* With `loading` after it, every lazy image of a listing was fetched at once
|
|
17
|
+
* in Safari (and the listing's rehearsal fetched its `about:blank`, a console
|
|
18
|
+
* error); with `srcset` after it, the `src` was fetched besides the candidate.
|
|
13
19
|
*/
|
|
14
20
|
export const imgOf = (im: ImageInfo, o: ImageOptions = {}) =>
|
|
15
|
-
html`<img data-lbx-img
|
|
21
|
+
html`<img data-lbx-img loading=${im.loading} fetchpriority=${im.fetchpriority ?? nothing} decoding="async" width=${im.width} height=${im.height} alt=${o.alt ?? ''} class=${o.class ?? nothing} title=${o.title ?? nothing} sizes=${im.sizes || nothing} srcset=${im.srcset || nothing} src=${im.src} />`;
|
|
16
22
|
|
|
17
23
|
/** The template helpers: `image(r, i, opts)` (the attributes) and `img(r, i, opts)` (an `<img>` in its placeholder box). */
|
|
18
24
|
export const imageHelpers = (cfg: ImagesConfig = {}, lazy = false) => ({
|
package/src/core/images.ts
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
import type { ImagesConfig, Result } from '../types';
|
|
17
17
|
import { first, safeUrl } from './result';
|
|
18
|
+
import { NAMED_PRELOAD } from './features';
|
|
18
19
|
|
|
19
20
|
/** What a template binds on an `<img>` (or its `<source>`), and on the image's box (`ph`). */
|
|
20
21
|
export interface ImageInfo {
|
|
@@ -51,7 +52,7 @@ const res = new Map<string, RegExp | null>();
|
|
|
51
52
|
const regex = (s: string) => {
|
|
52
53
|
if (!res.has(s))
|
|
53
54
|
try {
|
|
54
|
-
res.set(s,
|
|
55
|
+
res.set(s, RegExp(s));
|
|
55
56
|
} catch {
|
|
56
57
|
res.set(s, null);
|
|
57
58
|
}
|
|
@@ -158,7 +159,7 @@ export const firstRowImages = (cfg: ImagesConfig = {}, results: Result[], all =
|
|
|
158
159
|
const esc = (s: string) => s.replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<');
|
|
159
160
|
/** `<link rel=preload as=image>` markup for a server-rendered listing: every image of the widest first row, each for the widths it is in the first row at. */
|
|
160
161
|
export const preloadMarkup = (cfg: ImagesConfig = {}, results: Result[]) => {
|
|
161
|
-
if (cfg.preload === false) return '';
|
|
162
|
+
if ((NAMED_PRELOAD ? cfg.preload === false : false)) return '';
|
|
162
163
|
return firstRowImages(cfg, results, true)
|
|
163
164
|
.map((im) => {
|
|
164
165
|
const media = firstRowMedia(cfg, im.i);
|
package/src/core/integration.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// templates, translations and helpers the bundle would.
|
|
6
6
|
|
|
7
7
|
import type { LumaApi, LumaConfigType, LumaInitType } from '../types';
|
|
8
|
+
import { AUTOCOMPLETE, HOST_STYLES, INITS, PLP, RECOMMENDER, SEARCH, TRANSLATIONS } from './features';
|
|
8
9
|
|
|
9
10
|
export type Lazy<T> = Record<string, () => Promise<T>>;
|
|
10
11
|
|
|
@@ -52,26 +53,36 @@ export interface IntegrationFiles {
|
|
|
52
53
|
|
|
53
54
|
/** A folder's style-host.css; a category page without its own uses the search page's. */
|
|
54
55
|
export const hostStyle = (files: Pick<IntegrationFiles, 'hostStyles'>, folder: string): string => {
|
|
56
|
+
// None in the integration (core/features.ts): nothing of the host's to hide or hold.
|
|
57
|
+
if (!HOST_STYLES) return '';
|
|
55
58
|
const css = Object.entries(files.hostStyles).find(([p]) => folderOf(p) === folder)?.[1] ?? '';
|
|
56
|
-
return css || (folder === 'plp' ? hostStyle(files, 'search') : '');
|
|
59
|
+
return css || (PLP ? (folder === 'plp' ? hostStyle(files, 'search') : '') : '');
|
|
57
60
|
};
|
|
58
61
|
|
|
59
|
-
/**
|
|
62
|
+
/**
|
|
63
|
+
* `_config.ts` is JSON by contract (code belongs in `_init.ts`): a function
|
|
64
|
+
* in it is dropped. Said in development (the dev server, unit tests), where
|
|
65
|
+
* configs are written; a build (the bundle, the render service's) leaves the
|
|
66
|
+
* check out: ~0.1 kB gzip of every page.
|
|
67
|
+
*/
|
|
60
68
|
const parseConfigJson = (value: any, file: string) => {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
69
|
+
if (import.meta.env.DEV) {
|
|
70
|
+
const check = (v: any, key: string): void => {
|
|
71
|
+
if (typeof v === 'function') console.error(`[lbx] ${file}: "${key}" is a function; _config.ts is parsed as JSON, set functions in _init.ts`);
|
|
72
|
+
else if (v && typeof v === 'object') for (const [k, x] of Object.entries(v)) check(x, k);
|
|
73
|
+
};
|
|
74
|
+
check(value, '');
|
|
75
|
+
}
|
|
66
76
|
return JSON.parse(JSON.stringify(value ?? {}));
|
|
67
77
|
};
|
|
68
78
|
|
|
69
79
|
/** Folder -> slot in the config: `recommender home` -> recommenders.home. */
|
|
70
80
|
export const slot = (config: any, folder: string) => {
|
|
71
81
|
const [type, ...rest] = folder.split(' ');
|
|
72
|
-
if (type !== 'recommender') return (config[type] ??= {});
|
|
82
|
+
if (!RECOMMENDER || type !== 'recommender') return (config[type] ??= {});
|
|
73
83
|
const name = rest.join(' ');
|
|
74
|
-
|
|
84
|
+
// A build fails on such a folder (build/vite.mjs featureDefines): said here in development only.
|
|
85
|
+
if (import.meta.env.DEV && !name) throw Error(`[lbx] "${folder}": a recommender folder needs a name, e.g. "recommender home"`);
|
|
75
86
|
return ((config.recommenders ??= {})[name] ??= {});
|
|
76
87
|
};
|
|
77
88
|
|
|
@@ -83,32 +94,37 @@ export const loadConfig = async (files: Pick<IntegrationFiles, 'configs' | 'init
|
|
|
83
94
|
const config = { _global: {} } as LumaConfigType;
|
|
84
95
|
let globalInit: LumaInitType | undefined;
|
|
85
96
|
for (const [p, v] of Object.entries(files.configs)) Object.assign(slot(config, folderOf(p)), parseConfigJson(v, p));
|
|
86
|
-
for (const [p, v] of Object.entries(files.translations)) if (typeof v !== 'function') slot(config, folderOf(p)).translations = parseConfigJson(v, p);
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
97
|
+
if (TRANSLATIONS) for (const [p, v] of Object.entries(files.translations)) if (typeof v !== 'function') slot(config, folderOf(p)).translations = parseConfigJson(v, p);
|
|
98
|
+
// No _init.ts in the integration (core/features.ts INITS): no function sets initialize either.
|
|
99
|
+
if (INITS)
|
|
100
|
+
for (const [p, v] of Object.entries(files.inits)) {
|
|
101
|
+
if (folderOf(p) === '_global') globalInit = v;
|
|
102
|
+
else slot(config, folderOf(p)).initialize = v;
|
|
103
|
+
}
|
|
91
104
|
const Luma: LumaApi = { version, config };
|
|
92
105
|
if (globalInit && (await globalInit(config, Luma)) === false) return null;
|
|
93
106
|
if (config._global.disabled || !config._global.publishableKey || !config._global.channelId) return null;
|
|
94
|
-
const apps = [config.autocomplete, config.search, config.plp, ...Object.values(config.recommenders ?? {})];
|
|
95
|
-
for (const app of apps) if (app && !app.disabled && app.initialize && (await (app.initialize as any)(app, Luma)) === false) app.disabled = true;
|
|
107
|
+
const apps = [AUTOCOMPLETE ? config.autocomplete : null, SEARCH ? config.search : null, PLP ? config.plp : null, ...(RECOMMENDER ? Object.values(config.recommenders ?? {}) : [])];
|
|
108
|
+
if (INITS) for (const app of apps) if (app && !app.disabled && app.initialize && (await (app.initialize as any)(app, Luma)) === false) app.disabled = true;
|
|
96
109
|
// Anything a category page does not set is the search page's.
|
|
97
|
-
const
|
|
98
|
-
if (plp) for (const k of LISTING) if (plp[k] == null && search?.[k] != null) (plp as any)[k] = search[k];
|
|
99
|
-
for (const l of [search, plp]) if (l && l.reserveSelector == null && l.anchor) l.reserveSelector = [l.anchor].flat()[0];
|
|
110
|
+
const plp = PLP ? config.plp : null;
|
|
111
|
+
if (plp) for (const k of LISTING) if (plp[k] == null && config.search?.[k] != null) (plp as any)[k] = config.search[k];
|
|
112
|
+
if (SEARCH || PLP) for (const l of [config.search, config.plp]) if (l && l.reserveSelector == null && l.anchor) l.reserveSelector = [l.anchor].flat()[0];
|
|
100
113
|
return config;
|
|
101
114
|
};
|
|
102
115
|
|
|
103
116
|
/** A widget's lazy `_translations.ts` (see IntegrationFiles), into their folders' slots before its context is built. */
|
|
104
117
|
export const loadTranslations = (config: LumaConfigType, files: Pick<IntegrationFiles, 'translations'>, folders: readonly string[]) =>
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
118
|
+
// None in the integration (core/features.ts): Luma's dictionary alone.
|
|
119
|
+
TRANSLATIONS
|
|
120
|
+
? Promise.all(
|
|
121
|
+
Object.entries(files.translations)
|
|
122
|
+
.filter(([p, v]) => typeof v === 'function' && folders.includes(folderOf(p)))
|
|
123
|
+
.map(async ([p, f]) => {
|
|
124
|
+
slot(config, folderOf(p)).translations = parseConfigJson(await f(), p);
|
|
125
|
+
}),
|
|
126
|
+
)
|
|
127
|
+
: Promise.resolve([]);
|
|
112
128
|
|
|
113
129
|
/** A widget's files from a glob, in folder order; from _global/, files named after another widget stay out. */
|
|
114
130
|
export const widgetFiles = <X>(kind: string, folders: string[], glob: Lazy<X>) => {
|