create-expo-module 0.3.0 → 0.4.1

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.0",
3
+ "version": "0.4.1",
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,15 +36,19 @@
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",
42
- "prompts": "^2.4.2"
43
+ "github-username": "^6.0.0",
44
+ "ora": "^5.4.1",
45
+ "prompts": "^2.4.2",
46
+ "validate-npm-package-name": "^4.0.0"
43
47
  },
44
48
  "devDependencies": {
45
49
  "@types/ejs": "^3.1.0",
46
50
  "@types/prompts": "^2.0.14",
47
51
  "expo-module-scripts": "^2.0.0"
48
52
  },
49
- "gitHead": "5e7c69008fe51f86e96877116602ef1a50a4655f"
53
+ "gitHead": "066000e04629874e1c67f2b3b9a38af805d9831c"
50
54
  }
@@ -5,9 +5,14 @@ import downloadTarball from 'download-tarball';
5
5
  import ejs from 'ejs';
6
6
  import fs from 'fs-extra';
7
7
  import path from 'path';
8
- import prompts, { PromptObject } from 'prompts';
8
+ import prompts from 'prompts';
9
9
 
10
- import { PackageManagerName, resolvePackageManager } from './resolvePackageManager';
10
+ import { createExampleApp } from './createExampleApp';
11
+ import { installDependencies } from './packageManager';
12
+ import getPrompts from './prompts';
13
+ import { resolvePackageManager } from './resolvePackageManager';
14
+ import { CommandOptions, SubstitutionData } from './types';
15
+ import { newStep } from './utils';
11
16
 
12
17
  const packageJson = require('../package.json');
13
18
 
@@ -18,43 +23,6 @@ const CWD = process.env.INIT_CWD || process.cwd();
18
23
  // from `$package.json` file instead of the original one.
19
24
  const IGNORES_PATHS = ['.DS_Store', 'build', 'node_modules', 'package.json'];
20
25
 
21
- /**
22
- * Possible command options.
23
- */
24
- type CommandOptions = {
25
- target: string;
26
- source?: string;
27
- name?: string;
28
- description?: string;
29
- package?: string;
30
- author?: string;
31
- license?: string;
32
- repo?: string;
33
- withReadme: boolean;
34
- withChangelog: boolean;
35
- };
36
-
37
- /**
38
- * Represents an object that is passed to `ejs` when rendering the template.
39
- */
40
- type SubstitutionData = {
41
- project: {
42
- slug: string;
43
- name: string;
44
- version: string;
45
- description: string;
46
- package: string;
47
- };
48
- author: string;
49
- license: string;
50
- repo: string;
51
- };
52
-
53
- type CustomPromptObject = PromptObject & {
54
- name: string;
55
- resolvedValue?: string | null;
56
- };
57
-
58
26
  /**
59
27
  * The main function of the command.
60
28
  *
@@ -64,34 +32,38 @@ type CustomPromptObject = PromptObject & {
64
32
  async function main(target: string | undefined, options: CommandOptions) {
65
33
  const targetDir = target ? path.join(CWD, target) : CWD;
66
34
 
35
+ await fs.ensureDir(targetDir);
67
36
  await confirmTargetDirAsync(targetDir);
68
37
 
69
38
  options.target = targetDir;
70
- await fs.ensureDir(targetDir);
71
39
 
72
- const data = await askForSubstitutionDataAsync(targetDir, options);
40
+ const data = await askForSubstitutionDataAsync(targetDir);
41
+
42
+ // Make one line break between prompts and progress logs
43
+ console.log();
44
+
73
45
  const packageManager = await resolvePackageManager();
74
46
  const packagePath = options.source
75
47
  ? path.join(CWD, options.source)
76
48
  : await downloadPackageAsync(targetDir);
77
- const files = await getFilesAsync(packagePath);
78
49
 
79
- console.log('🎨 Creating Expo module from the template files...');
50
+ await newStep('Creating the module from template files', async (step) => {
51
+ await createModuleFromTemplate(packagePath, targetDir, data);
52
+ step.succeed('Created the module from template files');
53
+ });
80
54
 
81
- // Iterate through all template files.
82
- for (const file of files) {
83
- const renderedRelativePath = ejs.render(file.replace(/^\$/, ''), data, {
84
- openDelimiter: '{',
85
- closeDelimiter: '}',
86
- escape: (value: string) => value.replace('.', path.sep),
87
- });
88
- const fromPath = path.join(packagePath, file);
89
- const toPath = path.join(targetDir, renderedRelativePath);
90
- const template = await fs.readFile(fromPath, { encoding: 'utf8' });
91
- const renderedContent = ejs.render(template, data);
55
+ await newStep('Installing module dependencies', async (step) => {
56
+ await installDependencies(packageManager, targetDir);
57
+ step.succeed('Installed module dependencies');
58
+ });
92
59
 
93
- await fs.outputFile(toPath, renderedContent, { encoding: 'utf8' });
94
- }
60
+ await newStep('Compiling TypeScript files', async (step) => {
61
+ await spawnAsync(packageManager, ['run', 'build'], {
62
+ cwd: targetDir,
63
+ stdio: 'ignore',
64
+ });
65
+ step.succeed('Compiled TypeScript files');
66
+ });
95
67
 
96
68
  if (!options.source) {
97
69
  // Files in the downloaded tarball are wrapped in `package` dir.
@@ -104,10 +76,12 @@ async function main(target: string | undefined, options: CommandOptions) {
104
76
  if (!options.withChangelog) {
105
77
  await fs.remove(path.join(targetDir, 'CHANGELOG.md'));
106
78
  }
79
+ if (options.example) {
80
+ // Create "example" folder
81
+ await createExampleApp(data, targetDir, packageManager);
82
+ }
107
83
 
108
- // Install dependencies and build
109
- await postActionsAsync(packageManager, targetDir);
110
-
84
+ console.log();
111
85
  console.log('✅ Successfully created Expo module');
112
86
  }
113
87
 
@@ -145,120 +119,72 @@ async function getNpmTarballUrl(packageName: string, version: string = 'latest')
145
119
  return stdout.trim();
146
120
  }
147
121
 
148
- /**
149
- * Gets the username of currently logged in user. Used as a default in the prompt asking for the module author.
150
- */
151
- async function npmWhoamiAsync(targetDir: string): Promise<string | null> {
152
- try {
153
- const { stdout } = await spawnAsync('npm', ['whoami'], { cwd: targetDir });
154
- return stdout.trim();
155
- } catch {
156
- return null;
157
- }
158
- }
159
-
160
122
  /**
161
123
  * Downloads the template from NPM registry.
162
124
  */
