Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions baseline-export-bench.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
ÔùÅ Validation Warning:

Unknown option "reporters" with value ["default"] was found.
This is probably a typing mistake. Fixing it will remove this message.

Configuration Documentation:
https://jestjs.io/docs/configuration

console.log
[Export Management > Export detection] should detect exports x 56,768 ops/sec ┬▒2.09% (48553 runs sampled)

at Object.<anonymous> (tools/tinybench-utils.ts:1323:17)

console.log
[Export Management > Export addition] should add exports x 61,458 ops/sec ┬▒1.81% (54844 runs sampled)

at Object.<anonymous> (tools/tinybench-utils.ts:1323:17)

console.log
[Export Management > Export removal] should remove exports x 56,077 ops/sec ┬▒1.82% (50026 runs sampled)

at Object.<anonymous> (tools/tinybench-utils.ts:1323:17)

PASS benchmarks packages/workspace/src/generators/move-file/benchmarks/export-management.bench.ts
Export Management
Export detection
 should detect exports (25 ms)
Export addition
 should add exports (1 ms)
Export removal
 should remove exports

Test Suites: 1 passed, 1 total
Tests: 3 passed, 3 total
Snapshots: 0 total
Time: 4.658 s, estimated 5 s
Ran all test suites matching /export-management/i.
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import type { Tree } from '@nx/devkit';
import { treeReadCache } from '../tree-cache';

/**
* Cached export information for index (entrypoint) files to avoid reparsing.
*/
export interface IndexExports {
exports: Set<string>; // direct exports (file paths without extension)
reexports: Set<string>; // re-exported modules (file paths without extension)
}

interface CachedIndexExports extends IndexExports {
content: string; // content snapshot used to build this cache entry
}

// Internal cache keyed by normalized index file path
const indexExportsCache = new Map<string, CachedIndexExports>();

/** Clears all cached index export data. */
export function clearIndexExportsCache(): void {
indexExportsCache.clear();
}

/** Invalidates a single index file from the cache (e.g., after write). */
export function invalidateIndexExportsCacheEntry(indexPath: string): void {
indexExportsCache.delete(indexPath);
}

