@abtnode/core 1.6.14 → 1.6.18

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.
@@ -4,6 +4,7 @@
4
4
  /* eslint-disable no-underscore-dangle */
5
5
  const omit = require('lodash/omit');
6
6
  const uniq = require('lodash/uniq');
7
+ const cloneDeep = require('lodash/cloneDeep');
7
8
  const detectPort = require('detect-port');
8
9
  const Lock = require('@abtnode/util/lib/lock');
9
10
  const security = require('@abtnode/util/lib/security');
@@ -119,7 +120,7 @@ class BlockletState extends BaseState {
119
120
  }
120
121
 
121
122
  this.emit('remove', doc);
122
- return resolve(doc);
123
+ return resolve(formatBlocklet(doc, 'onRead', this.options.dek));
123
124
  });
124
125
  })
125
126
  );
@@ -130,9 +131,9 @@ class BlockletState extends BaseState {
130
131
  meta,
131
132
  source = BlockletSource.registry,
132
133
  status = BlockletStatus.added,
133
- deployedFrom,
134
+ deployedFrom = '',
134
135
  mode = BLOCKLET_MODES.PRODUCTION,
135
- childrenMeta = [],
136
+ children: rawChildren = [],
136
137
  } = {}) {
137
138
  return this.getBlocklet(did).then(
138
139
  (doc) =>
@@ -152,7 +153,7 @@ class BlockletState extends BaseState {
152
153
 
153
154
  const ports = await this.getBlockletPorts({ interfaces: sanitized.interfaces || [] });
154
155
 
155
- const children = await this.fillChildrenPorts(this.getChildrenFromMetas(childrenMeta), {
156
+ const children = await this.fillChildrenPorts(rawChildren, {
156
157
  defaultPort: getMaxPort(ports),
157
158
  });
158
159
 
@@ -196,7 +197,7 @@ class BlockletState extends BaseState {
196
197
  }
197
198
 
198
199
  try {
199
- const formatted = formatBlocklet(updates, 'onUpdate', this.options.dek);
200
+ const formatted = formatBlocklet(cloneDeep(updates), 'onUpdate', this.options.dek);
200
201
  const newDoc = await this.updateById(doc._id, { $set: formatted });
201
202
  resolve(newDoc);
202
203
  } catch (err) {
@@ -206,7 +207,7 @@ class BlockletState extends BaseState {
206
207
  );
207
208
  }
208
209
 
209
- upgradeBlocklet({ meta, source, deployedFrom, children } = {}) {
210
+ upgradeBlocklet({ meta, source, deployedFrom = '', children } = {}) {
210
211
  return this.getBlocklet(meta.did).then(
211
212
  (doc) =>
212
213
  // eslint-disable-next-line no-async-promise-executor
@@ -247,8 +248,10 @@ class BlockletState extends BaseState {
247
248
  },
248
249
  });
249
250
  lock.release();
250
- this.emit('upgrade', newDoc);
251
- resolve(newDoc);
251
+
252
+ const formatted = formatBlocklet(newDoc, 'onRead', this.options.dek);
253
+ this.emit('upgrade', formatted);
254
+ resolve(formatted);
252
255
  } catch (err) {
253
256
  lock.release();
254
257
  reject(err);
@@ -392,11 +395,12 @@ class BlockletState extends BaseState {
392
395
  if (typeof status === 'undefined') {
393
396
  throw new Error('Unsupported blocklet status');
394
397
  }
395
- const record = await this.getBlocklet(did);
396
- // when status change update record
397
- if (record.status === status) {
398
- return record;
398
+
399
+ const doc = await this.getBlocklet(did);
400
+ if (doc.status === status) {
401
+ return formatBlocklet(doc, 'onRead', this.options.dek);
399
402
  }
403
+
400
404
  const updates = { status, startedAt: undefined, stoppedAt: undefined };
401
405
  if (status === BlockletStatus.running) {
402
406
  updates.startedAt = new Date();
@@ -408,14 +412,8 @@ class BlockletState extends BaseState {
408
412
  updates.stoppedAt = new Date();
409
413
  }
410
414
 
411
- const res = await this.update(record._id, {
412
- $set: updates,
413
- });
414
- return res;
415
- }
416
-
417
- getChildrenFromMetas(childrenMeta) {
418
- return childrenMeta.map((x) => ({ meta: x }));
415
+ await this.update(doc._id, { $set: updates });
416
+ return formatBlocklet({ ...doc, ...updates }, 'onRead', this.options.dek);
419
417
  }
420
418
 
421
419
  async fillChildrenPorts(children, { defaultPort = 0, oldChildren } = {}) {
@@ -49,7 +49,10 @@ class NodeState extends BaseState {
49
49
  }
50
50
 
51
51
  isInitialized(doc) {
52
- return !!doc.nodeOwner;
52
+ const isOwnerConnected = !!doc.nodeOwner;
53
+ const isControlledBy3rdParty =
54
+ !doc.enablePassportIssuance && Array.isArray(doc.trustedPassports) && doc.trustedPassports.length > 0;
55
+ return isOwnerConnected || isControlledBy3rdParty;
53
56
  }
54
57
 
55
58
  /**
@@ -92,13 +95,15 @@ class NodeState extends BaseState {
92
95
  launcherInfo,
93
96
  didRegistry,
94
97
  didDomain,
98
+ enablePassportIssuance = true,
99
+ trustedPassports = [],
95
100
  } = this.options;
96
101
 
97
102
  if (nodeOwner && !validateOwner(nodeOwner)) {
98
103
  return reject(new Error('Node owner is invalid'));
99
104
  }
100
105
 
101
- const initialized = this.isInitialized({ nodeOwner });
106
+ const initialized = this.isInitialized({ nodeOwner, enablePassportIssuance, trustedPassports });
102
107
 
103
108
  return getDefaultConfigs()
104
109
  .then((defaultConfigs) =>
@@ -126,6 +131,9 @@ class NodeState extends BaseState {
126
131
  launcherInfo: launcherInfo || undefined,
127
132
  didRegistry,
128
133
  didDomain,
134
+ enablePassportIssuance,
135
+ trustedPassports,
136
+ customBlockletNumber: 0,
129
137
  },
130
138
  async (e, data) => {
131
139
  if (e) {
@@ -262,6 +270,13 @@ class NodeState extends BaseState {
262
270
  getBlockletRegistry() {
263
271
  return this.read().then((info) => info.blockletRegistryList.find((item) => item.selected).url);
264
272
  }
273
+
274
+ async increaseCustomBlockletNumber() {
275
+ const { _id, customBlockletNumber = 0 } = await this.read();
276
+ const num = customBlockletNumber + 1;
277
+ await this.update(_id, { $set: { customBlockletNumber: num } });
278
+ return num;
279
+ }
265
280
  }
266
281
 
267
282
  module.exports = NodeState;
@@ -6,9 +6,11 @@ const os = require('os');
6
6
  const tar = require('tar');
7
7
  const get = require('lodash/get');
8
8
  const intersection = require('lodash/intersection');
9
+ const intersectionBy = require('lodash/intersectionBy');
9
10
  const streamToPromise = require('stream-to-promise');
10
11
  const { Throttle } = require('stream-throttle');
11
12
  const ssri = require('ssri');
13
+ const diff = require('deep-diff');
12
14
 
13
15
  const { toHex } = require('@ocap/util');
14
16
  const logger = require('@abtnode/logger')('@abtnode/core:util:blocklet');
@@ -17,7 +19,10 @@ const sleep = require('@abtnode/util/lib/sleep');
17
19
  const ensureEndpointHealthy = require('@abtnode/util/lib/ensure-endpoint-healthy');
18
20
  const CustomError = require('@abtnode/util/lib/custom-error');
19
21
  const getFolderSize = require('@abtnode/util/lib/get-folder-size');
22
+ const normalizePathPrefix = require('@abtnode/util/lib/normalize-path-prefix');
23
+ const hashFiles = require('@abtnode/util/lib/hash-files');
20
24
  const { BLOCKLET_MAX_MEM_LIMIT_IN_MB } = require('@abtnode/constant');
25
+
21
26
  const {
22
27
  BlockletStatus,
23
28
  BlockletSource,
@@ -29,6 +34,7 @@ const {
29
34
  BLOCKLET_DEFAULT_PORT_NAME,
30
35
  BLOCKLET_INTERFACE_TYPE_WEB,
31
36
  BLOCKLET_CONFIGURABLE_KEY,
37
+ BLOCKLET_DYNAMIC_PATH_PREFIX,
32
38
  fromBlockletStatus,
33
39
  } = require('@blocklet/meta/lib/constants');
34
40
  const verifyMultiSig = require('@blocklet/meta/lib/verify-multi-sig');
@@ -41,6 +47,7 @@ const { forEachBlocklet } = require('@blocklet/meta/lib/util');
41
47
  const { validate: validateEngine, get: getEngine } = require('../blocklet/manager/engine');
42
48
 
43
49
  const isRequirementsSatisfied = require('./requirement');
50
+ const { getDidDomainForBlocklet } = require('./get-domain-for-blocklet');
44
51
  const { getServices } = require('./service');
45
52
  const {
46
53
  isBeforeInstalled,
@@ -60,8 +67,6 @@ const getBlockletEngineNameByPlatform = (blockletMeta) => getBlockletEngine(bloc
60
67
 
61
68
  const noop = () => {};
62
69
 
63
- const asyncFs = fs.promises;
64
-
65
70
  const statusMap = {
66
71
  online: BlockletStatus.running,
67
72
  launching: BlockletStatus.starting,
@@ -79,11 +84,14 @@ const PRIVATE_NODE_ENVS = [
79
84
  'ABT_NODE_TOKEN_SECRET',
80
85
  'ABT_NODE_SK',
81
86
  'ABT_NODE_SESSION_SECRET',
82
- 'ABT_NODE_NAME',
83
- 'ABT_NODE_DESCRIPTION',
84
87
  'ABT_NODE_BASE_URL',
85
88
  'ABT_NODE_LOG_LEVEL',
86
89
  'ABT_NODE_LOG_DIR',
90
+ // in /core/cli/bin/blocklet.js
91
+ 'CLI_MODE',
92
+ 'ABT_NODE_HOME',
93
+ 'PM2_HOME',
94
+ 'ABT_NODE_CONFIG_FILE',
87
95
  ];
88
96
 
89
97
  /**
@@ -114,19 +122,9 @@ const getBlockletDirs = (blocklet, { rootBlocklet, dataDirs, ensure = false } =
114
122
  logsDir = path.join(dataDirs.logs, rootBlocklet.meta.name);
115
123
  }
116
124
 
117
- if (ensure) {
118
- try {
119
- fs.mkdirSync(dataDir, { recursive: true });
120
- fs.mkdirSync(logsDir, { recursive: true });
121
- fs.mkdirSync(cacheDir, { recursive: true });
122
- } catch (err) {
123
- logger.error('make blocklet dir failed', { error: err });
124
- }
125
- }
126
-
127
125
  // get app dirs
128
126
 
129
- const { version, main, group } = blocklet.meta;
127
+ const { main, group } = blocklet.meta;
130
128
 
131
129
  const startFromDevEntry =
132
130
  blocklet.mode === BLOCKLET_MODES.DEVELOPMENT && blocklet.meta.scripts && blocklet.meta.scripts.dev;
@@ -141,13 +139,24 @@ const getBlockletDirs = (blocklet, { rootBlocklet, dataDirs, ensure = false } =
141
139
  if (blocklet.source === BlockletSource.local) {
142
140
  appDir = blocklet.deployedFrom;
143
141
  } else {
144
- appDir = path.join(dataDirs.blocklets, name, version);
142
+ appDir = getBundleDir(dataDirs.blocklets, blocklet.meta);
145
143
  }
146
144
 
147
145
  if (!appDir) {
148
146
  throw new Error('Can not determine blocklet directory, maybe invalid deployment from local blocklets');
149
147
  }
150
148
 
149
+ if (ensure) {
150
+ try {
151
+ fs.mkdirSync(dataDir, { recursive: true });
152
+ fs.mkdirSync(logsDir, { recursive: true });
153
+ fs.mkdirSync(cacheDir, { recursive: true });
154
+ fs.mkdirSync(appDir, { recursive: true }); // prevent getDiskInfo failed from custom blocklet
155
+ } catch (err) {
156
+ logger.error('make blocklet dir failed', { error: err });
157
+ }
158
+ }
159
+
151
160
  mainDir = appDir;
152
161
 
153
162
  if (!startFromDevEntry && !isBeforeInstalled(rootBlocklet.status)) {
@@ -242,12 +251,16 @@ const getRootSystemEnvironments = (blocklet, nodeInfo) => {
242
251
  const appName = title || name || result.name;
243
252
  const appDescription = description || result.description;
244
253
 
254
+ // FIXME: we should use https here when possible, eg, when did-gateway is available
255
+ const appUrl = `http://${getDidDomainForBlocklet({ name, daemonDid: nodeInfo.did, didDomain: nodeInfo.didDomain })}`;
256
+
245
257
  return {
246
258
  BLOCKLET_DID: did,
247
259
  BLOCKLET_APP_SK: appSk,
248
260
  BLOCKLET_APP_ID: appId,
249
261
  BLOCKLET_APP_NAME: appName,
250
262
  BLOCKLET_APP_DESCRIPTION: appDescription,
263
+ BLOCKLET_APP_URL: appUrl,
251
264
  };
252
265
  };
253
266
 
@@ -427,11 +440,13 @@ const startBlockletProcess = async (blocklet, { preStart = noop, nodeEnvironment
427
440
  const options = {
428
441
  namespace: 'blocklets',
429
442
  name: appId,
430
- max_memory_restart: `${maxMemoryRestart}M`,
443
+ cwd: appCwd,
431
444
  time: true,
432
445
  output: path.join(logsDir, 'output.log'),
433
446
  error: path.join(logsDir, 'error.log'),
434
- cwd: appCwd,
447
+ wait_ready: process.env.NODE_ENV !== 'test',
448
+ listen_timeout: 5000,
449
+ max_memory_restart: `${maxMemoryRestart}M`,
435
450
  max_restarts: b.mode === BLOCKLET_MODES.DEVELOPMENT ? 0 : 3,
436
451
  env: {
437
452
  ...env,
@@ -459,17 +474,17 @@ const startBlockletProcess = async (blocklet, { preStart = noop, nodeEnvironment
459
474
  const engine = getEngine(blockletEngineInfo.interpreter);
460
475
  options.interpreter = engine.interpreter === 'node' ? '' : engine.interpreter;
461
476
  options.interpreterArgs = engine.args || '';
462
-
463
477
  options.script = blockletEngineInfo.script || appMain;
464
-
465
- logger.debug('start.blocklet.engine.info', { blockletEngineInfo });
466
- logger.debug('start.blocklet.max_memory_restart', { maxMemoryRestart });
467
478
  }
468
479
 
469
480
  await pm2.startAsync(options);
470
- logger.info('blocklet started', {
471
- appId,
472
- });
481
+
482
+ const status = await getProcessState(appId);
483
+ if (status === BlockletStatus.error) {
484
+ throw new Error(`${appId} is not running within 5 seconds`);
485
+ }
486
+
487
+ logger.info('blocklet started', { appId, status });
473
488
  },
474
489
  { parallel: true }
475
490
  );
@@ -532,6 +547,10 @@ const getBlockletStatusFromProcess = async (blocklet) => {
532
547
 
533
548
  const list = await Promise.all(tasks);
534
549
 
550
+ if (!list.length) {
551
+ return blocklet.status;
552
+ }
553
+
535
554
  return getRootBlockletStatus(list);
536
555
  };
537
556
 
@@ -610,26 +629,76 @@ const reloadProcess = (appId) =>
610
629
  });
611
630
  });
612
631
 
613
- const getChildrenMeta = async (meta) => {
614
- const children = [];
615
- if (meta.children && meta.children.length) {
616
- for (const child of meta.children) {
617
- const m = await getBlockletMetaFromUrl(child.resolved);
618
- if (m.name !== child.name) {
619
- logger.error('Resolved child blocklet name does not match in the configuration', {
620
- expected: child.name,
621
- resolved: m.name,
622
- });
623
- throw new Error(
624
- `Child blocklet name does not match in the configuration. expected: ${child.name}, resolved: ${m.name}`
625
- );
626
- }
627
- validateBlockletMeta(m, { ensureDist: true });
628
- children.push(m);
632
+ const findWebInterface = (blocklet) => {
633
+ const meta = blocklet.meta || blocklet || {};
634
+ const { interfaces = [] } = meta;
635
+
636
+ if (!Array.isArray(interfaces)) {
637
+ return null;
638
+ }
639
+
640
+ return interfaces.find((x) => x.type === BLOCKLET_INTERFACE_TYPE_WEB);
641
+ };
642
+
643
+ /**
644
+ * this function has side effect on children
645
+ */
646
+ const parseChildren = async (src, { children, dynamic } = {}) => {
647
+ const configs = Array.isArray(src) ? src : src.children || [];
648
+
649
+ if (children) {
650
+ children.forEach((x) => {
651
+ x.dynamic = !!dynamic;
652
+ });
653
+ mergeMeta(configs, children);
654
+ return children;
655
+ }
656
+
657
+ if (!configs || !configs.length) {
658
+ return [];
659
+ }
660
+
661
+ const results = [];
662
+
663
+ for (const config of configs) {
664
+ const mountPoint = config.mountPoint || config.mountPoints[0].root.prefix;
665
+ if (!mountPoint) {
666
+ throw new Error(`MountPoint does not found in child ${config.name}`);
629
667
  }
668
+
669
+ const m = await getBlockletMetaFromUrl(config.resolved);
670
+ if (m.name !== config.name) {
671
+ logger.error('Resolved child blocklet name does not match in the configuration', {
672
+ expected: config.name,
673
+ resolved: m.name,
674
+ });
675
+ throw new Error(
676
+ `Child blocklet name does not match in the configuration. expected: ${config.name}, resolved: ${m.name}`
677
+ );
678
+ }
679
+ validateBlockletMeta(m, { ensureDist: true });
680
+
681
+ const webInterface = findWebInterface(m);
682
+ if (!webInterface) {
683
+ throw new Error(`Web interface does not found in child ${config.name}`);
684
+ }
685
+
686
+ const rule = webInterface.prefix;
687
+ if (rule !== BLOCKLET_DYNAMIC_PATH_PREFIX && normalizePathPrefix(rule) !== normalizePathPrefix(mountPoint)) {
688
+ throw new Error(`Prefix does not match in child ${config.name}. expected: ${rule}, resolved: ${mountPoint}`);
689
+ }
690
+
691
+ results.push({
692
+ mountPoint,
693
+ meta: m,
694
+ dynamic: !!dynamic,
695
+ sourceUrl: config.resolved,
696
+ });
630
697
  }
631
698
 
632
- return children;
699
+ mergeMeta(configs, results);
700
+
701
+ return results;
633
702
  };
634
703
 
635
704
  const validateBlocklet = (blocklet) =>
@@ -727,7 +796,7 @@ const verifyIntegrity = async ({ file, integrity: expected }) => {
727
796
  return true;
728
797
  };
729
798
 
730
- const pruneBlockletBundle = async (blocklets, installDir) => {
799
+ const pruneBlockletBundle = async ({ blocklets, installDir, blockletSettings }) => {
731
800
  for (const blocklet of blocklets) {
732
801
  if (
733
802
  [
@@ -745,53 +814,104 @@ const pruneBlockletBundle = async (blocklets, installDir) => {
745
814
  }
746
815
  }
747
816
 
748
- // blockletMap: { <name/version>: true }
749
- const blockletMap = blocklets.reduce((map, b) => {
750
- map[`${b.meta.name}/${b.meta.version}`] = true;
751
- for (const child of b.children || []) {
752
- map[`${child.meta.name}/${child.meta.version}`] = true;
817
+ // blockletMap: { <[scope/]name/version>: true }
818
+ const blockletMap = {};
819
+ for (const blocklet of blocklets) {
820
+ blockletMap[`${blocklet.meta.name}/${blocklet.meta.version}`] = true;
821
+ for (const child of blocklet.children || []) {
822
+ blockletMap[`${child.meta.name}/${child.meta.version}`] = true;
753
823
  }
754
- return map;
755
- }, {});
824
+ }
825
+ for (const setting of blockletSettings) {
826
+ for (const child of setting.children || []) {
827
+ blockletMap[`${child.meta.name}/${child.meta.version}`] = true;
828
+ }
829
+ }
756
830
 
757
- // appDirs: [{ key: <name/version>, dir: appDir }]
831
+ // appDirs: [{ key: <[scope/]name/version>, dir: appDir }]
758
832
  const appDirs = [];
759
833
 
760
834
  // fill appDirs
761
835
  try {
762
- const fillAppDirs = async (dir) => {
763
- if (fs.existsSync(path.join(dir, 'blocklet.yml'))) {
836
+ // @return root/scope/bundle/version
837
+ const getNextLevel = (level, name) => {
838
+ if (level === 'root') {
839
+ if (name.startsWith('@')) {
840
+ return 'scope';
841
+ }
842
+ return 'bundle';
843
+ }
844
+ if (level === 'scope') {
845
+ return 'bundle';
846
+ }
847
+ if (level === 'bundle') {
848
+ return 'version';
849
+ }
850
+ throw new Error(`Invalid level ${level}`);
851
+ };
852
+
853
+ const fillAppDirs = async (dir, level = 'root') => {
854
+ if (level === 'version') {
855
+ if (!fs.existsSync(path.join(dir, 'blocklet.yml'))) {
856
+ logger.error('blocklet.yml does not exist in blocklet bundle dir', { dir });
857
+ return;
858
+ }
859
+
764
860
  appDirs.push({
765
861
  key: path.relative(installDir, dir),
766
862
  dir,
767
863
  });
864
+
768
865
  return;
769
866
  }
867
+
770
868
  const nextDirs = [];
771
- for (const x of await asyncFs.readdir(dir)) {
772
- const nextDir = path.join(dir, x);
773
- // if blocklet.yml does not exist in dir but non-folder file exists in dir, stop finding
774
- if (!fs.lstatSync(nextDir).isDirectory()) {
775
- logger.error('blocklet.yml does not exist in blocklet bundle dir', { dir });
776
- return;
869
+ for (const x of await fs.promises.readdir(dir)) {
870
+ if (!fs.lstatSync(path.join(dir, x)).isDirectory()) {
871
+ logger.error('pruneBlockletBundle: invalid file in bundle storage', { dir, file: x });
872
+ // eslint-disable-next-line no-continue
873
+ continue;
777
874
  }
778
- nextDirs.push(nextDir);
875
+ nextDirs.push(x);
779
876
  }
780
877
 
781
878
  for (const x of nextDirs) {
782
- await fillAppDirs(x);
879
+ await fillAppDirs(path.join(dir, x), getNextLevel(level, x));
783
880
  }
784
881
  };
785
- await fillAppDirs(installDir);
882
+ await fillAppDirs(installDir, 'root');
786
883
  } catch (error) {
787
884
  logger.error('fill app dirs failed', { error });
788
885
  }
789
886
 
887
+ const ensureBundleDirRemoved = async (dir) => {
888
+ const relativeDir = path.relative(installDir, dir);
889
+ const arr = relativeDir.split('/').filter(Boolean);
890
+ const { length } = arr;
891
+ const bundleName = arr[length - 2];
892
+ const scopeName = length > 2 ? arr[length - 3] : '';
893
+ const bundleDir = path.join(installDir, scopeName, bundleName);
894
+ const isEmpty = (await fs.promises.readdir(bundleDir)).length === 0;
895
+ if (isEmpty) {
896
+ logger.info('Remove bundle folder', { bundleDir });
897
+ await fs.remove(bundleDir);
898
+ }
899
+ if (scopeName) {
900
+ const scopeDir = path.join(installDir, scopeName);
901
+ const isScopeEmpty = (await fs.promises.readdir(scopeDir)).length === 0;
902
+ if (isScopeEmpty) {
903
+ logger.info('Remove scope folder', { scopeDir });
904
+ await fs.remove(scopeDir);
905
+ }
906
+ }
907
+ };
908
+
790
909
  // remove trash
791
910
  for (const app of appDirs) {
792
911
  if (!blockletMap[app.key]) {
793
- logger.info('Remove blocklet bundle', { dir: app.dir });
912
+ logger.info('Remove app folder', { dir: app.dir });
794
913
  await fs.remove(app.dir);
914
+ await ensureBundleDirRemoved(app.dir);
795
915
  }
796
916
  }
797
917
 
@@ -829,15 +949,25 @@ const getRuntimeInfo = async (appId) => {
829
949
  };
830
950
  };
831
951
 
832
- const mergeMeta = (meta, childrenMeta = []) => {
952
+ /**
953
+ * merge services
954
+ * from meta.children[].mountPoints[].services
955
+ * to childrenMeta[].interfaces[].services
956
+ *
957
+ * @param {array<child>|object{children:array}} source e.g. [<config>] or { children: [<config>] }
958
+ * @param {array<meta|{meta}>} childrenMeta e.g. [<meta>] or [{ meta: <meta> }]
959
+ */
960
+
961
+ const mergeMeta = (source, childrenMeta = []) => {
833
962
  // configMap
834
963
  const configMap = {};
835
- (meta.children || []).forEach((x) => {
964
+ (Array.isArray(source) ? source : source.children || []).forEach((x) => {
836
965
  configMap[x.name] = x;
837
966
  });
838
967
 
839
968
  // merge service from config to child meta
840
- childrenMeta.forEach((childMeta) => {
969
+ childrenMeta.forEach((child) => {
970
+ const childMeta = child.meta || child;
841
971
  const config = configMap[childMeta.name];
842
972
  if (!config) {
843
973
  return;
@@ -897,10 +1027,84 @@ const getUpdateMetaList = (oldMetas = [], newMetas = []) => {
897
1027
  return newMetas.filter(({ version, did }) => did && version !== oldMap[did]);
898
1028
  };
899
1029
 
1030
+ const getSourceFromInstallParams = (params) => {
1031
+ if (params.url) {
1032
+ return BlockletSource.url;
1033
+ }
1034
+
1035
+ if (params.file) {
1036
+ return BlockletSource.upload;
1037
+ }
1038
+
1039
+ if (params.did) {
1040
+ return BlockletSource.registry;
1041
+ }
1042
+
1043
+ if (params.title && params.description) {
1044
+ return BlockletSource.custom;
1045
+ }
1046
+
1047
+ throw new Error('Can only install blocklet from store/url/upload/custom');
1048
+ };
1049
+
1050
+ const checkDuplicateComponents = (dynamicComponents, staticComponents) => {
1051
+ const duplicates = intersectionBy(dynamicComponents, staticComponents, 'meta.did');
1052
+ if (duplicates.length) {
1053
+ throw new Error(
1054
+ `Cannot add duplicate component${duplicates.length > 1 ? 's' : ''}: ${duplicates
1055
+ .map((x) => x.meta.title || x.meta.name)
1056
+ .join(', ')}`
1057
+ );
1058
+ }
1059
+ };
1060
+
1061
+ const getDiffFiles = async (inputFiles, sourceDir) => {
1062
+ if (!fs.existsSync(sourceDir)) {
1063
+ throw new Error(`${sourceDir} does not exist`);
1064
+ }
1065
+
1066
+ const files = inputFiles.reduce((obj, item) => {
1067
+ obj[item.file] = item.hash;
1068
+ return obj;
1069
+ }, {});
1070
+
1071
+ const { files: sourceFiles } = await hashFiles(sourceDir, {
1072
+ filter: (x) => x.indexOf('node_modules') === -1,
1073
+ concurrentHash: 1,
1074
+ });
1075
+
1076
+ const addSet = [];
1077
+ const changeSet = [];
1078
+ const deleteSet = [];
1079
+
1080
+ const diffFiles = diff(sourceFiles, files);
1081
+ if (diffFiles) {
1082
+ diffFiles.forEach((item) => {
1083
+ if (item.kind === 'D') {
1084
+ deleteSet.push(item.path[0]);
1085
+ }
1086
+ if (item.kind === 'E') {
1087
+ changeSet.push(item.path[0]);
1088
+ }
1089
+ if (item.kind === 'N') {
1090
+ addSet.push(item.path[0]);
1091
+ }
1092
+ });
1093
+ }
1094
+
1095
+ return {
1096
+ addSet,
1097
+ changeSet,
1098
+ deleteSet,
1099
+ };
1100
+ };
1101
+
1102
+ const getBundleDir = (installDir, bundle) => path.join(installDir, bundle.name, bundle.version);
1103
+
900
1104
  module.exports = {
901
1105
  forEachBlocklet,
902
1106
  getBlockletMetaFromUrl,
903
- getChildrenMeta,
1107
+ parseChildren,
904
1108
  getBlockletDirs,
905
1109
  getRootSystemEnvironments,
906
1110
  getSystemEnvironments,
@@ -926,4 +1130,9 @@ module.exports = {
926
1130
  mergeMeta,
927
1131
  fixAndVerifyBlockletMeta,
928
1132
  getUpdateMetaList,
1133
+ getSourceFromInstallParams,
1134
+ findWebInterface,
1135
+ checkDuplicateComponents,
1136
+ getDiffFiles,
1137
+ getBundleDir,
929
1138
  };
@@ -35,7 +35,7 @@ const defaultNodeConfigs = {
35
35
  },
36
36
  ],
37
37
  },
38
- registerUrl: { getDefaultValue: () => NODE_REGISTER_URL }, // removed in 1.5.1
38
+ registerUrl: { getDefaultValue: () => NODE_REGISTER_URL },
39
39
  webWalletUrl: { getDefaultValue: () => WEB_WALLET_URL },
40
40
  };
41
41