create-expo-module 1.0.13 → 1.0.14
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/e2e/jest.config.js +20 -0
- package/e2e/tsconfig.json +16 -0
- package/jest.config.js +7 -0
- package/package.json +5 -3
- package/src/create-expo-module.ts +203 -14
- package/src/types.ts +8 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
|
|
3
|
+
/** @type {import('jest').Config} */
|
|
4
|
+
module.exports = {
|
|
5
|
+
testEnvironment: 'node',
|
|
6
|
+
testTimeout: 1000 * 60 * 5, // e2e tests can be slow, default to 5m (module creation includes npm install)
|
|
7
|
+
testRegex: '/__tests__/.*(test|spec)\\.[jt]sx?$',
|
|
8
|
+
watchPlugins: ['jest-watch-typeahead/filename', 'jest-watch-typeahead/testname'],
|
|
9
|
+
rootDir: path.resolve(__dirname),
|
|
10
|
+
displayName: require('../package').name,
|
|
11
|
+
roots: ['.'],
|
|
12
|
+
transform: {
|
|
13
|
+
'^.+\\.tsx?$': [
|
|
14
|
+
'ts-jest',
|
|
15
|
+
{
|
|
16
|
+
tsconfig: path.resolve(__dirname, 'tsconfig.json'),
|
|
17
|
+
},
|
|
18
|
+
],
|
|
19
|
+
},
|
|
20
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "@tsconfig/node18/tsconfig.json",
|
|
3
|
+
"include": ["./__tests__"],
|
|
4
|
+
"compilerOptions": {
|
|
5
|
+
"outDir": "./build",
|
|
6
|
+
"module": "commonjs",
|
|
7
|
+
"moduleResolution": "node",
|
|
8
|
+
"types": ["node", "jest"],
|
|
9
|
+
"typeRoots": ["../../../node_modules/@types"],
|
|
10
|
+
"sourceMap": true,
|
|
11
|
+
"strictNullChecks": true,
|
|
12
|
+
"skipLibCheck": true,
|
|
13
|
+
"esModuleInterop": true,
|
|
14
|
+
"allowSyntheticDefaultImports": true
|
|
15
|
+
}
|
|
16
|
+
}
|
package/jest.config.js
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-expo-module",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.14",
|
|
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",
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
"build": "expo-module build",
|
|
9
9
|
"clean": "expo-module clean",
|
|
10
10
|
"lint": "expo-module lint",
|
|
11
|
+
"test": "expo-module test",
|
|
12
|
+
"test:e2e": "expo-module test --config e2e/jest.config.js --runInBand",
|
|
11
13
|
"expo-module": "expo-module"
|
|
12
14
|
},
|
|
13
15
|
"bin": {
|
|
@@ -33,7 +35,7 @@
|
|
|
33
35
|
},
|
|
34
36
|
"homepage": "https://github.com/expo/expo/tree/main/packages/expo",
|
|
35
37
|
"dependencies": {
|
|
36
|
-
"@expo/config": "~12.0.
|
|
38
|
+
"@expo/config": "~12.0.13",
|
|
37
39
|
"@expo/json-file": "^10.0.8",
|
|
38
40
|
"@expo/rudder-sdk-node": "^1.1.1",
|
|
39
41
|
"@expo/spawn-async": "^1.7.2",
|
|
@@ -54,5 +56,5 @@
|
|
|
54
56
|
"@types/prompts": "^2.4.9",
|
|
55
57
|
"expo-module-scripts": "^5.0.8"
|
|
56
58
|
},
|
|
57
|
-
"gitHead": "
|
|
59
|
+
"gitHead": "fabc6ba2a61c761989817c9656d568bce35bbff9"
|
|
58
60
|
}
|
|
@@ -7,6 +7,7 @@ import fs from 'fs';
|
|
|
7
7
|
import { boolish } from 'getenv';
|
|
8
8
|
import path from 'path';
|
|
9
9
|
import prompts from 'prompts';
|
|
10
|
+
import validateNpmPackage from 'validate-npm-package-name';
|
|
10
11
|
|
|
11
12
|
import { createExampleApp } from './createExampleApp';
|
|
12
13
|
import { installDependencies } from './packageManager';
|
|
@@ -23,6 +24,8 @@ import {
|
|
|
23
24
|
} from './resolvePackageManager';
|
|
24
25
|
import { eventCreateExpoModule, getTelemetryClient, logEventAsync } from './telemetry';
|
|
25
26
|
import { CommandOptions, LocalSubstitutionData, SubstitutionData } from './types';
|
|
27
|
+
import { findGitHubEmail, findMyName } from './utils/git';
|
|
28
|
+
import { findGitHubUserFromEmail, guessRepoUrl } from './utils/github';
|
|
26
29
|
import { newStep } from './utils/ora';
|
|
27
30
|
|
|
28
31
|
const debug = require('debug')('create-expo-module:main') as typeof console.log;
|
|
@@ -50,6 +53,44 @@ const DOCS_URL = 'https://docs.expo.dev/modules';
|
|
|
50
53
|
|
|
51
54
|
const FYI_LOCAL_DIR = 'https://expo.fyi/expo-module-local-autolinking.md';
|
|
52
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Determines if we're in an interactive environment.
|
|
58
|
+
* Non-interactive when: CI=1/true or non-TTY stdin.
|
|
59
|
+
*/
|
|
60
|
+
function isInteractive(): boolean {
|
|
61
|
+
// Check for CI environment
|
|
62
|
+
const ci = process.env.CI;
|
|
63
|
+
if (ci === '1' || ci === 'true' || ci?.toLowerCase() === 'true') {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
// Check for TTY
|
|
67
|
+
if (!process.stdin.isTTY) {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Converts a slug to a native module name (PascalCase).
|
|
75
|
+
*/
|
|
76
|
+
function slugToModuleName(slug: string): string {
|
|
77
|
+
return slug
|
|
78
|
+
.replace(/^@/, '')
|
|
79
|
+
.replace(/^./, (match) => match.toUpperCase())
|
|
80
|
+
.replace(/\W+(\w)/g, (_, p1) => p1.toUpperCase());
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Converts a slug to an Android package name.
|
|
85
|
+
*/
|
|
86
|
+
function slugToAndroidPackage(slug: string): string {
|
|
87
|
+
const namespace = slug
|
|
88
|
+
.replace(/\W/g, '')
|
|
89
|
+
.replace(/^(expo|reactnative)/, '')
|
|
90
|
+
.toLowerCase();
|
|
91
|
+
return `expo.modules.${namespace}`;
|
|
92
|
+
}
|
|
93
|
+
|
|
53
94
|
async function getCorrectLocalDirectory(targetOrSlug: string) {
|
|
54
95
|
let packageJsonPath: string | null = null;
|
|
55
96
|
for (let dir = CWD; path.dirname(dir) !== dir; dir = path.dirname(dir)) {
|
|
@@ -82,6 +123,11 @@ async function getCorrectLocalDirectory(targetOrSlug: string) {
|
|
|
82
123
|
* @param options An options object for `commander`.
|
|
83
124
|
*/
|
|
84
125
|
async function main(target: string | undefined, options: CommandOptions) {
|
|
126
|
+
const interactive = isInteractive();
|
|
127
|
+
if (!interactive) {
|
|
128
|
+
debug('Running in non-interactive mode');
|
|
129
|
+
}
|
|
130
|
+
|
|
85
131
|
if (options.local) {
|
|
86
132
|
console.log();
|
|
87
133
|
console.log(
|
|
@@ -93,7 +139,7 @@ async function main(target: string | undefined, options: CommandOptions) {
|
|
|
93
139
|
);
|
|
94
140
|
console.log();
|
|
95
141
|
}
|
|
96
|
-
const slug = await askForPackageSlugAsync(target, options.local);
|
|
142
|
+
const slug = await askForPackageSlugAsync(target, options.local, options);
|
|
97
143
|
const targetDir = options.local
|
|
98
144
|
? await getCorrectLocalDirectory(target || slug)
|
|
99
145
|
: path.join(CWD, target || slug);
|
|
@@ -102,11 +148,11 @@ async function main(target: string | undefined, options: CommandOptions) {
|
|
|
102
148
|
return;
|
|
103
149
|
}
|
|
104
150
|
await fs.promises.mkdir(targetDir, { recursive: true });
|
|
105
|
-
await confirmTargetDirAsync(targetDir);
|
|
151
|
+
await confirmTargetDirAsync(targetDir, options);
|
|
106
152
|
|
|
107
153
|
options.target = targetDir;
|
|
108
154
|
|
|
109
|
-
const data = await askForSubstitutionDataAsync(slug, options.local);
|
|
155
|
+
const data = await askForSubstitutionDataAsync(slug, options.local, options);
|
|
110
156
|
|
|
111
157
|
// Make one line break between prompts and progress logs
|
|
112
158
|
console.log();
|
|
@@ -351,8 +397,26 @@ async function createGitRepositoryAsync(targetDir: string) {
|
|
|
351
397
|
|
|
352
398
|
/**
|
|
353
399
|
* Asks the user for the package slug (npm package name).
|
|
400
|
+
* In non-interactive mode, uses the target path or 'my-module' as default.
|
|
354
401
|
*/
|
|
355
|
-
async function askForPackageSlugAsync(
|
|
402
|
+
async function askForPackageSlugAsync(
|
|
403
|
+
customTargetPath: string | undefined,
|
|
404
|
+
isLocal: boolean,
|
|
405
|
+
options: CommandOptions
|
|
406
|
+
): Promise<string> {
|
|
407
|
+
const interactive = isInteractive();
|
|
408
|
+
|
|
409
|
+
// In non-interactive mode, derive slug from target path or use default
|
|
410
|
+
if (!interactive) {
|
|
411
|
+
const targetBasename = customTargetPath && path.basename(customTargetPath);
|
|
412
|
+
const slug =
|
|
413
|
+
targetBasename && validateNpmPackage(targetBasename).validForNewPackages
|
|
414
|
+
? targetBasename
|
|
415
|
+
: 'my-module';
|
|
416
|
+
debug(`Non-interactive mode: using slug "${slug}"`);
|
|
417
|
+
return slug;
|
|
418
|
+
}
|
|
419
|
+
|
|
356
420
|
const { slug } = await prompts(
|
|
357
421
|
(isLocal ? getLocalFolderNamePrompt : getSlugPrompt)(customTargetPath),
|
|
358
422
|
{
|
|
@@ -365,29 +429,121 @@ async function askForPackageSlugAsync(customTargetPath?: string, isLocal = false
|
|
|
365
429
|
/**
|
|
366
430
|
* Asks the user for some data necessary to render the template.
|
|
367
431
|
* Some values may already be provided by command options, the prompt is skipped in that case.
|
|
432
|
+
* In non-interactive mode, uses defaults or CLI-provided values.
|
|
368
433
|
*/
|
|
369
434
|
async function askForSubstitutionDataAsync(
|
|
370
435
|
slug: string,
|
|
371
|
-
isLocal
|
|
436
|
+
isLocal: boolean,
|
|
437
|
+
options: CommandOptions
|
|
372
438
|
): Promise<SubstitutionData | LocalSubstitutionData> {
|
|
439
|
+
const interactive = isInteractive();
|
|
440
|
+
|
|
441
|
+
// Non-interactive mode: use CLI options and defaults
|
|
442
|
+
if (!interactive) {
|
|
443
|
+
return getSubstitutionDataFromOptions(slug, isLocal, options);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Interactive mode: prompt for values, but skip prompts for CLI-provided values
|
|
373
447
|
const promptQueries = await (
|
|
374
448
|
isLocal ? getLocalSubstitutionDataPrompts : getSubstitutionDataPrompts
|
|
375
449
|
)(slug);
|
|
376
450
|
|
|
451
|
+
// Filter out prompts for values already provided via CLI
|
|
452
|
+
const filteredPrompts = promptQueries.filter((prompt) => {
|
|
453
|
+
const name = prompt.name as string;
|
|
454
|
+
const cliValue = getCliValueForPrompt(name, options);
|
|
455
|
+
return cliValue === undefined;
|
|
456
|
+
});
|
|
457
|
+
|
|
377
458
|
// Stop the process when the user cancels/exits the prompt.
|
|
378
459
|
const onCancel = () => {
|
|
379
460
|
process.exit(0);
|
|
380
461
|
};
|
|
381
462
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
463
|
+
// Get values from prompts
|
|
464
|
+
const promptedValues =
|
|
465
|
+
filteredPrompts.length > 0 ? await prompts(filteredPrompts, { onCancel }) : {};
|
|
466
|
+
|
|
467
|
+
// Merge CLI-provided values with prompted values
|
|
468
|
+
const name = options.name ?? promptedValues.name ?? slugToModuleName(slug);
|
|
469
|
+
const projectPackage = options.package ?? promptedValues.package ?? slugToAndroidPackage(slug);
|
|
470
|
+
|
|
471
|
+
if (isLocal) {
|
|
472
|
+
return {
|
|
473
|
+
project: {
|
|
474
|
+
slug,
|
|
475
|
+
name,
|
|
476
|
+
package: projectPackage,
|
|
477
|
+
moduleName: handleSuffix(name, 'Module'),
|
|
478
|
+
viewName: handleSuffix(name, 'View'),
|
|
479
|
+
},
|
|
480
|
+
type: 'local',
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const description = options.description ?? promptedValues.description ?? 'My new module';
|
|
485
|
+
const authorName = options.authorName ?? promptedValues.authorName ?? (await findMyName()) ?? '';
|
|
486
|
+
const authorEmail =
|
|
487
|
+
options.authorEmail ?? promptedValues.authorEmail ?? (await findGitHubEmail()) ?? '';
|
|
488
|
+
const authorUrl =
|
|
489
|
+
options.authorUrl ??
|
|
490
|
+
promptedValues.authorUrl ??
|
|
491
|
+
(authorEmail ? ((await findGitHubUserFromEmail(authorEmail)) ?? '') : '');
|
|
492
|
+
const repo = options.repo ?? promptedValues.repo ?? (await guessRepoUrl(authorUrl, slug)) ?? '';
|
|
493
|
+
|
|
494
|
+
return {
|
|
495
|
+
project: {
|
|
496
|
+
slug,
|
|
497
|
+
name,
|
|
498
|
+
version: '0.1.0',
|
|
499
|
+
description,
|
|
500
|
+
package: projectPackage,
|
|
501
|
+
moduleName: handleSuffix(name, 'Module'),
|
|
502
|
+
viewName: handleSuffix(name, 'View'),
|
|
503
|
+
},
|
|
504
|
+
author: `${authorName} <${authorEmail}> (${authorUrl})`,
|
|
505
|
+
license: 'MIT',
|
|
389
506
|
repo,
|
|
390
|
-
|
|
507
|
+
type: 'remote',
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Gets the CLI value for a given prompt name.
|
|
513
|
+
*/
|
|
514
|
+
function getCliValueForPrompt(promptName: string, options: CommandOptions): string | undefined {
|
|
515
|
+
switch (promptName) {
|
|
516
|
+
case 'name':
|
|
517
|
+
return options.name;
|
|
518
|
+
case 'description':
|
|
519
|
+
return options.description;
|
|
520
|
+
case 'package':
|
|
521
|
+
return options.package;
|
|
522
|
+
case 'authorName':
|
|
523
|
+
return options.authorName;
|
|
524
|
+
case 'authorEmail':
|
|
525
|
+
return options.authorEmail;
|
|
526
|
+
case 'authorUrl':
|
|
527
|
+
return options.authorUrl;
|
|
528
|
+
case 'repo':
|
|
529
|
+
return options.repo;
|
|
530
|
+
default:
|
|
531
|
+
return undefined;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* Gets substitution data from CLI options and defaults (for non-interactive mode).
|
|
537
|
+
*/
|
|
538
|
+
async function getSubstitutionDataFromOptions(
|
|
539
|
+
slug: string,
|
|
540
|
+
isLocal: boolean,
|
|
541
|
+
options: CommandOptions
|
|
542
|
+
): Promise<SubstitutionData | LocalSubstitutionData> {
|
|
543
|
+
const name = options.name ?? slugToModuleName(slug);
|
|
544
|
+
const projectPackage = options.package ?? slugToAndroidPackage(slug);
|
|
545
|
+
|
|
546
|
+
debug(`Non-interactive mode: name="${name}", package="${projectPackage}"`);
|
|
391
547
|
|
|
392
548
|
if (isLocal) {
|
|
393
549
|
return {
|
|
@@ -402,6 +558,18 @@ async function askForSubstitutionDataAsync(
|
|
|
402
558
|
};
|
|
403
559
|
}
|
|
404
560
|
|
|
561
|
+
// For remote modules, resolve author info
|
|
562
|
+
const description = options.description ?? 'My new module';
|
|
563
|
+
const authorName = options.authorName ?? (await findMyName()) ?? '';
|
|
564
|
+
const authorEmail = options.authorEmail ?? (await findGitHubEmail()) ?? '';
|
|
565
|
+
const authorUrl =
|
|
566
|
+
options.authorUrl ?? (authorEmail ? ((await findGitHubUserFromEmail(authorEmail)) ?? '') : '');
|
|
567
|
+
const repo = options.repo ?? (await guessRepoUrl(authorUrl, slug)) ?? '';
|
|
568
|
+
|
|
569
|
+
debug(
|
|
570
|
+
`Non-interactive mode: description="${description}", authorName="${authorName}", authorEmail="${authorEmail}", authorUrl="${authorUrl}", repo="${repo}"`
|
|
571
|
+
);
|
|
572
|
+
|
|
405
573
|
return {
|
|
406
574
|
project: {
|
|
407
575
|
slug,
|
|
@@ -421,12 +589,25 @@ async function askForSubstitutionDataAsync(
|
|
|
421
589
|
|
|
422
590
|
/**
|
|
423
591
|
* Checks whether the target directory is empty and if not, asks the user to confirm if he wants to continue.
|
|
592
|
+
* In non-interactive mode, automatically continues (assumes intent to overwrite).
|
|
424
593
|
*/
|
|
425
|
-
async function confirmTargetDirAsync(targetDir: string): Promise<void> {
|
|
594
|
+
async function confirmTargetDirAsync(targetDir: string, options: CommandOptions): Promise<void> {
|
|
426
595
|
const files = await fs.promises.readdir(targetDir);
|
|
427
596
|
if (files.length === 0) {
|
|
428
597
|
return;
|
|
429
598
|
}
|
|
599
|
+
|
|
600
|
+
// In non-interactive mode, proceed automatically
|
|
601
|
+
if (!isInteractive()) {
|
|
602
|
+
debug(`Non-interactive mode: target directory "${targetDir}" is not empty, continuing anyway`);
|
|
603
|
+
console.log(
|
|
604
|
+
chalk.yellow(
|
|
605
|
+
`Warning: Target directory ${chalk.magenta(targetDir)} is not empty, continuing anyway.`
|
|
606
|
+
)
|
|
607
|
+
);
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
|
|
430
611
|
const { shouldContinue } = await prompts(
|
|
431
612
|
{
|
|
432
613
|
type: 'confirm',
|
|
@@ -503,6 +684,14 @@ program
|
|
|
503
684
|
'Whether to create a local module in the current project, skipping installing node_modules and creating the example directory.',
|
|
504
685
|
false
|
|
505
686
|
)
|
|
687
|
+
// Module configuration options (skip prompts when provided)
|
|
688
|
+
.option('--name <name>', 'Native module name (e.g., MyModule).')
|
|
689
|
+
.option('--description <description>', 'Module description.')
|
|
690
|
+
.option('--package <package>', 'Android package name (e.g., expo.modules.mymodule).')
|
|
691
|
+
.option('--author-name <name>', 'Author name for package.json.')
|
|
692
|
+
.option('--author-email <email>', 'Author email for package.json.')
|
|
693
|
+
.option('--author-url <url>', "URL to the author's profile (e.g., GitHub profile).")
|
|
694
|
+
.option('--repo <url>', 'URL of the repository.')
|
|
506
695
|
.action(main);
|
|
507
696
|
|
|
508
697
|
program
|
package/src/types.ts
CHANGED
|
@@ -10,6 +10,14 @@ export type CommandOptions = {
|
|
|
10
10
|
withChangelog: boolean;
|
|
11
11
|
example: boolean;
|
|
12
12
|
local: boolean;
|
|
13
|
+
// Module configuration options (skip prompts when provided)
|
|
14
|
+
name?: string;
|
|
15
|
+
description?: string;
|
|
16
|
+
package?: string;
|
|
17
|
+
authorName?: string;
|
|
18
|
+
authorEmail?: string;
|
|
19
|
+
authorUrl?: string;
|
|
20
|
+
repo?: string;
|
|
13
21
|
};
|
|
14
22
|
|
|
15
23
|
/**
|