Skip to content

Commit d608a46

Browse files
authored
fix(move-file): parse local exports in index export cache (#318)
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.
2 parents 8aea760 + 4b37a27 commit d608a46

File tree

11 files changed

+868
-42
lines changed

11 files changed

+868
-42
lines changed
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import {
2+
beforeAll,
3+
beforeAllIterations,
4+
describe,
5+
it,
6+
beforeCycle,
7+
afterCycle,
8+
} from '../../../../../../tools/tinybench-utils';
9+
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
10+
import type { Tree } from '@nx/devkit';
11+
import {
12+
getIndexExports,
13+
clearIndexExportsCache,
14+
} from '../export-management/index-exports-cache';
15+
16+
describe('Index Exports Cache', () => {
17+
let tree: Tree;
18+
let entryPoint: string;
19+
20+
// Move expensive tree creation to suite-level beforeAll
21+
beforeAll(() => {
22+
tree = createTreeWithEmptyWorkspace();
23+
entryPoint = 'libs/my-lib/src/index.ts';
24+
});
25+
26+
// Clear cache before each benchmark cycle
27+
beforeCycle(() => {
28+
clearIndexExportsCache();
29+
});
30+
31+
afterCycle(() => {
32+
clearIndexExportsCache();
33+
});
34+
35+
describe('Parse re-exports only', () => {
36+
beforeAllIterations(() => {
37+
tree.write(
38+
entryPoint,
39+
`export * from './lib/file1';
40+
export * from './lib/file2';
41+
export * from './lib/file3';
42+
export { foo, bar } from './lib/file4';`,
43+
);
44+
});
45+
46+
it('should parse re-exports', () => {
47+
getIndexExports(tree, entryPoint);
48+
});
49+
});
50+
51+
describe('Parse local named exports only', () => {
52+
beforeAllIterations(() => {
53+
tree.write(
54+
entryPoint,
55+
`export const FOO = 'foo';
56+
export const BAR = 'bar';
57+
export function myFunction() {}
58+
export class MyClass {}
59+
export interface IUser {}
60+
export type UserId = string;`,
61+
);
62+
});
63+
64+
it('should parse local named exports', () => {
65+
getIndexExports(tree, entryPoint);
66+
});
67+
});
68+
69+
describe('Parse mixed exports', () => {
70+
beforeAllIterations(() => {
71+
tree.write(
72+
entryPoint,
73+
`export * from './lib/utils';
74+
export { foo } from './lib/helpers';
75+
export const LOCAL_CONST = 'value';
76+
export function localFn() {}
77+
export class LocalClass {}
78+
const internal = 1;
79+
export { internal as exposed };
80+
export default LocalClass;`,
81+
);
82+
});
83+
84+
it('should parse mixed exports', () => {
85+
getIndexExports(tree, entryPoint);
86+
});
87+
});
88+
89+
describe('Cache hit performance', () => {
90+
beforeAllIterations(() => {
91+
tree.write(
92+
entryPoint,
93+
`export * from './lib/file1';
94+
export const FOO = 'foo';
95+
export function myFn() {}`,
96+
);
97+
// Prime the cache
98+
getIndexExports(tree, entryPoint);
99+
});
100+
101+
it('should retrieve from cache', () => {
102+
getIndexExports(tree, entryPoint);
103+
});
104+
});
105+
106+
describe('Large file with many exports', () => {
107+
beforeAllIterations(() => {
108+
// Generate a large index file with 50 mixed exports
109+
const lines: string[] = [];
110+
for (let i = 0; i < 25; i++) {
111+
lines.push(`export * from './lib/module${i}';`);
112+
lines.push(`export const CONST_${i} = ${i};`);
113+
}
114+
tree.write(entryPoint, lines.join('\n'));
115+
});
116+
117+
it('should parse large file with many exports', () => {
118+
getIndexExports(tree, entryPoint);
119+
});
120+
});
121+
122+
describe('Empty file', () => {
123+
beforeAllIterations(() => {
124+
tree.write(entryPoint, '');
125+
});
126+
127+
it('should handle empty file', () => {
128+
getIndexExports(tree, entryPoint);
129+
});
130+
});
131+
});

packages/workspace/src/generators/move-file/cache/clear-all-caches.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import { treeReadCache } from '../tree-cache';
2+
import { clearIndexExportsCache } from '../export-management/index-exports-cache';
3+
import { astCache } from '../ast-cache';
24

35
/**
46
* Clears all caches. Should be called when starting a new generator operation
@@ -10,6 +12,8 @@ import { treeReadCache } from '../tree-cache';
1012
* - Compiler paths cache
1113
* - Tree read cache
1214
* - Dependency graph cache
15+
* - Index exports cache
16+
* - AST cache
1317
*
1418
* @param projectSourceFilesCache - Cache for source files per project
1519
* @param fileExistenceCache - Cache for file existence checks
@@ -27,4 +31,6 @@ export function clearAllCaches(
2731
compilerPathsCache.value = undefined;
2832
treeReadCache.clear();
2933
dependencyGraphCache.clear();
34+
clearIndexExportsCache();
35+
astCache.clear();
3036
}

packages/workspace/src/generators/move-file/export-management/README.md

Lines changed: 70 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -8,42 +8,71 @@ This module provides functions for managing exports in project entry points (ind
88

99
## Functions
1010

11-
- **add-export-to-entrypoint.ts** - Add an export statement to a project's entry point file
12-
- **is-exported-from-project.ts** - Check if a file is exported from a project's entry point
13-
- **remove-export-from-entrypoint.ts** - Remove an export statement from a project's entry point
11+
- **index-exports-cache.ts** - Cache system for parsing and storing export information (both re-exports and local exports) from index files
12+
- **is-file-exported.ts** - Check if a file is re-exported from a project's entry point
13+
- **ensure-file-exported.ts** - Add an export statement to a project's entry point if not already present
14+
- **remove-file-export.ts** - Remove export statements for a file from a project's entry point
1415
- **should-export-file.ts** - Determine if a file should be exported based on generator options
15-
- **update-exports-in-entry-points.ts** - Update exports in both source and target entry points
16+
- **ensure-export-if-needed.ts** - Conditionally export a file based on strategy
17+
18+
## Index Exports Cache
19+
20+
The `index-exports-cache` module provides a cache system for parsing export information from index/entrypoint files. It supports:
21+
22+
### Export Detection
23+
24+
1. **Re-exports** (`reexports` Set)
25+
- `export * from './lib/utils'`
26+
- `export { foo, bar } from './lib/helpers'`
27+
- `export type { User } from './types'`
28+
29+
2. **Local Named Exports** (`exports` Set)
30+
- Named declarations: `export const FOO = ...`, `export function fn() {}`, `export class C {}`
31+
- TypeScript declarations: `export interface I {}`, `export type T = ...`, `export enum E {}`
32+
- Named lists: `export { a, b as c }` (without `from`)
33+
34+
3. **Default Exports** (`defaultExport` optional string)
35+
- Named: `export default MyComponent`
36+
- Anonymous: `export default function() {}`
37+
- Expressions: `export default { ... }`
38+
39+
### API
40+
41+
```typescript
42+
import { getIndexExports } from './index-exports-cache';
43+
44+
const exports = getIndexExports(tree, 'libs/mylib/src/index.ts');
45+
46+
// Check re-exports
47+
exports.reexports.has('./lib/utils'); // boolean
48+
49+
// Check local exports
50+
exports.exports.has('MyClass'); // boolean
51+
52+
// Check default export
53+
exports.defaultExport === 'MyComponent'; // boolean
54+
```
1655

1756
## Usage
1857

1958
```typescript
20-
import { isExportedFromProject } from './export-management/is-exported-from-project';
21-
import { addExportToEntrypoint } from './export-management/add-export-to-entrypoint';
22-
import { removeExportFromEntrypoint } from './export-management/remove-export-from-entrypoint';
59+
import { isFileExported } from './export-management/is-file-exported';
60+
import { ensureFileExported } from './export-management/ensure-file-exported';
61+
import { removeFileExport } from './export-management/remove-file-export';
2362

2463
// Check if file is exported
25-
const isExported = isExportedFromProject(
64+
const isExported = isFileExported(
2665
tree,
27-
sourceFilePath,
28-
sourceProject,
29-
entryPointPaths,
66+
project,
67+
'lib/utils.ts',
68+
cachedTreeExists,
3069
);
3170

32-
// Add export to target project
33-
addExportToEntrypoint(
34-
tree,
35-
targetFilePath,
36-
targetProject,
37-
targetEntryPointPaths,
38-
);
71+
// Add export to project
72+
ensureFileExported(tree, project, 'lib/new-file.ts', cachedTreeExists);
3973

40-
// Remove export from source project
41-
removeExportFromEntrypoint(
42-
tree,
43-
sourceFilePath,
44-
sourceProject,
45-
sourceEntryPointPaths,
46-
);
74+
// Remove export from project
75+
removeFileExport(tree, project, 'lib/old-file.ts', cachedTreeExists);
4776
```
4877

4978
## Export Patterns
@@ -54,6 +83,7 @@ This module handles various export patterns:
5483
- **Wildcard exports**: `export * from './components/button';`
5584
- **Default exports**: `export { default as Button } from './components/button';`
5685
- **Barrel files**: Index files that re-export multiple modules
86+
- **Local exports**: `export const FOO = ...`, `export function fn() {}`, etc.
5787

5888
## Entry Point Detection
5989

@@ -68,12 +98,24 @@ The module finds entry points by:
6898

6999
All export management functions have comprehensive unit tests covering:
70100

71-
- Export detection in various formats
101+
- Export detection in various formats (re-exports and local exports)
72102
- Export addition with proper formatting
73103
- Export removal without breaking other exports
104+
- Cache functionality and invalidation
74105
- Edge cases (no entry point, duplicate exports, etc.)
75106

76-
Total: **52 tests**
107+
**Total: 80+ tests** including comprehensive index-exports-cache coverage
108+
109+
## Performance
110+
111+
The index exports cache uses jscodeshift for AST parsing, which provides:
112+
113+
- Accurate parsing of all export patterns
114+
- Automatic caching to avoid re-parsing
115+
- Integration with existing AST cache
116+
- Efficient cache invalidation when files change
117+
118+
See `../benchmarks/index-exports-cache.bench.ts` for performance benchmarks.
77119

78120
## Formatting
79121

@@ -89,3 +131,4 @@ Export updates preserve code formatting:
89131
- [Core Operations](../core-operations/README.md) - Orchestrates export updates
90132
- [Project Analysis](../project-analysis/README.md) - Provides entry point paths
91133
- [Import Updates](../import-updates/README.md) - Complementary import updates
134+
- [Benchmarks](../benchmarks/README.md) - Performance benchmarks including export management

packages/workspace/src/generators/move-file/export-management/ensure-file-exported.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { logger } from '@nx/devkit';
44
import { getProjectEntryPointPaths } from '../project-analysis/get-project-entry-point-paths';
55
import { removeSourceFileExtension } from '../path-utils/remove-source-file-extension';
66
import { treeReadCache } from '../tree-cache';
7+
import { invalidateIndexExportsCache } from './index-exports-cache';
8+
import { astCache } from '../ast-cache';
79

810
/**
911
* Ensures the file is exported from the target project's entrypoint.
@@ -42,6 +44,8 @@ export function ensureFileExported(
4244
content += exportStatement;
4345
tree.write(indexPath, content);
4446
treeReadCache.invalidateFile(indexPath);
47+
invalidateIndexExportsCache(indexPath);
48+
astCache.invalidate(indexPath);
4549
logger.verbose(`Added export to ${indexPath}`);
4650
}
4751
}

0 commit comments

Comments
 (0)