microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
100.8k stars 12.46k forks source link

Support other JSX factories #3788

Closed tinganho closed 8 years ago

tinganho commented 9 years ago

Seems like Babel supports other JSX factories other than React.createElement(...):

Here is docs from deku another JSX framework from Segment.io Using .babelrc:

{
  "jsxPragma": "element"
}

Source: https://github.com/dekujs/deku/blob/master/docs/guides/jsx.md#babelrc

I propose we add something similar to the TS compiler options. Letting people to do a post build of preserved JSX element is not the nicest solution.

tinganho commented 9 years ago

To be more specific about JSX factories, I mean React.createElement is switched with whatever specified factory function. All the other rewires like attributes etc. remains the same. So I guess this is a small change.

PS. Also the jsx-transform supports other factory functions: https://github.com/alexmingoia/jsx-transform

tinganho commented 9 years ago

What's the status of this?

I'm creating right now a JSX framework. Though I'm using a dirty trick with reusing React's namespaces.

RyanCavanaugh commented 9 years ago

I think we're mostly waiting for feedback. There have been some other JSX emit suggestions and we'll need to prioritize based on how necessary (un-work-around-able) and useful they are.

mindplay-dk commented 9 years ago

Yeah, I am really excited about this feature and was trying it out with MS Code tonight - but then realized, two options for JSX processing, one just passes through JSX tags unprocessed, and the other is hard-wired to React... I don't want React - I want something lightweight. Like, for one, I was hoping maybe riotjs could be supported. Moreover, I don't want to be tied to any particular implementation.

Sounds like you're already aware of that though? :-)

How about providing a compiler (and tsconfig.json) switch that allows you to specify which implementation to use?

It's already type-checking against an interface, right?

So, if this interface was defined by Typescript, rather than defined by an external React.d.ts file, we could then implement this interface, specify the name of the implementation, and have the compiler type-check against an implementation of our choice?

Emit would be largely the same then, I think? Just with XYZ. instead of React. and type-checking against the specified implementation.

I think per-project (e.g. compiler-level) is fine, and much simpler than e.g. allowing you to somehow switch implementations on a per-file basis or something - I don't imagine you'd need/want to use more than one implementation in the same project (?)

I'm really excited about this feature!

Hope is arrives in a form that is also useful to non-Reacters. (Reactionist? Reactioners? Whatever.)

mhegazy commented 9 years ago

Type checking is not tied to any framework; type checking is driven by JSX namepsace declaration (see wiki page for more details).

Emitting with -jsx preserve will result in emitting your jsx into the output after transforming any TS specific concepts, e.g. internal modules, rewriting let and const, module imports, etc..

Any JSX library out there already has a compiler that supports transpiling jsx into their supported output. riotjs as well provides a compiler. the integration scenarios with TS is straight forward and with a very low over head.

For maintenance reasons, the typescript team would not want to take on the responsibility of emitting to multiple frameworks. we do care that these scenarios do work though, and the --jsx preserve is mainly for this.

mindplay-dk commented 9 years ago

Type checking is not tied to any framework; type checking is driven by JSX namepsace declaration

Alright, so we're half-way there :-)

For maintenance reasons, the typescript team would not want to take on the responsibility of emitting to multiple frameworks

That's not really what I'm asking for - literally all I'm asking is for, is to avoid the extra compile step in a second compiler, by allowing us to replace the emitted React. with something else. Why does that add any maintenance burden on your part?

I'd really like to avoid relying on a second compiler/tool - for one, how would that work in terms of source maps? You'd need to recursively resolve source-maps from one compiler via source-maps from another compiler? Or the second compiler would need to support source-maps and resolve references while compiling? That doesn't sound simple.

I would strongly prefer to leverage the JSX compiler built into my language - preserve does not sound like a good option.

mhegazy commented 9 years ago

There are other parts of the emitter than the name of the variable. for instance, in React, tag names starting with lower case, are treated as built in and always emitted as strings, where as uppercase ones are not. similarly, white spaces has its own rules in react emit. there is also serializing strings, etc.. I would expect frameworks to have differences, and I will find it misleading to say that we support other frameworks, where in fact we do not.

mindplay-dk commented 9 years ago

I would expect frameworks to have differences, and I will find it misleading to say that we support other frameworks, where in fact we do not.

I see. That's a shame. I thought the goal was to be a general-purpose programming language - now it sounds like Typescript is actually part framework and part language, or at the very least, it's now tailored for one particular framework.

