Skip to content

Commit 2830c67

Browse files
refactor(Device): use internal getters to support SSR (#6421)
1 parent 78bd237 commit 2830c67

File tree

8 files changed

+162
-43
lines changed

8 files changed

+162
-43
lines changed

.editorconfig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ root = true
55
[*]
66
charset = utf-8
77

8-
[*.{css,html,java,js,json,less,txt}]
8+
[*.{css,html,java,js,json,less,txt,ts}]
99
trim_trailing_whitespace = true
1010
end_of_line = lf
1111
tab_width = 4

packages/base/package-scripts.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,11 @@ const scripts = {
5858
styles: 'chokidar "src/css/*.css" -c "nps generateStyles"'
5959
},
6060
start: "nps prepare watch.withBundle",
61-
test: `node "${LIB}/test-runner/test-runner.js"`,
61+
test: {
62+
default: 'concurrently "nps test.wdio"',
63+
ssr: `mocha test/ssr`,
64+
wdio: `node "${LIB}/test-runner/test-runner.js"`
65+
},
6266
};
6367

6468

packages/base/src/Boot.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ const boot = async (): Promise<void> => {
2727
}
2828

2929
const bootExecutor = async (resolve: PromiseResolve) => {
30+
if (typeof document === "undefined") {
31+
resolve();
32+
return;
33+
}
3034
registerCurrentRuntime();
3135

3236
const openUI5Support = getFeature<typeof OpenUI5Support>("OpenUI5Support");

packages/base/src/Device.ts

Lines changed: 113 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,99 +1,178 @@
1-
const ua = navigator.userAgent;
2-
const touch = "ontouchstart" in window || navigator.maxTouchPoints > 0;
3-
const ie = /(msie|trident)/i.test(ua);
4-
const chrome = !ie && /(Chrome|CriOS)/.test(ua);
5-
const firefox = /Firefox/.test(ua);
6-
const safari = !ie && !chrome && /(Version|PhantomJS)\/(\d+\.\d+).*Safari/.test(ua);
7-
const webkit = !ie && /webkit/.test(ua);
8-
const windows = navigator.platform.indexOf("Win") !== -1;
9-
const iOS = !!(navigator.platform.match(/iPhone|iPad|iPod/)) || !!(navigator.userAgent.match(/Mac/) && "ontouchend" in document);
10-
const android = !windows && /Android/.test(ua);
11-
const androidPhone = android && /(?=android)(?=.*mobile)/i.test(ua);
12-
const ipad = /ipad/i.test(ua) || (/Macintosh/i.test(ua) && "ontouchend" in document);
13-
// With iOS 13 the string 'iPad' was removed from the user agent string through a browser setting, which is applied on all sites by default:
14-
// "Request Desktop Website -> All websites" (for more infos see: https://forums.developer.apple.com/thread/119186).
15-
// Therefore the OS is detected as MACINTOSH instead of iOS and the device is a tablet if the Device.support.touch is true.
1+
const isSSR = typeof document === "undefined";
2+
3+
const internals = {
4+
get userAgent() {
5+
if (isSSR) {
6+
return "";
7+
}
8+
return navigator.userAgent;
9+
},
10+
get touch() {
11+
if (isSSR) {
12+
return false;
13+
}
14+
return "ontouchstart" in window || navigator.maxTouchPoints > 0;
15+
},
16+
get ie() {
17+
if (isSSR) {
18+
return false;
19+
}
20+
return /(msie|trident)/i.test(internals.userAgent);
21+
},
22+
get chrome() {
23+
if (isSSR) {
24+
return false;
25+
}
26+
return !internals.ie && /(Chrome|CriOS)/.test(internals.userAgent);
27+
},
28+
get firefox() {
29+
if (isSSR) {
30+
return false;
31+
}
32+
return /Firefox/.test(internals.userAgent);
33+
},
34+
get safari() {
35+
if (isSSR) {
36+
return false;
37+
}
38+
return !internals.ie && !internals.chrome && /(Version|PhantomJS)\/(\d+\.\d+).*Safari/.test(internals.userAgent);
39+
},
40+
get webkit() {
41+
if (isSSR) {
42+
return false;
43+
}
44+
return !internals.ie && /webkit/.test(internals.userAgent);
45+
},
46+
get windows() {
47+
if (isSSR) {
48+
return false;
49+
}
50+
return navigator.platform.indexOf("Win") !== -1;
51+
},
52+
get iOS() {
53+
if (isSSR) {
54+
return false;
55+
}
56+
return !!(navigator.platform.match(/iPhone|iPad|iPod/)) || !!(internals.userAgent.match(/Mac/) && "ontouchend" in document);
57+
},
58+
get android() {
59+
if (isSSR) {
60+
return false;
61+
}
62+
return !internals.windows && /Android/.test(internals.userAgent);
63+
},
64+
get androidPhone() {
65+
if (isSSR) {
66+
return false;
67+
}
68+
return internals.android && /(?=android)(?=.*mobile)/i.test(internals.userAgent);
69+
},
70+
get ipad() {
71+
if (isSSR) {
72+
return false;
73+
}
74+
// With iOS 13 the string 'iPad' was removed from the user agent string through a browser setting, which is applied on all sites by default:
75+
// "Request Desktop Website -> All websites" (for more infos see: https://forums.developer.apple.com/thread/119186).
76+
// Therefore the OS is detected as MACINTOSH instead of iOS and the device is a tablet if the Device.support.touch is true.
77+
return /ipad/i.test(internals.userAgent) || (/Macintosh/i.test(internals.userAgent) && "ontouchend" in document);
78+
},
79+
};
1680

1781
let windowsVersion: number;
1882
let webkitVersion: number;
1983
let tablet: boolean;
2084

2185
const isWindows8OrAbove = () => {
22-
if (!windows) {
86+
if (isSSR) {
87+
return false;
88+
}
89+
90+
if (!internals.windows) {
2391
return false;
2492
}
2593

2694
if (windowsVersion === undefined) {
27-
const matches = ua.match(/Windows NT (\d+).(\d)/);
95+
const matches = internals.userAgent.match(/Windows NT (\d+).(\d)/);
2896
windowsVersion = matches ? parseFloat(matches[1]) : 0;
2997
}
3098

3199
return windowsVersion >= 8;
32100
};
33101

34102
const isWebkit537OrAbove = () => {
35-
if (!webkit) {
103+
if (isSSR) {
104+
return false;
105+
}
106+
107+
if (!internals.webkit) {
36108
return false;
37109
}
38110

39111
if (webkitVersion === undefined) {
40-
const matches = ua.match(/(webkit)[ /]([\w.]+)/);
112+
const matches = internals.userAgent.match(/(webkit)[ /]([\w.]+)/);
41113
webkitVersion = matches ? parseFloat(matches[1]) : 0;
42114
}
43115

44116
return webkitVersion >= 537.10;
45117
};
46118

47119
const detectTablet = () => {
120+
if (isSSR) {
121+
return false;
122+
}
123+
48124
if (tablet !== undefined) {
49125
return;
50126
}
51127

52-
if (ipad) {
128+
if (internals.ipad) {
53129
tablet = true;
54130
return;
55131
}
56132

57-
if (touch) {
133+
if (internals.touch) {
58134
if (isWindows8OrAbove()) {
59135
tablet = true;
60136
return;
61137
}
62138

63-
if (chrome && android) {
64-
tablet = !/Mobile Safari\/[.0-9]+/.test(ua);
139+
if (internals.chrome && internals.android) {
140+
tablet = !/Mobile Safari\/[.0-9]+/.test(internals.userAgent);
65141
return;
66142
}
67143

68144
let densityFactor = window.devicePixelRatio ? window.devicePixelRatio : 1; // may be undefined in Windows Phone devices
69-
if (android && isWebkit537OrAbove()) {
145+
if (internals.android && isWebkit537OrAbove()) {
70146
densityFactor = 1;
71147
}
72148

73149
tablet = (Math.min(window.screen.width / densityFactor, window.screen.height / densityFactor) >= 600);
74150
return;
75151
}
76152

77-
tablet = (ie && ua.indexOf("Touch") !== -1) || (android && !androidPhone);
153+
tablet = (internals.ie && internals.userAgent.indexOf("Touch") !== -1) || (internals.android && !internals.androidPhone);
78154
};
79155

80-
const supportsTouch = (): boolean => touch;
81-
const isIE = (): boolean => ie;
82-
const isSafari = (): boolean => safari;
83-
const isChrome = (): boolean => chrome;
84-
const isFirefox = (): boolean => firefox;
156+
const supportsTouch = (): boolean => internals.touch;
157+
const isIE = (): boolean => internals.ie;
158+
const isSafari = (): boolean => internals.safari;
159+
const isChrome = (): boolean => internals.chrome;
160+
const isFirefox = (): boolean => internals.firefox;
85161

86162
const isTablet = (): boolean => {
87163
detectTablet();
88-
return (touch || isWindows8OrAbove()) && tablet;
164+
return (internals.touch || isWindows8OrAbove()) && tablet;
89165
};
90166

91167
const isPhone = (): boolean => {
92168
detectTablet();
93-
return touch && !tablet;
169+
return internals.touch && !tablet;
94170
};
95171

96172
const isDesktop = (): boolean => {
173+
if (isSSR) {
174+
return false;
175+
}
97176
return (!isTablet() && !isPhone()) || isWindows8OrAbove();
98177
};
99178

@@ -102,11 +181,11 @@ const isCombi = (): boolean => {
102181
};
103182

104183
const isIOS = (): boolean => {
105-
return iOS;
184+
return internals.iOS;
106185
};
107186

108187
const isAndroid = (): boolean => {
109-
return android || androidPhone;
188+
return internals.android || internals.androidPhone;
110189
};
111190

112191
export {

packages/base/src/InitialConfiguration.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ const applyOpenUI5Configuration = () => {
178178
};
179179

180180
const initConfiguration = () => {
181-
if (initialized) {
181+
if (typeof document === "undefined" || initialized) {
182182
return;
183183
}
184184

packages/base/src/getSharedResource.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import getSingletonElementInstance from "./util/getSingletonElementInstance.js";
22

3-
const getSharedResourcesInstance = () => getSingletonElementInstance("ui5-shared-resources", document.head);
3+
const getSharedResourcesInstance = (): Record<string, unknown> | null => {
4+
if (typeof document === "undefined") {
5+
return null;
6+
}
7+
return getSingletonElementInstance("ui5-shared-resources", document.head) as unknown as Record<string, unknown>;
8+
};
49

510
/**
611
* Use this method to initialize/get resources that you would like to be shared among UI5 Web Components runtime instances.
@@ -15,6 +20,10 @@ const getSharedResource = <T>(namespace: string, initialValue: T): T => {
1520
const parts = namespace.split(".");
1621
let current = getSharedResourcesInstance() as Record<string, any>;
1722

23+
if (!current) {
24+
return initialValue;
25+
}
26+
1827
for (let i = 0; i < parts.length; i++) {
1928
const part = parts[i];
2029
const lastPart = i === parts.length - 1;

packages/base/src/theming/CustomStyle.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,22 @@ import EventProvider from "../EventProvider.js";
44

55
type CustomCSSChangeCallback = (tag: string) => void;
66

7-
const eventProvider = getSharedResource("CustomStyle.eventProvider", new EventProvider<string, void>());
7+
const getEventProvider = () => getSharedResource("CustomStyle.eventProvider", new EventProvider<string, void>());
88
const CUSTOM_CSS_CHANGE = "CustomCSSChange";
99

1010
const attachCustomCSSChange = (listener: CustomCSSChangeCallback) => {
11-
eventProvider.attachEvent(CUSTOM_CSS_CHANGE, listener);
11+
getEventProvider().attachEvent(CUSTOM_CSS_CHANGE, listener);
1212
};
1313

1414
const detachCustomCSSChange = (listener: CustomCSSChangeCallback) => {
15-
eventProvider.detachEvent(CUSTOM_CSS_CHANGE, listener);
15+
getEventProvider().detachEvent(CUSTOM_CSS_CHANGE, listener);
1616
};
1717

1818
const fireCustomCSSChange = (tag: string) => {
19-
return eventProvider.fireEvent(CUSTOM_CSS_CHANGE, tag);
19+
return getEventProvider().fireEvent(CUSTOM_CSS_CHANGE, tag);
2020
};
2121

22-
const customCSSFor = getSharedResource<Record<string, Array<string>>>("CustomStyle.customCSSFor", {});
22+
const getCustomCSSFor = () => getSharedResource<Record<string, Array<string>>>("CustomStyle.customCSSFor", {});
2323

2424
// Listen to the eventProvider, in case other copies of this CustomStyle module fire this
2525
// event, and this copy would therefore need to reRender the ui5 webcomponents; but
@@ -32,6 +32,7 @@ attachCustomCSSChange((tag: string) => {
3232
});
3333

3434
const addCustomCSS = (tag: string, css: string) => {
35+
const customCSSFor = getCustomCSSFor();
3536
if (!customCSSFor[tag]) {
3637
customCSSFor[tag] = [];
3738
}
@@ -51,6 +52,7 @@ const addCustomCSS = (tag: string, css: string) => {
5152
};
5253

5354
const getCustomCSS = (tag: string) => {
55+
const customCSSFor = getCustomCSSFor();
5456
return customCSSFor[tag] ? customCSSFor[tag].join("") : "";
5557
};
5658

packages/base/test/ssr/Device.mjs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import {assert} from "chai";
2+
import * as Device from "../../dist/Device.js";
3+
4+
describe('SSR / Device', () => {
5+
6+
it('all detections should return false', () => {
7+
assert.strictEqual(Device.supportsTouch(), false, `'supportsTouch' should be false`);
8+
assert.strictEqual(Device.isIE(), false, `'isIE' should be false`);
9+
assert.strictEqual(Device.isSafari(), false, `'isSafari' should be false`);
10+
assert.strictEqual(Device.isChrome(), false, `'isChrome' should be false`);
11+
assert.strictEqual(Device.isFirefox(), false, `'isFirefox' should be false`);
12+
assert.strictEqual(Device.isPhone(), false, `'isPhone' should be false`);
13+
assert.strictEqual(Device.isTablet(), false, `'isTablet' should be false`);
14+
assert.strictEqual(Device.isDesktop(), false, `'isDesktop' should be false`);
15+
assert.strictEqual(Device.isCombi(), false, `'isCombi' should be false`);
16+
assert.strictEqual(Device.isIOS(), false, `'isIOS' should be false`);
17+
assert.strictEqual(Device.isAndroid(), false, `'isAndroid' should be false`);
18+
})
19+
})
20+
21+

0 commit comments

Comments
 (0)