UNPKG

nitro-codegen

Version:

The code-generator for react-native-nitro-modules.

203 lines (171 loc) 6.39 kB
import type { PlatformSpec } from 'react-native-nitro-modules' import type { InterfaceDeclaration, Type, TypeAliasDeclaration } from 'ts-morph' import { Node, Symbol } from 'ts-morph' import { getBaseTypes } from './utils.js' export type Platform = keyof Required<PlatformSpec> export type Language = Required<PlatformSpec>[keyof PlatformSpec] const platformLanguages: { [K in Platform]: Language[] } = { ios: ['swift', 'c++'], android: ['kotlin', 'c++'], } const allPlatforms = Object.keys(platformLanguages) as Platform[] const allLanguages = Object.values(platformLanguages).flatMap((l) => l) function isValidLanguage(language: string | undefined): language is Language { if (language == null) { return false } return allLanguages.includes(language as Language) } function isValidPlatform(platform: string): platform is Platform { return allPlatforms.includes(platform as Platform) } function getLiteralValue(symbol: Symbol): string | undefined { const value = symbol.getValueDeclaration() if (value == null) { return undefined } const type = value.getType() const literal = type.getLiteralValue() if (typeof literal === 'string') { return literal } return undefined } // TODO: The type casting result here doesn't really work in TS. function isValidLanguageForPlatform( language: Language, platform: Platform ): language is Required<PlatformSpec>[typeof platform] { return platformLanguages[platform].includes(language) } function getPlatformSpec(typeName: string, platformSpecs: Type): PlatformSpec { const result: PlatformSpec = {} // Properties (ios, android) const properties = platformSpecs.getProperties() for (const property of properties) { // Property name (ios, android) const platform = property.getName() if (!isValidPlatform(platform)) { console.warn( ` ⚠️ ${typeName} does not properly extend HybridObject<T> - "${platform}" is not a valid Platform! ` + `Valid platforms are: [${allPlatforms.join(', ')}]` ) continue } // Value (swift, kotlin, c++) const language = getLiteralValue(property) if (!isValidLanguage(language)) { console.warn( ` ⚠️ ${typeName}: Language ${language} is not a valid language for ${platform}! ` + `Valid languages are: [${platformLanguages[platform].join(', ')}]` ) continue } // Double-check that language works on this platform (android: kotlin/c++, ios: swift/c++) if (!isValidLanguageForPlatform(language, platform)) { console.warn( ` ⚠️ ${typeName}: Language ${language} is not a valid language for ${platform}! ` + `Valid languages are: [${platformLanguages[platform].join(', ')}]` ) continue } // @ts-expect-error because TypeScript isn't smart enough yet to correctly cast after the `isValidLanguageForPlatform` check. result[platform] = language } return result } function isDirectlyType(type: Type, name: string): boolean { const symbol = type.getSymbol() ?? type.getAliasSymbol() if (symbol?.getName() === name) { return true } return false } function extendsType(type: Type, name: string, recursive: boolean): boolean { for (const base of getBaseTypes(type)) { const isHybrid = isDirectlyType(base, name) if (isHybrid) { return true } if (recursive) { const baseExtends = extendsType(base, name, recursive) if (baseExtends) { return true } } } return false } export function isDirectlyHybridObject(type: Type): boolean { return isDirectlyType(type, 'HybridObject') } export function extendsHybridObject(type: Type, recursive: boolean): boolean { return extendsType(type, 'HybridObject', recursive) } export function isHybridViewProps(type: Type): boolean { return extendsType(type, 'HybridViewProps', true) } export function isHybridViewMethods(type: Type): boolean { return extendsType(type, 'HybridViewMethods', true) } export function isHybridView(type: Type): boolean { // HybridViews are type aliases for `HybridView`, and `Props & Methods` are just intersected together. const unionTypes = type.getIntersectionTypes() for (const union of unionTypes) { const symbol = union.getSymbol() if (symbol == null) return false return symbol.getName() === 'HybridViewTag' } return false } export function isAnyHybridSubclass(type: Type): boolean { if (isDirectlyHybridObject(type)) return false if (isHybridView(type)) return true if (extendsHybridObject(type, true)) return true return false } /** * If the given interface ({@linkcode declaration}) extends `HybridObject`, * this method returns the platforms it exists on. * If it doesn't extend `HybridObject`, this returns `undefined`. */ export function getHybridObjectPlatforms( declaration: InterfaceDeclaration | TypeAliasDeclaration ): PlatformSpec | undefined { const base = getBaseTypes(declaration.getType()).find((t) => isDirectlyHybridObject(t) ) if (base == null) { // this type does not extend `HybridObject`. throw new Error( `Couldn't find HybridObject<..> base for ${declaration.getName()}! (${declaration.getText()})` ) } const genericArguments = base.getTypeArguments() const platformSpecsArgument = genericArguments[0] if (platformSpecsArgument == null) { // it uses `HybridObject` without generic arguments. This defaults to C++ return { android: 'c++', ios: 'c++' } } return getPlatformSpec(declaration.getName(), platformSpecsArgument) } export function getHybridViewPlatforms( view: InterfaceDeclaration | TypeAliasDeclaration ): PlatformSpec | undefined { if (Node.isTypeAliasDeclaration(view)) { const hybridViewTypeNode = view.getTypeNode() const isHybridViewType = Node.isTypeReference(hybridViewTypeNode) && hybridViewTypeNode.getTypeName().getText() === 'HybridView' if (!isHybridViewType) { return } const genericArguments = hybridViewTypeNode.getTypeArguments() const platformSpecArg = genericArguments[2] if (platformSpecArg != null) { return getPlatformSpec(view.getName(), platformSpecArg.getType()) } } // it uses `HybridObject` without generic arguments. This defaults to platform native languages return { ios: 'swift', android: 'kotlin' } }