* Check if an import is external (npm package, etc.) * * `context` is consulted for project-defined path aliases * (tsconfig/jsconfig `paths`). Without that check, custom prefixes * like `@components/*` would fail the bare-specifier heuristic and * be classified as external before alias resolut
( importPath: string, language: Language, context?: ResolutionContext )
| 296 | * be classified as external before alias resolution can run. |
| 297 | */ |
| 298 | function isExternalImport( |
| 299 | importPath: string, |
| 300 | language: Language, |
| 301 | context?: ResolutionContext |
| 302 | ): boolean { |
| 303 | // Relative imports are not external |
| 304 | if (importPath.startsWith('.')) { |
| 305 | return false; |
| 306 | } |
| 307 | |
| 308 | // Workspace-member imports (`@scope/ui`, `@scope/ui/widgets`) are LOCAL to |
| 309 | // a monorepo even though they look like bare npm specifiers. Consult the |
| 310 | // workspace map first so they aren't misclassified as external (#629). The |
| 311 | // map is null for single-package repos, so this is a no-op there. |
| 312 | const workspaces = context?.getWorkspacePackages?.(); |
| 313 | if (workspaces && resolveWorkspaceImport(importPath, workspaces)) { |
| 314 | return false; |
| 315 | } |
| 316 | |
| 317 | // Common external patterns |
| 318 | if (language === 'typescript' || language === 'javascript' || language === 'tsx' || language === 'jsx' || language === 'arkts') { |
| 319 | // Node built-ins |
| 320 | if (['fs', 'path', 'os', 'crypto', 'http', 'https', 'url', 'util', 'events', 'stream', 'child_process', 'buffer'].includes(importPath)) { |
| 321 | return true; |
| 322 | } |
| 323 | // Project-defined alias prefix? Treat as local. |
| 324 | const aliases = context?.getProjectAliases?.(); |
| 325 | if (aliases) { |
| 326 | for (const pat of aliases.patterns) { |
| 327 | if (importPath.startsWith(pat.prefix)) return false; |
| 328 | } |
| 329 | } |
| 330 | // Scoped packages or bare specifiers that don't start with aliases |
| 331 | if (!importPath.startsWith('@/') && !importPath.startsWith('~/') && !importPath.startsWith('src/')) { |
| 332 | // Likely an npm package |
| 333 | return true; |
| 334 | } |
| 335 | } |
| 336 | |
| 337 | if (language === 'python') { |
| 338 | // Standard library modules |
| 339 | const stdLibs = ['os', 'sys', 'json', 're', 'math', 'datetime', 'collections', 'typing', 'pathlib', 'logging']; |
| 340 | if (stdLibs.includes(importPath.split('.')[0]!)) { |
| 341 | return true; |
| 342 | } |
| 343 | } |
| 344 | |
| 345 | if (language === 'go') { |
| 346 | // Relative imports (rare in idiomatic Go but the grammar allows them). |
| 347 | if (importPath.startsWith('.')) { |
| 348 | return false; |
| 349 | } |
| 350 | // In-module imports look like `<module-path>/sub/pkg` — local to |
| 351 | // this project. Without the module-path check we'd flag every |
| 352 | // cross-package call in a Go monorepo as external (issue #388). |
| 353 | const mod = context?.getGoModule?.(); |
| 354 | if (mod && (importPath === mod.modulePath || importPath.startsWith(mod.modulePath + '/'))) { |
| 355 | return false; |
no test coverage detected