Skip to content

Commit 9740534

Browse files
mshanemcclaude
andcommitted
fix(services): clear SfProject instance cache on project file change - W-23095743
globalSfProjectCache invalidation alone left the stale value: SfProject.resolve returns a path-memoized instance (sfProject.js:436) whose parsed sfProjectJson is also memoized (sfProject.js:468), so a fresh cache lookup re-resolved the SAME instance and the edited sourceApiVersion was never read. Also clear the core SfProject.instances Map so retrieve/deploy/manifest pick up mid-session edits. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent a811ec5 commit 9740534

2 files changed

Lines changed: 28 additions & 2 deletions

File tree

packages/salesforcedx-vscode-services/src/core/projectService.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,19 @@ const globalSfProjectCache = Effect.runSync(
5959
}).pipe(Effect.withSpan('sfProjectCache'))
6060
);
6161

62-
/** Invalidate a single SfProject cache entry (per-key, NOT invalidateAll — preserves sibling workspace entries). */
63-
export const invalidateSfProjectCache = (cacheKey: string) => globalSfProjectCache.invalidate(cacheKey);
62+
/**
63+
* Invalidate the SfProject cache so the next `getSfProject` re-reads sfdx-project.json from disk.
64+
*
65+
* Two caches must be dropped together:
66+
* (1) our `globalSfProjectCache` — per-key invalidate (preserves sibling workspace entries).
67+
* (2) `@salesforce/core`'s static `SfProject.instances` Map — `SfProject.resolve` returns the memoized
68+
* instance per path, and each instance memoizes its parsed `sfProjectJson` (sfProject.js:468). Without
69+
* clearing it, a fresh `globalSfProjectCache` lookup re-resolves the SAME stale instance, so the edited
70+
* `sourceApiVersion` is never picked up. `clearInstances()` is the only public API (no per-path delete);
71+
* clearing all is safe — siblings simply re-resolve from disk on next use.
72+
*/
73+
export const invalidateSfProjectCache = (cacheKey: string) =>
74+
globalSfProjectCache.invalidate(cacheKey).pipe(Effect.tap(() => Effect.sync(() => SfProject.clearInstances())));
6475

6576
const TOOLS_DIR = 'tools';
6677
const SOBJECTS_DIR = 'sobjects';

packages/salesforcedx-vscode-services/test/jest/core/sfProjectFileWatcher.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
66
*/
77

8+
import { SfProject } from '@salesforce/core';
89
import * as Effect from 'effect/Effect';
910
import * as Fiber from 'effect/Fiber';
1011
import * as Layer from 'effect/Layer';
@@ -46,6 +47,8 @@ const runWatcherTest = (publishUri: string) => {
4647
};
4748

4849
describe('watchSfProjectFile', () => {
50+
afterEach(() => jest.restoreAllMocks());
51+
4952
it('invalidates the SfProject cache when sfdx-project.json changes', async () => {
5053
const spy = await runWatcherTest(PROJECT_FILE_PATH);
5154
expect(spy).toHaveBeenCalledTimes(1);
@@ -62,3 +65,15 @@ describe('watchSfProjectFile', () => {
6265
expect(spy).not.toHaveBeenCalled();
6366
});
6467
});
68+
69+
describe('invalidateSfProjectCache', () => {
70+
afterEach(() => jest.restoreAllMocks());
71+
72+
it('clears the @salesforce/core SfProject instance cache so memoized sfProjectJson is dropped', async () => {
73+
// Without clearInstances, SfProject.resolve returns the same memoized instance (sfProject.js:436),
74+
// whose parsed sfProjectJson is also memoized (sfProject.js:468) -> stale sourceApiVersion survives.
75+
const clearSpy = jest.spyOn(SfProject, 'clearInstances').mockImplementation(() => {});
76+
await Effect.runPromise(projectService.invalidateSfProjectCache('/Users/testuser/project'));
77+
expect(clearSpy).toHaveBeenCalledTimes(1);
78+
});
79+
});

0 commit comments

Comments
 (0)