@appsemble/cli 0.40.8 → 0.40.10

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/README.md CHANGED
@@ -1,9 +1,9 @@
1
- # ![](https://gitlab.com/appsemble/appsemble/-/raw/0.40.8/config/assets/logo.svg) Appsemble CLI
1
+ # ![](https://gitlab.com/appsemble/appsemble/-/raw/0.40.10/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.40.8/pipeline.svg)](https://gitlab.com/appsemble/appsemble/-/releases/0.40.8)
6
+ [![GitLab CI](https://gitlab.com/appsemble/appsemble/badges/0.40.10/pipeline.svg)](https://gitlab.com/appsemble/appsemble/-/releases/0.40.10)
7
7
  [![Prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg)](https://prettier.io)
8
8
 
9
9
  ## Table of Contents
@@ -271,5 +271,5 @@ appsemble run-cronjobs --interval 30
271
271
 
272
272
  ## License
273
273
 
274
- [LGPL-3.0-only](https://gitlab.com/appsemble/appsemble/-/blob/0.40.8/LICENSE.md) ©
274
+ [LGPL-3.0-only](https://gitlab.com/appsemble/appsemble/-/blob/0.40.10/LICENSE.md) ©
275
275
  [Appsemble](https://appsemble.com)
@@ -60,7 +60,7 @@ export function builder(yargs) {
60
60
  type: 'boolean',
61
61
  })
62
62
  .option('assets', {
63
- describe: 'Whether the assets from the `assets` directory should replace the seed assets of the app being updated.',
63
+ describe: 'Whether the assets of the app being updated should be synced with the `assets` directory. Unchanged assets are kept.',
64
64
  type: 'boolean',
65
65
  default: false,
66
66
  })
package/lib/app.d.ts CHANGED
@@ -38,7 +38,16 @@ interface DeleteAppArgs {
38
38
  export declare function deleteApp({ clientCredentials, id, remote }: DeleteAppArgs): Promise<void>;
39
39
  export declare function publishSeedResources(path: string, app: App, remote: string): Promise<void>;
40
40
  export declare function publishSeedAppMembers(path: string, app: App, remote: string): Promise<void>;
41
- export declare function publishSeedAssets(path: string, app: App, remote: string, assetsClonable: boolean): Promise<void>;
41
+ /**
42
+ * Read the seed asset files of an app.
43
+ *
44
+ * @param assetsPath The path to the assets directory.
45
+ * @returns The absolute file paths by the normalized name of the asset, or null if the directory
46
+ * doesn't exist.
47
+ * @throws AppsembleError if a file normalizes to an empty name, or two files to the same name.
48
+ */
49
+ export declare function getSeedAssetFiles(assetsPath: string): Promise<Map<string, string> | null>;
50
+ export declare function publishSeedAssets(path: string, files: Map<string, string> | null, app: App, remote: string, assetsClonable: boolean): Promise<void>;
42
51
  export declare function publishAppConfig(path: string, app: App, remote: string): Promise<void>;
43
52
  /**
44
53
  * @param path The path to the app directory.
package/lib/app.js CHANGED
@@ -2,8 +2,8 @@ import { createReadStream, createWriteStream, existsSync } from 'node:fs';
2
2
  import { mkdir, readdir, readFile, rm, stat } from 'node:fs/promises';
3
3
  import { basename, dirname, join, parse, relative, resolve } from 'node:path';
4
4
  import { inspect } from 'node:util';
5
- import { normalizeBlockName } from '@appsemble/lang-sdk';
6
- import { applyAppVariant, AppsembleError, authenticate, logger, opendirSafe, preProcessCSV, readData, writeData, } from '@appsemble/node-utils';
5
+ import { normalize, normalizeBlockName, } from '@appsemble/lang-sdk';
6
+ import { applyAppVariant, AppsembleError, authenticate, hashFile, logger, opendirSafe, preProcessCSV, readData, writeData, } from '@appsemble/node-utils';
7
7
  import { APP_VALIDATION_FAILED, } from '@appsemble/types';
8
8
  import { extractAppMessages, has, normalizeLocale } from '@appsemble/utils';
9
9
  import axios from 'axios';
@@ -363,40 +363,89 @@ export async function publishSeedAppMembers(path, app, remote) {
363
363
  logger.warn(`Missing members directory in ${path}. Skipping...`);
364
364
  }
365
365
  }
366
- export async function publishSeedAssets(path, app, remote, assetsClonable) {
366
+ /**
367
+ * Read the seed asset files of an app.
368
+ *
369
+ * @param assetsPath The path to the assets directory.
370
+ * @returns The absolute file paths by the normalized name of the asset, or null if the directory
371
+ * doesn't exist.
372
+ * @throws AppsembleError if a file normalizes to an empty name, or two files to the same name.
373
+ */
374
+ export async function getSeedAssetFiles(assetsPath) {
375
+ if (!existsSync(assetsPath)) {
376
+ return null;
377
+ }
378
+ const assetFiles = await readdir(assetsPath);
379
+ const normalizedPaths = assetFiles.map((assetFile) => normalizePath(join(assetsPath, assetFile)));
380
+ const files = await fg(normalizedPaths, { absolute: true, onlyFiles: true });
381
+ const filesByName = new Map();
382
+ for (const file of files) {
383
+ const name = normalize(parse(file).name);
384
+ if (!name) {
385
+ throw new AppsembleError(`Asset file ${basename(file)} normalizes to an empty name`);
386
+ }
387
+ const collision = filesByName.get(name);
388
+ if (collision) {
389
+ throw new AppsembleError(`Asset files ${basename(collision)} and ${basename(file)} both normalize to ${name}`);
390
+ }
391
+ filesByName.set(name, file);
392
+ }
393
+ return filesByName;
394
+ }
395
+ export async function publishSeedAssets(path, files, app, remote, assetsClonable) {
367
396
  const assetsPath = join(path, 'assets');
368
397
  logger.info(`Publishing seed assets from ${assetsPath}`);
369
- if (existsSync(assetsPath)) {
370
- logger.info(`Deleting existing seed assets from app ${app.id}`);
371
- try {
372
- await axios.delete(`/api/apps/${app.id}/assets`, {
373
- data: [],
398
+ if (!files) {
399
+ logger.warn(`Missing assets directory in ${path}. Skipping...`);
400
+ return;
401
+ }
402
+ const assetsUrl = `/api/apps/${app.id}/assets`;
403
+ try {
404
+ let serverAssets = [];
405
+ if (app.demoMode) {
406
+ // The asset endpoints only reach the ephemeral copies of a demo app, and the cleanup job
407
+ // gives those new ids on every run, so their seed assets can't be compared one by one.
408
+ logger.info(`Deleting existing seed assets from app ${app.id}`);
409
+ await axios.delete(assetsUrl, { data: [], baseURL: remote, params: { seed: true } });
410
+ }
411
+ else {
412
+ ({ data: serverAssets } = await axios.get(assetsUrl, {
374
413
  baseURL: remote,
375
- params: { seed: true },
376
- });
377
- const assetFiles = await readdir(assetsPath);
378
- const normalizedPaths = assetFiles.map((assetFile) => normalizePath(join(assetsPath, assetFile)));
379
- const files = await fg(normalizedPaths, { absolute: true, onlyFiles: true });
380
- logger.info(`Publishing ${files.length} asset(s)`);
381
- for (const assetFilePath of files) {
382
- await publishAsset({
383
- name: parse(assetFilePath).name,
384
- // @ts-expect-error 2322 null is not assignable to type (strictNullChecks)
385
- appId: app.id,
386
- path: assetFilePath,
387
- remote,
388
- seed: true,
389
- clonable: assetsClonable,
390
- });
414
+ params: { named: true },
415
+ }));
416
+ }
417
+ const serverAssetsByName = new Map(serverAssets.map((asset) => [asset.name, asset]));
418
+ for (const [name, file] of files) {
419
+ const serverAsset = serverAssetsByName.get(name);
420
+ if (serverAsset) {
421
+ if (serverAsset.sha256 === (await hashFile(file)) &&
422
+ serverAsset.clonable === assetsClonable) {
423
+ logger.info(`Keeping unchanged asset ${name}`);
424
+ continue;
425
+ }
426
+ await axios.delete(assetsUrl, { data: [serverAsset.id], baseURL: remote });
391
427
  }
428
+ await publishAsset({
429
+ name,
430
+ // @ts-expect-error 2322 null is not assignable to type (strictNullChecks)
431
+ appId: app.id,
432
+ path: file,
433
+ remote,
434
+ seed: true,
435
+ clonable: assetsClonable,
436
+ });
392
437
  }
393
- catch (error) {
394
- logger.error('Something went wrong when creating assets:');
395
- logger.error(error);
438
+ const removedSeedAssetIds = serverAssets
439
+ .filter(({ name, seed }) => seed && !files.has(name))
440
+ .map(({ id }) => id);
441
+ if (removedSeedAssetIds.length) {
442
+ logger.info(`Deleting ${removedSeedAssetIds.length} seed asset(s) without a local file`);
443
+ await axios.delete(assetsUrl, { data: removedSeedAssetIds, baseURL: remote });
396
444
  }
397
445
  }
398
- else {
399
- logger.warn(`Missing assets directory in ${path}. Skipping...`);
446
+ catch (error) {
447
+ logger.error('Something went wrong when creating assets:');
448
+ logger.error(error);
400
449
  }
401
450
  }
402
451
  function parseValueFromDefinition(value) {
@@ -899,6 +948,7 @@ export async function publishApp({ clientCredentials, context, dryRun, modifyCon
899
948
  logger.info(`Setting TOTP to ${totp}`);
900
949
  formData.append('totp', totp);
901
950
  }
951
+ const seedAssetFiles = assets && file.isDirectory() ? await getSeedAssetFiles(join(appVariantPath, 'assets')) : null;
902
952
  let authScope = 'apps:write';
903
953
  if (resources) {
904
954
  authScope += ' resources:write';
@@ -939,7 +989,7 @@ export async function publishApp({ clientCredentials, context, dryRun, modifyCon
939
989
  await publishAppConfig(appVariantPath, data, remote);
940
990
  // After uploading the app, publish seed resources and assets if they are available
941
991
  if (assets) {
942
- await publishSeedAssets(appVariantPath, data, remote, assetsClonable);
992
+ await publishSeedAssets(appVariantPath, seedAssetFiles, data, remote, assetsClonable);
943
993
  }
944
994
  if (resources) {
945
995
  await publishSeedResources(appVariantPath, data, remote);
@@ -1119,6 +1169,7 @@ export async function updateApp({ clientCredentials, context, force, path, ...op
1119
1169
  logger.info(`Setting TOTP to ${totp}`);
1120
1170
  formData.append('totp', totp);
1121
1171
  }
1172
+ const seedAssetFiles = assets && file.isDirectory() ? await getSeedAssetFiles(join(appVariantPath, 'assets')) : null;
1122
1173
  let authScope = 'apps:write';
1123
1174
  if (resources) {
1124
1175
  authScope += ' resources:write';
@@ -1163,7 +1214,7 @@ export async function updateApp({ clientCredentials, context, force, path, ...op
1163
1214
  await publishAppConfig(appVariantPath, data, remote);
1164
1215
  // After updating the app, publish seed resources and assets if they are available
1165
1216
  if (assets && (data.locked !== 'fullLock' || force)) {
1166
- await publishSeedAssets(appVariantPath, data, remote, assetsClonable);
1217
+ await publishSeedAssets(appVariantPath, seedAssetFiles, data, remote, assetsClonable);
1167
1218
  }
1168
1219
  if (resources && (data.locked !== 'fullLock' || force)) {
1169
1220
  await publishSeedResources(appVariantPath, data, remote);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@appsemble/cli",
3
- "version": "0.40.8",
3
+ "version": "0.40.10",
4
4
  "description": "The CLI for developing with Appsemble apps and blocks",
5
5
  "keywords": [
6
6
  "app",
@@ -43,10 +43,10 @@
43
43
  "test": "vitest"
44
44
  },
45
45
  "dependencies": {
46
- "@appsemble/node-utils": "0.40.8",
47
- "@appsemble/types": "0.40.8",
48
- "@appsemble/lang-sdk": "0.40.8",
49
- "@appsemble/utils": "0.40.8",
46
+ "@appsemble/node-utils": "0.40.10",
47
+ "@appsemble/types": "0.40.10",
48
+ "@appsemble/lang-sdk": "0.40.10",
49
+ "@appsemble/utils": "0.40.10",
50
50
  "@fortawesome/fontawesome-common-types": "^6.0.0",
51
51
  "@inquirer/prompts": "^8.0.0",
52
52
  "axios": "^1.0.0",
@@ -77,8 +77,8 @@
77
77
  "yargs": "^17.0.0"
78
78
  },
79
79
  "devDependencies": {
80
- "@appsemble/types": "0.40.8",
81
- "@appsemble/server": "0.40.8",
80
+ "@appsemble/types": "0.40.10",
81
+ "@appsemble/server": "0.40.10",
82
82
  "@types/concat-stream": "2.0.3",
83
83
  "@types/normalize-path": "3.0.2",
84
84
  "@types/postcss-import": "14.0.3",
@@ -6,7 +6,7 @@ pages:
6
6
  - name: Example Page A
7
7
  blocks:
8
8
  - type: action-button
9
- version: 0.40.8
9
+ version: 0.40.10
10
10
  parameters:
11
11
  icon: arrow-right
12
12
  actions:
@@ -17,7 +17,7 @@ pages:
17
17
  - name: Example Page B
18
18
  blocks:
19
19
  - type: action-button
20
- version: 0.40.8
20
+ version: 0.40.10
21
21
  parameters:
22
22
  icon: arrow-left
23
23
  actions:
@@ -5,7 +5,7 @@
5
5
  "*.css"
6
6
  ],
7
7
  "dependencies": {
8
- "@appsemble/sdk": "0.40.8",
8
+ "@appsemble/sdk": "0.40.10",
9
9
  "mini-jsx": "^4.0.0"
10
10
  }
11
11
  }
@@ -5,8 +5,8 @@
5
5
  "*.css"
6
6
  ],
7
7
  "dependencies": {
8
- "@appsemble/preact": "0.40.8",
9
- "@appsemble/sdk": "0.40.8",
8
+ "@appsemble/preact": "0.40.10",
9
+ "@appsemble/sdk": "0.40.10",
10
10
  "preact": "^10.0.0"
11
11
  }
12
12
  }
@@ -5,6 +5,6 @@
5
5
  "*.css"
6
6
  ],
7
7
  "dependencies": {
8
- "@appsemble/sdk": "0.40.8"
8
+ "@appsemble/sdk": "0.40.10"
9
9
  }
10
10
  }