Latest Threat Research:SANDWORM_MODE: Shai-Hulud-Style npm Worm Hijacks CI Workflows and Poisons AI Toolchains.Details
Socket
Book a DemoInstallSign in
Socket

create-expo-module

Package Overview
Dependencies
Maintainers
11
Versions
188
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

create-expo-module - npm Package Compare versions

Comparing version
1.0.14-canary-20260119-17896bf
to
1.0.14
+20
e2e/jest.config.js
const path = require('path');
/** @type {import('jest').Config} */
module.exports = {
testEnvironment: 'node',
testTimeout: 1000 * 60 * 5, // e2e tests can be slow, default to 5m (module creation includes npm install)
testRegex: '/__tests__/.*(test|spec)\\.[jt]sx?$',
watchPlugins: ['jest-watch-typeahead/filename', 'jest-watch-typeahead/testname'],
rootDir: path.resolve(__dirname),
displayName: require('../package').name,
roots: ['.'],
transform: {
'^.+\\.tsx?$': [
'ts-jest',
{
tsconfig: path.resolve(__dirname, 'tsconfig.json'),
},
],
},
};
{
"extends": "@tsconfig/node18/tsconfig.json",
"include": ["./__tests__"],
"compilerOptions": {
"outDir": "./build",
"module": "commonjs",
"moduleResolution": "node",
"types": ["node", "jest"],
"typeRoots": ["../../../node_modules/@types"],
"sourceMap": true,
"strictNullChecks": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true
}
}
/** @type {import('jest').Config} */
module.exports = {
...require('expo-module-scripts/jest-preset-cli'),
displayName: require('./package').name,
rootDir: __dirname,
testPathIgnorePatterns: ['<rootDir>/e2e/', '<rootDir>/node_modules/'],
};
+8
-5
{
"name": "create-expo-module",
"version": "1.0.14-canary-20260119-17896bf",
"version": "1.0.14",
"description": "The script to create the Expo module",

@@ -11,2 +11,4 @@ "main": "build/create-expo-module.js",

"lint": "expo-module lint",
"test": "expo-module test",
"test:e2e": "expo-module test --config e2e/jest.config.js --runInBand",
"expo-module": "expo-module"

@@ -37,4 +39,4 @@ },

"dependencies": {
"@expo/config": "12.0.14-canary-20260119-17896bf",
"@expo/json-file": "10.0.9-canary-20260119-17896bf",
"@expo/config": "~12.0.13",
"@expo/json-file": "^10.0.8",
"@expo/rudder-sdk-node": "^1.1.1",

@@ -57,4 +59,5 @@ "@expo/spawn-async": "^1.7.2",

"@types/prompts": "^2.4.9",
"expo-module-scripts": "5.0.9-canary-20260119-17896bf"
}
"expo-module-scripts": "^5.0.8"
},
"gitHead": "fabc6ba2a61c761989817c9656d568bce35bbff9"
}

@@ -10,2 +10,3 @@ import spawnAsync from '@expo/spawn-async';

import prompts from 'prompts';
import validateNpmPackage from 'validate-npm-package-name';

@@ -27,2 +28,4 @@ import { createExampleApp } from './createExampleApp';

import { CommandOptions, LocalSubstitutionData, SubstitutionData } from './types';
import { findGitHubEmail, findMyName } from './utils/git';
import { findGitHubUserFromEmail, guessRepoUrl } from './utils/github';
import { newStep } from './utils/ora';

@@ -55,2 +58,40 @@

