Skip to content

Commit bc1e4ea

Browse files
LorenzGitNathanWalker
authored andcommitted
feat: add tvOS core and webpack support
1 parent 04d4a92 commit bc1e4ea

28 files changed

Lines changed: 357 additions & 223 deletions

File tree

packages/core/global-types.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ declare const __COMMONJS__: boolean;
140140
declare const __ANDROID__: boolean;
141141
declare const __IOS__: boolean;
142142
declare const __VISIONOS__: boolean;
143+
declare const __TVOS__: boolean;
143144
declare const __APPLE__: boolean;
144145

145146
declare function setTimeout(callback: (...args: any[]) => void, ms: number, ...args: any[]): number;

packages/core/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export { ModuleNameResolver } from './module-name-resolver';
3737
export { _setResolver } from './module-name-resolver/helpers';
3838
export type { PlatformContext } from './module-name-resolver';
3939
export type { ModuleListProvider } from './module-name-resolver/helpers';
40-
export { isAndroid, isIOS, isVisionOS, isApple, Screen, Device, platformNames } from './platform';
40+
export { isAndroid, isIOS, isVisionOS, isTvOS, isApple, Screen, Device, platformNames } from './platform';
4141
export type { IDevice } from './platform';
4242
export { profile, enable as profilingEnable, disable as profilingDisable, time as profilingTime, uptime as profilingUptime, start as profilingStart, stop as profilingStop, isRunning as profilingIsRunning, dumpProfiles as profilingDumpProfiles, resetProfiles as profilingResetProfiles, startCPUProfile as profilingStartCPU, stopCPUProfile as profilingStopCPU } from './profiling';
4343
export type { InstrumentationMode, TimerInfo } from './profiling';

