Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JSX Support #56822

Open
arthurfiorette opened this issue Jan 30, 2025 · 19 comments
Open

JSX Support #56822

arthurfiorette opened this issue Jan 30, 2025 · 19 comments
Labels
feature request Issues that request new features to be added to Node.js.

Comments

@arthurfiorette
Copy link

arthurfiorette commented Jan 30, 2025

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-jsx

    import { jsx as _jsx } from "react/jsx-runtime";
    _jsx("h1", { children: "Hello world" });
  • react Output

    import React from "react";
    React.createElement("h1", null, "Hello world");
  • jsxFactory – Specifies the function to replace createElement when using the react target. For example, setting jsxFactory: 'h' replaces React.createElement with h.

  • jsxFragmentFactory – Determines the function used for fragments (<></>). If set, fragments will use the specified name instead of React.Fragment.

  • jsxImportSource – Used with the react-jsx target to change the import source. For example, setting jsxImportSource: '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

@arthurfiorette arthurfiorette added the feature request Issues that request new features to be added to Node.js. label Jan 30, 2025
@github-project-automation github-project-automation bot moved this to Awaiting Triage in Node.js feature requests Jan 30, 2025
@marco-ippolito
Copy link
Member

marco-ippolito commented Jan 30, 2025

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
})

@ljharb
Copy link
Member

ljharb commented Jan 30, 2025

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.

@marco-ippolito
Copy link
Member

marco-ippolito commented Jan 30, 2025

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

@ljharb
Copy link
Member

ljharb commented Jan 30, 2025

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.

@marco-ippolito
Copy link
Member

marco-ippolito commented Jan 30, 2025

@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

@arthurfiorette
Copy link
Author

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.

@ljharb
Copy link
Member

ljharb commented Jan 30, 2025

exactly. And when there’s multiple valid ways of handling a syntax, the existing solution for that is “custom loaders”.

@koshic
Copy link

koshic commented Jan 30, 2025

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 / support in Node.js just a step towards the community, because base framework functionality more than enough to implement any possible solution by using third-party tools / libraries / even native addons.

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.

@ljharb
Copy link
Member

ljharb commented Jan 30, 2025

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.

@marco-ippolito
Copy link
Member

marco-ippolito commented Jan 30, 2025

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' }

@koshic
Copy link

koshic commented Jan 30, 2025

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.

@ljharb
Copy link
Member

ljharb commented Jan 30, 2025

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.

@koshic
Copy link

koshic commented Jan 30, 2025

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.

@marco-ippolito
Copy link
Member

Thing is, it will always require source-maps, therefore always be available behind flag

@ljharb
Copy link
Member

ljharb commented Jan 30, 2025

@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)

@marco-ippolito
Copy link
Member

We should probably ask for comment from framework authors
@yyx990803 @ematipico feel free to ping more

@yyx990803
Copy link

yyx990803 commented Feb 2, 2025

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:

  1. 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>;
    }
  2. 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.

@AugustinMauroy
Copy link
Member

@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.

@arthurfiorette
Copy link
Author

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.

@marco-ippolito marco-ippolito marked this as a duplicate of #57079 Feb 18, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature request Issues that request new features to be added to Node.js.
Projects
Status: Awaiting Triage
Development

No branches or pull requests

6 participants