I think that's a very odd decision - instead of building a feature that could be generally useful and enable people to do new and original things, possibly catering to a lot more people and a lot of new ideas, you chose one specific framework, which seems really opinionated.

Just my opinion of course, but frameworks generally have a substantially shorter life-span than languages. IMO, this is going to to shorten the shelf-life of Typescript tremendously. I really wish you had made something that everyone could enjoy, regardless of their taste in frameworks...

danquirk commented 9 years ago

If you peruse the various discussions in issues like https://github.com/Microsoft/TypeScript/issues/3203 you'll see that quite a bit of effort and thought went into making the JSX support as generalized as possible while also supporting the primary use case of today (React). There is very little coupling and as noted with --jsx preserve it's very possible to run a second tool over your output to get the emit you need for the framework of choice.

benlesh commented 9 years ago

If I had a request, it would be to please stick to what Babel has already done in this area.

For example a comment at the top of a file can be used to adjust output as well: /** @jsx h */ changes it to h('pre', {}, [])

mindplay-dk commented 9 years ago

I feel like I'm hearing conflicting stories here:

the typescript team would not want to take on the responsibility of emitting to multiple frameworks

Okay, but:

quite a bit of effort and thought went into making the JSX support as generalized as possible

So what are you saying, folks?

JSX support should be generalized, but you want to emit for only one framework?

I'm afraid I don't follow. There's nothing general about supporting one select framework - that is, by my definition, the opposite of general.

To clarify, I'm not asking you to support other frameworks - I'm asking you to make this feature configurable enough to allow the community to implement support for other frameworks. I'm suggesting that making the hard-coded React reference configurable is one way to do that. It doesn't make you responsible for integration with any other frameworks, I don't believe anybody would assume that - but it would permit others to do so, without feeling like they have to "hack" Typescript or "polyfill" React.

@danquirk I did follow these discussion, and I participated in another thread than the one you mentioned above. I didn't feel like like anyone was listening though, which was odd, a bit of a let-down, and btw the only time I've ever participated in these discussions and felt like I was being ignored. It was my impression that minds were already made up and perhaps React was too popular a force to go up against. I still feel that way. As someone at my office remarked today, Typescript is now actually a framework and not just a language, which is kind of sad...

danquirk commented 9 years ago

It is entirely possible to make the feature support extension without taking on the cost of that support ourselves. You could imagine a world where --jsx preserve didn't exist, and we literally did only and always translate JSX elements to React calls. Likewise you can imagine a world where we baked in React typing definitions in such a way that alternative JSX based frameworks wouldn't be as easily used.

To clarify, I'm not asking you to support other frameworks - I'm asking you to make this feature configurable enough to allow the community to implement support for other frameworks. I'm suggesting that making the hard-coded React reference configurable is one way to do that. It doesn't make you responsible for integration with any other frameworks, I don't believe anybody would assume that - but it would permit others to do so, without feeling like they have to "hack" Typescript or "polyfill" React.

I understand the ask here, but can you clarify why --jsx preserve and a post build step doesn't do what you want? I get that it'd be simpler to just have a compiler flag/option to do the job rather than some separate tool but the end result is the same no? What rules in our current JSX support are actually blocking your use of riotjs (or whatever alternative you have in mind)? I think that's where the confusion is occurring. You're claiming we haven't made a generalized solution that supports other frameworks because it isn't supported in one particular way (a compiler flag) but we simply opted for a different way (at least for now) that accomplishes the same goal (hopefully).

As far as past discussions go, I'm sorry if you didn't feel heard. We definitely read all the feedback here and take it into consideration whether in explicit design meetings or random ruminations at our desks. Particularly for some of the larger discussions that occur over a long time frame (JSX, non-nullable types, etc) the GitHub discussion format breaks down a bit and it becomes difficult to track/keep up with in a satisfactory way. Sometimes I come in in the morning to many pages of new comments on an issue where I wish there was some forked/threaded view (a la reddit) because otherwise responding to older comments ends up making the whole thread a difficult to read, endless circle (the non-nullable type issue is a big offender here). Perhaps for some larger items we can consider alternate means of sharing and commenting on ideas/specs so we get more precision (ex ability to annotate individual sentences with a single relevant question) and less of everyone having to write small essays back and forth.

axefrog commented 9 years ago

I'm going to side with @mindplay-dk and @blesh here.

