svelte icon indicating copy to clipboard operation
svelte copied to clipboard

[Proposal] Run js expressions in markup template through Svelte script preprocessor code

Open swyxio opened this issue 4 years ago • 51 comments

Is your feature request related to a problem? Please describe. i would like to use babel/typescript syntax inside of Svelte markup template code.

For example, let's say i have the babel optional chaining enabled:

// rollup.config.js
    svelte({
	  // ...
      preprocess: {
        script: ({ content }) => {
          return require("@babel/core").transform(content, {
            plugins: ["@babel/plugin-proposal-optional-chaining"],
          });
        },
      },
    }),

This lets me use new JS syntax in my script tag:

<script>
  let foo = {
		bar: {
			baz: true
		}
	}
  let sub = foo?.ban?.baz
</script>

<main>
  <h1>Hello {sub}!</h1>
</main>

this is great! however we try to move it down and Svelte complains:

<script>
  let foo = {
		bar: {
			baz: true
		}
	}
</script>

<main>
  <h1>Hello {foo?.ban?.baz}!</h1>
  <!-- uh oh -->
</main>
Error:
[!] (plugin svelte) ParseError: Unexpected token
src/App.svelte
 6: 
 7: <main>
 8:   <h1>Hello {foo?.ban?.baz}!</h1>
                     ^
 9: </main>
ParseError: Unexpected token
    at error (/Users/swyx/Desktop/Work/testbed/trybabelrecipe/svelte-app/node_modules/svelte/src/compiler/utils/error.ts:25:16)
    at Parser$1.error (/Users/swyx/Desktop/Work/testbed/trybabelrecipe/svelte-app/node_modules/svelte/src/compiler/parse/index.ts:96:3)

