mirror of https://github.com/abpframework/abp.git
committed by
GitHub
22 changed files with 1041 additions and 13 deletions
@ -0,0 +1,56 @@ |
|||
# ContentSecurityStrategy |
|||
|
|||
`ContentSecurityStrategy` is an abstract class exposed by @abp/ng.core package. It helps you mark inline scripts or styles as safe in terms of [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy). |
|||
|
|||
|
|||
|
|||
|
|||
## API |
|||
|
|||
|
|||
### constructor(public nonce?: string) |
|||
|
|||
`nonce` enables whitelisting inline script or styles in order to avoid using `unsafe-inline` in [script-src](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src#Unsafe_inline_script) and [style-src](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/style-src#Unsafe_inline_styles) directives. |
|||
|
|||
|
|||
### applyCSP(element: HTMLScriptElement | HTMLStyleElement): void |
|||
|
|||
This method maps the aforementioned properties to the given `element`. |
|||
|
|||
|
|||
|
|||
|
|||
## LooseContentSecurityPolicy |
|||
|
|||
`LooseContentSecurityPolicy` is a class that extends `ContentSecurityStrategy`. It required `nonce` and marks given `<script>` or `<style>` tag with it. |
|||
|
|||
|
|||
|
|||
|
|||
## StrictContentSecurityPolicy |
|||
|
|||
`StrictContentSecurityPolicy` is a class that extends `ContentSecurityStrategy`. It does not mark inline scripts and styles as safe. You can consider it as a noop alternative. |
|||
|
|||
|
|||
|
|||
|
|||
## Predefined Content Security Strategies |
|||
|
|||
Predefined content security strategies are accessible via `CONTENT_SECURITY_STRATEGY` constant. |
|||
|
|||
|
|||
### Loose(nonce: string) |
|||
|
|||
`nonce` will be set. |
|||
|
|||
|
|||
### Strict() |
|||
|
|||
Nothing will be done. |
|||
|
|||
|
|||
|
|||
|
|||
## What's Next? |
|||
|
|||
TODO: Place new InsertionStrategy link here. |
|||
@ -0,0 +1,41 @@ |
|||
# CrossOriginStrategy |
|||
|
|||
`CrossOriginStrategy` is a class exposed by @abp/ng.core package. Its instances define how a source referenced by an element will be retrieved by the browser and are consumed by other classes such as `LoadingStrategy`. |
|||
|
|||
|
|||
## API |
|||
|
|||
|
|||
### constructor(public crossorigin: 'anonymous' | 'use-credentials', public integrity?: string) |
|||
|
|||
`crossorigin` is mapped to [the HTML attribute with the same name](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/crossorigin). |
|||
`integrity` is a hash for validating a remote resource. Its use is explained [here](https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity). |
|||
|
|||
|
|||
### setCrossOrigin(element: HTMLElement): void |
|||
|
|||
This method maps the aforementioned properties to the given `element`. |
|||
|
|||
|
|||
|
|||
|
|||
## Predefined Cross-Origin Strategies |
|||
|
|||
Predefined cross-origin strategies are accessible via `CROSS_ORIGIN_STRATEGY` constant. |
|||
|
|||
|
|||
### Anonymous(integrity?: string) |
|||
|
|||
`crossorigin` will be set as `"anonymous"` and `integrity` is optional. |
|||
|
|||
|
|||
### UseCredentials(integrity?: string) |
|||
|
|||
`crossorigin` will be set as `"use-credentials"` and `integrity` is optional. |
|||
|
|||
|
|||
|
|||
|
|||
## What's Next? |
|||
|
|||
- [LoadingStrategy](./Loading-Strategy.md) |
|||
@ -0,0 +1,56 @@ |
|||
# DomStrategy |
|||
|
|||
`DomStrategy` is a class exposed by @abp/ng.core package. Its instances define how an element will be attached to the DOM and are consumed by other classes such as `LoadingStrategy`. |
|||
|
|||
|
|||
## API |
|||
|
|||
|
|||
### constructor(public target?: HTMLElement, public position?: InsertPosition) |
|||
|
|||
`target` is an HTMLElement (_default: document.head_). |
|||
`position` defines where the created element will be placed. All possible values of `position` can be found [here](https://developer.mozilla.org/en-US/docs/Web/API/Element/insertAdjacentElement) (_default: 'beforeend'_). |
|||
|
|||
|
|||
### insertElement(element: HTMLElement): void |
|||
|
|||
This method inserts given `element` to `target` based on the `position`. |
|||
|
|||
|
|||
|
|||
|
|||
## Predefined Dom Strategies |
|||
|
|||
Predefined dom strategies are accessible via `DOM_STRATEGY` constant. |
|||
|
|||
|
|||
### AppendToBody() |
|||
|
|||
`insertElement` will place the given `element` at the end of `<body>`. |
|||
|
|||
|
|||
### AppendToHead() |
|||
|
|||
`insertElement` will place the given `element` at the end of `<head>`. |
|||
|
|||
|
|||
### PrependToHead() |
|||
|
|||
`insertElement` will place the given `element` at the beginning of `<head>`. |
|||
|
|||
|
|||
### AfterElement(target: HTMLElement) |
|||
|
|||
`insertElement` will place the given `element` after (as a sibling to) the `target`. |
|||
|
|||
|
|||
### BeforeElement(target: HTMLElement) |
|||
|
|||
`insertElement` will place the given `element` before (as a sibling to) the `target`. |
|||
|
|||
|
|||
|
|||
|
|||
## What's Next? |
|||
|
|||
- [LoadingStrategy](./Loading-Strategy.md) |
|||
@ -0,0 +1,136 @@ |
|||
# How to Lazy Load Scripts and Styles |
|||
|
|||
You can use the `LazyLoadService` in @abp/ng.core package in order to lazy loading scripts and styles in an easy and explicit way. |
|||
|
|||
|
|||
|
|||
|
|||
## Getting Started |
|||
|
|||
You do not have to provide the `LazyLoadService` at module or component level, because it is already **provided in root**. You can inject and start using it immediately in your components, directives, or services. |
|||
|
|||
```js |
|||
import { LazyLoadService } from '@abp/ng.core'; |
|||
|
|||
@Component({ |
|||
/* class metadata here */ |
|||
}) |
|||
class DemoComponent { |
|||
constructor(private lazyLoadService: LazyLoadService) {} |
|||
} |
|||
``` |
|||
|
|||
|
|||
|
|||
|
|||
## Usage |
|||
|
|||
You can use the `load` method of `LazyLoadService` to create a `<script>` or `<style>` element in the DOM at the desired position and force the browser to download the target resource. |
|||
|
|||
|
|||
|
|||
### How to Load Scripts |
|||
|
|||
The first parameter of `load` method expects a `LoadingStrategy`. If you pass a `ScriptLoadingStrategy` instance, the `LazyLoadService` will create a `<script>` element with given `src` and place it in the designated DOM position. |
|||
|
|||
```js |
|||
import { LazyLoadService, LOADING_STRATEGY } from '@abp/ng.core'; |
|||
|
|||
@Component({ |
|||
template: ` |
|||
<some-component *ngIf="libraryLoaded$ | async"></some-component> |
|||
` |
|||
}) |
|||
class DemoComponent { |
|||
libraryLoaded$ = this.lazyLoad.load( |
|||
LOADING_STRATEGY.AppendAnonymousScriptToHead('/assets/some-library.js'), |
|||
); |
|||
|
|||
constructor(private lazyLoadService: LazyLoadService) {} |
|||
} |
|||
``` |
|||
|
|||
The `load` method returns an observable to which you can subscibe in your component or with an `async` pipe. In the example above, the `NgIf` directive will render `<some-component>` only **if the script gets successfully loaded or is already loaded before**. |
|||
|
|||
> You can subscribe multiple times in your template with `async` pipe. The styles will only be loaded once. |
|||
|
|||
Please refer to [LoadingStrategy](./Loading-Strategy.md) to see all available loading strategies and how you can build your own loading strategy. |
|||
|
|||
|
|||
|
|||
### How to Load Styles |
|||
|
|||
If you pass a `StyleLoadingStrategy` instance as the first parameter of `load` method, the `LazyLoadService` will create a `<link>` element with given `href` and place it in the designated DOM position. |
|||
|
|||
```js |
|||
import { LazyLoadService, LOADING_STRATEGY } from '@abp/ng.core'; |
|||
|
|||
@Component({ |
|||
template: ` |
|||
<some-component *ngIf="stylesLoaded$ | async"></some-component> |
|||
` |
|||
}) |
|||
class DemoComponent { |
|||
stylesLoaded$ = this.lazyLoad.load( |
|||
LOADING_STRATEGY.AppendAnonymousStyleToHead('/assets/some-styles.css'), |
|||
); |
|||
|
|||
constructor(private lazyLoadService: LazyLoadService) {} |
|||
} |
|||
``` |
|||
|
|||
The `load` method returns an observable to which you can subscibe in your component or with an `AsyncPipe`. In the example above, the `NgIf` directive will render `<some-component>` only **if the style gets successfully loaded or is already loaded before**. |
|||
|
|||
> You can subscribe multiple times in your template with `async` pipe. The styles will only be loaded once. |
|||
|
|||
Please refer to [LoadingStrategy](./Loading-Strategy.md) to see all available loading strategies and how you can build your own loading strategy. |
|||
|
|||
|
|||
|
|||
### Advanced Usage |
|||
|
|||
You have quite a bit of freedom to define how your lazy load will be implemented. Here is an example: |
|||
|
|||
```js |
|||
const domStrategy = DOM_STRATEGY.PrependToHead(); |
|||
|
|||
const crossOriginStrategy = CROSS_ORIGIN_STRATEGY.Anonymous( |
|||
'sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh', |
|||
); |
|||
|
|||
const loadingStrategy = new StyleLoadingStrategy( |
|||
'https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css', |
|||
domStrategy, |
|||
crossOriginStrategy, |
|||
); |
|||
|
|||
this.lazyLoad.load(loadingStrategy, 1, 2000); |
|||
``` |
|||
|
|||
This code will create a `<link>` element with given url and integrity hash, insert it to to top of the `<head>` element, and retry once after 2 seconds if first try fails. |
|||
|
|||
|
|||
|
|||
|
|||
## API |
|||
|
|||
|
|||
|
|||
### loaded: Set<string> |
|||
|
|||
All previously loaded paths are available via this property. It is a simple [JavaScript Set](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set). |
|||
|
|||
|
|||
|
|||
### load(strategy: LoadingStrategy, retryTimes?: number, retryDelay?: number): Observable<Event> |
|||
|
|||
`strategy` parameter is the primary focus here and is explained above. |
|||
`retryTimes` defines how many times the loading will be tried again before fail (_default: 2_). |
|||
`retryDelay` defines how much delay there will be between retries (_default: 1000_). |
|||
|
|||
|
|||
|
|||
|
|||
## What's Next? |
|||
|
|||
- [TrackByService](./Track-By-Service.md) |
|||
@ -0,0 +1,75 @@ |
|||
# LoadingStrategy |
|||
|
|||
`LoadingStrategy` is an abstract class exposed by @abp/ng.core package. Its instances help you mark inline script or styles as safe in terms of [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy). |
|||
|
|||
|
|||
|
|||
|
|||
## API |
|||
|
|||
|
|||
### constructor(public path: string, protected domStrategy?: DomStrategy, protected crossOriginStrategy?: CrossOriginStrategy) |
|||
|
|||
`path` is set to `<script>` elements as `src` and `<link>` elements as `href` attribute. |
|||
`domStrategy` is the `DomStrategy` that will be used when inserting the created element. (_default: AppendToHead_) |
|||
`crossOriginStrategy` is the `CrossOriginStrategy` that will be used on the created element before inserting it. (_default: Anonymous_) |
|||
|
|||
|
|||
### createElement(): HTMLScriptElement | HTMLLinkElement |
|||
|
|||
This method creates and returns a `<script>` or `<link>` element with `path` set as `src` or `href`. |
|||
|
|||
|
|||
### createStream(): Observable<Event> |
|||
|
|||
This method creates and returns an observable stream that emits on success and throws on error. |
|||
|
|||
|
|||
|
|||
## ScriptLoadingStrategy |
|||
|
|||
`ScriptLoadingStrategy` is a class that extends `LoadingStrategy`. It lets you lazy load a script. |
|||
|
|||
|
|||
|
|||
## StyleLoadingStrategy |
|||
|
|||
`StyleLoadingStrategy` is a class that extends `LoadingStrategy`. It lets you lazy load a style. |
|||
|
|||
|
|||
|
|||
## Predefined Loading Strategies |
|||
|
|||
Predefined content security strategies are accessible via `LOADING_STRATEGY` constant. |
|||
|
|||
|
|||
### AppendAnonymousScriptToHead(src: string, integrity?: string) |
|||
|
|||
Sets given paremeters and `crossorigin="anonymous"` as attributes of created `<script>` element and places it at the **end** of `<head>` tag in the document. |
|||
|
|||
|
|||
### PrependAnonymousScriptToHead(src: string, integrity?: string) |
|||
|
|||
Sets given paremeters and `crossorigin="anonymous"` as attributes of created `<script>` element and places it at the **beginning** of `<head>` tag in the document. |
|||
|
|||
|
|||
### AppendAnonymousScriptToBody(src: string, integrity?: string) |
|||
|
|||
Sets given paremeters and `crossorigin="anonymous"` as attributes of created `<script>` element and places it at the **end** of `<body>` tag in the document. |
|||
|
|||
|
|||
### AppendAnonymousStyleToHead(href: string, integrity?: string) |
|||
|
|||
Sets given paremeters and `crossorigin="anonymous"` as attributes of created `<style>` element and places it at the **end** of `<head>` tag in the document. |
|||
|
|||
|
|||
### PrependAnonymousStyleToHead(href: string, integrity?: string) |
|||
|
|||
Sets given paremeters and `crossorigin="anonymous"` as attributes of created `<style>` element and places it at the **beginning** of `<head>` tag in the document. |
|||
|
|||
|
|||
|
|||
|
|||
## What's Next? |
|||
|
|||
- [LazyLoadService](./Lazy-Load-Service.md) |
|||
@ -0,0 +1,32 @@ |
|||
export abstract class ContentSecurityStrategy { |
|||
constructor(public nonce?: string) {} |
|||
|
|||
abstract applyCSP(element: HTMLScriptElement | HTMLStyleElement): void; |
|||
} |
|||
|
|||
export class LooseContentSecurityStrategy extends ContentSecurityStrategy { |
|||
constructor(nonce: string) { |
|||
super(nonce); |
|||
} |
|||
|
|||
applyCSP(element: HTMLScriptElement | HTMLStyleElement) { |
|||
element.setAttribute('nonce', this.nonce); |
|||
} |
|||
} |
|||
|
|||
export class StrictContentSecurityStrategy extends ContentSecurityStrategy { |
|||
constructor() { |
|||
super(); |
|||
} |
|||
|
|||
applyCSP(_: HTMLScriptElement | HTMLStyleElement) {} |
|||
} |
|||
|
|||
export const CONTENT_SECURITY_STRATEGY = { |
|||
Loose(nonce: string) { |
|||
return new LooseContentSecurityStrategy(nonce); |
|||
}, |
|||
Strict() { |
|||
return new StrictContentSecurityStrategy(); |
|||
}, |
|||
}; |
|||
@ -0,0 +1,17 @@ |
|||
export class CrossOriginStrategy { |
|||
constructor(public crossorigin: 'anonymous' | 'use-credentials', public integrity?: string) {} |
|||
|
|||
setCrossOrigin<T extends HTMLElement>(element: T) { |
|||
if (this.integrity) element.setAttribute('integrity', this.integrity); |
|||
element.setAttribute('crossorigin', this.crossorigin); |
|||
} |
|||
} |
|||
|
|||
export const CROSS_ORIGIN_STRATEGY = { |
|||
Anonymous(integrity?: string) { |
|||
return new CrossOriginStrategy('anonymous', integrity); |
|||
}, |
|||
UseCredentials(integrity?: string) { |
|||
return new CrossOriginStrategy('use-credentials', integrity); |
|||
}, |
|||
}; |
|||
@ -0,0 +1,28 @@ |
|||
export class DomStrategy { |
|||
constructor( |
|||
public target: HTMLElement = document.head, |
|||
public position: InsertPosition = 'beforeend', |
|||
) {} |
|||
|
|||
insertElement<T extends HTMLElement>(element: T) { |
|||
this.target.insertAdjacentElement(this.position, element); |
|||
} |
|||
} |
|||
|
|||
export const DOM_STRATEGY = { |
|||
AfterElement(element: HTMLElement) { |
|||
return new DomStrategy(element, 'afterend'); |
|||
}, |
|||
AppendToBody() { |
|||
return new DomStrategy(document.body, 'beforeend'); |
|||
}, |
|||
AppendToHead() { |
|||
return new DomStrategy(document.head, 'beforeend'); |
|||
}, |
|||
BeforeElement(element: HTMLElement) { |
|||
return new DomStrategy(element, 'beforebegin'); |
|||
}, |
|||
PrependToHead() { |
|||
return new DomStrategy(document.head, 'afterbegin'); |
|||
}, |
|||
}; |
|||
@ -0,0 +1,4 @@ |
|||
export * from './content-security.strategy'; |
|||
export * from './cross-origin.strategy'; |
|||
export * from './dom.strategy'; |
|||
export * from './loading.strategy'; |
|||
@ -0,0 +1,88 @@ |
|||
import { Observable, of } from 'rxjs'; |
|||
import { switchMap } from 'rxjs/operators'; |
|||
import { fromLazyLoad } from '../utils'; |
|||
import { CrossOriginStrategy, CROSS_ORIGIN_STRATEGY } from './cross-origin.strategy'; |
|||
import { DomStrategy, DOM_STRATEGY } from './dom.strategy'; |
|||
|
|||
export abstract class LoadingStrategy<T extends HTMLScriptElement | HTMLLinkElement = any> { |
|||
constructor( |
|||
public path: string, |
|||
protected domStrategy: DomStrategy = DOM_STRATEGY.AppendToHead(), |
|||
protected crossOriginStrategy: CrossOriginStrategy = CROSS_ORIGIN_STRATEGY.Anonymous(), |
|||
) {} |
|||
|
|||
abstract createElement(): T; |
|||
|
|||
createStream<E extends Event>(): Observable<E> { |
|||
return of(null).pipe( |
|||
switchMap(() => |
|||
fromLazyLoad<E>(this.createElement(), this.domStrategy, this.crossOriginStrategy), |
|||
), |
|||
); |
|||
} |
|||
} |
|||
|
|||
export class ScriptLoadingStrategy extends LoadingStrategy<HTMLScriptElement> { |
|||
constructor(src: string, domStrategy?: DomStrategy, crossOriginStrategy?: CrossOriginStrategy) { |
|||
super(src, domStrategy, crossOriginStrategy); |
|||
} |
|||
|
|||
createElement(): HTMLScriptElement { |
|||
const element = document.createElement('script'); |
|||
element.src = this.path; |
|||
|
|||
return element; |
|||
} |
|||
} |
|||
|
|||
export class StyleLoadingStrategy extends LoadingStrategy<HTMLLinkElement> { |
|||
constructor(href: string, domStrategy?: DomStrategy, crossOriginStrategy?: CrossOriginStrategy) { |
|||
super(href, domStrategy, crossOriginStrategy); |
|||
} |
|||
|
|||
createElement(): HTMLLinkElement { |
|||
const element = document.createElement('link'); |
|||
element.rel = 'stylesheet'; |
|||
element.href = this.path; |
|||
|
|||
return element; |
|||
} |
|||
} |
|||
|
|||
export const LOADING_STRATEGY = { |
|||
AppendAnonymousScriptToBody(src: string, integrity?: string) { |
|||
return new ScriptLoadingStrategy( |
|||
src, |
|||
DOM_STRATEGY.AppendToBody(), |
|||
CROSS_ORIGIN_STRATEGY.Anonymous(integrity), |
|||
); |
|||
}, |
|||
AppendAnonymousScriptToHead(src: string, integrity?: string) { |
|||
return new ScriptLoadingStrategy( |
|||
src, |
|||
DOM_STRATEGY.AppendToHead(), |
|||
CROSS_ORIGIN_STRATEGY.Anonymous(integrity), |
|||
); |
|||
}, |
|||
AppendAnonymousStyleToHead(src: string, integrity?: string) { |
|||
return new StyleLoadingStrategy( |
|||
src, |
|||
DOM_STRATEGY.AppendToHead(), |
|||
CROSS_ORIGIN_STRATEGY.Anonymous(integrity), |
|||
); |
|||
}, |
|||
PrependAnonymousScriptToHead(src: string, integrity?: string) { |
|||
return new ScriptLoadingStrategy( |
|||
src, |
|||
DOM_STRATEGY.PrependToHead(), |
|||
CROSS_ORIGIN_STRATEGY.Anonymous(integrity), |
|||
); |
|||
}, |
|||
PrependAnonymousStyleToHead(src: string, integrity?: string) { |
|||
return new StyleLoadingStrategy( |
|||
src, |
|||
DOM_STRATEGY.PrependToHead(), |
|||
CROSS_ORIGIN_STRATEGY.Anonymous(integrity), |
|||
); |
|||
}, |
|||
}; |
|||
@ -0,0 +1,41 @@ |
|||
import { |
|||
CONTENT_SECURITY_STRATEGY, |
|||
LooseContentSecurityStrategy, |
|||
StrictContentSecurityStrategy, |
|||
} from '../strategies'; |
|||
import { uuid } from '../utils'; |
|||
|
|||
describe('LooseContentSecurityStrategy', () => { |
|||
describe('#applyCSP', () => { |
|||
it('should set nonce attribute', () => { |
|||
const nonce = uuid(); |
|||
const strategy = new LooseContentSecurityStrategy(nonce); |
|||
const element = document.createElement('link'); |
|||
strategy.applyCSP(element); |
|||
|
|||
expect(element.getAttribute('nonce')).toBe(nonce); |
|||
}); |
|||
}); |
|||
}); |
|||
|
|||
describe('StrictContentSecurityStrategy', () => { |
|||
describe('#applyCSP', () => { |
|||
it('should not set nonce attribute', () => { |
|||
const strategy = new StrictContentSecurityStrategy(); |
|||
const element = document.createElement('link'); |
|||
strategy.applyCSP(element); |
|||
|
|||
expect(element.getAttribute('nonce')).toBeNull(); |
|||
}); |
|||
}); |
|||
}); |
|||
|
|||
describe('CONTENT_SECURITY_STRATEGY', () => { |
|||
test.each` |
|||
name | Strategy | nonce |
|||
${'Loose'} | ${LooseContentSecurityStrategy} | ${uuid()} |
|||
${'Strict'} | ${StrictContentSecurityStrategy} | ${undefined} |
|||
`('should successfully map $name to $Strategy.name', ({ name, Strategy, nonce }) => {
|
|||
expect(CONTENT_SECURITY_STRATEGY[name](nonce)).toEqual(new Strategy(nonce)); |
|||
}); |
|||
}); |
|||
@ -0,0 +1,38 @@ |
|||
import { CrossOriginStrategy, CROSS_ORIGIN_STRATEGY } from '../strategies'; |
|||
import { uuid } from '../utils'; |
|||
|
|||
describe('CrossOriginStrategy', () => { |
|||
describe('#setCrossOrigin', () => { |
|||
it('should set crossorigin attribute', () => { |
|||
const strategy = new CrossOriginStrategy('use-credentials'); |
|||
const element = document.createElement('link'); |
|||
strategy.setCrossOrigin(element); |
|||
|
|||
expect(element.crossOrigin).toBe('use-credentials'); |
|||
}); |
|||
|
|||
it('should set integrity attribute when given', () => { |
|||
const integrity = uuid(); |
|||
const strategy = new CrossOriginStrategy('anonymous', integrity); |
|||
const element = document.createElement('link'); |
|||
strategy.setCrossOrigin(element); |
|||
|
|||
expect(element.crossOrigin).toBe('anonymous'); |
|||
expect(element.getAttribute('integrity')).toBe(integrity); |
|||
}); |
|||
}); |
|||
}); |
|||
|
|||
describe('CROSS_ORIGIN_STRATEGY', () => { |
|||
test.each` |
|||
name | integrity | crossOrigin |
|||
${'Anonymous'} | ${undefined} | ${'anonymous'} |
|||
${'Anonymous'} | ${uuid()} | ${'anonymous'} |
|||
${'UseCredentials'} | ${undefined} | ${'use-credentials'} |
|||
${'UseCredentials'} | ${uuid()} | ${'use-credentials'} |
|||
`('should successfully map $name to CrossOriginStrategy', ({ name, integrity, crossOrigin }) => {
|
|||
expect(CROSS_ORIGIN_STRATEGY[name](integrity)).toEqual( |
|||
new CrossOriginStrategy(crossOrigin, integrity), |
|||
); |
|||
}); |
|||
}); |
|||
@ -0,0 +1,49 @@ |
|||
import { DomStrategy, DOM_STRATEGY } from '../strategies'; |
|||
|
|||
describe('DomStrategy', () => { |
|||
describe('#insertElement', () => { |
|||
it('should append element to head by default', () => { |
|||
const strategy = new DomStrategy(); |
|||
const element = document.createElement('script'); |
|||
strategy.insertElement(element); |
|||
|
|||
expect(document.head.lastChild).toBe(element); |
|||
}); |
|||
|
|||
it('should append element to body when body is given as target', () => { |
|||
const strategy = new DomStrategy(document.body); |
|||
const element = document.createElement('script'); |
|||
strategy.insertElement(element); |
|||
|
|||
expect(document.body.lastChild).toBe(element); |
|||
}); |
|||
|
|||
it('should prepend to head when position is given as "afterbegin"', () => { |
|||
const strategy = new DomStrategy(undefined, 'afterbegin'); |
|||
const element = document.createElement('script'); |
|||
strategy.insertElement(element); |
|||
|
|||
expect(document.head.firstChild).toBe(element); |
|||
}); |
|||
}); |
|||
}); |
|||
|
|||
describe('DOM_STRATEGY', () => { |
|||
const div = document.createElement('DIV'); |
|||
|
|||
beforeEach(() => { |
|||
document.body.innerHTML = ''; |
|||
document.body.appendChild(div); |
|||
}); |
|||
|
|||
test.each` |
|||
name | target | position |
|||
${'AfterElement'} | ${div} | ${'afterend'} |
|||
${'AppendToBody'} | ${document.body} | ${'beforeend'} |
|||
${'AppendToHead'} | ${document.head} | ${'beforeend'} |
|||
${'BeforeElement'} | ${div} | ${'beforebegin'} |
|||
${'PrependToHead'} | ${document.head} | ${'afterbegin'} |
|||
`('should successfully map $name to CrossOriginStrategy', ({ name, target, position }) => {
|
|||
expect(DOM_STRATEGY[name](target)).toEqual(new DomStrategy(target, position)); |
|||
}); |
|||
}); |
|||
@ -0,0 +1,113 @@ |
|||
import { DomStrategy, DOM_STRATEGY } from '../strategies'; |
|||
import { CrossOriginStrategy, CROSS_ORIGIN_STRATEGY } from '../strategies/cross-origin.strategy'; |
|||
import { uuid } from '../utils'; |
|||
import { fromLazyLoad } from '../utils/lazy-load-utils'; |
|||
|
|||
describe('Lazy Load Utils', () => { |
|||
describe('#fromLazyLoad', () => { |
|||
afterEach(() => { |
|||
jest.clearAllMocks(); |
|||
}); |
|||
|
|||
it('should append to head by default', () => { |
|||
const element = document.createElement('link'); |
|||
const spy = jest.spyOn(document.head, 'insertAdjacentElement'); |
|||
|
|||
fromLazyLoad(element); |
|||
expect(spy).toHaveBeenCalledWith('beforeend', element); |
|||
}); |
|||
|
|||
it('should allow setting a dom strategy', () => { |
|||
const element = document.createElement('link'); |
|||
const spy = jest.spyOn(document.head, 'insertAdjacentElement'); |
|||
|
|||
fromLazyLoad(element, DOM_STRATEGY.PrependToHead()); |
|||
expect(spy).toHaveBeenCalledWith('afterbegin', element); |
|||
}); |
|||
|
|||
it('should set crossorigin to "anonymous" by default', () => { |
|||
const element = document.createElement('link'); |
|||
|
|||
fromLazyLoad(element); |
|||
|
|||
expect(element.crossOrigin).toBe('anonymous'); |
|||
}); |
|||
|
|||
it('should not set integrity by default', () => { |
|||
const element = document.createElement('link'); |
|||
|
|||
fromLazyLoad(element); |
|||
|
|||
expect(element.getAttribute('integrity')).toBeNull(); |
|||
}); |
|||
|
|||
it('should allow setting a cross-origin strategy', () => { |
|||
const element = document.createElement('link'); |
|||
|
|||
const integrity = uuid(); |
|||
|
|||
fromLazyLoad(element, undefined, CROSS_ORIGIN_STRATEGY.UseCredentials(integrity)); |
|||
|
|||
expect(element.crossOrigin).toBe('use-credentials'); |
|||
expect(element.getAttribute('integrity')).toBe(integrity); |
|||
}); |
|||
|
|||
it('should emit error event on fail and clear callbacks', done => { |
|||
const error = new CustomEvent('error'); |
|||
const parentNode = { removeChild: jest.fn() }; |
|||
const element = ({ parentNode } as any) as HTMLLinkElement; |
|||
|
|||
fromLazyLoad( |
|||
element, |
|||
{ |
|||
insertElement(el: HTMLLinkElement) { |
|||
expect(el).toBe(element); |
|||
|
|||
setTimeout(() => { |
|||
el.onerror(error); |
|||
}, 0); |
|||
}, |
|||
} as DomStrategy, |
|||
{ |
|||
setCrossOrigin(_: HTMLLinkElement) {}, |
|||
} as CrossOriginStrategy, |
|||
).subscribe({ |
|||
error: value => { |
|||
expect(value).toBe(error); |
|||
expect(parentNode.removeChild).toHaveBeenCalledWith(element); |
|||
expect(element.onerror).toBeNull(); |
|||
done(); |
|||
}, |
|||
}); |
|||
}); |
|||
|
|||
it('should emit load event on success and clear callbacks', done => { |
|||
const success = new CustomEvent('load'); |
|||
const parentNode = { removeChild: jest.fn() }; |
|||
const element = ({ parentNode } as any) as HTMLLinkElement; |
|||
|
|||
fromLazyLoad( |
|||
element, |
|||
{ |
|||
insertElement(el: HTMLLinkElement) { |
|||
expect(el).toBe(element); |
|||
|
|||
setTimeout(() => { |
|||
el.onload(success); |
|||
}, 0); |
|||
}, |
|||
} as DomStrategy, |
|||
{ |
|||
setCrossOrigin(_: HTMLLinkElement) {}, |
|||
} as CrossOriginStrategy, |
|||
).subscribe({ |
|||
next: value => { |
|||
expect(value).toBe(success); |
|||
expect(parentNode.removeChild).not.toHaveBeenCalled(); |
|||
expect(element.onload).toBeNull(); |
|||
done(); |
|||
}, |
|||
}); |
|||
}); |
|||
}); |
|||
}); |
|||
@ -0,0 +1,102 @@ |
|||
import { |
|||
CROSS_ORIGIN_STRATEGY, |
|||
DOM_STRATEGY, |
|||
LOADING_STRATEGY, |
|||
ScriptLoadingStrategy, |
|||
StyleLoadingStrategy, |
|||
} from '../strategies'; |
|||
|
|||
const path = 'http://example.com/'; |
|||
|
|||
describe('ScriptLoadingStrategy', () => { |
|||
describe('#createElement', () => { |
|||
it('should return a script element with src attribute', () => { |
|||
const strategy = new ScriptLoadingStrategy(path); |
|||
const element = strategy.createElement(); |
|||
|
|||
expect(element.tagName).toBe('SCRIPT'); |
|||
expect(element.src).toBe(path); |
|||
}); |
|||
}); |
|||
|
|||
describe('#createStream', () => { |
|||
it('should use given dom and cross-origin strategies', done => { |
|||
const domStrategy = DOM_STRATEGY.PrependToHead(); |
|||
const crossOriginStrategy = CROSS_ORIGIN_STRATEGY.UseCredentials(); |
|||
|
|||
domStrategy.insertElement = jest.fn((el: HTMLScriptElement) => { |
|||
setTimeout(() => { |
|||
el.onload( |
|||
new CustomEvent('success', { |
|||
detail: { |
|||
crossOrigin: el.crossOrigin, |
|||
}, |
|||
}), |
|||
); |
|||
}, 0); |
|||
}) as any; |
|||
|
|||
const strategy = new ScriptLoadingStrategy(path, domStrategy, crossOriginStrategy); |
|||
|
|||
strategy.createStream<CustomEvent>().subscribe(event => { |
|||
expect(event.detail.crossOrigin).toBe('use-credentials'); |
|||
done(); |
|||
}); |
|||
}); |
|||
}); |
|||
}); |
|||
|
|||
describe('StyleLoadingStrategy', () => { |
|||
describe('#createElement', () => { |
|||
it('should return a style element with href and rel attributes', () => { |
|||
const strategy = new StyleLoadingStrategy(path); |
|||
const element = strategy.createElement(); |
|||
|
|||
expect(element.tagName).toBe('LINK'); |
|||
expect(element.href).toBe(path); |
|||
expect(element.rel).toBe('stylesheet'); |
|||
}); |
|||
}); |
|||
|
|||
describe('#createStream', () => { |
|||
it('should use given dom and cross-origin strategies', done => { |
|||
const domStrategy = DOM_STRATEGY.PrependToHead(); |
|||
const crossOriginStrategy = CROSS_ORIGIN_STRATEGY.UseCredentials(); |
|||
|
|||
domStrategy.insertElement = jest.fn((el: HTMLLinkElement) => { |
|||
setTimeout(() => { |
|||
el.onload( |
|||
new CustomEvent('success', { |
|||
detail: { |
|||
crossOrigin: el.crossOrigin, |
|||
}, |
|||
}), |
|||
); |
|||
}, 0); |
|||
}) as any; |
|||
|
|||
const strategy = new StyleLoadingStrategy(path, domStrategy, crossOriginStrategy); |
|||
|
|||
strategy.createStream<CustomEvent>().subscribe(event => { |
|||
expect(event.detail.crossOrigin).toBe('use-credentials'); |
|||
done(); |
|||
}); |
|||
}); |
|||
}); |
|||
}); |
|||
|
|||
describe('LOADING_STRATEGY', () => { |
|||
test.each` |
|||
name | Strategy | domStrategy |
|||
${'AppendAnonymousScriptToBody'} | ${ScriptLoadingStrategy} | ${'AppendToBody'} |
|||
${'AppendAnonymousScriptToHead'} | ${ScriptLoadingStrategy} | ${'AppendToHead'} |
|||
${'AppendAnonymousStyleToHead'} | ${StyleLoadingStrategy} | ${'AppendToHead'} |
|||
${'PrependAnonymousScriptToHead'} | ${ScriptLoadingStrategy} | ${'PrependToHead'} |
|||
${'PrependAnonymousStyleToHead'} | ${StyleLoadingStrategy} | ${'PrependToHead'} |
|||
`(
|
|||
'should successfully map $name to $Strategy.name with $domStrategy dom strategy', |
|||
({ name, Strategy, domStrategy }) => { |
|||
expect(LOADING_STRATEGY[name](path)).toEqual(new Strategy(path, DOM_STRATEGY[domStrategy]())); |
|||
}, |
|||
); |
|||
}); |
|||
@ -1,5 +1,6 @@ |
|||
export * from './common-utils'; |
|||
export * from './generator-utils'; |
|||
export * from './initial-utils'; |
|||
export * from './lazy-load-utils'; |
|||
export * from './route-utils'; |
|||
export * from './rxjs-utils'; |
|||
|
|||
@ -0,0 +1,51 @@ |
|||
import { Observable, Observer } from 'rxjs'; |
|||
import { CrossOriginStrategy, CROSS_ORIGIN_STRATEGY } from '../strategies/cross-origin.strategy'; |
|||
import { DomStrategy, DOM_STRATEGY } from '../strategies/dom.strategy'; |
|||
|
|||
export function fromLazyLoad<T extends Event>( |
|||
element: HTMLScriptElement | HTMLLinkElement, |
|||
domStrategy: DomStrategy = DOM_STRATEGY.AppendToHead(), |
|||
crossOriginStrategy: CrossOriginStrategy = CROSS_ORIGIN_STRATEGY.Anonymous(), |
|||
): Observable<T> { |
|||
crossOriginStrategy.setCrossOrigin(element); |
|||
domStrategy.insertElement(element); |
|||
|
|||
return new Observable((observer: Observer<T>) => { |
|||
element.onload = (event: T) => { |
|||
clearCallbacks(element); |
|||
observer.next(event); |
|||
observer.complete(); |
|||
}; |
|||
|
|||
const handleError = createErrorHandler(observer, element); |
|||
|
|||
element.onerror = handleError; |
|||
element.onabort = handleError; |
|||
element.onemptied = handleError; |
|||
element.onstalled = handleError; |
|||
element.onsuspend = handleError; |
|||
|
|||
return () => { |
|||
clearCallbacks(element); |
|||
observer.complete(); |
|||
}; |
|||
}); |
|||
} |
|||
|
|||
function createErrorHandler(observer: Observer<Event>, element: HTMLElement) { |
|||
/* tslint:disable-next-line:only-arrow-functions */ |
|||
return function(event: Event | string) { |
|||
clearCallbacks(element); |
|||
element.parentNode.removeChild(element); |
|||
observer.error(event); |
|||
}; |
|||
} |
|||
|
|||
function clearCallbacks(element: HTMLElement) { |
|||
element.onload = null; |
|||
element.onerror = null; |
|||
element.onabort = null; |
|||
element.onemptied = null; |
|||
element.onstalled = null; |
|||
element.onsuspend = null; |
|||
} |
|||
Loading…
Reference in new issue