@danquirk I think the way you settle this discussion is to explain to the folks here why what you're doing is any more complex than literally just dropping the string React.createClass into the generated output, and if not, why you can't make this configurable? React may have started this idea off, but it's become a highly useful convention amongst less-well-known frameworks that deal with a virtual DOM. Check out virtual-dom as an example. To have TypeScript married to one particular framework's namespace declaration, when all of them can conform to the exact same function signature, just makes no sense, especially given the fact that TypeScript is apparently supposed to be a language, and not a helper for one specific DOM rendering library.

Bottom line, transforming JSX to code does not have any actual dependency on React. It's just a transformation based on a now-popular convention. What the resultant function call is named should be irrelevant, and should certainly be configurable.

Given this fact, forcing another whole build step on developers seems unnecessary when you could have avoided doing so by simply removing the unnecessary coupling to the string React.createClass.

rwyborn commented 8 years ago

I would propose a very simple solution to this to just emit react code using a specific namespace that is not "React", most likely "JSX" as this aligns with the existing JSX.IntrinsicElements etc decl. So a simple example

var theDiv = <div></div>

Would be exported as

var theDiv = JSX.createElement("div")

If I want to use this with React then I can use any number of methods in the code to alias JSX.createElement to React.createElement. Likewise I can alias to any other backend I want.

In a project that is using modules this even gives me the ability to isolate the JSX createElement handler in a module specific way.... in module X it could be linked into React, in module Y it could be linked in to some other backend.

Given that there is already a bunch of Typescript specific JSX configuration that is created in the JSX namespace, it seems like only a small step to just emit element creation into this namespace as well.

EDIT: And yes technically you could alias the existing emitted React.createElement calls onto some other arbitrary non-React backend, but that's a bit kludgy for my liking and potentially problematic if you are mixing other parts of the React library in.

Emitting to the JSX namespace just seems like a nice clean solution that clearly abstracts the Typescript JSX support off into its own neat little box, and removes any direct coupling between JSX <-> React (which was a stated design goal to begin with for TS JSX support)

mindplay-dk commented 8 years ago

@axefrog big +1 for your last post, I couldn't have put it better myself.

@rwyborn I like what you're proposing - JSX is the technology, React is just one framework (regardless of the fact that it was the first) implementing that technology. What you propose is nice, simple, clean and it's neutral.

(The fact that the compiler emits the word React into source code really is mildly offensive to me - as though React had some special status or importance in the software world. I'm sorry, but these days, there's a new front-end framework every week. React is just one of them.)

mhegazy commented 8 years ago

We would be open to taking a PR that would allow you to specify the name of the factory to be used instead of "React", the same lookup rules still apply, i.e. the factory has to be defined in scope.

rwyborn commented 8 years ago

