Replace vue-template-compiler with babel-macros
What problem does this feature solve?
The main problem that I'm trying to solve here is that SFC's are not valid javascript. It complicates the ecosystem a lot. You can't just use a tool like webpack, jest, eslint, or prettier. You have to either install and configure some integration or make such integration yourself. And I suspect (without evidence though) that these integrations cause waste of computation resources because each of them runs vue-template-compiler
under the hood.
The illustrative example in my opinion is typescript integration. Because tsc
doesn't type check files with extensions other than js
/ts
/jsx
/tsx
/json
, we can't make proper integration with typescript itself. And we have to create typescript integration for webpack (fork-ts-checker-webpack-plugin
), for an editor (vetur
, volar
), and for CI (vetur
, vue-tsc
). So, in order to properly integrate vue with just typescript developers should install and configure at least: webpack-fork-ts-checker-plugin
, vue-tsc
, vue-dst
, vetur
. But still, all these packages won't provide such a smooth experience with typescript as react does. That's especially daunting considering all the hard work the core team invested in rewriting v3 using typescript.
What does the proposed API look like?
The idea is to ship the template compiler in a form of a tagged template, let's say tmpl
. This tmpl
tagged template comes in two forms: real tagged template for runtime compilation and babel-macros for build time compilation (like graphql
or css-in-js solutions do). So, it will look something like this
// App.ts
import { defineComponent, tmpl, ref } from 'vue';
import CustomButton from './CustomButton';
export default defineComponent({
name: 'App',
components: {
CustomButton,
},
props: {
name: { type: String, required },
},
setup() {
const counter = ref(0);
const increment = () => counter.value = counter.value + 1;
const decrement = () => counter.value = counter.value - 1;
return {
counter,
increment,
decrement,
};
},
render: tmpl`
<h1>Hello {{ name }}!</h1>
<div>
Here is your counter: {{ ref }}
<custom-button @click="increment">+</custom-button>
<custom-button @click="decrement">-</custom-button>
</div>
`,
})
Pros of this approach:
- SFC's are valid javascript, which allows to simplify or even eliminate some integrations. Most important, it can simplify integration with typescript. AFAIK, angular use similar approach, which allows them to type check templates.
- probably no breaking changes to the core (or no changes at all). The
tmpl
macros is just a way to compile template to render function, so (as I understand) it can be just passed to therender
option without any changes torender
. - simple and consistent API. Instead of having different ways to specify markup (
template
option,render
option, returning a function fromsetup
, SFC templates), there will be only one optionrender
and different ways to make render function (compile from JSX in build time, compile from a template in build time, compile from a template in runtime, write manually). - more gradual and simple adoption of the framework. Switching from compiling templates at runtime to compiling templates at build time becomes a matter of switching from tagged template to babel macros. No need to convert components with
template
option to SFCs. The source code of your components remains the same. - ability to author several components in a single file
- ability to reuse templates
- not sure about this, but it may improve sourcemaps and stack traces
This idea seems straightforward to me and I believe that someone else has already proposed it, but I can't find any issues or other public discussion about this. This may mean that I have missed something important, something that negates all the possible benefits. For example, we may need to provide some solution for css as well to ensure feature parity with SFC. So, I would be glad to hear what others think about this.
- SFC is a unit of composition. It is what makes HMR, scoped styling and future syntax like
<script setup>
straightforward and even possible. - Relying on Babel means you need an integration for every tool you use anyway. And Babel is slow.
- SFCs have integration for all the tools you've mentioned and they already work.
- For TypeScript, Volar +
vue-tsc
+vue-dts-gen
covers the TypeScript integration pretty well. The reason we have to provide these external tooling is because TypeScript team has been reluctant to open up a powerful enough TS plugin system to achieve all these in a more efficient manner while TSX is directly implemented despite being a non-standard syntax. This isn't an inherent problem in the SFC concept.
Now back to your proposal: what makes you think it'd be a good idea to throw away all the work around SFC and essentially re-invent the framework? Consider the cost of implementing what you proposed, re-educating users on how to write in a new syntax, configuring the new build setup (btw in Vite-based setups Babel is rarely needed rather than a requirement), and updating documentation... do you realize how much work that is, and the level of ecosystem fragmentation it implies?
AND most importantly, in your proposal Vue templates are inlined as JS strings. Note Vue templates are not like template-string-based solutions where expressions are embedded and enjoy intellisense automatically (e.g. lit-html) - we'd have to now hack the other way around to provide template intellisense in JS strings instead of in SFCs (which is XML at the top syntax level). I just don't see how this actually makes things any easier.
Getting SFC to the point where it is wasn't easy (which seems to be the main point of the proposal), but the work has already been done. It already works. For end users, it's just a matter of providing best practices and scaffolds to make things work out of the box. That's 100x easier than what you are proposing here.
For those who prefer a "just JS/TS" experience, just use TSX with Vue 3. It also already works and fits most of what you want.