Skip to content

Commit 138f797

Browse files
authored
[eas-cli] Add lockfile existence preflight check for eas build (#3665)
1 parent 51311f1 commit 138f797

3 files changed

Lines changed: 137 additions & 0 deletions

File tree

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { vol } from 'memfs';
2+
3+
import { ensureLockfileExistsAsync } from '../validateLockfile';
4+
5+
jest.mock('fs');
6+
7+
const mockResolveWorkspaceRoot = jest.fn();
8+
jest.mock('@expo/package-manager', () => {
9+
const actual = jest.requireActual('@expo/package-manager');
10+
return {
11+
...actual,
12+
resolveWorkspaceRoot: (...args: unknown[]) => mockResolveWorkspaceRoot(...args),
13+
};
14+
});
15+
16+
beforeEach(() => {
17+
vol.reset();
18+
mockResolveWorkspaceRoot.mockReturnValue(null);
19+
});
20+
21+
describe(ensureLockfileExistsAsync, () => {
22+
it('passes when package-lock.json exists', async () => {
23+
vol.fromJSON({ './package-lock.json': '' }, '/app');
24+
await expect(ensureLockfileExistsAsync('/app')).resolves.not.toThrow();
25+
});
26+
27+
it('passes when yarn.lock exists', async () => {
28+
vol.fromJSON({ './yarn.lock': '' }, '/app');
29+
await expect(ensureLockfileExistsAsync('/app')).resolves.not.toThrow();
30+
});
31+
32+
it('passes when pnpm-lock.yaml exists', async () => {
33+
vol.fromJSON({ './pnpm-lock.yaml': '' }, '/app');
34+
await expect(ensureLockfileExistsAsync('/app')).resolves.not.toThrow();
35+
});
36+
37+
it('passes when bun.lockb exists', async () => {
38+
vol.fromJSON({ './bun.lockb': '' }, '/app');
39+
await expect(ensureLockfileExistsAsync('/app')).resolves.not.toThrow();
40+
});
41+
42+
it('passes when bun.lock exists', async () => {
43+
vol.fromJSON({ './bun.lock': '' }, '/app');
44+
await expect(ensureLockfileExistsAsync('/app')).resolves.not.toThrow();
45+
});
46+
47+
it('throws when no lockfile exists', async () => {
48+
vol.fromJSON({ './package.json': '{}' }, '/app');
49+
await expect(ensureLockfileExistsAsync('/app')).rejects.toThrow(
50+
'No lockfile found in the project directory.'
51+
);
52+
});
53+
54+
it('passes when lockfile exists in workspace root', async () => {
55+
vol.fromJSON(
56+
{
57+
'./packages/my-app/package.json': '{}',
58+
'./yarn.lock': '',
59+
},
60+
'/monorepo'
61+
);
62+
mockResolveWorkspaceRoot.mockReturnValue('/monorepo');
63+
await expect(
64+
ensureLockfileExistsAsync('/monorepo/packages/my-app')
65+
).resolves.not.toThrow();
66+
});
67+
68+
it('throws when no lockfile in project dir or workspace root', async () => {
69+
vol.fromJSON(
70+
{
71+
'./packages/my-app/package.json': '{}',
72+
'./package.json': '{}',
73+
},
74+
'/monorepo'
75+
);
76+
mockResolveWorkspaceRoot.mockReturnValue('/monorepo');
77+
await expect(ensureLockfileExistsAsync('/monorepo/packages/my-app')).rejects.toThrow(
78+
'No lockfile found in the project directory.'
79+
);
80+
});
81+
});

packages/eas-cli/src/build/runBuildAndSubmit.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { createBuildContextAsync } from './createContext';
2222
import { evaluateConfigWithEnvVarsAsync } from './evaluateConfigWithEnvVarsAsync';
2323
import { prepareIosBuildAsync } from './ios/build';
2424
import { LocalBuildMode, LocalBuildOptions } from './local';
25+
import { ensureLockfileExistsAsync } from './validateLockfile';
2526
import { ensureExpoDevClientInstalledForDevClientBuildsAsync } from './utils/devClient';
2627
import { printBuildResults, printLogsUrls } from './utils/printBuildInfo';
2728
import { ensureRepoIsCleanAsync } from './utils/repository';
@@ -84,6 +85,7 @@ import { Client } from '../vcs/vcs';
8485

8586
let metroConfigValidated = false;
8687
let sdkVersionChecked = false;
88+
let lockfileChecked = false;
8789
let hasWarnedAboutUsageOverages = false;
8890

8991
export interface BuildFlags {
@@ -414,6 +416,13 @@ async function prepareAndStartBuildAsync({
414416
env,
415417
});
416418

419+
if (!lockfileChecked && !flags.localBuildOptions.localBuildMode) {
420+
if (!process.env.EAS_BUILD_SKIP_LOCKFILE_CHECK) {
421+
await ensureLockfileExistsAsync(projectDir);
422+
}
423+
lockfileChecked = true;
424+
}
425+
417426
if (!hasWarnedAboutUsageOverages && !flags.localBuildOptions.localBuildMode) {
418427
hasWarnedAboutUsageOverages = true;
419428
Log.newLine();
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import {
2+
BUN_LOCK_FILE,
3+
BUN_TEXT_LOCK_FILE,
4+
NPM_LOCK_FILE,
5+
PNPM_LOCK_FILE,
6+
YARN_LOCK_FILE,
7+
resolveWorkspaceRoot,
8+
} from '@expo/package-manager';
9+
import { pathExists } from 'fs-extra';
10+
import path from 'path';
11+
12+
const LOCKFILE_NAMES = [
13+
NPM_LOCK_FILE,
14+
YARN_LOCK_FILE,
15+
PNPM_LOCK_FILE,
16+
BUN_LOCK_FILE,
17+
BUN_TEXT_LOCK_FILE,
18+
];
19+
20+
async function hasLockfileAsync(dir: string): Promise<boolean> {
21+
for (const lockfile of LOCKFILE_NAMES) {
22+
if (await pathExists(path.join(dir, lockfile))) {
23+
return true;
24+
}
25+
}
26+
return false;
27+
}
28+
29+
export async function ensureLockfileExistsAsync(projectDir: string): Promise<void> {
30+
if (await hasLockfileAsync(projectDir)) {
31+
return;
32+
}
33+
34+
const workspaceRoot = resolveWorkspaceRoot(projectDir);
35+
if (workspaceRoot && workspaceRoot !== projectDir) {
36+
if (await hasLockfileAsync(workspaceRoot)) {
37+
return;
38+
}
39+
}
40+
41+
throw new Error(
42+
`No lockfile found in the project directory.\n` +
43+
`A lockfile is required to ensure deterministic dependency installation in EAS.\n` +
44+
`Run your package manager's install command (e.g. "npm install") to generate one.\n` +
45+
`To skip this check, run this command with EAS_BUILD_SKIP_LOCKFILE_CHECK=1.`
46+
);
47+
}

0 commit comments

Comments
 (0)