@chronsyn/eas-on-infra 0.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,264 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.configureRoute = void 0;
7
+ const node_path_1 = __importDefault(require("node:path"));
8
+ const node_child_process_1 = require("node:child_process");
9
+ const node_crypto_1 = __importDefault(require("node:crypto"));
10
+ const multer_1 = __importDefault(require("multer"));
11
+ const zod_1 = __importDefault(require("zod"));
12
+ const unzipper_1 = __importDefault(require("unzipper"));
13
+ const ZBodySchema = zod_1.default.object({
14
+ eas_access_token: zod_1.default.string(),
15
+ eas_profile: zod_1.default.string(),
16
+ eas_platform: zod_1.default.enum(['ios', 'android']),
17
+ installer: zod_1.default.enum([
18
+ 'npm', // npm i
19
+ 'yarn', // yarn
20
+ 'bun', // bun install
21
+ 'pnpm', // pnpm install
22
+ ]).optional()
23
+ }).strict();
24
+ const configureRoute = (app) => {
25
+ const multerDestination = !!process.env.UPLOAD_DIRECTORY
26
+ ? node_path_1.default.resolve(process.env.UPLOAD_DIRECTORY)
27
+ : node_path_1.default.resolve(process.cwd(), 'uploads');
28
+ app.post('/ingress', (0, multer_1.default)({
29
+ limits: {
30
+ fileSize: 300000000, // 300MB
31
+ fieldNameSize: 8 * 50,
32
+ files: 1,
33
+ fields: 20,
34
+ },
35
+ preservePath: false,
36
+ storage: multer_1.default.diskStorage({
37
+ destination: multerDestination,
38
+ filename: (req, file, cb) => {
39
+ cb(null, [new Date().getTime(), node_crypto_1.default.randomBytes(16).toString('hex'), '.zip'].join('_'));
40
+ }
41
+ }),
42
+ fileFilter: (req, file, cb) => {
43
+ if (file.mimetype.toLowerCase() !== 'application/zip') {
44
+ return cb(new Error('File rejected'));
45
+ }
46
+ return cb(null, true);
47
+ },
48
+ }).single('bundle'), async (req, res, next) => {
49
+ var _a, _b, _c, _d;
50
+ console.log(`Received ingress request...`);
51
+ const actualInputFilePath = (_a = req.file) === null || _a === void 0 ? void 0 : _a.path;
52
+ const projectFileExtractName = [
53
+ new Date().getTime(),
54
+ node_crypto_1.default.randomBytes(4).toString("hex")
55
+ ].join('_');
56
+ const extractedPath = node_path_1.default.resolve(process.env.BINARY_OUTPUT_DIRECTORY
57
+ ? (process.env.BINARY_OUTPUT_DIRECTORY,
58
+ projectFileExtractName)
59
+ : (process.cwd(),
60
+ 'extracted',
61
+ projectFileExtractName));
62
+ const directory = await unzipper_1.default.Open.file(actualInputFilePath);
63
+ await directory.extract({ path: extractedPath });
64
+ const parse = await ZBodySchema.safeParseAsync(req.body);
65
+ if (parse.error) {
66
+ res.status(500).json({ status: "error", error: parse.error });
67
+ return;
68
+ }
69
+ const { eas_access_token,
70
+ // eas_project_id,
71
+ eas_platform, eas_profile, installer = 'npm' } = parse.data;
72
+ const build_id = node_crypto_1.default.randomBytes(16).toString('hex');
73
+ const fn = [
74
+ new Date().getTime(),
75
+ eas_platform,
76
+ eas_profile,
77
+ build_id
78
+ ].join('__');
79
+ // If user has provided BINARY_OUTPUT_DIRECTORY in env, use that
80
+ // Else, fallback to output directory
81
+ const easBuildOutputPath = node_path_1.default.resolve(((_b = process === null || process === void 0 ? void 0 : process.env) === null || _b === void 0 ? void 0 : _b.BINARY_OUTPUT_DIRECTORY)
82
+ ? ((_c = process === null || process === void 0 ? void 0 : process.env) === null || _c === void 0 ? void 0 : _c.BINARY_OUTPUT_DIRECTORY, fn)
83
+ : (process.cwd(), 'builds', fn));
84
+ console.log(`EXTRACTED PATH => ${extractedPath}`);
85
+ console.log(`BUILD OUTPUT PATH => ${easBuildOutputPath}`);
86
+ // const eas_auto_submit = parse.data.eas_auto_submit === "true";
87
+ let platformParsed;
88
+ switch (true) {
89
+ case eas_platform === "ios":
90
+ platformParsed = 'ios';
91
+ break;
92
+ case eas_platform === 'android':
93
+ platformParsed = 'android';
94
+ break;
95
+ }
96
+ // TODO: chdir to directory, install dependencies
97
+ // TODO: check for `yarn.lock` to run `yarn install`, `package-lock.json` to run `npm i`, etc.
98
+ // const easBuildCommandWithArgs = [
99
+ // `git init &&`,
100
+ // `git add . &&`,
101
+ // `git commit -m "ready to build" &&`,
102
+ // `${builderToolToUse} &&`,
103
+ // `EXPO_TOKEN=${eas_access_token}`,
104
+ // 'eas build',
105
+ // '--profile', eas_profile.toString(),
106
+ // '--platform', eas_platform.toString(),
107
+ // '--local',
108
+ // '--non-interactive',
109
+ // '--output', `"${easBuildOutputPath}"`,
110
+ // ]
111
+ const easBinaryLocation = (0, node_child_process_1.execSync)(`which eas`, { encoding: 'utf-8' }).trim();
112
+ console.log("EAS BINARY => ", easBinaryLocation.toString());
113
+ // // All these are defined values - i.e. no user input
114
+ // // We're safe to exec them, as this also ensures steps are performed in the order expected
115
+ // try {
116
+ // execSync(`git init`)
117
+ // } catch (err) {
118
+ // console.error("Error with git init: ", err)
119
+ // }
120
+ // try {
121
+ // execSync(`git add .`);
122
+ // } catch (err) {
123
+ // console.error("Error with git add: ", err)
124
+ // }
125
+ // try {
126
+ // execSync(`git commit -m "ready to build"`);
127
+ // } catch (err) {
128
+ // console.error("Error with git commit: ", err)
129
+ // }
130
+ try {
131
+ switch (installer) {
132
+ case 'yarn':
133
+ (0, node_child_process_1.execSync)('yarn', {
134
+ stdio: 'inherit',
135
+ env: Object.assign(Object.assign({}, process.env), { PATH: process.env.PATH, EXPO_TOKEN: eas_access_token }),
136
+ });
137
+ break;
138
+ case 'bun':
139
+ (0, node_child_process_1.execSync)('bun install', {
140
+ stdio: 'inherit',
141
+ env: Object.assign(Object.assign({}, process.env), { PATH: process.env.PATH, EXPO_TOKEN: eas_access_token }),
142
+ });
143
+ break;
144
+ case "pnpm":
145
+ (0, node_child_process_1.execSync)('pnpm install', {
146
+ stdio: 'inherit',
147
+ env: Object.assign(Object.assign({}, process.env), { PATH: process.env.PATH, EXPO_TOKEN: eas_access_token }),
148
+ });
149
+ break;
150
+ case 'npm':
151
+ default:
152
+ (0, node_child_process_1.execSync)('npm i --force', {
153
+ stdio: 'inherit',
154
+ env: Object.assign(Object.assign({}, process.env), { PATH: process.env.PATH, EXPO_TOKEN: eas_access_token }),
155
+ });
156
+ break;
157
+ }
158
+ }
159
+ catch (err) {
160
+ console.error("Error with installer step: ", err);
161
+ }
162
+ process.chdir(extractedPath);
163
+ console.log(`Process cwd is now => `, process.cwd());
164
+ const easBuildArgs = [
165
+ `build`,
166
+ `--profile`, eas_profile.toString(),
167
+ `--platform`, eas_platform.toString(),
168
+ `--local`,
169
+ `--non-interactive`,
170
+ `--output`, easBuildOutputPath
171
+ ];
172
+ // We run this with spawn as there's som user input
173
+ // We've sanitised it as much as possible, but we still want to minimise risk
174
+ const easBuildResult = (0, node_child_process_1.spawn)('eas', easBuildArgs, {
175
+ stdio: 'inherit',
176
+ shell: false,
177
+ env: Object.assign(Object.assign({}, process.env), { PATH: process.env.PATH, EXPO_TOKEN: eas_access_token }),
178
+ });
179
+ easBuildResult.on('error', (error) => {
180
+ console.error('Error spawning process:', error);
181
+ });
182
+ easBuildResult.on('close', (code, signal) => {
183
+ console.log(`Process exited with code ${code}, signal: ${signal === null || signal === void 0 ? void 0 : signal.toString()}`);
184
+ });
185
+ easBuildResult.on('message', (msg) => {
186
+ console.log(`Process msg: ${msg.toString()}`);
187
+ });
188
+ // easBuildResult.addListener('spawn', () => {
189
+ // console.log('[EAS BUILD] [SPAWN]')
190
+ // })
191
+ // easBuildResult.addListener('disconnect', () => {
192
+ // console.log('[EAS BUILD] [DISCONNECT]')
193
+ // })
194
+ // easBuildResult.addListener('error', (msg) => {
195
+ // console.log('[EAS BUILD] [ERROR]', msg.toString())
196
+ // })
197
+ // easBuildResult.addListener('exit', () => {
198
+ // console.log('[EAS BUILD] COMPLETED!')
199
+ // })
200
+ // easBuildResult.stdout.on('close', () => {
201
+ // console.log('[STDOUT] [EAS BUILD] CLOSE!')
202
+ // })
203
+ // easBuildResult.stdout.on('data', (msg) => {
204
+ // console.log('[STDOUT] [EAS BUILD]', msg)
205
+ // })
206
+ // easBuildResult.stdout.on('end', () => {
207
+ // console.log('[STDOUT] [EAS BUILD] END!')
208
+ // })
209
+ // easBuildResult.stdout.on('error', (err) => {
210
+ // console.log('[STDOUT] [ERR] [EAS BUILD] ', err.message)
211
+ // })
212
+ // easBuildResult.stdout.on('pause', () => {
213
+ // console.log('[STDOUT] [EAS BUILD] PAUSE!')
214
+ // })
215
+ // easBuildResult.stdout.on('readable', () => {
216
+ // console.log('[STDOUT] [EAS BUILD] READABLE!')
217
+ // })
218
+ // easBuildResult.stdout.on('resume', () => {
219
+ // console.log('[STDOUT] [EAS BUILD] RESUME!')
220
+ // })
221
+ // easBuildResult.stderr.on('close', () => {
222
+ // console.log('[STDERR] [EAS BUILD] CLOSE!')
223
+ // })
224
+ // easBuildResult.stderr.on('data', (msg) => {
225
+ // console.log('[STDERR] [EAS BUILD]', msg)
226
+ // })
227
+ // easBuildResult.stderr.on('end', () => {
228
+ // console.log('[STDERR] [EAS BUILD] END!')
229
+ // })
230
+ // easBuildResult.stderr.on('error', (err) => {
231
+ // console.log('[STDERR] [ERR] [EAS BUILD] ', err.message)
232
+ // })
233
+ // easBuildResult.stderr.on('pause', () => {
234
+ // console.log('[STDERR] [EAS BUILD] PAUSE!')
235
+ // })
236
+ // easBuildResult.stderr.on('readable', () => {
237
+ // console.log('[STDERR] [EAS BUILD] READABLE!')
238
+ // })
239
+ // easBuildResult.stderr.on('resume', () => {
240
+ // console.log('[STDERR] [EAS BUILD] RESUME!')
241
+ // })
242
+ // exec(
243
+ // easBuildCommandWithArgs,
244
+ // {
245
+ // cwd: extractedPath
246
+ // },
247
+ // (err, stdout, stderr) => {
248
+ // if (err) console.log('[ERR] => ', err);
249
+ // if (stdout) console.log('[STDOUT] => ', stdout);
250
+ // if (stderr) console.log('[STDERR] => ', stderr);
251
+ // }
252
+ // )
253
+ const responseData = {
254
+ ok: true,
255
+ status: 'in_progress',
256
+ build_id,
257
+ command: easBuildArgs,
258
+ status_url: `${((_d = process.env.SITE_BASE_URL) !== null && _d !== void 0 ? _d : "")}/status/${build_id}`,
259
+ };
260
+ console.log("Processing... Response => ", JSON.stringify(responseData, null, 2));
261
+ res.status(200).json(responseData);
262
+ });
263
+ };
264
+ exports.configureRoute = configureRoute;
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@chronsyn/eas-on-infra",
3
+ "version": "0.0.1",
4
+ "main": "dist/application.core.js",
5
+ "private": false,
6
+ "scripts": {
7
+ "build": "yarn tsc",
8
+ "serve": "node dist/application.core.js"
9
+ },
10
+ "bin": {
11
+ "eas-on-infra": "./dist/client/root.js"
12
+ },
13
+ "_developer_comments": {
14
+ "client_example_command": "npx eas-on-infra build --profile production --platform ios --installer npm --config_file_path .env --build_server http://192.168.1.25:3000"
15
+ },
16
+ "keywords": [],
17
+ "author": "",
18
+ "license": "ISC",
19
+ "description": "",
20
+ "devDependencies": {
21
+ "@types/archiver": "^7.0.0",
22
+ "@types/express": "^5.0.6",
23
+ "@types/express-busboy": "^8.0.4",
24
+ "@types/express-fileupload": "^1.5.1",
25
+ "@types/multer": "^2.0.0",
26
+ "@types/node": "^25.0.2",
27
+ "@types/unzipper": "^0.10.11",
28
+ "@types/yargs": "^17.0.35",
29
+ "typescript": "^5.9.3"
30
+ },
31
+ "dependencies": {
32
+ "@socialgouv/streaming-file-encryption": "^1.1.0",
33
+ "archiver": "^7.0.1",
34
+ "busboy": "^1.6.0",
35
+ "dotenv": "^17.2.3",
36
+ "express": "^5.2.1",
37
+ "express-busboy": "^10.1.0",
38
+ "express-fileupload": "^1.5.2",
39
+ "form-data": "^4.0.5",
40
+ "multer": "^2.0.2",
41
+ "node-fetch": "^3.3.2",
42
+ "unzipper": "^0.12.3",
43
+ "yargs": "^18.0.0",
44
+ "zod": "^4.2.1"
45
+ }
46
+ }
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env node
2
+ import express from 'express';
3
+ import dotenv from 'dotenv';
4
+
5
+ import { configureRoute as postIngressRoute } from './ingress';
6
+
7
+ dotenv.config({})
8
+
9
+ const config = {
10
+ ...process.env
11
+ }
12
+
13
+ const app = express()
14
+
15
+ postIngressRoute(app)
16
+
17
+ export const runServer = () => app.listen(config.PORT, () => {
18
+ console.log(`App listening on ${config.PORT}`)
19
+ })
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env node
2
+ import yargs from 'yargs';
3
+ import { hideBin } from 'yargs/helpers';
4
+
5
+ import dotenv from 'dotenv';
6
+ import { runBuild } from "./send-build";
7
+ import { runServer } from '../application.core';
8
+ dotenv.config()
9
+
10
+ const argv = yargs(hideBin(process.argv)).parseSync()
11
+
12
+ const { $0, _, ...restArgs } = argv;
13
+
14
+ enum ESubcommand {
15
+ build = "build",
16
+ serve = "serve",
17
+ publish = "publish",
18
+ submit = "submit",
19
+ deploy = "deploy",
20
+ login = "login",
21
+ logout = "logout",
22
+ open = "open",
23
+ upload = "upload",
24
+ }
25
+
26
+ const [ subcommand ] = _ as [ESubcommand];
27
+
28
+ if (!subcommand) {
29
+ console.log(`You did not provide a command to run`)
30
+ process.exit(1);
31
+ }
32
+
33
+ switch (subcommand) {
34
+ case ESubcommand.build:
35
+ runBuild()
36
+ break;
37
+ case ESubcommand.serve:
38
+ runServer()
39
+ break;
40
+ default:
41
+ console.log(`The "${subcommand}" option is not implemented or not recognised.`)
42
+ break;
43
+ }
@@ -0,0 +1,205 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from "node:fs";
4
+ import fsPromises from "node:fs/promises";
5
+ import path from "node:path";
6
+ import crypto from "node:crypto";
7
+
8
+ import fetch from "node-fetch";
9
+
10
+ import FormData from "form-data";
11
+ import archiver from "archiver";
12
+
13
+ import yargs from 'yargs';
14
+ import { hideBin } from 'yargs/helpers';
15
+ import z from 'zod';
16
+
17
+ import dotenv from 'dotenv';
18
+ dotenv.config()
19
+
20
+ const argv = yargs(hideBin(process.argv)).parseSync()
21
+
22
+ const ZArgs = z.object({
23
+ access_token: z.string(),
24
+ profile: z.string(),
25
+ platform: z.enum(['ios', 'android']),
26
+ config_file_path: z.string(),
27
+ build_server: z.url(),
28
+ installer: z.enum([
29
+ 'npm', // npm i
30
+ 'yarn', // yarn
31
+ 'bun', // bun install
32
+ 'pnpm', // pnpm install
33
+ ]).optional()
34
+ }).strict()
35
+ const { $0, _, ...restArgs } = argv;
36
+
37
+ export const runBuild = async () => {
38
+
39
+ const parsedArgs = ZArgs.safeParse({
40
+ ...restArgs,
41
+ access_token: process?.env?.ELB_EAS_ACCESS_TOKEN ?? restArgs?.access_token
42
+ })
43
+
44
+ if (parsedArgs.error) {
45
+ throw new Error("Error processing command", { cause: parsedArgs.error })
46
+ }
47
+
48
+ // console.log(parsedArgs)
49
+
50
+ console.log("Zipping bundle...");
51
+
52
+ // Define a custome extension so we can still include zip files (in case user project has any in)
53
+ const bundleFileExtension = '.ELBZIP';
54
+ const bundleFileName = [new Date().getTime(), crypto.randomBytes(16).toString('hex'), bundleFileExtension].join('_');
55
+ const outputBundlePath = path.resolve(
56
+ process.cwd(),
57
+ bundleFileName
58
+ );
59
+ const outputBundleStream = fs.createWriteStream(outputBundlePath);
60
+ const archive = archiver('zip', { zlib: { level: 9 } })
61
+
62
+ // Catch warnings
63
+ archive.on("warning", (err) => {
64
+ if (err.code === 'ENOENT') {
65
+ console.warn(err)
66
+ } else {
67
+ throw err;
68
+ }
69
+ })
70
+
71
+ // Handle errors
72
+ archive.on("error", (err) => {
73
+ throw err;
74
+ })
75
+
76
+ // Pipe archive to file
77
+ archive.pipe(outputBundleStream);
78
+
79
+ const directories = (await fsPromises.readdir(process.cwd(), { withFileTypes: true }))
80
+ .filter(ent => ent.isDirectory())
81
+ .map(ent => ent.name)
82
+ .filter(ent => {
83
+ const ignoredEntries = [
84
+ 'node_modules',
85
+ ]
86
+
87
+ return !ignoredEntries.includes(ent)
88
+ })
89
+
90
+ const rootDirFiles = (await fsPromises.readdir(process.cwd(), { withFileTypes: true }))
91
+ .filter(ent => ent.isFile())
92
+ .map(ent => ent.name)
93
+ .filter(ent => {
94
+ if (parsedArgs?.data?.config_file_path) {
95
+ const fileIsEnvConfigFile = path.basename(parsedArgs.data.config_file_path);
96
+ // Include the config file the user specified
97
+ if (ent === fileIsEnvConfigFile) {
98
+ return true;
99
+ }
100
+ }
101
+
102
+ // Exclude other .env files
103
+ if (ent.includes(".env")) {
104
+ return false;
105
+ }
106
+
107
+ // Don't include any files which use the same extension as we use for our bundle
108
+ if (ent.includes(bundleFileExtension)) {
109
+ return false;
110
+ }
111
+ return true;
112
+ })
113
+
114
+ // console.log(JSON.stringify({directories, rootDirFiles, outputBundlePath}, null, 2))
115
+
116
+ for (const file of rootDirFiles) {
117
+ const filePath = path.resolve(process.cwd(), file)
118
+ archive.append(fs.createReadStream(filePath), { name: file })
119
+ }
120
+
121
+ for (const dir of directories) {
122
+ const dirPath = path.resolve(process.cwd(), dir);
123
+ archive.directory(dirPath, dir)
124
+ }
125
+
126
+ await archive.finalize()
127
+
128
+ outputBundleStream.on('close', async () => {
129
+ console.log("Bundle close!")
130
+
131
+ const formData = new FormData()
132
+ formData.append("bundle", fs.createReadStream(outputBundlePath))
133
+ formData.append("eas_profile", parsedArgs.data.profile);
134
+ formData.append("eas_platform", parsedArgs.data.platform);
135
+ formData.append("eas_access_token", parsedArgs.data.access_token);
136
+ formData.append("installer", parsedArgs.data.installer);
137
+
138
+ const request = await fetch(`${parsedArgs.data.build_server}/ingress`, {
139
+ method: 'POST',
140
+ body: formData,
141
+ // Note: node-fetch handles the headers automatically when using FormData
142
+ // But you might need to disable SSL verification if needed:
143
+ // agent: new (require('https').Agent)({ rejectUnauthorized: false })
144
+ });
145
+
146
+ const response = await request.json()
147
+
148
+ console.log(JSON.stringify(response, null, 2))
149
+ fs.rmSync(outputBundlePath)
150
+
151
+ // formData.append('bundle', {
152
+ // [Symbol.toStringTag]: 'file',
153
+ // name: bundleFileName,
154
+ // stream: () => fs.createReadStream(outputBundlePath)
155
+ // })
156
+
157
+ // formData.append('bundle', fileAsBlob, bundleFileName);
158
+
159
+ // formData.append("eas_profile", parsedArgs.data.profile);
160
+ // formData.append("eas_platform", parsedArgs.data.platform);
161
+ // formData.append("eas_access_token", parsedArgs.data.access_token);
162
+ // const url = [parsedArgs.data.build_server, 'ingress'].join('/');
163
+ // const options = {
164
+ // method: 'POST',
165
+ // headers: {
166
+ // 'Content-Type': 'multipart/form-data; boundary=---bundle'
167
+ // },
168
+ // body: formData
169
+ // }
170
+ // const request = await fetch(url, options)
171
+ });
172
+
173
+ outputBundleStream.on('end', () => {
174
+ console.log("Bundle end!")
175
+ });
176
+
177
+ // const fileAsBlob = await fs.openAsBlob(outputBundlePath);
178
+ // console.log(fileAsBlob);
179
+
180
+ // const formData = new FormData()
181
+ // formData.append("bundle", fs.createReadStream(outputBundlePath))
182
+
183
+ // // formData.append('bundle', {
184
+ // // [Symbol.toStringTag]: 'file',
185
+ // // name: bundleFileName,
186
+ // // stream: () => fs.createReadStream(outputBundlePath)
187
+ // // })
188
+
189
+ // // formData.append('bundle', fileAsBlob, bundleFileName);
190
+
191
+ // formData.append("eas_profile", parsedArgs.data.profile);
192
+ // formData.append("eas_platform", parsedArgs.data.platform);
193
+ // formData.append("eas_access_token", parsedArgs.data.access_token);
194
+ // const url = [parsedArgs.data.build_server, 'ingress'].join('/');
195
+ // const options = {
196
+ // method: 'POST',
197
+ // headers: {
198
+ // 'Content-Type': 'multipart/form-data; boundary=---bundle'
199
+ // },
200
+ // body: formData
201
+ // }
202
+ // const request = await fetch(url, options)
203
+ // fs.rmSync(outputBundlePath)
204
+
205
+ }