Skip to content

Commit 095ca7d

Browse files
fix: universal services stubbed
1 parent 30282cb commit 095ca7d

File tree

11 files changed

+351
-1
lines changed

11 files changed

+351
-1
lines changed

fuse.ts

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { resolve } from 'path'
44
import { argv } from 'yargs'
55
import shabang from './tools/scripts/fuse-shebang'
66
import { SparkyFile } from 'fuse-box/sparky/SparkyFile'
7+
import { unlinkSync } from 'fs'
78

89
const appName = 'fng'
910
const outputDir = '.build'
@@ -47,7 +48,7 @@ task('cp.jest', () => {
4748
return src('jest/**', { base: 'src/templates/unit-tests' }).dest('.build/')
4849
})
4950

50-
task('bundle', ['cp.jest', 'ng.svg'], () => {
51+
task('bundle', ['cp.jest', 'ng.util', 'ng.svg'], () => {
5152
bundle.instructions('> [src/index.ts]')
5253
!isProdBuild &&
5354
bundle.watch(`src/**`).completed(fp => shabang(fp.bundle, absOutputPath))
@@ -82,3 +83,37 @@ task('ng.svg', () => {
8283
config.bundle('index').instructions('> [src/modules/svg/test.ts]')
8384
config.run()
8485
})
86+
87+
task('ng.util', () => {
88+
const config = FuseBox.init({
89+
homeDir,
90+
target: 'universal@es5',
91+
output: `${outputDir}/modules/util/$name.js`,
92+
globals: {
93+
default: '*'
94+
},
95+
package: {
96+
name: 'default',
97+
main: outputPath
98+
}
99+
})
100+
src('**/*.ts', { base: 'src/modules/util' })
101+
.dest('.build/modules/util')
102+
.exec()
103+
.then(() => {
104+
return src('.build/modules/util/index.ts')
105+
.file('*', (file: SparkyFile) => file.rename('index.d.ts'))
106+
.dest('.build/modules/util/$name')
107+
.exec()
108+
.then(() => {
109+
unlinkSync('.build/modules/util/index.ts')
110+
})
111+
})
112+
113+
config
114+
.bundle('index')
115+
.instructions(
116+
'> [src/modules/util/tokens.ts] + [src/modules/util/window.service.ts]'
117+
)
118+
config.run()
119+
})

package-lock.json

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
"@types/node-sass": "^3.10.32",
4444
"@types/npm": "^2.0.29",
4545
"@types/sinon": "^5.0.1",
46+
"@types/ua-parser-js": "^0.7.32",
4647
"@types/yargs": "^11.0.0",
4748
"chokidar-cli": "^1.2.0",
4849
"condition-circle": "^2.0.1",
@@ -98,6 +99,7 @@
9899
"ts-node": "^7.0.0",
99100
"tslint": "^5.10.0",
100101
"typescript": "2.7.2",
102+
"ua-parser-js": "^0.7.18",
101103
"uglify-js": "^3.4.3",
102104
"yargs": "^12.0.1",
103105
"zone.js": "^0.8.26"
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { Inject, Injectable } from '@angular/core'
2+
import {
3+
HttpHandler,
4+
HttpInterceptor,
5+
HttpRequest,
6+
HttpResponse
7+
} from '@angular/common/http'
8+
import {
9+
CACHE_TAG_CONFIG,
10+
CACHE_TAG_FACTORY,
11+
CacheFactory,
12+
CacheTagConfig
13+
} from './http-cache-tag.server.module'
14+
import { map } from 'rxjs/operators'
15+
16+
// tslint:disable:no-class
17+
// tslint:disable:no-this
18+
// tslint:disable:no-if-statement
19+
@Injectable()
20+
export class HttpCacheTagInterceptor implements HttpInterceptor {
21+
constructor(
22+
@Inject(CACHE_TAG_CONFIG) private config: CacheTagConfig,
23+
@Inject(CACHE_TAG_FACTORY) private factory: CacheFactory
24+
) {
25+
if (!config.headerKey) throw new Error('missing config.headerKey')
26+
if (!config.cacheableResponseCodes)
27+
throw new Error('missing config.cacheableResponseCodes')
28+
}
29+
30+
isCacheableCode(code: number) {
31+
return this.config.cacheableResponseCodes.find(a => a === code)
32+
}
33+
34+
isCacheableUrl(url: string | null) {
35+
if (!this.config.cacheableUrls || !url || url === null) return true
36+
return this.config.cacheableUrls.test(url)
37+
}
38+
39+
intercept(req: HttpRequest<any>, next: HttpHandler) {
40+
return next.handle(req).pipe(
41+
map(event => {
42+
if (
43+
event instanceof HttpResponse &&
44+
this.isCacheableCode(event.status) &&
45+
this.isCacheableUrl(event.url)
46+
) {
47+
this.factory(event, this.config)
48+
}
49+
return event
50+
})
51+
)
52+
}
53+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import {
2+
InjectionToken,
3+
ModuleWithProviders,
4+
NgModule,
5+
Optional,
6+
SkipSelf
7+
} from '@angular/core'
8+
import { HTTP_INTERCEPTORS, HttpResponse } from '@angular/common/http'
9+
import { HttpCacheTagInterceptor } from './http-cache-tag-interceptor.service'
10+
11+
export const CACHE_TAG_CONFIG = new InjectionToken<CacheTagConfig>(
12+
'cfg.http.ct'
13+
)
14+
export const CACHE_TAG_FACTORY = new InjectionToken<CacheFactory>(
15+
'cfg.http.ctf'
16+
)
17+
18+
export interface CacheTagConfig {
19+
readonly headerKey: string
20+
readonly cacheableResponseCodes: ReadonlyArray<number>
21+
readonly cacheableUrls?: RegExp
22+
}
23+
24+
export type CacheFactory = (
25+
httpResponse: HttpResponse<any>,
26+
config: CacheTagConfig
27+
) => void
28+
29+
// tslint:disable-next-line:no-class
30+
@NgModule()
31+
export class HttpCacheTagModule {
32+
static forRoot(
33+
configProvider: any,
34+
factoryProvider: any
35+
): ModuleWithProviders {
36+
return {
37+
ngModule: HttpCacheTagModule,
38+
providers: [
39+
{
40+
provide: HttpCacheTagInterceptor,
41+
useClass: HttpCacheTagInterceptor,
42+
deps: [CACHE_TAG_CONFIG, CACHE_TAG_FACTORY]
43+
},
44+
{
45+
provide: HTTP_INTERCEPTORS,
46+
useExisting: HttpCacheTagInterceptor,
47+
multi: true
48+
},
49+
configProvider,
50+
factoryProvider
51+
]
52+
}
53+
}
54+
55+
constructor(
56+
@Optional()
57+
@SkipSelf()
58+
parentModule: HttpCacheTagModule
59+
) {
60+
// tslint:disable-next-line:no-if-statement
61+
if (parentModule)
62+
throw new Error(
63+
'HttpCachTageModule already loaded. Import in root module only.'
64+
)
65+
}
66+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export { HttpCacheTagInterceptor } from './http-cache-tag-interceptor.service'
2+
export {
3+
HttpCacheTagModule,
4+
CACHE_TAG_CONFIG,
5+
CACHE_TAG_FACTORY,
6+
CacheFactory,
7+
CacheTagConfig
8+
} from './http-cache-tag.server.module'
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { Directive, ElementRef, Renderer2 } from '@angular/core'
2+
3+
// tslint:disable-next-line:no-class
4+
@Directive({
5+
selector: 'a[fngExternalLink]'
6+
})
7+
export class ExternalLinkDirective {
8+
constructor(el: ElementRef, rd: Renderer2) {
9+
const anchor = el.nativeElement as HTMLAnchorElement
10+
rd.setAttribute(anchor, 'target', '_blank')
11+
rd.setAttribute(anchor, 'rel', 'noopener')
12+
}
13+
}

src/modules/util/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export { WINDOW } from './tokens'
2+
3+
export { IWindowService, WindowService } from './window.service'

src/modules/util/tokens.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { InjectionToken } from '@angular/core'
2+
3+
export const WINDOW = new InjectionToken<Window>('fng.window')
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import { REQUEST } from '@nguniversal/express-engine/tokens'
2+
import { Inject, Injectable, PLATFORM_ID } from '@angular/core'
3+
import { UAParser } from 'ua-parser-js'
4+
import { Request } from 'express'
5+
import { isPlatformServer } from '@angular/common'
6+
import { WINDOW } from './tokens'
7+
8+
export interface IUserAgentService {
9+
readonly userAgent: () => IUAParser.IResult
10+
readonly isiPhone: () => boolean
11+
readonly isiPad: () => boolean
12+
readonly isMobile: () => boolean
13+
readonly isTablet: () => boolean
14+
readonly isDesktop: () => boolean
15+
readonly isChrome: () => boolean
16+
readonly isFirefox: () => boolean
17+
readonly isSafari: () => boolean
18+
readonly isIE: () => boolean
19+
readonly isIE7: () => boolean
20+
readonly isIE8: () => boolean
21+
readonly isIE9: () => boolean
22+
readonly isIE10: () => boolean
23+
readonly isIE11: () => boolean
24+
readonly isWindows: () => boolean
25+
readonly isWindowsXP: () => boolean
26+
readonly isWindows7: () => boolean
27+
readonly isWindows8: () => boolean
28+
readonly isMac: () => boolean
29+
readonly isChromeOS: () => boolean
30+
readonly isiOS: () => boolean
31+
readonly isAndroid: () => boolean
32+
}
33+
34+
// tslint:disable:no-class
35+
// tslint:disable:no-this
36+
@Injectable()
37+
export class UserAgentService implements IUserAgentService {
38+
constructor(
39+
@Inject(PLATFORM_ID) private platformId: any,
40+
@Inject(REQUEST) private req: Request,
41+
@Inject(WINDOW) private _window: Window
42+
) {}
43+
44+
public userAgent(): IUAParser.IResult {
45+
const ua = isPlatformServer(this.platformId)
46+
? new UAParser(this.req.headers['user-agent'] as string | undefined)
47+
: new UAParser(this._window.navigator.userAgent)
48+
49+
return ua.getResult()
50+
}
51+
52+
isiPhone(): boolean {
53+
return this.userAgent().device.type === 'iPhone'
54+
}
55+
56+
isiPad(): boolean {
57+
return this.userAgent().device.type === 'iPad'
58+
}
59+
60+
isMobile(): boolean {
61+
return this.userAgent().device.type === 'mobile'
62+
}
63+
64+
isTablet(): boolean {
65+
return this.userAgent().device.type === 'tablet'
66+
}
67+
68+
isDesktop(): boolean {
69+
return !this.isTablet && !this.isMobile
70+
}
71+
72+
isChrome(): boolean {
73+
return this.userAgent().browser.name === 'Chrome'
74+
}
75+
76+
isFirefox(): boolean {
77+
return this.userAgent().browser.name === 'Firefox'
78+
}
79+
80+
isSafari(): boolean {
81+
return this.userAgent().browser.name === 'Safari'
82+
}
83+
84+
isIE(): boolean {
85+
return this.userAgent().browser.name === 'IE'
86+
}
87+
88+
isIE7(): boolean {
89+
return this.isIE && this.userAgent().browser.major === '7'
90+
}
91+
92+
isIE8(): boolean {
93+
return this.isIE && this.userAgent().browser.major === '8'
94+
}
95+
96+
isIE9(): boolean {
97+
return this.isIE && this.userAgent().browser.major === '9'
98+
}
99+
100+
isIE10(): boolean {
101+
return this.isIE && this.userAgent().browser.major === '10'
102+
}
103+
104+
isIE11(): boolean {
105+
return this.isIE && this.userAgent().browser.major === '11'
106+
}
107+
108+
isWindows(): boolean {
109+
return this.userAgent().os.name === 'Windows'
110+
}
111+
112+
isWindowsXP(): boolean {
113+
return this.isWindows && this.userAgent().os.version === 'XP'
114+
}
115+
116+
isWindows7(): boolean {
117+
return this.isWindows && this.userAgent().os.version === '7'
118+
}
119+
120+
isWindows8(): boolean {
121+
return this.isWindows && this.userAgent().os.version === '8'
122+
}
123+
124+
isMac(): boolean {
125+
return this.userAgent().os.name === 'Mac OS X'
126+
}
127+
128+
isChromeOS(): boolean {
129+
return this.userAgent().os.name === 'Chromium OS'
130+
}
131+
132+
isiOS(): boolean {
133+
return this.userAgent().os.name === 'iOS'
134+
}
135+
136+
isAndroid(): boolean {
137+
return this.userAgent().os.name === 'Android'
138+
}
139+
}

0 commit comments

Comments
 (0)