@appsemble/cli 0.20.44 → 0.21.0

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 (129) hide show
  1. package/README.md +145 -3
  2. package/commands/app/create.d.ts +2 -2
  3. package/commands/app/update.d.ts +2 -2
  4. package/commands/organization/create.d.ts +1 -1
  5. package/commands/organization/update.d.ts +1 -1
  6. package/commands/serve.d.ts +15 -0
  7. package/commands/serve.js +265 -0
  8. package/index.js +2 -0
  9. package/lib/app.d.ts +4 -4
  10. package/lib/app.js +14 -5
  11. package/lib/block.d.ts +2 -2
  12. package/lib/block.js +17 -3
  13. package/lib/block.test.js +6 -4
  14. package/lib/coercers.d.ts +2 -2
  15. package/lib/createServers.d.ts +19 -0
  16. package/lib/createServers.js +57 -0
  17. package/lib/getBlockConfigFromTypeScript.js +1 -0
  18. package/lib/getBlockConfigFromTypeScript.test.js +2 -2
  19. package/lib/organization.d.ts +1 -1
  20. package/package.json +23 -6
  21. package/server/controllers/action.d.ts +6 -0
  22. package/server/controllers/action.js +8 -0
  23. package/server/controllers/appMembers.d.ts +2 -0
  24. package/server/controllers/appMembers.js +4 -0
  25. package/server/controllers/appMessages.d.ts +2 -0
  26. package/server/controllers/appMessages.js +4 -0
  27. package/server/controllers/assets.d.ts +4 -0
  28. package/server/controllers/assets.js +6 -0
  29. package/server/controllers/index.d.ts +7 -0
  30. package/server/controllers/index.js +8 -0
  31. package/server/controllers/oauth2Provider.d.ts +2 -0
  32. package/server/controllers/oauth2Provider.js +4 -0
  33. package/server/controllers/resources.d.ts +7 -0
  34. package/server/controllers/resources.js +9 -0
  35. package/server/controllers/teams.d.ts +2 -0
  36. package/server/controllers/teams.js +4 -0
  37. package/server/db/index.d.ts +2 -0
  38. package/server/db/index.js +10 -0
  39. package/server/db/methods.d.ts +11 -0
  40. package/server/db/methods.js +232 -0
  41. package/server/db/methods.test.js +365 -0
  42. package/server/models/Resource.d.ts +14 -0
  43. package/server/models/Resource.js +23 -0
  44. package/server/models/Theme.d.ts +17 -0
  45. package/server/models/Theme.js +11 -0
  46. package/server/options/applyAppServiceSecrets.d.ts +3 -0
  47. package/server/options/applyAppServiceSecrets.js +2 -0
  48. package/server/options/checkRole.d.ts +1 -0
  49. package/server/options/checkRole.js +2 -0
  50. package/server/options/checkRole.test.d.ts +1 -0
  51. package/server/options/checkRole.test.js +12 -0
  52. package/server/options/createAppAsset.d.ts +2 -0
  53. package/server/options/createAppAsset.js +15 -0
  54. package/server/options/createAppResourcesWithAssets.d.ts +3 -0
  55. package/server/options/createAppResourcesWithAssets.js +18 -0
  56. package/server/options/createSettings.d.ts +2 -0
  57. package/server/options/createSettings.js +15 -0
  58. package/server/options/createTheme.d.ts +2 -0
  59. package/server/options/createTheme.js +5 -0
  60. package/server/options/deleteAppAsset.d.ts +2 -0
  61. package/server/options/deleteAppAsset.js +15 -0
  62. package/server/options/deleteAppResource.d.ts +2 -0
  63. package/server/options/deleteAppResource.js +5 -0
  64. package/server/options/email.d.ts +1 -0
  65. package/server/options/email.js +2 -0
  66. package/server/options/getApp.d.ts +3 -0
  67. package/server/options/getApp.js +5 -0
  68. package/server/options/getAppAssets.d.ts +2 -0
  69. package/server/options/getAppAssets.js +12 -0
  70. package/server/options/getAppBlockStyles.d.ts +2 -0
  71. package/server/options/getAppBlockStyles.js +26 -0
  72. package/server/options/getAppDetails.d.ts +2 -0
  73. package/server/options/getAppDetails.js +8 -0
  74. package/server/options/getAppIcon.d.ts +3 -0
  75. package/server/options/getAppIcon.js +6 -0
  76. package/server/options/getAppMembers.d.ts +3 -0
  77. package/server/options/getAppMembers.js +4 -0
  78. package/server/options/getAppMessages.d.ts +3 -0
  79. package/server/options/getAppMessages.js +4 -0
  80. package/server/options/getAppResource.d.ts +3 -0
  81. package/server/options/getAppResource.js +5 -0
  82. package/server/options/getAppResources.d.ts +3 -0
  83. package/server/options/getAppResources.js +5 -0
  84. package/server/options/getAppScreenshots.d.ts +2 -0
  85. package/server/options/getAppScreenshots.js +20 -0
  86. package/server/options/getAppStyles.d.ts +2 -0
  87. package/server/options/getAppStyles.js +7 -0
  88. package/server/options/getAppTeams.d.ts +2 -0
  89. package/server/options/getAppTeams.js +4 -0
  90. package/server/options/getAppUrl.d.ts +2 -0
  91. package/server/options/getAppUrl.js +5 -0
  92. package/server/options/getAppUserInfo.d.ts +3 -0
  93. package/server/options/getAppUserInfo.js +4 -0
  94. package/server/options/getBlockAsset.d.ts +2 -0
  95. package/server/options/getBlockAsset.js +28 -0
  96. package/server/options/getBlockAssetsPaths.d.ts +2 -0
  97. package/server/options/getBlockAssetsPaths.js +8 -0
  98. package/server/options/getBlockMessages.d.ts +2 -0
  99. package/server/options/getBlockMessages.js +4 -0
  100. package/server/options/getCsp.d.ts +2 -0
  101. package/server/options/getCsp.js +13 -0
  102. package/server/options/getDbUpdated.d.ts +1 -0
  103. package/server/options/getDbUpdated.js +2 -0
  104. package/server/options/getHost.d.ts +2 -0
  105. package/server/options/getHost.js +5 -0
  106. package/server/options/getTheme.d.ts +2 -0
  107. package/server/options/getTheme.js +8 -0
  108. package/server/options/index.d.ts +35 -0
  109. package/server/options/index.js +36 -0
  110. package/server/options/options.d.ts +2 -0
  111. package/server/options/options.js +39 -0
  112. package/server/options/parseQuery.d.ts +2 -0
  113. package/server/options/parseQuery.js +82 -0
  114. package/server/options/parseQuery.test.d.ts +1 -0
  115. package/server/options/parseQuery.test.js +43 -0
  116. package/server/options/reloadUser.d.ts +1 -0
  117. package/server/options/reloadUser.js +2 -0
  118. package/server/options/sendNotifications.d.ts +1 -0
  119. package/server/options/sendNotifications.js +2 -0
  120. package/server/options/updateAppResource.d.ts +3 -0
  121. package/server/options/updateAppResource.js +5 -0
  122. package/server/options/verifyResourceActionPermission.d.ts +1 -0
  123. package/server/options/verifyResourceActionPermission.js +2 -0
  124. package/server/routes/appRouter/index.d.ts +2 -0
  125. package/server/routes/appRouter/index.js +4 -0
  126. package/vitest.config.d.ts +2 -0
  127. package/vitest.setup.d.ts +1 -0
  128. package/{jest.setup.js → vitest.setup.js} +1 -1
  129. /package/{jest.setup.d.ts → server/db/methods.test.d.ts} +0 -0
