Skip to content

Commit 9d97ddf

Browse files
committed
fix(core): sandbox exclusions, multi-line typeof import detection, global ensurePackage mock (#35056)
## Current Behavior 1. **Sandboxing false positives**: `tsc --build` reads `.tsbuildinfo` files as an optimization hint, and the `nx-plugin-checks` lint rule reads `schema.json` from `dist/` directories. Both are flagged as sandbox violations even though they don't affect caching correctness. 2. **Missing dependencies in project graph**: `typeof import('...')` inside multi-line generic type parameters (e.g. `ensurePackage<typeof import('@nx/playwright')>()`) is not detected by the import analyzer. The newline between `<` and `import()` resets the import type to Dynamic, so packages like `@nx/playwright` and `@nx/storybook` are missing from the dependency graph. 3. **ensurePackage mock duplication**: Multiple test files individually mock `@nx/devkit` just to override `ensurePackage` so it resolves from source instead of `node_modules`. This is repetitive and easy to miss in new tests. ## Expected Behavior 1. **Sandboxing**: `.tsbuildinfo` reads are globally excluded. `dist/**/*.json` reads are excluded for lint targets. 2. **Import analyzer**: `typeof import('...')` inside multi-line generics is correctly detected as a static import by tracking angle bracket depth and preserving import type across newlines inside generics. 3. **ensurePackage mock**: A global `ensurePackage` mock in `scripts/unit-test-setup.js` replaces per-file mocks, using `jest.requireActual` to resolve from source code. ## Related Issue(s) <!-- No directly related open issues found --> --------- Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com> (cherry picked from commit c59040f)
1 parent 9442c13 commit 9d97ddf

6 files changed

Lines changed: 83 additions & 25 deletions

File tree

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
exclude-reads:
2+
- packages/nx/src/native/*.node
3+
exclude-writes:
4+
- '**/.swc/**'
5+
6+
task-exclusions:
7+
- target: lint
8+
exclude-reads:
9+
- '**/dist/**/*.json'

packages/angular/src/generators/application/application.spec.ts

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -30,17 +30,6 @@ jest.mock('@nx/cypress/src/utils/versions', () => ({
3030
getInstalledCypressMajorVersion: jest.fn(),
3131
}));
3232
jest.mock('enquirer');
33-
jest.mock('@nx/devkit', () => {
34-
const original = jest.requireActual('@nx/devkit');
35-
return {
36-
...original,
37-
ensurePackage: (pkg: string) => jest.requireActual(pkg),
38-
createProjectGraphAsync: jest.fn().mockResolvedValue({
39-
nodes: {},
40-
dependencies: {},
41-
}),
42-
};
43-
});
4433

4534
describe('app', () => {
4635
let appTree: Tree;

packages/nx/src/native/plugins/js/ts_import_locators.rs

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ struct State<'a> {
5858
pub import_type: ImportType,
5959
open_brace_count: i128,
6060
open_bracket_count: i128,
61+
open_angle_count: i128,
6162
blocks_stack: Vec<BlockType>,
6263
next_block_type: BlockType,
6364
}
@@ -70,6 +71,7 @@ impl<'a> State<'a> {
7071
previous_token: None,
7172
open_brace_count: 0,
7273
open_bracket_count: 0,
74+
open_angle_count: 0,
7375
blocks_stack: vec![],
7476
next_block_type: BlockType::Block,
7577
import_type: ImportType::Dynamic,
@@ -127,8 +129,8 @@ impl<'a> State<'a> {
127129
let new_line = self.lexer.had_line_break_before_last();
128130

129131
// This is the beginning of a new statement, reset the import type to the default
130-
// Reset import type when there is new line not in braces
131-
if new_line && self.open_brace_count == 0 {
132+
// Reset import type when there is new line not in braces or angle brackets (generics)
133+
if new_line && self.open_brace_count == 0 && self.open_angle_count == 0 {
132134
self.import_type = ImportType::Dynamic;
133135
}
134136

@@ -214,11 +216,17 @@ impl<'a> State<'a> {
214216
_ if is_identifier(&t.token) => {
215217
// Generic
216218
self.import_type = ImportType::Static;
219+
self.open_angle_count += 1;
217220
}
218221
_ => {}
219222
}
220223
}
221224
}
225+
BinOpToken::Gt => {
226+
if self.open_angle_count > 0 {
227+
self.open_angle_count -= 1;
228+
}
229+
}
222230

223231
BinOpToken::Div => {
224232
if let Some(t) = &self.previous_token {
@@ -1492,6 +1500,63 @@ import(myTag`react@${version}`);
14921500
);
14931501
}
14941502

1503+
#[test]
1504+
fn should_find_typeof_import_in_ensure_package_pattern() {
1505+
let temp_dir = TempDir::new().unwrap();
1506+
temp_dir
1507+
.child("test.ts")
1508+
.write_str(
1509+
r#"
1510+
import { ensurePackage } from '@nx/devkit';
1511+
1512+
const { configurationGenerator } = ensurePackage<
1513+
typeof import('@nx/playwright')
1514+
>('@nx/playwright', nxVersion);
1515+
1516+
const { storybookGenerator } = ensurePackage<
1517+
typeof import('@nx/storybook')
1518+
>('@nx/storybook', nxVersion);
1519+
1520+
const { cypressGenerator } = ensurePackage<
1521+
typeof import('@nx/cypress')
1522+
>('@nx/cypress', nxVersion);
1523+
"#,
1524+
)
1525+
.unwrap();
1526+
1527+
let test_file_path = temp_dir.display().to_string() + "/test.ts";
1528+
1529+
let results = find_imports(HashMap::from([(
1530+
String::from("a"),
1531+
vec![test_file_path.clone()],
1532+
)]))
1533+
.unwrap();
1534+
1535+
let result = results.get(0).unwrap();
1536+
1537+
assert!(
1538+
result
1539+
.static_import_expressions
1540+
.contains(&String::from("@nx/playwright")),
1541+
"Should detect @nx/playwright from typeof import in generic. Found: {:?}",
1542+
result.static_import_expressions
1543+
);
1544+
assert!(
1545+
result
1546+
.static_import_expressions
1547+
.contains(&String::from("@nx/storybook")),
1548+
"Should detect @nx/storybook from typeof import in generic. Found: {:?}",
1549+
result.static_import_expressions
1550+
);
1551+
assert!(
1552+
result
1553+
.static_import_expressions
1554+
.contains(&String::from("@nx/cypress")),
1555+
"Should detect @nx/cypress from typeof import in generic. Found: {:?}",
1556+
result.static_import_expressions
1557+
);
1558+
}
1559+
14951560
#[test]
14961561
#[ignore]
14971562
fn should_find_imports_properly_for_all_files_in_nx_repo() {

packages/web/src/generators/application/application.legacy.spec.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,6 @@ jest.mock('@nx/cypress/src/utils/versions', () => ({
1111
...jest.requireActual('@nx/cypress/src/utils/versions'),
1212
getInstalledCypressMajorVersion: jest.fn(),
1313
}));
14-
jest.mock('@nx/devkit', () => {
15-
return {
16-
...jest.requireActual('@nx/devkit'),
17-
ensurePackage: jest.fn((pkg) => jest.requireActual(pkg)),
18-
};
19-
});
2014
describe('web app generator (legacy)', () => {
2115
let tree: Tree;
2216
let mockedInstalledCypressVersion: jest.Mock<

packages/web/src/generators/application/application.spec.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,6 @@ jest.mock('@nx/cypress/src/utils/versions', () => ({
2222
...jest.requireActual('@nx/cypress/src/utils/versions'),
2323
getInstalledCypressMajorVersion: jest.fn(),
2424
}));
25-
jest.mock('@nx/devkit', () => {
26-
return {
27-
...jest.requireActual('@nx/devkit'),
28-
ensurePackage: jest.fn((pkg) => jest.requireActual(pkg)),
29-
};
30-
});
3125

3226
describe('app', () => {
3327
let tree: Tree;

scripts/unit-test-setup.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,5 +30,12 @@ module.exports = () => {
3030
dependencies: {},
3131
};
3232
}),
33+
/**
34+
* `ensurePackage` calls `require(pkg)` which resolves from node_modules
35+
* (the installed version) instead of the local source code. Using
36+
* `jest.requireActual` routes through Jest's module resolver which
37+
* respects tsconfig paths, so it picks up the source code instead.
38+
*/
39+
ensurePackage: jest.fn((pkg) => jest.requireActual(pkg)),
3340
}));
3441
};

0 commit comments

Comments
 (0)