refactor(core): separate lazy load & content insertion

pull/3453/head
Arman Ozak 6 years ago
parent 21095025a9
commit 7da8011eee

@ -1,7 +1,6 @@
import { Observable, of } from 'rxjs'; import { Observable, of } from 'rxjs';
import { switchMap } from 'rxjs/operators'; import { switchMap } from 'rxjs/operators';
import { fromLazyLoad } from '../utils'; import { fromLazyLoad } from '../utils';
import { ContentSecurityStrategy, CONTENT_SECURITY_STRATEGY } from './content-security.strategy';
import { CrossOriginStrategy, CROSS_ORIGIN_STRATEGY } from './cross-origin.strategy'; import { CrossOriginStrategy, CROSS_ORIGIN_STRATEGY } from './cross-origin.strategy';
import { DomStrategy, DOM_STRATEGY } from './dom.strategy'; import { DomStrategy, DOM_STRATEGY } from './dom.strategy';
@ -10,7 +9,6 @@ export abstract class LoadingStrategy<T extends HTMLScriptElement | HTMLLinkElem
public path: string, public path: string,
protected domStrategy: DomStrategy = DOM_STRATEGY.AppendToHead(), protected domStrategy: DomStrategy = DOM_STRATEGY.AppendToHead(),
protected crossOriginStrategy: CrossOriginStrategy = CROSS_ORIGIN_STRATEGY.Anonymous(), protected crossOriginStrategy: CrossOriginStrategy = CROSS_ORIGIN_STRATEGY.Anonymous(),
protected contentSecurityStrategy: ContentSecurityStrategy = CONTENT_SECURITY_STRATEGY.Loose(),
) {} ) {}
abstract createElement(): T; abstract createElement(): T;
@ -18,25 +16,15 @@ export abstract class LoadingStrategy<T extends HTMLScriptElement | HTMLLinkElem
createStream<E extends Event>(): Observable<E> { createStream<E extends Event>(): Observable<E> {
return of(null).pipe( return of(null).pipe(
switchMap(() => switchMap(() =>
fromLazyLoad<E>( fromLazyLoad<E>(this.createElement(), this.domStrategy, this.crossOriginStrategy),
this.createElement(),
this.domStrategy,
this.crossOriginStrategy,
this.contentSecurityStrategy,
),
), ),
); );
} }
} }
export class ScriptLoadingStrategy extends LoadingStrategy<HTMLScriptElement> { export class ScriptLoadingStrategy extends LoadingStrategy<HTMLScriptElement> {
constructor( constructor(src: string, domStrategy?: DomStrategy, crossOriginStrategy?: CrossOriginStrategy) {
src: string, super(src, domStrategy, crossOriginStrategy);
domStrategy?: DomStrategy,
crossOriginStrategy?: CrossOriginStrategy,
contentSecurityStrategy?: ContentSecurityStrategy,
) {
super(src, domStrategy, crossOriginStrategy, contentSecurityStrategy);
} }
createElement(): HTMLScriptElement { createElement(): HTMLScriptElement {
@ -48,13 +36,8 @@ export class ScriptLoadingStrategy extends LoadingStrategy<HTMLScriptElement> {
} }
export class StyleLoadingStrategy extends LoadingStrategy<HTMLLinkElement> { export class StyleLoadingStrategy extends LoadingStrategy<HTMLLinkElement> {
constructor( constructor(href: string, domStrategy?: DomStrategy, crossOriginStrategy?: CrossOriginStrategy) {
href: string, super(href, domStrategy, crossOriginStrategy);
domStrategy?: DomStrategy,
crossOriginStrategy?: CrossOriginStrategy,
contentSecurityStrategy?: ContentSecurityStrategy,
) {
super(href, domStrategy, crossOriginStrategy, contentSecurityStrategy);
} }
createElement(): HTMLLinkElement { createElement(): HTMLLinkElement {

@ -1,9 +1,4 @@
import { import { DomStrategy, DOM_STRATEGY } from '../strategies';
ContentSecurityStrategy,
CONTENT_SECURITY_STRATEGY,
DomStrategy,
DOM_STRATEGY,
} from '../strategies';
import { CrossOriginStrategy, CROSS_ORIGIN_STRATEGY } from '../strategies/cross-origin.strategy'; import { CrossOriginStrategy, CROSS_ORIGIN_STRATEGY } from '../strategies/cross-origin.strategy';
import { uuid } from '../utils'; import { uuid } from '../utils';
import { fromLazyLoad } from '../utils/lazy-load-utils'; import { fromLazyLoad } from '../utils/lazy-load-utils';
@ -57,24 +52,6 @@ describe('Lazy Load Utils', () => {
expect(element.getAttribute('integrity')).toBe(integrity); expect(element.getAttribute('integrity')).toBe(integrity);
}); });
it('should not set nonce by default', () => {
const element = document.createElement('link');
fromLazyLoad(element);
expect(element.getAttribute('nonce')).toBeNull();
});
it('should allow setting a content security strategy', () => {
const element = document.createElement('link');
const nonce = uuid();
fromLazyLoad(element, undefined, undefined, CONTENT_SECURITY_STRATEGY.Strict(nonce));
expect(element.getAttribute('nonce')).toBe(nonce);
});
it('should emit error event on fail and clear callbacks', done => { it('should emit error event on fail and clear callbacks', done => {
const error = new CustomEvent('error'); const error = new CustomEvent('error');
const parentNode = { removeChild: jest.fn() }; const parentNode = { removeChild: jest.fn() };
@ -94,9 +71,6 @@ describe('Lazy Load Utils', () => {
{ {
setCrossOrigin(_: HTMLLinkElement) {}, setCrossOrigin(_: HTMLLinkElement) {},
} as CrossOriginStrategy, } as CrossOriginStrategy,
{
applyCSP(_: HTMLLinkElement) {},
} as ContentSecurityStrategy,
).subscribe({ ).subscribe({
error: value => { error: value => {
expect(value).toBe(error); expect(value).toBe(error);
@ -126,9 +100,6 @@ describe('Lazy Load Utils', () => {
{ {
setCrossOrigin(_: HTMLLinkElement) {}, setCrossOrigin(_: HTMLLinkElement) {},
} as CrossOriginStrategy, } as CrossOriginStrategy,
{
applyCSP(_: HTMLLinkElement) {},
} as ContentSecurityStrategy,
).subscribe({ ).subscribe({
next: value => { next: value => {
expect(value).toBe(success); expect(value).toBe(success);

@ -1,15 +1,12 @@
import { import {
CONTENT_SECURITY_STRATEGY,
CROSS_ORIGIN_STRATEGY, CROSS_ORIGIN_STRATEGY,
DOM_STRATEGY, DOM_STRATEGY,
LOADING_STRATEGY, LOADING_STRATEGY,
ScriptLoadingStrategy, ScriptLoadingStrategy,
StyleLoadingStrategy, StyleLoadingStrategy,
} from '../strategies'; } from '../strategies';
import { uuid } from '../utils';
const path = 'http://example.com/'; const path = 'http://example.com/';
const nonce = uuid();
describe('ScriptLoadingStrategy', () => { describe('ScriptLoadingStrategy', () => {
describe('#createElement', () => { describe('#createElement', () => {
@ -26,7 +23,6 @@ describe('ScriptLoadingStrategy', () => {
it('should use given dom and cross-origin strategies', done => { it('should use given dom and cross-origin strategies', done => {
const domStrategy = DOM_STRATEGY.PrependToHead(); const domStrategy = DOM_STRATEGY.PrependToHead();
const crossOriginStrategy = CROSS_ORIGIN_STRATEGY.UseCredentials(); const crossOriginStrategy = CROSS_ORIGIN_STRATEGY.UseCredentials();
const contentSecurityStrategy = CONTENT_SECURITY_STRATEGY.Strict(nonce);
domStrategy.insertElement = jest.fn((el: HTMLScriptElement) => { domStrategy.insertElement = jest.fn((el: HTMLScriptElement) => {
setTimeout(() => { setTimeout(() => {
@ -34,23 +30,16 @@ describe('ScriptLoadingStrategy', () => {
new CustomEvent('success', { new CustomEvent('success', {
detail: { detail: {
crossOrigin: el.crossOrigin, crossOrigin: el.crossOrigin,
nonce: el.getAttribute('nonce'),
}, },
}), }),
); );
}, 0); }, 0);
}) as any; }) as any;
const strategy = new ScriptLoadingStrategy( const strategy = new ScriptLoadingStrategy(path, domStrategy, crossOriginStrategy);
path,
domStrategy,
crossOriginStrategy,
contentSecurityStrategy,
);
strategy.createStream<CustomEvent>().subscribe(event => { strategy.createStream<CustomEvent>().subscribe(event => {
expect(event.detail.crossOrigin).toBe('use-credentials'); expect(event.detail.crossOrigin).toBe('use-credentials');
expect(event.detail.nonce).toBe(nonce);
done(); done();
}); });
}); });
@ -73,7 +62,6 @@ describe('StyleLoadingStrategy', () => {
it('should use given dom and cross-origin strategies', done => { it('should use given dom and cross-origin strategies', done => {
const domStrategy = DOM_STRATEGY.PrependToHead(); const domStrategy = DOM_STRATEGY.PrependToHead();
const crossOriginStrategy = CROSS_ORIGIN_STRATEGY.UseCredentials(); const crossOriginStrategy = CROSS_ORIGIN_STRATEGY.UseCredentials();
const contentSecurityStrategy = CONTENT_SECURITY_STRATEGY.Strict(nonce);
domStrategy.insertElement = jest.fn((el: HTMLLinkElement) => { domStrategy.insertElement = jest.fn((el: HTMLLinkElement) => {
setTimeout(() => { setTimeout(() => {
@ -81,23 +69,16 @@ describe('StyleLoadingStrategy', () => {
new CustomEvent('success', { new CustomEvent('success', {
detail: { detail: {
crossOrigin: el.crossOrigin, crossOrigin: el.crossOrigin,
nonce: el.getAttribute('nonce'),
}, },
}), }),
); );
}, 0); }, 0);
}) as any; }) as any;
const strategy = new StyleLoadingStrategy( const strategy = new StyleLoadingStrategy(path, domStrategy, crossOriginStrategy);
path,
domStrategy,
crossOriginStrategy,
contentSecurityStrategy,
);
strategy.createStream<CustomEvent>().subscribe(event => { strategy.createStream<CustomEvent>().subscribe(event => {
expect(event.detail.crossOrigin).toBe('use-credentials'); expect(event.detail.crossOrigin).toBe('use-credentials');
expect(event.detail.nonce).toBe(nonce);
done(); done();
}); });
}); });

@ -1,8 +1,4 @@
import { Observable, Observer } from 'rxjs'; import { Observable, Observer } from 'rxjs';
import {
ContentSecurityStrategy,
CONTENT_SECURITY_STRATEGY,
} from '../strategies/content-security.strategy';
import { CrossOriginStrategy, CROSS_ORIGIN_STRATEGY } from '../strategies/cross-origin.strategy'; import { CrossOriginStrategy, CROSS_ORIGIN_STRATEGY } from '../strategies/cross-origin.strategy';
import { DomStrategy, DOM_STRATEGY } from '../strategies/dom.strategy'; import { DomStrategy, DOM_STRATEGY } from '../strategies/dom.strategy';
@ -10,10 +6,8 @@ export function fromLazyLoad<T extends Event>(
element: HTMLScriptElement | HTMLLinkElement, element: HTMLScriptElement | HTMLLinkElement,
domStrategy: DomStrategy = DOM_STRATEGY.AppendToHead(), domStrategy: DomStrategy = DOM_STRATEGY.AppendToHead(),
crossOriginStrategy: CrossOriginStrategy = CROSS_ORIGIN_STRATEGY.Anonymous(), crossOriginStrategy: CrossOriginStrategy = CROSS_ORIGIN_STRATEGY.Anonymous(),
contentSecurityStrategy: ContentSecurityStrategy = CONTENT_SECURITY_STRATEGY.Loose(),
): Observable<T> { ): Observable<T> {
crossOriginStrategy.setCrossOrigin(element); crossOriginStrategy.setCrossOrigin(element);
contentSecurityStrategy.applyCSP(element);
domStrategy.insertElement(element); domStrategy.insertElement(element);
return new Observable((observer: Observer<T>) => { return new Observable((observer: Observer<T>) => {

Loading…
Cancel
Save