Skip to content
This repository has been archived by the owner on Jan 18, 2024. It is now read-only.

[expo-cli] Generate-module fixes and improvements #2548

Merged
merged 5 commits into from
Aug 31, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,13 @@ This is the log of notable changes to Expo CLI and related packages.
- [expo-cli] expo publish - Log bundles after building ([#2527](https://github.com/expo/expo-cli/pull/2527) by [@EvanBacon](https://github.com/EvanBacon))
- [expo-cli] expo eas:build - Add --skip-credentials-check option ([#2442](https://github.com/expo/expo-cli/pull/2442) by [@satya164](https://github.com/satya164))
- [expo-cli] Add a `eas:build:init` command ([#2443](https://github.com/expo/expo-cli/pull/2443) by [@satya164](https://github.com/satya164))
- [expo-cli] expo generate-module - Support for templates with Android native unit tests ([#2548](https://github.com/expo/expo-cli/pull/2548) by [@barthap](https://github.com/barthap))

### 🐛 Bug fixes

- [expo-cli] expo upload:android - fix `--use-submission-service` not resulting in non-zero exit code when upload fails ([#2530](https://github.com/expo/expo-cli/pull/2530) by [@mymattcarroll](https://github.com/mymattcarroll))
- [expo-cli] Fix `generate-module` to support latest `expo-module-template` ([#2510](https://github.com/expo/expo-cli/pull/2510) by [@barthap](https://github.com/barthap))
- [expo-cli] Fix `generate-module` filename generation for modules without `expo-` prefix ([#2548](https://github.com/expo/expo-cli/pull/2548) by [@barthap](https://github.com/barthap))
- [image-utils] Fix setting background color when calling `Jimp.resize` ([#2535](https://github.com/expo/expo-cli/pull/2535) by [@cruzach](https://github.com/cruzach))

### 📦 Packages updated
Expand Down
144 changes: 111 additions & 33 deletions packages/expo-cli/src/commands/generate-module/configureModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,19 @@ import path from 'path';
import CommandError from '../../CommandError';
import { ModuleConfiguration } from './ModuleConfiguration';

// TODO (barthap): If ever updated to TS 4.0, change this to:
// type PreparedPrefixes = [nameWithExpoPrefix: string, nameWithoutExpoPrefix: string];
type PreparedPrefixes = [string, string];

/**
* prepares _Expo_ prefixes for specified name
* @param name module name, e.g. JS package name
* @param prefix prefix to prepare with, defaults to _Expo_
* @returns tuple `[nameWithPrefix: string, nameWithoutPrefix: string]`
*/
const preparePrefixes = (name: string, prefix: string = 'Expo'): PreparedPrefixes =>
name.startsWith(prefix) ? [name, name.substr(prefix.length)] : [`${prefix}${name}`, name];

const asyncForEach = async <T>(
array: T[],
callback: (value: T, index: number, array: T[]) => Promise<void>
Expand All @@ -14,11 +27,13 @@ const asyncForEach = async <T>(
}
};

/**
* Removes specified files. If one file doesn't exist already, skips it
* @param directoryPath directory containing files to remove
* @param filenames array of filenames to remove
*/
async function removeFiles(directoryPath: string, filenames: string[]) {
await asyncForEach(
filenames,
async filename => await fse.remove(path.resolve(directoryPath, filename))
);
await Promise.all(filenames.map(filename => fse.remove(path.resolve(directoryPath, filename))));
}

/**
Expand Down Expand Up @@ -58,20 +73,24 @@ const replaceContents = async (
directoryPath: string,
replaceFunction: (contentOfSingleFile: string) => string
) => {
for (const file of walkSync(directoryPath, { nodir: true })) {
replaceContent(file.path, replaceFunction);
}
await Promise.all(
walkSync(directoryPath, { nodir: true }).map(file => replaceContent(file.path, replaceFunction))
);
};

/**
* Replaces content in file
* Replaces content in file. Does nothing if the file doesn't exist
* @param filePath - provided file
* @param replaceFunction - function that converts current content into something different
*/
const replaceContent = async (
filePath: string,
replaceFunction: (contentOfSingleFile: string) => string
) => {
if (!fse.existsSync(filePath)) {
return;
}

const content = await fse.readFile(filePath, 'utf8');
const newContent = replaceFunction(content);
if (newContent !== content) {
Expand Down Expand Up @@ -154,10 +173,12 @@ async function configureIOS(
* Gets path to Android source base dir: android/src/main/[java|kotlin]
* Defaults to Java path if both exist
* @param androidPath path do module android/ directory
* @param flavor package flavor e.g main, test. Defaults to main
* @throws INVALID_TEMPLATE if none exist
* @returns path to flavor source base directory
*/
function findAndroidSourceDir(androidPath: string) {
const androidSrcPathBase = path.join(androidPath, 'src', 'main');
function findAndroidSourceDir(androidPath: string, flavor: string = 'main'): string {
const androidSrcPathBase = path.join(androidPath, 'src', flavor);

const javaExists = fse.pathExistsSync(path.join(androidSrcPathBase, 'java'));
const kotlinExists = fse.pathExistsSync(path.join(androidSrcPathBase, 'kotlin'));
Expand All @@ -172,6 +193,35 @@ function findAndroidSourceDir(androidPath: string) {
return path.join(androidSrcPathBase, javaExists ? 'java' : 'kotlin');
}

/**
* Finds java package name based on directory structure
* @param flavorSrcPath Path to source base directory: e.g. android/src/main/java
* @returns java package name
*/
function findTemplateAndroidPackage(flavorSrcPath: string) {
const srcFiles = walkSync(flavorSrcPath, {
filter: item => item.path.endsWith('.kt') || item.path.endsWith('.java'),
nodir: true,
traverseAll: true,
});

if (srcFiles.length === 0) {
throw new CommandError('INVALID TEMPLATE', 'No Android source files found in the template');
}

// srcFiles[0] will always be at the most top-level of the package structure
const packageDirNames = path.relative(flavorSrcPath, srcFiles[0].path).split('/').slice(0, -1);

if (packageDirNames.length === 0) {
throw new CommandError(
'INVALID TEMPLATE',
'Template Android sources must be within a package.'
);
}

return packageDirNames.join('.');
}

/**
* Prepares Android part, mainly by renaming all files and template words in files
* Sets all versions in Gradle to 1.0.0
Expand All @@ -183,9 +233,12 @@ async function configureAndroid(
{ javaPackage, jsPackageName, viewManager }: ModuleConfiguration
) {
const androidPath = path.join(modulePath, 'android');
const [, moduleName] = preparePrefixes(jsPackageName, 'Expo');

const androidSrcPath = findAndroidSourceDir(androidPath);
const templateJavaPackage = findTemplateAndroidPackage(androidSrcPath);

const sourceFilesPath = path.join(androidSrcPath, 'expo', 'modules', 'template');
const sourceFilesPath = path.join(androidSrcPath, ...templateJavaPackage.split('.'));
const destinationFilesPath = path.join(androidSrcPath, ...javaPackage.split('.'));

// remove ViewManager from template
Expand All @@ -204,13 +257,40 @@ async function configureAndroid(

// Remove leaf directory content
await fse.remove(sourceFilesPath);
// Cleanup all empty subdirs up to provided rootDir
await removeUponEmptyOrOnlyEmptySubdirs(path.join(androidSrcPath, 'expo'));
// Cleanup all empty subdirs up to template package root dir
await removeUponEmptyOrOnlyEmptySubdirs(
path.join(androidSrcPath, templateJavaPackage.split('.')[0])
);

// prepare tests
if (fse.existsSync(path.resolve(androidPath, 'src', 'test'))) {
const androidTestPath = findAndroidSourceDir(androidPath, 'test');
const templateTestPackage = findTemplateAndroidPackage(androidTestPath);
const testSourcePath = path.join(androidTestPath, ...templateTestPackage.split('.'));
const testDestinationPath = path.join(androidTestPath, ...javaPackage.split('.'));

const moduleName = jsPackageName.startsWith('Expo') ? jsPackageName.substring(4) : jsPackageName;
await fse.mkdirp(testDestinationPath);
await fse.copy(testSourcePath, testDestinationPath);
await fse.remove(testSourcePath);
await removeUponEmptyOrOnlyEmptySubdirs(
path.join(androidTestPath, templateTestPackage.split('.')[0])
);

await replaceContents(testDestinationPath, singleFileContent =>
singleFileContent.replace(new RegExp(templateTestPackage, 'g'), javaPackage)
);

await renameFilesWithExtensions(
testDestinationPath,
['.kt', '.java'],
[{ from: 'ModuleTemplateModuleTest', to: `${moduleName}ModuleTest` }]
);
}

// Replace contents of destination files
await replaceContents(androidPath, singleFileContent =>
singleFileContent
.replace(/expo\.modules\.template/g, javaPackage)
.replace(new RegExp(templateJavaPackage, 'g'), javaPackage)
.replace(/ModuleTemplate/g, moduleName)
.replace(/ExpoModuleTemplate/g, jsPackageName)
);
Expand All @@ -222,7 +302,7 @@ async function configureAndroid(
);
await renameFilesWithExtensions(
destinationFilesPath,
['.kt'],
['.kt', '.java'],
[
{ from: 'ModuleTemplateModule', to: `${moduleName}Module` },
{ from: 'ModuleTemplatePackage', to: `${moduleName}Package` },
Expand All @@ -241,9 +321,8 @@ async function configureTS(
modulePath: string,
{ jsPackageName, viewManager }: ModuleConfiguration
) {
const moduleNameWithoutExpoPrefix = jsPackageName.startsWith('Expo')
? jsPackageName.substr(4)
: 'Unimodule';
const [moduleNameWithExpoPrefix, moduleName] = preparePrefixes(jsPackageName);

const tsPath = path.join(modulePath, 'src');

// remove View Manager from template
Expand All @@ -261,26 +340,26 @@ async function configureTS(
await renameFilesWithExtensions(
path.join(tsPath, '__tests__'),
['.ts'],
[{ from: 'ModuleTemplate-test', to: `${moduleNameWithoutExpoPrefix}-test` }]
[{ from: 'ModuleTemplate-test', to: `${moduleName}-test` }]
);
await renameFilesWithExtensions(
tsPath,
['.tsx', '.ts'],
[
{ from: 'ExpoModuleTemplateView', to: `${jsPackageName}View` },
{ from: 'ExpoModuleTemplateNativeView', to: `${jsPackageName}NativeView` },
{ from: 'ExpoModuleTemplateNativeView.web', to: `${jsPackageName}NativeView.web` },
{ from: 'ExpoModuleTemplate', to: jsPackageName },
{ from: 'ExpoModuleTemplate.web', to: `${jsPackageName}.web` },
{ from: 'ModuleTemplate', to: moduleNameWithoutExpoPrefix },
{ from: 'ModuleTemplate.types', to: `${moduleNameWithoutExpoPrefix}.types` },
{ from: 'ExpoModuleTemplateView', to: `${moduleNameWithExpoPrefix}View` },
{ from: 'ExpoModuleTemplateNativeView', to: `${moduleNameWithExpoPrefix}NativeView` },
{ from: 'ExpoModuleTemplateNativeView.web', to: `${moduleNameWithExpoPrefix}NativeView.web` },
{ from: 'ExpoModuleTemplate', to: moduleNameWithExpoPrefix },
{ from: 'ExpoModuleTemplate.web', to: `${moduleNameWithExpoPrefix}.web` },
{ from: 'ModuleTemplate', to: moduleName },
{ from: 'ModuleTemplate.types', to: `${moduleName}.types` },
]
);

await replaceContents(tsPath, singleFileContent =>
singleFileContent
.replace(/ExpoModuleTemplate/g, jsPackageName)
.replace(/ModuleTemplate/g, moduleNameWithoutExpoPrefix)
.replace(/ExpoModuleTemplate/g, moduleNameWithExpoPrefix)
.replace(/ModuleTemplate/g, moduleName)
);
}

Expand All @@ -293,15 +372,14 @@ async function configureNPM(
modulePath: string,
{ npmModuleName, podName, jsPackageName }: ModuleConfiguration
) {
const moduleNameWithoutExpoPrefix = jsPackageName.startsWith('Expo')
? jsPackageName.substr(4)
: 'Unimodule';
const [, moduleName] = preparePrefixes(jsPackageName);

await replaceContent(path.join(modulePath, 'package.json'), singleFileContent =>
singleFileContent
.replace(/expo-module-template/g, npmModuleName)
.replace(/"version": "[\w.-]+"/, '"version": "1.0.0"')
.replace(/ExpoModuleTemplate/g, jsPackageName)
.replace(/ModuleTemplate/g, moduleNameWithoutExpoPrefix)
.replace(/ModuleTemplate/g, moduleName)
);
await replaceContent(path.join(modulePath, 'README.md'), readmeContent =>
readmeContent
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,6 @@ function isNpmPackage(template: string) {
!template.match(/^_/) && // don't start with _
template.toLowerCase() === template && // only lowercase
!/[~'!()*]/.test(template.split('/').slice(-1)[0]) && // don't contain any character from [~'!()*]
template.match(/^(@([^/]+?)\/)?([^/@]+)(@(\d\.\d\.\d)(-[^/@]+)?)?$/) // has shape (@scope/)?actual-package-name(@0.1.1(-tag.1)?)?
template.match(/^(@([^/]+?)\/)?([^/@]+)(@(((\d\.\d\.\d)(-[^/@]+)?)|latest|next))?$/) // has shape (@scope/)?actual-package-name(@0.1.1(-tag.1)?|tag-name)?
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const generateCocoaPodDefaultName = (moduleName: string) => {
if (moduleName.toLowerCase().startsWith('expo')) {
return `EX${wordsToUpperCase(moduleName.substring(4))}`;
}
return wordsToUpperCase(moduleName);
return `EX${wordsToUpperCase(moduleName)}`;
};

/**
Expand Down