@teambit/objects 0.0.702 → 0.0.704

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.
@@ -28,6 +28,13 @@ function _path() {
28
28
  };
29
29
  return data;
30
30
  }
31
+ function _stream() {
32
+ const data = require("stream");
33
+ _stream = function () {
34
+ return data;
35
+ };
36
+ return data;
37
+ }
31
38
  function _zlib() {
32
39
  const data = _interopRequireDefault(require("zlib"));
33
40
  _zlib = function () {
@@ -49,6 +56,13 @@ function _models() {
49
56
  };
50
57
  return data;
51
58
  }
59
+ function _objectList() {
60
+ const data = require("./object-list");
61
+ _objectList = function () {
62
+ return data;
63
+ };
64
+ return data;
65
+ }
52
66
  function _ref() {
53
67
  const data = _interopRequireDefault(require("./ref"));
54
68
  _ref = function () {
@@ -156,4 +170,105 @@ describe('Repository.list', () => {
156
170
  });
157
171
  });
158
172
 
173
+ /**
174
+ * the export flow moves every object through these methods, so for components with thousands of files, the number of
175
+ * fs operations and compressions per object is what makes an export fast or slow.
176
+ */
177
+ describe('Repository writing objects for export', () => {
178
+ let scopePath;
179
+ let repository;
180
+ beforeEach(async () => {
181
+ scopePath = await _fsExtra().default.mkdtemp(_path().default.join(_os().default.tmpdir(), 'bit-repository-spec-'));
182
+ const scopeJson = new (_legacy().ScopeJson)({
183
+ name: SCOPE_NAME,
184
+ version: '1.0.0',
185
+ groupName: null
186
+ }, _path().default.join(scopePath, 'scope.json'));
187
+ repository = await _repository().default.create({
188
+ scopePath,
189
+ scopeJson
190
+ });
191
+ });
192
+ afterEach(async () => {
193
+ await _fsExtra().default.remove(scopePath);
194
+ });
195
+ const createObjectList = async count => {
196
+ const sources = Array.from({
197
+ length: count
198
+ }, (_, i) => _models().Source.from(Buffer.from(`file ${i}`)));
199
+ return _objectList().ObjectList.fromBitObjects(sources);
200
+ };
201
+ it('should write a file per pending object and read them back', async () => {
202
+ const objectList = await createObjectList(SOURCES_COUNT);
203
+ const pendingDir = _path().default.join(scopePath, 'pending-objects', 'client-1');
204
+ await repository.writeObjectsToPendingDir(objectList, pendingDir);
205
+ for (const objectItem of objectList.objects) {
206
+ const hash = objectItem.ref.toString();
207
+ const written = await _fsExtra().default.readFile(_path().default.join(pendingDir, hash.slice(0, 2), hash.slice(2)));
208
+ (0, _chai().expect)(written.equals(objectItem.buffer)).to.be.true;
209
+ }
210
+ const loaded = await repository.readObjectsFromPendingDir(pendingDir);
211
+ (0, _chai().expect)(loaded.objects.map(o => o.ref.toString()).sort()).to.deep.equal(objectList.objects.map(o => o.ref.toString()).sort());
212
+ });
213
+ it('should write the given raw buffers of sources without re-compressing them, and leave no tmp files', async () => {
214
+ const objectList = await createObjectList(SOURCES_COUNT);
215
+ const {
216
+ bitObjectList,
217
+ rawSources
218
+ } = await objectList.toBitObjectsWithRawSources();
219
+ (0, _chai().expect)(rawSources.size).to.equal(SOURCES_COUNT);
220
+ bitObjectList.getAll().forEach(obj => {
221
+ obj.compressWithSize = async () => {
222
+ throw new Error('should not re-compress');
223
+ };
224
+ });
225
+ await repository.writeObjectsToTheFS(bitObjectList.getAll(), rawSources);
226
+ for (const objectItem of objectList.objects) {
227
+ const written = await _fsExtra().default.readFile(repository.objectPath(objectItem.ref));
228
+ (0, _chai().expect)(written.equals(objectItem.buffer)).to.be.true;
229
+ }
230
+ const objectDirs = await _fsExtra().default.readdir(repository.getPath());
231
+ const allFiles = (await Promise.all(objectDirs.map(dir => _fsExtra().default.readdir(_path().default.join(repository.getPath(), dir))))).flat();
232
+ (0, _chai().expect)(allFiles).to.have.lengthOf(SOURCES_COUNT);
233
+ (0, _chai().expect)(allFiles.filter(fileName => fileName.startsWith('.'))).to.have.lengthOf(0);
234
+ });
235
+ it('should not use a raw buffer of a source whose content does not match its ref', async () => {
236
+ const [first, second] = (await createObjectList(2)).objects;
237
+ const tampered = new (_objectList().ObjectList)([{
238
+ ref: first.ref,
239
+ buffer: second.buffer
240
+ }]);
241
+ const {
242
+ rawSources
243
+ } = await tampered.toBitObjectsWithRawSources();
244
+ (0, _chai().expect)(rawSources.size).to.equal(0);
245
+ });
246
+ it('should re-create an object dir that was removed after it was written to', async () => {
247
+ const source = _models().Source.from(Buffer.from('some content'));
248
+ await repository.writeObjectsToTheFS([source]);
249
+ await _fsExtra().default.remove(_path().default.dirname(repository.objectPath(source.hash())));
250
+ await repository.writeObjectsToTheFS([source]);
251
+ (0, _chai().expect)(await _fsExtra().default.pathExists(repository.objectPath(source.hash()))).to.be.true;
252
+ });
253
+ it('should keep the mode of an existing object file it overwrites, as write-file-atomic does', async () => {
254
+ const source = _models().Source.from(Buffer.from('some content'));
255
+ await repository.writeObjectsToTheFS([source]);
256
+ const objectPath = repository.objectPath(source.hash());
257
+ await _fsExtra().default.chmod(objectPath, 0o640);
258
+ await repository.writeObjectsToTheFS([source]);
259
+ (0, _chai().expect)((await _fsExtra().default.stat(objectPath)).mode & 0o777).to.equal(0o640);
260
+ });
261
+ it('should reject reading a truncated tar of objects rather than hang', async () => {
262
+ const objectList = await createObjectList(SOURCES_COUNT);
263
+ const chunks = [];
264
+ for await (const chunk of objectList.toTar()) chunks.push(chunk);
265
+ const truncated = Buffer.concat(chunks).subarray(0, 700);
266
+ let error;
267
+ await _objectList().ObjectList.fromTar(_stream().Readable.from([truncated])).catch(err => {
268
+ error = err;
269
+ });
270
+ (0, _chai().expect)(error).to.be.instanceOf(Error);
271
+ });
272
+ });
273
+
159
274
  //# sourceMappingURL=repository.spec.js.map
@@ -1 +1 @@
1
- {"version":3,"names":["_chai","data","require","_fsExtra","_interopRequireDefault","_os","_path","_zlib","_legacy","_models","_ref","_repository","_scopeIndex","e","__esModule","default","SCOPE_NAME","SOURCES_COUNT","describe","scopePath","scopeJson","repository","contentReads","original","onPostObjectRead","Repository","hasPostObjectReadTransformer","beforeEach","fs","mkdtemp","path","join","os","tmpdir","ScopeJson","name","version","groupName","create","modelComponent","ModelComponent","from","scope","lang","deprecated","bindingPrefix","versions","sources","Array","length","_","i","Source","Buffer","objects","forEach","object","validateBeforePersist","writeObjectsToTheFS","content","afterEach","remove","it","reset","loaded","load","expect","scopeIndex","getHashes","IndexType","components","to","have","lengthOf","equal","list","hash","repeat","unknownObject","zlib","deflateSync","objectPath","Ref","outputFile","be","instanceOf"],"sources":["repository.spec.ts"],"sourcesContent":["import { expect } from 'chai';\nimport fs from 'fs-extra';\nimport os from 'os';\nimport path from 'path';\nimport zlib from 'zlib';\nimport { ScopeJson } from '@teambit/legacy.scope';\nimport { ModelComponent, Source } from '../models';\nimport Ref from './ref';\nimport Repository from './repository';\nimport { IndexType } from './scope-index';\n\nconst SCOPE_NAME = 'my-scope';\nconst SOURCES_COUNT = 5;\n\n/**\n * `Repository.list(types)` is what rebuilds a missing `index.json`, and it's asked for a handful of\n * object types in a scope that is mostly file contents. the type is in each file's header, so the\n * rest of a file should only be read when its type is one that was asked for.\n */\ndescribe('Repository.list', () => {\n let scopePath: string;\n let scopeJson: ScopeJson;\n let repository: Repository;\n let contentReads: number;\n const original = {\n onPostObjectRead: Repository.onPostObjectRead,\n hasPostObjectReadTransformer: Repository.hasPostObjectReadTransformer,\n };\n\n beforeEach(async () => {\n scopePath = await fs.mkdtemp(path.join(os.tmpdir(), 'bit-repository-spec-'));\n scopeJson = new ScopeJson(\n { name: SCOPE_NAME, version: '1.0.0', groupName: null },\n path.join(scopePath, 'scope.json')\n );\n repository = await Repository.create({ scopePath, scopeJson });\n const modelComponent = ModelComponent.from({\n name: 'bar/foo',\n scope: SCOPE_NAME,\n lang: 'javascript',\n deprecated: false,\n bindingPrefix: '@bit',\n versions: {},\n });\n const sources = Array.from({ length: SOURCES_COUNT }, (_, i) => Source.from(Buffer.from(`file ${i}`)));\n const objects = [modelComponent, ...sources];\n objects.forEach((object) => {\n object.validateBeforePersist = false;\n });\n await repository.writeObjectsToTheFS(objects);\n\n contentReads = 0;\n // every full read of an object file goes through this hook, while a header read doesn't\n Repository.onPostObjectRead = (content) => {\n contentReads += 1;\n return content;\n };\n });\n\n afterEach(async () => {\n Repository.onPostObjectRead = original.onPostObjectRead;\n Repository.hasPostObjectReadTransformer = original.hasPostObjectReadTransformer;\n await fs.remove(scopePath);\n });\n\n it('should rebuild a missing index.json reading the contents of the wanted objects only', async () => {\n Repository.hasPostObjectReadTransformer = () => false;\n await Repository.reset(scopePath);\n const loaded = await Repository.load({ scopePath, scopeJson });\n expect(loaded.scopeIndex.getHashes(IndexType.components)).to.have.lengthOf(1);\n expect(contentReads).to.equal(1);\n });\n\n it('should read every object once when a transformer is registered, as the header is transformed too', async () => {\n Repository.hasPostObjectReadTransformer = () => true;\n const objects = await repository.list([ModelComponent]);\n expect(objects).to.have.lengthOf(1);\n expect(contentReads).to.equal(SOURCES_COUNT + 1);\n });\n\n it('should skip an object whose header names a type nothing is registered under, as the full read does', async () => {\n Repository.hasPostObjectReadTransformer = () => false;\n const hash = 'a'.repeat(40);\n const content = 'whatever';\n const unknownObject = zlib.deflateSync(Buffer.from(`NotARealType ${hash} ${content.length}\\0${content}`));\n const objectPath = repository.objectPath(Ref.from(hash));\n await fs.outputFile(objectPath, unknownObject);\n const objects = await repository.list([ModelComponent]);\n expect(objects).to.have.lengthOf(1);\n expect(objects[0]).to.be.instanceOf(ModelComponent);\n });\n});\n"],"mappings":"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","ignoreList":[]}
1
+ {"version":3,"names":["_chai","data","require","_fsExtra","_interopRequireDefault","_os","_path","_stream","_zlib","_legacy","_models","_objectList","_ref","_repository","_scopeIndex","e","__esModule","default","SCOPE_NAME","SOURCES_COUNT","describe","scopePath","scopeJson","repository","contentReads","original","onPostObjectRead","Repository","hasPostObjectReadTransformer","beforeEach","fs","mkdtemp","path","join","os","tmpdir","ScopeJson","name","version","groupName","create","modelComponent","ModelComponent","from","scope","lang","deprecated","bindingPrefix","versions","sources","Array","length","_","i","Source","Buffer","objects","forEach","object","validateBeforePersist","writeObjectsToTheFS","content","afterEach","remove","it","reset","loaded","load","expect","scopeIndex","getHashes","IndexType","components","to","have","lengthOf","equal","list","hash","repeat","unknownObject","zlib","deflateSync","objectPath","Ref","outputFile","be","instanceOf","createObjectList","count","ObjectList","fromBitObjects","objectList","pendingDir","writeObjectsToPendingDir","objectItem","ref","toString","written","readFile","slice","equals","buffer","true","readObjectsFromPendingDir","map","o","sort","deep","bitObjectList","rawSources","toBitObjectsWithRawSources","size","getAll","obj","compressWithSize","Error","objectDirs","readdir","getPath","allFiles","Promise","all","dir","flat","filter","fileName","startsWith","first","second","tampered","source","dirname","pathExists","chmod","stat","mode","chunks","chunk","toTar","push","truncated","concat","subarray","error","fromTar","Readable","catch","err"],"sources":["repository.spec.ts"],"sourcesContent":["import { expect } from 'chai';\nimport fs from 'fs-extra';\nimport os from 'os';\nimport path from 'path';\nimport { Readable } from 'stream';\nimport zlib from 'zlib';\nimport { ScopeJson } from '@teambit/legacy.scope';\nimport { ModelComponent, Source } from '../models';\nimport { ObjectList } from './object-list';\nimport Ref from './ref';\nimport Repository from './repository';\nimport { IndexType } from './scope-index';\n\nconst SCOPE_NAME = 'my-scope';\nconst SOURCES_COUNT = 5;\n\n/**\n * `Repository.list(types)` is what rebuilds a missing `index.json`, and it's asked for a handful of\n * object types in a scope that is mostly file contents. the type is in each file's header, so the\n * rest of a file should only be read when its type is one that was asked for.\n */\ndescribe('Repository.list', () => {\n let scopePath: string;\n let scopeJson: ScopeJson;\n let repository: Repository;\n let contentReads: number;\n const original = {\n onPostObjectRead: Repository.onPostObjectRead,\n hasPostObjectReadTransformer: Repository.hasPostObjectReadTransformer,\n };\n\n beforeEach(async () => {\n scopePath = await fs.mkdtemp(path.join(os.tmpdir(), 'bit-repository-spec-'));\n scopeJson = new ScopeJson(\n { name: SCOPE_NAME, version: '1.0.0', groupName: null },\n path.join(scopePath, 'scope.json')\n );\n repository = await Repository.create({ scopePath, scopeJson });\n const modelComponent = ModelComponent.from({\n name: 'bar/foo',\n scope: SCOPE_NAME,\n lang: 'javascript',\n deprecated: false,\n bindingPrefix: '@bit',\n versions: {},\n });\n const sources = Array.from({ length: SOURCES_COUNT }, (_, i) => Source.from(Buffer.from(`file ${i}`)));\n const objects = [modelComponent, ...sources];\n objects.forEach((object) => {\n object.validateBeforePersist = false;\n });\n await repository.writeObjectsToTheFS(objects);\n\n contentReads = 0;\n // every full read of an object file goes through this hook, while a header read doesn't\n Repository.onPostObjectRead = (content) => {\n contentReads += 1;\n return content;\n };\n });\n\n afterEach(async () => {\n Repository.onPostObjectRead = original.onPostObjectRead;\n Repository.hasPostObjectReadTransformer = original.hasPostObjectReadTransformer;\n await fs.remove(scopePath);\n });\n\n it('should rebuild a missing index.json reading the contents of the wanted objects only', async () => {\n Repository.hasPostObjectReadTransformer = () => false;\n await Repository.reset(scopePath);\n const loaded = await Repository.load({ scopePath, scopeJson });\n expect(loaded.scopeIndex.getHashes(IndexType.components)).to.have.lengthOf(1);\n expect(contentReads).to.equal(1);\n });\n\n it('should read every object once when a transformer is registered, as the header is transformed too', async () => {\n Repository.hasPostObjectReadTransformer = () => true;\n const objects = await repository.list([ModelComponent]);\n expect(objects).to.have.lengthOf(1);\n expect(contentReads).to.equal(SOURCES_COUNT + 1);\n });\n\n it('should skip an object whose header names a type nothing is registered under, as the full read does', async () => {\n Repository.hasPostObjectReadTransformer = () => false;\n const hash = 'a'.repeat(40);\n const content = 'whatever';\n const unknownObject = zlib.deflateSync(Buffer.from(`NotARealType ${hash} ${content.length}\\0${content}`));\n const objectPath = repository.objectPath(Ref.from(hash));\n await fs.outputFile(objectPath, unknownObject);\n const objects = await repository.list([ModelComponent]);\n expect(objects).to.have.lengthOf(1);\n expect(objects[0]).to.be.instanceOf(ModelComponent);\n });\n});\n\n/**\n * the export flow moves every object through these methods, so for components with thousands of files, the number of\n * fs operations and compressions per object is what makes an export fast or slow.\n */\ndescribe('Repository writing objects for export', () => {\n let scopePath: string;\n let repository: Repository;\n\n beforeEach(async () => {\n scopePath = await fs.mkdtemp(path.join(os.tmpdir(), 'bit-repository-spec-'));\n const scopeJson = new ScopeJson(\n { name: SCOPE_NAME, version: '1.0.0', groupName: null },\n path.join(scopePath, 'scope.json')\n );\n repository = await Repository.create({ scopePath, scopeJson });\n });\n\n afterEach(async () => {\n await fs.remove(scopePath);\n });\n\n const createObjectList = async (count: number) => {\n const sources = Array.from({ length: count }, (_, i) => Source.from(Buffer.from(`file ${i}`)));\n return ObjectList.fromBitObjects(sources);\n };\n\n it('should write a file per pending object and read them back', async () => {\n const objectList = await createObjectList(SOURCES_COUNT);\n const pendingDir = path.join(scopePath, 'pending-objects', 'client-1');\n await repository.writeObjectsToPendingDir(objectList, pendingDir);\n for (const objectItem of objectList.objects) {\n const hash = objectItem.ref.toString();\n const written = await fs.readFile(path.join(pendingDir, hash.slice(0, 2), hash.slice(2)));\n expect(written.equals(objectItem.buffer)).to.be.true;\n }\n const loaded = await repository.readObjectsFromPendingDir(pendingDir);\n expect(loaded.objects.map((o) => o.ref.toString()).sort()).to.deep.equal(\n objectList.objects.map((o) => o.ref.toString()).sort()\n );\n });\n\n it('should write the given raw buffers of sources without re-compressing them, and leave no tmp files', async () => {\n const objectList = await createObjectList(SOURCES_COUNT);\n const { bitObjectList, rawSources } = await objectList.toBitObjectsWithRawSources();\n expect(rawSources.size).to.equal(SOURCES_COUNT);\n bitObjectList.getAll().forEach((obj) => {\n obj.compressWithSize = async () => {\n throw new Error('should not re-compress');\n };\n });\n await repository.writeObjectsToTheFS(bitObjectList.getAll(), rawSources);\n for (const objectItem of objectList.objects) {\n const written = await fs.readFile(repository.objectPath(objectItem.ref));\n expect(written.equals(objectItem.buffer)).to.be.true;\n }\n const objectDirs = await fs.readdir(repository.getPath());\n const allFiles = (\n await Promise.all(objectDirs.map((dir) => fs.readdir(path.join(repository.getPath(), dir))))\n ).flat();\n expect(allFiles).to.have.lengthOf(SOURCES_COUNT);\n expect(allFiles.filter((fileName) => fileName.startsWith('.'))).to.have.lengthOf(0);\n });\n\n it('should not use a raw buffer of a source whose content does not match its ref', async () => {\n const [first, second] = (await createObjectList(2)).objects;\n const tampered = new ObjectList([{ ref: first.ref, buffer: second.buffer }]);\n const { rawSources } = await tampered.toBitObjectsWithRawSources();\n expect(rawSources.size).to.equal(0);\n });\n\n it('should re-create an object dir that was removed after it was written to', async () => {\n const source = Source.from(Buffer.from('some content'));\n await repository.writeObjectsToTheFS([source]);\n await fs.remove(path.dirname(repository.objectPath(source.hash())));\n await repository.writeObjectsToTheFS([source]);\n expect(await fs.pathExists(repository.objectPath(source.hash()))).to.be.true;\n });\n\n it('should keep the mode of an existing object file it overwrites, as write-file-atomic does', async () => {\n const source = Source.from(Buffer.from('some content'));\n await repository.writeObjectsToTheFS([source]);\n const objectPath = repository.objectPath(source.hash());\n await fs.chmod(objectPath, 0o640);\n await repository.writeObjectsToTheFS([source]);\n expect((await fs.stat(objectPath)).mode & 0o777).to.equal(0o640);\n });\n\n it('should reject reading a truncated tar of objects rather than hang', async () => {\n const objectList = await createObjectList(SOURCES_COUNT);\n const chunks: Buffer[] = [];\n for await (const chunk of objectList.toTar() as AsyncIterable<Buffer>) chunks.push(chunk);\n const truncated = Buffer.concat(chunks).subarray(0, 700);\n let error: Error | undefined;\n await ObjectList.fromTar(Readable.from([truncated])).catch((err) => {\n error = err;\n });\n expect(error).to.be.instanceOf(Error);\n });\n});\n"],"mappings":"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","ignoreList":[]}
@@ -4,12 +4,12 @@ import type { ComponentID } from '@teambit/component-id';
4
4
  import type { ObjectList } from './object-list';
5
5
  import type { BitObjectList } from './bit-object-list';
6
6
  import { getAllVersionsInfo } from '@teambit/component.snap-distance';
7
- import { Dependency } from '@teambit/graph';
8
7
 
9
8
  type BitIdNode = Node<ComponentID>;
10
- type DependencyEdge = Edge<Dependency>;
9
+ type DependencyType = 'runtime' | 'dev' | 'peer';
10
+ type DependencyEdge = Edge<DependencyType>;
11
11
 
12
- export class IdGraph extends Graph<ComponentID, Dependency> {
12
+ export class IdGraph extends Graph<ComponentID, DependencyType> {
13
13
  constructor(nodes: BitIdNode[] = [], edges: DependencyEdge[] = []) {
14
14
  super(nodes, edges);
15
15
  }
@@ -48,17 +48,14 @@ export async function bitObjectListToGraph(bitObjectsList: BitObjectList): Promi
48
48
  }
49
49
  const { dependencies, devDependencies, peerDependencies, extensionDependencies } =
50
50
  versionInfo.version.depsIdsGroupedByType;
51
- const addDep = (depId: ComponentID, edge: Dependency) => {
51
+ const addDep = (depId: ComponentID, edge: DependencyType) => {
52
52
  const depIdStr = depId.toString();
53
53
  nodes.push(new Node(depIdStr, depId));
54
54
  edges.push(new Edge(idStr, depIdStr, edge));
55
55
  };
56
- const runTime = new Dependency('runtime');
57
- const dev = new Dependency('dev');
58
- const peer = new Dependency('peer');
59
- dependencies.forEach((depId) => addDep(depId, runTime));
60
- [...devDependencies, ...extensionDependencies].forEach((depId) => addDep(depId, dev));
61
- peerDependencies.forEach((depId) => addDep(depId, peer));
56
+ dependencies.forEach((depId) => addDep(depId, 'runtime'));
57
+ [...devDependencies, ...extensionDependencies].forEach((depId) => addDep(depId, 'dev'));
58
+ peerDependencies.forEach((depId) => addDep(depId, 'peer'));
62
59
  });
63
60
  })