packages/core/module-name-resolver/qualifier-matcher/index.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,15 +87,24 @@ const minHeightQualifier: QualifierSpec = {
8787

8888
const platformQualifier: QualifierSpec = {
8989
isMatch: function (path: string): boolean {
90-
return path.includes('.android') || path.includes('.ios');
90+
return path.includes('.android') || path.includes('.ios') || path.includes('.tvos');
9191
},
9292
getMatchOccurences: function (path: string): Array<string> {
93-
return path.match(new RegExp('\\.android|\\.ios', 'g'));
93+
return path.match(new RegExp('\\.android|\\.ios|\\.tvos', 'g'));
9494
},
9595
getMatchValue(value: string, context: PlatformContext): number {
9696
const val = value.substring(1);
97+
const os = context.os.toLowerCase();
98+
if (val === os) {
99+
// exact platform: .tvos on tvOS, .ios on iOS, .android on Android
100+
return 2;
101+
}
102+
if (val === 'ios' && os === 'tvos') {
103+
// tvOS falls back to .ios files, below an explicit .tvos candidate
104+
return 1;
105+
}
97106

98-
return val === context.os.toLowerCase() ? 1 : -1;
107+
return -1;
99108
},
100109
};
101110

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { afterEach, describe, expect, it, vi } from 'vitest';
2+
3+
afterEach(() => {
4+
vi.unstubAllGlobals();
5+
vi.resetModules();
6+
});
7+
8+
describe('tvOS platform identity', () => {
9+
it('loads with bundlers that do not define the new tvOS flag', async () => {
10+
delete (globalThis as any).__TVOS__;
11+
const platform = await import('./common');
12+
expect(platform.isTvOS).toBe(false);
13+
const { Device } = await import('./device/index.ios');
14+
expect(Device.os).toBe('iOS');
15+
});
16+
17+
it('recognizes tvOS as both tvOS and iOS-compatible', async () => {
18+
vi.stubGlobal('__TVOS__', true);
19+
vi.stubGlobal('__IOS__', false);
20+
vi.stubGlobal('__VISIONOS__', false);
21+
const platform = await import('./common');
22+
expect(platform.isTvOS).toBe(true);
23+
expect(platform.isIOS).toBe(true);
24+
const { Device } = await import('./device/index.ios');
25+
expect(Device.os).toBe('tvOS');
26+
});
27+
});

packages/core/platform/common.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@ export const platformNames = {
55
android: 'Android',
66
ios: 'iOS',
77
visionos: 'visionOS',
8+
tvos: 'tvOS',
89
apple: 'apple',
910
};
1011

1112
export const isAndroid = !!__ANDROID__;
12-
export const isIOS = !!__IOS__ || !!__VISIONOS__;
13+
export const isTvOS = typeof __TVOS__ !== 'undefined' && !!__TVOS__;
14+
export const isIOS = !!__IOS__ || !!__VISIONOS__ || isTvOS;
1315
export const isVisionOS = !!__VISIONOS__;
1416
export const isApple = !!__APPLE__;

packages/core/platform/device/index.ios.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { platformNames } from '../common';
1+
import { isTvOS, platformNames } from '../common';
22

33
type DeviceType = 'Phone' | 'Tablet' | 'Vision';
44

@@ -15,6 +15,8 @@ class DeviceRef {
1515
get os(): string {
1616
if (__VISIONOS__) {
1717
return platformNames.visionos;
18+
} else if (isTvOS) {
19+
return platformNames.tvos;
1820
} else {
1921
return platformNames.ios;
2022
}

packages/core/platform/index.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ export const isApple: boolean;
2424
*/
2525
export const isVisionOS: boolean;
2626

27+
/**
28+
* Gets a value indicating if the app is running on the tvOS platform.
29+
*/
30+
export const isTvOS: boolean;
31+
2732
export * from './common';
2833
export * from './device';
2934
export * from './screen';

packages/core/platforms/ios/src/NativeScriptMainWindow.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ var hasMainInit = false
1010
var hasMainBoot = false
1111
var hasMainSetMainScene = false
1212

13-
@available(iOS 14.0, *)
13+
@available(iOS 14.0, tvOS 14.0, *)
1414
struct NativeScriptMainWindow: Scene {
1515

1616
#if os(visionOS)
@@ -144,7 +144,7 @@ final class NativeScriptWindowCommandCoordinator {
144144
}
145145
#endif
146146

147-
@available(iOS 13.0, *)
147+
@available(iOS 13.0, tvOS 13.0, *)
148148
struct NativeScriptAppView: UIViewRepresentable {
149149
/// A closure that's called when the window is found.
150150
var found: ((UIWindowScene?) -> Void)
@@ -173,7 +173,7 @@ struct NativeScriptAppView: UIViewRepresentable {
173173
}
174174
}
175175

176-
@available(iOS 13.0, *)
176+
@available(iOS 13.0, tvOS 13.0, *)
177177
@objc public class NativeScriptViewFactory: NSObject, NativeScriptEmbedderDelegate {
178178
@objc static var shared: NativeScriptViewFactory?
179179
@objc static var app: NativeScriptContainerCtrl?
@@ -195,7 +195,7 @@ struct NativeScriptAppView: UIViewRepresentable {
195195
return views!.object(forKey: id) as! UIView
196196
}
197197

198-
@available(iOS 15.0, *)
198+
@available(iOS 15.0, tvOS 15.0, *)
199199
@objc public static func getKeyWindow() -> UIWindow? {
200200
return UIApplication
201201
.shared

packages/core/ui/builder/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ if (typeof global.__metadata === 'undefined') {
4040
export const ios = platformNames.ios.toLowerCase();
4141
export const android = platformNames.android.toLowerCase();
4242
export const visionos = platformNames.visionos.toLowerCase();
43+
export const tvos = platformNames.tvos.toLowerCase();
4344
export const apple = platformNames.apple.toLowerCase();
4445
export const defaultNameSpaceMatcher = /tns\.xsd$/i;
4546

@@ -439,7 +440,7 @@ export namespace xml2ui {
439440
if (value) {
440441
const toLower = value.toLowerCase();
441442

442-
return toLower === android || toLower === ios || toLower === visionos || toLower === apple;
443+
return toLower === android || toLower === ios || toLower === visionos || toLower === tvos || toLower === apple;
443444
}
444445

445446
return false;

packages/core/ui/web-view/index.ios.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ import { booleanConverter } from '../core/view-base';
88

99
@NativeClass
1010
class WKNavigationDelegateImpl extends NSObject implements WKNavigationDelegate {
11-
public static ObjCProtocols = [WKNavigationDelegate];
11+
// WebKit does not exist on tvOS; keep this module loadable there (ui/index imports it eagerly).
12+
public static ObjCProtocols = typeof WKNavigationDelegate !== 'undefined' ? [WKNavigationDelegate] : [];
1213
public static initWithOwner(owner: WeakRef<WebView>): WKNavigationDelegateImpl {
1314
const handler = <WKNavigationDelegateImpl>WKNavigationDelegateImpl.new();
1415
handler._owner = owner;
@@ -99,7 +100,7 @@ class WKNavigationDelegateImpl extends NSObject implements WKNavigationDelegate
99100

100101
@NativeClass
101102
class WKUIDelegateImpl extends NSObject implements WKUIDelegate {
102-
public static ObjCProtocols = [WKUIDelegate];
103+
public static ObjCProtocols = typeof WKUIDelegate !== 'undefined' ? [WKUIDelegate] : [];
103104
public static initWithOwner(owner: WeakRef<WebView>): WKUIDelegateImpl {
104105
const handler = <WKUIDelegateImpl>WKUIDelegateImpl.new();
105106
handler._owner = owner;
@@ -179,6 +180,9 @@ export class WebView extends WebViewBase {
179180
}
180181

181182
createNativeView() {
183+
if (typeof WKWebView === 'undefined') {
184+
throw new Error('WebView is not available on this platform (WebKit is missing, e.g. tvOS).');
185+
}
182186
const jScript = "var meta = document.createElement('meta'); meta.setAttribute('name', 'viewport'); meta.setAttribute('content', 'initial-scale=1.0'); document.getElementsByTagName('head')[0].appendChild(meta);";
183187
const wkUScript = WKUserScript.alloc().initWithSourceInjectionTimeForMainFrameOnly(jScript, WKUserScriptInjectionTime.AtDocumentEnd, true);
184188
const wkUController = WKUserContentController.new();

0 commit comments

Comments
 (0)