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.
- package/build/create-expo-module.js +61 -114
- package/build/create-expo-module.js.map +1 -1
- package/build/createExampleApp.js +36 -35
- package/build/createExampleApp.js.map +1 -1
- package/build/prompts.d.ts +3 -0
- package/build/prompts.js +87 -0
- package/build/prompts.js.map +1 -0
- package/build/types.d.ts +1 -6
- package/build/types.js.map +1 -1
- package/build/utils.d.ts +19 -0
- package/build/utils.js +79 -0
- package/build/utils.js.map +1 -0
- package/package.json +5 -2
- package/src/create-expo-module.ts +81 -123
- package/src/createExampleApp.ts +42 -40
- package/src/prompts.ts +86 -0
- package/src/types.ts +2 -6
- package/src/utils.ts +75 -0
package/build/utils.d.ts
ADDED
|
@@ -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
|
+
"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": "
|
|
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 {
|
|
14
|
-
import {
|
|
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
|
|
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(
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
61
|
-
|
|
56
|
+
await newStep('Installing module dependencies', async (step) => {
|
|
57
|
+
await installDependencies(packageManager, targetDir);
|
|
58
|
+
step.succeed('Installed module dependencies');
|
|
59
|
+
});
|
|
62
60
|
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
*
|
|
124
|
+
* Downloads the template from NPM registry.
|
|
121
125
|
*/
|
|
122
|
-
async function
|
|
123
|
-
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
*
|
|
142
|
+
* Creates the module based on the `ejs` template (e.g. `expo-module-template` package).
|
|
133
143
|
*/
|
|
134
|
-
async function
|
|
135
|
-
|
|
144
|
+
async function createModuleFromTemplate(
|
|
145
|
+
templatePath: string,
|
|
146
|
+
targetPath: string,
|
|
147
|
+
data: SubstitutionData
|
|
148
|
+
) {
|
|
149
|
+
const files = await getFilesAsync(templatePath);
|
|
136
150
|
|
|
137
|
-
|
|
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
|
-
|
|
140
|
-
|
|
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
|
-
*
|
|
168
|
+
* Asks the user for the package slug (npm package name).
|
|
148
169
|
*/
|
|
149
|
-
async function
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
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
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
|
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('[
|
|
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)
|
package/src/createExampleApp.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
await moveFiles(appTargetPath, appTmpPath);
|
|
52
|
+
// Cleanup the "example" dir
|
|
53
|
+
await fs.rmdir(appTargetPath);
|
|
44
54
|
|
|
45
|
-
|
|
46
|
-
|
|
55
|
+
// Move the temporary example app to "example" dir
|
|
56
|
+
await fs.rename(appTmpPath, appTargetPath);
|
|
47
57
|
|
|
48
|
-
|
|
49
|
-
await fs.rename(appTmpPath, appTargetPath);
|
|
58
|
+
await addMissingAppConfigFields(appTargetPath, data);
|
|
50
59
|
|
|
51
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
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
|
-
|
|
139
|
-
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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>;
|