@cyberismo/data-handler 0.0.6 → 0.0.8

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 (175) hide show
  1. package/README.md +1 -0
  2. package/dist/card-metadata-updater.d.ts +1 -0
  3. package/dist/card-metadata-updater.js +7 -2
  4. package/dist/card-metadata-updater.js.map +1 -1
  5. package/dist/command-handler.d.ts +6 -1
  6. package/dist/command-handler.js +16 -15
  7. package/dist/command-handler.js.map +1 -1
  8. package/dist/command-manager.d.ts +15 -4
  9. package/dist/command-manager.js +41 -9
  10. package/dist/command-manager.js.map +1 -1
  11. package/dist/commands/calculate.d.ts +4 -10
  12. package/dist/commands/calculate.js +67 -78
  13. package/dist/commands/calculate.js.map +1 -1
  14. package/dist/commands/create.d.ts +1 -1
  15. package/dist/commands/create.js +15 -10
  16. package/dist/commands/create.js.map +1 -1
  17. package/dist/commands/export.js +3 -3
  18. package/dist/commands/export.js.map +1 -1
  19. package/dist/commands/import.d.ts +3 -8
  20. package/dist/commands/import.js +17 -15
  21. package/dist/commands/import.js.map +1 -1
  22. package/dist/commands/index.d.ts +1 -2
  23. package/dist/commands/index.js +1 -2
  24. package/dist/commands/index.js.map +1 -1
  25. package/dist/commands/remove.d.ts +1 -1
  26. package/dist/commands/remove.js +4 -10
  27. package/dist/commands/remove.js.map +1 -1
  28. package/dist/commands/show.d.ts +6 -3
  29. package/dist/commands/show.js +8 -5
  30. package/dist/commands/show.js.map +1 -1
  31. package/dist/commands/validate.d.ts +0 -8
  32. package/dist/commands/validate.js +6 -36
  33. package/dist/commands/validate.js.map +1 -1
  34. package/dist/containers/project/project-content-watcher.d.ts +28 -0
  35. package/dist/containers/project/project-content-watcher.js +54 -0
  36. package/dist/containers/project/project-content-watcher.js.map +1 -0
  37. package/dist/containers/project/project-paths.js +1 -1
  38. package/dist/containers/project/project-paths.js.map +1 -1
  39. package/dist/containers/project/resource-collector.d.ts +2 -1
  40. package/dist/containers/project/resource-collector.js +33 -23
  41. package/dist/containers/project/resource-collector.js.map +1 -1
  42. package/dist/containers/project.d.ts +9 -7
  43. package/dist/containers/project.js +58 -18
  44. package/dist/containers/project.js.map +1 -1
  45. package/dist/containers/template.d.ts +1 -0
  46. package/dist/containers/template.js +7 -2
  47. package/dist/containers/template.js.map +1 -1
  48. package/dist/exceptions/index.d.ts +20 -0
  49. package/dist/exceptions/index.js +16 -0
  50. package/dist/exceptions/index.js.map +1 -1
  51. package/dist/index.d.ts +2 -1
  52. package/dist/index.js.map +1 -1
  53. package/dist/interfaces/macros.d.ts +5 -1
  54. package/dist/interfaces/project-interfaces.d.ts +5 -0
  55. package/dist/interfaces/project-interfaces.js.map +1 -1
  56. package/dist/interfaces/resource-interfaces.d.ts +14 -22
  57. package/dist/interfaces/resource-interfaces.js +10 -9
  58. package/dist/interfaces/resource-interfaces.js.map +1 -1
  59. package/dist/macros/base-macro.d.ts +2 -0
  60. package/dist/macros/base-macro.js +66 -19
  61. package/dist/macros/base-macro.js.map +1 -1
  62. package/dist/macros/graph/index.d.ts +0 -1
  63. package/dist/macros/graph/index.js +16 -12
  64. package/dist/macros/graph/index.js.map +1 -1
  65. package/dist/macros/index.d.ts +30 -3
  66. package/dist/macros/index.js +36 -6
  67. package/dist/macros/index.js.map +1 -1
  68. package/dist/macros/report/index.d.ts +13 -10
  69. package/dist/macros/report/index.js +35 -38
  70. package/dist/macros/report/index.js.map +1 -1
  71. package/dist/module-manager.d.ts +32 -11
  72. package/dist/module-manager.js +301 -147
  73. package/dist/module-manager.js.map +1 -1
  74. package/dist/project-settings.js +8 -8
  75. package/dist/project-settings.js.map +1 -1
  76. package/dist/resources/array-handler.js +6 -1
  77. package/dist/resources/array-handler.js.map +1 -1
  78. package/dist/resources/card-type-resource.d.ts +13 -9
  79. package/dist/resources/card-type-resource.js +47 -23
  80. package/dist/resources/card-type-resource.js.map +1 -1
  81. package/dist/resources/create-defaults.d.ts +10 -9
  82. package/dist/resources/create-defaults.js +15 -12
  83. package/dist/resources/create-defaults.js.map +1 -1
  84. package/dist/resources/field-type-resource.d.ts +0 -1
  85. package/dist/resources/field-type-resource.js +2 -10
  86. package/dist/resources/field-type-resource.js.map +1 -1
  87. package/dist/resources/file-resource.d.ts +7 -7
  88. package/dist/resources/file-resource.js +32 -7
  89. package/dist/resources/file-resource.js.map +1 -1
  90. package/dist/resources/folder-resource.d.ts +10 -9
  91. package/dist/resources/folder-resource.js +10 -9
  92. package/dist/resources/folder-resource.js.map +1 -1
  93. package/dist/resources/report-resource.d.ts +5 -6
  94. package/dist/resources/report-resource.js +16 -7
  95. package/dist/resources/report-resource.js.map +1 -1
  96. package/dist/resources/template-resource.d.ts +5 -6
  97. package/dist/resources/template-resource.js +7 -6
  98. package/dist/resources/template-resource.js.map +1 -1
  99. package/dist/resources/workflow-resource.d.ts +15 -8
  100. package/dist/resources/workflow-resource.js +124 -8
  101. package/dist/resources/workflow-resource.js.map +1 -1
  102. package/dist/types/queries.d.ts +11 -10
  103. package/dist/types/queries.js +10 -9
  104. package/dist/types/queries.js.map +1 -1
  105. package/dist/utils/clingo-fact-builder.d.ts +1 -0
  106. package/dist/utils/clingo-fact-builder.js +8 -3
  107. package/dist/utils/clingo-fact-builder.js.map +1 -1
  108. package/dist/utils/clingo-facts.js +15 -11
  109. package/dist/utils/clingo-facts.js.map +1 -1
  110. package/dist/utils/constants.d.ts +18 -12
  111. package/dist/utils/constants.js +18 -11
  112. package/dist/utils/constants.js.map +1 -1
  113. package/dist/utils/log-utils.d.ts +15 -2
  114. package/dist/utils/log-utils.js +20 -37
  115. package/dist/utils/log-utils.js.map +1 -1
  116. package/dist/utils/report.d.ts +27 -0
  117. package/dist/utils/report.js +60 -0
  118. package/dist/utils/report.js.map +1 -0
  119. package/dist/utils/resource-utils.js +3 -0
  120. package/dist/utils/resource-utils.js.map +1 -1
  121. package/dist/utils/sanitize-svg.d.ts +3 -4
  122. package/dist/utils/sanitize-svg.js +4 -7
  123. package/dist/utils/sanitize-svg.js.map +1 -1
  124. package/dist/utils/validate.js +2 -1
  125. package/dist/utils/validate.js.map +1 -1
  126. package/package.json +9 -11
  127. package/src/card-metadata-updater.ts +7 -2
  128. package/src/command-handler.ts +23 -13
  129. package/src/command-manager.ts +54 -13
  130. package/src/commands/calculate.ts +90 -106
  131. package/src/commands/create.ts +18 -10
  132. package/src/commands/export.ts +3 -2
  133. package/src/commands/import.ts +30 -17
  134. package/src/commands/index.ts +0 -2
  135. package/src/commands/remove.ts +7 -12
  136. package/src/commands/show.ts +13 -5
  137. package/src/commands/validate.ts +14 -44
  138. package/src/containers/project/project-content-watcher.ts +65 -0
  139. package/src/containers/project/project-paths.ts +1 -1
  140. package/src/containers/project/resource-collector.ts +33 -14
  141. package/src/containers/project.ts +96 -19
  142. package/src/containers/template.ts +7 -2
  143. package/src/exceptions/index.ts +36 -0
  144. package/src/index.ts +2 -0
  145. package/src/interfaces/macros.ts +5 -1
  146. package/src/interfaces/project-interfaces.ts +8 -0
  147. package/src/interfaces/resource-interfaces.ts +15 -22
  148. package/src/macros/base-macro.ts +89 -25
  149. package/src/macros/graph/index.ts +22 -12
  150. package/src/macros/index.ts +61 -8
  151. package/src/macros/report/index.ts +42 -43
  152. package/src/module-manager.ts +383 -177
  153. package/src/project-settings.ts +9 -11
  154. package/src/resources/array-handler.ts +7 -2
  155. package/src/resources/card-type-resource.ts +61 -46
  156. package/src/resources/create-defaults.ts +15 -12
  157. package/src/resources/field-type-resource.ts +2 -17
  158. package/src/resources/file-resource.ts +46 -8
  159. package/src/resources/folder-resource.ts +11 -10
  160. package/src/resources/report-resource.ts +19 -7
  161. package/src/resources/template-resource.ts +7 -6
  162. package/src/resources/workflow-resource.ts +155 -8
  163. package/src/types/queries.ts +11 -10
  164. package/src/utils/clingo-fact-builder.ts +8 -3
  165. package/src/utils/clingo-facts.ts +18 -12
  166. package/src/utils/constants.ts +20 -12
  167. package/src/utils/log-utils.ts +24 -45
  168. package/src/utils/report.ts +86 -0
  169. package/src/utils/resource-utils.ts +4 -0
  170. package/src/utils/sanitize-svg.ts +4 -9
  171. package/src/utils/validate.ts +3 -2
  172. package/dist/commands/export-site.d.ts +0 -45
  173. package/dist/commands/export-site.js +0 -301
  174. package/dist/commands/export-site.js.map +0 -1
  175. package/src/commands/export-site.ts +0 -356
