create-expo-module 0.3.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,19 @@
1
+ import ora from 'ora';
2
+ export declare type StepOptions = ora.Options;
3
+ export declare function newStep<Result>(title: string, action: (step: ora.Ora) => Promise<Result> | Result, options?: StepOptions): Promise<Result>;
4
+ /**
5
+ * Finds user's name by reading it from the git config.
6
+ */
7
+ export declare function findMyName(): Promise<string>;
8
+ /**
9
+ * Finds user's email by reading it from the git config.
10
+ */
11
+ export declare function findGitHubEmail(): Promise<string>;
12
+ /**
13
+ * Get the GitHub username from an email address if the email can be found in any commits on GitHub.
14
+ */
15
+ export declare function findGitHubProfileUrl(email: string): Promise<string>;
16
+ /**
17
+ * Guesses the repository URL based on the author profile URL and the package slug.
18
+ */
19
+ export declare function guessRepoUrl(authorUrl: string, slug: string): Promise<string>;
package/build/utils.js ADDED
@@ -0,0 +1,79 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.guessRepoUrl = exports.findGitHubProfileUrl = exports.findGitHubEmail = exports.findMyName = exports.newStep = void 0;
7
+ const chalk_1 = __importDefault(require("chalk"));
8
+ const cross_spawn_1 = __importDefault(require("cross-spawn"));
9
+ const github_username_1 = __importDefault(require("github-username"));
10
+ const ora_1 = __importDefault(require("ora"));
11
+ async function newStep(title, action, options = {}) {
12
+ const disabled = process.env.CI || process.env.EXPO_DEBUG;
13
+ const step = (0, ora_1.default)({
14
+ text: chalk_1.default.bold(title),
15
+ isEnabled: !disabled,
16
+ stream: disabled ? process.stdout : process.stderr,
17
+ ...options,
18
+ });
19
+ step.start();
20
+ try {
21
+ return await action(step);
22
+ }
23
+ catch (error) {
24
+ step.fail();
25
+ console.error(error);
26
+ process.exit(1);
27
+ }
28
+ }
29
+ exports.newStep = newStep;
30
+ /**
31
+ * Finds user's name by reading it from the git config.
32
+ */
33
+ async function findMyName() {
34
+ try {
35
+ return cross_spawn_1.default.sync('git', ['config', '--get', 'user.name']).stdout.toString().trim();
36
+ }
37
+ catch {
38
+ return '';
39
+ }
40
+ }
41
+ exports.findMyName = findMyName;
42
+ /**
43
+ * Finds user's email by reading it from the git config.
44
+ */
45
+ async function findGitHubEmail() {
46
+ try {
47
+ return cross_spawn_1.default.sync('git', ['config', '--get', 'user.email']).stdout.toString().trim();
48
+ }
49
+ catch {
50
+ return '';
51
+ }
52
+ }
53
+ exports.findGitHubEmail = findGitHubEmail;
54
+ /**
55
+ * Get the GitHub username from an email address if the email can be found in any commits on GitHub.
56
+ */
57
+ async function findGitHubProfileUrl(email) {
58
+ var _a;
59
+ try {
60
+ const username = (_a = (await (0, github_username_1.default)(email))) !== null && _a !== void 0 ? _a : '';
61
+ return `https://github.com/${username}`;
62
+ }
63
+ catch {
64
+ return '';
65
+ }
66
+ }
67
+ exports.findGitHubProfileUrl = findGitHubProfileUrl;
68
+ /**
69
+ * Guesses the repository URL based on the author profile URL and the package slug.
70
+ */
71
+ async function guessRepoUrl(authorUrl, slug) {
72
+ if (/^https?:\/\/github.com\/[^/]+/.test(authorUrl)) {
73
+ const normalizedSlug = slug.replace(/^@/, '').replace(/\//g, '-');
74
+ return `${authorUrl}/${normalizedSlug}`;
75
+ }
76
+ return '';
77
+ }
78
+ exports.guessRepoUrl = guessRepoUrl;
79
+ //# sourceMappingURL=utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":";;;;;;AAAA,kDAA0B;AAC1B,8DAAgC;AAChC,sEAA6C;AAC7C,8CAAsB;AAIf,KAAK,UAAU,OAAO,CAC3B,KAAa,EACb,MAAmD,EACnD,UAAuB,EAAE;IAEzB,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,EAAE,IAAI,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC;IAC1D,MAAM,IAAI,GAAG,IAAA,aAAG,EAAC;QACf,IAAI,EAAE,eAAK,CAAC,IAAI,CAAC,KAAK,CAAC;QACvB,SAAS,EAAE,CAAC,QAAQ;QACpB,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM;QAClD,GAAG,OAAO;KACX,CAAC,CAAC;IAEH,IAAI,CAAC,KAAK,EAAE,CAAC;IAEb,IAAI;QACF,OAAO,MAAM,MAAM,CAAC,IAAI,CAAC,CAAC;KAC3B;IAAC,OAAO,KAAK,EAAE;QACd,IAAI,CAAC,IAAI,EAAE,CAAC;QACZ,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QACrB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;KACjB;AACH,CAAC;AAtBD,0BAsBC;AAED;;GAEG;AACI,KAAK,UAAU,UAAU;IAC9B,IAAI;QACF,OAAO,qBAAK,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,QAAQ,EAAE,OAAO,EAAE,WAAW,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,CAAC;KACrF;IAAC,MAAM;QACN,OAAO,EAAE,CAAC;KACX;AACH,CAAC;AAND,gCAMC;AAED;;GAEG;AACI,KAAK,UAAU,eAAe;IACnC,IAAI;QACF,OAAO,qBAAK,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,QAAQ,EAAE,OAAO,EAAE,YAAY,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,CAAC;KACtF;IAAC,MAAM;QACN,OAAO,EAAE,CAAC;KACX;AACH,CAAC;AAND,0CAMC;AAED;;GAEG;AACI,KAAK,UAAU,oBAAoB,CAAC,KAAa;;IACtD,IAAI;QACF,MAAM,QAAQ,GAAG,MAAA,CAAC,MAAM,IAAA,yBAAc,EAAC,KAAK,CAAC,CAAC,mCAAI,EAAE,CAAC;QACrD,OAAO,sBAAsB,QAAQ,EAAE,CAAC;KACzC;IAAC,MAAM;QACN,OAAO,EAAE,CAAC;KACX;AACH,CAAC;AAPD,oDAOC;AAED;;GAEG;AACI,KAAK,UAAU,YAAY,CAAC,SAAiB,EAAE,IAAY;IAChE,IAAI,+BAA+B,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE;QACnD,MAAM,cAAc,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;QAClE,OAAO,GAAG,SAAS,IAAI,cAAc,EAAE,CAAC;KACzC;IACD,OAAO,EAAE,CAAC;AACZ,CAAC;AAND,oCAMC","sourcesContent":["import chalk from 'chalk';\nimport spawn from 'cross-spawn';\nimport githubUsername from 'github-username';\nimport ora from 'ora';\n\nexport type StepOptions = ora.Options;\n\nexport async function newStep<Result>(\n title: string,\n action: (step: ora.Ora) => Promise<Result> | Result,\n options: StepOptions = {}\n): Promise<Result> {\n const disabled = process.env.CI || process.env.EXPO_DEBUG;\n const step = ora({\n text: chalk.bold(title),\n isEnabled: !disabled,\n stream: disabled ? process.stdout : process.stderr,\n ...options,\n });\n\n step.start();\n\n try {\n return await action(step);\n } catch (error) {\n step.fail();\n console.error(error);\n process.exit(1);\n }\n}\n\n/**\n * Finds user's name by reading it from the git config.\n */\nexport async function findMyName(): Promise<string> {\n try {\n return spawn.sync('git', ['config', '--get', 'user.name']).stdout.toString().trim();\n } catch {\n return '';\n }\n}\n\n/**\n * Finds user's email by reading it from the git config.\n */\nexport async function findGitHubEmail(): Promise<string> {\n try {\n return spawn.sync('git', ['config', '--get', 'user.email']).stdout.toString().trim();\n } catch {\n return '';\n }\n}\n\n/**\n * Get the GitHub username from an email address if the email can be found in any commits on GitHub.\n */\nexport async function findGitHubProfileUrl(email: string): Promise<string> {\n try {\n const username = (await githubUsername(email)) ?? '';\n return `https://github.com/${username}`;\n } catch {\n return '';\n }\n}\n\n/**\n * Guesses the repository URL based on the author profile URL and the package slug.\n */\nexport async function guessRepoUrl(authorUrl: string, slug: string) {\n if (/^https?:\\/\\/github.com\\/[^/]+/.test(authorUrl)) {\n const normalizedSlug = slug.replace(/^@/, '').replace(/\\//g, '-');\n return `${authorUrl}/${normalizedSlug}`;\n }\n return '';\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-expo-module",
3
- "version": "0.3.1",
3
+ "version": "0.5.0",
4
4
  "description": "The script to create the Expo module",
5
5
  "main": "build/create-expo-module.js",
6
6
  "types": "build/create-expo-module.d.ts",
@@ -36,9 +36,12 @@
36
36
  "@expo/spawn-async": "^1.5.0",
37
37
  "chalk": "^4.1.2",
38
38
  "commander": "^8.3.0",
39
+ "cross-spawn": "^7.0.3",
39
40
  "download-tarball": "^2.0.0",
40
41
  "ejs": "^3.1.7",
41
42
  "fs-extra": "^10.0.0",
43
+ "github-username": "^6.0.0",
44
+ "ora": "^5.4.1",
42
45
  "prompts": "^2.4.2",
43
46
  "validate-npm-package-name": "^4.0.0"
44
47
  },
