UNPKG

create-expo-module

Version:

The script to create the Expo module

393 lines 17.1 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const spawn_async_1 = __importDefault(require("@expo/spawn-async")); const chalk_1 = __importDefault(require("chalk")); const commander_1 = require("commander"); const download_tarball_1 = __importDefault(require("download-tarball")); const ejs_1 = __importDefault(require("ejs")); const find_up_1 = __importDefault(require("find-up")); const fs_1 = __importDefault(require("fs")); const getenv_1 = require("getenv"); const path_1 = __importDefault(require("path")); const prompts_1 = __importDefault(require("prompts")); const createExampleApp_1 = require("./createExampleApp"); const packageManager_1 = require("./packageManager"); const prompts_2 = require("./prompts"); const resolvePackageManager_1 = require("./resolvePackageManager"); const telemetry_1 = require("./telemetry"); const ora_1 = require("./utils/ora"); const debug = require('debug')('create-expo-module:main'); const packageJson = require('../package.json'); // Opt in to using beta versions const EXPO_BETA = (0, getenv_1.boolish)('EXPO_BETA', false); // `yarn run` may change the current working dir, then we should use `INIT_CWD` env. const CWD = process.env.INIT_CWD || process.cwd(); // Ignore some paths. Especially `package.json` as it is rendered // from `$package.json` file instead of the original one. const IGNORES_PATHS = [ '.DS_Store', 'build', 'node_modules', 'package.json', '.npmignore', '.gitignore', ]; // Url to the documentation on Expo Modules const DOCS_URL = 'https://docs.expo.dev/modules'; const FYI_LOCAL_DIR = 'https://expo.fyi/expo-module-local-autolinking.md'; async function getCorrectLocalDirectory(targetOrSlug) { const packageJsonPath = await (0, find_up_1.default)('package.json', { cwd: CWD }); if (!packageJsonPath) { console.log(chalk_1.default.red.bold('⚠️ This command should be run inside your Expo project when run with the --local flag.')); 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.')); return null; } return path_1.default.join(packageJsonPath, '..', 'modules', targetOrSlug); } /** * The main function of the command. * * @param target Path to the directory where to create the module. Defaults to current working dir. * @param options An options object for `commander`. */ async function main(target, options) { if (options.local) { console.log(); 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)}`); console.log(); } const slug = await askForPackageSlugAsync(target, options.local); const targetDir = options.local ? await getCorrectLocalDirectory(target || slug) : path_1.default.join(CWD, target || slug); if (!targetDir) { return; } await fs_1.default.promises.mkdir(targetDir, { recursive: true }); await confirmTargetDirAsync(targetDir); options.target = targetDir; const data = await askForSubstitutionDataAsync(slug, options.local); // Make one line break between prompts and progress logs console.log(); const packageManager = (0, resolvePackageManager_1.resolvePackageManager)(); const packagePath = options.source ? path_1.default.join(CWD, options.source) : await downloadPackageAsync(targetDir, options.local); await (0, telemetry_1.logEventAsync)((0, telemetry_1.eventCreateExpoModule)(packageManager, options)); await (0, ora_1.newStep)('Creating the module from template files', async (step) => { await createModuleFromTemplate(packagePath, targetDir, data); step.succeed('Created the module from template files'); }); if (!options.local) { await (0, ora_1.newStep)('Installing module dependencies', async (step) => { await (0, packageManager_1.installDependencies)(packageManager, targetDir); step.succeed('Installed module dependencies'); }); await (0, ora_1.newStep)('Compiling TypeScript files', async (step) => { await (0, spawn_async_1.default)(packageManager, ['run', 'build'], { cwd: targetDir, stdio: 'ignore', }); step.succeed('Compiled TypeScript files'); }); } if (!options.source) { // Files in the downloaded tarball are wrapped in `package` dir. // We should remove it after all. await fs_1.default.promises.rm(packagePath, { recursive: true, force: true }); } if (!options.local && data.type !== 'local') { if (!options.withReadme) { await fs_1.default.promises.rm(path_1.default.join(targetDir, 'README.md'), { force: true }); } if (!options.withChangelog) { await fs_1.default.promises.rm(path_1.default.join(targetDir, 'CHANGELOG.md'), { force: true }); } if (options.example) { // Create "example" folder await (0, createExampleApp_1.createExampleApp)(data, targetDir, packageManager); } await (0, ora_1.newStep)('Creating an empty Git repository', async (step) => { try { const result = await createGitRepositoryAsync(targetDir); if (result) { step.succeed('Created an empty Git repository'); } else if (result === null) { step.succeed('Skipped creating an empty Git repository, already within a Git repository'); } else if (result === false) { step.warn('Could not create an empty Git repository, see debug logs with EXPO_DEBUG=true'); } } catch (error) { step.fail(error.toString()); } }); } console.log(); if (options.local) { console.log(`✅ Successfully created Expo module in ${chalk_1.default.bold.italic(`modules/${slug}`)}`); printFurtherLocalInstructions(slug, data.project.moduleName); } else { console.log('✅ Successfully created Expo module'); printFurtherInstructions(targetDir, packageManager, options.example); } } /** * Recursively scans for the files within the directory. Returned paths are relative to the `root` path. */ async function getFilesAsync(root, dir = null) { const files = []; const baseDir = dir ? path_1.default.join(root, dir) : root; for (const file of await fs_1.default.promises.readdir(baseDir)) { const relativePath = dir ? path_1.default.join(dir, file) : file; if (IGNORES_PATHS.includes(relativePath) || IGNORES_PATHS.includes(file)) { continue; } const fullPath = path_1.default.join(baseDir, file); const stat = await fs_1.default.promises.lstat(fullPath); if (stat.isDirectory()) { files.push(...(await getFilesAsync(root, relativePath))); } else { files.push(relativePath); } } return files; } /** * Asks NPM registry for the url to the tarball. */ async function getNpmTarballUrl(packageName, version = 'latest') { debug(`Using module template ${chalk_1.default.bold(packageName)}@${chalk_1.default.bold(version)}`); const { stdout } = await (0, spawn_async_1.default)('npm', ['view', `${packageName}@${version}`, 'dist.tarball']); return stdout.trim(); } /** * Gets expo SDK version major from the local package.json. */ async function getLocalSdkMajorVersion() { const path = require.resolve('expo/package.json', { paths: [process.cwd()] }); if (!path) { return null; } const { version } = require(path) ?? {}; return version?.split('.')[0] ?? null; } /** * Selects correct version of the template based on the SDK version for local modules and EXPO_BETA flag. */ async function getTemplateVersion(isLocal) { if (EXPO_BETA) { return 'next'; } if (!isLocal) { return 'latest'; } try { const sdkVersionMajor = await getLocalSdkMajorVersion(); return sdkVersionMajor ? `sdk-${sdkVersionMajor}` : 'latest'; } catch { console.log(); console.warn(chalk_1.default.yellow("Couldn't determine the SDK version from the local project, using `latest` as the template version.")); return 'latest'; } } /** * Downloads the template from NPM registry. */ async function downloadPackageAsync(targetDir, isLocal = false) { return await (0, ora_1.newStep)('Downloading module template from npm', async (step) => { const templateVersion = await getTemplateVersion(isLocal); const packageName = isLocal ? 'expo-module-template-local' : 'expo-module-template'; try { await (0, download_tarball_1.default)({ url: await getNpmTarballUrl(packageName, templateVersion), dir: targetDir, }); } catch { console.log(); console.warn(chalk_1.default.yellow("Couldn't download the versioned template from npm, falling back to the latest version.")); await (0, download_tarball_1.default)({ url: await getNpmTarballUrl(packageName, 'latest'), dir: targetDir, }); } step.succeed('Downloaded module template from npm registry.'); return path_1.default.join(targetDir, 'package'); }); } function handleSuffix(name, suffix) { if (name.endsWith(suffix)) { return name; } return `${name}${suffix}`; } /** * Creates the module based on the `ejs` template (e.g. `expo-module-template` package). */ async function createModuleFromTemplate(templatePath, targetPath, data) { const files = await getFilesAsync(templatePath); // Iterate through all template files. for (const file of files) { const renderedRelativePath = ejs_1.default.render(file.replace(/^\$/, ''), data, { openDelimiter: '{', closeDelimiter: '}', escape: (value) => value.replace(/\./g, path_1.default.sep), }); const fromPath = path_1.default.join(templatePath, file); const toPath = path_1.default.join(targetPath, renderedRelativePath); const template = await fs_1.default.promises.readFile(fromPath, 'utf8'); const renderedContent = ejs_1.default.render(template, data); if (!fs_1.default.existsSync(path_1.default.dirname(toPath))) { await fs_1.default.promises.mkdir(path_1.default.dirname(toPath), { recursive: true }); } await fs_1.default.promises.writeFile(toPath, renderedContent, 'utf8'); } } async function createGitRepositoryAsync(targetDir) { // Check if we are inside a git repository already try { await (0, spawn_async_1.default)('git', ['rev-parse', '--is-inside-work-tree'], { stdio: 'ignore', cwd: targetDir, }); debug(chalk_1.default.dim('New project is already inside of a Git repository, skipping `git init`.')); return null; } catch (e) { if (e.errno === 'ENOENT') { debug(chalk_1.default.dim('Unable to initialize Git repo. `git` not in $PATH.')); return false; } } // Create a new git repository await (0, spawn_async_1.default)('git', ['init'], { stdio: 'ignore', cwd: targetDir }); await (0, spawn_async_1.default)('git', ['add', '-A'], { stdio: 'ignore', cwd: targetDir }); const commitMsg = `Initial commit\n\nGenerated by ${packageJson.name} ${packageJson.version}.`; await (0, spawn_async_1.default)('git', ['commit', '-m', commitMsg], { stdio: 'ignore', cwd: targetDir, }); debug(chalk_1.default.dim('Initialized a Git repository.')); return true; } /** * Asks the user for the package slug (npm package name). */ async function askForPackageSlugAsync(customTargetPath, isLocal = false) { const { slug } = await (0, prompts_1.default)((isLocal ? prompts_2.getLocalFolderNamePrompt : prompts_2.getSlugPrompt)(customTargetPath), { onCancel: () => process.exit(0), }); return slug; } /** * Asks the user for some data necessary to render the template. * Some values may already be provided by command options, the prompt is skipped in that case. */ async function askForSubstitutionDataAsync(slug, isLocal = false) { const promptQueries = await (isLocal ? prompts_2.getLocalSubstitutionDataPrompts : prompts_2.getSubstitutionDataPrompts)(slug); // Stop the process when the user cancels/exits the prompt. const onCancel = () => { process.exit(0); }; const { name, description, package: projectPackage, authorName, authorEmail, authorUrl, repo, } = await (0, prompts_1.default)(promptQueries, { onCancel }); if (isLocal) { return { project: { slug, name, package: projectPackage, moduleName: handleSuffix(name, 'Module'), viewName: handleSuffix(name, 'View'), }, type: 'local', }; } return { project: { slug, name, version: '0.1.0', description, package: projectPackage, moduleName: handleSuffix(name, 'Module'), viewName: handleSuffix(name, 'View'), }, author: `${authorName} <${authorEmail}> (${authorUrl})`, license: 'MIT', repo, type: 'remote', }; } /** * Checks whether the target directory is empty and if not, asks the user to confirm if he wants to continue. */ async function confirmTargetDirAsync(targetDir) { const files = await fs_1.default.promises.readdir(targetDir); if (files.length === 0) { return; } const { shouldContinue } = await (0, prompts_1.default)({ type: 'confirm', name: 'shouldContinue', message: `The target directory ${chalk_1.default.magenta(targetDir)} is not empty, do you want to continue anyway?`, initial: true, }, { onCancel: () => false, }); if (!shouldContinue) { process.exit(0); } } /** * Prints how the user can follow up once the script finishes creating the module. */ function printFurtherInstructions(targetDir, packageManager, includesExample) { if (includesExample) { const commands = [ `cd ${path_1.default.relative(CWD, targetDir)}`, (0, resolvePackageManager_1.formatRunCommand)(packageManager, 'open:ios'), (0, resolvePackageManager_1.formatRunCommand)(packageManager, 'open:android'), ]; console.log(); console.log('To start developing your module, navigate to the directory and open Android and iOS projects of the example app'); commands.forEach((command) => console.log(chalk_1.default.gray('>'), chalk_1.default.bold(command))); console.log(); } console.log(`Learn more on Expo Modules APIs: ${chalk_1.default.blue.bold(DOCS_URL)}`); } function printFurtherLocalInstructions(slug, name) { console.log(); console.log(`You can now import this module inside your application.`); console.log(`For example, you can add this line to your App.tsx or App.js file:`); console.log(`${chalk_1.default.gray.italic(`import ${name} from './modules/${slug}';`)}`); console.log(); console.log(`Learn more on Expo Modules APIs: ${chalk_1.default.blue.bold(DOCS_URL)}`); console.log(chalk_1.default.yellow(`Remember to re-build your native app (for example, with ${chalk_1.default.bold('npx expo run')}) when you make changes to the module. Native code changes are not reloaded with Fast Refresh.`)); } const program = new commander_1.Command(); program .name(packageJson.name) .version(packageJson.version) .description(packageJson.description) .arguments('[path]') .option('-s, --source <source_dir>', 'Local path to the template. By default it downloads `expo-module-template` from NPM.') .option('--with-readme', 'Whether to include README.md file.', false) .option('--with-changelog', 'Whether to include CHANGELOG.md file.', false) .option('--no-example', 'Whether to skip creating the example app.', false) .option('--local', 'Whether to create a local module in the current project, skipping installing node_modules and creating the example directory.', false) .action(main); program .hook('postAction', async () => { await (0, telemetry_1.getTelemetryClient)().flush?.(); }) .parse(process.argv); //# sourceMappingURL=create-expo-module.js.map