@teambit/objects 0.0.0-00042bc99514ea12d897f81bcd0181e2b034c615

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 (121) hide show
  1. package/dist/fixtures/version-model-extended.json +48 -0
  2. package/dist/fixtures/version-model-object.json +87 -0
  3. package/dist/index.d.ts +19 -0
  4. package/dist/index.js +371 -0
  5. package/dist/index.js.map +1 -0
  6. package/dist/models/dependencies-graph.d.ts +46 -0
  7. package/dist/models/dependencies-graph.js +105 -0
  8. package/dist/models/dependencies-graph.js.map +1 -0
  9. package/dist/models/detach-heads.d.ts +25 -0
  10. package/dist/models/detach-heads.js +84 -0
  11. package/dist/models/detach-heads.js.map +1 -0
  12. package/dist/models/export-metadata.d.ts +24 -0
  13. package/dist/models/export-metadata.js +74 -0
  14. package/dist/models/export-metadata.js.map +1 -0
  15. package/dist/models/index.d.ts +10 -0
  16. package/dist/models/index.js +125 -0
  17. package/dist/models/index.js.map +1 -0
  18. package/dist/models/lane-history.d.ts +43 -0
  19. package/dist/models/lane-history.js +131 -0
  20. package/dist/models/lane-history.js.map +1 -0
  21. package/dist/models/lane-history.spec.d.ts +1 -0
  22. package/dist/models/lane-history.spec.js +46 -0
  23. package/dist/models/lane-history.spec.js.map +1 -0
  24. package/dist/models/lane.d.ts +128 -0
  25. package/dist/models/lane.js +485 -0
  26. package/dist/models/lane.js.map +1 -0
  27. package/dist/models/model-component.d.ts +338 -0
  28. package/dist/models/model-component.js +1509 -0
  29. package/dist/models/model-component.js.map +1 -0
  30. package/dist/models/model-component.spec.d.ts +1 -0
  31. package/dist/models/model-component.spec.js +71 -0
  32. package/dist/models/model-component.spec.js.map +1 -0
  33. package/dist/models/scopeMeta.d.ts +20 -0
  34. package/dist/models/scopeMeta.js +68 -0
  35. package/dist/models/scopeMeta.js.map +1 -0
  36. package/dist/models/source.d.ts +10 -0
  37. package/dist/models/source.js +42 -0
  38. package/dist/models/source.js.map +1 -0
  39. package/dist/models/symlink.d.ts +30 -0
  40. package/dist/models/symlink.js +91 -0
  41. package/dist/models/symlink.js.map +1 -0
  42. package/dist/models/version-history.d.ts +59 -0
  43. package/dist/models/version-history.js +296 -0
  44. package/dist/models/version-history.js.map +1 -0
  45. package/dist/models/version.d.ts +288 -0
  46. package/dist/models/version.js +780 -0
  47. package/dist/models/version.js.map +1 -0
  48. package/dist/models/version.spec.d.ts +1 -0
  49. package/dist/models/version.spec.js +332 -0
  50. package/dist/models/version.spec.js.map +1 -0
  51. package/dist/objects/bit-object-list.d.ts +24 -0
  52. package/dist/objects/bit-object-list.js +65 -0
  53. package/dist/objects/bit-object-list.js.map +1 -0
  54. package/dist/objects/index.d.ts +5 -0
  55. package/dist/objects/index.js +60 -0
  56. package/dist/objects/index.js.map +1 -0
  57. package/dist/objects/object-list-to-graph.d.ts +13 -0
  58. package/dist/objects/object-list-to-graph.js +93 -0
  59. package/dist/objects/object-list-to-graph.js.map +1 -0
  60. package/dist/objects/object-list.d.ts +58 -0
  61. package/dist/objects/object-list.js +389 -0
  62. package/dist/objects/object-list.js.map +1 -0
  63. package/dist/objects/object.d.ts +35 -0
  64. package/dist/objects/object.js +189 -0
  65. package/dist/objects/object.js.map +1 -0
  66. package/dist/objects/objects-readable-generator.d.ts +31 -0
  67. package/dist/objects/objects-readable-generator.js +203 -0
  68. package/dist/objects/objects-readable-generator.js.map +1 -0
  69. package/dist/objects/raw-object.d.ts +23 -0
  70. package/dist/objects/raw-object.js +145 -0
  71. package/dist/objects/raw-object.js.map +1 -0
  72. package/dist/objects/ref.d.ts +14 -0
  73. package/dist/objects/ref.js +45 -0
  74. package/dist/objects/ref.js.map +1 -0
  75. package/dist/objects/repository.d.ts +165 -0
  76. package/dist/objects/repository.js +866 -0
  77. package/dist/objects/repository.js.map +1 -0
  78. package/dist/objects/scope-index.d.ts +74 -0
  79. package/dist/objects/scope-index.js +255 -0
  80. package/dist/objects/scope-index.js.map +1 -0
  81. package/dist/objects/scope-index.spec.d.ts +1 -0
  82. package/dist/objects/scope-index.spec.js +152 -0
  83. package/dist/objects/scope-index.spec.js.map +1 -0
  84. package/dist/objects.aspect.d.ts +2 -0
  85. package/dist/objects.aspect.js +18 -0
  86. package/dist/objects.aspect.js.map +1 -0
  87. package/dist/objects.main.runtime.d.ts +7 -0
  88. package/dist/objects.main.runtime.js +36 -0
  89. package/dist/objects.main.runtime.js.map +1 -0
  90. package/dist/preview-1781548568469.js +7 -0
  91. package/fixtures/version-model-extended.json +48 -0
  92. package/fixtures/version-model-object.json +87 -0
  93. package/models/dependencies-graph.ts +120 -0
  94. package/models/detach-heads.ts +79 -0
  95. package/models/export-metadata.ts +56 -0
  96. package/models/index.ts +11 -0
  97. package/models/lane-history.spec.ts +35 -0
  98. package/models/lane-history.ts +131 -0
  99. package/models/lane.ts +400 -0
  100. package/models/model-component.spec.ts +55 -0
  101. package/models/model-component.ts +1522 -0
  102. package/models/scopeMeta.ts +58 -0
  103. package/models/source.ts +31 -0
  104. package/models/symlink.ts +66 -0
  105. package/models/version-history.ts +283 -0
  106. package/models/version.spec.ts +280 -0
  107. package/models/version.ts +821 -0
  108. package/objects/bit-object-list.ts +59 -0
  109. package/objects/index.ts +6 -0
  110. package/objects/object-list-to-graph.ts +69 -0
  111. package/objects/object-list.ts +327 -0
  112. package/objects/object.ts +152 -0
  113. package/objects/objects-readable-generator.ts +174 -0
  114. package/objects/raw-object.ts +132 -0
  115. package/objects/ref.ts +45 -0
  116. package/objects/repository.ts +780 -0
  117. package/objects/scope-index.spec.ts +95 -0
  118. package/objects/scope-index.ts +197 -0
  119. package/package.json +97 -0
  120. package/types/asset.d.ts +41 -0
  121. package/types/style.d.ts +42 -0
