@sitecore-feaas/clientside 0.5.4 → 0.5.6
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/build/dom/rendering.js +20 -11
- package/build/dom/rendering.js.map +1 -1
- package/build/dom/stylesheet.js +11 -12
- package/build/dom/stylesheet.js.map +1 -1
- package/build/fetch.js +46 -22
- package/build/fetch.js.map +1 -1
- package/build/utils/data.js +11 -8
- package/build/utils/data.js.map +1 -1
- package/build/utils/settings.js +1 -1
- package/build/utils/settings.js.map +1 -1
- package/dist/browser/headless.cjs +4 -4
- package/dist/browser/headless.cjs.map +3 -3
- package/dist/browser/headless.esm.js +5 -5
- package/dist/browser/headless.esm.js.map +1 -1
- package/dist/browser/index.cjs +5 -5
- package/dist/browser/index.cjs.map +3 -3
- package/dist/browser/index.esm.js +5 -5
- package/dist/browser/index.esm.js.map +1 -1
- package/dist/browser/react.cjs +5 -5
- package/dist/browser/react.cjs.map +3 -3
- package/dist/browser/react.esm.js +5 -5
- package/dist/browser/react.esm.js.map +1 -1
- package/dist/node/headless.cjs +5 -5
- package/dist/node/headless.cjs.map +3 -3
- package/dist/node/headless.esm.js +5 -5
- package/dist/node/headless.esm.js.map +3 -3
- package/dist/node/index.cjs +4 -4
- package/dist/node/index.cjs.map +3 -3
- package/dist/node/index.esm.js +4 -4
- package/dist/node/index.esm.js.map +3 -3
- package/dist/node/react.cjs +4 -4
- package/dist/node/react.cjs.map +3 -3
- package/dist/node/react.esm.js +4 -4
- package/dist/node/react.esm.js.map +3 -3
- package/headless.cjs +5 -5
- package/headless.cjs.map +3 -3
- package/headless.js +5 -5
- package/headless.js.map +3 -3
- package/package.json +1 -1
- package/react.cjs +5 -5
- package/react.cjs.map +3 -3
- package/react.js +5 -5
- package/react.js.map +3 -3
- package/src/dom/rendering.ts +31 -16
- package/src/dom/stylesheet.ts +17 -15
- package/src/dom/thumbnail.ts +1 -1
- package/src/fetch.ts +54 -30
- package/src/utils/data.ts +11 -7
- package/src/utils/settings.ts +1 -1
- package/test/FEAASComponent.react.test.tsx +19 -3
- package/test/FEAASComponent.test.tsx +117 -20
- package/test/FEAASStylesheet.test.tsx +13 -11
- package/test/datasettings.test.ts +92 -0
- package/types/dom/rendering.d.ts.map +1 -1
- package/types/dom/stylesheet.d.ts +1 -1
- package/types/dom/stylesheet.d.ts.map +1 -1
- package/types/dom/thumbnail.d.ts +1 -1
- package/types/dom/thumbnail.d.ts.map +1 -1
- package/types/fetch.d.ts +39 -15
- package/types/fetch.d.ts.map +1 -1
- package/types/utils/data.d.ts +2 -2
- package/types/utils/data.d.ts.map +1 -1
- package/types/utils/settings.d.ts +5 -5
package/src/dom/rendering.ts
CHANGED
|
@@ -96,23 +96,33 @@ export function setDOMAttribute(element: HTMLElement, attribute: string, value:
|
|
|
96
96
|
} else if (attribute == 'data-embed-src') {
|
|
97
97
|
loadScript(element, value)
|
|
98
98
|
} else if (attribute == 'data-path-attributes') {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
var
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
99
|
+
if (!Array.isArray(value)) {
|
|
100
|
+
const oldKeys = element.getAttribute('data-attributes-keys')?.split(',').filter(Boolean) || []
|
|
101
|
+
if (!Array.isArray(value))
|
|
102
|
+
for (var property in value) {
|
|
103
|
+
var subvalue = value[property]
|
|
104
|
+
setDOMAttribute(
|
|
105
|
+
element,
|
|
106
|
+
property,
|
|
107
|
+
typeof subvalue == 'object' && subvalue ? JSON.stringify(subvalue) : subvalue,
|
|
108
|
+
config
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
for (var i = 0; i < oldKeys.length; i++) {
|
|
112
|
+
if (value == null || !(oldKeys[i] in value)) {
|
|
113
|
+
setDOMAttribute(element, oldKeys[i], null, config)
|
|
114
|
+
}
|
|
113
115
|
}
|
|
116
|
+
setDOMAttribute(element, 'data-attributes-keys', Object.keys(value || {}).join(','))
|
|
117
|
+
}
|
|
118
|
+
// BYOC components also accept their data object in datasources attribute.
|
|
119
|
+
if (element.tagName == 'FEAAS-EXTERNAL') {
|
|
120
|
+
const datasourceId =
|
|
121
|
+
String(element.getAttribute('data-path-attributes') || '')
|
|
122
|
+
.slice(0, -1)
|
|
123
|
+
.split('.')[0] || '_'
|
|
124
|
+
setDOMAttribute(element, 'datasources', JSON.stringify({ [datasourceId]: value }))
|
|
114
125
|
}
|
|
115
|
-
setDOMAttribute(element, 'data-attributes-keys', Object.keys(value || {}).join(','))
|
|
116
126
|
return
|
|
117
127
|
}
|
|
118
128
|
if (typeof value == 'boolean') {
|
|
@@ -198,7 +208,12 @@ export function renderDOMElement(
|
|
|
198
208
|
for (var i = 0; i < attrs.length; i++) {
|
|
199
209
|
const { name, value: datapath } = attrs[i]
|
|
200
210
|
if (name.startsWith('data-path') && name != 'data-path-scope') {
|
|
201
|
-
setDOMAttribute(
|
|
211
|
+
setDOMAttribute(
|
|
212
|
+
element,
|
|
213
|
+
name,
|
|
214
|
+
queryScopes(datascopes, datapath, true, undefined, element.tagName != 'FEAAS-EXTERNAL'),
|
|
215
|
+
config
|
|
216
|
+
)
|
|
202
217
|
}
|
|
203
218
|
if (name == 'data-embed-src') {
|
|
204
219
|
setDOMAttribute(element, name, element.getAttribute(name), config)
|
package/src/dom/stylesheet.ts
CHANGED
|
@@ -17,23 +17,25 @@ export function renderStylesheetPromise(props: FEAASCDNStylesheetProps, styleshe
|
|
|
17
17
|
}).then(() => stylesheet)
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
const validatedStylesheets = new WeakSet<HTMLStyleElement | HTMLLinkElement>()
|
|
20
21
|
export function loadStylesheet(props: FEAASCDNStylesheetProps, callback?: ValidatableStylesheetCallback) {
|
|
21
|
-
var foundStylesheet
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
callback(null, 'initial')
|
|
27
|
-
return Promise.resolve(foundStylesheet)
|
|
28
|
-
}
|
|
22
|
+
var foundStylesheet: HTMLStyleElement | HTMLLinkElement
|
|
23
|
+
var writableStylesheet: HTMLStyleElement
|
|
24
|
+
|
|
25
|
+
if (foundStylesheet && validatedStylesheets.has(foundStylesheet)) {
|
|
26
|
+
return Promise.resolve(foundStylesheet)
|
|
29
27
|
}
|
|
30
28
|
|
|
31
29
|
return fetchAndRevalidateStylesheet(props, (cssText, phase) => {
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
foundStylesheet ||= findStylesheet(props)
|
|
31
|
+
// dont need to write stylesheet for cached state if we already found a stylesheet
|
|
32
|
+
if (!(phase == 'initial' && foundStylesheet)) {
|
|
33
|
+
writableStylesheet ||=
|
|
34
|
+
!foundStylesheet || foundStylesheet.tagName == 'LINK' ? createStylesheet(props) : foundStylesheet
|
|
35
|
+
writableStylesheet.textContent = cssText
|
|
34
36
|
}
|
|
35
37
|
callback?.(cssText, phase)
|
|
36
|
-
}).then(() => foundStylesheet ||
|
|
38
|
+
}).then(() => foundStylesheet || writableStylesheet)
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
export function loadStylesheetAllowStale(props: FEAASCDNStylesheetProps) {
|
|
@@ -43,10 +45,10 @@ export function loadStylesheetAllowStale(props: FEAASCDNStylesheetProps) {
|
|
|
43
45
|
}
|
|
44
46
|
|
|
45
47
|
export function findStylesheet(props: FEAASCDNStylesheetProps) {
|
|
46
|
-
return (
|
|
47
|
-
document.
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
return (document.head.querySelector(`style[data-href="${getStylesheetURL(props)}"]`) ||
|
|
49
|
+
document.querySelector(`link[rel="stylesheet"][href="${getStylesheetURL(props)}"]`)) as
|
|
50
|
+
| HTMLStyleElement
|
|
51
|
+
| HTMLLinkElement
|
|
50
52
|
}
|
|
51
53
|
|
|
52
54
|
export function createStylesheet(props: FEAASCDNStylesheetProps) {
|
package/src/dom/thumbnail.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { SDK, Style, VersionModel, ComponentModel, StylesheetModel } from '
|
|
|
2
2
|
import { fetchAndRevalidate } from '../fetch.js'
|
|
3
3
|
import { renderComponent } from './component.js'
|
|
4
4
|
|
|
5
|
-
export type Callback = (image: HTMLImageElement, isCached: boolean) => void
|
|
5
|
+
export type Callback = (image: HTMLImageElement | null, isCached: boolean) => void
|
|
6
6
|
|
|
7
7
|
export const convertBase64toBlob = (base64: string) => fetch(base64).then((res) => res.blob())
|
|
8
8
|
|
package/src/fetch.ts
CHANGED
|
@@ -1,17 +1,5 @@
|
|
|
1
1
|
import { ControlledPromise } from './utils/promise.js'
|
|
2
2
|
|
|
3
|
-
/**
|
|
4
|
-
* Utility to cache-bust static files. It relies on reading Date header to detect cached response, so it requires CORS
|
|
5
|
-
* settings to be set.
|
|
6
|
-
*
|
|
7
|
-
* It accepts validator function that determines if response is usable. If response was pulled from cache and is valid,
|
|
8
|
-
* the optional callback function is fired right away. Then HEAD request is made bypassing the cache, to see if the
|
|
9
|
-
* response has an newer Last-Modified header. In case it does and it passes validation, third request is fired that
|
|
10
|
-
* actually fetches the new content and callback is invoked the second time.
|
|
11
|
-
*
|
|
12
|
-
* It works as promise, but also accepts callback to use cached blobs mid-flight and have access to headers. Any network
|
|
13
|
-
* errors will not be caught. Function throws when unable to fetch valid response.
|
|
14
|
-
*/
|
|
15
3
|
const phases = ['initial', 'check', 'updated'] as const
|
|
16
4
|
export type ValidatableRequestPhase = (typeof phases)[number]
|
|
17
5
|
export type FetchCache = Record<string, FetchCacheRequest>
|
|
@@ -38,6 +26,18 @@ export function clearFetchCache() {
|
|
|
38
26
|
fetchCache = {}
|
|
39
27
|
}
|
|
40
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Utility to cache-bust static files. It relies on reading Date header to detect cached response, so it requires CORS
|
|
31
|
+
* settings to be set.
|
|
32
|
+
*
|
|
33
|
+
* It accepts validator function that determines if response is usable. If response was pulled from cache and is valid,
|
|
34
|
+
* the optional callback function is fired right away. Then HEAD request is made bypassing the cache, to see if the
|
|
35
|
+
* response has an newer Last-Modified header. In case it does and it passes validation, third request is fired that
|
|
36
|
+
* actually fetches the new content and callback is invoked the second time.
|
|
37
|
+
*
|
|
38
|
+
* It works as promise, but also accepts callback to use cached blobs mid-flight and have access to headers. Any network
|
|
39
|
+
* errors will not be caught. Function throws when unable to fetch valid response.
|
|
40
|
+
*/
|
|
41
41
|
export async function fetchAndRevalidate(
|
|
42
42
|
url: ValidatableRequest['url'],
|
|
43
43
|
options?: ValidatableRequest['options'],
|
|
@@ -51,23 +51,22 @@ export async function fetchAndRevalidate(
|
|
|
51
51
|
.then((result) => result.response)
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
54
|
+
/**
|
|
55
|
+
* Executes a fetch request optionally validating and handling the response. Using callback function it's possible to
|
|
56
|
+
* observe successful response twice: Cached version first, and up-to-date later.
|
|
57
|
+
*
|
|
58
|
+
* @param {Object} request - An object containing request parameters.
|
|
59
|
+
* @param {string} request.url - The URL to fetch.
|
|
60
|
+
* @param {Object} request.options - Fetch request options.
|
|
61
|
+
* @param {Function} [request.validator] - Function to validate the response. Should return `false` if
|
|
62
|
+
* validation fails.
|
|
63
|
+
* @param {Function} [request.callback] - Function to execute after fetching. Receives the response,
|
|
64
|
+
* phase, and cache status.
|
|
65
|
+
* @param {Object} [request.cache] - Cache object to store or retrieve cached data.
|
|
66
|
+
* @param {ValidatableRequestPhase} [phase='initial'] - The phase of the request, defaults to 'initial'.
|
|
67
|
+
* @returns {Promise<Object>} A promise that resolves to an object containing request details and the response.
|
|
68
|
+
* @throws {Error} Throws an error if the fetch fails or if the response is not valid according to the validator.
|
|
69
|
+
*/
|
|
71
70
|
export async function fetchWithCacheAwareness(
|
|
72
71
|
{ url, options, validator, callback, cache }: ValidatableRequest,
|
|
73
72
|
phase: ValidatableRequestPhase = 'initial'
|
|
@@ -107,6 +106,31 @@ export async function fetchWithCacheAwareness(
|
|
|
107
106
|
}
|
|
108
107
|
}
|
|
109
108
|
|
|
109
|
+
export function getFetchCache(url: Parameters<typeof fetch>[0], dontCreate = false) {
|
|
110
|
+
const key = String(url).replace('?head', '')
|
|
111
|
+
var cache = fetchCache[key]
|
|
112
|
+
if (!cache && !dontCreate) {
|
|
113
|
+
cache = fetchCache[key] = {
|
|
114
|
+
phase: null,
|
|
115
|
+
initial: ControlledPromise(),
|
|
116
|
+
check: ControlledPromise(),
|
|
117
|
+
updated: ControlledPromise(),
|
|
118
|
+
final: ControlledPromise()
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return cache
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export class InvalidResponse extends Error {}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Validates cache of previousl fetch If the resource is cached and there is a newer version available, it will be
|
|
128
|
+
* re-fetched.
|
|
129
|
+
*
|
|
130
|
+
* @param request - The request object containing the URL, cache information, options, validator, and response of
|
|
131
|
+
* initial request.
|
|
132
|
+
* @returns A promise that resolves to the request object.
|
|
133
|
+
*/
|
|
110
134
|
export async function fetchToValidateCache(
|
|
111
135
|
request: ValidatableRequest & Required<Pick<ValidatableRequest, 'response'>>
|
|
112
136
|
) {
|
|
@@ -129,7 +153,7 @@ export async function fetchToValidateCache(
|
|
|
129
153
|
|
|
130
154
|
// there's a newer version of the file that we may fetch
|
|
131
155
|
if (head.headers.get('last-modified') != response.headers.get('last-modified')) {
|
|
132
|
-
//
|
|
156
|
+
// the new resource has to be re-fetched
|
|
133
157
|
return fetchBypassingCache(request)
|
|
134
158
|
}
|
|
135
159
|
}
|
package/src/utils/data.ts
CHANGED
|
@@ -7,11 +7,15 @@ export function queryObjectRaw(json: any, path: string) {
|
|
|
7
7
|
return []
|
|
8
8
|
}
|
|
9
9
|
}
|
|
10
|
-
export function queryObject(json: any, path: string) {
|
|
11
|
-
|
|
10
|
+
export function queryObject(json: any, path: string, flatten = true) {
|
|
11
|
+
const data = queryObjectRaw(json, path) || []
|
|
12
|
+
if (flatten) {
|
|
13
|
+
return data.flat(Infinity)
|
|
14
|
+
}
|
|
15
|
+
return data
|
|
12
16
|
}
|
|
13
17
|
|
|
14
|
-
export function queryScopes(scopes: any, datapath: string, singleValue = false, json: any = scopes) {
|
|
18
|
+
export function queryScopes(scopes: any, datapath: string, singleValue = false, json: any = scopes, flatten = true) {
|
|
15
19
|
var path = datapath
|
|
16
20
|
/* find closest parent scope */
|
|
17
21
|
const keys = Object.keys(scopes).sort((b, a) => a.length - b.length)
|
|
@@ -30,11 +34,11 @@ export function queryScopes(scopes: any, datapath: string, singleValue = false,
|
|
|
30
34
|
}
|
|
31
35
|
}
|
|
32
36
|
|
|
33
|
-
var collection = queryObject(json, path)
|
|
37
|
+
var collection = queryObject(json, path, flatten)
|
|
34
38
|
|
|
35
39
|
// If regular path didn't match any data, try fallbacks
|
|
36
40
|
// datasourceId.wrapper.$.path
|
|
37
|
-
if (
|
|
41
|
+
if (collection.length == 0) {
|
|
38
42
|
collection = [
|
|
39
43
|
// look up in `_` datasource id (catch-all id is used in JSS)
|
|
40
44
|
// _.wrapper.$.path
|
|
@@ -56,11 +60,11 @@ export function queryScopes(scopes: any, datapath: string, singleValue = false,
|
|
|
56
60
|
]
|
|
57
61
|
.filter((v, i, a) => a.indexOf(v) === i)
|
|
58
62
|
.reduce((v, path) => {
|
|
59
|
-
return v.length > 0 ? v : queryObject(json, path)
|
|
63
|
+
return v.length > 0 ? v : queryObject(json, path, flatten)
|
|
60
64
|
}, [] as any[])
|
|
61
65
|
}
|
|
62
66
|
|
|
63
|
-
if (singleValue
|
|
67
|
+
if (singleValue) {
|
|
64
68
|
return collection[0]
|
|
65
69
|
} else {
|
|
66
70
|
return collection
|
package/src/utils/settings.ts
CHANGED
|
@@ -52,7 +52,7 @@ export function clean(settings: any) {
|
|
|
52
52
|
* @returns {boolean} - True if the data is of type DataSettings, false otherwise.
|
|
53
53
|
*/
|
|
54
54
|
export const isDataSettings = (data: any): data is DataSettings => {
|
|
55
|
-
if (typeof data.url != 'string') return false
|
|
55
|
+
if (!data || typeof data.url != 'string') return false
|
|
56
56
|
|
|
57
57
|
for (var property in data) {
|
|
58
58
|
if (
|
|
@@ -9,8 +9,8 @@ function Hello(props: { firstName: string; telephone: string }) {
|
|
|
9
9
|
return React.createElement(
|
|
10
10
|
'div',
|
|
11
11
|
{},
|
|
12
|
-
`Hello my name is ${props.firstName} (${typeof props.firstName}) and my tel is ${
|
|
13
|
-
props.telephone
|
|
12
|
+
`Hello my name is ${props.firstName || 'Unknown'} (${typeof props.firstName}) and my tel is ${
|
|
13
|
+
props.telephone || 'Unknown'
|
|
14
14
|
} (${typeof props.telephone}). And context value is ${context}`
|
|
15
15
|
)
|
|
16
16
|
}
|
|
@@ -169,7 +169,23 @@ describe('React', () => {
|
|
|
169
169
|
></Component>
|
|
170
170
|
</section>
|
|
171
171
|
</DataContext.Provider>,
|
|
172
|
-
'<section><feaas-component class="-feaas" data="{"telephone":321}"><feaas-external data-external-id="MyTestComponent" data-path-attributes="$" first-name="Yarik" telephone="321" data-attributes-keys="telephone" hydrate="false"><div>Hello my name is Yarik (string) and my tel is 321 (number). And context value is INHERITED</div></feaas-external></feaas-component></section>'
|
|
172
|
+
'<section><feaas-component class="-feaas" data="{"telephone":321}"><feaas-external data-external-id="MyTestComponent" data-path-attributes="$" first-name="Yarik" telephone="321" data-attributes-keys="telephone" datasources="{"_":{"telephone":321}}" hydrate="false"><div>Hello my name is Yarik (string) and my tel is 321 (number). And context value is INHERITED</div></feaas-external></feaas-component></section>'
|
|
173
|
+
)
|
|
174
|
+
})
|
|
175
|
+
it('render not map attributes of external component with given array', async () => {
|
|
176
|
+
registerComponent(Hello, helloSchema)
|
|
177
|
+
validate(
|
|
178
|
+
<DataContext.Provider value='INHERITED'>
|
|
179
|
+
<section>
|
|
180
|
+
<Component
|
|
181
|
+
template={
|
|
182
|
+
'<feaas-external first-name="Yarik" data-path-attributes="$" data-external-id="MyTestComponent"></feaas-external>'
|
|
183
|
+
}
|
|
184
|
+
data={[{ telephone: 321 }]}
|
|
185
|
+
></Component>
|
|
186
|
+
</section>
|
|
187
|
+
</DataContext.Provider>,
|
|
188
|
+
'<section><feaas-component class="-feaas" data="[{"telephone":321}]"><feaas-external data-external-id="MyTestComponent" first-name="Yarik" data-path-attributes="$" datasources="{"_":[{"telephone":321}]}" hydrate="false"><div>Hello my name is Yarik (string) and my tel is Unknown (undefined). And context value is INHERITED</div></feaas-external></feaas-component></section>'
|
|
173
189
|
)
|
|
174
190
|
})
|
|
175
191
|
})
|
|
@@ -25,7 +25,7 @@ describe('Components', () => {
|
|
|
25
25
|
const spy = vi.spyOn(global, 'fetch')
|
|
26
26
|
spy.mockImplementation(async (url: string, options: Parameters<typeof fetch>[1] = {}) => {
|
|
27
27
|
if (url.match('CDN/components/L/C/V/R')) {
|
|
28
|
-
return new Response('
|
|
28
|
+
return new Response('FRESH', {
|
|
29
29
|
headers: {
|
|
30
30
|
date: now.toUTCString(),
|
|
31
31
|
'last-modified': earlier.toUTCString()
|
|
@@ -43,7 +43,7 @@ describe('Components', () => {
|
|
|
43
43
|
await expect(component.whenRendered).resolves.toEqual(component)
|
|
44
44
|
expect(spy).toHaveBeenCalledTimes(1)
|
|
45
45
|
|
|
46
|
-
expect(component.innerHTML).toContain('
|
|
46
|
+
expect(component.innerHTML).toContain('FRESH')
|
|
47
47
|
})
|
|
48
48
|
|
|
49
49
|
describe('fetch control', () => {
|
|
@@ -116,7 +116,7 @@ describe('Components', () => {
|
|
|
116
116
|
const spy = vi.spyOn(global, 'fetch')
|
|
117
117
|
spy.mockImplementation(async (url: string, options: Parameters<typeof fetch>[1] = {}) => {
|
|
118
118
|
if (url.match('CDN/components/L/C/V/R')) {
|
|
119
|
-
return new Response('
|
|
119
|
+
return new Response('FRESH', {
|
|
120
120
|
headers: {
|
|
121
121
|
date: now.toUTCString(),
|
|
122
122
|
'last-modified': earlier.toUTCString()
|
|
@@ -139,7 +139,7 @@ describe('Components', () => {
|
|
|
139
139
|
|
|
140
140
|
await expect(component.whenRendered).resolves.toEqual(component)
|
|
141
141
|
expect(spy).toHaveBeenCalledTimes(2)
|
|
142
|
-
expect(component.innerHTML).toBe('
|
|
142
|
+
expect(component.innerHTML).toBe('FRESH')
|
|
143
143
|
expect(document.head.querySelectorAll('style').length).toEqual(1)
|
|
144
144
|
})
|
|
145
145
|
it('should make data/css requests when fetch="stylesheet data"', async () => {
|
|
@@ -208,7 +208,7 @@ describe('Components', () => {
|
|
|
208
208
|
const spy = vi.spyOn(global, 'fetch')
|
|
209
209
|
spy.mockImplementation(async (url: string, options: Parameters<typeof fetch>[1] = {}) => {
|
|
210
210
|
if (url.match('http://a/components/b/c/d/1.html')) {
|
|
211
|
-
return new Response('
|
|
211
|
+
return new Response('FRESH', {
|
|
212
212
|
headers: {
|
|
213
213
|
date: now.toUTCString(),
|
|
214
214
|
'last-modified': earlier.toUTCString()
|
|
@@ -226,7 +226,7 @@ describe('Components', () => {
|
|
|
226
226
|
expect(spy).toHaveBeenCalledTimes(1)
|
|
227
227
|
|
|
228
228
|
expect(component.outerHTML).toBe(
|
|
229
|
-
`<feaas-component fetch="template data" src="http://a/components/b/c/d/1.html" cdn="http://a" library="b" component="c" version="d" revision="1" class="-feaas">
|
|
229
|
+
`<feaas-component fetch="template data" src="http://a/components/b/c/d/1.html" cdn="http://a" library="b" component="c" version="d" revision="1" class="-feaas">FRESH</feaas-component>`
|
|
230
230
|
)
|
|
231
231
|
})
|
|
232
232
|
|
|
@@ -234,7 +234,7 @@ describe('Components', () => {
|
|
|
234
234
|
const spy = vi.spyOn(global, 'fetch')
|
|
235
235
|
spy.mockImplementation(async (url: string, options: Parameters<typeof fetch>[1] = {}) => {
|
|
236
236
|
if (url.match('http://a/components/b/c/d/1.html')) {
|
|
237
|
-
return new Response('
|
|
237
|
+
return new Response('FRESH', {
|
|
238
238
|
headers: {
|
|
239
239
|
date: now.toUTCString(),
|
|
240
240
|
'last-modified': earlier.toUTCString()
|
|
@@ -260,16 +260,63 @@ describe('Components', () => {
|
|
|
260
260
|
expect(document.head.querySelectorAll('style')[1].getAttribute('data-href')).toBe('http://a/styles/b/staged.css')
|
|
261
261
|
})
|
|
262
262
|
|
|
263
|
-
it('should
|
|
263
|
+
it('should validate stylesheet when included as link before rendering component', async () => {
|
|
264
264
|
const spy = vi.spyOn(global, 'fetch')
|
|
265
265
|
spy.mockImplementation(async (url: string, options: Parameters<typeof fetch>[1] = {}) => {
|
|
266
266
|
if (url.match('http://a/components/b/c/d/1.html')) {
|
|
267
|
-
return new Response('
|
|
267
|
+
return new Response('FRESH', {
|
|
268
268
|
headers: {
|
|
269
269
|
date: now.toUTCString(),
|
|
270
270
|
'last-modified': earlier.toUTCString()
|
|
271
271
|
}
|
|
272
272
|
})
|
|
273
|
+
} else if (url.match('.css')) {
|
|
274
|
+
return new Response('FRESH', {
|
|
275
|
+
headers: {
|
|
276
|
+
date: /*String(options?.method).toLowerCase() == 'head' ? */ now.toUTCString(),
|
|
277
|
+
'last-modified': earlier.toUTCString()
|
|
278
|
+
}
|
|
279
|
+
})
|
|
280
|
+
}
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
document.body.innerHTML =
|
|
284
|
+
'<feaas-component src="http://a/components/b/c/d/1.html"></feaas-component><link rel="stylesheet" href="http://a/styles/b/published.css" />'
|
|
285
|
+
|
|
286
|
+
const component = document.body.firstElementChild as FEAASComponent
|
|
287
|
+
await expect(component.whenRendered).resolves.toEqual(component)
|
|
288
|
+
expect(spy).toHaveBeenCalledTimes(2)
|
|
289
|
+
expect(document.head.querySelectorAll('style').length).toBe(0)
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
it('should validate and re-write stylesheet when included as link before rendering component', async () => {
|
|
293
|
+
const spy = vi.spyOn(global, 'fetch')
|
|
294
|
+
spy.mockImplementation(async (url: string, options: Parameters<typeof fetch>[1] = {}) => {
|
|
295
|
+
if (url.match('http://a/components/b/c/d/1.html')) {
|
|
296
|
+
return new Response('FRESH', {
|
|
297
|
+
headers: {
|
|
298
|
+
date: now.toUTCString(),
|
|
299
|
+
'last-modified': earlier.toUTCString()
|
|
300
|
+
}
|
|
301
|
+
})
|
|
302
|
+
} else if (url.match('.css')) {
|
|
303
|
+
if (!options?.cache) {
|
|
304
|
+
// respond with cached css
|
|
305
|
+
return new Response('CACHED', {
|
|
306
|
+
headers: {
|
|
307
|
+
date: earliest.toUTCString(),
|
|
308
|
+
'last-modified': earliest.toUTCString()
|
|
309
|
+
}
|
|
310
|
+
})
|
|
311
|
+
} else {
|
|
312
|
+
// respond with up to date
|
|
313
|
+
return new Response('CACHED', {
|
|
314
|
+
headers: {
|
|
315
|
+
date: now.toUTCString(),
|
|
316
|
+
'last-modified': earliest.toUTCString()
|
|
317
|
+
}
|
|
318
|
+
})
|
|
319
|
+
}
|
|
273
320
|
}
|
|
274
321
|
})
|
|
275
322
|
|
|
@@ -278,15 +325,65 @@ describe('Components', () => {
|
|
|
278
325
|
|
|
279
326
|
const component = document.body.firstElementChild as FEAASComponent
|
|
280
327
|
await expect(component.whenRendered).resolves.toEqual(component)
|
|
328
|
+
expect(spy).toHaveBeenCalledTimes(3)
|
|
329
|
+
// render with stale stylesheet
|
|
330
|
+
expect(document.head.querySelectorAll('style').length).toBe(0)
|
|
331
|
+
await new Promise((r) => setTimeout(r, 50))
|
|
332
|
+
expect(spy).toHaveBeenCalledTimes(3)
|
|
281
333
|
expect(document.head.querySelectorAll('style').length).toBe(0)
|
|
282
334
|
})
|
|
283
335
|
|
|
336
|
+
it('should validate and re-write stylesheet when included as link before rendering component', async () => {
|
|
337
|
+
const spy = vi.spyOn(global, 'fetch')
|
|
338
|
+
spy.mockImplementation(async (url: string, options: Parameters<typeof fetch>[1] = {}) => {
|
|
339
|
+
if (url.match('http://a/components/b/c/d/1.html')) {
|
|
340
|
+
return new Response('FRESH', {
|
|
341
|
+
headers: {
|
|
342
|
+
date: now.toUTCString(),
|
|
343
|
+
'last-modified': earlier.toUTCString()
|
|
344
|
+
}
|
|
345
|
+
})
|
|
346
|
+
} else if (url.match('.css')) {
|
|
347
|
+
if (!options?.cache) {
|
|
348
|
+
// respond with cached css
|
|
349
|
+
return new Response('CACHED', {
|
|
350
|
+
headers: {
|
|
351
|
+
date: earliest.toUTCString(),
|
|
352
|
+
'last-modified': earliest.toUTCString()
|
|
353
|
+
}
|
|
354
|
+
})
|
|
355
|
+
} else {
|
|
356
|
+
if (String(options?.method).toLowerCase() == 'head') await new Promise((r) => setTimeout(r, 50))
|
|
357
|
+
// respond with up to date
|
|
358
|
+
return new Response('FRESH', {
|
|
359
|
+
headers: {
|
|
360
|
+
date: now.toUTCString(),
|
|
361
|
+
'last-modified': now.toUTCString()
|
|
362
|
+
}
|
|
363
|
+
})
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
})
|
|
367
|
+
|
|
368
|
+
document.body.innerHTML =
|
|
369
|
+
'<feaas-component src="http://a/components/b/c/d/1.html"></feaas-component><link rel="stylesheet" href="http://a/styles/b/published.css" />'
|
|
370
|
+
|
|
371
|
+
const component = document.body.firstElementChild as FEAASComponent
|
|
372
|
+
await expect(component.whenRendered).resolves.toEqual(component)
|
|
373
|
+
expect(spy).toHaveBeenCalledTimes(3)
|
|
374
|
+
// render with stale stylesheet
|
|
375
|
+
expect(document.head.querySelectorAll('style').length).toBe(0)
|
|
376
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
377
|
+
expect(spy).toHaveBeenCalledTimes(4)
|
|
378
|
+
expect(document.head.querySelectorAll('style').length).toBe(1)
|
|
379
|
+
})
|
|
380
|
+
|
|
284
381
|
it('should revalidate cached component response', async () => {
|
|
285
382
|
const spy = vi.spyOn(global, 'fetch')
|
|
286
383
|
spy.mockImplementation(async (url: string, options: Parameters<typeof fetch>[1] = {}) => {
|
|
287
384
|
if (url.match('CDN/components/L/C/V/R') && options.method != 'HEAD') {
|
|
288
385
|
// 1. Here's cached component
|
|
289
|
-
return new Response('
|
|
386
|
+
return new Response('CACHED', {
|
|
290
387
|
headers: {
|
|
291
388
|
date: earlier.toUTCString(),
|
|
292
389
|
'last-modified': earlier.toUTCString()
|
|
@@ -311,7 +408,7 @@ describe('Components', () => {
|
|
|
311
408
|
await expect(component.whenRendered).resolves.toEqual(component)
|
|
312
409
|
expect(spy).toHaveBeenCalledTimes(2)
|
|
313
410
|
|
|
314
|
-
expect(component.innerHTML).toContain('
|
|
411
|
+
expect(component.innerHTML).toContain('CACHED')
|
|
315
412
|
})
|
|
316
413
|
|
|
317
414
|
it('should reload component while using stale cache', async () => {
|
|
@@ -335,7 +432,7 @@ describe('Components', () => {
|
|
|
335
432
|
})
|
|
336
433
|
} else {
|
|
337
434
|
// Up to date version
|
|
338
|
-
return new Response('
|
|
435
|
+
return new Response('FRESH', {
|
|
339
436
|
headers: {
|
|
340
437
|
date: now.toUTCString(),
|
|
341
438
|
'last-modified': earlier.toUTCString()
|
|
@@ -356,7 +453,7 @@ describe('Components', () => {
|
|
|
356
453
|
// whenLoaded only fires when ensures that up-to-date version is loaded
|
|
357
454
|
await expect(component.whenLoaded).resolves.toBeTruthy()
|
|
358
455
|
expect(spy).toHaveBeenCalledTimes(3)
|
|
359
|
-
expect(component.innerHTML).toContain('
|
|
456
|
+
expect(component.innerHTML).toContain('FRESH')
|
|
360
457
|
})
|
|
361
458
|
|
|
362
459
|
it('should re-fetch component when attributes change', async () => {
|
|
@@ -418,7 +515,7 @@ describe('Components', () => {
|
|
|
418
515
|
})
|
|
419
516
|
} else {
|
|
420
517
|
await new Promise((resolve) => setTimeout(resolve, 200))
|
|
421
|
-
return new Response('
|
|
518
|
+
return new Response('FRESH {}', {
|
|
422
519
|
headers: {
|
|
423
520
|
date: now.toUTCString(),
|
|
424
521
|
'last-modified': earlier.toUTCString()
|
|
@@ -445,7 +542,7 @@ describe('Components', () => {
|
|
|
445
542
|
} else {
|
|
446
543
|
// Latest
|
|
447
544
|
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
448
|
-
return new Response('
|
|
545
|
+
return new Response('FRESH', {
|
|
449
546
|
headers: {
|
|
450
547
|
date: now.toUTCString(),
|
|
451
548
|
'last-modified': earlier.toUTCString()
|
|
@@ -459,20 +556,20 @@ describe('Components', () => {
|
|
|
459
556
|
const component = document.body.firstElementChild as FEAASComponent
|
|
460
557
|
expect(document.head.querySelector('style')).toBeNull()
|
|
461
558
|
await new Promise((resolve) => setTimeout(resolve, 50))
|
|
462
|
-
|
|
463
|
-
expect(stylesheet.innerHTML).toEqual('')
|
|
559
|
+
expect(document.head.querySelector('style')).toBeNull()
|
|
464
560
|
expect(component.innerHTML).toEqual('')
|
|
465
561
|
// Component will rendered with stale cached stylesheet
|
|
466
562
|
await expect(component.whenRendered).resolves.toEqual(component)
|
|
563
|
+
const stylesheet = document.head.querySelector('style') as HTMLStyleElement
|
|
467
564
|
expect(stylesheet.innerHTML).toEqual('YESTERDAY {}')
|
|
468
565
|
expect(component.innerHTML).toEqual('YESTERDAY')
|
|
469
566
|
// Component will update itself with stale cached stylesheet
|
|
470
|
-
await expect(component.whenLoaded).resolves.toEqual('
|
|
567
|
+
await expect(component.whenLoaded).resolves.toEqual('FRESH')
|
|
471
568
|
expect(stylesheet.innerHTML).toEqual('YESTERDAY {}')
|
|
472
|
-
expect(component.innerHTML).toEqual('
|
|
569
|
+
expect(component.innerHTML).toEqual('FRESH')
|
|
473
570
|
await new Promise((resolve) => setTimeout(resolve, 200))
|
|
474
571
|
// Stylesheet will update later
|
|
475
|
-
expect(stylesheet.innerHTML).toEqual('
|
|
572
|
+
expect(stylesheet.innerHTML).toEqual('FRESH {}')
|
|
476
573
|
})
|
|
477
574
|
|
|
478
575
|
it('render without data', () => {
|