@capawesome/cli 3.11.0 → 4.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.
Files changed (48) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/dist/commands/apps/builds/cancel.js +1 -1
  3. package/dist/commands/apps/builds/create.js +58 -50
  4. package/dist/commands/apps/builds/download.js +27 -3
  5. package/dist/commands/apps/bundles/create.js +5 -449
  6. package/dist/commands/apps/bundles/delete.js +3 -68
  7. package/dist/commands/apps/bundles/update.js +3 -66
  8. package/dist/commands/apps/channels/create.js +5 -8
  9. package/dist/commands/apps/channels/create.test.js +6 -9
  10. package/dist/commands/apps/channels/delete.js +3 -2
  11. package/dist/commands/apps/channels/get.js +2 -12
  12. package/dist/commands/apps/channels/get.test.js +1 -2
  13. package/dist/commands/apps/channels/list.js +2 -10
  14. package/dist/commands/apps/channels/list.test.js +2 -3
  15. package/dist/commands/apps/channels/pause.js +85 -0
  16. package/dist/commands/apps/channels/resume.js +85 -0
  17. package/dist/commands/apps/channels/update.js +4 -7
  18. package/dist/commands/apps/channels/update.test.js +2 -4
  19. package/dist/commands/apps/create.js +1 -1
  20. package/dist/commands/apps/delete.js +3 -2
  21. package/dist/commands/apps/deployments/cancel.js +1 -1
  22. package/dist/commands/apps/deployments/create.js +82 -31
  23. package/dist/commands/apps/devices/delete.js +3 -2
  24. package/dist/commands/apps/environments/create.js +1 -1
  25. package/dist/commands/apps/environments/delete.js +3 -2
  26. package/dist/commands/apps/liveupdates/bundle.js +117 -0
  27. package/dist/commands/apps/liveupdates/generate-manifest.js +39 -0
  28. package/dist/commands/{manifests/generate.test.js → apps/liveupdates/generate-manifest.test.js} +6 -6
  29. package/dist/commands/apps/liveupdates/register.js +291 -0
  30. package/dist/commands/apps/{bundles/create.test.js → liveupdates/register.test.js} +123 -111
  31. package/dist/commands/apps/liveupdates/rollback.js +171 -0
  32. package/dist/commands/apps/liveupdates/rollout.js +147 -0
  33. package/dist/commands/apps/liveupdates/upload.js +420 -0
  34. package/dist/commands/apps/liveupdates/upload.test.js +325 -0
  35. package/dist/commands/manifests/generate.js +2 -27
  36. package/dist/commands/organizations/create.js +1 -1
  37. package/dist/index.js +8 -0
  38. package/dist/services/app-builds.js +9 -2
  39. package/dist/services/app-channels.js +19 -0
  40. package/dist/services/app-deployments.js +24 -14
  41. package/dist/services/config.js +2 -0
  42. package/dist/utils/app-environments.js +2 -1
  43. package/dist/utils/time-format.js +26 -0
  44. package/package.json +3 -3
  45. package/dist/commands/apps/bundles/delete.test.js +0 -142
  46. package/dist/commands/apps/bundles/update.test.js +0 -144
  47. package/dist/utils/capacitor-config.js +0 -96
  48. package/dist/utils/package-json.js +0 -58
