Skip to content
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
3 changes: 3 additions & 0 deletions apps/bare-expo/android/gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -65,5 +65,8 @@ EXPO_ALLOW_GLIDE_LOGS=true

android.enableMinifyInReleaseBuilds=true

# Enable support for local modules in Expo CLI and Expo Modules Autolinking.
expo.localModules.enabled=true

# List of directories watched for local modules.
expo.localModules.watchedDirs=["../native-component-list/src/screens/LocalModules/localModulesExamples"]
11 changes: 5 additions & 6 deletions apps/bare-expo/metro.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ function findUpTSConfig(cwd) {
return findUpTSConfig(parent);
}

function findUpTSProjectRootOrAssert(dir) {
function findUpTSProjectRootOrThrow(dir) {
const tsProjectRoot = findUpTSConfig(dir);
if (!tsProjectRoot) {
throw new Error('Local modules watched dir needs to be inside a TS project with tsconfig.json');
Expand All @@ -64,15 +64,15 @@ config.resolver.resolveRequest = (context, moduleName, platform) => {
localModuleFileExtension = '.view.js';
}
if (localModuleFileExtension) {
const tsProjectRoot = findUpTSProjectRootOrAssert(path.dirname(context.originModulePath));
const relativePathToOriginModule = path.relative(
const tsProjectRoot = findUpTSProjectRootOrThrow(path.dirname(context.originModulePath));
const modulePathRelativeToTSRoot = path.relative(
tsProjectRoot,
fs.realpathSync(path.dirname(context.originModulePath))
);

const modulePath = path.resolve(
localModulesModulesPath,
relativePathToOriginModule,
modulePathRelativeToTSRoot,
moduleName.substring(0, moduleName.lastIndexOf('.')) + localModuleFileExtension
);

Expand All @@ -82,8 +82,7 @@ config.resolver.resolveRequest = (context, moduleName, platform) => {
};
}

const resolution = context.resolveRequest(context, moduleName, platform);
return resolution;
return context.resolveRequest(context, moduleName, platform);
};

// When testing on MacOS we need to include the `react-native-macos/Libraries/Core/InitializeCore` as prepended global module
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

package localModulesExamples

import expo.modules.kotlin.modules.Module
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ class ExpoWebView: ExpoView, WKNavigationDelegate {
webView.frame = bounds
}

func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
func webView(_ webView: WKWebView, _: WKNavigation) {
if let url = webView.url {
onLoad([
"url": url.absoluteString
Expand Down
164 changes: 74 additions & 90 deletions packages/@expo/cli/src/localModules/generation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { getConfig } from '@expo/config';
import { getPbxproj } from '@expo/config-plugins/build/ios/utils/Xcodeproj';
import Server from '@expo/metro/metro/Server';
import type MetroServer from '@expo/metro/metro/Server';
import { assert as consoleAssert } from 'console';
import fs from 'fs';
import path from 'path';

Expand All @@ -26,7 +25,7 @@ function findUpTSConfig(cwd: string): string | null {
return findUpTSConfig(parent);
}

function findUpTSProjectRootOrAssert(dir: string): string {
function findUpTSProjectRootOrThrow(dir: string): string {
const tsProjectRoot = findUpTSConfig(dir);
if (!tsProjectRoot) {
throw new Error('Local modules watched dir needs to be inside a TS project with tsconfig.json');
Expand Down Expand Up @@ -71,7 +70,7 @@ function getMirrorDirectories(projectRoot: string): {
};
}

function createFreshMirrorDirectories(projectRoot: string) {
function createFreshMirrorDirectories(projectRoot: string): void {
const { localModulesModulesPath, localModulesTypesPath } = getMirrorDirectories(projectRoot);

if (fs.existsSync(localModulesModulesPath)) {
Expand All @@ -84,26 +83,26 @@ function createFreshMirrorDirectories(projectRoot: string) {
fs.mkdirSync(localModulesTypesPath, { recursive: true });
}

function trimExtension(fileName: string) {
function trimExtension(fileName: string): string {
return fileName.substring(0, fileName.lastIndexOf('.'));
}

function typesAndLocalModulePathsForFile(
projectRoot: string,
watchedDirRoot: string,
watchedDirRootAbolutePath: string,
absoluteFilePath: string
) {
consoleAssert(!!absoluteFilePath);
consoleAssert(path.isAbsolute(absoluteFilePath));
): {
moduleTypesFilePath: string;
viewTypesFilePath: string;
viewExportPath: string;
moduleExportPath: string;
moduleName: string;
} {
const { localModulesModulesPath, localModulesTypesPath } = getMirrorDirectories(projectRoot);
const splitPath = absoluteFilePath.split('/');
const fileName = splitPath.at(-1);
if (!fileName) {
throw new Error('Invalid absoluteFilePath provided.');
}
const fileName = path.basename(absoluteFilePath);
const moduleName = trimExtension(fileName);

const watchedDirTSProjectRoot = findUpTSProjectRootOrAssert(watchedDirRoot);
const watchedDirTSProjectRoot = findUpTSProjectRootOrThrow(watchedDirRootAbolutePath);
const filePathRelativeToTSProjectRoot = path.relative(watchedDirTSProjectRoot, absoluteFilePath);
const filePathRelativeToTSProjectRootWithoutExtension = trimExtension(
filePathRelativeToTSProjectRoot
Expand All @@ -117,14 +116,14 @@ function typesAndLocalModulePathsForFile(
localModulesTypesPath,
filePathRelativeToTSProjectRootWithoutExtension + '.view.d.ts'
);
const viewExportPath = path.resolve(
localModulesModulesPath,
filePathRelativeToTSProjectRootWithoutExtension + '.view.js'
);
const moduleExportPath = path.resolve(
localModulesModulesPath,
filePathRelativeToTSProjectRootWithoutExtension + '.module.js'
);
const viewExportPath = path.resolve(
localModulesModulesPath,
filePathRelativeToTSProjectRootWithoutExtension + '.view.js'
);
return {
moduleTypesFilePath,
viewTypesFilePath,
Expand All @@ -148,7 +147,7 @@ function fileWatchedWithAnyNativeExtension(
return false;
}

export function updateXCodeProject(projectRoot: string) {
export function updateXCodeProject(projectRoot: string): void {
const pbxProject = getPbxproj(projectRoot);
const mainGroupUUID = pbxProject.getFirstProject().firstProject.mainGroup;
const mainTargetUUID = pbxProject.getFirstProject().firstProject.targets[0].value;
Expand Down Expand Up @@ -212,9 +211,12 @@ export function updateXCodeProject(projectRoot: string) {
fs.writeFileSync(pbxProject.filepath, pbxProject.writeSync());
}

function fileWatchedDirAncestor(projectRoot: string, filePathAbsolute: string): string | null {
function getWatchedDirAncestorAbsolutePath(
projectRoot: string,
filePathAbsolute: string
): string | null {
const watchedDirs = getConfig(projectRoot).exp.localModules?.watchedDirs ?? [];
const realRoot = fs.realpathSync(projectRoot);
const realRoot = path.resolve(projectRoot);
for (const dir of watchedDirs) {
const dirPathAbsolute = path.resolve(realRoot, dir);
if (filePathAbsolute.startsWith(dirPathAbsolute)) {
Expand All @@ -226,12 +228,12 @@ function fileWatchedDirAncestor(projectRoot: string, filePathAbsolute: string):

function onSourceFileCreated(
projectRoot: string,
watchedDirRoot: string,
watchedDirRootAbolutePath: string,
absoluteFilePath: string,
filesWatched?: Set<string>
) {
): void {
const { moduleTypesFilePath, viewTypesFilePath, viewExportPath, moduleExportPath, moduleName } =
typesAndLocalModulePathsForFile(projectRoot, watchedDirRoot, absoluteFilePath);
typesAndLocalModulePathsForFile(projectRoot, watchedDirRootAbolutePath, absoluteFilePath);

if (filesWatched && fileWatchedWithAnyNativeExtension(absoluteFilePath, filesWatched)) {
filesWatched.add(absoluteFilePath);
Expand Down Expand Up @@ -265,12 +267,15 @@ export default _default`
fs.writeFileSync(moduleTypesFilePath, 'const _default: any\nexport default _default');
}

async function generateMirrorDirectories(projectRoot: string, filesWatched?: Set<string>) {
async function generateMirrorDirectories(
projectRoot: string,
filesWatched?: Set<string>
): Promise<void> {
createFreshMirrorDirectories(projectRoot);

const generateExportsAndTypesForDirectory = async (
absoluteDirPath: string,
watchedDirRoot: string
watchedDirRootAbolutePath: string
) => {
for (const glob of excludePathsGlobs(projectRoot)) {
if (path.matchesGlob(absoluteDirPath, glob)) {
Expand All @@ -284,11 +289,16 @@ async function generateMirrorDirectories(projectRoot: string, filesWatched?: Set
if (
dirent.isFile() &&
isValidLocalModuleFileName(dirent.name) &&
absoluteDirentPath.startsWith(watchedDirRoot)
absoluteDirentPath.startsWith(watchedDirRootAbolutePath)
) {
onSourceFileCreated(projectRoot, watchedDirRoot, absoluteDirentPath, filesWatched);
onSourceFileCreated(
projectRoot,
watchedDirRootAbolutePath,
absoluteDirentPath,
filesWatched
);
} else if (dirent.isDirectory()) {
await generateExportsAndTypesForDirectory(absoluteDirentPath, watchedDirRoot);
await generateExportsAndTypesForDirectory(absoluteDirentPath, watchedDirRootAbolutePath);
}
}
};
Expand All @@ -305,32 +315,25 @@ async function generateMirrorDirectories(projectRoot: string, filesWatched?: Set
function excludePathsGlobs(projectRoot: string): string[] {
return [
path.resolve(projectRoot, '.expo'),
path.resolve(projectRoot, '.expo', './**'),
path.resolve(projectRoot, '.expo', './**/*'),
path.resolve(projectRoot, 'node_modules'),
path.resolve(projectRoot, 'node_modules', './**'),
path.resolve(projectRoot, 'node_modules', './**/*'),
path.resolve(projectRoot, 'localModules'),
path.resolve(projectRoot, 'localModules', './**'),
path.resolve(projectRoot, 'localModules', './**/*'),
path.resolve(projectRoot, 'android'),
path.resolve(projectRoot, 'android', './**'),
path.resolve(projectRoot, 'android', './**/*'),
path.resolve(projectRoot, 'ios'),
path.resolve(projectRoot, 'ios', './**'),
path.resolve(projectRoot, 'ios', './**/*'),
path.resolve(projectRoot, 'modules'),
path.resolve(projectRoot, 'modules', './**'),
path.resolve(projectRoot, 'modules', './**/*'),
];
}

export async function startModuleGenerationAsync({
projectRoot,
metro,
}: ModuleGenerationArguments) {
}: ModuleGenerationArguments): Promise<void> {
const dotExpoDir = ensureDotExpoProjectDirectoryInitialized(projectRoot);
const { exp } = getConfig(projectRoot);
const filesWatched = new Set<string>();

const isFileExcluded = (absolutePath: string) => {
Expand All @@ -345,21 +348,17 @@ export async function startModuleGenerationAsync({
createFreshMirrorDirectories(projectRoot);

const removeFileAndEmptyDirectories = (absoluteFilePath: string) => {
if (fs.lstatSync(absoluteFilePath).isSymbolicLink()) {
fs.unlinkSync(absoluteFilePath);
} else {
fs.rmSync(absoluteFilePath);
}
fs.rmSync(absoluteFilePath);
let dirNow: string = path.dirname(absoluteFilePath);
while (fs.readdirSync(dirNow).length === 0 && dirNow !== dotExpoDir) {
fs.rmdirSync(dirNow);
dirNow = path.dirname(dirNow);
}
};

const onSourceFileRemoved = (absoluteFilePath: string, watchedDirRoot: string) => {
const onSourceFileRemoved = (absoluteFilePath: string, watchedDirRootAbolutePath: string) => {
const { moduleTypesFilePath, moduleExportPath, viewExportPath, viewTypesFilePath } =
typesAndLocalModulePathsForFile(projectRoot, watchedDirRoot, absoluteFilePath);
typesAndLocalModulePathsForFile(projectRoot, watchedDirRootAbolutePath, absoluteFilePath);

filesWatched.delete(absoluteFilePath);
if (!fileWatchedWithAnyNativeExtension(absoluteFilePath, filesWatched)) {
Expand All @@ -370,55 +369,40 @@ export async function startModuleGenerationAsync({
}
};

const metroWatchKotlinAndSwiftFiles = async ({
projectRoot,
metro,
eventTypes = ['add', 'delete'],
}: {
metro: MetroServer | null;
projectRoot: string;
eventTypes?: string[];
}) => {
const watcher = metro?.getBundler().getBundler().getWatcher();

const isWatchedFileEvent = (event: Event, watchedDirAncestor: string | null): boolean => {
return (
event.metadata?.type !== 'd' &&
isValidLocalModuleFileName(path.basename(event.filePath)) &&
!isFileExcluded(event.filePath) &&
!!watchedDirAncestor
);
};
const watcher = metro?.getBundler().getBundler().getWatcher();
const eventTypes = ['add', 'delete', 'change'];

const listener = async ({ eventsQueue }: { eventsQueue: EventsQueue }) => {
for (const event of eventsQueue) {
const watchedDirAncestor = fileWatchedDirAncestor(
projectRoot,
fs.realpathSync(event.filePath)
);
if (
eventTypes.includes(event.type) &&
isWatchedFileEvent(event, watchedDirAncestor) &&
!!watchedDirAncestor
) {
const { filePath } = event;
if (event.type === 'add') {
onSourceFileCreated(projectRoot, filePath, watchedDirAncestor, filesWatched);
} else if (event.type === 'delete') {
onSourceFileRemoved(filePath, watchedDirAncestor);
}
const isWatchedFileEvent = (event: Event, watchedDirAncestor: string | null): boolean => {
return (
event.metadata?.type !== 'd' &&
isValidLocalModuleFileName(path.basename(event.filePath)) &&
!isFileExcluded(event.filePath) &&
!!watchedDirAncestor
);
};

const listener = async ({ eventsQueue }: { eventsQueue: EventsQueue }) => {
for (const event of eventsQueue) {
const watchedDirAncestor = getWatchedDirAncestorAbsolutePath(
projectRoot,
path.resolve(event.filePath)
);
if (
eventTypes.includes(event.type) &&
isWatchedFileEvent(event, watchedDirAncestor) &&
!!watchedDirAncestor
) {
const { filePath } = event;
if (event.type === 'add') {
onSourceFileCreated(projectRoot, watchedDirAncestor, filePath, filesWatched);
} else if (event.type === 'delete') {
onSourceFileRemoved(filePath, watchedDirAncestor);
}
}
};

watcher?.addListener('change', listener);

await generateMirrorDirectories(projectRoot, filesWatched);
}
};

metroWatchKotlinAndSwiftFiles({
projectRoot,
metro,
eventTypes: ['add', 'delete', 'change'],
});
watcher?.addListener('change', listener);

await generateMirrorDirectories(projectRoot, filesWatched);
}
Original file line number Diff line number Diff line change
Expand Up @@ -1388,18 +1388,22 @@ export class MetroBundlerDevServer extends BundlerDevServer {
});
}

public async startTypeScriptServices() {
public async startTypeScriptServices(): Promise<any> {
const { projectRoot, metro } = this;
startTypescriptTypeGenerationAsync({
const startTypescriptTypeGenerationPromise = startTypescriptTypeGenerationAsync({
server: this.instance?.server,
metro: this.metro,
projectRoot: this.projectRoot,
});

const { exp } = getConfig(this.projectRoot);
if (exp.experiments?.localModules === true) {
startModuleGenerationAsync({ projectRoot, metro });
return Promise.all([
startTypescriptTypeGenerationPromise,
startModuleGenerationAsync({ projectRoot, metro }),
]);
}
return startTypescriptTypeGenerationPromise;
}

protected getConfigModuleIds(): string[] {
Expand Down
Loading
Loading