@@ -11,16 +11,14 @@
11
11
  License along with this program. If not, see <https://www.gnu.org/licenses/>.
12
12
  */
13
13
 
14
- import fs from 'node:fs';
15
14
  import { join } from 'node:path';
16
15
  import { mkdir, readdir, rm } from 'node:fs/promises';
17
16
 
18
- import git from 'isomorphic-git';
19
- import http from 'isomorphic-git/http/node/index.js';
17
+ import { simpleGit, type SimpleGit } from 'simple-git';
20
18
 
21
19
  import { copyDir, deleteDir, pathExists } from './utils/file-utils.js';
22
- import type { Import } from './commands/index.js';
23
20
  import type {
21
+ Credentials,
24
22
  ModuleSetting,
25
23
  ModuleSettingOptions,
26
24
  } from './interfaces/project-interfaces.js';
@@ -31,8 +29,15 @@ import { readJsonFile } from './utils/json.js';
31
29
  import { Validate } from './commands/index.js';
32
30
 
33
31
  const FILE_PROTOCOL = 'file:';
34
- // todo: add support for git's default branch.
35
- const MAIN_BRANCH = 'main';
32
+ const HTTPS_PROTOCOL = 'https:';
33
+
34
+ // timeout in milliseconds for git client (no stdout / stderr activity)
35
+ const DEFAULT_TIMEOUT = 10000;
36
+
37
+ // When dependencies are built to a map, use map that has
38
+ // key: module name,
39
+ // value: list of unique prefixes
40
+ type DependencyGraph = Map<string, Set<string>>;
36
41
 
