@salesforce/source-tracking 2.1.2 → 2.2.2

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.
@@ -9,17 +9,22 @@ exports.SourceTracking = void 0;
9
9
  */
10
10
  const fs = require("fs");
11
11
  const path_1 = require("path");
12
- const os_1 = require("os");
13
12
  const core_1 = require("@salesforce/core");
14
13
  const kit_1 = require("@salesforce/kit");
15
14
  const ts_types_1 = require("@salesforce/ts-types");
16
15
  const source_deploy_retrieve_1 = require("@salesforce/source-deploy-retrieve");
16
+ // this is not exported by SDR (see the comments in SDR regarding its limitations)
17
+ const filePathGenerator_1 = require("@salesforce/source-deploy-retrieve/lib/src/utils/filePathGenerator");
17
18
  const remoteSourceTrackingService_1 = require("./shared/remoteSourceTrackingService");
18
19
  const localShadowRepo_1 = require("./shared/localShadowRepo");
20
+ const conflicts_1 = require("./shared/conflicts");
19
21
  const guards_1 = require("./shared/guards");
20
22
  const functions_1 = require("./shared/functions");
21
23
  const metadataKeys_1 = require("./shared/metadataKeys");
22
24
  const compatibility_1 = require("./compatibility");
25
+ const populateFilePaths_1 = require("./shared/populateFilePaths");
26
+ const populateTypesAndNames_1 = require("./shared/populateTypesAndNames");
27
+ const localComponentSetArray_1 = require("./shared/localComponentSetArray");
23
28
  /**
24
29
  * Manages source tracking files (remote and local)
25
30
  *
@@ -29,94 +34,46 @@ const compatibility_1 = require("./compatibility");
29
34
  class SourceTracking extends kit_1.AsyncCreatable {
30
35
  constructor(options) {
31
36
  super(options);
32
- this.registry = new source_deploy_retrieve_1.RegistryAccess();
33
37
  this.org = options.org;
38
+ this.orgId = this.org.getOrgId();
34
39
  this.projectPath = options.project.getPath();
35
40
  this.packagesDirs = options.project.getPackageDirectories();
36
41
  this.logger = core_1.Logger.childFromRoot('SourceTracking');
37
42
  this.project = options.project;
38
- this.hasSfdxTrackingFiles = (0, compatibility_1.hasSfdxTrackingFiles)(this.org.getOrgId(), this.projectPath);
43
+ this.ignoreConflicts = options.ignoreConflicts ?? false;
44
+ this.ignoreLocalCache = options.ignoreLocalCache ?? false;
45
+ this.subscribeSDREvents = options.subscribeSDREvents ?? false;
46
+ this.hasSfdxTrackingFiles = (0, compatibility_1.hasSfdxTrackingFiles)(this.orgId, this.projectPath);
39
47
  }
40
48
  // eslint-disable-next-line class-methods-use-this
41
49
  async init() {
42
- // reserved for future use
50
+ await this.maybeSubscribeLifecycleEvents();
43
51
  }
44
52
  /**
45
53
  *
46
- * @param byPackageDir if true, returns one ComponentSet for each packageDir with changes
54
+ * @param byPackageDir if true, returns a ComponentSet for each packageDir that has any changes
55
+ * * if false, returns an array containing one ComponentSet with all changes
56
+ * * if not specified, this method will follow what sfdx-project.json says
47
57
  * @returns ComponentSet[]
48
58
  */
