@appsemble/cli 0.20.8 → 0.20.11
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/__fixtures__/getBlockConfigFromTypeScript/comments/index.ts +40 -0
- package/__fixtures__/getBlockConfigFromTypeScript/comments/tsconfig.json +7 -0
- package/__fixtures__/getBlockConfigFromTypeScript/duplicateActions/index.ts +34 -0
- package/__fixtures__/getBlockConfigFromTypeScript/duplicateActions/tsconfig.json +7 -0
- package/__fixtures__/getBlockConfigFromTypeScript/duplicateEventEmitters/index.ts +34 -0
- package/__fixtures__/getBlockConfigFromTypeScript/duplicateEventEmitters/tsconfig.json +7 -0
- package/__fixtures__/getBlockConfigFromTypeScript/duplicateEventListeners/index.ts +34 -0
- package/__fixtures__/getBlockConfigFromTypeScript/duplicateEventListeners/tsconfig.json +7 -0
- package/__fixtures__/getBlockConfigFromTypeScript/duplicateParameters/index.ts +34 -0
- package/__fixtures__/getBlockConfigFromTypeScript/duplicateParameters/tsconfig.json +7 -0
- package/__fixtures__/getBlockConfigFromTypeScript/fontawesomeParameters/index.ts +11 -0
- package/__fixtures__/getBlockConfigFromTypeScript/fontawesomeParameters/tsconfig.json +7 -0
- package/__fixtures__/getBlockConfigFromTypeScript/tsError/index.ts +4 -0
- package/__fixtures__/getBlockConfigFromTypeScript/tsError/tsconfig.json +8 -0
- package/__fixtures__/getBlockConfigFromTypeScript/valid/index.ts +28 -0
- package/__fixtures__/getBlockConfigFromTypeScript/valid/tsconfig.json +7 -0
- package/__fixtures__/makePayload/no-icon/output/block.js +1 -0
- package/__fixtures__/makePayload/with-icon/icon.svg +2 -0
- package/__fixtures__/makePayload/with-icon/output/block.js +1 -0
- package/assets/appsemblerc.schema.json +150 -0
- package/changed/added/.gitkeep +0 -0
- package/changed/changed/.gitkeep +0 -0
- package/changed/deprecated/.gitkeep +0 -0
- package/changed/fixed/.gitkeep +0 -0
- package/changed/removed/.gitkeep +0 -0
- package/changed/security/.gitkeep +0 -0
- package/{dist/commands → commands}/app/create.d.ts +0 -0
- package/{dist/commands → commands}/app/create.js +0 -0
- package/commands/app/create.js.map +1 -0
- package/commands/app/create.ts +104 -0
- package/{dist/commands → commands}/app/extract-messages.d.ts +0 -0
- package/{dist/commands → commands}/app/extract-messages.js +0 -0
- package/commands/app/extract-messages.js.map +1 -0
- package/commands/app/extract-messages.ts +56 -0
- package/{dist/commands → commands}/app/index.d.ts +0 -0
- package/{dist/commands → commands}/app/index.js +0 -0
- package/commands/app/index.js.map +1 -0
- package/commands/app/index.ts +14 -0
- package/{dist/commands → commands}/app/update.d.ts +0 -0
- package/{dist/commands → commands}/app/update.js +0 -0
- package/commands/app/update.js.map +1 -0
- package/commands/app/update.ts +111 -0
- package/{dist/commands → commands}/asset/create.d.ts +0 -0
- package/{dist/commands → commands}/asset/create.js +0 -0
- package/commands/asset/create.js.map +1 -0
- package/commands/asset/create.ts +84 -0
- package/{dist/commands → commands}/asset/index.d.ts +0 -0
- package/{dist/commands → commands}/asset/index.js +0 -0
- package/commands/asset/index.js.map +1 -0
- package/commands/asset/index.ts +12 -0
- package/{dist/commands → commands}/block/build.d.ts +0 -0
- package/{dist/commands → commands}/block/build.js +0 -0
- package/commands/block/build.js.map +1 -0
- package/commands/block/build.ts +32 -0
- package/{dist/commands → commands}/block/extract-messages.d.ts +0 -0
- package/{dist/commands → commands}/block/extract-messages.js +0 -0
- package/commands/block/extract-messages.js.map +1 -0
- package/commands/block/extract-messages.ts +41 -0
- package/{dist/commands → commands}/block/index.d.ts +0 -0
- package/{dist/commands → commands}/block/index.js +0 -0
- package/commands/block/index.js.map +1 -0
- package/commands/block/index.ts +14 -0
- package/{dist/commands → commands}/block/publish.d.ts +0 -0
- package/{dist/commands → commands}/block/publish.js +0 -0
- package/commands/block/publish.js.map +1 -0
- package/commands/block/publish.ts +58 -0
- package/{dist/commands → commands}/cleanupResources.d.ts +0 -0
- package/{dist/commands → commands}/cleanupResources.js +0 -0
- package/commands/cleanupResources.js.map +1 -0
- package/commands/cleanupResources.ts +45 -0
- package/{dist/commands → commands}/config/get.d.ts +0 -0
- package/{dist/commands → commands}/config/get.js +0 -0
- package/commands/config/get.js.map +1 -0
- package/commands/config/get.ts +26 -0
- package/{dist/commands → commands}/config/index.d.ts +0 -0
- package/{dist/commands → commands}/config/index.js +0 -0
- package/commands/config/index.js.map +1 -0
- package/commands/config/index.ts +13 -0
- package/{dist/commands → commands}/config/set.d.ts +0 -0
- package/{dist/commands → commands}/config/set.js +0 -0
- package/commands/config/set.js.map +1 -0
- package/commands/config/set.ts +44 -0
- package/{dist/commands → commands}/login.d.ts +0 -0
- package/{dist/commands → commands}/login.js +0 -0
- package/commands/login.js.map +1 -0
- package/commands/login.ts +5 -0
- package/{dist/commands → commands}/logout.d.ts +0 -0
- package/{dist/commands → commands}/logout.js +0 -0
- package/commands/logout.js.map +1 -0
- package/commands/logout.ts +5 -0
- package/{dist/commands → commands}/migrate.d.ts +0 -0
- package/{dist/commands → commands}/migrate.js +5 -2
- package/commands/migrate.js.map +1 -0
- package/commands/migrate.ts +50 -0
- package/{dist/commands → commands}/organization/create.d.ts +0 -0
- package/{dist/commands → commands}/organization/create.js +0 -0
- package/commands/organization/create.js.map +1 -0
- package/commands/organization/create.ts +65 -0
- package/{dist/commands → commands}/organization/index.d.ts +0 -0
- package/{dist/commands → commands}/organization/index.js +0 -0
- package/commands/organization/index.js.map +1 -0
- package/commands/organization/index.ts +13 -0
- package/{dist/commands → commands}/organization/update.d.ts +0 -0
- package/{dist/commands → commands}/organization/update.js +0 -0
- package/commands/organization/update.js.map +1 -0
- package/commands/organization/update.ts +65 -0
- package/{dist/commands → commands}/resource/create.d.ts +0 -0
- package/{dist/commands → commands}/resource/create.js +0 -0
- package/commands/resource/create.js.map +1 -0
- package/commands/resource/create.ts +78 -0
- package/{dist/commands → commands}/resource/index.d.ts +0 -0
- package/{dist/commands → commands}/resource/index.js +0 -0
- package/commands/resource/index.js.map +1 -0
- package/commands/resource/index.ts +13 -0
- package/{dist/commands → commands}/resource/update.d.ts +0 -0
- package/{dist/commands → commands}/resource/update.js +0 -0
- package/commands/resource/update.js.map +1 -0
- package/commands/resource/update.ts +77 -0
- package/{dist/commands → commands}/runCronJobs.d.ts +0 -0
- package/{dist/commands → commands}/runCronJobs.js +0 -0
- package/commands/runCronJobs.js.map +1 -0
- package/commands/runCronJobs.ts +45 -0
- package/{dist/commands → commands}/start.d.ts +0 -0
- package/{dist/commands → commands}/start.js +4 -0
- package/commands/start.js.map +1 -0
- package/commands/start.ts +140 -0
- package/{dist/commands → commands}/team/create.d.ts +0 -0
- package/{dist/commands → commands}/team/create.js +0 -0
- package/commands/team/create.js.map +1 -0
- package/commands/team/create.ts +60 -0
- package/{dist/commands → commands}/team/delete.d.ts +0 -0
- package/{dist/commands → commands}/team/delete.js +0 -0
- package/commands/team/delete.js.map +1 -0
- package/commands/team/delete.ts +54 -0
- package/{dist/commands → commands}/team/index.d.ts +0 -0
- package/{dist/commands → commands}/team/index.js +0 -0
- package/commands/team/index.js.map +1 -0
- package/commands/team/index.ts +15 -0
- package/{dist/commands → commands}/team/member/delete.d.ts +0 -0
- package/{dist/commands → commands}/team/member/delete.js +0 -0
- package/commands/team/member/delete.js.map +1 -0
- package/commands/team/member/delete.ts +61 -0
- package/{dist/commands → commands}/team/member/index.d.ts +0 -0
- package/{dist/commands → commands}/team/member/index.js +0 -0
- package/commands/team/member/index.js.map +1 -0
- package/commands/team/member/index.ts +14 -0
- package/{dist/commands → commands}/team/member/invite.d.ts +0 -0
- package/{dist/commands → commands}/team/member/invite.js +0 -0
- package/commands/team/member/invite.js.map +1 -0
- package/commands/team/member/invite.ts +61 -0
- package/{dist/commands → commands}/team/member/update.d.ts +0 -0
- package/{dist/commands → commands}/team/member/update.js +0 -0
- package/commands/team/member/update.js.map +1 -0
- package/commands/team/member/update.ts +70 -0
- package/{dist/commands → commands}/team/update.d.ts +0 -0
- package/{dist/commands → commands}/team/update.js +0 -0
- package/commands/team/update.js.map +1 -0
- package/commands/team/update.ts +60 -0
- package/index.d.ts +2 -0
- package/{dist/index.js → index.js} +2 -6
- package/index.js.map +1 -0
- package/index.ts +74 -0
- package/jest.config.js +1 -0
- package/jest.setup.d.ts +1 -0
- package/jest.setup.js +6 -0
- package/jest.setup.js.map +1 -0
- package/jest.setup.ts +4 -0
- package/lib/__snapshots__/getBlockConfigFromTypeScript.test.ts.snap +3872 -0
- package/{dist/lib → lib}/app.d.ts +17 -17
- package/{dist/lib → lib}/app.js +29 -29
- package/lib/app.js.map +1 -0
- package/lib/app.ts +797 -0
- package/{dist/lib → lib}/asset.d.ts +0 -0
- package/{dist/lib → lib}/asset.js +0 -0
- package/lib/asset.js.map +1 -0
- package/lib/asset.ts +43 -0
- package/{dist/lib → lib}/authentication.d.ts +3 -3
- package/{dist/lib → lib}/authentication.js +47 -15
- package/lib/authentication.js.map +1 -0
- package/lib/authentication.ts +193 -0
- package/{dist/lib → lib}/block.d.ts +16 -16
- package/{dist/lib → lib}/block.js +35 -26
- package/lib/block.js.map +1 -0
- package/lib/block.test.d.ts +1 -0
- package/lib/block.test.js +106 -0
- package/lib/block.test.js.map +1 -0
- package/lib/block.test.ts +103 -0
- package/lib/block.ts +384 -0
- package/{dist/lib → lib}/coercers.d.ts +2 -2
- package/{dist/lib → lib}/coercers.js +2 -2
- package/lib/coercers.js.map +1 -0
- package/lib/coercers.test.d.ts +1 -0
- package/lib/coercers.test.js +28 -0
- package/lib/coercers.test.js.map +1 -0
- package/lib/coercers.test.ts +27 -0
- package/lib/coercers.ts +57 -0
- package/{dist/lib → lib}/getBlockConfigFromTypeScript.d.ts +1 -1
- package/{dist/lib → lib}/getBlockConfigFromTypeScript.js +38 -36
- package/lib/getBlockConfigFromTypeScript.js.map +1 -0
- package/lib/getBlockConfigFromTypeScript.test.d.ts +1 -0
- package/lib/getBlockConfigFromTypeScript.test.js +238 -0
- package/lib/getBlockConfigFromTypeScript.test.js.map +1 -0
- package/lib/getBlockConfigFromTypeScript.test.ts +281 -0
- package/lib/getBlockConfigFromTypeScript.ts +354 -0
- package/{dist/lib → lib}/initAxios.d.ts +1 -1
- package/{dist/lib → lib}/initAxios.js +3 -4
- package/lib/initAxios.js.map +1 -0
- package/lib/initAxios.ts +16 -0
- package/{dist/lib → lib}/loadWebpackConfig.d.ts +3 -3
- package/{dist/lib → lib}/loadWebpackConfig.js +3 -3
- package/lib/loadWebpackConfig.js.map +1 -0
- package/lib/loadWebpackConfig.ts +59 -0
- package/{dist/lib → lib}/organization.d.ts +1 -1
- package/{dist/lib → lib}/organization.js +0 -0
- package/lib/organization.js.map +1 -0
- package/lib/organization.ts +105 -0
- package/{dist/lib → lib}/output.d.ts +0 -0
- package/{dist/lib → lib}/output.js +0 -0
- package/lib/output.js.map +1 -0
- package/lib/output.ts +19 -0
- package/{dist/lib → lib}/processCss.d.ts +1 -1
- package/{dist/lib → lib}/processCss.js +3 -3
- package/lib/processCss.js.map +1 -0
- package/lib/processCss.ts +24 -0
- package/{dist/lib → lib}/resource.d.ts +0 -0
- package/{dist/lib → lib}/resource.js +1 -1
- package/lib/resource.js.map +1 -0
- package/lib/resource.ts +128 -0
- package/{dist/lib → lib}/serverImport.d.ts +1 -1
- package/{dist/lib → lib}/serverImport.js +2 -2
- package/lib/serverImport.js.map +1 -0
- package/lib/serverImport.ts +43 -0
- package/{dist/lib → lib}/team.d.ts +0 -0
- package/{dist/lib → lib}/team.js +0 -0
- package/lib/team.js.map +1 -0
- package/lib/team.ts +152 -0
- package/package.json +14 -12
- package/tsconfig.json +10 -0
- package/{dist/types.d.ts → types.d.ts} +0 -0
- package/{dist/types.js → types.js} +0 -0
- package/types.js.map +1 -0
- package/types.ts +110 -0
- package/dist/commands/app/create.js.map +0 -1
- package/dist/commands/app/extract-messages.js.map +0 -1
- package/dist/commands/app/index.js.map +0 -1
- package/dist/commands/app/update.js.map +0 -1
- package/dist/commands/asset/create.js.map +0 -1
- package/dist/commands/asset/index.js.map +0 -1
- package/dist/commands/block/build.js.map +0 -1
- package/dist/commands/block/extract-messages.js.map +0 -1
- package/dist/commands/block/index.js.map +0 -1
- package/dist/commands/block/publish.js.map +0 -1
- package/dist/commands/cleanupResources.js.map +0 -1
- package/dist/commands/config/get.js.map +0 -1
- package/dist/commands/config/index.js.map +0 -1
- package/dist/commands/config/set.js.map +0 -1
- package/dist/commands/login.js.map +0 -1
- package/dist/commands/logout.js.map +0 -1
- package/dist/commands/migrate.js.map +0 -1
- package/dist/commands/organization/create.js.map +0 -1
- package/dist/commands/organization/index.js.map +0 -1
- package/dist/commands/organization/update.js.map +0 -1
- package/dist/commands/resource/create.js.map +0 -1
- package/dist/commands/resource/index.js.map +0 -1
- package/dist/commands/resource/update.js.map +0 -1
- package/dist/commands/runCronJobs.js.map +0 -1
- package/dist/commands/start.js.map +0 -1
- package/dist/commands/team/create.js.map +0 -1
- package/dist/commands/team/delete.js.map +0 -1
- package/dist/commands/team/index.js.map +0 -1
- package/dist/commands/team/member/delete.js.map +0 -1
- package/dist/commands/team/member/index.js.map +0 -1
- package/dist/commands/team/member/invite.js.map +0 -1
- package/dist/commands/team/member/update.js.map +0 -1
- package/dist/commands/team/update.js.map +0 -1
- package/dist/index.d.ts +0 -2
- package/dist/index.js.map +0 -1
- package/dist/lib/app.js.map +0 -1
- package/dist/lib/asset.js.map +0 -1
- package/dist/lib/authentication.js.map +0 -1
- package/dist/lib/block.js.map +0 -1
- package/dist/lib/coercers.js.map +0 -1
- package/dist/lib/getBlockConfigFromTypeScript.js.map +0 -1
- package/dist/lib/initAxios.js.map +0 -1
- package/dist/lib/loadWebpackConfig.js.map +0 -1
- package/dist/lib/organization.js.map +0 -1
- package/dist/lib/output.js.map +0 -1
- package/dist/lib/processCss.js.map +0 -1
- package/dist/lib/readPackageJson.d.ts +0 -9
- package/dist/lib/readPackageJson.js +0 -16
- package/dist/lib/readPackageJson.js.map +0 -1
- package/dist/lib/resource.js.map +0 -1
- package/dist/lib/serverImport.js.map +0 -1
- package/dist/lib/team.js.map +0 -1
- package/dist/types.js.map +0 -1
package/lib/app.ts
ADDED
|
@@ -0,0 +1,797 @@
|
|
|
1
|
+
import { createReadStream, existsSync, ReadStream } from 'fs';
|
|
2
|
+
import { mkdir, readdir, readFile, stat } from 'fs/promises';
|
|
3
|
+
import { join, parse, relative, resolve } from 'path';
|
|
4
|
+
import { inspect } from 'util';
|
|
5
|
+
|
|
6
|
+
import { AppsembleError, logger, opendirSafe, readData, writeData } from '@appsemble/node-utils';
|
|
7
|
+
import { App, AppDefinition, AppsembleMessages, AppVisibility, Messages } from '@appsemble/types';
|
|
8
|
+
import { extractAppMessages, has, normalizeBlockName } from '@appsemble/utils';
|
|
9
|
+
import axios from 'axios';
|
|
10
|
+
import FormData from 'form-data';
|
|
11
|
+
|
|
12
|
+
import { AppsembleContext, AppsembleRC } from '../types';
|
|
13
|
+
import { authenticate } from './authentication';
|
|
14
|
+
import { traverseBlockThemes } from './block';
|
|
15
|
+
import { coerceRemote } from './coercers';
|
|
16
|
+
import { printAxiosError } from './output';
|
|
17
|
+
import { processCss } from './processCss';
|
|
18
|
+
import { createResource } from './resource';
|
|
19
|
+
|
|
20
|
+
interface CreateAppParams {
|
|
21
|
+
/**
|
|
22
|
+
* The OAuth2 client credentials to use.
|
|
23
|
+
*/
|
|
24
|
+
clientCredentials?: string;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* If specified, the context matching this name is used, overriding command line flags.
|
|
28
|
+
*/
|
|
29
|
+
context?: string;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The ID of the organization to upload for.
|
|
33
|
+
*/
|
|
34
|
+
organization: string;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The path in which the App YAML is located.
|
|
38
|
+
*/
|
|
39
|
+
path: string;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Visibility of the app in the public app store.
|
|
43
|
+
*/
|
|
44
|
+
visibility: AppVisibility;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The remote server to create the app on.
|
|
48
|
+
*/
|
|
49
|
+
remote: string;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Whether the App should be marked as a template.
|
|
53
|
+
*/
|
|
54
|
+
template: boolean;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The icon to upload.
|
|
58
|
+
*/
|
|
59
|
+
icon: NodeJS.ReadStream | ReadStream;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The background color to use for the icon in opaque contexts.
|
|
63
|
+
*/
|
|
64
|
+
iconBackground: string;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The maskable icon to upload.
|
|
68
|
+
*/
|
|
69
|
+
maskableIcon: NodeJS.ReadStream | ReadStream;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Whether the API should be called with a dry run.
|
|
73
|
+
*/
|
|
74
|
+
dryRun: boolean;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Whether resources from the `resources` directory should be created after creating the app.
|
|
78
|
+
*/
|
|
79
|
+
resources: boolean;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* If the app context is specified,
|
|
83
|
+
* modify it for the current context to include the id of the created app.
|
|
84
|
+
*/
|
|
85
|
+
modifyContext: boolean;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The ID to use for Google Analytics for the app.
|
|
89
|
+
*/
|
|
90
|
+
googleAnalyticsId?: string;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* The custom Sentry DSN for the app.
|
|
94
|
+
*/
|
|
95
|
+
sentryDsn?: string;
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The environment for the custom Sentry DSN for the app.
|
|
99
|
+
*/
|
|
100
|
+
sentryEnvironment?: string;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
interface UpdateAppParams {
|
|
104
|
+
/**
|
|
105
|
+
* The OAuth2 client credentials to use.
|
|
106
|
+
*/
|
|
107
|
+
clientCredentials: string;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* If specified, the context matching this name is used, overriding command line flags.
|
|
111
|
+
*/
|
|
112
|
+
context?: string;
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* The ID of the app to update.
|
|
116
|
+
*/
|
|
117
|
+
id?: number;
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* The path in which the App YAML is located.
|
|
121
|
+
*/
|
|
122
|
+
path: string;
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Visibility of the app in the public app store.
|
|
126
|
+
*/
|
|
127
|
+
visibility: AppVisibility;
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* The remote server to create the app on.
|
|
131
|
+
*/
|
|
132
|
+
remote: string;
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Whether the App should be marked as a template.
|
|
136
|
+
*/
|
|
137
|
+
template: boolean;
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Whether the locked property should be ignored.
|
|
141
|
+
*/
|
|
142
|
+
force: boolean;
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* The icon to upload.
|
|
146
|
+
*/
|
|
147
|
+
icon: NodeJS.ReadStream | ReadStream;
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* The background color to use for the icon in opaque contexts.
|
|
151
|
+
*/
|
|
152
|
+
iconBackground: string;
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* The maskable icon to upload.
|
|
156
|
+
*/
|
|
157
|
+
maskableIcon: NodeJS.ReadStream | ReadStream;
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* The ID to use for Google Analytics for the app.
|
|
161
|
+
*/
|
|
162
|
+
googleAnalyticsId?: string;
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* The custom Sentry DSN for the app.
|
|
166
|
+
*/
|
|
167
|
+
sentryDsn?: string;
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* The environment for the custom Sentry DSN for the app.
|
|
171
|
+
*/
|
|
172
|
+
sentryEnvironment?: string;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Traverses an app directory and appends the files it finds to the given FormData object.
|
|
177
|
+
*
|
|
178
|
+
* @param path The path of the app directory to traverse.
|
|
179
|
+
* @param context The Context to use from `.appsemblerc.yaml`.
|
|
180
|
+
* @param formData The FormData object to append the results into..
|
|
181
|
+
* @returns The context from `.appsemblerc.yaml` if a match was found.
|
|
182
|
+
*/
|
|
183
|
+
export async function traverseAppDirectory(
|
|
184
|
+
path: string,
|
|
185
|
+
context: string,
|
|
186
|
+
formData: FormData,
|
|
187
|
+
): Promise<[AppsembleContext, AppsembleRC, string]> {
|
|
188
|
+
let discoveredContext: AppsembleContext;
|
|
189
|
+
let rc: AppsembleRC;
|
|
190
|
+
let iconPath: string;
|
|
191
|
+
let maskableIconPath: string;
|
|
192
|
+
let yaml: string;
|
|
193
|
+
|
|
194
|
+
logger.info(`Traversing directory for App files in ${path} 🕵`);
|
|
195
|
+
await opendirSafe(path, async (filepath, filestat) => {
|
|
196
|
+
switch (filestat.name.toLowerCase()) {
|
|
197
|
+
case '.appsemblerc.yaml': {
|
|
198
|
+
logger.info(`Reading app settings from ${filepath}`);
|
|
199
|
+
[rc] = await readData<AppsembleRC>(filepath);
|
|
200
|
+
if ('iconBackground' in rc) {
|
|
201
|
+
formData.append('iconBackground', rc.iconBackground);
|
|
202
|
+
}
|
|
203
|
+
if (context && has(rc?.context, context)) {
|
|
204
|
+
discoveredContext = rc.context[context];
|
|
205
|
+
logger.verbose(`Using context: ${inspect(discoveredContext, { colors: true })}`);
|
|
206
|
+
}
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
case 'app-definition.yaml': {
|
|
211
|
+
logger.info(`Using app definition from ${filepath}`);
|
|
212
|
+
if (yaml !== undefined) {
|
|
213
|
+
throw new AppsembleError('Found duplicate app definition');
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const [, data] = await readData(filepath);
|
|
217
|
+
yaml = data;
|
|
218
|
+
formData.append('yaml', data);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
case 'icon.png':
|
|
223
|
+
case 'icon.svg':
|
|
224
|
+
iconPath = filepath;
|
|
225
|
+
return;
|
|
226
|
+
|
|
227
|
+
case 'maskable-icon.png':
|
|
228
|
+
maskableIconPath = filepath;
|
|
229
|
+
return;
|
|
230
|
+
|
|
231
|
+
case 'readme.md':
|
|
232
|
+
logger.info(`Including longDescription from ${filepath}`);
|
|
233
|
+
formData.append('longDescription', await readFile(filepath, 'utf8'));
|
|
234
|
+
return;
|
|
235
|
+
|
|
236
|
+
case 'screenshots':
|
|
237
|
+
return opendirSafe(
|
|
238
|
+
filepath,
|
|
239
|
+
(screenshotPath, screenshotStat) => {
|
|
240
|
+
logger.info(`Adding screenshot ${screenshotPath} 🖼️`);
|
|
241
|
+
if (!screenshotStat.isFile()) {
|
|
242
|
+
throw new AppsembleError(`Expected ${screenshotPath} to be an image file`);
|
|
243
|
+
}
|
|
244
|
+
formData.append('screenshots', createReadStream(screenshotPath));
|
|
245
|
+
},
|
|
246
|
+
{ allowMissing: true },
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
case 'theme':
|
|
250
|
+
return opendirSafe(filepath, async (themeDir, themeStat) => {
|
|
251
|
+
const name = themeStat.name.toLowerCase();
|
|
252
|
+
if (name !== 'core' && name !== 'shared') {
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
if (!themeStat.isDirectory()) {
|
|
256
|
+
throw new AppsembleError(`Expected ${themeDir} to be a directory`);
|
|
257
|
+
}
|
|
258
|
+
const css = await processCss(join(themeDir, 'index.css'));
|
|
259
|
+
formData.append(`${name}Style`, css);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
default:
|
|
263
|
+
logger.warn(`Found unused file ${filepath}`);
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
if (yaml === undefined) {
|
|
267
|
+
throw new AppsembleError('No app definition found');
|
|
268
|
+
}
|
|
269
|
+
discoveredContext ||= {};
|
|
270
|
+
discoveredContext.icon = discoveredContext.icon
|
|
271
|
+
? resolve(path, discoveredContext.icon)
|
|
272
|
+
: iconPath;
|
|
273
|
+
discoveredContext.maskableIcon = discoveredContext.maskableIcon
|
|
274
|
+
? resolve(path, discoveredContext.maskableIcon)
|
|
275
|
+
: maskableIconPath;
|
|
276
|
+
return [discoveredContext, rc, yaml];
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Upload messages for an app.
|
|
281
|
+
*
|
|
282
|
+
* @param path The path to the app directory.
|
|
283
|
+
* @param appId The app id to upload the messages for.
|
|
284
|
+
* @param remote The remote to upload the messages to.
|
|
285
|
+
* @param force Whether or not to force the update for locked apps.
|
|
286
|
+
*/
|
|
287
|
+
export async function uploadMessages(
|
|
288
|
+
path: string,
|
|
289
|
+
appId: number,
|
|
290
|
+
remote: string,
|
|
291
|
+
force: boolean,
|
|
292
|
+
): Promise<void> {
|
|
293
|
+
const result: Messages[] = [];
|
|
294
|
+
|
|
295
|
+
logger.info('Searching for translations 🕵');
|
|
296
|
+
await opendirSafe(
|
|
297
|
+
join(path, 'i18n'),
|
|
298
|
+
async (messageFile) => {
|
|
299
|
+
logger.verbose(`Processing ${messageFile} ⚙️`);
|
|
300
|
+
const { name: language } = parse(messageFile);
|
|
301
|
+
|
|
302
|
+
if (result.some((entry) => entry.language === language)) {
|
|
303
|
+
throw new AppsembleError(
|
|
304
|
+
`Found duplicate language “${language}”. Make sure each language only exists once in the directory.`,
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const [messages] = await readData<AppsembleMessages>(messageFile);
|
|
309
|
+
result.push({ force, language, messages });
|
|
310
|
+
},
|
|
311
|
+
{ allowMissing: true },
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
if (!result.length) {
|
|
315
|
+
logger.info('No translations found 🤷');
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
for (const language of result) {
|
|
319
|
+
await axios.post(`/api/apps/${appId}/messages`, language, { baseURL: remote });
|
|
320
|
+
logger.info(`Successfully uploaded messages for language “${language.language}” 🎉`);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* @param path The path to the app directory.
|
|
326
|
+
* @param languages A list of languages for which translations should be added in addition to the
|
|
327
|
+
* existing ones.
|
|
328
|
+
* @param verify A list of languages to verify.
|
|
329
|
+
* @param format The file format that should be used for the output.
|
|
330
|
+
*/
|
|
331
|
+
export async function writeAppMessages(
|
|
332
|
+
path: string,
|
|
333
|
+
languages: string[],
|
|
334
|
+
verify: string[],
|
|
335
|
+
format: 'json' | 'yaml',
|
|
336
|
+
): Promise<void> {
|
|
337
|
+
logger.info(`Extracting messages from ${path}`);
|
|
338
|
+
let app: AppDefinition;
|
|
339
|
+
let i18nDir = join(path, 'i18n');
|
|
340
|
+
const messageFiles = new Set<string>();
|
|
341
|
+
|
|
342
|
+
await opendirSafe(path, async (filepath, filestat) => {
|
|
343
|
+
switch (filestat.name.toLowerCase()) {
|
|
344
|
+
case 'app-definition.yaml': {
|
|
345
|
+
[app] = await readData<AppDefinition>(filepath);
|
|
346
|
+
break;
|
|
347
|
+
}
|
|
348
|
+
case 'i18n': {
|
|
349
|
+
// For case insensitivity
|
|
350
|
+
i18nDir = filepath;
|
|
351
|
+
const i18nFiles = await readdir(filepath);
|
|
352
|
+
for (const f of i18nFiles) {
|
|
353
|
+
messageFiles.add(join(filepath, f));
|
|
354
|
+
}
|
|
355
|
+
break;
|
|
356
|
+
}
|
|
357
|
+
default:
|
|
358
|
+
break;
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
if (!app) {
|
|
362
|
+
throw new AppsembleError(`Couldn’t find app definition for ${path}`);
|
|
363
|
+
}
|
|
364
|
+
// Ensure the i18n directory exists.
|
|
365
|
+
await mkdir(i18nDir, { recursive: true });
|
|
366
|
+
|
|
367
|
+
for (const lang of [...languages, ...verify]) {
|
|
368
|
+
messageFiles.add(join(i18nDir, `${lang}.${format}`));
|
|
369
|
+
}
|
|
370
|
+
const defaultLangFile = join(i18nDir, `${app.defaultLanguage || 'en'}.${format}`);
|
|
371
|
+
messageFiles.add(defaultLangFile);
|
|
372
|
+
const blockMessageKeys: AppsembleMessages['blocks'] = {};
|
|
373
|
+
const extractedMessages = extractAppMessages(app, (block) => {
|
|
374
|
+
const type = normalizeBlockName(block.type);
|
|
375
|
+
if (blockMessageKeys[type]) {
|
|
376
|
+
blockMessageKeys[type][block.version] = {};
|
|
377
|
+
} else {
|
|
378
|
+
blockMessageKeys[type] = {
|
|
379
|
+
[block.version]: {},
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
logger.verbose(`Found message IDs: ${inspect(extractedMessages)}`);
|
|
384
|
+
for (const filepath of messageFiles) {
|
|
385
|
+
logger.info(`Processing ${filepath}`);
|
|
386
|
+
let oldMessages: AppsembleMessages;
|
|
387
|
+
if (existsSync(filepath)) {
|
|
388
|
+
[oldMessages] = await readData<AppsembleMessages>(filepath);
|
|
389
|
+
} else if (verify) {
|
|
390
|
+
throw new AppsembleError(`Missing translations file: ${filepath}`);
|
|
391
|
+
} else {
|
|
392
|
+
oldMessages = {
|
|
393
|
+
core: {},
|
|
394
|
+
blocks: {},
|
|
395
|
+
...extractedMessages,
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const newMessageIds = Object.fromEntries(
|
|
400
|
+
Object.keys(extractedMessages.messageIds).map((key) => {
|
|
401
|
+
if (has(oldMessages.messageIds, key) && oldMessages.messageIds[key]) {
|
|
402
|
+
if (typeof oldMessages.messageIds[key] !== 'string') {
|
|
403
|
+
throw new AppsembleError(`Invalid translation key: messageIds.${key}`);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
return [key, oldMessages.messageIds[key]];
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (verify.includes(parse(filepath).name)) {
|
|
410
|
+
throw new AppsembleError(`Missing translation: messageIds.${key}`);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
return [key, ''];
|
|
414
|
+
}),
|
|
415
|
+
);
|
|
416
|
+
|
|
417
|
+
const newAppMessages = Object.fromEntries(
|
|
418
|
+
Object.keys(extractedMessages.app).map((key) => {
|
|
419
|
+
if (has(oldMessages.app, key) && oldMessages.app[key]) {
|
|
420
|
+
if (typeof oldMessages.app[key] !== 'string') {
|
|
421
|
+
throw new AppsembleError(`Invalid translation key: app.${key}`);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return [key, oldMessages.app[key]];
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if (filepath === defaultLangFile) {
|
|
428
|
+
return [key, extractedMessages.app[key]];
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
if (verify.includes(parse(filepath).name)) {
|
|
432
|
+
throw new AppsembleError(`Missing translation: app.${key}`);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
return [key, ''];
|
|
436
|
+
}),
|
|
437
|
+
);
|
|
438
|
+
|
|
439
|
+
const appMessagePrefixes = Object.keys(newAppMessages);
|
|
440
|
+
for (const [key, message] of Object.entries(oldMessages.app)) {
|
|
441
|
+
const match = /^(pages\.\d+(\..+)?)\.blocks\.\d+.+/.exec(key);
|
|
442
|
+
if (!match) {
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
if (appMessagePrefixes.includes(match[1])) {
|
|
446
|
+
newAppMessages[key] = message;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const coreMessages = oldMessages.core ?? {};
|
|
451
|
+
for (const [key, value] of Object.entries(coreMessages)) {
|
|
452
|
+
if (!value || typeof value !== 'string') {
|
|
453
|
+
throw new AppsembleError(`Invalid translation key: core.${key}`);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const blockMessages: AppsembleMessages['blocks'] = {};
|
|
458
|
+
if (oldMessages.blocks) {
|
|
459
|
+
for (const key of Object.keys(oldMessages.blocks)) {
|
|
460
|
+
if (!has(blockMessageKeys, key)) {
|
|
461
|
+
throw new AppsembleError(
|
|
462
|
+
`Invalid translation key: blocks.${key}\nThis block is not used in the app`,
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
for (const [blockName] of Object.entries(blockMessageKeys)) {
|
|
469
|
+
if (oldMessages.blocks?.[blockName]) {
|
|
470
|
+
blockMessages[blockName] = {};
|
|
471
|
+
|
|
472
|
+
for (const [version, oldValues] of Object.entries(oldMessages.blocks[blockName])) {
|
|
473
|
+
if (!has(blockMessageKeys[blockName], version)) {
|
|
474
|
+
throw new AppsembleError(
|
|
475
|
+
`Invalid translation key: blocks.${blockName}.${version}
|
|
476
|
+
This block version is not used in the app`,
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
for (const [oldValueKey, oldValue] of Object.entries(
|
|
481
|
+
oldMessages.blocks[blockName][version],
|
|
482
|
+
)) {
|
|
483
|
+
if (typeof oldValue !== 'string') {
|
|
484
|
+
throw new AppsembleError(
|
|
485
|
+
`Invalid translation key: blocks.${blockName}.${version}.${oldValueKey}`,
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
if (verify.includes(parse(filepath).name) && !oldValue) {
|
|
490
|
+
throw new AppsembleError(
|
|
491
|
+
`Missing translation: blocks.${blockName}.${version}.${oldValueKey}`,
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
blockMessages[blockName][version] = oldValues;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const result = {
|
|
502
|
+
app: newAppMessages,
|
|
503
|
+
...(Object.keys(newMessageIds).length && { messageIds: newMessageIds }),
|
|
504
|
+
...(Object.keys(blockMessages).length && { blocks: blockMessages }),
|
|
505
|
+
...(Object.keys(coreMessages).length && { core: coreMessages }),
|
|
506
|
+
};
|
|
507
|
+
|
|
508
|
+
await writeData(filepath, result);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Update an existing app.
|
|
514
|
+
*
|
|
515
|
+
* @param argv The command line options used for updating the app.
|
|
516
|
+
*/
|
|
517
|
+
export async function updateApp({
|
|
518
|
+
clientCredentials,
|
|
519
|
+
context,
|
|
520
|
+
force,
|
|
521
|
+
path,
|
|
522
|
+
...options
|
|
523
|
+
}: UpdateAppParams): Promise<void> {
|
|
524
|
+
const file = await stat(path);
|
|
525
|
+
const formData = new FormData();
|
|
526
|
+
let appsembleContext: AppsembleContext;
|
|
527
|
+
|
|
528
|
+
if (file.isFile()) {
|
|
529
|
+
const [, data] = await readData(path);
|
|
530
|
+
formData.append('yaml', data);
|
|
531
|
+
} else {
|
|
532
|
+
[appsembleContext] = await traverseAppDirectory(path, context, formData);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
const remote = appsembleContext.remote ?? options.remote;
|
|
536
|
+
const id = appsembleContext.id ?? options.id;
|
|
537
|
+
const template = appsembleContext.template ?? options.template ?? false;
|
|
538
|
+
const visibility = appsembleContext.visibility ?? options.visibility;
|
|
539
|
+
const iconBackground = appsembleContext.iconBackground ?? options.iconBackground;
|
|
540
|
+
const icon = options.icon ?? appsembleContext.icon;
|
|
541
|
+
const maskableIcon = options.maskableIcon ?? appsembleContext.maskableIcon;
|
|
542
|
+
const sentryDsn = appsembleContext.sentryDsn ?? options.sentryDsn;
|
|
543
|
+
const sentryEnvironment = appsembleContext.sentryEnvironment ?? options.sentryEnvironment;
|
|
544
|
+
const googleAnalyticsId = appsembleContext.googleAnalyticsId ?? options.googleAnalyticsId;
|
|
545
|
+
logger.info(`App id: ${id}`);
|
|
546
|
+
logger.verbose(`App remote: ${remote}`);
|
|
547
|
+
logger.verbose(`App is template: ${inspect(template, { colors: true })}`);
|
|
548
|
+
logger.verbose(`App visibility: ${visibility}`);
|
|
549
|
+
logger.verbose(`Icon background: ${iconBackground}`);
|
|
550
|
+
logger.verbose(`Force update: ${inspect(force, { colors: true })}`);
|
|
551
|
+
if (!id) {
|
|
552
|
+
throw new AppsembleError('The app id must be passed as a command line flag or in the context');
|
|
553
|
+
}
|
|
554
|
+
formData.append('force', String(force));
|
|
555
|
+
formData.append('template', String(template));
|
|
556
|
+
formData.append('visibility', visibility);
|
|
557
|
+
formData.append('iconBackground', iconBackground);
|
|
558
|
+
if (icon) {
|
|
559
|
+
const realIcon = typeof icon === 'string' ? createReadStream(icon) : icon;
|
|
560
|
+
logger.info(`Using icon from ${(realIcon as ReadStream).path ?? 'stdin'}`);
|
|
561
|
+
formData.append('icon', realIcon);
|
|
562
|
+
}
|
|
563
|
+
if (maskableIcon) {
|
|
564
|
+
const realIcon =
|
|
565
|
+
typeof maskableIcon === 'string' ? createReadStream(maskableIcon) : maskableIcon;
|
|
566
|
+
logger.info(`Using maskable icon from ${(realIcon as ReadStream).path ?? 'stdin'}`);
|
|
567
|
+
formData.append('maskableIcon', realIcon);
|
|
568
|
+
}
|
|
569
|
+
if (sentryDsn) {
|
|
570
|
+
logger.info(
|
|
571
|
+
`Using custom Sentry DSN ${sentryEnvironment ? `with environment ${sentryEnvironment}` : ''}`,
|
|
572
|
+
);
|
|
573
|
+
formData.append('sentryDsn', sentryDsn);
|
|
574
|
+
if (sentryEnvironment) {
|
|
575
|
+
formData.append('sentryEnvironment', sentryEnvironment);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
if (googleAnalyticsId) {
|
|
579
|
+
logger.info('Using Google Analytics');
|
|
580
|
+
formData.append('googleAnalyticsID', googleAnalyticsId);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
await authenticate(remote, 'apps:write', clientCredentials);
|
|
584
|
+
const { data } = await axios.patch<App>(`/api/apps/${id}`, formData, { baseURL: remote });
|
|
585
|
+
|
|
586
|
+
if (file.isDirectory()) {
|
|
587
|
+
// After uploading the app, upload block styles and messages if they are available
|
|
588
|
+
await traverseBlockThemes(path, data.id, remote, force);
|
|
589
|
+
await uploadMessages(path, data.id, remote, force);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
const { host, protocol } = new URL(remote);
|
|
593
|
+
logger.info(`Successfully updated app ${data.definition.name}! 🙌`);
|
|
594
|
+
logger.info(`App URL: ${protocol}//${data.path}.${data.OrganizationId}.${host}`);
|
|
595
|
+
logger.info(`App store page: ${new URL(`/apps/${data.id}`, remote)}`);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* Create a new App.
|
|
600
|
+
*
|
|
601
|
+
* @param options The options to use for creating an app.
|
|
602
|
+
*/
|
|
603
|
+
export async function createApp({
|
|
604
|
+
clientCredentials,
|
|
605
|
+
context,
|
|
606
|
+
dryRun,
|
|
607
|
+
modifyContext,
|
|
608
|
+
path,
|
|
609
|
+
resources,
|
|
610
|
+
...options
|
|
611
|
+
}: CreateAppParams): Promise<void> {
|
|
612
|
+
const file = await stat(path);
|
|
613
|
+
const formData = new FormData();
|
|
614
|
+
let appsembleContext: AppsembleContext;
|
|
615
|
+
let rc: AppsembleRC;
|
|
616
|
+
let yaml: string;
|
|
617
|
+
let filename = relative(process.cwd(), path);
|
|
618
|
+
|
|
619
|
+
if (file.isFile()) {
|
|
620
|
+
// Assuming file is App YAML
|
|
621
|
+
const [, data] = await readData(path);
|
|
622
|
+
yaml = data;
|
|
623
|
+
formData.append('yaml', data);
|
|
624
|
+
} else {
|
|
625
|
+
[appsembleContext, rc, yaml] = await traverseAppDirectory(path, context, formData);
|
|
626
|
+
filename = join(filename, 'app-definition.yaml');
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
const remote = appsembleContext.remote ?? options.remote;
|
|
630
|
+
const organizationId = appsembleContext.organization ?? options.organization;
|
|
631
|
+
const template = appsembleContext.template ?? options.template ?? false;
|
|
632
|
+
const visibility = appsembleContext.visibility ?? options.visibility;
|
|
633
|
+
const iconBackground = appsembleContext.iconBackground ?? options.iconBackground;
|
|
634
|
+
const icon = options.icon ?? appsembleContext.icon;
|
|
635
|
+
const maskableIcon = options.maskableIcon ?? appsembleContext.maskableIcon;
|
|
636
|
+
const sentryDsn = appsembleContext.sentryDsn ?? options.sentryDsn;
|
|
637
|
+
const sentryEnvironment = appsembleContext.sentryEnvironment ?? options.sentryEnvironment;
|
|
638
|
+
const googleAnalyticsId = appsembleContext.googleAnalyticsId ?? options.googleAnalyticsId;
|
|
639
|
+
logger.verbose(`App remote: ${remote}`);
|
|
640
|
+
logger.verbose(`App organzation: ${organizationId}`);
|
|
641
|
+
logger.verbose(`App is template: ${inspect(template, { colors: true })}`);
|
|
642
|
+
logger.verbose(`App visibility: ${visibility}`);
|
|
643
|
+
logger.verbose(`Icon background: ${iconBackground}`);
|
|
644
|
+
if (!organizationId) {
|
|
645
|
+
throw new AppsembleError(
|
|
646
|
+
'An organization id must be passed as a command line flag or in the context',
|
|
647
|
+
);
|
|
648
|
+
}
|
|
649
|
+
formData.append('OrganizationId', organizationId);
|
|
650
|
+
formData.append('template', String(template));
|
|
651
|
+
formData.append('visibility', visibility);
|
|
652
|
+
formData.append('iconBackground', iconBackground);
|
|
653
|
+
if (icon) {
|
|
654
|
+
const realIcon = typeof icon === 'string' ? createReadStream(icon) : icon;
|
|
655
|
+
logger.info(`Using icon from ${(realIcon as ReadStream).path ?? 'stdin'}`);
|
|
656
|
+
formData.append('icon', realIcon);
|
|
657
|
+
}
|
|
658
|
+
if (maskableIcon) {
|
|
659
|
+
const realIcon =
|
|
660
|
+
typeof maskableIcon === 'string' ? createReadStream(maskableIcon) : maskableIcon;
|
|
661
|
+
logger.info(`Using maskable icon from ${(realIcon as ReadStream).path ?? 'stdin'}`);
|
|
662
|
+
formData.append('maskableIcon', realIcon);
|
|
663
|
+
}
|
|
664
|
+
if (sentryDsn) {
|
|
665
|
+
logger.info(
|
|
666
|
+
`Using custom Sentry DSN ${sentryEnvironment ? `with environment ${sentryEnvironment}` : ''}`,
|
|
667
|
+
);
|
|
668
|
+
formData.append('sentryDsn', sentryDsn);
|
|
669
|
+
if (sentryEnvironment) {
|
|
670
|
+
formData.append('sentryEnvironment', sentryEnvironment);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
if (googleAnalyticsId) {
|
|
674
|
+
logger.info('Using Google Analytics');
|
|
675
|
+
formData.append('googleAnalyticsID', googleAnalyticsId);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
await authenticate(
|
|
679
|
+
remote,
|
|
680
|
+
resources ? 'apps:write resources:write' : 'apps:write',
|
|
681
|
+
clientCredentials,
|
|
682
|
+
);
|
|
683
|
+
let data: App;
|
|
684
|
+
try {
|
|
685
|
+
({ data } = await axios.post<App>('/api/apps', formData, {
|
|
686
|
+
baseURL: remote,
|
|
687
|
+
params: { dryRun: dryRun || undefined },
|
|
688
|
+
}));
|
|
689
|
+
} catch (error) {
|
|
690
|
+
if (!axios.isAxiosError(error)) {
|
|
691
|
+
throw error;
|
|
692
|
+
}
|
|
693
|
+
if ((error.response?.data as { message?: string })?.message !== 'App validation failed') {
|
|
694
|
+
throw error;
|
|
695
|
+
}
|
|
696
|
+
throw new AppsembleError(
|
|
697
|
+
printAxiosError(filename, yaml, (error.response.data as any).data.errors),
|
|
698
|
+
);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
if (dryRun) {
|
|
702
|
+
logger.info('Skipped uploading block themes and app messages.');
|
|
703
|
+
logger.info('Successfully ran dry run. The app should be valid when creating it.');
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
if (file.isDirectory() && !dryRun) {
|
|
708
|
+
// After uploading the app, upload block styles and messages if they are available
|
|
709
|
+
await traverseBlockThemes(path, data.id, remote, false);
|
|
710
|
+
await uploadMessages(path, data.id, remote, false);
|
|
711
|
+
|
|
712
|
+
if (resources && existsSync(join(path, 'resources'))) {
|
|
713
|
+
const resourcePath = join(path, 'resources');
|
|
714
|
+
try {
|
|
715
|
+
const resourceFiles = await readdir(resourcePath, { withFileTypes: true });
|
|
716
|
+
for (const resource of resourceFiles) {
|
|
717
|
+
if (resource.isFile()) {
|
|
718
|
+
const { name } = parse(resource.name);
|
|
719
|
+
await createResource({
|
|
720
|
+
appId: data.id,
|
|
721
|
+
path: join(resourcePath, resource.name),
|
|
722
|
+
remote,
|
|
723
|
+
resourceName: name,
|
|
724
|
+
});
|
|
725
|
+
} else if (resource.isDirectory()) {
|
|
726
|
+
const subDirectoryResources = await readdir(join(resourcePath, resource.name), {
|
|
727
|
+
withFileTypes: true,
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
for (const subResource of subDirectoryResources.filter((s) => s.isFile())) {
|
|
731
|
+
await createResource({
|
|
732
|
+
appId: data.id,
|
|
733
|
+
path: join(resourcePath, resource.name, subResource.name),
|
|
734
|
+
remote,
|
|
735
|
+
resourceName: resource.name,
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
} catch (error: unknown) {
|
|
741
|
+
logger.error('Something went wrong when creating resources:');
|
|
742
|
+
logger.error(error);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
if (modifyContext && appsembleContext && context && !dryRun) {
|
|
748
|
+
rc.context[context].id = data.id;
|
|
749
|
+
await writeData(join(path, '.appsemblerc.yaml'), rc);
|
|
750
|
+
|
|
751
|
+
logger.info(`Updated .appsemblerc: Set context.${context}.id to ${data.id}`);
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
const { host, protocol } = new URL(remote);
|
|
755
|
+
logger.info(`Successfully created app ${data.definition.name}! 🙌`);
|
|
756
|
+
logger.info(`App URL: ${protocol}//${data.path}.${data.OrganizationId}.${host}`);
|
|
757
|
+
logger.info(`App store page: ${new URL(`/apps/${data.id}`, remote)}`);
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
/**
|
|
761
|
+
* Resolve the app’s ID and remote based on the app context.
|
|
762
|
+
*
|
|
763
|
+
* @param appPath The path to the app.
|
|
764
|
+
* @param name Which context to use in the AppsembleRC file.
|
|
765
|
+
* @param defaultRemote The remote to fall back to.
|
|
766
|
+
* @param defaultAppId The app ID to fall back to.
|
|
767
|
+
* @returns The resolved app ID and remote.
|
|
768
|
+
*/
|
|
769
|
+
export async function resolveAppIdAndRemote(
|
|
770
|
+
appPath: string,
|
|
771
|
+
name: string,
|
|
772
|
+
defaultRemote: string,
|
|
773
|
+
defaultAppId: number,
|
|
774
|
+
): Promise<[number, string]> {
|
|
775
|
+
let id = defaultAppId;
|
|
776
|
+
let resolvedRemote = defaultRemote;
|
|
777
|
+
|
|
778
|
+
if (appPath) {
|
|
779
|
+
const rcPath = join(appPath, '.appsemblerc.yaml');
|
|
780
|
+
const [rc] = await readData<AppsembleRC>(rcPath);
|
|
781
|
+
const context = rc.context?.[name];
|
|
782
|
+
|
|
783
|
+
if (!defaultAppId) {
|
|
784
|
+
id = context?.id;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
if (context.remote) {
|
|
788
|
+
resolvedRemote = context.remote;
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
if (id == null) {
|
|
793
|
+
throw new AppsembleError(`App ID was not found in context.${name}.id nor in --app-id`);
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
return [id, coerceRemote(resolvedRemote)];
|
|
797
|
+
}
|