64
61
  );
@@ -46,6 +46,16 @@ export const FETCH_FORMAT_OBJECT_LIST = 'ObjectList';
46
46
  */
47
47
  export type ObjectItemsStream = Readable;
48
48
 
49
+ /**
50
+ * compressed buffer of an object (as stored on the filesystem) and the size of its inflated content.
51
+ */
52
+ export type CompressedObject = { buffer: Buffer; inflatedSize: number };
53
+
54
+ /**
55
+ * hash => compressed object
56
+ */
57
+ export type RawObjectsMap = Map<BitObject, CompressedObject & { ref: Ref }>;
58
+
49
59
  export class ObjectList {
50
60
  constructor(public objects: ObjectItem[] = []) {}
51
61
 
@@ -124,7 +134,10 @@ export class ObjectList {
124
134
  resolve(objects);
125
135
  });
126
136
 
127
- packStream.pipe(extract);
137
+ // unlike `pipe`, it rejects when the input stream fails or the extractor fails (e.g. a truncated tar).
138
+ pipeline(packStream, extract, (err) => {
139
+ if (err) reject(err);
140
+ });
128
141
  });
129
142
  return new ObjectList(objectItems);
130
143
  }
@@ -248,23 +261,46 @@ export class ObjectList {
248
261
  }