@mhegazy I was going to look at doing this. One thing I was thinking is that rather than be a compile option the factory name would be configured through a declaration in code, exactly the same as the JSX component prop element name is currently handled (see https://github.com/Microsoft/TypeScript/wiki/JSX#attribute-type-checking). I see these 2 cases as analogous, so you would do something like:

declare module JSX {
  interface ElementCreateFactory {
    MyCustomFactory;
  }
}

Agree?

mhegazy commented 8 years ago

that will not work in the transpile scenario, when the compiler does not have access to all files. one of the invariant we try to maintain, is that emit is a syntactic transformation that is independent from type information.

rwyborn commented 8 years ago

Ok fair enough. Compile option it is.

axefrog commented 8 years ago

@mhegazy Babel lets you specify whatever symbol you like for the transform. I'd really encourage the TypeScript team to consider separating concerns. React is not a TypeScript concern. Representing the DOM in a friendly way is (ala TSX) but there is no reason that TypeScript should be anchoring itself to something that, for all we know, could become legacy next year when the next great thing comes out. Mark my words, you'll be looking back and saying "we can't change that now, a bunch of customers are using it and we don't want to break it for them."

Check out these: https://github.com/Matt-Esch/virtual-dom https://github.com/paldepind/snabbdom

I'm pretty sure there are others.

RyanCavanaugh commented 8 years ago

@axefrog what specific thing are you addressing there? We already have --jsx preserve, which is the most agnostic thing we could possibly do. React emit exists as a convenience for those who don't want to chain in another tool.

axefrog commented 8 years ago

@RyanCavanaugh "React emit exists as a convenience for those who don't want to chain in another tool." ... what you mean to say is "React emit exists as a convenience that only people who specifically use React.js can use if they don't want to use another tool". I still cannot for the life of me figure out why it's so hard to output a function call that doesn't assume that React.js is the only game in town, or why you'd couple yourself to a single third-party rendering library when there's no good reason to do so. What are your thoughts on the fact that Babel will spit out any function call name that you want to use when transforming JSX?

rwyborn commented 8 years ago

@axefrog just to chime in, I am currently working on a PR that will let you specify the emit option in exactly the same way as the "pragma" jsx option in Babel (see https://babeljs.io/docs/plugins/transform-react-jsx/ ). This would make Typescript JSX support equivalent to Babel.

eta at some point, currently lower priority than my day job :)

RyanCavanaugh commented 8 years ago

@axefrog When we implemented JSX support, we decided that React emit + Preserve emit was the right trade-off in terms of convenience vs the engineering effort of adding more configurability. That was informed by the number of non-React JSX consumers in the world at the time (which is honestly still pretty low) and a general desire to reduce complexity where it makes sense. Non-React consumers do seem to be on the rise and that's why we're accepting PRs to add this functionality.

Again, this issue is marked Accepting PRs. If someone wants to implement this, they can. We are not opposed to allowing other factory names and we don't intend to strongly couple the compiler to any particular framework. Send us a pull request :wink:

axefrog commented 8 years ago

@RyanCavanaugh thanks, you've satisfied my argumentative urges :)

unional commented 8 years ago

@rwyborn are you giving this a try? I would suggest instead of JSX.createElement(...), it is better named as JSX.compose(...) so it would be more generic. Not all jsx compose to a DOM/VirtualDOM element. They simply convert to a compilation target that can be understand by the respective framework.

mindplay-dk commented 8 years ago

Not all jsx compose to a DOM/VirtualDOM element.

Very good point.

RyanCavanaugh commented 8 years ago

A comment from #6146 I'd like people to weigh in on


How does this look?

let x = <Foo />;
unional commented 8 years ago

A comment from #6146:

I would suggest to "pollute the global" with a meaningful namespace, such as JSX.make() or JSX.compose(). But simple make() might work too, just that it is a bit less convenience when I customize it in code (i.e. couldn't have a proper singleton such as JSX to hold states, when needed).

Apparently I don't know where should I add my comment to. :p

unional commented 8 years ago

Maybe make it more customizable such as in: https://github.com/alexmingoia/jsx-transform

Dynalon commented 8 years ago

:+1:. I support @blesh's comment: Babel uses the /** @jsx: factoryFn */ syntax which is quite handy. A compiler flag would also be great. If support for this lands in TS (as in Babel), I can see a whole new way of creating templates from .tsx files instead of mustache/handlebars etc:

I setup a basic - but working - JSX to HTMLElement factory inspired by plain-jsx in TypeScript here: https://gist.github.com/Dynalon/a8790a1fa66bfd2c26e1 though I had to create a React shim so that it compiles with TS 1.7.5.

mhegazy commented 8 years ago

thanks @rwyborn!

wisercoder commented 8 years ago

A library for creating templates from .tsx files instead of mustache/handlebars is available here: https://github.com/wisercoder/uibuilder This takes advantage of the new reactNamespace flag of TypeScript 1.8

gilboa23 commented 8 years ago

I know i'm a bit late into this discussion, but I would like to propose a simple and generic alternative. Add a new literal emit mode which transpiles the JSX expressions into object literals. This would make the output highly reusable in any custom JSX framework without dependencies on any predefined factory methods.

For example,

<MyComponent attr1="value1">
   <div class="style1">
      <SubComponent attr2="value2"/>
   </div>
</MyComponent>

would get transpiled into:

   { 
      "type": MyComponent,
      "attributes": { 
         "attr1": "value1" 
      },
      "children": [
         { 
            "type": "div",
            "attributes": { 
               "class": "style1" 
            },
            "children": [
               { 
                  "type": SubComponent,
                  "attributes": { 
                     "attr2": "value2" 
                  }
               }
            ]
         }
      ]
   }
mrcrowl commented 7 years ago

It appears this feature was eventually added as compiler option --jsxFactory

See https://www.typescriptlang.org/docs/handbook/compiler-options.html

ravihugo commented 6 years ago

For those finding this in Google - Typescript 2.8 now supports the per-file jsxFactory feature mentioned a couple of times in this issue:

/** @jsx dom */

https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-8.html