@@ -13,7 +13,8 @@ export default defineCommand({
13
13
  appId: z.string().optional().describe('ID of the app.'),
14
14
  environmentId: z.string().optional().describe('ID of the environment. Either the ID or name must be provided.'),
15
15
  name: z.string().optional().describe('Name of the environment. Either the ID or name must be provided.'),
16
- })),
16
+ yes: z.boolean().optional().describe('Skip confirmation prompt.'),
17
+ }), { y: 'yes' }),
17
18
  action: async (options, args) => {
18
19
  let { appId, environmentId, name } = options;
19
20
  if (!authorizationService.hasAuthorizationToken()) {
@@ -69,7 +70,7 @@ export default defineCommand({
69
70
  });
70
71
  environmentId = selectedEnvironmentId;
71
72
  }
72
- if (isInteractive()) {
73
+ if (!options.yes && isInteractive()) {
73
74
  const confirmed = await prompt('Are you sure you want to delete this environment?', {
74
75
  type: 'confirm',
75
76
  });
@@ -0,0 +1,117 @@
1
+ import { isInteractive } from '../../../utils/environment.js';
2
+ import { fileExistsAtPath, isDirectory } from '../../../utils/file.js';
3
+ import { generateManifestJson } from '../../../utils/manifest.js';
4
+ import { prompt } from '../../../utils/prompt.js';
5
+ import zip from '../../../utils/zip.js';
6
+ import { defineCommand, defineOptions } from '@robingenz/zli';
7
+ import consola from 'consola';
8
+ import fs from 'fs';
9
+ import pathModule from 'path';
10
+ import { z } from 'zod';
11
+ export default defineCommand({
12
+ description: 'Generate manifest file and compress web assets into a bundle.zip file.',
13
+ options: defineOptions(z.object({
14
+ inputPath: z.string().optional().describe('Path to the web assets directory.'),
15
+ outputPath: z
16
+ .string()
17
+ .optional()
18
+ .default('./bundle.zip')
19
+ .describe('Output path for the generated artifact file (default: ./bundle.zip).'),
20
+ overwrite: z
21
+ .boolean()
22
+ .optional()
23
+ .default(false)
24
+ .describe('Overwrite output file if it already exists (default: false).'),
25
+ skipManifest: z.boolean().optional().default(false).describe('Skip manifest file generation (default: false).'),
26
+ })),
27
+ action: async (options, args) => {
28
+ let { inputPath, outputPath, overwrite, skipManifest } = options;
29
+ // 1. Input path resolution
30
+ if (!inputPath) {
31
+ if (!isInteractive()) {
32
+ consola.error('You must provide an input path when running in non-interactive environment.');
33
+ process.exit(1);
34
+ }
35
+ consola.warn('Make sure you have built your web assets before creating a bundle (e.g., `npm run build`).');
36
+ const response = await prompt('Enter the path to the web assets directory (e.g., `dist` or `www`):', {
37
+ type: 'text',
38
+ });
39
+ inputPath = response;
40
+ }
41
+ // Convert to absolute path
42
+ inputPath = pathModule.resolve(inputPath);
43
+ // Validate input path exists
44
+ const inputExists = await fileExistsAtPath(inputPath);
45
+ if (!inputExists) {
46
+ consola.error(`Input path does not exist: ${inputPath}`);
47
+ process.exit(1);
48
+ }
49
+ // Validate input is a directory
50
+ const inputIsDirectory = await isDirectory(inputPath);
51
+ if (!inputIsDirectory) {
52
+ consola.error(`Input path must be a directory, not a file: ${inputPath}`);
53
+ process.exit(1);
54
+ }
55
+ // Validate directory contains index.html
56
+ const indexHtmlPath = pathModule.join(inputPath, 'index.html');
57
+ const hasIndexHtml = await fileExistsAtPath(indexHtmlPath);
58
+ if (!hasIndexHtml) {
59
+ consola.error(`Directory must contain an index.html file: ${inputPath}`);
60
+ process.exit(1);
61
+ }
62
+ // 2. Output path resolution
63
+ if (!outputPath) {
64
+ outputPath = './bundle.zip';
65
+ }
66
+ outputPath = pathModule.resolve(outputPath);
67
+ // 3. Check if output exists and handle overwrite
68
+ const outputExists = await fileExistsAtPath(outputPath);
69
+ if (outputExists) {
70
+ if (!overwrite) {
71
+ if (!isInteractive()) {
72
+ consola.error(`Output file already exists: ${outputPath}. Use --overwrite flag to skip confirmation or run in interactive mode.`);
73
+ process.exit(1);
74
+ }
75
+ const shouldOverwrite = await prompt('Output file already exists. Overwrite?', {
76
+ type: 'confirm',
77
+ initial: false,
78
+ });
79
+ if (!shouldOverwrite) {
80
+ consola.info('Operation cancelled.');
81
+ process.exit(0);
82
+ }
83
+ }
84
+ }
85
+ // Validate parent directory exists
86
+ const outputDir = pathModule.dirname(outputPath);
87
+ const outputDirExists = await fileExistsAtPath(outputDir);
88
+ if (!outputDirExists) {
89
+ consola.error(`Output directory does not exist: ${outputDir}`);
90
+ process.exit(1);
91
+ }
92
+ // 4. Generate bundle
93
+ consola.start('Generating bundle...');
94
+ try {
95
+ // Generate manifest (unless skipped)
96
+ if (!skipManifest) {
97
+ consola.info('Generating manifest file...');
98
+ await generateManifestJson(inputPath);
99
+ }
100
+ // Compress directory
101
+ consola.info('Compressing directory...');
102
+ const buffer = await zip.zipFolder(inputPath);
103
+ // Write output
104
+ consola.info(`Writing output to ${outputPath}...`);
105
+ await fs.promises.writeFile(outputPath, buffer);
106
+ // Success output
107
+ consola.success(`Bundle created successfully!`);
108
+ }
109
+ catch (error) {
110
+ consola.error('Failed to generate bundle:');
111
+ if (error instanceof Error) {
112
+ consola.error(error.message);
113
+ }
114
+ process.exit(1);
115
+ }
116
+ },
117
+ });
@@ -0,0 +1,39 @@
1
+ import { isInteractive } from '../../../utils/environment.js';
2
+ import { fileExistsAtPath } from '../../../utils/file.js';
3
+ import { generateManifestJson } from '../../../utils/manifest.js';
4
+ import { prompt } from '../../../utils/prompt.js';
5
+ import { defineCommand, defineOptions } from '@robingenz/zli';
6
+ import consola from 'consola';
7
+ import { z } from 'zod';
8
+ export default defineCommand({
9
+ description: 'Generate a manifest file.',
10
+ options: defineOptions(z.object({
11
+ path: z.string().optional().describe('Path to the web assets folder (e.g. `www` or `dist`).'),
12
+ })),
13
+ action: async (options, args) => {
14
+ let path = options.path;
15
+ if (!path) {
16
+ if (!isInteractive()) {
17
+ consola.error('You must provide the path to the web assets folder when running in non-interactive environment.');
18
+ process.exit(1);
19
+ }
20
+ consola.warn('Make sure you have built your web assets before generating the manifest (e.g., `npm run build`).');
21
+ path = await prompt('Enter the path to the web assets folder (e.g., `dist` or `www`):', {
22
+ type: 'text',
23
+ });
24
+ if (!path) {
25
+ consola.error('You must provide a path to the web assets folder.');
26
+ process.exit(1);
27
+ }
28
+ }
29
+ // Check if the path exists
30
+ const pathExists = await fileExistsAtPath(path);
31
+ if (!pathExists) {
32
+ consola.error(`The path does not exist.`);
33
+ process.exit(1);
34
+ }
35
+ // Generate the manifest file
36
+ await generateManifestJson(path);
37
+ consola.success('Manifest file generated.');
38
+ },
39
+ });
@@ -1,9 +1,9 @@
1
- import { fileExistsAtPath } from '../../utils/file.js';
2
- import { generateManifestJson } from '../../utils/manifest.js';
3
- import { prompt } from '../../utils/prompt.js';
1
+ import { fileExistsAtPath } from '../../../utils/file.js';
2
+ import { generateManifestJson } from '../../../utils/manifest.js';
3
+ import { prompt } from '../../../utils/prompt.js';
4
4
  import consola from 'consola';
5
5
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
6
- import generateManifestCommand from './generate.js';
6
+ import generateManifestCommand from './generate-manifest.js';
7
7
  // Mock dependencies
8
8
  vi.mock('@/utils/file.js');
9
9
  vi.mock('@/utils/manifest.js');
@@ -12,7 +12,7 @@ vi.mock('consola');
12
12
  vi.mock('@/utils/environment.js', () => ({
13
13
  isInteractive: () => true,
14
14
  }));
15
- describe('manifests-generate', () => {
15
+ describe('apps-liveupdates-generatemanifest', () => {
16
16
  const mockFileExistsAtPath = vi.mocked(fileExistsAtPath);
17
17
  const mockGenerateManifestJson = vi.mocked(generateManifestJson);
18
18
  const mockPrompt = vi.mocked(prompt);
@@ -41,7 +41,7 @@ describe('manifests-generate', () => {
41
41
  mockFileExistsAtPath.mockResolvedValue(true);
42
42
  mockGenerateManifestJson.mockResolvedValue(undefined);
43
43
  await generateManifestCommand.action(options, undefined);
44
- expect(mockPrompt).toHaveBeenCalledWith('Enter the path to the web assets folder:', {
44
+ expect(mockPrompt).toHaveBeenCalledWith('Enter the path to the web assets folder (e.g., `dist` or `www`):', {
45
45
  type: 'text',
46
46
  });
47
47
  expect(mockFileExistsAtPath).toHaveBeenCalledWith('./www');
@@ -0,0 +1,291 @@
1
+ import { DEFAULT_CONSOLE_BASE_URL } from '../../../config/consts.js';
2
+ import appBundlesService from '../../../services/app-bundles.js';
3
+ import appsService from '../../../services/apps.js';
4
+ import authorizationService from '../../../services/authorization-service.js';
5
+ import organizationsService from '../../../services/organizations.js';
6
+ import { createBufferFromPath, createBufferFromString, isPrivateKeyContent } from '../../../utils/buffer.js';
7
+ import { isInteractive } from '../../../utils/environment.js';
8
+ import { fileExistsAtPath } from '../../../utils/file.js';
9
+ import { createHash } from '../../../utils/hash.js';
10
+ import { formatPrivateKey } from '../../../utils/private-key.js';
11
+ import { prompt } from '../../../utils/prompt.js';
12
+ import { createSignature } from '../../../utils/signature.js';
13
+ import zip from '../../../utils/zip.js';
14
+ import { defineCommand, defineOptions } from '@robingenz/zli';
15
+ import consola from 'consola';
16
+ import { z } from 'zod';
17
+ export default defineCommand({
18
+ description: 'Register a self-hosted bundle URL.',
19
+ options: defineOptions(z.object({
20
+ androidMax: z.coerce
21
+ .string()
22
+ .optional()
23
+ .describe('The maximum Android version code (`versionCode`) that the bundle supports.'),
24
+ androidMin: z.coerce
25
+ .string()
26
+ .optional()
27
+ .describe('The minimum Android version code (`versionCode`) that the bundle supports.'),
28
+ androidEq: z.coerce
29
+ .string()
30
+ .optional()
31
+ .describe('The exact Android version code (`versionCode`) that the bundle does not support.'),
32
+ appId: z
33
+ .string({
34
+ message: 'App ID must be a UUID.',
35
+ })
36
+ .uuid({
37
+ message: 'App ID must be a UUID.',
38
+ })
39
+ .optional()
40
+ .describe('App ID to deploy to.'),
41
+ channel: z.string().optional().describe('Channel to associate the bundle with.'),
42
+ commitMessage: z
43
+ .string()
44
+ .optional()
45
+ .describe('The commit message related to the bundle. Deprecated, use `--git-ref` instead.'),
46
+ commitRef: z
47
+ .string()
48
+ .optional()
49
+ .describe('The commit ref related to the bundle. Deprecated, use `--git-ref` instead.'),
50
+ commitSha: z
51
+ .string()
52
+ .optional()
53
+ .describe('The commit sha related to the bundle. Deprecated, use `--git-ref` instead.'),
54
+ customProperty: z
55
+ .array(z.string().min(1).max(100))
56
+ .optional()
57
+ .describe('A custom property to assign to the bundle. Must be in the format `key=value`. Can be specified multiple times.'),
58
+ expiresInDays: z.coerce
59
+ .number({
60
+ message: 'Expiration days must be an integer.',
61
+ })
62
+ .int({
63
+ message: 'Expiration days must be an integer.',
64
+ })
65
+ .optional()
66
+ .describe('The number of days until the bundle is automatically deleted.'),
67
+ gitRef: z
68
+ .string()
69
+ .optional()
70
+ .describe('The Git reference (branch, tag, or commit SHA) to associate with the bundle.'),
71
+ iosMax: z
72
+ .string()
73
+ .optional()
74
+ .describe('The maximum iOS bundle version (`CFBundleVersion`) that the bundle supports.'),
75
+ iosMin: z
76
+ .string()
77
+ .optional()
78
+ .describe('The minimum iOS bundle version (`CFBundleVersion`) that the bundle supports.'),
79
+ iosEq: z
80
+ .string()
81
+ .optional()
82
+ .describe('The exact iOS bundle version (`CFBundleVersion`) that the bundle does not support.'),
83
+ path: z.string().optional().describe('Path to zip file for code signing only.'),
84
+ privateKey: z
85
+ .string()
86
+ .optional()
87
+ .describe('The private key to sign the bundle with. Can be a file path to a .pem file or the private key content as plain text.'),
88
+ rolloutPercentage: z.coerce
89
+ .number()
90
+ .int({
91
+ message: 'Percentage must be an integer.',
92
+ })
93
+ .min(0, {
94
+ message: 'Percentage must be at least 0.',
95
+ })
96
+ .max(100, {
97
+ message: 'Percentage must be at most 100.',
98
+ })
99
+ .optional()
100
+ .describe('The percentage of devices to deploy the bundle to. Must be an integer between 0 and 100.'),
101
+ url: z.string().optional().describe('The url to the self-hosted bundle file.'),
102
+ yes: z.boolean().optional().describe('Skip confirmation prompts.'),
103
+ }), { y: 'yes' }),
104
+ action: async (options, args) => {
105
+ let { androidEq, androidMax, androidMin, appId, channel, commitMessage, commitRef, commitSha, customProperty, expiresInDays, gitRef, iosEq, iosMax, iosMin, path, privateKey, rolloutPercentage, url, } = options;
106
+ // Check if the user is logged in
107
+ if (!authorizationService.hasAuthorizationToken()) {
108
+ consola.error('You must be logged in to run this command. Please run the `login` command first.');
109
+ process.exit(1);
110
+ }
111
+ // Calculate the expiration date
112
+ let expiresAt;
113
+ if (expiresInDays) {
114
+ const expiresAtDate = new Date();
115
+ expiresAtDate.setDate(expiresAtDate.getDate() + expiresInDays);
116
+ expiresAt = expiresAtDate.toISOString();
117
+ }
118
+ // Prompt for url if not provided
119
+ if (!url) {
120
+ if (!isInteractive()) {
121
+ consola.error('You must provide a url when running in non-interactive environment.');
122
+ process.exit(1);
123
+ }
124
+ else {
125
+ url = await prompt('Enter the URL to the self-hosted bundle file:', {
126
+ type: 'text',
127
+ });
128
+ if (!url) {
129
+ consola.error('You must provide a url to the self-hosted bundle file.');
130
+ process.exit(1);
131
+ }
132
+ }
133
+ }
134
+ // Prompt for appId if not provided
135
+ if (!appId) {
136
+ if (!isInteractive()) {
137
+ consola.error('You must provide an app ID when running in non-interactive environment.');
138
+ process.exit(1);
139
+ }
140
+ const organizations = await organizationsService.findAll();
141
+ if (organizations.length === 0) {
142
+ consola.error('You must create an organization before registering a bundle.');
143
+ process.exit(1);
144
+ }
145
+ // @ts-ignore wait till https://github.com/unjs/consola/pull/280 is merged
146
+ const organizationId = await prompt('Select the organization of the app for which you want to register a bundle.', {
147
+ type: 'select',
148
+ options: organizations.map((organization) => ({ label: organization.name, value: organization.id })),
149
+ });
150
+ if (!organizationId) {
151
+ consola.error('You must select the organization of an app for which you want to register a bundle.');
152
+ process.exit(1);
153
+ }
154
+ const apps = await appsService.findAll({
155
+ organizationId,
156
+ });
157
+ if (apps.length === 0) {
158
+ consola.error('You must create an app before registering a bundle.');
159
+ process.exit(1);
160
+ }
161
+ // @ts-ignore wait till https://github.com/unjs/consola/pull/280 is merged
162
+ appId = await prompt('Which app do you want to deploy to:', {
163
+ type: 'select',
164
+ options: apps.map((app) => ({ label: app.name, value: app.id })),
165
+ });
166
+ if (!appId) {
167
+ consola.error('You must select an app to deploy to.');
168
+ process.exit(1);
169
+ }
170
+ }
171
+ // Prompt for channel if interactive
172
+ if (!channel && !options.yes && isInteractive()) {
173
+ const shouldDeployToChannel = await prompt('Do you want to deploy to a specific channel?', {
174
+ type: 'confirm',
175
+ initial: false,
176
+ });
177
+ if (shouldDeployToChannel) {
178
+ channel = await prompt('Enter the channel name:', {
179
+ type: 'text',
180
+ });
181
+ if (!channel) {
182
+ consola.error('The channel name must be at least one character long.');
183
+ process.exit(1);
184
+ }
185
+ }
186
+ }
187
+ // Handle checksum and signature generation if path is provided
188
+ let checksum;
189
+ let signature;
190
+ if (path) {
191
+ // Validate that path is a zip file
192
+ if (!zip.isZipped(path)) {
193
+ consola.error('The path must be a zip file when providing a URL.');
194
+ process.exit(1);
195
+ }
196
+ // Check if the path exists
197
+ const pathExists = await fileExistsAtPath(path);
198
+ if (!pathExists) {
199
+ consola.error(`The path does not exist.`);
200
+ process.exit(1);
201
+ }
202
+ // Create the file buffer
203
+ const fileBuffer = await createBufferFromPath(path);
204
+ // Generate checksum
205
+ checksum = await createHash(fileBuffer);
206
+ // Handle private key for signing
207
+ if (privateKey) {
208
+ let privateKeyBuffer;
209
+ if (isPrivateKeyContent(privateKey)) {
210
+ // Handle plain text private key content
211
+ const formattedPrivateKey = formatPrivateKey(privateKey);
212
+ privateKeyBuffer = createBufferFromString(formattedPrivateKey);
213
+ }
214
+ else if (privateKey.endsWith('.pem')) {
215
+ // Handle file path
216
+ const fileExists = await fileExistsAtPath(privateKey);
217
+ if (fileExists) {
218
+ const keyBuffer = await createBufferFromPath(privateKey);
219
+ const keyContent = keyBuffer.toString('utf8');
220
+ const formattedPrivateKey = formatPrivateKey(keyContent);
221
+ privateKeyBuffer = createBufferFromString(formattedPrivateKey);
222
+ }
223
+ else {
224
+ consola.error('Private key file not found.');
225
+ process.exit(1);
226
+ }
227
+ }
228
+ else {
229
+ consola.error('Private key must be either a path to a .pem file or the private key content as plain text.');
230
+ process.exit(1);
231
+ }
232
+ // Sign the bundle
233
+ signature = await createSignature(privateKeyBuffer, fileBuffer);
234
+ }
235
+ }
236
+ // Get app details for confirmation
237
+ const app = await appsService.findOne({ appId });
238
+ const appName = app.name;
239
+ // Final confirmation before registering bundle
240
+ if (!options.yes && isInteractive()) {
241
+ const confirmed = await prompt(`Are you sure you want to register bundle from URL "${url}" for app "${appName}" (${appId})?`, {
242
+ type: 'confirm',
243
+ });
244
+ if (!confirmed) {
245
+ consola.info('Bundle registration cancelled.');
246
+ process.exit(0);
247
+ }
248
+ }
249
+ // Create the app bundle
250
+ consola.start('Registering bundle...');
251
+ const response = await appBundlesService.create({
252
+ appId,
253
+ artifactType: 'zip',
254
+ channelName: channel,
255
+ checksum,
256
+ eqAndroidAppVersionCode: androidEq,
257
+ eqIosAppVersionCode: iosEq,
258
+ gitCommitMessage: commitMessage,
259
+ gitCommitRef: commitRef,
260
+ gitCommitSha: commitSha,
261
+ gitRef,
262
+ customProperties: parseCustomProperties(customProperty),
263
+ expiresAt,
264
+ url,
265
+ maxAndroidAppVersionCode: androidMax,
266
+ maxIosAppVersionCode: iosMax,
267
+ minAndroidAppVersionCode: androidMin,
268
+ minIosAppVersionCode: iosMin,
269
+ rolloutPercentage: (rolloutPercentage ?? 100) / 100,
270
+ signature,
271
+ });
272
+ consola.info(`Bundle Artifact ID: ${response.id}`);
273
+ if (response.appDeploymentId) {
274
+ consola.info(`Deployment URL: ${DEFAULT_CONSOLE_BASE_URL}/apps/${appId}/deployments/${response.appDeploymentId}`);
275
+ }
276
+ consola.success('Live Update successfully registered.');
277
+ },
278
+ });
279
+ const parseCustomProperties = (customProperty) => {
280
+ let customProperties;
281
+ if (customProperty) {
282
+ customProperties = {};
283
+ for (const property of customProperty) {
284
+ const [key, value] = property.split('=');
285
+ if (key && value) {
286
+ customProperties[key] = value;
287
+ }
288
+ }
289
+ }
290
+ return customProperties;
291
+ };