eoas 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,68 @@
1
+ import spawnAsync from '@expo/spawn-async';
2
+
3
+ export async function isGitInstalledAsync(): Promise<boolean> {
4
+ try {
5
+ await spawnAsync('git', ['--help']);
6
+ } catch (error: any) {
7
+ if (error.code === 'ENOENT') {
8
+ return false;
9
+ }
10
+ throw error;
11
+ }
12
+ return true;
13
+ }
14
+
15
+ export async function doesGitRepoExistAsync(cwd: string | undefined): Promise<boolean> {
16
+ try {
17
+ await spawnAsync('git', ['rev-parse', '--git-dir'], {
18
+ cwd,
19
+ });
20
+ return true;
21
+ } catch {
22
+ return false;
23
+ }
24
+ }
25
+
26
+ interface GitStatusOptions {
27
+ showUntracked: boolean;
28
+ cwd: string | undefined;
29
+ }
30
+
31
+ export async function gitStatusAsync({ showUntracked, cwd }: GitStatusOptions): Promise<string> {
32
+ return (
33
+ await spawnAsync('git', ['status', '-s', showUntracked ? '-uall' : '-uno'], {
34
+ cwd,
35
+ })
36
+ ).stdout;
37
+ }
38
+
39
+ export async function getGitDiffOutputAsync(cwd: string | undefined): Promise<string> {
40
+ return (
41
+ await spawnAsync('git', ['--no-pager', 'diff'], {
42
+ cwd,
43
+ })
44
+ ).stdout;
45
+ }
46
+
47
+ export async function gitDiffAsync({
48
+ withPager = false,
49
+ cwd,
50
+ }: {
51
+ withPager?: boolean;
52
+ cwd: string | undefined;
53
+ }): Promise<void> {
54
+ const options = withPager ? [] : ['--no-pager'];
55
+ try {
56
+ await spawnAsync('git', [...options, 'diff'], {
57
+ stdio: ['ignore', 'inherit', 'inherit'],
58
+ cwd,
59
+ });
60
+ } catch (error: any) {
61
+ if (typeof error.message === 'string' && error.message.includes('SIGPIPE')) {
62
+ // This error is thrown when the user exits the pager with `q`.
63
+ // do nothing
64
+ return;
65
+ }
66
+ throw error;
67
+ }
68
+ }
@@ -0,0 +1,25 @@
1
+ import chalk from 'chalk';
2
+
3
+ import GitClient from './clients/git';
4
+ import GitNoCommitClient from './clients/gitNoCommit';
5
+ import NoVcsClient from './clients/noVcs';
6
+ import { Client } from './vcs';
7
+
8
+ const NO_VCS_WARNING = `Using EAS CLI without version control system is not recommended, use this mode only if you know what you are doing.`;
9
+
10
+ export function resolveVcsClient(requireCommit: boolean = false): Client {
11
+ if (process.env.EAS_NO_VCS) {
12
+ if (process.env.NODE_ENV !== 'test') {
13
+ // This log might be printed before cli arguments are evaluated,
14
+ // so it needs to go to stderr in case command is run in JSON
15
+ // only mode.
16
+ // eslint-disable-next-line no-console
17
+ console.error(chalk.yellow(NO_VCS_WARNING));
18
+ }
19
+ return new NoVcsClient();
20
+ }
21
+ if (requireCommit) {
22
+ return new GitClient();
23
+ }
24
+ return new GitNoCommitClient();
25
+ }
@@ -0,0 +1,88 @@
1
+ import fg from 'fast-glob';
2
+ import fs from 'fs-extra';
3
+ import createIgnore, { Ignore as SingleFileIgnore } from 'ignore';
4
+ import path from 'path';
5
+
6
+ const EASIGNORE_FILENAME = '.easignore';
7
+ const GITIGNORE_FILENAME = '.gitignore';
8
+
9
+ const DEFAULT_IGNORE = `
10
+ .git
11
+ node_modules
12
+ `;
13
+
14
+ export function getRootPath(): string {
15
+ const rootPath = process.env.EAS_PROJECT_ROOT ?? process.cwd();
16
+ if (!path.isAbsolute(rootPath)) {
17
+ return path.resolve(process.cwd(), rootPath);
18
+ }
19
+ return rootPath;
20
+ }
21
+
22
+ /**
23
+ * Ignore wraps the 'ignore' package to support multiple .gitignore files
24
+ * in subdirectories.
25
+ *
26
+ * Inconsistencies with git behavior:
27
+ * - if parent .gitignore has ignore rule and child has exception to that rule,
28
+ * file will still be ignored,
29
+ * - node_modules is always ignored,
30
+ * - if .easignore exists, .gitignore files are not used.
31
+ */
32
+ export class Ignore {
33
+ private ignoreMapping: (readonly [string, SingleFileIgnore])[] = [];
34
+
35
+ constructor(private readonly rootDir: string) {}
36
+
37
+ public async initIgnoreAsync(): Promise<void> {
38
+ const easIgnorePath = path.join(this.rootDir, EASIGNORE_FILENAME);
39
+ if (await fs.pathExists(easIgnorePath)) {
40
+ this.ignoreMapping = [
41
+ ['', createIgnore().add(DEFAULT_IGNORE)],
42
+ ['', createIgnore().add(await fs.readFile(easIgnorePath, 'utf-8'))],
43
+ ];
44
+ return;
45
+ }
46
+ const ignoreFilePaths = (
47
+ await fg(`**/${GITIGNORE_FILENAME}`, {
48
+ cwd: this.rootDir,
49
+ ignore: ['node_modules'],
50
+ followSymbolicLinks: false,
51
+ })
52
+ )
53
+ // ensure that parent dir is before child directories
54
+ .sort((a, b) => a.length - b.length && a.localeCompare(b));
55
+
56
+ const ignoreMapping = await Promise.all(
57
+ ignoreFilePaths.map(async filePath => {
58
+ return [
59
+ filePath.slice(0, filePath.length - GITIGNORE_FILENAME.length),
60
+ createIgnore().add(await fs.readFile(path.join(this.rootDir, filePath), 'utf-8')),
61
+ ] as const;
62
+ })
63
+ );
64
+ this.ignoreMapping = [['', createIgnore().add(DEFAULT_IGNORE)], ...ignoreMapping];
65
+ }
66
+
67
+ public ignores(relativePath: string): boolean {
68
+ for (const [prefix, ignore] of this.ignoreMapping) {
69
+ if (relativePath.startsWith(prefix) && ignore.ignores(relativePath.slice(prefix.length))) {
70
+ return true;
71
+ }
72
+ }
73
+ return false;
74
+ }
75
+ }
76
+
77
+ export async function makeShallowCopyAsync(src: string, dst: string): Promise<void> {
78
+ const ignore = new Ignore(src);
79
+ await ignore.initIgnoreAsync();
80
+ await fs.copy(src, dst, {
81
+ filter: (srcFilePath: string) => {
82
+ if (srcFilePath === src) {
83
+ return true;
84
+ }
85
+ return !ignore.ignores(path.relative(src, srcFilePath));
86
+ },
87
+ });
88
+ }
@@ -0,0 +1,90 @@
1
+ export abstract class Client {
2
+ // makeShallowCopyAsync should copy current project (result of getRootPathAsync()) to the specified
3
+ // destination, folder created this way will be uploaded "as is", so implementation should skip
4
+ // anything that is not committed to the repository. Most optimal solution is to create shallow clone
5
+ // using tooling provided by specific VCS, that respects all ignore rules
6
+ public abstract makeShallowCopyAsync(destinationPath: string): Promise<void>;
7
+
8
+ // Find root of the repository.
9
+ //
10
+ // On windows path might look different depending on implementation
11
+ // - git based clients will return "C:/path/to/repo"
12
+ // - non-git clients will return "C:\path\to\repo"
13
+ public abstract getRootPathAsync(): Promise<string>;
14
+
15
+ // (optional) ensureRepoExistsAsync should verify whether repository exists and tooling is installed
16
+ // it's not required for minimal support, but lack of validation might cause the failure at a later stage.
17
+ public async ensureRepoExistsAsync(): Promise<void> {}
18
+
19
+ // (optional) checks whether commit is necessary before calling makeShallowCopyAsync
20
+ //
21
+ // If it's not implemented method `makeShallowCopyAsync` needs to be able to include uncommitted changes
22
+ // when creating copy
23
+ public async isCommitRequiredAsync(): Promise<boolean> {
24
+ return false;
25
+ }
26
+
27
+ // (optional) hasUncommittedChangesAsync should check whether there are changes in local repository
28
+ public async hasUncommittedChangesAsync(): Promise<boolean | undefined> {
29
+ return undefined;
30
+ }
31
+
32
+ // (optional) commitAsync commits changes
33
+ //
34
+ // - Should be implemented if hasUncommittedChangesAsync is implemented
35
+ // - If it's not implemented method `makeShallowCopyAsync` needs to be able to include uncommitted changes
36
+ // in project copy
37
+ public async commitAsync(_arg: {
38
+ commitMessage: string;
39
+ commitAllFiles?: boolean;
40
+ nonInteractive: boolean;
41
+ }): Promise<void> {
42
+ // it should not be called unless hasUncommittedChangesAsync is implemented
43
+ throw new Error('commitAsync is not implemented');
44
+ }
45
+
46
+ // (optional) mark file as tracked, if this method is called on file, the next call to
47
+ // `commitAsync({ commitAllFiles: false })` should commit that file
48
+ public async trackFileAsync(_file: string): Promise<void> {}
49
+
50
+ // (optional) print diff of the changes that will be commited in the next call to
51
+ // `commitAsync({ commitAllFiles: false })`
52
+ public async showDiffAsync(): Promise<void> {}
53
+
54
+ /** (optional) print list of changed files */
55
+ public async showChangedFilesAsync(): Promise<void> {}
56
+
57
+ // (optional) returns hash of the last commit
58
+ // used for metadata - implementation can be safely skipped
59
+ public async getCommitHashAsync(): Promise<string | undefined> {
60
+ return undefined;
61
+ }
62
+
63
+ // (optional) returns name of the current branch
64
+ // used for EAS Update - implementation can be safely skipped
65
+ public async getBranchNameAsync(): Promise<string | null> {
66
+ return null;
67
+ }
68
+
69
+ // (optional) returns message of the last commit
70
+ // used for EAS Update - implementation can be safely skipped
71
+ public async getLastCommitMessageAsync(): Promise<string | null> {
72
+ return null;
73
+ }
74
+
75
+ // (optional) checks if the file is ignored, an implementation should ensure
76
+ // that if file exists and `isFileIgnoredAsync` returns true, then that file
77
+ // should not be included in the project tarball.
78
+ //
79
+ // @param filePath has to be a relative normalized path pointing to a file
80
+ // located under the root of the repository
81
+ public async isFileIgnoredAsync(_filePath: string): Promise<boolean> {
82
+ return false;
83
+ }
84
+
85
+ /**
86
+ * Whether this VCS client can get the last commit message.
87
+ * Used for EAS Update - implementation can be false for noVcs client.
88
+ */
89
+ public abstract canGetLastCommitMessage(): boolean;
90
+ }
@@ -0,0 +1,47 @@
1
+ import { AndroidConfig, IOSConfig } from '@expo/config-plugins';
2
+ import { Platform, Workflow } from '@expo/eas-build-job';
3
+ import fs from 'fs-extra';
4
+ import path from 'path';
5
+
6
+ import { Client } from './vcs/vcs';
7
+
8
+ export async function resolveWorkflowAsync(
9
+ projectDir: string,
10
+ platform: Platform,
11
+ vcsClient: Client
12
+ ): Promise<Workflow> {
13
+ let platformWorkflowMarkers: string[];
14
+ try {
15
+ platformWorkflowMarkers =
16
+ platform === Platform.ANDROID
17
+ ? [
18
+ path.join(projectDir, 'android/app/build.gradle'),
19
+ await AndroidConfig.Paths.getAndroidManifestAsync(projectDir),
20
+ ]
21
+ : [IOSConfig.Paths.getPBXProjectPath(projectDir)];
22
+ } catch {
23
+ return Workflow.MANAGED;
24
+ }
25
+
26
+ const vcsRootPath = path.normalize(await vcsClient.getRootPathAsync());
27
+ for (const marker of platformWorkflowMarkers) {
28
+ if (
29
+ (await fs.pathExists(marker)) &&
30
+ !(await vcsClient.isFileIgnoredAsync(path.relative(vcsRootPath, marker)))
31
+ ) {
32
+ return Workflow.GENERIC;
33
+ }
34
+ }
35
+ return Workflow.MANAGED;
36
+ }
37
+
38
+ export async function resolveWorkflowPerPlatformAsync(
39
+ projectDir: string,
40
+ vcsClient: Client
41
+ ): Promise<Record<Platform, Workflow>> {
42
+ const [android, ios] = await Promise.all([
43
+ resolveWorkflowAsync(projectDir, Platform.ANDROID, vcsClient),
44
+ resolveWorkflowAsync(projectDir, Platform.IOS, vcsClient),
45
+ ]);
46
+ return { android, ios };
47
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "extends": "@tsconfig/node18/tsconfig.json",
3
+ "compilerOptions": {
4
+ "declaration": true,
5
+ "importHelpers": true,
6
+ "module": "commonjs",
7
+ "moduleResolution": "node",
8
+ "noFallthroughCasesInSwitch": true,
9
+ "noImplicitOverride": true,
10
+ "noImplicitReturns": true,
11
+ "noUnusedLocals": true,
12
+ "noUnusedParameters": true,
13
+ "outDir": "dist",
14
+ "rootDir": "src"
15
+ },
16
+ "include": ["src/**/*"]
17
+ }