37
42
  /**
38
43
  * Class that handles module updates and imports.
@@ -40,10 +45,9 @@ const MAIN_BRANCH = 'main';
40
45
  export class ModuleManager {
41
46
  private modules: ModuleSetting[] = [];
42
47
  private tempModulesDir: string = '';
43
- constructor(
44
- private project: Project,
45
- private importCmd: Import,
46
- ) {
48
+ private defaultBranchCache: Map<string, string> = new Map();
49
+
50
+ constructor(private project: Project) {
47
51
  this.tempModulesDir = join(this.project.paths.tempFolder, 'modules');
48
52
  }
49
53
 
@@ -56,87 +60,121 @@ export class ModuleManager {
56
60
  await this.project.collectModuleResources();
57
61
  }
58
62
 
59
- // Handles a branch of a repository.
60
- private async branch(module: ModuleSetting) {
61
- if (module.branch === MAIN_BRANCH || module.branch === '' || !module.branch)
62
- return;
63
+ // Creates a map of what dependencies each module depend from.
64
+ private async buildDependencyGraph(
65
+ dependencies: ModuleSetting[],
66
+ ): Promise<DependencyGraph> {
67
+ const dependencyNames = dependencies.map((item) => item.name);
68
+ const dependencyGraph = new Map<string, Set<string>>() as DependencyGraph;
69
+ for (const dependency of dependencyNames) {
70
+ dependencyGraph.set(
71
+ dependency,
72
+ await this.transientDependencies(dependency),
73
+ );
74
+ }
75
+ return dependencyGraph;
76
+ }
63
77
 
64
- await git.checkout({
65
- fs,
66
- dir: join(this.tempModulesDir, module.name),
67
- ref: module.branch,
78
+ // Returns 'true' if 'moduleName' can be removed.
79
+ private canBeRemoved(
80
+ dependencies: DependencyGraph,
81
+ moduleName: string,
82
+ ): boolean {
83
+ let unused = true;
84
+ dependencies.forEach((transientDependencies, key) => {
85
+ if (key !== moduleName && transientDependencies.has(moduleName)) {
86
+ unused = false;
87
+ }
68
88
  });
69
- console.error(
70
- `... Switched to '${module.branch}' branch for module '${module.name}'`,
71
- );
89
+ return unused;
72
90
  }
73
91
 
74
92
  // Handles cloning of a repository.
75
93
  private async clone(
76
94
  module: ModuleSetting,
77
95
  verbose: boolean = true,
96
+ credentials?: Credentials,
78
97
  ): Promise<string> {
79
98
  if (!module.name || module.name === '') {
80
99
  module.name = this.repositoryName(module.location);
81
100
  }
82
101
 
83
- let repoUrl: URL;
84
- try {
85
- repoUrl = new URL(module.location);
86
- } catch {
87
- throw new Error(`Invalid repository URL: ${module.location}`);
88
- }
89
-
90
- if (
91
- process.env.CYBERISMO_GIT_USER &&
92
- process.env.CYBERISMO_GIT_TOKEN &&
93
- module.private
94
- ) {
102
+ const destinationPath = join(this.tempModulesDir, module.name);
103
+
104
+ let remote = module.location;
105
+ if (module.private) {
106
+ if (
107
+ credentials &&
108
+ credentials?.username &&
109
+ credentials?.token &&
110
+ module.location.startsWith(HTTPS_PROTOCOL)
111
+ ) {
112
+ if (verbose) {
113
+ console.log(
114
+ `... Using HTTPS with credentials '${credentials?.username}' for cloning '${module.name}'`,
115
+ );
116
+ }
117
+ try {
118
+ const repoUrl = new URL(module.location);
119
+ const user = credentials?.username;
120
+ const pass = credentials?.token;
121
+ const host = repoUrl.host;
122
+ const path = repoUrl.pathname;
123
+ remote = `https://${user}:${pass}@${host}${path}`;
124
+ } catch {
125
+ throw new Error(`Invalid repository URL: ${module.location}`);
126
+ }
127
+ } else if (module.location.startsWith('git@')) {
128
+ if (verbose) {
129
+ console.log(`... Using SSH for cloning '${module.name}'`);
130
+ }
131
+ }
132
+ } else {
95
133
  if (verbose) {
96
134
  console.log(
97
- `... Using credentials '${process.env.CYBERISMO_GIT_USER}' for cloning '${module.name}'`,
135
+ `... Using HTTPS without credentials for cloning '${module.name}'`,
98
136
  );
99
137
  }
100
- repoUrl.username = process.env.CYBERISMO_GIT_USER;
101
- repoUrl.password = process.env.CYBERISMO_GIT_TOKEN;
102
138
  }
103
- await git.clone({
104
- fs,
105
- http,
106
- dir: join(this.tempModulesDir, module.name),
107
- url: repoUrl.toString(),
108
- depth: 1,
109
- onAuth: () => {
110
- // Turn credentials 'off' when they are not available
111
- if (
112
- !process.env.CYBERISMO_GIT_USER ||
113
- !process.env.CYBERISMO_GIT_TOKEN
114
- ) {
115
- return undefined;
116
- }
117
- // Turn credentials 'off' for public repos
118
- if (!module.private) {
119
- return undefined;
120
- }
121
- return {
122
- username: process.env.CYBERISMO_GIT_USER,
123
- password: process.env.CYBERISMO_GIT_TOKEN,
124
- };
125
- },
126
- });
127
139
 
128
- if (verbose) {
129
- console.log(`... Cloned '${module.name}' to a temporary folder`);
140
+ try {
141
+ await mkdir(this.tempModulesDir, { recursive: true });
142
+ const cloneOptions = await this.setCloneOptions(module);
143
+ await rm(destinationPath, { recursive: true, force: true });
144
+
145
+ const git: SimpleGit = simpleGit({
146
+ timeout: {
147
+ block: DEFAULT_TIMEOUT,
148
+ },
149
+ });
150
+
151
+ await git
152
+ // turn off git prompts
153
+ .env({ GIT_TERMINAL_PROMPT: 0, GCM_INTERACTIVE: 'never' })
154
+ .clone(remote, destinationPath, cloneOptions);
155
+
156
+ if (verbose) {
157
+ console.log(`... Cloned '${module.name}' to a temporary folder`);
158
+ }
159
+ } catch (error) {
160
+ if (error instanceof Error)
161
+ throw new Error(
162
+ `Failed to clone module '${module.name}': ${error.message}`,
163
+ );
130
164
  }
165
+
131
166
  return module.name;
132
167
  }
133
168
 
134
169
  // Collects all module prefixes from module hierarchy into 'this.modules'.
135
170
  // Note that collected result can contain duplicates.
136
- private async collectModulePrefixes(modules: ModuleSetting[]) {
171
+ private async collectModulePrefixes(
172
+ modules: ModuleSetting[],
173
+ credentials?: Credentials,
174
+ ) {
137
175
  if (modules) {
138
176
  for (const module of modules) {
139
- await this.doCollectModulePrefix(module);
177
+ await this.doCollectModulePrefix(module, credentials);
140
178
  }
141
179
  }
142
180
  }
@@ -151,62 +189,97 @@ export class ModuleManager {
151
189
  }
152
190
  }
153
191
 
192
+ // Gets the default branch for a repository from remote or cache
193
+ private async defaultBranch(module: ModuleSetting): Promise<string> {
194
+ if (this.defaultBranchCache.has(module.location)) {
195
+ return this.defaultBranchCache.get(module.location)!;
196
+ }
197
+ // Set the default branch if branch was not specified
198
+ if (!module.branch) {
199
+ const destinationPath = join(this.tempModulesDir, module.name);
200
+ // Only return path after cloning
201
+ if (pathExists(destinationPath)) {
202
+ const git: SimpleGit = simpleGit({
203
+ timeout: {
204
+ block: DEFAULT_TIMEOUT,
205
+ },
206
+ });
207
+ const options = ['--abbrev-ref', 'HEAD'];
208
+ const defaultBranch = await git.cwd(destinationPath).revparse(options);
209
+ this.defaultBranchCache.set(module.location, defaultBranch);
210
+ return defaultBranch;
211
+ }
212
+ }
213
+ // The actual default branch will be updated later (after cloning).
214
+ return '';
215
+ }
216
+
217
+ // Fetches direct dependencies of a module.
218
+ private async dependencies(moduleName: string): Promise<Set<string>> {
219
+ const allModules = await this.project.modules();
220
+ if (!allModules) return new Set();
221
+ const module = allModules.find((m) => m.name === moduleName);
222
+ if (!module) {
223
+ throw new Error(`Module '${moduleName}' not found`);
224
+ }
225
+ const modulePath = join(module.path, module.name, 'cardsConfig.json');
226
+ const moduleConfiguration = (await readJsonFile(
227
+ modulePath,
228
+ )) as ProjectConfiguration;
229
+ return moduleConfiguration.modules
230
+ ? new Set(moduleConfiguration.modules.map((m) => m.name))
231
+ : new Set();
232
+ }
233
+
154
234
  // Collects one module's dependency prefixes to 'this.modules'.
155
235
  // Note that there can be duplicate entries.
156
- private async doCollectModulePrefix(module: ModuleSetting) {
236
+ private async doCollectModulePrefix(
237
+ module: ModuleSetting,
238
+ credentials?: Credentials,
239
+ ) {
157
240
  let moduleRoot = '';
158
241
  if (this.isFileModule(module)) {
159
242
  const urlStart = FILE_PROTOCOL.length;
160
243
  // Remove 'file:' from location
161
244
  moduleRoot = module.location.substring(urlStart, module.location.length);
162
245
  } else {
163
- await this.clone(module, false);
246
+ await this.clone(module, false, credentials);
164
247
  moduleRoot = join(this.tempModulesDir, module.name);
165
248
  }
166
249
 
167
250
  this.modules.push(module);
168
251
 
169
252
  const configuration = await this.configuration(moduleRoot);
170
- await this.collectModulePrefixes(configuration.modules);
253
+ await this.collectModulePrefixes(configuration.modules, credentials);
171
254
  }
172
255
 
173
256
  // Updates one module that is read from local file system.
174
257
  private async handleFileModule(module: ModuleSetting) {
175
- this.removeProtocolFromLocation(module);
258
+ this.stripProtocolFromLocation(module);
176
259
  await this.remove(module);
177
- await this.importFromFolder(module);
260
+ await this.importFromFolder(module.location, module.name);
178
261
  }
179
262
 
180
263
  // Updates one module that is received from Git.
181
264
  private async handleGitModule(module: ModuleSetting) {
182
265
  await this.clone(module);
183
- await this.branch(module);
266
+ const tempLocation = join(this.tempModulesDir, module.name);
184
267
  await this.remove(module);
185
- await this.importFromTemp(module);
268
+ await this.importFromFolder(tempLocation, module.name);
186
269
  }
187
270
 
188
271
  // Updates one module.
189
272
  private async handleModule(module: ModuleSetting) {
190
- return this.isFileModule(module)
191
- ? this.handleFileModule(module)
192
- : this.handleGitModule(module);
273
+ return this.isGitModule(module)
274
+ ? this.handleGitModule(module)
275
+ : this.handleFileModule(module);
193
276
  }
194
277
 
195
- // Handles importing a module from module settings 'location'
196
- private async importFromFolder(module: ModuleSetting) {
197
- await this.importCmd.updateExistingModule(module.location);
278
+ // Imports from a given folder. Is used both for .temp/<module name> and file locations.
279
+ private async importFromFolder(path: string, name: string) {
280
+ await this.importFileModule(path);
198
281
  console.log(
199
- `... Imported module '${module.name}' to '${this.project.configuration.name}'`,
200
- );
201
- }
202
-
203
- // Handles importing a module from '.temp' folder
204
- private async importFromTemp(module: ModuleSetting) {
205
- await this.importCmd.updateExistingModule(
206
- join(this.tempModulesDir, module.name),
207
- );
208
- console.log(
209
- `... Imported module '${module.name}' to '${this.project.configuration.name}'`,
282
+ `... Imported module '${name}' to '${this.project.configuration.name}'`,
210
283
  );
211
284
  }
212
285
 
@@ -216,20 +289,61 @@ export class ModuleManager {
216
289
  return module.location.startsWith('file:');
217
290
  }
218
291
 
219
- // Prepares '.temp/modules' for cloning
220
- private async prepare() {
221
- await mkdir(this.tempModulesDir, { recursive: true });
222
- for (const file of await readdir(this.tempModulesDir)) {
223
- await rm(join(this.tempModulesDir, file), {
224
- force: true,
225
- recursive: true,
226
- });
292
+ // Returns true if module is imported from git.
293
+ private isGitModule(module: ModuleSetting): boolean {
294
+ if (!module.location) return false;
295
+ return (
296
+ module.location.startsWith('https:') || module.location.startsWith('git@')
297
+ );
298
+ }
299
+
300
+ // Collect modules that could be removed from .cards/modules when
301
+ // 'moduleName' is removed.
302
+ private async orphanedModules(
303
+ dependencies: DependencyGraph,
304
+ moduleName: string,
305
+ ): Promise<string[]> {
306
+ const projectModules = this.project.configuration.modules;
307
+ const removableTransientModules: string[] = [];
308
+ if (dependencies.has(moduleName)) {
309
+ const deps = dependencies.get(moduleName);
310
+ for (const dependency of deps!) {
311
+ const projectDependency = projectModules.some(
312
+ (item) => item.name === dependency,
313
+ );
314
+ if (projectDependency) continue;
315
+
316
+ let orphanModule = true;
317
+ dependencies.forEach((transientDependencies, key) => {
318
+ if (key === moduleName) return;
319
+ if (transientDependencies.has(dependency)) {
320
+ orphanModule = false;
321
+ }
322
+ });
323
+
324
+ if (orphanModule) {
325
+ removableTransientModules.push(dependency);
326
+ }
327
+ }
227
328
  }
329
+ return removableTransientModules;
228
330
  }
229
331
 
230
- // Returns whether to use git or file system for handling the module.
231
- private protocol(module: ModuleSetting) {
232
- return this.isFileModule(module) ? 'file' : 'git';
332
+ // Prepares '.temp/modules' for cloning
333
+ private async prepare() {
334
+ try {
335
+ await mkdir(this.tempModulesDir, { recursive: true });
336
+ const files = await readdir(this.tempModulesDir);
337
+ for (const file of files) {
338
+ const filePath = join(this.tempModulesDir, file);
339
+ await rm(filePath, {
340
+ force: true,
341
+ recursive: true,
342
+ });
343
+ }
344
+ } catch (error) {
345
+ throw new Error(`Failed to prepare temporary directory: ${error}`);
346
+ }
233
347
  }
234
348
 
235
349
  // Handles removing an imported module.
@@ -245,47 +359,32 @@ export class ModuleManager {
245
359
  }
246
360
  }
247
361
 
248
- // Remove module files.
249
- private async removeModuleFiles(moduleName: string) {
250
- const module = await this.project.module(moduleName);
251
- if (!module) {
252
- throw new Error(`Module '${moduleName}' not found`);
253
- }
254
- await deleteDir(module.path);
255
- }
256
-
257
- // Updates module's 'location' not to have 'protocol:' in the beginning (only for "file:" needed).
258
- private removeProtocolFromLocation(module: ModuleSetting) {
259
- const protocol = this.protocol(module);
260
- module.location = module.location.substring(
261
- protocol.length + 1,
262
- module.location.length,
263
- );
264
- }
265
-
266
362
  // Checks for duplicate ModuleSetting entries and throws an error if modules
267
363
  // with the same name have different branches or locations.
268
- // Treats undefined branch, empty string branch, and "main" branch as equivalent.
364
+ // Treats undefined branch, empty string branch, and default branch as equivalent.
269
365
  // Returns an array with duplicate entries removed
270
- private removeDuplicates(modules: ModuleSetting[]): ModuleSetting[] {
366
+ private async removeDuplicates(
367
+ modules: ModuleSetting[],
368
+ ): Promise<ModuleSetting[]> {
271
369
  const moduleMap = new Map<string, ModuleSetting>();
272
370
 
273
- // Assume that empty, or missing branch means 'main'
274
- const normalizeBranch = (branch: string | undefined): string => {
275
- if (!branch || branch === '' || branch === MAIN_BRANCH) {
276
- return MAIN_BRANCH;
277
- }
278
- return branch;
371
+ // Normalize branch names by checking against the default branch for each module
372
+
373
+ const normalizeBranch = async (module: ModuleSetting) => {
374
+ return module.branch ? module.branch! : await this.defaultBranch(module);
279
375
  };
280
376
 
281
377
  for (const module of modules) {
282
378
  const existingModule = moduleMap.get(module.name);
283
379
  if (existingModule) {
284
- if (existingModule.private !== module.private) {
380
+ if (
381
+ (existingModule.private && !module.private) ||
382
+ (!existingModule.private && module.private)
383
+ ) {
285
384
  throw new Error(
286
385
  `Module conflict: '${module.name}' has different access:\n` +
287
- ` - ${existingModule.private || 'undefined'}\n` +
288
- ` - ${module.private || 'undefined'}`,
386
+ ` - ${Boolean(existingModule.private)}\n` +
387
+ ` - ${Boolean(module.private)}`,
289
388
  );
290
389
  }
291
390
  if (existingModule.location !== module.location) {
@@ -295,8 +394,8 @@ export class ModuleManager {
295
394
  ` - ${module.location}`,
296
395
  );
297
396
  }
298
- const existingBranch = normalizeBranch(existingModule.branch);
299
- const newBranch = normalizeBranch(module.branch);
397
+ const existingBranch = await normalizeBranch(existingModule);
398
+ const newBranch = await normalizeBranch(module);
300
399
 
301
400
  if (existingBranch !== newBranch) {
302
401
  throw new Error(
@@ -312,6 +411,15 @@ export class ModuleManager {
312
411
  return Array.from(moduleMap.values());
313
412
  }
314
413
 
414
+ // Remove module files.
415
+ private async removeModuleFiles(moduleName: string) {
416
+ const module = await this.project.module(moduleName);
417
+ if (!module) {
418
+ throw new Error(`Module '${moduleName}' not found`);
419
+ }
420
+ await deleteDir(module.path);
421
+ }
422
+
315
423
  // Gets repository name from gitUrl
316
424
  private repositoryName(gitUrl: string): string {
317
425
  const last = gitUrl.lastIndexOf('/');
@@ -319,6 +427,94 @@ export class ModuleManager {
319
427
  return repoName;
320
428
  }
321
429
 
430
+ // Sets cloning options with support for default branch.
431
+ private async setCloneOptions(module: ModuleSetting): Promise<string[]> {
432
+ const cloneOptions = ['--depth', '1'];
433
+ const defaultBranch = await this.defaultBranch(module);
434
+ // Only specify branch if it's different from the default branch
435
+ if (
436
+ module.branch &&
437
+ module.branch !== '' &&
438
+ module.branch !== defaultBranch
439
+ ) {
440
+ cloneOptions.push('--branch', module.branch);
441
+ }
442
+ return cloneOptions;
443
+ }
444
+
445
+ // Updates module's 'location' not to have 'protocol:' in the beginning (only for "file:" needed).
446
+ private stripProtocolFromLocation(module: ModuleSetting) {
447
+ const protocol = this.isFileModule(module) ? 'file' : 'git';
448
+ module.location = module.location.substring(
449
+ protocol.length + 1,
450
+ module.location.length,
451
+ );
452
+ }
453
+
454
+ // Fetches all dependencies for a module.
455
+ private async transientDependencies(
456
+ moduleName: string,
457
+ ): Promise<Set<string>> {
458
+ const dependencies = await this.dependencies(moduleName);
459
+ let transientDependencies: Set<string> = new Set(dependencies);
460
+ for (const dependency of dependencies) {
461
+ const depTransients = await this.transientDependencies(dependency);
462
+ transientDependencies = new Set([
463
+ ...transientDependencies,
464
+ ...depTransients,
465
+ ]);
466
+ }
467
+
468
+ return transientDependencies;
469
+ }
470
+
471
+ // Updates modules in the project.
472
+ private async update(module?: ModuleSetting, credentials?: Credentials) {
473
+ // Prints dots every half second so that user knows that something is ongoing
474
+ function start() {
475
+ console.log('... Collecting unique modules. This takes a moment.');
476
+ return setInterval(() => process.stdout.write(`.`), 500);
477
+ }
478
+
479
+ // Stops the above, and shows results
480
+ function finished(interval: NodeJS.Timeout, modules: string[]) {
481
+ clearInterval(interval);
482
+ if (modules.length > 0) {
483
+ console.log(`\n... Found modules: ${modules.join(', ')}`);
484
+ }
485
+ }
486
+
487
+ await this.prepare();
488
+
489
+ let modules = module ? [module] : this.project.configuration.modules;
490
+ if (modules.length === 0) {
491
+ throw new Error(`No modules in the project!`);
492
+ }
493
+ modules = modules.filter((module) => this.isGitModule(module));
494
+
495
+ const dotInterval = start();
496
+
497
+ let uniqueModules: ModuleSetting[] = [];
498
+ try {
499
+ await this.collectModulePrefixes(modules, credentials);
500
+ uniqueModules = await this.removeDuplicates(this.modules);
501
+ } finally {
502
+ finished(
503
+ dotInterval,
504
+ uniqueModules.map((item) => item.name),
505
+ );
506
+
507
+ // Update modules parallel.
508
+ const promises: Promise<void>[] = [];
509
+ uniqueModules.forEach((module) =>
510
+ promises.push(this.handleModule(module)),
511
+ );
512
+ await Promise.all(promises);
513
+ await deleteDir(this.tempModulesDir);
514
+ await this.project.collectModuleResources();
515
+ }
516
+ }
517
+
322
518
  // Checks that module prefix is not in use in the project
323
519
  private async validatePrefix(modulePrefix: string) {
324
520
  // Do not allow modules with same prefixes.
@@ -370,16 +566,24 @@ export class ModuleManager {
370
566
  * Imports module from gitUrl.
371
567
  * @param source Git URL to import from.
372
568
  * @param options Modules setting options.
569
+ * @param credentials Credentials for private repositories.
373
570
  * @returns module prefix as defined in its CardsConfig.json
374
571
  */
