Refactor workspace pattern discovery and cache workspace info#125
Refactor workspace pattern discovery and cache workspace info#125LadyBluenotes merged 5 commits intomainfrom
Conversation
|
View your CI Pipeline Execution ↗ for commit db75840
☁️ Nx Cloud last updated this comment at |
|
View your CI Pipeline Execution ↗ for commit 47e671a
☁️ Nx Cloud last updated this comment at |
commit: |
|
Warning Rate limit exceeded
To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughThis PR updates the YAML dependency to ChangesWorkspace Pattern Detection & API Refactoring
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Review rate limit: 0/1 reviews remaining, refill in 26 minutes and 6 seconds.Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/intent/src/workspace-patterns.ts`:
- Around line 168-176: The cache currently returns and stores mutable
arrays/objects by reference causing external mutations to corrupt the cache;
update readWorkspacePatterns (and any other functions that return cached
collections) to store immutable copies in workspacePatternsCache.set (e.g.,
store a shallow copy of the array/object) and to return a fresh copy to callers
(e.g., return [...cached] or Object.assign({}, cached)) instead of the raw
cached reference; locate uses of workspacePatternsCache.get/set and
readWorkspacePatternsUncached to apply the same copy-on-read and copy-on-write
protection.
In `@packages/intent/tests/workspace-patterns.test.ts`:
- Around line 192-215: The test that sets consoleErrorSpy in the "it('warns and
falls back when package.json workspaces contains non-string entries')" block
must always restore the mock; wrap the test body (everything after creating
consoleErrorSpy and before assertions) in a try/finally and call
consoleErrorSpy.mockRestore() inside the finally so the mock is restored even if
an assertion (like expect(readWorkspacePatterns(root)) or
expect(consoleErrorSpy).toHaveBeenCalledWith(...)) throws; reference the
existing consoleErrorSpy variable and the test block name to locate where to add
the try/finally.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: cfab35ac-4120-4174-9fa3-587913289a55
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (5)
package.jsonpackages/intent/package.jsonpackages/intent/src/cli-support.tspackages/intent/src/workspace-patterns.tspackages/intent/tests/workspace-patterns.test.ts
| export function readWorkspacePatterns(root: string): Array<string> | null { | ||
| const pnpmWs = join(root, 'pnpm-workspace.yaml') | ||
| if (existsSync(pnpmWs)) { | ||
| try { | ||
| const config = parseYaml(readFileSync(pnpmWs, 'utf8')) as Record< | ||
| string, | ||
| unknown | ||
| > | ||
| const patterns = parseWorkspacePatterns(config.packages) | ||
| if (patterns) { | ||
| return patterns | ||
| } | ||
| } catch (err: unknown) { | ||
| const verb = err instanceof SyntaxError ? 'parse' : 'read' | ||
| console.error( | ||
| `Warning: failed to ${verb} ${pnpmWs}: ${err instanceof Error ? err.message : err}`, | ||
| ) | ||
| } | ||
| if (workspacePatternsCache.has(root)) { | ||
| return workspacePatternsCache.get(root) ?? null | ||
| } | ||
|
|
||
| const pkgPath = join(root, 'package.json') | ||
| if (existsSync(pkgPath)) { | ||
| try { | ||
| const pkg = readJsonFile(pkgPath) as Record<string, unknown> | ||
| const workspaces = pkg.workspaces as Record<string, unknown> | undefined | ||
| const patterns = | ||
| parseWorkspacePatterns(workspaces) ?? | ||
| parseWorkspacePatterns(workspaces?.packages) | ||
| if (patterns) { | ||
| return patterns | ||
| } | ||
| } catch (err: unknown) { | ||
| const verb = err instanceof SyntaxError ? 'parse' : 'read' | ||
| console.error( | ||
| `Warning: failed to ${verb} ${pkgPath}: ${err instanceof Error ? err.message : err}`, | ||
| ) | ||
| } | ||
| } | ||
| const patterns = readWorkspacePatternsUncached(root) | ||
| workspacePatternsCache.set(root, patterns) | ||
| return patterns | ||
| } |
There was a problem hiding this comment.
Do not expose mutable cached references.
Lines [170], [201], and [217] return cached arrays/objects by reference. A downstream mutation (e.g., push, splice) will mutate cache contents and corrupt subsequent calls.
Suggested hardening
+function cloneWorkspaceInfo(info: WorkspaceInfo): WorkspaceInfo {
+ return {
+ root: info.root,
+ patterns: [...info.patterns],
+ packageDirs: [...info.packageDirs],
+ packageDirsWithSkills: [...info.packageDirsWithSkills],
+ }
+}
+
export function readWorkspacePatterns(root: string): Array<string> | null {
if (workspacePatternsCache.has(root)) {
- return workspacePatternsCache.get(root) ?? null
+ const cached = workspacePatternsCache.get(root)
+ return cached ? [...cached] : null
}
const patterns = readWorkspacePatternsUncached(root)
- workspacePatternsCache.set(root, patterns)
- return patterns
+ workspacePatternsCache.set(root, patterns ? [...patterns] : null)
+ return patterns ? [...patterns] : null
}
function readWorkspacePackageDirs(root: string): Array<string> | null {
if (workspacePackageDirsCache.has(root)) {
- return workspacePackageDirsCache.get(root) ?? null
+ const cached = workspacePackageDirsCache.get(root)
+ return cached ? [...cached] : null
}
@@
- workspacePackageDirsCache.set(root, packageDirs)
- return packageDirs
+ workspacePackageDirsCache.set(root, [...packageDirs])
+ return [...packageDirs]
}
export function getWorkspaceInfo(root: string): WorkspaceInfo | null {
if (workspaceInfoCache.has(root)) {
- return workspaceInfoCache.get(root) ?? null
+ const cached = workspaceInfoCache.get(root)
+ return cached ? cloneWorkspaceInfo(cached) : null
}
@@
- const info = {
+ const info: WorkspaceInfo = {
root,
patterns,
packageDirs,
packageDirsWithSkills,
}
- workspaceInfoCache.set(root, info)
- return info
+ workspaceInfoCache.set(root, cloneWorkspaceInfo(info))
+ return cloneWorkspaceInfo(info)
}Also applies to: 199-213, 215-240
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/intent/src/workspace-patterns.ts` around lines 168 - 176, The cache
currently returns and stores mutable arrays/objects by reference causing
external mutations to corrupt the cache; update readWorkspacePatterns (and any
other functions that return cached collections) to store immutable copies in
workspacePatternsCache.set (e.g., store a shallow copy of the array/object) and
to return a fresh copy to callers (e.g., return [...cached] or Object.assign({},
cached)) instead of the raw cached reference; locate uses of
workspacePatternsCache.get/set and readWorkspacePatternsUncached to apply the
same copy-on-read and copy-on-write protection.
| it('warns and falls back when package.json workspaces contains non-string entries', () => { | ||
| const root = createRoot() | ||
| const consoleErrorSpy = vi | ||
| .spyOn(console, 'error') | ||
| .mockImplementation(() => undefined) | ||
|
|
||
| writeFileSync( | ||
| join(root, 'package.json'), | ||
| JSON.stringify({ workspaces: [123] }), | ||
| ) | ||
| writeFileSync( | ||
| join(root, 'deno.json'), | ||
| JSON.stringify({ workspace: ['packages/*'] }), | ||
| ) | ||
|
|
||
| expect(readWorkspacePatterns(root)).toEqual(['packages/*']) | ||
| expect(consoleErrorSpy).toHaveBeenCalledWith( | ||
| expect.stringContaining( | ||
| `Warning: failed to read ${join(root, 'package.json')}`, | ||
| ), | ||
| ) | ||
|
|
||
| consoleErrorSpy.mockRestore() | ||
| }) |
There was a problem hiding this comment.
Guarantee mock restoration with try/finally.
Line [214] restoration won’t run if an earlier assertion fails. Wrap the test body so console.error is always restored.
Suggested fix
it('warns and falls back when package.json workspaces contains non-string entries', () => {
const root = createRoot()
const consoleErrorSpy = vi
.spyOn(console, 'error')
.mockImplementation(() => undefined)
-
- writeFileSync(
- join(root, 'package.json'),
- JSON.stringify({ workspaces: [123] }),
- )
- writeFileSync(
- join(root, 'deno.json'),
- JSON.stringify({ workspace: ['packages/*'] }),
- )
-
- expect(readWorkspacePatterns(root)).toEqual(['packages/*'])
- expect(consoleErrorSpy).toHaveBeenCalledWith(
- expect.stringContaining(
- `Warning: failed to read ${join(root, 'package.json')}`,
- ),
- )
-
- consoleErrorSpy.mockRestore()
+ try {
+ writeFileSync(
+ join(root, 'package.json'),
+ JSON.stringify({ workspaces: [123] }),
+ )
+ writeFileSync(
+ join(root, 'deno.json'),
+ JSON.stringify({ workspace: ['packages/*'] }),
+ )
+
+ expect(readWorkspacePatterns(root)).toEqual(['packages/*'])
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
+ expect.stringContaining(
+ `Warning: failed to read ${join(root, 'package.json')}`,
+ ),
+ )
+ } finally {
+ consoleErrorSpy.mockRestore()
+ }
})📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| it('warns and falls back when package.json workspaces contains non-string entries', () => { | |
| const root = createRoot() | |
| const consoleErrorSpy = vi | |
| .spyOn(console, 'error') | |
| .mockImplementation(() => undefined) | |
| writeFileSync( | |
| join(root, 'package.json'), | |
| JSON.stringify({ workspaces: [123] }), | |
| ) | |
| writeFileSync( | |
| join(root, 'deno.json'), | |
| JSON.stringify({ workspace: ['packages/*'] }), | |
| ) | |
| expect(readWorkspacePatterns(root)).toEqual(['packages/*']) | |
| expect(consoleErrorSpy).toHaveBeenCalledWith( | |
| expect.stringContaining( | |
| `Warning: failed to read ${join(root, 'package.json')}`, | |
| ), | |
| ) | |
| consoleErrorSpy.mockRestore() | |
| }) | |
| it('warns and falls back when package.json workspaces contains non-string entries', () => { | |
| const root = createRoot() | |
| const consoleErrorSpy = vi | |
| .spyOn(console, 'error') | |
| .mockImplementation(() => undefined) | |
| try { | |
| writeFileSync( | |
| join(root, 'package.json'), | |
| JSON.stringify({ workspaces: [123] }), | |
| ) | |
| writeFileSync( | |
| join(root, 'deno.json'), | |
| JSON.stringify({ workspace: ['packages/*'] }), | |
| ) | |
| expect(readWorkspacePatterns(root)).toEqual(['packages/*']) | |
| expect(consoleErrorSpy).toHaveBeenCalledWith( | |
| expect.stringContaining( | |
| `Warning: failed to read ${join(root, 'package.json')}`, | |
| ), | |
| ) | |
| } finally { | |
| consoleErrorSpy.mockRestore() | |
| } | |
| }) |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/intent/tests/workspace-patterns.test.ts` around lines 192 - 215, The
test that sets consoleErrorSpy in the "it('warns and falls back when
package.json workspaces contains non-string entries')" block must always restore
the mock; wrap the test body (everything after creating consoleErrorSpy and
before assertions) in a try/finally and call consoleErrorSpy.mockRestore()
inside the finally so the mock is restored even if an assertion (like
expect(readWorkspacePatterns(root)) or
expect(consoleErrorSpy).toHaveBeenCalledWith(...)) throws; reference the
existing consoleErrorSpy variable and the test block name to locate where to add
the try/finally.
Refactors workspace pattern discovery and reduces repeated workspace filesystem work in CLI paths.
jsonc-parserpnpm-workspace.yaml→package.json→deno.json→deno.jsoncdeno.json/deno.jsoncmanifests as workspace packagesgetWorkspaceInfo(root)and use it in stale target resolution so workspace package lists are resolved onceNotes
The JSONC parser change is primarily for correctness and maintainability. The CLI performance improvement comes from avoiding repeated workspace config reads, root discovery, and package resolution during a command, not from faster JSONC parsing.
Summary by CodeRabbit
Chores
New Features
Tests