49
- async localChangesAsComponentSet(byPackageDir = false) {
50
- var _a;
51
- const [projectConfig] = await Promise.all([this.project.resolveProjectConfig(), this.ensureLocalTracking()]);
52
- (_a = this.forceIgnore) !== null && _a !== void 0 ? _a : (this.forceIgnore = source_deploy_retrieve_1.ForceIgnore.findAndCreate(this.project.getDefaultPackage().name));
53
- const sourceApiVersion = (0, ts_types_1.getString)(projectConfig, 'sourceApiVersion');
54
- // optimistic resolution...some files may not be possible to resolve
55
- const resolverForNonDeletes = new source_deploy_retrieve_1.MetadataResolver();
56
- // we need virtual components for the deletes.
57
- // TODO: could we use the same for the non-deletes?
58
- const [allNonDeletes, allDeletes] = (await Promise.all([this.localRepo.getNonDeleteFilenames(), this.localRepo.getDeleteFilenames()]))
59
- // remove the forceIgnored items early
60
- .map((group) => group.filter((item) => this.forceIgnore.accepts(item)));
59
+ async localChangesAsComponentSet(byPackageDir) {
60
+ const [projectConfig] = await Promise.all([
61
+ this.project.resolveProjectConfig(),
62
+ this.ensureLocalTracking(),
63
+ ]);
64
+ const sourceApiVersion = projectConfig.sourceApiVersion;
65
+ const [nonDeletes, deletes] = await Promise.all([
66
+ this.localRepo.getNonDeleteFilenames(),
67
+ this.localRepo.getDeleteFilenames(),
68
+ ]);
61
69
  // it'll be easier to filter filenames and work with smaller component sets than to filter SourceComponents
62
- const groupings = (byPackageDir
63
- ? this.packagesDirs.map((pkgDir) => ({
64
- path: pkgDir.name,
65
- nonDeletes: allNonDeletes.filter((f) => (0, functions_1.pathIsInFolder)(f, pkgDir.name)),
66
- deletes: allDeletes.filter((f) => (0, functions_1.pathIsInFolder)(f, pkgDir.name)),
67
- }))
68
- : [
69
- {
70
- nonDeletes: allNonDeletes,
71
- deletes: allDeletes,
72
- path: this.packagesDirs.map((dir) => dir.name).join(';'),
73
- },
74
- ]).filter((group) => group.deletes.length || group.nonDeletes.length);
70
+ const groupings = (0, localComponentSetArray_1.getGroupedFiles)({
71
+ packageDirs: this.packagesDirs,
72
+ nonDeletes,
73
+ deletes,
74
+ }, byPackageDir ?? Boolean(projectConfig.pushPackageDirectoriesSequentially)); // if the users specified true or false for the param, that overrides the project config
75
75
  this.logger.debug(`will build array of ${groupings.length} componentSet(s)`);
76
- return groupings
77
- .map((grouping) => {
78
- this.logger.debug(`building componentSet for ${grouping.path} (deletes: ${grouping.deletes.length} nonDeletes: ${grouping.nonDeletes.length})`);
79
- const componentSet = new source_deploy_retrieve_1.ComponentSet();
80
- if (sourceApiVersion) {
81
- componentSet.sourceApiVersion = sourceApiVersion;
82
- }
83
- const resolverForDeletes = new source_deploy_retrieve_1.MetadataResolver(undefined, source_deploy_retrieve_1.VirtualTreeContainer.fromFilePaths(grouping.deletes));
84
- grouping.deletes
85
- .flatMap((filename) => resolverForDeletes.getComponentsFromPath(filename))
86
- .filter(guards_1.sourceComponentGuard)
87
- .map((component) => {
88
- // if the component is a file in a bundle type AND there are files from the bundle that are not deleted, set the bundle for deploy, not for delete
89
- if ((0, functions_1.isBundle)(component) && component.content && fs.existsSync(component.content)) {
90
- // all bundle types have a directory name
91
- try {
92
- resolverForNonDeletes
93
- .getComponentsFromPath((0, path_1.resolve)(component.content))
94
- .filter(guards_1.sourceComponentGuard)
95
- .map((nonDeletedComponent) => componentSet.add(nonDeletedComponent));
96
- }
97
- catch (e) {
98
- this.logger.warn(`unable to find component at ${component.content}. That's ok if it was supposed to be deleted`);
99
- }
100
- }
101
- else {
102
- componentSet.add(component, source_deploy_retrieve_1.DestructiveChangesType.POST);
103
- }
104
- });
105
- grouping.nonDeletes
106
- .flatMap((filename) => {
107
- try {
108
- return resolverForNonDeletes.getComponentsFromPath((0, path_1.resolve)(filename));
109
- }
110
- catch (e) {
111
- this.logger.warn(`unable to resolve ${filename}`);
112
- return undefined;
113
- }
114
- })
115
- .filter(guards_1.sourceComponentGuard)
116
- .map((component) => componentSet.add(component));
117
- return componentSet;
118
- })
119
- .filter((componentSet) => componentSet.size > 0);
76
+ return (0, localComponentSetArray_1.getComponentSets)(groupings, sourceApiVersion);
120
77
  }
121
78
  async remoteNonDeletesAsComponentSet() {
122
79
  const [changeResults, sourceBackedComponents] = await Promise.all([
@@ -176,14 +133,8 @@ class SourceTracking extends kit_1.AsyncCreatable {
176
133
  }
177
134
  return results;
178
135
  }
179
- /**
180
- * Get metadata changes made locally and in the org.
181
- *
182
- * @returns local and remote changed metadata
183
- *
184
- */
185
136
  async getChanges(options) {
186
- if ((options === null || options === void 0 ? void 0 : options.origin) === 'local') {
137
+ if (options?.origin === 'local') {
187
138
  await this.ensureLocalTracking();
188
139
  const filenames = await this.getLocalChangesAsFilenames(options.state);
189
140
  if (options.format === 'string') {
@@ -212,24 +163,24 @@ class SourceTracking extends kit_1.AsyncCreatable {
212
163
  .filter(guards_1.sourceComponentGuard);
213
164
  }
214
165
  }
215
- if ((options === null || options === void 0 ? void 0 : options.origin) === 'remote') {
166
+ if (options?.origin === 'remote') {
216
167
  await this.ensureRemoteTracking();
217
168
  const remoteChanges = await this.remoteSourceTrackingService.retrieveUpdates();
218
169
  this.logger.debug('remoteChanges', remoteChanges);
219
170
  const filteredChanges = remoteChanges
220
171
  .filter(remoteFilterByState[options.state])
221
- // skip any remote types not in the registry. Will emit node warnings
222
- .filter((rce) => this.registrySupportsType(rce.type));
172
+ // skip any remote types not in the registry. Will emit warnings
173
+ .filter((rce) => (0, metadataKeys_1.registrySupportsType)(rce.type));
223
174
  if (options.format === 'ChangeResult') {
224
175
  return filteredChanges.map((change) => (0, remoteSourceTrackingService_1.remoteChangeElementToChangeResult)(change));
225
176
  }
226
177
  if (options.format === 'ChangeResultWithPaths') {
227
- return this.populateFilePaths(filteredChanges.map((change) => (0, remoteSourceTrackingService_1.remoteChangeElementToChangeResult)(change)));
178
+ return (0, populateFilePaths_1.populateFilePaths)(filteredChanges.map((change) => (0, remoteSourceTrackingService_1.remoteChangeElementToChangeResult)(change)), this.project.getPackageDirectories().map((pkgDir) => pkgDir.path));
228
179
  }
229
180
  // turn it into a componentSet to resolve filenames
230
181
  const remoteChangesAsComponentSet = new source_deploy_retrieve_1.ComponentSet(filteredChanges.map((element) => ({
231
- type: element === null || element === void 0 ? void 0 : element.type,
232
- fullName: element === null || element === void 0 ? void 0 : element.name,
182
+ type: element?.type,
183
+ fullName: element?.name,
233
184
  })));
234
185
  const matchingLocalSourceComponentsSet = source_deploy_retrieve_1.ComponentSet.fromSource({
235
186
  fsPaths: this.packagesDirs.map((dir) => (0, path_1.resolve)(dir.fullPath)),
@@ -247,6 +198,13 @@ class SourceTracking extends kit_1.AsyncCreatable {
247
198
  }
248
199
  throw new Error(`unsupported options: ${JSON.stringify(options)}`);
249
200
  }
201
+ async maybeApplyRemoteDeletesToLocal(returnDeleteFileResponses) {
202
+ const changesToDelete = await this.getChanges({ origin: 'remote', state: 'delete', format: 'SourceComponent' });
203
+ const fileResponsesFromDelete = await this.deleteFilesAndUpdateTracking(changesToDelete);
204
+ return returnDeleteFileResponses
205
+ ? { componentSetFromNonDeletes: await this.remoteNonDeletesAsComponentSet(), fileResponsesFromDelete }
206
+ : this.remoteNonDeletesAsComponentSet();
207
+ }
250
208
  /**
251
209
  *
252
210
  * returns immediately if there are no changesToDelete
@@ -274,15 +232,12 @@ class SourceTracking extends kit_1.AsyncCreatable {
274
232
  })), true // skip polling because it's a pull
275
233
  ),
276
234
  ]);
277
- return filenames.map((filename) => {
278
- var _a, _b;
279
- return ({
280
- state: 'Deleted',
281
- filename,
282
- type: (_a = sourceComponentByFileName.get(filename)) === null || _a === void 0 ? void 0 : _a.type.name,
283
- fullName: (_b = sourceComponentByFileName.get(filename)) === null || _b === void 0 ? void 0 : _b.fullName,
284
- });
285
- });
235
+ return filenames.map((filename) => ({
236
+ state: 'Deleted',
237
+ filename,
238
+ type: sourceComponentByFileName.get(filename)?.type.name,
239
+ fullName: sourceComponentByFileName.get(filename)?.fullName,
240
+ }));
286
241
  }
287
242
  /**
288
243
  * Update tracking for the options passed.
@@ -290,12 +245,11 @@ class SourceTracking extends kit_1.AsyncCreatable {
290
245
  * @param options the files to update
291
246
  */
292
247
  async updateLocalTracking(options) {
293
- var _a, _b;
294
248
  await this.ensureLocalTracking();
295
249
  // relative paths make smaller trees AND isogit wants them relative
296
250
  const relativeOptions = {
297
- files: ((_a = options.files) !== null && _a !== void 0 ? _a : []).map((file) => this.ensureRelative(file)),
298
- deletedFiles: ((_b = options.deletedFiles) !== null && _b !== void 0 ? _b : []).map((file) => this.ensureRelative(file)),
251
+ files: (options.files ?? []).map((file) => (0, functions_1.ensureRelative)(file, this.projectPath)),
252
+ deletedFiles: (options.deletedFiles ?? []).map((file) => (0, functions_1.ensureRelative)(file, this.projectPath)),
299
253
  };
300
254
  // plot twist: if you delete a member of a bundle (ex: lwc/foo/foo.css) and push, it'll not be in the fileResponses (deployedFiles) or deletedFiles
301
255
  // what got deleted? Any local changes NOT in the fileResponses but part of a successfully deployed bundle
@@ -329,17 +283,23 @@ class SourceTracking extends kit_1.AsyncCreatable {
329
283
  }
330
284
  await this.remoteSourceTrackingService.syncSpecifiedElements(fileResponses);
331
285
  }
286
+ async reReadLocalTrackingCache() {
287
+ await this.localRepo.getStatus(true);
288
+ }
332
289
  /**
333
290
  * If the local tracking shadowRepo doesn't exist, it will be created.
334
- * Does nothing if it already exists.
291
+ * Does nothing if it already exists, unless you've instantiate SourceTracking to not cache local status, in which case it'll re-read your files
335
292
  * Useful before parallel operations
336
293
  */
337
294
  async ensureLocalTracking() {
338
295
  if (this.localRepo) {
296
+ if (this.ignoreLocalCache) {
297
+ await this.localRepo.getStatus(true);
298
+ }
339
299
  return;
340
300
  }
341
301
  this.localRepo = await localShadowRepo_1.ShadowRepo.getInstance({
342
- orgId: this.org.getOrgId(),
302
+ orgId: this.orgId,
343
303
  projectPath: (0, path_1.normalize)(this.projectPath),
344
304
  packageDirs: this.packagesDirs,
345
305
  hasSfdxTrackingFiles: this.hasSfdxTrackingFiles,
@@ -395,7 +355,7 @@ class SourceTracking extends kit_1.AsyncCreatable {
395
355
  * Deletes the remote tracking files
396
356
  */
397
357
  async clearRemoteTracking() {
398
- return remoteSourceTrackingService_1.RemoteSourceTrackingService.delete(this.org.getOrgId(), this.hasSfdxTrackingFiles);
358
+ return remoteSourceTrackingService_1.RemoteSourceTrackingService.delete(this.orgId, this.hasSfdxTrackingFiles);
399
359
  }
400
360
  /**
401
361
  * Sets the files to max revision so that no changes appear
@@ -430,201 +390,128 @@ class SourceTracking extends kit_1.AsyncCreatable {
430
390
  if (remoteChanges.length === 0) {
431
391
  return [];
432
392
  }
433
- // index the remoteChanges by filename
434
- const fileNameIndex = new Map();
435
- const metadataKeyIndex = new Map();
436
- remoteChanges.map((change) => {
437
- var _a;
438
- if (change.name && change.type) {
439
- metadataKeyIndex.set((0, functions_1.getMetadataKey)(change.name, change.type), change);
440
- }
441
- (_a = change.filenames) === null || _a === void 0 ? void 0 : _a.map((filename) => {
442
- fileNameIndex.set(filename, change);
443
- });
444
- });
445
- const conflicts = new Set();
446
- this.populateTypesAndNames({ elements: localChanges, excludeUnresolvable: true }).map((change) => {
447
- var _a;
448
- const metadataKey = (0, functions_1.getMetadataKey)(change.name, change.type);
449
- // option 1: name and type match
450
- if (metadataKeyIndex.has(metadataKey)) {
451
- conflicts.add({ ...metadataKeyIndex.get(metadataKey) });
452
- }
453
- else {
454
- // option 2: some of the filenames match
455
- (_a = change.filenames) === null || _a === void 0 ? void 0 : _a.map((filename) => {
456
- if (fileNameIndex.has(filename)) {
457
- conflicts.add({ ...fileNameIndex.get(filename) });
458
- }
459
- });
460
- }
393
+ this.forceIgnore ?? (this.forceIgnore = source_deploy_retrieve_1.ForceIgnore.findAndCreate(this.project.getDefaultPackage().path));
394
+ return (0, conflicts_1.getDedupedConflictsFromChanges)({
395
+ localChanges,
396
+ remoteChanges,
397
+ projectPath: this.projectPath,
398
+ forceIgnore: this.forceIgnore,
461
399
  });
462
- // deeply de-dupe
463
- return Array.from(conflicts);
464
400
  }
465
401
  /**
466
- * uses SDR to translate remote metadata records into local file paths (which only typically have the filename).
402
+ * handles both remote and local tracking
467
403
  *
468
- * @input elements: ChangeResult[]
469
- * @input excludeUnresolvable: boolean Filter out components where you can't get the name and type (that is, it's probably not a valid source component)
470
- * @input resolveDeleted: constructs a virtualTree instead of the actual filesystem--useful when the files no longer exist
471
- * @input useFsForceIgnore: (default behavior) use forceIgnore from the filesystem. If false, uses the base forceIgnore from SDR
404
+ * @param result FileResponse[]
472
405
  */
473
- populateTypesAndNames({ elements, excludeUnresolvable = false, resolveDeleted = false, useFsForceIgnore = true, }) {
474
- if (elements.length === 0) {
475
- return [];
406
+ async updateTrackingFromDeploy(deployResult) {
407
+ const successes = deployResult
408
+ .getFileResponses()
409
+ .filter((fileResponse) => fileResponse.state !== source_deploy_retrieve_1.ComponentStatus.Failed && fileResponse.filePath);
410
+ if (!successes.length) {
411
+ return;
476
412
  }
477
- this.logger.debug(`populateTypesAndNames for ${elements.length} change elements`);
478
- const filenames = elements.flatMap((element) => element.filenames).filter(ts_types_1.isString);
479
- // component set generated from the filenames on all local changes
480
- const resolver = new source_deploy_retrieve_1.MetadataResolver(undefined, resolveDeleted ? source_deploy_retrieve_1.VirtualTreeContainer.fromFilePaths(filenames) : undefined, useFsForceIgnore);
481
- const sourceComponents = filenames
482
- .flatMap((filename) => {
483
- try {
484
- return resolver.getComponentsFromPath(filename);
485
- }
486
- catch (e) {
487
- this.logger.warn(`unable to resolve ${filename}`);
488
- return undefined;
489
- }
490
- })
491
- .filter(guards_1.sourceComponentGuard);
492
- this.logger.debug(` matching SourceComponents have ${sourceComponents.length} items from local`);
493
- // make it simpler to find things later
494
- const elementMap = new Map();
495
- elements.map((element) => {
496
- var _a;
497
- (_a = element.filenames) === null || _a === void 0 ? void 0 : _a.map((filename) => {
498
- elementMap.set(this.ensureRelative(filename), element);
499
- });
500
- });
501
- // iterates the local components and sets their filenames
502
- sourceComponents.map((matchingComponent) => {
503
- var _a;
504
- if ((matchingComponent === null || matchingComponent === void 0 ? void 0 : matchingComponent.fullName) && (matchingComponent === null || matchingComponent === void 0 ? void 0 : matchingComponent.type.name)) {
505
- const filenamesFromMatchingComponent = [matchingComponent.xml, ...matchingComponent.walkContent()];
506
- // Set the ignored status at the component level so it can apply to all its files, some of which may not match the ignoreFile (ex: ApexClass)
507
- (_a = this.forceIgnore) !== null && _a !== void 0 ? _a : (this.forceIgnore = source_deploy_retrieve_1.ForceIgnore.findAndCreate(this.project.getDefaultPackage().path));
508
- const ignored = filenamesFromMatchingComponent
509
- .filter(ts_types_1.isString)
510
- .filter((filename) => !filename.includes('__tests__'))
511
- .some((filename) => this.forceIgnore.denies(filename));
512
- filenamesFromMatchingComponent.map((filename) => {
513
- if (filename && elementMap.has(filename)) {
514
- // add the type/name from the componentSet onto the element
515
- elementMap.set(filename, {
516
- origin: 'remote',
517
- ...elementMap.get(filename),
518
- type: matchingComponent.type.name,
519
- name: matchingComponent.fullName,
520
- ignored,
521
- });
413
+ await Promise.all([
414
+ this.updateLocalTracking({
415
+ // assertions allowed because filtered above
416
+ files: successes
417
+ .filter((fileResponse) => fileResponse.state !== source_deploy_retrieve_1.ComponentStatus.Deleted)
418
+ .map((fileResponse) => fileResponse.filePath),
419
+ deletedFiles: successes
420
+ .filter((fileResponse) => fileResponse.state === source_deploy_retrieve_1.ComponentStatus.Deleted)
421
+ .map((fileResponse) => fileResponse.filePath),
422
+ }),
423
+ this.updateRemoteTracking(successes.map(({ state, fullName, type, filePath }) => ({ state, fullName, type, filePath }))),
424
+ ]);
425
+ }
426
+ /**
427
+ * handles both remote and local tracking
428
+ *
429
+ * @param result FileResponse[]
430
+ */
431
+ async updateTrackingFromRetrieve(retrieveResult) {
432
+ const successes = retrieveResult
433
+ .getFileResponses()
434
+ .filter((fileResponse) => fileResponse.state !== source_deploy_retrieve_1.ComponentStatus.Failed);
435
+ if (!successes.length) {
436
+ return;
437
+ }
438
+ await Promise.all([
439
+ this.updateLocalTracking({
440
+ // assertion allowed because it's filtering out undefined
441
+ files: successes.map((fileResponse) => fileResponse.filePath).filter(Boolean),
442
+ }),
443
+ this.updateRemoteTracking(successes.map(({ state, fullName, type, filePath }) => ({ state, fullName, type, filePath })), true // retrieves don't need to poll for SourceMembers
444
+ ),
445
+ ]);
446
+ }
447
+ /**
448
+ * If you've already got an instance of STL, but need to change the conflicts setting
449
+ * normally you set this on instantiation
450
+ *
451
+ * @param value true/false
452
+ */
453
+ setIgnoreConflicts(value) {
454
+ this.ignoreConflicts = value;
455
+ }
456
+ async maybeSubscribeLifecycleEvents() {
457
+ if (this.subscribeSDREvents && (await this.org.tracksSource())) {
458
+ const lifecycle = core_1.Lifecycle.getInstance();
459
+ // the only thing STL uses pre events for is to check conflicts. So if you don't care about conflicts, don't listen!
460
+ if (!this.ignoreConflicts) {
461
+ this.logger.debug('subscribing to predeploy/retrieve events');
462
+ // subscribe to SDR `pre` events to handle conflicts before deploy/retrieve
463
+ lifecycle.on('scopedPreDeploy', async (e) => {
464
+ this.logger.debug('received scopedPreDeploy event');
465
+ if (e.orgId === this.orgId) {
466
+ (0, conflicts_1.throwIfConflicts)((0, conflicts_1.findConflictsInComponentSet)(e.componentSet, await this.getConflicts()));
467
+ }
468
+ });
469
+ lifecycle.on('scopedPreRetrieve', async (e) => {
470
+ this.logger.debug('received scopedPreRetrieve event');
471
+ if (e.orgId === this.orgId) {
472
+ (0, conflicts_1.throwIfConflicts)((0, conflicts_1.findConflictsInComponentSet)(e.componentSet, await this.getConflicts()));
522
473
  }
523
474
  });
524
475
  }
525
- });
526
- return excludeUnresolvable
527
- ? Array.from(new Set(elementMap.values())).filter((changeResult) => changeResult.name && changeResult.type)
528
- : Array.from(new Set(elementMap.values()));
476
+ // subscribe to SDR post-deploy event
477
+ this.logger.debug('subscribing to postdeploy/retrieve events');
478
+ // yes, the post hooks really have different payloads!
479
+ lifecycle.on('scopedPostDeploy', async (e) => {
480
+ this.logger.debug('received scopedPostDeploy event');
481
+ if (e.orgId === this.orgId) {
482
+ await this.updateTrackingFromDeploy(e.deployResult);
483
+ }
484
+ });
485
+ lifecycle.on('scopedPostRetrieve', async (e) => {
486
+ this.logger.debug('received scopedPostRetrieve event');
487
+ if (e.orgId === this.orgId) {
488
+ await this.updateTrackingFromRetrieve(e.retrieveResult);
489
+ }
490
+ });
491
+ }
529
492
  }
530
493
  async getLocalStatusRows() {
531
494
  await this.ensureLocalTracking();
532
495
  let results = [];
533
- const localDeletes = this.populateTypesAndNames({
496
+ const localDeletes = (0, populateTypesAndNames_1.populateTypesAndNames)({
534
497
  elements: await this.getChanges({ origin: 'local', state: 'delete', format: 'ChangeResult' }),
535
498
  excludeUnresolvable: true,
536
499
  resolveDeleted: true,
537
- useFsForceIgnore: false,
500
+ projectPath: this.projectPath,
538
501
  });
539
- const localAdds = this.populateTypesAndNames({
502
+ const localAdds = (0, populateTypesAndNames_1.populateTypesAndNames)({
540
503
  elements: await this.getChanges({ origin: 'local', state: 'add', format: 'ChangeResult' }),
541
504
  excludeUnresolvable: true,
542
- useFsForceIgnore: false,
505
+ projectPath: this.projectPath,
543
506
  });
544
- const localModifies = this.populateTypesAndNames({
507
+ const localModifies = (0, populateTypesAndNames_1.populateTypesAndNames)({
545
508
  elements: await this.getChanges({ origin: 'local', state: 'modify', format: 'ChangeResult' }),
546
509
  excludeUnresolvable: true,
547
- useFsForceIgnore: false,
510
+ projectPath: this.projectPath,
548
511
  });
549
512
  results = results.concat(localAdds.flatMap((item) => this.localChangesToOutputRow(item, 'add')), localModifies.flatMap((item) => this.localChangesToOutputRow(item, 'modify')), localDeletes.flatMap((item) => this.localChangesToOutputRow(item, 'delete')));
550
513
  return results;
551
514
  }
552
- registrySupportsType(type) {
553
- try {
554
- if (metadataKeys_1.mappingsForSourceMemberTypesToMetadataType.has(type)) {
555
- return true;
556
- }
557
- // this must use getTypeByName because findType doesn't support addressable child types (ex: customField!)
558
- this.registry.getTypeByName(type);
559
- return true;
560
- }
561
- catch (e) {
562
- process.emitWarning(`Unable to find type ${type} in registry`);
563
- return false;
564
- }
565
- }
566
- /**
567
- * uses SDR to translate remote metadata records into local file paths
568
- */
569
- populateFilePaths(elements) {
570
- if (elements.length === 0) {
571
- return [];
572
- }
573
- this.logger.debug('populateFilePaths for change elements', elements);
574
- // component set generated from an array of MetadataMember from all the remote changes
575
- // but exclude the ones that aren't in the registry
576
- const remoteChangesAsMetadataMember = elements
577
- .map((element) => {
578
- if (typeof element.type === 'string' && typeof element.name === 'string') {
579
- return {
580
- type: element.type,
581
- fullName: element.name,
582
- };
583
- }
584
- })
585
- .filter(guards_1.metadataMemberGuard);
586
- const remoteChangesAsComponentSet = new source_deploy_retrieve_1.ComponentSet(remoteChangesAsMetadataMember);
587
- this.logger.debug(` the generated component set has ${remoteChangesAsComponentSet.size.toString()} items`);
588
- if (remoteChangesAsComponentSet.size < elements.length) {
589
- // there *could* be something missing
590
- // some types (ex: LWC) show up as multiple files in the remote changes, but only one in the component set
591
- // iterate the elements to see which ones didn't make it into the component set
592
- const missingComponents = elements.filter((element) => !remoteChangesAsComponentSet.has({ type: element === null || element === void 0 ? void 0 : element.type, fullName: element === null || element === void 0 ? void 0 : element.name }));
593
- // Throw if anything was actually missing
594
- if (missingComponents.length > 0) {
595
- throw new Error(`unable to generate complete component set for ${elements
596
- .map((element) => `${element.name} (${element.type})`)
597
- .join(os_1.EOL)}`);
598
- }
599
- }
600
- const matchingLocalSourceComponentsSet = source_deploy_retrieve_1.ComponentSet.fromSource({
601
- fsPaths: this.packagesDirs.map((dir) => dir.path),
602
- include: remoteChangesAsComponentSet,
603
- });
604
- this.logger.debug(` local source-backed component set has ${matchingLocalSourceComponentsSet.size.toString()} items from remote`);
605
- // make it simpler to find things later
606
- const elementMap = new Map();
607
- elements.map((element) => {
608
- elementMap.set((0, functions_1.getKeyFromObject)(element), element);
609
- });
610
- // iterates the local components and sets their filenames
611
- for (const matchingComponent of matchingLocalSourceComponentsSet.getSourceComponents().toArray()) {
612
- if (matchingComponent.fullName && matchingComponent.type.name) {
613
- this.logger.debug(`${matchingComponent.fullName}|${matchingComponent.type.name} matches ${matchingComponent.xml} and maybe ${matchingComponent.walkContent().toString()}`);
614
- const key = (0, functions_1.getMetadataKey)(matchingComponent.type.name, matchingComponent.fullName);
615
- elementMap.set(key, {
616
- ...elementMap.get(key),
617
- modified: true,
618
- origin: 'remote',
619
- filenames: [matchingComponent.xml, ...matchingComponent.walkContent()].filter((filename) => filename),
620
- });
621
- }
622
- }
623
- return Array.from(elementMap.values());
624
- }
625
- ensureRelative(filePath) {
626
- return (0, path_1.isAbsolute)(filePath) ? (0, path_1.relative)(this.projectPath, filePath) : filePath;
627
- }
628
515
  async getLocalChangesAsFilenames(state) {
629
516
  if (state === 'modify') {
630
517
  return this.localRepo.getModifyFilenames();
@@ -641,39 +528,34 @@ class SourceTracking extends kit_1.AsyncCreatable {
641
528
  throw new Error(`unable to get local changes for state ${state}`);
642
529
  }
643
530
  localChangesToOutputRow(input, localType) {
644
- var _a, _b, _c;
645
531
  this.logger.debug('converting ChangeResult to a row', input);
646
- const baseObject = {
647
- type: (_a = input.type) !== null && _a !== void 0 ? _a : '',
648
- origin: 'local',
649
- state: localType,
650
- fullName: (_b = input.name) !== null && _b !== void 0 ? _b : '',
651
- // ignored property will be set in populateTypesAndNames
652
- ignored: (_c = input.ignored) !== null && _c !== void 0 ? _c : false,
653
- };
532
+ this.forceIgnore ?? (this.forceIgnore = source_deploy_retrieve_1.ForceIgnore.findAndCreate(this.project.getDefaultPackage().path));
654
533
  if (input.filenames) {
655
534
  return input.filenames.map((filename) => ({
656
- ...baseObject,
535
+ type: input.type ?? '',
536
+ state: localType,
537
+ fullName: input.name ?? '',
657
538
  filePath: filename,
658
539
  origin: 'local',
540
+ ignored: this.forceIgnore.denies(filename),
659
541
  }));
660
542
  }
661
543
  throw new Error('no filenames found for local ChangeResult');
662
544
  }
663
- // this will eventually have async call to figure out the target file locations for remote changes
545
+ // reserve the right to do something more sophisticated in the future
546
+ // via async for figuring out hypothetical filenames (ex: getting default packageDir)
664
547
  // eslint-disable-next-line @typescript-eslint/require-await
665
548
  async remoteChangesToOutputRows(input) {
666
- var _a, _b, _c, _d;
667
549
  this.logger.debug('converting ChangeResult to a row', input);
668
- (_a = this.forceIgnore) !== null && _a !== void 0 ? _a : (this.forceIgnore = source_deploy_retrieve_1.ForceIgnore.findAndCreate(this.project.getDefaultPackage().path));
550
+ this.forceIgnore ?? (this.forceIgnore = source_deploy_retrieve_1.ForceIgnore.findAndCreate(this.project.getDefaultPackage().path));
669
551
  const baseObject = {
670
- type: (_b = input.type) !== null && _b !== void 0 ? _b : '',
552
+ type: input.type ?? '',
671
553
  origin: input.origin,
672
554
  state: stateFromChangeResult(input),
673
- fullName: (_c = input.name) !== null && _c !== void 0 ? _c : '',
555
+ fullName: input.name ?? '',
674
556
  };
675
557
  // it's easy to check ignores if the filePaths exist locally
676
- if ((_d = input.filenames) === null || _d === void 0 ? void 0 : _d.length) {
558
+ if (input.filenames?.length) {
677
559
  return input.filenames.map((filename) => ({
678
560
  ...baseObject,
679
561
  filePath: filename,
@@ -681,7 +563,18 @@ class SourceTracking extends kit_1.AsyncCreatable {
681
563
  }));
682
564
  }
683
565
  // when the file doesn't exist locally, there are no filePaths
684
- // So we can't say whether it's ignored or not
566
+ // SDR can generate the hypothetical place it *would* go and check that
567
+ if (input.name && input.type) {
568
+ return [
569
+ {
570
+ ...baseObject,
571
+ ignored: (0, filePathGenerator_1.filePathsFromMetadataComponent)({
572
+ fullName: input.name,
573
+ type: new source_deploy_retrieve_1.RegistryAccess().getTypeByName(input.type),
574
+ }).some((hypotheticalFilePath) => this.forceIgnore.denies(hypotheticalFilePath)),
575
+ },
576
+ ];
577
+ }
685
578
  return [baseObject];
686
579
  }
687
580
  }