Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: add opf-load-once capability on js and css resources #19825

Merged
merged 10 commits into from
Jan 13, 2025
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,13 @@ describe('OpfResourceLoaderService', () => {
const mockScriptResource = {
url: 'script-url',
type: OpfDynamicScriptResourceType.SCRIPT,
attributes: [{ key: 'opf-load-once', value: 'true' }],
};

const mockStyleResource = {
url: 'style-url',
type: OpfDynamicScriptResourceType.STYLES,
attributes: [{ key: 'opf-load-once', value: 'true' }],
};

spyOn<any>(opfResourceLoaderService, 'loadScript').and.callThrough();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,12 @@ export class OpfResourceLoaderService {
protected document = inject(DOCUMENT);
protected platformId = inject(PLATFORM_ID);

protected readonly OPF_RESOURCE_ATTRIBUTE_KEY = 'data-opf-resource';
protected readonly CORS_DEFAULT_VALUE = 'anonymous';
protected readonly OPF_RESOURCE_LOAD_ONCE_ATTRIBUTE_KEY = 'opf-load-once';
protected readonly OPF_RESOURCE_ATTRIBUTE_KEY = 'data-opf-resource';

protected embedStyles(embedOptions: {
attributes?: OpfKeyValueMap[];
attributes?: { [key: string]: string };
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it new type different than OpfKeyValueMap[]?

Copy link
Contributor Author

@FollowTheFlo FollowTheFlo Jan 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, it was set as 'any'.
OpfKeyValueMap is {key:string,value:string}, we receive the list in this format from resource object (resource.attributes).
We perform a conversion to [key: string]: string to make handling easier and have same type as attributes in ScriptLoader service

src: string;
sri?: string;
callback?: EventListener;
Expand All @@ -38,22 +39,18 @@ export class OpfResourceLoaderService {
link.href = src;
link.rel = 'stylesheet';
link.type = 'text/css';
link.setAttribute(this.OPF_RESOURCE_ATTRIBUTE_KEY, 'true');
if (sri) {
link.integrity = sri;
const corsKeyvalue = attributes?.find(
(attr) => attr.key === 'crossorigin' && !!attr.value?.length
);
link.crossOrigin = corsKeyvalue?.value ?? this.CORS_DEFAULT_VALUE;
link.crossOrigin = attributes?.['crossorigin'] ?? this.CORS_DEFAULT_VALUE;
delete attributes?.['crossorigin'];
}
if (attributes?.length) {
attributes.forEach((attribute) => {
const { key, value } = attribute;

attributes &&
Object.keys(attributes)?.forEach((key) => {
if (!(key in link)) {
link.setAttribute(key, value);
link.setAttribute(key, attributes[key as keyof object]);
}
});
}

if (callback) {
link.addEventListener('load', callback);
Expand All @@ -74,39 +71,53 @@ export class OpfResourceLoaderService {
return this.scriptLoader.hasScript(src);
}

/**
* Create attributes intended to script and link elements.
*
* Return attributes list including keyValueList and OPF specific attribute with below logic:
*
* 1. Resource loads only once: 'opf-load-once' key detected, no additional attribute added.
* 2. Resource deleted at page/payment change: 'data-opf-resource' attribute is added.
*/

FollowTheFlo marked this conversation as resolved.
Show resolved Hide resolved
protected createAttributesList(keyValueList?: OpfKeyValueMap[] | undefined): {
[key: string]: string;
} {
const attributes: { [key: string]: string } = {};
keyValueList?.forEach((keyValue: OpfKeyValueMap) => {
attributes[keyValue.key] = keyValue.value;
});
if (attributes?.[this.OPF_RESOURCE_LOAD_ONCE_ATTRIBUTE_KEY] === 'true') {
attributes[this.OPF_RESOURCE_ATTRIBUTE_KEY] = 'true';
}
delete attributes?.[this.OPF_RESOURCE_LOAD_ONCE_ATTRIBUTE_KEY];
return attributes;
}

/**
* Loads a script specified in the resource object.
*
* The returned Promise is resolved when the script is loaded or already present.
* The returned Promise is rejected when a loading error occurs.
*/

protected loadScript(resource: OpfDynamicScriptResource): Promise<void> {
return new Promise((resolve, reject) => {
const attributes: any = {
const attributes: { [key: string]: string } = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we create type for it in a model if we cannot use: OpfKeyValueMap?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mentioned above, we need both:
OpfKeyValueMap is the original format (resource.attributes received from server)
{ [key: string]: string } is format needed for ScriptLoaderService
Thus a conversion is needed.

type: 'text/javascript',
[this.OPF_RESOURCE_ATTRIBUTE_KEY]: true,
...this.createAttributesList(resource.attributes),
};

if (resource?.sri) {
attributes['integrity'] = resource.sri;
const corsKeyvalue: OpfKeyValueMap | undefined =
resource?.attributes?.find(
(attr) => attr.key === 'crossorigin' && !!attr.value?.length
);
attributes['crossOrigin'] =
corsKeyvalue?.value ?? this.CORS_DEFAULT_VALUE;
}

if (resource.attributes) {
resource.attributes.forEach((attribute) => {
attributes[attribute.key] = attribute.value;
});
attributes?.['crossorigin'] ?? this.CORS_DEFAULT_VALUE;
delete attributes?.['crossorigin'];
}

if (resource.url && !this.hasScript(resource.url)) {
if (resource?.url && !this.hasScript(resource.url)) {
this.scriptLoader.embedScript({
attributes,
src: resource.url,
attributes: attributes,
callback: () => resolve(),
errorCallback: () => reject(),
disableKeyRestriction: true,
Expand All @@ -123,11 +134,12 @@ export class OpfResourceLoaderService {
* The returned Promise is resolved when the stylesheet is loaded or already present.
* The returned Promise is rejected when a loading error occurs.
*/

protected loadStyles(resource: OpfDynamicScriptResource): Promise<void> {
return new Promise((resolve, reject) => {
if (resource.url && !this.hasStyles(resource.url)) {
this.embedStyles({
attributes: resource?.attributes,
attributes: this.createAttributesList(resource?.attributes),
src: resource.url,
sri: resource?.sri,
callback: () => resolve(),
Expand Down Expand Up @@ -167,6 +179,7 @@ export class OpfResourceLoaderService {
* The returned Promise is resolved when all resources are loaded.
* The returned Promise is also resolved (not rejected!) immediately when any loading error occurs.
*/

loadResources(
scripts: OpfDynamicScriptResource[] = [],
styles: OpfDynamicScriptResource[] = []
Expand Down
Loading