375
- public async importGitModule(source: string, options?: ModuleSettingOptions) {
376
- const repoName = await this.clone({
377
- name: '',
378
- location: source,
379
- ...options,
380
- });
381
- await this.branch({ name: repoName, location: source, ...options });
382
- const clonePath = join(this.project.paths.tempFolder, 'modules', repoName);
572
+ public async importGitModule(
573
+ source: string,
574
+ options?: ModuleSettingOptions,
575
+ credentials?: Credentials,
576
+ ) {
577
+ const clonedName = await this.clone(
578
+ {
579
+ name: this.repositoryName(source),
580
+ location: source,
581
+ ...options,
582
+ },
583
+ undefined,
584
+ credentials,
585
+ );
586
+ const clonePath = join(this.tempModulesDir, clonedName);
383
587
  const modulePrefix = (await this.configuration(clonePath)).cardKeyPrefix;
384
588
  await this.validatePrefix(modulePrefix);
385
589
 
@@ -393,51 +597,53 @@ export class ModuleManager {
393
597
  }
394
598
 
395
599
  /**
396
- * Updates all imported modules.
600
+ * Removed module from project.
601
+ * If module is not used by any other modules, then will remove the module from disk as well.
602
+ * Otherwise, only updates project configuration.
603
+ * @param moduleName Name of the module to remove
397
604
  */
398
- public async update() {
399
- // Prints dots every half second so that user knows that something is ongoing
400
- function start() {
401
- console.log('... Collecting unique modules. This takes a moment.');
402
- return setInterval(() => process.stdout.write(`.`), 500);
403
- }
605
+ public async removeModule(moduleName: string) {
606
+ const projectModules = this.project.configuration.modules;
607
+ const dependencies = await this.buildDependencyGraph(projectModules);
608
+ const module = await this.project.module(moduleName);
404
609
 
405
- // Stops the above, and shows results
406
- function finished(interval: NodeJS.Timeout, modules: string[]) {
407
- clearInterval(interval);
408
- console.log(`\n... Found modules: ${modules.join(', ')}`);
610
+ if (!module) {
611
+ throw new Error(`Module '${moduleName}' not found`);
409
612
  }
410
613
 
411
- await this.prepare();
412
-
413
- const modules = this.project.configuration.modules;
414
- if (modules.length === 0) {
415
- throw new Error(`No modules in the project!`);
614
+ // Project module can always be removed from project configuration,
615
+ // but modules under .cards/modules must be checked not to be used by
616
+ // other modules.
617
+ if (this.canBeRemoved(dependencies, moduleName)) {
618
+ const orphans = await this.orphanedModules(dependencies, moduleName);
619
+ await deleteDir(module.path);
620
+ for (const moduleToDelete of orphans) {
621
+ const modulePath = join(
622
+ this.project.paths.modulesFolder,
623
+ moduleToDelete,
624
+ );
625
+ await deleteDir(modulePath);
626
+ }
627
+ await this.project.collectModuleResources();
416
628
  }
629
+ await this.project.configuration.removeModule(moduleName);
630
+ }
417
631
 
418
- const dotInterval = start();
419
-
420
- // Collect prefixes from project's dependency modules.
421
- await this.collectModulePrefixes(modules);
422
-
423
- let uniqueModules: ModuleSetting[] = [];
424
- try {
425
- uniqueModules = this.removeDuplicates(this.modules);
426
- } finally {
427
- finished(
428
- dotInterval,
429
- uniqueModules.map((item) => item.name),
430
- );
431
-
432
- // Update modules parallel.
433
- const promises: Promise<void>[] = [];
434
- uniqueModules.forEach((module) =>
435
- promises.push(this.handleModule(module)),
436
- );
437
- await Promise.all(promises);
632
+ /**
633
+ * Imports module from a local file path or a git URL.
634
+ * @param module Module to update. If not provided, updates all modules.
635
+ * @param credentials Optional credentials for private repositories.
636
+ * @returns Module prefix as defined in its CardsConfig.json
637
+ */
638
+ public async updateModule(module: ModuleSetting, credentials?: Credentials) {
639
+ return this.update(module, credentials);
640
+ }
438
641
 
439
- await deleteDir(this.tempModulesDir);
440
- await this.project.collectModuleResources();
441
- }
642
+ /**
643
+ * Updates all imported modules.
644
+ * @param credentials Optional credentials for private modules.
645
+ */
646
+ public async updateModules(credentials?: Credentials) {
647
+ return this.update(undefined, credentials);
442
648
  }
443
649
  }