mirror of
https://github.com/expo/eas-cli.git
synced 2026-11-14 17:50:32 -10:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0abc4f1554 | ||
|
|
5ea4ac4c39 |
No files matched your search
@@ -0,0 +1,166 @@
|
||||
import { Env } from '@expo/eas-build-job';
|
||||
import path from 'path';
|
||||
import resolveFrom from 'resolve-from';
|
||||
|
||||
import { createMockLogger } from '../../__tests__/utils/logger';
|
||||
import { configureExpoTransitiveDependenciesNodePathAsync } from '../expoTransitiveDependenciesNodePath';
|
||||
|
||||
jest.mock('resolve-from', () => ({
|
||||
__esModule: true,
|
||||
default: Object.assign(jest.fn(), {
|
||||
silent: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
const mockedResolveFrom = resolveFrom as jest.MockedFunction<typeof resolveFrom> & {
|
||||
silent: jest.MockedFunction<typeof resolveFrom.silent>;
|
||||
};
|
||||
|
||||
describe(configureExpoTransitiveDependenciesNodePathAsync, () => {
|
||||
beforeEach(() => {
|
||||
mockedResolveFrom.silent.mockReset();
|
||||
});
|
||||
|
||||
it('extends NODE_PATH with Expo transitive dependency locations', async () => {
|
||||
const projectDir = '/workingdir/build/apps/app';
|
||||
const packagerDir = '/workingdir/build';
|
||||
const expoPackageDir = '/workingdir/build/node_modules/.pnpm/expo@52/node_modules/expo';
|
||||
const expoNodeModulesDir = path.join(expoPackageDir, 'node_modules');
|
||||
const env: Env = { NODE_PATH: '/custom/node_modules' };
|
||||
const logger = createMockLogger();
|
||||
|
||||
mockResolveFrom({
|
||||
[resolutionKey(projectDir, 'expo/package.json')]: path.join(expoPackageDir, 'package.json'),
|
||||
[resolutionKey(expoPackageDir, 'babel-preset-expo/package.json')]: path.join(
|
||||
expoNodeModulesDir,
|
||||
'babel-preset-expo',
|
||||
'package.json'
|
||||
),
|
||||
[resolutionKey(expoPackageDir, 'expo-asset/package.json')]: path.join(
|
||||
expoNodeModulesDir,
|
||||
'expo-asset',
|
||||
'package.json'
|
||||
),
|
||||
});
|
||||
|
||||
await configureExpoTransitiveDependenciesNodePathAsync({
|
||||
projectDir,
|
||||
packagerDir,
|
||||
env,
|
||||
logger,
|
||||
});
|
||||
|
||||
expect(env.NODE_PATH).toBe(['/custom/node_modules', expoNodeModulesDir].join(path.delimiter));
|
||||
expect(logger.info).toHaveBeenCalledWith(
|
||||
`Extending NODE_PATH with Expo dependency paths: ${expoNodeModulesDir}`
|
||||
);
|
||||
});
|
||||
|
||||
it('does not update NODE_PATH when expo is not installed', async () => {
|
||||
const env: Env = { NODE_PATH: '/custom/node_modules' };
|
||||
const logger = createMockLogger();
|
||||
|
||||
await configureExpoTransitiveDependenciesNodePathAsync({
|
||||
projectDir: '/workingdir/build',
|
||||
packagerDir: '/workingdir/build',
|
||||
env,
|
||||
logger,
|
||||
});
|
||||
|
||||
expect(env.NODE_PATH).toBe('/custom/node_modules');
|
||||
expect(logger.info).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not update NODE_PATH when transitive dependencies already resolve from the project', async () => {
|
||||
const projectDir = '/workingdir/build';
|
||||
const expoPackageDir = '/workingdir/build/node_modules/expo';
|
||||
const env: Env = {};
|
||||
const logger = createMockLogger();
|
||||
|
||||
mockResolveFrom({
|
||||
[resolutionKey(projectDir, 'expo/package.json')]: path.join(expoPackageDir, 'package.json'),
|
||||
[resolutionKey(projectDir, 'babel-preset-expo/package.json')]:
|
||||
'/workingdir/build/node_modules/babel-preset-expo/package.json',
|
||||
[resolutionKey(projectDir, 'expo-asset/package.json')]:
|
||||
'/workingdir/build/node_modules/expo-asset/package.json',
|
||||
});
|
||||
|
||||
await configureExpoTransitiveDependenciesNodePathAsync({
|
||||
projectDir,
|
||||
packagerDir: projectDir,
|
||||
env,
|
||||
logger,
|
||||
});
|
||||
|
||||
expect(env.NODE_PATH).toBeUndefined();
|
||||
expect(logger.info).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('skips transitive dependencies that do not resolve from expo', async () => {
|
||||
const projectDir = '/workingdir/build';
|
||||
const expoPackageDir = '/workingdir/build/node_modules/.pnpm/expo@52/node_modules/expo';
|
||||
const expoNodeModulesDir = path.join(expoPackageDir, 'node_modules');
|
||||
const env: Env = {};
|
||||
const logger = createMockLogger();
|
||||
|
||||
mockResolveFrom({
|
||||
[resolutionKey(projectDir, 'expo/package.json')]: path.join(expoPackageDir, 'package.json'),
|
||||
[resolutionKey(expoPackageDir, 'babel-preset-expo/package.json')]: path.join(
|
||||
expoNodeModulesDir,
|
||||
'babel-preset-expo',
|
||||
'package.json'
|
||||
),
|
||||
});
|
||||
|
||||
await configureExpoTransitiveDependenciesNodePathAsync({
|
||||
projectDir,
|
||||
packagerDir: projectDir,
|
||||
env,
|
||||
logger,
|
||||
});
|
||||
|
||||
expect(env.NODE_PATH).toBe(expoNodeModulesDir);
|
||||
});
|
||||
|
||||
it('does not duplicate existing NODE_PATH entries', async () => {
|
||||
const projectDir = '/workingdir/build';
|
||||
const expoPackageDir = '/workingdir/build/node_modules/.pnpm/expo@52/node_modules/expo';
|
||||
const expoNodeModulesDir = path.join(expoPackageDir, 'node_modules');
|
||||
const env: Env = { NODE_PATH: expoNodeModulesDir };
|
||||
const logger = createMockLogger();
|
||||
|
||||
mockResolveFrom({
|
||||
[resolutionKey(projectDir, 'expo/package.json')]: path.join(expoPackageDir, 'package.json'),
|
||||
[resolutionKey(expoPackageDir, 'babel-preset-expo/package.json')]: path.join(
|
||||
expoNodeModulesDir,
|
||||
'babel-preset-expo',
|
||||
'package.json'
|
||||
),
|
||||
[resolutionKey(expoPackageDir, 'expo-asset/package.json')]: path.join(
|
||||
expoNodeModulesDir,
|
||||
'expo-asset',
|
||||
'package.json'
|
||||
),
|
||||
});
|
||||
|
||||
await configureExpoTransitiveDependenciesNodePathAsync({
|
||||
projectDir,
|
||||
packagerDir: projectDir,
|
||||
env,
|
||||
logger,
|
||||
});
|
||||
|
||||
expect(env.NODE_PATH).toBe(expoNodeModulesDir);
|
||||
expect(logger.info).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
function mockResolveFrom(resolutions: Record<string, string>): void {
|
||||
mockedResolveFrom.silent.mockImplementation((fromDir, moduleId) => {
|
||||
return resolutions[resolutionKey(fromDir, moduleId)];
|
||||
});
|
||||
}
|
||||
|
||||
function resolutionKey(fromDir: string, moduleId: string): string {
|
||||
return `${fromDir}:${moduleId}`;
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import { Env } from '@expo/eas-build-job';
|
||||
import { bunyan } from '@expo/logger';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import resolveFrom from 'resolve-from';
|
||||
|
||||
const EXPO_TRANSITIVE_DEPENDENCIES_TO_RESOLVE = ['babel-preset-expo', 'expo-asset'];
|
||||
|
||||
export async function configureExpoTransitiveDependenciesNodePathAsync({
|
||||
projectDir,
|
||||
packagerDir,
|
||||
env,
|
||||
logger,
|
||||
}: {
|
||||
projectDir: string;
|
||||
packagerDir: string;
|
||||
env: Env;
|
||||
logger: bunyan;
|
||||
}): Promise<void> {
|
||||
const projectResolutionRoots = uniqueItems([projectDir, packagerDir]);
|
||||
const expoPackageJsonPath = resolvePackageJson(projectResolutionRoots, 'expo');
|
||||
if (!expoPackageJsonPath) {
|
||||
return;
|
||||
}
|
||||
|
||||
const expoResolutionRoots = await getPackageResolutionRootsAsync(expoPackageJsonPath);
|
||||
const nodePathEntriesToAdd = new Set<string>();
|
||||
|
||||
for (const packageName of EXPO_TRANSITIVE_DEPENDENCIES_TO_RESOLVE) {
|
||||
if (resolvePackageJson(projectResolutionRoots, packageName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const packageJsonPath = resolvePackageJson(expoResolutionRoots, packageName);
|
||||
if (!packageJsonPath) {
|
||||
continue;
|
||||
}
|
||||
|
||||
nodePathEntriesToAdd.add(getNodeModulesDirForPackage(packageJsonPath));
|
||||
}
|
||||
|
||||
if (nodePathEntriesToAdd.size === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const existingNodePathEntries = splitNodePath(env.NODE_PATH);
|
||||
const addedNodePathEntries = [...nodePathEntriesToAdd].filter(
|
||||
nodePathEntry => !existingNodePathEntries.includes(nodePathEntry)
|
||||
);
|
||||
if (addedNodePathEntries.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
env.NODE_PATH = [...existingNodePathEntries, ...addedNodePathEntries].join(path.delimiter);
|
||||
logger.info(
|
||||
`Extending NODE_PATH with Expo dependency paths: ${addedNodePathEntries.join(path.delimiter)}`
|
||||
);
|
||||
}
|
||||
|
||||
function resolvePackageJson(fromDirs: string[], packageName: string): string | null {
|
||||
for (const fromDir of fromDirs) {
|
||||
const packageJsonPath = resolveFrom.silent(fromDir, `${packageName}/package.json`);
|
||||
if (packageJsonPath) {
|
||||
return packageJsonPath;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function getPackageResolutionRootsAsync(packageJsonPath: string): Promise<string[]> {
|
||||
const packageDir = path.dirname(packageJsonPath);
|
||||
try {
|
||||
return uniqueItems([packageDir, await fs.realpath(packageDir)]);
|
||||
} catch {
|
||||
return [packageDir];
|
||||
}
|
||||
}
|
||||
|
||||
function getNodeModulesDirForPackage(packageJsonPath: string): string {
|
||||
const packageDir = path.dirname(packageJsonPath);
|
||||
return path.dirname(packageDir);
|
||||
}
|
||||
|
||||
function splitNodePath(nodePath: string | undefined): string[] {
|
||||
return nodePath?.split(path.delimiter).filter(Boolean) ?? [];
|
||||
}
|
||||
|
||||
function uniqueItems<T>(items: T[]): T[] {
|
||||
return [...new Set(items)];
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import nullthrows from 'nullthrows';
|
||||
import path from 'path';
|
||||
|
||||
import { resolveEnvFromBuildProfileAsync, runEasBuildInternalAsync } from './easBuildInternal';
|
||||
import { configureExpoTransitiveDependenciesNodePathAsync } from './expoTransitiveDependenciesNodePath';
|
||||
import { installDependenciesAsync, resolvePackagerDir } from './installDependencies';
|
||||
import { prepareProjectSourcesAsync } from './projectSources';
|
||||
import { BuildContext } from '../context';
|
||||
@@ -86,6 +87,7 @@ export async function setupAsync<TJob extends BuildJob>(ctx: BuildContext<TJob>)
|
||||
});
|
||||
|
||||
await ctx.runBuildPhase(BuildPhase.INSTALL_DEPENDENCIES, async () => {
|
||||
const packagerDir = resolvePackagerDir(ctx);
|
||||
const expoVersion =
|
||||
ctx.metadata?.sdkVersion ??
|
||||
getPackageVersionFromPackageJson({
|
||||
@@ -101,12 +103,19 @@ export async function setupAsync<TJob extends BuildJob>(ctx: BuildContext<TJob>)
|
||||
});
|
||||
|
||||
await runInstallDependenciesAsync(ctx, {
|
||||
cwd: packagerDir,
|
||||
useFrozenLockfile: shouldUseFrozenLockfile({
|
||||
env: ctx.env,
|
||||
sdkVersion: expoVersion,
|
||||
reactNativeVersion,
|
||||
}),
|
||||
});
|
||||
await configureExpoTransitiveDependenciesNodePathAsync({
|
||||
projectDir: ctx.getReactNativeProjectDirectory(),
|
||||
packagerDir,
|
||||
env: ctx.env,
|
||||
logger: ctx.logger,
|
||||
});
|
||||
});
|
||||
|
||||
await ctx.runBuildPhase(BuildPhase.READ_APP_CONFIG, async () => {
|
||||
@@ -196,8 +205,10 @@ async function runExpoDoctor<TJob extends Job>(ctx: BuildContext<TJob>): Promise
|
||||
async function runInstallDependenciesAsync<TJob extends Job>(
|
||||
ctx: BuildContext<TJob>,
|
||||
{
|
||||
cwd,
|
||||
useFrozenLockfile,
|
||||
}: {
|
||||
cwd: string;
|
||||
useFrozenLockfile: boolean;
|
||||
}
|
||||
): Promise<void> {
|
||||
@@ -218,7 +229,7 @@ async function runInstallDependenciesAsync<TJob extends Job>(
|
||||
killTimeout.refresh();
|
||||
}
|
||||
},
|
||||
cwd: resolvePackagerDir(ctx),
|
||||
cwd,
|
||||
useFrozenLockfile,
|
||||
})
|
||||
).spawnPromise;
|
||||
|
||||
Reference in new issue
Block a user