'use strict'; import prettier from 'prettier'; import ts from 'typescript'; import { Argument, ArrayType, BasicType, ClassDeclaration, ConstantDeclaration, ConstructorDeclaration, DictionaryType, EnumType, EventDeclaration, FileTypeInformation, FunctionDeclaration, ModuleClassDeclaration, ParametrizedType, PropDeclaration, PropertyDeclaration, RecordType, SumType, Type, TypeKind, ViewDeclaration, } from './typeInformation'; const prefix = `Automatically generated by expo-type-information.`; const exportModifier = () => ts.factory.createModifier(ts.SyntaxKind.ExportKeyword); const declareModifier = () => ts.factory.createModifier(ts.SyntaxKind.DeclareKeyword); const asyncModifier = () => ts.factory.createModifier(ts.SyntaxKind.AsyncKeyword); const readonlyModifier = () => ts.factory.createModifier(ts.SyntaxKind.ReadonlyKeyword); const staticModifier = () => ts.factory.createModifier(ts.SyntaxKind.StaticKeyword); const constModifier = () => ts.factory.createModifier(ts.SyntaxKind.ConstKeyword); const defaultModifier = () => ts.factory.createModifier(ts.SyntaxKind.DefaultKeyword); const unknownKeywordType = () => ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword); const anyKeywordType = () => ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword); const voidKeywordType = () => ts.factory.createKeywordTypeNode(ts.SyntaxKind.VoidKeyword); const newlineIdentifier = () => ts.factory.createIdentifier('\n\n'); /** * A helper type which contains the generated file content and name. */ export type OutputFile = { /** * @field Generated file content. */ content: string; /** * @field Generated file base name (e.g. `ExpoSettings.types.ts`). */ name: string; }; export interface GenerationContext { fileInfo: FileTypeInformation; module: ModuleClassDeclaration; view: ViewDeclaration | null; missingTypes: Set; } let freeId = 0; function getNextFreeId() { freeId += 1; return freeId; } export function createDefaultGenerationContext( fileInfo: FileTypeInformation ): GenerationContext | null { const module = fileInfo.moduleClasses[0]; if (!module) { return null; } const view = module.views[0] ?? null; return { fileInfo, module, view, missingTypes: getMissingTypeIdentifiers(fileInfo), }; } export function getBasicTypesIdentifiers(): Set { return new Set(['any', 'number', 'string', 'undefined', 'null', 'Map', 'Set', 'Promise']); } function getAllNonBasicTypes(fileInfo: FileTypeInformation): Set { return fileInfo.declaredTypeIdentifiers .union(fileInfo.usedTypeIdentifiers) .difference(getBasicTypesIdentifiers()); } function createGeneratedPrefix(): ts.Node[] { return [ ts.addSyntheticLeadingComment( ts.factory.createIdentifier(''), ts.SyntaxKind.MultiLineCommentTrivia, prefix ), ]; } function constructModifiersArray(modifiers: { exported?: boolean; declare?: boolean; async?: boolean; readonly?: boolean; isStatic?: boolean; }): ts.Modifier[] { const modifiersArray: ts.Modifier[] = []; if (modifiers.exported) modifiersArray.push(exportModifier()); if (modifiers.declare) modifiersArray.push(declareModifier()); if (modifiers.async) modifiersArray.push(asyncModifier()); if (modifiers.readonly) modifiersArray.push(readonlyModifier()); if (modifiers.isStatic) modifiersArray.push(staticModifier()); return modifiersArray; } export function joinTSNodesWithNewlines(nodes: ts.Node[][]): ts.Node[] { const new_nodes: ts.Node[] = []; for (const node of nodes) { if (node.length > 0) { new_nodes.push(...node); new_nodes.push(newlineIdentifier()); } } return new_nodes; } function mapBasicTypeToTypeNode(basicType: BasicType): ts.TypeNode { if (basicType === BasicType.UNRESOLVED) { return ts.addSyntheticTrailingComment( unknownKeywordType(), ts.SyntaxKind.MultiLineCommentTrivia, "The type couldn't be resolved automatically." ); } const BASIC_TYPE_MAP: Record = { [BasicType.ANY]: ts.SyntaxKind.AnyKeyword, [BasicType.BOOLEAN]: ts.SyntaxKind.BooleanKeyword, [BasicType.NUMBER]: ts.SyntaxKind.NumberKeyword, [BasicType.STRING]: ts.SyntaxKind.StringKeyword, [BasicType.VOID]: ts.SyntaxKind.VoidKeyword, [BasicType.UNDEFINED]: ts.SyntaxKind.UndefinedKeyword, [BasicType.UNRESOLVED]: ts.SyntaxKind.UndefinedKeyword, // This is handled earlier }; return ts.factory.createKeywordTypeNode(BASIC_TYPE_MAP[basicType]); } export function mapTypeToTsTypeNode(type: Type): ts.TypeNode { switch (type.kind) { case TypeKind.BASIC: return mapBasicTypeToTypeNode(type.type as BasicType); case TypeKind.IDENTIFIER: return ts.factory.createTypeReferenceNode(type.type as string); case TypeKind.SUM: return ts.factory.createUnionTypeNode((type.type as SumType).types.map(mapTypeToTsTypeNode)); case TypeKind.ARRAY: return ts.factory.createArrayTypeNode(mapTypeToTsTypeNode(type.type as ArrayType)); case TypeKind.DICTIONARY: { const dictionaryType = type.type as DictionaryType; const name = 'key'; const typeNode = mapTypeToTsTypeNode(dictionaryType.key); const valueType = mapTypeToTsTypeNode(dictionaryType.value); return ts.factory.createTypeLiteralNode([ ts.factory.createIndexSignature( undefined, [createParameter({ name, type: typeNode })], valueType ), ]); } // Technically this one should only be the top one and it should be handled somewhere else // for example when creating arguemnt adding the '?' token. // // However we can just make it (type | undefined) in here. // TODO(@HubertBer): Maybe also need null? case TypeKind.OPTIONAL: return ts.factory.createUnionTypeNode([ mapTypeToTsTypeNode(type.type as Type), mapBasicTypeToTypeNode(BasicType.UNDEFINED), ]); case TypeKind.PARAMETRIZED: return ts.factory.createTypeReferenceNode( (type.type as ParametrizedType).name, (type.type as ParametrizedType).types.map(mapTypeToTsTypeNode) ); } return mapBasicTypeToTypeNode(BasicType.UNRESOLVED); } // // ts.factory wrapper functions // function createImportDeclaration({ defaultImportName, namedImportsNames, importFromName, }: { defaultImportName?: string; namedImportsNames?: string[]; importFromName: string; }): ts.Node[] { const hasDefault = !!defaultImportName; const hasNamed = namedImportsNames && namedImportsNames.length > 0; if (!hasDefault && !hasNamed) { return []; } const defaultImport = hasDefault ? ts.factory.createIdentifier(defaultImportName) : undefined; const namedImports = hasNamed ? ts.factory.createNamedImports( namedImportsNames.map((name) => ts.factory.createImportSpecifier(false, undefined, ts.factory.createIdentifier(name)) ) ) : undefined; return [ ts.factory.createImportDeclaration( undefined, ts.factory.createImportClause(undefined, defaultImport, namedImports), ts.factory.createStringLiteral(importFromName) ), ]; } function createParameter({ modifiers, name, type, questionToken, dotDotDotToken, initializer, }: { modifiers?: ts.Modifier[]; name: string | ts.BindingName; type?: ts.TypeNode; questionToken?: ts.QuestionToken; dotDotDotToken?: ts.DotDotDotToken; initializer?: ts.Expression; }): ts.ParameterDeclaration { return ts.factory.createParameterDeclaration( modifiers, dotDotDotToken, name, questionToken, type, initializer ); } function createProperty({ modifiers, name, typeNode, initializer, optional, }: { modifiers?: ts.Modifier[]; name: string | ts.PropertyName; typeNode?: ts.TypeNode; initializer?: ts.Expression; optional?: boolean; }): ts.PropertyDeclaration { return ts.factory.createPropertyDeclaration( modifiers, name, optional ? ts.factory.createToken(ts.SyntaxKind.QuestionToken) : undefined, typeNode, initializer ); } function createPropertySignature({ name, typeNode, optional, modifiers, }: { name: string | ts.PropertyName; typeNode?: ts.TypeNode; optional?: boolean; modifiers?: ts.Modifier[]; }): ts.PropertySignature { return ts.factory.createPropertySignature( modifiers, name, optional ? ts.factory.createToken(ts.SyntaxKind.QuestionToken) : undefined, typeNode ); } function createCall({ expression, args, typeArgs, }: { expression: string | ts.Expression; args?: ts.Expression[]; typeArgs?: ts.TypeNode[]; }): ts.CallExpression { return ts.factory.createCallExpression( typeof expression === 'string' ? ts.factory.createIdentifier(expression) : expression, typeArgs, args ); } function createRequireNativeModuleExpression({ moduleType, moduleName, }: { moduleType?: string; moduleName: string; }) { return ts.factory.createCallExpression( ts.factory.createIdentifier('requireNativeModule'), moduleType ? [ts.factory.createTypeReferenceNode(moduleType)] : undefined, [ts.factory.createStringLiteral(moduleName)] ); } function createExportDefaultAsDeclaration({ exportAsName, importFromName, }: { exportAsName: string; importFromName: string; }): ts.Node[] { return [ ts.factory.createExportDeclaration( undefined, false, ts.factory.createNamedExports([ ts.factory.createExportSpecifier( false, ts.factory.createIdentifier('default'), ts.factory.createIdentifier(exportAsName) ), ]), ts.factory.createStringLiteral(importFromName) ), ]; } function createTypeAlias({ exported, alias, typeParams, type, }: { exported?: boolean; alias: string; typeParams?: ts.TypeParameterDeclaration[]; type: ts.TypeNode; }) { return ts.factory.createTypeAliasDeclaration( constructModifiersArray({ exported }), alias, typeParams, type ); } function createRequireNativeViewDeclaration(module: ModuleClassDeclaration, view: ViewDeclaration) { return [ createParameter({ modifiers: [constModifier()], name: view.name, initializer: createCall({ expression: 'requireNativeView', typeArgs: [ts.factory.createTypeReferenceNode(getViewPropsTypeName(view))], args: [ ts.factory.createStringLiteral(module.name), ts.factory.createStringLiteral(view.name), ], }), }), ]; } function createExportAllDeclaration({ importFromName, justTypes, }: { importFromName: string; justTypes?: boolean; }): ts.Node[] { return [ ts.factory.createExportDeclaration( undefined, justTypes ?? false, undefined, ts.factory.createStringLiteral(importFromName) ), ]; } function createExportDefault(name: string = '_default'): ts.Node[] { return [ts.factory.createExportDefault(ts.factory.createIdentifier(name))]; } function createComponentType(propsTypeName: string) { return ts.factory.createTypeReferenceNode('React.JSXElementConstructor', [ ts.factory.createTypeReferenceNode(propsTypeName), ]); } function getMissingTypeIdentifiers(fileTypeInformation: FileTypeInformation): Set { return fileTypeInformation.usedTypeIdentifiers .difference(fileTypeInformation.declaredTypeIdentifiers) .difference(getBasicTypesIdentifiers()); } function buildPropsMembers({ props, events }: ViewDeclaration): ts.TypeElement[] { const buildEventPropertySignature = (eventDeclaration: EventDeclaration) => { const name = eventDeclaration; const typeNode = ts.factory.createFunctionTypeNode( undefined, [createParameter({ name: 'event', type: anyKeywordType() })], voidKeywordType() ); // TODO(@HubertBer) check whether we have ways of making events not optional return createPropertySignature({ name, typeNode, optional: true }); }; const buildPropPropertySignature = (propDeclaration: PropDeclaration) => { const propTypeArgument = propDeclaration.arguments[1]?.type; if (!propDeclaration || !propDeclaration.arguments || !propTypeArgument) { return undefined; } const name = propDeclaration.name; const typeNode = mapTypeToTsTypeNode(propTypeArgument); return createPropertySignature({ name, typeNode }); }; return [ ...(props.map(buildPropPropertySignature).filter((p) => p) as ts.PropertySignature[]), ...events.map(buildEventPropertySignature), ]; } export function buildViewPropsInterface( view: ViewDeclaration | null, options: { exported?: boolean } ): ts.Node[] { if (!view) { return []; } return [ ts.factory.createInterfaceDeclaration( constructModifiersArray(options), getViewPropsTypeName(view), undefined, [ ts.factory.createHeritageClause(ts.SyntaxKind.ExtendsKeyword, [ ts.factory.createExpressionWithTypeArguments( ts.factory.createIdentifier('ViewProps'), undefined ), ]), ], buildPropsMembers(view) ), ]; } function buildClassProperty(declaration: PropertyDeclaration): ts.PropertyDeclaration { return createProperty({ modifiers: [readonlyModifier()], name: declaration.name, typeNode: mapTypeToTsTypeNode(declaration.type), }); } function getModuleClassEventsTypeName(moduleClassDeclaration: ModuleClassDeclaration): string { return `${moduleClassDeclaration.name}Events`; } function buildNativeModuleClassDeclaration({ moduleClassDeclaration, exportedModuleName, }: { moduleClassDeclaration: ModuleClassDeclaration; exportedModuleName?: string; }): ts.Node[] { const buildClassTypeProperty = (classDeclaration: ClassDeclaration) => createProperty({ // TODO(@HubertBer): that's a hack, but I couldn't find a proper way to do this // The problem is that declare class semantics seem somewhat different than class semantics. name: classDeclaration.name, typeNode: ts.factory.createTypeQueryNode(ts.factory.createIdentifier(classDeclaration.name)), }); const buildSyncMethod = (functionDeclaration: FunctionDeclaration): ts.MethodDeclaration => buildFunction({ functionDeclaration, method: true, declaration: true, }) as ts.MethodDeclaration; const buildAsyncMethod = (functionDeclaration: FunctionDeclaration): ts.MethodDeclaration => buildFunction({ functionDeclaration, async: true, method: true, declaration: true, }) as ts.MethodDeclaration; return [ ts.factory.createClassDeclaration( [exportModifier(), declareModifier()], exportedModuleName ?? `${moduleClassDeclaration.name}NativeModuleType`, undefined, [ ts.factory.createHeritageClause(ts.SyntaxKind.ExtendsKeyword, [ ts.factory.createExpressionWithTypeArguments( ts.factory.createIdentifier('NativeModule'), [ ts.factory.createTypeReferenceNode( getModuleClassEventsTypeName(moduleClassDeclaration) ), ] ), ]), ], [ ...moduleClassDeclaration.constants.map(buildClassProperty), ...moduleClassDeclaration.properties.map(buildClassProperty), ...moduleClassDeclaration.functions.map(buildSyncMethod), ...moduleClassDeclaration.asyncFunctions.map(buildAsyncMethod), ...moduleClassDeclaration.classes.map(buildClassTypeProperty), ] ), ]; } function buildArgumentDeclarationAndName(arg: Argument): { argDeclaration: ts.ParameterDeclaration; argName: string; } { const argName = arg.name ?? '_' + getNextFreeId(); const argDeclaration = createParameter({ name: argName, type: mapTypeToTsTypeNode(arg.type), }); return { argDeclaration, argName }; } function buildArgumentDeclaration(arg: Argument): ts.ParameterDeclaration { return buildArgumentDeclarationAndName(arg).argDeclaration; } export type buildFunctionOptions = { functionDeclaration: FunctionDeclaration; async?: boolean; method?: boolean; exported?: boolean; declaration?: boolean; returnStatement?: null | ts.ReturnStatement[]; overrideArgumentDeclarations?: ts.ParameterDeclaration[]; omitReturnType?: boolean; }; export function buildFunction({ functionDeclaration, async, method, exported, declaration, returnStatement, overrideArgumentDeclarations, omitReturnType, }: buildFunctionOptions): ts.FunctionDeclaration | ts.MethodDeclaration { const functionModifiers = constructModifiersArray({ exported, async: async && !declaration, isStatic: functionDeclaration.isStatic, }); const customReturn = !!returnStatement; const bareReturnTypeNode = mapTypeToTsTypeNode(functionDeclaration.returnType); const wrapWithPromiseType = (typeNode: ts.TypeNode): ts.TypeNode => ts.factory.createTypeReferenceNode('Promise', [typeNode]); let returnTypeNode: ts.TypeNode | undefined = async ? wrapWithPromiseType(bareReturnTypeNode) : bareReturnTypeNode; if (omitReturnType) { returnTypeNode = undefined; } const argumentDeclarations = overrideArgumentDeclarations ?? functionDeclaration.arguments.map(buildArgumentDeclaration); if (method) { return ts.factory.createMethodDeclaration( functionModifiers, undefined, functionDeclaration.name, undefined, undefined, argumentDeclarations, returnTypeNode, declaration ? undefined : ts.factory.createBlock(customReturn ? returnStatement : []) ); } return ts.factory.createFunctionDeclaration( functionModifiers, undefined, functionDeclaration.name, undefined, argumentDeclarations, returnTypeNode, declaration ? undefined : ts.factory.createBlock(customReturn ? returnStatement : []) ); } export function buildConstructor( constructor: ConstructorDeclaration, declaration: boolean ): ts.ClassElement { return ts.factory.createConstructorDeclaration( undefined, constructor.arguments.map(buildArgumentDeclaration), declaration ? undefined : ts.factory.createBlock([]) ); } type BuildClassOptions = { classDeclaration: ClassDeclaration; exported?: boolean; declaration?: boolean; getFunctionReturnBlock?: (functionDeclaration: FunctionDeclaration) => ts.ReturnStatement[]; }; // TODO(@HubertBer): figure out what about inheritance, should or should not inherit SharedObject export function buildClass({ classDeclaration, exported, declaration, getFunctionReturnBlock, }: BuildClassOptions): ts.ClassDeclaration { const getReturnStatement = (method: FunctionDeclaration) => !declaration && getFunctionReturnBlock ? getFunctionReturnBlock(method) : null; const buildMethod = (method: FunctionDeclaration, async?: boolean) => buildFunction({ functionDeclaration: method, method: true, async, declaration, returnStatement: getReturnStatement(method), }) as ts.MethodDeclaration; const classMembers = [ ...classDeclaration.methods.map((m) => buildMethod(m)), ...classDeclaration.asyncMethods.map((m) => buildMethod(m, true)), ...(declaration ? classDeclaration.properties.map(buildClassProperty) : []), classDeclaration.constructor ? buildConstructor(classDeclaration.constructor, declaration ?? false) : undefined, ].filter((x) => x !== undefined); return ts.factory.createClassDeclaration( constructModifiersArray({ exported, declare: declaration }), ts.factory.createIdentifier(classDeclaration.name), undefined, [], classMembers ); } function buildModuleDefaultExport({ moduleName, moduleType, declaration, }: { moduleName: string; moduleType?: string; declaration?: boolean; }): ts.Node[] { const name = '_default'; const type = moduleType ? ts.factory.createTypeReferenceNode(moduleType) : undefined; return [ createParameter({ modifiers: [constModifier()], name, type, initializer: declaration ? undefined : createRequireNativeModuleExpression({ moduleName, moduleType }), }), ts.factory.createExportDefault(ts.factory.createIdentifier('_default')), ]; } export function buildUnknownTypeAlias( identifier: string, exported: boolean, inferredTypeParametersCount: Map ): ts.Statement { const paramCount = inferredTypeParametersCount.get(identifier); const typeParamsList = []; for (let i = 0; i < (paramCount ?? 0); i += 1) { typeParamsList.push(ts.factory.createTypeParameterDeclaration(undefined, 'T' + i)); } const typeParams = (paramCount ?? 0) === 0 ? undefined : typeParamsList; return createTypeAlias({ exported, alias: identifier, type: unknownKeywordType(), typeParams }); } export function buildRecordTypeAlias(recordType: RecordType, exported: boolean): ts.Node { return createTypeAlias({ exported, alias: recordType.name, type: ts.factory.createTypeLiteralNode( recordType.fields.map((field) => { const optional = field.type.kind === TypeKind.OPTIONAL; const typeNode = mapTypeToTsTypeNode(optional ? (field.type.type as Type) : field.type); const name = field.name ?? '_' + getNextFreeId(); return createPropertySignature({ name, optional, typeNode }); }) ), }); } export function buildEnumTypeDeclaration( enumType: EnumType, exported: boolean, declared: boolean ): ts.Node { return ts.factory.createEnumDeclaration( constructModifiersArray({ exported, declare: declared }), enumType.name, enumType.cases.map((enumcase) => ts.factory.createEnumMember( enumcase, enumType.stringBacked ? ts.factory.createStringLiteral(enumcase) : undefined ) ) ); } function buildMissingTypesDeclarations(ctx: GenerationContext): ts.Node[] { if (ctx.missingTypes.size === 0) { return []; } const header = ts.addSyntheticLeadingComment( ts.factory.createIdentifier(''), ts.SyntaxKind.SingleLineCommentTrivia, ` These types haven't been defined in provided file(s).`, true ); const aliases = [...ctx.missingTypes].map((identifier) => buildUnknownTypeAlias(identifier, true, ctx.fileInfo.inferredTypeParametersCount) ); return [header, ...aliases]; } function buildDefaultViewComponent({ componentName, propsTypeAlias, }: { componentName: string; propsTypeAlias: string; }) { const jsxElement = ts.factory.createJsxSelfClosingElement( ts.factory.createIdentifier(componentName), undefined, ts.factory.createJsxAttributes([ ts.factory.createJsxSpreadAttribute(ts.factory.createIdentifier('props')), ]) ); const functionBody = ts.factory.createBlock([ts.factory.createReturnStatement(jsxElement)]); return [ ts.factory.createFunctionExpression( [exportModifier(), defaultModifier()], undefined, componentName + 'Component', undefined, [ createParameter({ name: 'props', type: ts.factory.createTypeReferenceNode(propsTypeAlias), }), ], undefined, functionBody ), ]; } function buildModuleEventsTypeDeclaration( moduleClassDeclaration: ModuleClassDeclaration, { exported }: { exported?: boolean } ): ts.Node[] { const createEventType = () => ts.factory.createFunctionTypeNode( undefined, [ ts.factory.createParameterDeclaration( undefined, ts.factory.createToken(ts.SyntaxKind.DotDotDotToken), ts.factory.createIdentifier('args'), undefined, ts.factory.createArrayTypeNode( ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword) ), undefined ), ], voidKeywordType() ); return [ ts.addSyntheticLeadingComment( createTypeAlias({ exported, alias: getModuleClassEventsTypeName(moduleClassDeclaration), type: ts.factory.createTypeLiteralNode( moduleClassDeclaration.events.map((event) => { return createPropertySignature({ name: event, typeNode: createEventType(), }); }) ), }), ts.SyntaxKind.SingleLineCommentTrivia, ` These events may have arguments that weren't resolved!` ), ]; } export function buildExposedTypesDeclarations( ctx: GenerationContext, options: { exported?: boolean; declare?: boolean } ): ts.Node[] { const recordDeclarationMap = (recordType: RecordType) => buildRecordTypeAlias(recordType, options.exported ?? false); const enumDeclarationMap = (enumType: EnumType) => buildEnumTypeDeclaration(enumType, options.exported ?? false, options.declare ?? false); const classDeclarationMap = (classDeclaration: ClassDeclaration) => buildClass({ classDeclaration, exported: true, declaration: true }); return joinTSNodesWithNewlines([ createImportDeclaration({ namedImportsNames: ['NativeModule'], importFromName: 'expo' }), buildMissingTypesDeclarations(ctx), ctx.fileInfo.records.flatMap(recordDeclarationMap), ctx.fileInfo.enums.flatMap(enumDeclarationMap), ctx.module.classes.map(classDeclarationMap), buildModuleEventsTypeDeclaration(ctx.module, options), ]); } function buildModuleDeclarationNodes(ctx: GenerationContext): ts.Node[] { return joinTSNodesWithNewlines([ createGeneratedPrefix(), buildExposedTypesDeclarations(ctx, { exported: true }), buildNativeModuleClassDeclaration({ moduleClassDeclaration: ctx.module }), buildModuleDefaultExport({ moduleName: ctx.module.name, moduleType: ctx.module.name, declaration: true, }), ] as ts.Node[][]); } export function getViewPropsTypeName(view: ViewDeclaration): string { return view.name + (view.name.endsWith('View') ? 'Props' : 'ViewProps'); } function buildViewDeclarationNodes(ctx: GenerationContext): ts.Node[] { if (!ctx.view) { return []; } const viewComponentType = createComponentType(getViewPropsTypeName(ctx.view)); const modifiers = [declareModifier(), constModifier()]; return joinTSNodesWithNewlines([ createGeneratedPrefix(), createImportDeclaration({ namedImportsNames: ['ViewProps'], importFromName: 'react-native' }), buildMissingTypesDeclarations(ctx), buildViewPropsInterface(ctx.view, {}), [createParameter({ modifiers, name: '_default', type: viewComponentType })], createExportDefault(), ]); } function buildJSXIntrinsicsViewNodes(ctx: GenerationContext): ts.Node[] { const name = ctx.module.name; const propsTypeNode = ctx.view ? ts.factory.createTypeLiteralNode(buildPropsMembers(ctx.view)) : undefined; const jsxIntrinsicElementsNodes = []; if (ctx.view) { const globalIdentifier = ts.factory.createIdentifier('global'); const jsxIdentifier = ts.factory.createIdentifier('JSX'); const intrinsicElementsIdentifier = ts.factory.createIdentifier('IntrinsicElements'); jsxIntrinsicElementsNodes.push( ts.factory.createModuleDeclaration( [declareModifier()], globalIdentifier, ts.factory.createModuleBlock([ ts.factory.createModuleDeclaration( undefined, jsxIdentifier, ts.factory.createModuleBlock([ ts.factory.createInterfaceDeclaration( undefined, intrinsicElementsIdentifier, undefined, undefined, [createPropertySignature({ name, typeNode: propsTypeNode })] ), ]), ts.NodeFlags.Namespace ), ]), ts.NodeFlags.GlobalAugmentation ) ); } return joinTSNodesWithNewlines([ buildExposedTypesDeclarations(ctx, { declare: true }), jsxIntrinsicElementsNodes, ]); } function buildNativeModuleGeneratedNodes(ctx: GenerationContext): ts.Node[] { return joinTSNodesWithNewlines([ createGeneratedPrefix(), createImportDeclaration({ namedImportsNames: ['ViewProps'], importFromName: 'react-native' }), buildExposedTypesDeclarations(ctx, { exported: true }), buildViewPropsInterface(ctx.view, { exported: true }), buildNativeModuleClassDeclaration({ moduleClassDeclaration: ctx.module }), ]); } function buildStableNativeModuleInterface(ctx: GenerationContext): ts.Node[] { const generatedModuleAlias = ctx.module.name; const generatedModuleTypeAlias = `${ctx.module.name}NativeModuleType`; const generatedFilePath = `./${ctx.module.name}.generated`; const exportedFunctionReturnStatement = ( functionDeclaration: FunctionDeclaration, overrideArguments?: ts.Identifier[] ) => { const expression = `${generatedModuleAlias}.${functionDeclaration.name}`; const args = overrideArguments ?? functionDeclaration.arguments.map((arg) => ts.factory.createIdentifier(arg.name ?? 'unnamedArgument') ); return ts.factory.createReturnStatement( createCall({ expression, args, }) ); }; const mapFunctionDeclarationTemplate = (isAsync: boolean) => (functionDeclaration: FunctionDeclaration) => { const argumentDeclarations = []; const argumentNames = []; for (const arg of functionDeclaration.arguments) { const { argDeclaration, argName } = buildArgumentDeclarationAndName(arg); argumentDeclarations.push(argDeclaration); argumentNames.push(argName); } return [ buildFunction({ functionDeclaration, async: isAsync, exported: true, returnStatement: [ exportedFunctionReturnStatement( functionDeclaration, argumentNames.map(ts.factory.createIdentifier) ), ], overrideArgumentDeclarations: argumentDeclarations, omitReturnType: true, }), ]; }; const mapSyncFunctionDeclaration = mapFunctionDeclarationTemplate(false); const mapAsyncFunctionDeclaration = mapFunctionDeclarationTemplate(true); const buildConstantExportProperty = (constant: ConstantDeclaration): ts.Node => { const typeNode = mapTypeToTsTypeNode(constant.type); const modifiers = [exportModifier(), constModifier()]; const initializer = ts.factory.createIdentifier(`${generatedModuleAlias}.${constant.name}`); return createProperty({ modifiers, name: constant.name, typeNode, initializer }); }; return joinTSNodesWithNewlines([ ctx.view ? createImportDeclaration({ importFromName: 'react', defaultImportName: 'React' }) : [], createImportDeclaration({ namedImportsNames: [ ...ctx.fileInfo.usedTypeIdentifiers.difference(getBasicTypesIdentifiers()), ...[ getModuleClassEventsTypeName(ctx.module), generatedModuleTypeAlias, ctx.view ? getViewPropsTypeName(ctx.view) : null, ].filter((v) => v !== null), ], importFromName: generatedFilePath, }), createImportDeclaration({ namedImportsNames: ['requireNativeModule', 'requireNativeView'], importFromName: 'expo', }), [ createParameter({ modifiers: [constModifier()], name: ctx.module.name, type: ts.factory.createTypeReferenceNode(generatedModuleTypeAlias), initializer: createRequireNativeModuleExpression({ moduleName: ctx.module.name, moduleType: generatedModuleTypeAlias, }), }), ], ctx.view ? createRequireNativeViewDeclaration(ctx.module, ctx.view) : [], ctx.module.constants.map(buildConstantExportProperty), ctx.module.functions.flatMap(mapSyncFunctionDeclaration), ctx.module.asyncFunctions.flatMap(mapAsyncFunctionDeclaration), ctx.view ? buildDefaultViewComponent({ componentName: ctx.view.name, propsTypeAlias: getViewPropsTypeName(ctx.view), }) : [], ]); } async function tsNodesToString(elements: ts.Node[]): Promise { const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed }); const resultFile = ts.createSourceFile('', '', ts.ScriptTarget.Latest, false, ts.ScriptKind.TSX); const viewTypes = ts.factory.createNodeArray(elements); const printedTs = printer.printList( ts.ListFormat.MultiLine | ts.ListFormat.PreserveLines, viewTypes, resultFile ); return await prettifyCode(printedTs, 'typescript'); } /** * Helper function that takes a file content string and formats it using `prettier` formatter. * @param text Content of a JavaScript/TypeScript file to format. * @param parser An option of which parser to use to format the file. * @default "babel" * @returns A promise which resolves to the `text` string after formatting using `prettier` with the given `parser`. * @header TypescriptGeneration */ export async function prettifyCode(text: string, parser: 'babel' | 'typescript' = 'babel') { return await prettier.format(text, { parser, tabWidth: 2, printWidth: 100, trailingComma: 'none', singleQuote: true, }); } /** * Generates the TypeScript string content for a native View's type declaration file. * @param fileTypeInformation The abstracted type information of an Expo module. * @returns A promise that resolves to a string containing the TypeScript declaration file content or `null` if the generation has failed. * @header TypescriptGeneration */ export async function generateViewTypesFileContent( fileTypeInformation: FileTypeInformation ): Promise { const ctx = createDefaultGenerationContext(fileTypeInformation); if (!ctx) { return null; } return tsNodesToString(buildViewDeclarationNodes(ctx)); } /** * Generates the TypeScript string content for a native View's type declaration file which mounts the View props on the global `JSXIntrinsics`. * @param fileTypeInformation The abstracted type information of an Expo module. * @returns A promise that resolves to a string containing the TypeScript declaration file content or `null` if the generation has failed. * @header TypescriptGeneration */ export async function generateJSXIntrinsicsFileContent( fileTypeInformation: FileTypeInformation ): Promise { const ctx = createDefaultGenerationContext(fileTypeInformation); if (!ctx) { return null; } return tsNodesToString(buildJSXIntrinsicsViewNodes(ctx)); } /** * Generates the TypeScript string content for a native module type declaration file. * @param fileTypeInformation The abstracted type information of an Expo module. * @returns A promise that resolves to a string containing the TypeScript module declaration file content or `null` if the generation has failed. * @header TypescriptGeneration */ export async function generateModuleTypesFileContent( fileTypeInformation: FileTypeInformation ): Promise { const ctx = createDefaultGenerationContext(fileTypeInformation); if (!ctx) { return null; } return tsNodesToString(buildModuleDeclarationNodes(ctx)); } /** * Generates a short TypeScript interface for an Expo module. This creates the content for two files: a volatile generated file containing raw type definitions, * and a stable user-facing file that wraps and exports the native module methods in new functions. * @param fileTypeInformation The abstracted type information of an Expo module. * @returns A promise that resolves to an object containing the string contents for both the volatile generated file and the stable TypeScript interface file. * @header TypescriptGeneration */ export async function generateConciseTsInterface( fileTypeInformation: FileTypeInformation ): Promise<{ volatileGeneratedFileContent: string; moduleTypescriptInterfaceFileContent: string; }> { const ctx = createDefaultGenerationContext(fileTypeInformation); if (!ctx) { return { volatileGeneratedFileContent: '', moduleTypescriptInterfaceFileContent: '' }; } const volatileGeneratedFileContent = await tsNodesToString(buildNativeModuleGeneratedNodes(ctx)); const moduleTypescriptInterfaceFileContent = await tsNodesToString( buildStableNativeModuleInterface(ctx) ); return { volatileGeneratedFileContent, moduleTypescriptInterfaceFileContent, }; } /** * Generates a full, multi-file TypeScript interface for an Expo module. * The generated interface is separated into a file with type definitions, a file which wraps the native module, a file for each view defined in a module and an index file which reexports all definitions from the other files. * * @param fileTypeInformation The abstracted type information of an Expo module. * @returns A promise that resolves to an object containing the string contents for all of the generated files or `null` if the generation has failed. * @header TypescriptGeneration */ export async function generateFullTsInterface(fileTypeInformation: FileTypeInformation): Promise<{ moduleTypesFile: OutputFile; moduleViewsFiles: OutputFile[]; moduleNativeFile: OutputFile; indexFile: OutputFile; } | null> { const ctx = createDefaultGenerationContext(fileTypeInformation); if (!ctx) { return null; } const moduleNativeFileImportName = `${ctx?.module.name}Module`; const moduleTypesFileImportName = `${ctx?.module.name}.types`; const moduleViewsFilesImportNames: string[] = []; const moduleTypesFileNodes = joinTSNodesWithNewlines([ createGeneratedPrefix(), createImportDeclaration({ namedImportsNames: ['ViewProps'], importFromName: 'react-native' }), buildExposedTypesDeclarations(ctx, { exported: true }), ...ctx.module.views.map((view) => buildViewPropsInterface(view, { exported: true })), ]); const moduleViewFilesNodes = []; for (const view of ctx.module.views) { const moduleViewFileNodes = joinTSNodesWithNewlines([ createGeneratedPrefix(), createImportDeclaration({ namedImportsNames: ['requireNativeView'], importFromName: 'expo', }), createImportDeclaration({ namedImportsNames: [getViewPropsTypeName(view)], importFromName: `./${moduleTypesFileImportName}`, }), createRequireNativeViewDeclaration(ctx.module, view), buildDefaultViewComponent({ componentName: view.name, propsTypeAlias: getViewPropsTypeName(view), }), ]); moduleViewFilesNodes.push(moduleViewFileNodes); moduleViewsFilesImportNames.push(`${view.name}View`); } const moduleNativeModuleNodes = joinTSNodesWithNewlines([ createGeneratedPrefix(), createImportDeclaration({ namedImportsNames: ['requireNativeModule', 'NativeModule'], importFromName: 'expo', }), createImportDeclaration({ namedImportsNames: [ ...getAllNonBasicTypes(ctx.fileInfo), getModuleClassEventsTypeName(ctx.module), ], importFromName: `./${moduleTypesFileImportName}`, }), buildNativeModuleClassDeclaration({ moduleClassDeclaration: ctx.module, exportedModuleName: ctx.module.name, }), buildModuleDefaultExport({ moduleName: ctx.module.name, moduleType: ctx.module.name }), ]); const indexFileNodes = joinTSNodesWithNewlines([ createGeneratedPrefix(), createExportAllDeclaration({ importFromName: `./${moduleTypesFileImportName}`, justTypes: true, }), createExportDefaultAsDeclaration({ exportAsName: ctx.module.name, importFromName: `./${moduleNativeFileImportName}`, }), ...ctx.module.views.map((view, idx) => createExportDefaultAsDeclaration({ exportAsName: view.name, importFromName: `./${moduleViewsFilesImportNames[idx]}`, }) ), ]); const [ moduleTypesFileContent, moduleViewFilesContents, moduleNativeFileContent, indexFileContent, ] = await Promise.all([ tsNodesToString(moduleTypesFileNodes), Promise.all(moduleViewFilesNodes.map(tsNodesToString)), tsNodesToString(moduleNativeModuleNodes), tsNodesToString(indexFileNodes), ]); const moduleTypesFile = { content: moduleTypesFileContent, name: `${moduleTypesFileImportName}.ts`, }; const moduleViewsFiles = moduleViewFilesContents.map((moduleViewFileContent, idx) => { return { content: moduleViewFileContent, name: `${moduleViewsFilesImportNames[idx]}.tsx`, }; }); const moduleNativeFile = { content: moduleNativeFileContent, name: `${moduleNativeFileImportName}.ts`, }; const indexFile = { content: indexFileContent, name: `index.ts` }; return { moduleTypesFile, moduleViewsFiles, moduleNativeFile, indexFile, }; }