@teambit/tracker 1.0.107 → 1.0.108

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.
@@ -0,0 +1,734 @@
1
+ import arrayDiff from 'array-difference';
2
+ import fs from 'fs-extra';
3
+ import ignore from 'ignore';
4
+ import groupby from 'lodash.groupby';
5
+ import * as path from 'path';
6
+ import R from 'ramda';
7
+ import format from 'string-format';
8
+ import { Analytics } from '@teambit/legacy/dist/analytics/analytics';
9
+ import { ComponentID } from '@teambit/component-id';
10
+ import { BitIdStr, BitId } from '@teambit/legacy-bit-id';
11
+ import { PACKAGE_JSON, VERSION_DELIMITER } from '@teambit/legacy/dist/constants';
12
+ import BitMap from '@teambit/legacy/dist/consumer/bit-map';
13
+ import Consumer from '@teambit/legacy/dist/consumer/consumer';
14
+ import GeneralError from '@teambit/legacy/dist/error/general-error';
15
+ import logger from '@teambit/legacy/dist/logger/logger';
16
+ import { calculateFileInfo, glob, isAutoGeneratedFile, isDir, pathNormalizeToLinux } from '@teambit/legacy/dist/utils';
17
+ import { BitError } from '@teambit/bit-error';
18
+ import { PathLinux, PathLinuxRelative, PathOsBased } from '@teambit/legacy/dist/utils/path';
19
+ import ComponentMap, {
20
+ ComponentMapFile,
21
+ Config,
22
+ getIgnoreListHarmony,
23
+ } from '@teambit/legacy/dist/consumer/bit-map/component-map';
24
+ import MissingMainFile from '@teambit/legacy/dist/consumer/bit-map/exceptions/missing-main-file';
25
+ import {
26
+ DuplicateIds,
27
+ EmptyDirectory,
28
+ ExcludedMainFile,
29
+ MainFileIsDir,
30
+ NoFiles,
31
+ PathsNotExist,
32
+ } from '@teambit/legacy/dist/consumer/component-ops/add-components/exceptions';
33
+ import { AddingIndividualFiles } from '@teambit/legacy/dist/consumer/component-ops/add-components/exceptions/adding-individual-files';
34
+ import MissingMainFileMultipleComponents from '@teambit/legacy/dist/consumer/component-ops/add-components/exceptions/missing-main-file-multiple-components';
35
+ import { ParentDirTracked } from '@teambit/legacy/dist/consumer/component-ops/add-components/exceptions/parent-dir-tracked';
36
+ import PathOutsideConsumer from '@teambit/legacy/dist/consumer/component-ops/add-components/exceptions/path-outside-consumer';
37
+ import VersionShouldBeRemoved from '@teambit/legacy/dist/consumer/component-ops/add-components/exceptions/version-should-be-removed';
38
+ import { linkToNodeModulesByIds } from '@teambit/workspace.modules.node-modules-linker';
39
+ import { Workspace } from '@teambit/workspace';
40
+ import determineMainFile from './determine-main-file';
41
+
42
+ export type AddResult = { id: ComponentID; files: ComponentMapFile[] };
43
+ export type Warnings = {
44
+ alreadyUsed: Record<string, any>;
45
+ emptyDirectory: string[];
46
+ existInScope: ComponentID[];
47
+ };
48
+ export type AddActionResults = { addedComponents: AddResult[]; warnings: Warnings };
49
+ export type PathOrDSL = PathOsBased | string; // can be a path or a DSL, e.g: tests/{PARENT}/{FILE_NAME}
50
+ // @ts-ignore AUTO-ADDED-AFTER-MIGRATION-PLEASE-FIX!
51
+ // @ts-ignore AUTO-ADDED-AFTER-MIGRATION-PLEASE-FIX!
52
+ type PathsStats = { [PathOsBased]: { isDir: boolean } };
53
+ export type AddedComponent = {
54
+ componentId: ComponentID;
55
+ files: ComponentMapFile[];
56
+ mainFile?: PathOsBased | null | undefined;
57
+ trackDir: PathOsBased;
58
+ idFromPath:
59
+ | {
60
+ name: string;
61
+ namespace: string;
62
+ }
63
+ | null
64
+ | undefined;
65
+ immediateDir?: string;
66
+ };
67
+ const REGEX_DSL_PATTERN = /{([^}]+)}/g;
68
+
69
+ export type AddProps = {
70
+ componentPaths: PathOsBased[];
71
+ id?: string;
72
+ main?: PathOsBased;
73
+ namespace?: string;
74
+ override: boolean;
75
+ trackDirFeature?: boolean;
76
+ defaultScope?: string;
77
+ config?: Config;
78
+ shouldHandleOutOfSync?: boolean;
79
+ env?: string;
80
+ };
81
+ // This is the contxt of the add operation. By default, the add is executed in the same folder in which the consumer is located and it is the process.cwd().
82
+ // In that case , give the value false to overridenConsumer .
83
+ // There is a possibility to execute add when the process.cwd() is different from the project directory. In that case , when add is done on a folder wchih is
84
+ // Different from process.cwd(), transfer true.
85
+ // Required for determining if the paths are relative to consumer or to process.cwd().
86
+ export type AddContext = {
87
+ workspace: Workspace;
88
+ alternateCwd?: string;
89
+ };
90
+
91
+ export default class AddComponents {
92
+ workspace: Workspace;
93
+ consumer: Consumer;
94
+ bitMap: BitMap;
95
+ componentPaths: PathOsBased[];
96
+ id: string | null | undefined; // id entered by the user
97
+ main: PathOsBased | null | undefined;
98
+ namespace: string | null | undefined;
99
+ override: boolean; // (default = false) replace the files array or only add files.
100
+ trackDirFeature: boolean | null | undefined;
101
+ warnings: Warnings;
102
+ // @ts-ignore AUTO-ADDED-AFTER-MIGRATION-PLEASE-FIX!
103
+ ignoreList: string[];
104
+ gitIgnore: any;
105
+ alternateCwd: string | null | undefined;
106
+ addedComponents: AddResult[];
107
+ defaultScope?: string; // helpful for out-of-sync
108
+ config?: Config;
109
+ shouldHandleOutOfSync?: boolean; // only bit-add (not bit-create/new) should handle out-of-sync scenario
110
+ constructor(context: AddContext, addProps: AddProps) {
111
+ this.alternateCwd = context.alternateCwd;
112
+ this.workspace = context.workspace;
113
+ this.consumer = context.workspace.consumer;
114
+ this.bitMap = this.consumer.bitMap;
115
+ this.componentPaths = this.joinConsumerPathIfNeeded(addProps.componentPaths);
116
+ this.id = addProps.id;
117
+ this.main = addProps.main;
118
+ this.namespace = addProps.namespace;
119
+ this.override = addProps.override;
120
+ this.trackDirFeature = addProps.trackDirFeature;
121
+ this.warnings = {
122
+ alreadyUsed: {},
123
+ emptyDirectory: [],
124
+ existInScope: [],
125
+ };
126
+ this.addedComponents = [];
127
+ this.defaultScope = addProps.defaultScope;
128
+ this.config = addProps.config;
129
+ this.shouldHandleOutOfSync = addProps.shouldHandleOutOfSync;
130
+ }
131
+
132
+ joinConsumerPathIfNeeded(paths: PathOrDSL[]): PathOrDSL[] {
133
+ if (paths.length > 0) {
134
+ if (this.alternateCwd !== undefined && this.alternateCwd !== null) {
135
+ const alternate = this.alternateCwd;
136
+ return paths.map((file) => path.join(alternate, file));
137
+ }
138
+ return paths;
139
+ }
140
+ return [];
141
+ }
142
+
143
+ /**
144
+ * @param {string[]} files - array of file-paths from which it should search for the dsl patterns.
145
+ * @param {*} filesWithPotentialDsl - array of file-path which may have DSL patterns
146
+ *
147
+ * @returns array of file-paths from 'files' parameter that match the patterns from 'filesWithPotentialDsl' parameter
148
+ */
149
+ async getFilesAccordingToDsl(files: PathLinux[], filesWithPotentialDsl: PathOrDSL[]): Promise<PathLinux[]> {
150
+ const filesListAllMatches = filesWithPotentialDsl.map(async (dsl) => {
151
+ const filesListMatch = files.map(async (file) => {
152
+ const fileInfo = calculateFileInfo(file);
153
+ const generatedFile = format(dsl, fileInfo);
154
+ const matches = await glob(generatedFile);
155
+ const matchesAfterGitIgnore = this.gitIgnore.filter(matches);
156
+ return matchesAfterGitIgnore.filter((match) => fs.existsSync(match));
157
+ });
158
+ return Promise.all(filesListMatch);
159
+ });
160
+
161
+ const filesListFlatten = R.flatten(await Promise.all(filesListAllMatches));
162
+ const filesListUnique = R.uniq(filesListFlatten);
163
+ return filesListUnique.map((file) => {
164
+ // when files array has the test file with different letter case, use the one from the file array
165
+ const fileNormalized = pathNormalizeToLinux(file);
166
+ const fileWithCorrectCase = files.find((f) => f.toLowerCase() === fileNormalized.toLowerCase()) || fileNormalized;
167
+ const relativeToConsumer = this.consumer.getPathRelativeToConsumer(fileWithCorrectCase);
168
+ return pathNormalizeToLinux(relativeToConsumer);
169
+ });
170
+ }
171
+
172
+ /**
173
+ * for imported component, the package.json in the root directory is a bit-generated file and as
174
+ * such, it should be ignored
175
+ */
176
+ _isPackageJsonOnRootDir(pathRelativeToConsumerRoot: PathLinux, componentMap: ComponentMap) {
177
+ if (!componentMap.rootDir) {
178
+ throw new Error('_isPackageJsonOnRootDir should not get called on non imported components');
179
+ }
180
+ return path.join(componentMap.rootDir, PACKAGE_JSON) === path.normalize(pathRelativeToConsumerRoot);
181
+ }
182
+
183
+ /**
184
+ * imported components might have wrapDir, when they do, files should not be added outside of
185
+ * that wrapDir
186
+ */
187
+ _isOutsideOfWrapDir(pathRelativeToConsumerRoot: PathLinux, componentMap: ComponentMap) {
188
+ if (!componentMap.rootDir) {
189
+ throw new Error('_isOutsideOfWrapDir should not get called on non imported components');
190
+ }
191
+ if (!componentMap.wrapDir) return false;
192
+ const wrapDirRelativeToConsumerRoot = path.join(componentMap.rootDir, componentMap.wrapDir);
193
+ return !path.normalize(pathRelativeToConsumerRoot).startsWith(wrapDirRelativeToConsumerRoot);
194
+ }
195
+
196
+ /**
197
+ * Add or update existing (imported and new) component according to bitmap
198
+ * there are 3 options:
199
+ * 1. a user is adding a new component. there is no record for this component in bit.map
200
+ * 2. a user is updating an existing component. there is a record for this component in bit.map
201
+ * 3. some or all the files of this component were previously added as another component-id.
202
+ */
203
+ async addOrUpdateComponentInBitMap(component: AddedComponent): Promise<AddResult | null | undefined> {
204
+ const consumerPath = this.consumer.getPath();
205
+ const parsedBitId = component.componentId;
206
+ const componentFromScope = await this.consumer.scope.getModelComponentIfExist(parsedBitId);
207
+ const files: ComponentMapFile[] = component.files;
208
+ const foundComponentFromBitMap = this.bitMap.getComponentIfExist(component.componentId, {
209
+ ignoreVersion: true,
210
+ });
211
+ const componentFilesP = files.map(async (file: ComponentMapFile) => {
212
+ // $FlowFixMe null is removed later on
213
+ const filePath = path.join(consumerPath, file.relativePath);
214
+ const isAutoGenerated = await isAutoGeneratedFile(filePath);
215
+ if (isAutoGenerated) {
216
+ return null;
217
+ }
218
+ const caseSensitive = false;
219
+ const existingIdOfFile = this.bitMap.getComponentIdByPath(file.relativePath, caseSensitive);
220
+ const idOfFileIsDifferent = existingIdOfFile && !existingIdOfFile.isEqual(parsedBitId);
221
+ if (idOfFileIsDifferent) {
222
+ // not imported component file but exists in bitmap
223
+ // @ts-ignore AUTO-ADDED-AFTER-MIGRATION-PLEASE-FIX!
224
+ if (this.warnings.alreadyUsed[existingIdOfFile]) {
225
+ // @ts-ignore AUTO-ADDED-AFTER-MIGRATION-PLEASE-FIX!
226
+ this.warnings.alreadyUsed[existingIdOfFile].push(file.relativePath);
227
+ } else {
228
+ // @ts-ignore AUTO-ADDED-AFTER-MIGRATION-PLEASE-FIX!
229
+ this.warnings.alreadyUsed[existingIdOfFile] = [file.relativePath];
230
+ }
231
+ return null;
232
+ }
233
+ if (!foundComponentFromBitMap && componentFromScope && this.shouldHandleOutOfSync) {
234
+ const newId = componentFromScope.toComponentIdWithLatestVersion();
235
+ if (!this.defaultScope || this.defaultScope === newId.scope) {
236
+ // otherwise, if defaultScope !== newId.scope, they're different components,
237
+ // and no need to change the id.
238
+ // for more details about this scenario, see https://github.com/teambit/bit/issues/1543, last case.
239
+ component.componentId = newId;
240
+ this.warnings.existInScope.push(newId);
241
+ }
242
+ }
243
+ return file;
244
+ });
245
+ // @ts-ignore it can't be null due to the filter function
246
+ const componentFiles: ComponentMapFile[] = (await Promise.all(componentFilesP)).filter((file) => file);
247
+ if (!componentFiles.length) return { id: component.componentId, files: [] };
248
+ if (foundComponentFromBitMap) {
249
+ this._updateFilesAccordingToExistingRootDir(foundComponentFromBitMap, componentFiles, component);
250
+ }
251
+ if (this.trackDirFeature) {
252
+ // @ts-ignore AUTO-ADDED-AFTER-MIGRATION-PLEASE-FIX!
253
+ if (this.bitMap._areFilesArraysEqual(foundComponentFromBitMap.files, componentFiles)) {
254
+ // @ts-ignore AUTO-ADDED-AFTER-MIGRATION-PLEASE-FIX!
255
+ return foundComponentFromBitMap;
256
+ }
257
+ }
258
+ if (!this.override && foundComponentFromBitMap) {
259
+ this._updateFilesWithCurrentLetterCases(foundComponentFromBitMap, componentFiles);
260
+ component.files = this._mergeFilesWithExistingComponentMapFiles(componentFiles, foundComponentFromBitMap.files);
261
+ } else {
262
+ component.files = componentFiles;
263
+ }
264
+
265
+ const { componentId, trackDir } = component;
266
+ const mainFile = determineMainFile(component, foundComponentFromBitMap);
267
+ const getRootDir = (): PathLinuxRelative => {
268
+ if (this.trackDirFeature) throw new Error('track dir should not calculate the rootDir');
269
+ if (foundComponentFromBitMap) return foundComponentFromBitMap.rootDir;
270
+ if (!trackDir) throw new Error(`addOrUpdateComponentInBitMap expect to have trackDir for non-legacy workspace`);
271
+ const fileNotInsideTrackDir = componentFiles.find(
272
+ (file) => !pathNormalizeToLinux(file.relativePath).startsWith(`${pathNormalizeToLinux(trackDir)}/`)
273
+ );
274
+ if (fileNotInsideTrackDir) {
275
+ // we check for this error before. however, it's possible that a user have one trackDir
276
+ // and another dir for the tests.
277
+ throw new AddingIndividualFiles(fileNotInsideTrackDir.relativePath);
278
+ }
279
+ return pathNormalizeToLinux(trackDir);
280
+ };
281
+ const getComponentMap = async (): Promise<ComponentMap> => {
282
+ if (this.trackDirFeature) {
283
+ return this.bitMap.addFilesToComponent({ componentId, files: component.files });
284
+ }
285
+ const rootDir = getRootDir();
286
+ const getDefaultScope = async () => {
287
+ if (componentId.hasScope()) return undefined;
288
+ return this.getDefaultScope(rootDir, componentId.fullName);
289
+ };
290
+ const defaultScope = await getDefaultScope();
291
+ const componentMap = this.bitMap.addComponent({
292
+ componentId: new ComponentID(componentId._legacy, defaultScope),
293
+ files: component.files,
294
+ defaultScope,
295
+ config: this.config,
296
+ mainFile,
297
+ // @ts-ignore AUTO-ADDED-AFTER-MIGRATION-PLEASE-FIX!
298
+ override: this.override,
299
+ });
300
+ componentMap.changeRootDirAndUpdateFilesAccordingly(rootDir);
301
+ return componentMap;
302
+ };
303
+ const componentMap = await getComponentMap();
304
+ return { id: componentId, files: componentMap.files };
305
+ }
306
+
307
+ /**
308
+ * current componentFiles are relative to the workspace. we want them relative to the rootDir.
309
+ */
310
+ _updateFilesAccordingToExistingRootDir(
311
+ foundComponentFromBitMap: ComponentMap,
312
+ componentFiles: ComponentMapFile[],
313
+ component: AddedComponent
314
+ ) {
315
+ const existingRootDir = foundComponentFromBitMap.rootDir;
316
+ if (!existingRootDir) return; // nothing to do.
317
+ const areFilesInsideExistingRootDir = componentFiles.every((file) =>
318
+ pathNormalizeToLinux(file.relativePath).startsWith(`${existingRootDir}/`)
319
+ );
320
+ if (areFilesInsideExistingRootDir) {
321
+ ComponentMap.changeFilesPathAccordingToItsRootDir(existingRootDir, componentFiles);
322
+ return;
323
+ }
324
+ // some (or all) added files are outside the existing rootDir, the rootDir needs to be changed
325
+ // if a directory was added and it's a parent of the existing rootDir, change the rootDir to
326
+ // the currently added rootDir.
327
+ const currentlyAddedDir = pathNormalizeToLinux(component.trackDir);
328
+ const currentlyAddedDirParentOfRootDir = currentlyAddedDir && existingRootDir.startsWith(`${currentlyAddedDir}/`);
329
+ if (currentlyAddedDirParentOfRootDir) {
330
+ foundComponentFromBitMap.changeRootDirAndUpdateFilesAccordingly(currentlyAddedDir);
331
+ ComponentMap.changeFilesPathAccordingToItsRootDir(currentlyAddedDir, componentFiles);
332
+ return;
333
+ }
334
+ throw new GeneralError(`unable to add individual files outside the root dir (${existingRootDir}) of ${component.componentId}.
335
+ you can add the directory these files are located at and it'll change the root dir of the component accordingly`);
336
+ // we might want to change the behavior here to not throw an error and only change the rootDir to "."
337
+ // foundComponentFromBitMap.changeRootDirAndUpdateFilesAccordingly('.');
338
+ }
339
+
340
+ /**
341
+ * the risk with merging the currently added files with the existing bitMap files is overriding
342
+ * the `test` property. e.g. the component directory is re-added without adding the tests flag to
343
+ * track new files in that directory. in this case, we want to preserve the `test` property.
344
+ */
345
+ _mergeFilesWithExistingComponentMapFiles(
346
+ componentFiles: ComponentMapFile[],
347
+ existingComponentMapFile: ComponentMapFile[]
348
+ ) {
349
+ return R.unionWith(R.eqBy(R.prop('relativePath')), existingComponentMapFile, componentFiles);
350
+ }
351
+
352
+ /**
353
+ * if an existing file is for example uppercase and the new file is lowercase it has different
354
+ * behavior according to the OS. some OS are case sensitive, some are not.
355
+ * it's safer to avoid saving both files and instead, replacing the old file with the new one.
356
+ * in case a file has replaced and it is also a mainFile, replace the mainFile as well
357
+ */
358
+ _updateFilesWithCurrentLetterCases(currentComponentMap: ComponentMap, newFiles: ComponentMapFile[]) {
359
+ const currentFiles = currentComponentMap.files;
360
+ currentFiles.forEach((currentFile) => {
361
+ const sameFile = newFiles.find(
362
+ (newFile) => newFile.relativePath.toLowerCase() === currentFile.relativePath.toLowerCase()
363
+ );
364
+ if (sameFile && currentFile.relativePath !== sameFile.relativePath) {
365
+ if (currentComponentMap.mainFile === currentFile.relativePath) {
366
+ currentComponentMap.mainFile = sameFile.relativePath;
367
+ }
368
+ currentFile.relativePath = sameFile.relativePath;
369
+ }
370
+ });
371
+ }
372
+
373
+ /**
374
+ * if the id is already saved in bitmap file, it might have more data (such as scope, version)
375
+ * use that id instead.
376
+ */
377
+ private _getIdAccordingToExistingComponent(currentId: BitIdStr): ComponentID | undefined {
378
+ const idWithScope = this.defaultScope ? `${this.defaultScope}/${currentId}` : currentId;
379
+ const existingComponentId = this.bitMap.getExistingBitId(idWithScope, false);
380
+ if (currentId.includes(VERSION_DELIMITER)) {
381
+ if (
382
+ !existingComponentId || // this id is new, it shouldn't have a version
383
+ !existingComponentId.hasVersion() || // this component is new, it shouldn't have a version
384
+ // user shouldn't add files to a an existing component with different version
385
+ existingComponentId.version !== ComponentID.getVersionFromString(currentId)
386
+ ) {
387
+ throw new VersionShouldBeRemoved(currentId);
388
+ }
389
+ }
390
+ return existingComponentId;
391
+ }
392
+
393
+ _getIdAccordingToTrackDir(dir: PathOsBased): ComponentID | null | undefined {
394
+ const dirNormalizedToLinux = pathNormalizeToLinux(dir);
395
+ const trackDirs = this.bitMap.getAllTrackDirs();
396
+ if (!trackDirs) return null;
397
+ return trackDirs[dirNormalizedToLinux];
398
+ }
399
+
400
+ /**
401
+ * used for updating main file if exists or doesn't exists
402
+ */
403
+ _addMainFileToFiles(files: ComponentMapFile[]): PathOsBased | null | undefined {
404
+ let mainFile = this.main;
405
+ if (mainFile && mainFile.match(REGEX_DSL_PATTERN)) {
406
+ // it's a DSL
407
+ files.forEach((file) => {
408
+ const fileInfo = calculateFileInfo(file.relativePath);
409
+ const generatedFile = format(mainFile, fileInfo);
410
+ const foundFile = this._findMainFileInFiles(generatedFile, files);
411
+ if (foundFile) {
412
+ mainFile = foundFile.relativePath;
413
+ }
414
+ if (fs.existsSync(generatedFile) && !foundFile) {
415
+ const shouldIgnore = this.gitIgnore.ignores(generatedFile);
416
+ if (shouldIgnore) {
417
+ // check if file is in exclude list
418
+ throw new ExcludedMainFile(generatedFile);
419
+ }
420
+ files.push({
421
+ relativePath: pathNormalizeToLinux(generatedFile),
422
+ test: false,
423
+ name: path.basename(generatedFile),
424
+ });
425
+ mainFile = generatedFile;
426
+ }
427
+ });
428
+ }
429
+ if (!mainFile) return undefined;
430
+ if (this.alternateCwd) {
431
+ mainFile = path.join(this.alternateCwd, mainFile);
432
+ }
433
+ const mainFileRelativeToConsumer = this.consumer.getPathRelativeToConsumer(mainFile);
434
+ const mainPath = this.consumer.toAbsolutePath(mainFileRelativeToConsumer);
435
+ if (fs.existsSync(mainPath)) {
436
+ const shouldIgnore = this.gitIgnore.ignores(mainFileRelativeToConsumer);
437
+ if (shouldIgnore) throw new ExcludedMainFile(mainFileRelativeToConsumer);
438
+ if (isDir(mainPath)) {
439
+ throw new MainFileIsDir(mainPath);
440
+ }
441
+ const foundFile = this._findMainFileInFiles(mainFileRelativeToConsumer, files);
442
+ if (foundFile) {
443
+ return foundFile.relativePath;
444
+ }
445
+ files.push({
446
+ relativePath: pathNormalizeToLinux(mainFileRelativeToConsumer),
447
+ test: false,
448
+ name: path.basename(mainFileRelativeToConsumer),
449
+ });
450
+ return mainFileRelativeToConsumer;
451
+ }
452
+ return mainFile;
453
+ }
454
+
455
+ _findMainFileInFiles(mainFile: string, files: ComponentMapFile[]) {
456
+ const normalizedMainFile = pathNormalizeToLinux(mainFile).toLowerCase();
457
+ return files.find((file) => file.relativePath.toLowerCase() === normalizedMainFile);
458
+ }
459
+
460
+ private async getDefaultScope(rootDir: string, componentName: string): Promise<string> {
461
+ return (this.defaultScope ||
462
+ (await this.workspace.componentDefaultScopeFromComponentDirAndName(rootDir, componentName))) as string;
463
+ }
464
+
465
+ /**
466
+ * given the component paths, prepare the id, mainFile and files to be added later on to bitmap
467
+ * the id of the component is either entered by the user or, if not entered, concluded by the path.
468
+ * e.g. bar/foo.js, the id would be bar/foo.
469
+ * in case bitmap has already the same id, the complete id is taken from bitmap (see _getIdAccordingToExistingComponent)
470
+ */
471
+ async addOneComponent(componentPath: PathOsBased): Promise<AddedComponent> {
472
+ let finalBitId: ComponentID | undefined; // final id to use for bitmap file
473
+ let idFromPath;
474
+ if (this.id) {
475
+ finalBitId = this._getIdAccordingToExistingComponent(this.id);
476
+ }
477
+ const relativeComponentPath = this.consumer.getPathRelativeToConsumer(componentPath);
478
+ this._throwForOutsideConsumer(relativeComponentPath);
479
+ this.throwForExistingParentDir(relativeComponentPath);
480
+ const matches = await glob(path.join(relativeComponentPath, '**'), {
481
+ cwd: this.consumer.getPath(),
482
+ nodir: true,
483
+ });
484
+
485
+ if (!matches.length) throw new EmptyDirectory(componentPath);
486
+
487
+ const filteredMatches = this.gitIgnore.filter(matches);
488
+
489
+ if (!filteredMatches.length) {
490
+ throw new NoFiles(matches);
491
+ }
492
+
493
+ const filteredMatchedFiles = filteredMatches.map((match: PathOsBased) => {
494
+ return { relativePath: pathNormalizeToLinux(match), test: false, name: path.basename(match) };
495
+ });
496
+ const resolvedMainFile = this._addMainFileToFiles(filteredMatchedFiles);
497
+
498
+ const absoluteComponentPath = path.resolve(componentPath);
499
+ const splitPath = absoluteComponentPath.split(path.sep);
500
+ const lastDir = splitPath[splitPath.length - 1];
501
+ const idOfTrackDir = this._getIdAccordingToTrackDir(componentPath);
502
+ if (!finalBitId) {
503
+ if (this.id) {
504
+ const bitId = BitId.parse(this.id, false);
505
+ const defaultScope = await this.getDefaultScope(relativeComponentPath, bitId.name);
506
+ finalBitId = new ComponentID(bitId, defaultScope);
507
+ } else if (idOfTrackDir) {
508
+ finalBitId = idOfTrackDir;
509
+ } else {
510
+ const nameSpaceOrDir = this.namespace || splitPath[splitPath.length - 2];
511
+ if (!this.namespace) {
512
+ idFromPath = { namespace: BitId.getValidIdChunk(nameSpaceOrDir), name: BitId.getValidIdChunk(lastDir) };
513
+ }
514
+ const bitId = BitId.getValidBitId(nameSpaceOrDir, lastDir);
515
+ const defaultScope = await this.getDefaultScope(relativeComponentPath, bitId.name);
516
+ finalBitId = new ComponentID(bitId, defaultScope);
517
+ }
518
+ }
519
+ const trackDir = relativeComponentPath;
520
+ const addedComp = {
521
+ componentId: finalBitId,
522
+ files: filteredMatchedFiles,
523
+ mainFile: resolvedMainFile,
524
+ trackDir,
525
+ idFromPath,
526
+ immediateDir: lastDir,
527
+ };
528
+
529
+ return addedComp;
530
+ }
531
+
532
+ getIgnoreList(): string[] {
533
+ const consumerPath = this.consumer.getPath();
534
+ return getIgnoreListHarmony(consumerPath);
535
+ }
536
+
537
+ async add(): Promise<AddActionResults> {
538
+ this.ignoreList = this.getIgnoreList();
539
+ this.gitIgnore = ignore().add(this.ignoreList); // add ignore list
540
+
541
+ let componentPathsStats: PathsStats = {};
542
+
543
+ const resolvedComponentPathsWithoutGitIgnore = R.flatten(
544
+ await Promise.all(this.componentPaths.map((componentPath) => glob(componentPath)))
545
+ );
546
+ this.gitIgnore = ignore().add(this.ignoreList); // add ignore list
547
+
548
+ const resolvedComponentPathsWithGitIgnore = this.gitIgnore.filter(resolvedComponentPathsWithoutGitIgnore);
549
+ // Run diff on both arrays to see what was filtered out because of the gitignore file
550
+ const diff = arrayDiff(resolvedComponentPathsWithGitIgnore, resolvedComponentPathsWithoutGitIgnore);
551
+
552
+ if (R.isEmpty(resolvedComponentPathsWithoutGitIgnore)) {
553
+ throw new PathsNotExist(this.componentPaths);
554
+ }
555
+ if (!R.isEmpty(resolvedComponentPathsWithGitIgnore)) {
556
+ componentPathsStats = validatePaths(resolvedComponentPathsWithGitIgnore);
557
+ } else {
558
+ throw new NoFiles(diff);
559
+ }
560
+ Object.keys(componentPathsStats).forEach((compPath) => {
561
+ if (!componentPathsStats[compPath].isDir) {
562
+ throw new AddingIndividualFiles(compPath);
563
+ }
564
+ });
565
+ if (Object.keys(componentPathsStats).length > 1 && this.id) {
566
+ throw new BitError(
567
+ `the --id flag (${this.id}) is used for a single component only, however, got ${this.componentPaths.length} paths`
568
+ );
569
+ }
570
+ // if a user entered multiple paths and entered an id, he wants all these paths to be one component
571
+ // conversely, if a user entered multiple paths without id, he wants each dir as an individual component
572
+ const isMultipleComponents = Object.keys(componentPathsStats).length > 1;
573
+ if (isMultipleComponents) {
574
+ await this.addMultipleComponents(componentPathsStats);
575
+ } else {
576
+ logger.debugAndAddBreadCrumb('add-components', 'adding one component');
577
+ // when a user enters more than one directory, he would like to keep the directories names
578
+ // so then when a component is imported, it will write the files into the original directories
579
+ const addedOne = await this.addOneComponent(Object.keys(componentPathsStats)[0]);
580
+ await this._removeNamespaceIfNotNeeded([addedOne]);
581
+ if (!R.isEmpty(addedOne.files)) {
582
+ const addedResult = await this.addOrUpdateComponentInBitMap(addedOne);
583
+ if (addedResult) this.addedComponents.push(addedResult);
584
+ }
585
+ }
586
+ await this.linkComponents(this.addedComponents.map((item) => item.id));
587
+ Analytics.setExtraData('num_components', this.addedComponents.length);
588
+ return { addedComponents: this.addedComponents, warnings: this.warnings };
589
+ }
590
+
591
+ async linkComponents(ids: ComponentID[]) {
592
+ if (this.trackDirFeature) {
593
+ // if trackDirFeature is set, it happens during the component-load and because we load the
594
+ // components in the next line, it gets into an infinite loop.
595
+ return;
596
+ }
597
+ await linkToNodeModulesByIds(this.workspace, ids);
598
+ }
599
+
600
+ async addMultipleComponents(componentPathsStats: PathsStats): Promise<void> {
601
+ logger.debugAndAddBreadCrumb('add-components', 'adding multiple components');
602
+ this._removeDirectoriesWhenTheirFilesAreAdded(componentPathsStats);
603
+ const added = await this._tryAddingMultiple(componentPathsStats);
604
+ validateNoDuplicateIds(added);
605
+ await this._removeNamespaceIfNotNeeded(added);
606
+ await this._addMultipleToBitMap(added);
607
+ }
608
+
609
+ /**
610
+ * some uses of wildcards might add directories and their files at the same time, in such cases
611
+ * only the files are needed and the directories can be ignored.
612
+ * @see https://github.com/teambit/bit/issues/1406 for more details
613
+ */
614
+ _removeDirectoriesWhenTheirFilesAreAdded(componentPathsStats: PathsStats) {
615
+ const allPaths = Object.keys(componentPathsStats);
616
+ allPaths.forEach((componentPath) => {
617
+ const foundDir = allPaths.find((p) => p === path.dirname(componentPath));
618
+ if (foundDir && componentPathsStats[foundDir]) {
619
+ logger.debug(`add-components._removeDirectoriesWhenTheirFilesAreAdded, ignoring ${foundDir}`);
620
+ delete componentPathsStats[foundDir];
621
+ }
622
+ });
623
+ }
624
+
625
+ async _addMultipleToBitMap(added: AddedComponent[]): Promise<void> {
626
+ const missingMainFiles = [];
627
+ await Promise.all(
628
+ added.map(async (component) => {
629
+ if (!R.isEmpty(component.files)) {
630
+ try {
631
+ const addedComponent = await this.addOrUpdateComponentInBitMap(component);
632
+ if (addedComponent && addedComponent.files.length) this.addedComponents.push(addedComponent);
633
+ } catch (err: any) {
634
+ if (!(err instanceof MissingMainFile)) throw err;
635
+ // @ts-ignore AUTO-ADDED-AFTER-MIGRATION-PLEASE-FIX!
636
+ missingMainFiles.push(err);
637
+ }
638
+ }
639
+ })
640
+ );
641
+ if (missingMainFiles.length) {
642
+ // @ts-ignore AUTO-ADDED-AFTER-MIGRATION-PLEASE-FIX!
643
+ throw new MissingMainFileMultipleComponents(missingMainFiles.map((err) => err.componentId).sort());
644
+ }
645
+ }
646
+
647
+ async _removeNamespaceIfNotNeeded(addedComponents: AddedComponent[]) {
648
+ const allIds = this.bitMap.getAllBitIdsFromAllLanes();
649
+ await Promise.all(
650
+ addedComponents.map(async (addedComponent) => {
651
+ if (!addedComponent.idFromPath) return; // when the id was not generated from the path do nothing.
652
+ const componentsWithSameName = addedComponents.filter(
653
+ (a) => a.idFromPath && a.idFromPath.name === addedComponent.idFromPath?.name
654
+ );
655
+ const bitIdFromNameOnly = new BitId({ name: addedComponent.idFromPath.name });
656
+ const defaultScope = await this.getDefaultScope(addedComponent.trackDir, bitIdFromNameOnly.name);
657
+ const componentIdFromNameOnly = new ComponentID(bitIdFromNameOnly, defaultScope);
658
+ const existingComponentWithSameName = allIds.searchWithoutScopeAndVersion(componentIdFromNameOnly);
659
+ if (componentsWithSameName.length === 1 && !existingComponentWithSameName) {
660
+ addedComponent.componentId = componentIdFromNameOnly;
661
+ }
662
+ })
663
+ );
664
+ }
665
+
666
+ async _tryAddingMultiple(componentPathsStats: PathsStats): Promise<AddedComponent[]> {
667
+ const addedP = Object.keys(componentPathsStats).map(async (onePath) => {
668
+ try {
669
+ const addedComponent = await this.addOneComponent(onePath);
670
+ return addedComponent;
671
+ } catch (err: any) {
672
+ if (!(err instanceof EmptyDirectory)) throw err;
673
+ this.warnings.emptyDirectory.push(onePath);
674
+ return null;
675
+ }
676
+ });
677
+ const added = await Promise.all(addedP);
678
+ return R.reject(R.isNil, added);
679
+ }
680
+
681
+ _throwForOutsideConsumer(relativeToConsumerPath: PathOsBased) {
682
+ if (relativeToConsumerPath.startsWith('..')) {
683
+ throw new PathOutsideConsumer(relativeToConsumerPath);
684
+ }
685
+ }
686
+
687
+ private throwForExistingParentDir(relativeToConsumerPath: PathOsBased) {
688
+ const isParentDir = (parent: string) => {
689
+ const relative = path.relative(parent, relativeToConsumerPath);
690
+ return relative && !relative.startsWith('..') && !path.isAbsolute(relative);
691
+ };
692
+ this.bitMap.components.forEach((componentMap) => {
693
+ if (!componentMap.rootDir) return;
694
+ if (isParentDir(componentMap.rootDir)) {
695
+ throw new ParentDirTracked(
696
+ componentMap.rootDir,
697
+ componentMap.id.toStringWithoutVersion(),
698
+ relativeToConsumerPath
699
+ );
700
+ }
701
+ });
702
+ }
703
+ }
704
+
705
+ /**
706
+ * validatePaths - validate if paths entered by user exist and if not throw an error
707
+ *
708
+ * @param {string[]} fileArray - array of paths
709
+ * @returns {PathsStats} componentPathsStats
710
+ */
711
+ function validatePaths(fileArray: string[]): PathsStats {
712
+ const componentPathsStats = {};
713
+ fileArray.forEach((componentPath) => {
714
+ if (!fs.existsSync(componentPath)) {
715
+ throw new PathsNotExist([componentPath]);
716
+ }
717
+ componentPathsStats[componentPath] = {
718
+ isDir: isDir(componentPath),
719
+ };
720
+ });
721
+ return componentPathsStats;
722
+ }
723
+
724
+ /**
725
+ * validate that no two files where added with the same id in the same bit add command
726
+ */
727
+ function validateNoDuplicateIds(addComponents: Record<string, any>[]) {
728
+ const duplicateIds = {};
729
+ const newGroupedComponents = groupby(addComponents, 'componentId');
730
+ Object.keys(newGroupedComponents).forEach((key) => {
731
+ if (newGroupedComponents[key].length > 1) duplicateIds[key] = newGroupedComponents[key];
732
+ });
733
+ if (!R.isEmpty(duplicateIds) && !R.isNil(duplicateIds)) throw new DuplicateIds(duplicateIds);
734
+ }