Poking round with some net frameworks like Subsequent and Astro I used to be posed the query of how onerous is a few of the stuff they’re doing and will we do it customized? My preliminary response was no manner, these frameworks do quite a bit and I’d not need that burden. But it surely sat behind my head and what I actually wished was some knowledge. Might I enumerate all of the options that have been usefully attention-grabbing and the way onerous the have been to implement to offer a extra knowledgeable choice? Not that this modifications something, maintenance and edge-case catching is like 90% of the work however can we really make minimal variations of some options? So to check this I need to begin with react SSR. I’ve really performed a bit bit on this earlier than within the context of a tiny static-site generator. That one used a couple of methods (learn: higher libraries) to make it simpler however this time I wished a extra actual React expertise which requires taming the dragon, warts and all.
The very first thing we have to do is add help for SSR so we have now a baseline. React and React DOM have a lot of the performance accessible to us. Since we’re utilizing Deno we do not even must litter our venture with an package deal.json, we will simply use npm identifiers.
import * as React from "npm:react";
import * as ReactDom from "npm:react-dom";
Since we have to disambiguate what several types of js/jsx/ts/tsx will we’ll use an additional extension .react.js
. Then we will add some path matchers to the index route:
filePaths.push(...[
".html",
".server.js",
".server.ts",
".server.jsx",
".server.tsx",
".react.js",
".react.jsx",
".react.tsx"
].map(ext => baseDir + inputPath + ext));
These information must have a particular format so we will correctly use them. I am selecting to make use of the NextJS paradigm, so we have now a default export which is the react element, and a getServerSideProps
operate which will get invoked to to generate the preliminary props.
//app.react.jsx
import * as React from "npm:react";
export async operate getServerProps(){
return {
identify: "John Doe",
userId: 123,
dob: "5/6/1990"
};
}
export default operate App(props){
return <>
<h1>My React App</h1>
<p>Howdy {props.identify}!</p>
<p>Your userId is {props.userId}</p>
<p>Your date of start is {props.dob}</p>
</>;
}
Then when producing responses we test the extension. We have already got a handler for .server.{js/jsx/ts/tsx}
so proper beneath that we add one for react.
//server.js - beneath matcher for .server. information
if (/.react.(js|ts|jsx|tsx)/.take a look at(fileMatch[1])) {
const mod = await import(fileMatch[1]);
const props = await mod.getServerProps();
const stream = await ReactDom.renderToReadableStream(React.createElement(mod.default, props));
return new Response(stream, {
headers: {
"Content material-Sort": "textual content/html"
}
})
}
At this level we will kind of render. It takes the default head/physique tags the browser generates however we will see it did not less than template in our values if we navigate to /app
.
To enhance we’d like to ensure the outer shell can also be rendered. We’ll must create a folder exterior of routes as a result of we do not need it to be picked up, I’ll name it layouts
it will by no means be accessible to the shopper so every part in right here it just for server rendering. We’ll name the element that gives the outer construction doc.jsx
.
//elements/doc.jsx
import * as React from "npm:react";
export operate Doc(props){
return <html lang="en">
<head>
<title>{props.title ?? "dev server"}</title>
<hyperlink rel="stylesheet" href="https://style-tricks.com/ndesmic/css/app.css" />
</head>
<physique>
<predominant id="react-root">{props.kids}</predominant>
</physique>
</html>
}
We will add an additional property for the title on the web page
//app.react.jsx
export operate getTitle(){
return "My React App";
}
Then we simply cross our web page element into this (you additionally must import Doc
):
//server.js - modifying the place we match .react. information
if (/.react.(js|ts|jsx|tsx)/.take a look at(fileMatch[1])) {
const mod = await import(fileMatch[1]);
const props = await mod.getServerProps();
- const stream = await ReactDom.renderToReadableStream(React.createElement(mod.default, props))
+ const title = mod.getTitle?.();
+ const stream = await ReactDom.renderToReadableStream(
+ React.createElement(Doc, { title },
+ React.createElement(mod.default, props)));
return new Response(stream, {
headers: {
"Content material-Sort": "textual content/html"
}
})
}
Now after we render we should always get a whole web page with title and the font needs to be arial letting us know that we’re really loading kinds accurately.
Hydration
At this level we will render however what if we strive one thing with dynamic conduct? Let’s add a folder for elements beneath routes
referred to as js/elements
.
//./routes/js/elements/counter.jsx
import * as React from "npm:react";
export operate Counter() {
const [value, setValue] = React.useState(0);
return <div>
<div className="counter">{worth}</div>
<button onClick={() => setValue(worth - 1)}>-</button>
<button onClick={() => setValue(worth + 1)}>+</button>
</div>
}
//app.react.js
import * as React from "npm:react";
+import { Counter } from "./js/elements/counter.jsx";
export async operate getServerProps(){
return {
identify: "John Doe",
userId: 123,
dob: "5/6/1990"
};
}
export operate getTitle(){
return "My React App";
}
export default operate App(props){
return <>
<h1>My React App</h1>
<p>Howdy {props.identify}!</p>
<p>Your userId is {props.userId}</p>
<p>Your date of start is {props.dob}</p>
+ <Counter />
</>;
}
It renders however we will not push the buttons. There is not any client-side scripts to hook up! Let’s have a look at what we will do. Let’s begin with our _document
, we all know we’ll want so as to add a script tag to one thing so let’s wire that up.
import * as React from "npm:react";
export operate Doc(props){
return <html lang="en">
<head>
<title>{props.title ?? "dev server"}</title>
<hyperlink rel="stylesheet" href="https://style-tricks.com/ndesmic/css/app.css" />
</head>
<physique>
<predominant id="react-root">{props.kids}</predominant>
+ <script kind="module" dangerouslySetInnerHTML={{ __html: props.script ?? "" }}></script>
</physique>
</html>
}
There have been a couple of methods we might have performed this. We might have generated a file however that’s extra sophisticated than simply injecting some js (although maybe higher in the long run…). So script
is a totally in-lined script. So now we will cross in a script
prop. However what’s our script? We all know that doc.jsx
shouldn’t be going to the shopper, nevertheless it appears very probably that not less than a part of route/app.react.jsx
is. So how can we do this? Let’s simply strive passing in the entire web page script.
//server.js - modifying the place we match .react. information
if (/.react.(js|ts|jsx|tsx)/.take a look at(fileMatch[1])) {
const mod = await import(fileMatch[1]);
const props = await mod.getServerProps();
const title = mod.getTitle?.();
let script = await Deno.readTextFile(fileMatch[1]);
if(mod.default.identify === "default"){
throw new Error("Web page default exports should even have names");
}
script += `
n
n
ReactDom.hydrateRoot(doc.querySelector("#react-root"), React.createElement("${mod.default.identify}", ${JSON.stringify(props)}));`
console.log(script)
const stream = await ReactDom.renderToReadableStream(
React.createElement(Doc, { title, script },
React.createElement(mod.default, props)));
return new Response(stream, {
headers: {
"Content material-Sort": "textual content/html"
}
})
}
So this is the place we will attempt to brute-force it. What we do is get the entire file as textual content, after which append the hydration command to the underside.
ReactDom.hydrateRoot(doc.querySelector("react-root"), React.createElement("${mod.default.identify}", JSON.
parse(${JSON.stringify(props)})));
We’d like the identify of the element and the props. The props we have already got, we simply must serialize them. NextJS additionally has the limitation that server props should be serializable and that is as a result of it is doing one thing like this, serializing them to the DOM for hydration. The identify is a hack (and we’ll see this has an issue, see in the event you can spot it but). Default exports with out names have the identify default
as their export identify. The issue with nameless default exports is that they can’t be referenced inside their very own module. Since we’re re-using the module code, we’d like to ensure we have now referenceable identify to cross to hydrateRoot
. Once we load this, we’ll discover that it would not fairly work. Whereas Deno can fortunately take care of JSX, the browser can not, the module should be transpiled. Deno can not do that for us and it is a very advanced factor to implement so whereas we satisfaction ourselves on not pulling in further libraries, we have to transfer to the subsequent easiest device, esbuild.
esbuild is a bundler, and nothing however a bundler. It is very slim in scope and intensely quick in order a library it is one thing that I can actually suggest. Observe that esbuild publishes each to npm and to deno.land/x so we will use both. I am unsure if there’s a lot of a distinction however perhaps the deno model works higher with deno, so lets use that.
Cleanup
Okay so we have kinda settled on a path ahead. However first let’s take a while to refactor. We have been modifying a small piece of code in server.js
and I feel it might actually stand to be break up out. So for this I’ll create a brand new folder referred to as “responders”. This can maintain on to implementations of various file responses specifically the controllers for .server.js/.server.ts
and .react.jsx/.react.tsx
.
//./responders/server-responder.js
import { resolve, toFileUrl } from "https://deno.land/std@0.205.0/path/mod.ts";
export async operate serverResponder(path, req){
const moduleImportPath = toFileUrl(resolve(Deno.cwd(), path));
const mod = await import(moduleImportPath);
if (req.technique === "GET") {
return mod.get?.(req) ?? mod.default?.(req)
?? new Response("Technique not allowed", { standing: 405 });
} else if (req.technique === "DELETE") {
return mod.del?.(req)
?? new Response("Technique not allowed", { standing: 405 });
} else {
return mod[req.method.toLowerCase()]?.(req)
?? new Response("Technique not allowed", { standing: 405 });
}
}
//./responders/react-responder.js
import * as React from "npm:react";
import * as ReactDom from "npm:react-dom/server";
import { Doc } from "../layouts/doc.jsx";
import { remodel } from "https://deno.land/x/esbuild/mod.js";
import { resolve, toFileUrl } from "https://deno.land/std@0.205.0/path/mod.ts";
export async operate reactResponder(path){
const moduleImportPath = toFileUrl(resolve(Deno.cwd(), path));
const mod = await import(moduleImportPath);
const props = await mod.getServerProps();
const title = mod.getTitle?.();
const web page = await Deno.readTextFile(path);
const transpiledPage = await remodel(web page, { loader: "tsx" });
const script = `
${transpiledPage.code}
ReactDom.hydrateRoot(doc.querySelector("#react-root"), React.createElement("${mod.default.identify}", ${ JSON.stringify(props) }));
`;
const stream = await ReactDom.renderToReadableStream(
React.createElement(Doc, { title, script },
React.createElement(mod.default, props)));
return new Response(stream, {
headers: {
"Content material-Sort": "textual content/html"
}
});
}
Right here I am including esbuild and transpiling the supply with the .tsx
loader in order that the JSX is transformed into React.createElement
calls (typescript may even be transformed for shopper use). Then we’re doing what we have been earlier than. Observe that we have to resolve the module url accurately as a result of we’re not at root so we will use the trail relative to the CWD and convert to a file url so we will use it with import
(it is not going to settle for home windows paths).
Now we will load these on demand. That is good as a result of it implies that we don’t must pay the price of having react if we aren’t utilizing it, we’ll by no means load it. After all as a result of we do reference it within the responder and the elements Deno will cache it so it might take a bit extra work to ensure Deno completely by no means tries something good.
// server.js - within the filetype swap
swap(ext){
case ".js":
case ".ts":
case ".tsx":
case ".jsx": {
if(/.server.(js|ts|jsx|tsx)/.take a look at(fileMatch[1])){
const mod = await import("./responders/server-responder.js");
return mod.serverResponder(fileMatch[1], req);
}
if (/.react.(js|ts|jsx|tsx)/.take a look at(fileMatch[1])) {
const mod = await import("./responders/react-responder.js");
return mod.reactResponder(fileMatch[1]);
}
}
// falls by means of
default: {
const file = await Deno.open(fileMatch[1]);
return new Response(file.readable, {
headers: {
"Content material-Sort": typeByExtension(ext)
}
});
}
}
So now we have mounted the syntax error however we one other one. We will not discover npm:react
. To unravel this we will use import maps.
//server-responder.js - proper earlier than changing to stream
const importMap = {
imports: {
"npm:react": "https://esm.sh/react@18.2.0"
}
};
const stream = await ReactDom.renderToReadableStream(
React.createElement(Doc, { title, script, importMap },
React.createElement(mod.default, props)));
return new Response(stream, {
headers: {
"Content material-Sort": "textual content/html"
}
});
In doc.jsx
we will use it.
//./layouts/doc.jsx
import * as React from "npm:react";
export operate Doc(props){
return <html lang="en">
<head>
<title>{props.title ?? "dev server"}</title>
<hyperlink rel="stylesheet" href="https://style-tricks.com/ndesmic/css/app.css" />
{
props.importMap
? <script kind="importmap" dangerouslySetInnerHTML={{ __html: JSON.stringify(props.importMap) }} />
: null
}
</head>
<physique>
<predominant id="react-root">{props.kids}</predominant>
<script kind="module" dangerouslySetInnerHTML={{ __html: props.script ?? "" }}></script>
</physique>
</html>
}
This can clear up the react error. What this does is inform the browser to map npm:react
to another url, on this case it is the react hosted on esm.sh. Observe that it is vital that we use a CDN that gives precise ESM transpilation as a result of React within the 12 months 2023 doesn’t have a local ESM export and is actually holding all the ecosystem again. We might have hosted it ourselves too, it is simply extra work. Now because it simply so occurs that the route from the app.react.jsx
is similar each server-side and client-side (a pleasant side-effect of not separating belongings and routes by folders) so we do not really need an import map for it client-side, it simply works!
Nonetheless we have now one other downside. The element file continues to be JSX and we will not use that client-side. So We have to get a bit smarter and since we have already added esbuild nothing is stopping us from transpiling for the shopper.
Transpiling JSX and TSX
We’ll make a slight change to the best way we detect the extname
. In Deno it will solely get the ultimate a part of the trail (eg foo.server.js
will return js
not server.js
) however we actually need the file identify to be something earlier than the primary .
and every part after to be the true extension identify as this makes matching simpler.
//server.js - within the filetype swap
const filePath = fileMatch[1];
const ext = filePath.break up(".").filter(x => x).slice(1).be a part of(".");
swap(ext){
case "server.js":
case "server.ts":
case "server.jsx":
case "server.tsx":
{
const mod = await import("./responders/server-responder.js");
return mod.serverResponder(filePath, req);
}
case "react.jsx":
case "react.tsx":
{
const mod = await import("./responders/react-responder.js");
return mod.reactResponder(filePath);
}
// falls by means of
default: {
const file = await Deno.open(filePath);
return new Response(file.readable, {
headers: {
"Content material-Sort": typeByExtension(ext)
}
});
}
}
The brand new ext
takes into consideration the compound extension (notice the .filter(x => x)
which can take away empty dots which solves for paths that begin with dots like ./path/to/foo.server.js
). This cleans up the matcher so it is simpler to see what’s occurring. We’ll simply add new ones for jsx, ts and tsx.
// responders/transpile-responder.js
import { remodel } from "https://deno.land/x/esbuild/mod.js";
export async operate transpileResponder(path) {
const codeText = await Deno.readTextFile(path);
const transpiled = await remodel(codeText, { loader: "tsx" });
return new Response(transpiled.code, {
headers: {
"Content material-Sort": "textual content/javascript"
}
});
}
And slot this into our matcher:
//server.js - within the filetype swap
case "jsx":
case "ts":
case "tsx":
{
const mod = await import("./responders/transpile-responder.js");
return mod.transpileResponder(filePath);
}
And we will transpile all non-javascript on the fly!
Now after we attempt to load the web page we have now legitimate javascript however a hydration error. React’s hydration errors are terrible and do not inform you something however with some strategic rendering to string we will determine what occurred. It seems we SSR’d with
<h1>My React App</h1>
<p>Howdy <!-- -->John Doe<!-- -->!</p>
<p>Your userId is <!-- -->123</p>
<p>Your date of start is <!-- -->5/6/1990</p>
<div>
<div class="counter">0</div>
<button>-</button>
<button>+</button>
</div>
However we hydrated with
<App identify="John Doe" userId="123" dob="5/6/1990"></App>
That’s, we unintentionally add the string "App"
as an alternative of the reference to the App
operate. We simply must delete the quotes round mod.default.identify
-> React.createElement(${mod.default.identify}, ${ JSON.stringify(props) })
. Lastly, it really works with no errors!
Un-shipping server code
So whereas functionally this appears to work we have now one other downside. Since we simply took the web page code and pushed it down we even have the getServerProps
and getTitle
capabilities on the shopper too. This aren’t supposed for use there and this may really trigger issues or be harmful if we expose server-side particulars we did not imply to. So we have to discover a solution to take away them. What we all know is that just about something exterior the default export is admittedly for the server, if we used variables from exterior the scope they may reference issues we did not need, we additionally simply haven’t got something apart from the props on the shopper. The serialization types a boundary we should not attain exterior. This locations a man-made restrict on the code we will write nevertheless it additionally simply makes extra sense and so we’ll make a rule: the web page element can not reference exterior variables, they should be handed in as props. With this rule in place we will take a look at this in a different way. We solely must ship the code within the default export.
We will really do a cool trick. If we simply need sure capabilities to be shipped we will use Operate.toString()
to get their supply code.
// responder/react-responder.js
export async operate reactResponder(path){
const moduleImportPath = toFileUrl(resolve(Deno.cwd(), path));
const mod = await import(moduleImportPath);
const props = await mod.getServerProps();
const title = mod.getTitle?.();
const script = `
${mod.default.toString() }
import * as React from "https://esm.sh/react@18.2.0";
import * as ReactDom from "https://esm.sh/react-dom@18.2.0";
ReactDom.hydrateRoot(doc.querySelector("#react-root"), React.createElement(${mod.default.identify}, ${ JSON.stringify(props) }));
`;
const stream = await ReactDom.renderToReadableStream(
React.createElement(Doc, { title, script },
React.createElement(mod.default, props)));
return new Response(stream, {
headers: {
"Content material-Sort": "textual content/html"
}
});
}
This can easy serialize the JS textual content of mod.default
which incorporates the operate signature. Since Deno could have already transpiled the JSX for run-time we do not even must run esbuild, we received it totally free. Additionally, as a result of we’re not utilizing the server-side imports we do not want the import map both, we will simply manually set these (you may nonetheless need the import map although to make book-keeping simpler). So our code is now less complicated, smaller and quicker. Very good! But it surely comes with a brand new downside. On the shopper we do not know what Counter
is as a result of we misplaced that import. It is a difficult difficulty. We do not know which imports have been for the server-side stuff and which have been for the element. Even when we did we do not have entry to the import names at run-time. It appears that is the place you want good bundler. You principally need to take the default export after which tree-shake every part else out of the module. Sadly esbuild can not do that, it would not take AST plugins and for good cause, this enables it to be actually optimized. In order that appears to be finish, we have to not solely have bundler however we have to write a plugin for it.
Another methods I checked out:
- Regex magic – That is tremendous brittle and ugly. Parsing accurately requires precise context-free grammar otherwise you’ll at all times have points.
-
Utilizing proxies to gather references to elements after a render – I believed this is able to work however whereas we will get the names and references we will not decide the module from which they got here eg (
npm:my-component
). We might add new guidelines saying no exterior elements (which I personally like however that is a deal breaking in a variety of use-cases). - Manually setting import data in a getClientDeps operate – This might require guide updating which sucks nevertheless it’s particularly unhealthy as a result of you could keep in mind to replace it any time you will have an precise dependency change and there might be nothing to implement this till the web page would not load.
After some thought one of the simplest ways round this with out sprinkling magic complexity in every single place like NextJS is to only phase the information. All elements sans the layouts are precise elements in their very own file with their very own imports and can work on the shopper. There isn’t any intermixing server and shopper code. The .react.js
information will as an alternative simply inform us the place to search for the element file for the foundation of the shopper react tree.
That is what I got here up
// .responders/react-responder.js
import React from "npm:react";
import ReactDom from "npm:react-dom/server";
import { Doc } from "../layouts/doc.jsx";
import { resolve, toFileUrl } from "https://deno.land/std@0.205.0/path/mod.ts";
export async operate reactResponder(path){
const moduleImportPath = toFileUrl(resolve(Deno.cwd(), path));
const mod = await import(moduleImportPath);
const props = await mod.getServerProps();
const title = await mod.getTitle?.();
const [componentPath, exportName] = await mod.getRootComponent();
const script = `
import * as React from "npm:react";
import * as ReactDom from "npm:react-dom";
import { ${exportName ?? "default"} } from "${componentPath}";
ReactDom.hydrateRoot(doc.querySelector("#react-root"), React.createElement(${exportName}, ${ JSON.stringify(props) }));
`;
const componentMod = await import(new URL(componentPath, moduleImportPath));
const importMap = {
imports: {
"npm:react": "https://esm.sh/react@18.2.0",
"npm:react-dom": "https://esm.sh/react-dom@18.2.0"
}
};
const stream = await ReactDom.renderToReadableStream(
React.createElement(Doc, { title, script, importMap },
React.createElement(componentMod[exportName], props)));
return new Response(stream, {
headers: {
"Content material-Sort": "textual content/html"
}
});
}
I normalized all react references to npm:react
to only make issues simpler, we’d like the import map once more. And the precise route file:
// ./routes/app.react.jsx
export operate getServerProps(){
return {
identify: "John Doe",
userId: 123,
dob: "5/6/1990"
};
}
export operate getTitle(){
return "My React App";
}
export operate getRootComponent(){
return ["./js/components/app.jsx", "App"];
}
It is just about simply knowledge, we might even theoretically convert it to json (I added path matching for js and ts since these make sense now). It feels heavy for a file which might be why NextJS wished to merge it however that is straightforward to implement and it is fairly clear (what the trail is relative to is probably not apparent, we resolve it as relative to .react.js
file). For completeness this is ./elements/app.jsx
import * as React from "npm:react";
import { Counter } from "./counter.jsx";
export operate App(props) {
return <>
<h1>My React App</h1>
<p>Howdy {props.identify}!</p>
<p>Your userId is {props.userId}</p>
<p>Your date of start is {props.dob}</p>
<Counter />
</>;
}
Refactor responders to plugins
I had the concept to make dynamic fetches when utilizing frameworks like react so we get these libraries on demand. It was a cool thought nevertheless it would not absolutely work as a result of we nonetheless should learn about these file sorts forward of time to match their extensions. I feel it may be higher to only use a easy plugin system, the place we register them up-front. This implies the matching logic could be moved to the responder module and it will clear up server.js
a bit and hopefully keep away from some duplicated code later. We’ll use the operate match(path) => boolean
to match paths and we’ll use defaultPaths(barePath) => string[]
to generate doable default paths for paths that don’t finish in extensions.
//./responders/react-responder.js
const extensions = [
"react.js",
"react.jsx",
"react.ts",
"react.tsx",
];
export operate match(path){
const ext = path.break up(".").filter(x => x).slice(1).be a part of(".");
return extensions.contains(ext);
}
export operate defaultPaths(barePath){
return extensions.map(ext => barePath + "." + ext);
}
//...
//./responders/server-responder.js
export const extensions = [
"server.js",
"server.ts",
"server.jsx",
"server.tsx"
];
export operate match(path){
const ext = path.break up(".").filter(x => x).slice(1).be a part of(".");
return extensions.contains(ext);
}
export operate defaultPaths(barePath) {
return extensions.map(ext => barePath + "." + ext);
}
//...
//./responders/transpile-responder.js
const extensions = [
"jsx",
"ts",
"tsx"
];
export operate match(path) {
const ext = path.break up(".").filter(x => x).slice(1).be a part of(".");
return extensions.contains(ext);
}
export operate defaultPaths(barePath) {
return extensions.map(ext => barePath + "." + ext);
}
//...
It is a tad redundant to at all times get a file extension for match
nevertheless it makes it simpler to share the extension knowledge with the defaultPaths
. We’ll additionally change the principle operate of every responder to be the default export so it is constant. We additionally want to assemble up all of the responder modules.
//server.js
import { toFileUrl, resolve, be a part of } from "https://deno.land/std@0.205.0/path/mod.ts";
const responders = await Promise.all(Array.from(Deno.readDirSync("./responders")).map(f => import(toFileUrl(resolve(be a part of(Deno.cwd(), "./responders"), f.identify)))));
A little bit of path magic to make it work. The we’ll want to repair up the default paths.
//./server.js in serve
//...
//normalize path
if (inputPath.endsWith("/")) {
inputPath += "index";
}
if(!inputPath.contains(".")){
potentialFilePaths.push(baseDir + inputPath + ".html");
potentialFilePaths.push(...responders.flatMap(responder => responder.defaultPaths(baseDir + inputPath)));
} else {
const path = baseDir + inputPath;
potentialFilePaths.push(path);
}
//discover
const fileMatch = await probeStat(potentialFilePaths);
//...
Lastly I wished to maintain the default file responder as a part of server.js
. We’ll at all times want not less than one so if there’s nothing within the responder folder then the entire thing turns into a fundamental file server.
Conclusion
So getting this to work with out bundler magic could be very onerous. It isn’t shocking why NextJS is investing in a bundler. Although one factor that basically stands out is how a lot complexity we add for simply miniscule dev ergonomics. Not utilizing JSX and utilizing one thing like htm would make all this simpler (eradicating the bundler completely), it is a variety of overhead to keep away from a few quotes. React ought to actually have a tagged-template mode. Additionally all of that is indirection is definitely unhealthy for dev ergonomics too! One of many causes I did it is because I am completely sick of magic caches and sorting by means of code that is been crushed by a bundler into one thing I do not acknowledge and may’t simply debug. Whereas we will not eliminate this utterly (ts/jsx) this preserves the module import graph utterly on the client-side making it straightforward to seek out issues as you might be working and preserving line numbers. This clearly shouldn’t be helpful for a manufacturing construct and there is a variety of work that would wish to go in to help each modes over the identical code, nevertheless it’s miserable no instruments actually work like this for native growth.