This is somewhat of a break of the mental model, since Svelte should accept the same kinds of JS inside templates as it does inside script tags. You can imagine other good usecases for this, e.g. {#if data?.foo?.bar}

in order to fix this, i would have to hook into a markup preprocessor, and then parse the template contents to sift out the js expressions, and then transpile it, and then stitch the markup back together. seems like repeat work to what Svelte already does anyway.

Describe the solution you'd like

Svelte should reuse the script preprocessor for in-template js expressions, as well as for <script> tags.

Describe alternatives you've considered

no change

How important is this feature to you?

it's a nice to have. i dont have any proof, but i suspect this might help the TypeScript effort too in case any assertions are needed (this is rare, though; more likely the "new syntax" usecase is more important than the "need to specify types in expressions" usecase)

swyxio avatar Apr 21 '20 14:04 swyxio

tagging @pngwn for any other comments, and maybe @Conduitry and @tanhauhau. happy to work on this if you think this is a good idea. main tradeoff i can think off is that it might make svelte template compilation a tiny bit slower, but i dont believe it will be noticeable.

swyxio avatar Apr 21 '20 14:04 swyxio

I feel like running script preprocessors on every JS expression in the template would slow down compilation considerably but have no data to back that up.

The other option here would be to expose an api to parse the template without passing the JS expressions to acorn yet. At least then you could write a relatively simple markup preprocessor to handle this case.

pngwn avatar Apr 21 '20 14:04 pngwn

The main technical problem here is that it involves Svelte identifying template expressions written in a language that it cannot parse. The beginnings of template expressions are found via the { but the end is found by telling Acorn 'okay parse as much as you can here as an expression' and then Svelte makes sure there's a } after where Acorn says it parsed until.

As a sort-of solution, the preprocessor could just count opening an closing braces, but this would be thrown off by, say, expressions containing strings containing mismatched braces. A proper solution to this seems more to be to create a new callback that will be called, one at a time, with the start of each expression, and returns the preprocessed code and how many characters it ate, or else throws an exception. I don't have any opinions yet on what this API should look like or what it should be a part of.

Conduitry avatar Apr 21 '20 14:04 Conduitry

If completely avoiding duplicating Svelte parser logic in a preprocessor is not a goal, this all could be fairly straightforwardly implemented in userland in a template preprocessor. The template preprocessor runs before the script or style preprocessors, and the initial intention was that it would do something like convert Pug to HTML, but there's nothing stopping it from transforming any part of the component that it wants. The script and style preprocessors are pretty much sugar on top of the template preprocessor, in that they extract the script or style tags and then operate only on that, leaving the rest of the component unchanged.

Conduitry avatar Apr 21 '20 14:04 Conduitry

indeed we discussed a userland preprocessor solution - but it seemed less than ideal bc it would basically duplicate work that Svelte/Acorn is already doing, and involve wiring up babel twice (once for script, once for template)

as a Svelte user, the mental model for script preprocessor is "ok this thing works on everything inside the <script> tag". but that's not the only place that Javascript appears in a Svelte component.

re: the speed - the syntax transpiles here are going to be very light. will have to benchmark to know for sure ofc but we're definitely not going thru the whole es5 dance here.

I dont know much about the Svelte-Acorn handoff, so this wrinkle with the parsing is interesting. happy to explore that new callback, but i wonder if it should just be the same callback that the preprocessor is

swyxio avatar Apr 21 '20 14:04 swyxio

Wanted to chime in here as no googling found this issue (attributing this to the age of the issue) which lead to me creating the above duplicate.

To distill what I was trying to say in the above issue I think irrespective of the decision made here it would be good to have some formal documentation on this (also mentioned in #3388) to guide new users and help mitigate wasted effort in trying to set this up when it is currently not possible.

chopfitzroy avatar Apr 25 '20 04:04 chopfitzroy

based on what conduitry said, it sounds like some R&D is needed on improving that svelte-acorn parsing. having different systems take care of the { and the } seems a little brittle? idk

swyxio avatar Apr 25 '20 21:04 swyxio

just throwing some of my thoughts over here, the current preprocessor is more of a string based preprocessor, doing string replacements before letting svelte compiles it. this has few implications:

  • we dont want to parse the code before parsing the code, therefore, we use regex to quickly match out the <script> and <style> tag. however matching { } brackets and is hard.
  • we discard ast and sourcemap from the preprocessor, for example, if using a typescript preprocessor, we use the generated js code and discard away typescript ast, and reparse the js code in svelte. if typescript AST is estree compliant, why spend extra effort on parsing them again?
    • and this could be make using sourcemap from preprocessor easier

so i would like to propose that, maybe instead of preprocessor, we can have a parser plugin.

we can have a default acorn plugin to help parse JS, but also allow user to provide custom JS parser, as long as they are estree compliant. same idea can go to css too.

tanhauhau avatar Apr 27 '20 10:04 tanhauhau

i didnt quite understand the difference between preprocessor or parser plugin, but i also didnt really understand the whole post haha. if this seems like the better idea, where is a good place to start?

also...what does "same idea can go to css too" mean? how would this help?

swyxio avatar Apr 27 '20 14:04 swyxio

oh i think i didnt explain it clearly..

i think what i was trying to say is that currently it's hard to make its hard to run js expression in markup through preprocessor as it is run before the svelte parsing.

so maybe an alternative is to provide a way to tap into the parsing, to allow user to provide custom callback to parse JS expression

tanhauhau avatar Apr 27 '20 14:04 tanhauhau

If we did integrate this into the parsing by making it an option to the compiler (rather than handling it in an earlier preprocessing step), we'd need to enforce that those plugins are synchronous. And then we'd have two different ways of doing what would seem to users to be very similar things, and each would have different limitations, which sounds confusing.

Conduitry avatar Apr 27 '20 16:04 Conduitry

yeah we definitely dont want that ☝️ . ok so are we back to a preprocessor solution? how hard is matching { } brackets? i have no experience with it but want to be sure this assumption is correct.

going back to conduitry's initial thoughts:

The beginnings of template expressions are found via the { but the end is found by telling Acorn 'okay parse as much as you can here as an expression' and then Svelte makes sure there's a } after where Acorn says it parsed until.

i just find this a little weird and wonder if it can be better? would there be side benefits of parsing the { and } in svelte, and then handing off the complete chunks to Acorn?

and i'll be transparent, if it just seems not worth it, im happy to back off/close. just thought like itd be a good idea if it were easy.

swyxio avatar Apr 27 '20 18:04 swyxio

Without some understanding of the underlying language used within the { }, we can't determine where the expression stops. Consider {"}"}, a weird but perfectly valid expression to write in the template. Without knowing how quoted strings work in JS, Svelte can't parse this correctly. This is why we pass off "}"}blahblah... to Acorn, which says 'okay I can parse "}" as an expression', and then Svelte makes sure there's a } after the part that Acorn parsed, and then continues on its way with blahblah....

Running preprocessors on the <script> tags doesn't pose this same challenge, because these always end when </script> is found, which can be done without any understanding of the language used within the body of the tag.

There probably is still a reasonable way to handle this within Svelte using preprocessors (perhaps by running through the { } bits in series as we parse the template), but I don't have an obvious suggestion for what the API for that would look like yet. Re-parsing the contents of the { } expressions after preprocessing is probably unavoidable, but it might be possible to avoid doing a full parse of the rest of the component during preprocessing (e.g., it might work to just strip out the <script> and <style> tags, and look for {s in the rest of the file, without parsing anything, and call the callback, which returns the compiled-down JS as well as how many characters it consumed).

Conduitry avatar Apr 27 '20 19:04 Conduitry

While looking at https://github.com/UnwrittenFun/prettier-plugin-svelte/issues/70 it occurred to me that things like the string <style> happening within the <script> tag is something that Svelte preprocessors also have trouble with. If we've found a <script> then everything up until the </script> is part of that script, no matter what it might look like. And, similarly, we wouldn't want preprocessors to try to do anything with something like {'<script>'}.

What I'm getting at is that I'm starting to look more positively on the idea of going through the input file in order and calling the preprocessors in series. Glossing over some details: In the general situation of a partially preprocessed file, we look for the next occurrence of <script> or <style> or {, whichever happens first. For <script> or <style> we find the next </script> or </style>, pass those contents off to the preprocessor, wait for it to respond, and then pick up again after the closing tag. For { we pass the entire rest of the file to the preprocessor, wait for it to respond, and then pick up again where it's told us to, and ensure that we see optional whitespace followed by a }.

As I was writing this, I realized that one of the detail I glossed over was how to handle {'<script>'} if we weren't tasked with doing anything with template expressions. Do we use our own embedded copy of Acorn to parse it anyway so that we can skip over it and not try to improperly preprocess the <script> (knowing that it's going to be parsed again anyway during compilation)? Do we not worry about trying to nicely handle this unless the user has specified that they want to preprocess template expressions (this seems confusing)?

Conduitry avatar May 01 '20 13:05 Conduitry

that's encouraging! altho i'm not sure i follow what the solution is. are we happy with the current behavior of {'<script>'}? is there a bug we are also trying to fix here?

i feel like we make things a little harder for ourselves with the freewheeling order of script, style, and template. i thought for a while about proposing a fixed order to make life easier for ourselves, but decided against it bc we dont want to break the language (and personally, i enjoy doing script -> template -> style, i know its a little weird).

swyxio avatar May 01 '20 14:05 swyxio

Fun thing i just found in the TS 3.9 RC: https://devblogs.microsoft.com/typescript/announcing-typescript-3-9-rc/#breaking-changes

image

swyxio avatar May 02 '20 14:05 swyxio

I was hoping the same, but it seems that vuejs can't achieve this either.

It is hard! Hope svelte can get rid of this problem, that would be awesome!!

multics avatar Jun 16 '20 11:06 multics

When thinking about a solution, please also take into consideration how to handle source maps. Right now this is already problematic because there may be a script and a module-script tag, each producing its own source maps. Ideally, only one big source map would be returned after transpiling everything, as part of the result of the preprocess function. Not sure how much of that is handled by #5015

dummdidumm avatar Jul 22 '20 06:07 dummdidumm

How about we treat all codes and templates are written in typescript with types or without types? Svelte generates typescript code first, then compiles typescript to js.

gfreezy avatar Jul 24 '20 07:07 gfreezy

For anyone ending up here looking for a way to get optional chaining and other modern syntax to work, adding esbuild (or babel) to your rollup or webpack config is the quickest way to get this to work.

Adding esbuild 0.8 to the Sapper rollup.config.js:

import esbuild from '@cush/rollup-plugin-esbuild';

// Add this after the commonjs plugin
esbuild({
  target: 'es2015',
  exclude: /inject_styles\.js/, // Needed for sapper
  loaders: {
    '.js': 'js',
    '.ts': 'ts',
    '.svelte': 'js',
  },
}),

jonatansberg avatar Nov 30 '20 19:11 jonatansberg

@multics Vue supports it now.

wenfangdu avatar Sep 25 '21 23:09 wenfangdu

For anyone ending up here looking for a way to get optional chaining and other modern syntax to work, adding esbuild (or babel) to your rollup or webpack config is the quickest way to get this to work.

Adding esbuild 0.8 to the Sapper rollup.config.js:

import esbuild from '@cush/rollup-plugin-esbuild';

// Add this after the commonjs plugin
esbuild({
  target: 'es2015',
  exclude: /inject_styles\.js/, // Needed for sapper
  loaders: {
    '.js': 'js',
    '.ts': 'ts',
    '.svelte': 'js',
  },
}),

how would this work for sveltekit?

jmsunseri avatar Apr 22 '22 06:04 jmsunseri

I wish this worked too, {/* @ts-ignore */} in markup template.

omar2205 avatar Jun 28 '22 20:06 omar2205

Ran into the "no ts outside of script tags" problem for the first time today:

<BasicPresentation presentation={frame.presentation as Basic} />

trying to do a type assertion. Just started learning svelte (using svelteKit) and it's been really great but this really bums me out. Going back to nextjs for now, but I really hope support for this gets added

jhuggett avatar Jun 30 '22 01:06 jhuggett

@jhuggett You might be able to work around that:

<script lang="ts">

	// Do your type assertion inside the <script> area!
	$: presentation = frame.presentation as Basic;

</script>

<BasicPresentation {presentation} />

AverageHelper avatar Jul 21 '22 18:07 AverageHelper

I'm also getting this problem, which combined with #1575 gets very annoying to use typescript with svelte/sveltekit

Alexandre-Fernandez avatar Aug 03 '22 21:08 Alexandre-Fernandez

I don't think Svelte can honestly say it has TS support until TS works in templates too. Introducing additional type assertions into the script section to workaround this issue works but adds unneccessary cruft and makes code harder to understand.

brgrz avatar Nov 01 '22 08:11 brgrz

I don't think Svelte can honestly say it has TS support until TS works in templates too. Introducing additional type assertions into the script section to workaround this issue works but adds unneccessary cruft and makes code harder to understand.

It's also not really an option if you're doing any sort of loops inside of the template section and trying to use TS there

letoast avatar Nov 06 '22 21:11 letoast

I hope TypeScript syntax support in templates will come, too!

One thing which confuses me though: The typescript checking seems to work? So I am currently adding //@ts-ignore to some lines here and there (ugh).

I am migrating a Sapper project to Sveltekit right now (also occurs on a fresh sverdle for me). Setting TS compiler option checkJs to false does not help. Just removinglang="ts" from the script tag. VS Code Svelte and Typescript extionsions pretty default configured - a few changed settings should not affect this.

Is it just me with a weird configuration I don't see or could this also be related to some Svelte Thing? VS Code extension, Langauge Server, ... !?

blynx avatar Nov 22 '22 18:11 blynx

I hope TypeScript syntax support in templates will come, too! (2)

DLandDS avatar Nov 23 '22 03:11 DLandDS