@devicecloud.dev/dcd 4.1.2 → 4.1.3
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.
- package/dist/commands/cloud.d.ts +26 -34
- package/dist/commands/cloud.js +117 -513
- package/dist/config/flags/api.flags.d.ts +7 -0
- package/dist/config/flags/api.flags.js +19 -0
- package/dist/config/flags/binary.flags.d.ts +8 -0
- package/dist/config/flags/binary.flags.js +20 -0
- package/dist/config/flags/device.flags.d.ts +14 -0
- package/dist/config/flags/device.flags.js +46 -0
- package/dist/config/flags/environment.flags.d.ts +11 -0
- package/dist/config/flags/environment.flags.js +37 -0
- package/dist/config/flags/execution.flags.d.ts +13 -0
- package/dist/config/flags/execution.flags.js +50 -0
- package/dist/config/flags/output.flags.d.ts +18 -0
- package/dist/config/flags/output.flags.js +61 -0
- package/dist/constants.d.ts +28 -24
- package/dist/constants.js +21 -206
- package/dist/gateways/api-gateway.d.ts +3 -3
- package/dist/methods.d.ts +0 -4
- package/dist/methods.js +15 -80
- package/dist/services/device-validation.service.d.ts +29 -0
- package/dist/services/device-validation.service.js +72 -0
- package/dist/{plan.d.ts → services/execution-plan.service.d.ts} +1 -1
- package/dist/{plan.js → services/execution-plan.service.js} +10 -10
- package/dist/{planMethods.js → services/execution-plan.utils.js} +0 -1
- package/dist/services/metadata-extractor.service.d.ts +46 -0
- package/dist/services/metadata-extractor.service.js +138 -0
- package/dist/services/moropo.service.d.ts +20 -0
- package/dist/services/moropo.service.js +113 -0
- package/dist/services/report-download.service.d.ts +40 -0
- package/dist/services/report-download.service.js +110 -0
- package/dist/services/results-polling.service.d.ts +45 -0
- package/dist/services/results-polling.service.js +210 -0
- package/dist/services/test-submission.service.d.ts +41 -0
- package/dist/services/test-submission.service.js +116 -0
- package/dist/services/version.service.d.ts +31 -0
- package/dist/services/version.service.js +81 -0
- package/dist/types/{schema.types.d.ts → generated/schema.types.d.ts} +349 -349
- package/dist/types/index.d.ts +6 -0
- package/dist/types/index.js +24 -0
- package/dist/utils/compatibility.d.ts +5 -0
- package/oclif.manifest.json +195 -209
- package/package.json +2 -9
- /package/dist/{planMethods.d.ts → services/execution-plan.utils.d.ts} +0 -0
- /package/dist/types/{device.types.d.ts → domain/device.types.d.ts} +0 -0
- /package/dist/types/{device.types.js → domain/device.types.js} +0 -0
- /package/dist/types/{schema.types.js → generated/schema.types.js} +0 -0
|
@@ -14,14 +14,14 @@ export declare const ApiGateway: {
|
|
|
14
14
|
downloadArtifactsZip(baseUrl: string, apiKey: string, uploadId: string, results: "ALL" | "FAILED", artifactsPath?: string): Promise<void>;
|
|
15
15
|
finaliseUpload(baseUrl: string, apiKey: string, id: string, metadata: TAppMetadata, path: string, sha: string): Promise<Record<string, never>>;
|
|
16
16
|
getBinaryUploadUrl(baseUrl: string, apiKey: string, platform: "android" | "ios"): Promise<{
|
|
17
|
+
id: string;
|
|
17
18
|
message: string;
|
|
18
19
|
path: string;
|
|
19
20
|
token: string;
|
|
20
|
-
id: string;
|
|
21
21
|
}>;
|
|
22
22
|
getResultsForUpload(baseUrl: string, apiKey: string, uploadId: string): Promise<{
|
|
23
|
+
results?: import("../types/generated/schema.types").components["schemas"]["TResultResponse"][];
|
|
23
24
|
statusCode?: number;
|
|
24
|
-
results?: import("../types/schema.types").components["schemas"]["TResultResponse"][];
|
|
25
25
|
}>;
|
|
26
26
|
getUploadStatus(baseUrl: string, apiKey: string, options: {
|
|
27
27
|
name?: string;
|
|
@@ -37,7 +37,7 @@ export declare const ApiGateway: {
|
|
|
37
37
|
}>;
|
|
38
38
|
uploadFlow(baseUrl: string, apiKey: string, testFormData: FormData): Promise<{
|
|
39
39
|
message?: string;
|
|
40
|
-
results?: import("../types/schema.types").components["schemas"]["IDBResult"][];
|
|
40
|
+
results?: import("../types/generated/schema.types").components["schemas"]["IDBResult"][];
|
|
41
41
|
}>;
|
|
42
42
|
/**
|
|
43
43
|
* Generic report download method that handles both junit and allure reports
|
package/dist/methods.d.ts
CHANGED
|
@@ -1,12 +1,8 @@
|
|
|
1
1
|
import * as archiver from 'archiver';
|
|
2
|
-
import { TAppMetadata } from './types';
|
|
3
2
|
export declare const toBuffer: (archive: archiver.Archiver) => Promise<Buffer<ArrayBuffer>>;
|
|
4
3
|
export declare const compressFolderToBlob: (sourceDir: string) => Promise<Blob>;
|
|
5
4
|
export declare const compressFilesFromRelativePath: (basePath: string, files: string[], commonRoot: string) => Promise<Buffer<ArrayBuffer>>;
|
|
6
5
|
export declare const verifyAppZip: (zipPath: string) => Promise<void>;
|
|
7
|
-
export declare const extractAppMetadataAndroid: (appFilePath: string) => Promise<TAppMetadata>;
|
|
8
|
-
export declare const extractAppMetadataIosZip: (appFilePath: string) => Promise<TAppMetadata>;
|
|
9
|
-
export declare const extractAppMetadataIos: (appFolderPath: string) => Promise<TAppMetadata>;
|
|
10
6
|
export declare const uploadBinary: (filePath: string, apiUrl: string, apiKey: string, ignoreShaCheck?: boolean, log?: boolean) => Promise<string>;
|
|
11
7
|
/**
|
|
12
8
|
* Writes JSON data to a file with error handling
|
package/dist/methods.js
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.formatDurationSeconds = exports.writeJSONFile = exports.uploadBinary = exports.
|
|
3
|
+
exports.formatDurationSeconds = exports.writeJSONFile = exports.uploadBinary = exports.verifyAppZip = exports.compressFilesFromRelativePath = exports.compressFolderToBlob = exports.toBuffer = void 0;
|
|
4
4
|
const core_1 = require("@oclif/core");
|
|
5
|
-
// required polyfill for node 18
|
|
6
|
-
const file_1 = require("@web-std/file");
|
|
7
|
-
const AppInfoParser = require("app-info-parser");
|
|
8
5
|
const archiver = require("archiver");
|
|
9
|
-
const bplist_parser_1 = require("bplist-parser");
|
|
10
6
|
const node_crypto_1 = require("node:crypto");
|
|
11
7
|
const node_fs_1 = require("node:fs");
|
|
12
8
|
const promises_1 = require("node:fs/promises");
|
|
13
9
|
const path = require("node:path");
|
|
14
10
|
const node_stream_1 = require("node:stream");
|
|
15
11
|
const StreamZip = require("node-stream-zip");
|
|
16
|
-
const plist_1 = require("plist");
|
|
17
|
-
const cloud_1 = require("./commands/cloud");
|
|
18
12
|
const api_gateway_1 = require("./gateways/api-gateway");
|
|
19
13
|
const supabase_gateway_1 = require("./gateways/supabase-gateway");
|
|
14
|
+
const metadata_extractor_service_1 = require("./services/metadata-extractor.service");
|
|
15
|
+
const mimeTypeLookupByExtension = {
|
|
16
|
+
apk: 'application/vnd.android.package-archive',
|
|
17
|
+
yaml: 'application/x-yaml',
|
|
18
|
+
zip: 'application/zip',
|
|
19
|
+
};
|
|
20
20
|
const toBuffer = async (archive) => {
|
|
21
21
|
const chunks = [];
|
|
22
22
|
const writable = new node_stream_1.Writable();
|
|
@@ -76,64 +76,6 @@ const verifyAppZip = async (zipPath) => {
|
|
|
76
76
|
zip.close();
|
|
77
77
|
};
|
|
78
78
|
exports.verifyAppZip = verifyAppZip;
|
|
79
|
-
const extractAppMetadataAndroid = async (appFilePath) => {
|
|
80
|
-
const parser = new AppInfoParser(appFilePath);
|
|
81
|
-
const result = await parser.parse();
|
|
82
|
-
return { appId: result.package, platform: 'android' };
|
|
83
|
-
};
|
|
84
|
-
exports.extractAppMetadataAndroid = extractAppMetadataAndroid;
|
|
85
|
-
const parseInfoPlist = async (buffer) => {
|
|
86
|
-
let data;
|
|
87
|
-
const bufferType = buffer[0];
|
|
88
|
-
if (bufferType === 60 ||
|
|
89
|
-
bufferType === '<' ||
|
|
90
|
-
bufferType === 239) {
|
|
91
|
-
data = (0, plist_1.parse)(buffer.toString());
|
|
92
|
-
}
|
|
93
|
-
else if (bufferType === 98) {
|
|
94
|
-
data = (0, bplist_parser_1.parseBuffer)(buffer)[0];
|
|
95
|
-
}
|
|
96
|
-
else {
|
|
97
|
-
throw new Error('Unknown plist buffer type.');
|
|
98
|
-
}
|
|
99
|
-
return data;
|
|
100
|
-
};
|
|
101
|
-
const extractAppMetadataIosZip = async (appFilePath) => new Promise((resolve, reject) => {
|
|
102
|
-
const zip = new StreamZip({ file: appFilePath });
|
|
103
|
-
zip.on('ready', () => {
|
|
104
|
-
// Get all entries and sort them by path depth
|
|
105
|
-
const entries = Object.values(zip.entries());
|
|
106
|
-
const sortedEntries = entries.sort((a, b) => {
|
|
107
|
-
const aDepth = a.name.split('/').length;
|
|
108
|
-
const bDepth = b.name.split('/').length;
|
|
109
|
-
return aDepth - bDepth;
|
|
110
|
-
});
|
|
111
|
-
// Find the first Info.plist in the shallowest directory
|
|
112
|
-
const infoPlist = sortedEntries.find((e) => e.name.endsWith('.app/Info.plist'));
|
|
113
|
-
if (!infoPlist) {
|
|
114
|
-
reject(new Error('Failed to find info plist'));
|
|
115
|
-
return;
|
|
116
|
-
}
|
|
117
|
-
const buffer = zip.entryDataSync(infoPlist.name);
|
|
118
|
-
parseInfoPlist(buffer)
|
|
119
|
-
.then((data) => {
|
|
120
|
-
const appId = data.CFBundleIdentifier;
|
|
121
|
-
zip.close();
|
|
122
|
-
resolve({ appId, platform: 'ios' });
|
|
123
|
-
})
|
|
124
|
-
.catch(reject);
|
|
125
|
-
});
|
|
126
|
-
zip.on('error', reject);
|
|
127
|
-
});
|
|
128
|
-
exports.extractAppMetadataIosZip = extractAppMetadataIosZip;
|
|
129
|
-
const extractAppMetadataIos = async (appFolderPath) => {
|
|
130
|
-
const infoPlistPath = path.normalize(path.join(appFolderPath, 'Info.plist'));
|
|
131
|
-
const buffer = await (0, promises_1.readFile)(infoPlistPath);
|
|
132
|
-
const data = await parseInfoPlist(buffer);
|
|
133
|
-
const appId = data.CFBundleIdentifier;
|
|
134
|
-
return { appId, platform: 'ios' };
|
|
135
|
-
};
|
|
136
|
-
exports.extractAppMetadataIos = extractAppMetadataIos;
|
|
137
79
|
const uploadBinary = async (filePath, apiUrl, apiKey, ignoreShaCheck = false, log = true) => {
|
|
138
80
|
if (log) {
|
|
139
81
|
core_1.ux.action.start('Checking and uploading binary', 'Initializing', {
|
|
@@ -143,14 +85,14 @@ const uploadBinary = async (filePath, apiUrl, apiKey, ignoreShaCheck = false, lo
|
|
|
143
85
|
let file;
|
|
144
86
|
if (filePath?.endsWith('.app')) {
|
|
145
87
|
const zippedAppBlob = await (0, exports.compressFolderToBlob)(filePath);
|
|
146
|
-
file = new
|
|
88
|
+
file = new File([zippedAppBlob], filePath + '.zip');
|
|
147
89
|
}
|
|
148
90
|
else {
|
|
149
91
|
const fileBuffer = await (0, promises_1.readFile)(filePath);
|
|
150
92
|
const binaryBlob = new Blob([new Uint8Array(fileBuffer)], {
|
|
151
|
-
type:
|
|
93
|
+
type: mimeTypeLookupByExtension[filePath.split('.').pop()],
|
|
152
94
|
});
|
|
153
|
-
file = new
|
|
95
|
+
file = new File([binaryBlob], filePath);
|
|
154
96
|
}
|
|
155
97
|
let sha;
|
|
156
98
|
try {
|
|
@@ -179,18 +121,11 @@ const uploadBinary = async (filePath, apiUrl, apiKey, ignoreShaCheck = false, lo
|
|
|
179
121
|
const { id, message, path, token } = await api_gateway_1.ApiGateway.getBinaryUploadUrl(apiUrl, apiKey, filePath?.endsWith('.apk') ? 'android' : 'ios');
|
|
180
122
|
if (!path)
|
|
181
123
|
throw new Error(message);
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
? await (0, exports.extractAppMetadataIosZip)(filePath)
|
|
188
|
-
: await (0, exports.extractAppMetadataIos)(filePath);
|
|
189
|
-
}
|
|
190
|
-
catch {
|
|
191
|
-
if (log) {
|
|
192
|
-
core_1.ux.warn('Failed to extract app metadata, please share with support@devicecloud.dev so we can improve our parsing.');
|
|
193
|
-
}
|
|
124
|
+
// Extract app metadata using the service
|
|
125
|
+
const metadataExtractor = new metadata_extractor_service_1.MetadataExtractorService();
|
|
126
|
+
const metadata = await metadataExtractor.extract(filePath);
|
|
127
|
+
if (!metadata) {
|
|
128
|
+
throw new Error(`Failed to extract metadata from ${filePath}. Supported formats: .apk, .app, .zip`);
|
|
194
129
|
}
|
|
195
130
|
const env = apiUrl === 'https://api.devicecloud.dev' ? 'prod' : 'dev';
|
|
196
131
|
await supabase_gateway_1.SupabaseGateway.uploadToSignedUrl(env, path, token, file);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { CompatibilityData } from '../utils/compatibility';
|
|
2
|
+
export interface DeviceValidationOptions {
|
|
3
|
+
debug?: boolean;
|
|
4
|
+
logger?: (message: string) => void;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Service for validating device configurations against compatibility data
|
|
8
|
+
*/
|
|
9
|
+
export declare class DeviceValidationService {
|
|
10
|
+
/**
|
|
11
|
+
* Validate Android device configuration
|
|
12
|
+
* @param androidApiLevel Android API level to validate
|
|
13
|
+
* @param androidDevice Android device model to validate
|
|
14
|
+
* @param googlePlay Whether Google Play services are enabled
|
|
15
|
+
* @param compatibilityData Compatibility data from API
|
|
16
|
+
* @param options Validation options
|
|
17
|
+
* @throws Error if device/API level combination is not supported
|
|
18
|
+
*/
|
|
19
|
+
validateAndroidDevice(androidApiLevel: string | undefined, androidDevice: string | undefined, googlePlay: boolean, compatibilityData: CompatibilityData, options?: DeviceValidationOptions): void;
|
|
20
|
+
/**
|
|
21
|
+
* Validate iOS device configuration
|
|
22
|
+
* @param iOSVersion iOS version to validate
|
|
23
|
+
* @param iOSDevice iOS device model to validate
|
|
24
|
+
* @param compatibilityData Compatibility data from API
|
|
25
|
+
* @param options Validation options
|
|
26
|
+
* @throws Error if device/version combination is not supported
|
|
27
|
+
*/
|
|
28
|
+
validateiOSDevice(iOSVersion: string | undefined, iOSDevice: string | undefined, compatibilityData: CompatibilityData, options?: DeviceValidationOptions): void;
|
|
29
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DeviceValidationService = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Service for validating device configurations against compatibility data
|
|
6
|
+
*/
|
|
7
|
+
class DeviceValidationService {
|
|
8
|
+
/**
|
|
9
|
+
* Validate Android device configuration
|
|
10
|
+
* @param androidApiLevel Android API level to validate
|
|
11
|
+
* @param androidDevice Android device model to validate
|
|
12
|
+
* @param googlePlay Whether Google Play services are enabled
|
|
13
|
+
* @param compatibilityData Compatibility data from API
|
|
14
|
+
* @param options Validation options
|
|
15
|
+
* @throws Error if device/API level combination is not supported
|
|
16
|
+
*/
|
|
17
|
+
validateAndroidDevice(androidApiLevel, androidDevice, googlePlay, compatibilityData, options = {}) {
|
|
18
|
+
const { debug = false, logger } = options;
|
|
19
|
+
if (!androidApiLevel && !androidDevice) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const androidDeviceID = androidDevice || 'pixel-7';
|
|
23
|
+
const lookup = googlePlay
|
|
24
|
+
? compatibilityData.androidPlay
|
|
25
|
+
: compatibilityData.android;
|
|
26
|
+
const supportedAndroidVersions = lookup?.[androidDeviceID] || [];
|
|
27
|
+
const version = androidApiLevel || '34';
|
|
28
|
+
if (supportedAndroidVersions.length === 0) {
|
|
29
|
+
throw new Error(`We don't support that device configuration - please check the docs for supported devices: https://docs.devicecloud.dev/getting-started/devices-configuration`);
|
|
30
|
+
}
|
|
31
|
+
if (Array.isArray(supportedAndroidVersions) &&
|
|
32
|
+
!supportedAndroidVersions.includes(version)) {
|
|
33
|
+
throw new Error(`${androidDeviceID} ${googlePlay ? '(Play Store) ' : ''}only supports these Android API levels: ${supportedAndroidVersions.join(', ')}`);
|
|
34
|
+
}
|
|
35
|
+
if (debug && logger) {
|
|
36
|
+
logger(`DEBUG: Android device: ${androidDeviceID}`);
|
|
37
|
+
logger(`DEBUG: Android API level: ${version}`);
|
|
38
|
+
logger(`DEBUG: Google Play enabled: ${googlePlay}`);
|
|
39
|
+
logger(`DEBUG: Supported Android versions: ${supportedAndroidVersions.join(', ')}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Validate iOS device configuration
|
|
44
|
+
* @param iOSVersion iOS version to validate
|
|
45
|
+
* @param iOSDevice iOS device model to validate
|
|
46
|
+
* @param compatibilityData Compatibility data from API
|
|
47
|
+
* @param options Validation options
|
|
48
|
+
* @throws Error if device/version combination is not supported
|
|
49
|
+
*/
|
|
50
|
+
validateiOSDevice(iOSVersion, iOSDevice, compatibilityData, options = {}) {
|
|
51
|
+
const { debug = false, logger } = options;
|
|
52
|
+
if (!iOSVersion && !iOSDevice) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const iOSDeviceID = iOSDevice || 'iphone-14';
|
|
56
|
+
const supportediOSVersions = compatibilityData?.ios?.[iOSDeviceID] || [];
|
|
57
|
+
const version = iOSVersion || '17';
|
|
58
|
+
if (supportediOSVersions.length === 0) {
|
|
59
|
+
throw new Error(`Device ${iOSDeviceID} is not supported. Please check the docs for supported devices: https://docs.devicecloud.dev/getting-started/devices-configuration`);
|
|
60
|
+
}
|
|
61
|
+
if (Array.isArray(supportediOSVersions) &&
|
|
62
|
+
!supportediOSVersions.includes(version)) {
|
|
63
|
+
throw new Error(`${iOSDeviceID} only supports these iOS versions: ${supportediOSVersions.join(', ')}`);
|
|
64
|
+
}
|
|
65
|
+
if (debug && logger) {
|
|
66
|
+
logger(`DEBUG: iOS device: ${iOSDeviceID}`);
|
|
67
|
+
logger(`DEBUG: iOS version: ${version}`);
|
|
68
|
+
logger(`DEBUG: Supported iOS versions: ${supportediOSVersions.join(', ')}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
exports.DeviceValidationService = DeviceValidationService;
|
|
@@ -20,7 +20,7 @@ interface IExecutionOrder {
|
|
|
20
20
|
continueOnFailure: boolean;
|
|
21
21
|
flowsOrder: string[];
|
|
22
22
|
}
|
|
23
|
-
interface IExecutionPlan {
|
|
23
|
+
export interface IExecutionPlan {
|
|
24
24
|
allExcludeTags?: null | string[];
|
|
25
25
|
allIncludeTags?: null | string[];
|
|
26
26
|
flowMetadata: Record<string, Record<string, unknown>>;
|
|
@@ -4,14 +4,14 @@ exports.plan = plan;
|
|
|
4
4
|
const glob_1 = require("glob");
|
|
5
5
|
const fs = require("node:fs");
|
|
6
6
|
const path = require("node:path");
|
|
7
|
-
const
|
|
7
|
+
const execution_plan_utils_1 = require("./execution-plan.utils");
|
|
8
8
|
async function checkDependencies(input) {
|
|
9
9
|
const checkedDependencies = [];
|
|
10
10
|
const uncheckedDependencies = [input];
|
|
11
11
|
while (uncheckedDependencies.length > 0) {
|
|
12
12
|
const fileToCheck = uncheckedDependencies.shift();
|
|
13
|
-
const { config, testSteps } = (0,
|
|
14
|
-
const { allErrors, allFiles } = (0,
|
|
13
|
+
const { config, testSteps } = (0, execution_plan_utils_1.readTestYamlFileAsJson)(fileToCheck);
|
|
14
|
+
const { allErrors, allFiles } = (0, execution_plan_utils_1.processDependencies)({
|
|
15
15
|
config,
|
|
16
16
|
input: fileToCheck,
|
|
17
17
|
testSteps,
|
|
@@ -21,7 +21,7 @@ async function checkDependencies(input) {
|
|
|
21
21
|
allErrors.join('\n'));
|
|
22
22
|
}
|
|
23
23
|
for (const file of allFiles) {
|
|
24
|
-
if (!(0,
|
|
24
|
+
if (!(0, execution_plan_utils_1.isFlowFile)(file)) {
|
|
25
25
|
// js/media files don't have dependencies
|
|
26
26
|
checkedDependencies.push(file);
|
|
27
27
|
}
|
|
@@ -45,7 +45,7 @@ function getWorkspaceConfig(input, unfilteredFlowFiles) {
|
|
|
45
45
|
const possibleConfigPaths = new Set([path.join(input, 'config.yaml'), path.join(input, 'config.yml')].map((p) => path.normalize(p)));
|
|
46
46
|
const configFilePath = unfilteredFlowFiles.find((file) => possibleConfigPaths.has(path.normalize(file)));
|
|
47
47
|
const config = configFilePath
|
|
48
|
-
? (0,
|
|
48
|
+
? (0, execution_plan_utils_1.readYamlFileAsJson)(configFilePath)
|
|
49
49
|
: {};
|
|
50
50
|
return config;
|
|
51
51
|
}
|
|
@@ -75,7 +75,7 @@ async function plan(input, includeTags, excludeTags, excludeFlows, configFile, d
|
|
|
75
75
|
normalizedInput.endsWith('config.yml')) {
|
|
76
76
|
throw new Error('If using config.yaml, pass the workspace folder path, not the config file or a custom path via --config');
|
|
77
77
|
}
|
|
78
|
-
const { config } = (0,
|
|
78
|
+
const { config } = (0, execution_plan_utils_1.readTestYamlFileAsJson)(normalizedInput);
|
|
79
79
|
const flowOverrides = {};
|
|
80
80
|
if (config) {
|
|
81
81
|
flowMetadata[normalizedInput] = config;
|
|
@@ -90,7 +90,7 @@ async function plan(input, includeTags, excludeTags, excludeFlows, configFile, d
|
|
|
90
90
|
totalFlowFiles: 1,
|
|
91
91
|
};
|
|
92
92
|
}
|
|
93
|
-
let unfilteredFlowFiles = await (0,
|
|
93
|
+
let unfilteredFlowFiles = await (0, execution_plan_utils_1.readDirectory)(normalizedInput, execution_plan_utils_1.isFlowFile);
|
|
94
94
|
if (unfilteredFlowFiles.length === 0) {
|
|
95
95
|
throw new Error(`Flow directory does not contain any Flow files: ${path.resolve(normalizedInput)}`);
|
|
96
96
|
}
|
|
@@ -101,7 +101,7 @@ async function plan(input, includeTags, excludeTags, excludeFlows, configFile, d
|
|
|
101
101
|
if (!fs.existsSync(configFilePath)) {
|
|
102
102
|
throw new Error(`Config file does not exist: ${configFilePath}`);
|
|
103
103
|
}
|
|
104
|
-
workspaceConfig = (0,
|
|
104
|
+
workspaceConfig = (0, execution_plan_utils_1.readYamlFileAsJson)(configFilePath);
|
|
105
105
|
}
|
|
106
106
|
else {
|
|
107
107
|
workspaceConfig = getWorkspaceConfig(normalizedInput, unfilteredFlowFiles);
|
|
@@ -154,7 +154,7 @@ async function plan(input, includeTags, excludeTags, excludeFlows, configFile, d
|
|
|
154
154
|
const configPerFlowFile =
|
|
155
155
|
// eslint-disable-next-line unicorn/no-array-reduce
|
|
156
156
|
unfilteredFlowFiles.reduce((acc, filePath) => {
|
|
157
|
-
const { config } = (0,
|
|
157
|
+
const { config } = (0, execution_plan_utils_1.readTestYamlFileAsJson)(filePath);
|
|
158
158
|
acc[filePath] = config;
|
|
159
159
|
return acc;
|
|
160
160
|
}, {});
|
|
@@ -207,7 +207,7 @@ async function plan(input, includeTags, excludeTags, excludeFlows, configFile, d
|
|
|
207
207
|
if (debug && flowOrder !== normalizedFlowOrder) {
|
|
208
208
|
console.log(`[DEBUG] Stripping trailing extension: "${flowOrder}" -> "${normalizedFlowOrder}"`);
|
|
209
209
|
}
|
|
210
|
-
return (0,
|
|
210
|
+
return (0, execution_plan_utils_1.getFlowsToRunInSequence)(pathsByName, [normalizedFlowOrder], debug);
|
|
211
211
|
})
|
|
212
212
|
.flat() || [];
|
|
213
213
|
if (debug) {
|
|
@@ -4,7 +4,6 @@ exports.processDependencies = exports.checkIfFilesExistInWorkspace = exports.rea
|
|
|
4
4
|
exports.getFlowsToRunInSequence = getFlowsToRunInSequence;
|
|
5
5
|
exports.isFlowFile = isFlowFile;
|
|
6
6
|
exports.readDirectory = readDirectory;
|
|
7
|
-
/* eslint-disable unicorn/filename-case */
|
|
8
7
|
const yaml = require("js-yaml");
|
|
9
8
|
const fs = require("node:fs");
|
|
10
9
|
const path = require("node:path");
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export interface TAppMetadata {
|
|
2
|
+
appId: string;
|
|
3
|
+
platform: 'android' | 'ios';
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Interface for platform-specific metadata extractors
|
|
7
|
+
*/
|
|
8
|
+
export interface IMetadataExtractor {
|
|
9
|
+
canHandle(filePath: string): boolean;
|
|
10
|
+
extract(filePath: string): Promise<TAppMetadata>;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Extracts metadata from Android APK files
|
|
14
|
+
*/
|
|
15
|
+
export declare class AndroidMetadataExtractor implements IMetadataExtractor {
|
|
16
|
+
canHandle(filePath: string): boolean;
|
|
17
|
+
extract(filePath: string): Promise<TAppMetadata>;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Extracts metadata from iOS .app directories
|
|
21
|
+
*/
|
|
22
|
+
export declare class IosAppMetadataExtractor implements IMetadataExtractor {
|
|
23
|
+
canHandle(filePath: string): boolean;
|
|
24
|
+
extract(filePath: string): Promise<TAppMetadata>;
|
|
25
|
+
private parseInfoPlist;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Extracts metadata from iOS .zip files containing .app bundles
|
|
29
|
+
*/
|
|
30
|
+
export declare class IosZipMetadataExtractor implements IMetadataExtractor {
|
|
31
|
+
canHandle(filePath: string): boolean;
|
|
32
|
+
extract(filePath: string): Promise<TAppMetadata>;
|
|
33
|
+
private parseInfoPlist;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Service for extracting app metadata from various file formats
|
|
37
|
+
*/
|
|
38
|
+
export declare class MetadataExtractorService {
|
|
39
|
+
private extractors;
|
|
40
|
+
/**
|
|
41
|
+
* Extract app metadata from a file
|
|
42
|
+
* @param filePath - Path to the app file (.apk, .app, or .zip)
|
|
43
|
+
* @returns App metadata or undefined if extraction fails
|
|
44
|
+
*/
|
|
45
|
+
extract(filePath: string): Promise<TAppMetadata | undefined>;
|
|
46
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MetadataExtractorService = exports.IosZipMetadataExtractor = exports.IosAppMetadataExtractor = exports.AndroidMetadataExtractor = void 0;
|
|
4
|
+
const AppInfoParser = require("app-info-parser");
|
|
5
|
+
const bplist_parser_1 = require("bplist-parser");
|
|
6
|
+
const promises_1 = require("node:fs/promises");
|
|
7
|
+
const path = require("node:path");
|
|
8
|
+
const StreamZip = require("node-stream-zip");
|
|
9
|
+
const plist_1 = require("plist");
|
|
10
|
+
/**
|
|
11
|
+
* Extracts metadata from Android APK files
|
|
12
|
+
*/
|
|
13
|
+
class AndroidMetadataExtractor {
|
|
14
|
+
canHandle(filePath) {
|
|
15
|
+
return filePath.endsWith('.apk');
|
|
16
|
+
}
|
|
17
|
+
async extract(filePath) {
|
|
18
|
+
const parser = new AppInfoParser(filePath);
|
|
19
|
+
const result = await parser.parse();
|
|
20
|
+
return { appId: result.package, platform: 'android' };
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
exports.AndroidMetadataExtractor = AndroidMetadataExtractor;
|
|
24
|
+
/**
|
|
25
|
+
* Extracts metadata from iOS .app directories
|
|
26
|
+
*/
|
|
27
|
+
class IosAppMetadataExtractor {
|
|
28
|
+
canHandle(filePath) {
|
|
29
|
+
return filePath.endsWith('.app');
|
|
30
|
+
}
|
|
31
|
+
async extract(filePath) {
|
|
32
|
+
const infoPlistPath = path.normalize(path.join(filePath, 'Info.plist'));
|
|
33
|
+
const buffer = await (0, promises_1.readFile)(infoPlistPath);
|
|
34
|
+
const data = await this.parseInfoPlist(buffer);
|
|
35
|
+
const appId = data.CFBundleIdentifier;
|
|
36
|
+
return { appId, platform: 'ios' };
|
|
37
|
+
}
|
|
38
|
+
async parseInfoPlist(buffer) {
|
|
39
|
+
let data;
|
|
40
|
+
const bufferType = buffer[0];
|
|
41
|
+
if (bufferType === 60 ||
|
|
42
|
+
bufferType === '<' ||
|
|
43
|
+
bufferType === 239) {
|
|
44
|
+
data = (0, plist_1.parse)(buffer.toString());
|
|
45
|
+
}
|
|
46
|
+
else if (bufferType === 98) {
|
|
47
|
+
data = (0, bplist_parser_1.parseBuffer)(buffer)[0];
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
throw new Error('Unknown plist buffer type.');
|
|
51
|
+
}
|
|
52
|
+
return data;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
exports.IosAppMetadataExtractor = IosAppMetadataExtractor;
|
|
56
|
+
/**
|
|
57
|
+
* Extracts metadata from iOS .zip files containing .app bundles
|
|
58
|
+
*/
|
|
59
|
+
class IosZipMetadataExtractor {
|
|
60
|
+
canHandle(filePath) {
|
|
61
|
+
return filePath.endsWith('.zip');
|
|
62
|
+
}
|
|
63
|
+
async extract(filePath) {
|
|
64
|
+
return new Promise((resolve, reject) => {
|
|
65
|
+
const zip = new StreamZip({ file: filePath });
|
|
66
|
+
zip.on('ready', () => {
|
|
67
|
+
// Get all entries and sort them by path depth
|
|
68
|
+
const entries = Object.values(zip.entries());
|
|
69
|
+
const sortedEntries = entries.sort((a, b) => {
|
|
70
|
+
const aDepth = a.name.split('/').length;
|
|
71
|
+
const bDepth = b.name.split('/').length;
|
|
72
|
+
return aDepth - bDepth;
|
|
73
|
+
});
|
|
74
|
+
// Find the first Info.plist in the shallowest directory
|
|
75
|
+
const infoPlist = sortedEntries.find((e) => e.name.endsWith('.app/Info.plist'));
|
|
76
|
+
if (!infoPlist) {
|
|
77
|
+
reject(new Error('Failed to find info plist'));
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const buffer = zip.entryDataSync(infoPlist.name);
|
|
81
|
+
this.parseInfoPlist(buffer)
|
|
82
|
+
.then((data) => {
|
|
83
|
+
const appId = data.CFBundleIdentifier;
|
|
84
|
+
zip.close();
|
|
85
|
+
resolve({ appId, platform: 'ios' });
|
|
86
|
+
})
|
|
87
|
+
.catch(reject);
|
|
88
|
+
});
|
|
89
|
+
zip.on('error', reject);
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
async parseInfoPlist(buffer) {
|
|
93
|
+
let data;
|
|
94
|
+
const bufferType = buffer[0];
|
|
95
|
+
if (bufferType === 60 ||
|
|
96
|
+
bufferType === '<' ||
|
|
97
|
+
bufferType === 239) {
|
|
98
|
+
data = (0, plist_1.parse)(buffer.toString());
|
|
99
|
+
}
|
|
100
|
+
else if (bufferType === 98) {
|
|
101
|
+
data = (0, bplist_parser_1.parseBuffer)(buffer)[0];
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
throw new Error('Unknown plist buffer type.');
|
|
105
|
+
}
|
|
106
|
+
return data;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
exports.IosZipMetadataExtractor = IosZipMetadataExtractor;
|
|
110
|
+
/**
|
|
111
|
+
* Service for extracting app metadata from various file formats
|
|
112
|
+
*/
|
|
113
|
+
class MetadataExtractorService {
|
|
114
|
+
extractors = [
|
|
115
|
+
new AndroidMetadataExtractor(),
|
|
116
|
+
new IosZipMetadataExtractor(),
|
|
117
|
+
new IosAppMetadataExtractor(),
|
|
118
|
+
];
|
|
119
|
+
/**
|
|
120
|
+
* Extract app metadata from a file
|
|
121
|
+
* @param filePath - Path to the app file (.apk, .app, or .zip)
|
|
122
|
+
* @returns App metadata or undefined if extraction fails
|
|
123
|
+
*/
|
|
124
|
+
async extract(filePath) {
|
|
125
|
+
const extractor = this.extractors.find((e) => e.canHandle(filePath));
|
|
126
|
+
if (!extractor) {
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
try {
|
|
130
|
+
return await extractor.extract(filePath);
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
console.warn('Failed to extract app metadata, please share with support@devicecloud.dev so we can improve our parsing.');
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
exports.MetadataExtractorService = MetadataExtractorService;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface MoropoDownloadOptions {
|
|
2
|
+
apiKey: string;
|
|
3
|
+
branchName?: string;
|
|
4
|
+
debug?: boolean;
|
|
5
|
+
json?: boolean;
|
|
6
|
+
logger?: (message: string) => void;
|
|
7
|
+
quiet?: boolean;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Service for downloading and extracting Moropo tests from the Moropo API
|
|
11
|
+
*/
|
|
12
|
+
export declare class MoropoService {
|
|
13
|
+
private readonly MOROPO_API_URL;
|
|
14
|
+
/**
|
|
15
|
+
* Download and extract Moropo tests from the API
|
|
16
|
+
* @param options Download configuration options
|
|
17
|
+
* @returns Path to the extracted Moropo tests directory
|
|
18
|
+
*/
|
|
19
|
+
downloadAndExtract(options: MoropoDownloadOptions): Promise<string>;
|
|
20
|
+
}
|