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