nitro-codegen
Version:
The code-generator for react-native-nitro-modules.
187 lines (162 loc) • 5.91 kB
text/typescript
import path from 'path'
import { prettifyDirectory } from './prettifyDirectory.js'
import fs from 'fs/promises'
import { existsSync } from 'fs'
import { Logger } from './Logger.js'
import chalk from 'chalk'
import { execSync } from 'child_process'
import { randomUUID } from 'crypto'
export async function initNewNitroModule(
baseDirectory: string,
moduleName: string,
ref: string = 'main'
): Promise<void> {
Logger.info(
`⚙️ Creating new Nitro Module "${chalk.bold(moduleName)}" in ${chalk.underline(prettifyDirectory(baseDirectory))}...`
)
const directory = path.join(baseDirectory, moduleName)
if (existsSync(directory)) {
Logger.error(
`❌ A folder named "${chalk.underline(moduleName)}" already exists in the directory ${chalk.underline(prettifyDirectory(baseDirectory))}!`
)
process.exit(1)
}
await fs.mkdir(directory)
const modulePath = await downloadGitHubFolder(
'mrousavy',
'nitro',
ref,
'packages/template',
directory
)
Logger.info(`🏗️ Constructing template...`)
const cleanLibraryName = moduleName.replace('react-native-', '')
const cxxNamespace = cleanLibraryName.replaceAll('-', '')
let camelCaseName = cleanLibraryName
.split('-')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
.join('')
if (!camelCaseName.startsWith('Nitro')) {
camelCaseName = 'Nitro' + camelCaseName
}
await replaceTemplate(modulePath, `cxxNamespace`, cxxNamespace)
await replaceTemplate(modulePath, `androidNamespace`, cxxNamespace)
await replaceTemplate(modulePath, `androidCxxLibName`, camelCaseName)
await replaceTemplate(modulePath, `iosModuleName`, camelCaseName)
await replaceTemplate(modulePath, `packageName`, moduleName)
await replaceTemplate(modulePath, `packageDescription`, moduleName)
Logger.info(
`🎉 Created Nitro Module "${chalk.bold(moduleName)}" in ${chalk.underline(prettifyDirectory(directory))}!`
)
Logger.info(
`👉 To install dependencies, use your package manager of choice (e.g. ${chalk.bold('npm install')})`
)
Logger.info(
`👉 To create your first Hybrid Object, add a ${chalk.underline(`*.nitro.ts`)} file, declare a TypeScript interface that extends ${chalk.bold('HybridObject<...>')}, and run ${chalk.bold('npx nitro-codegen')}.`
)
Logger.info(
`👉 To test your module in an app, create a new React Native app somewhere (e.g. in ${chalk.underline(`./${moduleName}/example`)}) and add ${chalk.bold(moduleName)} as a local dependency.`
)
}
async function isGitInstalled(): Promise<boolean> {
try {
execSync('git --version')
return true
} catch (error) {
return false
}
}
async function downloadGitHubFolder(
owner: string,
repo: string,
branch: string,
folder: string,
outputPath: string
): Promise<string> {
if (!isGitInstalled()) {
console.error(
`❌ ${chalk.bold('git')} is not installed or available in the current path! Make sure to install ${chalk.bold('git')} and try again.`
)
process.exit(1)
}
const repoUrl = `https://github.com/${owner}/${repo}`
Logger.info(`⏳ Cloning ${chalk.underline(repoUrl)}...`)
const tempPath = randomUUID()
const tempDir = path.join(outputPath, tempPath)
execSync(
`git clone --depth 1 --filter=blob:none -b ${branch} --quiet --sparse ${repoUrl} ${tempDir}`
)
const prettyOutputPath = prettifyDirectory(outputPath)
const initialDir = process.cwd()
try {
process.chdir(tempDir)
Logger.debug(`⏳ Checking out ${chalk.underline(folder)}...`)
execSync(`git sparse-checkout set ${folder}`)
Logger.debug(
`📁 Copying files from ${chalk.underline(`${prettyOutputPath}/${tempPath}`)} to ${chalk.underline(prettyOutputPath)}...`
)
const sourcePath = path.join(tempDir, folder)
await fs.mkdir(outputPath, { recursive: true })
await copyFolder(sourcePath, outputPath)
return outputPath
} finally {
// change dir back to original
process.chdir(initialDir)
Logger.debug(
`🗑️ Removing temporary folder ${chalk.underline(prettifyDirectory(tempDir))}...`
)
await fs.rm(tempDir, { recursive: true, force: true })
}
}
async function copyFolder(src: string, dest: string) {
const entries = await fs.readdir(src, { withFileTypes: true })
for (const entry of entries) {
const srcPath = path.join(src, entry.name)
const destPath = path.join(dest, entry.name)
if (entry.isDirectory()) {
await fs.mkdir(destPath, { recursive: true })
await copyFolder(srcPath, destPath)
} else {
await fs.copyFile(srcPath, destPath)
}
}
}
async function replaceTemplate(
dir: string,
templateName: string,
replacementValue: string
): Promise<void> {
const replaceInFile = async (filePath: string) => {
const content = await fs.readFile(filePath, 'utf8')
const updatedContent = content.replaceAll(
`$$${templateName}$$`,
replacementValue
)
if (content !== updatedContent) {
await fs.writeFile(filePath, updatedContent, 'utf8')
}
}
const replaceInName = (name: string) => {
return name.replaceAll(`$$${templateName}$$`, replacementValue)
}
const processDirectory = async (currentDir: string) => {
const entries = await fs.readdir(currentDir, { withFileTypes: true })
for (const entry of entries) {
const oldPath = path.join(currentDir, entry.name)
const updatedName = replaceInName(entry.name)
const newPath = path.join(currentDir, updatedName)
if (oldPath !== newPath) {
await fs.rename(oldPath, newPath)
}
if (entry.isDirectory()) {
await processDirectory(newPath)
} else if (entry.isFile()) {
await replaceInFile(newPath)
}
}
}
if (!existsSync(dir)) {
throw new Error(`The directory "${dir}" does not exist.`)
}
await processDirectory(dir)
}