@@ -47,5 +50,5 @@
47
50
  "@types/prompts": "^2.0.14",
48
51
  "expo-module-scripts": "^2.0.0"
49
52
  },
50
- "gitHead": "093d340c3fd67ff4375af3a471c7d044aee893c9"
53
+ "gitHead": "1ba5fa454b86aef6f32c0f4ce0f34e4ea98e969f"
51
54
  }
@@ -6,12 +6,13 @@ import ejs from 'ejs';
6
6
  import fs from 'fs-extra';
7
7
  import path from 'path';
8
8
  import prompts from 'prompts';
9
- import validateNpmPackage from 'validate-npm-package-name';
10
9
 
11
10
  import { createExampleApp } from './createExampleApp';
12
11
  import { installDependencies } from './packageManager';
13
- import { PackageManagerName, resolvePackageManager } from './resolvePackageManager';
14
- import { CommandOptions, CustomPromptObject, SubstitutionData } from './types';
12
+ import { getSlugPrompt, getSubstitutionDataPrompts } from './prompts';
13
+ import { resolvePackageManager } from './resolvePackageManager';
14
+ import { CommandOptions, SubstitutionData } from './types';
15
+ import { newStep } from './utils';
15
16
 
16
17
  const packageJson = require('../package.json');
17
18
 