/**
* Determines if we're in an interactive environment.
* Non-interactive when: CI=1/true or non-TTY stdin.
*/
function isInteractive(): boolean {
// Check for CI environment
const ci = process.env.CI;
if (ci === '1' || ci === 'true' || ci?.toLowerCase() === 'true') {
return false;
}
// Check for TTY
if (!process.stdin.isTTY) {
return false;
}
return true;
}
/**
* Converts a slug to a native module name (PascalCase).
*/
function slugToModuleName(slug: string): string {
return slug
.replace(/^@/, '')
.replace(/^./, (match) => match.toUpperCase())
.replace(/\W+(\w)/g, (_, p1) => p1.toUpperCase());
}
/**
* Converts a slug to an Android package name.
*/
function slugToAndroidPackage(slug: string): string {
const namespace = slug
.replace(/\W/g, '')
.replace(/^(expo|reactnative)/, '')
.toLowerCase();
return `expo.modules.${namespace}`;
}
async function getCorrectLocalDirectory(targetOrSlug: string) {

@@ -88,2 +129,7 @@ let packageJsonPath: string | null = null;

async function main(target: string | undefined, options: CommandOptions) {
const interactive = isInteractive();
if (!interactive) {
debug('Running in non-interactive mode');
}
if (options.local) {

@@ -100,3 +146,3 @@ console.log();

}
const slug = await askForPackageSlugAsync(target, options.local);
const slug = await askForPackageSlugAsync(target, options.local, options);
const targetDir = options.local

@@ -110,7 +156,7 @@ ? await getCorrectLocalDirectory(target || slug)

await fs.promises.mkdir(targetDir, { recursive: true });
await confirmTargetDirAsync(targetDir);
await confirmTargetDirAsync(targetDir, options);
options.target = targetDir;
const data = await askForSubstitutionDataAsync(slug, options.local);
const data = await askForSubstitutionDataAsync(slug, options.local, options);

@@ -360,4 +406,22 @@ // Make one line break between prompts and progress logs

* Asks the user for the package slug (npm package name).
* In non-interactive mode, uses the target path or 'my-module' as default.
*/
async function askForPackageSlugAsync(customTargetPath?: string, isLocal = false): Promise<string> {
async function askForPackageSlugAsync(
customTargetPath: string | undefined,
isLocal: boolean,
options: CommandOptions
): Promise<string> {
const interactive = isInteractive();
// In non-interactive mode, derive slug from target path or use default
if (!interactive) {
const targetBasename = customTargetPath && path.basename(customTargetPath);
const slug =
targetBasename && validateNpmPackage(targetBasename).validForNewPackages
? targetBasename
: 'my-module';
debug(`Non-interactive mode: using slug "${slug}"`);
return slug;
}
const { slug } = await prompts(

@@ -375,7 +439,17 @@ (isLocal ? getLocalFolderNamePrompt : getSlugPrompt)(customTargetPath),

* Some values may already be provided by command options, the prompt is skipped in that case.
* In non-interactive mode, uses defaults or CLI-provided values.
*/
async function askForSubstitutionDataAsync(
slug: string,
isLocal = false
isLocal: boolean,
options: CommandOptions
): Promise<SubstitutionData | LocalSubstitutionData> {
const interactive = isInteractive();
// Non-interactive mode: use CLI options and defaults
if (!interactive) {
return getSubstitutionDataFromOptions(slug, isLocal, options);
}
// Interactive mode: prompt for values, but skip prompts for CLI-provided values
const promptQueries = await (

@@ -385,2 +459,9 @@ isLocal ? getLocalSubstitutionDataPrompts : getSubstitutionDataPrompts

// Filter out prompts for values already provided via CLI
const filteredPrompts = promptQueries.filter((prompt) => {
const name = prompt.name as string;
const cliValue = getCliValueForPrompt(name, options);
return cliValue === undefined;
});
// Stop the process when the user cancels/exits the prompt.

@@ -391,12 +472,87 @@ const onCancel = () => {

const {
name,
description,
package: projectPackage,
authorName,
authorEmail,
authorUrl,
// Get values from prompts
const promptedValues =
filteredPrompts.length > 0 ? await prompts(filteredPrompts, { onCancel }) : {};
// Merge CLI-provided values with prompted values
const name = options.name ?? promptedValues.name ?? slugToModuleName(slug);
const projectPackage = options.package ?? promptedValues.package ?? slugToAndroidPackage(slug);
if (isLocal) {
return {
project: {
slug,
name,
package: projectPackage,
moduleName: handleSuffix(name, 'Module'),
viewName: handleSuffix(name, 'View'),
},
type: 'local',
};
}
const description = options.description ?? promptedValues.description ?? 'My new module';
const authorName = options.authorName ?? promptedValues.authorName ?? (await findMyName()) ?? '';
const authorEmail =
options.authorEmail ?? promptedValues.authorEmail ?? (await findGitHubEmail()) ?? '';
const authorUrl =
options.authorUrl ??
promptedValues.authorUrl ??
(authorEmail ? ((await findGitHubUserFromEmail(authorEmail)) ?? '') : '');
const repo = options.repo ?? promptedValues.repo ?? (await guessRepoUrl(authorUrl, slug)) ?? '';
return {
project: {
slug,
name,
version: '0.1.0',
description,
package: projectPackage,
moduleName: handleSuffix(name, 'Module'),
viewName: handleSuffix(name, 'View'),
},
author: `${authorName} <${authorEmail}> (${authorUrl})`,
license: 'MIT',
repo,
} = await prompts(promptQueries, { onCancel });
type: 'remote',
};
}
/**
* Gets the CLI value for a given prompt name.
*/
function getCliValueForPrompt(promptName: string, options: CommandOptions): string | undefined {
switch (promptName) {
case 'name':
return options.name;
case 'description':
return options.description;
case 'package':
return options.package;
case 'authorName':
return options.authorName;
case 'authorEmail':
return options.authorEmail;
case 'authorUrl':
return options.authorUrl;
case 'repo':
return options.repo;
default:
return undefined;
}
}
/**
* Gets substitution data from CLI options and defaults (for non-interactive mode).
*/
async function getSubstitutionDataFromOptions(
slug: string,
isLocal: boolean,
options: CommandOptions
): Promise<SubstitutionData | LocalSubstitutionData> {
const name = options.name ?? slugToModuleName(slug);
const projectPackage = options.package ?? slugToAndroidPackage(slug);
debug(`Non-interactive mode: name="${name}", package="${projectPackage}"`);
if (isLocal) {

@@ -415,2 +571,14 @@ return {

// For remote modules, resolve author info
const description = options.description ?? 'My new module';
const authorName = options.authorName ?? (await findMyName()) ?? '';
const authorEmail = options.authorEmail ?? (await findGitHubEmail()) ?? '';
const authorUrl =
options.authorUrl ?? (authorEmail ? ((await findGitHubUserFromEmail(authorEmail)) ?? '') : '');
const repo = options.repo ?? (await guessRepoUrl(authorUrl, slug)) ?? '';
debug(
`Non-interactive mode: description="${description}", authorName="${authorName}", authorEmail="${authorEmail}", authorUrl="${authorUrl}", repo="${repo}"`
);
return {

@@ -435,4 +603,5 @@ project: {

* Checks whether the target directory is empty and if not, asks the user to confirm if he wants to continue.
* In non-interactive mode, automatically continues (assumes intent to overwrite).
*/
async function confirmTargetDirAsync(targetDir: string): Promise<void> {
async function confirmTargetDirAsync(targetDir: string, options: CommandOptions): Promise<void> {
const files = await fs.promises.readdir(targetDir);

@@ -442,2 +611,14 @@ if (files.length === 0) {

}
// In non-interactive mode, proceed automatically
if (!isInteractive()) {
debug(`Non-interactive mode: target directory "${targetDir}" is not empty, continuing anyway`);
console.log(
chalk.yellow(
`Warning: Target directory ${chalk.magenta(targetDir)} is not empty, continuing anyway.`
)
);
return;
}
const { shouldContinue } = await prompts(

@@ -519,2 +700,10 @@ {

)
// Module configuration options (skip prompts when provided)
.option('--name <name>', 'Native module name (e.g., MyModule).')
.option('--description <description>', 'Module description.')
.option('--package <package>', 'Android package name (e.g., expo.modules.mymodule).')
.option('--author-name <name>', 'Author name for package.json.')
.option('--author-email <email>', 'Author email for package.json.')
.option('--author-url <url>', "URL to the author's profile (e.g., GitHub profile).")
.option('--repo <url>', 'URL of the repository.')
.action(main);

@@ -521,0 +710,0 @@

@@ -13,2 +13,10 @@ import { PromptObject } from 'prompts';

local: boolean;
// Module configuration options (skip prompts when provided)
name?: string;
description?: string;
package?: string;
authorName?: string;
authorEmail?: string;
authorUrl?: string;
repo?: string;
};

@@ -15,0 +23,0 @@