package/README.md CHANGED
@@ -1,20 +1,24 @@
1
- # ![](https://gitlab.com/appsemble/appsemble/-/raw/0.20.44/config/assets/logo.svg) Appsemble CLI
1
+ # ![](https://gitlab.com/appsemble/appsemble/-/raw/0.21.0/config/assets/logo.svg) Appsemble CLI
2
2
 
3
3
  > Manage apps and blocks from the command line.
4
4
 
5
5
  [![npm](https://img.shields.io/npm/v/@appsemble/cli)](https://www.npmjs.com/package/@appsemble/cli)
6
- [![GitLab CI](https://gitlab.com/appsemble/appsemble/badges/0.20.44/pipeline.svg)](https://gitlab.com/appsemble/appsemble/-/releases/0.20.44)
6
+ [![GitLab CI](https://gitlab.com/appsemble/appsemble/badges/0.21.0/pipeline.svg)](https://gitlab.com/appsemble/appsemble/-/releases/0.21.0)
7
7
  [![Prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg)](https://prettier.io)
8
8
 
9
9
  ## Table of Contents
10
10
 
11
11
  - [Installation](#installation)
12
12
  - [Usage](#usage)
13
+ - [Development server](#development-server)
13
14
  - [Authentication](#authentication)
14
15
  - [Organizations](#organizations)
15
16
  - [Apps](#apps)
16
17
  - [Blocks](#blocks)
18
+ - [Teams](#teams)
17
19
  - [Assets](#assets)
20
+ - [Resources](#resources)
21
+ - [Cronjobs](#cronjobs)
18
22
  - [License](#license)
19
23
 
20
24
  ## Installation
@@ -33,6 +37,76 @@ appsemble --help
33
37
 
34
38
  Every subcommand also supports the `--help` flag.
35
39
 
40
+ ### Development server
41
+
42
+ The development server can create an app from a specified folder containing an `app-definition.yml`
43
+ file. It will check what blocks are needed for the app and will try to load them from the local
44
+ workspaces, listed in the `package.json` file in the root of the project, if they are present. This
45
+ way, all default Appsemble blocks shipped with Appsemble are loaded automatically.
46
+
47
+ Once the development server is started, making a change to a block’s code or styles will reflect in
48
+ the browser immediately after refreshing the page, without the need of increasing the block’s
49
+ version. Running docker database containers, creating a user account and creating an organization
50
+ are not needed.
51
+
52
+ The Appsemble CLI can be used to start the development server. For example, the following command
53
+ serves `my-app`:
54
+
55
+ ```sh
56
+ appsemble serve <path-to-app-directory>
57
+ ```
58
+
59
+ This will serve the app on `http://localhost:9090`.
60
+
61
+ A different port can be specified with the `--port` parameter.
62
+
63
+ The following option allows you to view the app with a specified role:
64
+
65
+ ```sh
66
+ appsemble serve <path-to-app-directory> --user-role test
67
+ ```
68
+
69
+ The following option allows you to view the app as a Manager of a team:
70
+
71
+ ```sh
72
+ appsemble serve <path-to-app-directory> --team-role Manager
73
+ ```
74
+
75
+ App data is stored within a `db.json` file in your machine’s cache directory. Each app has their own
76
+ directory `<my-app>`.
77
+
78
+ ```sh
79
+ MacOS - /Users/<my-name>/Library/Caches/appsemble/<my-app>
80
+ Linux - /home/<my-name>/.cache/appsemble/<my-app>
81
+ Windows - C:\Users\<my-name>\AppData\Local\appsemble\Cache\<my-app>
82
+ ```
83
+
84
+ App assets will be served from the local file system.
85
+
86
+ The development server will automatically fetch all blocks that are needed for the served app but
87
+ are missing from the local workspaces. These are typically third-party or proprietary blocks. The
88
+ development server will use `https://appsemble.app` as the default remote server to fetch blocks
89
+ from. The following option allows you to specify a different remote server:
90
+
91
+ ```sh
92
+ appsemble serve <path-to-app-directory> --remote <remote>
93
+ ```
94
+
95
+ The development server will use the corresponding block directory in your machine’s cache directory
96
+ to store and read block manifests and assets.
97
+
98
+ ```sh
99
+ MacOS - /Users/<my-name>/Library/Caches/appsemble/blocks/<organisation>/<block-name>/<block-version>
100
+ Linux - /home/<my-name>/.cache/appsemble/blocks/<organisation>/<block-name>/<block-version>
101
+ Windows - C:\Users\<my-name>\AppData\Local\appsemble\Cache\blocks\<organisation>\<block-name>\<block-version>
102
+ ```
103
+
104
+ You can overwrite the existing block cache with the following option:
105
+
106
+ ```sh
107
+ appsemble serve <path-to-app-directory> --remote <remote> --overwrite-block-cache
108
+ ```
109
+
36
110
  ### Authentication
37
111
 
38
112
  The Appsemble CLI uses OAuth2 client credentials to authenticate to the Appsemble server.
@@ -69,6 +143,12 @@ The Appsemble CLI can be used to create and update apps. For example, to create
69
143
  appsemble app create apps/my-app
70
144
  ```
71
145
 
146
+ Similarly, to update an app using an app directory, run the following command:
147
+
148
+ ```sh
149
+ appsemble app update --id <app-id> <path/to/updated app>.
150
+ ```
151
+
72
152
  For a more in depth explanation of how to build apps, use our
73
153
  [app building guide](https://appsemble.app/docs/guide).
74
154
 
@@ -94,6 +174,15 @@ npm install webpack@webpack-4 @appsemble/webpack-config
94
174
  For a more in-depth explanation of how to build apps, use our
95
175
  [block development guide](https://appsemble.app/docs/development/developing-blocks).
96
176
 
177
+ ### Teams
178
+
179
+ The Appsemble CLI can be used to manage teams for apps. For example, the following command creates a
180
+ new team named `My Team`:
181
+
182
+ ```sh
183
+ appsemble team create 'My Team' --app-id 1 --context development
184
+ ```
185
+
97
186
  ### Assets
98
187
 
99
188
  The Appsemble CLI can be used to upload assets from disk. For example, the following command creates
@@ -103,7 +192,60 @@ an asset named `example-asset`:
103
192
  appsemble asset create --app-id 1 path/to/example-asset.png
104
193
  ```
105
194
 
195
+ ### Resources
196
+
197
+ The Appsemble CLI can be used to create a resource from a JSON file or directory:
198
+
199
+ ```json
200
+ [
201
+ {
202
+ "title": "My Resource",
203
+ "description": "This record is an example."
204
+ }
205
+ ]
206
+ ```
207
+
208
+ ```sh
209
+ appsemble resource create --app-id 1 --context development --app path/to/my-app my-resource path/to/resources/my-resource.json
210
+ ```
211
+
212
+ ```sh
213
+ appsemble resource create --app-id 1 --context development --app path/to/my-app my-resource path/to/resources/*
214
+ ```
215
+
216
+ And resources can also be updated when they contain an id in the JSON file.
217
+
218
+ ```json
219
+ [
220
+ {
221
+ "id": 1,
222
+ "title": "My Updated Resource",
223
+ "description": "This will be the updated content of the first my-resource record."
224
+ }
225
+ ]
226
+ ```
227
+
228
+ ```sh
229
+ appsemble resource update --app-id 1 --context development --app path/to/my-app my-resource path/to/resources/*
230
+ ```
231
+
232
+ ### Cronjobs
233
+
234
+ The Appsemble CLI can be used to run app cronjobs. The following command runs all cronjobs that were
235
+ scheduled to run in the past 5 minutes:
236
+
237
+ ```sh
238
+ appsemble run-cronjobs
239
+ ```
240
+
241
+ How often jobs are run (more accurately how far back the job can be scheduled for it to run) the
242
+ time interval (in minutes) can also be set with:
243
+
244
+ ```sh
245
+ appsemble run-cronjobs --interval 30
246
+ ```
247
+
106
248
  ## License
107
249
 
108
- [LGPL-3.0-only](https://gitlab.com/appsemble/appsemble/-/blob/0.20.44/LICENSE.md) ©
250
+ [LGPL-3.0-only](https://gitlab.com/appsemble/appsemble/-/blob/0.21.0/LICENSE.md) ©
109
251
  [Appsemble](https://appsemble.com)
@@ -1,5 +1,5 @@
1
- /// <reference types="node" />
2
- /// <reference types="node" />
1
+ /// <reference types="node" resolution-mode="require"/>
2
+ /// <reference types="node" resolution-mode="require"/>
3
3
  import { type ReadStream } from 'node:fs';
4
4
  import { type AppVisibility } from '@appsemble/types';
5
5
  import { type Argv } from 'yargs';
@@ -1,5 +1,5 @@
1
- /// <reference types="node" />
2
- /// <reference types="node" />
1
+ /// <reference types="node" resolution-mode="require"/>
2
+ /// <reference types="node" resolution-mode="require"/>
3
3
  import { type ReadStream } from 'node:fs';
4
4
  import { type AppVisibility } from '@appsemble/types';
5
5
  import { type Argv } from 'yargs';
@@ -1,4 +1,4 @@
1
- /// <reference types="node" />
1
+ /// <reference types="node" resolution-mode="require"/>
2
2
  import { type ReadStream } from 'node:fs';
3
3
  import { type Argv } from 'yargs';
4
4
  import { type BaseArguments } from '../../types.js';
@@ -1,4 +1,4 @@
1
- /// <reference types="node" />
1
+ /// <reference types="node" resolution-mode="require"/>
2
2
  import { type ReadStream } from 'node:fs';
3
3
  import { type Argv } from 'yargs';
4
4
  import { type BaseArguments } from '../../types.js';
@@ -0,0 +1,15 @@
1
+ import { type Argv } from 'yargs';
2
+ import { type BaseArguments } from '../types.js';
3
+ interface ServeArguments extends BaseArguments {
4
+ path: string;
5
+ port: number;
6
+ 'api-port': number;
7
+ 'user-role': string;
8
+ 'team-role': string;
9
+ 'overwrite-block-cache': boolean;
10
+ }
11
+ export declare const command = "serve path";
12
+ export declare const description = "Serve an app with a local development server.";
13
+ export declare function builder(yargs: Argv): Argv<any>;
14
+ export declare function handler(argv: ServeArguments): Promise<void>;
15
+ export {};
@@ -0,0 +1,265 @@
1
+ import { createWriteStream, existsSync } from 'node:fs';
2
+ import { mkdir, readFile } from 'node:fs/promises';
3
+ import http from 'node:http';
4
+ import { basename, extname, join, parse } from 'node:path';
5
+ import { Readable } from 'node:stream';
6
+ import { AppsembleError, logger, opendirSafe, readData, writeData, } from '@appsemble/node-utils';
7
+ import { asciiLogo, getAppBlocks, normalize, parseBlockName, } from '@appsemble/utils';
8
+ import axios from 'axios';
9
+ import csvToJson from 'csvtojson';
10
+ import FormData from 'form-data';
11
+ import globalCacheDir from 'global-cache-dir';
12
+ import { traverseAppDirectory } from '../lib/app.js';
13
+ import { buildBlock, getBlockConfig, makePayload } from '../lib/block.js';
14
+ import { createApiServer, createStaticServer } from '../lib/createServers.js';
15
+ import { loadWebpackConfig } from '../lib/loadWebpackConfig.js';
16
+ import { setAppName } from '../server/db/methods.js';
17
+ import { Resource } from '../server/models/Resource.js';
18
+ export const command = 'serve path';
19
+ export const description = 'Serve an app with a local development server.';
20
+ export function builder(yargs) {
21
+ return yargs
22
+ .positional('path', {
23
+ describe: 'The path to the app to publish.',
24
+ })
25
+ .option('port', {
26
+ desc: 'The static HTTP server port to use. This is the port where the app is accessible.',
27
+ type: 'number',
28
+ default: 9090,
29
+ })
30
+ .option('api-port', {
31
+ desc: 'The Appsemble development HTTP server port to use. This is the port where the Appsemble development API endpoints are available.',
32
+ type: 'number',
33
+ default: 9191,
34
+ })
35
+ .option('user-role', {
36
+ desc: 'The role to set to the mocked authenticated user.',
37
+ type: 'string',
38
+ })
39
+ .option('team-role', {
40
+ desc: 'The role to set to the mocked authenticated user in the team.',
41
+ type: 'string',
42
+ default: 'member',
43
+ })
44
+ .option('remote', {
45
+ desc: 'The address to fetch remote blocks from.',
46
+ type: 'string',
47
+ })
48
+ .option('overwrite-block-cache', {
49
+ desc: 'Whether to overwrite remote blocks cache if it exists.',
50
+ type: 'boolean',
51
+ default: false,
52
+ });
53
+ }
54
+ export async function handler(argv) {
55
+ const appPath = join(process.cwd(), argv.path);
56
+ const [, , , appsembleApp] = await traverseAppDirectory(appPath, 'development', new FormData());
57
+ const appRoles = appsembleApp.definition.roles;
58
+ const passedUserRole = argv['user-role'];
59
+ if (passedUserRole && !(appRoles === null || appRoles === void 0 ? void 0 : appRoles.includes(passedUserRole))) {
60
+ throw appRoles
61
+ ? new AppsembleError(`The specified role "${passedUserRole}" is not supported by this app. Supported roles are [${appRoles}]`)
62
+ : new AppsembleError('This app does not support roles');
63
+ }
64
+ const appSecurity = appsembleApp.definition.security;
65
+ const appMembers = [
66
+ {
67
+ id: '1',
68
+ name: 'dev',
69
+ primaryEmail: 'dev@example.com',
70
+ role: passedUserRole || (appSecurity === null || appSecurity === void 0 ? void 0 : appSecurity.default.role),
71
+ },
72
+ ];
73
+ const allowedTeamRoles = ['manager', 'member'];
74
+ const passedTeamRole = argv['team-role'];
75
+ if (passedTeamRole && !allowedTeamRoles.includes(passedTeamRole)) {
76
+ throw new AppsembleError(`The specified team role ${passedTeamRole} is not supported. Allowed roles are [member,manager]`);
77
+ }
78
+ const appTeams = [
79
+ {
80
+ id: 1,
81
+ name: 'team',
82
+ size: 1,
83
+ role: passedTeamRole,
84
+ annotations: {},
85
+ },
86
+ ];
87
+ const appUserInfo = {
88
+ email: 'dev@example.com',
89
+ email_verified: true,
90
+ name: 'dev',
91
+ sub: '1',
92
+ };
93
+ const appName = normalize(appsembleApp.definition.name);
94
+ const appId = 1;
95
+ setAppName(appName);
96
+ const identifiableBlocks = getAppBlocks(appsembleApp.definition);
97
+ const localIdentifiableBlocks = [];
98
+ const remoteIdentifiableBlocks = [];
99
+ for (const identifiableBlock of identifiableBlocks) {
100
+ const [, blockName] = parseBlockName(identifiableBlock.type);
101
+ if (existsSync(join(process.cwd(), 'blocks', blockName))) {
102
+ localIdentifiableBlocks.push(identifiableBlock);
103
+ }
104
+ else {
105
+ remoteIdentifiableBlocks.push(identifiableBlock);
106
+ }
107
+ }
108
+ const localBlocksConfigs = await Promise.all(localIdentifiableBlocks.map(async (identifiableBlock) => {
109
+ const [organization, blockName] = parseBlockName(identifiableBlock.type);
110
+ const blockConfig = await getBlockConfig(join(process.cwd(), 'blocks', blockName));
111
+ return {
112
+ ...blockConfig,
113
+ OrganizationId: organization,
114
+ };
115
+ }));
116
+ const localBlocksPromises = localBlocksConfigs.map(async (blockConfig) => {
117
+ await buildBlock(blockConfig);
118
+ const [, blockManifest] = await makePayload(blockConfig);
119
+ blockManifest.version = localIdentifiableBlocks.find((identifiableBlock) => identifiableBlock.type === blockManifest.name).version;
120
+ return blockManifest;
121
+ });
122
+ const cacheDir = await globalCacheDir('appsemble');
123
+ const remoteBlocksPromises = remoteIdentifiableBlocks.map(async (identifiableBlock) => {
124
+ const [organization, blockName] = parseBlockName(identifiableBlock.type);
125
+ const blockCacheDir = join(cacheDir, 'blocks', organization, blockName, identifiableBlock.version);
126
+ const cachedBlockManifest = join(blockCacheDir, 'manifest.json');
127
+ const cacheExists = existsSync(cachedBlockManifest);
128
+ if (!cacheExists || (cacheExists && argv['overwrite-block-cache'])) {
129
+ const blockUrl = `/api/blocks/@${organization}/${blockName}/versions/${identifiableBlock.version}`;
130
+ const { data: blockManifest } = await axios.get(String(new URL(blockUrl, argv.remote)));
131
+ await writeData(cachedBlockManifest, blockManifest);
132
+ const assetsDir = join(blockCacheDir, 'assets');
133
+ if (!existsSync(assetsDir)) {
134
+ await mkdir(assetsDir);
135
+ }
136
+ const blockFilesPromises = blockManifest.files.map(async (filename) => {
137
+ const writer = createWriteStream(join(assetsDir, filename));
138
+ const { data: content } = await axios.get(String(new URL(`${blockUrl}/asset?filename=${filename}`, argv.remote)), {
139
+ responseType: 'stream',
140
+ });
141
+ content.pipe(writer);
142
+ });
143
+ await Promise.all(blockFilesPromises);
144
+ return blockManifest;
145
+ }
146
+ const [blockManifest] = await readData(cachedBlockManifest);
147
+ return blockManifest;
148
+ });
149
+ const localBlocks = await Promise.all(localBlocksPromises);
150
+ const remoteBlocks = await Promise.all(remoteBlocksPromises);
151
+ const appBlocks = [...localBlocks, ...remoteBlocks];
152
+ const webpackConfigs = await Promise.all(localBlocksConfigs.map((blockConfig) => loadWebpackConfig(blockConfig, 'development', join(blockConfig.dir, blockConfig.output))));
153
+ // Get app messages
154
+ const appMessages = [];
155
+ await opendirSafe(join(appPath, 'i18n'), async (path) => {
156
+ logger.verbose(`Processing ${path} ⚙️`);
157
+ const { name: language } = parse(path);
158
+ if (appMessages.some((entry) => entry.language === language)) {
159
+ throw new AppsembleError(`Found duplicate language “${language}”. Make sure each language only exists once in the directory.`);
160
+ }
161
+ const [messages] = await readData(path);
162
+ appMessages.push({ language, messages });
163
+ }, { allowMissing: true });
164
+ // Get app assets
165
+ const appAssets = [];
166
+ await opendirSafe(join(appPath, 'assets'), (path) => {
167
+ logger.verbose(`Processing ${path} ⚙️`);
168
+ const extension = extname(path);
169
+ const name = basename(path, extension);
170
+ appAssets.push({
171
+ id: name,
172
+ mime: extension,
173
+ filename: path,
174
+ name,
175
+ });
176
+ }, { allowMissing: true });
177
+ // Get app resources
178
+ await opendirSafe(join(appPath, 'resources'), async (path, stat) => {
179
+ logger.verbose(`Processing ${path} ⚙️`);
180
+ let resources;
181
+ if (path.endsWith('.csv')) {
182
+ const data = await readFile(path);
183
+ const stream = Readable.from(data);
184
+ resources = await csvToJson({}).fromStream(stream);
185
+ }
186
+ else {
187
+ const [resource] = await readData(path);
188
+ if (typeof resource !== 'object') {
189
+ throw new AppsembleError(`File at ${path} does not contain an object or array of objects`);
190
+ }
191
+ resources = [].concat(resource);
192
+ }
193
+ logger.info(`Creating resource(s) from ${path}`);
194
+ const { name } = parse(stat.name);
195
+ await Resource.bulkCreate(resources, name, true);
196
+ }, { allowMissing: true });
197
+ const stubbedApp = {
198
+ ...appsembleApp,
199
+ id: appId,
200
+ path: appPath,
201
+ coreStyle: appsembleApp.coreStyle || '',
202
+ sharedStyle: appsembleApp.sharedStyle || '',
203
+ $updated: new Date().toISOString(),
204
+ };
205
+ const appsembleApiServer = createApiServer({
206
+ argv,
207
+ context: {
208
+ appHost: `http://localhost:${argv.port}`,
209
+ appsembleApp: stubbedApp,
210
+ appBlocks,
211
+ appMessages,
212
+ ...(appSecurity
213
+ ? {
214
+ appMembers,
215
+ appUserInfo,
216
+ appTeams,
217
+ }
218
+ : {}),
219
+ appAssets,
220
+ blockConfigs: localBlocksConfigs,
221
+ },
222
+ });
223
+ appsembleApiServer.on('error', (err) => {
224
+ if (err.expose) {
225
+ return;
226
+ }
227
+ logger.error(err);
228
+ });
229
+ const api = http.createServer(appsembleApiServer.callback());
230
+ const staticServer = await createStaticServer({
231
+ argv,
232
+ context: {
233
+ apiUrl: `http://localhost:${argv['api-port']}`,
234
+ appHost: `http://localhost:${argv.port}`,
235
+ appsembleApp: stubbedApp,
236
+ appBlocks,
237
+ appMessages,
238
+ ...(appSecurity
239
+ ? {
240
+ appMembers,
241
+ appUserInfo,
242
+ appTeams,
243
+ }
244
+ : {}),
245
+ appAssets,
246
+ blockConfigs: localBlocksConfigs,
247
+ },
248
+ webpackConfigs,
249
+ });
250
+ staticServer.on('error', (err) => {
251
+ if (err.expose) {
252
+ return;
253
+ }
254
+ logger.error(err);
255
+ });
256
+ const app = http.createServer(staticServer.callback());
257
+ api.listen(argv['api-port'], '::', () => {
258
+ logger.info(asciiLogo);
259
+ logger.info(`The api can be found on\n> http://localhost:${argv['api-port']}`);
260
+ });
261
+ app.listen(argv.port, '::', () => {
262
+ logger.info(`\nThe app can be found on\n> http://localhost:${argv.port}`);
263
+ });
264
+ }
265
+ //# sourceMappingURL=serve.js.map
package/index.js CHANGED
@@ -14,6 +14,7 @@ import * as migrate from './commands/migrate.js';
14
14
  import * as organization from './commands/organization/index.js';
15
15
  import * as resource from './commands/resource/index.js';
16
16
  import * as runCronJobs from './commands/runCronJobs.js';
17
+ import * as serve from './commands/serve.js';
17
18
  import * as start from './commands/start.js';
18
19
  import * as team from './commands/team/index.js';
19
20
  import { CREDENTIALS_ENV_VAR } from './lib/authentication.js';
@@ -56,6 +57,7 @@ let parser = yargs(process.argv.slice(2))
56
57
  .command(resource)
57
58
  .command(runCronJobs)
58
59
  .command(start)
60
+ .command(serve)
59
61
  .command(team)
60
62
  .demandCommand(1)
61
63
  // .strict() isn’t used because of https://github.com/yargs/yargs/issues/2058
package/lib/app.d.ts CHANGED
@@ -1,7 +1,7 @@
1
- /// <reference types="node" />
2
- /// <reference types="node" />
1
+ /// <reference types="node" resolution-mode="require"/>
2
+ /// <reference types="node" resolution-mode="require"/>
3
3
  import { type ReadStream } from 'node:fs';
4
- import { type AppVisibility } from '@appsemble/types';
4
+ import { type App, type AppVisibility } from '@appsemble/types';
5
5
  import FormData from 'form-data';
6
6
  import { type AppsembleContext, type AppsembleRC } from '../types.js';
7
7
  interface CreateAppParams {
@@ -137,7 +137,7 @@ interface UpdateAppParams {
137
137
  * @param formData The FormData object to append the results into..
138
138
  * @returns The context from `.appsemblerc.yaml` if a match was found.
139
139
  */
140
- export declare function traverseAppDirectory(path: string, context: string, formData: FormData): Promise<[AppsembleContext, AppsembleRC, string]>;
140
+ export declare function traverseAppDirectory(path: string, context: string, formData: FormData): Promise<[AppsembleContext, AppsembleRC, string, App]>;
141
141
  /**
142
142
  * Upload messages for an app.
143
143
  *
package/lib/app.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { createReadStream, existsSync } from 'node:fs';
2
2
  import { mkdir, readdir, readFile, stat } from 'node:fs/promises';
3
- import { join, parse, relative, resolve } from 'node:path';
3
+ import { basename, join, parse, relative, resolve } from 'node:path';
4
4
  import { inspect } from 'node:util';
5
5
  import { AppsembleError, logger, opendirSafe, readData, writeData } from '@appsemble/node-utils';
6
6
  import { extractAppMessages, has, normalizeBlockName } from '@appsemble/utils';
@@ -26,6 +26,9 @@ export async function traverseAppDirectory(path, context, formData) {
26
26
  let iconPath;
27
27
  let maskableIconPath;
28
28
  let yaml;
29
+ const gatheredData = {
30
+ screenshotUrls: [],
31
+ };
29
32
  logger.info(`Traversing directory for App files in ${path} 🕵`);
30
33
  await opendirSafe(path, async (filepath, filestat) => {
31
34
  switch (filestat.name.toLowerCase()) {
@@ -34,6 +37,7 @@ export async function traverseAppDirectory(path, context, formData) {
34
37
  [rc] = await readData(filepath);
35
38
  if ('iconBackground' in rc) {
36
39
  formData.append('iconBackground', rc.iconBackground);
40
+ gatheredData.iconBackground = rc.iconBackground;
37
41
  }
38
42
  if (context && has(rc === null || rc === void 0 ? void 0 : rc.context, context)) {
39
43
  discoveredContext = rc.context[context];
@@ -45,14 +49,16 @@ export async function traverseAppDirectory(path, context, formData) {
45
49
  if (yaml !== undefined) {
46
50
  throw new AppsembleError('Found duplicate app definition');
47
51
  }
48
- const [, data] = await readData(filepath);
49
- yaml = data;
50
- formData.append('yaml', data);
52
+ const [data, dataString] = await readData(filepath);
53
+ yaml = dataString;
54
+ formData.append('yaml', dataString);
55
+ gatheredData.definition = data;
51
56
  return;
52
57
  }
53
58
  case 'icon.png':
54
59
  case 'icon.svg':
55
60
  iconPath = filepath;
61
+ gatheredData.iconUrl = filepath;
56
62
  return;
57
63
  case 'maskable-icon.png':
58
64
  maskableIconPath = filepath;
@@ -60,6 +66,7 @@ export async function traverseAppDirectory(path, context, formData) {
60
66
  case 'readme.md':
61
67
  logger.info(`Including longDescription from ${filepath}`);
62
68
  formData.append('longDescription', await readFile(filepath, 'utf8'));
69
+ gatheredData.longDescription = await readFile(filepath, 'utf8');
63
70
  return;
64
71
  case 'screenshots':
65
72
  return opendirSafe(filepath, (screenshotPath, screenshotStat) => {
@@ -68,6 +75,7 @@ export async function traverseAppDirectory(path, context, formData) {
68
75
  throw new AppsembleError(`Expected ${screenshotPath} to be an image file`);
69
76
  }
70
77
  formData.append('screenshots', createReadStream(screenshotPath));
78
+ gatheredData.screenshotUrls.push(basename(screenshotPath));
71
79
  }, { allowMissing: true });
72
80
  case 'theme':
73
81
  return opendirSafe(filepath, async (themeDir, themeStat) => {
@@ -80,6 +88,7 @@ export async function traverseAppDirectory(path, context, formData) {
80
88
  }
81
89
  const css = await processCss(join(themeDir, 'index.css'));
82
90
  formData.append(`${name}Style`, css);
91
+ gatheredData[`${name}Style`] = await processCss(join(themeDir, 'index.css'));
83
92
  });
84
93
  default:
85
94
  logger.warn(`Found unused file ${filepath}`);
@@ -95,7 +104,7 @@ export async function traverseAppDirectory(path, context, formData) {
95
104
  discoveredContext.maskableIcon = discoveredContext.maskableIcon
96
105
  ? resolve(path, discoveredContext.maskableIcon)
97
106
  : maskableIconPath;
98
- return [discoveredContext, rc, yaml];
107
+ return [discoveredContext, rc, yaml, gatheredData];
99
108
  }
100
109
  /**
101
110
  * Upload messages for an app.
package/lib/block.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type BlockConfig } from '@appsemble/types';
1
+ import { type BlockConfig, type BlockManifest } from '@appsemble/types';
2
2
  import FormData from 'form-data';
3
3
  import { type Stats } from 'webpack';
4
4
  /**
@@ -30,7 +30,7 @@ export declare function discoverBlocks(root: string): Promise<BlockConfig[]>;
30
30
  * @param config The block configuration
31
31
  * @returns The payload that should be sent to the version endpoint.
32
32
  */
33
- export declare function makePayload(config: BlockConfig): Promise<FormData>;
33
+ export declare function makePayload(config: BlockConfig): Promise<[FormData, BlockManifest]>;
34
34
  /**
35
35
  * Publish a new block version.
36
36
  *