Skip to content

Conversation

@LayZeeDK
Copy link
Member

@LayZeeDK LayZeeDK commented Oct 29, 2025

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

  • New index-exports-cache.ts with:
    • getIndexExports(tree, indexPath): regex parses export/re-export statements once per content snapshot
    • Content snapshot stored to auto-invalidate when file content changes via treeReadCache
    • clear / invalidate helpers (prepared for future wiring)
  • isFileExported now consults cache instead of building a fresh RegExp per entrypoint scan
  • Normalizes stored specifiers (ensures leading ./) for consistent lookup
  • No changes to public generator API; all tests unchanged

Benchmarks (ran 2025-10-29T20:53:33Z, updated at 2025-10-29T20:55:40.852Z)
Baseline (before):

  • Export detection: 56,768 ops/sec ±2.09%
  • Export addition: 61,458 ops/sec ±1.81%
  • Export removal: 56,077 ops/sec ±1.82%

Updated (after):

  • Export detection: 62,156 ops/sec ±1.66% (+~9.5%)
  • Export addition: 61,446 ops/sec ±1.30% (~parity)
  • Export removal: 62,213 ops/sec ±4.02% (+~11%)

Net Effect

  • Export presence checks (dominant path) show ~9–11% throughput improvement.
  • No regressions measured.
  • All 770 tests pass; lint and formatting pass.

Risk / Mitigation

  • Cache invalidation leverages existing treeReadCache invalidation when files are written; content snapshot comparison prevents stale data if file changes without explicit invalidation.
  • Limited scope: only read paths; write logic unaffected.

Follow-ups (optional)

  • Expose and wire clearIndexExportsCache() into global cache clearing utilities.
  • Expand regex to capture additional export patterns if needed (e.g., default export forwarding aliases).

Closes #161.

@LayZeeDK LayZeeDK changed the title perf(move-file): cache index file export analysis (#161) perf(move-file): cache index file export analysis Oct 29, 2025
@github-actions github-actions bot force-pushed the feat/161-index-exports-cache branch from 70f97e9 to a188b59 Compare October 29, 2025 21:00
@LayZeeDK LayZeeDK requested a review from Copilot October 29, 2025 21:03
Copy link
Contributor

Copilot AI left a 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.

Comment on lines 56 to 64
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);
}
Copy link

Copilot AI Oct 29, 2025

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.

Suggested change
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.

Copilot uses AI. Check for mistakes.

// Match: export * from './path'; OR export { ... } from './path'; OR export {default as X} from './path';
const reExportPattern =
/export\s+(?:\*|\{[^}]+\})\s+from\s+['"](\.\.?\/[^'";]+)['"];?/g;
Copy link

Copilot AI Oct 29, 2025

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.

Suggested change
/export\s+(?:\*|\{[^}]+\})\s+from\s+['"](\.\.?\/[^'";]+)['"];?/g;
/export\s+(?:\*|\{[^}]+\})\s+from\s+['"](\.(?:\.\/|\/)[^'";]+)['"];?/g;

Copilot uses AI. Check for mistakes.
Comment on lines 56 to 57
for (const spec of reexports) {
const withoutExt = spec.replace(/\.(ts|tsx|js|jsx)$/i, '');
Copy link

Copilot AI Oct 29, 2025

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.

Copilot uses AI. Check for mistakes.
@LayZeeDK
Copy link
Member Author

LayZeeDK commented Oct 29, 2025

Applied review feedback:

  • Separated reexports from exports (no conflation).
  • Added clarifying comments and left exports empty until local export parsing is implemented.
  • Updated isFileExported to rely solely on reexports.

All tests & lint pass.

LayZeeDK added a commit that referenced this pull request Oct 29, 2025
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

perf(move-file): cache index file export analysis

2 participants