249
262
 
250
263
  addIfNotExist(objectItems: ObjectItem[]) {
264
+ const existing = new Set(this.objects.map((object) => ObjectList.combineScopeAndHash(object)));
251
265
  objectItems.forEach((objectItem) => {
252
- const exists = this.objects.find(
253
- (object) => object.ref.isEqual(objectItem.ref) && object.scope === objectItem.scope
254
- );
255
- if (!exists) {
256
- this.objects.push(objectItem);
257
- }
266
+ const key = ObjectList.combineScopeAndHash(objectItem);
267
+ if (existing.has(key)) return;
268
+ existing.add(key);
269
+ this.objects.push(objectItem);
258
270
  });
259
271
  }
260
272
 
261
273
  async toBitObjects(throwForUnknownTypes = false): Promise<BitObjectList> {
274
+ const { bitObjectList } = await this.parseObjects(throwForUnknownTypes);
275
+ return bitObjectList;
276
+ }
277
+
278
+ /**
279
+ * same as `toBitObjects`, but also returns the original compressed buffers of the Source objects (files/artifacts)
280
+ * whose content matches their ref. Sources are immutable, so these buffers can be written to the filesystem as is,
281
+ * saving the costly re-compression. (significant for components with thousands of files).
282
+ */
283
+ async toBitObjectsWithRawSources(): Promise<{ bitObjectList: BitObjectList; rawSources: RawObjectsMap }> {
284
+ return this.parseObjects(false, true);
285
+ }
286
+
287
+ private async parseObjects(
288
+ throwForUnknownTypes: boolean,
289
+ collectRawSources = false
290
+ ): Promise<{ bitObjectList: BitObjectList; rawSources: RawObjectsMap }> {
262
291
  const concurrency = concurrentIOLimit();
292
+ const rawSources: RawObjectsMap = new Map();
263
293
  const bitObjects = await pMapPool(
264
294
  this.objects,
265
295
  async (object) => {
266
296
  try {
267
- return await BitObject.parseObject(object.buffer);
297
+ const { object: bitObject, inflatedSize } = await BitObject.parseObjectWithSize(object.buffer);
298
+ if (collectRawSources && bitObject.getType() === 'Source') {
299
+ if (bitObject.hash().isEqual(object.ref)) {
300
+ rawSources.set(bitObject, { buffer: object.buffer, inflatedSize, ref: object.ref });
301
+ }
302
+ }
303
+ return bitObject;
268
304
  } catch (err) {
269
305
  if (throwForUnknownTypes || !(err instanceof UnknownObjectType)) {
270
306
  throw err;
@@ -277,7 +313,7 @@ export class ObjectList {
277
313
  },
278
314
  { concurrency }
279
315
  );
280
- return new BitObjectList(compact(bitObjects));
316
+ return { bitObjectList: new BitObjectList(compact(bitObjects)), rawSources };
281
317
  }
282
318
 
283
319
  static async fromBitObjects(bitObjects: BitObject[]): Promise<ObjectList> {
@@ -302,6 +338,7 @@ export class ObjectList {
302
338
 
303
339
  splitByScopeName(): { [scopeName: string]: ObjectList } {
304
340
  const objectListPerScope: { [scopeName: string]: ObjectList } = {};
341
+ const added = new Set<string>();
305
342
  this.objects.forEach((obj) => {
306
343
  if (obj.type === ExportMetadata.name) {
307
344
  return; // no scope for this type. it's general for all export data from all scopes
@@ -309,11 +346,11 @@ export class ObjectList {
309
346
  if (!obj.scope) {
310
347
  throw new Error(`ObjectList: unable to split by scopeName, the scopeName is missing for ${obj.ref.hash}`);
311
348
  }
312
- if (objectListPerScope[obj.scope]) {
313
- objectListPerScope[obj.scope].addIfNotExist([obj]);
314
- } else {
315
- objectListPerScope[obj.scope] = new ObjectList([obj]);
316
- }
349
+ const key = ObjectList.combineScopeAndHash(obj);
350
+ if (added.has(key)) return;
351
+ added.add(key);
352
+ if (!objectListPerScope[obj.scope]) objectListPerScope[obj.scope] = new ObjectList();
353
+ objectListPerScope[obj.scope].objects.push(obj);
317
354
  });
318
355
  return objectListPerScope;
319
356
  }
@@ -2,9 +2,11 @@ import { expect } from 'chai';
2
2
  import fs from 'fs-extra';
3
3
  import os from 'os';
4
4
  import path from 'path';
5
+ import { Readable } from 'stream';
5
6
  import zlib from 'zlib';
6
7
  import { ScopeJson } from '@teambit/legacy.scope';
7
8
  import { ModelComponent, Source } from '../models';
9
+ import { ObjectList } from './object-list';
8
10
  import Ref from './ref';
9
11
  import Repository from './repository';
10
12
  import { IndexType } from './scope-index';
@@ -90,3 +92,103 @@ describe('Repository.list', () => {
90
92
  expect(objects[0]).to.be.instanceOf(ModelComponent);
91
93
  });
92
94
  });
95
+
96
+ /**
97
+ * the export flow moves every object through these methods, so for components with thousands of files, the number of
98
+ * fs operations and compressions per object is what makes an export fast or slow.
99
+ */
100
+ describe('Repository writing objects for export', () => {
101
+ let scopePath: string;
102
+ let repository: Repository;
103
+
104
+ beforeEach(async () => {
105
+ scopePath = await fs.mkdtemp(path.join(os.tmpdir(), 'bit-repository-spec-'));
106
+ const scopeJson = new ScopeJson(
107
+ { name: SCOPE_NAME, version: '1.0.0', groupName: null },
108
+ path.join(scopePath, 'scope.json')
109
+ );
110
+ repository = await Repository.create({ scopePath, scopeJson });
111
+ });
112
+
113
+ afterEach(async () => {
114
+ await fs.remove(scopePath);
115
+ });
116
+
117
+ const createObjectList = async (count: number) => {
118
+ const sources = Array.from({ length: count }, (_, i) => Source.from(Buffer.from(`file ${i}`)));
119
+ return ObjectList.fromBitObjects(sources);
120
+ };
121
+
122
+ it('should write a file per pending object and read them back', async () => {
123
+ const objectList = await createObjectList(SOURCES_COUNT);
124
+ const pendingDir = path.join(scopePath, 'pending-objects', 'client-1');
125
+ await repository.writeObjectsToPendingDir(objectList, pendingDir);
126
+ for (const objectItem of objectList.objects) {
127
+ const hash = objectItem.ref.toString();
128
+ const written = await fs.readFile(path.join(pendingDir, hash.slice(0, 2), hash.slice(2)));
129
+ expect(written.equals(objectItem.buffer)).to.be.true;
130
+ }
131
+ const loaded = await repository.readObjectsFromPendingDir(pendingDir);
132
+ expect(loaded.objects.map((o) => o.ref.toString()).sort()).to.deep.equal(
133
+ objectList.objects.map((o) => o.ref.toString()).sort()
134
+ );
135
+ });
136
+
137
+ it('should write the given raw buffers of sources without re-compressing them, and leave no tmp files', async () => {
138
+ const objectList = await createObjectList(SOURCES_COUNT);
139
+ const { bitObjectList, rawSources } = await objectList.toBitObjectsWithRawSources();
140
+ expect(rawSources.size).to.equal(SOURCES_COUNT);
141
+ bitObjectList.getAll().forEach((obj) => {
142
+ obj.compressWithSize = async () => {
143
+ throw new Error('should not re-compress');
144
+ };
145
+ });
146
+ await repository.writeObjectsToTheFS(bitObjectList.getAll(), rawSources);
147
+ for (const objectItem of objectList.objects) {
148
+ const written = await fs.readFile(repository.objectPath(objectItem.ref));
149
+ expect(written.equals(objectItem.buffer)).to.be.true;
150
+ }
151
+ const objectDirs = await fs.readdir(repository.getPath());
152
+ const allFiles = (
153
+ await Promise.all(objectDirs.map((dir) => fs.readdir(path.join(repository.getPath(), dir))))
154
+ ).flat();
155
+ expect(allFiles).to.have.lengthOf(SOURCES_COUNT);
156
+ expect(allFiles.filter((fileName) => fileName.startsWith('.'))).to.have.lengthOf(0);
157
+ });
158
+
159
+ it('should not use a raw buffer of a source whose content does not match its ref', async () => {
160
+ const [first, second] = (await createObjectList(2)).objects;
161
+ const tampered = new ObjectList([{ ref: first.ref, buffer: second.buffer }]);
162
+ const { rawSources } = await tampered.toBitObjectsWithRawSources();
163
+ expect(rawSources.size).to.equal(0);
164
+ });
165
+
166
+ it('should re-create an object dir that was removed after it was written to', async () => {
167
+ const source = Source.from(Buffer.from('some content'));
168
+ await repository.writeObjectsToTheFS([source]);
169
+ await fs.remove(path.dirname(repository.objectPath(source.hash())));
170
+ await repository.writeObjectsToTheFS([source]);
171
+ expect(await fs.pathExists(repository.objectPath(source.hash()))).to.be.true;
172
+ });
173
+
174
+ it('should keep the mode of an existing object file it overwrites, as write-file-atomic does', async () => {
175
+ const source = Source.from(Buffer.from('some content'));
176
+ await repository.writeObjectsToTheFS([source]);
177
+ const objectPath = repository.objectPath(source.hash());
178
+ await fs.chmod(objectPath, 0o640);
179
+ await repository.writeObjectsToTheFS([source]);
180
+ expect((await fs.stat(objectPath)).mode & 0o777).to.equal(0o640);
181
+ });
182
+
183
+ it('should reject reading a truncated tar of objects rather than hang', async () => {
184
+ const objectList = await createObjectList(SOURCES_COUNT);
185
+ const chunks: Buffer[] = [];
186
+ for await (const chunk of objectList.toTar() as AsyncIterable<Buffer>) chunks.push(chunk);
187
+ const truncated = Buffer.concat(chunks).subarray(0, 700);
188
+ let error: Error | undefined;
189
+ await ObjectList.fromTar(Readable.from([truncated])).catch((err) => {
190
+ error = err;
191
+ });
192
+ expect(error).to.be.instanceOf(Error);
193
+ });
194
+ });
@@ -5,11 +5,11 @@ import { BitError } from '@teambit/bit-error';
5
5
  import type { ComponentID } from '@teambit/component-id';
6
6
  import { HASH_SIZE, isSnap } from '@teambit/component-version';
7
7
  import * as path from 'path';
8
+ import { userInfo } from 'os';
8
9
  import { pMapPool } from '@teambit/toolbox.promise.map-pool';
9
10
  import { OBJECTS_DIR } from '@teambit/legacy.constants';
10
11
  import { logger } from '@teambit/legacy.logger';
11
- import type { ChownOptions, PathOsBasedAbsolute } from '@teambit/legacy.utils';
12
- import { writeFile } from '@teambit/legacy.utils';
12
+ import type { PathOsBasedAbsolute } from '@teambit/legacy.utils';
13
13
  import { glob } from 'glob';
14
14
  import { removeEmptyDir } from '@teambit/toolbox.fs.remove-empty-dir';
15
15
  import { concurrentIOLimit } from '@teambit/harmony.modules.concurrency';
@@ -24,7 +24,7 @@ import {
24
24
  import type { IndexType, IndexItem } from './scope-index';
25
25
  import { ScopeIndex } from './scope-index';
26
26
  import BitObject from './object';
27
- import type { ObjectItem } from './object-list';
27
+ import type { ObjectItem, RawObjectsMap, CompressedObject } from './object-list';
28
28
  import { ObjectList } from './object-list';
29
29
  import BitRawObject from './raw-object';
30
30
  import Ref from './ref';
@@ -37,6 +37,8 @@ type ContentTransformer = (content: Buffer) => Buffer;
37
37
  const OBJECTS_BACKUP_DIR = `${OBJECTS_DIR}.bak`;
38
38
  const TRASH_DIR = 'trash';
39
39
  const MAX_COMPRESSED_SIZE_TO_CACHE = 100 * 1024; // don't cache big files (mainly artifacts) to prevent out-of-memory
40
+ let objectFileWriteCounter = 0;
41
+ type ObjectChownOptions = { uid: number; gid: number } | null;
40
42
  /**
41
43
  * how much of an object file to read when all that's needed is its header. the header holds the
42
44
  * type and is a few dozen bytes, which 512 compressed bytes always cover - and every byte past it
@@ -790,14 +792,13 @@ export default class Repository {
790
792
  /**
791
793
  * write all objects to the FS and index the components/lanes/symlink objects
792
794
  */
793
- async writeObjectsToTheFS(objects: BitObject[]): Promise<void> {
795
+ async writeObjectsToTheFS(objects: BitObject[], rawObjects?: RawObjectsMap): Promise<void> {
794
796
  const count = objects.length;
795
797
  if (!count) return;
796
798
  logger.trace(`Repository.writeObjectsToTheFS: started writing ${count} objects`);
797
799
  const concurrency = concurrentIOLimit();
798
- await pMapPool(objects, (obj) => this._writeOne(obj), {
799
- concurrency,
800
- });
800
+ const chownOptions = await this.getChownOptions();
801
+ await pMapPool(objects, (obj) => this._writeOne(obj, rawObjects?.get(obj), chownOptions), { concurrency });
801
802
  logger.trace(`Repository.writeObjectsToTheFS: completed writing ${count} objects`);
802
803
 
803
804
  const added = this.scopeIndex.addMany(objects);
@@ -820,16 +821,21 @@ export default class Repository {
820
821
  * this method doesn't write to scopeIndex. so using this method for ModelComponent or
821
822
  * Symlink makes the index outdated.
822
823
  */
823
- async _writeOne(object: BitObject): Promise<void> {
824
- const { buffer: contents, inflatedSize } = await object.compressWithSize();
825
- const options: ChownOptions = {};
826
- if (this.scopeJson.groupName) options.gid = await resolveGroupId(this.scopeJson.groupName);
827
- const hash = object.hash();
824
+ async _writeOne(
825
+ object: BitObject,
826
+ raw?: CompressedObject & { ref: Ref },
827
+ chownOptions?: ObjectChownOptions
828
+ ): Promise<void> {
829
+ // when the already-compressed buffer of an unchanged object is available, skip the costly re-compression.
830
+ const { buffer: contents, inflatedSize } = raw || (await object.compressWithSize());
831
+ const options = chownOptions === undefined ? await this.getChownOptions() : chownOptions;
832
+ // hashing a Source is sha1 of its content. the raw buffer's ref was already checked against it.
833
+ const hash = raw?.ref || object.hash();
828
834
  const objectPath = this.objectPath(hash);
829
835
  logger.trace(`repository._writeOne: ${objectPath}`);
830
836
  // Run hook to transform content pre persisting
831
837
  const transformedContent = this.onPersist(contents);
832
- await writeFile(objectPath, transformedContent, options);
838
+ await this.writeObjectFile(objectPath, transformedContent, options);
833
839
  // once written, the object is the up-to-date one. this also replaces the size-estimate of objects that
834
840
  // were cached by `add()`.
835
841
  if (this.cache.has(hash.toString())) this.cache.set(hash.toString(), object, inflatedSize);
@@ -850,27 +856,73 @@ export default class Repository {
850
856
  const ref = object.ref.toString();
851
857
  if (!isSnap(ref)) throw new BitError(`invalid object ref: ${JSON.stringify(ref)}`);
852
858
  }
853
- const options: ChownOptions = {};
854
- if (this.scopeJson.groupName) options.gid = await resolveGroupId(this.scopeJson.groupName);
855
- await Promise.all(
856
- objectList.objects.map(async (object) => {
857
- const objPath = path.join(pendingDir, this.hashPath(object.ref));
858
- await writeFile(objPath, object.buffer, options);
859
- })
859
+ const chownOptions = await this.getChownOptions();
860
+ await pMapPool(
861
+ objectList.objects,
862
+ (object) => this.writeObjectFile(path.join(pendingDir, this.hashPath(object.ref)), object.buffer, chownOptions),
863
+ { concurrency: concurrentIOLimit() }
860
864
  );
861
865
  }
862
866
 
863
867
  async readObjectsFromPendingDir(pendingDir: PathOsBasedAbsolute) {
864
868
  const refs = await this.listRefs(pendingDir);
865
- const objects = await Promise.all(
866
- refs.map(async (ref) => {
869
+ const objects = await pMapPool(
870
+ refs,
871
+ async (ref) => {
867
872
  const buffer = await fs.readFile(path.join(pendingDir, this.hashPath(ref)));
868
873
  return { ref, buffer };
869
- })
874
+ },
875
+ { concurrency: concurrentIOLimit() }
870
876
  );
871
877
  return new ObjectList(objects);
872
878
  }
873
879
 
880
+ /**
881
+ * the owner to set on written object files. `null` when the scope has no group (or it can't be resolved), in which
882
+ * case the owner of an existing file is kept.
883
+ */
884
+ private async getChownOptions(): Promise<ObjectChownOptions> {
885
+ if (!this.scopeJson.groupName) return null;
886
+ const gid = await resolveGroupId(this.scopeJson.groupName);
887
+ return gid ? { uid: userInfo().uid, gid } : null;
888
+ }
889
+
890
+ /**
891
+ * writes the file atomically (tmp file + rename), the same way `writeFile` of legacy.utils does with write-file-atomic,
892
+ * including keeping the mode (and owner, when no chown is given) of an existing file and ignoring the same chown/chmod
893
+ * errors. it's called for every object, so it saves the fs calls write-file-atomic and `ensureDir` do on every write
894
+ * (realpath, stat of the dir, exit handlers). the parent dir is created only when the first write fails with ENOENT.
895
+ * the tmp file is a dotfile, which `listRefs()` ignores in case it's left behind.
896
+ */
897
+ private async writeObjectFile(filePath: string, contents: Buffer, chownOptions: ObjectChownOptions) {
898
+ const existing = await fs.stat(filePath).catch(() => undefined);
899
+ const mode = existing?.mode;
900
+ // process.getuid is undefined on Windows, where there's no owner to keep.
901
+ const keepOwner =
902
+ existing && typeof process.getuid === 'function' ? { uid: existing.uid, gid: existing.gid } : null;
903
+ const chown = chownOptions || keepOwner;
904
+ objectFileWriteCounter += 1;
905
+ const tmpPath = path.join(
906
+ path.dirname(filePath),
907
+ `.${path.basename(filePath)}.${process.pid}.${objectFileWriteCounter}`
908
+ );
909
+ try {
910
+ try {
911
+ await fs.writeFile(tmpPath, contents, { mode });
912
+ } catch (err: any) {
913
+ if (err.code !== 'ENOENT') throw err;
914
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
915
+ await fs.writeFile(tmpPath, contents, { mode });
916
+ }
917
+ if (chown) await fs.chown(tmpPath, chown.uid, chown.gid).catch(throwUnlessChownErrOk);
918
+ if (mode) await fs.chmod(tmpPath, mode).catch(throwUnlessChownErrOk);
919
+ await fs.rename(tmpPath, filePath);
920
+ } catch (err) {
921
+ await fs.remove(tmpPath).catch(() => {});
922
+ throw err;
923
+ }
924
+ }
925
+
874
926
  private hashPath(ref: Ref) {
875
927
  const hash = ref.toString();
876
928
  return path.join(hash.slice(0, 2), hash.slice(2));
@@ -893,6 +945,16 @@ async function removeFile(filePath: string, propagateDirs = false): Promise<bool
893
945
  return true;
894
946
  }
895
947
 
948
+ /**
949
+ * same as write-file-atomic: a non-root user can't always chown/chmod, and it's not an error.
950
+ */
951
+ function throwUnlessChownErrOk(err: any) {
952
+ if (err.code === 'ENOSYS') return;
953
+ const nonRoot = !process.getuid || process.getuid() !== 0;
954
+ if (nonRoot && (err.code === 'EINVAL' || err.code === 'EPERM')) return;
955
+ throw err;
956
+ }
957
+
896
958
  async function resolveGroupId(groupName: string): Promise<number | null | undefined> {
897
959
  // unix group ids are meaningless on Windows, which has no /etc/group and no process.getgid
898
960
  if (!process.getgid) return null;