Skip to content

Improve diagnostic for implicit JSX runtime imports in non-module files - #64445

Open
Shrey Shekhar (sh011) wants to merge 3 commits into
microsoft:mainfrom
sh011:sh011-fix-64438-incorrect-error-messages
Open

Shrey Shekhar (sh011) wants to merge 3 commits into
microsoft:mainfrom
sh011:sh011-fix-64438-incorrect-error-messages

Conversation

@sh011

Copy link
Copy Markdown

Fixes #64438

Analysis

When compiling a file containing JSX with implicit runtime imports (for example, configured with --jsx preserve and --jsxImportSource @solidjs/web), TypeScript attempts to resolve the implicit JSX runtime import path (@solidjs/web/jsx-runtime or react/jsx-runtime).

In projects using tools like SolidJS, Babel, or Vite where --jsx preserve is standard, files without top-level import or export statements are treated as global scripts unless "moduleDetection": "force" is enabled:

// @jsx: preserve
// @jsxImportSource: @solidjs/web
// @module: esnext

const x = <><div>hi</div></>;

Previously, when resolving implicit imports for global scripts via getJsxNamespaceContainerForImplicitImport in tsc/internal/checker/jsx.go, resolution failure always reported error TS2875:

"This JSX tag requires the module path '{0}' to exist, but none could be found. Make sure you have types for the appropriate package installed."

This diagnostic was confusing and unhelpful because the required package and type definitions could already be installed in node_modules. The actual issue was that global scripts cannot import external modules.

Fix

  1. Added diagnostic TS2884:

    "This JSX tag requires the module path '{0}' to exist, but none could be found. If this file is not intended to be a global script, set 'moduleDetection' to 'force' or add an empty 'export {}' statement."

  2. Differentiated non-module files in JSX checker:
    In getJsxNamespaceContainerForImplicitImport (tsc/internal/checker/jsx.go), we check !ast.IsExternalModule(file) when resolving the implicit external module. When the file is not an external module, we now emit TS2884 instead of TS2875.

  3. Added tests & baseline updates:

    • Added test case tsc/testdata/tests/cases/compiler/jsxImportSourceNonModule.tsx.
    • Generated baselines for the new test case (.errors.txt, .js, .symbols, .types).
    • Updated baselines for commentsOnJSXExpressionsArePreserved(jsx=react-jsx,module=commonjs,moduledetection=legacy) and commentsOnJSXExpressionsArePreserved(jsx=react-jsxdev,module=commonjs,moduledetection=legacy).

When a file uses JSX with implicit runtime imports (such as with
`jsx: preserve` and `jsxImportSource: <package>`) but has no top-level
imports or exports, the file is treated as a global script. In this
case, module resolution for the JSX runtime path fails.

Previously, TypeScript emitted TS2875 ("Make sure you have types for the
appropriate package installed"), which was misleading when types were
installed but the file was simply not treated as a module.

Now, if `!ast.IsExternalModule(file)`, TypeScript emits TS2884, advising
the user to set 'moduleDetection' to 'force' or add an empty 'export {}'
statement.

Fixes microsoft#64438
@github-project-automation github-project-automation Bot moved this to Not started in PR Backlog Sep 25, 2026
Copilot AI balanced review requested due to automatic review settings September 25, 2026 13:35
@typescript-automation typescript-automation Bot added the For Uncommitted Bug PR for untriaged, rejected, closed or missing bug label Sep 25, 2026
@typescript-automation

Copy link
Copy Markdown
Contributor

This PR doesn't have any linked issues. Please open an issue that references this PR. From there we can discuss and prioritise.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@sh011

Copy link
Copy Markdown
Author

@microsoft-github-policy-service agree

Comment thread tsc/internal/checker/jsx.go Outdated
}
errorMessage := diagnostics.This_JSX_tag_requires_the_module_path_0_to_exist_but_none_could_be_found_Make_sure_you_have_types_for_the_appropriate_package_installed
if !ast.IsExternalModule(file) {
errorMessage = diagnostics.This_JSX_tag_requires_the_module_path_0_to_exist_but_none_could_be_found_If_this_file_is_not_intended_to_be_a_global_script_set_moduleDetection_to_force_or_add_an_empty_export_statement

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why do we need to resolve? Is it because resolution can still succeed if there's an ambient declaration? Add a test for when this occurs in a global file with an ambient.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Yes. In a global script, module resolution won't pull in packages from node_modules, but resolveExternalModule still checks ambient modules via tryFindAmbientModule. If there’s an ambient declare module '...' in the program, it resolves fine and shouldn't error out.

So, I've added a test case in jsxImportSourceNonModuleAmbient.tsx with an ambient declaration in a global file to make sure that the path stays green.

"category": "Error",
"code": 2883
},
"This JSX tag requires the module path '{0}' to exist, but none could be found. If this file is not intended to be a global script, set 'moduleDetection' to 'force' or add an empty 'export {}' statement.": {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I don't think I like this message because the 2 sentences don't seem to have anything to do with each other. Not sure what's better but I guess it depends on the answer to the first question.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I understand now, the wording "none could be found" makes it sound like the package is missing from disk, so jumping straight to moduleDetection felt out of place. Since the actual issue is that global scripts can't import external modules, I updated the message to:

"This JSX tag requires the module path '{0}' to exist, but external modules cannot be imported in a global script. If this file is not intended to be a global script, set 'moduleDetection' to 'force' or add an empty 'export {}' statement."

Is that fine or do you think this might need any more changes?

…e in global scripts

- Update TS2884 to clarify that external modules cannot be imported in
  a global script file, directly connecting the missing module path to
  the suggestion for moduleDetection/export {}.
- Add test demonstrating that implicit JSX runtime resolution succeeds
  in global scripts when an ambient module declaration is present.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

For Uncommitted Bug PR for untriaged, rejected, closed or missing bug

Projects

Status: Waiting on author

Development

Successfully merging this pull request may close these issues.

Incorrect/unhelpful error message for non-module jsx file

3 participants