nitro-codegen
Version:
The code-generator for react-native-nitro-modules.
246 lines (220 loc) • 7.83 kB
text/typescript
import { Project } from 'ts-morph'
import {
extendsHybridObject,
isHybridView,
getHybridObjectPlatforms,
getHybridViewPlatforms,
type Platform,
} from './getPlatformSpecs.js'
import { generatePlatformFiles } from './createPlatformSpec.js'
import path from 'path'
import { prettifyDirectory } from './prettifyDirectory.js'
import {
capitalizeName,
errorToString,
filterDuplicateFiles,
indent,
} from './utils.js'
import { writeFile } from './writeFile.js'
import chalk from 'chalk'
import { groupByPlatform, type SourceFile } from './syntax/SourceFile.js'
import { Logger } from './Logger.js'
import { NitroConfig } from './config/NitroConfig.js'
import { createIOSAutolinking } from './autolinking/createIOSAutolinking.js'
import { createAndroidAutolinking } from './autolinking/createAndroidAutolinking.js'
import type { Autolinking } from './autolinking/Autolinking.js'
import { createGitAttributes } from './createGitAttributes.js'
import type { PlatformSpec } from 'react-native-nitro-modules'
interface NitrogenOptions {
baseDirectory: string
outputDirectory: string
}
interface NitrogenResult {
generatedFiles: string[]
targetSpecsCount: number
generatedSpecsCount: number
}
export async function runNitrogen({
baseDirectory,
outputDirectory,
}: NitrogenOptions): Promise<NitrogenResult> {
let targetSpecs = 0
let generatedSpecs = 0
// Create the TS project
const project = new Project({
compilerOptions: {
strict: true,
strictNullChecks: true,
noUncheckedIndexedAccess: true,
},
})
const ignorePaths = NitroConfig.getIgnorePaths()
const globPattern = [path.join(baseDirectory, '**', '*.nitro.ts')]
ignorePaths.forEach((ignorePath) => {
globPattern.push('!' + path.join(baseDirectory, ignorePath))
})
project.addSourceFilesAtPaths(globPattern)
// Loop through all source files to log them
Logger.info(
chalk.reset(
`🚀 Nitrogen runs at ${chalk.underline(prettifyDirectory(baseDirectory))}`
)
)
for (const dir of project.getDirectories()) {
const specs = dir.getSourceFiles().length
const relativePath = prettifyDirectory(dir.getPath())
Logger.info(
` 🔍 Nitrogen found ${specs} spec${specs === 1 ? '' : 's'} in ${chalk.underline(relativePath)}`
)
}
// If no source files are found, we can exit
if (project.getSourceFiles().length === 0) {
const searchDir = prettifyDirectory(
path.join(path.resolve(baseDirectory), '**', '*.nitro.ts')
)
Logger.error(
`❌ Nitrogen didn't find any spec files in ${chalk.underline(searchDir)}! ` +
`To create a Nitro Module, create a TypeScript file with the "${chalk.underline('.nitro.ts')}" suffix ` +
'and export an interface that extends HybridObject<T>.'
)
process.exit()
}
const usedPlatforms: Platform[] = []
const filesAfter: string[] = []
const writtenFiles: SourceFile[] = []
for (const sourceFile of project.getSourceFiles()) {
Logger.info(`⏳ Parsing ${sourceFile.getBaseName()}...`)
const startedWithSpecs = generatedSpecs
// Find all interfaceDeclarations in the given file
const declarations = [
...sourceFile.getInterfaces(),
...sourceFile.getTypeAliases(),
]
for (const declaration of declarations) {
let typeName = declaration.getName()
try {
let platformSpec: PlatformSpec
if (isHybridView(declaration.getType())) {
// Hybrid View Props
const targetPlatforms = getHybridViewPlatforms(declaration)
if (targetPlatforms == null) {
// It does not extend HybridView, continue..
continue
}
platformSpec = targetPlatforms
} else if (extendsHybridObject(declaration.getType(), true)) {
// Hybrid View
const targetPlatforms = getHybridObjectPlatforms(declaration)
if (targetPlatforms == null) {
// It does not extend HybridObject, continue..
continue
}
platformSpec = targetPlatforms
} else {
continue
}
const platforms = Object.keys(platformSpec) as Platform[]
if (platforms.length === 0) {
Logger.warn(
`⚠️ ${typeName} does not declare any platforms in HybridObject<T> - nothing can be generated.`
)
continue
}
targetSpecs++
Logger.info(
` ⚙️ Generating specs for HybridObject "${chalk.bold(typeName)}"...`
)
// Create all files and throw it into a big list
const allFiles = platforms
.flatMap((p) => {
usedPlatforms.push(p)
const language = platformSpec[p]!
return generatePlatformFiles(declaration.getType(), language)
})
.filter(filterDuplicateFiles)
// Group the files by platform ({ ios: [], android: [], shared: [] })
const filesPerPlatform = groupByPlatform(allFiles)
// Loop through each platform one by one so that it has some kind of order (per-platform)
for (const [p, files] of Object.entries(filesPerPlatform)) {
const platform = p as SourceFile['platform']
const language =
platform === 'shared' ? 'c++' : platformSpec[platform]
if (language == null) {
// if the language was never specified in the spec, skip it
continue
}
if (files.length === 0) {
// if no files exist on this platform, skip it
continue
}
Logger.info(
` ${chalk.dim(platform)}: Generating ${capitalizeName(language)} code...`
)
// Write the actual files for this specific platform.
for (const file of files) {
const basePath = path.join(
outputDirectory,
file.platform,
file.language
)
const actualPath = await writeFile(basePath, file)
filesAfter.push(actualPath)
writtenFiles.push(file)
}
}
// Done!
generatedSpecs++
} catch (error) {
const message = indent(errorToString(error), ' ')
Logger.error(
chalk.redBright(
` ❌ Failed to generate spec for ${typeName}! ${message}`
)
)
process.exitCode = 1
}
}
if (generatedSpecs === startedWithSpecs) {
Logger.error(
chalk.redBright(
` ❌ No specs found in ${sourceFile.getBaseName()}!`
)
)
}
}
// Autolinking
Logger.info(`⛓️ Setting up build configs for autolinking...`)
const autolinkingFiles: Autolinking[] = []
if (usedPlatforms.includes('ios')) {
autolinkingFiles.push(createIOSAutolinking())
}
if (usedPlatforms.includes('android')) {
autolinkingFiles.push(createAndroidAutolinking(writtenFiles))
}
for (const autolinking of autolinkingFiles) {
Logger.info(
` Creating autolinking build setup for ${chalk.dim(autolinking.platform)}...`
)
for (const file of autolinking.sourceFiles) {
const basePath = path.join(outputDirectory, file.platform)
const actualPath = await writeFile(
basePath,
file as unknown as SourceFile
)
filesAfter.push(actualPath)
}
}
try {
// write a .gitattributes file
const markAsGenerated = NitroConfig.getGitAttributesGeneratedFlag()
const file = await createGitAttributes(markAsGenerated, outputDirectory)
filesAfter.push(file)
} catch {
Logger.error(`❌ Failed to write ${chalk.dim(`.gitattributes`)}!`)
}
return {
generatedFiles: filesAfter,
targetSpecsCount: targetSpecs,
generatedSpecsCount: generatedSpecs,
}
}