1"use strict"; 2var __importDefault = (this && this.__importDefault) || function (mod) { 3 return (mod && mod.__esModule) ? mod : { "default": mod }; 4}; 5Object.defineProperty(exports, "__esModule", { value: true }); 6const spawn_async_1 = __importDefault(require("@expo/spawn-async")); 7const chalk_1 = __importDefault(require("chalk")); 8const commander_1 = require("commander"); 9const download_tarball_1 = __importDefault(require("download-tarball")); 10const ejs_1 = __importDefault(require("ejs")); 11const find_up_1 = __importDefault(require("find-up")); 12const fs_extra_1 = __importDefault(require("fs-extra")); 13const getenv_1 = require("getenv"); 14const path_1 = __importDefault(require("path")); 15const prompts_1 = __importDefault(require("prompts")); 16const createExampleApp_1 = require("./createExampleApp"); 17const packageManager_1 = require("./packageManager"); 18const prompts_2 = require("./prompts"); 19const resolvePackageManager_1 = require("./resolvePackageManager"); 20const telemetry_1 = require("./telemetry"); 21const utils_1 = require("./utils"); 22const debug = require('debug')('create-expo-module:main'); 23const packageJson = require('../package.json'); 24// Opt in to using beta versions 25const EXPO_BETA = (0, getenv_1.boolish)('EXPO_BETA', false); 26// `yarn run` may change the current working dir, then we should use `INIT_CWD` env. 27const CWD = process.env.INIT_CWD || process.cwd(); 28// Ignore some paths. Especially `package.json` as it is rendered 29// from `$package.json` file instead of the original one. 30const IGNORES_PATHS = [ 31 '.DS_Store', 32 'build', 33 'node_modules', 34 'package.json', 35 '.npmignore', 36 '.gitignore', 37]; 38// Url to the documentation on Expo Modules 39const DOCS_URL = 'https://docs.expo.dev/modules'; 40const FYI_LOCAL_DIR = 'https://expo.fyi/expo-module-local-autolinking.md'; 41async function getCorrectLocalDirectory(targetOrSlug) { 42 const packageJsonPath = await (0, find_up_1.default)('package.json', { cwd: CWD }); 43 if (!packageJsonPath) { 44 console.log(chalk_1.default.red.bold('⚠️ This command should be run inside your Expo project when run with the --local flag.')); 45 console.log(chalk_1.default.red('For native modules to autolink correctly, you need to place them in the `modules` directory in the root of the project.')); 46 return null; 47 } 48 return path_1.default.join(packageJsonPath, '..', 'modules', targetOrSlug); 49} 50/** 51 * The main function of the command. 52 * 53 * @param target Path to the directory where to create the module. Defaults to current working dir. 54 * @param command An object from `commander`. 55 */ 56async function main(target, options) { 57 if (options.local) { 58 console.log(); 59 console.log(`${chalk_1.default.gray('The local module will be created in the ')}${chalk_1.default.gray.bold.italic('modules')} ${chalk_1.default.gray('directory in the root of your project. Learn more: ')}${chalk_1.default.gray.bold(FYI_LOCAL_DIR)}`); 60 console.log(); 61 } 62 const slug = await askForPackageSlugAsync(target, options.local); 63 const targetDir = options.local 64 ? await getCorrectLocalDirectory(target || slug) 65 : path_1.default.join(CWD, target || slug); 66 if (!targetDir) { 67 return; 68 } 69 await fs_extra_1.default.ensureDir(targetDir); 70 await confirmTargetDirAsync(targetDir); 71 options.target = targetDir; 72 const data = await askForSubstitutionDataAsync(slug, options.local); 73 // Make one line break between prompts and progress logs 74 console.log(); 75 const packageManager = await (0, resolvePackageManager_1.resolvePackageManager)(); 76 const packagePath = options.source 77 ? path_1.default.join(CWD, options.source) 78 : await downloadPackageAsync(targetDir, options.local); 79 (0, telemetry_1.logEventAsync)((0, telemetry_1.eventCreateExpoModule)(packageManager, options)); 80 await (0, utils_1.newStep)('Creating the module from template files', async (step) => { 81 await createModuleFromTemplate(packagePath, targetDir, data); 82 step.succeed('Created the module from template files'); 83 }); 84 if (!options.local) { 85 await (0, utils_1.newStep)('Installing module dependencies', async (step) => { 86 await (0, packageManager_1.installDependencies)(packageManager, targetDir); 87 step.succeed('Installed module dependencies'); 88 }); 89 await (0, utils_1.newStep)('Compiling TypeScript files', async (step) => { 90 await (0, spawn_async_1.default)(packageManager, ['run', 'build'], { 91 cwd: targetDir, 92 stdio: 'ignore', 93 }); 94 step.succeed('Compiled TypeScript files'); 95 }); 96 } 97 if (!options.source) { 98 // Files in the downloaded tarball are wrapped in `package` dir. 99 // We should remove it after all. 100 await fs_extra_1.default.remove(packagePath); 101 } 102 if (!options.local && data.type !== 'local') { 103 if (!options.withReadme) { 104 await fs_extra_1.default.remove(path_1.default.join(targetDir, 'README.md')); 105 } 106 if (!options.withChangelog) { 107 await fs_extra_1.default.remove(path_1.default.join(targetDir, 'CHANGELOG.md')); 108 } 109 if (options.example) { 110 // Create "example" folder 111 await (0, createExampleApp_1.createExampleApp)(data, targetDir, packageManager); 112 } 113 await (0, utils_1.newStep)('Creating an empty Git repository', async (step) => { 114 try { 115 const result = await createGitRepositoryAsync(targetDir); 116 if (result) { 117 step.succeed('Created an empty Git repository'); 118 } 119 else if (result === null) { 120 step.succeed('Skipped creating an empty Git repository, already within a Git repository'); 121 } 122 else if (result === false) { 123 step.warn('Could not create an empty Git repository, see debug logs with EXPO_DEBUG=true'); 124 } 125 } 126 catch (e) { 127 step.fail(e.toString()); 128 } 129 }); 130 } 131 console.log(); 132 if (options.local) { 133 console.log(`✅ Successfully created Expo module in ${chalk_1.default.bold.italic(`modules/${slug}`)}`); 134 printFurtherLocalInstructions(slug, data.project.moduleName); 135 } 136 else { 137 console.log('✅ Successfully created Expo module'); 138 printFurtherInstructions(targetDir, packageManager, options.example); 139 } 140} 141/** 142 * Recursively scans for the files within the directory. Returned paths are relative to the `root` path. 143 */ 144async function getFilesAsync(root, dir = null) { 145 const files = []; 146 const baseDir = dir ? path_1.default.join(root, dir) : root; 147 for (const file of await fs_extra_1.default.readdir(baseDir)) { 148 const relativePath = dir ? path_1.default.join(dir, file) : file; 149 if (IGNORES_PATHS.includes(relativePath) || IGNORES_PATHS.includes(file)) { 150 continue; 151 } 152 const fullPath = path_1.default.join(baseDir, file); 153 const stat = await fs_extra_1.default.lstat(fullPath); 154 if (stat.isDirectory()) { 155 files.push(...(await getFilesAsync(root, relativePath))); 156 } 157 else { 158 files.push(relativePath); 159 } 160 } 161 return files; 162} 163/** 164 * Asks NPM registry for the url to the tarball. 165 */ 166async function getNpmTarballUrl(packageName, version = 'latest') { 167 debug(`Using module template ${chalk_1.default.bold(packageName)}@${chalk_1.default.bold(version)}`); 168 const { stdout } = await (0, spawn_async_1.default)('npm', ['view', `${packageName}@${version}`, 'dist.tarball']); 169 return stdout.trim(); 170} 171/** 172 * Downloads the template from NPM registry. 173 */ 174async function downloadPackageAsync(targetDir, isLocal = false) { 175 return await (0, utils_1.newStep)('Downloading module template from npm', async (step) => { 176 const tarballUrl = await getNpmTarballUrl(isLocal ? 'expo-module-template-local' : 'expo-module-template', EXPO_BETA ? 'next' : 'latest'); 177 await (0, download_tarball_1.default)({ 178 url: tarballUrl, 179 dir: targetDir, 180 }); 181 step.succeed('Downloaded module template from npm'); 182 return path_1.default.join(targetDir, 'package'); 183 }); 184} 185function handleSuffix(name, suffix) { 186 if (name.endsWith(suffix)) { 187 return name; 188 } 189 return `${name}${suffix}`; 190} 191/** 192 * Creates the module based on the `ejs` template (e.g. `expo-module-template` package). 193 */ 194async function createModuleFromTemplate(templatePath, targetPath, data) { 195 const files = await getFilesAsync(templatePath); 196 // Iterate through all template files. 197 for (const file of files) { 198 const renderedRelativePath = ejs_1.default.render(file.replace(/^\$/, ''), data, { 199 openDelimiter: '{', 200 closeDelimiter: '}', 201 escape: (value) => value.replace(/\./g, path_1.default.sep), 202 }); 203 const fromPath = path_1.default.join(templatePath, file); 204 const toPath = path_1.default.join(targetPath, renderedRelativePath); 205 const template = await fs_extra_1.default.readFile(fromPath, { encoding: 'utf8' }); 206 const renderedContent = ejs_1.default.render(template, data); 207 await fs_extra_1.default.outputFile(toPath, renderedContent, { encoding: 'utf8' }); 208 } 209} 210async function createGitRepositoryAsync(targetDir) { 211 // Check if we are inside a git repository already 212 try { 213 await (0, spawn_async_1.default)('git', ['rev-parse', '--is-inside-work-tree'], { 214 stdio: 'ignore', 215 cwd: targetDir, 216 }); 217 debug(chalk_1.default.dim('New project is already inside of a Git repo, skipping git init.')); 218 return null; 219 } 220 catch (e) { 221 if (e.errno === 'ENOENT') { 222 debug(chalk_1.default.dim('Unable to initialize Git repo. `git` not in $PATH.')); 223 return false; 224 } 225 } 226 // Create a new git repository 227 await (0, spawn_async_1.default)('git', ['init'], { stdio: 'ignore', cwd: targetDir }); 228 await (0, spawn_async_1.default)('git', ['add', '-A'], { stdio: 'ignore', cwd: targetDir }); 229 const commitMsg = `Initial commit\n\nGenerated by ${packageJson.name} ${packageJson.version}.`; 230 await (0, spawn_async_1.default)('git', ['commit', '-m', commitMsg], { 231 stdio: 'ignore', 232 cwd: targetDir, 233 }); 234 debug(chalk_1.default.dim('Initialized a Git repository.')); 235 return true; 236} 237/** 238 * Asks the user for the package slug (npm package name). 239 */ 240async function askForPackageSlugAsync(customTargetPath, isLocal = false) { 241 const { slug } = await (0, prompts_1.default)((isLocal ? prompts_2.getLocalFolderNamePrompt : prompts_2.getSlugPrompt)(customTargetPath), { 242 onCancel: () => process.exit(0), 243 }); 244 return slug; 245} 246/** 247 * Asks the user for some data necessary to render the template. 248 * Some values may already be provided by command options, the prompt is skipped in that case. 249 */ 250async function askForSubstitutionDataAsync(slug, isLocal = false) { 251 const promptQueries = await (isLocal 252 ? prompts_2.getLocalSubstitutionDataPrompts 253 : prompts_2.getSubstitutionDataPrompts)(slug); 254 // Stop the process when the user cancels/exits the prompt. 255 const onCancel = () => { 256 process.exit(0); 257 }; 258 const { name, description, package: projectPackage, authorName, authorEmail, authorUrl, repo, } = await (0, prompts_1.default)(promptQueries, { onCancel }); 259 if (isLocal) { 260 return { 261 project: { 262 slug, 263 name, 264 package: projectPackage, 265 moduleName: handleSuffix(name, 'Module'), 266 viewName: handleSuffix(name, 'View'), 267 }, 268 type: 'local', 269 }; 270 } 271 return { 272 project: { 273 slug, 274 name, 275 version: '0.1.0', 276 description, 277 package: projectPackage, 278 moduleName: handleSuffix(name, 'Module'), 279 viewName: handleSuffix(name, 'View'), 280 }, 281 author: `${authorName} <${authorEmail}> (${authorUrl})`, 282 license: 'MIT', 283 repo, 284 type: 'remote', 285 }; 286} 287/** 288 * Checks whether the target directory is empty and if not, asks the user to confirm if he wants to continue. 289 */ 290async function confirmTargetDirAsync(targetDir) { 291 const files = await fs_extra_1.default.readdir(targetDir); 292 if (files.length === 0) { 293 return; 294 } 295 const { shouldContinue } = await (0, prompts_1.default)({ 296 type: 'confirm', 297 name: 'shouldContinue', 298 message: `The target directory ${chalk_1.default.magenta(targetDir)} is not empty, do you want to continue anyway?`, 299 initial: true, 300 }, { 301 onCancel: () => false, 302 }); 303 if (!shouldContinue) { 304 process.exit(0); 305 } 306} 307/** 308 * Prints how the user can follow up once the script finishes creating the module. 309 */ 310function printFurtherInstructions(targetDir, packageManager, includesExample) { 311 if (includesExample) { 312 const commands = [ 313 `cd ${path_1.default.relative(CWD, targetDir)}`, 314 (0, resolvePackageManager_1.formatRunCommand)(packageManager, 'open:ios'), 315 (0, resolvePackageManager_1.formatRunCommand)(packageManager, 'open:android'), 316 ]; 317 console.log(); 318 console.log('To start developing your module, navigate to the directory and open iOS and Android projects of the example app'); 319 commands.forEach((command) => console.log(chalk_1.default.gray('>'), chalk_1.default.bold(command))); 320 console.log(); 321 } 322 console.log(`Learn more on Expo Modules APIs: ${chalk_1.default.blue.bold(DOCS_URL)}`); 323} 324function printFurtherLocalInstructions(slug, name) { 325 console.log(); 326 console.log(`You can now import this module inside your application.`); 327 console.log(`For example, you can add this line to your App.js or App.tsx file:`); 328 console.log(`${chalk_1.default.gray.italic(`import { hello } from './modules/${slug}';`)}`); 329 console.log(); 330 console.log(`Learn more on Expo Modules APIs: ${chalk_1.default.blue.bold(DOCS_URL)}`); 331 console.log(chalk_1.default.yellow(`Remember you need to rebuild your development client or reinstall pods to see the changes.`)); 332} 333const program = new commander_1.Command(); 334program 335 .name(packageJson.name) 336 .version(packageJson.version) 337 .description(packageJson.description) 338 .arguments('[path]') 339 .option('-s, --source <source_dir>', 'Local path to the template. By default it downloads `expo-module-template` from NPM.') 340 .option('--with-readme', 'Whether to include README.md file.', false) 341 .option('--with-changelog', 'Whether to include CHANGELOG.md file.', false) 342 .option('--no-example', 'Whether to skip creating the example app.', false) 343 .option('--local', 'Whether to create a local module in the current project, skipping installing node_modules and creating the example directory.', false) 344 .action(main); 345program 346 .hook('postAction', async () => { 347 await (0, telemetry_1.getTelemetryClient)().flush?.(); 348}) 349 .parse(process.argv); 350//# sourceMappingURL=create-expo-module.js.map