@merkur/integration 0.37.0 → 0.37.11

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/lib/index.cjs CHANGED
@@ -57,165 +57,357 @@ const exported = {
57
57
  test,
58
58
  };
59
59
 
60
- function _loadScript(asset, root) {
61
- return new Promise((resolve, reject) => {
62
- const scriptElement = root.querySelector(`script[src='${asset.source}']`);
60
+ const isLoadedSymbol = Symbol.for('isLoaded');
61
+ const loadingPromiseSymbol = Symbol.for('loadingPromise');
62
+
63
+ function _attachElementToAsset(asset, element) {
64
+ return {
65
+ ...asset,
66
+ element,
67
+ };
68
+ }
63
69
 
64
- if (scriptElement) {
65
- if (!asset.test || exported.test(asset.test)) {
66
- resolve();
67
- }
70
+ function _handleAssetError({
71
+ asset,
72
+ message = `Error loading asset ${asset.source}.`,
73
+ }) {
74
+ if (asset.optional) {
75
+ console.warn(message);
76
+
77
+ return _attachElementToAsset(asset, null);
78
+ }
79
+
80
+ const error = new Error(message);
81
+ error.asset = asset;
82
+
83
+ throw error;
84
+ }
68
85
 
69
- scriptElement.addEventListener('load', resolve);
70
- scriptElement.addEventListener(
71
- 'error',
72
- asset.optional ? resolve : reject,
73
- );
74
- return;
86
+ function _addListenersToAssetElement(asset, element, resolve, reject) {
87
+ element.addEventListener('load', () => {
88
+ resolve(_attachElementToAsset(asset, element));
89
+ element[isLoadedSymbol] = true;
90
+ delete element[loadingPromiseSymbol];
91
+ });
92
+ element.addEventListener('error', () => {
93
+ if (element.parentNode) {
94
+ element.remove();
75
95
  }
76
96
 
77
- const script = document.createElement('script');
97
+ try {
98
+ resolve(_handleAssetError({ asset }));
99
+ } catch (error) {
100
+ reject(error);
101
+ }
102
+ });
103
+ }
78
104
 
79
- if (asset.type === 'script') {
80
- script.defer = true;
81
- script.onload = resolve;
82
- script.onerror = (error) => {
83
- script.remove();
105
+ function _loadStyle(asset, root) {
106
+ if (asset.type === 'inlineStyle') {
107
+ const style = document.createElement('style');
108
+ style.innerHTML = asset.source;
109
+ root.appendChild(style);
110
+
111
+ return _attachElementToAsset(asset, style);
112
+ }
113
+
114
+ const link = document.createElement('link');
115
+
116
+ link[loadingPromiseSymbol] = new Promise((resolve, reject) => {
117
+ _addListenersToAssetElement(asset, link, resolve, reject);
118
+ link.rel = 'stylesheet';
119
+ link.href = asset.source;
120
+
121
+ root.appendChild(link);
122
+ });
123
+
124
+ return link[loadingPromiseSymbol];
125
+ }
126
+
127
+ async function loadStyleAssets(assets, root = document.head) {
128
+ const styleElements = Array.from(root.querySelectorAll('style'));
129
+
130
+ return Promise.all(
131
+ assets.map((asset) => {
132
+ if (
133
+ !['stylesheet', 'inlineStyle'].includes(asset.type) ||
134
+ !asset.source
135
+ ) {
136
+ return _attachElementToAsset(asset, null);
137
+ }
138
+
139
+ if (asset.type === 'stylesheet') {
140
+ const link = root.querySelector(`link[href='${asset.source}']`);
141
+
142
+ if (link) {
143
+ if (link[loadingPromiseSymbol]) {
144
+ return link[loadingPromiseSymbol];
145
+ }
84
146
 
85
- asset.optional ? resolve(error) : reject(error);
86
- };
87
- script.src = asset.source;
88
-
89
- const { attr } = asset;
90
- if (attr && Object.keys(attr).length) {
91
- for (const name in attr) {
92
- const value = attr[name];
93
-
94
- if (typeof value === 'boolean') {
95
- if (value) {
96
- script.setAttribute(name, '');
97
- } else {
98
- script.removeAttribute(name);
99
- }
147
+ return _attachElementToAsset(asset, link);
148
+ }
149
+ }
150
+
151
+ if (asset.type === 'inlineStyle') {
152
+ const inlineStyle = styleElements.find(
153
+ (element) => element.innerHTML === asset.source,
154
+ );
155
+
156
+ if (inlineStyle) {
157
+ return _attachElementToAsset(asset, inlineStyle);
158
+ }
159
+ }
160
+
161
+ return _loadStyle(asset, root);
162
+ }),
163
+ );
164
+ }
165
+
166
+ function _findScriptElement(scriptElements, asset) {
167
+ if (asset.type === 'json') {
168
+ return scriptElements.find(
169
+ (element) => element.dataset.src === asset.source,
170
+ );
171
+ }
172
+
173
+ if (!['script', 'inlineScript', 'inlineJson'].includes(asset.type)) {
174
+ return null;
175
+ }
176
+
177
+ const attributeKey = asset.type === 'script' ? 'src' : 'textContent';
178
+ const source =
179
+ asset.type === 'inlineJson' ? JSON.stringify(asset.source) : asset.source;
180
+
181
+ return (
182
+ scriptElements.find((element) => element[attributeKey] === source) || null
183
+ );
184
+ }
185
+
186
+ function _loadScript(asset, root) {
187
+ const script = document.createElement('script');
188
+
189
+ if (asset.type === 'inlineScript') {
190
+ script.textContent = asset.source;
191
+ root.appendChild(script);
192
+
193
+ return _attachElementToAsset(asset, script);
194
+ }
195
+
196
+ script[loadingPromiseSymbol] = new Promise((resolve, reject) => {
197
+ script.defer = true;
198
+ _addListenersToAssetElement(asset, script, resolve, reject);
199
+ script.src = asset.source;
200
+
201
+ const { attr } = asset;
202
+ if (attr && Object.keys(attr).length) {
203
+ for (const name in attr) {
204
+ const value = attr[name];
205
+
206
+ if (typeof value === 'boolean') {
207
+ if (value) {
208
+ script.setAttribute(name, '');
100
209
  } else {
101
- script.setAttribute(name, value);
210
+ script.removeAttribute(name);
102
211
  }
212
+ } else {
213
+ script.setAttribute(name, value);
103
214
  }
104
215
  }
105
- } else {
106
- script.text = asset.source;
107
- resolve();
108
216
  }
109
217
 
110
218
  root.appendChild(script);
111
219
  });
112
- }
113
220
 
114
- function _loadStyle(asset, root) {
115
- return new Promise((resolve, reject) => {
116
- if (asset.type === 'stylesheet') {
117
- const link = document.createElement('link');
118
- link.onload = resolve;
119
- link.onerror = reject;
120
- link.rel = 'stylesheet';
121
- link.href = asset.source;
122
-
123
- root.appendChild(link);
124
- } else {
125
- const style = document.createElement('style');
126
- style.innerHTML = asset.source;
127
-
128
- root.appendChild(style);
129
- resolve();
130
- }
131
- });
221
+ return script[loadingPromiseSymbol];
132
222
  }
133
223
 
134
- function loadStyleAssets(assets, root = document.head) {
135
- const styleElements = root.querySelectorAll('style');
136
- const stylesToRender = assets.filter(
137
- (asset) =>
138
- asset.source &&
139
- ((asset.type === 'stylesheet' &&
140
- !root.querySelector(`link[href='${asset.source}']`)) ||
141
- (asset.type === 'inlineStyle' &&
142
- Array.from(styleElements).reduce((acc, cur) => {
143
- if (cur.innerHTML === asset.source) {
144
- return false;
145
- }
146
-
147
- return acc;
148
- }, true))),
149
- );
224
+ async function loadScriptAssets(assets, root = document.head) {
225
+ const scriptElements = Array.from(root.querySelectorAll('script'));
226
+
227
+ return Promise.all(
228
+ assets.map((asset) => {
229
+ if (!['script', 'inlineScript'].includes(asset.type) || !asset.source) {
230
+ return _attachElementToAsset(asset, null);
231
+ }
232
+
233
+ const { source } = asset;
234
+ const _asset = Object.assign({}, asset);
235
+
236
+ if (source === Object(source)) {
237
+ if (source.es13 && exported.isES13Supported()) {
238
+ _asset.source = source.es13;
239
+ } else if (source.es11 && exported.isES11Supported()) {
240
+ _asset.source = source.es11;
241
+ } else if (source.es9 && exported.isES9Supported()) {
242
+ _asset.source = source.es9;
243
+ } else {
244
+ _asset.source = null;
245
+ }
246
+
247
+ if (!_asset.source) {
248
+ return _handleAssetError({
249
+ asset: _asset,
250
+ message: `Asset '${_asset.name}' is missing ES variant and could not be loaded.`,
251
+ });
252
+ }
253
+ }
254
+
255
+ if (_asset.test && exported.test(_asset.test)) {
256
+ return _attachElementToAsset(
257
+ _asset,
258
+ _findScriptElement(scriptElements, _asset),
259
+ );
260
+ }
150
261
 
151
- return Promise.all(stylesToRender.map((asset) => _loadStyle(asset, root)));
262
+ const script = _findScriptElement(scriptElements, _asset);
263
+
264
+ if (script && _asset.type === 'script') {
265
+ if (script[loadingPromiseSymbol]) {
266
+ return script[loadingPromiseSymbol];
267
+ }
268
+
269
+ if (script[isLoadedSymbol]) {
270
+ return _attachElementToAsset(_asset, script);
271
+ }
272
+
273
+ return new Promise((resolve, reject) =>
274
+ _addListenersToAssetElement(_asset, script, resolve, reject),
275
+ );
276
+ } else if (script && _asset.type === 'inlineScript') {
277
+ return _attachElementToAsset(_asset, script);
278
+ }
279
+
280
+ return _loadScript(_asset, root);
281
+ }),
282
+ );
152
283
  }
153
284
 
154
- async function loadScriptAssets(assets, root = document.head) {
155
- const scriptElements = root.querySelectorAll('script');
156
- const scriptsToRender = assets.reduce((scripts, asset) => {
157
- const { source } = asset;
158
- const _asset = Object.assign({}, asset);
285
+ async function _fetchData(source) {
286
+ const response = await fetch(source);
159
287
 
160
- if (_asset.type !== 'script' && _asset.type !== 'inlineScript') {
161
- return scripts;
162
- }
288
+ if (!response.ok) {
289
+ throw new Error(
290
+ `Failed to fetch from '${source}' with status ${response.status} ${response.statusText}.`,
291
+ );
292
+ }
293
+
294
+ return response.text();
295
+ }
163
296
 
164
- if (source === Object(source)) {
165
- if (source.es13 && exported.isES13Supported()) {
166
- _asset.source = source.es13;
167
- } else if (source.es11 && exported.isES11Supported()) {
168
- _asset.source = source.es11;
169
- } else if (source.es9 && exported.isES9Supported()) {
170
- _asset.source = source.es9;
171
- } else {
172
- _asset.source = null;
297
+ function _removeElementAfterTimeout(element, timeout) {
298
+ if (timeout) {
299
+ setTimeout(() => {
300
+ if (element.parentNode) {
301
+ element.remove();
173
302
  }
303
+ }, timeout);
304
+ }
305
+ }
306
+
307
+ function _loadJsonAsset(asset, root) {
308
+ const script = document.createElement('script');
309
+ script.type = 'application/json';
310
+
311
+ if (asset.type === 'inlineJson') {
312
+ script.textContent = JSON.stringify(asset.source);
313
+ root.appendChild(script);
314
+ _removeElementAfterTimeout(script, asset.ttl);
315
+
316
+ return _attachElementToAsset(asset, script);
317
+ }
174
318
 
175
- if (!_asset.source) {
176
- const message = `Asset '${_asset.name}' is missing ES variant and could not be loaded.`;
319
+ script[loadingPromiseSymbol] = new Promise((resolve, reject) => {
320
+ script.dataset.src = asset.source;
321
+ root.appendChild(script);
177
322
 
178
- if (!_asset.optional) {
179
- const error = new Error(message);
180
- error.asset = _asset;
323
+ (async () => {
324
+ try {
325
+ const textContent = await _fetchData(asset.source);
326
+ script.textContent = textContent;
327
+ delete script[loadingPromiseSymbol];
328
+ _removeElementAfterTimeout(script, asset.ttl);
329
+ resolve(_attachElementToAsset(asset, script));
330
+ } catch (error) {
331
+ script.remove();
181
332
 
182
- throw error;
333
+ try {
334
+ resolve(
335
+ _handleAssetError({
336
+ asset,
337
+ message: `Error loading JSON asset '${asset.name}': ${error.message}`,
338
+ }),
339
+ );
340
+ } catch (error) {
341
+ reject(error);
183
342
  }
343
+ }
344
+ })();
345
+ });
184
346
 
185
- console.warn(message);
186
- return scripts;
347
+ return script[loadingPromiseSymbol];
348
+ }
349
+
350
+ async function loadJsonAssets(assets, root = document.head) {
351
+ const scriptElements = Array.from(
352
+ root.querySelectorAll('script[type="application/json"]'),
353
+ );
354
+
355
+ return Promise.all(
356
+ assets.map((asset) => {
357
+ if (!['json', 'inlineJson'].includes(asset.type) || !asset.source) {
358
+ return _attachElementToAsset(asset, null);
187
359
  }
188
- }
189
360
 
190
- if (
191
- Array.from(scriptElements).reduce((acc, cur) => {
192
- if (cur.text === _asset.source) {
193
- return true;
361
+ const script = _findScriptElement(scriptElements, asset);
362
+
363
+ if (script) {
364
+ if (script[loadingPromiseSymbol]) {
365
+ return script[loadingPromiseSymbol];
194
366
  }
195
367
 
196
- return acc;
197
- }, false) ||
198
- (_asset.test ? exported.test(_asset.test) : false)
199
- ) {
200
- return scripts;
201
- }
368
+ if (script.textContent) {
369
+ return _attachElementToAsset(asset, script);
370
+ }
202
371
 
203
- scripts.push(_asset);
372
+ return _handleAssetError({
373
+ asset,
374
+ message: `JSON asset '${asset.name}' is missing textContent and could not be loaded.`,
375
+ });
376
+ }
204
377
 
205
- return scripts;
206
- }, []);
378
+ return _loadJsonAsset(asset, root);
379
+ }),
380
+ );
381
+ }
382
+
383
+ function _mergeResults(results) {
384
+ return results.reduce((acc, results) => {
385
+ results.forEach((result, index) => {
386
+ if (!acc[index]) {
387
+ acc[index] = result;
388
+ } else if (result.element) {
389
+ acc[index] = result;
390
+ }
391
+ });
207
392
 
208
- return Promise.all(scriptsToRender.map((asset) => _loadScript(asset, root)));
393
+ return acc;
394
+ }, []);
209
395
  }
210
396
 
211
- function loadAssets(assets, root) {
212
- return Promise.all([
397
+ async function loadAssets(assets, root) {
398
+ const results = await Promise.all([
213
399
  loadScriptAssets(assets, root),
214
400
  loadStyleAssets(assets, root),
401
+ loadJsonAssets(assets, root),
215
402
  ]);
403
+
404
+ return _mergeResults(results);
216
405
  }
217
406
 
407
+ exports.isLoadedSymbol = isLoadedSymbol;
218
408
  exports.loadAssets = loadAssets;
409
+ exports.loadJsonAssets = loadJsonAssets;
219
410
  exports.loadScriptAssets = loadScriptAssets;
220
411
  exports.loadStyleAssets = loadStyleAssets;
412
+ exports.loadingPromiseSymbol = loadingPromiseSymbol;
221
413
  exports.testScript = exported;