@@ -29,39 +30,41 @@ const IGNORES_PATHS = ['.DS_Store', 'build', 'node_modules', 'package.json'];
29
30
  * @param command An object from `commander`.
30
31
  */
31
32
  async function main(target: string | undefined, options: CommandOptions) {
32
- const targetDir = target ? path.join(CWD, target) : CWD;
33
+ const slug = await askForPackageSlugAsync(target);
34
+ const targetDir = path.join(CWD, target || slug);
33
35
 
34
36
  await fs.ensureDir(targetDir);
35
37
  await confirmTargetDirAsync(targetDir);
36
38
 
37
39
  options.target = targetDir;
38
40
 
39
- const data = await askForSubstitutionDataAsync(targetDir, options);
41
+ const data = await askForSubstitutionDataAsync(slug);
42
+
43
+ // Make one line break between prompts and progress logs
44
+ console.log();
45
+
40
46
  const packageManager = await resolvePackageManager();
41
47
  const packagePath = options.source
42
48
  ? path.join(CWD, options.source)
43
49
  : await downloadPackageAsync(targetDir);
44
- const files = await getFilesAsync(packagePath);
45
50
 
46
- console.log('🎨 Creating Expo module from the template files...');
47
-
48
- // Iterate through all template files.
49
- for (const file of files) {
50
- const renderedRelativePath = ejs.render(file.replace(/^\$/, ''), data, {
51
- openDelimiter: '{',
52
- closeDelimiter: '}',
53
- escape: (value: string) => value.replace('.', path.sep),
54
- });
55
- const fromPath = path.join(packagePath, file);
56
- const toPath = path.join(targetDir, renderedRelativePath);
57
- const template = await fs.readFile(fromPath, { encoding: 'utf8' });
58
- const renderedContent = ejs.render(template, data);
51
+ await newStep('Creating the module from template files', async (step) => {
52
+ await createModuleFromTemplate(packagePath, targetDir, data);
53
+ step.succeed('Created the module from template files');
54
+ });
59
55
 
60
- await fs.outputFile(toPath, renderedContent, { encoding: 'utf8' });
61
- }
56
+ await newStep('Installing module dependencies', async (step) => {
57
+ await installDependencies(packageManager, targetDir);
58
+ step.succeed('Installed module dependencies');
59
+ });
62
60
 
63
- // Install dependencies and build
64
- await postActionsAsync(packageManager, targetDir);
61
+ await newStep('Compiling TypeScript files', async (step) => {
62
+ await spawnAsync(packageManager, ['run', 'build'], {
63
+ cwd: targetDir,
64
+ stdio: 'ignore',
65
+ });
66
+ step.succeed('Compiled TypeScript files');
67
+ });
65
68
 
66
69
  if (!options.source) {
67
70
  // Files in the downloaded tarball are wrapped in `package` dir.
@@ -79,6 +82,7 @@ async function main(target: string | undefined, options: CommandOptions) {
79
82
  await createExampleApp(data, targetDir, packageManager);
80
83
  }
81
84
 
85
+ console.log();
82
86
  console.log('✅ Successfully created Expo module');
83
87
  }
84
88
 
@@ -117,120 +121,80 @@ async function getNpmTarballUrl(packageName: string, version: string = 'latest')
117
121
  }