163
125
  async function downloadPackageAsync(targetDir: string): Promise<string> {
164
- const tarballUrl = await getNpmTarballUrl('expo-module-template');
126
+ return await newStep('Downloading module template from npm', async (step) => {
127
+ const tarballUrl = await getNpmTarballUrl('expo-module-template');
128
+
129
+ await downloadTarball({
130
+ url: tarballUrl,
131
+ dir: targetDir,
132
+ });
165
133
 
166
- console.log('⬇️ Downloading module template from npm...');
134
+ step.succeed('Downloaded module template from npm');
167
135
 
168
- await downloadTarball({
169
- url: tarballUrl,
170
- dir: targetDir,
136
+ return path.join(targetDir, 'package');
171
137
  });
172
- return path.join(targetDir, 'package');
173
138
  }
174
139
 
175
140
  /**
176
- * Installs dependencies and builds TypeScript files.
141
+ * Creates the module based on the `ejs` template (e.g. `expo-module-template` package).
177
142
  */
178
- async function postActionsAsync(packageManager: PackageManagerName, targetDir: string) {
179
- async function run(...args: string[]) {
180
- await spawnAsync(packageManager, args, {
181
- cwd: targetDir,
182
- stdio: 'ignore',
183
- });
184
- }
143
+ async function createModuleFromTemplate(
144
+ templatePath: string,
145
+ targetPath: string,
146
+ data: SubstitutionData
147
+ ) {
148
+ const files = await getFilesAsync(templatePath);
185
149
 
186
- console.log('📦 Installing dependencies...');
187
- await run('install');
150
+ // Iterate through all template files.
151
+ for (const file of files) {
152
+ const renderedRelativePath = ejs.render(file.replace(/^\$/, ''), data, {
153
+ openDelimiter: '{',
154
+ closeDelimiter: '}',
155
+ escape: (value: string) => value.replace('.', path.sep),
156
+ });
157
+ const fromPath = path.join(templatePath, file);
158
+ const toPath = path.join(targetPath, renderedRelativePath);
159
+ const template = await fs.readFile(fromPath, { encoding: 'utf8' });
160
+ const renderedContent = ejs.render(template, data);
188
161
 
189
- console.log('🛠 Compiling TypeScript files...');
190
- await run('run', 'build');
162
+ await fs.outputFile(toPath, renderedContent, { encoding: 'utf8' });
163
+ }
191
164
  }
192
165
 
193
166
  /**
194
167
  * Asks the user for some data necessary to render the template.
195
168
  * Some values may already be provided by command options, the prompt is skipped in that case.
196
169
  */
197
- async function askForSubstitutionDataAsync(
198
- targetDir: string,
199
- options: CommandOptions
200
- ): Promise<SubstitutionData> {
201
- const defaultPackageSlug = path.basename(targetDir);
202
- const defaultProjectName = defaultPackageSlug
203
- .replace(/^./, (match) => match.toUpperCase())
204
- .replace(/\W+(\w)/g, (_, p1) => p1.toUpperCase());
205
-
206
- const promptQueries: CustomPromptObject[] = [
207
- {
208
- type: 'text',
209
- name: 'slug',
210
- message: 'What is the package slug?',
211
- initial: defaultPackageSlug,
212
- resolvedValue: options.target ? defaultPackageSlug : null,
213
- },
214
- {
215
- type: 'text',
216
- name: 'name',
217
- message: 'What is the project name?',
218
- initial: defaultProjectName,
219
- },
220
- {
221
- type: 'text',
222
- name: 'description',
223
- message: 'How would you describe the module?',
224
- },
225
- {
226
- type: 'text',
227
- name: 'package',
228
- message: 'What is the Android package name?',
229
- initial: `expo.modules.${defaultPackageSlug.replace(/\W/g, '').toLowerCase()}`,
230
- },
231
- {
232
- type: 'text',
233
- name: 'author',
234
- message: 'Who is the author?',
235
- initial: (await npmWhoamiAsync(targetDir)) ?? '',
236
- },
237
- {
238
- type: 'text',
239
- name: 'license',
240
- message: 'What is the license?',
241
- initial: 'MIT',
242
- },
243
- {
244
- type: 'text',
245
- name: 'repo',
246
- message: 'What is the repository URL?',
247
- },
248
- ];
170
+ async function askForSubstitutionDataAsync(targetDir: string): Promise<SubstitutionData> {
171
+ const promptQueries = await getPrompts(targetDir);
249
172
 
250
173
  // Stop the process when the user cancels/exits the prompt.
251
174
  const onCancel = () => {
252
175
  process.exit(0);
253
176
  };
254
177
 
255
- const answers: Record<string, string> = {};
256
- for (const query of promptQueries) {
257
- const { name, resolvedValue } = query;
258
- answers[name] = resolvedValue ?? options[name] ?? (await prompts(query, { onCancel }))[name];
259
- }
260
-
261
- const { slug, name, description, package: projectPackage, author, license, repo } = answers;
178
+ const {
179
+ slug,
180
+ name,
181
+ description,
182
+ package: projectPackage,
183
+ authorName,
184
+ authorEmail,
185
+ authorUrl,
186
+ repo,
187
+ } = await prompts(promptQueries, { onCancel });
262
188
 
263
189
  return {
264
190
  project: {
@@ -268,8 +194,8 @@ async function askForSubstitutionDataAsync(
268
194
  description,
269
195
  package: projectPackage,
270
196
  },
271
- author,
272
- license,
197
+ author: `${authorName} <${authorEmail}> (${authorUrl})`,
198
+ license: 'MIT',
273
199
  repo,
274
200
  };
275
201
  }
@@ -312,14 +238,9 @@ program
312
238
  '-s, --source <source_dir>',
313
239
  'Local path to the template. By default it downloads `expo-module-template` from NPM.'
314
240
  )
315
- .option('-n, --name <module_name>', 'Name of the native module.')
316
- .option('-d, --description <description>', 'Description of the module.')
317
- .option('-p, --package <package>', 'The Android package name.')
318
- .option('-a, --author <author>', 'The author name.')
319
- .option('-l, --license <license>', 'The license that the module is distributed with.')
320
- .option('-r, --repo <repo_url>', 'The URL to the repository.')
321
241
  .option('--with-readme', 'Whether to include README.md file.', false)
322
242
  .option('--with-changelog', 'Whether to include CHANGELOG.md file.', false)
243
+ .option('--no-example', 'Whether to skip creating the example app.', false)
323
244
  .action(main);
324
245
 
325
246
  program.parse(process.argv);
@@ -0,0 +1,159 @@
1
+ import spawnAsync from '@expo/spawn-async';
2
+ import fs from 'fs-extra';
3
+ import path from 'path';
4
+
5
+ import { installDependencies } from './packageManager';
6
+ import { PackageManagerName } from './resolvePackageManager';
7
+ import { SubstitutionData } from './types';
8
+ import { newStep } from './utils';
9
+
10
+ // These dependencies will be removed from the example app (`expo init` adds them)
11
+ const DEPENDENCIES_TO_REMOVE = ['expo-status-bar', 'expo-splash-screen'];
12
+
13
+ /**
14
+ * Initializes a new Expo project as an example app.
15
+ */
16
+ export async function createExampleApp(
17
+ data: SubstitutionData,
18
+ targetDir: string,
19
+ packageManager: PackageManagerName
20
+ ): Promise<void> {
21
+ // Package name for the example app
22
+ const exampleProjectSlug = `${data.project.slug}-example`;
23
+
24
+ // `expo init` creates a new folder with the same name as the project slug
25
+ const appTmpPath = path.join(targetDir, exampleProjectSlug);
26
+
27
+ // Path to the target example dir
28
+ const appTargetPath = path.join(targetDir, 'example');
29
+
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
+ 'expo',
38
+ ['init', exampleProjectSlug, '--template', 'expo-template-blank-typescript'],
39
+ {
40
+ cwd: targetDir,
41
+ stdio: ['ignore', 'ignore', 'inherit'],
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);
51
+
52
+ // Cleanup the "example" dir
53
+ await fs.rmdir(appTargetPath);
54
+
55
+ // Move the temporary example app to "example" dir
56
+ await fs.rename(appTmpPath, appTargetPath);
57
+
58
+ await addMissingAppConfigFields(appTargetPath, data);
59
+
60
+ step.succeed('Configured the example app');
61
+ });
62
+
63
+ await prebuildExampleApp(appTargetPath);
64
+
65
+ await modifyPackageJson(appTargetPath);
66
+
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
+ });
72
+ }
73
+
74
+ /**
75
+ * Copies files from one directory to another.
76
+ */
77
+ async function moveFiles(fromPath: string, toPath: string): Promise<void> {
78
+ for (const file of await fs.readdir(fromPath)) {
79
+ await fs.move(path.join(fromPath, file), path.join(toPath, file), {
80
+ overwrite: true,
81
+ });
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Adds missing configuration that are required to run `expo prebuild`.
87
+ */
88
+ async function addMissingAppConfigFields(appPath: string, data: SubstitutionData): Promise<void> {
89
+ const appConfigPath = path.join(appPath, 'app.json');
90
+ const appConfig = await fs.readJson(appConfigPath);
91
+ const appId = `${data.project.package}.example`;
92
+
93
+ // Android package name needs to be added to app.json
94
+ if (!appConfig.expo.android) {
95
+ appConfig.expo.android = {};
96
+ }
97
+ appConfig.expo.android.package = appId;
98
+
99
+ // Specify iOS bundle identifier
100
+ if (!appConfig.expo.ios) {
101
+ appConfig.expo.ios = {};
102
+ }
103
+ appConfig.expo.ios.bundleIdentifier = appId;
104
+
105
+ await fs.writeJson(appConfigPath, appConfig, {
106
+ spaces: 2,
107
+ });
108
+ }
109
+
110
+ /**
111
+ * Applies necessary changes to **package.json** of the example app.
112
+ * It means setting the autolinking config and removing unnecessary dependencies.
113
+ */
114
+ async function modifyPackageJson(appPath: string): Promise<void> {
115
+ const packageJsonPath = path.join(appPath, 'package.json');
116
+ const packageJson = await fs.readJson(packageJsonPath);
117
+
118
+ if (!packageJson.expo) {
119
+ packageJson.expo = {};
120
+ }
121
+
122
+ // Set the native modules dir to the root folder,
123
+ // so that the autolinking can detect and link the module.
124
+ packageJson.expo.autolinking = {
125
+ nativeModulesDir: '..',
126
+ };
127
+
128
+ // Remove unnecessary dependencies
129
+ for (const dependencyToRemove of DEPENDENCIES_TO_REMOVE) {
130
+ delete packageJson.dependencies[dependencyToRemove];
131
+ }
132
+
133
+ await fs.writeJson(packageJsonPath, packageJson, {
134
+ spaces: 2,
135
+ });
136
+ }
137
+
138
+ /**
139
+ * Runs `expo prebuild` in the example app.
140
+ */
141
+ async function prebuildExampleApp(exampleAppPath: string): Promise<void> {
142
+ await newStep('Prebuilding the example app', async (step) => {
143
+ await spawnAsync('expo', ['prebuild', '--no-install'], {
144
+ cwd: exampleAppPath,
145
+ stdio: ['ignore', 'ignore', 'pipe'],
146
+ });
147
+ step.succeed('Prebuilt the example app');
148
+ });
149
+ }
150
+
151
+ /**
152
+ * Runs `pod install` in the iOS project at the given path.
153
+ */
154
+ async function podInstall(appPath: string): Promise<void> {
155
+ await spawnAsync('pod', ['install'], {
156
+ cwd: path.join(appPath, 'ios'),
157
+ stdio: ['ignore', 'ignore', 'pipe'],
158
+ });
159
+ }
@@ -0,0 +1,14 @@
1
+ import spawnAsync from '@expo/spawn-async';
2
+
3
+ import { PackageManagerName } from './resolvePackageManager';
4
+
5
+ export async function installDependencies(
6
+ packageManager: PackageManagerName,
7
+ appPath: string,
8
+ ...args: string[]
9
+ ) {
10
+ await spawnAsync(packageManager, ['install', ...args], {
11
+ cwd: appPath,
12
+ stdio: 'ignore',
13
+ });
14
+ }