/**
* Get (and cache) export info for an index/entrypoint file.
* Lightweight regex based extraction – sufficient for current export patterns.
*/
export function getIndexExports(tree: Tree, indexPath: string): IndexExports {
const content = treeReadCache.read(tree, indexPath, 'utf-8') || '';

const cached = indexExportsCache.get(indexPath);
if (cached && cached.content === content) return cached;

const exports = new Set<string>(); // local exports (currently none parsed)
const reexports = new Set<string>(); // export ... from / export * from specifiers

// 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.
// Match: export * from './path'; specially capture star exports for potential future distinction
// Simple capture group for path without extension processing here

let match: RegExpExecArray | null;
while ((match = reExportPattern.exec(content))) {
const spec = match[1];
reexports.add(spec);
Copy link

Copilot AI Nov 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The captured path specifiers are stored as-is without removing file extensions, but fileWithoutExt in is-file-exported.ts has extensions removed. This creates an inconsistency: if index files contain export * from './utils.ts', the cache stores './utils.ts' but the comparison checks for './utils'. Either strip extensions when storing in cache or when comparing, ensuring both sides match.

Suggested change
reexports.add(spec);
// Remove file extension (e.g., .ts, .js, .jsx, .tsx) from specifier
const specWithoutExt = spec.replace(/\.[tj]sx?$/, '');
reexports.add(specWithoutExt);

Copilot uses AI. Check for mistakes.
}

// Note: We do NOT add reexports to the exports set to avoid conflating concepts.
// exports holds normalized local export specifiers (if/when we add direct export collection logic).
// For current patterns (only re-exports), exports remains empty; callers may consult reexports directly.
// Future enhancement: parse local declarations (e.g. export { foo, bar }; without 'from').

const result: CachedIndexExports = { exports, reexports, content };
indexExportsCache.set(indexPath, result);
return result;
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import type { Tree } from '@nx/devkit';
import type { ProjectConfiguration } from '@nx/devkit';
import { getProjectEntryPointPaths } from '../project-analysis/get-project-entry-point-paths';
import { removeSourceFileExtension } from '../path-utils/remove-source-file-extension';
import { escapeRegex } from '../security-utils/escape-regex';

import { treeReadCache } from '../tree-cache';
import { getIndexExports } from './index-exports-cache';

/**
* Checks if a file is exported from the project's entrypoint.
Expand Down Expand Up @@ -31,7 +32,6 @@ export function isFileExported(
const indexPaths = getProjectEntryPointPaths(tree, project);

const fileWithoutExt = removeSourceFileExtension(file);
const escapedFile = escapeRegex(fileWithoutExt);

return indexPaths.some((indexPath) => {
if (!cachedTreeExists(tree, indexPath)) {
Expand All @@ -41,12 +41,10 @@ export function isFileExported(
if (!content) {
return false;
}
// Support: export ... from "path"
// Support: export * from "path"
// Support: export { Something } from "path"
const exportPattern = new RegExp(
`export\\s+(?:\\*|\\{[^}]+\\}|.+)\\s+from\\s+['"]\\.?\\.?/.*${escapedFile}['"]`,
);
return exportPattern.test(content);
// Use cached export analysis for index file
const indexExports = getIndexExports(tree, indexPath);
// Compare against file path without extension (as stored)
// Since local exports are not yet collected, rely on reexports for detection.
Copy link

Copilot AI Nov 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment on line 47 states 'Since local exports are not yet collected, rely on reexports for detection' but doesn't explain why local exports are excluded or what impact this has. Add clarification that this currently only detects re-exported files (export...from statements) and won't detect files with direct exports in the index that don't use re-export syntax.

Suggested change
// Since local exports are not yet collected, rely on reexports for detection.
// Since local exports are not yet collected, rely on reexports for detection.
// NOTE: This currently only detects files that are re-exported using `export ... from` statements.
// Files that are directly exported in the index (without using re-export syntax) will NOT be detected.

Copilot uses AI. Check for mistakes.
return indexExports.reexports.has(`./${fileWithoutExt}`);
Copy link

Copilot AI Nov 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The export check always prefixes with ./, but the cached reexports may contain paths starting with ../ (parent directory references). This will cause false negatives when checking if files exported via parent-relative paths (e.g., export * from '../sibling/file') are detected. The path normalization should account for both ./ and ../ patterns or normalize paths consistently before comparison.

Copilot uses AI. Check for mistakes.
});
}
37 changes: 37 additions & 0 deletions updated-export-bench.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
ÔùÅ Validation Warning:

Unknown option "reporters" with value ["default"] was found.
This is probably a typing mistake. Fixing it will remove this message.

Configuration Documentation:
https://jestjs.io/docs/configuration

console.log
[Export Management > Export detection] should detect exports x 62,156 ops/sec ┬▒1.66% (55877 runs sampled)

at Object.<anonymous> (tools/tinybench-utils.ts:1323:17)

console.log
[Export Management > Export addition] should add exports x 61,446 ops/sec ┬▒1.30% (54407 runs sampled)

at Object.<anonymous> (tools/tinybench-utils.ts:1323:17)

console.log
[Export Management > Export removal] should remove exports x 62,213 ops/sec ┬▒4.02% (56818 runs sampled)

at Object.<anonymous> (tools/tinybench-utils.ts:1323:17)

PASS benchmarks packages/workspace/src/generators/move-file/benchmarks/export-management.bench.ts
Export Management
Export detection
 should detect exports (13 ms)
Export addition
 should add exports (1 ms)
Export removal
 should remove exports (1 ms)

Test Suites: 1 passed, 1 total
Tests: 3 passed, 3 total
Snapshots: 0 total
Time: 4.375 s, estimated 5 s
Ran all test suites matching /export-management/i.