@@ -0,0 +1,35 @@
1
+ import { expect } from 'chai';
2
+
3
+ import { LaneHistory } from './lane-history';
4
+
5
+ const makeItem = (date: string) => ({
6
+ log: { date, username: 'tester' },
7
+ components: [],
8
+ });
9
+
10
+ describe('LaneHistory', () => {
11
+ describe('getHistoryIds', () => {
12
+ it('returns ids sorted by date even when insertion order is not chronological', () => {
13
+ const laneHistory = LaneHistory.parse(
14
+ JSON.stringify({
15
+ name: 'tmp4',
16
+ scope: 'my-scope',
17
+ laneHash: 'abc',
18
+ history: {
19
+ 'newer-local': makeItem('1746369091000'), // 5/4/2026, 12:51 PM
20
+ 'older-remote-1': makeItem('1746127977000'), // 5/1/2026, 3:32 PM
21
+ 'older-remote-2': makeItem('1746127986000'), // 5/1/2026, 3:33 PM
22
+ 'between-remote': makeItem('1746358729000'), // 5/4/2026, 9:58 AM
23
+ },
24
+ })
25
+ );
26
+
27
+ expect(laneHistory.getHistoryIds()).to.deep.equal([
28
+ 'older-remote-1',
29
+ 'older-remote-2',
30
+ 'between-remote',
31
+ 'newer-local',
32
+ ]);
33
+ });
34
+ });
35
+ });
@@ -0,0 +1,131 @@
1
+ import { v4 } from 'uuid';
2
+ import { getStringifyArgs } from '@teambit/legacy.utils';
3
+ import { getBasicLog } from '@teambit/harmony.modules.get-basic-log';
4
+ import { BitObject } from '../objects';
5
+ import type Lane from './lane';
6
+
7
+ type Log = { date: string; username?: string; email?: string; message?: string };
8
+
9
+ export type HistoryItem = {
10
+ log: Log;
11
+ components: string[];
12
+ deleted?: string[];
13
+ // hidden lane.updateDependents at the time of the snapshot. Recorded in their own field so
14
+ // `historyItem.components` keeps its workspace-checkout/revert contract intact — those flows
15
+ // must not materialize hidden entries into the bitmap.
16
+ updateDependents?: string[];
17
+ };
18
+
19
+ type History = { [uuid: string]: HistoryItem };
20
+
21
+ type LaneHistoryProps = {
22
+ name: string;
23
+ scope: string;
24
+ laneHash: string;
25
+ history: History;
26
+ };
27
+
28
+ export class LaneHistory extends BitObject {
29
+ private name: string;
30
+ private scope: string;
31
+ private laneHash: string;
32
+ private history: History;
33
+
34
+ constructor(props: LaneHistoryProps) {
35
+ super();
36
+ this.name = props.name;
37
+ this.scope = props.scope;
38
+ this.laneHash = props.laneHash;
39
+ this.history = props.history;
40
+ }
41
+
42
+ id() {
43
+ return `${this.laneHash}:${LaneHistory.name}`;
44
+ }
45
+
46
+ static fromLaneObject(laneObject: Lane): LaneHistory {
47
+ return new LaneHistory({
48
+ scope: laneObject.scope,
49
+ name: laneObject.name,
50
+ laneHash: laneObject.hash().toString(),
51
+ history: {},
52
+ });
53
+ }
54
+
55
+ toObject(): Record<string, any> {
56
+ return {
57
+ name: this.name,
58
+ scope: this.scope,
59
+ laneHash: this.laneHash,
60
+ history: this.history,
61
+ };
62
+ }
63
+
64
+ toString(pretty: boolean): string {
65
+ const args = getStringifyArgs(pretty);
66
+ return JSON.stringify(this.toObject(), ...args);
67
+ }
68
+
69
+ toBuffer(pretty): Buffer {
70
+ return Buffer.from(this.toString(pretty));
71
+ }
72
+
73
+ getHistory(): History {
74
+ return this.history;
75
+ }
76
+
77
+ getHistoryIds(): string[] {
78
+ return Object.entries(this.history)
79
+ .sort(([, a], [, b]) => Number(a.log.date) - Number(b.log.date))
80
+ .map(([id]) => id);
81
+ }
82
+
83
+ async addHistory(laneObj: Lane, msg?: string, historyKey?: string) {
84
+ const log: Log = await getBasicLog();
85
+ if (msg) log.message = msg;
86
+ const components = laneObj.toComponentIds().toStringArray();
87
+ const deleted = laneObj.components
88
+ .filter((c) => c.isDeleted)
89
+ .map((c) => c.id.changeVersion(c.head.toString()).toString());
90
+ // Always write `updateDependents` (even when empty) so checkout/revert can distinguish a
91
+ // post-PR entry that legitimately had no hidden entries (drop current hidden) from a legacy
92
+ // pre-PR entry that never recorded the field at all (leave current hidden alone).
93
+ const updateDependents = (laneObj.updateDependents || []).map((id) => id.toString());
94
+ this.history[historyKey || v4()] = {
95
+ log,
96
+ components,
97
+ ...(deleted.length && { deleted }),
98
+ updateDependents,
99
+ };
100
+ }
101
+
102
+ removeHistoryEntries(keys: string[]) {
103
+ for (const key of keys) {
104
+ delete this.history[key];
105
+ }
106
+ }
107
+
108
+ merge(laneHistory: LaneHistory) {
109
+ this.history = { ...this.history, ...laneHistory.history };
110
+ }
111
+
112
+ static create(name: string, scope: string, laneHash: string) {
113
+ return new LaneHistory({
114
+ name,
115
+ scope,
116
+ laneHash,
117
+ history: {},
118
+ });
119
+ }
120
+
121
+ static parse(contents: string): LaneHistory {
122
+ const parsed = JSON.parse(contents);
123
+ const props: LaneHistoryProps = {
124
+ name: parsed.name,
125
+ scope: parsed.scope,
126
+ laneHash: parsed.laneHash,
127
+ history: parsed.history,
128
+ };
129
+ return new LaneHistory(props);
130
+ }
131
+ }
package/models/lane.ts ADDED
@@ -0,0 +1,400 @@
1
+ import { v4 } from 'uuid';
2
+ import { gte } from 'semver';
3
+ import { cloneDeep, isEqual, pickBy } from 'lodash';
4
+ import { BitError } from '@teambit/bit-error';
5
+ import { ComponentID, ComponentIdList } from '@teambit/component-id';
6
+ import { isSnap } from '@teambit/component-version';
7
+ import { LaneId, DEFAULT_LANE, LANE_REMOTE_DELIMITER } from '@teambit/lane-id';
8
+ import type { Scope } from '@teambit/legacy.scope';
9
+ import { CFG_USER_EMAIL_KEY, CFG_USER_NAME_KEY, PREVIOUS_DEFAULT_LANE } from '@teambit/legacy.constants';
10
+ import { ValidationError } from '@teambit/legacy.cli.error';
11
+ import { logger } from '@teambit/legacy.logger';
12
+ import { getStringifyArgs } from '@teambit/legacy.utils';
13
+ import { sha1 } from '@teambit/toolbox.crypto.sha1';
14
+ import { hasVersionByRef } from '@teambit/component.snap-distance';
15
+ import type { Repository } from '../objects';
16
+ import { BitObject, Ref } from '../objects';
17
+ import type Version from './version';
18
+ import { getConfig } from '@teambit/config-store';
19
+
20
+ export type Log = { date: string; username?: string; email?: string; profileImage?: string };
21
+
22
+ export type LaneProps = {
23
+ name: string;
24
+ scope: string;
25
+ log: Log;
26
+ components?: LaneComponent[];
27
+ hash: string;
28
+ schema?: string;
29
+ readmeComponent?: LaneReadmeComponent;
30
+ forkedFrom?: LaneId;
31
+ updateDependents?: ComponentID[];
32
+ overrideUpdateDependents?: boolean;
33
+ };
34
+
35
+ const OLD_LANE_SCHEMA = '0.0.0';
36
+ const SCHEMA_INCLUDING_DELETED_COMPONENTS_DATA = '1.0.0';
37
+ const CURRENT_LANE_SCHEMA = SCHEMA_INCLUDING_DELETED_COMPONENTS_DATA;
38
+
39
+ export type LaneComponent = { id: ComponentID; head: Ref; isDeleted?: boolean };
40
+ export type LaneReadmeComponent = { id: ComponentID; head: Ref | null };
41
+ export default class Lane extends BitObject {
42
+ name: string;
43
+ scope: string;
44
+ components: LaneComponent[];
45
+ log: Log;
46
+ schema: string;
47
+ readmeComponent?: LaneReadmeComponent;
48
+ forkedFrom?: LaneId;
49
+ _hash: string; // reason for the underscore prefix is that we already have hash as a method
50
+ isNew = false; // doesn't get saved in the object. only needed for in-memory instance
51
+ hasChanged = false; // doesn't get saved in the object. only needed for in-memory instance
52
+ /**
53
+ * components that are dependents of components in the lane and need to be re-snapped against the
54
+ * lane's heads. populated by the bare-scope cascade producer (UI "snap updates" / `bit _snap`)
55
+ * and by `bit _merge-lane`. these entries are part of the lane's graph (Ripple CI builds them,
56
+ * merge engine refreshes them) but are intentionally hidden from workspace-facing flows
57
+ * (`bit status`, `bit compile`, `bit install`, the bitmap).
58
+ */
59
+ updateDependents?: ComponentID[];
60
+ private overrideUpdateDependents?: boolean;
61
+ constructor(props: LaneProps) {
62
+ super();
63
+ if (!props.name) throw new TypeError('Lane constructor expects to get a name parameter');
64
+ this.name = props.name;
65
+ this.scope = props.scope;
66
+ this.components = props.components || [];
67
+ this.log = props.log || { date: Date.now().toString() };
68
+ this._hash = props.hash;
69
+ this.readmeComponent = props.readmeComponent;
70
+ this.forkedFrom = props.forkedFrom;
71
+ this.schema = props.schema || OLD_LANE_SCHEMA;
72
+ this.updateDependents = props.updateDependents;
73
+ this.overrideUpdateDependents = props.overrideUpdateDependents;
74
+ }
75
+ id(): string {
76
+ return this.scope + LANE_REMOTE_DELIMITER + this.name;
77
+ }
78
+ hash(): Ref {
79
+ if (!this._hash) {
80
+ throw new Error('hash is missing from a Lane object');
81
+ }
82
+ return new Ref(this._hash);
83
+ }
84
+ changeName(name: string) {
85
+ this.name = name;
86
+ this.hasChanged = true;
87
+ }
88
+ changeScope(scope: string) {
89
+ this.scope = scope;
90
+ this.hasChanged = true;
91
+ }
92
+ refs(): Ref[] {
93
+ return this.components.map((c) => c.head);
94
+ }
95
+ validateBeforePersisting(str: string) {
96
+ logger.debug(`validating lane object: ${this.hash().toString()} ${this.id()}`);
97
+ const lane = Lane.parse(str, this.hash().toString());
98
+ lane.validate();
99
+ }
100
+ toObject() {
101
+ const obj = pickBy(
102
+ {
103
+ name: this.name,
104
+ scope: this.scope,
105
+ components: this.components.map((component) => ({
106
+ id: { scope: component.id.scope, name: component.id.fullName },
107
+ head: component.head.toString(),
108
+ ...(component.isDeleted && { isDeleted: component.isDeleted }),
109
+ })),
110
+ log: this.log,
111
+ readmeComponent: this.readmeComponent && {
112
+ id: { scope: this.readmeComponent.id.scope, name: this.readmeComponent.id.fullName },
113
+ head: this.readmeComponent.head?.toString() ?? null,
114
+ },
115
+ forkedFrom: this.forkedFrom && this.forkedFrom.toObject(),
116
+ schema: this.schema,
117
+ updateDependents: this.updateDependents?.map((c) => c.toString()),
118
+ overrideUpdateDependents: this.overrideUpdateDependents,
119
+ },
120
+ (val) => !!val
121
+ );
122
+ return obj;
123
+ }
124
+ static from(props: LaneProps): Lane {
125
+ return new Lane(props);
126
+ }
127
+ static create(
128
+ name: string,
129
+ scope: string,
130
+ forkedFrom?: LaneId,
131
+ bitCloudUser?: {
132
+ username?: string;
133
+ email?: string;
134
+ profileImage?: string;
135
+ }
136
+ ) {
137
+ const log = {
138
+ date: Date.now().toString(),
139
+ username: bitCloudUser?.username || getConfig(CFG_USER_NAME_KEY),
140
+ email: bitCloudUser?.email || getConfig(CFG_USER_EMAIL_KEY),
141
+ profileImage: bitCloudUser?.profileImage,
142
+ };
143
+ const lane = new Lane({ name, scope, hash: sha1(v4()), log, forkedFrom, schema: CURRENT_LANE_SCHEMA });
144
+ lane.isNew = true;
145
+ lane.hasChanged = true;
146
+ return lane;
147
+ }
148
+ static parse(contents: string, hash: string): Lane {
149
+ const laneObject = JSON.parse(contents);
150
+ return Lane.from({
151
+ name: laneObject.name,
152
+ scope: laneObject.scope,
153
+ log: laneObject.log,
154
+ components: laneObject.components.map((component) => ({
155
+ id: ComponentID.fromObject({ scope: component.id.scope, name: component.id.name }),
156
+ head: new Ref(component.head),
157
+ isDeleted: component.isDeleted,
158
+ })),
159
+ readmeComponent: laneObject.readmeComponent && {
160
+ id: ComponentID.fromObject({
161
+ scope: laneObject.readmeComponent.id.scope,
162
+ name: laneObject.readmeComponent.id.name,
163
+ }),
164
+ head: laneObject.readmeComponent.head && new Ref(laneObject.readmeComponent.head),
165
+ },
166
+ forkedFrom: laneObject.forkedFrom && LaneId.from(laneObject.forkedFrom.name, laneObject.forkedFrom.scope),
167
+ updateDependents: laneObject.updateDependents?.map((c) => ComponentID.fromString(c)),
168
+ overrideUpdateDependents: laneObject.overrideUpdateDependents,
169
+ hash: laneObject.hash || hash,
170
+ schema: laneObject.schema,
171
+ });
172
+ }
173
+ toBuffer(pretty?: boolean) {
174
+ const args = getStringifyArgs(pretty);
175
+ const obj = this.toObject();
176
+ const str = JSON.stringify(obj, ...args);
177
+ if (this.validateBeforePersist) this.validateBeforePersisting(str);
178
+ return Buffer.from(str);
179
+ }
180
+ addComponent(component: LaneComponent) {
181
+ const existsComponent = this.getComponent(component.id);
182
+ if (existsComponent) {
183
+ if (
184
+ !existsComponent.head.isEqual(component.head) ||
185
+ Boolean(existsComponent.isDeleted) !== Boolean(component.isDeleted)
186
+ ) {
187
+ this.hasChanged = true;
188
+ }
189
+ existsComponent.id = component.id;
190
+ existsComponent.head = component.head;
191
+ existsComponent.isDeleted = component.isDeleted;
192
+ } else {
193
+ logger.debug(`Lane.addComponent, adding component ${component.id.toString()} to lane ${this.id()}`);
194
+ this.components.push(component);
195
+ this.hasChanged = true;
196
+ }
197
+ }
198
+ findUpdateDependent(componentId: ComponentID): ComponentID | undefined {
199
+ return this.updateDependents?.find((c) => c.isEqualWithoutVersion(componentId));
200
+ }
201
+ getUpdateDependentAsLaneComponent(componentId: ComponentID): LaneComponent | undefined {
202
+ const found = this.findUpdateDependent(componentId);
203
+ if (!found?.version) return undefined;
204
+ return { id: found.changeVersion(undefined), head: Ref.from(found.version) };
205
+ }
206
+ removeComponentFromUpdateDependentsIfExist(componentId: ComponentID) {
207
+ if (!this.findUpdateDependent(componentId)) return;
208
+ this.updateDependents = this.updateDependents?.filter((c) => !c.isEqualWithoutVersion(componentId));
209
+ if (!this.updateDependents?.length) this.updateDependents = undefined;
210
+ this.hasChanged = true;
211
+ }
212
+ addComponentToUpdateDependents(componentId: ComponentID) {
213
+ if (!componentId.hasVersion()) {
214
+ throw new ValidationError(`Lane.addComponentToUpdateDependents: ${componentId.toString()} is missing a version`);
215
+ }
216
+ this.removeComponentFromUpdateDependentsIfExist(componentId);
217
+ (this.updateDependents ||= []).push(componentId);
218
+ this.hasChanged = true;
219
+ }
220
+ removeAllUpdateDependents() {
221
+ if (!this.updateDependents?.length) return;
222
+ this.updateDependents = undefined;
223
+ this.hasChanged = true;
224
+ }
225
+ /**
226
+ * Signals an explicit `_snap --update-dependents` insertion. On export, the receiving scope
227
+ * gates its "add this new hidden entry" branch on this flag so a stale workspace lane can't
228
+ * resurrect entries the Cloud UI dropped. Workspace cascades — which only update existing
229
+ * entries — deliberately do NOT set the flag. The field is also serialized by `Lane.toObject`
230
+ * for older remotes that gate their pre-diverge-check override branch on it.
231
+ */
232
+ setOverrideUpdateDependents(overrideUpdateDependents: boolean) {
233
+ this.overrideUpdateDependents = overrideUpdateDependents;
234
+ this.hasChanged = true;
235
+ }
236
+
237
+ shouldOverrideUpdateDependents(): boolean {
238
+ return Boolean(this.overrideUpdateDependents);
239
+ }
240
+
241
+ removeComponent(id: ComponentID): boolean {
242
+ const existsComponent = this.getComponent(id);
243
+ if (!existsComponent) return false;
244
+ this.components = this.components.filter((c) => !c.id.isEqualWithoutVersion(id));
245
+ this.hasChanged = true;
246
+ return true;
247
+ }
248
+ getComponent(id: ComponentID): LaneComponent | undefined {
249
+ return this.components.find((c) => c.id.isEqualWithoutVersion(id));
250
+ }
251
+ getComponentHead(bitId: ComponentID): Ref | null {
252
+ const found = this.components.find((c) => c.id.isEqual(bitId));
253
+ if (found) return found.head;
254
+ return null;
255
+ }
256
+ setLaneComponents(laneComponents: LaneComponent[]) {
257
+ // this gets called when adding lane-components from other lanes/remotes, so it's better to
258
+ // clone the objects to not change the original data.
259
+ this.components = laneComponents.map((c) => ({
260
+ id: c.id.clone(),
261
+ head: c.head.clone(),
262
+ ...(c.isDeleted && { isDeleted: c.isDeleted }),
263
+ }));
264
+ this.hasChanged = true;
265
+ }
266
+ setReadmeComponent(id?: ComponentID) {
267
+ const previousReadme = this.readmeComponent;
268
+ if (!id) {
269
+ this.readmeComponent = undefined;
270
+ if (previousReadme) this.hasChanged = true;
271
+ return;
272
+ }
273
+ const readmeComponent = this.getComponent(id);
274
+ if (!readmeComponent) {
275
+ this.readmeComponent = { id, head: null };
276
+ } else {
277
+ this.readmeComponent = readmeComponent;
278
+ }
279
+ if (
280
+ !previousReadme ||
281
+ !previousReadme.id.isEqual(id) ||
282
+ previousReadme.head?.toString() !== this.readmeComponent.head?.toString()
283
+ ) {
284
+ this.hasChanged = true;
285
+ }
286
+ }
287
+
288
+ async isFullyMerged(scope: Scope): Promise<boolean> {
289
+ const { unmerged } = await this.getMergedAndUnmergedIds(scope);
290
+ return unmerged.length === 0;
291
+ }
292
+ async getMergedAndUnmergedIds(scope: Scope): Promise<{ merged: ComponentID[]; unmerged: ComponentID[] }> {
293
+ const merged: ComponentID[] = [];
294
+ const unmerged: ComponentID[] = [];
295
+ await Promise.all(
296
+ this.components.map(async (component) => {
297
+ const modelComponent = await scope.getModelComponentIfExist(component.id);
298
+ if (!modelComponent) {
299
+ unmerged.push(component.id);
300
+ return;
301
+ }
302
+ const startTraverseFrom = modelComponent.getHead() || null; // it's important to have it as null and not as undefined, see hasVersionByRef
303
+ const headExist = await hasVersionByRef(modelComponent, component.head, scope.objects, startTraverseFrom);
304
+ if (headExist) merged.push(component.id);
305
+ else unmerged.push(component.id);
306
+ })
307
+ );
308
+ return { merged, unmerged };
309
+ }
310
+ /**
311
+ * @deprecated use toComponentIds instead
312
+ */
313
+ toBitIds(): ComponentIdList {
314
+ return this.toComponentIds();
315
+ }
316
+ toComponentIds(): ComponentIdList {
317
+ return ComponentIdList.fromArray(this.components.map((c) => c.id.changeVersion(c.head.toString())));
318
+ }
319
+ toComponentIdsIncludeUpdateDependents(): ComponentIdList {
320
+ return ComponentIdList.fromArray(this.toComponentIds().concat(this.updateDependents || []));
321
+ }
322
+ toLaneId() {
323
+ return new LaneId({ scope: this.scope, name: this.name });
324
+ }
325
+ collectObjectsById(repo: Repository): Promise<Array<{ id: ComponentID; objects: BitObject[] }>> {
326
+ return Promise.all(
327
+ this.components.map(async (component) => {
328
+ const headVersion = (await component.head.load(repo)) as Version;
329
+ const objects = [headVersion, ...headVersion.collect(repo)];
330
+ return { id: component.id, objects };
331
+ })
332
+ );
333
+ }
334
+ includeDeletedData(): boolean {
335
+ return gte(this.schema, SCHEMA_INCLUDING_DELETED_COMPONENTS_DATA);
336
+ }
337
+ setSchemaToSupportDeletedData() {
338
+ this.schema = SCHEMA_INCLUDING_DELETED_COMPONENTS_DATA;
339
+ this.hasChanged = true;
340
+ }
341
+ setSchemaToNotSupportDeletedData() {
342
+ this.schema = OLD_LANE_SCHEMA;
343
+ this.hasChanged = true;
344
+ }
345
+ getCompHeadIncludeUpdateDependents(componentId: ComponentID): Ref | undefined {
346
+ const comp = this.getComponent(componentId);
347
+ if (comp) return comp.head;
348
+ return this.getUpdateDependentAsLaneComponent(componentId)?.head;
349
+ }
350
+ validate() {
351
+ const message = `unable to save Lane object "${this.id()}"`;
352
+ const bitIds = this.toComponentIds();
353
+ this.components.forEach((component) => {
354
+ if (bitIds.filterWithoutVersion(component.id).length > 1) {
355
+ throw new ValidationError(`${message}, the following component is duplicated "${component.id.fullName}"`);
356
+ }
357
+ if (!isSnap(component.head.hash)) {
358
+ throw new ValidationError(
359
+ `${message}, lane component ${component.id.toStringWithoutVersion()} head should be a hash, got ${
360
+ component.head.hash
361
+ }`
362
+ );
363
+ }
364
+ });
365
+ if (this.name === DEFAULT_LANE) {
366
+ throw new BitError(`${message}, this name is reserved as the default lane`);
367
+ }
368
+ if (this.name === PREVIOUS_DEFAULT_LANE) {
369
+ throw new BitError(`${message}, this name is reserved as the old default lane`);
370
+ }
371
+ }
372
+ isEqual(lane: Lane): boolean {
373
+ if (this.id() !== lane.id()) return false;
374
+ // include isDeleted in equality so a soft-delete flip with the same head still counts as a
375
+ // state change — `Lane.isEqual` is used by importers to decide whether to write a
376
+ // LaneHistory entry.
377
+ const normalize = (l: Lane) =>
378
+ l.components
379
+ .map((c) => ({
380
+ id: c.id.toStringWithoutVersion(),
381
+ head: c.head.toString(),
382
+ isDeleted: Boolean(c.isDeleted),
383
+ }))
384
+ .sort((a, b) =>
385
+ `${a.id}@${a.head}:${a.isDeleted ? 1 : 0}`.localeCompare(`${b.id}@${b.head}:${b.isDeleted ? 1 : 0}`)
386
+ );
387
+ const thisUpdDeps = (this.updateDependents || []).map((c) => c.toString()).sort();
388
+ const otherUpdDeps = (lane.updateDependents || []).map((c) => c.toString()).sort();
389
+ return isEqual(normalize(this), normalize(lane)) && isEqual(thisUpdDeps, otherUpdDeps);
390
+ }
391
+ clone() {
392
+ return new Lane({
393
+ ...this,
394
+ hash: this._hash,
395
+ overrideUpdateDependents: this.overrideUpdateDependents,
396
+ components: cloneDeep(this.components),
397
+ updateDependents: this.updateDependents ? cloneDeep(this.updateDependents) : undefined,
398
+ });
399
+ }
400
+ }
@@ -0,0 +1,55 @@
1
+ import { expect } from 'chai';
2
+
3
+ import ModelComponent from './model-component';
4
+ import { clone } from 'lodash';
5
+
6
+ const modelComponentFixture = {
7
+ name: 'bar/foo',
8
+ scope: 'remote-scope',
9
+ versions: {
10
+ '0.0.1': '125a37bdb17220bdc1406a9a28a3dde4eec91225',
11
+ },
12
+ lang: 'javascript',
13
+ deprecated: false,
14
+ bindingPrefix: '@bit',
15
+ remotes: [
16
+ {
17
+ url: 'file:///tmp/remote-scope',
18
+ name: 'remote-scope',
19
+ date: '1572532837438',
20
+ },
21
+ ],
22
+ state: {
23
+ versions: {
24
+ '0.0.1': {
25
+ local: true,
26
+ },
27
+ },
28
+ },
29
+ };
30
+
31
+ const getModelComponentFixture = (): typeof modelComponentFixture => {
32
+ return clone(modelComponentFixture);
33
+ };
34
+
35
+ const getModelComponent = (obj: object): ModelComponent => {
36
+ return ModelComponent.parse(JSON.stringify(obj));
37
+ };
38
+
39
+ describe('ModelComponent', () => {
40
+ describe('validate', () => {
41
+ let validateFunc: Function;
42
+ describe('duplicate hashes', () => {
43
+ let modelComponent: ModelComponent;
44
+ before(() => {
45
+ const fixture = getModelComponentFixture();
46
+ fixture.versions['0.0.2'] = fixture.versions['0.0.1'];
47
+ modelComponent = getModelComponent(fixture);
48
+ validateFunc = () => modelComponent.validate();
49
+ });
50
+ it('should throw an error', () => {
51
+ expect(validateFunc).to.throw('the following hash(es) are duplicated');
52
+ });
53
+ });
54
+ });
55
+ });