-
-
Notifications
You must be signed in to change notification settings - Fork 0
perf(move-file): cache index file export analysis #316
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
70f97e9 to
a188b59
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
Adds a caching mechanism for index file export analysis to improve performance in move-file operations by avoiding repeated regex parsing of entrypoint files.
- Introduces an index exports cache that stores parsed export/re-export specifiers with content-based invalidation
- Replaces per-call regex compilation with cached export analysis for ~9-11% performance improvement
- Maintains existing API compatibility with no changes to public interfaces or test behavior
Reviewed Changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| index-exports-cache.ts | New cache implementation with regex-based export parsing and content-keyed invalidation |
| is-file-exported.ts | Updated to use cached export analysis instead of inline regex compilation |
| updated-export-bench.txt | Performance benchmark results showing improved throughput |
| baseline-export-bench.txt | Baseline performance metrics for comparison |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| for (const spec of reexports) { | ||
| const withoutExt = spec.replace(/\.(ts|tsx|js|jsx)$/i, ''); | ||
| // Ensure stored paths are normalized to start with './' | ||
| const normalized = | ||
| withoutExt.startsWith('./') || withoutExt.startsWith('../') | ||
| ? withoutExt | ||
| : `./${withoutExt}`; | ||
| exports.add(normalized); | ||
| } |
Copilot
AI
Oct 29, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The logic adds reexports to the exports set, which conflates two different types of exports. The reexports should remain in the reexports set, and exports should contain actual local exports from the file.
| for (const spec of reexports) { | |
| const withoutExt = spec.replace(/\.(ts|tsx|js|jsx)$/i, ''); | |
| // Ensure stored paths are normalized to start with './' | |
| const normalized = | |
| withoutExt.startsWith('./') || withoutExt.startsWith('../') | |
| ? withoutExt | |
| : `./${withoutExt}`; | |
| exports.add(normalized); | |
| } | |
| // Note: Do not add reexports to exports. Exports should only contain actual local exports. | |
| // If needed, add logic here to capture local exports (e.g., export { foo, bar };) in the future. |
|
|
||
| // Match: export * from './path'; OR export { ... } from './path'; OR export {default as X} from './path'; | ||
| const reExportPattern = | ||
| /export\s+(?:\*|\{[^}]+\})\s+from\s+['"](\.\.?\/[^'";]+)['"];?/g; |
Copilot
AI
Oct 29, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The regex pattern allows paths that don't start with './' or '../' by using \.\.?\/ which makes the second dot optional. This could match invalid paths like ./x incorrectly. The pattern should be (\.\.?\/[^'";]+) or more specifically (\.(?:\.\/|\/)[^'";]+) to properly match relative paths.
| /export\s+(?:\*|\{[^}]+\})\s+from\s+['"](\.\.?\/[^'";]+)['"];?/g; | |
| /export\s+(?:\*|\{[^}]+\})\s+from\s+['"](\.(?:\.\/|\/)[^'";]+)['"];?/g; |
| for (const spec of reexports) { | ||
| const withoutExt = spec.replace(/\.(ts|tsx|js|jsx)$/i, ''); |
Copilot
AI
Oct 29, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The file extension removal logic duplicates functionality that likely exists elsewhere in the codebase (e.g., removeSourceFileExtension that's imported in the other file). Consider extracting this to a shared utility or reusing existing functionality.
|
Applied review feedback:
All tests & lint pass. |
Implementing index export cache to parse local exports in addition to
re-exports
## Plan
- [x] Create IndexExports type and interface in export-management
- [x] Create getIndexExports function to parse local exports using AST
- [x] Update isFileExported to use the new cache system
- [x] Add comprehensive unit tests for getIndexExports
- [x] Add benchmark tests for performance validation
- [x] Update existing tests to cover local export scenarios
- [x] Run linting, build, and all tests
- [x] Format code
- [x] Update documentation
- [x] Address code review feedback
- [x] Update JSDoc comments to reflect cache usage
## Recent Changes
Updated documentation in `isFileExported()` to clarify:
- Function now uses the index exports cache system
- Cache parses local declarations but function only checks re-exports
- This addresses review feedback about outdated documentation
The distinction is important: `isFileExported()` checks if a **file** is
re-exported (e.g., `export * from './lib/utils'`), not if **symbols**
from that file are present as local exports in the index.
<!-- START COPILOT CODING AGENT SUFFIX -->
<details>
<summary>Original prompt</summary>
----
*This section details on the original issue you should resolve*
<issue_title>feat(move-file): parse local exports in index export
cache</issue_title>
<issue_description>## Summary
Enhance the index exports cache to parse and record *local* export
declarations in entrypoint (index) files in addition to existing
re-export ("export ... from") patterns.
Currently `getIndexExports()` only captures reexports and leaves
`exports` empty, forcing callers (e.g. `isFileExported`) to rely solely
on `reexports`. This prevents accurate detection of locally defined
exports (e.g. `export { foo, bar }`, `export const X = ...`, `export
function fn() {}`, `export class C {}`, and `export default ...`).
Supporting these will improve correctness of move-file validation
(unexported dependency checks, alias conversions) and reduce false
warnings.
## Motivation / Problem
When a project’s public surface is defined via local declarations in
`index.ts` rather than re-exporting from leaf modules, the current cache
misses them. As a result:
- `isFileExported()` incorrectly reports a file as unexported if it’s
aggregated via local declarations copied into the index.
- Move-file generator may warn about supposedly unexported relative
dependencies.
- Potential future optimizations (e.g., differentiating default vs named
exports) are blocked.
## Scope
Add robust parsing of local export declarations for
TypeScript/JavaScript index files, keeping the lightweight approach
(regex or jscodeshift) while maintaining performance. Preserve
separation between `exports` and `reexports` (per review feedback in PR
#316).
## Proposed Design
1. Reuse existing AST caching (jscodeshift + `astCache`) for structural
accuracy while minimizing parse cost.
2. Collect and normalize local export specifiers:
- Named declarations: `export const A`, `export function b`, `export
class C`, `export interface D`, `export type E`, `export enum F`.
- Named list: `export { a, b as c }` (without `from`).
- Default exports: `export default X`, `export default function() {}`,
`export default class {}`.
3. Represent cache structure:
```ts
interface IndexExports {
exports: Set<string>; // local named export identifiers (normalized)
reexports: Set<string>; // existing re-export specifiers ('./lib/util')
defaultExport?: string; // identifier or synthetic marker
('<anonymous>') if unnamed default
}
```
4. Normalization Rules:
- Local identifiers stored exactly as defined (no extension logic
needed).
- Anonymous default exports mapped to a sentinel (e.g. `<default>` or
`<anonymous-default>`).
5. Backwards compatibility: existing callers using `reexports` remain
unchanged; add helper `isLocalSymbolExported(name)` if needed later.
6. Performance Strategy:
- Parse AST once via `astCache.getAST`; fall back to regex only if AST
unavailable.
- Skip expensive symbol resolution—pure syntactic discovery.
- Maintain content snapshot invalidation (existing logic).
7. Update `isFileExported` logic:
- If import specifier points to a file path: keep current re-export
check.
- If validation wants to infer export of an internal symbol (future),
can consult `exports` set.
## Tasks
- [ ] Extend `IndexExports` interface (add `defaultExport` field).
- [ ] Implement local export parsing function
(`collectLocalExports(ast): { names: Set<string>; default?: string }`).
- [ ] Integrate parsing into `getIndexExports` (only when content
contains `export`).
- [ ] Add unit tests covering:
- Basic named exports (const/function/class/interface/type/enum).
- Named list / alias exports (with `as`).
- Default exports (named and anonymous).
- Mixed local + re-exports in same file.
- Empty index file (returns empty sets, undefined default).
- Cache hit behavior (content unchanged).
- Cache invalidation (content change updates sets).
- [ ] Update `is-file-exported.spec.ts` to include a scenario with local
declarations.
- [ ] Add benchmark cases to `export-management.bench.ts` for local
export detection (ensure negligible regression; target <5% throughput
impact).
- [ ] Update PR/Documentation: mention difference between local exports
and reexports.
## Acceptance Criteria
- All new tests pass; existing 770 tests remain green.
- Benchmarks show <5% performance degradation for export detection suite
or an offsetting improvement if AST parsing reduces regex overhead.
- `isFileExported` correctly returns true for files represented by local
declarations (when those files exist or are logically aggregated).
- No conflation between `exports` and `reexports`; clear docs in
`index-exports-cache.ts`.
## Performance Considerations
- AST parsing is already cached; incremental cost is node filtering for
export declarations.
- Provide fallback to fast regex ONLY if AST is unavailable (e.g., parse
error).
- Keep operations allocation-light (pre-size arrays where possible,
avoid repeated string normaliz...
</details>
- Fixes #317
<!-- START COPILOT CODING AGENT TIPS -->
---
💡 You can make Copilot smarter by setting up custom instructions,
customizing its development environment and configuring Model Context
Protocol (MCP) servers. Learn more [Copilot coding agent
tips](https://gh.io/copilot-coding-agent-tips) in the docs.
perf(move-file): cache index file export analysis (#161)
Summary
Adds an index exports cache to avoid reparsing entrypoint (index) files for each export check during move-file operations. Replaces repeated regex scans with a cached, content-keyed extraction of export/re-export specifiers.
Implementation
Benchmarks (ran 2025-10-29T20:53:33Z, updated at 2025-10-29T20:55:40.852Z)
Baseline (before):
Updated (after):
Net Effect
Risk / Mitigation
Follow-ups (optional)
Closes #161.