From 209c2b86ebd5f1504f1cd467a859aa49bb06d096 Mon Sep 17 00:00:00 2001 From: Jason Jean Date: Sat, 21 Mar 2026 00:19:51 -0400 Subject: [PATCH] fix(core): skip import-equals namespace aliases in native scanner The Rust import scanner was mishandling TypeScript's `import X = Y.Z` namespace alias syntax. After encountering this pattern, it would scan forward for string literals and treat them as module specifiers, causing phantom npm dependencies on case-insensitive filesystems (macOS). Now the scanner checks if an identifier after `import` is followed by `=`. If so, it distinguishes `import X = require('module')` (valid CommonJS import) from `import X = Y.Z` (namespace alias to skip). Fixes #34644 --- .../native/plugins/js/ts_import_locators.rs | 100 +++++++++++++++++- 1 file changed, 99 insertions(+), 1 deletion(-) diff --git a/packages/nx/src/native/plugins/js/ts_import_locators.rs b/packages/nx/src/native/plugins/js/ts_import_locators.rs index f0a299b04b0..18a0db7b8d1 100644 --- a/packages/nx/src/native/plugins/js/ts_import_locators.rs +++ b/packages/nx/src/native/plugins/js/ts_import_locators.rs @@ -384,7 +384,29 @@ fn find_specifier_in_import(state: &mut State) -> Option<(String, ImportType)> { } } } - _ => {} + _ => { + // Check if this is an `import X = ...` statement + // `import X = require('module')` is a valid CommonJS import + // `import X = Y.Z` is a namespace alias, NOT a module import + if let Some(maybe_eq) = state.next() { + if let Token::AssignOp(_) = &maybe_eq.token { + // After `=`, check if followed by `require(` + if let Some(maybe_require) = state.next() { + match &maybe_require.token { + Token::Word(Ident(i)) if i == "require" => { + // import X = require('module') -- continue to find specifier + } + _ => { + // import X = Y.Z -- namespace alias, skip + return None; + } + } + } + } + // If not `=`, token was consumed but the while loop below + // will continue scanning from the current position + } + } }, // Matches: import 'a'; Token::Str { value, .. } => { @@ -1517,6 +1539,82 @@ import(myTag`react@${version}`); } } + #[test] + fn should_not_treat_import_equals_namespace_alias_as_module_import() { + let temp_dir = TempDir::new().unwrap(); + temp_dir + .child("test.ts") + .write_str( + r#" + import { Component } from '@angular/core'; + + export namespace Foo { + export enum Bar { A = 'A' } + } + + import MyBar = Foo.Bar; + + export type LogLevel = 'Debug' | 'Info' | 'Warn' | 'Error'; + + export function getLevel(): LogLevel { + return 'Debug'; + } + + switch (value) { + case 'Open': + break; + } + "#, + ) + .unwrap(); + + let test_file_path = temp_dir.display().to_string() + "/test.ts"; + + let results = find_imports(HashMap::from([( + String::from("a"), + vec![test_file_path.clone()], + )])) + .unwrap(); + + let result = results.get(0).unwrap(); + + // Only '@angular/core' should be detected -- not 'Debug', 'Open', 'Foo', etc. + assert_eq!( + result.static_import_expressions, + vec![String::from("@angular/core")] + ); + assert_eq!(result.dynamic_import_expressions, Vec::::new()); + } + + #[test] + fn should_still_detect_import_equals_require_as_module_import() { + let temp_dir = TempDir::new().unwrap(); + temp_dir + .child("test.ts") + .write_str( + r#" + import path = require('path'); + import { readFile } from 'fs'; + "#, + ) + .unwrap(); + + let test_file_path = temp_dir.display().to_string() + "/test.ts"; + + let results = find_imports(HashMap::from([( + String::from("a"), + vec![test_file_path.clone()], + )])) + .unwrap(); + + let result = results.get(0).unwrap(); + + assert_eq!( + result.static_import_expressions, + vec![String::from("path"), String::from("fs")] + ); + } + // This function finds imports with the ast which verifies that the imports we find are the same as the ones typescript finds fn find_imports_with_ast(file_path: String) -> anyhow::Result { let cm = Arc::::default()