JSX Support
What is the problem this feature will solve?
Following up on nodejs/node#56322 and nodejs/node#56392.
JSX and its various flavors have undergone frequent configuration changes since their inception. While supporting JSX without any configuration might seem ideal, it is both impractical and problematic. Just as "type": "module" was introduced to address CommonJS/ESM interoperability, we must continue relying on existing configuration files—tsconfig.json or jsconfig.json—to determine the correct transformation to apply.
The presence or absence of JSX should not rely on node's recent work for supporting typescript.
JSX has multiple targets, primarily react and react-jsx, which produce different outputs from the same input. For example:
<h1>Hello world</h1>
-
react-jsximport { jsx as _jsx } from "react/jsx-runtime"; _jsx("h1", { children: "Hello world" }); -
reactOutputimport React from "react"; React.createElement("h1", null, "Hello world"); -
jsxFactory– Specifies the function to replacecreateElementwhen using thereacttarget. For example, settingjsxFactory: 'h'replacesReact.createElementwithh. -
jsxFragmentFactory– Determines the function used for fragments (<></>). If set, fragments will use the specified name instead ofReact.Fragment. -
jsxImportSource– Used with thereact-jsxtarget to change the import source. For example, settingjsxImportSource: 'preact'results in:import { jsx as _jsx } from "preact/jsx-runtime";
Their combination is what is needed for any library that supports JSX, such as Vue.js, Kita, React, Preact, Astro, and others...
Given the diversity of JavaScript frameworks, adopting a default configuration favoring any single framework would inevitably create issues for others.
One alternative is to add fields to package.json to define some JSX settings, eliminating the need for a jsconfig.json or tsconfig.json file. However, rewriting this (ideal or not) standard is not something the community will like.
In the long run, adding support for a native JSX transform function, similar to Deno’s implementation, could significantly enhance Node.js performance in SSR scenarios.
Deno's native transform has demonstrated 7-20× faster rendering times and 50% reduction in garbage collection overhead. Such improvements would benefit SSR frameworks like Next.js, positioning Node.js competitively once again.
What is the feature you are proposing to solve the problem?
I propose that nodejs transform JSX syntax and support .jsx and .tsx files.
What alternatives have you considered?
No response
I was exploring this idea. We could create a hook that framework can implement. Example Every frameworks can do something like:
module.registerJsxHook((obj) =>{
React.createElement(// logic to convert object into react)
})
<h1>Hello world</h1>
Is transpiled into:
process.getBuiltInModule('module').jsxHandler({
// The object transformed
})
What happens when there's more than one hook registered in an application, as would be virtually guaranteed to happen?
There's a reason jsx isn't in the language yet, or in any platform.
What happens when there's more than one hook registered in an application, as would be virtually guaranteed to happen?
Care to explain why? Also
There's a reason jsx isn't in the language yet, or in any platform.
Expand pls
What I mean is, the rules for the syntax are pretty universal and clear - the problem is that there's a plethora of transformations of it. If you had to pick only one, you'd pick React's - but which one? The pre-17 one, that's way more widely used, or the 17+ one, that's the future of React?
If you only pick one, then you ace out anyone using an alternative transformation.
If you allow it to be customizable, and it's not customizable per module (which would require syntax, for ESM, and thus TC39 support - although with CJS you could do it by just injecting a new function into scope), then you've created a global state and capability that means that any code that can set it, can override the meaning of what appears to be syntax - which is a massive security hole and attack vector.
In other words, I think that the only way to support jsx that's even remotely reasonable here is with a loader, and users can already use custom loaders to achieve it - so "node supports jsx" would just mean that node ships a loader by default, which would mean node is picking a winner - and, if node's picking a winner, per the usage data, it's going to be React, and that's not exactly fair to React competitors.
There may come a time in the future when the JS language can answer some of these questions - or when userland has largely settled on a single jsx transformation - but until that time, it feels at best incredibly risky and worst the precursor to an abject disaster for node to attempt to address this itself.
@ljharb I understand the technical issues, but I don't agree with marking a problem as "won't fix" without exploring it. This has been one major historical problem of the loaders space, which has burned out several people. While I understand its not an easy fix, it would be more useful to talk about blockers and requirements for this to happen. The jsx support is something that can be fixed, just like typescript support, and we shoud look into fixing it
or when userland has largely settled on a single jsx transformation
A single transformation target is not what JSX needs.
Any attempt to standardize it into a single format seems prone to failure. JSX community has already proven different syntaxes are better for different use cases:
- deno precompile
- react-jsx
- jsx native
- and so on...
then you've created a global state and capability that means that any code that can set it
jsx: react-jsx prefers a importable module following some rules.
jsx: react requires React to be available in local scope, the following code is valid:
function template(React) {
return <div></div>
}
And that's part of how JSX is supposed to be.
exactly. And when there’s multiple valid ways of handling a syntax, the existing solution for that is “custom loaders”.
exactly. And when there’s multiple valid ways of handling a syntax, the existing solution for that is “custom loaders”.
Those arguments are 100% applicable to TypeScript support too. But, Node.js team finally decided to choose one option (SWC in amaro) and implement basic support level keeping hooks only for advanced scenarios. What is the difference between JSX and TS? Both can be compiled by various ways, both can have overcomplicated external configuration, both are tightly coupled with 3rd-party ecosystem, etc.
JSX / TS /
What I want to say - JSX support is mostly not a technical question. May be it's need to have TSC decision or something like that.
I'm not aware of an alternative semantic meaning for compiled TS output - it's JavaScript. Babel, tsc, swc, all compile TS with different approaches that result in the same runtime JS. JSX is vastly different, and the various approaches result in widely varying runtime behavior.
I'm not aware of an alternative semantic meaning for compiled TS output - it's JavaScript. Babel, tsc, swc, all compile TS with different approaches that result in the same runtime JS. JSX is vastly different, and the various approaches result in widely varying runtime behavior.
it is possible to standardize the transpilation to a certain point and have the framework hook in with some Node API. We probably should ask framework maintainers.
For example:
<div></div>
is transpiled to:
// Dont mind the content of the object
process.getBuiltInModule('module').jsxHandler({ tag: 'div' });
The framework can register a hook to do something with the object that is passed: { tag: 'div' }
Babel, tsc, swc, all compile TS with different approaches that result in the same runtime JS
It's not quite true - generated code is different even for the one particular tool.
For example, tsc has a lot of flags that directly affect JS output: preserveConstEnums, downlevelIteration, importHelpers, noUncheckedSideEffectImports, esModuleInterop, verbatimModuleSyntax, importsNotUsedAsValues, preserveValueImports, etc.
SWC has even more. This is why current state of TS support in Node is a compromise with some defaults and this is why we have 'Full TypeScript support' chapter in the documentation.
That is indeed how it could technically be achieved - but then we've got global state (the current JSX handler) and apps could have multiple JSX transpilations in the same application, even ignoring third party dependencies.
but then we've got global state (the current JSX handler)
it's neither better nor worse than hooks you proposed - exactly the same behavior. But if you want to discuss technical details - jsxHandler can accept callback(filename) to return compilation options conditionally, depends on framework configuration.
Thing is, it will always require source-maps, therefore always be available behind flag
@koshic a custom userland loader can choose when to selectively apply a given transformation.
@marco-ippolito if it's always available behind a flag, and it never applies to third-party code, then certainly a lot of the risks i've discussed are mitigated or don't apply, but I'm still concerned about it (especially about the impact it could have on future standardization of jsx syntax)
We should probably ask for comment from framework authors @yyx990803 @ematipico feel free to ping more
Aside from the problems raised by @ljharb, I doubt this is going to be useful beyond simple use cases.
From a meta-framework perspective (e.g. Next / Nuxt), the framework would prefer complete control over how JSX is compiled for their specific use cases and will most likely opt-out even this is supported in Node. From a perf perspective, frameworks also want to transpile JSX ahead of time during build instead of paying the transform cost at runtime bootup.
From a design perspective, the fundamental issue I see is that:
- Node wants to avoid its behavior being determined by external config like
tsconfig.json. - At the same time, how JSX should be compiled is an open canvas that has to be configured in some way.
Node's existing TS support suffers from similar problems IMO, but less critical due to limited permutations of options that affect transform output.
Transpiling to process.getBuiltInModule('module').jsxHandler({ tag: 'div' }) has two problems:
-
The global-ness issue and prevents different JSX transpiles from being used in the same project. Even with
callback(filename), the filename alone doesn't provide enough information to determine how the file should be transformed. This can possibly be addressed by supporting TS-style per-file JSX pragma:/** @jsxImportSource preact */ export function App() { return <h1>Hello World</h1>; } -
Not all JSX transforms transform JSX tags 1:1 to function calls. For example, the Deno optimization mentioned concatenates static tags into strings, and similar optimizations happen in Solid and Vue Vapor mode JSX transforms too. Some transforms even need to alter original AST structure or inject additional statements.
This is a more fundamental issue that IMO makes "built-in JSX support" a feature that is not practical, no even non-feasible to have in Node.js.
@marco-ippolito module.registerJsxHook is not a good idea. Why add this API when the register API already exists and can solve this problem.
And IMO JSX sould be handle by customization hooks. Maybe have official one.
JSX seems an obvious next step after finishing TS support in node when talking about integrating existing syntax into node. It might not fill enough edge cases to make a big framework blindly change to it but there are still use cases for JSX that are present in a lot of tools like https://jsx.email.
When compared to complete TS support In nodejs, this comes as a very low priority but I think its not something we can ignore.
For whatever it is worth, we use JSX to generate XML from various data inputs in node. The idea that JSX is just a neat syntax to write otherwise really painful code is I think strong. In my opinion it would even make for a fair addition to ECMAScript itself. As someone who has now written multiple non-React internal JSX 'handling' functions... I really don't care how exactly the function is 'specified' or 'found'. Currently we always rely on jsxFactory and depending on the specific case the create function might be either a local variable, or a top level import. I am not going to claim to have any knowledge about JSX syntax stabillity or anything like that... but I just wanted to give real world feedback that the value of JSX syntax goes beyond "big frameworks might rely on it".
At the same time, the more situations we don't need code transformers, the better, because code transformers absolutely hurt the debugging experience no matter what.
There has been no activity on this feature request for 5 months. To help maintain relevant open issues, please add the https://github.com/nodejs/node/labels/never-stale label or close this issue if it should be closed. If not, the issue will be automatically closed 6 months after the last non-automated comment. For more information on how the project manages feature requests, please consult the feature request management document.
JSX is part of today's ecosystem. JSX being framework agnostic implies that multiple stuff might be using jsx at the same time. People can use it to render xml, html strings, email templates and anything else they ever want. Even if big frameworks will need to keep transpiling JSX in the future, not a problem! Still, Implementing native support brings simple use-cases much more compelling to the scene.
The same way TS support was added for simplicity, since it was possible to run TS code before and a lot of people will keep using transpilers in their process of running TS code in the future, JSX should be handled the same way.
Specially now that we have amaro set up and, besides settling down on settings/DX, this should not be as hard as it was to set up TS support in the first place.