118
122
 
119
123
  /**
120
- * Gets the username of currently logged in user. Used as a default in the prompt asking for the module author.
124
+ * Downloads the template from NPM registry.
121
125
  */
122
- async function npmWhoamiAsync(targetDir: string): Promise<string | null> {
123
- try {
124
- const { stdout } = await spawnAsync('npm', ['whoami'], { cwd: targetDir });
125
- return stdout.trim();
126
- } catch {
127
- return null;
128
- }
126
+ async function downloadPackageAsync(targetDir: string): Promise<string> {
127
+ return await newStep('Downloading module template from npm', async (step) => {
128
+ const tarballUrl = await getNpmTarballUrl('expo-module-template');
129
+
130
+ await downloadTarball({
131
+ url: tarballUrl,
132
+ dir: targetDir,
133
+ });
134
+
135
+ step.succeed('Downloaded module template from npm');
136
+
137
+ return path.join(targetDir, 'package');
138
+ });
129
139
  }
130
140
 
131
141
  /**
132
- * Downloads the template from NPM registry.
142
+ * Creates the module based on the `ejs` template (e.g. `expo-module-template` package).
133
143
  */
134
- async function downloadPackageAsync(targetDir: string): Promise<string> {
135
- const tarballUrl = await getNpmTarballUrl('expo-module-template');
144
+ async function createModuleFromTemplate(
145
+ templatePath: string,
146
+ targetPath: string,
147
+ data: SubstitutionData
148
+ ) {
149
+ const files = await getFilesAsync(templatePath);
136
150
 
137
- console.log('⬇️ Downloading module template from npm...');
151
+ // Iterate through all template files.
152
+ for (const file of files) {
153
+ const renderedRelativePath = ejs.render(file.replace(/^\$/, ''), data, {
154
+ openDelimiter: '{',
155
+ closeDelimiter: '}',
156
+ escape: (value: string) => value.replace('.', path.sep),
157
+ });
158
+ const fromPath = path.join(templatePath, file);
159
+ const toPath = path.join(targetPath, renderedRelativePath);
160
+ const template = await fs.readFile(fromPath, { encoding: 'utf8' });
161
+ const renderedContent = ejs.render(template, data);
138
162
 
139
- await downloadTarball({
140
- url: tarballUrl,
141
- dir: targetDir,
142
- });
143
- return path.join(targetDir, 'package');
163
+ await fs.outputFile(toPath, renderedContent, { encoding: 'utf8' });
164
+ }
144
165
  }
145
166
 
146
167
  /**
147
- * Installs dependencies and builds TypeScript files.
168
+ * Asks the user for the package slug (npm package name).
148
169
  */
149
- async function postActionsAsync(packageManager: PackageManagerName, targetDir: string) {
150
- console.log('📦 Installing module dependencies...');
151
- await installDependencies(packageManager, targetDir);
152
-
153
- console.log('🛠 Compiling TypeScript files...');
154
- await spawnAsync(packageManager, ['run', 'build'], {
155
- cwd: targetDir,
156
- stdio: 'ignore',
170
+ async function askForPackageSlugAsync(customTargetPath?: string): Promise<string> {
171
+ const { slug } = await prompts(getSlugPrompt(customTargetPath), {
172
+ onCancel: () => process.exit(0),
157
173
  });
174
+ return slug;
158
175
  }
159
176
 
160
177
  /**
161
178
  * Asks the user for some data necessary to render the template.
162
179
  * Some values may already be provided by command options, the prompt is skipped in that case.
163
180
  */
164
- async function askForSubstitutionDataAsync(
165
- targetDir: string,
166
- options: CommandOptions
167
- ): Promise<SubstitutionData> {
168
- const defaultPackageSlug = path.basename(targetDir);
169
- const useDefaultSlug = options.target && validateNpmPackage(defaultPackageSlug);
170
- const defaultProjectName = defaultPackageSlug
171
- .replace(/^./, (match) => match.toUpperCase())
172
- .replace(/\W+(\w)/g, (_, p1) => p1.toUpperCase());
173
-
174
- const promptQueries: CustomPromptObject[] = [
175
- {
176
- type: 'text',
177
- name: 'slug',
178
- message: 'What is the package slug?',
179
- initial: defaultPackageSlug,
180
- resolvedValue: useDefaultSlug ? defaultPackageSlug : null,
181
- validate: (input) =>
182
- validateNpmPackage(input).validForNewPackages || 'Must be a valid npm package name',
183
- },
184
- {
185
- type: 'text',
186
- name: 'name',
187
- message: 'What is the project name?',
188
- initial: defaultProjectName,
189
- },
190
- {
191
- type: 'text',
192
- name: 'description',
193
- message: 'How would you describe the module?',
194
- validate: (input) => !!input || 'Cannot be empty',
195
- },
196
- {
197
- type: 'text',
198
- name: 'package',
199
- message: 'What is the Android package name?',
200
- initial: `expo.modules.${defaultPackageSlug.replace(/\W/g, '').toLowerCase()}`,
201
- },
202
- {
203
- type: 'text',
204
- name: 'author',
205
- message: 'Who is the author?',
206
- initial: (await npmWhoamiAsync(targetDir)) ?? '',
207
- },
208
- {
209
- type: 'text',
210
- name: 'license',
211
- message: 'What is the license?',
212
- initial: 'MIT',
213
- },
214
- {
215
- type: 'text',
216
- name: 'repo',
217
- message: 'What is the repository URL?',
218
- validate: (input) => /^https?:\/\//.test(input) || 'Must be a valid URL',
219
- },
220
- ];
181
+ async function askForSubstitutionDataAsync(slug: string): Promise<SubstitutionData> {
182
+ const promptQueries = await getSubstitutionDataPrompts(slug);
221
183
 
222
184
  // Stop the process when the user cancels/exits the prompt.
223
185
  const onCancel = () => {
224
186
  process.exit(0);
225
187
  };
226
188
 
227
- const answers: Record<string, string> = {};
228
- for (const query of promptQueries) {
229
- const { name, resolvedValue } = query;
230
- answers[name] = resolvedValue ?? options[name] ?? (await prompts(query, { onCancel }))[name];
231
- }
232
-
233
- const { slug, name, description, package: projectPackage, author, license, repo } = answers;
189
+ const {
190
+ name,
191
+ description,
192
+ package: projectPackage,
193
+ authorName,
194
+ authorEmail,
195
+ authorUrl,
196
+ repo,
197
+ } = await prompts(promptQueries, { onCancel });
234
198
 
235
199
  return {
236
200
  project: {
@@ -240,8 +204,8 @@ async function askForSubstitutionDataAsync(
240
204
  description,
241
205
  package: projectPackage,
242
206
  },
243
- author,
244
- license,
207
+ author: `${authorName} <${authorEmail}> (${authorUrl})`,
208
+ license: 'MIT',
245
209
  repo,
246
210
  };
247
211
  }
@@ -261,7 +225,7 @@ async function confirmTargetDirAsync(targetDir: string): Promise<void> {
261
225
  name: 'shouldContinue',
262
226
  message: `The target directory ${chalk.magenta(
263
227
  targetDir
264
- )} is not empty.\nDo you want to continue anyway?`,
228
+ )} is not empty, do you want to continue anyway?`,
265
229
  initial: true,
266
230
  },
267
231
  {
@@ -279,17 +243,11 @@ program
279
243
  .name(packageJson.name)
280
244
  .version(packageJson.version)
281
245
  .description(packageJson.description)
282
- .arguments('[target_dir]')
246
+ .arguments('[path]')
283
247
  .option(
284
248
  '-s, --source <source_dir>',
285
249
  'Local path to the template. By default it downloads `expo-module-template` from NPM.'
286
250
  )
287
- .option('-n, --name <module_name>', 'Name of the native module.')
288
- .option('-d, --description <description>', 'Description of the module.')
289
- .option('-p, --package <package>', 'The Android package name.')
290
- .option('-a, --author <author>', 'The author name.')
291
- .option('-l, --license <license>', 'The license that the module is distributed with.')
292
- .option('-r, --repo <repo_url>', 'The URL to the repository.')
293
251
  .option('--with-readme', 'Whether to include README.md file.', false)
294
252
  .option('--with-changelog', 'Whether to include CHANGELOG.md file.', false)
295
253
  .option('--no-example', 'Whether to skip creating the example app.', false)
@@ -5,6 +5,7 @@ import path from 'path';
5
5
  import { installDependencies } from './packageManager';
6
6
  import { PackageManagerName } from './resolvePackageManager';
7
7
  import { SubstitutionData } from './types';
8
+ import { newStep } from './utils';
8
9
 
9
10
  // These dependencies will be removed from the example app (`expo init` adds them)
10
11
  const DEPENDENCIES_TO_REMOVE = ['expo-status-bar', 'expo-splash-screen'];
@@ -17,49 +18,57 @@ export async function createExampleApp(
17
18
  targetDir: string,
18
19
  packageManager: PackageManagerName
19
20
  ): Promise<void> {
20
- console.log('🎭 Creating the example app...');
21
-
21
+ // Package name for the example app
22
22
  const exampleProjectSlug = `${data.project.slug}-example`;
23
23
 
24
- await spawnAsync(
25
- 'expo',
26
- ['init', exampleProjectSlug, '--template', 'expo-template-blank-typescript'],
27
- {
28
- cwd: targetDir,
29
- stdio: ['ignore', 'ignore', 'inherit'],
30
- }
31
- );
32
-
33
24
  // `expo init` creates a new folder with the same name as the project slug
34
25
  const appTmpPath = path.join(targetDir, exampleProjectSlug);
35
26
 
36
27
  // Path to the target example dir
37
28
  const appTargetPath = path.join(targetDir, 'example');
38
29
 
39
- console.log('🛠 Configuring the example app...');
30
+ if (!(await fs.pathExists(appTargetPath))) {
31
+ // The template doesn't include the example app, so just skip this phase
32
+ return;
33
+ }
34
+
35
+ await newStep('Initializing the example app', async (step) => {
36
+ await spawnAsync(
37
+ packageManager,
38
+ ['create', 'expo-app', exampleProjectSlug, '--template', 'blank-typescript'],
39
+ {
40
+ cwd: targetDir,
41
+ stdio: 'ignore',
42
+ }
43
+ );
44
+ step.succeed('Initialized the example app');
45
+ });
46
+
47
+ await newStep('Configuring the example app', async (step) => {
48
+ // "example" folder already exists and contains template files,
49
+ // that should replace these created by `expo init`.
50
+ await moveFiles(appTargetPath, appTmpPath);
40
51
 
41
- // "example" folder already exists and contains template files,
42
- // that should replace these created by `expo init`.
43
- await moveFiles(appTargetPath, appTmpPath);
52
+ // Cleanup the "example" dir
53
+ await fs.rmdir(appTargetPath);
44
54
 
45
- // Cleanup the "example" dir
46
- await fs.rmdir(appTargetPath);
55
+ // Move the temporary example app to "example" dir
56
+ await fs.rename(appTmpPath, appTargetPath);
47
57
 
48
- // Move the temporary example app to "example" dir
49
- await fs.rename(appTmpPath, appTargetPath);
58
+ await addMissingAppConfigFields(appTargetPath, data);
50
59
 
51
- await addMissingAppConfigFields(appTargetPath, data);
60
+ step.succeed('Configured the example app');
61
+ });
52
62
 
53
- console.log('👷 Prebuilding the example app...');
54
63
  await prebuildExampleApp(appTargetPath);
55
64
 
56
65
  await modifyPackageJson(appTargetPath);
57
66
 
58
- console.log('📦 Installing dependencies in the example app...');
59
- await installDependencies(packageManager, appTargetPath);
60
-
61
- console.log('🥥 Installing iOS pods in the example app...');
62
- await podInstall(appTargetPath);
67
+ await newStep('Installing dependencies in the example app', async (step) => {
68
+ await installDependencies(packageManager, appTargetPath);
69
+ await podInstall(appTargetPath);
70
+ step.succeed('Installed dependencies in the example app');
71
+ });
63
72
  }
64
73
 
65
74
  /**
@@ -130,28 +139,21 @@ async function modifyPackageJson(appPath: string): Promise<void> {
130
139
  * Runs `expo prebuild` in the example app.
131
140
  */
132
141
  async function prebuildExampleApp(exampleAppPath: string): Promise<void> {
133
- try {
142
+ await newStep('Prebuilding the example app', async (step) => {
134
143
  await spawnAsync('expo', ['prebuild', '--no-install'], {
135
144
  cwd: exampleAppPath,
136
145
  stdio: ['ignore', 'ignore', 'pipe'],
137
146
  });
138
- } catch (error: any) {
139
- console.error(error.stderr);
140
- process.exit(1);
141
- }
147
+ step.succeed('Prebuilt the example app');
148
+ });
142
149
  }
143
150
 
144
151
  /**
145
152
  * Runs `pod install` in the iOS project at the given path.
146
153
  */
147
154
  async function podInstall(appPath: string): Promise<void> {
148
- try {
149
- await spawnAsync('pod', ['install'], {
150
- cwd: path.join(appPath, 'ios'),
151
- stdio: ['ignore', 'ignore', 'pipe'],
152
- });
153
- } catch (error: any) {
154
- console.error(error.stderr);
155
- process.exit(1);
156
- }
155
+ await spawnAsync('pod', ['install'], {
156
+ cwd: path.join(appPath, 'ios'),
157
+ stdio: ['ignore', 'ignore', 'pipe'],
158
+ });
157
159
  }
package/src/prompts.ts ADDED
@@ -0,0 +1,86 @@
1
+ import path from 'path';
2
+ import { Answers, PromptObject } from 'prompts';
3
+ import validateNpmPackage from 'validate-npm-package-name';
4
+
5
+ import { findGitHubEmail, findGitHubProfileUrl, findMyName, guessRepoUrl } from './utils';
6
+
7
+ export function getSlugPrompt(customTargetPath?: string | null): PromptObject<string> {
8
+ const targetBasename = customTargetPath && path.basename(customTargetPath);
9
+ const initial =
10
+ targetBasename && validateNpmPackage(targetBasename).validForNewPackages
11
+ ? targetBasename
12
+ : 'my-module';
13
+
14
+ return {
15
+ type: 'text',
16
+ name: 'slug',
17
+ message: 'What is the name of the npm package?',
18
+ initial,
19
+ validate: (input) =>
20
+ validateNpmPackage(input).validForNewPackages || 'Must be a valid npm package name',
21
+ };
22
+ }
23
+
24
+ export async function getSubstitutionDataPrompts(slug: string): Promise<PromptObject<string>[]> {
25
+ return [
26
+ {
27
+ type: 'text',
28
+ name: 'name',
29
+ message: 'What is the native module name?',
30
+ initial: () => {
31
+ return slug
32
+ .replace(/^@/, '')
33
+ .replace(/^./, (match) => match.toUpperCase())
34
+ .replace(/\W+(\w)/g, (_, p1) => p1.toUpperCase());
35
+ },
36
+ validate: (input) => !!input || 'The native module name cannot be empty',
37
+ },
38
+ {
39
+ type: 'text',
40
+ name: 'description',
41
+ message: 'How would you describe the module?',
42
+ initial: 'My new module',
43
+ validate: (input) => !!input || 'The description cannot be empty',
44
+ },
45
+ {
46
+ type: 'text',
47
+ name: 'package',
48
+ message: 'What is the Android package name?',
49
+ initial: () => {
50
+ const namespace = slug
51
+ .replace(/\W/g, '')
52
+ .replace(/^(expo|reactnative)/, '')
53
+ .toLowerCase();
54
+ return `expo.modules.${namespace}`;
55
+ },
56
+ validate: (input) => !!input || 'The Android package name cannot be empty',
57
+ },
58
+ {
59
+ type: 'text',
60
+ name: 'authorName',
61
+ message: 'What is the name of the package author?',
62
+ initial: await findMyName(),
63
+ validate: (input) => !!input || 'Cannot be empty',
64
+ },
65
+ {
66
+ type: 'text',
67
+ name: 'authorEmail',
68
+ message: 'What is the email address of the author?',
69
+ initial: await findGitHubEmail(),
70
+ },
71
+ {
72
+ type: 'text',
73
+ name: 'authorUrl',
74
+ message: "What is the URL to the author's GitHub profile?",
75
+ initial: async (_, answers: Answers<string>) =>
76
+ await findGitHubProfileUrl(answers.authorEmail),
77
+ },
78
+ {
79
+ type: 'text',
80
+ name: 'repo',
81
+ message: 'What is the URL for the repository?',
82
+ initial: async (_, answers: Answers<string>) => await guessRepoUrl(answers.authorUrl, slug),
83
+ validate: (input) => /^https?:\/\//.test(input) || 'Must be a valid URL',
84
+ },
85
+ ];
86
+ }
package/src/types.ts CHANGED
@@ -6,12 +6,6 @@ import { PromptObject } from 'prompts';
6
6
  export type CommandOptions = {
7
7
  target: string;
8
8
  source?: string;
9
- name?: string;
10
- description?: string;
11
- package?: string;
12
- author?: string;
13
- license?: string;
14
- repo?: string;
15
9
  withReadme: boolean;
16
10
  withChangelog: boolean;
17
11
  example: boolean;
@@ -37,3 +31,5 @@ export type CustomPromptObject = PromptObject & {
37
31
  name: string;
38
32
  resolvedValue?: string | null;
39
33
  };
34
+
35
+ export type Answers = Record<string, string>;